Commit | Line | Data |
---|---|---|
11ade432 AE |
1 | <?php |
2 | namespace wcf\system\package; | |
cfedc216 | 3 | use wcf\data\package\installation\queue\PackageInstallationQueueEditor; |
8d84809f | 4 | use wcf\data\package\installation\queue\PackageInstallationQueueList; |
abfda06b | 5 | use wcf\data\package\Package; |
d54c09a0 | 6 | use wcf\system\exception\SystemException; |
11ade432 | 7 | use wcf\system\WCF; |
cfedc216 | 8 | use wcf\util\FileUtil; |
11ade432 AE |
9 | use 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 | */ |
19 | class 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 | } |