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