Merge pull request #5987 from WoltLab/acp-dahsboard-box-hight
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationNodeBuilder.class.php
CommitLineData
11ade432 1<?php
a9229942 2
11ade432 3namespace wcf\system\package;
a9229942 4
cfedc216 5use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
8d84809f 6use wcf\data\package\installation\queue\PackageInstallationQueueList;
abfda06b 7use wcf\data\package\Package;
d54c09a0 8use wcf\system\exception\SystemException;
11ade432 9use wcf\system\WCF;
cfedc216 10use wcf\util\FileUtil;
11ade432
AE
11use wcf\util\StringUtil;
12
13/**
a17de04e 14 * Creates a logical node-based installation tree.
a9229942
TD
15 *
16 * @author Alexander Ebert
17 * @copyright 2001-2019 WoltLab GmbH
18 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
11ade432 19 */
a9229942
TD
20class PackageInstallationNodeBuilder
21{
22 /**
23 * true if current node is empty
24 * @var bool
25 */
26 public $emptyNode = true;
27
28 /**
29 * active package installation dispatcher
30 * @var PackageInstallationDispatcher
31 */
32 public $installation;
33
34 /**
35 * current installation node
36 * @var string
37 */
38 public $node = '';
39
40 /**
41 * current parent installation node
42 * @var string
43 */
44 public $parentNode = '';
45
46 /**
47 * list of requirements to be checked before package installation
48 * @var mixed[][]
49 */
50 public $requirements = [];
51
52 /**
53 * current sequence number within one node
54 * @var int
55 */
56 public $sequenceNo = 0;
57
58 /**
59 * list of packages about to be installed
60 * @var string[]
61 */
5fa21ab8 62 private static $pendingPackages = [];
a9229942
TD
63
64 /**
65 * Creates a new instance of PackageInstallationNodeBuilder
66 *
67 * @param PackageInstallationDispatcher $installation
68 */
69 public function __construct(PackageInstallationDispatcher $installation)
70 {
71 $this->installation = $installation;
72 }
73
74 /**
75 * Sets parent node.
a9229942 76 */
abcd8c26 77 public function setParentNode(string $parentNode)
a9229942
TD
78 {
79 $this->parentNode = $parentNode;
80 }
81
82 /**
83 * Builds nodes for current installation queue.
84 */
85 public function buildNodes()
86 {
cbc112dc 87 $manifest = new PackageManifest($this->installation->getArchive());
1ddd4486
TD
88 $auditLogger = new AuditLogger();
89
90 $package = $this->installation->getPackage();
91 switch ($this->installation->getAction()) {
92 case 'install':
2ae98f90
TD
93 $currentPackageVersion = null;
94
1ddd4486
TD
95 $auditLogger->log(
96 <<<EOT
97 Building installation nodes
98 ===========================
99 Process#: {$this->installation->queue->processNo}
100 Queue#: {$this->installation->queue->queueID}
101 Parent Queue#: {$this->installation->queue->parentQueueID}
102 Parent Node: {$this->parentNode}
103
104 Archive: {$this->installation->getArchive()->getArchive()}
105 Manifest ({$manifest->getHash()}):
106 ---
107 {$manifest->getManifest()}
108 ---
109 EOT
110 );
111 break;
112 case 'update':
206061fb
TD
113 $currentPackageVersion = self::$pendingPackages[$package->package] ?? $package->packageVersion;
114
1ddd4486
TD
115 $auditLogger->log(
116 <<<EOT
117 Building update nodes
118 =====================
119 Process#: {$this->installation->queue->processNo}
120 Queue#: {$this->installation->queue->queueID}
121 Parent Queue#: {$this->installation->queue->parentQueueID}
122 Parent Node: {$this->parentNode}
123
206061fb
TD
124 Package: {$package->package} ({$currentPackageVersion})
125
1ddd4486
TD
126 Archive: {$this->installation->getArchive()->getArchive()}
127 Manifest ({$manifest->getHash()}):
128 ---
129 {$manifest->getManifest()}
130 ---
131 EOT
132 );
133 break;
134 }
cbc112dc 135
a9229942
TD
136 // required packages
137 $this->buildRequirementNodes();
138
2ae98f90 139 $this->buildStartMarkerNode($currentPackageVersion);
3a80c99b 140
a9229942 141 // install package itself
858fc81f 142 if ($this->installation->getAction() == 'install') {
a9229942
TD
143 $this->buildPackageNode();
144 }
145
146 // package installation plugins
206061fb
TD
147 switch ($this->installation->getAction()) {
148 case 'install':
149 $instructions = $this->installation->getArchive()->getInstallInstructions();
150
151 break;
152 case 'update':
153 $instructions = $this->installation->getArchive()->getUpdateInstructionsFor($currentPackageVersion) ?? [];
154
155 break;
156 default:
157 throw new \LogicException('Unreachable');
158 }
159
160 $this->buildPluginNodes($instructions);
a9229942 161
2406351b
TD
162 // register package version
163 self::$pendingPackages[$this->installation->getArchive()->getPackageInfo('name')] = $this->installation->getArchive()->getPackageInfo('version');
164
a9229942 165 // optional packages (ignored on update)
858fc81f 166 if ($this->installation->getAction() == 'install') {
a9229942
TD
167 $this->buildOptionalNodes();
168 }
169
858fc81f 170 if ($this->installation->getAction() == 'update') {
a9229942
TD
171 $this->buildPackageNode();
172 }
173
3a80c99b
TD
174 $this->buildEndMarkerNode();
175
1ddd4486 176 $auditLogger->log(
cbc112dc
TD
177 <<<EOT
178 Finished building nodes
179 =======================
180 Process#: {$this->installation->queue->processNo}
181 Queue#: {$this->installation->queue->queueID}
182 Final Node: {$this->node}
183 EOT
184 );
185
a9229942
TD
186 // child queues
187 $this->buildChildQueues();
188 }
189
190 /**
191 * Returns the succeeding node.
a9229942 192 */
abcd8c26 193 public function getNextNode(string $parentNode = ''): string
a9229942
TD
194 {
195 $sql = "SELECT node
0860deb6 196 FROM wcf1_package_installation_node
a9229942
TD
197 WHERE processNo = ?
198 AND parentNode = ?";
0860deb6 199 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
200 $statement->execute([
201 $this->installation->queue->processNo,
202 $parentNode,
203 ]);
204 $row = $statement->fetchArray();
205
206 if (!$row) {
207 return '';
208 }
209
210 return $row['node'];
211 }
212
213 /**
214 * Returns package name associated with given queue id.
a9229942 215 */
abcd8c26 216 public function getPackageNameByQueue(int $queueID): string
a9229942
TD
217 {
218 $sql = "SELECT packageName
0860deb6 219 FROM wcf1_package_installation_queue
a9229942 220 WHERE queueID = ?";
0860deb6 221 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
222 $statement->execute([$queueID]);
223 $row = $statement->fetchArray();
224
225 if (!$row) {
226 return '';
227 }
228
229 return $row['packageName'];
230 }
231
232 /**
233 * Returns installation type by queue id.
a9229942 234 */
abcd8c26 235 public function getInstallationTypeByQueue(int $queueID): string
a9229942
TD
236 {
237 $sql = "SELECT action
0860deb6 238 FROM wcf1_package_installation_queue
a9229942 239 WHERE queueID = ?";
0860deb6 240 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
241 $statement->execute([$queueID]);
242 $row = $statement->fetchArray();
243
244 return $row['action'];
245 }
246
247 /**
248 * Returns data for current node.
249 *
a9229942
TD
250 * @return array
251 */
abcd8c26 252 public function getNodeData(string $node)
a9229942
TD
253 {
254 $sql = "SELECT nodeType, nodeData, sequenceNo
0860deb6 255 FROM wcf1_package_installation_node
a9229942
TD
256 WHERE processNo = ?
257 AND node = ?
258 ORDER BY sequenceNo ASC";
0860deb6 259 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
260 $statement->execute([
261 $this->installation->queue->processNo,
262 $node,
263 ]);
264
265 return $statement->fetchAll(\PDO::FETCH_ASSOC);
266 }
267
268 /**
269 * Marks a node as completed.
a9229942 270 */
abcd8c26 271 public function completeNode(string $node)
a9229942 272 {
0860deb6 273 $sql = "UPDATE wcf1_package_installation_node
a9229942
TD
274 SET done = 1
275 WHERE processNo = ?
276 AND node = ?";
0860deb6 277 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
278 $statement->execute([
279 $this->installation->queue->processNo,
280 $node,
281 ]);
282 }
283
284 /**
285 * Removes all nodes associated with queue's process no.
286 *
287 * CAUTION: This method SHOULD NOT be called within the installation process!
288 */
289 public function purgeNodes()
290 {
0860deb6 291 $sql = "DELETE FROM wcf1_package_installation_node
a9229942 292 WHERE processNo = ?";
0860deb6 293 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
294 $statement->execute([
295 $this->installation->queue->processNo,
296 ]);
297
0860deb6 298 $sql = "DELETE FROM wcf1_package_installation_form
a9229942 299 WHERE queueID = ?";
0860deb6 300 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
301 $statement->execute([
302 $this->installation->queue->queueID,
303 ]);
304 }
305
306 /**
307 * Calculates current setup process.
308 *
309 * @param string $node
310 * @return int
311 */
312 public function calculateProgress($node)
313 {
314 $progress = [
315 'done' => 0,
316 'outstanding' => 0,
317 ];
318
319 $sql = "SELECT done
0860deb6 320 FROM wcf1_package_installation_node
a9229942 321 WHERE processNo = ?";
0860deb6 322 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
323 $statement->execute([
324 $this->installation->queue->processNo,
325 ]);
326 while ($row = $statement->fetchArray()) {
327 if ($row['done']) {
328 $progress['done']++;
329 } else {
330 $progress['outstanding']++;
331 }
332 }
333
334 if (!$progress['done']) {
335 return 0;
336 } elseif (!$progress['outstanding']) {
337 return 100;
338 } else {
339 $total = $progress['done'] + $progress['outstanding'];
340
341 return \round(($progress['done'] / $total) * 100);
342 }
343 }
344
345 /**
346 * Duplicates a node by re-inserting it and moving all descendants into a new tree.
347 *
348 * @param string $node
349 * @param int $sequenceNo
350 */
351 public function cloneNode($node, $sequenceNo)
352 {
353 $newNode = $this->getToken();
354
355 // update descendants
6cb37f33 356 $this->shiftNodes($node, $newNode);
a9229942
TD
357
358 // create a copy of current node (prevents empty nodes)
359 $sql = "SELECT nodeType, nodeData, done
0860deb6 360 FROM wcf1_package_installation_node
a9229942
TD
361 WHERE node = ?
362 AND processNo = ?
363 AND sequenceNo = ?";
0860deb6 364 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
365 $statement->execute([
366 $node,
367 $this->installation->queue->processNo,
368 $sequenceNo,
369 ]);
370 $row = $statement->fetchArray();
371
0860deb6 372 $sql = "INSERT INTO wcf1_package_installation_node
a9229942
TD
373 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData, done)
374 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
0860deb6 375 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
376 $statement->execute([
377 $this->installation->queue->queueID,
378 $this->installation->queue->processNo,
242b90e8 379 $sequenceNo,
a9229942
TD
380 $newNode,
381 $node,
382 $row['nodeType'],
383 $row['nodeData'],
384 $row['done'],
385 ]);
386
387 // move other child-nodes greater than $sequenceNo into new node
0860deb6 388 $sql = "UPDATE wcf1_package_installation_node
a9229942 389 SET parentNode = ?,
242b90e8 390 node = ?
a9229942
TD
391 WHERE node = ?
392 AND processNo = ?
393 AND sequenceNo > ?";
0860deb6 394 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
395 $statement->execute([
396 $node,
397 $newNode,
a9229942
TD
398 $node,
399 $this->installation->queue->processNo,
400 $sequenceNo,
401 ]);
402 }
a9229942
TD
403
404 /**
405 * Shifts nodes to allow dynamic inserts at runtime.
a9229942 406 */
832ecbeb 407 public function shiftNodes(string $oldParentNode, string $newParentNode)
a9229942 408 {
0860deb6 409 $sql = "UPDATE wcf1_package_installation_node
a9229942
TD
410 SET parentNode = ?
411 WHERE parentNode = ?
412 AND processNo = ?";
0860deb6 413 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
414 $statement->execute([
415 $newParentNode,
416 $oldParentNode,
417 $this->installation->queue->processNo,
418 ]);
419 }
420
2ae98f90 421 protected function buildStartMarkerNode(?string $currentPackageVersion)
3a80c99b
TD
422 {
423 if (!empty($this->node)) {
424 $this->parentNode = $this->node;
425 $this->sequenceNo = 0;
426 }
427 $this->node = $this->getToken();
428
429 $sql = "INSERT INTO wcf1_package_installation_node
430 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
431 VALUES (?, ?, ?, ?, ?, ?, ?)";
432 $statement = WCF::getDB()->prepare($sql);
433 $statement->execute([
434 $this->installation->queue->queueID,
435 $this->installation->queue->processNo,
436 $this->sequenceNo,
437 $this->node,
438 $this->parentNode,
439 'start',
2ae98f90
TD
440 \serialize([
441 'currentPackageVersion' => $currentPackageVersion,
442 ]),
3a80c99b
TD
443 ]);
444 }
445
446 protected function buildEndMarkerNode()
447 {
448 if (!empty($this->node)) {
449 $this->parentNode = $this->node;
450 $this->sequenceNo = 0;
451 }
452 $this->node = $this->getToken();
453
454 $sql = "INSERT INTO wcf1_package_installation_node
455 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
456 VALUES (?, ?, ?, ?, ?, ?, ?)";
457 $statement = WCF::getDB()->prepare($sql);
458 $statement->execute([
459 $this->installation->queue->queueID,
460 $this->installation->queue->processNo,
461 $this->sequenceNo,
462 $this->node,
463 $this->parentNode,
464 'end',
465 \serialize([]),
466 ]);
467 }
468
a9229942
TD
469 /**
470 * Builds package node used to install the package itself.
471 */
472 protected function buildPackageNode()
473 {
474 if (!empty($this->node)) {
475 $this->parentNode = $this->node;
476 $this->sequenceNo = 0;
477 }
478
479 $this->node = $this->getToken();
480
0860deb6 481 $sql = "INSERT INTO wcf1_package_installation_node
a9229942
TD
482 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
483 VALUES (?, ?, ?, ?, ?, ?, ?)";
0860deb6 484 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
485 $statement->execute([
486 $this->installation->queue->queueID,
487 $this->installation->queue->processNo,
488 $this->sequenceNo,
489 $this->node,
490 $this->parentNode,
491 'package',
492 \serialize([
493 'package' => $this->installation->getArchive()->getPackageInfo('name'),
494 'packageName' => $this->installation->getArchive()->getLocalizedPackageInfo('packageName'),
495 'packageDescription' => $this->installation->getArchive()->getLocalizedPackageInfo('packageDescription'),
496 'packageVersion' => $this->installation->getArchive()->getPackageInfo('version'),
497 'packageDate' => $this->installation->getArchive()->getPackageInfo('date'),
498 'packageURL' => $this->installation->getArchive()->getPackageInfo('packageURL'),
499 'isApplication' => $this->installation->getArchive()->getPackageInfo('isApplication'),
500 'author' => $this->installation->getArchive()->getAuthorInfo('author'),
11cc34c2 501 'authorURL' => $this->installation->getArchive()->getAuthorInfo('authorURL') ?: '',
a9229942
TD
502 'installDate' => TIME_NOW,
503 'updateDate' => TIME_NOW,
504 'requirements' => $this->requirements,
505 'applicationDirectory' => $this->installation->getArchive()->getPackageInfo('applicationDirectory') ?: '',
506 ]),
507 ]);
508 }
509
510 /**
511 * Builds nodes for required packages, whereas each has it own node.
512 *
513 * @return string
514 * @throws SystemException
515 */
516 protected function buildRequirementNodes()
517 {
518 $queue = $this->installation->queue;
519
520 // handle requirements
521 $requiredPackages = $this->installation->getArchive()->getOpenRequirements();
522 foreach ($requiredPackages as $packageName => $package) {
523 if (!isset($package['file'])) {
524 if (
944f69cf
TD
525 isset(self::$pendingPackages[$packageName])
526 && (
527 !isset($package['minversion'])
528 || Package::compareVersion(self::$pendingPackages[$packageName], $package['minversion']) >= 0
529 )
a9229942
TD
530 ) {
531 // the package will already be installed and no
532 // minversion is given or the package which will be
533 // installed satisfies the minversion, thus we can
534 // ignore this requirement
535 continue;
536 }
537
538 // requirements will be checked once package is about to be installed
539 $this->requirements[$packageName] = [
540 'minVersion' => $package['minversion'] ?? '',
541 'packageID' => $package['packageID'],
542 ];
543
544 continue;
545 }
546
547 if ($this->node == '' && !empty($this->parentNode)) {
548 $this->node = $this->parentNode;
549 }
550
551 // extract package
552 $index = $this->installation->getArchive()->getTar()->getIndexByFilename($package['file']);
553 if ($index === false) {
554 // workaround for WCFSetup
555 if (!PACKAGE_ID && $packageName == 'com.woltlab.wcf') {
556 continue;
557 }
558
559 throw new SystemException("Unable to find required package '" . $package['file'] . "' within archive of package '" . $this->installation->queue->package . "'.");
560 }
561
562 $fileName = FileUtil::getTemporaryFilename(
563 'package_',
564 \preg_replace('!^.*(?=\.(?:tar\.gz|tgz|tar)$)!i', '', \basename($package['file']))
565 );
566 $this->installation->getArchive()->getTar()->extract($index, $fileName);
567
568 // get archive data
569 $archive = new PackageArchive($fileName);
570 $archive->openArchive();
571
572 // check if delivered package has correct identifier
573 if ($archive->getPackageInfo('name') != $packageName) {
574 throw new SystemException("Invalid package file delivered for '" . $packageName . "' requirement of package '" . $this->installation->getArchive()->getPackageInfo('name') . "' (delivered package: '" . $archive->getPackageInfo('name') . "').");
575 }
576
577 // check if delivered version satisfies minversion
578 if (
579 isset($package['minversion']) && Package::compareVersion(
580 $package['minversion'],
581 $archive->getPackageInfo('version')
582 ) > 0
583 ) {
584 throw new SystemException("Package '" . $this->installation->getArchive()->getPackageInfo('name') . "' requires package '" . $packageName . "' at least in version " . $package['minversion'] . ", but only delivers version " . $archive->getPackageInfo('version') . ".");
585 }
586
587 // get package id
588 $sql = "SELECT packageID
0860deb6 589 FROM wcf1_package
a9229942 590 WHERE package = ?";
0860deb6 591 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
592 $statement->execute([$archive->getPackageInfo('name')]);
593 $row = $statement->fetchArray();
594 $packageID = ($row === false) ? null : $row['packageID'];
595
596 // check if package will already be installed
597 if (isset(self::$pendingPackages[$packageName])) {
598 if (
599 Package::compareVersion(
600 self::$pendingPackages[$packageName],
601 $archive->getPackageInfo('version')
602 ) >= 0
603 ) {
604 // the version to be installed satisfies the required version
605 continue;
606 } else {
607 // the new delivered required version of the package has a
608 // higher version number, thus update/replace the existing
609 // package installation queue
610 }
611 }
612
613 if ($archive->getPackageInfo('name') === 'com.woltlab.wcf') {
614 WCF::checkWritability();
615 }
616
617 // create new queue
618 $queue = PackageInstallationQueueEditor::create([
619 'parentQueueID' => $queue->queueID,
620 'processNo' => $queue->processNo,
621 'userID' => WCF::getUser()->userID,
622 'package' => $archive->getPackageInfo('name'),
623 'packageID' => $packageID,
624 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
625 'archive' => $fileName,
626 'action' => $packageID ? 'update' : 'install',
627 ]);
628
a9229942
TD
629 // spawn nodes
630 $installation = new PackageInstallationDispatcher($queue);
631 $installation->nodeBuilder->setParentNode($this->node);
632 $installation->nodeBuilder->buildNodes();
633 $this->node = $installation->nodeBuilder->getCurrentNode();
634 }
635 }
636
637 /**
638 * Returns current node
a9229942 639 */
abcd8c26 640 public function getCurrentNode(): string
a9229942
TD
641 {
642 return $this->node;
643 }
644
645 /**
646 * Builds package installation plugin nodes, whereas pips could be grouped within
647 * one node, differ from each by nothing but the sequence number.
648 *
649 * @return string
650 */
206061fb 651 protected function buildPluginNodes(array $instructions)
a9229942 652 {
a9229942 653 $count = \count($instructions);
0c10c799
TD
654
655 if ($count === 0) {
656 // Abort if an empty list of instructions is received. This most likely indicates that
657 // the update instructions have been erroneously discarded.
658 throw new \Exception('Received an empty list of instructions.');
659 }
660
ade27213
TD
661 if (!empty($this->node)) {
662 $this->parentNode = $this->node;
663 $this->sequenceNo = 0;
664 }
665
666 $this->node = $this->getToken();
667
ade27213
TD
668 $this->emptyNode = true;
669
a9229942 670 $i = 0;
d4cccb2f 671 $pluginNodes = [];
a9229942
TD
672 foreach ($instructions as $pip) {
673 $i++;
674
675 if (isset($pip['attributes']['run']) && ($pip['attributes']['run'] == 'standalone')) {
676 // move into a new node unless current one is empty
677 if (!$this->emptyNode) {
678 $this->parentNode = $this->node;
679 $this->node = $this->getToken();
680 $this->sequenceNo = 0;
681 }
682 $pluginNodes[] = [
683 'data' => $pip,
684 'node' => $this->node,
685 'parentNode' => $this->parentNode,
686 'sequenceNo' => $this->sequenceNo,
687 ];
688
689 // create a new node for following PIPs, unless it is the last one
690 if ($i < $count) {
691 $this->parentNode = $this->node;
692 $this->node = $this->getToken();
693 $this->sequenceNo = 0;
694
695 $this->emptyNode = true;
696 }
697 } else {
698 $this->sequenceNo++;
699
700 $pluginNodes[] = [
701 'data' => $pip,
702 'node' => $this->node,
703 'parentNode' => $this->parentNode,
704 'sequenceNo' => $this->sequenceNo,
705 ];
706
707 $this->emptyNode = false;
708 }
709 }
710
f24dd47e 711 \assert($pluginNodes !== []);
a9229942 712
126361be 713 $sql = "INSERT INTO wcf1_package_installation_node
f24dd47e
TD
714 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
715 VALUES (?, ?, ?, ?, ?, ?, ?)";
126361be 716 $statement = WCF::getDB()->prepare($sql);
f24dd47e
TD
717
718 foreach ($pluginNodes as $nodeData) {
719 $statement->execute([
720 $this->installation->queue->queueID,
721 $this->installation->queue->processNo,
722 $nodeData['sequenceNo'],
723 $nodeData['node'],
724 $nodeData['parentNode'],
725 'pip',
726 \serialize($nodeData['data']),
727 ]);
a9229942
TD
728 }
729 }
730
731 /**
732 * Builds nodes for optional packages, whereas each package exists within
733 * one node with the same parent node, separated by sequence no (which does
734 * not really matter at this point).
735 */
736 protected function buildOptionalNodes()
737 {
738 $packages = [];
739
740 $optionalPackages = $this->installation->getArchive()->getOptionals();
741 foreach ($optionalPackages as $package) {
742 // check if already installed
743 if (Package::isAlreadyInstalled($package['name'])) {
744 continue;
745 }
746
747 // extract package
748 $index = $this->installation->getArchive()->getTar()->getIndexByFilename($package['file']);
749 if ($index === false) {
750 throw new SystemException("Unable to find required package '" . $package['file'] . "' within archive.");
751 }
752
753 $fileName = FileUtil::getTemporaryFilename(
754 'package_',
755 \preg_replace('!^.*(?=\.(?:tar\.gz|tgz|tar)$)!i', '', \basename($package['file']))
756 );
757 $this->installation->getArchive()->getTar()->extract($index, $fileName);
758
759 // get archive data
760 $archive = new PackageArchive($fileName);
761 $archive->openArchive();
762
763 // check if all requirements are met
764 $isInstallable = true;
765 foreach ($archive->getOpenRequirements() as $packageName => $requiredPackage) {
766 if (!isset($requiredPackage['file'])) {
767 // requirement is neither installed nor shipped, check if it is about to be installed
768 if (!isset(self::$pendingPackages[$packageName])) {
769 $isInstallable = false;
770 break;
771 }
772 }
773 }
774
775 // check for exclusions
776 $excludedPackages = $archive->getConflictedExcludedPackages();
777 if (!empty($excludedPackages)) {
778 $isInstallable = false;
779 }
780
781 $excludingPackages = $archive->getConflictedExcludingPackages();
782 if (!empty($excludingPackages)) {
783 $isInstallable = false;
784 }
785
786 $packages[] = [
787 'archive' => $fileName,
788 'isInstallable' => $isInstallable,
789 'package' => $archive->getPackageInfo('name'),
790 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
791 'packageDescription' => $archive->getLocalizedPackageInfo('packageDescription'),
792 'selected' => 0,
793 ];
794
795 self::$pendingPackages[$archive->getPackageInfo('name')] = $archive->getPackageInfo('version');
796 }
797
798 if (!empty($packages)) {
799 $this->parentNode = $this->node;
800 $this->node = $this->getToken();
801 $this->sequenceNo = 0;
802
0860deb6 803 $sql = "INSERT INTO wcf1_package_installation_node
a9229942
TD
804 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
805 VALUES (?, ?, ?, ?, ?, ?, ?)";
0860deb6 806 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
807 $statement->execute([
808 $this->installation->queue->queueID,
809 $this->installation->queue->processNo,
810 $this->sequenceNo,
811 $this->node,
812 $this->parentNode,
813 'optionalPackages',
814 \serialize($packages),
815 ]);
816 }
817 }
818
819 /**
820 * Recursively build nodes for child queues.
821 */
822 protected function buildChildQueues()
823 {
824 $queueList = new PackageInstallationQueueList();
825 $queueList->getConditionBuilder()->add(
826 "package_installation_queue.parentQueueID = ?",
827 [$this->installation->queue->queueID]
828 );
94d12f75
MS
829 $queueList->getConditionBuilder()->add("package_installation_queue.queueID NOT IN (
830 SELECT queueID
831 FROM wcf" . WCF_N . "_package_installation_node
832 )");
a9229942
TD
833 $queueList->readObjects();
834
835 foreach ($queueList as $queue) {
836 $installation = new PackageInstallationDispatcher($queue);
837
838 // work-around for iterative package updates
72ad0733 839 if (isset(self::$pendingPackages[$queue->package])) {
a9229942 840 $installation->setPreviousPackage([
72ad0733
TD
841 'package' => $queue->package,
842 'packageVersion' => self::$pendingPackages[$queue->package],
a9229942
TD
843 ]);
844 }
845
846 $installation->nodeBuilder->setParentNode($this->node);
847 $installation->nodeBuilder->buildNodes();
848 $this->node = $installation->nodeBuilder->getCurrentNode();
849 }
850 }
851
852 /**
853 * Returns a short SHA1-hash.
a9229942 854 */
abcd8c26 855 protected function getToken(): string
a9229942
TD
856 {
857 return \mb_substr(StringUtil::getRandomID(), 0, 8);
858 }
859
860 /**
861 * Returns queue id based upon current node.
862 *
a9229942
TD
863 * @return int|null
864 */
abcd8c26 865 public function getQueueByNode(int $processNo, string $node)
a9229942
TD
866 {
867 $sql = "SELECT queueID
0860deb6 868 FROM wcf1_package_installation_node
a9229942
TD
869 WHERE processNo = ?
870 AND node = ?";
0860deb6 871 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
872 $statement->execute([
873 $processNo,
874 $node,
875 ]);
876 $row = $statement->fetchArray();
877
878 if ($row === false) {
879 // PHP <7.4 _silently_ returns `null` when attempting to read an array index
880 // when the source value equals `false`.
c0b28aa2 881 return null;
a9229942
TD
882 }
883
884 return $row['queueID'];
885 }
11ade432 886}