ModuleManager.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. declare(strict_types = 1);
  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. * Module and plugin loader code, one of the main features of GNU social
  21. *
  22. * Loads plugins from `plugins/enabled`, instances them
  23. * and hooks its events
  24. *
  25. * @package GNUsocial
  26. * @category Modules
  27. *
  28. * @author Hugo Sales <hugo@hsal.es>
  29. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  30. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  31. */
  32. namespace App\Core;
  33. use App\Kernel;
  34. use App\Util\Formatting;
  35. use AppendIterator;
  36. use Exception;
  37. use FilesystemIterator;
  38. use Functional as F;
  39. use RecursiveDirectoryIterator;
  40. use RecursiveIteratorIterator;
  41. use Symfony\Component\Config\Loader\LoaderInterface;
  42. use Symfony\Component\DependencyInjection\ContainerBuilder;
  43. use Symfony\Component\DependencyInjection\Reference;
  44. class ModuleManager
  45. {
  46. protected static $loader;
  47. /**
  48. * @codeCoverageIgnore
  49. */
  50. public static function setLoader($l)
  51. {
  52. self::$loader = $l;
  53. }
  54. protected array $modules = [];
  55. protected array $events = [];
  56. /**
  57. * Add the $fqcn class from $path as a module
  58. */
  59. public function add(string $fqcn, string $path)
  60. {
  61. [$type, $module] = preg_split('/\\\\/', $fqcn, 0, \PREG_SPLIT_NO_EMPTY);
  62. self::$loader->addPsr4("\\{$type}\\{$module}\\", \dirname($path));
  63. $id = Formatting::camelCaseToSnakeCase($type . '.' . $module);
  64. $obj = new $fqcn();
  65. $this->modules[$id] = $obj;
  66. }
  67. /**
  68. * Container-build-time step that preprocesses the registering of events
  69. */
  70. public function preRegisterEvents()
  71. {
  72. foreach ($this->modules as $id => $obj) {
  73. F\map(
  74. F\select(
  75. get_class_methods($obj),
  76. F\ary(F\partial_right('App\Util\Formatting::startsWith', 'on'), 1),
  77. ),
  78. function (string $m) use ($obj) {
  79. $ev = mb_substr($m, 2);
  80. $this->events[$ev] ??= [];
  81. $this->events[$ev][] = [$obj, $m];
  82. },
  83. );
  84. }
  85. }
  86. /**
  87. * Compiler pass responsible for registering all modules
  88. */
  89. public static function process(?ContainerBuilder $container = null)
  90. {
  91. $module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php'));
  92. $module_manager = new self();
  93. $entity_paths = [];
  94. foreach ($module_paths as $path) {
  95. $type = ucfirst(preg_replace('%' . INSTALLDIR . '/(component|plugin)s/.*%', '\1', $path));
  96. $dir = \dirname($path);
  97. $module = basename($dir); // component or plugin
  98. $fqcn = "\\{$type}\\{$module}\\{$module}";
  99. $module_manager->add($fqcn, $path);
  100. if (!\is_null($container) && file_exists($dir = $dir . '/Entity') && is_dir($dir)) {
  101. // Happens at compile time, so it's hard to do integration testing. However,
  102. // everything would break if this did :')
  103. // @codeCoverageIgnoreStart
  104. $entity_paths[] = $dir;
  105. $container->findDefinition('doctrine.orm.default_metadata_driver')->addMethodCall(
  106. 'addDriver',
  107. [new Reference('app.schemadef_driver'), "{$type}\\{$module}\\Entity"],
  108. );
  109. // @codeCoverageIgnoreEnd
  110. }
  111. }
  112. if (!\is_null($container)) {
  113. // @codeCoverageIgnoreStart
  114. $container->findDefinition('app.schemadef_driver')
  115. ->addMethodCall('addPaths', ['$paths' => $entity_paths]);
  116. // @codeCoverageIgnoreEnd
  117. }
  118. $module_manager->preRegisterEvents();
  119. file_put_contents(MODULE_CACHE_FILE, "<?php\nreturn " . var_export($module_manager, true) . ';');
  120. }
  121. /**
  122. * Serialize this class, for dumping into the cache
  123. */
  124. public static function __set_state($state)
  125. {
  126. $obj = new self();
  127. $obj->modules = $state['modules'];
  128. $obj->events = $state['events'];
  129. return $obj;
  130. }
  131. /**
  132. * Load the modules at runtime. In production requires the cache
  133. * file to exist, in dev it rebuilds this cache
  134. */
  135. public function loadModules()
  136. {
  137. if ($_ENV['APP_ENV'] === 'prod' && !file_exists(MODULE_CACHE_FILE)) {
  138. // @codeCoverageIgnoreStart
  139. throw new Exception('The application needs to be compiled before using in production');
  140. // @codeCoverageIgnoreEnd
  141. } else {
  142. $rdi = new AppendIterator();
  143. $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/components', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)));
  144. $rdi->append(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(INSTALLDIR . '/plugins', FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)));
  145. $time = file_exists(MODULE_CACHE_FILE) ? filemtime(MODULE_CACHE_FILE) : 0;
  146. if ($_ENV['APP_ENV'] === 'test' || F\some($rdi, fn ($e) => $e->getMTime() > $time)) {
  147. Log::info('Rebuilding plugin cache at runtime. This means we can\'t update DB definitions');
  148. self::process();
  149. }
  150. }
  151. $obj = require MODULE_CACHE_FILE;
  152. foreach ($obj->modules as $module) {
  153. $module->loadConfig();
  154. }
  155. foreach ($obj->events as $event => $callables) {
  156. foreach ($callables as $callable) {
  157. Event::addHandler($event, $callable);
  158. }
  159. }
  160. }
  161. /**
  162. * Load Module settings and setup Twig template load paths
  163. *
  164. * Happens at "compile time"
  165. *
  166. * @codeCoverageIgnore
  167. */
  168. public static function configureContainer(ContainerBuilder $container, LoaderInterface $loader): array
  169. {
  170. $template_modules = array_merge(glob(INSTALLDIR . '/components/*/templates'), glob(INSTALLDIR . '/plugins/*/templates'));
  171. // Regular template location
  172. $templates = ['%kernel.project_dir%/templates' => 'default_path', '%kernel.project_dir%/public' => 'public_path'];
  173. // Path => alias
  174. foreach ($template_modules as $mod) {
  175. $templates[$mod] = null;
  176. }
  177. $container->loadFromExtension('twig', ['paths' => $templates]);
  178. $modules = array_merge(glob(INSTALLDIR . '/components/*'), glob(INSTALLDIR . '/plugins/*'));
  179. $module_configs = [];
  180. foreach ($modules as $mod) {
  181. $path = "{$mod}/config" . Kernel::CONFIG_EXTS;
  182. $loader->load($path, 'glob'); // Is supposed to, but doesn't return anything that would let us identify if loading worked
  183. foreach (explode(',', mb_substr(Kernel::CONFIG_EXTS, 2, -1)) as $ext) {
  184. if (file_exists("{$mod}/config.{$ext}")) {
  185. $module_configs[basename(mb_strtolower($mod))] = basename(\dirname(mb_strtolower($mod)));
  186. break;
  187. }
  188. }
  189. }
  190. return $module_configs;
  191. }
  192. }