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