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