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 | } |