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
;
16 use wcf\util\FileUtil
;
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
;
25 * Executes package installation.
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
32 class PackageCLICommand
implements IArgumentedCLICommand
{
35 * @var \Zend\Console\Getopt
37 protected $argv = null;
40 * required data for app installation
43 protected $appData = [];
46 * Initializes the argument parser.
48 public function __construct() {
49 $this->argv
= new ArgvParser([]);
55 public function execute(array $parameters) {
56 $this->argv
->setArguments($parameters);
59 if (count($this->argv
->getRemainingArgs()) !== 2) {
60 throw new ArgvException('', $this->getUsage());
63 list($action, $package) = $this->argv
->getRemainingArgs();
64 CLIWCF
::getReader()->setHistoryEnabled(false);
68 $this->install($package);
72 $this->uninstall($package);
76 throw new ArgvException('', $this->getUsage());
82 * Installs the specified package.
86 private function install($file) {
87 // PackageStartInstallForm::validateDownloadPackage()
88 if (FileUtil
::isURL($file)) {
90 $archive = new PackageArchive($file, null);
93 if (VERBOSITY
>= 1) Log
::info("Downloading '".$file."'");
94 $file = $archive->downloadArchive();
96 catch (SystemException
$e) {
97 $this->error('notFound', ['file' => $file]);
101 // probably local path
102 if (!file_exists($file)) {
103 $this->error('notFound', ['file' => $file]);
106 $archive = new PackageArchive($file, null);
109 // PackageStartInstallForm::validateArchive()
110 // try to open the archive
112 $archive->openArchive();
114 catch (SystemException
$e) {
115 $this->error('noValidPackage');
118 // try to find existing package
120 FROM wcf".WCF_N
."_package
122 $statement = CLIWCF
::getDB()->prepareStatement($sql);
123 $statement->execute([$archive->getPackageInfo('name')]);
124 $row = $statement->fetchArray();
126 if ($row !== false) {
127 $package = new Package(null, $row);
130 // check update or install support
131 if ($package !== null) {
132 CLIWCF
::getSession()->checkPermissions(['admin.configuration.package.canUpdatePackage']);
134 $archive->setPackage($package);
135 if (!$archive->isValidUpdate()) {
136 $this->error('noValidUpdate');
140 CLIWCF
::getSession()->checkPermissions(['admin.configuration.package.canInstallPackage']);
142 if (!$archive->isValidInstall()) {
143 $this->error('noValidInstall');
145 else if ($archive->getPackageInfo('isApplication') && $archive->hasUniqueAbbreviation()) {
146 $this->error('noUniqueAbbreviation');
148 else if ($archive->isAlreadyInstalled()) {
149 $this->error('uniqueAlreadyInstalled');
151 else if ($archive->getPackageInfo('isApplication') && !$archive->isAlreadyInstalled()) {
152 $this->appData
['abbreviation'] = Package
::getAbbreviation($archive->getPackageInfo('name'));
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));
159 if (file_exists($directory . 'global.php')) {
160 $this->error('directoryAlreadyInUse');
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'];
168 $domainPath = CLIWCF
::getReader()->readLine(WCF
::getLanguage()->get('wcf.acp.application.domainPath').'> ');
169 if ($domainPath === null) exit;
170 $this->appData
['domainPath'] = StringUtil
::trim($domainPath);
174 // PackageStartInstallForm::save()
175 $processNo = PackageInstallationQueue
::getNewProcessNo();
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,
185 'action' => $package !== null ?
'update' : 'install',
186 'isApplication' => ($package !== null) ?
$package->isApplication
: intval($archive->getPackageInfo('isApplication'))
189 // PackageInstallationDispatcher::openQueue()
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]);
198 FROM wcf".WCF_N
."_package_installation_queue
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');
209 $queueID = $packageInstallation['queueID'];
212 // PackageInstallationConfirmPage::readParameters()
213 $queue = new PackageInstallationQueue($queueID);
214 if (!$queue->queueID ||
$queue->done
) {
215 $this->error('internalReadParameters');
219 // PackageInstallationConfirmPage::readData()
220 $missingPackages = 0;
221 $packageInstallationDispatcher = new PackageInstallationDispatcher($queue);
224 $requirements = $packageInstallationDispatcher->getArchive()->getRequirements();
225 $openRequirements = $packageInstallationDispatcher->getArchive()->getOpenRequirements();
227 foreach ($requirements as &$requirement) {
228 if (isset($openRequirements[$requirement['name']])) {
229 $requirement['status'] = 'missing';
230 $requirement['action'] = $openRequirements[$requirement['name']]['action'];
232 if (!isset($requirement['file'])) {
233 if ($requirement['action'] === 'update') {
234 $requirement['status'] = 'missingVersion';
235 $requirement['existingVersion'] = $openRequirements[$requirement['name']]['existingVersion'];
240 $requirement['status'] = 'delivered';
241 $packageArchive = new PackageArchive($packageInstallationDispatcher->getArchive()->extractTar($requirement['file']));
242 $packageArchive->openArchive();
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');
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';
261 $requirement['status'] = 'installed';
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
278 // AbstractDialogAction::readParameters()
280 $queueID = $queue->queueID
;
283 // initialize progressbar
284 $progressbar = new ProgressBar(new ConsoleProgressBar([
285 'width' => CLIWCF
::getTerminal()->getWidth(),
287 ConsoleProgressBar
::ELEMENT_PERCENT
,
288 ConsoleProgressBar
::ELEMENT_BAR
,
289 ConsoleProgressBar
::ELEMENT_TEXT
291 'textWidth' => min(floor(CLIWCF
::getTerminal()->getWidth() / 2), 50)
294 // InstallPackageAction::readParameters()
297 $queue = new PackageInstallationQueue($queueID);
299 if (!$queue->queueID
) {
300 echo "InstallPackageAction::readParameters()";
303 $installation = new PackageInstallationDispatcher($queue);
309 // InstallPackageAction::stepPrepare()
310 // update package information
311 $installation->updatePackage();
313 // clean-up previously created nodes
314 $installation->nodeBuilder
->purgeNodes();
316 if ($package !== null && $package->package
=== 'com.woltlab.wcf') {
317 WCF
::checkWritability();
321 $installation->nodeBuilder
->buildNodes();
322 $node = $installation->nodeBuilder
->getNextNode();
323 $queueID = $installation->nodeBuilder
->getQueueByNode($installation->queue
->processNo
, $node);
327 $currentAction = $installation->nodeBuilder
->getPackageNameByQueue($queueID);
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']
337 if (empty($_SERVER['HTTP_HOST'])) $_SERVER['HTTP_HOST'] = $this->appData
['domainName'];
340 $step_ = $installation->install($node);
341 $queueID = $installation->nodeBuilder
->getQueueByNode($installation->queue
->processNo
, $step_->getNode());
343 if ($step_->hasDocument()) {
344 $progress = $installation->nodeBuilder
->calculateProgress($node);
345 $node = $step_->getNode();
346 $currentAction = $installation->nodeBuilder
->getPackageNameByQueue($queueID);
349 if ($step_->getNode() == '') {
350 // perform final actions
351 $installation->completeSetup();
352 // InstallPackageAction::finalize()
353 CacheHandler
::getInstance()->flushAll();
354 // /InstallPackageAction::finalize()
358 $currentAction = CLIWCF
::getLanguage()->get('wcf.acp.package.installation.step.install.success');
363 // continue with next node
364 $progress = $installation->nodeBuilder
->calculateProgress($node);
365 $node = $step_->getNode();
366 $currentAction = $installation->nodeBuilder
->getPackageNameByQueue($queueID);
372 $progressbar->update($progress, $currentAction);
375 $progressbar->finish();
379 * Uninstalls the specified package.
380 * $package may either be the packageID or the package identifier.
382 * @param mixed $package
384 private function uninstall($package) {
385 if (Package
::isValidPackageName($package)) {
386 $packageID = PackageCache
::getInstance()->getPackageID($package);
389 $packageID = $package;
392 // UninstallPackageAction::prepare()
393 $package = new Package($packageID);
394 if (!$package->packageID ||
!$package->canUninstall()) {
395 $this->error('invalidUninstallation');
398 // get new process no
399 $processNo = PackageInstallationQueue
::getNewProcessNo();
402 $queue = PackageInstallationQueueEditor
::create([
403 'processNo' => $processNo,
404 'userID' => CLIWCF
::getUser()->userID
,
405 'packageName' => $package->getName(),
406 'packageID' => $package->packageID
,
407 'action' => 'uninstall'
410 // initialize uninstallation
411 $installation = new PackageUninstallationDispatcher($queue);
413 $installation->nodeBuilder
->purgeNodes();
414 $installation->nodeBuilder
->buildNodes();
416 CLIWCF
::getTPL()->assign([
420 $queueID = $installation->nodeBuilder
->getQueueByNode($queue->processNo
, $installation->nodeBuilder
->getNextNode());
422 $node = $installation->nodeBuilder
->getNextNode();
423 $currentAction = CLIWCF
::getLanguage()->get('wcf.package.installation.step.uninstalling');
426 // initialize progressbar
427 $progressbar = new ProgressBar(new ConsoleProgressBar([
428 'width' => CLIWCF
::getTerminal()->getWidth(),
430 ConsoleProgressBar
::ELEMENT_PERCENT
,
431 ConsoleProgressBar
::ELEMENT_BAR
,
432 ConsoleProgressBar
::ELEMENT_TEXT
434 'textWidth' => min(floor(CLIWCF
::getTerminal()->getWidth() / 2), 50)
437 // InstallPackageAction::readParameters()
440 $queue = new PackageInstallationQueue($queueID);
441 $installation = new PackageUninstallationDispatcher($queue);
445 $_node = $installation->uninstall($node);
449 $installation->nodeBuilder
->purgeNodes();
450 // UninstallPackageAction::finalize()
451 CacheHandler
::getInstance()->flushAll();
452 // /UninstallPackageAction::finalize()
455 $currentAction = CLIWCF
::getLanguage()->get('wcf.acp.package.uninstallation.step.success');
462 // continue with next node
463 $queueID = $installation->nodeBuilder
->getQueueByNode($installation->queue
->processNo
, $installation->nodeBuilder
->getNextNode($node));
465 $progress = $installation->nodeBuilder
->calculateProgress($node);
469 $progressbar->update($progress, $currentAction);
472 $progressbar->finish();
476 * Displays an error message.
478 * @param string $name
479 * @param array $parameters
481 public function error($name, array $parameters = []) {
482 Log
::error('package.'.$name.':'.JSON
::encode($parameters));
485 throw new ArgvException(CLIWCF
::getLanguage()->getDynamicVariable('wcf.acp.package.error.'.$name, $parameters), $this->getUsage());
488 throw new ArgvException(CLIWCF
::getLanguage()->get('wcf.acp.package.error.'.$name), $this->argv
->getUsageMessage());
495 public function getUsage() {
496 return str_replace($_SERVER['argv'][0].' [ options ]', 'package [ options ] <install|uninstall> <package>', $this->argv
->getUsageMessage());
502 public function canAccess() {
503 return CLIWCF
::getSession()->getPermission('admin.configuration.package.canInstallPackage') || CLIWCF
::getSession()->getPermission('admin.configuration.package.canUpdatePackage');