You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
12 KiB
462 lines
12 KiB
1 year ago
|
<?php declare(strict_types = 0);
|
||
|
/*
|
||
|
** Zabbix
|
||
|
** Copyright (C) 2001-2023 Zabbix SIA
|
||
|
**
|
||
|
** This program is free software; you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation; either version 2 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program 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 General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program; if not, write to the Free Software
|
||
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
**/
|
||
|
|
||
|
|
||
|
use CController as CAction;
|
||
|
|
||
|
use Zabbix\Core\{
|
||
|
CModule,
|
||
|
CWidget
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Module manager class for testing and loading user modules.
|
||
|
*/
|
||
|
final class CModuleManager {
|
||
|
|
||
|
/**
|
||
|
* Lowest supported manifest version.
|
||
|
*/
|
||
|
private const MIN_MANIFEST_VERSION = 2;
|
||
|
|
||
|
/**
|
||
|
* Highest supported manifest version.
|
||
|
*/
|
||
|
private const MAX_MANIFEST_VERSION = 2;
|
||
|
|
||
|
/**
|
||
|
* Root path of modules.
|
||
|
*/
|
||
|
private string $root_path;
|
||
|
|
||
|
/**
|
||
|
* Current action name.
|
||
|
*/
|
||
|
private string $action_name;
|
||
|
|
||
|
/**
|
||
|
* Manifest data of added modules.
|
||
|
*/
|
||
|
private array $manifests = [];
|
||
|
|
||
|
/**
|
||
|
* DB moduleids of added modules.
|
||
|
*/
|
||
|
private array $moduleids = [];
|
||
|
|
||
|
/**
|
||
|
* List of instantiated, initialized modules.
|
||
|
*/
|
||
|
private array $modules = [];
|
||
|
|
||
|
/**
|
||
|
* List of errors caused by module initialization.
|
||
|
*/
|
||
|
private array $errors = [];
|
||
|
|
||
|
/**
|
||
|
* @param string $root_path Root path of modules.
|
||
|
*/
|
||
|
public function __construct(string $root_path) {
|
||
|
$this->root_path = $root_path;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add module and prepare it's manifest data.
|
||
|
*
|
||
|
* @param string $relative_path Relative path to the module.
|
||
|
* @param string|null $moduleid DB module ID.
|
||
|
* @param string|null $id Stored module ID to optionally check the manifest module ID against.
|
||
|
* @param array|null $config Override configuration to use instead of one stored in the manifest file.
|
||
|
*
|
||
|
* @return array|null Either manifest data or null if manifest file had errors or IDs didn't match.
|
||
|
*/
|
||
|
public function addModule(string $relative_path, string $moduleid = null, string $id = null,
|
||
|
array $config = null): ?array {
|
||
|
|
||
|
$manifest = $this->loadManifest($relative_path);
|
||
|
|
||
|
// Ignore module without a valid manifest.
|
||
|
if ($manifest === null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Ignore module with an unexpected id.
|
||
|
if ($id !== null && $manifest['id'] !== $id) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Use override configuration, if supplied.
|
||
|
if (is_array($config)) {
|
||
|
$manifest['config'] = $config;
|
||
|
}
|
||
|
|
||
|
$this->manifests[$relative_path] = $manifest;
|
||
|
$this->moduleids[$relative_path] = $moduleid;
|
||
|
|
||
|
return $manifest;
|
||
|
}
|
||
|
|
||
|
public function initModules(): void {
|
||
|
[
|
||
|
'conflicts' => $this->errors,
|
||
|
'conflicting_manifests' => $conflicting_manifests
|
||
|
] = $this->checkConflicts();
|
||
|
|
||
|
$non_conflicting_manifests = array_diff_key($this->manifests, array_flip($conflicting_manifests));
|
||
|
|
||
|
foreach ($non_conflicting_manifests as $relative_path => $manifest) {
|
||
|
$base_classname = $manifest['type'] === CModule::TYPE_WIDGET ? CWidget::class : CModule::class;
|
||
|
$classname = $manifest['type'] === CModule::TYPE_WIDGET ? 'Widget' : 'Module';
|
||
|
|
||
|
$module_class = $base_classname;
|
||
|
|
||
|
try {
|
||
|
if (is_file($this->root_path.'/'.$relative_path.'/'.$classname.'.php')) {
|
||
|
$module_class = implode('\\', [$manifest['namespace'], $classname]);
|
||
|
|
||
|
if (!class_exists($module_class)) {
|
||
|
$this->errors[] = _s('Wrong %1$s.php class name for module located at %2$s.', $classname,
|
||
|
$relative_path
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @var CModule $instance */
|
||
|
$instance = new $module_class($manifest, $this->moduleids[$relative_path], $relative_path);
|
||
|
|
||
|
if ($instance instanceof $base_classname) {
|
||
|
$instance->init();
|
||
|
|
||
|
$this->modules[$instance->getId()] = $instance;
|
||
|
}
|
||
|
else {
|
||
|
$this->errors[] = _s('%1$s.php class must extend %2$s for module located at %3$s.',
|
||
|
$classname, $base_classname, $relative_path
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
catch (Throwable $e) {
|
||
|
$this->errors[] = _s('%1$s - thrown by module located at %2$s.', $e->getMessage(), $relative_path);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get initialized modules.
|
||
|
*/
|
||
|
public function getModules(): array {
|
||
|
return $this->modules;
|
||
|
}
|
||
|
|
||
|
public function setActionName(string $action_name): self {
|
||
|
$this->action_name = $action_name;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get loaded module instance associated with action.
|
||
|
*
|
||
|
* @return CModule|null
|
||
|
*/
|
||
|
public function getActionModule(): ?CModule {
|
||
|
/** @var CModule $module */
|
||
|
foreach ($this->modules as $module) {
|
||
|
if (array_key_exists($this->action_name, $module->getActions())) {
|
||
|
return $module;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get initialized widget modules.
|
||
|
*/
|
||
|
public function getWidgets(): array {
|
||
|
$widgets = [];
|
||
|
|
||
|
/** @var CWidget $widget */
|
||
|
foreach ($this->modules as $widget) {
|
||
|
if (!($widget instanceof CWidget)) {
|
||
|
continue;
|
||
|
}
|
||
|
$widgets[$widget->getId()] = $widget;
|
||
|
}
|
||
|
|
||
|
return $widgets;
|
||
|
}
|
||
|
|
||
|
public function getWidgetsDefaults(): array {
|
||
|
$widget_defaults = [];
|
||
|
|
||
|
/** @var CWidget $widget */
|
||
|
foreach (APP::ModuleManager()->getWidgets() as $widget) {
|
||
|
$widget_defaults[$widget->getId()] = $widget->getDefaults();
|
||
|
}
|
||
|
|
||
|
return $widget_defaults;
|
||
|
}
|
||
|
|
||
|
public function getModule($module_id): ?CModule {
|
||
|
if (!array_key_exists($module_id, $this->modules)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return $this->modules[$module_id];
|
||
|
}
|
||
|
|
||
|
public function getManifests(): array {
|
||
|
return $this->manifests;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get namespaces of all added modules.
|
||
|
*/
|
||
|
public function getNamespaces(): array {
|
||
|
$namespaces = [];
|
||
|
|
||
|
foreach ($this->manifests as $relative_path => $manifest) {
|
||
|
$namespaces[$manifest['namespace']] = [$this->root_path.'/'.$relative_path];
|
||
|
}
|
||
|
|
||
|
return $namespaces;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get actions of all initialized modules.
|
||
|
*/
|
||
|
public function getActions(): array {
|
||
|
$actions = [];
|
||
|
|
||
|
/** @var CModule $module */
|
||
|
foreach ($this->modules as $module) {
|
||
|
foreach ($module->getActions() as $name => $data) {
|
||
|
$actions[$name] = [
|
||
|
'class' => implode('\\', [$module->getNamespace(), 'Actions',
|
||
|
str_replace('/', '\\', $data['class'])
|
||
|
]),
|
||
|
'layout' => array_key_exists('layout', $data) ? $data['layout'] : 'layout.htmlpage',
|
||
|
'view' => array_key_exists('view', $data) ? $data['view'] : null
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $actions;
|
||
|
}
|
||
|
|
||
|
public function getAssets(): array {
|
||
|
$assets = [];
|
||
|
|
||
|
/** @var CModule $module */
|
||
|
foreach ($this->modules as $module) {
|
||
|
if ($module->getType() === CModule::TYPE_WIDGET && !CRouter::isDashboardAction($this->action_name)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$assets[$module->getId()] = $module->getAssets();
|
||
|
}
|
||
|
|
||
|
return $assets;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get errors encountered while module initialization.
|
||
|
*/
|
||
|
public function getErrors(): array {
|
||
|
return $this->errors;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check added modules for conflicts.
|
||
|
*
|
||
|
* @return array Lists of conflicts and conflicting modules.
|
||
|
*/
|
||
|
public function checkConflicts(): array {
|
||
|
$ids = [];
|
||
|
$namespaces = [];
|
||
|
$actions = [];
|
||
|
|
||
|
foreach ($this->manifests as $relative_path => $manifest) {
|
||
|
$ids[$manifest['id']][] = $relative_path;
|
||
|
$namespaces[$manifest['namespace']][] = $relative_path;
|
||
|
foreach (array_keys($manifest['actions']) as $action_name) {
|
||
|
$actions[$action_name][] = $relative_path;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach (['ids', 'namespaces', 'actions'] as $var) {
|
||
|
$$var = array_filter($$var, static function($list) {
|
||
|
return count($list) > 1;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$conflicts = [];
|
||
|
$conflicting_manifests = [];
|
||
|
|
||
|
foreach ($ids as $id => $relative_paths) {
|
||
|
$conflicts[] = _s('Identical ID (%1$s) is used by modules located at %2$s.', $id,
|
||
|
implode(', ', $relative_paths)
|
||
|
);
|
||
|
$conflicting_manifests = array_merge($conflicting_manifests, $relative_paths);
|
||
|
}
|
||
|
|
||
|
foreach ($namespaces as $namespace => $relative_paths) {
|
||
|
$conflicts[] = _s('Identical namespace (%1$s) is used by modules located at %2$s.', $namespace,
|
||
|
implode(', ', $relative_paths)
|
||
|
);
|
||
|
$conflicting_manifests = array_merge($conflicting_manifests, $relative_paths);
|
||
|
}
|
||
|
|
||
|
$relative_paths = array_unique(array_reduce($actions, static function($carry, $item) {
|
||
|
return array_merge($carry, $item);
|
||
|
}, []));
|
||
|
|
||
|
if ($relative_paths) {
|
||
|
$conflicts[] = _s('Identical actions are used by modules located at %1$s.', implode(', ', $relative_paths));
|
||
|
$conflicting_manifests = array_merge($conflicting_manifests, $relative_paths);
|
||
|
}
|
||
|
|
||
|
$this->errors = $conflicts;
|
||
|
|
||
|
return [
|
||
|
'conflicts' => $conflicts,
|
||
|
'conflicting_manifests' => array_unique($conflicting_manifests)
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Publish an event to all loaded modules. The module of the responsible action will be served last.
|
||
|
*
|
||
|
* @param CAction $action Action responsible for the current request.
|
||
|
* @param string $event Event to publish.
|
||
|
*/
|
||
|
public function publishEvent(CAction $action, string $event): void {
|
||
|
$action_module = $this->getActionModule();
|
||
|
|
||
|
foreach ($this->modules as $module) {
|
||
|
if ($module != $action_module) {
|
||
|
$module->$event($action);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($action_module) {
|
||
|
$action_module->$event($action);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load and parse module manifest file.
|
||
|
*
|
||
|
* @param string $relative_path Relative path to the module.
|
||
|
*
|
||
|
* @return array|null Either manifest data or null if manifest file had errors.
|
||
|
*/
|
||
|
private function loadManifest(string $relative_path): ?array {
|
||
|
$relative_path_parts = explode('/', $relative_path, 2);
|
||
|
|
||
|
if (count($relative_path_parts) != 2) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$manifest_file_name = $this->root_path.'/'.$relative_path.'/manifest.json';
|
||
|
|
||
|
if (!is_file($manifest_file_name) || !is_readable($manifest_file_name)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$manifest = file_get_contents($manifest_file_name);
|
||
|
|
||
|
if ($manifest === false) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$manifest = json_decode($manifest, true);
|
||
|
|
||
|
if (!is_array($manifest)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Check required keys in manifest.
|
||
|
if (array_diff_key(array_flip(['manifest_version', 'id', 'name', 'namespace', 'version']), $manifest)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Check manifest version.
|
||
|
if (!is_numeric($manifest['manifest_version']) || $manifest['manifest_version'] < self::MIN_MANIFEST_VERSION
|
||
|
|| $manifest['manifest_version'] > self::MAX_MANIFEST_VERSION) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (trim($manifest['id']) === '' || trim($manifest['name']) === '') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Check manifest namespace syntax.
|
||
|
if (!preg_match('/^[0-9a-z_]+$/i', $manifest['namespace'])) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$manifest['namespace'] = ucfirst($relative_path_parts[0]).'\\'.$manifest['namespace'];
|
||
|
|
||
|
// Check module type.
|
||
|
if (array_key_exists('type', $manifest)
|
||
|
&& !in_array($manifest['type'], [CModule::TYPE_MODULE, CModule::TYPE_WIDGET], true)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Ensure empty defaults.
|
||
|
$manifest += [
|
||
|
'type' => CModule::TYPE_MODULE,
|
||
|
'author' => '',
|
||
|
'url' => '',
|
||
|
'description' => '',
|
||
|
'actions' => [],
|
||
|
'assets' => [],
|
||
|
'config' => []
|
||
|
];
|
||
|
|
||
|
$manifest['assets'] += [
|
||
|
'css' => [],
|
||
|
'js' => []
|
||
|
];
|
||
|
|
||
|
if ($manifest['type'] === CModule::TYPE_WIDGET) {
|
||
|
if (!array_key_exists('widget', $manifest)) {
|
||
|
$manifest['widget'] = [];
|
||
|
}
|
||
|
|
||
|
$manifest['widget'] += [
|
||
|
'name' => '',
|
||
|
'form_class' => CWidget::DEFAULT_FORM_CLASS,
|
||
|
'js_class' => CWidget::DEFAULT_JS_CLASS,
|
||
|
'size' => CWidget::DEFAULT_SIZE,
|
||
|
'refresh_rate' => CWidget::DEFAULT_REFRESH_RATE,
|
||
|
'use_time_selector' => false
|
||
|
];
|
||
|
}
|
||
|
|
||
|
return $manifest;
|
||
|
}
|
||
|
}
|