MyShaarli/application/plugin/PluginManager.php
ArthurHoaro 7e3dc0ba98 Better handling of plugin incompatibility
If a PHP is raised while executing plugin hook, Shaarli will display an error instead of rendering the error page (or just ending in fatal error for default hooks).
Also added phpErrorHandler which is handled differently that regular errorHandler by Slim.:
2020-08-27 12:04:36 +02:00

243 lines
6.8 KiB
PHP

<?php
namespace Shaarli\Plugin;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
/**
* Class PluginManager
*
* Use to manage, load and execute plugins.
*/
class PluginManager
{
/**
* List of authorized plugins from configuration file.
*
* @var array $authorizedPlugins
*/
private $authorizedPlugins = [];
/**
* List of loaded plugins.
*
* @var array $loadedPlugins
*/
private $loadedPlugins = array();
/**
* @var ConfigManager Configuration Manager instance.
*/
protected $conf;
/**
* @var array List of plugin errors.
*/
protected $errors;
/**
* Plugins subdirectory.
*
* @var string $PLUGINS_PATH
*/
public static $PLUGINS_PATH = 'plugins';
/**
* Plugins meta files extension.
*
* @var string $META_EXT
*/
public static $META_EXT = 'meta';
/**
* Constructor.
*
* @param ConfigManager $conf Configuration Manager instance.
*/
public function __construct(&$conf)
{
$this->conf = $conf;
$this->errors = array();
}
/**
* Load plugins listed in $authorizedPlugins.
*
* @param array $authorizedPlugins Names of plugin authorized to be loaded.
*
* @return void
*/
public function load($authorizedPlugins)
{
$this->authorizedPlugins = $authorizedPlugins;
$dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR);
$dirnames = array_map('basename', $dirs);
foreach ($this->authorizedPlugins as $plugin) {
$index = array_search($plugin, $dirnames);
// plugin authorized, but its folder isn't listed
if ($index === false) {
continue;
}
try {
$this->loadPlugin($dirs[$index], $plugin);
} catch (PluginFileNotFoundException $e) {
error_log($e->getMessage());
}
}
}
/**
* Execute all plugins registered hook.
*
* @param string $hook name of the hook to trigger.
* @param array $data list of data to manipulate passed by reference.
* @param array $params additional parameters such as page target.
*
* @return void
*/
public function executeHooks($hook, &$data, $params = array())
{
if (!empty($params['target'])) {
$data['_PAGE_'] = $params['target'];
}
if (isset($params['loggedin'])) {
$data['_LOGGEDIN_'] = $params['loggedin'];
}
if (isset($params['basePath'])) {
$data['_BASE_PATH_'] = $params['basePath'];
}
foreach ($this->loadedPlugins as $plugin) {
$hookFunction = $this->buildHookName($hook, $plugin);
if (function_exists($hookFunction)) {
try {
$data = call_user_func($hookFunction, $data, $this->conf);
} catch (\Throwable $e) {
$error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
$this->errors = array_unique(array_merge($this->errors, [$error]));
}
}
}
}
/**
* Load a single plugin from its files.
* Call the init function if it exists, and collect errors.
* Add them in $loadedPlugins if successful.
*
* @param string $dir plugin's directory.
* @param string $pluginName plugin's name.
*
* @return void
* @throws \Shaarli\Plugin\Exception\PluginFileNotFoundException - plugin files not found.
*/
private function loadPlugin($dir, $pluginName)
{
if (!is_dir($dir)) {
throw new PluginFileNotFoundException($pluginName);
}
$pluginFilePath = $dir . '/' . $pluginName . '.php';
if (!is_file($pluginFilePath)) {
throw new PluginFileNotFoundException($pluginName);
}
$conf = $this->conf;
include_once $pluginFilePath;
$initFunction = $pluginName . '_init';
if (function_exists($initFunction)) {
$errors = call_user_func($initFunction, $this->conf);
if (!empty($errors)) {
$this->errors = array_merge($this->errors, $errors);
}
}
$this->loadedPlugins[] = $pluginName;
}
/**
* Construct normalize hook name for a specific plugin.
*
* Format:
* hook_<plugin_name>_<hook_name>
*
* @param string $hook hook name.
* @param string $pluginName plugin name.
*
* @return string - plugin's hook name.
*/
public function buildHookName($hook, $pluginName)
{
return 'hook_' . $pluginName . '_' . $hook;
}
/**
* Retrieve plugins metadata from *.meta (INI) files into an array.
* Metadata contains:
* - plugin description [description]
* - parameters split with ';' [parameters]
*
* Respects plugins order from settings.
*
* @return array plugins metadata.
*/
public function getPluginsMeta()
{
$metaData = array();
$dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
// Browse all plugin directories.
foreach ($dirs as $pluginDir) {
$plugin = basename($pluginDir);
$metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
if (!is_file($metaFile) || !is_readable($metaFile)) {
continue;
}
$metaData[$plugin] = parse_ini_file($metaFile);
$metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
if (isset($metaData[$plugin]['description'])) {
$metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
}
// Read parameters and format them into an array.
if (isset($metaData[$plugin]['parameters'])) {
$params = explode(';', $metaData[$plugin]['parameters']);
} else {
$params = array();
}
$metaData[$plugin]['parameters'] = array();
foreach ($params as $param) {
if (empty($param)) {
continue;
}
$metaData[$plugin]['parameters'][$param]['value'] = '';
// Optional parameter description in parameter.PARAM_NAME=
if (isset($metaData[$plugin]['parameter.' . $param])) {
$metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.' . $param]);
}
}
}
return $metaData;
}
/**
* Return the list of encountered errors.
*
* @return array List of errors (empty array if none exists).
*/
public function getErrors()
{
return $this->errors;
}
}