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