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