DirectInstallCommand.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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\Grav;
  11. use Grav\Common\Filesystem\Folder;
  12. use Grav\Common\HTTP\Response;
  13. use Grav\Common\GPM\GPM;
  14. use Grav\Common\GPM\Installer;
  15. use Grav\Console\GpmCommand;
  16. use RuntimeException;
  17. use Symfony\Component\Console\Input\InputArgument;
  18. use Symfony\Component\Console\Input\InputOption;
  19. use Symfony\Component\Console\Question\ConfirmationQuestion;
  20. use ZipArchive;
  21. use function is_array;
  22. use function is_callable;
  23. /**
  24. * Class DirectInstallCommand
  25. * @package Grav\Console\Gpm
  26. */
  27. class DirectInstallCommand extends GpmCommand
  28. {
  29. /** @var string */
  30. protected $all_yes;
  31. /** @var string */
  32. protected $destination;
  33. /**
  34. * @return void
  35. */
  36. protected function configure(): void
  37. {
  38. $this
  39. ->setName('direct-install')
  40. ->setAliases(['directinstall'])
  41. ->addArgument(
  42. 'package-file',
  43. InputArgument::REQUIRED,
  44. 'Installable package local <path> or remote <URL>. Can install specific version'
  45. )
  46. ->addOption(
  47. 'all-yes',
  48. 'y',
  49. InputOption::VALUE_NONE,
  50. 'Assumes yes (or best approach) instead of prompting'
  51. )
  52. ->addOption(
  53. 'destination',
  54. 'd',
  55. InputOption::VALUE_OPTIONAL,
  56. 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from',
  57. GRAV_ROOT
  58. )
  59. ->setDescription('Installs Grav, plugin, or theme directly from a file or a URL')
  60. ->setHelp('The <info>direct-install</info> command installs Grav, plugin, or theme directly from a file or a URL');
  61. }
  62. /**
  63. * @return int
  64. */
  65. protected function serve(): int
  66. {
  67. $input = $this->getInput();
  68. $io = $this->getIO();
  69. if (!class_exists(ZipArchive::class)) {
  70. $io->title('Direct Install');
  71. $io->error('php-zip extension needs to be enabled!');
  72. return 1;
  73. }
  74. // Making sure the destination is usable
  75. $this->destination = realpath($input->getOption('destination'));
  76. if (!Installer::isGravInstance($this->destination) ||
  77. !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
  78. ) {
  79. $io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
  80. return 1;
  81. }
  82. $this->all_yes = $input->getOption('all-yes');
  83. $package_file = $input->getArgument('package-file');
  84. $question = new ConfirmationQuestion("Are you sure you want to direct-install <cyan>{$package_file}</cyan> [y|N] ", false);
  85. $answer = $this->all_yes ? true : $io->askQuestion($question);
  86. if (!$answer) {
  87. $io->writeln('exiting...');
  88. $io->newLine();
  89. return 1;
  90. }
  91. $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
  92. $tmp_zip = $tmp_dir . uniqid('/Grav-', false);
  93. $io->newLine();
  94. $io->writeln("Preparing to install <cyan>{$package_file}</cyan>");
  95. $zip = null;
  96. if (Response::isRemote($package_file)) {
  97. $io->write(' |- Downloading package... 0%');
  98. try {
  99. $zip = GPM::downloadPackage($package_file, $tmp_zip);
  100. } catch (RuntimeException $e) {
  101. $io->newLine();
  102. $io->writeln(" `- <red>ERROR: {$e->getMessage()}</red>");
  103. $io->newLine();
  104. return 1;
  105. }
  106. if ($zip) {
  107. $io->write("\x0D");
  108. $io->write(' |- Downloading package... 100%');
  109. $io->newLine();
  110. }
  111. } elseif (is_file($package_file)) {
  112. $io->write(' |- Copying package... 0%');
  113. $zip = GPM::copyPackage($package_file, $tmp_zip);
  114. if ($zip) {
  115. $io->write("\x0D");
  116. $io->write(' |- Copying package... 100%');
  117. $io->newLine();
  118. }
  119. }
  120. if ($zip && file_exists($zip)) {
  121. $tmp_source = $tmp_dir . uniqid('/Grav-', false);
  122. $io->write(' |- Extracting package... ');
  123. $extracted = Installer::unZip($zip, $tmp_source);
  124. if (!$extracted) {
  125. $io->write("\x0D");
  126. $io->writeln(' |- Extracting package... <red>failed</red>');
  127. Folder::delete($tmp_source);
  128. Folder::delete($tmp_zip);
  129. return 1;
  130. }
  131. $io->write("\x0D");
  132. $io->writeln(' |- Extracting package... <green>ok</green>');
  133. $type = GPM::getPackageType($extracted);
  134. if (!$type) {
  135. $io->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
  136. $io->newLine();
  137. Folder::delete($tmp_source);
  138. Folder::delete($tmp_zip);
  139. return 1;
  140. }
  141. $blueprint = GPM::getBlueprints($extracted);
  142. if ($blueprint) {
  143. if (isset($blueprint['dependencies'])) {
  144. $dependencies = [];
  145. foreach ($blueprint['dependencies'] as $dependency) {
  146. if (is_array($dependency)) {
  147. if (isset($dependency['name'])) {
  148. $dependencies[] = $dependency['name'];
  149. }
  150. if (isset($dependency['github'])) {
  151. $dependencies[] = $dependency['github'];
  152. }
  153. } else {
  154. $dependencies[] = $dependency;
  155. }
  156. }
  157. $io->writeln(' |- Dependencies found... <cyan>[' . implode(',', $dependencies) . ']</cyan>');
  158. $question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false);
  159. $answer = $this->all_yes ? true : $io->askQuestion($question);
  160. if (!$answer) {
  161. $io->writeln('exiting...');
  162. $io->newLine();
  163. Folder::delete($tmp_source);
  164. Folder::delete($tmp_zip);
  165. return 1;
  166. }
  167. }
  168. }
  169. if ($type === 'grav') {
  170. $io->write(' |- Checking destination... ');
  171. Installer::isValidDestination(GRAV_ROOT . '/system');
  172. if (Installer::IS_LINK === Installer::lastErrorCode()) {
  173. $io->write("\x0D");
  174. $io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
  175. $io->writeln(" '- <red>ERROR: symlinks found...</red> <yellow>" . GRAV_ROOT . '</yellow>');
  176. $io->newLine();
  177. Folder::delete($tmp_source);
  178. Folder::delete($tmp_zip);
  179. return 1;
  180. }
  181. $io->write("\x0D");
  182. $io->writeln(' |- Checking destination... <green>ok</green>');
  183. $io->write(' |- Installing package... ');
  184. $this->upgradeGrav($zip, $extracted);
  185. } else {
  186. $name = GPM::getPackageName($extracted);
  187. if (!$name) {
  188. $io->writeln('<red>ERROR: Name could not be determined.</red> Please specify with --name|-n');
  189. $io->newLine();
  190. Folder::delete($tmp_source);
  191. Folder::delete($tmp_zip);
  192. return 1;
  193. }
  194. $install_path = GPM::getInstallPath($type, $name);
  195. $is_update = file_exists($install_path);
  196. $io->write(' |- Checking destination... ');
  197. Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
  198. if (Installer::lastErrorCode() === Installer::IS_LINK) {
  199. $io->write("\x0D");
  200. $io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
  201. $io->writeln(" '- <red>ERROR: symlink found...</red> <yellow>" . GRAV_ROOT . DS . $install_path . '</yellow>');
  202. $io->newLine();
  203. Folder::delete($tmp_source);
  204. Folder::delete($tmp_zip);
  205. return 1;
  206. }
  207. $io->write("\x0D");
  208. $io->writeln(' |- Checking destination... <green>ok</green>');
  209. $io->write(' |- Installing package... ');
  210. Installer::install(
  211. $zip,
  212. $this->destination,
  213. $options = [
  214. 'install_path' => $install_path,
  215. 'theme' => (($type === 'theme')),
  216. 'is_update' => $is_update
  217. ],
  218. $extracted
  219. );
  220. // clear cache after successful upgrade
  221. $this->clearCache();
  222. }
  223. Folder::delete($tmp_source);
  224. $io->write("\x0D");
  225. if (Installer::lastErrorCode()) {
  226. $io->writeln(" '- <red>" . Installer::lastErrorMsg() . '</red>');
  227. $io->newLine();
  228. } else {
  229. $io->writeln(' |- Installing package... <green>ok</green>');
  230. $io->writeln(" '- <green>Success!</green> ");
  231. $io->newLine();
  232. }
  233. } else {
  234. $io->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
  235. Folder::delete($tmp_zip);
  236. return 1;
  237. }
  238. Folder::delete($tmp_zip);
  239. return 0;
  240. }
  241. /**
  242. * @param string $zip
  243. * @param string $folder
  244. * @return void
  245. */
  246. private function upgradeGrav(string $zip, string $folder): void
  247. {
  248. if (!is_dir($folder)) {
  249. Installer::setError('Invalid source folder');
  250. }
  251. try {
  252. $script = $folder . '/system/install.php';
  253. /** Install $installer */
  254. if ((file_exists($script) && $install = include $script) && is_callable($install)) {
  255. $install($zip);
  256. } else {
  257. throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
  258. }
  259. } catch (Exception $e) {
  260. Installer::setError($e->getMessage());
  261. }
  262. }
  263. }