2 namespace wcf\system\package
;
3 use wcf\data\application\Application
;
4 use wcf\data\application\ApplicationEditor
;
5 use wcf\data\devtools\project\DevtoolsProjectAction
;
6 use wcf\data\language\category\LanguageCategory
;
7 use wcf\data\language\LanguageEditor
;
8 use wcf\data\language\LanguageList
;
9 use wcf\data\option\OptionEditor
;
10 use wcf\data\package\installation\queue\PackageInstallationQueue
;
11 use wcf\data\package\installation\queue\PackageInstallationQueueEditor
;
12 use wcf\data\package\Package
;
13 use wcf\data\package\PackageEditor
;
14 use wcf\data\user\User
;
15 use wcf\data\user\UserAction
;
16 use wcf\system\application\ApplicationHandler
;
17 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder
;
18 use wcf\system\cache\CacheHandler
;
19 use wcf\system\database\statement\PreparedStatement
;
20 use wcf\system\database\util\PreparedStatementConditionBuilder
;
21 use wcf\system\devtools\DevtoolsSetup
;
22 use wcf\system\event\EventHandler
;
23 use wcf\system\exception\ImplementationException
;
24 use wcf\system\exception\SystemException
;
25 use wcf\system\form\container\GroupFormElementContainer
;
26 use wcf\system\form\container\MultipleSelectionFormElementContainer
;
27 use wcf\system\form\element\MultipleSelectionFormElement
;
28 use wcf\system\form\element\TextInputFormElement
;
29 use wcf\system\form\FormDocument
;
30 use wcf\system\language\LanguageFactory
;
31 use wcf\system\package\plugin\IPackageInstallationPlugin
;
32 use wcf\system\request\LinkHandler
;
33 use wcf\system\request\RouteHandler
;
34 use wcf\system\setup\IFileHandler
;
35 use wcf\system\setup\Installer
;
36 use wcf\system\style\StyleHandler
;
37 use wcf\system\user\storage\UserStorageHandler
;
39 use wcf\util\FileUtil
;
40 use wcf\util\HeaderUtil
;
42 use wcf\util\StringUtil
;
45 * PackageInstallationDispatcher handles the whole installation process.
47 * @author Alexander Ebert
48 * @copyright 2001-2019 WoltLab GmbH
49 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
50 * @package WoltLabSuite\Core\System\Package
52 class PackageInstallationDispatcher
{
54 * current installation type
57 protected $action = '';
60 * instance of PackageArchive
66 * instance of PackageInstallationNodeBuilder
67 * @var PackageInstallationNodeBuilder
78 * instance of PackageInstallationQueue
79 * @var PackageInstallationQueue
84 * default name of the config file
87 const CONFIG_FILE
= 'app.config.inc.php';
90 * data of previous package in queue
93 protected $previousPackageData;
96 * Creates a new instance of PackageInstallationDispatcher.
98 * @param PackageInstallationQueue $queue
100 public function __construct(PackageInstallationQueue
$queue) {
101 $this->queue
= $queue;
102 $this->nodeBuilder
= new PackageInstallationNodeBuilder($this);
104 $this->action
= $this->queue
->action
;
108 * Sets data of previous package in queue.
110 * @param string[] $packageData
112 public function setPreviousPackage(array $packageData) {
113 $this->previousPackageData
= $packageData;
117 * Installs node components and returns next node.
119 * @param string $node
120 * @return PackageInstallationStep
121 * @throws SystemException
123 public function install($node) {
124 $nodes = $this->nodeBuilder
->getNodeData($node);
126 // guard against possible issues with empty instruction blocks, including
127 // these blocks that contain no valid instructions at all (e.g. typo from
129 throw new SystemException("Failed to retrieve nodes for identifier '".$node."', the query returned no results.");
132 // invoke node-specific actions
134 foreach ($nodes as $data) {
135 $nodeData = unserialize($data['nodeData']);
136 $this->logInstallationStep($data);
138 switch ($data['nodeType']) {
140 $step = $this->installPackage($nodeData);
144 $step = $this->executePIP($nodeData);
147 case 'optionalPackages':
148 $step = $this->selectOptionalPackages($node, $nodeData);
152 die("Unknown node type: '".$data['nodeType']."'");
156 if ($step->splitNode()) {
158 if ($step->getException() !== null && $step->getException()->getMessage()) {
159 $log .= ': ' . $step->getException()->getMessage();
162 $this->logInstallationStep($data, $log);
163 $this->nodeBuilder
->cloneNode($node, $data['sequenceNo']);
168 // mark node as completed
169 $this->nodeBuilder
->completeNode($node);
172 $node = $this->nodeBuilder
->getNextNode($node);
173 $step->setNode($node);
175 // perform post-install/update actions
177 $this->logInstallationStep([], 'start cleanup');
179 // update "last update time" option
180 $sql = "UPDATE wcf".WCF_N
."_option
182 WHERE optionName = ?";
183 $statement = WCF
::getDB()->prepareStatement($sql);
184 $statement->execute([
189 // update options.inc.php
190 OptionEditor
::resetCache();
192 if ($this->action
== 'install') {
193 // save localized package infos
194 $this->saveLocalizedPackageInfos();
196 // remove all cache files after WCFSetup
198 CacheHandler
::getInstance()->flushAll();
200 $sql = "UPDATE wcf".WCF_N
."_option
202 WHERE optionName = ?";
203 $statement = WCF
::getDB()->prepareStatement($sql);
205 $statement->execute([
206 StringUtil
::getUUID(),
210 if (file_exists(WCF_DIR
. 'cookiePrefix.txt')) {
211 $statement->execute([
216 @unlink
(WCF_DIR
. 'cookiePrefix.txt');
220 $statement->execute([
224 $statement->execute([
228 $statement->execute([
234 $statement->execute([
235 bin2hex(\random_bytes
(20)),
239 catch (\Throwable
$e) {
240 // ignore, the secret will stay empty and crypto operations
241 // depending on it will fail
244 if (WCF
::getSession()->getVar('__wcfSetup_developerMode')) {
245 $statement->execute([
249 $statement->execute([
253 $statement->execute([
257 $statement->execute([
259 'enable_developer_tools'
261 $statement->execute([
263 'log_missing_language_items'
266 foreach (DevtoolsSetup
::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
267 $statement->execute([
273 foreach (DevtoolsSetup
::getInstance()->getUsers() as $newUser) {
275 (new UserAction([], 'create', [
277 'email' => $newUser['email'],
278 'password' => $newUser['password'],
279 'username' => $newUser['username']
285 ]))->executeAction();
287 catch (SystemException
$e) {
288 // ignore errors due to event listeners missing at this
289 // point during installation
293 if (($importPath = DevtoolsSetup
::getInstance()->getDevtoolsImportPath()) !== '') {
294 (new DevtoolsProjectAction([], 'quickSetup', [
295 'path' => $importPath
296 ]))->executeAction();
300 if (WCF
::getSession()->getVar('__wcfSetup_imagick')) {
301 $statement->execute([
303 'image_adapter_type',
307 // update options.inc.php
308 OptionEditor
::resetCache();
310 WCF
::getSession()->register('__wcfSetup_completed', true);
313 // rebuild application paths
314 ApplicationHandler
::rebuild();
317 // remove template listener cache
318 TemplateListenerCodeCacheBuilder
::getInstance()->reset();
320 // reset language cache
321 LanguageFactory
::getInstance()->clearCache();
322 LanguageFactory
::getInstance()->deleteLanguageCache();
325 StyleHandler
::resetStylesheets();
327 // clear user storage
328 UserStorageHandler
::getInstance()->clear();
330 // rebuild config files for affected applications
331 $sql = "SELECT package.packageID
332 FROM wcf".WCF_N
."_package_installation_queue queue,
333 wcf".WCF_N
."_package package
334 WHERE queue.processNo = ?
335 AND package.packageID = queue.packageID
336 AND package.isApplication = ?";
337 $statement = WCF
::getDB()->prepareStatement($sql);
338 $statement->execute([
339 $this->queue
->processNo
,
342 while ($row = $statement->fetchArray()) {
343 Package
::writeConfigFile($row['packageID']);
346 EventHandler
::getInstance()->fireAction($this, 'postInstall');
349 $sql = "SELECT archive
350 FROM wcf".WCF_N
."_package_installation_queue
351 WHERE processNo = ?";
352 $statement = WCF
::getDB()->prepareStatement($sql);
353 $statement->execute([$this->queue
->processNo
]);
354 while ($row = $statement->fetchArray()) {
355 @unlink
($row['archive']);
359 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
360 WHERE processNo = ?";
361 $statement = WCF
::getDB()->prepareStatement($sql);
362 $statement->execute([$this->queue
->processNo
]);
364 $this->logInstallationStep([], 'finished cleanup');
371 * Logs an installation step.
373 * @param array $node data of the executed node
374 * @param string $log optional additional log text
376 protected function logInstallationStep(array $node = [], $log = '') {
377 $logEntry = "[" . TIME_NOW
. "]\n";
379 $logEntry .= 'sequenceNo: ' . $node['sequenceNo'] . "\n";
380 $logEntry .= 'nodeType: ' . $node['nodeType'] . "\n";
381 $logEntry .= "nodeData:\n";
383 $nodeData = unserialize($node['nodeData']);
384 foreach ($nodeData as $index => $value) {
385 $logEntry .= "\t" . $index . ': ' . (!is_object($value) && !is_array($value) ?
$value : JSON
::encode($value)) . "\n";
390 $logEntry .= 'additional information: ' . $log . "\n";
393 $logEntry .= str_repeat('-', 30) . "\n\n";
396 WCF_DIR
. 'log/' . date('Y-m-d', TIME_NOW
) . '-update-' . $this->queue
->queueID
. '.txt',
403 * Returns current package archive.
405 * @return PackageArchive
407 public function getArchive() {
408 if ($this->archive
=== null) {
409 // check if we're doing an iterative update of the same package
410 if ($this->previousPackageData
!== null && $this->getPackage()->package
== $this->previousPackageData
['package']) {
411 if (Package
::compareVersion($this->getPackage()->packageVersion
, $this->previousPackageData
['packageVersion'], '<')) {
412 // fake package to simulate the package version required by current archive
413 $this->getPackage()->setPackageVersion($this->previousPackageData
['packageVersion']);
417 $this->archive
= new PackageArchive($this->queue
->archive
, $this->getPackage());
419 if (FileUtil
::isURL($this->archive
->getArchive())) {
420 // get return value and update entry in
421 // package_installation_queue with this value
422 $archive = $this->archive
->downloadArchive();
423 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
424 $queueEditor->update(['archive' => $archive]);
427 $this->archive
->openArchive();
430 return $this->archive
;
434 * Installs current package.
436 * @param mixed[] $nodeData
437 * @return PackageInstallationStep
438 * @throws SystemException
440 protected function installPackage(array $nodeData) {
441 $installationStep = new PackageInstallationStep();
443 // check requirements
444 if (!empty($nodeData['requirements'])) {
445 foreach ($nodeData['requirements'] as $package => $requirementData) {
446 // get existing package
447 if ($requirementData['packageID']) {
448 $sql = "SELECT packageName, packageVersion
449 FROM wcf".WCF_N
."_package
450 WHERE packageID = ?";
451 $statement = WCF
::getDB()->prepareStatement($sql);
452 $statement->execute([$requirementData['packageID']]);
455 // try to find matching package
456 $sql = "SELECT packageName, packageVersion
457 FROM wcf".WCF_N
."_package
459 $statement = WCF
::getDB()->prepareStatement($sql);
460 $statement->execute([$package]);
462 $row = $statement->fetchArray();
464 // package is required but not available
465 if ($row === false) {
466 throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
469 // check version requirements
470 if ($requirementData['minVersion']) {
471 if (Package
::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
472 throw new SystemException("Package '".$nodeData['packageName']."' requires package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but only version '".$row['packageVersion']."' is installed");
477 unset($nodeData['requirements']);
479 $applicationDirectory = '';
480 if (isset($nodeData['applicationDirectory'])) {
481 $applicationDirectory = $nodeData['applicationDirectory'];
482 unset($nodeData['applicationDirectory']);
486 if ($this->queue
->packageID
) {
487 $packageEditor = new PackageEditor(new Package($this->queue
->packageID
));
488 unset($nodeData['installDate']);
489 $packageEditor->update($nodeData);
491 // delete old excluded packages
492 $sql = "DELETE FROM wcf".WCF_N
."_package_exclusion
493 WHERE packageID = ?";
494 $statement = WCF
::getDB()->prepareStatement($sql);
495 $statement->execute([$this->queue
->packageID
]);
497 // delete old compatibility versions
498 $sql = "DELETE FROM wcf".WCF_N
."_package_compatibility
499 WHERE packageID = ?";
500 $statement = WCF
::getDB()->prepareStatement($sql);
501 $statement->execute([$this->queue
->packageID
]);
503 // delete old requirements and dependencies
504 $sql = "DELETE FROM wcf".WCF_N
."_package_requirement
505 WHERE packageID = ?";
506 $statement = WCF
::getDB()->prepareStatement($sql);
507 $statement->execute([$this->queue
->packageID
]);
510 // create package entry
511 $package = $this->createPackage($nodeData);
513 // update package id for current queue
514 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
515 $queueEditor->update(['packageID' => $package->packageID
]);
518 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
519 $this->package
= null;
521 if ($package->isApplication
) {
522 $host = str_replace(RouteHandler
::getProtocol(), '', RouteHandler
::getHost());
523 $path = RouteHandler
::getPath(['acp']);
525 // insert as application
526 ApplicationEditor
::create([
527 'domainName' => $host,
528 'domainPath' => $path,
529 'cookieDomain' => $host,
530 'packageID' => $package->packageID
535 // save excluded packages
536 if (count($this->getArchive()->getExcludedPackages())) {
537 $sql = "INSERT INTO wcf".WCF_N
."_package_exclusion
538 (packageID, excludedPackage, excludedPackageVersion)
540 $statement = WCF
::getDB()->prepareStatement($sql);
542 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
543 $statement->execute([
544 $this->queue
->packageID
,
545 $excludedPackage['name'],
546 !empty($excludedPackage['version']) ?
$excludedPackage['version'] : ''
551 // save compatible versions
552 if (!empty($this->getArchive()->getCompatibleVersions())) {
553 $sql = "INSERT INTO wcf".WCF_N
."_package_compatibility
556 $statement = WCF
::getDB()->prepareStatement($sql);
558 foreach ($this->getArchive()->getCompatibleVersions() as $version) {
559 $statement->execute([
560 $this->queue
->packageID
,
566 // insert requirements and dependencies
567 $requirements = $this->getArchive()->getAllExistingRequirements();
568 if (!empty($requirements)) {
569 $sql = "INSERT INTO wcf".WCF_N
."_package_requirement
570 (packageID, requirement)
572 $statement = WCF
::getDB()->prepareStatement($sql);
574 foreach ($requirements as $identifier => $possibleRequirements) {
575 $requirement = array_shift($possibleRequirements);
577 $statement->execute([
578 $this->queue
->packageID
,
579 $requirement['packageID']
584 if ($this->getPackage()->isApplication
&& $this->getPackage()->package
!= 'com.woltlab.wcf' && $this->getAction() == 'install' && empty($this->getPackage()->packageDir
)) {
585 $document = $this->promptPackageDir($applicationDirectory);
586 if ($document !== null && $document instanceof FormDocument
) {
587 $installationStep->setDocument($document);
590 $installationStep->setSplitNode();
593 return $installationStep;
597 * Creates a new package based on the given data and returns it.
599 * @param array $packageData
603 protected function createPackage(array $packageData) {
604 return PackageEditor
::create($packageData);
608 * Saves the localized package info.
610 protected function saveLocalizedPackageInfos() {
611 $package = new Package($this->queue
->packageID
);
613 // localize package information
614 $sql = "INSERT INTO wcf".WCF_N
."_language_item
615 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
616 VALUES (?, ?, ?, ?, ?)";
617 $statement = WCF
::getDB()->prepareStatement($sql);
620 $languageList = new LanguageList();
621 $languageList->readObjects();
623 // workaround for WCFSetup
626 FROM wcf".WCF_N
."_language_category
627 WHERE languageCategory = ?";
628 $statement2 = WCF
::getDB()->prepareStatement($sql);
629 $statement2->execute(['wcf.acp.package']);
630 $languageCategory = $statement2->fetchObject(LanguageCategory
::class);
633 $languageCategory = LanguageFactory
::getInstance()->getCategory('wcf.acp.package');
637 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
639 // save package description
640 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
642 // update description and name
643 $packageEditor = new PackageEditor($package);
644 $packageEditor->update([
645 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue
->packageID
,
646 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue
->packageID
651 * Saves a localized package info.
653 * @param PreparedStatement $statement
654 * @param LanguageList $languageList
655 * @param LanguageCategory $languageCategory
656 * @param Package $package
657 * @param string $infoName
659 protected function saveLocalizedPackageInfo(PreparedStatement
$statement, $languageList, LanguageCategory
$languageCategory, Package
$package, $infoName) {
660 $infoValues = $this->getArchive()->getPackageInfo($infoName);
662 // get default value for languages without specified information
664 if (isset($infoValues['default'])) {
665 $defaultValue = $infoValues['default'];
667 else if (isset($infoValues['en'])) {
668 // fallback to English
669 $defaultValue = $infoValues['en'];
671 else if (isset($infoValues[WCF
::getLanguage()->getFixedLanguageCode()])) {
672 // fallback to the language of the current user
673 $defaultValue = $infoValues[WCF
::getLanguage()->getFixedLanguageCode()];
675 else if ($infoName == 'packageName') {
676 // fallback to the package identifier for the package name
677 $defaultValue = $this->getArchive()->getPackageInfo('name');
680 foreach ($languageList as $language) {
681 $value = $defaultValue;
682 if (isset($infoValues[$language->languageCode
])) {
683 $value = $infoValues[$language->languageCode
];
686 $statement->execute([
687 $language->languageID
,
688 'wcf.acp.package.'.$infoName.'.package'.$package->packageID
,
690 $languageCategory->languageCategoryID
,
697 * Executes a package installation plugin.
699 * @param mixed[] $nodeData
700 * @return PackageInstallationStep
701 * @throws SystemException
703 protected function executePIP(array $nodeData) {
704 $step = new PackageInstallationStep();
706 if ($nodeData['pip'] == PackageArchive
::VOID_MARKER
) {
710 // fetch all pips associated with current PACKAGE_ID and include pips
711 // previously installed by current installation queue
712 $sql = "SELECT pluginName, className
713 FROM wcf".WCF_N
."_package_installation_plugin
714 WHERE pluginName = ?";
715 $statement = WCF
::getDB()->prepareStatement($sql);
716 $statement->execute([$nodeData['pip']]);
717 $row = $statement->fetchArray();
720 if (!$row ||
(strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
721 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
724 // valdidate class definition
725 $className = $row['className'];
726 if (!class_exists($className)) {
727 throw new SystemException("unable to find class '".$className."'");
731 if (empty($nodeData['value'])) {
732 $defaultValue = call_user_func([$className, 'getDefaultFilename']);
734 $nodeData['value'] = $defaultValue;
738 $plugin = new $className($this, $nodeData);
740 if (!($plugin instanceof IPackageInstallationPlugin
)) {
741 throw new ImplementationException($className, IPackageInstallationPlugin
::class);
747 $document = $plugin->{$this->action
}();
749 catch (SplitNodeException
$e) {
750 $step->setSplitNode($e);
753 if ($document !== null && ($document instanceof FormDocument
)) {
754 $step->setDocument($document);
755 $step->setSplitNode();
762 * Displays a list to select optional packages or installs selection.
764 * @param string $currentNode
765 * @param array $nodeData
766 * @return PackageInstallationStep
768 protected function selectOptionalPackages($currentNode, array $nodeData) {
769 $installationStep = new PackageInstallationStep();
771 $document = $this->promptOptionalPackages($nodeData);
772 if ($document !== null && $document instanceof FormDocument
) {
773 $installationStep->setDocument($document);
774 $installationStep->setSplitNode();
776 // insert new nodes for each package
777 else if (is_array($document)) {
778 // get target child node
779 $node = $currentNode;
780 $queue = $this->queue
;
783 foreach ($nodeData as $package) {
784 if (in_array($package['package'], $document)) {
785 // ignore uninstallable packages
786 if (!$package['isInstallable']) {
791 $this->nodeBuilder
->shiftNodes($currentNode, 'tempNode');
795 $queue = PackageInstallationQueueEditor
::create([
796 'parentQueueID' => $queue->queueID
,
797 'processNo' => $this->queue
->processNo
,
798 'userID' => WCF
::getUser()->userID
,
799 'package' => $package['package'],
800 'packageName' => $package['packageName'],
801 'archive' => $package['archive'],
802 'action' => $queue->action
805 $installation = new PackageInstallationDispatcher($queue);
806 $installation->nodeBuilder
->setParentNode($node);
807 $installation->nodeBuilder
->buildNodes();
808 $node = $installation->nodeBuilder
->getCurrentNode();
812 @unlink
($package['archive']);
818 $this->nodeBuilder
->shiftNodes('tempNode', $node);
822 return $installationStep;
826 * Extracts files from .tar(.gz) archive and installs them
828 * @param string $targetDir
829 * @param string $sourceArchive
830 * @param IFileHandler $fileHandler
833 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
834 return new Installer($targetDir, $sourceArchive, $fileHandler);
838 * Returns current package.
840 * @return \wcf\data\package\Package
842 public function getPackage() {
843 if ($this->package
=== null) {
844 $this->package
= new Package($this->queue
->packageID
);
847 return $this->package
;
851 * Prompts for a text input for package directory (applies for applications only)
853 * @param string $applicationDirectory
854 * @return FormDocument
856 protected function promptPackageDir($applicationDirectory) {
857 // check for pre-defined directories originating from WCFSetup
858 $directory = WCF
::getSession()->getVar('__wcfSetup_directories');
859 $abbreviation = Package
::getAbbreviation($this->getPackage()->package
);
860 if ($directory !== null) {
861 $directory = $directory[$abbreviation] ??
null;
863 else if (ENABLE_ENTERPRISE_MODE
&& defined('ENTERPRISE_MODE_APP_DIRECTORIES') && is_array(ENTERPRISE_MODE_APP_DIRECTORIES
)) {
864 $directory = ENTERPRISE_MODE_APP_DIRECTORIES
[$abbreviation] ??
null;
867 if ($directory === null && !PackageInstallationFormManager
::findForm($this->queue
, 'packageDir')) {
868 $container = new GroupFormElementContainer();
869 $packageDir = new TextInputFormElement($container);
870 $packageDir->setName('packageDir');
871 $packageDir->setLabel(WCF
::getLanguage()->get('wcf.acp.package.packageDir.input'));
873 // check if there are packages installed in a parent
874 // directory of WCF, or if packages are below it
875 $sql = "SELECT packageDir
876 FROM wcf".WCF_N
."_package
877 WHERE packageDir <> ''";
878 $statement = WCF
::getDB()->prepareStatement($sql);
879 $statement->execute();
882 while ($column = $statement->fetchColumn()) {
883 if ($isParent !== null) {
887 if (preg_match('~^\.\./[^\.]~', $column)) {
890 else if (mb_strpos($column, '.') !== 0) {
895 $defaultPath = WCF_DIR
;
896 if ($isParent === false) {
897 $defaultPath = dirname(WCF_DIR
);
899 if (!$applicationDirectory) {
900 $applicationDirectory = Package
::getAbbreviation($this->getPackage()->package
);
902 $defaultPath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeparator($defaultPath)) . $applicationDirectory . '/';
904 $packageDir->setValue($defaultPath);
905 $container->appendChild($packageDir);
907 $document = new FormDocument('packageDir');
908 $document->appendContainer($container);
910 PackageInstallationFormManager
::registerForm($this->queue
, $document);
914 if ($directory !== null) {
916 $packageDir = $directory;
919 $document = PackageInstallationFormManager
::getForm($this->queue
, 'packageDir');
920 $document->handleRequest();
921 $packageDir = FileUtil
::addTrailingSlash(FileUtil
::getRealPath(FileUtil
::unifyDirSeparator($document->getValue('packageDir'))));
922 if ($packageDir === '/') $packageDir = '';
925 if ($packageDir !== null) {
926 // validate package dir
927 if ($document !== null && file_exists($packageDir . 'global.php')) {
928 $document->setError('packageDir', WCF
::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
933 $packageEditor = new PackageEditor($this->getPackage());
934 $packageEditor->update([
935 'packageDir' => FileUtil
::getRelativePath(WCF_DIR
, $packageDir)
938 // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
939 // faked and differ from the real filesystem path
941 $wcfDomainPath = ApplicationHandler
::getInstance()->getWCF()->domainPath
;
944 $sql = "SELECT domainPath
945 FROM wcf".WCF_N
."_application
946 WHERE packageID = ?";
947 $statement = WCF
::getDB()->prepareStatement($sql);
948 $statement->execute([1]);
949 $row = $statement->fetchArray();
951 $wcfDomainPath = $row['domainPath'];
954 $documentRoot = substr(FileUtil
::unifyDirSeparator(WCF_DIR
), 0, -strlen(FileUtil
::unifyDirSeparator($wcfDomainPath)));
955 $domainPath = FileUtil
::getRelativePath($documentRoot, $packageDir);
956 if ($domainPath === './') {
957 // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory
961 $domainPath = FileUtil
::addLeadingSlash($domainPath);
963 // update application path
964 $application = new Application($this->getPackage()->packageID
);
965 $applicationEditor = new ApplicationEditor($application);
966 $applicationEditor->update(['domainPath' => $domainPath]);
968 // create directory and set permissions
969 @mkdir
($packageDir, 0777, true);
970 FileUtil
::makeWritable($packageDir);
978 * Prompts a selection of optional packages.
980 * @param string[][] $packages
983 protected function promptOptionalPackages(array $packages) {
984 if (!PackageInstallationFormManager
::findForm($this->queue
, 'optionalPackages')) {
985 $container = new MultipleSelectionFormElementContainer();
986 $container->setName('optionalPackages');
987 $container->setLabel(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages'));
988 $container->setDescription(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
990 foreach ($packages as $package) {
991 $optionalPackage = new MultipleSelectionFormElement($container);
992 $optionalPackage->setName('optionalPackages');
993 $optionalPackage->setLabel($package['packageName']);
994 $optionalPackage->setValue($package['package']);
995 $optionalPackage->setDescription($package['packageDescription']);
996 if (!$package['isInstallable']) {
997 $optionalPackage->setDisabledMessage(WCF
::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements'));
1000 $container->appendChild($optionalPackage);
1003 $document = new FormDocument('optionalPackages');
1004 $document->appendContainer($container);
1006 PackageInstallationFormManager
::registerForm($this->queue
, $document);
1010 $document = PackageInstallationFormManager
::getForm($this->queue
, 'optionalPackages');
1011 $document->handleRequest();
1013 return $document->getValue('optionalPackages');
1018 * Returns current package id.
1022 public function getPackageID() {
1023 return $this->queue
->packageID
;
1027 * Returns current package name.
1029 * @return string package name
1032 public function getPackageName() {
1033 return $this->queue
->packageName
;
1037 * Returns current package installation type.
1041 public function getAction() {
1042 return $this->action
;
1046 * Opens the package installation queue and
1047 * starts the installation, update or uninstallation of the first entry.
1049 * @param integer $parentQueueID
1050 * @param integer $processNo
1052 public static function openQueue($parentQueueID = 0, $processNo = 0) {
1053 $conditions = new PreparedStatementConditionBuilder();
1054 $conditions->add("userID = ?", [WCF
::getUser()->userID
]);
1055 $conditions->add("parentQueueID = ?", [$parentQueueID]);
1056 if ($processNo != 0) $conditions->add("processNo = ?", [$processNo]);
1057 $conditions->add("done = ?", [0]);
1060 FROM wcf".WCF_N
."_package_installation_queue
1062 ORDER BY queueID ASC";
1063 $statement = WCF
::getDB()->prepareStatement($sql);
1064 $statement->execute($conditions->getParameters());
1065 $packageInstallation = $statement->fetchArray();
1067 if (!isset($packageInstallation['queueID'])) {
1068 $url = LinkHandler
::getInstance()->getLink('PackageList');
1069 HeaderUtil
::redirect($url);
1073 $url = LinkHandler
::getInstance()->getLink('PackageInstallationConfirm', [], 'queueID='.$packageInstallation['queueID']);
1074 HeaderUtil
::redirect($url);
1080 * Checks the package installation queue for outstanding entries.
1084 public static function checkPackageInstallationQueue() {
1085 $sql = "SELECT queueID
1086 FROM wcf".WCF_N
."_package_installation_queue
1088 AND parentQueueID = 0
1090 ORDER BY queueID ASC";
1091 $statement = WCF
::getDB()->prepareStatement($sql);
1092 $statement->execute([WCF
::getUser()->userID
]);
1093 $row = $statement->fetchArray();
1099 return $row['queueID'];
1103 * Executes post-setup actions.
1105 public function completeSetup() {
1107 $sql = "SELECT archive
1108 FROM wcf".WCF_N
."_package_installation_queue
1109 WHERE processNo = ?";
1110 $statement = WCF
::getDB()->prepareStatement($sql);
1111 $statement->execute([$this->queue
->processNo
]);
1112 while ($row = $statement->fetchArray()) {
1113 @unlink
($row['archive']);
1117 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
1118 WHERE processNo = ?";
1119 $statement = WCF
::getDB()->prepareStatement($sql);
1120 $statement->execute([$this->queue
->processNo
]);
1122 // clear language files once whole installation is completed
1123 LanguageEditor
::deleteLanguageFiles();
1126 CacheHandler
::getInstance()->flushAll();
1130 * Updates queue information.
1132 public function updatePackage() {
1133 if (empty($this->queue
->packageName
)) {
1134 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
1135 $queueEditor->update([
1136 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
1140 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
1145 * Validates specific php requirements.
1147 * @param array $requirements
1150 public static function validatePHPRequirements(array $requirements) {
1153 // validate php version
1154 if (isset($requirements['version'])) {
1156 if (version_compare(PHP_VERSION
, $requirements['version'], '>=')) {
1161 $errors['version'] = [
1162 'required' => $requirements['version'],
1163 'installed' => PHP_VERSION
1168 // validate extensions
1169 if (isset($requirements['extensions'])) {
1170 foreach ($requirements['extensions'] as $extension) {
1171 $passed = extension_loaded($extension) ?
true : false;
1174 $errors['extension'][] = [
1175 'extension' => $extension
1181 // validate settings
1182 if (isset($requirements['settings'])) {
1183 foreach ($requirements['settings'] as $setting => $value) {
1184 $iniValue = ini_get($setting);
1186 $passed = self
::compareSetting($setting, $value, $iniValue);
1188 $errors['setting'][] = [
1189 'setting' => $setting,
1190 'required' => $value,
1191 'installed' => ($iniValue === false) ?
'(unknown)' : $iniValue
1197 // validate functions
1198 if (isset($requirements['functions'])) {
1199 foreach ($requirements['functions'] as $function) {
1200 $function = mb_strtolower($function);
1202 $passed = self
::functionExists($function);
1204 $errors['function'][] = [
1205 'function' => $function
1212 if (isset($requirements['classes'])) {
1213 foreach ($requirements['classes'] as $class) {
1216 // see: http://de.php.net/manual/en/language.oop5.basic.php
1217 if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
1218 $globalClass = '\\'.$class;
1220 if (class_exists($globalClass, false)) {
1226 $errors['class'][] = [
1237 * Validates if an function exists and is not blacklisted by suhosin extension.
1239 * @param string $function
1241 * @see http://de.php.net/manual/en/function.function-exists.php#77980
1243 protected static function functionExists($function) {
1244 if (extension_loaded('suhosin')) {
1245 $blacklist = @ini_get
('suhosin.executor.func.blacklist');
1246 if (!empty($blacklist)) {
1247 $blacklist = explode(',', $blacklist);
1248 foreach ($blacklist as $disabledFunction) {
1249 $disabledFunction = mb_strtolower(StringUtil
::trim($disabledFunction));
1251 if ($function == $disabledFunction) {
1258 return function_exists($function);
1262 * Compares settings, converting values into compareable ones.
1264 * @param string $setting
1265 * @param string $value
1266 * @param mixed $compareValue
1269 protected static function compareSetting($setting, $value, $compareValue) {
1270 if ($compareValue === false) return false;
1272 $value = mb_strtolower($value);
1273 $trueValues = ['1', 'on', 'true'];
1274 $falseValues = ['0', 'off', 'false'];
1276 // handle values considered as 'true'
1277 if (in_array($value, $trueValues)) {
1278 return $compareValue ?
true : false;
1280 // handle values considered as 'false'
1281 else if (in_array($value, $falseValues)) {
1282 return (!$compareValue) ?
true : false;
1284 else if (!is_numeric($value)) {
1285 $compareValue = self
::convertShorthandByteValue($compareValue);
1286 $value = self
::convertShorthandByteValue($value);
1289 return ($compareValue >= $value) ?
true : false;
1293 * Converts shorthand byte values into an integer representing bytes.
1295 * @param string $value
1297 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1299 protected static function convertShorthandByteValue($value) {
1300 // convert into bytes
1301 $lastCharacter = mb_substr($value, -1);
1302 switch ($lastCharacter) {
1305 return (int)$value * 1073741824;
1310 return (int)$value * 1048576;
1315 return (int)$value * 1024;