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