Commit | Line | Data |
---|---|---|
11ade432 | 1 | <?php |
a9229942 | 2 | |
11ade432 | 3 | namespace wcf\system\package; |
a9229942 | 4 | |
c244fcc6 | 5 | use wcf\data\application\Application; |
11ade432 | 6 | use wcf\data\application\ApplicationEditor; |
4fd750c1 | 7 | use wcf\data\devtools\project\DevtoolsProjectAction; |
a2ad7897 | 8 | use wcf\data\language\category\LanguageCategory; |
11ade432 | 9 | use wcf\data\language\LanguageEditor; |
a2ad7897 | 10 | use wcf\data\language\LanguageList; |
8aa93ba8 | 11 | use wcf\data\option\OptionEditor; |
ec1b1daf TB |
12 | use wcf\data\package\installation\queue\PackageInstallationQueue; |
13 | use wcf\data\package\installation\queue\PackageInstallationQueueEditor; | |
2bc9f31d TB |
14 | use wcf\data\package\Package; |
15 | use wcf\data\package\PackageEditor; | |
e1b6d09d | 16 | use wcf\data\user\User; |
f8d85495 | 17 | use wcf\data\user\UserAction; |
f1c1fc65 | 18 | use wcf\system\application\ApplicationHandler; |
f1dc282d | 19 | use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder; |
11ade432 | 20 | use wcf\system\cache\CacheHandler; |
a2ad7897 | 21 | use wcf\system\database\statement\PreparedStatement; |
11ade432 | 22 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
f8d85495 | 23 | use wcf\system\devtools\DevtoolsSetup; |
fb14b37f | 24 | use wcf\system\event\EventHandler; |
7b9ff46b | 25 | use wcf\system\exception\ImplementationException; |
11ade432 | 26 | use wcf\system\exception\SystemException; |
a99fcf96 MS |
27 | use wcf\system\form\container\GroupFormElementContainer; |
28 | use wcf\system\form\container\MultipleSelectionFormElementContainer; | |
29 | use wcf\system\form\element\MultipleSelectionFormElement; | |
30 | use wcf\system\form\element\TextInputFormElement; | |
b4282c86 | 31 | use wcf\system\form\FormDocument; |
a2ad7897 | 32 | use wcf\system\language\LanguageFactory; |
4d4a5125 | 33 | use wcf\system\package\plugin\IPackageInstallationPlugin; |
3e0e6b2c | 34 | use wcf\system\request\LinkHandler; |
3dcfb497 | 35 | use wcf\system\request\RouteHandler; |
4e25add7 | 36 | use wcf\system\setup\IFileHandler; |
8f3fc897 | 37 | use wcf\system\setup\Installer; |
cd0fbe9a | 38 | use wcf\system\style\StyleHandler; |
fb14b37f | 39 | use wcf\system\user\storage\UserStorageHandler; |
2bc9f31d | 40 | use wcf\system\WCF; |
4ca13e5a | 41 | use wcf\util\CryptoUtil; |
11ade432 AE |
42 | use wcf\util\FileUtil; |
43 | use wcf\util\HeaderUtil; | |
dc2c6fc4 | 44 | use wcf\util\JSON; |
11ade432 AE |
45 | use wcf\util\StringUtil; |
46 | ||
47 | /** | |
48 | * PackageInstallationDispatcher handles the whole installation process. | |
a9229942 TD |
49 | * |
50 | * @author Alexander Ebert | |
51 | * @copyright 2001-2019 WoltLab GmbH | |
52 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
53 | * @package WoltLabSuite\Core\System\Package | |
11ade432 | 54 | */ |
a9229942 TD |
55 | class PackageInstallationDispatcher |
56 | { | |
57 | /** | |
58 | * current installation type | |
59 | * @var string | |
60 | */ | |
61 | protected $action = ''; | |
62 | ||
63 | /** | |
64 | * instance of PackageArchive | |
65 | * @var PackageArchive | |
66 | */ | |
67 | public $archive; | |
68 | ||
69 | /** | |
70 | * instance of PackageInstallationNodeBuilder | |
71 | * @var PackageInstallationNodeBuilder | |
72 | */ | |
73 | public $nodeBuilder; | |
74 | ||
75 | /** | |
76 | * instance of Package | |
77 | * @var Package | |
78 | */ | |
79 | public $package; | |
80 | ||
81 | /** | |
82 | * instance of PackageInstallationQueue | |
83 | * @var PackageInstallationQueue | |
84 | */ | |
85 | public $queue; | |
86 | ||
87 | /** | |
88 | * default name of the config file | |
89 | * @var string | |
90 | */ | |
91 | const CONFIG_FILE = 'app.config.inc.php'; | |
92 | ||
93 | /** | |
94 | * data of previous package in queue | |
95 | * @var string[] | |
96 | */ | |
97 | protected $previousPackageData; | |
98 | ||
99 | /** | |
100 | * Creates a new instance of PackageInstallationDispatcher. | |
101 | * | |
102 | * @param PackageInstallationQueue $queue | |
103 | */ | |
104 | public function __construct(PackageInstallationQueue $queue) | |
105 | { | |
106 | $this->queue = $queue; | |
107 | $this->nodeBuilder = new PackageInstallationNodeBuilder($this); | |
108 | ||
109 | $this->action = $this->queue->action; | |
110 | } | |
111 | ||
112 | /** | |
113 | * Sets data of previous package in queue. | |
114 | * | |
115 | * @param string[] $packageData | |
116 | */ | |
117 | public function setPreviousPackage(array $packageData) | |
118 | { | |
119 | $this->previousPackageData = $packageData; | |
120 | } | |
121 | ||
122 | /** | |
123 | * Installs node components and returns next node. | |
124 | * | |
125 | * @param string $node | |
126 | * @return PackageInstallationStep | |
127 | * @throws SystemException | |
128 | */ | |
129 | public function install($node) | |
130 | { | |
131 | $nodes = $this->nodeBuilder->getNodeData($node); | |
132 | if (empty($nodes)) { | |
133 | // guard against possible issues with empty instruction blocks, including | |
134 | // these blocks that contain no valid instructions at all (e.g. typo from | |
135 | // copy & paste) | |
136 | throw new SystemException( | |
137 | "Failed to retrieve nodes for identifier '{$node}', the query returned no results." | |
138 | ); | |
139 | } | |
140 | ||
141 | // invoke node-specific actions | |
142 | $step = null; | |
143 | foreach ($nodes as $data) { | |
144 | $nodeData = \unserialize($data['nodeData']); | |
145 | $this->logInstallationStep($data); | |
146 | ||
147 | switch ($data['nodeType']) { | |
148 | case 'package': | |
149 | $step = $this->installPackage($nodeData); | |
150 | break; | |
151 | ||
152 | case 'pip': | |
153 | $step = $this->executePIP($nodeData); | |
154 | break; | |
155 | ||
156 | case 'optionalPackages': | |
157 | $step = $this->selectOptionalPackages($node, $nodeData); | |
158 | break; | |
159 | ||
160 | default: | |
161 | exit("Unknown node type: '" . $data['nodeType'] . "'"); | |
162 | break; | |
163 | } | |
164 | ||
165 | if ($step->splitNode()) { | |
166 | $log = 'split node'; | |
167 | if ($step->getException() !== null && $step->getException()->getMessage()) { | |
168 | $log .= ': ' . $step->getException()->getMessage(); | |
169 | } | |
170 | ||
171 | $this->logInstallationStep($data, $log); | |
172 | $this->nodeBuilder->cloneNode($node, $data['sequenceNo']); | |
173 | break; | |
174 | } | |
175 | } | |
176 | ||
177 | // mark node as completed | |
178 | $this->nodeBuilder->completeNode($node); | |
179 | ||
180 | // assign next node | |
181 | $node = $this->nodeBuilder->getNextNode($node); | |
182 | $step->setNode($node); | |
183 | ||
184 | // perform post-install/update actions | |
185 | if ($node == '') { | |
186 | $this->logInstallationStep([], 'start cleanup'); | |
187 | ||
188 | // update "last update time" option | |
189 | $sql = "UPDATE wcf" . WCF_N . "_option | |
190 | SET optionValue = ? | |
191 | WHERE optionName = ?"; | |
192 | $statement = WCF::getDB()->prepareStatement($sql); | |
193 | $statement->execute([ | |
194 | TIME_NOW, | |
195 | 'last_update_time', | |
196 | ]); | |
197 | ||
198 | // update options.inc.php | |
199 | OptionEditor::resetCache(); | |
200 | ||
201 | if ($this->action == 'install') { | |
202 | // save localized package infos | |
203 | $this->saveLocalizedPackageInfos(); | |
204 | ||
205 | // remove all cache files after WCFSetup | |
206 | if (!PACKAGE_ID) { | |
207 | CacheHandler::getInstance()->flushAll(); | |
208 | ||
209 | $sql = "UPDATE wcf" . WCF_N . "_option | |
210 | SET optionValue = ? | |
211 | WHERE optionName = ?"; | |
212 | $statement = WCF::getDB()->prepareStatement($sql); | |
213 | ||
214 | $statement->execute([ | |
215 | StringUtil::getUUID(), | |
216 | 'wcf_uuid', | |
217 | ]); | |
218 | ||
219 | if (\file_exists(WCF_DIR . 'cookiePrefix.txt')) { | |
220 | $statement->execute([ | |
221 | COOKIE_PREFIX, | |
222 | 'cookie_prefix', | |
223 | ]); | |
224 | ||
225 | @\unlink(WCF_DIR . 'cookiePrefix.txt'); | |
226 | } | |
227 | ||
228 | $user = new User(1); | |
229 | $statement->execute([ | |
230 | $user->username, | |
231 | 'mail_from_name', | |
232 | ]); | |
233 | $statement->execute([ | |
234 | $user->email, | |
235 | 'mail_from_address', | |
236 | ]); | |
237 | $statement->execute([ | |
238 | $user->email, | |
239 | 'mail_admin_address', | |
240 | ]); | |
241 | ||
242 | $statement->execute([ | |
243 | // We do not use the cache-timing safe class Hex, because we run the | |
244 | // function during the setup. | |
245 | $signatureSecret = \bin2hex(\random_bytes(20)), | |
246 | 'signature_secret', | |
247 | ]); | |
248 | \define('SIGNATURE_SECRET', $signatureSecret); | |
249 | HeaderUtil::setCookie( | |
5c221ccb | 250 | 'user_session', |
a9229942 TD |
251 | // We do not use the cache-timing safe class Hex, because we run the |
252 | // function during the setup. | |
253 | CryptoUtil::createSignedString( | |
254 | \pack( | |
255 | 'CA20C', | |
256 | 1, | |
257 | \hex2bin(WCF::getSession()->sessionID), | |
258 | 0 | |
259 | ) | |
260 | ) | |
261 | ); | |
262 | ||
263 | if (WCF::getSession()->getVar('__wcfSetup_developerMode')) { | |
1a4deb98 | 264 | $this->setupDeveloperMode(); |
a9229942 TD |
265 | } |
266 | ||
267 | if (WCF::getSession()->getVar('__wcfSetup_imagick')) { | |
268 | $statement->execute([ | |
269 | 'imagick', | |
270 | 'image_adapter_type', | |
271 | ]); | |
272 | } | |
273 | ||
274 | // update options.inc.php | |
275 | OptionEditor::resetCache(); | |
276 | ||
277 | WCF::getSession()->register('__wcfSetup_completed', true); | |
278 | } | |
279 | ||
280 | // rebuild application paths | |
281 | ApplicationHandler::rebuild(); | |
282 | } | |
283 | ||
284 | // remove template listener cache | |
285 | TemplateListenerCodeCacheBuilder::getInstance()->reset(); | |
286 | ||
287 | // reset language cache | |
288 | LanguageFactory::getInstance()->clearCache(); | |
289 | LanguageFactory::getInstance()->deleteLanguageCache(); | |
290 | ||
291 | // reset stylesheets | |
292 | StyleHandler::resetStylesheets(); | |
293 | ||
294 | // clear user storage | |
295 | UserStorageHandler::getInstance()->clear(); | |
296 | ||
297 | // rebuild config files for affected applications | |
298 | $sql = "SELECT package.packageID | |
299 | FROM wcf" . WCF_N . "_package_installation_queue queue, | |
300 | wcf" . WCF_N . "_package package | |
301 | WHERE queue.processNo = ? | |
302 | AND package.packageID = queue.packageID | |
303 | AND package.isApplication = ?"; | |
304 | $statement = WCF::getDB()->prepareStatement($sql); | |
305 | $statement->execute([ | |
306 | $this->queue->processNo, | |
307 | 1, | |
308 | ]); | |
309 | while ($row = $statement->fetchArray()) { | |
310 | Package::writeConfigFile($row['packageID']); | |
311 | } | |
312 | ||
313 | EventHandler::getInstance()->fireAction($this, 'postInstall'); | |
314 | ||
315 | // remove archives | |
316 | $sql = "SELECT archive | |
317 | FROM wcf" . WCF_N . "_package_installation_queue | |
318 | WHERE processNo = ?"; | |
319 | $statement = WCF::getDB()->prepareStatement($sql); | |
320 | $statement->execute([$this->queue->processNo]); | |
321 | while ($row = $statement->fetchArray()) { | |
322 | @\unlink($row['archive']); | |
323 | } | |
324 | ||
325 | // delete queues | |
326 | $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_queue | |
327 | WHERE processNo = ?"; | |
328 | $statement = WCF::getDB()->prepareStatement($sql); | |
329 | $statement->execute([$this->queue->processNo]); | |
330 | ||
331 | $this->logInstallationStep([], 'finished cleanup'); | |
332 | } | |
333 | ||
334 | return $step; | |
335 | } | |
336 | ||
1a4deb98 MS |
337 | /** |
338 | * @since 5.5 | |
339 | */ | |
340 | protected function setupDeveloperMode(): void | |
341 | { | |
342 | $sql = "UPDATE wcf" . WCF_N . "_option | |
343 | SET optionValue = ? | |
344 | WHERE optionName = ?"; | |
345 | $statement = WCF::getDB()->prepareStatement($sql); | |
346 | ||
347 | $statement->execute([ | |
348 | 1, | |
349 | 'enable_debug_mode', | |
350 | ]); | |
351 | $statement->execute([ | |
352 | 'public', | |
353 | 'exception_privacy', | |
354 | ]); | |
355 | $statement->execute([ | |
356 | 'debugFolder', | |
357 | 'mail_send_method', | |
358 | ]); | |
359 | $statement->execute([ | |
360 | 1, | |
361 | 'enable_developer_tools', | |
362 | ]); | |
363 | $statement->execute([ | |
364 | 1, | |
365 | 'log_missing_language_items', | |
366 | ]); | |
367 | ||
368 | foreach (DevtoolsSetup::getInstance()->getOptionOverrides() as $optionName => $optionValue) { | |
369 | $statement->execute([ | |
370 | $optionValue, | |
371 | $optionName, | |
372 | ]); | |
373 | } | |
374 | ||
375 | foreach (DevtoolsSetup::getInstance()->getUsers() as $newUser) { | |
376 | try { | |
377 | (new UserAction([], 'create', [ | |
378 | 'data' => [ | |
379 | 'email' => $newUser['email'], | |
380 | 'password' => $newUser['password'], | |
381 | 'username' => $newUser['username'], | |
382 | ], | |
383 | 'groups' => [ | |
384 | 1, | |
385 | 3, | |
386 | ], | |
387 | ]))->executeAction(); | |
388 | } catch (SystemException $e) { | |
389 | // ignore errors due to event listeners missing at this | |
390 | // point during installation | |
391 | } | |
392 | } | |
393 | ||
394 | $importPath = DevtoolsSetup::getInstance()->getDevtoolsImportPath(); | |
395 | if ($importPath !== '') { | |
396 | (new DevtoolsProjectAction([], 'quickSetup', [ | |
397 | 'path' => $importPath, | |
398 | ]))->executeAction(); | |
399 | } | |
5906df65 MS |
400 | |
401 | $packageServerLogin = DevtoolsSetup::getInstance()->getPackageServerLogin(); | |
402 | if (!empty($packageServerLogin)) { | |
403 | // All update servers installed at this point are only our own servers for which the same | |
404 | // login data can be used. | |
405 | $sql = "UPDATE wcf1_package_update_server | |
406 | SET loginUsername = ?, | |
407 | loginPassword = ?"; | |
408 | $statement = WCF::getDB()->prepare($sql); | |
409 | $statement->execute([ | |
410 | $packageServerLogin['username'], | |
411 | $packageServerLogin['password'], | |
412 | ]); | |
413 | } | |
1a4deb98 MS |
414 | } |
415 | ||
a9229942 TD |
416 | /** |
417 | * Logs an installation step. | |
418 | * | |
419 | * @param array $node data of the executed node | |
420 | * @param string $log optional additional log text | |
421 | */ | |
422 | protected function logInstallationStep(array $node = [], $log = '') | |
423 | { | |
424 | $logEntry = "[" . TIME_NOW . "]\n"; | |
425 | if (!empty($node)) { | |
426 | $logEntry .= 'sequenceNo: ' . $node['sequenceNo'] . "\n"; | |
427 | $logEntry .= 'nodeType: ' . $node['nodeType'] . "\n"; | |
428 | $logEntry .= "nodeData:\n"; | |
429 | ||
430 | $nodeData = \unserialize($node['nodeData']); | |
431 | foreach ($nodeData as $index => $value) { | |
432 | $logEntry .= "\t" . $index . ': ' . (!\is_object($value) && !\is_array($value) ? $value : JSON::encode($value)) . "\n"; | |
433 | } | |
434 | } | |
435 | ||
436 | if ($log) { | |
437 | $logEntry .= 'additional information: ' . $log . "\n"; | |
438 | } | |
439 | ||
440 | $logEntry .= \str_repeat('-', 30) . "\n\n"; | |
441 | ||
442 | \file_put_contents( | |
443 | WCF_DIR . 'log/' . \date('Y-m-d', TIME_NOW) . '-update-' . $this->queue->queueID . '.txt', | |
444 | $logEntry, | |
445 | \FILE_APPEND | |
446 | ); | |
447 | } | |
448 | ||
449 | /** | |
450 | * Returns current package archive. | |
451 | * | |
452 | * @return PackageArchive | |
453 | */ | |
454 | public function getArchive() | |
455 | { | |
456 | if ($this->archive === null) { | |
457 | // check if we're doing an iterative update of the same package | |
458 | if ( | |
459 | $this->previousPackageData !== null | |
460 | && $this->getPackage()->package == $this->previousPackageData['package'] | |
461 | ) { | |
462 | if ( | |
463 | Package::compareVersion( | |
464 | $this->getPackage()->packageVersion, | |
465 | $this->previousPackageData['packageVersion'], | |
466 | '<' | |
467 | ) | |
468 | ) { | |
469 | // fake package to simulate the package version required by current archive | |
470 | $this->getPackage()->setPackageVersion($this->previousPackageData['packageVersion']); | |
471 | } | |
472 | } | |
473 | ||
474 | $this->archive = new PackageArchive($this->queue->archive, $this->getPackage()); | |
475 | ||
476 | if (FileUtil::isURL($this->archive->getArchive())) { | |
477 | // get return value and update entry in | |
478 | // package_installation_queue with this value | |
479 | $archive = $this->archive->downloadArchive(); | |
480 | $queueEditor = new PackageInstallationQueueEditor($this->queue); | |
481 | $queueEditor->update(['archive' => $archive]); | |
482 | } | |
483 | ||
484 | $this->archive->openArchive(); | |
485 | } | |
486 | ||
487 | return $this->archive; | |
488 | } | |
489 | ||
490 | /** | |
491 | * Installs current package. | |
492 | * | |
493 | * @param mixed[] $nodeData | |
494 | * @return PackageInstallationStep | |
495 | * @throws SystemException | |
496 | */ | |
497 | protected function installPackage(array $nodeData) | |
498 | { | |
499 | $installationStep = new PackageInstallationStep(); | |
500 | ||
501 | // check requirements | |
502 | if (!empty($nodeData['requirements'])) { | |
503 | foreach ($nodeData['requirements'] as $package => $requirementData) { | |
504 | // get existing package | |
505 | if ($requirementData['packageID']) { | |
506 | $sql = "SELECT packageName, packageVersion | |
507 | FROM wcf" . WCF_N . "_package | |
508 | WHERE packageID = ?"; | |
509 | $statement = WCF::getDB()->prepareStatement($sql); | |
510 | $statement->execute([$requirementData['packageID']]); | |
511 | } else { | |
512 | // try to find matching package | |
513 | $sql = "SELECT packageName, packageVersion | |
514 | FROM wcf" . WCF_N . "_package | |
515 | WHERE package = ?"; | |
516 | $statement = WCF::getDB()->prepareStatement($sql); | |
517 | $statement->execute([$package]); | |
518 | } | |
519 | $row = $statement->fetchArray(); | |
520 | ||
521 | // package is required but not available | |
522 | if ($row === false) { | |
523 | throw new SystemException("Package '" . $package . "' is required by '" . $nodeData['packageName'] . "', but is neither installed nor shipped."); | |
524 | } | |
525 | ||
526 | // check version requirements | |
527 | if ($requirementData['minVersion']) { | |
528 | if (Package::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) { | |
529 | throw new SystemException("Package '" . $nodeData['packageName'] . "' requires package '" . $row['packageName'] . "' in version '" . $requirementData['minVersion'] . "', but only version '" . $row['packageVersion'] . "' is installed"); | |
530 | } | |
531 | } | |
532 | } | |
533 | } | |
534 | unset($nodeData['requirements']); | |
535 | ||
536 | $applicationDirectory = ''; | |
537 | if (isset($nodeData['applicationDirectory'])) { | |
538 | $applicationDirectory = $nodeData['applicationDirectory']; | |
539 | unset($nodeData['applicationDirectory']); | |
540 | } | |
541 | ||
542 | // update package | |
543 | if ($this->queue->packageID) { | |
544 | $packageEditor = new PackageEditor(new Package($this->queue->packageID)); | |
545 | unset($nodeData['installDate']); | |
546 | $packageEditor->update($nodeData); | |
547 | ||
548 | // delete old excluded packages | |
549 | $sql = "DELETE FROM wcf" . WCF_N . "_package_exclusion | |
550 | WHERE packageID = ?"; | |
551 | $statement = WCF::getDB()->prepareStatement($sql); | |
552 | $statement->execute([$this->queue->packageID]); | |
553 | ||
554 | // delete old compatibility versions | |
555 | $sql = "DELETE FROM wcf" . WCF_N . "_package_compatibility | |
556 | WHERE packageID = ?"; | |
557 | $statement = WCF::getDB()->prepareStatement($sql); | |
558 | $statement->execute([$this->queue->packageID]); | |
559 | ||
560 | // delete old requirements and dependencies | |
561 | $sql = "DELETE FROM wcf" . WCF_N . "_package_requirement | |
562 | WHERE packageID = ?"; | |
563 | $statement = WCF::getDB()->prepareStatement($sql); | |
564 | $statement->execute([$this->queue->packageID]); | |
565 | } else { | |
566 | // create package entry | |
567 | $package = $this->createPackage($nodeData); | |
568 | ||
569 | // update package id for current queue | |
570 | $queueEditor = new PackageInstallationQueueEditor($this->queue); | |
571 | $queueEditor->update(['packageID' => $package->packageID]); | |
572 | ||
573 | // reload queue | |
574 | $this->queue = new PackageInstallationQueue($this->queue->queueID); | |
575 | $this->package = null; | |
576 | ||
577 | if ($package->isApplication) { | |
578 | $host = \str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost()); | |
579 | $path = RouteHandler::getPath(['acp']); | |
580 | ||
c079b5ec TD |
581 | $isTainted = 1; |
582 | if ($this->getPackage()->package == 'com.woltlab.wcf') { | |
583 | // com.woltlab.wcf is special, because promptPackageDir() will not be executed. | |
584 | $isTainted = 0; | |
585 | } | |
586 | ||
a9229942 TD |
587 | // insert as application |
588 | ApplicationEditor::create([ | |
589 | 'domainName' => $host, | |
590 | 'domainPath' => $path, | |
591 | 'cookieDomain' => $host, | |
592 | 'packageID' => $package->packageID, | |
c079b5ec | 593 | 'isTainted' => $isTainted, |
a9229942 TD |
594 | ]); |
595 | } | |
596 | } | |
597 | ||
598 | // save excluded packages | |
599 | if (\count($this->getArchive()->getExcludedPackages())) { | |
600 | $sql = "INSERT INTO wcf" . WCF_N . "_package_exclusion | |
601 | (packageID, excludedPackage, excludedPackageVersion) | |
602 | VALUES (?, ?, ?)"; | |
603 | $statement = WCF::getDB()->prepareStatement($sql); | |
604 | ||
605 | foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) { | |
606 | $statement->execute([ | |
607 | $this->queue->packageID, | |
608 | $excludedPackage['name'], | |
609 | !empty($excludedPackage['version']) ? $excludedPackage['version'] : '', | |
610 | ]); | |
611 | } | |
612 | } | |
613 | ||
614 | // save compatible versions | |
615 | if (!empty($this->getArchive()->getCompatibleVersions())) { | |
616 | $sql = "INSERT INTO wcf" . WCF_N . "_package_compatibility | |
617 | (packageID, version) | |
618 | VALUES (?, ?)"; | |
619 | $statement = WCF::getDB()->prepareStatement($sql); | |
620 | ||
621 | foreach ($this->getArchive()->getCompatibleVersions() as $version) { | |
622 | $statement->execute([ | |
623 | $this->queue->packageID, | |
624 | $version, | |
625 | ]); | |
626 | } | |
627 | } | |
628 | ||
629 | // insert requirements and dependencies | |
630 | $requirements = $this->getArchive()->getAllExistingRequirements(); | |
631 | if (!empty($requirements)) { | |
632 | $sql = "INSERT INTO wcf" . WCF_N . "_package_requirement | |
633 | (packageID, requirement) | |
634 | VALUES (?, ?)"; | |
635 | $statement = WCF::getDB()->prepareStatement($sql); | |
636 | ||
13b11e4c | 637 | foreach ($requirements as $possibleRequirements) { |
a9229942 TD |
638 | $requirement = \array_shift($possibleRequirements); |
639 | ||
640 | $statement->execute([ | |
641 | $this->queue->packageID, | |
642 | $requirement['packageID'], | |
643 | ]); | |
644 | } | |
645 | } | |
646 | ||
647 | if ( | |
648 | $this->getPackage()->isApplication | |
649 | && $this->getPackage()->package != 'com.woltlab.wcf' | |
650 | && $this->getAction() == 'install' | |
651 | && empty($this->getPackage()->packageDir) | |
652 | ) { | |
653 | $document = $this->promptPackageDir($applicationDirectory); | |
654 | if ($document !== null && $document instanceof FormDocument) { | |
655 | $installationStep->setDocument($document); | |
656 | } | |
657 | ||
658 | $installationStep->setSplitNode(); | |
659 | } | |
660 | ||
661 | return $installationStep; | |
662 | } | |
663 | ||
664 | /** | |
665 | * Creates a new package based on the given data and returns it. | |
666 | * | |
667 | * @param array $packageData | |
668 | * @return Package | |
669 | * @since 5.2 | |
670 | */ | |
671 | protected function createPackage(array $packageData) | |
672 | { | |
673 | return PackageEditor::create($packageData); | |
674 | } | |
675 | ||
676 | /** | |
677 | * Saves the localized package info. | |
678 | */ | |
679 | protected function saveLocalizedPackageInfos() | |
680 | { | |
681 | $package = new Package($this->queue->packageID); | |
682 | ||
683 | // localize package information | |
684 | $sql = "INSERT INTO wcf" . WCF_N . "_language_item | |
685 | (languageID, languageItem, languageItemValue, languageCategoryID, packageID) | |
686 | VALUES (?, ?, ?, ?, ?)"; | |
687 | $statement = WCF::getDB()->prepareStatement($sql); | |
688 | ||
689 | // get language list | |
690 | $languageList = new LanguageList(); | |
691 | $languageList->readObjects(); | |
692 | ||
693 | // workaround for WCFSetup | |
694 | if (!PACKAGE_ID) { | |
695 | $sql = "SELECT * | |
696 | FROM wcf" . WCF_N . "_language_category | |
697 | WHERE languageCategory = ?"; | |
698 | $statement2 = WCF::getDB()->prepareStatement($sql); | |
699 | $statement2->execute(['wcf.acp.package']); | |
700 | $languageCategory = $statement2->fetchObject(LanguageCategory::class); | |
701 | } else { | |
702 | $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.acp.package'); | |
703 | } | |
704 | ||
705 | // save package name | |
706 | $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName'); | |
707 | ||
708 | // save package description | |
709 | $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription'); | |
710 | ||
711 | // update description and name | |
712 | $packageEditor = new PackageEditor($package); | |
713 | $packageEditor->update([ | |
714 | 'packageDescription' => 'wcf.acp.package.packageDescription.package' . $this->queue->packageID, | |
715 | 'packageName' => 'wcf.acp.package.packageName.package' . $this->queue->packageID, | |
716 | ]); | |
717 | } | |
718 | ||
719 | /** | |
720 | * Saves a localized package info. | |
721 | * | |
722 | * @param PreparedStatement $statement | |
723 | * @param LanguageList $languageList | |
724 | * @param LanguageCategory $languageCategory | |
725 | * @param Package $package | |
726 | * @param string $infoName | |
727 | */ | |
728 | protected function saveLocalizedPackageInfo( | |
729 | PreparedStatement $statement, | |
730 | $languageList, | |
731 | LanguageCategory $languageCategory, | |
732 | Package $package, | |
733 | $infoName | |
734 | ) { | |
735 | $infoValues = $this->getArchive()->getPackageInfo($infoName); | |
736 | ||
737 | // get default value for languages without specified information | |
738 | $defaultValue = ''; | |
739 | if (isset($infoValues['default'])) { | |
740 | $defaultValue = $infoValues['default']; | |
741 | } elseif (isset($infoValues['en'])) { | |
742 | // fallback to English | |
743 | $defaultValue = $infoValues['en']; | |
744 | } elseif (isset($infoValues[WCF::getLanguage()->getFixedLanguageCode()])) { | |
745 | // fallback to the language of the current user | |
746 | $defaultValue = $infoValues[WCF::getLanguage()->getFixedLanguageCode()]; | |
747 | } elseif ($infoName == 'packageName') { | |
748 | // fallback to the package identifier for the package name | |
749 | $defaultValue = $this->getArchive()->getPackageInfo('name'); | |
750 | } | |
751 | ||
752 | foreach ($languageList as $language) { | |
753 | $value = $defaultValue; | |
754 | if (isset($infoValues[$language->languageCode])) { | |
755 | $value = $infoValues[$language->languageCode]; | |
756 | } | |
757 | ||
758 | $statement->execute([ | |
759 | $language->languageID, | |
760 | 'wcf.acp.package.' . $infoName . '.package' . $package->packageID, | |
761 | $value, | |
762 | $languageCategory->languageCategoryID, | |
763 | 1, | |
764 | ]); | |
765 | } | |
766 | } | |
767 | ||
768 | /** | |
769 | * Executes a package installation plugin. | |
770 | * | |
771 | * @param mixed[] $nodeData | |
772 | * @return PackageInstallationStep | |
773 | * @throws SystemException | |
774 | */ | |
775 | protected function executePIP(array $nodeData) | |
776 | { | |
777 | $step = new PackageInstallationStep(); | |
778 | ||
779 | if ($nodeData['pip'] == PackageArchive::VOID_MARKER) { | |
780 | return $step; | |
781 | } | |
782 | ||
783 | // fetch all pips associated with current PACKAGE_ID and include pips | |
784 | // previously installed by current installation queue | |
785 | $sql = "SELECT pluginName, className | |
786 | FROM wcf" . WCF_N . "_package_installation_plugin | |
787 | WHERE pluginName = ?"; | |
788 | $statement = WCF::getDB()->prepareStatement($sql); | |
789 | $statement->execute([$nodeData['pip']]); | |
790 | $row = $statement->fetchArray(); | |
791 | ||
792 | // PIP is unknown | |
793 | if (!$row || (\strcmp($nodeData['pip'], $row['pluginName']) !== 0)) { | |
794 | throw new SystemException("unable to find package installation plugin '" . $nodeData['pip'] . "'"); | |
795 | } | |
796 | ||
797 | // valdidate class definition | |
798 | $className = $row['className']; | |
799 | if (!\class_exists($className)) { | |
800 | throw new SystemException("unable to find class '" . $className . "'"); | |
801 | } | |
802 | ||
803 | // set default value | |
804 | if (empty($nodeData['value'])) { | |
805 | $defaultValue = \call_user_func([$className, 'getDefaultFilename']); | |
806 | if ($defaultValue) { | |
807 | $nodeData['value'] = $defaultValue; | |
808 | } | |
809 | } | |
810 | ||
811 | $plugin = new $className($this, $nodeData); | |
812 | ||
813 | if (!($plugin instanceof IPackageInstallationPlugin)) { | |
814 | throw new ImplementationException($className, IPackageInstallationPlugin::class); | |
815 | } | |
816 | ||
817 | // execute PIP | |
818 | $document = null; | |
819 | try { | |
820 | $document = $plugin->{$this->action}(); | |
821 | } catch (SplitNodeException $e) { | |
822 | $step->setSplitNode($e); | |
823 | } | |
824 | ||
825 | if ($document !== null && ($document instanceof FormDocument)) { | |
826 | $step->setDocument($document); | |
827 | $step->setSplitNode(); | |
828 | } | |
829 | ||
830 | return $step; | |
831 | } | |
832 | ||
833 | /** | |
834 | * Displays a list to select optional packages or installs selection. | |
835 | * | |
836 | * @param string $currentNode | |
837 | * @param array $nodeData | |
838 | * @return PackageInstallationStep | |
839 | */ | |
840 | protected function selectOptionalPackages($currentNode, array $nodeData) | |
841 | { | |
842 | $installationStep = new PackageInstallationStep(); | |
843 | ||
844 | $document = $this->promptOptionalPackages($nodeData); | |
845 | if ($document !== null && $document instanceof FormDocument) { | |
846 | $installationStep->setDocument($document); | |
847 | $installationStep->setSplitNode(); | |
848 | } // insert new nodes for each package | |
849 | elseif (\is_array($document)) { | |
850 | // get target child node | |
851 | $node = $currentNode; | |
852 | $queue = $this->queue; | |
853 | $shiftNodes = false; | |
854 | ||
855 | foreach ($nodeData as $package) { | |
856 | if (\in_array($package['package'], $document)) { | |
857 | // ignore uninstallable packages | |
858 | if (!$package['isInstallable']) { | |
859 | continue; | |
860 | } | |
861 | ||
862 | if (!$shiftNodes) { | |
863 | $this->nodeBuilder->shiftNodes($currentNode, 'tempNode'); | |
864 | $shiftNodes = true; | |
865 | } | |
866 | ||
867 | $queue = PackageInstallationQueueEditor::create([ | |
868 | 'parentQueueID' => $queue->queueID, | |
869 | 'processNo' => $this->queue->processNo, | |
870 | 'userID' => WCF::getUser()->userID, | |
871 | 'package' => $package['package'], | |
872 | 'packageName' => $package['packageName'], | |
873 | 'archive' => $package['archive'], | |
874 | 'action' => $queue->action, | |
875 | ]); | |
876 | ||
877 | $installation = new self($queue); | |
878 | $installation->nodeBuilder->setParentNode($node); | |
879 | $installation->nodeBuilder->buildNodes(); | |
880 | $node = $installation->nodeBuilder->getCurrentNode(); | |
881 | } else { | |
882 | // remove archive | |
883 | @\unlink($package['archive']); | |
884 | } | |
885 | } | |
886 | ||
887 | // shift nodes | |
888 | if ($shiftNodes) { | |
889 | $this->nodeBuilder->shiftNodes('tempNode', $node); | |
890 | } | |
891 | } | |
892 | ||
893 | return $installationStep; | |
894 | } | |
895 | ||
896 | /** | |
897 | * Extracts files from .tar(.gz) archive and installs them | |
898 | * | |
899 | * @param string $targetDir | |
900 | * @param string $sourceArchive | |
901 | * @param IFileHandler $fileHandler | |
902 | * @return Installer | |
903 | */ | |
904 | public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) | |
905 | { | |
906 | return new Installer($targetDir, $sourceArchive, $fileHandler); | |
907 | } | |
908 | ||
909 | /** | |
910 | * Returns current package. | |
911 | * | |
912 | * @return \wcf\data\package\Package | |
913 | */ | |
914 | public function getPackage() | |
915 | { | |
916 | if ($this->package === null) { | |
917 | $this->package = new Package($this->queue->packageID); | |
918 | } | |
919 | ||
920 | return $this->package; | |
921 | } | |
922 | ||
923 | /** | |
924 | * Prompts for a text input for package directory (applies for applications only) | |
925 | * | |
926 | * @param string $applicationDirectory | |
c0b28aa2 | 927 | * @return FormDocument|null |
a9229942 TD |
928 | */ |
929 | protected function promptPackageDir($applicationDirectory) | |
930 | { | |
931 | // check for pre-defined directories originating from WCFSetup | |
932 | $directory = WCF::getSession()->getVar('__wcfSetup_directories'); | |
933 | $abbreviation = Package::getAbbreviation($this->getPackage()->package); | |
934 | if ($directory !== null) { | |
935 | $directory = $directory[$abbreviation] ?? null; | |
936 | } elseif ( | |
937 | ENABLE_ENTERPRISE_MODE | |
938 | && \defined('ENTERPRISE_MODE_APP_DIRECTORIES') | |
939 | && \is_array(ENTERPRISE_MODE_APP_DIRECTORIES) | |
940 | ) { | |
941 | $directory = ENTERPRISE_MODE_APP_DIRECTORIES[$abbreviation] ?? null; | |
942 | } | |
943 | ||
944 | if ($directory === null && !PackageInstallationFormManager::findForm($this->queue, 'packageDir')) { | |
945 | $container = new GroupFormElementContainer(); | |
946 | $packageDir = new TextInputFormElement($container); | |
947 | $packageDir->setName('packageDir'); | |
948 | $packageDir->setLabel(WCF::getLanguage()->get('wcf.acp.package.packageDir.input')); | |
949 | ||
950 | // check if there are packages installed in a parent | |
951 | // directory of WCF, or if packages are below it | |
952 | $sql = "SELECT packageDir | |
953 | FROM wcf" . WCF_N . "_package | |
954 | WHERE packageDir <> ''"; | |
955 | $statement = WCF::getDB()->prepareStatement($sql); | |
956 | $statement->execute(); | |
957 | ||
958 | $isParent = null; | |
959 | while ($column = $statement->fetchColumn()) { | |
960 | if ($isParent !== null) { | |
961 | continue; | |
962 | } | |
963 | ||
964 | if (\preg_match('~^\.\./[^\.]~', $column)) { | |
965 | $isParent = false; | |
966 | } elseif (\mb_strpos($column, '.') !== 0) { | |
967 | $isParent = true; | |
968 | } | |
969 | } | |
970 | ||
971 | $defaultPath = WCF_DIR; | |
972 | if ($isParent === false) { | |
973 | $defaultPath = \dirname(WCF_DIR); | |
974 | } | |
975 | if (!$applicationDirectory) { | |
976 | $applicationDirectory = Package::getAbbreviation($this->getPackage()->package); | |
977 | } | |
978 | $defaultPath = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator($defaultPath)) . $applicationDirectory . '/'; | |
979 | ||
980 | $packageDir->setValue($defaultPath); | |
981 | $container->appendChild($packageDir); | |
982 | ||
983 | $document = new FormDocument('packageDir'); | |
984 | $document->appendContainer($container); | |
985 | ||
986 | PackageInstallationFormManager::registerForm($this->queue, $document); | |
987 | ||
988 | return $document; | |
989 | } else { | |
990 | if ($directory !== null) { | |
991 | $document = null; | |
992 | $packageDir = $directory; | |
993 | } else { | |
994 | $document = PackageInstallationFormManager::getForm($this->queue, 'packageDir'); | |
995 | $document->handleRequest(); | |
996 | $packageDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(FileUtil::unifyDirSeparator( | |
997 | $document->getValue('packageDir') | |
998 | ))); | |
999 | if ($packageDir === '/') { | |
1000 | $packageDir = ''; | |
1001 | } | |
1002 | } | |
1003 | ||
1004 | if ($packageDir !== null) { | |
1005 | // validate package dir | |
1006 | if ($document !== null && \file_exists($packageDir . 'global.php')) { | |
1007 | $document->setError( | |
1008 | 'packageDir', | |
1009 | WCF::getLanguage()->get('wcf.acp.package.packageDir.notAvailable') | |
1010 | ); | |
1011 | ||
1012 | return $document; | |
1013 | } | |
1014 | ||
1015 | // set package dir | |
1016 | $packageEditor = new PackageEditor($this->getPackage()); | |
1017 | $packageEditor->update([ | |
1018 | 'packageDir' => FileUtil::getRelativePath(WCF_DIR, $packageDir), | |
1019 | ]); | |
1020 | ||
1021 | // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are | |
1022 | // faked and differ from the real filesystem path | |
1023 | if (PACKAGE_ID) { | |
1024 | $wcfDomainPath = ApplicationHandler::getInstance()->getWCF()->domainPath; | |
1025 | } else { | |
1026 | $sql = "SELECT domainPath | |
1027 | FROM wcf" . WCF_N . "_application | |
1028 | WHERE packageID = ?"; | |
1029 | $statement = WCF::getDB()->prepareStatement($sql); | |
1030 | $statement->execute([1]); | |
1031 | $row = $statement->fetchArray(); | |
1032 | ||
1033 | $wcfDomainPath = $row['domainPath']; | |
1034 | } | |
1035 | ||
1036 | $documentRoot = \substr( | |
1037 | FileUtil::unifyDirSeparator(WCF_DIR), | |
1038 | 0, | |
1039 | -\strlen(FileUtil::unifyDirSeparator($wcfDomainPath)) | |
1040 | ); | |
1041 | $domainPath = FileUtil::getRelativePath($documentRoot, $packageDir); | |
1042 | if ($domainPath === './') { | |
1043 | // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory | |
1044 | $domainPath = '/'; | |
1045 | } | |
1046 | ||
1047 | $domainPath = FileUtil::addLeadingSlash($domainPath); | |
1048 | ||
bd2e2531 | 1049 | // update application path and untaint application |
a9229942 TD |
1050 | $application = new Application($this->getPackage()->packageID); |
1051 | $applicationEditor = new ApplicationEditor($application); | |
bd2e2531 TD |
1052 | $applicationEditor->update([ |
1053 | 'domainPath' => $domainPath, | |
1054 | 'isTainted' => 0, | |
1055 | ]); | |
a9229942 TD |
1056 | |
1057 | // create directory and set permissions | |
1058 | @\mkdir($packageDir, 0777, true); | |
1059 | FileUtil::makeWritable($packageDir); | |
1060 | } | |
1061 | ||
c0b28aa2 | 1062 | return null; |
a9229942 TD |
1063 | } |
1064 | } | |
1065 | ||
1066 | /** | |
1067 | * Prompts a selection of optional packages. | |
1068 | * | |
1069 | * @param string[][] $packages | |
1070 | * @return mixed | |
1071 | */ | |
1072 | protected function promptOptionalPackages(array $packages) | |
1073 | { | |
1074 | if (!PackageInstallationFormManager::findForm($this->queue, 'optionalPackages')) { | |
1075 | $container = new MultipleSelectionFormElementContainer(); | |
1076 | $container->setName('optionalPackages'); | |
1077 | $container->setLabel(WCF::getLanguage()->get('wcf.acp.package.optionalPackages')); | |
1078 | $container->setDescription(WCF::getLanguage()->get('wcf.acp.package.optionalPackages.description')); | |
1079 | ||
1080 | foreach ($packages as $package) { | |
1081 | $optionalPackage = new MultipleSelectionFormElement($container); | |
1082 | $optionalPackage->setName('optionalPackages'); | |
1083 | $optionalPackage->setLabel($package['packageName']); | |
1084 | $optionalPackage->setValue($package['package']); | |
1085 | $optionalPackage->setDescription($package['packageDescription']); | |
1086 | if (!$package['isInstallable']) { | |
1087 | $optionalPackage->setDisabledMessage( | |
1088 | WCF::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements') | |
1089 | ); | |
1090 | } | |
1091 | ||
1092 | $container->appendChild($optionalPackage); | |
1093 | } | |
1094 | ||
1095 | $document = new FormDocument('optionalPackages'); | |
1096 | $document->appendContainer($container); | |
1097 | ||
1098 | PackageInstallationFormManager::registerForm($this->queue, $document); | |
1099 | ||
1100 | return $document; | |
1101 | } else { | |
1102 | $document = PackageInstallationFormManager::getForm($this->queue, 'optionalPackages'); | |
1103 | $document->handleRequest(); | |
1104 | ||
1105 | return $document->getValue('optionalPackages'); | |
1106 | } | |
1107 | } | |
1108 | ||
1109 | /** | |
1110 | * Returns current package id. | |
1111 | * | |
1112 | * @return int | |
1113 | */ | |
1114 | public function getPackageID() | |
1115 | { | |
1116 | return $this->queue->packageID; | |
1117 | } | |
1118 | ||
1119 | /** | |
1120 | * Returns current package name. | |
1121 | * | |
1122 | * @return string package name | |
1123 | * @since 3.0 | |
1124 | */ | |
1125 | public function getPackageName() | |
1126 | { | |
1127 | return $this->queue->packageName; | |
1128 | } | |
1129 | ||
1130 | /** | |
1131 | * Returns current package installation type. | |
1132 | * | |
1133 | * @return string | |
1134 | */ | |
1135 | public function getAction() | |
1136 | { | |
1137 | return $this->action; | |
1138 | } | |
1139 | ||
1140 | /** | |
1141 | * Opens the package installation queue and | |
1142 | * starts the installation, update or uninstallation of the first entry. | |
1143 | * | |
1144 | * @param int $parentQueueID | |
1145 | * @param int $processNo | |
1146 | */ | |
1147 | public static function openQueue($parentQueueID = 0, $processNo = 0) | |
1148 | { | |
1149 | $conditions = new PreparedStatementConditionBuilder(); | |
1150 | $conditions->add("userID = ?", [WCF::getUser()->userID]); | |
1151 | $conditions->add("parentQueueID = ?", [$parentQueueID]); | |
1152 | if ($processNo != 0) { | |
1153 | $conditions->add("processNo = ?", [$processNo]); | |
1154 | } | |
1155 | $conditions->add("done = ?", [0]); | |
1156 | ||
1157 | $sql = "SELECT * | |
1158 | FROM wcf" . WCF_N . "_package_installation_queue | |
1159 | " . $conditions . " | |
1160 | ORDER BY queueID ASC"; | |
1161 | $statement = WCF::getDB()->prepareStatement($sql); | |
1162 | $statement->execute($conditions->getParameters()); | |
1163 | $packageInstallation = $statement->fetchArray(); | |
1164 | ||
1165 | if (!isset($packageInstallation['queueID'])) { | |
1166 | $url = LinkHandler::getInstance()->getLink('PackageList'); | |
1167 | HeaderUtil::redirect($url); | |
1168 | ||
1169 | exit; | |
1170 | } else { | |
1171 | $url = LinkHandler::getInstance()->getLink( | |
1172 | 'PackageInstallationConfirm', | |
1173 | [], | |
1174 | 'queueID=' . $packageInstallation['queueID'] | |
1175 | ); | |
1176 | HeaderUtil::redirect($url); | |
1177 | ||
1178 | exit; | |
1179 | } | |
1180 | } | |
1181 | ||
1182 | /** | |
1183 | * Checks the package installation queue for outstanding entries. | |
1184 | * | |
1185 | * @return int | |
1186 | */ | |
1187 | public static function checkPackageInstallationQueue() | |
1188 | { | |
1189 | $sql = "SELECT queueID | |
1190 | FROM wcf" . WCF_N . "_package_installation_queue | |
1191 | WHERE userID = ? | |
1192 | AND parentQueueID = 0 | |
1193 | AND done = 0 | |
1194 | ORDER BY queueID ASC"; | |
1195 | $statement = WCF::getDB()->prepareStatement($sql); | |
1196 | $statement->execute([WCF::getUser()->userID]); | |
1197 | $row = $statement->fetchArray(); | |
1198 | ||
1199 | if (!$row) { | |
1200 | return 0; | |
1201 | } | |
1202 | ||
1203 | return $row['queueID']; | |
1204 | } | |
1205 | ||
1206 | /** | |
1207 | * Executes post-setup actions. | |
1208 | */ | |
1209 | public function completeSetup() | |
1210 | { | |
1211 | // remove archives | |
1212 | $sql = "SELECT archive | |
1213 | FROM wcf" . WCF_N . "_package_installation_queue | |
1214 | WHERE processNo = ?"; | |
1215 | $statement = WCF::getDB()->prepareStatement($sql); | |
1216 | $statement->execute([$this->queue->processNo]); | |
1217 | while ($row = $statement->fetchArray()) { | |
1218 | @\unlink($row['archive']); | |
1219 | } | |
1220 | ||
1221 | // delete queues | |
1222 | $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_queue | |
1223 | WHERE processNo = ?"; | |
1224 | $statement = WCF::getDB()->prepareStatement($sql); | |
1225 | $statement->execute([$this->queue->processNo]); | |
1226 | ||
1227 | // clear language files once whole installation is completed | |
1228 | LanguageEditor::deleteLanguageFiles(); | |
1229 | ||
1230 | // reset all caches | |
1231 | CacheHandler::getInstance()->flushAll(); | |
1232 | } | |
1233 | ||
1234 | /** | |
1235 | * Updates queue information. | |
1236 | */ | |
1237 | public function updatePackage() | |
1238 | { | |
1239 | if (empty($this->queue->packageName)) { | |
1240 | $queueEditor = new PackageInstallationQueueEditor($this->queue); | |
1241 | $queueEditor->update([ | |
1242 | 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName'), | |
1243 | ]); | |
1244 | ||
1245 | // reload queue | |
1246 | $this->queue = new PackageInstallationQueue($this->queue->queueID); | |
1247 | } | |
1248 | } | |
1249 | ||
1250 | /** | |
1251 | * Validates specific php requirements. | |
1252 | * | |
1253 | * @param array $requirements | |
1254 | * @return mixed[][] | |
1255 | */ | |
1256 | public static function validatePHPRequirements(array $requirements) | |
1257 | { | |
1258 | $errors = []; | |
1259 | ||
1260 | // validate php version | |
1261 | if (isset($requirements['version'])) { | |
1262 | $passed = false; | |
1263 | if (\version_compare(\PHP_VERSION, $requirements['version'], '>=')) { | |
1264 | $passed = true; | |
1265 | } | |
1266 | ||
1267 | if (!$passed) { | |
1268 | $errors['version'] = [ | |
1269 | 'required' => $requirements['version'], | |
1270 | 'installed' => \PHP_VERSION, | |
1271 | ]; | |
1272 | } | |
1273 | } | |
1274 | ||
1275 | // validate extensions | |
1276 | if (isset($requirements['extensions'])) { | |
1277 | foreach ($requirements['extensions'] as $extension) { | |
1278 | $passed = \extension_loaded($extension) ? true : false; | |
1279 | ||
1280 | if (!$passed) { | |
1281 | $errors['extension'][] = [ | |
1282 | 'extension' => $extension, | |
1283 | ]; | |
1284 | } | |
1285 | } | |
1286 | } | |
1287 | ||
1288 | // validate settings | |
1289 | if (isset($requirements['settings'])) { | |
1290 | foreach ($requirements['settings'] as $setting => $value) { | |
1291 | $iniValue = \ini_get($setting); | |
1292 | ||
1293 | $passed = self::compareSetting($setting, $value, $iniValue); | |
1294 | if (!$passed) { | |
1295 | $errors['setting'][] = [ | |
1296 | 'setting' => $setting, | |
1297 | 'required' => $value, | |
1298 | 'installed' => ($iniValue === false) ? '(unknown)' : $iniValue, | |
1299 | ]; | |
1300 | } | |
1301 | } | |
1302 | } | |
1303 | ||
1304 | // validate functions | |
1305 | if (isset($requirements['functions'])) { | |
1306 | foreach ($requirements['functions'] as $function) { | |
1307 | $function = \mb_strtolower($function); | |
1308 | ||
1309 | $passed = self::functionExists($function); | |
1310 | if (!$passed) { | |
1311 | $errors['function'][] = [ | |
1312 | 'function' => $function, | |
1313 | ]; | |
1314 | } | |
1315 | } | |
1316 | } | |
1317 | ||
1318 | // validate classes | |
1319 | if (isset($requirements['classes'])) { | |
1320 | foreach ($requirements['classes'] as $class) { | |
1321 | $passed = false; | |
1322 | ||
1323 | // see: http://de.php.net/manual/en/language.oop5.basic.php | |
1324 | if (\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) { | |
1325 | $globalClass = '\\' . $class; | |
1326 | ||
1327 | if (\class_exists($globalClass, false)) { | |
1328 | $passed = true; | |
1329 | } | |
1330 | } | |
1331 | ||
1332 | if (!$passed) { | |
1333 | $errors['class'][] = [ | |
1334 | 'class' => $class, | |
1335 | ]; | |
1336 | } | |
1337 | } | |
1338 | } | |
1339 | ||
1340 | return $errors; | |
1341 | } | |
1342 | ||
1343 | /** | |
1344 | * Validates if an function exists and is not blacklisted by suhosin extension. | |
1345 | * | |
1346 | * @param string $function | |
1347 | * @return bool | |
1348 | * @see http://de.php.net/manual/en/function.function-exists.php#77980 | |
1349 | */ | |
1350 | protected static function functionExists($function) | |
1351 | { | |
1352 | if (\extension_loaded('suhosin')) { | |
1353 | $blacklist = @\ini_get('suhosin.executor.func.blacklist'); | |
1354 | if (!empty($blacklist)) { | |
1355 | $blacklist = \explode(',', $blacklist); | |
1356 | foreach ($blacklist as $disabledFunction) { | |
1357 | $disabledFunction = \mb_strtolower(StringUtil::trim($disabledFunction)); | |
1358 | ||
1359 | if ($function == $disabledFunction) { | |
1360 | return false; | |
1361 | } | |
1362 | } | |
1363 | } | |
1364 | } | |
1365 | ||
1366 | return \function_exists($function); | |
1367 | } | |
1368 | ||
1369 | /** | |
1370 | * Compares settings, converting values into compareable ones. | |
1371 | * | |
1372 | * @param string $setting | |
1373 | * @param string $value | |
1374 | * @param mixed $compareValue | |
1375 | * @return bool | |
1376 | */ | |
1377 | protected static function compareSetting($setting, $value, $compareValue) | |
1378 | { | |
1379 | if ($compareValue === false) { | |
1380 | return false; | |
1381 | } | |
1382 | ||
1383 | $value = \mb_strtolower($value); | |
1384 | $trueValues = ['1', 'on', 'true']; | |
1385 | $falseValues = ['0', 'off', 'false']; | |
1386 | ||
1387 | // handle values considered as 'true' | |
1388 | if (\in_array($value, $trueValues)) { | |
1389 | return $compareValue ? true : false; | |
1390 | } // handle values considered as 'false' | |
1391 | elseif (\in_array($value, $falseValues)) { | |
1392 | return (!$compareValue) ? true : false; | |
1393 | } elseif (!\is_numeric($value)) { | |
1394 | $compareValue = self::convertShorthandByteValue($compareValue); | |
1395 | $value = self::convertShorthandByteValue($value); | |
1396 | } | |
1397 | ||
1398 | return ($compareValue >= $value) ? true : false; | |
1399 | } | |
1400 | ||
1401 | /** | |
1402 | * Converts shorthand byte values into an integer representing bytes. | |
1403 | * | |
1404 | * @param string $value | |
1405 | * @return int | |
1406 | * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes | |
1407 | */ | |
1408 | protected static function convertShorthandByteValue($value) | |
1409 | { | |
1410 | // convert into bytes | |
1411 | $lastCharacter = \mb_substr($value, -1); | |
1412 | switch ($lastCharacter) { | |
1413 | // gigabytes | |
1414 | case 'g': | |
1415 | return (int)$value * 1073741824; | |
1416 | break; | |
1417 | ||
1418 | // megabytes | |
1419 | case 'm': | |
1420 | return (int)$value * 1048576; | |
1421 | break; | |
1422 | ||
1423 | // kilobytes | |
1424 | case 'k': | |
1425 | return (int)$value * 1024; | |
1426 | break; | |
1427 | ||
1428 | default: | |
1429 | return $value; | |
1430 | break; | |
1431 | } | |
1432 | } | |
11ade432 | 1433 | } |