mirror of
https://github.com/magicbug/Cloudlog
synced 2026-08-13 17:49:35 -04:00
Automatically disable plugins when runtime errors or invalid plugin entry/metadata are detected. Adds a disable_plugin_after_failure helper to both Cloudlog_hooks and Plugin_manager that marks the plugin as 'disabled' in plugins_model and logs the reason. Cloudlog_hooks now disables plugins for missing hook methods, exceptions in filters/actions, invalid entry paths/class names, include failures, and construction failures. Plugin_manager disables plugins for invalid award method names, instantiation failures, missing award methods, award render exceptions, and include failures. Documentation updated to mention the new auto-disable behavior.
200 lines
6.9 KiB
PHP
200 lines
6.9 KiB
PHP
<?php
|
|
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
class Cloudlog_hooks {
|
|
|
|
private $CI;
|
|
private $plugin_instances = array();
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI = &get_instance();
|
|
$this->CI->load->model('plugins_model');
|
|
}
|
|
|
|
public function apply_filters($hook_name, $payload, $context = array())
|
|
{
|
|
$handlers = $this->get_handlers_for_hook($hook_name);
|
|
if (empty($handlers)) {
|
|
return $payload;
|
|
}
|
|
|
|
$current = $payload;
|
|
foreach ($handlers as $handler) {
|
|
$plugin_instance = $this->load_plugin_instance($handler['plugin'], $handler['manifest']);
|
|
if (!$plugin_instance) {
|
|
continue;
|
|
}
|
|
|
|
$method = $handler['method'];
|
|
if (!method_exists($plugin_instance, $method)) {
|
|
$this->disable_plugin_after_failure(
|
|
$handler['plugin']->plugin_slug,
|
|
'Hook method not found [' . $hook_name . '] method=' . $method
|
|
);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$returned = $plugin_instance->$method($current, $context);
|
|
if ($returned !== null) {
|
|
$current = $returned;
|
|
}
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'Plugin filter failed [' . $hook_name . '] plugin=' . $handler['plugin']->plugin_slug . ' error=' . $e->getMessage());
|
|
$this->disable_plugin_after_failure(
|
|
$handler['plugin']->plugin_slug,
|
|
'Hook filter exception [' . $hook_name . '] ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
|
|
public function do_action($hook_name, $payload = array(), $context = array())
|
|
{
|
|
$handlers = $this->get_handlers_for_hook($hook_name);
|
|
if (empty($handlers)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($handlers as $handler) {
|
|
$plugin_instance = $this->load_plugin_instance($handler['plugin'], $handler['manifest']);
|
|
if (!$plugin_instance) {
|
|
continue;
|
|
}
|
|
|
|
$method = $handler['method'];
|
|
if (!method_exists($plugin_instance, $method)) {
|
|
$this->disable_plugin_after_failure(
|
|
$handler['plugin']->plugin_slug,
|
|
'Hook method not found [' . $hook_name . '] method=' . $method
|
|
);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$plugin_instance->$method($payload, $context);
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'Plugin action failed [' . $hook_name . '] plugin=' . $handler['plugin']->plugin_slug . ' error=' . $e->getMessage());
|
|
$this->disable_plugin_after_failure(
|
|
$handler['plugin']->plugin_slug,
|
|
'Hook action exception [' . $hook_name . '] ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function get_handlers_for_hook($hook_name)
|
|
{
|
|
if (!$this->CI->plugins_model->table_exists()) {
|
|
return array();
|
|
}
|
|
|
|
$enabled_plugins = $this->CI->plugins_model->get_enabled();
|
|
if (empty($enabled_plugins)) {
|
|
return array();
|
|
}
|
|
|
|
$handlers = array();
|
|
|
|
foreach ($enabled_plugins as $plugin) {
|
|
$manifest = json_decode((string)$plugin->plugin_manifest, true);
|
|
if (!is_array($manifest) || !isset($manifest['hooks']) || !is_array($manifest['hooks'])) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($manifest['hooks'][$hook_name])) {
|
|
continue;
|
|
}
|
|
|
|
$method = $manifest['hooks'][$hook_name];
|
|
if (!is_string($method) || $method === '') {
|
|
continue;
|
|
}
|
|
|
|
$handlers[] = array(
|
|
'plugin' => $plugin,
|
|
'manifest' => $manifest,
|
|
'method' => $method,
|
|
);
|
|
}
|
|
|
|
return $handlers;
|
|
}
|
|
|
|
private function load_plugin_instance($plugin, $manifest)
|
|
{
|
|
$slug = $plugin->plugin_slug;
|
|
if (isset($this->plugin_instances[$slug])) {
|
|
return $this->plugin_instances[$slug];
|
|
}
|
|
|
|
$entry_file = isset($manifest['entry']) ? trim((string)$manifest['entry']) : 'Plugin.php';
|
|
$class_name = isset($manifest['class']) ? trim((string)$manifest['class']) : 'Plugin';
|
|
|
|
if (!preg_match('/^[A-Za-z0-9_\/.-]+$/', $entry_file)) {
|
|
log_message('error', 'Plugin entry path invalid for ' . $slug);
|
|
$this->disable_plugin_after_failure($slug, 'Invalid plugin entry path');
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $class_name)) {
|
|
log_message('error', 'Plugin class invalid for ' . $slug);
|
|
$this->disable_plugin_after_failure($slug, 'Invalid plugin class name');
|
|
return null;
|
|
}
|
|
|
|
$plugin_path = APPPATH . 'plugins' . DIRECTORY_SEPARATOR . $slug . DIRECTORY_SEPARATOR;
|
|
$entry_path = realpath($plugin_path . $entry_file);
|
|
$plugin_root = realpath($plugin_path);
|
|
|
|
if ($plugin_root === false || $entry_path === false || strpos($entry_path, $plugin_root) !== 0) {
|
|
log_message('error', 'Plugin entry file missing or outside plugin root for ' . $slug);
|
|
$this->disable_plugin_after_failure($slug, 'Plugin entry file missing or outside plugin root');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
require_once $entry_path;
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'Plugin entry include failed (' . $slug . '): ' . $e->getMessage());
|
|
$this->disable_plugin_after_failure($slug, 'Plugin entry include failed: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
|
|
if (!class_exists($class_name)) {
|
|
log_message('error', 'Plugin class not found: ' . $class_name . ' (' . $slug . ')');
|
|
$this->disable_plugin_after_failure($slug, 'Plugin class not found: ' . $class_name);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$instance = new $class_name($this->CI);
|
|
} catch (Throwable $e) {
|
|
log_message('error', 'Plugin construction failed (' . $slug . '): ' . $e->getMessage());
|
|
$this->disable_plugin_after_failure($slug, 'Plugin construction failed: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
|
|
$this->plugin_instances[$slug] = $instance;
|
|
|
|
return $instance;
|
|
}
|
|
|
|
private function disable_plugin_after_failure($slug, $reason)
|
|
{
|
|
if (!preg_match('/^[a-z0-9_-]+$/', (string)$slug)) {
|
|
return;
|
|
}
|
|
|
|
if ($this->CI->plugins_model->table_exists()) {
|
|
$this->CI->plugins_model->set_status($slug, 'disabled');
|
|
}
|
|
|
|
unset($this->plugin_instances[$slug]);
|
|
log_message('error', 'Plugin auto-disabled: ' . $slug . ' reason=' . $reason);
|
|
}
|
|
}
|