1 <?php
  2 
  3 /**
  4  * Manager class for packages.
  5  *
  6  * @package redaxo\core\packages
  7  */
  8 abstract class rex_package_manager
  9 {
 10     use rex_factory_trait;
 11 
 12     /**
 13      * @var rex_package
 14      */
 15     protected $package;
 16 
 17     protected $generatePackageOrder = true;
 18 
 19     protected $message;
 20 
 21     private $i18nPrefix;
 22 
 23     /**
 24      * Constructor.
 25      *
 26      * @param rex_package $package    Package
 27      * @param string      $i18nPrefix Prefix for i18n
 28      */
 29     protected function __construct(rex_package $package, $i18nPrefix)
 30     {
 31         $this->package = $package;
 32         $this->i18nPrefix = $i18nPrefix;
 33     }
 34 
 35     /**
 36      * Creates the manager for the package.
 37      *
 38      * @param rex_package $package Package
 39      *
 40      * @return static
 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      * Returns the message.
 54      *
 55      * @return string
 56      */
 57     public function getMessage()
 58     {
 59         return $this->message;
 60     }
 61 
 62     /**
 63      * Installs a package.
 64      *
 65      * @param bool $installDump When TRUE, the sql dump will be importet
 66      *
 67      * @throws rex_functional_exception
 68      *
 69      * @return bool TRUE on success, FALSE on error
 70      */
 71     public function install($installDump = true)
 72     {
 73         try {
 74             // check package directory perms
 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             // check package.yml
 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             // check requirements and conflicts
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             // include install.php
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             // import install.sql
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             // copy assets
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      * Uninstalls a package.
172      *
173      * @param bool $installDump When TRUE, the sql dump will be importet
174      *
175      * @throws rex_functional_exception
176      *
177      * @return bool TRUE on success, FALSE on error
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             // include uninstall.php
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             // import uninstall.sql
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             // delete assets
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             // clear cache of package
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      * Activates a package.
244      *
245      * @return bool TRUE on success, FALSE on error
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             // error while config generation, rollback addon status
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      * Deactivates a package.
283      *
284      * @return bool TRUE on success, FALSE on error
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             // clear cache of package
295             $this->package->clearCache();
296 
297             // reload autoload cache when addon is deactivated,
298             // so the index doesn't contain outdated class definitions
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      * Deletes a package.
315      *
316      * @return bool TRUE on success, FALSE on error
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      * Deletes a package.
331      *
332      * @param bool $ignoreState
333      *
334      * @return bool TRUE on success, FALSE on error
335      */
336     protected function _delete($ignoreState = false)
337     {
338         // if package is installed, uninstall it first
339         if ($this->package->isInstalled() && !$this->uninstall() && !$ignoreState) {
340             // message is set by uninstall()
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      * @param string $addonName
359      * @param string $pluginName
360      *
361      * @return string
362      */
363     abstract protected function wrongPackageId($addonName, $pluginName = null);
364 
365     /**
366      * Checks whether the requirements are met.
367      *
368      * @return bool
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      * Checks whether the redaxo requirement is met.
422      *
423      * @param string $redaxoVersion REDAXO version
424      *
425      * @return bool
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      * Checks whether the package requirement is met.
439      *
440      * @param string $packageId Package ID
441      *
442      * @return bool
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      * Checks whether the package is in conflict with other packages.
469      *
470      * @return bool
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      * Checks whether the package is in conflict with another package.
509      *
510      * @param string $packageId Package ID
511      *
512      * @return bool
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      * Checks if another Package which is activated, depends on the given package.
535      *
536      * @return bool
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      * Translates the given key.
563      *
564      * @param string $key Key
565      *
566      * @return string Tranlates text
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      * Generates the package order.
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      * Saves the package config.
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      * Synchronizes the packages with the file system.
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      * Checks the version of the requirement.
697      *
698      * @param string $version     Version
699      * @param string $constraints Constraint list, separated by comma
700      *
701      * @throws rex_exception
702      *
703      * @return bool
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                     // add "-foo" to get a version lower than a "-dev" version
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                     // add "-foo" to get a version lower than a "-dev" version
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      * Returns the subfolders of the given folder.
756      *
757      * @param string $folder Folder
758      *
759      * @return string[]
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