Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
49 / 49 |
| ModuleManager | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
21 | |
100.00% |
49 / 49 |
| __construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| setLoader | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
| add | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
| preRegisterEvents | |
100.00% |
1 / 1 |
2 | |
100.00% |
9 / 9 |
|||
| process | |
100.00% |
1 / 1 |
6 | |
100.00% |
14 / 14 |
|||
| __set_state | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| loadModules | |
100.00% |
1 / 1 |
8 | |
100.00% |
13 / 13 |
|||
| 1 | <?php |
| 2 | |
| 3 | // {{{ License |
| 4 | // This file is part of GNU social - https://www.gnu.org/software/social |
| 5 | // |
| 6 | // GNU social is free software: you can redistribute it and/or modify |
| 7 | // it under the terms of the GNU Affero General Public License as published by |
| 8 | // the Free Software Foundation, either version 3 of the License, or |
| 9 | // (at your option) any later version. |
| 10 | // |
| 11 | // GNU social is distributed in the hope that it will be useful, |
| 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | // GNU Affero General Public License for more details. |
| 15 | // |
| 16 | // You should have received a copy of the GNU Affero General Public License |
| 17 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
| 18 | // }}} |
| 19 | |
| 20 | /** |
| 21 | * Module and plugin loader code, one of the main features of GNU social |
| 22 | * |
| 23 | * Loads plugins from `plugins/enabled`, instances them |
| 24 | * and hooks its events |
| 25 | * |
| 26 | * @package GNUsocial |
| 27 | * @category Modules |
| 28 | * |
| 29 | * @author Hugo Sales <hugo@hsal.es> |
| 30 | * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org |
| 31 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
| 32 | */ |
| 33 | |
| 34 | namespace App\Core; |
| 35 | |
| 36 | use App\Util\Formatting; |
| 37 | use AppendIterator; |
| 38 | use FilesystemIterator; |
| 39 | use Functional as F; |
| 40 | use RecursiveDirectoryIterator; |
| 41 | use RecursiveIteratorIterator; |
| 42 | use Symfony\Component\DependencyInjection\ContainerBuilder; |
| 43 | use Symfony\Component\DependencyInjection\Reference; |
| 44 | |
| 45 | class ModuleManager |
| 46 | { |
| 47 | public function __construct() |
| 48 | { |
| 49 | if (!defined('CACHE_FILE')) { |
| 50 | define('CACHE_FILE', INSTALLDIR . '/var/cache/module_manager.php'); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | protected static $loader; |
| 55 | /** @codeCoverageIgnore */ |
| 56 | public static function setLoader($l) |
| 57 | { |
| 58 | self::$loader = $l; |
| 59 | } |
| 60 | |
| 61 | protected array $modules = []; |
| 62 | protected array $events = []; |
| 63 | |
| 64 | /** |
| 65 | * Add the $fqcn class from $path as a module |
| 66 | */ |
| 67 | public function add(string $fqcn, string $path) |
| 68 | { |
| 69 | [$type, $module] = preg_split('/\\\\/', $fqcn, 0, PREG_SPLIT_NO_EMPTY); |
| 70 | self::$loader->addPsr4("\\{$type}\\{$module}\\", dirname($path)); |
| 71 | $id = Formatting::camelCaseToSnakeCase($type . '.' . $module); |
| 72 | $obj = new $fqcn(); |
| 73 | $this->modules[$id] = $obj; |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Container-build-time step that preprocesses the registering of events |
| 78 | */ |
| 79 | public function preRegisterEvents() |
| 80 | { |
| 81 | foreach ($this->modules as $id => $obj) { |
| 82 | F\map(F\select(get_class_methods($obj), |
| 83 | F\ary(F\partial_right('App\Util\Formatting::startsWith', 'on'), 1)), |
| 84 | function (string $m) use ($obj) { |
| 85 | $ev = substr($m, 2); |
| 86 | $this->events[$ev] = $this->events[$ev] ?? []; |
| 87 | $this->events[$ev][] = [$obj, $m]; |
| 88 | } |
| 89 | ); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Compiler pass responsible for registering all modules |
| 95 | */ |
| 96 | public static function process(?ContainerBuilder $container = null) |
| 97 | { |
| 98 | $module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php')); |
| 99 | $module_manager = new self(); |
| 100 | $entity_paths = []; |
| 101 | foreach ($module_paths as $path) { |
| 102 | $type = ucfirst(preg_replace('%' . INSTALLDIR . '/(component|plugin)s/.*%', '\1', $path)); |
| 103 | $dir = dirname($path); |
| 104 | $module = basename($dir); // component or plugin |
| 105 | $fqcn = "\\{$type}\\{$module}\\{$module}"; |
| 106 | $module_manager->add($fqcn, $path); |
| 107 | if (!is_null($container) && file_exists($dir = $dir . '/Entity') && is_dir($dir)) { |
| 108 | // Happens at compile time, so it's hard to do integration testing. However, |
| 109 | // everything would break if this did :') |
| 110 | // @codeCoverageIgnoreStart |
| 111 | $entity_paths[] = $dir; |
| 112 | $container->findDefinition('doctrine.orm.default_metadata_driver')->addMethodCall( |
| 113 | 'addDriver', |
| 114 | [new Reference('app.schemadef_driver'), "{$type}\\{$module}\\Entity"] |
| 115 | ); |
| 116 | // @codeCoverageIgnoreEnd |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | if (!is_null($container)) { |
| 121 | // @codeCoverageIgnoreStart |
| 122 | $container->findDefinition('app.schemadef_driver') |
| 123 | ->addMethodCall('addPaths', ['$paths' => $entity_paths]); |
| 124 | // @codeCoverageIgnoreEnd |
| 125 | } |
| 126 | |
| 127 | $module_manager->preRegisterEvents(); |
| 128 | |
| 129 | file_put_contents(CACHE_FILE, "<?php\nreturn " . var_export($module_manager, true) . ';'); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Serialize this class, for dumping into the cache |
| 134 | * |
| 135 | * @param mixed $state |
| 136 | */ |
| 137 | public static function __set_state($state) |
| 138 | { |
| 139 | $obj = new self(); |
| 140 | $obj->modules = $state['modules']; |
| 141 | $obj->events = $state['events']; |
| 142 | return $obj; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Load the modules at runtime. In production requires the cache |
| 147 | * file to exist, in dev it rebuilds this cache |
| 148 | */ |
| 149 | public function loadModules() |
| 150 | { |
| 151 | if ($_ENV['APP_ENV'] === 'prod' && !file_exists(CACHE_FILE)) { |
| 152 | // @codeCoverageIgnoreStart |
| 153 | throw new Exception('The application needs to be compiled before using in production'); |
| 154 | // @codeCoverageIgnoreEnd |
| 155 | } else { |
| 156 | $rdi = new AppendIterator(); |
| 157 | $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/components', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS))); |
| 158 | $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/plugins', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS))); |
| 159 | $time = file_exists(CACHE_FILE) ? filemtime(CACHE_FILE) : 0; |
| 160 | |
| 161 | if ($_ENV['APP_ENV'] === 'test' || F\some($rdi, function ($e) use ($time) { return $e->getMTime() > $time; })) { |
| 162 | Log::info('Rebuilding plugin cache at runtime. This means we can\'t update DB definitions'); |
| 163 | self::process(); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | $obj = require CACHE_FILE; |
| 168 | |
| 169 | foreach ($obj->events as $event => $callables) { |
| 170 | foreach ($callables as $callable) { |
| 171 | Event::addHandler($event, $callable); |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | } |