Installer.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php
  2. namespace Grav\Common\GPM;
  3. use Grav\Common\Filesystem\Folder;
  4. use Symfony\Component\Yaml\Yaml;
  5. class Installer
  6. {
  7. /** @const No error */
  8. const OK = 0;
  9. /** @const Target already exists */
  10. const EXISTS = 1;
  11. /** @const Target is a symbolic link */
  12. const IS_LINK = 2;
  13. /** @const Target doesn't exist */
  14. const NOT_FOUND = 4;
  15. /** @const Target is not a directory */
  16. const NOT_DIRECTORY = 8;
  17. /** @const Target is not a Grav instance */
  18. const NOT_GRAV_ROOT = 16;
  19. /** @const Error while trying to open the ZIP package */
  20. const ZIP_OPEN_ERROR = 32;
  21. /** @const Error while trying to extract the ZIP package */
  22. const ZIP_EXTRACT_ERROR = 64;
  23. /**
  24. * Destination folder on which validation checks are applied
  25. * @var string
  26. */
  27. protected static $target;
  28. /**
  29. * Error Code
  30. * @var integer
  31. */
  32. protected static $error = 0;
  33. /**
  34. * Default options for the install
  35. * @var array
  36. */
  37. protected static $options = [
  38. 'overwrite' => true,
  39. 'ignore_symlinks' => true,
  40. 'sophisticated' => false,
  41. 'theme' => false,
  42. 'install_path' => '',
  43. 'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK]
  44. ];
  45. /**
  46. * Installs a given package to a given destination.
  47. *
  48. * @param string $package The local path to the ZIP package
  49. * @param string $destination The local path to the Grav Instance
  50. * @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
  51. *
  52. * @return boolean True if everything went fine, False otherwise.
  53. */
  54. public static function install($package, $destination, $options = [])
  55. {
  56. $destination = rtrim($destination, DS);
  57. $options = array_merge(self::$options, $options);
  58. $install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS);
  59. if (!self::isGravInstance($destination) || !self::isValidDestination($install_path, $options['exclude_checks'])) {
  60. return false;
  61. }
  62. if (self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] ||
  63. self::lastErrorCode() == self::EXISTS && !$options['overwrite']) {
  64. return false;
  65. }
  66. // Pre install checks
  67. static::flightProcessing('pre_install', $install_path);
  68. $zip = new \ZipArchive();
  69. $archive = $zip->open($package);
  70. $tmp = CACHE_DIR . 'tmp/Grav-' . uniqid();
  71. if ($archive !== true) {
  72. self::$error = self::ZIP_OPEN_ERROR;
  73. return false;
  74. }
  75. Folder::mkdir($tmp);
  76. $unzip = $zip->extractTo($tmp);
  77. if (!$unzip) {
  78. self::$error = self::ZIP_EXTRACT_ERROR;
  79. $zip->close();
  80. Folder::delete($tmp);
  81. return false;
  82. }
  83. if (!$options['sophisticated']) {
  84. if ($options['theme']) {
  85. self::copyInstall($zip, $install_path, $tmp);
  86. } else {
  87. self::moveInstall($zip, $install_path, $tmp);
  88. }
  89. } else {
  90. self::sophisticatedInstall($zip, $install_path, $tmp);
  91. }
  92. Folder::delete($tmp);
  93. $zip->close();
  94. // Post install checks
  95. static::flightProcessing('post_install', $install_path);
  96. self::$error = self::OK;
  97. return true;
  98. }
  99. protected static function flightProcessing($state, $install_path)
  100. {
  101. $blueprints_path = $install_path . DS . 'blueprints.yaml';
  102. if (file_exists($blueprints_path)) {
  103. $package_yaml = Yaml::parse(file_get_contents($blueprints_path));
  104. if (isset($package_yaml['install'][$state]['create'])) {
  105. foreach ((array) $package_yaml['install']['pre_install']['create'] as $file) {
  106. Folder::mkdir($install_path . '/' . ltrim($file, '/'));
  107. }
  108. }
  109. if (isset($package_yaml['install'][$state]['remove'])) {
  110. foreach ((array) $package_yaml['install']['pre_install']['remove'] as $file) {
  111. Folder::delete($install_path . '/' . ltrim($file, '/'));
  112. }
  113. }
  114. }
  115. }
  116. public static function moveInstall(\ZipArchive $zip, $install_path, $tmp)
  117. {
  118. $container = $zip->getNameIndex(0);
  119. if (file_exists($install_path)) {
  120. Folder::delete($install_path);
  121. }
  122. Folder::move($tmp . DS . $container, $install_path);
  123. return true;
  124. }
  125. public static function copyInstall(\ZipArchive $zip, $install_path, $tmp)
  126. {
  127. $firstDir = $zip->getNameIndex(0);
  128. if (empty($firstDir)) {
  129. throw new \RuntimeException("Directory $firstDir is missing");
  130. } else {
  131. $tmp = realpath($tmp . DS . $firstDir);
  132. Folder::rcopy($tmp, $install_path);
  133. }
  134. return true;
  135. }
  136. public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp)
  137. {
  138. for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) {
  139. $filename = $zip->getNameIndex($i);
  140. $fileinfo = pathinfo($filename);
  141. $depth = count(explode(DS, rtrim($filename, '/')));
  142. if ($depth > 2) {
  143. continue;
  144. }
  145. $path = $install_path . DS . $fileinfo['basename'];
  146. if (is_link($path)) {
  147. continue;
  148. } else {
  149. if (is_dir($path)) {
  150. Folder::delete($path);
  151. Folder::move($tmp . DS . $filename, $path);
  152. if ($fileinfo['basename'] == 'bin') {
  153. foreach (glob($path . DS . '*') as $file) {
  154. @chmod($file, 0755);
  155. }
  156. }
  157. } else {
  158. @unlink($path);
  159. @copy($tmp . DS . $filename, $path);
  160. }
  161. }
  162. }
  163. return true;
  164. }
  165. /**
  166. * Uninstalls one or more given package
  167. *
  168. * @param string $path The slug of the package(s)
  169. * @param array $options Options to use for uninstalling
  170. *
  171. * @return boolean True if everything went fine, False otherwise.
  172. */
  173. public static function uninstall($path, $options = [])
  174. {
  175. $options = array_merge(self::$options, $options);
  176. if (!self::isValidDestination($path, $options['exclude_checks'])
  177. ) {
  178. return false;
  179. }
  180. return Folder::delete($path);
  181. }
  182. /**
  183. * Runs a set of checks on the destination and sets the Error if any
  184. *
  185. * @param string $destination The directory to run validations at
  186. * @param array $exclude An array of constants to exclude from the validation
  187. *
  188. * @return boolean True if validation passed. False otherwise
  189. */
  190. public static function isValidDestination($destination, $exclude = [])
  191. {
  192. self::$error = 0;
  193. self::$target = $destination;
  194. if (is_link($destination)) {
  195. self::$error = self::IS_LINK;
  196. } elseif (file_exists($destination)) {
  197. self::$error = self::EXISTS;
  198. } elseif (!file_exists($destination)) {
  199. self::$error = self::NOT_FOUND;
  200. } elseif (!is_dir($destination)) {
  201. self::$error = self::NOT_DIRECTORY;
  202. }
  203. if (count($exclude) && in_array(self::$error, $exclude)) {
  204. return true;
  205. }
  206. return !(self::$error);
  207. }
  208. /**
  209. * Validates if the given path is a Grav Instance
  210. *
  211. * @param string $target The local path to the Grav Instance
  212. *
  213. * @return boolean True if is a Grav Instance. False otherwise
  214. */
  215. public static function isGravInstance($target)
  216. {
  217. self::$error = 0;
  218. self::$target = $target;
  219. if (
  220. !file_exists($target . DS . 'index.php') ||
  221. !file_exists($target . DS . 'bin') ||
  222. !file_exists($target . DS . 'user') ||
  223. !file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml')
  224. ) {
  225. self::$error = self::NOT_GRAV_ROOT;
  226. }
  227. return !self::$error;
  228. }
  229. /**
  230. * Returns the last error occurred in a string message format
  231. * @return string The message of the last error
  232. */
  233. public static function lastErrorMsg()
  234. {
  235. $msg = 'Unknown Error';
  236. switch (self::$error) {
  237. case 0:
  238. $msg = 'No Error';
  239. break;
  240. case self::EXISTS:
  241. $msg = 'The target path "' . self::$target . '" already exists';
  242. break;
  243. case self::IS_LINK:
  244. $msg = 'The target path "' . self::$target . '" is a symbolic link';
  245. break;
  246. case self::NOT_FOUND:
  247. $msg = 'The target path "' . self::$target . '" does not appear to exist';
  248. break;
  249. case self::NOT_DIRECTORY:
  250. $msg = 'The target path "' . self::$target . '" does not appear to be a folder';
  251. break;
  252. case self::NOT_GRAV_ROOT:
  253. $msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance';
  254. break;
  255. case self::ZIP_OPEN_ERROR:
  256. $msg = 'Unable to open the package file';
  257. break;
  258. case self::ZIP_EXTRACT_ERROR:
  259. $msg = 'An error occurred while extracting the package';
  260. break;
  261. default:
  262. return 'Unknown error';
  263. break;
  264. }
  265. return $msg;
  266. }
  267. /**
  268. * Returns the last error code of the occurred error
  269. * @return integer The code of the last error
  270. */
  271. public static function lastErrorCode()
  272. {
  273. return self::$error;
  274. }
  275. /**
  276. * Allows to manually set an error
  277. * @param $error the Error code
  278. */
  279. public static function setError($error)
  280. {
  281. self::$error = $error;
  282. }
  283. }