123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- <?php
- declare(strict_types = 1);
- // {{{ License
- // This file is part of GNU social - https://www.gnu.org/software/social
- //
- // GNU social is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // GNU social is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
- // }}}
- /**
- * Module and plugin loader code, one of the main features of GNU social
- *
- * Loads plugins from `plugins/enabled`, instances them
- * and hooks its events
- *
- * @package GNUsocial
- * @category Modules
- *
- * @author Hugo Sales <hugo@hsal.es>
- * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
- * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
- */
- namespace App\Core;
- use App\Kernel;
- use App\Util\Formatting;
- use AppendIterator;
- use Exception;
- use FilesystemIterator;
- use Functional as F;
- use RecursiveDirectoryIterator;
- use RecursiveIteratorIterator;
- use Symfony\Component\Config\Loader\LoaderInterface;
- use Symfony\Component\DependencyInjection\ContainerBuilder;
- use Symfony\Component\DependencyInjection\Reference;
- class ModuleManager
- {
- protected static $loader;
- /**
- * @codeCoverageIgnore
- */
- public static function setLoader($l)
- {
- self::$loader = $l;
- }
- protected array $modules = [];
- protected array $events = [];
- /**
- * Add the $fqcn class from $path as a module
- */
- public function add(string $fqcn, string $path)
- {
- [$type, $module] = preg_split('/\\\\/', $fqcn, 0, \PREG_SPLIT_NO_EMPTY);
- self::$loader->addPsr4("\\{$type}\\{$module}\\", \dirname($path));
- $id = Formatting::camelCaseToSnakeCase($type . '.' . $module);
- $obj = new $fqcn();
- $this->modules[$id] = $obj;
- }
- /**
- * Container-build-time step that preprocesses the registering of events
- */
- public function preRegisterEvents()
- {
- foreach ($this->modules as $id => $obj) {
- F\map(
- F\select(
- get_class_methods($obj),
- F\ary(F\partial_right('App\Util\Formatting::startsWith', 'on'), 1),
- ),
- function (string $m) use ($obj) {
- $ev = mb_substr($m, 2);
- $this->events[$ev] ??= [];
- $this->events[$ev][] = [$obj, $m];
- },
- );
- }
- }
- /**
- * Compiler pass responsible for registering all modules
- */
- public static function process(?ContainerBuilder $container = null)
- {
- $module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php'));
- $module_manager = new self();
- $entity_paths = [];
- foreach ($module_paths as $path) {
- $type = ucfirst(preg_replace('%' . INSTALLDIR . '/(component|plugin)s/.*%', '\1', $path));
- $dir = \dirname($path);
- $module = basename($dir); // component or plugin
- $fqcn = "\\{$type}\\{$module}\\{$module}";
- $module_manager->add($fqcn, $path);
- if (!\is_null($container) && file_exists($dir = $dir . '/Entity') && is_dir($dir)) {
- // Happens at compile time, so it's hard to do integration testing. However,
- // everything would break if this did :')
- // @codeCoverageIgnoreStart
- $entity_paths[] = $dir;
- $container->findDefinition('doctrine.orm.default_metadata_driver')->addMethodCall(
- 'addDriver',
- [new Reference('app.schemadef_driver'), "{$type}\\{$module}\\Entity"],
- );
- // @codeCoverageIgnoreEnd
- }
- }
- if (!\is_null($container)) {
- // @codeCoverageIgnoreStart
- $container->findDefinition('app.schemadef_driver')
- ->addMethodCall('addPaths', ['$paths' => $entity_paths]);
- // @codeCoverageIgnoreEnd
- }
- $module_manager->preRegisterEvents();
- file_put_contents(MODULE_CACHE_FILE, "<?php\nreturn " . var_export($module_manager, true) . ';');
- }
- /**
- * Serialize this class, for dumping into the cache
- */
- public static function __set_state($state)
- {
- $obj = new self();
- $obj->modules = $state['modules'];
- $obj->events = $state['events'];
- return $obj;
- }
- /**
- * Load the modules at runtime. In production requires the cache
- * file to exist, in dev it rebuilds this cache
- */
- public function loadModules()
- {
- if ($_ENV['APP_ENV'] === 'prod' && !file_exists(MODULE_CACHE_FILE)) {
- // @codeCoverageIgnoreStart
- throw new Exception('The application needs to be compiled before using in production');
- // @codeCoverageIgnoreEnd
- } else {
- $rdi = new AppendIterator();
- $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/components', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)));
- $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/plugins', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)));
- $time = file_exists(MODULE_CACHE_FILE) ? filemtime(MODULE_CACHE_FILE) : 0;
- if ($_ENV['APP_ENV'] === 'test' || F\some($rdi, fn ($e) => $e->getMTime() > $time)) {
- Log::info('Rebuilding plugin cache at runtime. This means we can\'t update DB definitions');
- self::process();
- }
- }
- $obj = require MODULE_CACHE_FILE;
- foreach ($obj->modules as $module) {
- $module->loadConfig();
- }
- foreach ($obj->events as $event => $callables) {
- foreach ($callables as $callable) {
- Event::addHandler($event, $callable);
- }
- }
- }
- /**
- * Load Module settings and setup Twig template load paths
- *
- * Happens at "compile time"
- *
- * @codeCoverageIgnore
- */
- public static function configureContainer(ContainerBuilder $container, LoaderInterface $loader): array
- {
- $template_modules = array_merge(glob(INSTALLDIR . '/components/*/templates'), glob(INSTALLDIR . '/plugins/*/templates'));
- // Regular template location
- $templates = ['%kernel.project_dir%/templates' => 'default_path', '%kernel.project_dir%/public' => 'public_path'];
- // Path => alias
- foreach ($template_modules as $mod) {
- $templates[$mod] = null;
- }
- $container->loadFromExtension('twig', ['paths' => $templates]);
- $modules = array_merge(glob(INSTALLDIR . '/components/*'), glob(INSTALLDIR . '/plugins/*'));
- $module_configs = [];
- foreach ($modules as $mod) {
- $path = "{$mod}/config" . Kernel::CONFIG_EXTS;
- $loader->load($path, 'glob'); // Is supposed to, but doesn't return anything that would let us identify if loading worked
- foreach (explode(',', mb_substr(Kernel::CONFIG_EXTS, 2, -1)) as $ext) {
- if (file_exists("{$mod}/config.{$ext}")) {
- $module_configs[basename(mb_strtolower($mod))] = basename(\dirname(mb_strtolower($mod)));
- break;
- }
- }
- }
- return $module_configs;
- }
- }
|