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