Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
49 / 49
ModuleManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
21
100.00% covered (success)
100.00%
49 / 49
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 setLoader
n/a
0 / 0
1
n/a
0 / 0
 add
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 preRegisterEvents
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
9 / 9
 process
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 __set_state
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 loadModules
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
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
34namespace App\Core;
35
36use App\Util\Formatting;
37use AppendIterator;
38use FilesystemIterator;
39use Functional as F;
40use RecursiveDirectoryIterator;
41use RecursiveIteratorIterator;
42use Symfony\Component\DependencyInjection\ContainerBuilder;
43use Symfony\Component\DependencyInjection\Reference;
44
45class 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}