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