mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-24 12:07:31 +09:00
* fix(plugins): honor version range boundaries and compatibility status Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): preserve null hook diagnostics and enforce dependency versions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): report config failures and confine error attribution Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): guard entrypoint frames and bind realm filenames Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): propagate install hook failures on develop Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): match realm filenames literally Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): defer release entries to final backlog PR Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(plugins): add standard GPL headers Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(coverage): require 80 percent of changed PHP statements Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: share changed-code coverage contracts and enforce shell policy Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): run request coverage with portable path matching Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: retain bootstrap coverage and validate request dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: synchronize native coverage fixture dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: measure coverage at the exact PR revision Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2541 lines
74 KiB
PHP
2541 lines
74 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| 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. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
function do_hook(string $name) : array {
|
|
$data = func_get_args();
|
|
$data = api_plugin_hook($name, $data);
|
|
|
|
return $data;
|
|
}
|
|
|
|
function do_hook_function(string $name, mixed $parm = null) : mixed {
|
|
return api_plugin_hook_function($name, $parm);
|
|
}
|
|
|
|
function api_user_realm_auth(string $filename = '') : bool {
|
|
return api_plugin_user_realm_auth($filename);
|
|
}
|
|
|
|
/**
|
|
* This function executes a hook.
|
|
*
|
|
* @param string $name Name of hook to fire
|
|
*
|
|
* @return array The function args to be passed to the next plugin
|
|
*/
|
|
function api_plugin_hook(string $name) : array {
|
|
global $plugin_hooks, $plugins_integrated;
|
|
|
|
static $hook_cache = [];
|
|
|
|
$args = func_get_args();
|
|
$ret = '';
|
|
|
|
if (defined('IN_CACTI_INSTALL') || !db_table_exists('plugin_hooks')) {
|
|
return $args;
|
|
}
|
|
|
|
if (!isset($hook_cache[$name])) {
|
|
// order the plugins by order
|
|
$result = db_fetch_assoc_prepared('SELECT ph.name, ph.file, ph.function
|
|
FROM plugin_hooks AS ph
|
|
LEFT JOIN plugin_config AS pc
|
|
ON pc.directory = ph.name
|
|
WHERE ph.status = 1
|
|
AND hook = ?
|
|
ORDER BY pc.id ASC',
|
|
[$name],
|
|
true
|
|
);
|
|
|
|
$hook_cache[$name] = $result;
|
|
} else {
|
|
$result = $hook_cache[$name];
|
|
}
|
|
|
|
if (!empty($result)) {
|
|
foreach ($result as $hdata) {
|
|
$plugin_name = $hdata['name'];
|
|
$plugin_file = $hdata['file'];
|
|
|
|
// Security check
|
|
if (str_contains($plugin_file, '..') || str_contains($plugin_name, '..')) {
|
|
cacti_log("ERROR: Attempted inclusion of not plugin file $plugin_file from $plugin_name with the hook name $name", false, 'SECURITY');
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!in_array($plugin_name, $plugins_integrated, true)) {
|
|
$plugin_func = $hdata['function'];
|
|
$plugin_file = $hdata['file'];
|
|
$full_path = CACTI_PATH_PLUGINS . '/' . $plugin_name . '/' . $plugin_file;
|
|
$debounce = 'mpf_' . $plugin_name . '_' . $plugin_func;
|
|
|
|
if (file_exists($full_path)) {
|
|
include_once($full_path);
|
|
}
|
|
|
|
if (function_exists($plugin_func)) {
|
|
api_plugin_run_plugin_hook($name, $plugin_name, $plugin_func, $args);
|
|
} elseif (debounce_run_notification($debounce)) {
|
|
cacti_log(sprintf('WARNING: Function "%s" does not exist in %s/%s for hook "%s"' . PHP_EOL, $plugin_func, $plugin_name, $plugin_file, $name), false, 'PLUGIN', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Variable-length argument lists have a slight problem when
|
|
// passing values by reference. Pity. This is a workaround.
|
|
return $args;
|
|
}
|
|
|
|
function api_plugin_hook_function(string $name, mixed $parm = null) : mixed {
|
|
global $plugin_hooks, $plugins_integrated;
|
|
|
|
static $hook_cache = [];
|
|
|
|
$ret = $parm;
|
|
|
|
if (defined('IN_CACTI_INSTALL') || !db_table_exists('plugin_hooks')) {
|
|
return $ret;
|
|
}
|
|
|
|
if (!isset($hook_cache[$name])) {
|
|
// order the plugins by order
|
|
$result = db_fetch_assoc_prepared('SELECT ph.name, ph.file, ph.function
|
|
FROM plugin_hooks AS ph
|
|
LEFT JOIN plugin_config AS pc
|
|
ON pc.directory = ph.name
|
|
WHERE ph.status = 1
|
|
AND hook = ?
|
|
ORDER BY pc.id ASC',
|
|
[$name],
|
|
true
|
|
);
|
|
|
|
$hook_cache[$name] = $result;
|
|
} else {
|
|
$result = $hook_cache[$name];
|
|
}
|
|
|
|
if (empty($ret)) {
|
|
$null_ret = true;
|
|
} else {
|
|
$null_ret = false;
|
|
}
|
|
|
|
if (cacti_sizeof($result)) {
|
|
foreach ($result as $hdata) {
|
|
if (!in_array($hdata['name'], $plugins_integrated, true)) {
|
|
if (str_contains($hdata['file'], '..') || str_contains($hdata['name'], '..')) {
|
|
cacti_log("ERROR: Attempted inclusion of not plugin file {$hdata['file']} from {$hdata['name']}", false, 'SECURITY');
|
|
|
|
continue;
|
|
}
|
|
|
|
$message = '';
|
|
|
|
if (api_plugin_can_install($hdata['name'], $message)) {
|
|
$p[] = $hdata['name'];
|
|
|
|
if (file_exists(CACTI_PATH_PLUGINS . '/' . $hdata['name'] . '/' . $hdata['file'])) {
|
|
require_once(CACTI_PATH_PLUGINS . '/' . $hdata['name'] . '/' . $hdata['file']);
|
|
}
|
|
|
|
$function = $hdata['function'];
|
|
|
|
if (function_exists($function)) {
|
|
if (is_array($ret)) {
|
|
$is_array = true;
|
|
} else {
|
|
$is_array = false;
|
|
}
|
|
|
|
$ret = api_plugin_run_plugin_hook_function($name, $hdata['name'], $function, $ret);
|
|
|
|
if (($is_array && !is_array($ret)) || ($ret == null && $null_ret === false)) {
|
|
if (cacti_sizeof($result) > 1) {
|
|
cacti_log(sprintf("WARNING: Plugin hook '%s' from Plugin '%s' must return the calling array or variable, and it is not doing so. Please report this to the Plugin author.", $function, $hdata['name']), false, 'PLUGIN');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Variable-length argument lists have a slight problem when
|
|
// passing values by reference. Pity. This is a workaround.
|
|
return $ret;
|
|
}
|
|
|
|
function api_plugin_run_plugin_hook(string $hook, string $plugin, string $function, mixed $args) : mixed {
|
|
global $menu;
|
|
|
|
if (POLLER_ID > 1) {
|
|
// Let's control the menu
|
|
$orig_menu = $menu;
|
|
|
|
$required_capabilities = [
|
|
// Poller related
|
|
'poller_top' => ['remote_collect'], // Poller Top, api_plugin_hook
|
|
'poller_bottom' => ['remote_poller'], // Poller execution, api_plugin_hook
|
|
'update_host_status' => ['remote_collect'], // Processing poller output, api_plugin_hook
|
|
'poller_output' => ['remote_collect'], // Poller output activities
|
|
'poller_finishing' => ['remote_collect'], // Poller post processing, api_plugin_hook
|
|
'poller_exiting' => ['remote_collect'], // Poller exception handling, api_plugin_hook
|
|
|
|
// GUI Related
|
|
'page_head' => ['online_view', 'offline_view'], // Content, Navigation, api_plugin_hook
|
|
'top_header_tabs' => ['online_view', 'offline_view'], // Top Tabs, api_plugin_hook
|
|
'top_graph_header_tabs' => ['online_view', 'offline_view'], // Top Tabs, api_plugin_hook
|
|
'graph_buttons' => ['online_view', 'offline_view'], // Buttons by graphs, api_plugin_hook
|
|
'graphs_new_top_links' => ['online_mgmt', 'offline_mgmt'] // Buttons by graphs, api_plugin_hook
|
|
];
|
|
|
|
$plugin_capabilities = api_plugin_remote_capabilities($plugin);
|
|
|
|
if ($plugin_capabilities === false) {
|
|
$function($args);
|
|
} elseif (api_plugin_hook_is_remote_collect($hook, $plugin, $required_capabilities)) {
|
|
if (api_plugin_status_run($hook, $required_capabilities, $plugin_capabilities, $plugin)) {
|
|
$function($args);
|
|
}
|
|
} elseif (isset($required_capabilities[$hook])) {
|
|
if (api_plugin_status_run($hook, $required_capabilities, $plugin_capabilities, $plugin)) {
|
|
$function($args);
|
|
}
|
|
} elseif (CACTI_CONNECTION == 'online' ||
|
|
((api_plugin_has_capability($plugin, 'offline_mgmt') || api_plugin_has_capability($plugin, 'offline_view'))
|
|
&& CACTI_CONNECTION != 'online')) {
|
|
$function($args);
|
|
} else {
|
|
// Don't run as they are not required
|
|
}
|
|
|
|
// See if we need to restore the menu to original
|
|
$remote_hooks = [
|
|
'config_arrays',
|
|
'config_insert',
|
|
];
|
|
|
|
if (in_array($hook, $remote_hooks, true) && (CACTI_CONNECTION == 'offline' || CACTI_CONNECTION == 'recovery')) {
|
|
if (!api_plugin_has_capability($plugin, 'offline_mgmt')) {
|
|
if ($orig_menu !== $menu) {
|
|
$menu = $orig_menu;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$function($args);
|
|
}
|
|
|
|
return $args;
|
|
}
|
|
|
|
function api_plugin_run_plugin_hook_function(string $hook, string $plugin, string $function, mixed $ret) : mixed {
|
|
if (POLLER_ID > 1) {
|
|
$required_capabilities = [
|
|
// Poller related
|
|
'poller_output' => ['remote_collect'], // Processing poller output, api_plugin_hook_function
|
|
'cacti_stats_update' => ['remote_collect'], // Updating Cacti stats
|
|
|
|
// GUI Related
|
|
'top_header' => ['online_view', 'offline_view'], // Top Tabs, api_plugin_hook_function
|
|
'top_graph_header' => ['online_view', 'offline_view'], // Top Tabs, api_plugin_hook_function
|
|
'rrd_graph_graph_options' => ['online_view', 'offline_view'], // Buttons by graphs, api_plugin_hook_function
|
|
'data_sources_table' => ['online_mgmt', 'offline_mgmt'], // Buttons by graphs, api_plugin_hook_function
|
|
|
|
'device_action_array' => ['online_mgmt', 'offline_mgmt'], // Actions Dropdown, api_plugin_hook_function
|
|
'data_source_action_array' => ['online_mgmt', 'offline_mgmt'], // Actions Dropdown, api_plugin_hook_function
|
|
'graphs_action_array' => ['online_mgmt', 'offline_mgmt'], // Actions Dropdown, api_plugin_hook_function
|
|
];
|
|
|
|
$plugin_capabilities = api_plugin_remote_capabilities($plugin);
|
|
|
|
if ($plugin_capabilities === false) {
|
|
// we will run if capabilities are not set
|
|
$ret = $function($ret);
|
|
} elseif (api_plugin_hook_is_remote_collect($hook, $plugin, $required_capabilities)) {
|
|
// run if hook is remote_collect and we support it
|
|
if (api_plugin_status_run($hook, $required_capabilities, $plugin_capabilities, $plugin)) {
|
|
$ret = $function($ret);
|
|
}
|
|
} elseif (isset($required_capabilities[$hook])) {
|
|
// run if hook is remote_collect and we support it
|
|
if (api_plugin_status_run($hook, $required_capabilities, $plugin_capabilities, $plugin)) {
|
|
$ret = $function($ret);
|
|
}
|
|
} else {
|
|
$ret = $function($ret);
|
|
}
|
|
} else {
|
|
$ret = $function($ret);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
function api_plugin_hook_is_remote_collect(string $hook, string $plugin, array $required_capabilities) : bool {
|
|
if (isset($required_capabilities[$hook])) {
|
|
foreach ($required_capabilities[$hook] as $capability) {
|
|
if (str_contains($capability, 'remote_collect')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_get_dependencies(string $plugin) : mixed {
|
|
$file = CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO';
|
|
|
|
$returndeps = [];
|
|
|
|
if (file_exists($file)) {
|
|
$info = parse_ini_file($file, true);
|
|
|
|
if (isset($info['info']['requires']) && trim($info['info']['requires']) != '') {
|
|
$parts = array_map('trim', explode(' ', $info['info']['requires']));
|
|
|
|
foreach ($parts as $p) {
|
|
$vparts = explode(':', $p);
|
|
|
|
if (isset($vparts[1])) {
|
|
$returndeps[$vparts[0]] = $vparts[1];
|
|
} else {
|
|
$returndeps[$p] = true;
|
|
}
|
|
}
|
|
|
|
return $returndeps;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_minimum_version(string $plugin, string $version) : bool {
|
|
if (strlen($version)) {
|
|
$plugin_version = db_fetch_cell_prepared('SELECT version
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
$result = cacti_version_compare($version, $plugin_version, '<=');
|
|
} else {
|
|
$plugin_version = '<not read>';
|
|
$result = true;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
function api_plugin_installed(string $plugin) : bool {
|
|
$plugin_data = db_fetch_row_prepared('SELECT directory, status
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
if (cacti_sizeof($plugin_data)) {
|
|
if ($plugin_data['status'] >= 1) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_remote_capabilities(string $plugin) : mixed {
|
|
global $info_data;
|
|
|
|
if ($plugin == 'internal') {
|
|
return 'online_view:1 online_mgmt:1 offline_view:1 offline_mgmt:1 remote_collect:1';
|
|
}
|
|
|
|
$file = CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO';
|
|
|
|
if (!isset($info_data[$plugin])) {
|
|
if (file_exists($file)) {
|
|
$info = parse_ini_file($file, true);
|
|
|
|
if (cacti_sizeof($info)) {
|
|
$info_data[$plugin] = $info['info'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($info_data[$plugin]) && isset($info_data[$plugin]['capabilities'])) {
|
|
return $info_data[$plugin]['capabilities'];
|
|
} else {
|
|
return 'online_view:0 online_mgmt:0 offline_view:0 offline_mgmt:0 remote_collect:0';
|
|
}
|
|
}
|
|
|
|
function api_plugin_has_capability(string $plugin, string $capability) : bool {
|
|
$capabilities = api_plugin_remote_capabilities($plugin);
|
|
|
|
if ($capabilities !== false && str_contains($capabilities, "$capability:1")) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function api_plugin_status_run(string $hook, array $required_capabilities, string $plugin_capabilities, string $plugin = '') : bool {
|
|
$status = CACTI_CONNECTION;
|
|
|
|
if ($plugin == '') {
|
|
cacti_log('WARNING: The function \'api_plugin_status_run\' API has changed. Please add the $plugin attribute to the last position', false, 'PLUGIN');
|
|
$plugin = 'Unknown';
|
|
}
|
|
|
|
// Don't run if not a supported hook
|
|
if (!isset($required_capabilities[$hook])) {
|
|
cacti_log(sprintf('WARNING: Not running hook %s for plugin %s as its not a supported Remote Hook', $hook, $plugin), false, 'PLUGIN');
|
|
|
|
return false;
|
|
}
|
|
|
|
foreach ($required_capabilities[$hook] as $capability) {
|
|
if ($capability == 'remote_collect') {
|
|
if (str_contains($plugin_capabilities, "$capability:1")) {
|
|
return true;
|
|
}
|
|
} elseif ($capability == 'remote_poller') {
|
|
if (str_contains($plugin_capabilities, "$capability:1")) {
|
|
return true;
|
|
}
|
|
} elseif ($status == 'online' && !str_contains($capability, 'online')) {
|
|
continue;
|
|
} elseif (($status == 'offline' || $status == 'recovery') && !str_contains($capability, 'offline')) {
|
|
continue;
|
|
}
|
|
|
|
if (str_contains($plugin_capabilities, "$capability:1")) {
|
|
return true;
|
|
}
|
|
|
|
switch ($capability) {
|
|
case 'offline_view': // if the plugin has mgmt, it's assumed to have view
|
|
if (str_contains($plugin_capabilities, 'offline_mgmt:1')) {
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
case 'online_view': // if the plugin has mgmt, it's assumed to have view
|
|
if (str_contains($plugin_capabilities, 'offline_mgmt:1')) {
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_db_table_create(string $plugin, string $table, array $data) : void {
|
|
include_once(CACTI_PATH_LIBRARY . '/database.php');
|
|
|
|
// GHSA-h5wg-qf6f-7f6r: reject an unsafe DDL identifier outright; normalizing it could silently collide with an unrelated table.
|
|
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
|
cacti_log("ERROR: Refusing to create plugin table with invalid identifier '$table'", false, 'PLUGIN');
|
|
|
|
return;
|
|
}
|
|
|
|
$result = db_fetch_assoc('SHOW TABLES');
|
|
$tables = [];
|
|
|
|
foreach ($result as $arr) {
|
|
foreach ($arr as $t) {
|
|
$tables[] = $t;
|
|
}
|
|
}
|
|
|
|
if (!in_array($table, $tables, true)) {
|
|
$c = 0;
|
|
$sql = 'CREATE TABLE `' . $table . "` (\n";
|
|
|
|
foreach ($data['columns'] as $column) {
|
|
if (isset($column['name'])) {
|
|
if ($c > 0) {
|
|
$sql .= ",\n";
|
|
}
|
|
|
|
$sql .= '`' . $column['name'] . '`';
|
|
|
|
if (isset($column['type'])) {
|
|
$sql .= ' ' . $column['type'];
|
|
}
|
|
|
|
if (isset($column['unsigned'])) {
|
|
$sql .= ' unsigned';
|
|
}
|
|
|
|
if (isset($column['NULL']) && $column['NULL'] == false) {
|
|
$sql .= ' NOT NULL';
|
|
}
|
|
|
|
if (isset($column['NULL']) && $column['NULL'] == true && !isset($column['default'])) {
|
|
$sql .= ' default NULL';
|
|
}
|
|
|
|
if (isset($column['default'])) {
|
|
if (cacti_strtolower($column['type']) == 'timestamp' && $column['default'] === 'CURRENT_TIMESTAMP') {
|
|
$sql .= ' default CURRENT_TIMESTAMP';
|
|
} else {
|
|
$sql .= ' default ' . (is_numeric($column['default']) ? $column['default'] : "'" . $column['default'] . "'");
|
|
}
|
|
}
|
|
|
|
if (isset($column['auto_increment'])) {
|
|
$sql .= ' auto_increment';
|
|
}
|
|
|
|
$c++;
|
|
}
|
|
}
|
|
|
|
if (isset($data['primary'])) {
|
|
// db_format_index_create() accepts a scalar column name (the legacy form,
|
|
// required for backward compatibility) or an array of columns.
|
|
$sql .= ",\n PRIMARY KEY (" . db_format_index_create($data['primary']) . ')';
|
|
}
|
|
|
|
if (isset($data['keys']) && cacti_sizeof($data['keys'])) {
|
|
foreach ($data['keys'] as $key) {
|
|
if (isset($key['name'])) {
|
|
$sql .= ",\n INDEX `" . $key['name'] . '` (' . db_format_index_create($key['columns']) . ')';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($data['unique_keys'])) {
|
|
foreach ($data['unique_keys'] as $key) {
|
|
if (isset($key['name'])) {
|
|
$sql .= ",\n UNIQUE INDEX `" . $key['name'] . '` (' . db_format_index_create($key['columns']) . ')';
|
|
}
|
|
}
|
|
}
|
|
|
|
$sql .= ') ENGINE = ' . $data['type'];
|
|
|
|
if (isset($data['charset'])) {
|
|
$sql .= ' DEFAULT CHARSET = ' . $data['charset'];
|
|
}
|
|
|
|
if (isset($data['row_format']) && cacti_strtolower(db_get_global_variable('innodb_file_format')) == 'barracuda') {
|
|
$sql .= ' ROW_FORMAT = ' . $data['row_format'];
|
|
}
|
|
|
|
if (isset($data['comment'])) {
|
|
$sql .= " COMMENT = '" . $data['comment'] . "'";
|
|
}
|
|
|
|
if (db_execute($sql)) {
|
|
db_execute_prepared("REPLACE INTO plugin_db_changes
|
|
(plugin, `table`, `column`, `method`)
|
|
VALUES (?, ?, '', 'create')",
|
|
[$plugin, $table]);
|
|
|
|
if (isset($data['collate'])) {
|
|
db_execute("ALTER TABLE `$table` COLLATE = " . $data['collate']);
|
|
}
|
|
}
|
|
} else {
|
|
// Table already exists: refresh its schema to match the definition.
|
|
// Do not record another 'create' change - plugin_db_changes has no
|
|
// unique key, so a REPLACE here would duplicate the ownership record
|
|
// on every call and could mark a pre-existing/shared table as
|
|
// plugin-created, causing it to be dropped on uninstall.
|
|
db_update_table($table, $data, true);
|
|
}
|
|
}
|
|
|
|
function api_plugin_drop_table(string $table) : void {
|
|
// GHSA-h5wg-qf6f-7f6r: reject an unsafe identifier outright so a crafted name cannot extend the DROP across other tables or collide after normalization.
|
|
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
|
cacti_log("ERROR: Refusing to drop plugin table with invalid identifier '$table'", false, 'PLUGIN');
|
|
|
|
return;
|
|
}
|
|
|
|
db_execute('DROP TABLE IF EXISTS `' . $table . '`');
|
|
|
|
api_plugin_drop_remote_table($table);
|
|
}
|
|
|
|
function api_plugin_db_changes_remove(string $plugin) : void {
|
|
$tables = db_fetch_assoc_prepared("SELECT `table`
|
|
FROM plugin_db_changes
|
|
WHERE plugin = ?
|
|
AND method ='create'",
|
|
[$plugin], false);
|
|
|
|
if (cacti_sizeof($tables)) {
|
|
foreach ($tables as $table) {
|
|
db_execute('DROP TABLE IF EXISTS `' . $table['table'] . '`;');
|
|
|
|
api_plugin_drop_remote_table($table['table']);
|
|
}
|
|
|
|
db_execute_prepared("DELETE FROM plugin_db_changes
|
|
WHERE plugin = ?
|
|
AND method ='create'",
|
|
[$plugin], false);
|
|
}
|
|
|
|
$columns = db_fetch_assoc_prepared("SELECT `table`, `column`
|
|
FROM plugin_db_changes
|
|
WHERE plugin = ?
|
|
AND method ='addcolumn'",
|
|
[$plugin], false);
|
|
|
|
if (cacti_count($columns)) {
|
|
foreach ($columns as $column) {
|
|
db_execute('ALTER TABLE `' . $column['table'] . '` DROP `' . $column['column'] . '`');
|
|
}
|
|
|
|
db_execute_prepared("DELETE FROM plugin_db_changes
|
|
WHERE plugin = ?
|
|
AND method = 'addcolumn'",
|
|
[$plugin], false);
|
|
}
|
|
}
|
|
|
|
function api_plugin_db_add_column(string $plugin, string $table, array $column) : void {
|
|
global $database_default;
|
|
|
|
// Example: api_plugin_db_add_column ('thold', 'plugin_config',
|
|
// array('name' => 'test' . rand(1, 200), 'type' => 'varchar (255)', 'NULL' => false));
|
|
|
|
include_once(CACTI_PATH_LIBRARY . '/database.php');
|
|
|
|
$result = db_fetch_assoc('SHOW COLUMNS FROM `' . $table . '`');
|
|
$columns = [];
|
|
|
|
foreach ($result as $arr) {
|
|
foreach ($arr as $t) {
|
|
$columns[] = $t;
|
|
}
|
|
}
|
|
|
|
if (isset($column['name']) && !in_array($column['name'], $columns, true)) {
|
|
$sql = 'ALTER TABLE `' . $table . '` ADD `' . $column['name'] . '`';
|
|
|
|
if (isset($column['type'])) {
|
|
$sql .= ' ' . $column['type'];
|
|
}
|
|
|
|
if (isset($column['unsigned'])) {
|
|
$sql .= ' unsigned';
|
|
}
|
|
|
|
if (isset($column['NULL']) && $column['NULL'] == false) {
|
|
$sql .= ' NOT NULL';
|
|
}
|
|
|
|
if (isset($column['NULL']) && $column['NULL'] == true && !isset($column['default'])) {
|
|
$sql .= ' default NULL';
|
|
}
|
|
|
|
if (isset($column['default'])) {
|
|
if (cacti_strtolower($column['type']) == 'timestamp' && $column['default'] === 'CURRENT_TIMESTAMP') {
|
|
$sql .= ' default CURRENT_TIMESTAMP';
|
|
} else {
|
|
$sql .= ' default ' . (is_numeric($column['default']) ? $column['default'] : "'" . $column['default'] . "'");
|
|
}
|
|
}
|
|
|
|
if (isset($column['auto_increment'])) {
|
|
$sql .= ' auto_increment';
|
|
}
|
|
|
|
if (isset($column['after'])) {
|
|
$sql .= ' AFTER ' . $column['after'];
|
|
}
|
|
|
|
if (db_execute($sql)) {
|
|
db_execute_prepared("INSERT INTO plugin_db_changes
|
|
(plugin, `table`, `column`, `method`)
|
|
VALUES (?, ?, ?, 'addcolumn')",
|
|
[$plugin, $table, $column['name']]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function api_plugin_can_install(string $plugin, string &$message) : bool {
|
|
$dependencies = api_plugin_get_dependencies($plugin);
|
|
$message = '';
|
|
$proceed = true;
|
|
|
|
if (is_array($dependencies) && cacti_sizeof($dependencies)) {
|
|
foreach ($dependencies as $dependency => $version) {
|
|
if (!api_plugin_installed($dependency)) {
|
|
$message .= __('\'%s\' must first be installed before \'%s\' is installed. ', ucwords($dependency), ucwords($plugin));
|
|
|
|
$proceed = false;
|
|
} elseif ($version !== true) {
|
|
$installed_version = db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', [$dependency]);
|
|
|
|
if (!is_string($installed_version) || !plugin_valid_version_range($version, $installed_version)) {
|
|
$message .= __('\'%s\' versions \'%s\' above or in range are required to install \'%s\'. ', ucwords($dependency), $version, ucwords($plugin));
|
|
|
|
$proceed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $proceed;
|
|
}
|
|
|
|
function api_plugin_install(string $plugin) : bool {
|
|
if (!defined('IN_CACTI_INSTALL')) {
|
|
define('IN_CACTI_INSTALL', 1);
|
|
define('IN_PLUGIN_INSTALL', 1);
|
|
}
|
|
|
|
$message = '';
|
|
|
|
$dependencies = api_plugin_get_dependencies($plugin);
|
|
|
|
$proceed = api_plugin_can_install($plugin, $message);
|
|
|
|
if (!$proceed) {
|
|
$message .= '<br><br>' . __('Plugin cannot be installed.');
|
|
|
|
raise_message('dependency_check', $message, MESSAGE_LEVEL_ERROR);
|
|
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
}
|
|
|
|
if (!file_exists(CACTI_PATH_PLUGINS . "/$plugin/setup.php")) {
|
|
cacti_log('ERROR: Plugin \'' . preg_replace('/[^a-zA-Z0-9_\-]/', '', $plugin) . '\' setup.php not found, cannot install', false, 'PLUGIN');
|
|
raise_message('plugin_missing', __('Plugin setup file not found.'), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
exit;
|
|
}
|
|
|
|
require_once(CACTI_PATH_PLUGINS . "/$plugin/setup.php");
|
|
|
|
$exists = db_fetch_assoc_prepared('SELECT id
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin], false);
|
|
|
|
if (cacti_sizeof($exists)) {
|
|
db_execute_prepared('DELETE FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
}
|
|
|
|
$name = $author = $webpage = $version = '';
|
|
$function = 'plugin_' . $plugin . '_version';
|
|
|
|
if (function_exists($function)) {
|
|
$info = $function();
|
|
$name = $info['longname'];
|
|
|
|
if (isset($info['homepage'])) {
|
|
$webpage = $info['homepage'];
|
|
} elseif (isset($info['webpage'])) {
|
|
$webpage = $info['webpage'];
|
|
} else {
|
|
$webpage = 'Not Stated';
|
|
}
|
|
|
|
$author = $info['author'];
|
|
$version = $info['version'];
|
|
} elseif (str_contains($plugin, 'plugin_')) {
|
|
raise_message('directory_error', __esc('The Plugin directory \'%s\' needs to be renamed to remove \'plugin_\' from the name before it can be installed.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
|
|
return false;
|
|
} else {
|
|
raise_message('version_error', __esc('The Plugin in the directory \'%s\' does not include an version function \'%s()\'. This function must exist for the plugin to be installed.', $plugin, $function), MESSAGE_LEVEL_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
db_execute_prepared('INSERT INTO plugin_config
|
|
(directory, name, author, webpage, version, last_updated)
|
|
VALUES (?, ?, ?, ?, ?, NOW())',
|
|
[$plugin, $name, $author, $webpage, $version]);
|
|
|
|
$function = 'plugin_' . $plugin . '_install';
|
|
|
|
if (function_exists($function)) {
|
|
try {
|
|
$installed = $function();
|
|
} catch (Throwable $e) {
|
|
cacti_log(sprintf("ERROR: Plugin '%s' install hook threw %s.", $plugin, get_class($e)), false, 'PLUGIN');
|
|
$installed = false;
|
|
}
|
|
|
|
// Legacy install hooks return null on success. Only strict false or
|
|
// an exception signals failure; retain tables for diagnosis/recovery.
|
|
if ($installed === false) {
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 2
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_disable_hooks_all($plugin);
|
|
cacti_log(sprintf("ERROR: Plugin '%s' installation failed. Its hooks are disabled and it needs configuration.", $plugin), false, 'PLUGIN');
|
|
raise_message('install_error', __esc('Plugin %s installation failed. Its hooks are disabled. Review the plugin setup and Cacti log before enabling it.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
$ready = api_plugin_check_config($plugin);
|
|
|
|
if ($ready) {
|
|
// Set the plugin as "disabled" so it can go live
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 4
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
cacti_log(sprintf('NOTE: Cacti Plugin %s has been installed by %s', $plugin, get_username()), false, 'PLUGIN');
|
|
} else {
|
|
// Set the plugin as "needs configuration"
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 2
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
cacti_log(sprintf('WARNING: Cacti Plugin %s was not installed by %s due to Configuration Issues', $plugin, get_username()), false, 'PLUGIN');
|
|
}
|
|
} else {
|
|
raise_message('install_error', __esc('The Plugin in the directory \'%s\' does not include an install function \'%s()\'. This function must exist for the plugin to be installed.', $plugin, $function), MESSAGE_LEVEL_ERROR);
|
|
|
|
return false;
|
|
}
|
|
|
|
api_plugin_replicate_config();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* api_plugin_upgrade_register - Check the current version vs. the info version
|
|
* and if it finds that they are different, it will update the version
|
|
* and return true or false depending on if the version was changed.
|
|
*
|
|
* @param string $plugin The name of the plugin
|
|
*
|
|
* @return bool True if the version changed else false
|
|
*/
|
|
function api_plugin_upgrade_register(string $plugin) : bool {
|
|
$info = plugin_load_info_file(CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO');
|
|
|
|
if ($info) {
|
|
$details = db_fetch_row_prepared('SELECT *
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
if (cacti_sizeof($details)) {
|
|
$id = $details['id'];
|
|
$version = $details['version'];
|
|
|
|
if (isset($info['webpage'])) {
|
|
$info['homepage'] = $info['webpage'];
|
|
}
|
|
|
|
if ($version != $info['version']) {
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET name = ?, author = ?, webpage = ?, version = ?, last_updated = NOW()
|
|
WHERE id = ?',
|
|
[
|
|
$info['longname'],
|
|
$info['author'],
|
|
$info['homepage'],
|
|
$info['version'],
|
|
$id
|
|
]
|
|
);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_uninstall_integrated() : void {
|
|
global $plugin_hooks, $plugins_integrated;
|
|
|
|
foreach ($plugins_integrated as $plugin) {
|
|
api_plugin_uninstall($plugin, false);
|
|
}
|
|
}
|
|
|
|
function api_plugin_hooks_found(string $plugin) : mixed {
|
|
return db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_hooks WHERE name = ?', [$plugin]) ? true : false;
|
|
}
|
|
|
|
function api_plugin_realms_found(string $plugin) : mixed {
|
|
return db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_realms WHERE plugin = ?', [$plugin]) ? true : false;
|
|
}
|
|
|
|
function api_plugin_uninstall(string $plugin, bool $tables = true) : void {
|
|
$plugin_found = false;
|
|
|
|
if (file_exists(CACTI_PATH_PLUGINS . "/$plugin/setup.php")) {
|
|
cacti_log(sprintf('NOTE: Loading setup.php for plugin %s (uninstall)', $plugin), false, 'PLUGIN', POLLER_VERBOSITY_DEBUG);
|
|
require_once(CACTI_PATH_PLUGINS . "/$plugin/setup.php");
|
|
|
|
// Run the Plugin's Uninstall Function first
|
|
$function = "plugin_{$plugin}_uninstall";
|
|
|
|
if (function_exists($function)) {
|
|
$function();
|
|
|
|
$plugin_found = true;
|
|
}
|
|
}
|
|
|
|
if (api_plugin_hooks_found($plugin)) {
|
|
api_plugin_remove_hooks($plugin);
|
|
}
|
|
|
|
if (api_plugin_realms_found($plugin)) {
|
|
api_plugin_remove_realms($plugin);
|
|
}
|
|
|
|
db_execute_prepared('DELETE FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
if ($tables) {
|
|
api_plugin_db_changes_remove($plugin);
|
|
} else {
|
|
db_execute_prepared('DELETE FROM plugin_db_changes
|
|
WHERE plugin = ?',
|
|
[$plugin]);
|
|
}
|
|
|
|
if ($plugin_found) {
|
|
api_plugin_replicate_config();
|
|
|
|
cacti_log(sprintf('NOTE: Cacti Plugin %s has been uninstalled by %s', $plugin, get_username()), false, 'PLUGIN');
|
|
}
|
|
}
|
|
|
|
// Null is an invalid legacy hook response diagnosed by the plugin UI.
|
|
function api_plugin_check_config(string $plugin) : ?bool {
|
|
clearstatcache();
|
|
|
|
if (file_exists(CACTI_PATH_PLUGINS . "/$plugin/setup.php")) {
|
|
cacti_log(sprintf('NOTE: Loading setup.php for plugin %s (check_config)', $plugin), false, 'PLUGIN', POLLER_VERBOSITY_DEBUG);
|
|
require_once(CACTI_PATH_PLUGINS . "/$plugin/setup.php");
|
|
|
|
$function = "plugin_{$plugin}_check_config";
|
|
|
|
if (function_exists($function)) {
|
|
$result = $function();
|
|
|
|
if ($result === null) {
|
|
raise_message('plugin_config', __esc('Plugin \'%s\' Check Configuration function returned a null response which is invalid. Please check with Plugin Developer for an update.', $plugin), MESSAGE_LEVEL_WARN);
|
|
cacti_log(sprintf('WARNING: Cacti Plugin %s configuration check returned null', $plugin), false, 'PLUGIN');
|
|
} elseif (!$result) {
|
|
raise_message('plugin_config', __esc('Plugin \'%s\' failed its configuration check. Review the plugin settings before enabling it.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
cacti_log(sprintf('WARNING: Cacti Plugin %s configuration check failed', $plugin), false, 'PLUGIN');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_enable(string $plugin) : void {
|
|
$ready = api_plugin_check_config($plugin);
|
|
|
|
if ($ready) {
|
|
api_plugin_enable_hooks($plugin);
|
|
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 1
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
$username = get_username();
|
|
|
|
cacti_log(sprintf('NOTE: Cacti Plugin %s has been enabled by %s', $plugin, $username != '' ? $username : 'cli user'), false, 'PLUGIN');
|
|
}
|
|
}
|
|
|
|
function api_plugin_is_enabled(string $plugin) : bool {
|
|
static $pstatus = [];
|
|
|
|
if (isset($pstatus[$plugin])) {
|
|
return $pstatus[$plugin];
|
|
}
|
|
|
|
$status = db_fetch_cell_prepared('SELECT status
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin], '', false);
|
|
|
|
if ($status == '1') {
|
|
$pstatus[$plugin] = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
$pstatus[$plugin] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_disable(string $plugin) : void {
|
|
api_plugin_disable_hooks($plugin);
|
|
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 4
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
|
|
cacti_log(sprintf('WARNING: Cacti Plugin %s has been disabled by %s', $plugin, get_username()), false, 'PLUGIN');
|
|
}
|
|
|
|
function api_plugin_remove_data(string $plugin) : void {
|
|
$setup_file = CACTI_PATH_BASE . "/plugins/$plugin/setup.php";
|
|
|
|
if (file_exists($setup_file)) {
|
|
require_once($setup_file);
|
|
|
|
$rmdata_function = "plugin_{$plugin}_remove_data";
|
|
|
|
if (function_exists($rmdata_function)) {
|
|
$rmdata_function();
|
|
|
|
raise_message('rmdata_complete', __esc('Data for Plugin %s including Tables and Settings has been removed.', $plugin), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('rmdata_not_complete', __esc('Data for Plugin %s including Tables and Settings has not been removed due to missing removal function.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
}
|
|
}
|
|
|
|
function api_plugin_replicate_config() : void {
|
|
include_once(CACTI_PATH_LIBRARY . '/poller.php');
|
|
|
|
$gone_time = read_config_option('poller_interval') * 2;
|
|
|
|
$pollers = array_rekey(
|
|
db_fetch_assoc('SELECT
|
|
id,
|
|
UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_status) AS last_polled
|
|
FROM poller
|
|
WHERE id > 1
|
|
AND disabled=""'),
|
|
'id', 'last_polled'
|
|
);
|
|
|
|
if (cacti_sizeof($pollers)) {
|
|
foreach ($pollers as $poller_id => $last_polled) {
|
|
if ($last_polled < $gone_time) {
|
|
replicate_out($poller_id, 'plugins');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function api_plugin_drop_remote_table(string $table) : void {
|
|
include_once(CACTI_PATH_LIBRARY . '/poller.php');
|
|
|
|
// GHSA-h5wg-qf6f-7f6r: self-protect so no caller can push an unsafe identifier to the remote pollers.
|
|
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
|
|
cacti_log("ERROR: Refusing to drop remote plugin table with invalid identifier '$table'", false, 'PLUGIN');
|
|
|
|
return;
|
|
}
|
|
|
|
$gone_time = read_config_option('poller_interval') * 2;
|
|
|
|
$pollers = array_rekey(
|
|
db_fetch_assoc('SELECT
|
|
id,
|
|
UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_status) AS last_polled
|
|
FROM poller
|
|
WHERE id > 1
|
|
AND disabled=""'),
|
|
'id', 'last_polled'
|
|
);
|
|
|
|
if (cacti_sizeof($pollers)) {
|
|
foreach ($pollers as $poller_id => $last_polled) {
|
|
$rcnn_id = poller_connect_to_remote($poller_id);
|
|
|
|
if ($rcnn_id !== false) {
|
|
db_execute('DROP TABLE IF EXISTS `' . $table . '`', false, $rcnn_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function api_plugin_disable_all(string $plugin) : void {
|
|
api_plugin_disable_hooks_all($plugin);
|
|
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET status = 7
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_moveup(string $plugin) : void {
|
|
$id = db_fetch_cell_prepared('SELECT id
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
if (!empty($id)) {
|
|
$prior_id = db_fetch_cell_prepared('SELECT MAX(id)
|
|
FROM plugin_config
|
|
WHERE id < ?',
|
|
[$id]);
|
|
|
|
// MAX() on an empty set returns NULL; without a guard, the UPDATE
|
|
// below would set id = NULL on the current plugin, which non-strict
|
|
// MariaDB/MySQL silently stores as 0, corrupting the primary key.
|
|
if (!empty($prior_id)) {
|
|
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
|
|
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $prior_id]);
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$prior_id, $id]);
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
|
|
}
|
|
}
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_movedown(string $plugin) : void {
|
|
$id = db_fetch_cell_prepared('SELECT id FROM plugin_config WHERE directory = ?', [$plugin]);
|
|
|
|
if (!empty($id)) {
|
|
$next_id = db_fetch_cell_prepared('SELECT MIN(id) FROM plugin_config WHERE id > ?', [$id]);
|
|
|
|
// MIN() on an empty set returns NULL; same NULL→0 corruption risk as moveup.
|
|
if (!empty($next_id)) {
|
|
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
|
|
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$temp_id, $next_id]);
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$next_id, $id]);
|
|
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', [$id, $temp_id]);
|
|
}
|
|
}
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_register_hook(string $plugin, string $hook, string $function, string $file, bool $enable = false) : bool {
|
|
$status = 0;
|
|
|
|
if (!api_plugin_valid_entrypoint($plugin, __FUNCTION__)) {
|
|
return false;
|
|
}
|
|
|
|
$exists = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM plugin_hooks
|
|
WHERE name = ?
|
|
AND hook = ?',
|
|
[$plugin, $hook], '', false);
|
|
|
|
if (!$exists) {
|
|
// enable the hooks if they are system level hooks to enable configuration
|
|
$settings = ['config_settings', 'config_arrays', 'config_form'];
|
|
$status = (!in_array($hook, $settings, true) ? 0 : 1);
|
|
|
|
if ($enable) {
|
|
$status = 1;
|
|
}
|
|
|
|
db_execute_prepared('INSERT INTO plugin_hooks
|
|
(name, hook, `function`, file, status)
|
|
VALUES (?, ?, ?, ?, ?)',
|
|
[$plugin, $hook, $function, $file, $status]);
|
|
} else {
|
|
if ($enable == true) {
|
|
$status = 1;
|
|
}
|
|
|
|
// enable the hook automatically if other hooks are already enabled
|
|
// for this plugin.
|
|
if (!$status) {
|
|
$exists = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM plugin_hooks
|
|
WHERE name = ?
|
|
AND status = 1',
|
|
[$plugin]);
|
|
|
|
if ($exists > 0) {
|
|
$status = 1;
|
|
}
|
|
}
|
|
|
|
db_execute_prepared('UPDATE plugin_hooks
|
|
SET `function` = ?, `status` = ?,
|
|
`file` = ?
|
|
WHERE `name` = ?
|
|
AND `hook` = ?',
|
|
[$function, $status, $file, $plugin, $hook]);
|
|
}
|
|
|
|
api_plugin_replicate_config();
|
|
|
|
return true;
|
|
}
|
|
|
|
function api_plugin_remove_hooks(string $plugin) : void {
|
|
db_execute_prepared('DELETE FROM plugin_hooks
|
|
WHERE name = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_enable_hooks(string $plugin) : void {
|
|
db_execute_prepared('UPDATE plugin_hooks
|
|
SET status = 1
|
|
WHERE name = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_disable_hooks(string $plugin) : void {
|
|
db_execute_prepared("UPDATE plugin_hooks
|
|
SET status = 4
|
|
WHERE name = ?
|
|
AND hook != 'config_settings'
|
|
AND hook != 'config_arrays'
|
|
AND hook != 'config_form'",
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_disable_hooks_all(string $plugin) : void {
|
|
db_execute_prepared('UPDATE plugin_hooks
|
|
SET status = 0
|
|
WHERE name = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_valid_entrypoint(string $plugin, string $function) : bool {
|
|
// File-scope calls have no third frame and are not install entrypoints.
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
|
|
$caller = $backtrace[2]['function'] ?? '';
|
|
|
|
if (!preg_match('/(install|upgrade|setup)/i', $caller)) {
|
|
cacti_log(sprintf('WARNING: Plugin \'%s\' is attempting to call \'%s\' improperly in function \'%s\'', $plugin, $function, $caller !== '' ? $caller : 'file scope'), false, 'PLUGIN');
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function api_plugin_register_realm(string $plugin, string $file, string $display, mixed $admin = true) : bool {
|
|
if (!api_plugin_valid_entrypoint($plugin, __FUNCTION__)) {
|
|
return false;
|
|
}
|
|
|
|
$files = explode(',', $file);
|
|
$params = [$plugin];
|
|
|
|
$sql_where = '(';
|
|
|
|
foreach ($files as $tfile) {
|
|
$sql_where .= ($sql_where != '(' ? ' OR ' : '') .
|
|
' (file = ? OR file LIKE ? ESCAPE \'!\' OR file LIKE ? ESCAPE \'!\' OR file LIKE ? ESCAPE \'!\')';
|
|
$literal = str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $tfile);
|
|
array_push($params, $tfile, $literal . ',%', '%,' . $literal . ',%', '%,' . $literal);
|
|
}
|
|
$sql_where .= ')';
|
|
|
|
$realm_ids = db_fetch_assoc_prepared("SELECT id
|
|
FROM plugin_realms
|
|
WHERE plugin = ?
|
|
AND $sql_where",
|
|
$params);
|
|
|
|
if (cacti_sizeof($realm_ids) == 1) {
|
|
$realm_id = $realm_ids[0]['id'];
|
|
} elseif (cacti_sizeof($realm_ids) > 1) {
|
|
$realm_id = $realm_ids[0]['id'];
|
|
cacti_log('WARNING: Registering Realm for Plugin ' . $plugin . ' and Filenames ' . $file . ' is ambiguous. Using first matching Realm. Contact the plugin owner to resolve this issue.', false, 'PLUGIN');
|
|
|
|
unset($realm_ids[0]);
|
|
|
|
foreach ($realm_ids as $id) {
|
|
$realm_info = db_fetch_row_prepared('SELECT *
|
|
FROM plugin_realms
|
|
WHERE id = ?',
|
|
[$id['id']]);
|
|
|
|
if ($file == $realm_info['file']) {
|
|
db_execute_prepared('UPDATE IGNORE user_auth_realm
|
|
SET realm_id = ?
|
|
WHERE realm_id = ?',
|
|
[$realm_id + 100, $realm_info['id'] + 100]);
|
|
|
|
db_execute_prepared('UPDATE IGNORE user_auth_group_realm
|
|
SET realm_id = ?
|
|
WHERE realm_id = ?',
|
|
[$realm_id + 100, $realm_info['id'] + 100]);
|
|
|
|
db_execute_prepared('DELETE FROM plugin_realms
|
|
WHERE id = ?',
|
|
[$realm_info['id']]);
|
|
} elseif (str_contains($realm_info['file'], (string) $file)) {
|
|
if (str_starts_with($realm_info['file'], $file)) {
|
|
$file = substr($file, strlen($file) - 1);
|
|
} else {
|
|
$file = str_replace(',' . $file, '', $realm_info['file']);
|
|
$file = str_replace(',,', ',', $file);
|
|
}
|
|
|
|
db_execute_prepared('UPDATE plugin_realms
|
|
SET file = ?
|
|
WHERE id = ?',
|
|
[$file, $realm_info['id']]);
|
|
}
|
|
}
|
|
} else {
|
|
$realm_id = false;
|
|
}
|
|
|
|
if ($realm_id === false) {
|
|
db_execute_prepared('REPLACE INTO plugin_realms
|
|
(plugin, file, display)
|
|
VALUES (?, ?, ?)',
|
|
[$plugin, $file, $display]);
|
|
|
|
if ($admin) {
|
|
$realm_id = db_fetch_cell_prepared('SELECT id
|
|
FROM plugin_realms
|
|
WHERE plugin = ?
|
|
AND file = ?',
|
|
[$plugin, $file], '', false);
|
|
|
|
$realm_id += 100;
|
|
|
|
$user_ids[] = read_config_option('admin_user');
|
|
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_ids[] = $_SESSION[SESS_USER_ID];
|
|
}
|
|
|
|
if (cacti_sizeof($user_ids)) {
|
|
foreach ($user_ids as $user_id) {
|
|
db_execute_prepared('REPLACE INTO user_auth_realm
|
|
(user_id, realm_id)
|
|
VALUES (?, ?)',
|
|
[$user_id, $realm_id]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
db_execute_prepared('UPDATE plugin_realms
|
|
SET display = ?,
|
|
file = ?
|
|
WHERE id = ?',
|
|
[$display, $file, $realm_id]);
|
|
}
|
|
|
|
api_plugin_replicate_config();
|
|
|
|
return true;
|
|
}
|
|
|
|
function api_plugin_remove_realms(string $plugin) : void {
|
|
$realms = db_fetch_assoc_prepared('SELECT id
|
|
FROM plugin_realms
|
|
WHERE plugin = ?',
|
|
[$plugin], false);
|
|
|
|
foreach ($realms as $realm) {
|
|
$id = $realm['id'] + 100;
|
|
db_execute_prepared('DELETE FROM user_auth_realm
|
|
WHERE realm_id = ?',
|
|
[$id]);
|
|
|
|
db_execute_prepared('DELETE FROM user_auth_group_realm
|
|
WHERE realm_id = ?',
|
|
[$id]);
|
|
}
|
|
|
|
db_execute_prepared('DELETE FROM plugin_realms
|
|
WHERE plugin = ?',
|
|
[$plugin]);
|
|
|
|
api_plugin_replicate_config();
|
|
}
|
|
|
|
function api_plugin_load_realms() : void {
|
|
global $user_auth_realms, $user_auth_realm_filenames;
|
|
|
|
$plugin_realms = db_fetch_assoc('SELECT *
|
|
FROM plugin_realms
|
|
ORDER BY plugin, display');
|
|
|
|
if (cacti_sizeof($plugin_realms)) {
|
|
foreach ($plugin_realms as $plugin_realm) {
|
|
$plugin_files = explode(',', $plugin_realm['file']);
|
|
|
|
foreach ($plugin_files as $plugin_file) {
|
|
$user_auth_realm_filenames[$plugin_file] = $plugin_realm['id'] + 100;
|
|
}
|
|
|
|
$user_auth_realms[$plugin_realm['id'] + 100] = $plugin_realm['display'];
|
|
}
|
|
}
|
|
}
|
|
|
|
function api_plugin_user_realm_auth(string $filename = '') : bool {
|
|
global $user_auth_realm_filenames;
|
|
// list all realms that this user has access to
|
|
|
|
if ($filename != '' && isset($user_auth_realm_filenames[basename($filename)])) {
|
|
if (is_realm_allowed($user_auth_realm_filenames[basename($filename)])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function api_plugin_reorder(array $new_order) : void {
|
|
if (cacti_sizeof($new_order)) {
|
|
$plugins = db_fetch_assoc('SELECT * FROM plugin_config ORDER BY id');
|
|
$columns = array_keys($plugins[0]);
|
|
$order = [];
|
|
|
|
$plugins_reorder = array_rekey($plugins, 'id', $columns);
|
|
|
|
foreach ($new_order as $plugin) {
|
|
$id = str_replace('line', '', $plugin);
|
|
input_validate_input_number($id, 'id');
|
|
|
|
$order[] = $id;
|
|
}
|
|
|
|
$sequence = 1;
|
|
|
|
$sql = 'REPLACE INTO plugin_config
|
|
(id, directory, name, status, author, webpage, version, last_updated)
|
|
VALUES ';
|
|
|
|
$params = [];
|
|
|
|
if (cacti_sizeof($order)) {
|
|
foreach ($order as $id) {
|
|
if (isset($plugins_reorder[$id])) {
|
|
$plugins_reorder[$id]['id'] = $sequence;
|
|
|
|
$sql .= ($sequence > 1 ? ',' : '') . '(?, ?, ?, ?, ?, ?, ?, ?)';
|
|
|
|
$params[] = $plugins_reorder[$id]['id'];
|
|
$params[] = $plugins_reorder[$id]['directory'];
|
|
$params[] = $plugins_reorder[$id]['name'];
|
|
$params[] = $plugins_reorder[$id]['status'];
|
|
$params[] = $plugins_reorder[$id]['author'];
|
|
$params[] = $plugins_reorder[$id]['webpage'];
|
|
$params[] = $plugins_reorder[$id]['version'];
|
|
$params[] = $plugins_reorder[$id]['last_updated'];
|
|
|
|
$sequence++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// resequence it one transaction
|
|
db_execute_prepared($sql, $params);
|
|
|
|
// remove anything invalid
|
|
db_execute_prepared('DELETE FROM plugin_config WHERE id >= ?', [$sequence]);
|
|
}
|
|
}
|
|
|
|
function api_plugin_get_available_file_contents(string $plugin, string $tag, string $filetype) : bool {
|
|
include_once(CACTI_PATH_INCLUDE . '/vendor/parsedown/Parsedown.php');
|
|
|
|
if (db_column_exists('plugin_available', $filetype)) {
|
|
$contents = db_fetch_cell_prepared("SELECT $filetype AS data
|
|
FROM plugin_available
|
|
WHERE plugin = ?
|
|
AND tag_name = ?",
|
|
[$plugin, $tag]);
|
|
|
|
if ($contents != '') {
|
|
$contents = base64_decode($contents, true);
|
|
|
|
$Parsedown = new Parsedown();
|
|
|
|
print $Parsedown->text($contents);
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function api_plugin_archive_remove(string $plugin, string $id) : void {
|
|
db_execute_prepared('DELETE FROM plugin_archive
|
|
WHERE plugin = ? AND id = ?',
|
|
[$plugin, $id]);
|
|
|
|
raise_message('plugin_archive_removed', __esc('The Archive for Plugin \'%s\' has been removed.', $plugin), MESSAGE_LEVEL_INFO);
|
|
}
|
|
|
|
function api_plugin_archive_restore(string $plugin, string $id, string $type = 'archive') : bool {
|
|
if ($type == 'archive') {
|
|
$archive = db_fetch_cell_prepared('SELECT archive
|
|
FROM plugin_archive
|
|
WHERE plugin = ?
|
|
AND id = ?',
|
|
[$plugin, $id]);
|
|
|
|
$new_updated = db_fetch_cell_prepared('SELECT last_updated
|
|
FROM plugin_archive
|
|
WHERE plugin = ?
|
|
AND id = ?',
|
|
[$plugin, $id]);
|
|
} else {
|
|
$archive = db_fetch_cell_prepared('SELECT archive
|
|
FROM plugin_available
|
|
WHERE plugin = ?
|
|
AND tag_name = ?',
|
|
[$plugin, $id]);
|
|
|
|
$new_updated = db_fetch_cell_prepared('SELECT published_at
|
|
FROM plugin_available
|
|
WHERE plugin = ?
|
|
AND tag_name = ?',
|
|
[$plugin, $id]);
|
|
}
|
|
|
|
if ($archive != '') {
|
|
$tmpfile = sys_get_temp_dir() . '/' . $plugin . '_' . random_int(0, mt_getrandmax()) . '.tar.gz';
|
|
$pharfile = "phar://{$tmpfile}";
|
|
|
|
$file_data = base64_decode($archive, true);
|
|
|
|
if ($file_data != '') {
|
|
// set the restore path to the plugin directory
|
|
$restore_path = CACTI_PATH_BASE . "/plugins/$plugin";
|
|
|
|
// write the archive to the temporary directory
|
|
file_put_contents($tmpfile, $file_data);
|
|
|
|
// open the archive
|
|
$archive = new PharData($tmpfile);
|
|
|
|
// create directory if required
|
|
if (!is_dir($restore_path)) {
|
|
if (!mkdir($restore_path, 0755, true)) {
|
|
if ($type == 'archive') {
|
|
raise_message('restore_failed', __esc('Restore failed! The Plugin \'%s\' archive Restore failed. Unable to create directory \'%s\'.', $plugin, $restore_path), MESSAGE_LEVEL_ERROR);
|
|
} else {
|
|
raise_message('restore_failed', __esc('Restore failed! The available Plugin \'%s\' Load failed. Unable to create directory \'%s\'.', $plugin, $restore_path), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
|
|
$archive->__destruct();
|
|
unlink($tmpfile);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// get the list of files and directories from inside the archive file
|
|
$archive_files = [];
|
|
|
|
foreach (new RecursiveIteratorIterator($archive) as $file) {
|
|
/**
|
|
* archives from github have an extra directory
|
|
* remove it.
|
|
*
|
|
* an example from the resulting array:
|
|
* [CHANGELOG.md] => /Cacti-plugin_cycle-e941f17/CHANGELOG.md
|
|
*/
|
|
if ($type != 'archive') {
|
|
$pfile = str_replace($pharfile, '', $file->getPathname());
|
|
$tfile = ltrim($pfile, '/');
|
|
$paths = explode('/', $tfile);
|
|
$bad_path = array_shift($paths);
|
|
$tfile = implode('/', $paths);
|
|
|
|
// skip hidden files like .github
|
|
if (str_starts_with($tfile, '.') && $tfile != '.htaccess') {
|
|
continue;
|
|
}
|
|
|
|
$archive_files[$tfile] = $pfile;
|
|
} else {
|
|
$tfile = str_replace("phar://{$tmpfile}", '', $file->getPathname());
|
|
|
|
$archive_files[$tfile] = $tfile;
|
|
}
|
|
}
|
|
|
|
// get the list of files in the current plugin directory
|
|
$dir_iterator = new RecursiveDirectoryIterator($restore_path);
|
|
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
|
|
|
|
$current_files = [];
|
|
|
|
foreach ($iterator as $file) {
|
|
$file = str_replace($restore_path, '', $file);
|
|
|
|
if (str_ends_with($file, '.')) {
|
|
continue;
|
|
}
|
|
|
|
$current_files[$file] = $file;
|
|
}
|
|
|
|
// relative plugin data locations that should not be remove
|
|
$info_file = CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO';
|
|
$noremove = [];
|
|
|
|
if (file_exists($info_file)) {
|
|
$info = plugin_load_info_file($info_file);
|
|
|
|
if (isset($info['noremove'])) {
|
|
$noremove = explode(' ', $info['noremove']);
|
|
}
|
|
}
|
|
|
|
// remove files that are not in the archive
|
|
foreach ($current_files as $file) {
|
|
if (!is_dir("$restore_path/$file") && !isset($archive_files[$file])) {
|
|
if (basename($file) !== 'config.php' && basename($file) != 'config_local.php') {
|
|
if (!in_array(dirname($file), $noremove, true)) {
|
|
// Let's not do that until we figure out weathermap.
|
|
// We need a discussion by the cactigroup as well
|
|
// unlink("$restore_path/$file");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// load the archive into memory
|
|
Phar::loadPhar($tmpfile, 'my.tgz');
|
|
|
|
/**
|
|
* put yourself into the base directory in order to
|
|
* be able to use basenames to create things like
|
|
* directories.
|
|
*/
|
|
chdir($restore_path);
|
|
|
|
foreach ($archive_files as $basefile => $pharpath) {
|
|
$output = file_get_contents("phar://my.tgz{$pharpath}");
|
|
|
|
if (strlen((string) $output)) {
|
|
$rfile = ltrim($basefile, '/');
|
|
|
|
/**
|
|
* Archive entry names are attacker controlled once the
|
|
* repository URL is. An entry that climbs out of the
|
|
* plugin directory is skipped rather than written.
|
|
*/
|
|
if (validate_relative_path_within($rfile, $restore_path) === false) {
|
|
cacti_log(sprintf('WARNING: Plugin \'%s\' archive entry \'%s\' escapes the plugin directory and was skipped.', $plugin, $basefile), false, 'PLUGIN');
|
|
|
|
continue;
|
|
}
|
|
|
|
if (basename($rfile) != $rfile) {
|
|
if (!is_dir(dirname($rfile)) && !mkdir(dirname($rfile), 0755, true)) {
|
|
if ($type == 'archive') {
|
|
raise_message('restore_failed', __esc('Restore failed! The archived Plugin \'%s\' Restore failed. Unable to create directory %s', $plugin, dirname($basefile)), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('restore_failed', __esc('Load failed! The available Plugin \'%s\' Load failed. Unable to create directory %s', $plugin, dirname($basefile)), MESSAGE_LEVEL_INFO);
|
|
}
|
|
|
|
$archive->__destruct();
|
|
unlink($tmpfile);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
file_put_contents($restore_path . '/' . $basefile, $output);
|
|
}
|
|
}
|
|
|
|
$archive->__destruct();
|
|
|
|
// remove the archive file
|
|
unlink($tmpfile);
|
|
|
|
if ($type == 'archive') {
|
|
raise_message('archive_restored', __esc('Restore succeeded! The archived Plugin \'%s\' Restore succeeded.', $plugin), MESSAGE_LEVEL_INFO);
|
|
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET last_updated = ?
|
|
WHERE directory = ?',
|
|
[$new_updated, $plugin]);
|
|
} else {
|
|
raise_message('archive_restored', __esc('Load succeeded! The available Plugin \'%s\' Load succeeded.', $plugin), MESSAGE_LEVEL_INFO);
|
|
|
|
if ($id == 'develop') {
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET last_updated = NOW()
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
} else {
|
|
db_execute_prepared('UPDATE plugin_config
|
|
SET last_updated = ?
|
|
WHERE directory = ?',
|
|
[$new_updated, $plugin]);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
if ($type == 'archive') {
|
|
raise_message('archive_failed', __esc('Restore failed! The archived Plugin \'%s\' Restore failed. Check the cacti.log for warnings.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
} else {
|
|
raise_message('archive_failed', __esc('Load failed! The available Plugin \'%s\' Load failed. Check the cacti.log for warnings.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
if ($type == 'archive') {
|
|
raise_message('plugin_archive_not_found', __esc('Restore failed! Unable to locate the archive record for Plugin \'%s\' in the database.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
} else {
|
|
raise_message('plugin_archive_not_found', __esc('Load failed! Unable to locate the available record for Plugin \'%s\' in the database.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function api_plugin_archive(string $plugin, string $note = '') : void {
|
|
$plugin_data = db_fetch_row_prepared('SELECT *
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$plugin]);
|
|
|
|
if (cacti_sizeof($plugin_data)) {
|
|
$tmpfile = sys_get_temp_dir() . '/' . $plugin . '_' . random_int(0, mt_getrandmax()) . '.tar';
|
|
$tmpafile = "$tmpfile.gz";
|
|
$path = CACTI_PATH_BASE . "/plugins/$plugin";
|
|
$md5sum = md5sum_path($path);
|
|
$pi_path = CACTI_PATH_PLUGINS;
|
|
|
|
// create the tar file
|
|
$archive = new PharData($tmpfile);
|
|
$archive->buildFromDirectory($path);
|
|
|
|
// create the tar.gz file
|
|
$archive->compress(Phar::GZ);
|
|
$archive->__destruct();
|
|
|
|
// delete the tar file
|
|
unlink($tmpfile);
|
|
|
|
$info_file = CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO';
|
|
$compat = '';
|
|
$requires = '';
|
|
|
|
if (file_exists($info_file)) {
|
|
$info = plugin_load_info_file($info_file);
|
|
|
|
if (isset($info['compat'])) {
|
|
$compat = $info['compat'];
|
|
}
|
|
|
|
if (isset($info['requires'])) {
|
|
$requires = $info['requires'];
|
|
}
|
|
}
|
|
|
|
if (file_exists($tmpafile)) {
|
|
db_execute_prepared('INSERT INTO plugin_archive
|
|
(plugin, description, author, webpage, version, requires, compat, user_id, dir_md5sum, last_updated, archive_note, archive)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
$plugin,
|
|
$plugin_data['name'],
|
|
$plugin_data['author'],
|
|
$plugin_data['webpage'],
|
|
$plugin_data['version'],
|
|
$requires,
|
|
$compat,
|
|
SESS_USER_ID,
|
|
$md5sum,
|
|
date('Y-m-d H:i:s'),
|
|
$note,
|
|
base64_encode((string) file_get_contents($tmpafile))
|
|
]
|
|
);
|
|
|
|
unlink($tmpafile);
|
|
|
|
raise_message('plugin_archived', __esc('The Plugin \'%s\' has been archived successfully.', $plugin), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('plugin_archive_failed', __esc('The Plugin \'%s\' archiving process has failed. Check the Cacti log for errors.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
} else {
|
|
raise_message('plugin_archive_failed', __esc('The Plugin \'%s\' archiving process has failed due to the plugin directory being missing.', $plugin), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
}
|
|
|
|
function plugin_config_arrays() : void {
|
|
global $menu;
|
|
|
|
if (POLLER_ID == 1) {
|
|
$menu[__('Configuration')]['plugins.php'] = __('Plugins');
|
|
} elseif (CACTI_CONNECTION == 'online') {
|
|
$menu[__('Configuration')]['plugins.php'] = __('Plugins');
|
|
}
|
|
|
|
api_plugin_load_realms();
|
|
}
|
|
|
|
function plugin_draw_navigation_text(array $nav) : array {
|
|
$nav['plugins.php:'] = ['title' => __('Plugins'), 'mapping' => 'index.php:', 'url' => 'plugins.php', 'level' => '1'];
|
|
|
|
return $nav;
|
|
}
|
|
|
|
function plugin_is_compatible(string $plugin) : array {
|
|
$info = plugin_load_info_file(CACTI_PATH_PLUGINS . '/' . $plugin . '/INFO');
|
|
|
|
if ($info !== false) {
|
|
if (!isset($info['compat']) || !plugin_valid_version_range($info['compat'], CACTI_VERSION)) {
|
|
return ['compat' => false, 'requires' => __('Requires: Cacti >= %s', $info['compat'])];
|
|
}
|
|
} else {
|
|
return ['compat' => false, 'requires' => __('Legacy Plugin')];
|
|
}
|
|
|
|
return ['compat' => true, 'requires' => __('Requires: Cacti >= %s', $info['compat'])];
|
|
}
|
|
|
|
/**
|
|
* Match whitespace-separated, attached comparison operators. A bare version
|
|
* retains the legacy minimum-version meaning. All constraints must match.
|
|
*/
|
|
function plugin_valid_version_range(string $range_string, string $compare_version = CACTI_VERSION) : bool {
|
|
$compares = preg_split('/\s+/', trim($range_string));
|
|
|
|
if ($compares === false) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($compares as $constraint) {
|
|
if (!preg_match('/^(<=|>=|<|>|=)?([0-9][0-9A-Za-z._-]*)$/D', $constraint, $parts)) {
|
|
cacti_log('Invalid version comparison');
|
|
|
|
return false;
|
|
}
|
|
|
|
$direction = $parts[1] !== '' ? $parts[1] : '>=';
|
|
|
|
if (!cacti_version_compare($compare_version, $parts[2], $direction)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function plugin_valid_dependencies(string $required) : bool {
|
|
if ($required == '') {
|
|
return true;
|
|
}
|
|
|
|
if (str_contains($required, ' ')) {
|
|
$requires = array_map('trim', explode(' ', $required));
|
|
} else {
|
|
$requires[] = $required;
|
|
}
|
|
|
|
foreach ($requires as $r) {
|
|
$parts = explode(':', $r);
|
|
$dplugin = $parts[0];
|
|
$compares = $parts[1];
|
|
|
|
$version = db_fetch_cell_prepared('SELECT version
|
|
FROM plugin_config
|
|
WHERE directory = ?',
|
|
[$dplugin]);
|
|
|
|
if (empty($version)) {
|
|
return false;
|
|
}
|
|
|
|
if (!plugin_valid_version_range($compares, $version)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function plugin_load_info_defaults(string $file, mixed $info, array $defaults = []) : mixed {
|
|
$result = $info;
|
|
|
|
if ($file != '') {
|
|
$dir = basename(dirname($file));
|
|
} else {
|
|
$dir = 'unknown';
|
|
}
|
|
|
|
if (!is_array($result)) {
|
|
$result = [];
|
|
}
|
|
|
|
$info_fields = [
|
|
'name' => ucfirst($dir),
|
|
'requires' => '',
|
|
'longname' => ucfirst($dir),
|
|
'status' => file_exists($file) ? 0 : -4,
|
|
'version' => __('Unknown'),
|
|
'author' => __('Unknown'),
|
|
'homepage' => $info['webpage'] ?? __('Not Stated'),
|
|
'compat' => '1.0.0',
|
|
'capabilities' => '',
|
|
'directory' => $dir,
|
|
];
|
|
|
|
$info_fields += $defaults;
|
|
|
|
foreach ($info_fields as $name => $value) {
|
|
if (!array_key_exists($name, $result)) {
|
|
$result[$name] = $value;
|
|
}
|
|
}
|
|
|
|
if ($info_fields['status'] == 0) {
|
|
if (str_contains($dir, ' ')) {
|
|
$result['status'] = -3;
|
|
} elseif (cacti_strtolower($dir) != cacti_strtolower($result['name'])) {
|
|
$result['status'] = -2;
|
|
} elseif (!isset($result['compat']) || !plugin_valid_version_range($result['compat'], CACTI_VERSION)) {
|
|
$result['status'] = -1;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Removes rows left behind by plugins that are no longer installed.
|
|
*
|
|
* A plugin directory can disappear without its uninstall ever running, and the
|
|
* rows keyed on that directory then outlive it. The realm rows matter most:
|
|
* they carry user and group grants, and a later plugin taking the same realm id
|
|
* would inherit them.
|
|
*
|
|
* @return void
|
|
*/
|
|
function plugin_clean_old_plugin_info() : void {
|
|
db_execute_prepared('DELETE ph
|
|
FROM plugin_hooks AS ph
|
|
LEFT JOIN plugin_config AS pc
|
|
ON ph.name = pc.directory
|
|
WHERE pc.directory IS NULL
|
|
AND ph.name != ?',
|
|
['internal']);
|
|
|
|
db_execute_prepared('DELETE pd
|
|
FROM plugin_db_changes AS pd
|
|
LEFT JOIN plugin_config AS pc
|
|
ON pd.plugin = pc.directory
|
|
WHERE pc.directory IS NULL
|
|
AND pd.plugin != ?',
|
|
['internal']);
|
|
|
|
$realms = db_fetch_assoc_prepared('SELECT pr.id
|
|
FROM plugin_realms AS pr
|
|
LEFT JOIN plugin_config AS pc
|
|
ON pr.plugin = pc.directory
|
|
WHERE pc.directory IS NULL
|
|
AND pr.plugin != ?',
|
|
['internal']);
|
|
|
|
if (cacti_sizeof($realms)) {
|
|
$realm_ids = array_column($realms, 'id');
|
|
|
|
// plugin realms are offset by 100 in the user tables
|
|
$granted = array_map(static fn ($id) => $id + 100, $realm_ids);
|
|
|
|
/* Three statements whatever the number of orphans, rather than three per
|
|
orphan: this runs on every render of the plugin page. The placeholder
|
|
list is sized to the values so the ids stay bound rather than
|
|
interpolated. */
|
|
$holders = implode(',', array_fill(0, cacti_sizeof($realm_ids), '?'));
|
|
|
|
db_execute_prepared("DELETE FROM user_auth_realm WHERE realm_id IN ($holders)", $granted);
|
|
db_execute_prepared("DELETE FROM user_auth_group_realm WHERE realm_id IN ($holders)", $granted);
|
|
db_execute_prepared("DELETE FROM plugin_realms WHERE id IN ($holders)", $realm_ids);
|
|
}
|
|
}
|
|
|
|
function plugin_load_info_file(string $file) : mixed {
|
|
$info = false;
|
|
|
|
if (file_exists($file)) {
|
|
if (is_readable($file)) {
|
|
$info = parse_ini_file($file, true, INI_SCANNER_RAW);
|
|
|
|
if (cacti_sizeof($info) && array_key_exists('info', $info)) {
|
|
$info = plugin_load_info_defaults($file, $info['info']);
|
|
} else {
|
|
cacti_log('WARNING: Loading plugin INFO file failed. Parsing INI file failed.', false, 'WEBUI');
|
|
}
|
|
} else {
|
|
cacti_log('WARNING: Loading plugin INFO file failed. INFO file not readable.', false, 'WEBUI');
|
|
}
|
|
} else {
|
|
cacti_log('WARNING: Loading plugin INFO file failed. INFO file does not exist.', false, 'WEBUI');
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
function plugin_fetch_latest_plugins() : mixed {
|
|
global $debug;
|
|
|
|
$start = microtime(true);
|
|
|
|
$repo = plugin_validate_repository_url(read_config_option('github_repository'));
|
|
$user = trim(read_config_option('github_user'));
|
|
|
|
if ($repo == '' || $user == '') {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __('Unable to retrieve Cacti Plugins due to the Base API Repository URL or User not being set in Configuration > Settings > General > GitHub/GitLab API Settings.'), MESSAGE_LEVEL_ERROR);
|
|
} else {
|
|
print 'Unable to retrieve Cacti Plugins due to the Base API Repository URL or User not being set in Configuration > Settings > General > GitHub/GitLab API Settings.' . PHP_EOL;
|
|
}
|
|
|
|
return false;
|
|
} else {
|
|
debug("Fetched User $user with key");
|
|
}
|
|
|
|
$avail_plugins = [];
|
|
|
|
$plugins = plugin_make_github_request("$repo/users/$user/repos", 'json');
|
|
|
|
if ($plugins === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __('No plugins found at GitHub/GitLab location.'), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
print 'WARNING: No plugins found at GitHub/GitLab location.' . PHP_EOL;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cacti_sizeof($plugins)) {
|
|
foreach ($plugins as $pi) {
|
|
debug("Queueing Plugin {$pi['full_name']}");
|
|
|
|
if (isset($pi['full_name'])) {
|
|
if (str_contains($pi['full_name'], 'plugin_')) {
|
|
$plugin = explode('plugin_', $pi['full_name'])[1];
|
|
|
|
$avail_plugins[$plugin]['name'] = $plugin;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$updated = 0;
|
|
|
|
if (cacti_sizeof($avail_plugins)) {
|
|
foreach ($avail_plugins as $plugin_name => $pi_details) {
|
|
$details = plugin_make_github_request("$repo/repos/$user/plugin_{$plugin_name}/releases", 'json');
|
|
|
|
if ($details === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('releases_warning', __esc('The Cacti plugin %s has no releases.', $plugin_name), MESSAGE_LEVEL_WARN);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
printf('The Cacti plugin %s has no releases.' . PHP_EOL, $plugin_name);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cacti_sizeof($details)) {
|
|
$json_data = $details;
|
|
|
|
// insert latest release
|
|
if (isset($json_data[0]['tag_name'])) {
|
|
$tag_name = $json_data[0]['tag_name'];
|
|
|
|
$avail_plugins[$plugin_name][$tag_name]['body'] = $json_data[0]['body'] ?? ''; // @phpstan-ignore offsetAssign.dimType
|
|
$avail_plugins[$plugin_name][$tag_name]['published_at'] = date('Y-m-d H:i:s', strtotime($json_data[0]['published_at']));
|
|
|
|
$published_at = date('Y-m-d H:i:s', strtotime($json_data[0]['published_at']));
|
|
$tag_name = $json_data[0]['tag_name'];
|
|
|
|
$unchanged = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM plugin_available
|
|
WHERE plugin = ?
|
|
AND published_at = ?
|
|
AND tag_name = ?',
|
|
[$plugin_name, $published_at, $tag_name]
|
|
);
|
|
|
|
if ($unchanged) {
|
|
$skip = true;
|
|
cacti_log(sprintf('SKIPPED: Plugin:\'%s\', Tag/Release:\'%s\' Skipped as it has not changed', $plugin_name, $tag_name), false, 'PLUGIN');
|
|
} else {
|
|
$skip = false;
|
|
}
|
|
|
|
if (!$skip) {
|
|
$updated++;
|
|
|
|
$pstart = microtime(true);
|
|
|
|
$files = [
|
|
'changelog' => "$repo/repos/$user/plugin_{$plugin_name}/contents/CHANGELOG.md?ref=$tag_name",
|
|
'readme' => "$repo/repos/$user/plugin_{$plugin_name}/contents/README.md?ref=$tag_name",
|
|
'info' => "$repo/repos/$user/plugin_{$plugin_name}/contents/INFO?ref=$tag_name",
|
|
'archive' => "$repo/repos/$user/plugin_{$plugin_name}/tarball?ref=$tag_name"
|
|
];
|
|
|
|
$ofiles = [];
|
|
|
|
foreach ($files as $file => $url) {
|
|
if ($file != 'archive') {
|
|
$file_details = plugin_make_github_request($url, 'json');
|
|
|
|
if ($file_details === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __('Unable to get archive from GitHub/GitLab location.'), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
print 'WARNING: Unable to get archive from GitHub/GitLab location.' . PHP_EOL;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isset($file_details['content'])) {
|
|
$ofiles[$file] = base64_decode($file_details['content'], true);
|
|
} else {
|
|
$ofiles[$file] = '';
|
|
}
|
|
} else {
|
|
$file_details = plugin_make_github_request($url, 'file');
|
|
|
|
if ($file_details === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __esc('Unable to get %s from GitHub/GitLab location.', $file), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
printf('WARNING: Unable to get %s from GitHub/GitLab location.' . PHP_EOL, $file);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$ofiles[$file] = $file_details;
|
|
}
|
|
}
|
|
|
|
$compat = '';
|
|
$requires = '';
|
|
$description = '';
|
|
$webpage = '';
|
|
$author = '';
|
|
|
|
if ($ofiles['info'] != '') {
|
|
$lines = explode("\n", $ofiles['info']);
|
|
|
|
foreach ($lines as $l) {
|
|
if (str_contains($l, 'compat ')) {
|
|
$compat = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'requires ')) {
|
|
$requires = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'longname ')) {
|
|
$description = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'homepage ')) {
|
|
$webpage = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'author ')) {
|
|
$author = trim(explode('=', $l)[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
db_execute_prepared('REPLACE INTO plugin_available
|
|
(plugin, description, author, webpage, tag_name, compat, requires, published_at, body, info, readme, changelog, archive)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
$plugin_name,
|
|
$description,
|
|
$author,
|
|
$webpage,
|
|
$json_data[0]['tag_name'],
|
|
$compat,
|
|
$requires,
|
|
date('Y-m-d H:i:s', strtotime($json_data[0]['published_at'])),
|
|
base64_encode($json_data[0]['body']),
|
|
base64_encode((string) $ofiles['info']),
|
|
base64_encode((string) $ofiles['readme']),
|
|
base64_encode((string) $ofiles['changelog']),
|
|
base64_encode($ofiles['archive'])
|
|
]
|
|
);
|
|
|
|
$pend = microtime(true);
|
|
|
|
cacti_log(sprintf('UPDATED: Plugin:\'%s\', Tag/Release:\'%s\' Updated in %0.2f seconds.', $plugin_name, $json_data[0]['tag_name'], $pend - $pstart), false, 'PLUGIN');
|
|
}
|
|
}
|
|
}
|
|
|
|
$develop = plugin_make_github_request("$repo/repos/$user/plugin_{$plugin_name}?rel=develop", 'json');
|
|
|
|
if ($develop === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __esc('Unable to get develop repo data for plugin %s from GitHub/GitLab location.', $plugin_name), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
printf('Unable to get develop repo data for plugin %s from GitHub/GitLab location.' . PHP_EOL, $plugin_name);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cacti_sizeof($develop) && isset($develop['pushed_at'])) {
|
|
$published_at = date('Y-m-d H:i:s', strtotime($develop['pushed_at']));
|
|
$tag_name = (isset($develop['default_branch']) && $develop['default_branch'] != '') ? $develop['default_branch'] : 'develop';
|
|
|
|
$unchanged = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM plugin_available
|
|
WHERE plugin = ?
|
|
AND published_at = ?
|
|
AND tag_name = ?',
|
|
[$plugin_name, $published_at, $tag_name]
|
|
);
|
|
|
|
if ($unchanged) {
|
|
$skip = true;
|
|
cacti_log(sprintf('SKIPPED: Plugin:\'%s\', Tag/Release:\'%s\' Skipped as it has not changed', $plugin_name, $tag_name), false, 'PLUGIN');
|
|
} else {
|
|
$skip = false;
|
|
}
|
|
|
|
if (!$skip) {
|
|
$updated++;
|
|
|
|
$pstart = microtime(true);
|
|
|
|
$avail_plugins[$plugin_name][$tag_name]['body'] = '';
|
|
$avail_plugins[$plugin_name][$tag_name]['published_at'] = $published_at;
|
|
|
|
// insert default branch
|
|
$files = [
|
|
'changelog' => "$repo/repos/$user/plugin_{$plugin_name}/contents/CHANGELOG.md?ref=$tag_name",
|
|
'readme' => "$repo/repos/$user/plugin_{$plugin_name}/contents/README.md?ref=$tag_name",
|
|
'info' => "$repo/repos/$user/plugin_{$plugin_name}/contents/INFO?ref=$tag_name",
|
|
'archive' => "$repo/repos/$user/plugin_{$plugin_name}/tarball?ref=$tag_name"
|
|
];
|
|
|
|
$ofiles = [];
|
|
|
|
foreach ($files as $file => $url) {
|
|
if ($file != 'archive') {
|
|
$file_details = plugin_make_github_request($url, 'json');
|
|
|
|
if ($file_details === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __('Unable to get archive from GitHub/GitLab location.'), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
print 'WARNING: Unable to get archive from GitHub/GitLab location.' . PHP_EOL;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isset($file_details['content'])) {
|
|
$ofiles[$file] = base64_decode($file_details['content'], true);
|
|
} else {
|
|
$ofiles[$file] = '';
|
|
}
|
|
} else {
|
|
$file_details = plugin_make_github_request($url, 'file');
|
|
|
|
if ($file_details === false) {
|
|
if (CACTI_WEB) {
|
|
raise_message('plugins_failed', __esc('Unable to get %s from GitHub/GitLab location.', $file), MESSAGE_LEVEL_ERROR);
|
|
header('Location: plugins.php');
|
|
|
|
exit;
|
|
} else {
|
|
printf('WARNING: Unable to get %s from GitHub/GitLab location.' . PHP_EOL, $file);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$ofiles[$file] = $file_details;
|
|
}
|
|
}
|
|
|
|
$compat = '';
|
|
$requires = '';
|
|
$description = '';
|
|
$author = '';
|
|
$webpage = '';
|
|
|
|
if ($ofiles['info'] != '') {
|
|
$lines = explode("\n", $ofiles['info']);
|
|
|
|
foreach ($lines as $l) {
|
|
if (str_contains($l, 'compat ')) {
|
|
$compat = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'requires ')) {
|
|
$requires = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'longname ')) {
|
|
$description = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'homepage ')) {
|
|
$webpage = trim(explode('=', $l)[1]);
|
|
} elseif (str_contains($l, 'author ')) {
|
|
$author = trim(explode('=', $l)[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
db_execute_prepared('REPLACE INTO plugin_available
|
|
(plugin, description, author, webpage, tag_name, compat, requires, published_at, body, info, readme, changelog, archive)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
$plugin_name,
|
|
$description,
|
|
$author,
|
|
$webpage,
|
|
$tag_name,
|
|
$compat,
|
|
$requires,
|
|
$published_at,
|
|
'',
|
|
base64_encode((string) $ofiles['info']),
|
|
base64_encode((string) $ofiles['readme']),
|
|
base64_encode((string) $ofiles['changelog']),
|
|
base64_encode($ofiles['archive'])
|
|
]
|
|
);
|
|
|
|
$pend = microtime(true);
|
|
|
|
cacti_log(sprintf('UPDATED: Plugin:\'%s\', Tag/Release:\'%s\' Updated in %0.2f seconds.', $plugin_name, $tag_name, $pend - $pstart), false, 'PLUGIN');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$end = microtime(true);
|
|
|
|
$total_plugins = cacti_sizeof($avail_plugins);
|
|
$updated_plugins = $updated;
|
|
|
|
if (cacti_sizeof($avail_plugins)) {
|
|
raise_message('plugins_fetched', __esc('There were \'%s\' Plugins found at The Cacti Groups GitHub site and \'%s\' Plugins Tags/Releases were retrieved and updated in %0.2f seconds.', $total_plugins, $updated_plugins, $end - $start), MESSAGE_LEVEL_INFO);
|
|
} else {
|
|
raise_message('plugins_fetched', __esc('Unable to reach The Cacti Groups GitHub site. No plugin data retrieved in %0.2f seconds.', $end - $start), MESSAGE_LEVEL_WARN);
|
|
}
|
|
|
|
cacti_log(sprintf('PLUGIN STATS: Time:%0.2f Plugins:%d Updated:%d', $end - $start, cacti_sizeof($avail_plugins), $updated_plugins), false, 'SYSTEM');
|
|
|
|
return $avail_plugins;
|
|
}
|
|
|
|
function plugin_make_github_request(string $url, string $type = 'json') : mixed {
|
|
$pat = read_config_option('github_access_token');
|
|
|
|
$use_pat = false;
|
|
|
|
if ($pat != '') {
|
|
$use_pat = true;
|
|
}
|
|
|
|
$ch = curl_init();
|
|
$file = '';
|
|
$fh = false;
|
|
|
|
if ($ch) {
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'CactiServer ' . CACTI_VERSION);
|
|
|
|
$headers = [];
|
|
$header[] = 'X-GitHub-Api-Version: 2022-11-28';
|
|
|
|
if ($type == 'json') {
|
|
$headers[] = 'Content-Type: application/json';
|
|
} elseif ($type == 'file') {
|
|
$file = sys_get_temp_dir() . '/curlfile.output.' . random_int(0, mt_getrandmax()) . '.tgz';
|
|
|
|
$fh = fopen($file, 'w');
|
|
|
|
curl_setopt($ch, CURLOPT_FILE, $fh);
|
|
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
}
|
|
|
|
if ($use_pat) {
|
|
$headers[] = "Authorization: Bearer $pat";
|
|
}
|
|
|
|
if (cacti_sizeof($headers)) {
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
}
|
|
|
|
$data = curl_exec($ch);
|
|
$info = curl_getinfo($ch);
|
|
$errno = curl_errno($ch);
|
|
$error = curl_error($ch);
|
|
|
|
if ($info['http_code'] == 403 || $info['http_code'] == 429) {
|
|
$json_data = json_decode((string) $data, true);
|
|
|
|
if (isset($json_data['message'])) {
|
|
raise_message('rate_limited', $json_data['message'], MESSAGE_LEVEL_ERROR);
|
|
} else {
|
|
raise_message('rate_limited', __('You have been rate limited by Google. Please create yourself a personal access token before trying this again'), MESSAGE_LEVEL_ERROR);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($errno == 0) {
|
|
if ($type == 'json') {
|
|
return json_decode((string) $data, true);
|
|
}
|
|
|
|
if ($type == 'raw') {
|
|
return $data;
|
|
}
|
|
|
|
if ($type == 'file' && $file != '' && $fh !== false) {
|
|
fclose($fh);
|
|
|
|
$data = file_get_contents($file);
|
|
|
|
unlink($file);
|
|
|
|
return $data;
|
|
}
|
|
} else {
|
|
raise_message('curl_error', "Curl Experienced an error with url:$url, error:$error", MESSAGE_LEVEL_ERROR);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* plugin_validate_repository_url - constrains the plugin repository API base
|
|
*
|
|
* The repository URL drives every plugin fetch, so an arbitrary value lets an
|
|
* administrator point plugin distribution at attacker controlled infrastructure.
|
|
*
|
|
* @param mixed $url The configured repository URL
|
|
*
|
|
* @return string The trimmed URL, or an empty string when unusable
|
|
*/
|
|
function plugin_validate_repository_url(mixed $url) : string {
|
|
$url = trim((string) $url, "/\n\r ");
|
|
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
|
|
$parts = parse_url($url);
|
|
|
|
if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) {
|
|
return '';
|
|
}
|
|
|
|
if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* The value is concatenated with a path, so a query string or fragment
|
|
* would produce a malformed request. Userinfo is not meaningful for an
|
|
* API base and would leak credentials into the log.
|
|
*/
|
|
foreach (['user', 'pass', 'query', 'fragment'] as $part) {
|
|
if (isset($parts[$part])) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return $url;
|
|
}
|