Update disclaimer text
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationNodeBuilder.class.php
1 <?php
2
3 namespace wcf\system\package;
4
5 use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
6 use wcf\data\package\installation\queue\PackageInstallationQueueList;
7 use wcf\data\package\Package;
8 use wcf\system\exception\SystemException;
9 use wcf\system\WCF;
10 use wcf\util\FileUtil;
11 use wcf\util\StringUtil;
12
13 /**
14 * Creates a logical node-based installation tree.
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>
19 */
20 class 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 */
62 private static $pendingPackages = [];
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.
76 */
77 public function setParentNode(string $parentNode)
78 {
79 $this->parentNode = $parentNode;
80 }
81
82 /**
83 * Builds nodes for current installation queue.
84 */
85 public function buildNodes()
86 {
87 $manifest = new PackageManifest($this->installation->getArchive());
88 $auditLogger = new AuditLogger();
89
90 $package = $this->installation->getPackage();
91 switch ($this->installation->getAction()) {
92 case 'install':
93 $currentPackageVersion = null;
94
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':
113 $currentPackageVersion = self::$pendingPackages[$package->package] ?? $package->packageVersion;
114
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
124 Package: {$package->package} ({$currentPackageVersion})
125
126 Archive: {$this->installation->getArchive()->getArchive()}
127 Manifest ({$manifest->getHash()}):
128 ---
129 {$manifest->getManifest()}
130 ---
131 EOT
132 );
133 break;
134 }
135
136 // required packages
137 $this->buildRequirementNodes();
138
139 $this->buildStartMarkerNode($currentPackageVersion);
140
141 // install package itself
142 if ($this->installation->getAction() == 'install') {
143 $this->buildPackageNode();
144 }
145
146 // package installation plugins
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);
161
162 // register package version
163 self::$pendingPackages[$this->installation->getArchive()->getPackageInfo('name')] = $this->installation->getArchive()->getPackageInfo('version');
164
165 // optional packages (ignored on update)
166 if ($this->installation->getAction() == 'install') {
167 $this->buildOptionalNodes();
168 }
169
170 if ($this->installation->getAction() == 'update') {
171 $this->buildPackageNode();
172 }
173
174 $this->buildEndMarkerNode();
175
176 $auditLogger->log(
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
186 // child queues
187 $this->buildChildQueues();
188 }
189
190 /**
191 * Returns the succeeding node.
192 */
193 public function getNextNode(string $parentNode = ''): string
194 {
195 $sql = "SELECT node
196 FROM wcf1_package_installation_node
197 WHERE processNo = ?
198 AND parentNode = ?";
199 $statement = WCF::getDB()->prepare($sql);
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.
215 */
216 public function getPackageNameByQueue(int $queueID): string
217 {
218 $sql = "SELECT packageName
219 FROM wcf1_package_installation_queue
220 WHERE queueID = ?";
221 $statement = WCF::getDB()->prepare($sql);
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.
234 */
235 public function getInstallationTypeByQueue(int $queueID): string
236 {
237 $sql = "SELECT action
238 FROM wcf1_package_installation_queue
239 WHERE queueID = ?";
240 $statement = WCF::getDB()->prepare($sql);
241 $statement->execute([$queueID]);
242 $row = $statement->fetchArray();
243
244 return $row['action'];
245 }
246
247 /**
248 * Returns data for current node.
249 *
250 * @return array
251 */
252 public function getNodeData(string $node)
253 {
254 $sql = "SELECT nodeType, nodeData, sequenceNo
255 FROM wcf1_package_installation_node
256 WHERE processNo = ?
257 AND node = ?
258 ORDER BY sequenceNo ASC";
259 $statement = WCF::getDB()->prepare($sql);
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.
270 */
271 public function completeNode(string $node)
272 {
273 $sql = "UPDATE wcf1_package_installation_node
274 SET done = 1
275 WHERE processNo = ?
276 AND node = ?";
277 $statement = WCF::getDB()->prepare($sql);
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 {
291 $sql = "DELETE FROM wcf1_package_installation_node
292 WHERE processNo = ?";
293 $statement = WCF::getDB()->prepare($sql);
294 $statement->execute([
295 $this->installation->queue->processNo,
296 ]);
297
298 $sql = "DELETE FROM wcf1_package_installation_form
299 WHERE queueID = ?";
300 $statement = WCF::getDB()->prepare($sql);
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
320 FROM wcf1_package_installation_node
321 WHERE processNo = ?";
322 $statement = WCF::getDB()->prepare($sql);
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
356 $this->shiftNodes($node, $newNode);
357
358 // create a copy of current node (prevents empty nodes)
359 $sql = "SELECT nodeType, nodeData, done
360 FROM wcf1_package_installation_node
361 WHERE node = ?
362 AND processNo = ?
363 AND sequenceNo = ?";
364 $statement = WCF::getDB()->prepare($sql);
365 $statement->execute([
366 $node,
367 $this->installation->queue->processNo,
368 $sequenceNo,
369 ]);
370 $row = $statement->fetchArray();
371
372 $sql = "INSERT INTO wcf1_package_installation_node
373 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData, done)
374 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
375 $statement = WCF::getDB()->prepare($sql);
376 $statement->execute([
377 $this->installation->queue->queueID,
378 $this->installation->queue->processNo,
379 $sequenceNo,
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
388 $sql = "UPDATE wcf1_package_installation_node
389 SET parentNode = ?,
390 node = ?
391 WHERE node = ?
392 AND processNo = ?
393 AND sequenceNo > ?";
394 $statement = WCF::getDB()->prepare($sql);
395 $statement->execute([
396 $node,
397 $newNode,
398 $node,
399 $this->installation->queue->processNo,
400 $sequenceNo,
401 ]);
402 }
403
404 /**
405 * Shifts nodes to allow dynamic inserts at runtime.
406 */
407 public function shiftNodes(string $oldParentNode, string $newParentNode)
408 {
409 $sql = "UPDATE wcf1_package_installation_node
410 SET parentNode = ?
411 WHERE parentNode = ?
412 AND processNo = ?";
413 $statement = WCF::getDB()->prepare($sql);
414 $statement->execute([
415 $newParentNode,
416 $oldParentNode,
417 $this->installation->queue->processNo,
418 ]);
419 }
420
421 protected function buildStartMarkerNode(?string $currentPackageVersion)
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',
440 \serialize([
441 'currentPackageVersion' => $currentPackageVersion,
442 ]),
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
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
481 $sql = "INSERT INTO wcf1_package_installation_node
482 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
483 VALUES (?, ?, ?, ?, ?, ?, ?)";
484 $statement = WCF::getDB()->prepare($sql);
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'),
501 'authorURL' => $this->installation->getArchive()->getAuthorInfo('authorURL') ?: '',
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 (
525 isset(self::$pendingPackages[$packageName])
526 && (
527 !isset($package['minversion'])
528 || Package::compareVersion(self::$pendingPackages[$packageName], $package['minversion']) >= 0
529 )
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
589 FROM wcf1_package
590 WHERE package = ?";
591 $statement = WCF::getDB()->prepare($sql);
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
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
639 */
640 public function getCurrentNode(): string
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 */
651 protected function buildPluginNodes(array $instructions)
652 {
653 $count = \count($instructions);
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
661 if (!empty($this->node)) {
662 $this->parentNode = $this->node;
663 $this->sequenceNo = 0;
664 }
665
666 $this->node = $this->getToken();
667
668 $this->emptyNode = true;
669
670 $i = 0;
671 $pluginNodes = [];
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
711 \assert($pluginNodes !== []);
712
713 $sql = "INSERT INTO wcf1_package_installation_node
714 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
715 VALUES (?, ?, ?, ?, ?, ?, ?)";
716 $statement = WCF::getDB()->prepare($sql);
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 ]);
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
803 $sql = "INSERT INTO wcf1_package_installation_node
804 (queueID, processNo, sequenceNo, node, parentNode, nodeType, nodeData)
805 VALUES (?, ?, ?, ?, ?, ?, ?)";
806 $statement = WCF::getDB()->prepare($sql);
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 );
829 $queueList->getConditionBuilder()->add("package_installation_queue.queueID NOT IN (
830 SELECT queueID
831 FROM wcf" . WCF_N . "_package_installation_node
832 )");
833 $queueList->readObjects();
834
835 foreach ($queueList as $queue) {
836 $installation = new PackageInstallationDispatcher($queue);
837
838 // work-around for iterative package updates
839 if (isset(self::$pendingPackages[$queue->package])) {
840 $installation->setPreviousPackage([
841 'package' => $queue->package,
842 'packageVersion' => self::$pendingPackages[$queue->package],
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.
854 */
855 protected function getToken(): string
856 {
857 return \mb_substr(StringUtil::getRandomID(), 0, 8);
858 }
859
860 /**
861 * Returns queue id based upon current node.
862 *
863 * @return int|null
864 */
865 public function getQueueByNode(int $processNo, string $node)
866 {
867 $sql = "SELECT queueID
868 FROM wcf1_package_installation_node
869 WHERE processNo = ?
870 AND node = ?";
871 $statement = WCF::getDB()->prepare($sql);
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`.
881 return null;
882 }
883
884 return $row['queueID'];
885 }
886 }