<?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);
}
|