2 namespace wcf\system\package
;
3 use wcf\data\application\Application
;
4 use wcf\data\application\ApplicationEditor
;
5 use wcf\data\language\category\LanguageCategory
;
6 use wcf\data\language\LanguageEditor
;
7 use wcf\data\language\LanguageList
;
8 use wcf\data\option\OptionEditor
;
9 use wcf\data\package\installation\queue\PackageInstallationQueue
;
10 use wcf\data\package\installation\queue\PackageInstallationQueueEditor
;
11 use wcf\data\package\Package
;
12 use wcf\data\package\PackageEditor
;
13 use wcf\system\application\ApplicationHandler
;
14 use wcf\system\cache\CacheHandler
;
15 use wcf\system\database\statement\PreparedStatement
;
16 use wcf\system\database\util\PreparedStatementConditionBuilder
;
17 use wcf\system\exception\SystemException
;
18 use wcf\system\form\container
;
19 use wcf\system\form\element
;
20 use wcf\system\form\FormDocument
;
22 use wcf\system\language\LanguageFactory
;
23 use wcf\system\menu\acp\ACPMenu
;
24 use wcf\system\request\LinkHandler
;
25 use wcf\system\request\RouteHandler
;
27 use wcf\util\FileUtil
;
28 use wcf\util\HeaderUtil
;
29 use wcf\util\StringUtil
;
32 * PackageInstallationDispatcher handles the whole installation process.
34 * @author Alexander Ebert
35 * @copyright 2001-2012 WoltLab GmbH
36 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
37 * @package com.woltlab.wcf
38 * @subpackage system.package
39 * @category Community Framework
41 class PackageInstallationDispatcher
{
43 * current installation type
46 protected $action = '';
49 * instance of PackageArchive
50 * @var wcf\system\package\PackageArchive
52 public $archive = null;
55 * instance of PackageInstallationNodeBuilder
56 * @var wcf\system\package\PackageInstallationNodeBuilder
58 public $nodeBuilder = null;
62 * @var wcf\data\package\Package
64 public $package = null;
67 * instance of PackageInstallationQueue
68 * @var wcf\system\package\PackageInstallationQueue
73 * default name of the config file
76 const CONFIG_FILE
= 'config.inc.php';
79 * Creates a new instance of PackageInstallationDispatcher.
81 * @param wcf\data\package\installation\queue\PackageInstallationQueue $queue
83 public function __construct(PackageInstallationQueue
$queue) {
84 $this->queue
= $queue;
85 $this->nodeBuilder
= new PackageInstallationNodeBuilder($this);
87 $this->action
= $this->queue
->action
;
91 * Installs node components and returns next node.
94 * @return wcf\system\package\PackageInstallationStep
96 public function install($node) {
97 $nodes = $this->nodeBuilder
->getNodeData($node);
99 // invoke node-specific actions
100 foreach ($nodes as $data) {
101 $nodeData = unserialize($data['nodeData']);
103 switch ($data['nodeType']) {
105 $step = $this->installPackage($nodeData);
109 $step = $this->executePIP($nodeData);
112 case 'optionalPackages':
113 $step = $this->selectOptionalPackages($node, $nodeData);
117 die("Unknown node type: '".$data['nodeType']."'");
121 if ($step->splitNode()) {
122 $this->nodeBuilder
->cloneNode($node, $data['sequenceNo']);
127 // mark node as completed
128 $this->nodeBuilder
->completeNode($node);
131 $node = $this->nodeBuilder
->getNextNode($node);
132 $step->setNode($node);
134 // update options.inc.php and save localized package infos
136 OptionEditor
::resetCache();
138 if ($this->action
== 'install') {
139 $this->saveLocalizedPackageInfos();
141 // remove all cache files after WCFSetup
143 CacheHandler
::getInstance()->clear(WCF_DIR
.'cache/', 'cache.*.php');
146 // rebuild application paths
147 ApplicationHandler
::rebuild();
148 ApplicationEditor
::setup();
156 * Returns current package archive.
158 * @return wcf\system\package\PackageArchive
160 public function getArchive() {
161 if ($this->archive
=== null) {
162 $this->archive
= new PackageArchive($this->queue
->archive
, $this->getPackage());
164 if (FileUtil
::isURL($this->archive
->getArchive())) {
165 // get return value and update entry in
166 // package_installation_queue with this value
167 $archive = $this->archive
->downloadArchive();
168 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
169 $queueEditor->update(array(
170 'archive' => $archive
174 $this->archive
->openArchive();
177 return $this->archive
;
181 * Installs current package.
183 * @param array $nodeData
185 protected function installPackage(array $nodeData) {
186 $installationStep = new PackageInstallationStep();
188 // check requirements
189 if (!empty($nodeData['requirements'])) {
190 foreach ($nodeData['requirements'] as $package => $requirementData) {
191 // get existing package
192 if ($requirementData['packageID']) {
193 $sql = "SELECT packageName, packageVersion
194 FROM wcf".WCF_N
."_package
195 WHERE packageID = ?";
196 $statement = WCF
::getDB()->prepareStatement($sql);
197 $statement->execute(array($requirementData['packageID']));
200 // try to find matching package
201 $sql = "SELECT packageName, packageVersion
202 FROM wcf".WCF_N
."_package
204 $statement = WCF
::getDB()->prepareStatement($sql);
205 $statement->execute(array($package));
207 $row = $statement->fetchArray();
209 // package is required but not available
210 if ($row === false) {
211 throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
214 // check version requirements
215 if ($requirementData['minVersion']) {
216 if (Package
::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
217 throw new SystemException("Package '".$nodeData['packageName']."' requires the package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but version '".$row['packageVersion']."'");
222 unset($nodeData['requirements']);
224 if (!$this->queue
->packageID
) {
225 // create package entry
226 $package = PackageEditor
::create($nodeData);
228 // update package id for current queue
229 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
230 $queueEditor->update(array(
231 'packageID' => $package->packageID
234 // save excluded packages
235 if (count($this->getArchive()->getExcludedPackages()) > 0) {
236 $sql = "INSERT INTO wcf".WCF_N
."_package_exclusion
237 (packageID, excludedPackage, excludedPackageVersion)
239 $statement = WCF
::getDB()->prepareStatement($sql);
241 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
242 $statement->execute(array($package->packageID
, $excludedPackage['name'], (!empty($excludedPackage['version']) ?
$excludedPackage['version'] : '')));
246 // if package is plugin to com.woltlab.wcf it must not have any other requirement
247 $requirements = $this->getArchive()->getRequirements();
249 // insert requirements and dependencies
250 $requirements = $this->getArchive()->getAllExistingRequirements();
251 if (!empty($requirements)) {
252 $sql = "INSERT INTO wcf".WCF_N
."_package_requirement
253 (packageID, requirement)
255 $statement = WCF
::getDB()->prepareStatement($sql);
257 foreach ($requirements as $identifier => $possibleRequirements) {
258 if (count($possibleRequirements) == 1) {
259 $requirement = array_shift($possibleRequirements);
262 $requirement = $possibleRequirements[$this->selectedRequirements
[$identifier]];
265 $statement->execute(array($package->packageID
, $requirement['packageID']));
269 // build requirement map
270 Package
::rebuildPackageRequirementMap($package->packageID
);
273 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
274 $this->package
= null;
276 if ($package->isApplication
) {
277 $host = StringUtil
::replace(RouteHandler
::getProtocol(), '', RouteHandler
::getHost());
278 $path = RouteHandler
::getPath(array('acp'));
280 // insert as application
281 ApplicationEditor
::create(array(
282 'domainName' => $host,
283 'domainPath' => $path,
284 'cookieDomain' => $host,
285 'cookiePath' => $path,
286 'packageID' => $package->packageID
291 if ($this->getPackage()->isApplication
&& $this->getPackage()->package
!= 'com.woltlab.wcf' && $this->getAction() == 'install') {
292 if (empty($this->getPackage()->packageDir
)) {
293 $document = $this->promptPackageDir();
294 if ($document !== null && $document instanceof form\FormDocument
) {
295 $installationStep->setDocument($document);
298 $installationStep->setSplitNode();
302 return $installationStep;
306 * Saves the localized package infos.
308 * @todo license and readme
310 protected function saveLocalizedPackageInfos() {
311 $package = new Package($this->queue
->packageID
);
313 // localize package information
314 $sql = "INSERT INTO wcf".WCF_N
."_language_item
315 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
316 VALUES (?, ?, ?, ?, ?)";
317 $statement = WCF
::getDB()->prepareStatement($sql);
320 $languageList = new LanguageList();
321 $languageList->sqlLimit
= 0;
322 $languageList->readObjects();
324 // workaround for WCFSetup
327 FROM wcf".WCF_N
."_language_category
328 WHERE languageCategory = ?";
329 $statement2 = WCF
::getDB()->prepareStatement($sql);
330 $statement2->execute(array('wcf.acp.package'));
331 $languageCategory = $statement2->fetchObject('wcf\data\language\category\LanguageCategory');
334 $languageCategory = LanguageFactory
::getInstance()->getCategory('wcf.acp.package');
338 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
340 // save package description
341 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
343 // update description and name
344 $packageEditor = new PackageEditor($package);
345 $packageEditor->update(array(
346 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue
->packageID
,
347 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue
->packageID
352 * Saves a localized package info.
354 * @param wcf\system\database\statement\PreparedStatement $statement
355 * @param wcf\data\language\LanguageList $languageList
356 * @param wcf\data\language\category\LanguageCategory $languageCategory
357 * @param wcf\data\package\Package $package
358 * @param string $infoName
360 protected function saveLocalizedPackageInfo(PreparedStatement
$statement, $languageList, LanguageCategory
$languageCategory, Package
$package, $infoName) {
361 $infoValues = $this->getArchive()->getPackageInfo($infoName);
363 // get default value for languages without specified information
365 if (isset($infoValues['default'])) {
366 $defaultValue = $infoValues['default'];
368 else if (isset($infoValues['en'])) {
369 // fallback to English
370 $defaultValue = $infoValues['en'];
372 else if (isset($infoValues[WCF
::getLanguage()->getFixedLanguageCode()])) {
373 // fallback to the language of the current user
374 $defaultValue = $infoValues[WCF
::getLanguage()->getFixedLanguageCode()];
376 else if ($infoName == 'packageName') {
377 // fallback to the package identifier for the package name
378 $defaultValue = $this->archive
->getPackageInfo('name');
381 foreach ($languageList as $language) {
382 $value = $defaultValue;
383 if (isset($infoValues[$language->languageCode
])) {
384 $value = $infoValues[$language->languageCode
];
387 $statement->execute(array(
388 $language->languageID
,
389 'wcf.acp.package.'.$infoName.'.package'.$package->packageID
,
391 $languageCategory->languageCategoryID
,
398 * Executes a package installation plugin.
403 protected function executePIP(array $nodeData) {
404 $step = new PackageInstallationStep();
406 // fetch all pips associated with current PACKAGE_ID and include pips
407 // previously installed by current installation queue
408 $sql = "SELECT pluginName, className
409 FROM wcf".WCF_N
."_package_installation_plugin
410 WHERE pluginName = ?";
411 $statement = WCF
::getDB()->prepareStatement($sql);
412 $statement->execute(array(
415 $row = $statement->fetchArray();
418 if (!$row ||
(strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
419 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
422 // valdidate class definition
423 $className = $row['className'];
424 if (!class_exists($className)) {
425 throw new SystemException("unable to find class '".$className."'");
428 $plugin = new $className($this, $nodeData);
430 if (!($plugin instanceof \wcf\system\package\plugin\IPackageInstallationPlugin
)) {
431 throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
436 $document = $plugin->{$this->action
}();
438 catch (SplitNodeException
$e) {
439 $step->setSplitNode();
442 if ($document !== null && ($document instanceof FormDocument
)) {
443 $step->setDocument($document);
444 $step->setSplitNode();
451 protected function selectOptionalPackages($currentNode, array $nodeData) {
452 $installationStep = new PackageInstallationStep();
454 $document = $this->promptOptionalPackages($nodeData);
455 if ($document !== null && $document instanceof form\FormDocument
) {
456 $installationStep->setDocument($document);
457 $installationStep->setSplitNode();
459 // insert new nodes for each package
460 else if (is_array($document)) {
461 // get target child node
462 $node = $currentNode;
463 $queue = $this->queue
;
466 foreach ($nodeData as $package) {
467 if (in_array($package['package'], $document)) {
469 $this->nodeBuilder
->shiftNodes($currentNode, 'tempNode');
473 $queue = PackageInstallationQueueEditor
::create(array(
474 'parentQueueID' => $queue->queueID
,
475 'processNo' => $this->queue
->processNo
,
476 'userID' => WCF
::getUser()->userID
,
477 'package' => $package['package'],
478 'packageName' => $package['packageName'],
479 'archive' => $package['archive'],
480 'action' => $queue->action
483 $installation = new PackageInstallationDispatcher($queue);
484 $installation->nodeBuilder
->setParentNode($node);
485 $installation->nodeBuilder
->buildNodes();
486 $node = $installation->nodeBuilder
->getCurrentNode();
492 $this->nodeBuilder
->shiftNodes('tempNode', $node);
496 return $installationStep;
500 * Extracts files from .tar (or .tar.gz) archive and installs them
502 * @param string $targetDir
503 * @param string $sourceArchive
504 * @param FileHandler $fileHandler
505 * @return wcf\system\setup\Installer
507 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
508 return new \wcf\system\setup\
Installer($targetDir, $sourceArchive, $fileHandler);
512 * Returns current package.
514 * @return wcf\data\package\Package
516 public function getPackage() {
517 if ($this->package
=== null) {
518 $this->package
= new Package($this->queue
->packageID
);
521 return $this->package
;
525 * Prompts for a text input for package directory (applies for applications only)
527 * @return wcf\system\form\FormDocument
529 protected function promptPackageDir() {
530 if (!PackageInstallationFormManager
::findForm($this->queue
, 'packageDir')) {
532 $container = new container\
GroupFormElementContainer();
533 $packageDir = new element\
TextInputFormElement($container);
534 $packageDir->setName('packageDir');
535 $packageDir->setLabel(WCF
::getLanguage()->get('wcf.acp.package.packageDir.input'));
537 $path = RouteHandler
::getPath(array('wcf', 'acp'));
538 $defaultPath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeperator($_SERVER['DOCUMENT_ROOT'] . $path));
539 $packageDir->setValue($defaultPath);
540 $container->appendChild($packageDir);
542 $document = new form\
FormDocument('packageDir');
543 $document->appendContainer($container);
545 PackageInstallationFormManager
::registerForm($this->queue
, $document);
549 $document = PackageInstallationFormManager
::getForm($this->queue
, 'packageDir');
550 $document->handleRequest();
551 $packageDir = $document->getValue('packageDir');
553 if ($packageDir !== null) {
554 // validate package dir
555 if (file_exists(FileUtil
::addTrailingSlash($packageDir) . 'global.php')) {
556 $document->setError('packageDir', WCF
::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
561 $packageEditor = new PackageEditor($this->getPackage());
562 $packageEditor->update(array(
563 'packageDir' => FileUtil
::getRelativePath(WCF_DIR
, $packageDir)
567 $domainPath = FileUtil
::getRelativePath(FileUtil
::unifyDirSeperator($_SERVER['DOCUMENT_ROOT']), FileUtil
::unifyDirSeperator($packageDir));
569 // work-around for applications installed in document root
570 if ($domainPath == './') {
574 $domainPath = FileUtil
::addLeadingSlash(FileUtil
::addTrailingSlash($domainPath));
576 // update application path
577 $application = new Application($this->getPackage()->packageID
);
578 $applicationEditor = new ApplicationEditor($application);
579 $applicationEditor->update(array(
580 'domainPath' => $domainPath,
581 'cookiePath' => $domainPath
584 // create directory and set permissions
585 @mkdir
($packageDir, 0777, true);
586 @chmod
($packageDir, 0777);
594 protected function promptOptionalPackages(array $packages) {
595 if (!PackageInstallationFormManager
::findForm($this->queue
, 'optionalPackages')) {
596 $container = new container\
MultipleSelectionFormElementContainer();
597 $container->setName('optionalPackages');
599 foreach ($packages as $package) {
600 $optionalPackage = new element\
MultipleSelectionFormElement($container);
601 $optionalPackage->setName('optionalPackages');
602 $optionalPackage->setLabel($package['packageName']);
603 $optionalPackage->setValue($package['package']);
605 $container->appendChild($optionalPackage);
608 $document = new form\
FormDocument('optionalPackages');
609 $document->appendContainer($container);
611 PackageInstallationFormManager
::registerForm($this->queue
, $document);
615 $document = PackageInstallationFormManager
::getForm($this->queue
, 'optionalPackages');
616 $document->handleRequest();
618 return $document->getValue('optionalPackages');
623 * Returns current package id.
627 public function getPackageID() {
628 return $this->queue
->packageID
;
632 * Returns current package installation type.
636 public function getAction() {
637 return $this->action
;
641 * Opens the package installation queue and
642 * starts the installation, update or uninstallation of the first entry.
644 * @param integer $parentQueueID
645 * @param integer $processNo
647 public static function openQueue($parentQueueID = 0, $processNo = 0) {
648 $conditions = new PreparedStatementConditionBuilder();
649 $conditions->add("userID = ?", array(WCF
::getUser()->userID
));
650 $conditions->add("parentQueueID = ?", array($parentQueueID));
651 if ($processNo != 0) $conditions->add("processNo = ?", array($processNo));
652 $conditions->add("done = ?", array(0));
655 FROM wcf".WCF_N
."_package_installation_queue
657 ORDER BY queueID ASC";
658 $statement = WCF
::getDB()->prepareStatement($sql);
659 $statement->execute($conditions->getParameters());
660 $packageInstallation = $statement->fetchArray();
662 if (!isset($packageInstallation['queueID'])) {
663 $url = LinkHandler
::getInstance()->getLink('PackageList');
664 HeaderUtil
::redirect($url);
668 $url = LinkHandler
::getInstance()->getLink('PackageInstallationConfirm', array(), 'action='.$packageInstallation['action'].'&queueID='.$packageInstallation['queueID']);
669 HeaderUtil
::redirect($url);
675 * Checks the package installation queue for outstanding entries.
679 public static function checkPackageInstallationQueue() {
680 $sql = "SELECT queueID
681 FROM wcf".WCF_N
."_package_installation_queue
683 AND parentQueueID = 0
685 ORDER BY queueID ASC";
686 $statement = WCF
::getDB()->prepareStatement($sql);
687 $statement->execute(array(WCF
::getUser()->userID
));
688 $row = $statement->fetchArray();
694 return $row['queueID'];
698 * Executes post-setup actions.
700 public function completeSetup() {
701 // rebuild dependencies
702 Package
::rebuildPackageDependencies($this->queue
->packageID
);
704 // mark queue as done
705 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
706 $queueEditor->update(array(
711 $this->nodeBuilder
->purgeNodes();
713 // update package version
714 if ($this->action
== 'update') {
715 $packageEditor = new PackageEditor($this->getPackage());
716 $packageEditor->update(array(
717 'updateDate' => TIME_NOW
,
718 'packageVersion' => $this->archive
->getPackageInfo('version')
722 // clear language files once whole installation is completed
723 LanguageEditor
::deleteLanguageFiles();
726 CacheHandler
::getInstance()->clear(WCF_DIR
.'cache/', '*');
730 * Updates queue information.
732 public function updatePackage() {
733 if (empty($this->queue
->packageName
)) {
734 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
735 $queueEditor->update(array(
736 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
740 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
745 * Validates specific php requirements.
747 * @param array $requirements
748 * @return array<array>
750 public static function validatePHPRequirements(array $requirements) {
753 // validate php version
754 if (isset($requirements['version'])) {
756 if (version_compare(PHP_VERSION
, $requirements['version'], '>=')) {
761 $errors['version'] = array(
762 'required' => $requirements['version'],
763 'installed' => PHP_VERSION
768 // validate extensions
769 if (isset($requirements['extensions'])) {
770 foreach ($requirements['extensions'] as $extension) {
771 $passed = (extension_loaded($extension)) ?
true : false;
774 $errors['extension'][] = array(
775 'extension' => $extension
782 if (isset($requirements['settings'])) {
783 foreach ($requirements['settings'] as $setting => $value) {
784 $iniValue = ini_get($setting);
786 $passed = self
::compareSetting($setting, $value, $iniValue);
788 $errors['setting'][] = array(
789 'setting' => $setting,
790 'required' => $value,
791 'installed' => ($iniValue === false) ?
'(unknown)' : $iniValue
797 // validate functions
798 if (isset($requirements['functions'])) {
799 foreach ($requirements['functions'] as $function) {
800 $function = StringUtil
::toLowerCase($function);
802 $passed = self
::functionExists($function);
804 $errors['function'][] = array(
805 'function' => $function
812 if (isset($requirements['classes'])) {
813 foreach ($requirements['classes'] as $class) {
816 // see: http://de.php.net/manual/en/language.oop5.basic.php
817 if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
818 $globalClass = '\\'.$class;
820 if (class_exists($globalClass, false)) {
826 $errors['class'][] = array(
838 * Validates if an function exists and is not blacklisted by suhosin extension.
840 * @param string $function
842 * @see http://de.php.net/manual/en/function.function-exists.php#77980
844 protected static function functionExists($function) {
845 if (extension_loaded('suhosin')) {
846 $blacklist = @ini_get
('suhosin.executor.func.blacklist');
847 if (!empty($blacklist)) {
848 $blacklist = explode(',', $blacklist);
849 foreach ($blacklist as $disabledFunction) {
850 $disabledFunction = StringUtil
::toLowerCase(StringUtil
::trim($disabledFunction));
852 if ($function == $disabledFunction) {
859 return function_exists($function);
863 * Compares settings, converting values into compareable ones.
865 * @param string $setting
866 * @param string $value
867 * @param mixed $compareValue
870 protected static function compareSetting($setting, $value, $compareValue) {
871 if ($compareValue === false) return false;
873 $value = StringUtil
::toLowerCase($value);
874 $trueValues = array('1', 'on', 'true');
875 $falseValues = array('0', 'off', 'false');
877 // handle values considered as 'true'
878 if (in_array($value, $trueValues)) {
879 return ($compareValue) ?
true : false;
881 // handle values considered as 'false'
882 else if (in_array($value, $falseValues)) {
883 return (!$compareValue) ?
true : false;
885 else if (!is_numeric($value)) {
886 $compareValue = self
::convertShorthandByteValue($compareValue);
887 $value = self
::convertShorthandByteValue($value);
890 return ($compareValue >= $value) ?
true : false;
894 * Converts shorthand byte values into an integer representing bytes.
896 * @param string $value
898 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
900 protected static function convertShorthandByteValue($value) {
901 // convert into bytes
902 $lastCharacter = StringUtil
::substring($value, -1);
903 switch ($lastCharacter) {
906 return (int)$value * 1073741824;
911 return (int)$value * 1048576;
916 return (int)$value * 1024;