Merge branch '3.1' into 5.2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationNodeBuilder.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\system\package;
cfedc216 3use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
8d84809f 4use wcf\data\package\installation\queue\PackageInstallationQueueList;
abfda06b 5use wcf\data\package\Package;
d54c09a0 6use wcf\system\exception\SystemException;
11ade432 7use wcf\system\WCF;
cfedc216 8use wcf\util\FileUtil;
11ade432
AE
9use wcf\util\StringUtil;
10
11/**
a17de04e 12 * Creates a logical node-based installation tree.
9f959ced 13 *
11ade432 14 * @author Alexander Ebert
e1ff2cac 15 * @copyright 2001-2019 WoltLab GmbH
11ade432 16 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 17 * @package WoltLabSuite\Core\System\Package
11ade432
AE
18 */
19class PackageInstallationNodeBuilder {
d01a7177
AE
20 /**
21 * true if current node is empty
22 * @var boolean
23 */
24 public $emptyNode = true;
25
11ade432 26 /**
b1a0a33c 27 * active package installation dispatcher
4e25add7 28 * @var PackageInstallationDispatcher
11ade432 29 */
e1ff2cac 30 public $installation;
11ade432
AE
31
32 /**
b1a0a33c 33 * current installation node
11ade432
AE
34 * @var string
35 */
36 public $node = '';
37
38 /**
b1a0a33c 39 * current parent installation node
11ade432
AE
40 * @var string
41 */
42 public $parentNode = '';
43
a1ece4d9
AE
44 /**
45 * list of requirements to be checked before package installation
7a23a706 46 * @var mixed[][]
a1ece4d9 47 */
058cbd6a 48 public $requirements = [];
a1ece4d9 49
11ade432
AE
50 /**
51 * current sequence number within one node
11ade432
AE
52 * @var integer
53 */
54 public $sequenceNo = 0;
55
a0b7c762
AE
56 /**
57 * list of packages about to be installed
7a23a706 58 * @var string[]
a0b7c762 59 */
058cbd6a 60 protected static $pendingPackages = [];
a0b7c762 61
11ade432
AE
62 /**
63 * Creates a new instance of PackageInstallationNodeBuilder
9f959ced 64 *
11ade432
AE
65 * @param PackageInstallationDispatcher $installation
66 */
67 public function __construct(PackageInstallationDispatcher $installation) {
68 $this->installation = $installation;
69 }
70
f2719139
AE
71 /**
72 * Sets parent node.
73 *
74 * @param string $parentNode
75 */
76 public function setParentNode($parentNode) {
77 $this->parentNode = $parentNode;
78 }
79
11ade432
AE
80 /**
81 * Builds nodes for current installation queue.
82 */
83 public function buildNodes() {
84 // required packages
85 $this->buildRequirementNodes();
86
7ddf2af9
AE
87 // register package version
88 self::$pendingPackages[$this->installation->getArchive()->getPackageInfo('name')] = $this->installation->getArchive()->getPackageInfo('version');
89
11ade432 90 // install package itself
7ddf2af9
AE
91 if ($this->installation->queue->action == 'install') {
92 $this->buildPackageNode();
93 }
11ade432
AE
94
95 // package installation plugins
96 $this->buildPluginNodes();
97
77f5aa21
AE
98 // optional packages (ignored on update)
99 if ($this->installation->queue->action == 'install') {
100 $this->buildOptionalNodes();
101 }
8d84809f 102
7ddf2af9
AE
103 if ($this->installation->queue->action == 'update') {
104 $this->buildPackageNode();
105 }
968c95f1
AE
106
107 // child queues
108 $this->buildChildQueues();
11ade432
AE
109 }
110
111 /**
112 * Returns the succeeding node.
9f959ced 113 *
11ade432
AE
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
cfedc216 120 WHERE processNo = ?
11ade432
AE
121 AND parentNode = ?";
122 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 123 $statement->execute([
11ade432
AE
124 $this->installation->queue->processNo,
125 $parentNode
058cbd6a 126 ]);
11ade432
AE
127 $row = $statement->fetchArray();
128
129 if (!$row) {
130 return '';
131 }
132
133 return $row['node'];
134 }
135
e70175d6
AE
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);
058cbd6a 147 $statement->execute([$queueID]);
e70175d6
AE
148 $row = $statement->fetchArray();
149
150 if (!$row) {
151 return '';
152 }
153
154 return $row['packageName'];
155 }
156
824d9e90
AE
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);
058cbd6a 168 $statement->execute([$queueID]);
824d9e90
AE
169 $row = $statement->fetchArray();
170
171 return $row['action'];
172 }
173
11ade432
AE
174 /**
175 * Returns data for current node.
9f959ced 176 *
11ade432
AE
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
cfedc216 183 WHERE processNo = ?
11ade432
AE
184 AND node = ?
185 ORDER BY sequenceNo ASC";
186 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 187 $statement->execute([
11ade432
AE
188 $this->installation->queue->processNo,
189 $node
058cbd6a 190 ]);
11ade432 191
069e3013 192 return $statement->fetchAll(\PDO::FETCH_ASSOC);
11ade432
AE
193 }
194
195 /**
196 * Marks a node as completed.
9f959ced 197 *
11ade432
AE
198 * @param string $node
199 */
200 public function completeNode($node) {
201 $sql = "UPDATE wcf".WCF_N."_package_installation_node
202 SET done = 1
cfedc216 203 WHERE processNo = ?
11ade432
AE
204 AND node = ?";
205 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 206 $statement->execute([
11ade432
AE
207 $this->installation->queue->processNo,
208 $node
058cbd6a 209 ]);
11ade432
AE
210 }
211
212 /**
213 * Removes all nodes associated with queue's process no.
9f959ced 214 *
11ade432
AE
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);
058cbd6a 221 $statement->execute([
11ade432 222 $this->installation->queue->processNo
058cbd6a 223 ]);
11ade432
AE
224
225 $sql = "DELETE FROM wcf".WCF_N."_package_installation_form
226 WHERE queueID = ?";
227 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 228 $statement->execute([
11ade432 229 $this->installation->queue->queueID
058cbd6a 230 ]);
11ade432
AE
231 }
232
233 /**
234 * Calculates current setup process.
9f959ced 235 *
11ade432
AE
236 * @param string $node
237 * @return integer
238 */
239 public function calculateProgress($node) {
058cbd6a 240 $progress = [
11ade432
AE
241 'done' => 0,
242 'outstanding' => 0
058cbd6a 243 ];
11ade432
AE
244
245 $sql = "SELECT done
246 FROM wcf".WCF_N."_package_installation_node
f2719139 247 WHERE processNo = ?";
11ade432 248 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 249 $statement->execute([
11ade432 250 $this->installation->queue->processNo
058cbd6a 251 ]);
11ade432
AE
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 }
1a1f7979 264 else if (!$progress['outstanding']) {
11ade432
AE
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.
9f959ced 275 *
11ade432
AE
276 * @param string $node
277 * @param integer $sequenceNo
278 */
456008db 279 public function cloneNode($node, $sequenceNo) {
11ade432
AE
280 $newNode = $this->getToken();
281
282 // update descendants
283 $sql = "UPDATE wcf".WCF_N."_package_installation_node
284 SET parentNode = ?
285 WHERE parentNode = ?
11ade432
AE
286 AND processNo = ?";
287 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 288 $statement->execute([
11ade432
AE
289 $newNode,
290 $node,
11ade432 291 $this->installation->queue->processNo
058cbd6a 292 ]);
11ade432
AE
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 = ?
11ade432
AE
298 AND processNo = ?
299 AND sequenceNo = ?";
300 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 301 $statement->execute([
11ade432 302 $node,
11ade432
AE
303 $this->installation->queue->processNo,
304 $sequenceNo
058cbd6a 305 ]);
11ade432
AE
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);
058cbd6a 312 $statement->execute([
11ade432
AE
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']
058cbd6a 321 ]);
11ade432
AE
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 = ?
11ade432
AE
329 AND processNo = ?
330 AND sequenceNo > ?";
331 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 332 $statement->execute([
11ade432
AE
333 $node,
334 $newNode,
335 $sequenceNo,
336 $node,
11ade432
AE
337 $this->installation->queue->processNo,
338 $sequenceNo
058cbd6a 339 ]);
11ade432
AE
340 }
341
456008db
AE
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 *
ac52543a 347 * @param string $beforeNode
a0c6927a 348 * @param callable $callback
456008db 349 */
a0c6927a 350 public function insertNode($beforeNode, callable $callback) {
456008db
AE
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);
058cbd6a 359 $statement->execute([
456008db
AE
360 $newNode,
361 $beforeNode,
362 $this->installation->queue->processNo
058cbd6a 363 ]);
456008db
AE
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);
058cbd6a 381 $statement->execute([
456008db
AE
382 $newParentNode,
383 $oldParentNode,
384 $this->installation->queue->processNo
058cbd6a 385 ]);
456008db
AE
386 }
387
11ade432
AE
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();
322d635b 398
11ade432
AE
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);
058cbd6a 403 $statement->execute([
11ade432
AE
404 $this->installation->queue->queueID,
405 $this->installation->queue->processNo,
406 $this->sequenceNo,
407 $this->node,
408 $this->parentNode,
409 'package',
058cbd6a 410 serialize([
11ade432 411 'package' => $this->installation->getArchive()->getPackageInfo('name'),
a2ad7897 412 'packageName' => $this->installation->getArchive()->getLocalizedPackageInfo('packageName'),
a2ad7897 413 'packageDescription' => $this->installation->getArchive()->getLocalizedPackageInfo('packageDescription'),
11ade432
AE
414 'packageVersion' => $this->installation->getArchive()->getPackageInfo('version'),
415 'packageDate' => $this->installation->getArchive()->getPackageInfo('date'),
416 'packageURL' => $this->installation->getArchive()->getPackageInfo('packageURL'),
aac1247e 417 'isApplication' => $this->installation->getArchive()->getPackageInfo('isApplication'),
11ade432 418 'author' => $this->installation->getArchive()->getAuthorInfo('author'),
b68f0af4 419 'authorURL' => $this->installation->getArchive()->getAuthorInfo('authorURL') !== null ? $this->installation->getArchive()->getAuthorInfo('authorURL') : '',
11ade432 420 'installDate' => TIME_NOW,
a1ece4d9 421 'updateDate' => TIME_NOW,
e1ff2cac
AE
422 'requirements' => $this->requirements,
423 'applicationDirectory' => $this->installation->getArchive()->getPackageInfo('applicationDirectory') ?: '',
058cbd6a
MS
424 ])
425 ]);
11ade432
AE
426 }
427
428 /**
429 * Builds nodes for required packages, whereas each has it own node.
9f959ced 430 *
11ade432 431 * @return string
2b770bdd 432 * @throws SystemException
11ade432
AE
433 */
434 protected function buildRequirementNodes() {
f2719139 435 $queue = $this->installation->queue;
11ade432 436
639e325b
AE
437 // handle requirements
438 $requiredPackages = $this->installation->getArchive()->getOpenRequirements();
11ade432
AE
439 foreach ($requiredPackages as $packageName => $package) {
440 if (!isset($package['file'])) {
322d635b
MS
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
a1ece4d9 449 // requirements will be checked once package is about to be installed
058cbd6a 450 $this->requirements[$packageName] = [
63b9817b 451 'minVersion' => isset($package['minversion']) ? $package['minversion'] : '',
a1ece4d9 452 'packageID' => $package['packageID']
058cbd6a 453 ];
e0431d96 454
11ade432
AE
455 continue;
456 }
457
f2719139
AE
458 if ($this->node == '' && !empty($this->parentNode)) {
459 $this->node = $this->parentNode;
460 }
11ade432 461
f2719139
AE
462 // extract package
463 $index = $this->installation->getArchive()->getTar()->getIndexByFilename($package['file']);
464 if ($index === false) {
d54c09a0
AE
465 // workaround for WCFSetup
466 if (!PACKAGE_ID && $packageName == 'com.woltlab.wcf') {
467 continue;
468 }
469
4202971e 470 throw new SystemException("Unable to find required package '".$package['file']."' within archive of package '".$this->installation->queue->package."'.");
11ade432 471 }
f2719139
AE
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
6eaad580
MS
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
55009267
MS
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
b3782430
AE
490 // get package id
491 $sql = "SELECT packageID
492 FROM wcf".WCF_N."_package
493 WHERE package = ?";
494 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 495 $statement->execute([$archive->getPackageInfo('name')]);
b3782430
AE
496 $row = $statement->fetchArray();
497 $packageID = ($row === false) ? null : $row['packageID'];
498
322d635b
MS
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
322d635b
MS
509 }
510 }
511
3b1e1aee
MS
512 if ($archive->getPackageInfo('name') === 'com.woltlab.wcf') {
513 WCF::checkWritability();
514 }
515
f2719139 516 // create new queue
058cbd6a 517 $queue = PackageInstallationQueueEditor::create([
f2719139
AE
518 'parentQueueID' => $queue->queueID,
519 'processNo' => $queue->processNo,
520 'userID' => WCF::getUser()->userID,
521 'package' => $archive->getPackageInfo('name'),
b3782430 522 'packageID' => $packageID,
a2ad7897 523 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
f2719139 524 'archive' => $fileName,
63b9817b 525 'action' => $packageID ? 'update' : 'install'
058cbd6a 526 ]);
f2719139 527
322d635b
MS
528 self::$pendingPackages[$archive->getPackageInfo('name')] = $archive->getPackageInfo('version');
529
f2719139
AE
530 // spawn nodes
531 $installation = new PackageInstallationDispatcher($queue);
532 $installation->nodeBuilder->setParentNode($this->node);
533 $installation->nodeBuilder->buildNodes();
534 $this->node = $installation->nodeBuilder->getCurrentNode();
11ade432
AE
535 }
536 }
537
f2719139
AE
538 /**
539 * Returns current node
540 *
541 * @return string
542 */
543 public function getCurrentNode() {
544 return $this->node;
545 }
546
11ade432
AE
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.
9f959ced 550 *
11ade432
AE
551 * @return string
552 */
553 protected function buildPluginNodes() {
7ddf2af9
AE
554 if (!empty($this->node)) {
555 $this->parentNode = $this->node;
556 $this->sequenceNo = 0;
557 }
558
559 $this->node = $this->getToken();
560
058cbd6a 561 $pluginNodes = [];
11ade432 562
d01a7177 563 $this->emptyNode = true;
11ade432 564 $instructions = ($this->installation->getAction() == 'install') ? $this->installation->getArchive()->getInstallInstructions() : $this->installation->getArchive()->getUpdateInstructions();
36ec5593
AE
565 $count = count($instructions);
566 $i = 0;
11ade432 567 foreach ($instructions as $pip) {
36ec5593
AE
568 $i++;
569
eef3c4aa 570 if (isset($pip['attributes']['run']) && ($pip['attributes']['run'] == 'standalone')) {
d01a7177
AE
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 }
058cbd6a 577 $pluginNodes[] = [
11ade432
AE
578 'data' => $pip,
579 'node' => $this->node,
580 'parentNode' => $this->parentNode,
581 'sequenceNo' => $this->sequenceNo
058cbd6a 582 ];
c7aa597c 583
36ec5593
AE
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 }
11ade432
AE
592 }
593 else {
594 $this->sequenceNo++;
595
058cbd6a 596 $pluginNodes[] = [
11ade432
AE
597 'data' => $pip,
598 'node' => $this->node,
599 'parentNode' => $this->parentNode,
600 'sequenceNo' => $this->sequenceNo
058cbd6a 601 ];
d01a7177
AE
602
603 $this->emptyNode = false;
11ade432
AE
604 }
605 }
606
607 // insert nodes
15fa2802 608 if (!empty($pluginNodes)) {
11ade432
AE
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);
15fa2802 613
11ade432 614 foreach ($pluginNodes as $index => $nodeData) {
058cbd6a 615 $statement->execute([
11ade432
AE
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'])
058cbd6a 623 ]);
11ade432
AE
624 }
625 }
626 }
627
628 /**
629 * Builds nodes for optional packages, whereas each package exists within
1615fc2e 630 * one node with the same parent node, separated by sequence no (which does
11ade432
AE
631 * not really matter at this point).
632 */
633 protected function buildOptionalNodes() {
058cbd6a 634 $packages = [];
11ade432
AE
635
636 $optionalPackages = $this->installation->getArchive()->getOptionals();
637 foreach ($optionalPackages as $package) {
299400c5
AE
638 // check if already installed
639 if (Package::isAlreadyInstalled($package['name'])) {
640 continue;
641 }
642
492816d3
AE
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();
11ade432 655
a0b7c762
AE
656 // check if all requirements are met
657 $isInstallable = true;
62e40a61
MS
658 foreach ($archive->getOpenRequirements() as $packageName => $requiredPackage) {
659 if (!isset($requiredPackage['file'])) {
a0b7c762 660 // requirement is neither installed nor shipped, check if it is about to be installed
322d635b 661 if (!isset(self::$pendingPackages[$packageName])) {
a0b7c762
AE
662 $isInstallable = false;
663 break;
664 }
665 }
666 }
667
bce74466
AE
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
058cbd6a 679 $packages[] = [
492816d3 680 'archive' => $fileName,
a0b7c762 681 'isInstallable' => $isInstallable,
492816d3 682 'package' => $archive->getPackageInfo('name'),
a2ad7897 683 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
9e51ceb8 684 'packageDescription' => $archive->getLocalizedPackageInfo('packageDescription'),
492816d3 685 'selected' => 0
058cbd6a 686 ];
a0b7c762 687
322d635b 688 self::$pendingPackages[$archive->getPackageInfo('name')] = $archive->getPackageInfo('version');
11ade432
AE
689 }
690
492816d3 691 if (!empty($packages)) {
11ade432
AE
692 $this->parentNode = $this->node;
693 $this->node = $this->getToken();
492816d3 694 $this->sequenceNo = 0;
11ade432
AE
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);
058cbd6a 700 $statement->execute([
492816d3
AE
701 $this->installation->queue->queueID,
702 $this->installation->queue->processNo,
703 $this->sequenceNo,
704 $this->node,
705 $this->parentNode,
706 'optionalPackages',
707 serialize($packages)
058cbd6a 708 ]);
11ade432
AE
709 }
710 }
711
8d84809f
AE
712 /**
713 * Recursively build nodes for child queues.
714 */
715 protected function buildChildQueues() {
716 $queueList = new PackageInstallationQueueList();
058cbd6a 717 $queueList->getConditionBuilder()->add("package_installation_queue.parentQueueID = ?", [$this->installation->queue->queueID]);
092e2f08 718 $queueList->getConditionBuilder()->add("package_installation_queue.queueID NOT IN (SELECT queueID FROM wcf".WCF_N."_package_installation_node)");
8d84809f
AE
719 $queueList->readObjects();
720
721 foreach ($queueList as $queue) {
722 $installation = new PackageInstallationDispatcher($queue);
39935629
AE
723
724 // work-around for iterative package updates
725 if ($this->installation->queue->action == 'update' && $queue->package == $this->installation->queue->package) {
058cbd6a 726 $installation->setPreviousPackage([
39935629
AE
727 'package' => $this->installation->getArchive()->getPackageInfo('name'),
728 'packageVersion' => $this->installation->getArchive()->getPackageInfo('version')
058cbd6a 729 ]);
39935629
AE
730 }
731
8d84809f
AE
732 $installation->nodeBuilder->setParentNode($this->node);
733 $installation->nodeBuilder->buildNodes();
734 $this->node = $installation->nodeBuilder->getCurrentNode();
735 }
736 }
737
11ade432
AE
738 /**
739 * Returns a short SHA1-hash.
55009267 740 *
11ade432
AE
741 * @return string
742 */
743 protected function getToken() {
838e315b 744 return mb_substr(StringUtil::getRandomID(), 0, 8);
11ade432 745 }
cfedc216
AE
746
747 /**
748 * Returns queue id based upon current node.
749 *
750 * @param integer $processNo
751 * @param string $node
39e6306a 752 * @return integer|null
cfedc216
AE
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);
058cbd6a 760 $statement->execute([
cfedc216
AE
761 $processNo,
762 $node
058cbd6a 763 ]);
cfedc216
AE
764 $row = $statement->fetchArray();
765
39e6306a
AE
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
cfedc216
AE
772 return $row['queueID'];
773 }
11ade432 774}