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