activate(1, true); } return !$module->hasMissingDependencies(); } private static function resolveDepsByName(string $name): bool { if (!isset(self::$modules[$name])) return false; return self::resolveDeps(self::$modules[$name]); } /** * * @param \Module $mod the module to check * @return boolean true iff module deps are all found and enabled */ private static function resolveDeps(Module $mod): bool { if (!$mod->depsChecked) { $mod->depsChecked = true; foreach ($mod->dependencies as $dep) { if (!self::resolveDepsByName($dep)) { trigger_error("Disabling module {$mod->name}: Dependency $dep failed.", E_USER_WARNING); $mod->depsMissing = true; return false; } } } return !$mod->depsMissing; } /** * @return \Module[] List of valid, enabled modules */ public static function getEnabled(bool $sortById = false): array { $ret = array(); $sort = array(); foreach (self::$modules as $module) { if (self::resolveDeps($module)) { $ret[] = $module; if ($sortById) { $sort[] = $module->name; } } } if ($sortById) { array_multisort($sort, SORT_ASC, $ret); } return $ret; } /** * @return \Module[] List of all modules, including with missing deps */ public static function getAll(): array { foreach (self::$modules as $module) { self::resolveDeps($module); } return self::$modules; } /** * @return \Module[] List of modules that have been activated */ public static function getActivated(): array { $ret = array(); $i = 0; foreach (self::$modules as $module) { if ($module->activated !== false) { $ret[sprintf('%05d_%d', $module->activated, $i++)] = $module; } } ksort($ret); return $ret; } public static function init(): void { if (self::$modules !== null) return; $dh = opendir('modules'); if ($dh === false) return; self::$modules = array(); while (($dir = readdir($dh)) !== false) { if (empty($dir) || preg_match('/[^a-zA-Z0-9_]/', $dir)) continue; if (!is_file('modules/' . $dir . '/config.json')) continue; $name = strtolower($dir); self::$modules[$name] = new Module($dir); } closedir($dh); } /* * Non-static */ /** @var ?string category id */ private $category = null; private $clientPlugin = false; private $depsMissing = false; private $depsChecked = false; private $activated = false; private $directActivation = false; private $dependencies = array(); private $name; private $collapse; /** * @var array assoc list of 'filename.css' => true|false (true = always load, false = only if module is main module) */ private $css = array(); /** * @var array assoc list of 'filename.js' => true|false (true = always load, false = only if module is main module) */ private $scripts = array(); private function __construct(string $name) { $file = 'modules/' . $name . '/config.json'; $json = @json_decode(@file_get_contents($file), true); foreach (['dependencies', 'css', 'scripts'] as $key) { if (isset($json[$key]) && is_array($json[$key])) { $this->$key = $json[$key]; } } if (isset($json['category']) && is_string($json['category'])) { $this->category = $json['category']; } $this->collapse = isset($json['collapse']) && $json['collapse']; if (isset($json['client-plugin'])) { $this->clientPlugin = (bool)$json['client-plugin']; } $this->name = $name; } public function hasMissingDependencies(): bool { return $this->depsMissing; } public function newPage(): Page { $modulePath = 'modules/' . $this->name . '/page.inc.php'; if (!file_exists($modulePath)) { ErrorHandler::traceError("Module doesn't have a page: " . $modulePath); } require_once $modulePath; $class = 'Page_' . $this->name; return new $class(); } public function activate(?int $depth, ?bool $direct): bool { if ($this->depsMissing) return false; if ($this->activated !== false && ($this->directActivation || !$direct)) return true; if ($depth === null && $direct === null) { // This is the current page, always load its scripts $this->clientPlugin = true; $direct = true; } if ($this->activated === false) { spl_autoload_register(function ($class) { $file = 'modules/' . $this->name . '/inc/' . preg_replace('/[^a-z0-9]/', '', strtolower($class)) . '.inc.php'; if (!file_exists($file)) return; require_once $file; }); } $this->activated = $depth; if ($direct) { $this->directActivation = true; } foreach ($this->dependencies as $dep) { $get = self::get($dep); if ($get !== false) { $get->activate($depth + 1, $direct && $this->clientPlugin); } } return true; } public function getDependencies(): array { $deps = array(); $this->getDepsInternal($deps); return array_keys($deps); } private function getDepsInternal(array &$deps): void { if (!is_array($this->dependencies)) return; foreach ($this->dependencies as $dep) { if (isset($deps[$dep])) // Handle cyclic dependencies continue; $deps[$dep] = true; $mod = self::get($dep); if ($mod === false) continue; $mod->getDepsInternal($deps); } } public function getIdentifier(): string { return $this->name; } public function getDisplayName(): string { $string = Dictionary::translateFileModule($this->name, 'module', 'module_name', false); if ($string === false) { return '!!' . $this->name . '!!'; } return $string; } public function getPageTitle(): string { $val = Dictionary::translateFileModule($this->name, 'module', 'page_title', false); if ($val !== false) return $val; return $this->getDisplayName(); } public function getCategory(): ?string { return $this->category; } public function getCategoryName(): string { return Dictionary::getCategoryName($this->category); } public function doCollapse(): bool { return $this->collapse; } public function getDir(): string { return 'modules/' . $this->name; } public function getScripts(): array { if ($this->directActivation && $this->clientPlugin) { if (!in_array('clientscript.js', $this->scripts) && file_exists($this->getDir() . '/clientscript.js')) { $this->scripts[] = 'clientscript.js'; } return $this->scripts; } return []; } public function getCss(): array { if ($this->directActivation && $this->clientPlugin) { if (!in_array('style.css', $this->css) && file_exists($this->getDir() . '/style.css')) { $this->css[] = 'style.css'; } return $this->css; } return []; } }