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