Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / cli / command / PackageCLICommand.class.php
1 <?php
2 namespace wcf\system\cli\command;
3 use phpline\internal\Log;
4 use wcf\data\package\installation\queue\PackageInstallationQueue;
5 use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
6 use wcf\data\package\Package;
7 use wcf\data\package\PackageCache;
8 use wcf\system\cache\CacheHandler;
9 use wcf\system\database\util\PreparedStatementConditionBuilder;
10 use wcf\system\exception\SystemException;
11 use wcf\system\package\PackageArchive;
12 use wcf\system\package\PackageInstallationDispatcher;
13 use wcf\system\package\PackageUninstallationDispatcher;
14 use wcf\system\CLIWCF;
15 use wcf\system\WCF;
16 use wcf\util\FileUtil;
17 use wcf\util\JSON;
18 use wcf\util\StringUtil;
19 use Zend\Console\Exception\RuntimeException as ArgvException;
20 use Zend\Console\Getopt as ArgvParser;
21 use Zend\ProgressBar\Adapter\Console as ConsoleProgressBar;
22 use Zend\ProgressBar\ProgressBar;
23
24 /**
25 * Executes package installation.
26 *
27 * @author Tim Duesterhus
28 * @copyright 2001-2019 WoltLab GmbH
29 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
30 * @package WoltLabSuite\Core\System\Cli\Command
31 */
32 class PackageCLICommand implements IArgumentedCLICommand {
33 /**
34 * arguments parser
35 * @var \Zend\Console\Getopt
36 */
37 protected $argv = null;
38
39 /**
40 * required data for app installation
41 * @var string[]
42 */
43 protected $appData = [];
44
45 /**
46 * Initializes the argument parser.
47 */
48 public function __construct() {
49 $this->argv = new ArgvParser([]);
50 }
51
52 /**
53 * @inheritDoc
54 */
55 public function execute(array $parameters) {
56 $this->argv->setArguments($parameters);
57 $this->argv->parse();
58
59 if (count($this->argv->getRemainingArgs()) !== 2) {
60 throw new ArgvException('', $this->getUsage());
61 }
62
63 list($action, $package) = $this->argv->getRemainingArgs();
64 CLIWCF::getReader()->setHistoryEnabled(false);
65
66 switch ($action) {
67 case 'install':
68 $this->install($package);
69 break;
70
71 case 'uninstall':
72 $this->uninstall($package);
73 break;
74
75 default:
76 throw new ArgvException('', $this->getUsage());
77 break;
78 }
79 }
80
81 /**
82 * Installs the specified package.
83 *
84 * @param string $file
85 */
86 private function install($file) {
87 // PackageStartInstallForm::validateDownloadPackage()
88 if (FileUtil::isURL($file)) {
89 // download package
90 $archive = new PackageArchive($file, null);
91
92 try {
93 if (VERBOSITY >= 1) Log::info("Downloading '".$file."'");
94 $file = $archive->downloadArchive();
95 }
96 catch (SystemException $e) {
97 $this->error('notFound', ['file' => $file]);
98 }
99 }
100 else {
101 // probably local path
102 if (!file_exists($file)) {
103 $this->error('notFound', ['file' => $file]);
104 }
105
106 $archive = new PackageArchive($file, null);
107 }
108
109 // PackageStartInstallForm::validateArchive()
110 // try to open the archive
111 try {
112 $archive->openArchive();
113 }
114 catch (SystemException $e) {
115 $this->error('noValidPackage');
116 }
117
118 // try to find existing package
119 $sql = "SELECT *
120 FROM wcf".WCF_N."_package
121 WHERE package = ?";
122 $statement = CLIWCF::getDB()->prepareStatement($sql);
123 $statement->execute([$archive->getPackageInfo('name')]);
124 $row = $statement->fetchArray();
125 $package = null;
126 if ($row !== false) {
127 $package = new Package(null, $row);
128 }
129
130 // check update or install support
131 if ($package !== null) {
132 CLIWCF::getSession()->checkPermissions(['admin.configuration.package.canUpdatePackage']);
133
134 $archive->setPackage($package);
135 if (!$archive->isValidUpdate()) {
136 $this->error('noValidUpdate');
137 }
138 }
139 else {
140 CLIWCF::getSession()->checkPermissions(['admin.configuration.package.canInstallPackage']);
141
142 if (!$archive->isValidInstall()) {
143 $this->error('noValidInstall');
144 }
145 else if ($archive->getPackageInfo('isApplication') && $archive->hasUniqueAbbreviation()) {
146 $this->error('noUniqueAbbreviation');
147 }
148 else if ($archive->isAlreadyInstalled()) {
149 $this->error('uniqueAlreadyInstalled');
150 }
151 else if ($archive->getPackageInfo('isApplication') && !$archive->isAlreadyInstalled()) {
152 $this->appData['abbreviation'] = Package::getAbbreviation($archive->getPackageInfo('name'));
153
154 $directory = CLIWCF::getReader()->readLine(WCF::getLanguage()->get('wcf.acp.package.packageDir.input').'> ');
155 if ($directory === null) exit;
156 $directory = StringUtil::trim($directory);
157 $this->appData['installationDirectory'] = FileUtil::removeTrailingSlash(FileUtil::addTrailingSlash($directory));
158
159 if (file_exists($directory . 'global.php')) {
160 $this->error('directoryAlreadyInUse');
161 }
162
163 $domain = CLIWCF::getReader()->readLine(WCF::getLanguage()->get('wcf.acp.application.domainName').'> ');
164 if ($domain === null) exit;
165 $this->appData['domainName'] = StringUtil::trim($domain);
166 $this->appData['cookieDomain'] = $this->appData['domainName'];
167
168 $domainPath = CLIWCF::getReader()->readLine(WCF::getLanguage()->get('wcf.acp.application.domainPath').'> ');
169 if ($domainPath === null) exit;
170 $this->appData['domainPath'] = StringUtil::trim($domainPath);
171 }
172 }
173
174 // PackageStartInstallForm::save()
175 $processNo = PackageInstallationQueue::getNewProcessNo();
176
177 // insert queue
178 PackageInstallationQueueEditor::create([
179 'processNo' => $processNo,
180 'userID' => CLIWCF::getUser()->userID,
181 'package' => $archive->getPackageInfo('name'),
182 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
183 'packageID' => ($package !== null) ? $package->packageID : null,
184 'archive' => $file,
185 'action' => $package !== null ? 'update' : 'install',
186 'isApplication' => ($package !== null) ? $package->isApplication : intval($archive->getPackageInfo('isApplication'))
187 ]);
188
189 // PackageInstallationDispatcher::openQueue()
190 $parentQueueID = 0;
191 $conditions = new PreparedStatementConditionBuilder();
192 $conditions->add("userID = ?", [CLIWCF::getUser()->userID]);
193 $conditions->add("parentQueueID = ?", [$parentQueueID]);
194 if ($processNo != 0) $conditions->add("processNo = ?", [$processNo]);
195 $conditions->add("done = ?", [0]);
196
197 $sql = "SELECT *
198 FROM wcf".WCF_N."_package_installation_queue
199 ".$conditions."
200 ORDER BY queueID ASC";
201 $statement = CLIWCF::getDB()->prepareStatement($sql);
202 $statement->execute($conditions->getParameters());
203 $packageInstallation = $statement->fetchArray();
204 if (!isset($packageInstallation['queueID'])) {
205 $this->error('internalOpenQueue');
206 return;
207 }
208 else {
209 $queueID = $packageInstallation['queueID'];
210 }
211
212 // PackageInstallationConfirmPage::readParameters()
213 $queue = new PackageInstallationQueue($queueID);
214 if (!$queue->queueID || $queue->done) {
215 $this->error('internalReadParameters');
216 return;
217 }
218
219 // PackageInstallationConfirmPage::readData()
220 $missingPackages = 0;
221 $packageInstallationDispatcher = new PackageInstallationDispatcher($queue);
222
223 // get requirements
224 $requirements = $packageInstallationDispatcher->getArchive()->getRequirements();
225 $openRequirements = $packageInstallationDispatcher->getArchive()->getOpenRequirements();
226
227 foreach ($requirements as &$requirement) {
228 if (isset($openRequirements[$requirement['name']])) {
229 $requirement['status'] = 'missing';
230 $requirement['action'] = $openRequirements[$requirement['name']]['action'];
231
232 if (!isset($requirement['file'])) {
233 if ($requirement['action'] === 'update') {
234 $requirement['status'] = 'missingVersion';
235 $requirement['existingVersion'] = $openRequirements[$requirement['name']]['existingVersion'];
236 }
237 $missingPackages++;
238 }
239 else {
240 $requirement['status'] = 'delivered';
241 $packageArchive = new PackageArchive($packageInstallationDispatcher->getArchive()->extractTar($requirement['file']));
242 $packageArchive->openArchive();
243
244 // make sure that the delivered package is correct
245 if ($requirement['name'] != $packageArchive->getPackageInfo('name')) {
246 $requirement['status'] = 'invalidDeliveredPackage';
247 $requirement['deliveredPackage'] = $packageArchive->getPackageInfo('name');
248 $missingPackages++;
249 }
250 else if (isset($requirement['minversion'])) {
251 // make sure that the delivered version is sufficient
252 if (Package::compareVersion($requirement['minversion'], $packageArchive->getPackageInfo('version')) > 0) {
253 $requirement['deliveredVersion'] = $packageArchive->getPackageInfo('version');
254 $requirement['status'] = 'missingVersion';
255 $missingPackages++;
256 }
257 }
258 }
259 }
260 else {
261 $requirement['status'] = 'installed';
262 }
263 }
264 unset($requirement);
265
266 // PackageInstallationConfirmPage::assignVariables/show()
267 $excludingPackages = $packageInstallationDispatcher->getArchive()->getConflictedExcludingPackages();
268 $excludedPackages = $packageInstallationDispatcher->getArchive()->getConflictedExcludedPackages();
269 if (!($missingPackages == 0 && count($excludingPackages) == 0 && count($excludedPackages) == 0)) {
270 $this->error('missingPackagesOrExclude', [
271 'requirements' => $requirements,
272 'excludingPackages' => $excludingPackages,
273 'excludedPackages' => $excludedPackages
274 ]);
275 return;
276 }
277
278 // AbstractDialogAction::readParameters()
279 $step = 'prepare';
280 $queueID = $queue->queueID;
281 $node = '';
282
283 // initialize progressbar
284 $progressbar = new ProgressBar(new ConsoleProgressBar([
285 'width' => CLIWCF::getTerminal()->getWidth(),
286 'elements' => [
287 ConsoleProgressBar::ELEMENT_PERCENT,
288 ConsoleProgressBar::ELEMENT_BAR,
289 ConsoleProgressBar::ELEMENT_TEXT
290 ],
291 'textWidth' => min(floor(CLIWCF::getTerminal()->getWidth() / 2), 50)
292 ]));
293
294 // InstallPackageAction::readParameters()
295 $finished = false;
296 while (!$finished) {
297 $queue = new PackageInstallationQueue($queueID);
298
299 if (!$queue->queueID) {
300 echo "InstallPackageAction::readParameters()";
301 return;
302 }
303 $installation = new PackageInstallationDispatcher($queue);
304
305 $progress = 0;
306 $currentAction = '';
307 switch ($step) {
308 case 'prepare':
309 // InstallPackageAction::stepPrepare()
310 // update package information
311 $installation->updatePackage();
312
313 // clean-up previously created nodes
314 $installation->nodeBuilder->purgeNodes();
315
316 if ($package !== null && $package->package === 'com.woltlab.wcf') {
317 WCF::checkWritability();
318 }
319
320 // create node tree
321 $installation->nodeBuilder->buildNodes();
322 $node = $installation->nodeBuilder->getNextNode();
323 $queueID = $installation->nodeBuilder->getQueueByNode($installation->queue->processNo, $node);
324
325 $step = 'install';
326 $progress = 0;
327 $currentAction = $installation->nodeBuilder->getPackageNameByQueue($queueID);
328 break;
329
330 case 'install':
331 // InstallPackageAction::stepInstall()
332 // workaround for app installation via CLI
333 if (!empty($this->appData)) {
334 WCF::getSession()->register('__wcfSetup_directories', [
335 $this->appData['abbreviation'] => $this->appData['installationDirectory']
336 ]);
337 if (empty($_SERVER['HTTP_HOST'])) $_SERVER['HTTP_HOST'] = $this->appData['domainName'];
338 }
339
340 $step_ = $installation->install($node);
341 $queueID = $installation->nodeBuilder->getQueueByNode($installation->queue->processNo, $step_->getNode());
342
343 if ($step_->hasDocument()) {
344 $progress = $installation->nodeBuilder->calculateProgress($node);
345 $node = $step_->getNode();
346 $currentAction = $installation->nodeBuilder->getPackageNameByQueue($queueID);
347 }
348 else {
349 if ($step_->getNode() == '') {
350 // perform final actions
351 $installation->completeSetup();
352 // InstallPackageAction::finalize()
353 CacheHandler::getInstance()->flushAll();
354 // /InstallPackageAction::finalize()
355
356 // show success
357 $progress = 100;
358 $currentAction = CLIWCF::getLanguage()->get('wcf.acp.package.installation.step.install.success');
359 $finished = true;
360 continue 2;
361 }
362 else {
363 // continue with next node
364 $progress = $installation->nodeBuilder->calculateProgress($node);
365 $node = $step_->getNode();
366 $currentAction = $installation->nodeBuilder->getPackageNameByQueue($queueID);
367 }
368 }
369 break;
370 }
371
372 $progressbar->update($progress, $currentAction);
373 }
374
375 $progressbar->finish();
376 }
377
378 /**
379 * Uninstalls the specified package.
380 * $package may either be the packageID or the package identifier.
381 *
382 * @param mixed $package
383 */
384 private function uninstall($package) {
385 if (Package::isValidPackageName($package)) {
386 $packageID = PackageCache::getInstance()->getPackageID($package);
387 }
388 else {
389 $packageID = $package;
390 }
391
392 // UninstallPackageAction::prepare()
393 $package = new Package($packageID);
394 if (!$package->packageID || !$package->canUninstall()) {
395 $this->error('invalidUninstallation');
396 }
397
398 // get new process no
399 $processNo = PackageInstallationQueue::getNewProcessNo();
400
401 // create queue
402 $queue = PackageInstallationQueueEditor::create([
403 'processNo' => $processNo,
404 'userID' => CLIWCF::getUser()->userID,
405 'packageName' => $package->getName(),
406 'packageID' => $package->packageID,
407 'action' => 'uninstall'
408 ]);
409
410 // initialize uninstallation
411 $installation = new PackageUninstallationDispatcher($queue);
412
413 $installation->nodeBuilder->purgeNodes();
414 $installation->nodeBuilder->buildNodes();
415
416 CLIWCF::getTPL()->assign([
417 'queue' => $queue
418 ]);
419
420 $queueID = $installation->nodeBuilder->getQueueByNode($queue->processNo, $installation->nodeBuilder->getNextNode());
421 $step = 'uninstall';
422 $node = $installation->nodeBuilder->getNextNode();
423 $currentAction = CLIWCF::getLanguage()->get('wcf.package.installation.step.uninstalling');
424 $progress = 0;
425
426 // initialize progressbar
427 $progressbar = new ProgressBar(new ConsoleProgressBar([
428 'width' => CLIWCF::getTerminal()->getWidth(),
429 'elements' => [
430 ConsoleProgressBar::ELEMENT_PERCENT,
431 ConsoleProgressBar::ELEMENT_BAR,
432 ConsoleProgressBar::ELEMENT_TEXT
433 ],
434 'textWidth' => min(floor(CLIWCF::getTerminal()->getWidth() / 2), 50)
435 ]));
436
437 // InstallPackageAction::readParameters()
438 $finished = false;
439 while (!$finished) {
440 $queue = new PackageInstallationQueue($queueID);
441 $installation = new PackageUninstallationDispatcher($queue);
442
443 switch ($step) {
444 case 'uninstall':
445 $_node = $installation->uninstall($node);
446
447 if ($_node == '') {
448 // remove node data
449 $installation->nodeBuilder->purgeNodes();
450 // UninstallPackageAction::finalize()
451 CacheHandler::getInstance()->flushAll();
452 // /UninstallPackageAction::finalize()
453
454 // show success
455 $currentAction = CLIWCF::getLanguage()->get('wcf.acp.package.uninstallation.step.success');
456 $progress = 100;
457 $step = 'success';
458 $finished = true;
459 continue 2;
460 }
461
462 // continue with next node
463 $queueID = $installation->nodeBuilder->getQueueByNode($installation->queue->processNo, $installation->nodeBuilder->getNextNode($node));
464 $step = 'uninstall';
465 $progress = $installation->nodeBuilder->calculateProgress($node);
466 $node = $_node;
467 }
468
469 $progressbar->update($progress, $currentAction);
470 }
471
472 $progressbar->finish();
473 }
474
475 /**
476 * Displays an error message.
477 *
478 * @param string $name
479 * @param array $parameters
480 */
481 public function error($name, array $parameters = []) {
482 Log::error('package.'.$name.':'.JSON::encode($parameters));
483
484 if ($parameters) {
485 throw new ArgvException(CLIWCF::getLanguage()->getDynamicVariable('wcf.acp.package.error.'.$name, $parameters), $this->getUsage());
486 }
487 else {
488 throw new ArgvException(CLIWCF::getLanguage()->get('wcf.acp.package.error.'.$name), $this->argv->getUsageMessage());
489 }
490 }
491
492 /**
493 * @inheritDoc
494 */
495 public function getUsage() {
496 return str_replace($_SERVER['argv'][0].' [ options ]', 'package [ options ] <install|uninstall> <package>', $this->argv->getUsageMessage());
497 }
498
499 /**
500 * @inheritDoc
501 */
502 public function canAccess() {
503 return CLIWCF::getSession()->getPermission('admin.configuration.package.canInstallPackage') || CLIWCF::getSession()->getPermission('admin.configuration.package.canUpdatePackage');
504 }
505 }