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