SelfupgradeCommand.php 10 KB

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