pdo = Db::pdo(); $this->storage = array(); } private function __clone() { // } public static function getInstance(): PluginHost { if (self::$instance == null) self::$instance = new self(); return self::$instance; } private function register_plugin(string $name, Plugin $plugin) { //array_push($this->plugins, $plugin); $this->plugins[$name] = $plugin; } // needed for compatibility with API 1 function get_link() { return false; } // needed for compatibility with API 2 (?) function get_dbh() { return false; } function get_pdo(): PDO { return $this->pdo; } function get_plugin_names() { $names = array(); foreach ($this->plugins as $p) { array_push($names, get_class($p)); } return $names; } function get_plugins() { return $this->plugins; } function get_plugin(string $name) { return $this->plugins[strtolower($name)] ?? null; } function run_hooks(string $hook, ...$args) { $method = strtolower($hook); foreach ($this->get_hooks($hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { $plugin->$method(...$args); } catch (Exception $ex) { user_error($ex, E_USER_WARNING); } catch (Error $err) { user_error($err, E_USER_WARNING); } } } function run_hooks_until(string $hook, $check, ...$args) { $method = strtolower($hook); foreach ($this->get_hooks($hook) as $plugin) { try { $result = $plugin->$method(...$args); if ($result == $check) return true; } catch (Exception $ex) { user_error($ex, E_USER_WARNING); } catch (Error $err) { user_error($err, E_USER_WARNING); } } return false; } function run_hooks_callback(string $hook, Closure $callback, ...$args) { $method = strtolower($hook); foreach ($this->get_hooks($hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { if ($callback($plugin->$method(...$args), $plugin)) break; } catch (Exception $ex) { user_error($ex, E_USER_WARNING); } catch (Error $err) { user_error($err, E_USER_WARNING); } } } function chain_hooks_callback(string $hook, Closure $callback, &...$args) { $method = strtolower($hook); foreach ($this->get_hooks($hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { if ($callback($plugin->$method(...$args), $plugin)) break; } catch (Exception $ex) { user_error($ex, E_USER_WARNING); } catch (Error $err) { user_error($err, E_USER_WARNING); } } } function add_hook(string $type, Plugin $sender, int $priority = 50) { $priority = (int) $priority; if (!method_exists($sender, strtolower($type))) { user_error( sprintf("Plugin %s tried to register a hook without implementation: %s", get_class($sender), $type), E_USER_WARNING ); return; } if (empty($this->hooks[$type])) { $this->hooks[$type] = []; } if (empty($this->hooks[$type][$priority])) { $this->hooks[$type][$priority] = []; } array_push($this->hooks[$type][$priority], $sender); ksort($this->hooks[$type]); } function del_hook(string $type, Plugin $sender) { if (is_array($this->hooks[$type])) { foreach (array_keys($this->hooks[$type]) as $prio) { $key = array_search($sender, $this->hooks[$type][$prio]); if ($key !== false) { unset($this->hooks[$type][$prio][$key]); } } } } function get_hooks(string $type) { if (isset($this->hooks[$type])) { $tmp = []; foreach (array_keys($this->hooks[$type]) as $prio) { $tmp = array_merge($tmp, $this->hooks[$type][$prio]); } return $tmp; } else { return []; } } function load_all(int $kind, int $owner_uid = null, bool $skip_init = false) { $plugins = array_merge(glob("plugins/*"), glob("plugins.local/*")); $plugins = array_filter($plugins, "is_dir"); $plugins = array_map("basename", $plugins); asort($plugins); $this->load(join(",", $plugins), $kind, $owner_uid, $skip_init); } function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false) { $plugins = explode(",", $classlist); $this->owner_uid = (int) $owner_uid; foreach ($plugins as $class) { $class = trim($class); $class_file = strtolower(basename(clean($class))); // try system plugin directory first $file = dirname(__DIR__) . "/plugins/$class_file/init.php"; if (!file_exists($file)) { $file = dirname(__DIR__) . "/plugins.local/$class_file/init.php"; if (!file_exists($file)) continue; } if (!isset($this->plugins[$class])) { try { if (file_exists($file)) require_once $file; } catch (Error $err) { user_error($err, E_USER_WARNING); continue; } if (class_exists($class) && is_subclass_of($class, "Plugin")) { $plugin = new $class($this); $plugin_api = $plugin->api_version(); if ($plugin_api < self::API_VERSION) { user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING); continue; } if (file_exists(dirname($file) . "/locale")) { _bindtextdomain($class, dirname($file) . "/locale"); _bind_textdomain_codeset($class, "UTF-8"); } $this->last_registered = $class; try { switch ($kind) { case $this::KIND_SYSTEM: if ($this->is_system($plugin)) { if (!$skip_init) $plugin->init($this); $this->register_plugin($class, $plugin); } break; case $this::KIND_USER: if (!$this->is_system($plugin)) { if (!$skip_init) $plugin->init($this); $this->register_plugin($class, $plugin); } break; case $this::KIND_ALL: if (!$skip_init) $plugin->init($this); $this->register_plugin($class, $plugin); break; } } catch (Exception $ex) { user_error($ex, E_USER_WARNING); } catch (Error $err) { user_error($err, E_USER_WARNING); } } } } $this->load_data(); } function is_system(Plugin $plugin) { $about = $plugin->about(); return $about[3] ?? false; } // only system plugins are allowed to modify routing function add_handler(string $handler, $method, Plugin $sender) { $handler = str_replace("-", "_", strtolower($handler)); $method = strtolower($method); if ($this->is_system($sender)) { if (!is_array($this->handlers[$handler])) { $this->handlers[$handler] = array(); } $this->handlers[$handler][$method] = $sender; } } function del_handler(string $handler, $method, Plugin $sender) { $handler = str_replace("-", "_", strtolower($handler)); $method = strtolower($method); if ($this->is_system($sender)) { unset($this->handlers[$handler][$method]); } } function lookup_handler($handler, $method) { $handler = str_replace("-", "_", strtolower($handler)); $method = strtolower($method); if (isset($this->handlers[$handler])) { if (isset($this->handlers[$handler]["*"])) { return $this->handlers[$handler]["*"]; } else { return $this->handlers[$handler][$method]; } } return false; } function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = "") { $command = str_replace("-", "_", strtolower($command)); $this->commands[$command] = array("description" => $description, "suffix" => $suffix, "arghelp" => $arghelp, "class" => $sender); } function del_command(string $command) { $command = "-" . strtolower($command); unset($this->commands[$command]); } function lookup_command($command) { $command = "-" . strtolower($command); if (is_array($this->commands[$command])) { return $this->commands[$command]["class"]; } else { return false; } } function get_commands() { return $this->commands; } function run_commands(array $args) { foreach ($this->get_commands() as $command => $data) { if (isset($args[$command])) { $command = str_replace("-", "", $command); $data["class"]->$command($args); } } } private function load_data() { if ($this->owner_uid && !$this->data_loaded && get_schema_version() > 100) { $sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage WHERE owner_uid = ?"); $sth->execute([$this->owner_uid]); while ($line = $sth->fetch()) { $this->storage[$line["name"]] = unserialize($line["content"]); } $this->data_loaded = true; } } private function save_data(string $plugin) { if ($this->owner_uid) { if (!$this->pdo_data) $this->pdo_data = Db::instance()->pdo_connect(); $this->pdo_data->beginTransaction(); $sth = $this->pdo_data->prepare("SELECT id FROM ttrss_plugin_storage WHERE owner_uid= ? AND name = ?"); $sth->execute([$this->owner_uid, $plugin]); if (!isset($this->storage[$plugin])) $this->storage[$plugin] = array(); $content = serialize($this->storage[$plugin]); if ($sth->fetch()) { $sth = $this->pdo_data->prepare("UPDATE ttrss_plugin_storage SET content = ? WHERE owner_uid= ? AND name = ?"); $sth->execute([$content, $this->owner_uid, $plugin]); } else { $sth = $this->pdo_data->prepare("INSERT INTO ttrss_plugin_storage (name,owner_uid,content) VALUES (?, ?, ?)"); $sth->execute([$plugin, $this->owner_uid, $content]); } $this->pdo_data->commit(); } } function set(Plugin $sender, string $name, $value) { $idx = get_class($sender); if (!isset($this->storage[$idx])) $this->storage[$idx] = array(); $this->storage[$idx][$name] = $value; $this->save_data(get_class($sender)); } function set_array(Plugin $sender, array $params) { $idx = get_class($sender); if (!isset($this->storage[$idx])) $this->storage[$idx] = array(); foreach ($params as $name => $value) $this->storage[$idx][$name] = $value; $this->save_data(get_class($sender)); } function get(Plugin $sender, string $name, $default_value = false) { $idx = get_class($sender); $this->load_data(); if (isset($this->storage[$idx][$name])) { return $this->storage[$idx][$name]; } else { return $default_value; } } function get_array(Plugin $sender, string $name, array $default_value = []) { $tmp = $this->get($sender, $name); if (!is_array($tmp)) $tmp = $default_value; return $tmp; } function get_all($sender) { $idx = get_class($sender); return $this->storage[$idx] ?? []; } function clear_data(Plugin $sender) { if ($this->owner_uid) { $idx = get_class($sender); unset($this->storage[$idx]); $sth = $this->pdo->prepare("DELETE FROM ttrss_plugin_storage WHERE name = ? AND owner_uid = ?"); $sth->execute([$idx, $this->owner_uid]); } } // Plugin feed functions are *EXPERIMENTAL*! // cat_id: only -1 is supported (Special) function add_feed(int $cat_id, string $title, string $icon, Plugin $sender) { if (empty($this->feeds[$cat_id])) $this->feeds[$cat_id] = []; $id = count($this->feeds[$cat_id]); array_push($this->feeds[$cat_id], ['id' => $id, 'title' => $title, 'sender' => $sender, 'icon' => $icon]); return $id; } function get_feeds(int $cat_id) { return $this->feeds[$cat_id] ?? []; } // convert feed_id (e.g. -129) to pfeed_id first function get_feed_handler(int $pfeed_id) { foreach ($this->feeds as $cat) { foreach ($cat as $feed) { if ($feed['id'] == $pfeed_id) { return $feed['sender']; } } } } static function pfeed_to_feed_id(int $pfeed) { return PLUGIN_FEED_BASE_INDEX - 1 - abs($pfeed); } static function feed_to_pfeed_id(int $feed) { return PLUGIN_FEED_BASE_INDEX - 1 + abs($feed); } function add_api_method(string $name, Plugin $sender) { if ($this->is_system($sender)) { $this->api_methods[strtolower($name)] = $sender; } } function get_api_method(string $name) { return $this->api_methods[$name]; } function add_filter_action(Plugin $sender, string $action_name, string $action_desc) { $sender_class = get_class($sender); if (!isset($this->plugin_actions[$sender_class])) $this->plugin_actions[$sender_class] = array(); array_push($this->plugin_actions[$sender_class], array("action" => $action_name, "description" => $action_desc, "sender" => $sender)); } function get_filter_actions() { return $this->plugin_actions; } function get_owner_uid() { return $this->owner_uid; } // handled by classes/pluginhandler.php, requires valid session function get_method_url(Plugin $sender, string $method, $params = []) { return Config::get_self_url() . "/backend.php?" . http_build_query( array_merge( [ "op" => "pluginhandler", "plugin" => strtolower(get_class($sender)), "method" => $method ], $params)); } // shortcut syntax (disabled for now) /* function get_method_url(Plugin $sender, string $method, $params) { return Config::get_self_url() . "/backend.php?" . http_build_query( array_merge( [ "op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method), ], $params)); } */ // WARNING: endpoint in public.php, exposed to unauthenticated users function get_public_method_url(Plugin $sender, string $method, $params = []) { if ($sender->is_public_method($method)) { return Config::get_self_url() . "/public.php?" . http_build_query( array_merge( [ "op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method), ], $params)); } else { user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private."); } } function get_plugin_dir(Plugin $plugin) { $ref = new ReflectionClass(get_class($plugin)); return dirname($ref->getFileName()); } // TODO: use get_plugin_dir() function is_local(Plugin $plugin) { $ref = new ReflectionClass(get_class($plugin)); return basename(dirname(dirname($ref->getFileName()))) == "plugins.local"; } }