Merge remote-tracking branch 'upstream/master' into versionableDBO
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationDispatcher.class.php
1 <?php
2 namespace wcf\system\package;
3 use wcf\system\style\StyleHandler;
4
5 use wcf\data\application\Application;
6 use wcf\data\application\ApplicationEditor;
7 use wcf\data\language\category\LanguageCategory;
8 use wcf\data\language\LanguageEditor;
9 use wcf\data\language\LanguageList;
10 use wcf\data\option\OptionEditor;
11 use wcf\data\package\installation\queue\PackageInstallationQueue;
12 use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
13 use wcf\data\package\Package;
14 use wcf\data\package\PackageEditor;
15 use wcf\system\application\ApplicationHandler;
16 use wcf\system\cache\CacheHandler;
17 use wcf\system\database\statement\PreparedStatement;
18 use wcf\system\database\util\PreparedStatementConditionBuilder;
19 use wcf\system\exception\SystemException;
20 use wcf\system\form\container;
21 use wcf\system\form\element;
22 use wcf\system\form\FormDocument;
23 use wcf\system\form;
24 use wcf\system\language\LanguageFactory;
25 use wcf\system\package\plugin\IPackageInstallationPlugin;
26 use wcf\system\package\plugin\ObjectTypePackageInstallationPlugin;
27 use wcf\system\package\plugin\SQLPackageInstallationPlugin;
28 use wcf\system\request\LinkHandler;
29 use wcf\system\request\RouteHandler;
30 use wcf\system\WCF;
31 use wcf\util\FileUtil;
32 use wcf\util\HeaderUtil;
33 use wcf\util\StringUtil;
34
35 /**
36 * PackageInstallationDispatcher handles the whole installation process.
37 *
38 * @author Alexander Ebert
39 * @copyright 2001-2012 WoltLab GmbH
40 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
41 * @package com.woltlab.wcf
42 * @subpackage system.package
43 * @category Community Framework
44 */
45 class PackageInstallationDispatcher {
46 /**
47 * current installation type
48 * @var string
49 */
50 protected $action = '';
51
52 /**
53 * instance of PackageArchive
54 * @var wcf\system\package\PackageArchive
55 */
56 public $archive = null;
57
58 /**
59 * instance of PackageInstallationNodeBuilder
60 * @var wcf\system\package\PackageInstallationNodeBuilder
61 */
62 public $nodeBuilder = null;
63
64 /**
65 * instance of Package
66 * @var wcf\data\package\Package
67 */
68 public $package = null;
69
70 /**
71 * instance of PackageInstallationQueue
72 * @var wcf\system\package\PackageInstallationQueue
73 */
74 public $queue = null;
75
76 /**
77 * default name of the config file
78 * @var string
79 */
80 const CONFIG_FILE = 'config.inc.php';
81
82 /**
83 * holds state of structuring version tables
84 * @var boolean
85 */
86 protected $requireRestructureVersionTables = false;
87
88 /**
89 * Creates a new instance of PackageInstallationDispatcher.
90 *
91 * @param wcf\data\package\installation\queue\PackageInstallationQueue $queue
92 */
93 public function __construct(PackageInstallationQueue $queue) {
94 $this->queue = $queue;
95 $this->nodeBuilder = new PackageInstallationNodeBuilder($this);
96
97 $this->action = $this->queue->action;
98 }
99
100 /**
101 * Installs node components and returns next node.
102 *
103 * @param string $node
104 * @return wcf\system\package\PackageInstallationStep
105 */
106 public function install($node) {
107 $nodes = $this->nodeBuilder->getNodeData($node);
108
109 // invoke node-specific actions
110 foreach ($nodes as $data) {
111 $nodeData = unserialize($data['nodeData']);
112
113 switch ($data['nodeType']) {
114 case 'package':
115 $step = $this->installPackage($nodeData);
116 break;
117
118 case 'pip':
119 $step = $this->executePIP($nodeData);
120 break;
121
122 case 'optionalPackages':
123 $step = $this->selectOptionalPackages($node, $nodeData);
124 break;
125
126 default:
127 die("Unknown node type: '".$data['nodeType']."'");
128 break;
129 }
130
131 if ($step->splitNode()) {
132 $this->nodeBuilder->cloneNode($node, $data['sequenceNo']);
133 break;
134 }
135 }
136
137 // mark node as completed
138 $this->nodeBuilder->completeNode($node);
139
140 // assign next node
141 $node = $this->nodeBuilder->getNextNode($node);
142 $step->setNode($node);
143
144 // perform post-install/update actions
145 if ($node == '') {
146 // update options.inc.php
147 OptionEditor::resetCache();
148
149 if ($this->action == 'install') {
150 // save localized package infos
151 $this->saveLocalizedPackageInfos();
152
153 // remove all cache files after WCFSetup
154 if (!PACKAGE_ID) {
155 CacheHandler::getInstance()->clear(WCF_DIR.'cache/', 'cache.*.php');
156 }
157
158 // rebuild application paths
159 ApplicationHandler::rebuild();
160 ApplicationEditor::setup();
161 }
162
163 // remove template listener cache
164 CacheHandler::getInstance()->clear(WCF_DIR.'cache/templateListener/', '*.php');
165
166 // reset language cache
167 LanguageFactory::getInstance()->clearCache();
168 LanguageFactory::getInstance()->deleteLanguageCache();
169
170 // reset stylesheets
171 StyleHandler::resetStylesheets();
172 }
173
174 if ($this->requireRestructureVersionTables) {
175 $this->restructureVersionTables();
176 }
177
178 return $step;
179 }
180
181 /**
182 * Returns current package archive.
183 *
184 * @return wcf\system\package\PackageArchive
185 */
186 public function getArchive() {
187 if ($this->archive === null) {
188 $this->archive = new PackageArchive($this->queue->archive, $this->getPackage());
189
190 if (FileUtil::isURL($this->archive->getArchive())) {
191 // get return value and update entry in
192 // package_installation_queue with this value
193 $archive = $this->archive->downloadArchive();
194 $queueEditor = new PackageInstallationQueueEditor($this->queue);
195 $queueEditor->update(array(
196 'archive' => $archive
197 ));
198 }
199
200 $this->archive->openArchive();
201 }
202
203 return $this->archive;
204 }
205
206 /**
207 * Installs current package.
208 *
209 * @param array $nodeData
210 */
211 protected function installPackage(array $nodeData) {
212 $installationStep = new PackageInstallationStep();
213
214 // check requirements
215 if (!empty($nodeData['requirements'])) {
216 foreach ($nodeData['requirements'] as $package => $requirementData) {
217 // get existing package
218 if ($requirementData['packageID']) {
219 $sql = "SELECT packageName, packageVersion
220 FROM wcf".WCF_N."_package
221 WHERE packageID = ?";
222 $statement = WCF::getDB()->prepareStatement($sql);
223 $statement->execute(array($requirementData['packageID']));
224 }
225 else {
226 // try to find matching package
227 $sql = "SELECT packageName, packageVersion
228 FROM wcf".WCF_N."_package
229 WHERE package = ?";
230 $statement = WCF::getDB()->prepareStatement($sql);
231 $statement->execute(array($package));
232 }
233 $row = $statement->fetchArray();
234
235 // package is required but not available
236 if ($row === false) {
237 throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
238 }
239
240 // check version requirements
241 if ($requirementData['minVersion']) {
242 if (Package::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
243 throw new SystemException("Package '".$nodeData['packageName']."' requires the package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but version '".$row['packageVersion']."'");
244 }
245 }
246 }
247 }
248 unset($nodeData['requirements']);
249
250 if (!$this->queue->packageID) {
251 // create package entry
252 $package = PackageEditor::create($nodeData);
253
254 // update package id for current queue
255 $queueEditor = new PackageInstallationQueueEditor($this->queue);
256 $queueEditor->update(array(
257 'packageID' => $package->packageID
258 ));
259
260 // save excluded packages
261 if (count($this->getArchive()->getExcludedPackages()) > 0) {
262 $sql = "INSERT INTO wcf".WCF_N."_package_exclusion
263 (packageID, excludedPackage, excludedPackageVersion)
264 VALUES (?, ?, ?)";
265 $statement = WCF::getDB()->prepareStatement($sql);
266
267 foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
268 $statement->execute(array($package->packageID, $excludedPackage['name'], (!empty($excludedPackage['version']) ? $excludedPackage['version'] : '')));
269 }
270 }
271
272 // if package is plugin to com.woltlab.wcf it must not have any other requirement
273 $requirements = $this->getArchive()->getRequirements();
274
275 // insert requirements and dependencies
276 $requirements = $this->getArchive()->getAllExistingRequirements();
277 if (!empty($requirements)) {
278 $sql = "INSERT INTO wcf".WCF_N."_package_requirement
279 (packageID, requirement)
280 VALUES (?, ?)";
281 $statement = WCF::getDB()->prepareStatement($sql);
282
283 foreach ($requirements as $identifier => $possibleRequirements) {
284 if (count($possibleRequirements) == 1) {
285 $requirement = array_shift($possibleRequirements);
286 }
287 else {
288 $requirement = $possibleRequirements[$this->selectedRequirements[$identifier]];
289 }
290
291 $statement->execute(array($package->packageID, $requirement['packageID']));
292 }
293 }
294
295 // build requirement map
296 Package::rebuildPackageRequirementMap($package->packageID);
297
298 // reload queue
299 $this->queue = new PackageInstallationQueue($this->queue->queueID);
300 $this->package = null;
301
302 if ($package->isApplication) {
303 $host = StringUtil::replace(RouteHandler::getProtocol(), '', RouteHandler::getHost());
304 $path = RouteHandler::getPath(array('acp'));
305
306 // insert as application
307 ApplicationEditor::create(array(
308 'domainName' => $host,
309 'domainPath' => $path,
310 'cookieDomain' => $host,
311 'cookiePath' => $path,
312 'packageID' => $package->packageID
313 ));
314 }
315 }
316
317 if ($this->getPackage()->isApplication && $this->getPackage()->package != 'com.woltlab.wcf' && $this->getAction() == 'install') {
318 if (empty($this->getPackage()->packageDir)) {
319 $document = $this->promptPackageDir();
320 if ($document !== null && $document instanceof form\FormDocument) {
321 $installationStep->setDocument($document);
322 }
323
324 $installationStep->setSplitNode();
325 }
326 }
327
328 return $installationStep;
329 }
330
331 /**
332 * Saves the localized package infos.
333 *
334 * @todo license and readme
335 */
336 protected function saveLocalizedPackageInfos() {
337 $package = new Package($this->queue->packageID);
338
339 // localize package information
340 $sql = "INSERT INTO wcf".WCF_N."_language_item
341 (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
342 VALUES (?, ?, ?, ?, ?)";
343 $statement = WCF::getDB()->prepareStatement($sql);
344
345 // get language list
346 $languageList = new LanguageList();
347 $languageList->sqlLimit = 0;
348 $languageList->readObjects();
349
350 // workaround for WCFSetup
351 if (!PACKAGE_ID) {
352 $sql = "SELECT *
353 FROM wcf".WCF_N."_language_category
354 WHERE languageCategory = ?";
355 $statement2 = WCF::getDB()->prepareStatement($sql);
356 $statement2->execute(array('wcf.acp.package'));
357 $languageCategory = $statement2->fetchObject('wcf\data\language\category\LanguageCategory');
358 }
359 else {
360 $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.acp.package');
361 }
362
363 // save package name
364 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
365
366 // save package description
367 $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
368
369 // update description and name
370 $packageEditor = new PackageEditor($package);
371 $packageEditor->update(array(
372 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue->packageID,
373 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue->packageID
374 ));
375 }
376
377 /**
378 * Saves a localized package info.
379 *
380 * @param wcf\system\database\statement\PreparedStatement $statement
381 * @param wcf\data\language\LanguageList $languageList
382 * @param wcf\data\language\category\LanguageCategory $languageCategory
383 * @param wcf\data\package\Package $package
384 * @param string $infoName
385 */
386 protected function saveLocalizedPackageInfo(PreparedStatement $statement, $languageList, LanguageCategory $languageCategory, Package $package, $infoName) {
387 $infoValues = $this->getArchive()->getPackageInfo($infoName);
388
389 // get default value for languages without specified information
390 $defaultValue = '';
391 if (isset($infoValues['default'])) {
392 $defaultValue = $infoValues['default'];
393 }
394 else if (isset($infoValues['en'])) {
395 // fallback to English
396 $defaultValue = $infoValues['en'];
397 }
398 else if (isset($infoValues[WCF::getLanguage()->getFixedLanguageCode()])) {
399 // fallback to the language of the current user
400 $defaultValue = $infoValues[WCF::getLanguage()->getFixedLanguageCode()];
401 }
402 else if ($infoName == 'packageName') {
403 // fallback to the package identifier for the package name
404 $defaultValue = $this->archive->getPackageInfo('name');
405 }
406
407 foreach ($languageList as $language) {
408 $value = $defaultValue;
409 if (isset($infoValues[$language->languageCode])) {
410 $value = $infoValues[$language->languageCode];
411 }
412
413 $statement->execute(array(
414 $language->languageID,
415 'wcf.acp.package.'.$infoName.'.package'.$package->packageID,
416 $value,
417 $languageCategory->languageCategoryID,
418 1
419 ));
420 }
421 }
422
423 /**
424 * Executes a package installation plugin.
425 *
426 * @param array step
427 * @return boolean
428 */
429 protected function executePIP(array $nodeData) {
430 $step = new PackageInstallationStep();
431
432 // fetch all pips associated with current PACKAGE_ID and include pips
433 // previously installed by current installation queue
434 $sql = "SELECT pluginName, className
435 FROM wcf".WCF_N."_package_installation_plugin
436 WHERE pluginName = ?";
437 $statement = WCF::getDB()->prepareStatement($sql);
438 $statement->execute(array(
439 $nodeData['pip']
440 ));
441 $row = $statement->fetchArray();
442
443 // PIP is unknown
444 if (!$row || (strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
445 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
446 }
447
448 // valdidate class definition
449 $className = $row['className'];
450 if (!class_exists($className)) {
451 throw new SystemException("unable to find class '".$className."'");
452 }
453
454 $plugin = new $className($this, $nodeData);
455
456 if (!($plugin instanceof IPackageInstallationPlugin)) {
457 throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
458 }
459
460 if ($plugin instanceof SQLPackageInstallationPlugin || $plugin instanceof ObjectTypePackageInstallationPlugin) {
461 $this->requireRestructureVersionTables = true;
462 }
463
464 // execute PIP
465 try {
466 $document = $plugin->{$this->action}();
467 }
468 catch (SplitNodeException $e) {
469 $step->setSplitNode();
470 }
471
472 if ($document !== null && ($document instanceof FormDocument)) {
473 $step->setDocument($document);
474 $step->setSplitNode();
475 }
476
477 return $step;
478 }
479
480 // @todo: comment
481 protected function selectOptionalPackages($currentNode, array $nodeData) {
482 $installationStep = new PackageInstallationStep();
483
484 $document = $this->promptOptionalPackages($nodeData);
485 if ($document !== null && $document instanceof form\FormDocument) {
486 $installationStep->setDocument($document);
487 $installationStep->setSplitNode();
488 }
489 // insert new nodes for each package
490 else if (is_array($document)) {
491 // get target child node
492 $node = $currentNode;
493 $queue = $this->queue;
494 $shiftNodes = false;
495
496 foreach ($nodeData as $package) {
497 if (in_array($package['package'], $document)) {
498 if (!$shiftNodes) {
499 $this->nodeBuilder->shiftNodes($currentNode, 'tempNode');
500 $shiftNodes = true;
501 }
502
503 $queue = PackageInstallationQueueEditor::create(array(
504 'parentQueueID' => $queue->queueID,
505 'processNo' => $this->queue->processNo,
506 'userID' => WCF::getUser()->userID,
507 'package' => $package['package'],
508 'packageName' => $package['packageName'],
509 'archive' => $package['archive'],
510 'action' => $queue->action
511 ));
512
513 $installation = new PackageInstallationDispatcher($queue);
514 $installation->nodeBuilder->setParentNode($node);
515 $installation->nodeBuilder->buildNodes();
516 $node = $installation->nodeBuilder->getCurrentNode();
517 }
518 }
519
520 // shift nodes
521 if ($shiftNodes) {
522 $this->nodeBuilder->shiftNodes('tempNode', $node);
523 }
524 }
525
526 return $installationStep;
527 }
528
529 /**
530 * Extracts files from .tar (or .tar.gz) archive and installs them
531 *
532 * @param string $targetDir
533 * @param string $sourceArchive
534 * @param FileHandler $fileHandler
535 * @return wcf\system\setup\Installer
536 */
537 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
538 return new \wcf\system\setup\Installer($targetDir, $sourceArchive, $fileHandler);
539 }
540
541 /**
542 * Returns current package.
543 *
544 * @return wcf\data\package\Package
545 */
546 public function getPackage() {
547 if ($this->package === null) {
548 $this->package = new Package($this->queue->packageID);
549 }
550
551 return $this->package;
552 }
553
554 /**
555 * Prompts for a text input for package directory (applies for applications only)
556 *
557 * @return wcf\system\form\FormDocument
558 */
559 protected function promptPackageDir() {
560 if (!PackageInstallationFormManager::findForm($this->queue, 'packageDir')) {
561
562 $container = new container\GroupFormElementContainer();
563 $packageDir = new element\TextInputFormElement($container);
564 $packageDir->setName('packageDir');
565 $packageDir->setLabel(WCF::getLanguage()->get('wcf.acp.package.packageDir.input'));
566
567 $path = RouteHandler::getPath(array('wcf', 'acp'));
568 $defaultPath = FileUtil::addTrailingSlash(FileUtil::unifyDirSeperator($_SERVER['DOCUMENT_ROOT'] . $path));
569 $packageDir->setValue($defaultPath);
570 $container->appendChild($packageDir);
571
572 $document = new form\FormDocument('packageDir');
573 $document->appendContainer($container);
574
575 PackageInstallationFormManager::registerForm($this->queue, $document);
576 return $document;
577 }
578 else {
579 $document = PackageInstallationFormManager::getForm($this->queue, 'packageDir');
580 $document->handleRequest();
581 $packageDir = $document->getValue('packageDir');
582
583 if ($packageDir !== null) {
584 // validate package dir
585 if (file_exists(FileUtil::addTrailingSlash($packageDir) . 'global.php')) {
586 $document->setError('packageDir', WCF::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
587 return $document;
588 }
589
590 // set package dir
591 $packageEditor = new PackageEditor($this->getPackage());
592 $packageEditor->update(array(
593 'packageDir' => FileUtil::getRelativePath(WCF_DIR, $packageDir)
594 ));
595
596 // parse domain path
597 $domainPath = FileUtil::getRelativePath(FileUtil::unifyDirSeperator($_SERVER['DOCUMENT_ROOT']), FileUtil::unifyDirSeperator($packageDir));
598
599 // work-around for applications installed in document root
600 if ($domainPath == './') {
601 $domainPath = '';
602 }
603
604 $domainPath = FileUtil::addLeadingSlash(FileUtil::addTrailingSlash($domainPath));
605
606 // update application path
607 $application = new Application($this->getPackage()->packageID);
608 $applicationEditor = new ApplicationEditor($application);
609 $applicationEditor->update(array(
610 'domainPath' => $domainPath,
611 'cookiePath' => $domainPath
612 ));
613
614 // create directory and set permissions
615 @mkdir($packageDir, 0777, true);
616 @chmod($packageDir, 0777);
617 }
618
619 return null;
620 }
621 }
622
623 // @todo: comment
624 protected function promptOptionalPackages(array $packages) {
625 if (!PackageInstallationFormManager::findForm($this->queue, 'optionalPackages')) {
626 $container = new container\MultipleSelectionFormElementContainer();
627 $container->setName('optionalPackages');
628
629 foreach ($packages as $package) {
630 $optionalPackage = new element\MultipleSelectionFormElement($container);
631 $optionalPackage->setName('optionalPackages');
632 $optionalPackage->setLabel($package['packageName']);
633 $optionalPackage->setValue($package['package']);
634
635 $container->appendChild($optionalPackage);
636 }
637
638 $document = new form\FormDocument('optionalPackages');
639 $document->appendContainer($container);
640
641 PackageInstallationFormManager::registerForm($this->queue, $document);
642 return $document;
643 }
644 else {
645 $document = PackageInstallationFormManager::getForm($this->queue, 'optionalPackages');
646 $document->handleRequest();
647
648 return $document->getValue('optionalPackages');
649 }
650 }
651
652 /**
653 * Returns current package id.
654 *
655 * @return integer
656 */
657 public function getPackageID() {
658 return $this->queue->packageID;
659 }
660
661 /**
662 * Returns current package installation type.
663 *
664 * @return string
665 */
666 public function getAction() {
667 return $this->action;
668 }
669
670 /**
671 * Opens the package installation queue and
672 * starts the installation, update or uninstallation of the first entry.
673 *
674 * @param integer $parentQueueID
675 * @param integer $processNo
676 */
677 public static function openQueue($parentQueueID = 0, $processNo = 0) {
678 $conditions = new PreparedStatementConditionBuilder();
679 $conditions->add("userID = ?", array(WCF::getUser()->userID));
680 $conditions->add("parentQueueID = ?", array($parentQueueID));
681 if ($processNo != 0) $conditions->add("processNo = ?", array($processNo));
682 $conditions->add("done = ?", array(0));
683
684 $sql = "SELECT *
685 FROM wcf".WCF_N."_package_installation_queue
686 ".$conditions."
687 ORDER BY queueID ASC";
688 $statement = WCF::getDB()->prepareStatement($sql);
689 $statement->execute($conditions->getParameters());
690 $packageInstallation = $statement->fetchArray();
691
692 if (!isset($packageInstallation['queueID'])) {
693 $url = LinkHandler::getInstance()->getLink('PackageList');
694 HeaderUtil::redirect($url);
695 exit;
696 }
697 else {
698 $url = LinkHandler::getInstance()->getLink('PackageInstallationConfirm', array(), 'action='.$packageInstallation['action'].'&queueID='.$packageInstallation['queueID']);
699 HeaderUtil::redirect($url);
700 exit;
701 }
702 }
703
704 /**
705 * Checks the package installation queue for outstanding entries.
706 *
707 * @return integer
708 */
709 public static function checkPackageInstallationQueue() {
710 $sql = "SELECT queueID
711 FROM wcf".WCF_N."_package_installation_queue
712 WHERE userID = ?
713 AND parentQueueID = 0
714 AND done = 0
715 ORDER BY queueID ASC";
716 $statement = WCF::getDB()->prepareStatement($sql);
717 $statement->execute(array(WCF::getUser()->userID));
718 $row = $statement->fetchArray();
719
720 if (!$row) {
721 return 0;
722 }
723
724 return $row['queueID'];
725 }
726
727 /**
728 * Executes post-setup actions.
729 */
730 public function completeSetup() {
731 // mark queue as done
732 $queueEditor = new PackageInstallationQueueEditor($this->queue);
733 $queueEditor->update(array(
734 'done' => 1
735 ));
736
737 // remove node data
738 $this->nodeBuilder->purgeNodes();
739
740 // update package version
741 if ($this->action == 'update') {
742 $packageEditor = new PackageEditor($this->getPackage());
743 $packageEditor->update(array(
744 'updateDate' => TIME_NOW,
745 'packageVersion' => $this->archive->getPackageInfo('version')
746 ));
747 }
748
749 // clear language files once whole installation is completed
750 LanguageEditor::deleteLanguageFiles();
751
752 // reset all caches
753 CacheHandler::getInstance()->clear(WCF_DIR.'cache/', '*');
754 }
755
756 /**
757 * Updates queue information.
758 */
759 public function updatePackage() {
760 if (empty($this->queue->packageName)) {
761 $queueEditor = new PackageInstallationQueueEditor($this->queue);
762 $queueEditor->update(array(
763 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
764 ));
765
766 // reload queue
767 $this->queue = new PackageInstallationQueue($this->queue->queueID);
768 }
769 }
770
771 /**
772 * Validates specific php requirements.
773 *
774 * @param array $requirements
775 * @return array<array>
776 */
777 public static function validatePHPRequirements(array $requirements) {
778 $errors = array();
779
780 // validate php version
781 if (isset($requirements['version'])) {
782 $passed = false;
783 if (version_compare(PHP_VERSION, $requirements['version'], '>=')) {
784 $passed = true;
785 }
786
787 if (!$passed) {
788 $errors['version'] = array(
789 'required' => $requirements['version'],
790 'installed' => PHP_VERSION
791 );
792 }
793 }
794
795 // validate extensions
796 if (isset($requirements['extensions'])) {
797 foreach ($requirements['extensions'] as $extension) {
798 $passed = (extension_loaded($extension)) ? true : false;
799
800 if (!$passed) {
801 $errors['extension'][] = array(
802 'extension' => $extension
803 );
804 }
805 }
806 }
807
808 // validate settings
809 if (isset($requirements['settings'])) {
810 foreach ($requirements['settings'] as $setting => $value) {
811 $iniValue = ini_get($setting);
812
813 $passed = self::compareSetting($setting, $value, $iniValue);
814 if (!$passed) {
815 $errors['setting'][] = array(
816 'setting' => $setting,
817 'required' => $value,
818 'installed' => ($iniValue === false) ? '(unknown)' : $iniValue
819 );
820 }
821 }
822 }
823
824 // validate functions
825 if (isset($requirements['functions'])) {
826 foreach ($requirements['functions'] as $function) {
827 $function = StringUtil::toLowerCase($function);
828
829 $passed = self::functionExists($function);
830 if (!$passed) {
831 $errors['function'][] = array(
832 'function' => $function
833 );
834 }
835 }
836 }
837
838 // validate classes
839 if (isset($requirements['classes'])) {
840 foreach ($requirements['classes'] as $class) {
841 $passed = false;
842
843 // see: http://de.php.net/manual/en/language.oop5.basic.php
844 if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
845 $globalClass = '\\'.$class;
846
847 if (class_exists($globalClass, false)) {
848 $passed = true;
849 }
850 }
851
852 if (!$passed) {
853 $errors['class'][] = array(
854 'class' => $class
855 );
856 }
857 }
858
859 }
860
861 return $errors;
862 }
863
864 /**
865 * Validates if an function exists and is not blacklisted by suhosin extension.
866 *
867 * @param string $function
868 * @return boolean
869 * @see http://de.php.net/manual/en/function.function-exists.php#77980
870 */
871 protected static function functionExists($function) {
872 if (extension_loaded('suhosin')) {
873 $blacklist = @ini_get('suhosin.executor.func.blacklist');
874 if (!empty($blacklist)) {
875 $blacklist = explode(',', $blacklist);
876 foreach ($blacklist as $disabledFunction) {
877 $disabledFunction = StringUtil::toLowerCase(StringUtil::trim($disabledFunction));
878
879 if ($function == $disabledFunction) {
880 return false;
881 }
882 }
883 }
884 }
885
886 return function_exists($function);
887 }
888
889 /**
890 * Compares settings, converting values into compareable ones.
891 *
892 * @param string $setting
893 * @param string $value
894 * @param mixed $compareValue
895 * @return boolean
896 */
897 protected static function compareSetting($setting, $value, $compareValue) {
898 if ($compareValue === false) return false;
899
900 $value = StringUtil::toLowerCase($value);
901 $trueValues = array('1', 'on', 'true');
902 $falseValues = array('0', 'off', 'false');
903
904 // handle values considered as 'true'
905 if (in_array($value, $trueValues)) {
906 return ($compareValue) ? true : false;
907 }
908 // handle values considered as 'false'
909 else if (in_array($value, $falseValues)) {
910 return (!$compareValue) ? true : false;
911 }
912 else if (!is_numeric($value)) {
913 $compareValue = self::convertShorthandByteValue($compareValue);
914 $value = self::convertShorthandByteValue($value);
915 }
916
917 return ($compareValue >= $value) ? true : false;
918 }
919
920 /**
921 * Converts shorthand byte values into an integer representing bytes.
922 *
923 * @param string $value
924 * @return integer
925 * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
926 */
927 protected static function convertShorthandByteValue($value) {
928 // convert into bytes
929 $lastCharacter = StringUtil::substring($value, -1);
930 switch ($lastCharacter) {
931 // gigabytes
932 case 'g':
933 return (int)$value * 1073741824;
934 break;
935
936 // megabytes
937 case 'm':
938 return (int)$value * 1048576;
939 break;
940
941 // kilobytes
942 case 'k':
943 return (int)$value * 1024;
944 break;
945
946 default:
947 return $value;
948 break;
949 }
950 }
951
952 /*
953 * Restructure version tables.
954 */
955 protected function restructureVersionTables() {
956 $objectTypes = \wcf\system\version\VersionHandler::getInstance()->getObjectTypes();
957
958 if (empty($objectTypes)) {
959 return;
960 }
961
962 // base structure of version tables
963 $versionTableBaseColumns = array();
964 $versionTableBaseColumns[] = array('name' => 'versionID', 'data' => array('type' => 'INT', 'key' => 'PRIMARY', 'autoIncrement' => 'AUTO_INCREMENT'));
965 $versionTableBaseColumns[] = array('name' => 'versionUserID', 'data' => array('type' => 'INT'));
966 $versionTableBaseColumns[] = array('name' => 'versionUsername', 'data' => array('type' => 'VARCHAR', 'length' => 255));
967 $versionTableBaseColumns[] = array('name' => 'versionTime', 'data' => array('type' => 'INT'));
968
969 foreach ($objectTypes as $objectTypeID => $objectType) {
970 // get structure of base table
971 $baseTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseTableName());
972 // get structure of version table
973 $versionTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseVersionTableName());
974
975 if (empty($versionTableColumns)){
976 $columns = array_merge($versionTableBaseColumns, $baseTableColumns);
977
978 WCF::getDB()->getEditor()->createTable($objectType::getDatabaseVersionTableName(), $columns);
979 }
980 else {
981 // check garbage columns in versioned table
982 foreach ($versionTableColumns as $columnData) {
983 if (!array_search($columnData['name'], $baseTableColumns, true)) {
984 // delete column
985 WCF::getDB()->getEditor()->dropColumn($objectType::getDatabaseVersionTableName(), $columnData['name']);
986 }
987 }
988
989 // check new columns for versioned table
990 foreach ($baseTableColumns as $columnData) {
991 if (!array_search($columnData['name'], $versionTableColumns, true)) {
992 // add colum
993 WCF::getDB()->getEditor()->addColumn($objectType::getDatabaseVersionTableName(), $columnData['name'], $columnData['data']);
994 }
995 }
996 }
997 }
998 }
999 }