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