Install.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. <?php
  2. /**
  3. * @package Grav\Installer
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Installer;
  9. use Composer\Autoload\ClassLoader;
  10. use Grav\Common\Cache;
  11. use Grav\Common\GPM\Installer;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Plugins;
  14. /**
  15. * Grav installer.
  16. *
  17. * NOTE: This class can be initialized during upgrade from an older version of Grav. Make sure it runs there!
  18. */
  19. class Install
  20. {
  21. private $requires = [
  22. 'php' => [
  23. 'name' => 'PHP',
  24. 'versions' => [
  25. '7.3' => '7.3.1',
  26. '7.2' => '7.2.0',
  27. '7.1' => '7.1.3',
  28. '' => '7.2.14'
  29. ]
  30. ],
  31. 'grav' => [
  32. 'name' => 'Grav',
  33. 'versions' => [
  34. '1.5' => '1.5.0',
  35. '' => '1.5.7'
  36. ]
  37. ],
  38. 'plugins' => [
  39. 'admin' => [
  40. 'name' => 'Admin',
  41. 'optional' => true,
  42. 'versions' => [
  43. '1.8' => '1.8.0',
  44. '' => '1.8.16'
  45. ]
  46. ],
  47. 'email' => [
  48. 'name' => 'Email',
  49. 'optional' => true,
  50. 'versions' => [
  51. '2.7' => '2.7.0',
  52. '' => '2.7.2'
  53. ]
  54. ],
  55. 'form' => [
  56. 'name' => 'Form',
  57. 'optional' => true,
  58. 'versions' => [
  59. '2.16' => '2.16.0',
  60. '' => '2.16.4'
  61. ]
  62. ],
  63. 'login' => [
  64. 'name' => 'Login',
  65. 'optional' => true,
  66. 'versions' => [
  67. '2.8' => '2.8.0',
  68. '' => '2.8.3'
  69. ]
  70. ],
  71. ]
  72. ];
  73. private $ignores = [
  74. 'backup',
  75. 'cache',
  76. 'images',
  77. 'logs',
  78. 'tmp',
  79. 'user',
  80. '.htaccess',
  81. 'robots.txt'
  82. ];
  83. private $classMap = [
  84. // 'Grav\\Installer\\Test' => __DIR__ . '/Test.php',
  85. ];
  86. private $zip;
  87. private $location;
  88. private static $instance;
  89. public static function instance()
  90. {
  91. if (null === self::$instance) {
  92. self::$instance = new static();
  93. }
  94. return self::$instance;
  95. }
  96. private function __construct()
  97. {
  98. }
  99. public function setZip(string $zip)
  100. {
  101. $this->zip = $zip;
  102. return $this;
  103. }
  104. public function __invoke(?string $zip)
  105. {
  106. $this->zip = $zip;
  107. $failedRequirements = $this->checkRequirements();
  108. if ($failedRequirements) {
  109. $error = ['Following requirements have failed:'];
  110. foreach ($failedRequirements as $name => $req) {
  111. $error[] = "{$req['title']} >= <strong>v{$req['minimum']}</strong> required, you have <strong>v{$req['installed']}</strong>";
  112. }
  113. throw new \RuntimeException(implode("<br />\n", $error));
  114. }
  115. $this->prepare();
  116. $this->install();
  117. $this->finalize();
  118. }
  119. /**
  120. * NOTE: This method can only be called after $grav['plugins']->init().
  121. *
  122. * @return array List of failed requirements. If the list is empty, installation can go on.
  123. */
  124. public function checkRequirements(): array
  125. {
  126. $results = [];
  127. $this->checkVersion($results, 'php','php', $this->requires['php'], PHP_VERSION);
  128. $this->checkVersion($results, 'grav', 'grav', $this->requires['grav'], GRAV_VERSION);
  129. $this->checkPlugins($results, $this->requires['plugins']);
  130. return $results;
  131. }
  132. /**
  133. * @throws \RuntimeException
  134. */
  135. public function prepare(): void
  136. {
  137. // Locate the new Grav update and the target site from the filesystem.
  138. $location = dirname(realpath(__DIR__), 4);
  139. $target = dirname(realpath(GRAV_ROOT . '/index.php'));
  140. if ($location === $target) {
  141. // We cannot copy files into themselves, abort!
  142. throw new \RuntimeException('Grav has already been installed here!', 400);
  143. }
  144. // Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail!
  145. foreach ($this->classMap as $class_name => $path) {
  146. if (\class_exists($class_name, false)) {
  147. throw new \RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500);
  148. }
  149. }
  150. $grav = Grav::instance();
  151. /** @var ClassLoader $loader */
  152. $loader = $grav['loader'];
  153. // Override Grav\Installer classes by using this version of Grav.
  154. $loader->addClassMap($this->classMap);
  155. $this->location = $location;
  156. }
  157. /**
  158. * @throws \RuntimeException
  159. */
  160. public function install(): void
  161. {
  162. if (!$this->location) {
  163. throw new \RuntimeException('Oops, installer was run without prepare()!', 500);
  164. }
  165. try {
  166. Installer::install(
  167. $this->zip,
  168. GRAV_ROOT,
  169. ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $this->ignores],
  170. $this->location,
  171. !($this->zip && is_file($this->zip))
  172. );
  173. } catch (\Exception $e) {
  174. Installer::setError($e->getMessage());
  175. }
  176. $errorCode = Installer::lastErrorCode();
  177. $success = !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)));
  178. if (!$success) {
  179. throw new \RuntimeException(Installer::lastErrorMsg());
  180. }
  181. }
  182. /**
  183. * @throws \RuntimeException
  184. */
  185. public function finalize(): void
  186. {
  187. Cache::clearCache();
  188. clearstatcache();
  189. if (function_exists('opcache_reset')) {
  190. @opcache_reset();
  191. }
  192. }
  193. protected function checkVersion(array &$results, $type, $name, array $check, $version): void
  194. {
  195. if (null === $version && !empty($check['optional'])) {
  196. return;
  197. }
  198. $major = $minor = 0;
  199. $versions = $check['versions'] ?? [];
  200. foreach ($versions as $major => $minor) {
  201. if (!$major || version_compare($version, $major, '<')) {
  202. continue;
  203. }
  204. if (version_compare($version, $minor, '>=')) {
  205. return;
  206. }
  207. break;
  208. }
  209. if (!$major) {
  210. $minor = reset($versions);
  211. }
  212. $recommended = end($versions);
  213. if (version_compare($recommended, $minor, '<=')) {
  214. $recommended = null;
  215. }
  216. $results[$name] = [
  217. 'type' => $type,
  218. 'name' => $name,
  219. 'title' => $check['name'] ?? $name,
  220. 'installed' => $version,
  221. 'minimum' => $minor,
  222. 'recommended' => $recommended
  223. ];
  224. }
  225. protected function checkPlugins(array &$results, array $plugins): void
  226. {
  227. if (!\class_exists('Plugins')) {
  228. return;
  229. }
  230. foreach ($plugins as $name => $check) {
  231. $plugin = Plugins::get($name);
  232. if (!$plugin) {
  233. $this->checkVersion($results, 'plugin', $name, $check, null);
  234. continue;
  235. }
  236. $blueprint = $plugin->blueprints();
  237. $version = (string)$blueprint->get('version');
  238. $check['name'] = ($blueprint->get('name') ?? $check['name'] ?? $name) . ' Plugin';
  239. $this->checkVersion($results, 'plugin', $name, $check, $version);
  240. }
  241. }
  242. }