3 namespace wcf\system\package
;
5 use wcf\data\application\Application
;
6 use wcf\data\application\ApplicationEditor
;
7 use wcf\data\devtools\project\DevtoolsProjectAction
;
8 use wcf\data\language\category\LanguageCategory
;
9 use wcf\data\language\LanguageEditor
;
10 use wcf\data\language\LanguageList
;
11 use wcf\data\option\OptionEditor
;
12 use wcf\data\package\installation\queue\PackageInstallationQueue
;
13 use wcf\data\package\installation\queue\PackageInstallationQueueEditor
;
14 use wcf\data\package\Package
;
15 use wcf\data\package\PackageEditor
;
16 use wcf\data\user\User
;
17 use wcf\data\user\UserAction
;
18 use wcf\system\application\ApplicationHandler
;
19 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder
;
20 use wcf\system\cache\CacheHandler
;
21 use wcf\system\database\statement\PreparedStatement
;
22 use wcf\system\database\util\PreparedStatementConditionBuilder
;
23 use wcf\system\devtools\DevtoolsSetup
;
24 use wcf\system\event\EventHandler
;
25 use wcf\system\exception\ImplementationException
;
26 use wcf\system\exception\SystemException
;
27 use wcf\system\form\container\GroupFormElementContainer
;
28 use wcf\system\form\container\MultipleSelectionFormElementContainer
;
29 use wcf\system\form\element\MultipleSelectionFormElement
;
30 use wcf\system\form\element\TextInputFormElement
;
31 use wcf\system\form\FormDocument
;
32 use wcf\system\language\LanguageFactory
;
33 use wcf\system\package\plugin\IPackageInstallationPlugin
;
34 use wcf\system\request\LinkHandler
;
35 use wcf\system\request\RouteHandler
;
36 use wcf\system\setup\IFileHandler
;
37 use wcf\system\setup\Installer
;
38 use wcf\system\style\StyleHandler
;
39 use wcf\system\user\storage\UserStorageHandler
;
41 use wcf\util\CryptoUtil
;
42 use wcf\util\FileUtil
;
43 use wcf\util\HeaderUtil
;
45 use wcf\util\StringUtil
;
48 * PackageInstallationDispatcher handles the whole installation process.
50 * @author Alexander Ebert
51 * @copyright 2001-2019 WoltLab GmbH
52 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
53 * @package WoltLabSuite\Core\System\Package
55 class PackageInstallationDispatcher
58 * current installation type
61 protected $action = '';
64 * instance of PackageArchive
70 * instance of PackageInstallationNodeBuilder
71 * @var PackageInstallationNodeBuilder
82 * instance of PackageInstallationQueue
83 * @var PackageInstallationQueue
88 * default name of the config file
91 const CONFIG_FILE
= 'app.config.inc.php';
94 * data of previous package in queue
97 protected $previousPackageData;
100 * Creates a new instance of PackageInstallationDispatcher.
102 * @param PackageInstallationQueue $queue
104 public function __construct(PackageInstallationQueue
$queue)
106 $this->queue
= $queue;
107 $this->nodeBuilder
= new PackageInstallationNodeBuilder($this);
109 $this->action
= $this->queue
->action
;
113 * Sets data of previous package in queue.
115 * @param string[] $packageData
117 public function setPreviousPackage(array $packageData)
119 $this->previousPackageData
= $packageData;
123 * Installs node components and returns next node.
125 * @param string $node
126 * @return PackageInstallationStep
127 * @throws SystemException
129 public function install($node)
131 $nodes = $this->nodeBuilder
->getNodeData($node);
133 // guard against possible issues with empty instruction blocks, including
134 // these blocks that contain no valid instructions at all (e.g. typo from
136 throw new SystemException(
137 "Failed to retrieve nodes for identifier '{$node}', the query returned no results."
141 // invoke node-specific actions
143 foreach ($nodes as $data) {
144 $nodeData = \
unserialize($data['nodeData']);
145 $this->logInstallationStep($data);
147 switch ($data['nodeType']) {
149 $step = $this->installPackage($nodeData);
153 $step = $this->executePIP($nodeData);
156 case 'optionalPackages':
157 $step = $this->selectOptionalPackages($node, $nodeData);
161 exit("Unknown node type: '" . $data['nodeType'] . "'");
165 if ($step->splitNode()) {
167 if ($step->getException() !== null && $step->getException()->getMessage()) {
168 $log .= ': ' . $step->getException()->getMessage();
171 $this->logInstallationStep($data, $log);
172 $this->nodeBuilder
->cloneNode($node, $data['sequenceNo']);
177 // mark node as completed
178 $this->nodeBuilder
->completeNode($node);
181 $node = $this->nodeBuilder
->getNextNode($node);
182 $step->setNode($node);
184 // perform post-install/update actions
186 $this->logInstallationStep([], 'start cleanup');
188 // update "last update time" option
189 $sql = "UPDATE wcf" . WCF_N
. "_option
191 WHERE optionName = ?";
192 $statement = WCF
::getDB()->prepareStatement($sql);
193 $statement->execute([
198 // update options.inc.php
199 OptionEditor
::resetCache();
201 if ($this->action
== 'install') {
202 // save localized package infos
203 $this->saveLocalizedPackageInfos();
205 // remove all cache files after WCFSetup
207 CacheHandler
::getInstance()->flushAll();
209 $sql = "UPDATE wcf" . WCF_N
. "_option
211 WHERE optionName = ?";
212 $statement = WCF
::getDB()->prepareStatement($sql);
214 $statement->execute([
215 StringUtil
::getUUID(),
219 if (\file_exists
(WCF_DIR
. 'cookiePrefix.txt')) {
220 $statement->execute([
225 @\
unlink(WCF_DIR
. 'cookiePrefix.txt');
229 $statement->execute([
233 $statement->execute([
237 $statement->execute([
239 'mail_admin_address',
242 $statement->execute([
243 // We do not use the cache-timing safe class Hex, because we run the
244 // function during the setup.
245 $signatureSecret = \bin
2hex
(\random_bytes
(20)),
248 \
define('SIGNATURE_SECRET', $signatureSecret);
249 HeaderUtil
::setCookie(
251 // We do not use the cache-timing safe class Hex, because we run the
252 // function during the setup.
253 CryptoUtil
::createSignedString(
257 \
hex2bin(WCF
::getSession()->sessionID
),
263 if (WCF
::getSession()->getVar('__wcfSetup_developerMode')) {
264 $statement->execute([
268 $statement->execute([
272 $statement->execute([
276 $statement->execute([
278 'enable_developer_tools',
280 $statement->execute([
282 'log_missing_language_items',
285 foreach (DevtoolsSetup
::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
286 $statement->execute([
292 foreach (DevtoolsSetup
::getInstance()->getUsers() as $newUser) {
294 (new UserAction([], 'create', [
296 'email' => $newUser['email'],
297 'password' => $newUser['password'],
298 'username' => $newUser['username'],
304 ]))->executeAction();
305 } catch (SystemException
$e) {
306 // ignore errors due to event listeners missing at this
307 // point during installation
311 if (($importPath = DevtoolsSetup
::getInstance()->getDevtoolsImportPath()) !== '') {
312 (new DevtoolsProjectAction([], 'quickSetup', [
313 'path' => $importPath,
314 ]))->executeAction();
318 if (WCF
::getSession()->getVar('__wcfSetup_imagick')) {
319 $statement->execute([
321 'image_adapter_type',
325 // update options.inc.php
326 OptionEditor
::resetCache();
328 WCF
::getSession()->register('__wcfSetup_completed', true);
331 // rebuild application paths
332 ApplicationHandler
::rebuild();
335 // remove template listener cache
336 TemplateListenerCodeCacheBuilder
::getInstance()->reset();
338 // reset language cache
339 LanguageFactory
::getInstance()->clearCache();
340 LanguageFactory
::getInstance()->deleteLanguageCache();
343 StyleHandler
::resetStylesheets();
345 // clear user storage
346 UserStorageHandler
::getInstance()->clear();
348 // rebuild config files for affected applications
349 $sql = "SELECT package.packageID
350 FROM wcf" . WCF_N
. "_package_installation_queue queue,
351 wcf" . WCF_N
. "_package package
352 WHERE queue.processNo = ?
353 AND package.packageID = queue.packageID
354 AND package.isApplication = ?";
355 $statement = WCF
::getDB()->prepareStatement($sql);
356 $statement->execute([
357 $this->queue
->processNo
,
360 while ($row = $statement->fetchArray()) {
361 Package
::writeConfigFile($row['packageID']);
364 EventHandler
::getInstance()->fireAction($this, 'postInstall');
367 $sql = "SELECT archive
368 FROM wcf" . WCF_N
. "_package_installation_queue
369 WHERE processNo = ?";
370 $statement = WCF
::getDB()->prepareStatement($sql);
371 $statement->execute([$this->queue
->processNo
]);
372 while ($row = $statement->fetchArray()) {
373 @\
unlink($row['archive']);
377 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_queue
378 WHERE processNo = ?";
379 $statement = WCF
::getDB()->prepareStatement($sql);
380 $statement->execute([$this->queue
->processNo
]);
382 $this->logInstallationStep([], 'finished cleanup');
389 * Logs an installation step.
391 * @param array $node data of the executed node
392 * @param string $log optional additional log text
394 protected function logInstallationStep(array $node = [], $log = '')
396 $logEntry = "[" . TIME_NOW
. "]\n";
398 $logEntry .= 'sequenceNo: ' . $node['sequenceNo'] . "\n";
399 $logEntry .= 'nodeType: ' . $node['nodeType'] . "\n";
400 $logEntry .= "nodeData:\n";
402 $nodeData = \
unserialize($node['nodeData']);
403 foreach ($nodeData as $index => $value) {
404 $logEntry .= "\t" . $index . ': ' . (!\
is_object($value) && !\
is_array($value) ?
$value : JSON
::encode($value)) . "\n";
409 $logEntry .= 'additional information: ' . $log . "\n";
412 $logEntry .= \
str_repeat('-', 30) . "\n\n";
415 WCF_DIR
. 'log/' . \
date('Y-m-d', TIME_NOW
) . '-update-' . $this->queue
->queueID
. '.txt',
422 * Returns current package archive.
424 * @return PackageArchive
426 public function getArchive()
428 if ($this->archive
=== null) {
429 // check if we're doing an iterative update of the same package
431 $this->previousPackageData
!== null
432 && $this->getPackage()->package
== $this->previousPackageData
['package']
435 Package
::compareVersion(
436 $this->getPackage()->packageVersion
,
437 $this->previousPackageData
['packageVersion'],
441 // fake package to simulate the package version required by current archive
442 $this->getPackage()->setPackageVersion($this->previousPackageData
['packageVersion']);
446 $this->archive
= new PackageArchive($this->queue
->archive
, $this->getPackage());
448 if (FileUtil
::isURL($this->archive
->getArchive())) {
449 // get return value and update entry in
450 // package_installation_queue with this value
451 $archive = $this->archive
->downloadArchive();
452 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
453 $queueEditor->update(['archive' => $archive]);
456 $this->archive
->openArchive();
459 return $this->archive
;
463 * Installs current package.
465 * @param mixed[] $nodeData
466 * @return PackageInstallationStep
467 * @throws SystemException
469 protected function installPackage(array $nodeData)
471 $installationStep = new PackageInstallationStep();
473 // check requirements
474 if (!empty($nodeData['requirements'])) {
475 foreach ($nodeData['requirements'] as $package => $requirementData) {
476 // get existing package
477 if ($requirementData['packageID']) {
478 $sql = "SELECT packageName, packageVersion
479 FROM wcf" . WCF_N
. "_package
480 WHERE packageID = ?";
481 $statement = WCF
::getDB()->prepareStatement($sql);
482 $statement->execute([$requirementData['packageID']]);
484 // try to find matching package
485 $sql = "SELECT packageName, packageVersion
486 FROM wcf" . WCF_N
. "_package
488 $statement = WCF
::getDB()->prepareStatement($sql);
489 $statement->execute([$package]);
491 $row = $statement->fetchArray();
493 // package is required but not available
494 if ($row === false) {
495 throw new SystemException("Package '" . $package . "' is required by '" . $nodeData['packageName'] . "', but is neither installed nor shipped.");
498 // check version requirements
499 if ($requirementData['minVersion']) {
500 if (Package
::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
501 throw new SystemException("Package '" . $nodeData['packageName'] . "' requires package '" . $row['packageName'] . "' in version '" . $requirementData['minVersion'] . "', but only version '" . $row['packageVersion'] . "' is installed");
506 unset($nodeData['requirements']);
508 $applicationDirectory = '';
509 if (isset($nodeData['applicationDirectory'])) {
510 $applicationDirectory = $nodeData['applicationDirectory'];
511 unset($nodeData['applicationDirectory']);
515 if ($this->queue
->packageID
) {
516 $packageEditor = new PackageEditor(new Package($this->queue
->packageID
));
517 unset($nodeData['installDate']);
518 $packageEditor->update($nodeData);
520 // delete old excluded packages
521 $sql = "DELETE FROM wcf" . WCF_N
. "_package_exclusion
522 WHERE packageID = ?";
523 $statement = WCF
::getDB()->prepareStatement($sql);
524 $statement->execute([$this->queue
->packageID
]);
526 // delete old compatibility versions
527 $sql = "DELETE FROM wcf" . WCF_N
. "_package_compatibility
528 WHERE packageID = ?";
529 $statement = WCF
::getDB()->prepareStatement($sql);
530 $statement->execute([$this->queue
->packageID
]);
532 // delete old requirements and dependencies
533 $sql = "DELETE FROM wcf" . WCF_N
. "_package_requirement
534 WHERE packageID = ?";
535 $statement = WCF
::getDB()->prepareStatement($sql);
536 $statement->execute([$this->queue
->packageID
]);
538 // create package entry
539 $package = $this->createPackage($nodeData);
541 // update package id for current queue
542 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
543 $queueEditor->update(['packageID' => $package->packageID
]);
546 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
547 $this->package
= null;
549 if ($package->isApplication
) {
550 $host = \
str_replace(RouteHandler
::getProtocol(), '', RouteHandler
::getHost());
551 $path = RouteHandler
::getPath(['acp']);
554 if ($this->getPackage()->package
== 'com.woltlab.wcf') {
555 // com.woltlab.wcf is special, because promptPackageDir() will not be executed.
559 // insert as application
560 ApplicationEditor
::create([
561 'domainName' => $host,
562 'domainPath' => $path,
563 'cookieDomain' => $host,
564 'packageID' => $package->packageID
,
565 'isTainted' => $isTainted,
570 // save excluded packages
571 if (\
count($this->getArchive()->getExcludedPackages())) {
572 $sql = "INSERT INTO wcf" . WCF_N
. "_package_exclusion
573 (packageID, excludedPackage, excludedPackageVersion)
575 $statement = WCF
::getDB()->prepareStatement($sql);
577 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
578 $statement->execute([
579 $this->queue
->packageID
,
580 $excludedPackage['name'],
581 !empty($excludedPackage['version']) ?
$excludedPackage['version'] : '',
586 // save compatible versions
587 if (!empty($this->getArchive()->getCompatibleVersions())) {
588 $sql = "INSERT INTO wcf" . WCF_N
. "_package_compatibility
591 $statement = WCF
::getDB()->prepareStatement($sql);
593 foreach ($this->getArchive()->getCompatibleVersions() as $version) {
594 $statement->execute([
595 $this->queue
->packageID
,
601 // insert requirements and dependencies
602 $requirements = $this->getArchive()->getAllExistingRequirements();
603 if (!empty($requirements)) {
604 $sql = "INSERT INTO wcf" . WCF_N
. "_package_requirement
605 (packageID, requirement)
607 $statement = WCF
::getDB()->prepareStatement($sql);
609 foreach ($requirements as $possibleRequirements) {
610 $requirement = \array_shift
($possibleRequirements);
612 $statement->execute([
613 $this->queue
->packageID
,
614 $requirement['packageID'],
620 $this->getPackage()->isApplication
621 && $this->getPackage()->package
!= 'com.woltlab.wcf'
622 && $this->getAction() == 'install'
623 && empty($this->getPackage()->packageDir
)
625 $document = $this->promptPackageDir($applicationDirectory);
626 if ($document !== null && $document instanceof FormDocument
) {
627 $installationStep->setDocument($document);
630 $installationStep->setSplitNode();
633 return $installationStep;
637 * Creates a new package based on the given data and returns it.
639 * @param array $packageData
643 protected function createPackage(array $packageData)
645 return PackageEditor
::create($packageData);
649 * Saves the localized package info.
651 protected function saveLocalizedPackageInfos()
653 $package = new Package($this->queue
->packageID
);
655 // localize package information
656 $sql = "INSERT INTO wcf" . WCF_N
. "_language_item
657 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
658 VALUES (?, ?, ?, ?, ?)";
659 $statement = WCF
::getDB()->prepareStatement($sql);
662 $languageList = new LanguageList();
663 $languageList->readObjects();
665 // workaround for WCFSetup
668 FROM wcf" . WCF_N
. "_language_category
669 WHERE languageCategory = ?";
670 $statement2 = WCF
::getDB()->prepareStatement($sql);
671 $statement2->execute(['wcf.acp.package']);
672 $languageCategory = $statement2->fetchObject(LanguageCategory
::class);
674 $languageCategory = LanguageFactory
::getInstance()->getCategory('wcf.acp.package');
678 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
680 // save package description
681 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
683 // update description and name
684 $packageEditor = new PackageEditor($package);
685 $packageEditor->update([
686 'packageDescription' => 'wcf.acp.package.packageDescription.package' . $this->queue
->packageID
,
687 'packageName' => 'wcf.acp.package.packageName.package' . $this->queue
->packageID
,
692 * Saves a localized package info.
694 * @param PreparedStatement $statement
695 * @param LanguageList $languageList
696 * @param LanguageCategory $languageCategory
697 * @param Package $package
698 * @param string $infoName
700 protected function saveLocalizedPackageInfo(
701 PreparedStatement
$statement,
703 LanguageCategory
$languageCategory,
707 $infoValues = $this->getArchive()->getPackageInfo($infoName);
709 // get default value for languages without specified information
711 if (isset($infoValues['default'])) {
712 $defaultValue = $infoValues['default'];
713 } elseif (isset($infoValues['en'])) {
714 // fallback to English
715 $defaultValue = $infoValues['en'];
716 } elseif (isset($infoValues[WCF
::getLanguage()->getFixedLanguageCode()])) {
717 // fallback to the language of the current user
718 $defaultValue = $infoValues[WCF
::getLanguage()->getFixedLanguageCode()];
719 } elseif ($infoName == 'packageName') {
720 // fallback to the package identifier for the package name
721 $defaultValue = $this->getArchive()->getPackageInfo('name');
724 foreach ($languageList as $language) {
725 $value = $defaultValue;
726 if (isset($infoValues[$language->languageCode
])) {
727 $value = $infoValues[$language->languageCode
];
730 $statement->execute([
731 $language->languageID
,
732 'wcf.acp.package.' . $infoName . '.package' . $package->packageID
,
734 $languageCategory->languageCategoryID
,
741 * Executes a package installation plugin.
743 * @param mixed[] $nodeData
744 * @return PackageInstallationStep
745 * @throws SystemException
747 protected function executePIP(array $nodeData)
749 $step = new PackageInstallationStep();
751 if ($nodeData['pip'] == PackageArchive
::VOID_MARKER
) {
755 // fetch all pips associated with current PACKAGE_ID and include pips
756 // previously installed by current installation queue
757 $sql = "SELECT pluginName, className
758 FROM wcf" . WCF_N
. "_package_installation_plugin
759 WHERE pluginName = ?";
760 $statement = WCF
::getDB()->prepareStatement($sql);
761 $statement->execute([$nodeData['pip']]);
762 $row = $statement->fetchArray();
765 if (!$row ||
(\
strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
766 throw new SystemException("unable to find package installation plugin '" . $nodeData['pip'] . "'");
769 // valdidate class definition
770 $className = $row['className'];
771 if (!\
class_exists($className)) {
772 throw new SystemException("unable to find class '" . $className . "'");
776 if (empty($nodeData['value'])) {
777 $defaultValue = \
call_user_func([$className, 'getDefaultFilename']);
779 $nodeData['value'] = $defaultValue;
783 $plugin = new $className($this, $nodeData);
785 if (!($plugin instanceof IPackageInstallationPlugin
)) {
786 throw new ImplementationException($className, IPackageInstallationPlugin
::class);
792 $document = $plugin->{$this->action
}();
793 } catch (SplitNodeException
$e) {
794 $step->setSplitNode($e);
797 if ($document !== null && ($document instanceof FormDocument
)) {
798 $step->setDocument($document);
799 $step->setSplitNode();
806 * Displays a list to select optional packages or installs selection.
808 * @param string $currentNode
809 * @param array $nodeData
810 * @return PackageInstallationStep
812 protected function selectOptionalPackages($currentNode, array $nodeData)
814 $installationStep = new PackageInstallationStep();
816 $document = $this->promptOptionalPackages($nodeData);
817 if ($document !== null && $document instanceof FormDocument
) {
818 $installationStep->setDocument($document);
819 $installationStep->setSplitNode();
820 } // insert new nodes for each package
821 elseif (\
is_array($document)) {
822 // get target child node
823 $node = $currentNode;
824 $queue = $this->queue
;
827 foreach ($nodeData as $package) {
828 if (\
in_array($package['package'], $document)) {
829 // ignore uninstallable packages
830 if (!$package['isInstallable']) {
835 $this->nodeBuilder
->shiftNodes($currentNode, 'tempNode');
839 $queue = PackageInstallationQueueEditor
::create([
840 'parentQueueID' => $queue->queueID
,
841 'processNo' => $this->queue
->processNo
,
842 'userID' => WCF
::getUser()->userID
,
843 'package' => $package['package'],
844 'packageName' => $package['packageName'],
845 'archive' => $package['archive'],
846 'action' => $queue->action
,
849 $installation = new self($queue);
850 $installation->nodeBuilder
->setParentNode($node);
851 $installation->nodeBuilder
->buildNodes();
852 $node = $installation->nodeBuilder
->getCurrentNode();
855 @\
unlink($package['archive']);
861 $this->nodeBuilder
->shiftNodes('tempNode', $node);
865 return $installationStep;
869 * Extracts files from .tar(.gz) archive and installs them
871 * @param string $targetDir
872 * @param string $sourceArchive
873 * @param IFileHandler $fileHandler
876 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null)
878 return new Installer($targetDir, $sourceArchive, $fileHandler);
882 * Returns current package.
884 * @return \wcf\data\package\Package
886 public function getPackage()
888 if ($this->package
=== null) {
889 $this->package
= new Package($this->queue
->packageID
);
892 return $this->package
;
896 * Prompts for a text input for package directory (applies for applications only)
898 * @param string $applicationDirectory
899 * @return FormDocument
901 protected function promptPackageDir($applicationDirectory)
903 // check for pre-defined directories originating from WCFSetup
904 $directory = WCF
::getSession()->getVar('__wcfSetup_directories');
905 $abbreviation = Package
::getAbbreviation($this->getPackage()->package
);
906 if ($directory !== null) {
907 $directory = $directory[$abbreviation] ??
null;
909 ENABLE_ENTERPRISE_MODE
910 && \
defined('ENTERPRISE_MODE_APP_DIRECTORIES')
911 && \
is_array(ENTERPRISE_MODE_APP_DIRECTORIES
)
913 $directory = ENTERPRISE_MODE_APP_DIRECTORIES
[$abbreviation] ??
null;
916 if ($directory === null && !PackageInstallationFormManager
::findForm($this->queue
, 'packageDir')) {
917 $container = new GroupFormElementContainer();
918 $packageDir = new TextInputFormElement($container);
919 $packageDir->setName('packageDir');
920 $packageDir->setLabel(WCF
::getLanguage()->get('wcf.acp.package.packageDir.input'));
922 // check if there are packages installed in a parent
923 // directory of WCF, or if packages are below it
924 $sql = "SELECT packageDir
925 FROM wcf" . WCF_N
. "_package
926 WHERE packageDir <> ''";
927 $statement = WCF
::getDB()->prepareStatement($sql);
928 $statement->execute();
931 while ($column = $statement->fetchColumn()) {
932 if ($isParent !== null) {
936 if (\
preg_match('~^\.\./[^\.]~', $column)) {
938 } elseif (\
mb_strpos($column, '.') !== 0) {
943 $defaultPath = WCF_DIR
;
944 if ($isParent === false) {
945 $defaultPath = \
dirname(WCF_DIR
);
947 if (!$applicationDirectory) {
948 $applicationDirectory = Package
::getAbbreviation($this->getPackage()->package
);
950 $defaultPath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeparator($defaultPath)) . $applicationDirectory . '/';
952 $packageDir->setValue($defaultPath);
953 $container->appendChild($packageDir);
955 $document = new FormDocument('packageDir');
956 $document->appendContainer($container);
958 PackageInstallationFormManager
::registerForm($this->queue
, $document);
962 if ($directory !== null) {
964 $packageDir = $directory;
966 $document = PackageInstallationFormManager
::getForm($this->queue
, 'packageDir');
967 $document->handleRequest();
968 $packageDir = FileUtil
::addTrailingSlash(FileUtil
::getRealPath(FileUtil
::unifyDirSeparator(
969 $document->getValue('packageDir')
971 if ($packageDir === '/') {
976 if ($packageDir !== null) {
977 // validate package dir
978 if ($document !== null && \file_exists
($packageDir . 'global.php')) {
981 WCF
::getLanguage()->get('wcf.acp.package.packageDir.notAvailable')
988 $packageEditor = new PackageEditor($this->getPackage());
989 $packageEditor->update([
990 'packageDir' => FileUtil
::getRelativePath(WCF_DIR
, $packageDir),
993 // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
994 // faked and differ from the real filesystem path
996 $wcfDomainPath = ApplicationHandler
::getInstance()->getWCF()->domainPath
;
998 $sql = "SELECT domainPath
999 FROM wcf" . WCF_N
. "_application
1000 WHERE packageID = ?";
1001 $statement = WCF
::getDB()->prepareStatement($sql);
1002 $statement->execute([1]);
1003 $row = $statement->fetchArray();
1005 $wcfDomainPath = $row['domainPath'];
1008 $documentRoot = \
substr(
1009 FileUtil
::unifyDirSeparator(WCF_DIR
),
1011 -\
strlen(FileUtil
::unifyDirSeparator($wcfDomainPath))
1013 $domainPath = FileUtil
::getRelativePath($documentRoot, $packageDir);
1014 if ($domainPath === './') {
1015 // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory
1019 $domainPath = FileUtil
::addLeadingSlash($domainPath);
1021 // update application path and untaint application
1022 $application = new Application($this->getPackage()->packageID
);
1023 $applicationEditor = new ApplicationEditor($application);
1024 $applicationEditor->update([
1025 'domainPath' => $domainPath,
1029 // create directory and set permissions
1030 @\
mkdir($packageDir, 0777, true);
1031 FileUtil
::makeWritable($packageDir);
1039 * Prompts a selection of optional packages.
1041 * @param string[][] $packages
1044 protected function promptOptionalPackages(array $packages)
1046 if (!PackageInstallationFormManager
::findForm($this->queue
, 'optionalPackages')) {
1047 $container = new MultipleSelectionFormElementContainer();
1048 $container->setName('optionalPackages');
1049 $container->setLabel(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages'));
1050 $container->setDescription(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
1052 foreach ($packages as $package) {
1053 $optionalPackage = new MultipleSelectionFormElement($container);
1054 $optionalPackage->setName('optionalPackages');
1055 $optionalPackage->setLabel($package['packageName']);
1056 $optionalPackage->setValue($package['package']);
1057 $optionalPackage->setDescription($package['packageDescription']);
1058 if (!$package['isInstallable']) {
1059 $optionalPackage->setDisabledMessage(
1060 WCF
::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements')
1064 $container->appendChild($optionalPackage);
1067 $document = new FormDocument('optionalPackages');
1068 $document->appendContainer($container);
1070 PackageInstallationFormManager
::registerForm($this->queue
, $document);
1074 $document = PackageInstallationFormManager
::getForm($this->queue
, 'optionalPackages');
1075 $document->handleRequest();
1077 return $document->getValue('optionalPackages');
1082 * Returns current package id.
1086 public function getPackageID()
1088 return $this->queue
->packageID
;
1092 * Returns current package name.
1094 * @return string package name
1097 public function getPackageName()
1099 return $this->queue
->packageName
;
1103 * Returns current package installation type.
1107 public function getAction()
1109 return $this->action
;
1113 * Opens the package installation queue and
1114 * starts the installation, update or uninstallation of the first entry.
1116 * @param int $parentQueueID
1117 * @param int $processNo
1119 public static function openQueue($parentQueueID = 0, $processNo = 0)
1121 $conditions = new PreparedStatementConditionBuilder();
1122 $conditions->add("userID = ?", [WCF
::getUser()->userID
]);
1123 $conditions->add("parentQueueID = ?", [$parentQueueID]);
1124 if ($processNo != 0) {
1125 $conditions->add("processNo = ?", [$processNo]);
1127 $conditions->add("done = ?", [0]);
1130 FROM wcf" . WCF_N
. "_package_installation_queue
1132 ORDER BY queueID ASC";
1133 $statement = WCF
::getDB()->prepareStatement($sql);
1134 $statement->execute($conditions->getParameters());
1135 $packageInstallation = $statement->fetchArray();
1137 if (!isset($packageInstallation['queueID'])) {
1138 $url = LinkHandler
::getInstance()->getLink('PackageList');
1139 HeaderUtil
::redirect($url);
1143 $url = LinkHandler
::getInstance()->getLink(
1144 'PackageInstallationConfirm',
1146 'queueID=' . $packageInstallation['queueID']
1148 HeaderUtil
::redirect($url);
1155 * Checks the package installation queue for outstanding entries.
1159 public static function checkPackageInstallationQueue()
1161 $sql = "SELECT queueID
1162 FROM wcf" . WCF_N
. "_package_installation_queue
1164 AND parentQueueID = 0
1166 ORDER BY queueID ASC";
1167 $statement = WCF
::getDB()->prepareStatement($sql);
1168 $statement->execute([WCF
::getUser()->userID
]);
1169 $row = $statement->fetchArray();
1175 return $row['queueID'];
1179 * Executes post-setup actions.
1181 public function completeSetup()
1184 $sql = "SELECT archive
1185 FROM wcf" . WCF_N
. "_package_installation_queue
1186 WHERE processNo = ?";
1187 $statement = WCF
::getDB()->prepareStatement($sql);
1188 $statement->execute([$this->queue
->processNo
]);
1189 while ($row = $statement->fetchArray()) {
1190 @\
unlink($row['archive']);
1194 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_queue
1195 WHERE processNo = ?";
1196 $statement = WCF
::getDB()->prepareStatement($sql);
1197 $statement->execute([$this->queue
->processNo
]);
1199 // clear language files once whole installation is completed
1200 LanguageEditor
::deleteLanguageFiles();
1203 CacheHandler
::getInstance()->flushAll();
1207 * Updates queue information.
1209 public function updatePackage()
1211 if (empty($this->queue
->packageName
)) {
1212 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
1213 $queueEditor->update([
1214 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName'),
1218 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
1223 * Validates specific php requirements.
1225 * @param array $requirements
1228 public static function validatePHPRequirements(array $requirements)
1232 // validate php version
1233 if (isset($requirements['version'])) {
1235 if (\version_compare
(\PHP_VERSION
, $requirements['version'], '>=')) {
1240 $errors['version'] = [
1241 'required' => $requirements['version'],
1242 'installed' => \PHP_VERSION
,
1247 // validate extensions
1248 if (isset($requirements['extensions'])) {
1249 foreach ($requirements['extensions'] as $extension) {
1250 $passed = \
extension_loaded($extension) ?
true : false;
1253 $errors['extension'][] = [
1254 'extension' => $extension,
1260 // validate settings
1261 if (isset($requirements['settings'])) {
1262 foreach ($requirements['settings'] as $setting => $value) {
1263 $iniValue = \
ini_get($setting);
1265 $passed = self
::compareSetting($setting, $value, $iniValue);
1267 $errors['setting'][] = [
1268 'setting' => $setting,
1269 'required' => $value,
1270 'installed' => ($iniValue === false) ?
'(unknown)' : $iniValue,
1276 // validate functions
1277 if (isset($requirements['functions'])) {
1278 foreach ($requirements['functions'] as $function) {
1279 $function = \
mb_strtolower($function);
1281 $passed = self
::functionExists($function);
1283 $errors['function'][] = [
1284 'function' => $function,
1291 if (isset($requirements['classes'])) {
1292 foreach ($requirements['classes'] as $class) {
1295 // see: http://de.php.net/manual/en/language.oop5.basic.php
1296 if (\
preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
1297 $globalClass = '\\' . $class;
1299 if (\
class_exists($globalClass, false)) {
1305 $errors['class'][] = [
1316 * Validates if an function exists and is not blacklisted by suhosin extension.
1318 * @param string $function
1320 * @see http://de.php.net/manual/en/function.function-exists.php#77980
1322 protected static function functionExists($function)
1324 if (\
extension_loaded('suhosin')) {
1325 $blacklist = @\
ini_get('suhosin.executor.func.blacklist');
1326 if (!empty($blacklist)) {
1327 $blacklist = \
explode(',', $blacklist);
1328 foreach ($blacklist as $disabledFunction) {
1329 $disabledFunction = \
mb_strtolower(StringUtil
::trim($disabledFunction));
1331 if ($function == $disabledFunction) {
1338 return \function_exists
($function);
1342 * Compares settings, converting values into compareable ones.
1344 * @param string $setting
1345 * @param string $value
1346 * @param mixed $compareValue
1349 protected static function compareSetting($setting, $value, $compareValue)
1351 if ($compareValue === false) {
1355 $value = \
mb_strtolower($value);
1356 $trueValues = ['1', 'on', 'true'];
1357 $falseValues = ['0', 'off', 'false'];
1359 // handle values considered as 'true'
1360 if (\
in_array($value, $trueValues)) {
1361 return $compareValue ?
true : false;
1362 } // handle values considered as 'false'
1363 elseif (\
in_array($value, $falseValues)) {
1364 return (!$compareValue) ?
true : false;
1365 } elseif (!\
is_numeric($value)) {
1366 $compareValue = self
::convertShorthandByteValue($compareValue);
1367 $value = self
::convertShorthandByteValue($value);
1370 return ($compareValue >= $value) ?
true : false;
1374 * Converts shorthand byte values into an integer representing bytes.
1376 * @param string $value
1378 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1380 protected static function convertShorthandByteValue($value)
1382 // convert into bytes
1383 $lastCharacter = \
mb_substr($value, -1);
1384 switch ($lastCharacter) {
1387 return (int)$value * 1073741824;
1392 return (int)$value * 1048576;
1397 return (int)$value * 1024;