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\exception\CryptoException
;
40 use wcf\util\FileUtil
;
41 use wcf\util\HeaderUtil
;
42 use wcf\util\StringUtil
;
45 * PackageInstallationDispatcher handles the whole installation process.
47 * @author Alexander Ebert
48 * @copyright 2001-2018 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']);
137 switch ($data['nodeType']) {
139 $step = $this->installPackage($nodeData);
143 $step = $this->executePIP($nodeData);
146 case 'optionalPackages':
147 $step = $this->selectOptionalPackages($node, $nodeData);
151 die("Unknown node type: '".$data['nodeType']."'");
155 if ($step->splitNode()) {
156 $this->nodeBuilder
->cloneNode($node, $data['sequenceNo']);
161 // mark node as completed
162 $this->nodeBuilder
->completeNode($node);
165 $node = $this->nodeBuilder
->getNextNode($node);
166 $step->setNode($node);
168 // perform post-install/update actions
170 // update "last update time" option
171 $sql = "UPDATE wcf".WCF_N
."_option
173 WHERE optionName = ?";
174 $statement = WCF
::getDB()->prepareStatement($sql);
175 $statement->execute([
180 // update options.inc.php
181 OptionEditor
::resetCache();
183 if ($this->action
== 'install') {
184 // save localized package infos
185 $this->saveLocalizedPackageInfos();
187 // remove all cache files after WCFSetup
189 CacheHandler
::getInstance()->flushAll();
191 $sql = "UPDATE wcf".WCF_N
."_option
193 WHERE optionName = ?";
194 $statement = WCF
::getDB()->prepareStatement($sql);
196 $statement->execute([
197 StringUtil
::getUUID(),
201 if (file_exists(WCF_DIR
. 'cookiePrefix.txt')) {
202 $statement->execute([
207 @unlink
(WCF_DIR
. 'cookiePrefix.txt');
211 $statement->execute([
215 $statement->execute([
219 $statement->execute([
225 $statement->execute([
226 bin2hex(\random_bytes
(20)),
230 catch (CryptoException
$e) {
231 // ignore, the secret will stay empty and crypto operations
232 // depending on it will fail
235 if (WCF
::getSession()->getVar('__wcfSetup_developerMode')) {
236 $statement->execute([
240 $statement->execute([
244 $statement->execute([
248 $statement->execute([
250 'enable_developer_tools'
253 foreach (DevtoolsSetup
::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
254 $statement->execute([
260 foreach (DevtoolsSetup
::getInstance()->getUsers() as $newUser) {
261 (new UserAction([], 'create', [
263 'email' => $newUser['email'],
264 'password' => $newUser['password'],
265 'username' => $newUser['username']
271 ]))->executeAction();
274 if (($importPath = DevtoolsSetup
::getInstance()->getDevtoolsImportPath()) !== '') {
275 (new DevtoolsProjectAction([], 'quickSetup', [
276 'path' => $importPath
277 ]))->executeAction();
281 // update options.inc.php
282 OptionEditor
::resetCache();
285 // rebuild application paths
286 ApplicationHandler
::rebuild();
289 // remove template listener cache
290 TemplateListenerCodeCacheBuilder
::getInstance()->reset();
292 // reset language cache
293 LanguageFactory
::getInstance()->clearCache();
294 LanguageFactory
::getInstance()->deleteLanguageCache();
297 StyleHandler
::resetStylesheets();
299 // clear user storage
300 UserStorageHandler
::getInstance()->clear();
302 // rebuild config files for affected applications
303 $sql = "SELECT package.packageID
304 FROM wcf".WCF_N
."_package_installation_queue queue,
305 wcf".WCF_N
."_package package
306 WHERE queue.processNo = ?
307 AND package.packageID = queue.packageID
308 AND package.isApplication = ?";
309 $statement = WCF
::getDB()->prepareStatement($sql);
310 $statement->execute([
311 $this->queue
->processNo
,
314 while ($row = $statement->fetchArray()) {
315 Package
::writeConfigFile($row['packageID']);
318 EventHandler
::getInstance()->fireAction($this, 'postInstall');
321 $sql = "SELECT archive
322 FROM wcf".WCF_N
."_package_installation_queue
323 WHERE processNo = ?";
324 $statement = WCF
::getDB()->prepareStatement($sql);
325 $statement->execute([$this->queue
->processNo
]);
326 while ($row = $statement->fetchArray()) {
327 @unlink
($row['archive']);
331 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
332 WHERE processNo = ?";
333 $statement = WCF
::getDB()->prepareStatement($sql);
334 $statement->execute([$this->queue
->processNo
]);
341 * Returns current package archive.
343 * @return PackageArchive
345 public function getArchive() {
346 if ($this->archive
=== null) {
347 // check if we're doing an iterative update of the same package
348 if ($this->previousPackageData
!== null && $this->getPackage()->package
== $this->previousPackageData
['package']) {
349 if (Package
::compareVersion($this->getPackage()->packageVersion
, $this->previousPackageData
['packageVersion'], '<')) {
350 // fake package to simulate the package version required by current archive
351 $this->getPackage()->setPackageVersion($this->previousPackageData
['packageVersion']);
355 $this->archive
= new PackageArchive($this->queue
->archive
, $this->getPackage());
357 if (FileUtil
::isURL($this->archive
->getArchive())) {
358 // get return value and update entry in
359 // package_installation_queue with this value
360 $archive = $this->archive
->downloadArchive();
361 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
362 $queueEditor->update(['archive' => $archive]);
365 $this->archive
->openArchive();
368 return $this->archive
;
372 * Installs current package.
374 * @param mixed[] $nodeData
375 * @return PackageInstallationStep
376 * @throws SystemException
378 protected function installPackage(array $nodeData) {
379 $installationStep = new PackageInstallationStep();
381 // check requirements
382 if (!empty($nodeData['requirements'])) {
383 foreach ($nodeData['requirements'] as $package => $requirementData) {
384 // get existing package
385 if ($requirementData['packageID']) {
386 $sql = "SELECT packageName, packageVersion
387 FROM wcf".WCF_N
."_package
388 WHERE packageID = ?";
389 $statement = WCF
::getDB()->prepareStatement($sql);
390 $statement->execute([$requirementData['packageID']]);
393 // try to find matching package
394 $sql = "SELECT packageName, packageVersion
395 FROM wcf".WCF_N
."_package
397 $statement = WCF
::getDB()->prepareStatement($sql);
398 $statement->execute([$package]);
400 $row = $statement->fetchArray();
402 // package is required but not available
403 if ($row === false) {
404 throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
407 // check version requirements
408 if ($requirementData['minVersion']) {
409 if (Package
::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
410 throw new SystemException("Package '".$nodeData['packageName']."' requires package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but only version '".$row['packageVersion']."' is installed");
415 unset($nodeData['requirements']);
418 if ($this->queue
->packageID
) {
419 $packageEditor = new PackageEditor(new Package($this->queue
->packageID
));
420 unset($nodeData['installDate']);
421 $packageEditor->update($nodeData);
423 // delete old excluded packages
424 $sql = "DELETE FROM wcf".WCF_N
."_package_exclusion
425 WHERE packageID = ?";
426 $statement = WCF
::getDB()->prepareStatement($sql);
427 $statement->execute([$this->queue
->packageID
]);
429 // delete old compatibility versions
430 $sql = "DELETE FROM wcf".WCF_N
."_package_compatibility
431 WHERE packageID = ?";
432 $statement = WCF
::getDB()->prepareStatement($sql);
433 $statement->execute([$this->queue
->packageID
]);
435 // delete old requirements and dependencies
436 $sql = "DELETE FROM wcf".WCF_N
."_package_requirement
437 WHERE packageID = ?";
438 $statement = WCF
::getDB()->prepareStatement($sql);
439 $statement->execute([$this->queue
->packageID
]);
442 // create package entry
443 $package = PackageEditor
::create($nodeData);
445 // update package id for current queue
446 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
447 $queueEditor->update(['packageID' => $package->packageID
]);
450 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
451 $this->package
= null;
453 if ($package->isApplication
) {
454 $host = str_replace(RouteHandler
::getProtocol(), '', RouteHandler
::getHost());
455 $path = RouteHandler
::getPath(['acp']);
457 // insert as application
458 ApplicationEditor
::create([
459 'domainName' => $host,
460 'domainPath' => $path,
461 'cookieDomain' => $host,
462 'packageID' => $package->packageID
467 // save excluded packages
468 if (count($this->getArchive()->getExcludedPackages())) {
469 $sql = "INSERT INTO wcf".WCF_N
."_package_exclusion
470 (packageID, excludedPackage, excludedPackageVersion)
472 $statement = WCF
::getDB()->prepareStatement($sql);
474 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
475 $statement->execute([
476 $this->queue
->packageID
,
477 $excludedPackage['name'],
478 !empty($excludedPackage['version']) ?
$excludedPackage['version'] : ''
483 // save compatible versions
484 if (!empty($this->getArchive()->getCompatibleVersions())) {
485 $sql = "INSERT INTO wcf".WCF_N
."_package_compatibility
488 $statement = WCF
::getDB()->prepareStatement($sql);
490 foreach ($this->getArchive()->getCompatibleVersions() as $version) {
491 $statement->execute([
492 $this->queue
->packageID
,
498 // insert requirements and dependencies
499 $requirements = $this->getArchive()->getAllExistingRequirements();
500 if (!empty($requirements)) {
501 $sql = "INSERT INTO wcf".WCF_N
."_package_requirement
502 (packageID, requirement)
504 $statement = WCF
::getDB()->prepareStatement($sql);
506 foreach ($requirements as $identifier => $possibleRequirements) {
507 $requirement = array_shift($possibleRequirements);
509 $statement->execute([
510 $this->queue
->packageID
,
511 $requirement['packageID']
516 if ($this->getPackage()->isApplication
&& $this->getPackage()->package
!= 'com.woltlab.wcf' && $this->getAction() == 'install') {
517 if (empty($this->getPackage()->packageDir
)) {
518 $document = $this->promptPackageDir();
519 if ($document !== null && $document instanceof FormDocument
) {
520 $installationStep->setDocument($document);
523 $installationStep->setSplitNode();
527 return $installationStep;
531 * Saves the localized package info.
533 protected function saveLocalizedPackageInfos() {
534 $package = new Package($this->queue
->packageID
);
536 // localize package information
537 $sql = "INSERT INTO wcf".WCF_N
."_language_item
538 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
539 VALUES (?, ?, ?, ?, ?)";
540 $statement = WCF
::getDB()->prepareStatement($sql);
543 $languageList = new LanguageList();
544 $languageList->readObjects();
546 // workaround for WCFSetup
549 FROM wcf".WCF_N
."_language_category
550 WHERE languageCategory = ?";
551 $statement2 = WCF
::getDB()->prepareStatement($sql);
552 $statement2->execute(['wcf.acp.package']);
553 $languageCategory = $statement2->fetchObject(LanguageCategory
::class);
556 $languageCategory = LanguageFactory
::getInstance()->getCategory('wcf.acp.package');
560 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
562 // save package description
563 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
565 // update description and name
566 $packageEditor = new PackageEditor($package);
567 $packageEditor->update([
568 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue
->packageID
,
569 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue
->packageID
574 * Saves a localized package info.
576 * @param PreparedStatement $statement
577 * @param LanguageList $languageList
578 * @param LanguageCategory $languageCategory
579 * @param Package $package
580 * @param string $infoName
582 protected function saveLocalizedPackageInfo(PreparedStatement
$statement, $languageList, LanguageCategory
$languageCategory, Package
$package, $infoName) {
583 $infoValues = $this->getArchive()->getPackageInfo($infoName);
585 // get default value for languages without specified information
587 if (isset($infoValues['default'])) {
588 $defaultValue = $infoValues['default'];
590 else if (isset($infoValues['en'])) {
591 // fallback to English
592 $defaultValue = $infoValues['en'];
594 else if (isset($infoValues[WCF
::getLanguage()->getFixedLanguageCode()])) {
595 // fallback to the language of the current user
596 $defaultValue = $infoValues[WCF
::getLanguage()->getFixedLanguageCode()];
598 else if ($infoName == 'packageName') {
599 // fallback to the package identifier for the package name
600 $defaultValue = $this->getArchive()->getPackageInfo('name');
603 foreach ($languageList as $language) {
604 $value = $defaultValue;
605 if (isset($infoValues[$language->languageCode
])) {
606 $value = $infoValues[$language->languageCode
];
609 $statement->execute([
610 $language->languageID
,
611 'wcf.acp.package.'.$infoName.'.package'.$package->packageID
,
613 $languageCategory->languageCategoryID
,
620 * Executes a package installation plugin.
622 * @param mixed[] $nodeData
623 * @return PackageInstallationStep
624 * @throws SystemException
626 protected function executePIP(array $nodeData) {
627 $step = new PackageInstallationStep();
629 // fetch all pips associated with current PACKAGE_ID and include pips
630 // previously installed by current installation queue
631 $sql = "SELECT pluginName, className
632 FROM wcf".WCF_N
."_package_installation_plugin
633 WHERE pluginName = ?";
634 $statement = WCF
::getDB()->prepareStatement($sql);
635 $statement->execute([$nodeData['pip']]);
636 $row = $statement->fetchArray();
639 if (!$row ||
(strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
640 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
643 // valdidate class definition
644 $className = $row['className'];
645 if (!class_exists($className)) {
646 throw new SystemException("unable to find class '".$className."'");
650 if (empty($nodeData['value'])) {
651 $defaultValue = call_user_func([$className, 'getDefaultFilename']);
653 $nodeData['value'] = $defaultValue;
657 $plugin = new $className($this, $nodeData);
659 if (!($plugin instanceof IPackageInstallationPlugin
)) {
660 throw new ImplementationException($className, IPackageInstallationPlugin
::class);
666 $document = $plugin->{$this->action
}();
668 catch (SplitNodeException
$e) {
669 $step->setSplitNode();
672 if ($document !== null && ($document instanceof FormDocument
)) {
673 $step->setDocument($document);
674 $step->setSplitNode();
681 * Displays a list to select optional packages or installs selection.
683 * @param string $currentNode
684 * @param array $nodeData
685 * @return PackageInstallationStep
687 protected function selectOptionalPackages($currentNode, array $nodeData) {
688 $installationStep = new PackageInstallationStep();
690 $document = $this->promptOptionalPackages($nodeData);
691 if ($document !== null && $document instanceof FormDocument
) {
692 $installationStep->setDocument($document);
693 $installationStep->setSplitNode();
695 // insert new nodes for each package
696 else if (is_array($document)) {
697 // get target child node
698 $node = $currentNode;
699 $queue = $this->queue
;
702 foreach ($nodeData as $package) {
703 if (in_array($package['package'], $document)) {
704 // ignore uninstallable packages
705 if (!$package['isInstallable']) {
710 $this->nodeBuilder
->shiftNodes($currentNode, 'tempNode');
714 $queue = PackageInstallationQueueEditor
::create([
715 'parentQueueID' => $queue->queueID
,
716 'processNo' => $this->queue
->processNo
,
717 'userID' => WCF
::getUser()->userID
,
718 'package' => $package['package'],
719 'packageName' => $package['packageName'],
720 'archive' => $package['archive'],
721 'action' => $queue->action
724 $installation = new PackageInstallationDispatcher($queue);
725 $installation->nodeBuilder
->setParentNode($node);
726 $installation->nodeBuilder
->buildNodes();
727 $node = $installation->nodeBuilder
->getCurrentNode();
731 @unlink
($package['archive']);
737 $this->nodeBuilder
->shiftNodes('tempNode', $node);
741 return $installationStep;
745 * Extracts files from .tar(.gz) archive and installs them
747 * @param string $targetDir
748 * @param string $sourceArchive
749 * @param IFileHandler $fileHandler
752 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
753 return new Installer($targetDir, $sourceArchive, $fileHandler);
757 * Returns current package.
759 * @return \wcf\data\package\Package
761 public function getPackage() {
762 if ($this->package
=== null) {
763 $this->package
= new Package($this->queue
->packageID
);
766 return $this->package
;
770 * Prompts for a text input for package directory (applies for applications only)
772 * @return FormDocument
774 protected function promptPackageDir() {
775 // check for pre-defined directories originating from WCFSetup
776 $directory = WCF
::getSession()->getVar('__wcfSetup_directories');
777 if ($directory !== null) {
778 $abbreviation = Package
::getAbbreviation($this->getPackage()->package
);
779 $directory = isset($directory[$abbreviation]) ?
$directory[$abbreviation] : null;
782 if ($directory === null && !PackageInstallationFormManager
::findForm($this->queue
, 'packageDir')) {
783 $container = new GroupFormElementContainer();
784 $packageDir = new TextInputFormElement($container);
785 $packageDir->setName('packageDir');
786 $packageDir->setLabel(WCF
::getLanguage()->get('wcf.acp.package.packageDir.input'));
788 // check if there are packages installed in a parent
789 // directory of WCF, or if packages are below it
790 $sql = "SELECT packageDir
791 FROM wcf".WCF_N
."_package
792 WHERE packageDir <> ''";
793 $statement = WCF
::getDB()->prepareStatement($sql);
794 $statement->execute();
797 while ($column = $statement->fetchColumn()) {
798 if ($isParent !== null) {
802 if (preg_match('~^\.\./[^\.]~', $column)) {
805 else if (mb_strpos($column, '.') !== 0) {
810 $defaultPath = WCF_DIR
;
811 if ($isParent === false) {
812 $defaultPath = dirname(WCF_DIR
);
814 $defaultPath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeparator($defaultPath)) . Package
::getAbbreviation($this->getPackage()->package
) . '/';
816 $packageDir->setValue($defaultPath);
817 $container->appendChild($packageDir);
819 $document = new FormDocument('packageDir');
820 $document->appendContainer($container);
822 PackageInstallationFormManager
::registerForm($this->queue
, $document);
826 if ($directory !== null) {
828 $packageDir = $directory;
831 $document = PackageInstallationFormManager
::getForm($this->queue
, 'packageDir');
832 $document->handleRequest();
833 $packageDir = FileUtil
::addTrailingSlash(FileUtil
::getRealPath(FileUtil
::unifyDirSeparator($document->getValue('packageDir'))));
834 if ($packageDir === '/') $packageDir = '';
837 if ($packageDir !== null) {
838 // validate package dir
839 if ($document !== null && file_exists($packageDir . 'global.php')) {
840 $document->setError('packageDir', WCF
::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
845 $packageEditor = new PackageEditor($this->getPackage());
846 $packageEditor->update([
847 'packageDir' => FileUtil
::getRelativePath(WCF_DIR
, $packageDir)
850 // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
851 // faked and differ from the real filesystem path
853 $wcfDomainPath = ApplicationHandler
::getInstance()->getWCF()->domainPath
;
856 $sql = "SELECT domainPath
857 FROM wcf".WCF_N
."_application
858 WHERE packageID = ?";
859 $statement = WCF
::getDB()->prepareStatement($sql);
860 $statement->execute([1]);
861 $row = $statement->fetchArray();
863 $wcfDomainPath = $row['domainPath'];
866 $documentRoot = substr(FileUtil
::unifyDirSeparator(WCF_DIR
), 0, -strlen(FileUtil
::unifyDirSeparator($wcfDomainPath)));
867 $domainPath = FileUtil
::getRelativePath($documentRoot, $packageDir);
868 if ($domainPath === './') {
869 // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory
873 $domainPath = FileUtil
::addLeadingSlash($domainPath);
875 // update application path
876 $application = new Application($this->getPackage()->packageID
);
877 $applicationEditor = new ApplicationEditor($application);
878 $applicationEditor->update(['domainPath' => $domainPath]);
880 // create directory and set permissions
881 @mkdir
($packageDir, 0777, true);
882 FileUtil
::makeWritable($packageDir);
890 * Prompts a selection of optional packages.
892 * @param string[][] $packages
895 protected function promptOptionalPackages(array $packages) {
896 if (!PackageInstallationFormManager
::findForm($this->queue
, 'optionalPackages')) {
897 $container = new MultipleSelectionFormElementContainer();
898 $container->setName('optionalPackages');
899 $container->setLabel(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages'));
900 $container->setDescription(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
902 foreach ($packages as $package) {
903 $optionalPackage = new MultipleSelectionFormElement($container);
904 $optionalPackage->setName('optionalPackages');
905 $optionalPackage->setLabel($package['packageName']);
906 $optionalPackage->setValue($package['package']);
907 $optionalPackage->setDescription($package['packageDescription']);
908 if (!$package['isInstallable']) {
909 $optionalPackage->setDisabledMessage(WCF
::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements'));
912 $container->appendChild($optionalPackage);
915 $document = new FormDocument('optionalPackages');
916 $document->appendContainer($container);
918 PackageInstallationFormManager
::registerForm($this->queue
, $document);
922 $document = PackageInstallationFormManager
::getForm($this->queue
, 'optionalPackages');
923 $document->handleRequest();
925 return $document->getValue('optionalPackages');
930 * Returns current package id.
934 public function getPackageID() {
935 return $this->queue
->packageID
;
939 * Returns current package name.
941 * @return string package name
944 public function getPackageName() {
945 return $this->queue
->packageName
;
949 * Returns current package installation type.
953 public function getAction() {
954 return $this->action
;
958 * Opens the package installation queue and
959 * starts the installation, update or uninstallation of the first entry.
961 * @param integer $parentQueueID
962 * @param integer $processNo
964 public static function openQueue($parentQueueID = 0, $processNo = 0) {
965 $conditions = new PreparedStatementConditionBuilder();
966 $conditions->add("userID = ?", [WCF
::getUser()->userID
]);
967 $conditions->add("parentQueueID = ?", [$parentQueueID]);
968 if ($processNo != 0) $conditions->add("processNo = ?", [$processNo]);
969 $conditions->add("done = ?", [0]);
972 FROM wcf".WCF_N
."_package_installation_queue
974 ORDER BY queueID ASC";
975 $statement = WCF
::getDB()->prepareStatement($sql);
976 $statement->execute($conditions->getParameters());
977 $packageInstallation = $statement->fetchArray();
979 if (!isset($packageInstallation['queueID'])) {
980 $url = LinkHandler
::getInstance()->getLink('PackageList');
981 HeaderUtil
::redirect($url);
985 $url = LinkHandler
::getInstance()->getLink('PackageInstallationConfirm', [], 'queueID='.$packageInstallation['queueID']);
986 HeaderUtil
::redirect($url);
992 * Checks the package installation queue for outstanding entries.
996 public static function checkPackageInstallationQueue() {
997 $sql = "SELECT queueID
998 FROM wcf".WCF_N
."_package_installation_queue
1000 AND parentQueueID = 0
1002 ORDER BY queueID ASC";
1003 $statement = WCF
::getDB()->prepareStatement($sql);
1004 $statement->execute([WCF
::getUser()->userID
]);
1005 $row = $statement->fetchArray();
1011 return $row['queueID'];
1015 * Executes post-setup actions.
1017 public function completeSetup() {
1019 $sql = "SELECT archive
1020 FROM wcf".WCF_N
."_package_installation_queue
1021 WHERE processNo = ?";
1022 $statement = WCF
::getDB()->prepareStatement($sql);
1023 $statement->execute([$this->queue
->processNo
]);
1024 while ($row = $statement->fetchArray()) {
1025 @unlink
($row['archive']);
1029 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
1030 WHERE processNo = ?";
1031 $statement = WCF
::getDB()->prepareStatement($sql);
1032 $statement->execute([$this->queue
->processNo
]);
1034 // clear language files once whole installation is completed
1035 LanguageEditor
::deleteLanguageFiles();
1038 CacheHandler
::getInstance()->flushAll();
1042 * Updates queue information.
1044 public function updatePackage() {
1045 if (empty($this->queue
->packageName
)) {
1046 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
1047 $queueEditor->update([
1048 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
1052 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
1057 * Validates specific php requirements.
1059 * @param array $requirements
1062 public static function validatePHPRequirements(array $requirements) {
1065 // validate php version
1066 if (isset($requirements['version'])) {
1068 if (version_compare(PHP_VERSION
, $requirements['version'], '>=')) {
1073 $errors['version'] = [
1074 'required' => $requirements['version'],
1075 'installed' => PHP_VERSION
1080 // validate extensions
1081 if (isset($requirements['extensions'])) {
1082 foreach ($requirements['extensions'] as $extension) {
1083 $passed = extension_loaded($extension) ?
true : false;
1086 $errors['extension'][] = [
1087 'extension' => $extension
1093 // validate settings
1094 if (isset($requirements['settings'])) {
1095 foreach ($requirements['settings'] as $setting => $value) {
1096 $iniValue = ini_get($setting);
1098 $passed = self
::compareSetting($setting, $value, $iniValue);
1100 $errors['setting'][] = [
1101 'setting' => $setting,
1102 'required' => $value,
1103 'installed' => ($iniValue === false) ?
'(unknown)' : $iniValue
1109 // validate functions
1110 if (isset($requirements['functions'])) {
1111 foreach ($requirements['functions'] as $function) {
1112 $function = mb_strtolower($function);
1114 $passed = self
::functionExists($function);
1116 $errors['function'][] = [
1117 'function' => $function
1124 if (isset($requirements['classes'])) {
1125 foreach ($requirements['classes'] as $class) {
1128 // see: http://de.php.net/manual/en/language.oop5.basic.php
1129 if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
1130 $globalClass = '\\'.$class;
1132 if (class_exists($globalClass, false)) {
1138 $errors['class'][] = [
1150 * Validates if an function exists and is not blacklisted by suhosin extension.
1152 * @param string $function
1154 * @see http://de.php.net/manual/en/function.function-exists.php#77980
1156 protected static function functionExists($function) {
1157 if (extension_loaded('suhosin')) {
1158 $blacklist = @ini_get
('suhosin.executor.func.blacklist');
1159 if (!empty($blacklist)) {
1160 $blacklist = explode(',', $blacklist);
1161 foreach ($blacklist as $disabledFunction) {
1162 $disabledFunction = mb_strtolower(StringUtil
::trim($disabledFunction));
1164 if ($function == $disabledFunction) {
1171 return function_exists($function);
1175 * Compares settings, converting values into compareable ones.
1177 * @param string $setting
1178 * @param string $value
1179 * @param mixed $compareValue
1182 protected static function compareSetting($setting, $value, $compareValue) {
1183 if ($compareValue === false) return false;
1185 $value = mb_strtolower($value);
1186 $trueValues = ['1', 'on', 'true'];
1187 $falseValues = ['0', 'off', 'false'];
1189 // handle values considered as 'true'
1190 if (in_array($value, $trueValues)) {
1191 return $compareValue ?
true : false;
1193 // handle values considered as 'false'
1194 else if (in_array($value, $falseValues)) {
1195 return (!$compareValue) ?
true : false;
1197 else if (!is_numeric($value)) {
1198 $compareValue = self
::convertShorthandByteValue($compareValue);
1199 $value = self
::convertShorthandByteValue($value);
1202 return ($compareValue >= $value) ?
true : false;
1206 * Converts shorthand byte values into an integer representing bytes.
1208 * @param string $value
1210 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1212 protected static function convertShorthandByteValue($value) {
1213 // convert into bytes
1214 $lastCharacter = mb_substr($value, -1);
1215 switch ($lastCharacter) {
1218 return (int)$value * 1073741824;
1223 return (int)$value * 1048576;
1228 return (int)$value * 1024;