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