1 <?php
2
3 4 5 6 7
8 abstract class rex_package_manager
9 {
10 use rex_factory_trait;
11
12 13 14
15 protected $package;
16
17 protected $generatePackageOrder = true;
18
19 protected $message;
20
21 private $i18nPrefix;
22
23 24 25 26 27 28
29 protected function __construct(rex_package $package, $i18nPrefix)
30 {
31 $this->package = $package;
32 $this->i18nPrefix = $i18nPrefix;
33 }
34
35 36 37 38 39 40 41
42 public static function factory(rex_package $package)
43 {
44 if (static::class == self::class) {
45 $class = $package instanceof rex_plugin ? 'rex_plugin_manager' : 'rex_addon_manager';
46 return $class::factory($package);
47 }
48 $class = static::getFactoryClass();
49 return new $class($package);
50 }
51
52 53 54 55 56
57 public function getMessage()
58 {
59 return $this->message;
60 }
61
62 63 64 65 66 67 68 69 70
71 public function install($installDump = true)
72 {
73 try {
74
75 $install_dir = $this->package->getPath();
76 if (!rex_dir::isWritable($install_dir)) {
77 throw new rex_functional_exception($this->i18n('dir_not_writable', $install_dir));
78 }
79
80
81 $packageFile = $this->package->getPath(rex_package::FILE_PACKAGE);
82 if (!is_readable($packageFile)) {
83 throw new rex_functional_exception($this->i18n('missing_yml_file'));
84 }
85 try {
86 rex_file::getConfig($packageFile);
87 } catch (rex_yaml_parse_exception $e) {
88 throw new rex_functional_exception($this->i18n('invalid_yml_file') . ' ' . $e->getMessage());
89 }
90 $packageId = $this->package->getProperty('package');
91 if ($packageId === null) {
92 throw new rex_functional_exception($this->i18n('missing_id', $this->package->getPackageId()));
93 }
94 if ($packageId != $this->package->getPackageId()) {
95 $parts = explode('/', $packageId, 2);
96 throw new rex_functional_exception($this->wrongPackageId($parts[0], isset($parts[1]) ? $parts[1] : null));
97 }
98 if ($this->package->getVersion() === null) {
99 throw new rex_functional_exception($this->i18n('missing_version'));
100 }
101
102
103 $message = '';
104 if (!$this->checkRequirements()) {
105 $message = $this->message;
106 }
107 if (!$this->checkConflicts()) {
108 $message .= $this->message;
109 }
110 if ($message) {
111 throw new rex_functional_exception($message);
112 }
113
114 $reinstall = $this->package->getProperty('install');
115 $this->package->setProperty('install', true);
116
117 rex_autoload::addDirectory($this->package->getPath('lib'));
118 rex_autoload::addDirectory($this->package->getPath('vendor'));
119 rex_i18n::addDirectory($this->package->getPath('lang'));
120
121
122 if (is_readable($this->package->getPath(rex_package::FILE_INSTALL))) {
123 $this->package->includeFile(rex_package::FILE_INSTALL);
124
125 if (($instmsg = $this->package->getProperty('installmsg', '')) != '') {
126 throw new rex_functional_exception($instmsg);
127 }
128 if (!$this->package->isInstalled()) {
129 throw new rex_functional_exception($this->i18n('no_reason'));
130 }
131 }
132
133
134 $installSql = $this->package->getPath(rex_package::FILE_INSTALL_SQL);
135 if ($installDump === true && is_readable($installSql)) {
136 rex_sql_util::importDump($installSql);
137 }
138
139 if (!$reinstall) {
140 $this->package->setProperty('status', true);
141 }
142 $this->saveConfig();
143 if ($this->generatePackageOrder) {
144 self::generatePackageOrder();
145 }
146
147
148 $assets = $this->package->getPath('assets');
149 if (is_dir($assets)) {
150 if (!rex_dir::copy($assets, $this->package->getAssetsPath())) {
151 throw new rex_functional_exception($this->i18n('install_cant_copy_files'));
152 }
153 }
154
155 $this->message = $this->i18n($reinstall ? 'reinstalled' : 'installed', $this->package->getName());
156
157 return true;
158 } catch (rex_functional_exception $e) {
159 $this->message = $e->getMessage();
160 } catch (rex_sql_exception $e) {
161 $this->message = 'SQL error: ' . $e->getMessage();
162 }
163
164 $this->package->setProperty('install', false);
165 $this->message = $this->i18n('no_install', $this->package->getName()) . '<br />' . $this->message;
166
167 return false;
168 }
169
170 171 172 173 174 175 176 177 178
179 public function uninstall($installDump = true)
180 {
181 $isActivated = $this->package->isAvailable();
182 if ($isActivated && !$this->deactivate()) {
183 return false;
184 }
185
186 try {
187 $this->package->setProperty('install', false);
188
189
190 if (is_readable($this->package->getPath(rex_package::FILE_UNINSTALL))) {
191 if (!$isActivated) {
192 rex_i18n::addDirectory($this->package->getPath('lang'));
193 }
194
195 $this->package->includeFile(rex_package::FILE_UNINSTALL);
196
197 if (($instmsg = $this->package->getProperty('installmsg', '')) != '') {
198 throw new rex_functional_exception($instmsg);
199 }
200 if ($this->package->isInstalled()) {
201 throw new rex_functional_exception($this->i18n('no_reason'));
202 }
203 }
204
205
206 $uninstallSql = $this->package->getPath(rex_package::FILE_UNINSTALL_SQL);
207 if ($installDump === true && is_readable($uninstallSql)) {
208 rex_sql_util::importDump($uninstallSql);
209 }
210
211
212 $assets = $this->package->getAssetsPath();
213 if (is_dir($assets) && !rex_dir::delete($assets)) {
214 throw new rex_functional_exception($this->i18n('install_cant_delete_files'));
215 }
216
217
218 $this->package->clearCache();
219
220 rex_config::removeNamespace($this->package->getPackageId());
221
222 $this->saveConfig();
223 $this->message = $this->i18n('uninstalled', $this->package->getName());
224
225 return true;
226 } catch (rex_functional_exception $e) {
227 $this->message = $e->getMessage();
228 } catch (rex_sql_exception $e) {
229 $this->message = 'SQL error: ' . $e->getMessage();
230 }
231
232 $this->package->setProperty('install', true);
233 if ($isActivated) {
234 $this->package->setProperty('status', true);
235 }
236 $this->saveConfig();
237 $this->message = $this->i18n('no_uninstall', $this->package->getName()) . '<br />' . $this->message;
238
239 return false;
240 }
241
242 243 244 245 246
247 public function activate()
248 {
249 if ($this->package->isInstalled()) {
250 $state = '';
251 if (!$this->checkRequirements()) {
252 $state .= $this->message;
253 }
254 if (!$this->checkConflicts()) {
255 $state .= $this->message;
256 }
257 $state = $state ?: true;
258
259 if ($state === true) {
260 $this->package->setProperty('status', true);
261 $this->saveConfig();
262 }
263 if ($state === true && $this->generatePackageOrder) {
264 self::generatePackageOrder();
265 }
266 } else {
267 $state = $this->i18n('not_installed', $this->package->getName());
268 }
269
270 if ($state !== true) {
271
272 $this->package->setProperty('status', false);
273 $this->message = $this->i18n('no_activation', $this->package->getName()) . '<br />' . $state;
274 return false;
275 }
276
277 $this->message = $this->i18n('activated', $this->package->getName());
278 return true;
279 }
280
281 282 283 284 285
286 public function deactivate()
287 {
288 $state = $this->checkDependencies();
289
290 if ($state === true) {
291 $this->package->setProperty('status', false);
292 $this->saveConfig();
293
294
295 $this->package->clearCache();
296
297
298
299 rex_autoload::removeCache();
300
301 if ($this->generatePackageOrder) {
302 self::generatePackageOrder();
303 }
304
305 $this->message = $this->i18n('deactivated', $this->package->getName());
306 return true;
307 }
308
309 $this->message = $this->i18n('no_deactivation', $this->package->getName()) . '<br />' . $this->message;
310 return false;
311 }
312
313 314 315 316 317
318 public function delete()
319 {
320 if ($this->package->isSystemPackage()) {
321 $this->message = $this->i18n('systempackage_delete_not_allowed');
322 return false;
323 }
324 $state = $this->_delete();
325 self::synchronizeWithFileSystem();
326 return $state;
327 }
328
329 330 331 332 333 334 335
336 protected function _delete($ignoreState = false)
337 {
338
339 if ($this->package->isInstalled() && !$this->uninstall() && !$ignoreState) {
340
341 return false;
342 }
343
344 if (!rex_dir::delete($this->package->getPath()) && !$ignoreState) {
345 $this->message = $this->i18n('not_deleted', $this->package->getName());
346 return false;
347 }
348
349 if (!$ignoreState) {
350 $this->saveConfig();
351 $this->message = $this->i18n('deleted', $this->package->getName());
352 }
353
354 return true;
355 }
356
357 358 359 360 361 362
363 abstract protected function wrongPackageId($addonName, $pluginName = null);
364
365 366 367 368 369
370 public function checkRequirements()
371 {
372 $requirements = $this->package->getProperty('requires', []);
373
374 if (!is_array($requirements)) {
375 $this->message = $this->i18n('requirement_wrong_format');
376
377 return false;
378 }
379
380 if (!$this->checkRedaxoRequirement(rex::getVersion())) {
381 return false;
382 }
383
384 $state = [];
385
386 if (isset($requirements['php'])) {
387 if (!is_array($requirements['php'])) {
388 $requirements['php'] = ['version' => $requirements['php']];
389 }
390 if (isset($requirements['php']['version']) && !self::matchVersionConstraints(PHP_VERSION, $requirements['php']['version'])) {
391 $state[] = $this->i18n('requirement_error_php_version', PHP_VERSION, $requirements['php']['version']);
392 }
393 if (isset($requirements['php']['extensions']) && $requirements['php']['extensions']) {
394 $extensions = (array) $requirements['php']['extensions'];
395 foreach ($extensions as $reqExt) {
396 if (is_string($reqExt) && !extension_loaded($reqExt)) {
397 $state[] = $this->i18n('requirement_error_php_extension', $reqExt);
398 }
399 }
400 }
401 }
402
403 if (empty($state)) {
404 if (isset($requirements['packages']) && is_array($requirements['packages'])) {
405 foreach ($requirements['packages'] as $package => $_) {
406 if (!$this->checkPackageRequirement($package)) {
407 $state[] = $this->message;
408 }
409 }
410 }
411 }
412
413 if (empty($state)) {
414 return true;
415 }
416 $this->message = implode('<br />', $state);
417 return false;
418 }
419
420 421 422 423 424 425 426
427 public function checkRedaxoRequirement($redaxoVersion)
428 {
429 $requirements = $this->package->getProperty('requires', []);
430 if (isset($requirements['redaxo']) && !self::matchVersionConstraints($redaxoVersion, $requirements['redaxo'])) {
431 $this->message = $this->i18n('requirement_error_redaxo_version', $redaxoVersion, $requirements['redaxo']);
432 return false;
433 }
434 return true;
435 }
436
437 438 439 440 441 442 443
444 public function checkPackageRequirement($packageId)
445 {
446 $requirements = $this->package->getProperty('requires', []);
447 if (!isset($requirements['packages'][$packageId])) {
448 return true;
449 }
450 $package = rex_package::get($packageId);
451 if (!$package->isAvailable()) {
452 $this->message = $this->i18n('requirement_error_' . $package->getType(), $packageId);
453 return false;
454 }
455 if (!self::matchVersionConstraints($package->getVersion(), $requirements['packages'][$packageId])) {
456 $this->message = $this->i18n(
457 'requirement_error_' . $package->getType() . '_version',
458 $package->getPackageId(),
459 $package->getVersion(),
460 $requirements['packages'][$packageId]
461 );
462 return false;
463 }
464 return true;
465 }
466
467 468 469 470 471
472 public function checkConflicts()
473 {
474 $state = [];
475 $conflicts = $this->package->getProperty('conflicts', []);
476
477 if (isset($conflicts['packages']) && is_array($conflicts['packages'])) {
478 foreach ($conflicts['packages'] as $package => $_) {
479 if (!$this->checkPackageConflict($package)) {
480 $state[] = $this->message;
481 }
482 }
483 }
484
485 foreach (rex_package::getAvailablePackages() as $package) {
486 $conflicts = $package->getProperty('conflicts', []);
487
488 if (!isset($conflicts['packages'][$this->package->getPackageId()])) {
489 continue;
490 }
491
492 $constraints = $conflicts['packages'][$this->package->getPackageId()];
493 if (!is_string($constraints) || !$constraints || $constraints === '*') {
494 $state[] = $this->i18n('reverse_conflict_error_' . $package->getType(), $package->getPackageId());
495 } elseif (self::matchVersionConstraints($this->package->getVersion(), $constraints)) {
496 $state[] = $this->i18n('reverse_conflict_error_' . $package->getType() . '_version', $package->getPackageId(), $constraints);
497 }
498 }
499
500 if (empty($state)) {
501 return true;
502 }
503 $this->message = implode('<br />', $state);
504 return false;
505 }
506
507 508 509 510 511 512 513
514 public function checkPackageConflict($packageId)
515 {
516 $conflicts = $this->package->getProperty('conflicts', []);
517 $package = rex_package::get($packageId);
518 if (!isset($conflicts['packages'][$packageId]) || !$package->isAvailable()) {
519 return true;
520 }
521 $constraints = $conflicts['packages'][$packageId];
522 if (!is_string($constraints) || !$constraints || $constraints === '*') {
523 $this->message = $this->i18n('conflict_error_' . $package->getType(), $package->getPackageId());
524 return false;
525 }
526 if (self::matchVersionConstraints($package->getVersion(), $constraints)) {
527 $this->message = $this->i18n('conflict_error_' . $package->getType() . '_version', $package->getPackageId(), $constraints);
528 return false;
529 }
530 return true;
531 }
532
533 534 535 536 537
538 public function checkDependencies()
539 {
540 $i18nPrefix = 'package_dependencies_error_';
541 $state = [];
542
543 foreach (rex_package::getAvailablePackages() as $package) {
544 if ($package === $this->package || $package->getAddon() === $this->package) {
545 continue;
546 }
547
548 $requirements = $package->getProperty('requires', []);
549 if (isset($requirements['packages'][$this->package->getPackageId()])) {
550 $state[] = rex_i18n::msg($i18nPrefix . $package->getType(), $package->getPackageId());
551 }
552 }
553
554 if (empty($state)) {
555 return true;
556 }
557 $this->message = implode('<br />', $state);
558 return false;
559 }
560
561 562 563 564 565 566 567
568 protected function i18n($key)
569 {
570 $args = func_get_args();
571 $key = $this->i18nPrefix . $args[0];
572 if (!rex_i18n::hasMsg($key)) {
573 $key = 'package_' . $args[0];
574 }
575 $args[0] = $key;
576
577 return call_user_func_array(['rex_i18n', 'msg'], $args);
578 }
579
580 581 582
583 public static function generatePackageOrder()
584 {
585 $early = [];
586 $normal = [];
587 $late = [];
588 $requires = [];
589 $add = function ($id) use (&$add, &$normal, &$requires) {
590 $normal[] = $id;
591 unset($requires[$id]);
592 foreach ($requires as $rp => &$ps) {
593 unset($ps[$id]);
594 if (empty($ps)) {
595 $add($rp);
596 }
597 }
598 };
599 foreach (rex_package::getAvailablePackages() as $package) {
600 $id = $package->getPackageId();
601 $load = $package->getProperty('load');
602 if ($package instanceof rex_plugin
603 && !in_array($load, ['early', 'normal', 'late'])
604 && in_array($addonLoad = $package->getAddon()->getProperty('load'), ['early', 'late'])
605 ) {
606 $load = $addonLoad;
607 }
608 if ($load === 'early') {
609 $early[] = $id;
610 } elseif ($load === 'late') {
611 $late[] = $id;
612 } else {
613 $req = $package->getProperty('requires');
614 if ($package instanceof rex_plugin) {
615 $req['packages'][$package->getAddon()->getPackageId()] = true;
616 }
617 if (isset($req['packages']) && is_array($req['packages'])) {
618 foreach ($req['packages'] as $packageId => $reqP) {
619 $package = rex_package::get($packageId);
620 if (!in_array($package, $normal) && !in_array($package->getProperty('load'), ['early', 'late'])) {
621 $requires[$id][$packageId] = true;
622 }
623 }
624 }
625 if (!isset($requires[$id])) {
626 $add($id);
627 }
628 }
629 }
630 rex::setConfig('package-order', array_merge($early, $normal, array_keys($requires), $late));
631 }
632
633 634 635
636 protected static function saveConfig()
637 {
638 $config = [];
639 foreach (rex_addon::getRegisteredAddons() as $addonName => $addon) {
640 $config[$addonName]['install'] = $addon->isInstalled();
641 $config[$addonName]['status'] = $addon->isAvailable();
642 foreach ($addon->getRegisteredPlugins() as $pluginName => $plugin) {
643 $config[$addonName]['plugins'][$pluginName]['install'] = $plugin->isInstalled();
644 $config[$addonName]['plugins'][$pluginName]['status'] = $plugin->getProperty('status');
645 }
646 }
647 rex::setConfig('package-config', $config);
648 }
649
650 651 652
653 public static function synchronizeWithFileSystem()
654 {
655 $config = rex::getConfig('package-config');
656 $addons = self::readPackageFolder(rex_path::src('addons'));
657 $registeredAddons = array_keys(rex_addon::getRegisteredAddons());
658 foreach (array_diff($registeredAddons, $addons) as $addonName) {
659 $manager = rex_addon_manager::factory(rex_addon::get($addonName));
660 $manager->_delete(true);
661 unset($config[$addonName]);
662 }
663 foreach ($addons as $addonName) {
664 if (!rex_addon::exists($addonName)) {
665 $config[$addonName]['install'] = false;
666 $config[$addonName]['status'] = false;
667 $registeredPlugins = [];
668 } else {
669 $addon = rex_addon::get($addonName);
670 $config[$addonName]['install'] = $addon->isInstalled();
671 $config[$addonName]['status'] = $addon->isAvailable();
672 $registeredPlugins = array_keys($addon->getRegisteredPlugins());
673 }
674 $plugins = self::readPackageFolder(rex_path::addon($addonName, 'plugins'));
675 foreach (array_diff($registeredPlugins, $plugins) as $pluginName) {
676 $manager = rex_plugin_manager::factory(rex_plugin::get($addonName, $pluginName));
677 $manager->_delete(true);
678 unset($config[$addonName]['plugins'][$pluginName]);
679 }
680 foreach ($plugins as $pluginName) {
681 $plugin = rex_plugin::get($addonName, $pluginName);
682 $config[$addonName]['plugins'][$pluginName]['install'] = $plugin->isInstalled();
683 $config[$addonName]['plugins'][$pluginName]['status'] = $plugin->getProperty('status');
684 }
685 if (isset($config[$addonName]['plugins']) && is_array($config[$addonName]['plugins'])) {
686 ksort($config[$addonName]['plugins']);
687 }
688 }
689 ksort($config);
690
691 rex::setConfig('package-config', $config);
692 rex_addon::initialize();
693 }
694
695 696 697 698 699 700 701 702 703 704
705 private static function matchVersionConstraints($version, $constraints)
706 {
707 $rawConstraints = array_filter(array_map('trim', explode(',', $constraints)));
708 $constraints = [];
709 foreach ($rawConstraints as $constraint) {
710 if ($constraint === '*') {
711 continue;
712 }
713
714 if (!preg_match('/^(?<op>==?|<=?|>=?|!=|~|\^|) ?(?<version>\d+(?:\.\d+)*)(?<wildcard>\.\*)?(?<prerelease>[ -.]?[a-z]+(?:[ -.]?\d+)?)?$/i', $constraint, $match)
715 || isset($match['wildcard']) && $match['wildcard'] && ($match['op'] != '' || isset($match['prerelease']) && $match['prerelease'])
716 ) {
717 throw new rex_exception('Unknown version constraint "' . $constraint . '"!');
718 }
719
720 if (isset($match['wildcard']) && $match['wildcard']) {
721 $constraints[] = ['>=', $match['version']];
722 $pos = strrpos($match['version'], '.') + 1;
723 $sub = substr($match['version'], $pos);
724 $constraints[] = ['<', substr_replace($match['version'], $sub + 1, $pos)];
725 } elseif (in_array($match['op'], ['~', '^'])) {
726 $constraints[] = ['>=', $match['version'] . (isset($match['prerelease']) ? $match['prerelease'] : '')];
727 if ('^' === $match['op'] || false === $pos = strrpos($match['version'], '.')) {
728
729 $constraints[] = ['<', ((int) $match['version'] + 1) . '-foo'];
730 } else {
731 $main = '';
732 $sub = substr($match['version'], 0, $pos);
733 if (($pos = strrpos($sub, '.')) !== false) {
734 $main = substr($sub, 0, $pos + 1);
735 $sub = substr($sub, $pos + 1);
736 }
737
738 $constraints[] = ['<', $main . ($sub + 1) . '-foo'];
739 }
740 } else {
741 $constraints[] = [$match['op'] ?: '=', $match['version'] . (isset($match['prerelease']) ? $match['prerelease'] : '')];
742 }
743 }
744
745 foreach ($constraints as $constraint) {
746 if (!rex_string::versionCompare($version, $constraint[1], $constraint[0])) {
747 return false;
748 }
749 }
750
751 return true;
752 }
753
754 755 756 757 758 759 760
761 private static function readPackageFolder($folder)
762 {
763 $packages = [];
764
765 if (is_dir($folder)) {
766 foreach (rex_finder::factory($folder)->dirsOnly() as $file) {
767 $packages[] = $file->getBasename();
768 }
769 }
770
771 return $packages;
772 }
773 }
774