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\data\
object\type\ObjectTypeCache
;
15 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder
;
16 use wcf\system\cache\CacheHandler
;
17 use wcf\system\database\statement\PreparedStatement
;
18 use wcf\system\database\util\PreparedStatementConditionBuilder
;
19 use wcf\system\event\EventHandler
;
20 use wcf\system\exception\SystemException
;
21 use wcf\system\form\container\GroupFormElementContainer
;
22 use wcf\system\form\container\MultipleSelectionFormElementContainer
;
23 use wcf\system\form\element\MultipleSelectionFormElement
;
24 use wcf\system\form\element\TextInputFormElement
;
25 use wcf\system\form\FormDocument
;
26 use wcf\system\language\LanguageFactory
;
27 use wcf\system\package\plugin\IPackageInstallationPlugin
;
28 use wcf\system\package\plugin\ObjectTypePackageInstallationPlugin
;
29 use wcf\system\package\plugin\SQLPackageInstallationPlugin
;
30 use wcf\system\request\LinkHandler
;
31 use wcf\system\request\RouteHandler
;
32 use wcf\system\setup\Installer
;
33 use wcf\system\style\StyleHandler
;
34 use wcf\system\user\storage\UserStorageHandler
;
36 use wcf\util\FileUtil
;
37 use wcf\util\HeaderUtil
;
38 use wcf\util\StringUtil
;
41 * PackageInstallationDispatcher handles the whole installation process.
43 * @author Alexander Ebert
44 * @copyright 2001-2014 WoltLab GmbH
45 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
46 * @package com.woltlab.wcf
47 * @subpackage system.package
48 * @category Community Framework
50 class PackageInstallationDispatcher
{
52 * current installation type
55 protected $action = '';
58 * instance of PackageArchive
59 * @var \wcf\system\package\PackageArchive
61 public $archive = null;
64 * instance of PackageInstallationNodeBuilder
65 * @var \wcf\system\package\PackageInstallationNodeBuilder
67 public $nodeBuilder = null;
71 * @var \wcf\data\package\Package
73 public $package = null;
76 * instance of PackageInstallationQueue
77 * @var \wcf\system\package\PackageInstallationQueue
82 * default name of the config file
85 const CONFIG_FILE
= 'config.inc.php';
88 * holds state of structuring version tables
91 protected $requireRestructureVersionTables = false;
94 * data of previous package in queue
97 protected $previousPackageData = null;
100 * Creates a new instance of PackageInstallationDispatcher.
102 * @param \wcf\data\package\installation\queue\PackageInstallationQueue $queue
104 public function __construct(PackageInstallationQueue
$queue) {
105 $this->queue
= $queue;
106 $this->nodeBuilder
= new PackageInstallationNodeBuilder($this);
108 $this->action
= $this->queue
->action
;
112 * Sets data of previous package in queue.
114 * @param array<string> $packageData
116 public function setPreviousPackage(array $packageData) {
117 $this->previousPackageData
= $packageData;
121 * Installs node components and returns next node.
123 * @param string $node
124 * @return \wcf\system\package\PackageInstallationStep
126 public function install($node) {
127 $nodes = $this->nodeBuilder
->getNodeData($node);
129 // invoke node-specific actions
130 foreach ($nodes as $data) {
131 $nodeData = unserialize($data['nodeData']);
133 switch ($data['nodeType']) {
135 $step = $this->installPackage($nodeData);
139 $step = $this->executePIP($nodeData);
142 case 'optionalPackages':
143 $step = $this->selectOptionalPackages($node, $nodeData);
147 die("Unknown node type: '".$data['nodeType']."'");
151 if ($step->splitNode()) {
152 $this->nodeBuilder
->cloneNode($node, $data['sequenceNo']);
157 // mark node as completed
158 $this->nodeBuilder
->completeNode($node);
162 $node = $this->nodeBuilder
->getNextNode($node);
163 $step->setNode($node);
165 // perform post-install/update actions
167 // update options.inc.php
168 OptionEditor
::resetCache();
170 if ($this->action
== 'install') {
171 // save localized package infos
172 $this->saveLocalizedPackageInfos();
174 // remove all cache files after WCFSetup
176 CacheHandler
::getInstance()->flushAll();
178 if (WCF
::getSession()->getVar('__wcfSetup_developerMode')) {
179 $sql = "UPDATE wcf".WCF_N
."_option
181 WHERE optionName = ?";
182 $statement = WCF
::getDB()->prepareStatement($sql);
183 $statement->execute(array(
188 // update options.inc.php
189 OptionEditor
::resetCache();
193 // rebuild application paths
194 ApplicationHandler
::rebuild();
195 ApplicationEditor
::setup();
198 // remove template listener cache
199 TemplateListenerCodeCacheBuilder
::getInstance()->reset();
201 // reset language cache
202 LanguageFactory
::getInstance()->clearCache();
203 LanguageFactory
::getInstance()->deleteLanguageCache();
206 StyleHandler
::resetStylesheets();
208 // clear user storage
209 UserStorageHandler
::getInstance()->clear();
211 // rebuild config files for affected applications
212 $sql = "SELECT package.packageID
213 FROM wcf".WCF_N
."_package_installation_queue queue,
214 wcf".WCF_N
."_package package
215 WHERE queue.processNo = ?
216 AND package.packageID = queue.packageID
217 AND package.packageID <> ?
218 AND package.isApplication = ?";
219 $statement = WCF
::getDB()->prepareStatement($sql);
220 $statement->execute(array(
221 $this->queue
->processNo
,
225 while ($row = $statement->fetchArray()) {
226 Package
::writeConfigFile($row['packageID']);
229 EventHandler
::getInstance()->fireAction($this, 'postInstall');
232 $sql = "SELECT archive
233 FROM wcf".WCF_N
."_package_installation_queue
234 WHERE processNo = ?";
235 $statement = WCF
::getDB()->prepareStatement($sql);
236 $statement->execute(array($this->queue
->processNo
));
237 while ($row = $statement->fetchArray()) {
238 @unlink
($row['archive']);
242 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
243 WHERE processNo = ?";
244 $statement = WCF
::getDB()->prepareStatement($sql);
245 $statement->execute(array($this->queue
->processNo
));
248 if ($this->requireRestructureVersionTables
) {
249 $this->restructureVersionTables();
256 * Returns current package archive.
258 * @return \wcf\system\package\PackageArchive
260 public function getArchive() {
261 if ($this->archive
=== null) {
262 $package = $this->getPackage();
263 // check if we're doing an iterative update of the same package
264 if ($this->previousPackageData
!== null && $this->getPackage()->package
== $this->previousPackageData
['package']) {
265 if (Package
::compareVersion($this->getPackage()->packageVersion
, $this->previousPackageData
['packageVersion'], '<')) {
266 // fake package to simulate the package version required by current archive
267 $this->getPackage()->setPackageVersion($this->previousPackageData
['packageVersion']);
271 $this->archive
= new PackageArchive($this->queue
->archive
, $this->getPackage());
273 if (FileUtil
::isURL($this->archive
->getArchive())) {
274 // get return value and update entry in
275 // package_installation_queue with this value
276 $archive = $this->archive
->downloadArchive();
277 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
278 $queueEditor->update(array(
279 'archive' => $archive
283 $this->archive
->openArchive();
286 return $this->archive
;
290 * Installs current package.
292 * @param array $nodeData
294 protected function installPackage(array $nodeData) {
295 $installationStep = new PackageInstallationStep();
297 // check requirements
298 if (!empty($nodeData['requirements'])) {
299 foreach ($nodeData['requirements'] as $package => $requirementData) {
300 // get existing package
301 if ($requirementData['packageID']) {
302 $sql = "SELECT packageName, packageVersion
303 FROM wcf".WCF_N
."_package
304 WHERE packageID = ?";
305 $statement = WCF
::getDB()->prepareStatement($sql);
306 $statement->execute(array($requirementData['packageID']));
309 // try to find matching package
310 $sql = "SELECT packageName, packageVersion
311 FROM wcf".WCF_N
."_package
313 $statement = WCF
::getDB()->prepareStatement($sql);
314 $statement->execute(array($package));
316 $row = $statement->fetchArray();
318 // package is required but not available
319 if ($row === false) {
320 throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
323 // check version requirements
324 if ($requirementData['minVersion']) {
325 if (Package
::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
326 throw new SystemException("Package '".$nodeData['packageName']."' requires package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but only version '".$row['packageVersion']."' is installed");
331 unset($nodeData['requirements']);
334 if ($this->queue
->packageID
) {
335 $packageEditor = new PackageEditor(new Package($this->queue
->packageID
));
336 $packageEditor->update($nodeData);
338 // delete old excluded packages
339 $sql = "DELETE FROM wcf".WCF_N
."_package_exclusion
340 WHERE packageID = ?";
341 $statement = WCF
::getDB()->prepareStatement($sql);
342 $statement->execute(array($this->queue
->packageID
));
344 // delete old requirements and dependencies
345 $sql = "DELETE FROM wcf".WCF_N
."_package_requirement
346 WHERE packageID = ?";
347 $statement = WCF
::getDB()->prepareStatement($sql);
348 $statement->execute(array($this->queue
->packageID
));
351 // create package entry
352 $package = PackageEditor
::create($nodeData);
354 // update package id for current queue
355 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
356 $queueEditor->update(array(
357 'packageID' => $package->packageID
361 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
362 $this->package
= null;
364 if ($package->isApplication
) {
365 $host = str_replace(RouteHandler
::getProtocol(), '', RouteHandler
::getHost());
366 $path = RouteHandler
::getPath(array('acp'));
368 // insert as application
369 ApplicationEditor
::create(array(
370 'domainName' => $host,
371 'domainPath' => $path,
372 'cookieDomain' => $host,
373 'cookiePath' => $path,
374 'packageID' => $package->packageID
379 // save excluded packages
380 if (count($this->getArchive()->getExcludedPackages())) {
381 $sql = "INSERT INTO wcf".WCF_N
."_package_exclusion
382 (packageID, excludedPackage, excludedPackageVersion)
384 $statement = WCF
::getDB()->prepareStatement($sql);
386 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
387 $statement->execute(array($this->queue
->packageID
, $excludedPackage['name'], (!empty($excludedPackage['version']) ?
$excludedPackage['version'] : '')));
391 // insert requirements and dependencies
392 $requirements = $this->getArchive()->getAllExistingRequirements();
393 if (!empty($requirements)) {
394 $sql = "INSERT INTO wcf".WCF_N
."_package_requirement
395 (packageID, requirement)
397 $statement = WCF
::getDB()->prepareStatement($sql);
399 foreach ($requirements as $identifier => $possibleRequirements) {
400 $requirement = array_shift($possibleRequirements);
402 $statement->execute(array($this->queue
->packageID
, $requirement['packageID']));
406 if ($this->getPackage()->isApplication
&& $this->getPackage()->package
!= 'com.woltlab.wcf' && $this->getAction() == 'install') {
407 if (empty($this->getPackage()->packageDir
)) {
408 $document = $this->promptPackageDir();
409 if ($document !== null && $document instanceof FormDocument
) {
410 $installationStep->setDocument($document);
413 $installationStep->setSplitNode();
417 return $installationStep;
421 * Saves the localized package infos.
423 * @todo license and readme
425 protected function saveLocalizedPackageInfos() {
426 $package = new Package($this->queue
->packageID
);
428 // localize package information
429 $sql = "INSERT INTO wcf".WCF_N
."_language_item
430 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
431 VALUES (?, ?, ?, ?, ?)";
432 $statement = WCF
::getDB()->prepareStatement($sql);
435 $languageList = new LanguageList();
436 $languageList->readObjects();
438 // workaround for WCFSetup
441 FROM wcf".WCF_N
."_language_category
442 WHERE languageCategory = ?";
443 $statement2 = WCF
::getDB()->prepareStatement($sql);
444 $statement2->execute(array('wcf.acp.package'));
445 $languageCategory = $statement2->fetchObject('wcf\data\language\category\LanguageCategory');
448 $languageCategory = LanguageFactory
::getInstance()->getCategory('wcf.acp.package');
452 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
454 // save package description
455 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
457 // update description and name
458 $packageEditor = new PackageEditor($package);
459 $packageEditor->update(array(
460 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue
->packageID
,
461 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue
->packageID
466 * Saves a localized package info.
468 * @param \wcf\system\database\statement\PreparedStatement $statement
469 * @param \wcf\data\language\LanguageList $languageList
470 * @param \wcf\data\language\category\LanguageCategory $languageCategory
471 * @param \wcf\data\package\Package $package
472 * @param string $infoName
474 protected function saveLocalizedPackageInfo(PreparedStatement
$statement, $languageList, LanguageCategory
$languageCategory, Package
$package, $infoName) {
475 $infoValues = $this->getArchive()->getPackageInfo($infoName);
477 // get default value for languages without specified information
479 if (isset($infoValues['default'])) {
480 $defaultValue = $infoValues['default'];
482 else if (isset($infoValues['en'])) {
483 // fallback to English
484 $defaultValue = $infoValues['en'];
486 else if (isset($infoValues[WCF
::getLanguage()->getFixedLanguageCode()])) {
487 // fallback to the language of the current user
488 $defaultValue = $infoValues[WCF
::getLanguage()->getFixedLanguageCode()];
490 else if ($infoName == 'packageName') {
491 // fallback to the package identifier for the package name
492 $defaultValue = $this->getArchive()->getPackageInfo('name');
495 foreach ($languageList as $language) {
496 $value = $defaultValue;
497 if (isset($infoValues[$language->languageCode
])) {
498 $value = $infoValues[$language->languageCode
];
501 $statement->execute(array(
502 $language->languageID
,
503 'wcf.acp.package.'.$infoName.'.package'.$package->packageID
,
505 $languageCategory->languageCategoryID
,
512 * Executes a package installation plugin.
517 protected function executePIP(array $nodeData) {
518 $step = new PackageInstallationStep();
520 // fetch all pips associated with current PACKAGE_ID and include pips
521 // previously installed by current installation queue
522 $sql = "SELECT pluginName, className
523 FROM wcf".WCF_N
."_package_installation_plugin
524 WHERE pluginName = ?";
525 $statement = WCF
::getDB()->prepareStatement($sql);
526 $statement->execute(array(
529 $row = $statement->fetchArray();
532 if (!$row ||
(strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
533 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
536 // valdidate class definition
537 $className = $row['className'];
538 if (!class_exists($className)) {
539 throw new SystemException("unable to find class '".$className."'");
542 $plugin = new $className($this, $nodeData);
544 if (!($plugin instanceof IPackageInstallationPlugin
)) {
545 throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
548 if ($plugin instanceof SQLPackageInstallationPlugin ||
$plugin instanceof ObjectTypePackageInstallationPlugin
) {
549 $this->requireRestructureVersionTables
= true;
555 $document = $plugin->{$this->action
}();
557 catch (SplitNodeException
$e) {
558 $step->setSplitNode();
561 if ($document !== null && ($document instanceof FormDocument
)) {
562 $step->setDocument($document);
563 $step->setSplitNode();
570 * Displays a list to select optional packages or installs selection.
572 * @param string $currentNode
573 * @param array $nodeData
574 * @return \wcf\system\package\PackageInstallationStep
576 protected function selectOptionalPackages($currentNode, array $nodeData) {
577 $installationStep = new PackageInstallationStep();
579 $document = $this->promptOptionalPackages($nodeData);
580 if ($document !== null && $document instanceof FormDocument
) {
581 $installationStep->setDocument($document);
582 $installationStep->setSplitNode();
584 // insert new nodes for each package
585 else if (is_array($document)) {
586 // get target child node
587 $node = $currentNode;
588 $queue = $this->queue
;
591 foreach ($nodeData as $package) {
592 if (in_array($package['package'], $document)) {
593 // ignore uninstallable packages
594 if (!$package['isInstallable']) {
599 $this->nodeBuilder
->shiftNodes($currentNode, 'tempNode');
603 $queue = PackageInstallationQueueEditor
::create(array(
604 'parentQueueID' => $queue->queueID
,
605 'processNo' => $this->queue
->processNo
,
606 'userID' => WCF
::getUser()->userID
,
607 'package' => $package['package'],
608 'packageName' => $package['packageName'],
609 'archive' => $package['archive'],
610 'action' => $queue->action
613 $installation = new PackageInstallationDispatcher($queue);
614 $installation->nodeBuilder
->setParentNode($node);
615 $installation->nodeBuilder
->buildNodes();
616 $node = $installation->nodeBuilder
->getCurrentNode();
620 @unlink
($package['archive']);
626 $this->nodeBuilder
->shiftNodes('tempNode', $node);
630 return $installationStep;
634 * Extracts files from .tar(.gz) archive and installs them
636 * @param string $targetDir
637 * @param string $sourceArchive
638 * @param FileHandler $fileHandler
639 * @return \wcf\system\setup\Installer
641 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
642 return new Installer($targetDir, $sourceArchive, $fileHandler);
646 * Returns current package.
648 * @return \wcf\data\package\Package
650 public function getPackage() {
651 if ($this->package
=== null) {
652 $this->package
= new Package($this->queue
->packageID
);
655 return $this->package
;
659 * Prompts for a text input for package directory (applies for applications only)
661 * @return \wcf\system\form\FormDocument
663 protected function promptPackageDir() {
664 if (!PackageInstallationFormManager
::findForm($this->queue
, 'packageDir')) {
666 $container = new GroupFormElementContainer();
667 $packageDir = new TextInputFormElement($container);
668 $packageDir->setName('packageDir');
669 $packageDir->setLabel(WCF
::getLanguage()->get('wcf.acp.package.packageDir.input'));
671 $defaultPath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeparator(dirname(WCF_DIR
)));
672 // check if there is already an application
673 $sql = "SELECT COUNT(*) AS count
674 FROM wcf".WCF_N
."_package
675 WHERE packageDir = ?";
676 $statement = WCF
::getDB()->prepareStatement($sql);
677 $statement->execute(array('../'));
678 $row = $statement->fetchArray();
681 $defaultPath .= strtolower(Package
::getAbbreviation($this->getPackage()->package
)) . '/';
684 $packageDir->setValue($defaultPath);
685 $container->appendChild($packageDir);
687 $document = new FormDocument('packageDir');
688 $document->appendContainer($container);
690 PackageInstallationFormManager
::registerForm($this->queue
, $document);
694 $document = PackageInstallationFormManager
::getForm($this->queue
, 'packageDir');
695 $document->handleRequest();
696 $packageDir = FileUtil
::addTrailingSlash(FileUtil
::getRealPath(FileUtil
::unifyDirSeparator($document->getValue('packageDir'))));
697 if ($packageDir === '/') $packageDir = '';
699 if ($packageDir !== null) {
700 // validate package dir
701 if (file_exists($packageDir . 'global.php')) {
702 $document->setError('packageDir', WCF
::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
707 $packageEditor = new PackageEditor($this->getPackage());
708 $packageEditor->update(array(
709 'packageDir' => FileUtil
::getRelativePath(WCF_DIR
, $packageDir)
712 // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
713 // faked and differ from the real filesystem path
715 $wcfDomainPath = ApplicationHandler
::getInstance()->getWCF()->domainPath
;
718 $sql = "SELECT domainPath
719 FROM wcf".WCF_N
."_application
720 WHERE packageID = ?";
721 $statement = WCF
::getDB()->prepareStatement($sql);
722 $statement->execute(array(1));
723 $row = $statement->fetchArray();
725 $wcfDomainPath = $row['domainPath'];
728 $documentRoot = str_replace($wcfDomainPath, '', FileUtil
::unifyDirSeparator(WCF_DIR
));
729 $domainPath = str_replace($documentRoot, '', $packageDir);
731 // update application path
732 $application = new Application($this->getPackage()->packageID
);
733 $applicationEditor = new ApplicationEditor($application);
734 $applicationEditor->update(array(
735 'domainPath' => $domainPath,
736 'cookiePath' => $domainPath
739 // create directory and set permissions
740 @mkdir
($packageDir, 0777, true);
741 FileUtil
::makeWritable($packageDir);
749 * Prompts a selection of optional packages.
753 protected function promptOptionalPackages(array $packages) {
754 if (!PackageInstallationFormManager
::findForm($this->queue
, 'optionalPackages')) {
755 $container = new MultipleSelectionFormElementContainer();
756 $container->setName('optionalPackages');
757 $container->setLabel(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages'));
758 $container->setDescription(WCF
::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
760 foreach ($packages as $package) {
761 $optionalPackage = new MultipleSelectionFormElement($container);
762 $optionalPackage->setName('optionalPackages');
763 $optionalPackage->setLabel($package['packageName']);
764 $optionalPackage->setValue($package['package']);
765 $optionalPackage->setDescription($package['packageDescription']);
766 if (!$package['isInstallable']) {
767 $optionalPackage->setDisabledMessage(WCF
::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements'));
770 $container->appendChild($optionalPackage);
773 $document = new FormDocument('optionalPackages');
774 $document->appendContainer($container);
776 PackageInstallationFormManager
::registerForm($this->queue
, $document);
780 $document = PackageInstallationFormManager
::getForm($this->queue
, 'optionalPackages');
781 $document->handleRequest();
783 return $document->getValue('optionalPackages');
788 * Returns current package id.
792 public function getPackageID() {
793 return $this->queue
->packageID
;
797 * Returns current package installation type.
801 public function getAction() {
802 return $this->action
;
806 * Opens the package installation queue and
807 * starts the installation, update or uninstallation of the first entry.
809 * @param integer $parentQueueID
810 * @param integer $processNo
812 public static function openQueue($parentQueueID = 0, $processNo = 0) {
813 $conditions = new PreparedStatementConditionBuilder();
814 $conditions->add("userID = ?", array(WCF
::getUser()->userID
));
815 $conditions->add("parentQueueID = ?", array($parentQueueID));
816 if ($processNo != 0) $conditions->add("processNo = ?", array($processNo));
817 $conditions->add("done = ?", array(0));
820 FROM wcf".WCF_N
."_package_installation_queue
822 ORDER BY queueID ASC";
823 $statement = WCF
::getDB()->prepareStatement($sql);
824 $statement->execute($conditions->getParameters());
825 $packageInstallation = $statement->fetchArray();
827 if (!isset($packageInstallation['queueID'])) {
828 $url = LinkHandler
::getInstance()->getLink('PackageList');
829 HeaderUtil
::redirect($url);
833 $url = LinkHandler
::getInstance()->getLink('PackageInstallationConfirm', array(), 'queueID='.$packageInstallation['queueID']);
834 HeaderUtil
::redirect($url);
840 * Checks the package installation queue for outstanding entries.
844 public static function checkPackageInstallationQueue() {
845 $sql = "SELECT queueID
846 FROM wcf".WCF_N
."_package_installation_queue
848 AND parentQueueID = 0
850 ORDER BY queueID ASC";
851 $statement = WCF
::getDB()->prepareStatement($sql);
852 $statement->execute(array(WCF
::getUser()->userID
));
853 $row = $statement->fetchArray();
859 return $row['queueID'];
863 * Executes post-setup actions.
865 public function completeSetup() {
867 $sql = "SELECT archive
868 FROM wcf".WCF_N
."_package_installation_queue
869 WHERE processNo = ?";
870 $statement = WCF
::getDB()->prepareStatement($sql);
871 $statement->execute(array($this->queue
->processNo
));
872 while ($row = $statement->fetchArray()) {
873 @unlink
($row['archive']);
877 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
878 WHERE processNo = ?";
879 $statement = WCF
::getDB()->prepareStatement($sql);
880 $statement->execute(array($this->queue
->processNo
));
882 // clear language files once whole installation is completed
883 LanguageEditor
::deleteLanguageFiles();
886 CacheHandler
::getInstance()->flushAll();
890 * Updates queue information.
892 public function updatePackage() {
893 if (empty($this->queue
->packageName
)) {
894 $queueEditor = new PackageInstallationQueueEditor($this->queue
);
895 $queueEditor->update(array(
896 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
900 $this->queue
= new PackageInstallationQueue($this->queue
->queueID
);
905 * Validates specific php requirements.
907 * @param array $requirements
908 * @return array<array>
910 public static function validatePHPRequirements(array $requirements) {
913 // validate php version
914 if (isset($requirements['version'])) {
916 if (version_compare(PHP_VERSION
, $requirements['version'], '>=')) {
921 $errors['version'] = array(
922 'required' => $requirements['version'],
923 'installed' => PHP_VERSION
928 // validate extensions
929 if (isset($requirements['extensions'])) {
930 foreach ($requirements['extensions'] as $extension) {
931 $passed = (extension_loaded($extension)) ?
true : false;
934 $errors['extension'][] = array(
935 'extension' => $extension
942 if (isset($requirements['settings'])) {
943 foreach ($requirements['settings'] as $setting => $value) {
944 $iniValue = ini_get($setting);
946 $passed = self
::compareSetting($setting, $value, $iniValue);
948 $errors['setting'][] = array(
949 'setting' => $setting,
950 'required' => $value,
951 'installed' => ($iniValue === false) ?
'(unknown)' : $iniValue
957 // validate functions
958 if (isset($requirements['functions'])) {
959 foreach ($requirements['functions'] as $function) {
960 $function = mb_strtolower($function);
962 $passed = self
::functionExists($function);
964 $errors['function'][] = array(
965 'function' => $function
972 if (isset($requirements['classes'])) {
973 foreach ($requirements['classes'] as $class) {
976 // see: http://de.php.net/manual/en/language.oop5.basic.php
977 if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
978 $globalClass = '\\'.$class;
980 if (class_exists($globalClass, false)) {
986 $errors['class'][] = array(
998 * Validates if an function exists and is not blacklisted by suhosin extension.
1000 * @param string $function
1002 * @see http://de.php.net/manual/en/function.function-exists.php#77980
1004 protected static function functionExists($function) {
1005 if (extension_loaded('suhosin')) {
1006 $blacklist = @ini_get
('suhosin.executor.func.blacklist');
1007 if (!empty($blacklist)) {
1008 $blacklist = explode(',', $blacklist);
1009 foreach ($blacklist as $disabledFunction) {
1010 $disabledFunction = mb_strtolower(StringUtil
::trim($disabledFunction));
1012 if ($function == $disabledFunction) {
1019 return function_exists($function);
1023 * Compares settings, converting values into compareable ones.
1025 * @param string $setting
1026 * @param string $value
1027 * @param mixed $compareValue
1030 protected static function compareSetting($setting, $value, $compareValue) {
1031 if ($compareValue === false) return false;
1033 $value = mb_strtolower($value);
1034 $trueValues = array('1', 'on', 'true');
1035 $falseValues = array('0', 'off', 'false');
1037 // handle values considered as 'true'
1038 if (in_array($value, $trueValues)) {
1039 return ($compareValue) ?
true : false;
1041 // handle values considered as 'false'
1042 else if (in_array($value, $falseValues)) {
1043 return (!$compareValue) ?
true : false;
1045 else if (!is_numeric($value)) {
1046 $compareValue = self
::convertShorthandByteValue($compareValue);
1047 $value = self
::convertShorthandByteValue($value);
1050 return ($compareValue >= $value) ?
true : false;
1054 * Converts shorthand byte values into an integer representing bytes.
1056 * @param string $value
1058 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1060 protected static function convertShorthandByteValue($value) {
1061 // convert into bytes
1062 $lastCharacter = mb_substr($value, -1);
1063 switch ($lastCharacter) {
1066 return (int)$value * 1073741824;
1071 return (int)$value * 1048576;
1076 return (int)$value * 1024;
1086 * Restructure version tables.
1088 protected function restructureVersionTables() {
1089 $objectTypes = ObjectTypeCache
::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
1091 if (empty($objectTypes)) {
1095 // base structure of version tables
1096 $versionTableBaseColumns = array();
1097 $versionTableBaseColumns[] = array('name' => 'versionID', 'data' => array('type' => 'INT', 'length' => 10, 'key' => 'PRIMARY', 'autoIncrement' => 'AUTO_INCREMENT'));
1098 $versionTableBaseColumns[] = array('name' => 'versionUserID', 'data' => array('type' => 'INT', 'length' => 10));
1099 $versionTableBaseColumns[] = array('name' => 'versionUsername', 'data' => array('type' => 'VARCHAR', 'length' => 255));
1100 $versionTableBaseColumns[] = array('name' => 'versionTime', 'data' => array('type' => 'INT', 'length' => 10));
1102 foreach ($objectTypes as $objectType) {
1103 if (!class_exists($objectType->className
)) {
1104 // versionable database object isn't available anymore
1105 // the object type gets deleted later on during the uninstallation
1108 $baseTableColumns = WCF
::getDB()->getEditor()->getColumns(call_user_func(array($objectType->className
, 'getDatabaseTableName')));
1110 // remove primary key from base table columns
1111 foreach ($baseTableColumns as $key => $column) {
1112 if ($column['data']['key'] == 'PRIMARY') {
1113 $baseTableColumns[$key]['data']['key'] = '';
1115 $baseTableColumns[$key]['data']['autoIncrement'] = false;
1118 // get structure of version table
1119 $versionTableColumns = array();
1121 $versionTableColumns = WCF
::getDB()->getEditor()->getColumns(call_user_func(array($objectType->className
, 'getDatabaseVersionTableName')));
1123 catch (\Exception
$e) { }
1125 if (empty($versionTableColumns)) {
1126 $columns = array_merge($versionTableBaseColumns, $baseTableColumns);
1127 WCF
::getDB()->getEditor()->createTable(call_user_func(array($objectType->className
, 'getDatabaseVersionTableName')), $columns);
1129 // add version table to plugin
1130 $sql = "INSERT INTO wcf".WCF_N
."_package_installation_sql_log
1131 (packageID, sqlTable)
1133 $statement = WCF
::getDB()->prepareStatement($sql);
1134 $statement->execute(array(
1135 $this->queue
->packageID
,
1136 call_user_func(array($objectType->className
, 'getDatabaseVersionTableName'))
1140 $baseTableColumnNames = $versionTableColumnNames = $versionTableBaseColumnNames = array();
1141 foreach ($baseTableColumns as $column) {
1142 $baseTableColumnNames[] = $column['name'];
1144 foreach ($versionTableColumns as $column) {
1145 $versionTableColumnNames[] = $column['name'];
1147 foreach ($versionTableBaseColumns as $column) {
1148 $versionTableBaseColumnNames[] = $column['name'];
1151 // check garbage columns in versioned table
1152 foreach ($versionTableColumns as $columnData) {
1153 if (!in_array($columnData['name'], $baseTableColumnNames) && !in_array($columnData['name'], $versionTableBaseColumnNames)) {
1155 WCF
::getDB()->getEditor()->dropColumn(call_user_func(array($objectType->className
, 'getDatabaseVersionTableName')), $columnData['name']);
1159 // check new columns for versioned table
1160 foreach ($baseTableColumns as $columnData) {
1161 if (!in_array($columnData['name'], $versionTableColumnNames)) {
1163 WCF
::getDB()->getEditor()->addColumn(call_user_func(array($objectType->className
, 'getDatabaseVersionTableName')), $columnData['name'], $columnData['data']);