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