277 lines
10 KiB
PHP
277 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file classes/plugins/PluginRegistry.php
|
|
*
|
|
* Copyright (c) 2014-2021 Simon Fraser University
|
|
* Copyright (c) 2000-2021 John Willinsky
|
|
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
|
*
|
|
* @class PluginRegistry
|
|
*
|
|
* @ingroup plugins
|
|
*
|
|
* @see Plugin
|
|
*
|
|
* @brief Registry class for managing plugins.
|
|
*/
|
|
|
|
namespace PKP\plugins;
|
|
|
|
use APP\core\Application;
|
|
use Exception;
|
|
use FilesystemIterator;
|
|
use Illuminate\Support\Arr;
|
|
use PKP\core\Registry;
|
|
use ReflectionObject;
|
|
|
|
class PluginRegistry
|
|
{
|
|
/** Base path of plugins */
|
|
public const PLUGINS_PREFIX = 'plugins/';
|
|
|
|
/**
|
|
* Return all plugins in the given category as an array, or, if the
|
|
* category is not specified, all plugins in an associative array of
|
|
* arrays by category.
|
|
*/
|
|
public static function &getPlugins(?string $category = null): array
|
|
{
|
|
$plugins = & Registry::get('plugins', true, []); // Reference necessary
|
|
if ($category !== null) {
|
|
$plugins[$category] ??= [];
|
|
return $plugins[$category];
|
|
}
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Get all plugins in a single array.
|
|
*/
|
|
public static function getAllPlugins(): array
|
|
{
|
|
return array_reduce(static::getPlugins(), fn (array $output, array $pluginsByCategory) => $output += $pluginsByCategory, []);
|
|
}
|
|
|
|
/**
|
|
* Register a plugin with the registry in the given category.
|
|
*
|
|
* @param string $category the name of the category to extend
|
|
* @param Plugin $plugin The instantiated plugin to add
|
|
* @param string $path The path the plugin was found in
|
|
* @param int $mainContextId To identify enabled plug-ins
|
|
* we need a context. This context is usually taken from the
|
|
* request but sometimes there is no context in the request
|
|
* (e.g. when executing CLI commands). Then the main context
|
|
* can be given as an explicit ID.
|
|
*
|
|
* @return bool True IFF the plugin was registered successfully
|
|
*/
|
|
public static function register(string $category, Plugin $plugin, string $path, ?int $mainContextId = null): bool
|
|
{
|
|
$pluginName = $plugin->getName();
|
|
$plugins = & static::getPlugins();
|
|
|
|
// If the plugin is already loaded or failed/refused to register
|
|
if (isset($plugins[$category][$pluginName]) || !$plugin->register($category, $path, $mainContextId)) {
|
|
return false;
|
|
}
|
|
|
|
$plugins[$category][$pluginName] = $plugin;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get a plugin by category and name.
|
|
*/
|
|
public static function getPlugin(string $category, string $name): ?Plugin
|
|
{
|
|
return static::getPlugins()[$category][$name] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Load all plugins for a given category.
|
|
*
|
|
* @param string $category The name of the category to load
|
|
* @param bool $enabledOnly if true load only enabled
|
|
* plug-ins (db-installation required), otherwise look on
|
|
* disk and load all available plug-ins (no db required).
|
|
* @param int $mainContextId To identify enabled plug-ins
|
|
* we need a context. This context is usually taken from the
|
|
* request but sometimes there is no context in the request
|
|
* (e.g. when executing CLI commands). Then the main context
|
|
* can be given as an explicit ID.
|
|
*
|
|
* @return array Set of plugins, sorted in sequence.
|
|
*/
|
|
public static function loadCategory(string $category, bool $enabledOnly = false, ?int $mainContextId = null): array
|
|
{
|
|
static $cache;
|
|
$key = implode("\0", func_get_args());
|
|
$plugins = $cache[$key] ??= $enabledOnly && Application::isInstalled()
|
|
? static::_loadFromDatabase($category, $mainContextId)
|
|
: static::_loadFromDisk($category);
|
|
|
|
// Fire a hook prior to registering plugins for a category
|
|
// n.b.: this should not be used from a PKPPlugin::register() call to "jump categories"
|
|
Hook::call('PluginRegistry::loadCategory', [&$category, &$plugins]);
|
|
|
|
// Register the plugins in sequence.
|
|
ksort($plugins);
|
|
array_walk_recursive($plugins, fn (Plugin $plugin, string $pluginPath) => static::register($category, $plugin, $pluginPath, $mainContextId));
|
|
|
|
// Return the list of successfully-registered plugins.
|
|
$plugins = & static::getPlugins($category);
|
|
|
|
// Fire a hook after all plugins of a category have been loaded, so they
|
|
// are able to interact if required
|
|
Hook::call("PluginRegistry::categoryLoaded::{$category}", [&$plugins]);
|
|
|
|
// Sort the plugins by priority before returning.
|
|
uasort($plugins, fn (Plugin $a, Plugin $b) => $a->getSeq() - $b->getSeq());
|
|
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Load a specific plugin from a category by path name.
|
|
* Similar to loadCategory, except that it only loads a single plugin
|
|
* within a category rather than loading all.
|
|
*
|
|
* @param int $mainContextId To identify enabled plug-ins
|
|
* we need a context. This context is usually taken from the
|
|
* request but sometimes there is no context in the request
|
|
* (e.g. when executing CLI commands). Then the main context
|
|
* can be given as an explicit ID.
|
|
*/
|
|
public static function loadPlugin(string $category, string $pluginName, ?int $mainContextId = null): ?Plugin
|
|
{
|
|
if ($plugin = static::_instantiatePlugin($category, $pluginName)) {
|
|
static::register($category, $plugin, self::PLUGINS_PREFIX . "{$category}/{$pluginName}", $mainContextId);
|
|
}
|
|
return $plugin;
|
|
}
|
|
|
|
/**
|
|
* Get a list of the various plugin categories available.
|
|
*
|
|
* NB: The categories are returned in the order in which they
|
|
* have to be registered and/or installed. Plug-ins in categories
|
|
* later in the list may depend on plug-ins in earlier
|
|
* categories.
|
|
*/
|
|
public static function getCategories(): array
|
|
{
|
|
$categories = Application::get()->getPluginCategories();
|
|
Hook::call('PluginRegistry::getCategories', [&$categories]);
|
|
return $categories;
|
|
}
|
|
|
|
/**
|
|
* Load all plugins in the system and return them in a single array.
|
|
*/
|
|
public static function loadAllPlugins(bool $enabledOnly = false): array
|
|
{
|
|
// Retrieve and register categories (order is significant).
|
|
$categories = static::getCategories();
|
|
return array_reduce($categories, fn (array $plugins, string $category) => $plugins + static::loadCategory($category, $enabledOnly), []);
|
|
}
|
|
|
|
/**
|
|
* Instantiate a plugin.
|
|
*/
|
|
private static function _instantiatePlugin(string $category, string $pluginName, ?string $classToCheck = null): ?Plugin
|
|
{
|
|
if (!preg_match('/^[a-z0-9]+$/i', $pluginName)) {
|
|
throw new Exception("Invalid product name \"{$pluginName}\"");
|
|
}
|
|
|
|
// First, try a namespaced class name matching the installation directory.
|
|
$pluginClassName = "\\APP\\plugins\\{$category}\\{$pluginName}\\" . ucfirst($pluginName) . 'Plugin';
|
|
$plugin = class_exists($pluginClassName)
|
|
? new $pluginClassName()
|
|
: static::_deprecatedInstantiatePlugin($category, $pluginName);
|
|
|
|
$classToCheck = $classToCheck ?: Plugin::class;
|
|
$isObject = is_object($plugin);
|
|
// Complements $classToCheck with a namespace when needed
|
|
if (!str_contains($classToCheck, '\\') && $isObject && ($reflection = new ReflectionObject($plugin))->inNamespace()) {
|
|
$classToCheck = "{$reflection->getNamespaceName()}\\{$classToCheck}";
|
|
}
|
|
if ($plugin !== null && !($plugin instanceof $classToCheck)) {
|
|
$type = $isObject ? $plugin::class : gettype($plugin);
|
|
error_log(new Exception("Plugin {$pluginName} expected to inherit from {$classToCheck}, actual type {$type}"));
|
|
return null;
|
|
}
|
|
return $plugin;
|
|
}
|
|
|
|
/**
|
|
* Attempts to retrieve plugins from the database.
|
|
*/
|
|
private static function _loadFromDatabase(string $category, ?int $mainContextId = null): array
|
|
{
|
|
$plugins = [];
|
|
$categoryDir = static::PLUGINS_PREFIX . $category;
|
|
$products = Application::get()->getEnabledProducts("plugins.{$category}", $mainContextId);
|
|
foreach ($products as $product) {
|
|
$name = $product->getProduct();
|
|
if ($plugin = static::_instantiatePlugin($category, $name, $product->getProductClassname())) {
|
|
$plugins[$plugin->getSeq()]["{$categoryDir}/{$name}"] = $plugin;
|
|
}
|
|
}
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Get all plug-ins from disk without querying the database, used during installation.
|
|
*/
|
|
private static function _loadFromDisk(string $category): array
|
|
{
|
|
$categoryDir = static::PLUGINS_PREFIX . $category;
|
|
if (!is_dir($categoryDir)) {
|
|
return [];
|
|
}
|
|
$plugins = [];
|
|
foreach (new FilesystemIterator($categoryDir) as $path) {
|
|
if (!$path->isDir()) {
|
|
continue;
|
|
}
|
|
$pluginName = $path->getFilename();
|
|
if ($plugin = static::_instantiatePlugin($category, $pluginName)) {
|
|
$plugins[$plugin->getSeq()]["{$categoryDir}/{$pluginName}"] = $plugin;
|
|
}
|
|
}
|
|
return $plugins;
|
|
}
|
|
|
|
/**
|
|
* Instantiate a plugin.
|
|
*
|
|
* @deprecated 3.4.0 Old way to instantiate a plugin
|
|
*/
|
|
private static function _deprecatedInstantiatePlugin(string $category, string $pluginName): ?Plugin
|
|
{
|
|
$pluginPath = static::PLUGINS_PREFIX . "{$category}/{$pluginName}";
|
|
// Try the plug-in wrapper for backwards compatibility.
|
|
$pluginWrapper = "{$pluginPath}/index.php";
|
|
if (file_exists($pluginWrapper)) {
|
|
return include $pluginWrapper;
|
|
}
|
|
|
|
// Try the well-known plug-in class name next (with and without ".inc.php")
|
|
$pluginClassName = ucfirst($pluginName) . ucfirst($category) . 'Plugin';
|
|
if (Arr::first(['.inc.php', '.php'], fn (string $suffix) => file_exists("{$pluginPath}/{$pluginClassName}{$suffix}"))) {
|
|
$pluginPackage = "plugins.{$category}.{$pluginName}";
|
|
return instantiate("{$pluginPackage}.{$pluginClassName}", $pluginClassName, $pluginPackage, 'register');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!PKP_STRICT_MODE) {
|
|
class_alias('\PKP\plugins\PluginRegistry', '\PluginRegistry');
|
|
define('PLUGINS_PREFIX', PluginRegistry::PLUGINS_PREFIX);
|
|
}
|