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