Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationDispatcher.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\system\package;
c244fcc6 3use wcf\data\application\Application;
11ade432 4use wcf\data\application\ApplicationEditor;
a2ad7897 5use wcf\data\language\category\LanguageCategory;
11ade432 6use wcf\data\language\LanguageEditor;
a2ad7897 7use wcf\data\language\LanguageList;
8aa93ba8 8use wcf\data\option\OptionEditor;
ec1b1daf
TB
9use wcf\data\package\installation\queue\PackageInstallationQueue;
10use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
2bc9f31d
TB
11use wcf\data\package\Package;
12use wcf\data\package\PackageEditor;
f1c1fc65 13use wcf\system\application\ApplicationHandler;
14d48464 14use wcf\data\object\type\ObjectTypeCache;
f1dc282d 15use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
11ade432 16use wcf\system\cache\CacheHandler;
a2ad7897 17use wcf\system\database\statement\PreparedStatement;
11ade432 18use wcf\system\database\util\PreparedStatementConditionBuilder;
fb14b37f 19use wcf\system\event\EventHandler;
11ade432 20use wcf\system\exception\SystemException;
a99fcf96
MS
21use wcf\system\form\container\GroupFormElementContainer;
22use wcf\system\form\container\MultipleSelectionFormElementContainer;
23use wcf\system\form\element\MultipleSelectionFormElement;
24use wcf\system\form\element\TextInputFormElement;
b4282c86 25use wcf\system\form\FormDocument;
a2ad7897 26use wcf\system\language\LanguageFactory;
4d4a5125
K
27use wcf\system\package\plugin\IPackageInstallationPlugin;
28use wcf\system\package\plugin\ObjectTypePackageInstallationPlugin;
29use wcf\system\package\plugin\SQLPackageInstallationPlugin;
3e0e6b2c 30use wcf\system\request\LinkHandler;
3dcfb497 31use wcf\system\request\RouteHandler;
8f3fc897 32use wcf\system\setup\Installer;
cd0fbe9a 33use wcf\system\style\StyleHandler;
fb14b37f 34use wcf\system\user\storage\UserStorageHandler;
2bc9f31d 35use wcf\system\WCF;
11ade432
AE
36use wcf\util\FileUtil;
37use wcf\util\HeaderUtil;
38use wcf\util\StringUtil;
39
40/**
41 * PackageInstallationDispatcher handles the whole installation process.
9f959ced 42 *
11ade432 43 * @author Alexander Ebert
ca4ba303 44 * @copyright 2001-2014 WoltLab GmbH
11ade432
AE
45 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
46 * @package com.woltlab.wcf
47 * @subpackage system.package
9f959ced 48 * @category Community Framework
11ade432
AE
49 */
50class PackageInstallationDispatcher {
51 /**
52 * current installation type
11ade432
AE
53 * @var string
54 */
55 protected $action = '';
56
57 /**
58 * instance of PackageArchive
0ad90fc3 59 * @var \wcf\system\package\PackageArchive
11ade432
AE
60 */
61 public $archive = null;
62
63 /**
64 * instance of PackageInstallationNodeBuilder
0ad90fc3 65 * @var \wcf\system\package\PackageInstallationNodeBuilder
11ade432
AE
66 */
67 public $nodeBuilder = null;
68
69 /**
70 * instance of Package
0ad90fc3 71 * @var \wcf\data\package\Package
11ade432
AE
72 */
73 public $package = null;
74
75 /**
76 * instance of PackageInstallationQueue
0ad90fc3 77 * @var \wcf\system\package\PackageInstallationQueue
11ade432
AE
78 */
79 public $queue = null;
80
81 /**
82 * default name of the config file
9f959ced 83 * @var string
11ade432
AE
84 */
85 const CONFIG_FILE = 'config.inc.php';
86
82a7de7f
K
87 /**
88 * holds state of structuring version tables
06355ec3 89 * @var boolean
82a7de7f 90 */
39935629
AE
91 protected $requireRestructureVersionTables = false;
92
93 /**
94 * data of previous package in queue
95 * @var array<string>
96 */
97 protected $previousPackageData = null;
82a7de7f 98
11ade432
AE
99 /**
100 * Creates a new instance of PackageInstallationDispatcher.
59dc0db6 101 *
0ad90fc3 102 * @param \wcf\data\package\installation\queue\PackageInstallationQueue $queue
11ade432
AE
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
39935629
AE
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
11ade432
AE
120 /**
121 * Installs node components and returns next node.
9f959ced 122 *
11ade432 123 * @param string $node
0ad90fc3 124 * @return \wcf\system\package\PackageInstallationStep
11ade432
AE
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;
cfedc216 141
492816d3 142 case 'optionalPackages':
456008db 143 $step = $this->selectOptionalPackages($node, $nodeData);
492816d3
AE
144 break;
145
cfedc216
AE
146 default:
147 die("Unknown node type: '".$data['nodeType']."'");
148 break;
11ade432
AE
149 }
150
151 if ($step->splitNode()) {
456008db 152 $this->nodeBuilder->cloneNode($node, $data['sequenceNo']);
11ade432
AE
153 break;
154 }
155 }
156
157 // mark node as completed
158 $this->nodeBuilder->completeNode($node);
159
160 // assign next node
d7802a05 161 $tmp = $node;
8aa93ba8
AE
162 $node = $this->nodeBuilder->getNextNode($node);
163 $step->setNode($node);
164
07e26cb9 165 // perform post-install/update actions
8aa93ba8 166 if ($node == '') {
07e26cb9 167 // update options.inc.php
8aa93ba8 168 OptionEditor::resetCache();
5475a95d 169
1e996eac 170 if ($this->action == 'install') {
07e26cb9 171 // save localized package infos
1e996eac 172 $this->saveLocalizedPackageInfos();
14077e5c
AE
173
174 // remove all cache files after WCFSetup
175 if (!PACKAGE_ID) {
b401cd0d 176 CacheHandler::getInstance()->flushAll();
d43bb3f0
AE
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 ));
05fe4769
AE
187
188 // update options.inc.php
189 OptionEditor::resetCache();
d43bb3f0 190 }
14077e5c 191 }
f1c1fc65
AE
192
193 // rebuild application paths
194 ApplicationHandler::rebuild();
131de123 195 ApplicationEditor::setup();
1e996eac 196 }
07e26cb9
AE
197
198 // remove template listener cache
b401cd0d 199 TemplateListenerCodeCacheBuilder::getInstance()->reset();
6dfe934f 200
07e26cb9
AE
201 // reset language cache
202 LanguageFactory::getInstance()->clearCache();
203 LanguageFactory::getInstance()->deleteLanguageCache();
83c78288
AE
204
205 // reset stylesheets
206 StyleHandler::resetStylesheets();
ca9a7a6a 207
e3fc42e8
MS
208 // clear user storage
209 UserStorageHandler::getInstance()->clear();
fb14b37f 210
824d9e90
AE
211 // rebuild config files for affected applications
212 $sql = "SELECT package.packageID
26cf6096
TD
213 FROM wcf".WCF_N."_package_installation_queue queue,
214 wcf".WCF_N."_package package
824d9e90
AE
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
fb14b37f 229 EventHandler::getInstance()->fireAction($this, 'postInstall');
824d9e90 230
36fe10e2
AE
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']);
36fe10e2
AE
239 }
240
241 // delete queues
824d9e90
AE
242 $sql = "DELETE FROM wcf".WCF_N."_package_installation_queue
243 WHERE processNo = ?";
244 $statement = WCF::getDB()->prepareStatement($sql);
b953f892 245 $statement->execute(array($this->queue->processNo));
e3fc42e8 246 }
82a7de7f
K
247
248 if ($this->requireRestructureVersionTables) {
249 $this->restructureVersionTables();
e3fc42e8 250 }
11ade432
AE
251
252 return $step;
253 }
254
255 /**
256 * Returns current package archive.
9f959ced 257 *
0ad90fc3 258 * @return \wcf\system\package\PackageArchive
11ade432
AE
259 */
260 public function getArchive() {
261 if ($this->archive === null) {
39935629
AE
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']) {
d8782831 265 if (Package::compareVersion($this->getPackage()->packageVersion, $this->previousPackageData['packageVersion'], '<')) {
39935629
AE
266 // fake package to simulate the package version required by current archive
267 $this->getPackage()->setPackageVersion($this->previousPackageData['packageVersion']);
268 }
269 }
270
ca38121b 271 $this->archive = new PackageArchive($this->queue->archive, $this->getPackage());
11ade432
AE
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.
9f959ced 291 *
11ade432
AE
292 * @param array $nodeData
293 */
294 protected function installPackage(array $nodeData) {
295 $installationStep = new PackageInstallationStep();
296
a1ece4d9
AE
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) {
95cda2bf 326 throw new SystemException("Package '".$nodeData['packageName']."' requires package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but only version '".$row['packageVersion']."' is installed");
a1ece4d9
AE
327 }
328 }
329 }
330 }
331 unset($nodeData['requirements']);
332
b3782430
AE
333 // update package
334 if ($this->queue->packageID) {
335 $packageEditor = new PackageEditor(new Package($this->queue->packageID));
336 $packageEditor->update($nodeData);
337
a48477ee
MS
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));
b3782430 343
a93b160e
MS
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));
b3782430
AE
349 }
350 else {
11ade432
AE
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
11ade432
AE
360 // reload queue
361 $this->queue = new PackageInstallationQueue($this->queue->queueID);
362 $this->package = null;
363
aac1247e 364 if ($package->isApplication) {
838e315b 365 $host = str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost());
3dcfb497 366 $path = RouteHandler::getPath(array('acp'));
08d76680 367
11ade432
AE
368 // insert as application
369 ApplicationEditor::create(array(
3dcfb497
AE
370 'domainName' => $host,
371 'domainPath' => $path,
82542641
AE
372 'cookieDomain' => $host,
373 'cookiePath' => $path,
11ade432
AE
374 'packageID' => $package->packageID
375 ));
376 }
11ade432
AE
377 }
378
a48477ee
MS
379 // save excluded packages
380 if (count($this->getArchive()->getExcludedPackages())) {
381 $sql = "INSERT INTO wcf".WCF_N."_package_exclusion
a93b160e
MS
382 (packageID, excludedPackage, excludedPackageVersion)
383 VALUES (?, ?, ?)";
a48477ee
MS
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
a93b160e
MS
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) {
ba4e725b 400 $requirement = array_shift($possibleRequirements);
a93b160e
MS
401
402 $statement->execute(array($this->queue->packageID, $requirement['packageID']));
403 }
404 }
405
aac1247e 406 if ($this->getPackage()->isApplication && $this->getPackage()->package != 'com.woltlab.wcf' && $this->getAction() == 'install') {
11ade432
AE
407 if (empty($this->getPackage()->packageDir)) {
408 $document = $this->promptPackageDir();
6b63ee9a 409 if ($document !== null && $document instanceof FormDocument) {
11ade432
AE
410 $installationStep->setDocument($document);
411 }
412
413 $installationStep->setSplitNode();
414 }
415 }
11ade432
AE
416
417 return $installationStep;
418 }
419
5475a95d
MS
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();
5475a95d
MS
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
a2ad7897
MS
465 /**
466 * Saves a localized package info.
467 *
0ad90fc3
MW
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
a2ad7897
MS
472 * @param string $infoName
473 */
5475a95d 474 protected function saveLocalizedPackageInfo(PreparedStatement $statement, $languageList, LanguageCategory $languageCategory, Package $package, $infoName) {
a2ad7897
MS
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
164c2641 492 $defaultValue = $this->getArchive()->getPackageInfo('name');
a2ad7897
MS
493 }
494
5475a95d 495 foreach ($languageList as $language) {
a2ad7897
MS
496 $value = $defaultValue;
497 if (isset($infoValues[$language->languageCode])) {
498 $value = $infoValues[$language->languageCode];
499 }
a17de04e 500
a2ad7897
MS
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
11ade432
AE
511 /**
512 * Executes a package installation plugin.
9f959ced 513 *
ffdf6f14 514 * @param array step
11ade432
AE
515 * @return boolean
516 */
517 protected function executePIP(array $nodeData) {
ffdf6f14 518 $step = new PackageInstallationStep();
11ade432
AE
519
520 // fetch all pips associated with current PACKAGE_ID and include pips
521 // previously installed by current installation queue
fd2f91ae 522 $sql = "SELECT pluginName, className
b6ae7d04
MW
523 FROM wcf".WCF_N."_package_installation_plugin
524 WHERE pluginName = ?";
11ade432
AE
525 $statement = WCF::getDB()->prepareStatement($sql);
526 $statement->execute(array(
b6ae7d04 527 $nodeData['pip']
11ade432
AE
528 ));
529 $row = $statement->fetchArray();
530
b6ae7d04 531 // PIP is unknown
fd2f91ae 532 if (!$row || (strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
b6ae7d04 533 throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
11ade432
AE
534 }
535
536 // valdidate class definition
537 $className = $row['className'];
538 if (!class_exists($className)) {
4fe0b42b 539 throw new SystemException("unable to find class '".$className."'");
11ade432
AE
540 }
541
542 $plugin = new $className($this, $nodeData);
543
4d4a5125 544 if (!($plugin instanceof IPackageInstallationPlugin)) {
2dfb09ee 545 throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
11ade432
AE
546 }
547
4d4a5125 548 if ($plugin instanceof SQLPackageInstallationPlugin || $plugin instanceof ObjectTypePackageInstallationPlugin) {
82a7de7f
K
549 $this->requireRestructureVersionTables = true;
550 }
551
11ade432 552 // execute PIP
381495c4 553 $document = null;
11ade432 554 try {
ffdf6f14 555 $document = $plugin->{$this->action}();
11ade432
AE
556 }
557 catch (SplitNodeException $e) {
ffdf6f14 558 $step->setSplitNode();
11ade432
AE
559 }
560
ffdf6f14
AE
561 if ($document !== null && ($document instanceof FormDocument)) {
562 $step->setDocument($document);
563 $step->setSplitNode();
564 }
565
566 return $step;
11ade432
AE
567 }
568
cd0fbe9a
AE
569 /**
570 * Displays a list to select optional packages or installs selection.
571 *
572 * @param string $currentNode
573 * @param array $nodeData
0ad90fc3 574 * @return \wcf\system\package\PackageInstallationStep
cd0fbe9a 575 */
456008db 576 protected function selectOptionalPackages($currentNode, array $nodeData) {
492816d3
AE
577 $installationStep = new PackageInstallationStep();
578
579 $document = $this->promptOptionalPackages($nodeData);
6b63ee9a 580 if ($document !== null && $document instanceof FormDocument) {
492816d3 581 $installationStep->setDocument($document);
456008db
AE
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)) {
a0b7c762
AE
593 // ignore uninstallable packages
594 if (!$package['isInstallable']) {
595 continue;
596 }
597
456008db
AE
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 }
fe1e50bb
AE
618 else {
619 // remove archive
620 @unlink($package['archive']);
621 }
456008db
AE
622 }
623
624 // shift nodes
625 if ($shiftNodes) {
626 $this->nodeBuilder->shiftNodes('tempNode', $node);
627 }
492816d3 628 }
492816d3
AE
629
630 return $installationStep;
631 }
632
11ade432 633 /**
cd0fbe9a 634 * Extracts files from .tar(.gz) archive and installs them
a17de04e 635 *
9f959ced
MS
636 * @param string $targetDir
637 * @param string $sourceArchive
11ade432 638 * @param FileHandler $fileHandler
0ad90fc3 639 * @return \wcf\system\setup\Installer
11ade432
AE
640 */
641 public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
8f3fc897 642 return new Installer($targetDir, $sourceArchive, $fileHandler);
11ade432
AE
643 }
644
645 /**
646 * Returns current package.
9f959ced 647 *
0ad90fc3 648 * @return \wcf\data\package\Package
11ade432
AE
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 /**
aac1247e 659 * Prompts for a text input for package directory (applies for applications only)
9f959ced 660 *
0ad90fc3 661 * @return \wcf\system\form\FormDocument
11ade432
AE
662 */
663 protected function promptPackageDir() {
664 if (!PackageInstallationFormManager::findForm($this->queue, 'packageDir')) {
eb89d921 665
a99fcf96
MS
666 $container = new GroupFormElementContainer();
667 $packageDir = new TextInputFormElement($container);
11ade432
AE
668 $packageDir->setName('packageDir');
669 $packageDir->setLabel(WCF::getLanguage()->get('wcf.acp.package.packageDir.input'));
670
f1c5deb4 671 $defaultPath = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator(dirname(WCF_DIR)));
c00510a9
AE
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
11ade432
AE
684 $packageDir->setValue($defaultPath);
685 $container->appendChild($packageDir);
686
6b63ee9a 687 $document = new FormDocument('packageDir');
11ade432
AE
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();
72eca8cd
TD
696 $packageDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(FileUtil::unifyDirSeparator($document->getValue('packageDir'))));
697 if ($packageDir === '/') $packageDir = '';
11ade432
AE
698
699 if ($packageDir !== null) {
9b623913 700 // validate package dir
97021967 701 if (file_exists($packageDir . 'global.php')) {
9b623913 702 $document->setError('packageDir', WCF::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
9bdcaaaf 703 return $document;
9b623913
AE
704 }
705
706 // set package dir
11ade432
AE
707 $packageEditor = new PackageEditor($this->getPackage());
708 $packageEditor->update(array(
709 'packageDir' => FileUtil::getRelativePath(WCF_DIR, $packageDir)
710 ));
711
97021967
AE
712 // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
713 // faked and differ from the real filesystem path
1428f1ca
AE
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
f2024036 728 $documentRoot = str_replace($wcfDomainPath, '', FileUtil::unifyDirSeparator(WCF_DIR));
76fb30f2 729 $domainPath = str_replace($documentRoot, '', $packageDir);
c244fcc6
AE
730
731 // update application path
732 $application = new Application($this->getPackage()->packageID);
733 $applicationEditor = new ApplicationEditor($application);
734 $applicationEditor->update(array(
a66a4f9a
AE
735 'domainPath' => $domainPath,
736 'cookiePath' => $domainPath
c244fcc6
AE
737 ));
738
11ade432
AE
739 // create directory and set permissions
740 @mkdir($packageDir, 0777, true);
1232bce2 741 FileUtil::makeWritable($packageDir);
11ade432
AE
742 }
743
744 return null;
745 }
746 }
747
cd0fbe9a
AE
748 /**
749 * Prompts a selection of optional packages.
750 *
751 * @return mixed
752 */
492816d3
AE
753 protected function promptOptionalPackages(array $packages) {
754 if (!PackageInstallationFormManager::findForm($this->queue, 'optionalPackages')) {
a99fcf96 755 $container = new MultipleSelectionFormElementContainer();
eb89d921 756 $container->setName('optionalPackages');
6fb53650
MW
757 $container->setLabel(WCF::getLanguage()->get('wcf.acp.package.optionalPackages'));
758 $container->setDescription(WCF::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
492816d3
AE
759
760 foreach ($packages as $package) {
a99fcf96 761 $optionalPackage = new MultipleSelectionFormElement($container);
e1e35281 762 $optionalPackage->setName('optionalPackages');
492816d3
AE
763 $optionalPackage->setLabel($package['packageName']);
764 $optionalPackage->setValue($package['package']);
970ae117 765 $optionalPackage->setDescription($package['packageDescription']);
c13895fa 766 if (!$package['isInstallable']) {
a0b7c762
AE
767 $optionalPackage->setDisabledMessage(WCF::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements'));
768 }
492816d3
AE
769
770 $container->appendChild($optionalPackage);
771 }
772
6b63ee9a 773 $document = new FormDocument('optionalPackages');
492816d3
AE
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
456008db 783 return $document->getValue('optionalPackages');
492816d3
AE
784 }
785 }
786
11ade432
AE
787 /**
788 * Returns current package id.
9f959ced 789 *
11ade432
AE
790 * @return integer
791 */
792 public function getPackageID() {
793 return $this->queue->packageID;
794 }
795
796 /**
797 * Returns current package installation type.
9f959ced 798 *
11ade432
AE
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.
9f959ced 808 *
11ade432 809 * @param integer $parentQueueID
39bea7dd 810 * @param integer $processNo
11ade432
AE
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'])) {
6dcaf901 828 $url = LinkHandler::getInstance()->getLink('PackageList');
3e0e6b2c 829 HeaderUtil::redirect($url);
11ade432
AE
830 exit;
831 }
832 else {
ec45d5e8 833 $url = LinkHandler::getInstance()->getLink('PackageInstallationConfirm', array(), 'queueID='.$packageInstallation['queueID']);
3e0e6b2c 834 HeaderUtil::redirect($url);
11ade432
AE
835 exit;
836 }
837 }
838
11ade432
AE
839 /**
840 * Checks the package installation queue for outstanding entries.
9f959ced 841 *
11ade432
AE
842 * @return integer
843 */
844 public static function checkPackageInstallationQueue() {
845 $sql = "SELECT queueID
846 FROM wcf".WCF_N."_package_installation_queue
39bea7dd 847 WHERE userID = ?
11ade432
AE
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
11ade432
AE
862 /**
863 * Executes post-setup actions.
864 */
865 public function completeSetup() {
14bb2ca0
AE
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 }
11ade432 875
14bb2ca0
AE
876 // delete queues
877 $sql = "DELETE FROM wcf".WCF_N."_package_installation_queue
878 WHERE processNo = ?";
879 $statement = WCF::getDB()->prepareStatement($sql);
b953f892 880 $statement->execute(array($this->queue->processNo));
ca38121b 881
cfedc216
AE
882 // clear language files once whole installation is completed
883 LanguageEditor::deleteLanguageFiles();
11ade432 884
cfedc216 885 // reset all caches
b401cd0d 886 CacheHandler::getInstance()->flushAll();
11ade432
AE
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(
a2ad7897 896 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
11ade432
AE
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) {
838e315b 960 $function = mb_strtolower($function);
11ade432
AE
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
a17de04e 1003 */
11ade432
AE
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) {
838e315b 1010 $disabledFunction = mb_strtolower(StringUtil::trim($disabledFunction));
11ade432
AE
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
838e315b 1033 $value = mb_strtolower($value);
11ade432
AE
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
838e315b 1062 $lastCharacter = mb_substr($value, -1);
11ade432
AE
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 }
a99fcf96 1084
82a7de7f
K
1085 /*
1086 * Restructure version tables.
1087 */
1088 protected function restructureVersionTables() {
14d48464 1089 $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
82a7de7f
K
1090
1091 if (empty($objectTypes)) {
1092 return;
1093 }
1094
4d4a5125 1095 // base structure of version tables
82a7de7f 1096 $versionTableBaseColumns = array();
14d48464 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));
82a7de7f 1099 $versionTableBaseColumns[] = array('name' => 'versionUsername', 'data' => array('type' => 'VARCHAR', 'length' => 255));
14d48464 1100 $versionTableBaseColumns[] = array('name' => 'versionTime', 'data' => array('type' => 'INT', 'length' => 10));
a99fcf96 1101
e3fc42e8 1102 foreach ($objectTypes as $objectType) {
4457f91f 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 }
14d48464 1108 $baseTableColumns = WCF::getDB()->getEditor()->getColumns(call_user_func(array($objectType->className, 'getDatabaseTableName')));
2d63c13c 1109
14d48464 1110 // remove primary key from base table columns
df48b4e6 1111 foreach ($baseTableColumns as $key => $column) {
1112 if ($column['data']['key'] == 'PRIMARY') {
14d48464 1113 $baseTableColumns[$key]['data']['key'] = '';
1114 }
1115 $baseTableColumns[$key]['data']['autoIncrement'] = false;
1116 }
1117
4d4a5125 1118 // get structure of version table
14d48464 1119 $versionTableColumns = array();
1120 try {
1121 $versionTableColumns = WCF::getDB()->getEditor()->getColumns(call_user_func(array($objectType->className, 'getDatabaseVersionTableName')));
1122 }
1123 catch (\Exception $e) { }
82a7de7f 1124
5fe135db 1125 if (empty($versionTableColumns)) {
82a7de7f 1126 $columns = array_merge($versionTableBaseColumns, $baseTableColumns);
14d48464 1127 WCF::getDB()->getEditor()->createTable(call_user_func(array($objectType->className, 'getDatabaseVersionTableName')), $columns);
2d63c13c 1128
4457f91f 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 ));
82a7de7f
K
1138 }
1139 else {
ed787005 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 }
2d63c13c 1150
4d4a5125 1151 // check garbage columns in versioned table
82a7de7f 1152 foreach ($versionTableColumns as $columnData) {
ed787005 1153 if (!in_array($columnData['name'], $baseTableColumnNames) && !in_array($columnData['name'], $versionTableBaseColumnNames)) {
4d4a5125 1154 // delete column
14d48464 1155 WCF::getDB()->getEditor()->dropColumn(call_user_func(array($objectType->className, 'getDatabaseVersionTableName')), $columnData['name']);
82a7de7f
K
1156 }
1157 }
1158
4d4a5125 1159 // check new columns for versioned table
82a7de7f 1160 foreach ($baseTableColumns as $columnData) {
ed787005 1161 if (!in_array($columnData['name'], $versionTableColumnNames)) {
4d4a5125 1162 // add colum
14d48464 1163 WCF::getDB()->getEditor()->addColumn(call_user_func(array($objectType->className, 'getDatabaseVersionTableName')), $columnData['name'], $columnData['data']);
82a7de7f
K
1164 }
1165 }
1166 }
1167 }
a99fcf96 1168 }
11ade432 1169}