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