]> BookStack Code Mirror - system-cli/blob - src/Commands/DownloadVendorCommand.php
Bumped version
[system-cli] / src / Commands / DownloadVendorCommand.php
1 <?php declare(strict_types=1);
2
3 namespace Cli\Commands;
4
5 use Cli\Services\AppLocator;
6 use Exception;
7 use RecursiveDirectoryIterator;
8 use RecursiveIteratorIterator;
9 use Symfony\Component\Console\Command\Command;
10 use Symfony\Component\Console\Input\InputInterface;
11 use Symfony\Component\Console\Input\InputOption;
12 use Symfony\Component\Console\Output\OutputInterface;
13 use ZipArchive;
14
15 class DownloadVendorCommand extends Command
16 {
17     protected function configure(): void
18     {
19         $this->setName('download-vendor');
20         $this->setDescription('Download and extract PHP vendor files in a BookStack instance.');
21         $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to download into', '');
22     }
23
24     /**
25      * @throws Exception
26      */
27     protected function execute(InputInterface $input, OutputInterface $output): int
28     {
29         $appDir = AppLocator::require($input->getOption('app-directory'));
30
31         $output->writeln("<info>Checking app version...</info>");
32         $version = AppLocator::getVersion($appDir);
33         if (empty($version)) {
34             throw new CommandError("Could not determine instance BookStack version.");
35         }
36         $targetChecksum = $this->getTargetChecksum($appDir);
37
38         $output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>");
39         $zip = $this->downloadVendorZip($version);
40
41         $output->writeln("<info>Validating downloaded ZIP...</info>");
42         $this->verifyZipChecksum($zip, $targetChecksum);
43
44         $output->writeln("<info>Deleting existing vendor/ directory...</info>");
45         try {
46             $this->deleteAppVendorFiles($appDir);
47         } catch (Exception $exception) {
48             unlink($zip);
49             throw $exception;
50         }
51
52         $output->writeln("<info>Extracting ZIP into BookStack instance...</info>");
53         $this->extractZip($zip, $appDir);
54
55         $output->writeln("<info>Cleaning up old app services...</info>");
56         $cleaned = $this->cleanupAppServices($appDir);
57         if (!$cleaned) {
58             $output->writeln("<warning>Failed to remove exising app services file</warning>");
59         }
60
61         $output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>");
62
63         return Command::SUCCESS;
64     }
65
66     protected function cleanupAppServices(string $appDir): bool
67     {
68         $filesToClear = [
69             implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'services.php']),
70             implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'packages.php']),
71         ];
72
73         $status = true;
74
75         foreach ($filesToClear as $file) {
76             if (file_exists($file)) {
77                 if (@unlink($file) === false) {
78                     $status = false;
79                 }
80             }
81         }
82
83         return $status;
84     }
85
86     protected function extractZip(string $zipPath, string $appDir): void
87     {
88         $zip = new ZipArchive();
89         $opened = $zip->open($zipPath, ZipArchive::RDONLY);
90         $extracted = $zip->extractTo($appDir);
91         $closed = $zip->close();
92
93         unlink($zipPath);
94         if (!$opened || !$extracted || !$closed) {
95             throw new CommandError("Failed to extract ZIP files into {$appDir}");
96         }
97     }
98
99     protected function deleteAppVendorFiles(string $appDir): void
100     {
101         $targetDir = $appDir . DIRECTORY_SEPARATOR . 'vendor';
102         if (!is_dir($targetDir)) {
103             return;
104         }
105
106         $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
107         $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
108         foreach($files as $file) {
109             if ($file->isDir()){
110                 rmdir($file->getPathname());
111             } else {
112                 unlink($file->getPathname());
113             }
114         }
115
116         $deleted = rmdir($targetDir);
117         if (!$deleted) {
118             throw new CommandError("Could not delete existing app vendor directory.");
119         }
120     }
121
122     protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
123     {
124         $zipChecksum = hash_file('sha256', $zipPath);
125         if ($zipChecksum !== $targetChecksum) {
126             unlink($zipPath);
127             throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
128         }
129     }
130
131     protected function downloadVendorZip(string $version): string
132     {
133         $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
134         $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
135
136         $targetFile = @fopen($targetUrl, 'rb');
137         if ($targetFile === false) {
138             throw new CommandError("Failed to download ZIP file from $targetUrl");
139         }
140
141         file_put_contents($tempFile, $targetFile);
142
143         return $tempFile;
144     }
145
146     /**
147      * @throws CommandError
148      */
149     protected function getTargetChecksum(string $appDir): string
150     {
151         $checksumFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'dev', 'checksums', 'vendor']);
152         $checksum = '';
153         if (file_exists($checksumFile)) {
154             $checksum = trim(file_get_contents($checksumFile));
155         }
156
157         if (empty($checksum)) {
158             throw new CommandError("Could not find a vendor checksum for validation.");
159         }
160
161         return $checksum;
162     }
163 }