Install.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. /**
  3. * @package Grav\Installer
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 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 Exception;
  11. use Grav\Common\Cache;
  12. use Grav\Common\GPM\Installer;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Plugins;
  15. use RuntimeException;
  16. use function class_exists;
  17. use function dirname;
  18. use function function_exists;
  19. use function is_string;
  20. /**
  21. * Grav installer.
  22. *
  23. * NOTE: This class can be initialized during upgrade from an older version of Grav. Make sure it runs there!
  24. */
  25. final class Install
  26. {
  27. /** @var int Installer version. */
  28. public $version = 1;
  29. /** @var array */
  30. public $requires = [
  31. 'php' => [
  32. 'name' => 'PHP',
  33. 'versions' => [
  34. '8.1' => '8.1.0',
  35. '8.0' => '8.0.0',
  36. '7.4' => '7.4.1',
  37. '7.3' => '7.3.6',
  38. '' => '8.0.13'
  39. ]
  40. ],
  41. 'grav' => [
  42. 'name' => 'Grav',
  43. 'versions' => [
  44. '1.6' => '1.6.0',
  45. '' => '1.6.28'
  46. ]
  47. ],
  48. 'plugins' => [
  49. 'admin' => [
  50. 'name' => 'Admin',
  51. 'optional' => true,
  52. 'versions' => [
  53. '1.9' => '1.9.0',
  54. '' => '1.9.13'
  55. ]
  56. ],
  57. 'email' => [
  58. 'name' => 'Email',
  59. 'optional' => true,
  60. 'versions' => [
  61. '3.0' => '3.0.0',
  62. '' => '3.0.10'
  63. ]
  64. ],
  65. 'form' => [
  66. 'name' => 'Form',
  67. 'optional' => true,
  68. 'versions' => [
  69. '4.1' => '4.1.0',
  70. '4.0' => '4.0.0',
  71. '3.0' => '3.0.0',
  72. '' => '4.1.2'
  73. ]
  74. ],
  75. 'login' => [
  76. 'name' => 'Login',
  77. 'optional' => true,
  78. 'versions' => [
  79. '3.3' => '3.3.0',
  80. '3.0' => '3.0.0',
  81. '' => '3.3.6'
  82. ]
  83. ],
  84. ]
  85. ];
  86. /** @var array */
  87. public $ignores = [
  88. 'backup',
  89. 'cache',
  90. 'images',
  91. 'logs',
  92. 'tmp',
  93. 'user',
  94. '.htaccess',
  95. 'robots.txt'
  96. ];
  97. /** @var array */
  98. private $classMap = [
  99. InstallException::class => __DIR__ . '/InstallException.php',
  100. Versions::class => __DIR__ . '/Versions.php',
  101. VersionUpdate::class => __DIR__ . '/VersionUpdate.php',
  102. VersionUpdater::class => __DIR__ . '/VersionUpdater.php',
  103. YamlUpdater::class => __DIR__ . '/YamlUpdater.php',
  104. ];
  105. /** @var string|null */
  106. private $zip;
  107. /** @var string|null */
  108. private $location;
  109. /** @var VersionUpdater|null */
  110. private $updater;
  111. /** @var static */
  112. private static $instance;
  113. /**
  114. * @return static
  115. */
  116. public static function instance()
  117. {
  118. if (null === self::$instance) {
  119. self::$instance = new static();
  120. }
  121. return self::$instance;
  122. }
  123. private function __construct()
  124. {
  125. }
  126. /**
  127. * @param string|null $zip
  128. * @return $this
  129. */
  130. public function setZip(?string $zip)
  131. {
  132. $this->zip = $zip;
  133. return $this;
  134. }
  135. /**
  136. * @param string|null $zip
  137. * @return void
  138. */
  139. #[\ReturnTypeWillChange]
  140. public function __invoke(?string $zip)
  141. {
  142. $this->zip = $zip;
  143. $failedRequirements = $this->checkRequirements();
  144. if ($failedRequirements) {
  145. $error = ['Following requirements have failed:'];
  146. foreach ($failedRequirements as $name => $req) {
  147. $error[] = "{$req['title']} >= <strong>v{$req['minimum']}</strong> required, you have <strong>v{$req['installed']}</strong>";
  148. }
  149. $errors = implode("<br />\n", $error);
  150. if (\defined('GRAV_CLI') && GRAV_CLI) {
  151. $errors = "\n\n" . strip_tags($errors) . "\n\n";
  152. $errors .= <<<ERR
  153. Please install Grav 1.6.31 first by running following commands:
  154. wget -q https://getgrav.org/download/core/grav-update/1.6.31 -O tmp/grav-update-v1.6.31.zip
  155. bin/gpm direct-install -y tmp/grav-update-v1.6.31.zip
  156. rm tmp/grav-update.zip
  157. ERR;
  158. }
  159. throw new RuntimeException($errors);
  160. }
  161. $this->prepare();
  162. $this->install();
  163. $this->finalize();
  164. }
  165. /**
  166. * NOTE: This method can only be called after $grav['plugins']->init().
  167. *
  168. * @return array List of failed requirements. If the list is empty, installation can go on.
  169. */
  170. public function checkRequirements(): array
  171. {
  172. $results = [];
  173. $this->checkVersion($results, 'php', 'php', $this->requires['php'], PHP_VERSION);
  174. $this->checkVersion($results, 'grav', 'grav', $this->requires['grav'], GRAV_VERSION);
  175. $this->checkPlugins($results, $this->requires['plugins']);
  176. return $results;
  177. }
  178. /**
  179. * @return void
  180. * @throws RuntimeException
  181. */
  182. public function prepare(): void
  183. {
  184. // Locate the new Grav update and the target site from the filesystem.
  185. $location = realpath(__DIR__);
  186. $target = realpath(GRAV_ROOT . '/index.php');
  187. if (!$location) {
  188. throw new RuntimeException('Internal Error', 500);
  189. }
  190. if ($target && dirname($location, 4) === dirname($target)) {
  191. // We cannot copy files into themselves, abort!
  192. throw new RuntimeException('Grav has already been installed here!', 400);
  193. }
  194. // Load the installer classes.
  195. foreach ($this->classMap as $class_name => $path) {
  196. // Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail!
  197. if (class_exists($class_name, false)) {
  198. throw new RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500);
  199. }
  200. require $path;
  201. }
  202. $this->legacySupport();
  203. $this->location = dirname($location, 4);
  204. $versions = Versions::instance(USER_DIR . 'config/versions.yaml');
  205. $this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', $this->getVersion(), $versions);
  206. $this->updater->preflight();
  207. }
  208. /**
  209. * @return void
  210. * @throws RuntimeException
  211. */
  212. public function install(): void
  213. {
  214. if (!$this->location) {
  215. throw new RuntimeException('Oops, installer was run without prepare()!', 500);
  216. }
  217. try {
  218. if (null === $this->updater) {
  219. $versions = Versions::instance(USER_DIR . 'config/versions.yaml');
  220. $this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', $this->getVersion(), $versions);
  221. }
  222. // Update user/config/version.yaml before copying the files to avoid frontend from setting the version schema.
  223. $this->updater->install();
  224. Installer::install(
  225. $this->zip ?? '',
  226. GRAV_ROOT,
  227. ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $this->ignores],
  228. $this->location,
  229. !($this->zip && is_file($this->zip))
  230. );
  231. } catch (Exception $e) {
  232. Installer::setError($e->getMessage());
  233. }
  234. $errorCode = Installer::lastErrorCode();
  235. $success = !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)));
  236. if (!$success) {
  237. throw new RuntimeException(Installer::lastErrorMsg());
  238. }
  239. }
  240. /**
  241. * @return void
  242. * @throws RuntimeException
  243. */
  244. public function finalize(): void
  245. {
  246. // Finalize can be run without installing Grav first.
  247. if (null === $this->updater) {
  248. $versions = Versions::instance(USER_DIR . 'config/versions.yaml');
  249. $this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', GRAV_VERSION, $versions);
  250. $this->updater->install();
  251. }
  252. $this->updater->postflight();
  253. Cache::clearCache('all');
  254. clearstatcache();
  255. if (function_exists('opcache_reset')) {
  256. @opcache_reset();
  257. }
  258. }
  259. /**
  260. * @param array $results
  261. * @param string $type
  262. * @param string $name
  263. * @param array $check
  264. * @param string|null $version
  265. * @return void
  266. */
  267. protected function checkVersion(array &$results, $type, $name, array $check, $version): void
  268. {
  269. if (null === $version && !empty($check['optional'])) {
  270. return;
  271. }
  272. $major = $minor = 0;
  273. $versions = $check['versions'] ?? [];
  274. foreach ($versions as $major => $minor) {
  275. if (!$major || version_compare($version ?? '0', $major, '<')) {
  276. continue;
  277. }
  278. if (version_compare($version ?? '0', $minor, '>=')) {
  279. return;
  280. }
  281. break;
  282. }
  283. if (!$major) {
  284. $minor = reset($versions);
  285. }
  286. $recommended = end($versions);
  287. if (version_compare($recommended, $minor, '<=')) {
  288. $recommended = null;
  289. }
  290. $results[$name] = [
  291. 'type' => $type,
  292. 'name' => $name,
  293. 'title' => $check['name'] ?? $name,
  294. 'installed' => $version,
  295. 'minimum' => $minor,
  296. 'recommended' => $recommended
  297. ];
  298. }
  299. /**
  300. * @param array $results
  301. * @param array $plugins
  302. * @return void
  303. */
  304. protected function checkPlugins(array &$results, array $plugins): void
  305. {
  306. if (!class_exists('Plugins')) {
  307. return;
  308. }
  309. foreach ($plugins as $name => $check) {
  310. $plugin = Plugins::get($name);
  311. if (!$plugin) {
  312. $this->checkVersion($results, 'plugin', $name, $check, null);
  313. continue;
  314. }
  315. $blueprint = $plugin->blueprints();
  316. $version = (string)$blueprint->get('version');
  317. $check['name'] = ($blueprint->get('name') ?? $check['name'] ?? $name) . ' Plugin';
  318. $this->checkVersion($results, 'plugin', $name, $check, $version);
  319. }
  320. }
  321. /**
  322. * @return string
  323. */
  324. protected function getVersion(): string
  325. {
  326. $definesFile = "{$this->location}/system/defines.php";
  327. $content = file_get_contents($definesFile);
  328. if (false === $content) {
  329. return '';
  330. }
  331. preg_match("/define\('GRAV_VERSION', '([^']+)'\);/mu", $content, $matches);
  332. return $matches[1] ?? '';
  333. }
  334. protected function legacySupport(): void
  335. {
  336. // Support install for Grav 1.6.0 - 1.6.20 by loading the original class from the older version of Grav.
  337. class_exists(\Grav\Console\Cli\CacheCommand::class, true);
  338. }
  339. }