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