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
;
10 use wcf\util\StringUtil
;
13 * Creates a logical node-based installation tree.
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
22 class PackageInstallationNodeBuilder
{
24 * true if current node is empty
27 public $emptyNode = true;
30 * active package installation dispatcher
31 * @var \wcf\system\package\PackageInstallationDispatcher
33 public $installation = null;
36 * current installation node
42 * current parent installation node
45 public $parentNode = '';
48 * list of requirements to be checked before package installation
51 public $requirements = array();
54 * current sequence number within one node
57 public $sequenceNo = 0;
60 * list of packages about to be installed
63 protected static $pendingPackages = array();
66 * Creates a new instance of PackageInstallationNodeBuilder
68 * @param PackageInstallationDispatcher $installation
70 public function __construct(PackageInstallationDispatcher
$installation) {
71 $this->installation
= $installation;
77 * @param string $parentNode
79 public function setParentNode($parentNode) {
80 $this->parentNode
= $parentNode;
84 * Builds nodes for current installation queue.
86 public function buildNodes() {
88 $this->buildRequirementNodes();
90 // register package version
91 self
::$pendingPackages[$this->installation
->getArchive()->getPackageInfo('name')] = $this->installation
->getArchive()->getPackageInfo('version');
93 // install package itself
94 if ($this->installation
->queue
->action
== 'install') {
95 $this->buildPackageNode();
98 // package installation plugins
99 $this->buildPluginNodes();
101 // optional packages (ignored on update)
102 if ($this->installation
->queue
->action
== 'install') {
103 $this->buildOptionalNodes();
107 $this->buildChildQueues();
109 if ($this->installation
->queue
->action
== 'update') {
110 $this->buildPackageNode();
115 * Returns the succeeding node.
117 * @param string $parentNode
120 public function getNextNode($parentNode = '') {
122 FROM wcf".WCF_N
."_package_installation_node
125 $statement = WCF
::getDB()->prepareStatement($sql);
126 $statement->execute(array(
127 $this->installation
->queue
->processNo
,
130 $row = $statement->fetchArray();
140 * Returns package name associated with given queue id.
142 * @param integer $queueID
145 public function getPackageNameByQueue($queueID) {
146 $sql = "SELECT packageName
147 FROM wcf".WCF_N
."_package_installation_queue
149 $statement = WCF
::getDB()->prepareStatement($sql);
150 $statement->execute(array($queueID));
151 $row = $statement->fetchArray();
157 return $row['packageName'];
161 * Returns installation type by queue id.
163 * @param integer $queueID
166 public function getInstallationTypeByQueue($queueID) {
167 $sql = "SELECT action
168 FROM wcf".WCF_N
."_package_installation_queue
170 $statement = WCF
::getDB()->prepareStatement($sql);
171 $statement->execute(array($queueID));
172 $row = $statement->fetchArray();
174 return $row['action'];
178 * Returns data for current node.
180 * @param string $node
183 public function getNodeData($node) {
184 $sql = "SELECT nodeType, nodeData, sequenceNo
185 FROM wcf".WCF_N
."_package_installation_node
188 ORDER BY sequenceNo ASC";
189 $statement = WCF
::getDB()->prepareStatement($sql);
190 $statement->execute(array(
191 $this->installation
->queue
->processNo
,
195 while ($row = $statement->fetchArray()) {
203 * Marks a node as completed.
205 * @param string $node
207 public function completeNode($node) {
208 $sql = "UPDATE wcf".WCF_N
."_package_installation_node
212 $statement = WCF
::getDB()->prepareStatement($sql);
213 $statement->execute(array(
214 $this->installation
->queue
->processNo
,
220 * Removes all nodes associated with queue's process no.
222 * CAUTION: This method SHOULD NOT be called within the installation process!
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
232 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_form
234 $statement = WCF
::getDB()->prepareStatement($sql);
235 $statement->execute(array(
236 $this->installation
->queue
->queueID
241 * Calculates current setup process.
243 * @param string $node
246 public function calculateProgress($node) {
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
259 while ($row = $statement->fetchArray()) {
264 $progress['outstanding']++
;
268 if (!$progress['done']) {
271 else if (!$progress['outstanding']) {
275 $total = $progress['done'] +
$progress['outstanding'];
276 return round(($progress['done'] / $total) * 100);
281 * Duplicates a node by re-inserting it and moving all descendants into a new tree.
283 * @param string $node
284 * @param integer $sequenceNo
286 public function cloneNode($node, $sequenceNo) {
287 $newNode = $this->getToken();
289 // update descendants
290 $sql = "UPDATE wcf".WCF_N
."_package_installation_node
294 $statement = WCF
::getDB()->prepareStatement($sql);
295 $statement->execute(array(
298 $this->installation
->queue
->processNo
301 // create a copy of current node (prevents empty nodes)
302 $sql = "SELECT nodeType, nodeData, done
303 FROM wcf".WCF_N
."_package_installation_node
307 $statement = WCF
::getDB()->prepareStatement($sql);
308 $statement->execute(array(
310 $this->installation
->queue
->processNo
,
313 $row = $statement->fetchArray();
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
,
330 // move other child-nodes greater than $sequenceNo into new node
331 $sql = "UPDATE wcf".WCF_N
."_package_installation_node
334 sequenceNo = (sequenceNo - ?)
338 $statement = WCF
::getDB()->prepareStatement($sql);
339 $statement->execute(array(
344 $this->installation
->queue
->processNo
,
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().
354 * @param string $beforeNode
355 * @param \wcf\system\Callback $callback
357 public function insertNode($beforeNode, Callback
$callback) {
358 $newNode = $this->getToken();
360 // update descendants
361 $sql = "UPDATE wcf".WCF_N
."_package_installation_node
365 $statement = WCF
::getDB()->prepareStatement($sql);
366 $statement->execute(array(
369 $this->installation
->queue
->processNo
373 $callback($beforeNode, $newNode);
377 * Shifts nodes to allow dynamic inserts at runtime.
379 * @param string $oldParentNode
380 * @param string $newParentNode
382 public function shiftNodes($oldParentNode, $newParentNode) {
383 $sql = "UPDATE wcf".WCF_N
."_package_installation_node
387 $statement = WCF
::getDB()->prepareStatement($sql);
388 $statement->execute(array(
391 $this->installation
->queue
->processNo
396 * Builds package node used to install the package itself.
398 protected function buildPackageNode() {
399 if (!empty($this->node
)) {
400 $this->parentNode
= $this->node
;
401 $this->sequenceNo
= 0;
404 $this->node
= $this->getToken();
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
,
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
436 * Builds nodes for required packages, whereas each has it own node.
440 protected function buildRequirementNodes() {
441 $queue = $this->installation
->queue
;
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
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']
464 if ($this->node
== '' && !empty($this->parentNode
)) {
465 $this->node
= $this->parentNode
;
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') {
476 throw new SystemException("Unable to find required package '".$package['file']."' within archive of package '".$this->installation
->queue
->package
."'.");
479 $fileName = FileUtil
::getTemporaryFilename('package_', preg_replace('!^.*(?=\.(?:tar\.gz|tgz|tar)$)!i', '', basename($package['file'])));
480 $this->installation
->getArchive()->getTar()->extract($index, $fileName);
483 $archive = new PackageArchive($fileName);
484 $archive->openArchive();
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')."').");
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').".");
497 $sql = "SELECT packageID
498 FROM wcf".WCF_N
."_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'];
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
512 // the new delivered required version of the package has a
513 // higher version number, thus update/replace the existing
514 // package installation 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')
532 self
::$pendingPackages[$archive->getPackageInfo('name')] = $archive->getPackageInfo('version');
535 $installation = new PackageInstallationDispatcher($queue);
536 $installation->nodeBuilder
->setParentNode($this->node
);
537 $installation->nodeBuilder
->buildNodes();
538 $this->node
= $installation->nodeBuilder
->getCurrentNode();
543 * Returns current node
547 public function getCurrentNode() {
552 * Builds package installation plugin nodes, whereas pips could be grouped within
553 * one node, differ from each by nothing but the sequence number.
557 protected function buildPluginNodes() {
558 if (!empty($this->node
)) {
559 $this->parentNode
= $this->node
;
560 $this->sequenceNo
= 0;
563 $this->node
= $this->getToken();
565 $pluginNodes = array();
567 $this->emptyNode
= true;
568 $instructions = ($this->installation
->getAction() == 'install') ?
$this->installation
->getArchive()->getInstallInstructions() : $this->installation
->getArchive()->getUpdateInstructions();
569 $count = count($instructions);
571 foreach ($instructions as $pip) {
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;
581 $pluginNodes[] = array(
583 'node' => $this->node
,
584 'parentNode' => $this->parentNode
,
585 'sequenceNo' => $this->sequenceNo
588 // create a new node for following PIPs, unless it is the last one
590 $this->parentNode
= $this->node
;
591 $this->node
= $this->getToken();
592 $this->sequenceNo
= 0;
594 $this->emptyNode
= true;
600 $pluginNodes[] = array(
602 'node' => $this->node
,
603 'parentNode' => $this->parentNode
,
604 'sequenceNo' => $this->sequenceNo
607 $this->emptyNode
= false;
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);
618 foreach ($pluginNodes as $index => $nodeData) {
619 $statement->execute(array(
620 $this->installation
->queue
->queueID
,
621 $this->installation
->queue
->processNo
,
622 $nodeData['sequenceNo'],
624 $nodeData['parentNode'],
626 serialize($nodeData['data'])
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).
637 protected function buildOptionalNodes() {
640 $optionalPackages = $this->installation
->getArchive()->getOptionals();
641 foreach ($optionalPackages as $package) {
642 // check if already installed
643 if (Package
::isAlreadyInstalled($package['name'])) {
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.");
653 $fileName = FileUtil
::getTemporaryFilename('package_', preg_replace('!^.*(?=\.(?:tar\.gz|tgz|tar)$)!i', '', basename($package['file'])));
654 $this->installation
->getArchive()->getTar()->extract($index, $fileName);
657 $archive = new PackageArchive($fileName);
658 $archive->openArchive();
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;
672 // check for exclusions
673 $excludedPackages = $archive->getConflictedExcludedPackages();
674 if (!empty($excludedPackages)) {
675 $isInstallable = false;
678 $excludingPackages = $archive->getConflictedExcludingPackages();
679 if (!empty($excludingPackages)) {
680 $isInstallable = false;
684 'archive' => $fileName,
685 'isInstallable' => $isInstallable,
686 'package' => $archive->getPackageInfo('name'),
687 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
688 'packageDescription' => $archive->getLocalizedPackageInfo('packageDescription'),
692 self
::$pendingPackages[$archive->getPackageInfo('name')] = $archive->getPackageInfo('version');
695 if (!empty($packages)) {
696 $this->parentNode
= $this->node
;
697 $this->node
= $this->getToken();
698 $this->sequenceNo
= 0;
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
,
717 * Recursively build nodes for child queues.
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();
725 foreach ($queueList as $queue) {
726 $installation = new PackageInstallationDispatcher($queue);
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')
736 $installation->nodeBuilder
->setParentNode($this->node
);
737 $installation->nodeBuilder
->buildNodes();
738 $this->node
= $installation->nodeBuilder
->getCurrentNode();
743 * Returns a short SHA1-hash.
747 protected function getToken() {
748 return mb_substr(StringUtil
::getRandomID(), 0, 8);
752 * Returns queue id based upon current node.
754 * @param integer $processNo
755 * @param string $node
758 public function getQueueByNode($processNo, $node) {
759 $sql = "SELECT queueID
760 FROM wcf".WCF_N
."_package_installation_node
763 $statement = WCF
::getDB()->prepareStatement($sql);
764 $statement->execute(array(
768 $row = $statement->fetchArray();
770 return $row['queueID'];