SelfupgradeCommand.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <?php
  2. /**
  3. * @package Grav\Console\Gpm
  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\Console\Gpm;
  9. use Exception;
  10. use Grav\Common\Filesystem\Folder;
  11. use Grav\Common\HTTP\Response;
  12. use Grav\Common\GPM\Installer;
  13. use Grav\Common\GPM\Upgrader;
  14. use Grav\Common\Grav;
  15. use Grav\Console\GpmCommand;
  16. use Grav\Installer\Install;
  17. use RuntimeException;
  18. use Symfony\Component\Console\Input\ArrayInput;
  19. use Symfony\Component\Console\Input\InputOption;
  20. use Symfony\Component\Console\Question\ConfirmationQuestion;
  21. use ZipArchive;
  22. use function is_callable;
  23. use function strlen;
  24. /**
  25. * Class SelfupgradeCommand
  26. * @package Grav\Console\Gpm
  27. */
  28. class SelfupgradeCommand extends GpmCommand
  29. {
  30. /** @var array */
  31. protected $data;
  32. /** @var string */
  33. protected $file;
  34. /** @var array */
  35. protected $types = ['plugins', 'themes'];
  36. /** @var string|null */
  37. private $tmp;
  38. /** @var Upgrader */
  39. private $upgrader;
  40. /** @var string */
  41. protected $all_yes;
  42. /** @var string */
  43. protected $overwrite;
  44. /** @var int */
  45. protected $timeout;
  46. /**
  47. * @return void
  48. */
  49. protected function configure(): void
  50. {
  51. $this
  52. ->setName('self-upgrade')
  53. ->setAliases(['selfupgrade', 'selfupdate'])
  54. ->addOption(
  55. 'force',
  56. 'f',
  57. InputOption::VALUE_NONE,
  58. 'Force re-fetching the data from remote'
  59. )
  60. ->addOption(
  61. 'all-yes',
  62. 'y',
  63. InputOption::VALUE_NONE,
  64. 'Assumes yes (or best approach) instead of prompting'
  65. )
  66. ->addOption(
  67. 'overwrite',
  68. 'o',
  69. InputOption::VALUE_NONE,
  70. 'Option to overwrite packages if they already exist'
  71. )
  72. ->addOption(
  73. 'timeout',
  74. 't',
  75. InputOption::VALUE_OPTIONAL,
  76. 'Option to set the timeout in seconds when downloading the update (0 for no timeout)',
  77. 30
  78. )
  79. ->setDescription('Detects and performs an update of Grav itself when available')
  80. ->setHelp('The <info>update</info> command updates Grav itself when a new version is available');
  81. }
  82. /**
  83. * @return int
  84. */
  85. protected function serve(): int
  86. {
  87. $input = $this->getInput();
  88. $io = $this->getIO();
  89. if (!class_exists(ZipArchive::class)) {
  90. $io->title('GPM Self Upgrade');
  91. $io->error('php-zip extension needs to be enabled!');
  92. return 1;
  93. }
  94. $this->upgrader = new Upgrader($input->getOption('force'));
  95. $this->all_yes = $input->getOption('all-yes');
  96. $this->overwrite = $input->getOption('overwrite');
  97. $this->timeout = (int) $input->getOption('timeout');
  98. $this->displayGPMRelease();
  99. $update = $this->upgrader->getAssets()['grav-update'];
  100. $local = $this->upgrader->getLocalVersion();
  101. $remote = $this->upgrader->getRemoteVersion();
  102. $release = strftime('%c', strtotime($this->upgrader->getReleaseDate()));
  103. if (!$this->upgrader->meetsRequirements()) {
  104. $io->writeln('<red>ATTENTION:</red>');
  105. $io->writeln(' Grav has increased the minimum PHP requirement.');
  106. $io->writeln(' You are currently running PHP <red>' . phpversion() . '</red>, but PHP <green>' . $this->upgrader->minPHPVersion() . '</green> is required.');
  107. $io->writeln(' Additional information: <white>http://getgrav.org/blog/changing-php-requirements</white>');
  108. $io->newLine();
  109. $io->writeln('Selfupgrade aborted.');
  110. $io->newLine();
  111. return 1;
  112. }
  113. if (!$this->overwrite && !$this->upgrader->isUpgradable()) {
  114. $io->writeln("You are already running the latest version of <green>Grav v{$local}</green>");
  115. $io->writeln("which was released on {$release}");
  116. $config = Grav::instance()['config'];
  117. $schema = $config->get('versions.core.grav.schema');
  118. if ($schema !== GRAV_SCHEMA && version_compare($schema, GRAV_SCHEMA, '<')) {
  119. $io->newLine();
  120. $io->writeln('However post-install scripts have not been run.');
  121. if (!$this->all_yes) {
  122. $question = new ConfirmationQuestion(
  123. 'Would you like to run the scripts? [Y|n] ',
  124. true
  125. );
  126. $answer = $io->askQuestion($question);
  127. } else {
  128. $answer = true;
  129. }
  130. if ($answer) {
  131. // Finalize installation.
  132. Install::instance()->finalize();
  133. $io->write(' |- Running post-install scripts... ');
  134. $io->writeln(" '- <green>Success!</green> ");
  135. $io->newLine();
  136. }
  137. }
  138. return 0;
  139. }
  140. Installer::isValidDestination(GRAV_ROOT . '/system');
  141. if (Installer::IS_LINK === Installer::lastErrorCode()) {
  142. $io->writeln('<red>ATTENTION:</red> Grav is symlinked, cannot upgrade, aborting...');
  143. $io->newLine();
  144. $io->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}.");
  145. return 1;
  146. }
  147. // not used but preloaded just in case!
  148. new ArrayInput([]);
  149. $io->writeln("Grav v<cyan>{$remote}</cyan> is now available [release date: {$release}].");
  150. $io->writeln('You are currently using v<cyan>' . GRAV_VERSION . '</cyan>.');
  151. if (!$this->all_yes) {
  152. $question = new ConfirmationQuestion(
  153. 'Would you like to read the changelog before proceeding? [y|N] ',
  154. false
  155. );
  156. $answer = $io->askQuestion($question);
  157. if ($answer) {
  158. $changelog = $this->upgrader->getChangelog(GRAV_VERSION);
  159. $io->newLine();
  160. foreach ($changelog as $version => $log) {
  161. $title = $version . ' [' . $log['date'] . ']';
  162. $content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) {
  163. return "\n" . ucfirst($match[1]) . ':';
  164. }, $log['content']);
  165. $io->writeln($title);
  166. $io->writeln(str_repeat('-', strlen($title)));
  167. $io->writeln($content);
  168. $io->newLine();
  169. }
  170. $question = new ConfirmationQuestion('Press [ENTER] to continue.', true);
  171. $io->askQuestion($question);
  172. }
  173. $question = new ConfirmationQuestion('Would you like to upgrade now? [y|N] ', false);
  174. $answer = $io->askQuestion($question);
  175. if (!$answer) {
  176. $io->writeln('Aborting...');
  177. return 1;
  178. }
  179. }
  180. $io->newLine();
  181. $io->writeln("Preparing to upgrade to v<cyan>{$remote}</cyan>..");
  182. $io->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%");
  183. $this->file = $this->download($update);
  184. $io->write(' |- Installing upgrade... ');
  185. $installation = $this->upgrade();
  186. $error = 0;
  187. if (!$installation) {
  188. $io->writeln(" '- <red>Installation failed or aborted.</red>");
  189. $io->newLine();
  190. $error = 1;
  191. } else {
  192. $io->writeln(" '- <green>Success!</green> ");
  193. $io->newLine();
  194. }
  195. if ($this->tmp && is_dir($this->tmp)) {
  196. Folder::delete($this->tmp);
  197. }
  198. return $error;
  199. }
  200. /**
  201. * @param array $package
  202. * @return string
  203. */
  204. private function download(array $package): string
  205. {
  206. $io = $this->getIO();
  207. $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
  208. $this->tmp = $tmp_dir . '/grav-update-' . uniqid('', false);
  209. $options = [
  210. 'timeout' => $this->timeout,
  211. ];
  212. $output = Response::get($package['download'], $options, [$this, 'progress']);
  213. Folder::create($this->tmp);
  214. $io->write("\x0D");
  215. $io->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%");
  216. $io->newLine();
  217. file_put_contents($this->tmp . DS . $package['name'], $output);
  218. return $this->tmp . DS . $package['name'];
  219. }
  220. /**
  221. * @return bool
  222. */
  223. private function upgrade(): bool
  224. {
  225. $io = $this->getIO();
  226. $this->upgradeGrav($this->file);
  227. $errorCode = Installer::lastErrorCode();
  228. if ($errorCode) {
  229. $io->write("\x0D");
  230. // extra white spaces to clear out the buffer properly
  231. $io->writeln(' |- Installing upgrade... <red>error</red> ');
  232. $io->writeln(" | '- " . Installer::lastErrorMsg());
  233. return false;
  234. }
  235. $io->write("\x0D");
  236. // extra white spaces to clear out the buffer properly
  237. $io->writeln(' |- Installing upgrade... <green>ok</green> ');
  238. return true;
  239. }
  240. /**
  241. * @param array $progress
  242. * @return void
  243. */
  244. public function progress(array $progress): void
  245. {
  246. $io = $this->getIO();
  247. $io->write("\x0D");
  248. $io->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad(
  249. $progress['percent'],
  250. 5,
  251. ' ',
  252. STR_PAD_LEFT
  253. ) . '%');
  254. }
  255. /**
  256. * @param int|float $size
  257. * @param int $precision
  258. * @return string
  259. */
  260. public function formatBytes($size, int $precision = 2): string
  261. {
  262. $base = log($size) / log(1024);
  263. $suffixes = array('', 'k', 'M', 'G', 'T');
  264. return round(1024 ** ($base - floor($base)), $precision) . $suffixes[(int)floor($base)];
  265. }
  266. /**
  267. * @param string $zip
  268. * @return void
  269. */
  270. private function upgradeGrav(string $zip): void
  271. {
  272. try {
  273. $folder = Installer::unZip($zip, $this->tmp . '/zip');
  274. if ($folder === false) {
  275. throw new RuntimeException(Installer::lastErrorMsg());
  276. }
  277. $script = $folder . '/system/install.php';
  278. if ((file_exists($script) && $install = include $script) && is_callable($install)) {
  279. $install($zip);
  280. } else {
  281. throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
  282. }
  283. } catch (Exception $e) {
  284. Installer::setError($e->getMessage());
  285. }
  286. }
  287. }