<?php
/**
* @defgroup plugins Plugins
* Implements a plugin structure that can be used to flexibly extend PKP
* software via the use of a set of plugin categories.
*/
/**
* @file classes/plugins/Plugin.inc.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 Plugin
* @ingroup plugins
* @see PluginRegistry, PluginSettingsDAO
*
* @brief Abstract class for plugins
*
* For best performance, a plug-in should not be instantiated if it is
* disabled or the current page/operation does not require the plug-in's
* functionality.
*
* Newer plug-ins support enable/disable and request filter settings that
* enable the PKP library plug-in framework to lazy-load plug-ins only
* when their functionality is actually being required for a request.
*
* For backwards compatibility we need to assume that older plug-ins
* do not support lazy-load because their register() method and hooks
* may have side-effects required on all requests. We have no way of
* knowing on which pages these side effects are important so we need
* to load legacy plug-ins on all pages.
*
* In these cases the register() function will be called on every request
* when the category the plug-in belongs to is being loaded. This was the
* default behavior before plug-in lazy load was introduced.
*
* Plug-ins that want to enable lazy-load have to include a 'lazy-load'
* setting in their version.xml:
*
* <lazy-load>1</lazy-load>
*/
// Define the well-known file name for filter configuration data.
define('PLUGIN_FILTER_DATAFILE', 'filterConfig.xml');
define('PLUGIN_TEMPLATE_RESOURCE_PREFIX', 'plugins');
abstract class Plugin {
/** @var string Path name to files for this plugin */
var $pluginPath;
/** @var string Category name this plugin is registered to*/
var $pluginCategory;
/** @var PKPRequest the current request object */
var $request;
/**
* Constructor
*/
function __construct() {
}
/*
* Public Plugin API (Registration and Initialization)
*/
/**
* Load and initialize the plug-in and register plugin hooks.
*
* For backwards compatibility this method will be called whenever
* the plug-in's category is being loaded. If, however, registerOn()
* returns an array then this method will only be called when
* the plug-in is enabled and an entry in the result set of
* registerOn() matches the current request operation. An empty array
* matches all request operations.
*
* @param $category String Name of category plugin was registered to
* @param $path String The path the plugin was found in
* @param $mainContextId integer To identify if the plugin is enabled
* 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 boolean True iff plugin registered successfully; if false,
* the plugin will not be executed.
*/
function register($category, $path, $mainContextId = null) {
$this->pluginPath = $path;
$this->pluginCategory = $category;
if ($this->getInstallMigration()) {
HookRegistry::register ('Installer::postInstall', [$this, 'updateSchema']);
}
if ($this->getInstallSitePluginSettingsFile()) {
HookRegistry::register ('Installer::postInstall', [$this, 'installSiteSettings']);
}
if ($this->getInstallControlledVocabFiles()) {
HookRegistry::register ('Installer::postInstall', [$this, 'installControlledVocabs']);
}
if ($this->getInstallEmailTemplatesFile()) {
HookRegistry::register ('Installer::postInstall', [$this, 'installEmailTemplates']);
HookRegistry::register ('PKPLocale::installLocale', [$this, 'installLocale']);
}
if ($this->getInstallEmailTemplateDataFile()) {
HookRegistry::register ('Installer::postInstall', [$this, 'installEmailTemplateData']);
}
if ($this->getContextSpecificPluginSettingsFile()) {
HookRegistry::register ('Context::add', [$this, 'installContextSpecificSettings']);
}
HookRegistry::register ('Installer::postInstall', [$this, 'installFilters']);
$this->_registerTemplateResource();
return true;
}
/**
* Protected methods (may be overridden by custom plugins)
*/
//
// Plugin Display
//
/**
* Get the name of this plugin. The name must be unique within
* its category, and should be suitable for part of a filename
* (ie short, no spaces, and no dependencies on cases being unique).
*
* @return string name of plugin
*/
abstract function getName();
/**
* Get the display name for this plugin.
*
* @return string
*/
abstract function getDisplayName();
/**
* Get a description of this plugin.
*
* @return string
*/
abstract function getDescription();
//
// Plugin Behavior and Management
//
/**
* Return a number indicating the sequence in which this plugin
* should be registered compared to others of its category.
* Higher = later.
*
* @return integer
*/
function getSeq() {
return 0;
}
/**
* Site-wide plugins should override this function to return true.
*
* @return boolean
*/
function isSitePlugin() {
return false;
}
/**
* Perform a management function.
* @param $args array
* @param $request PKPRequest
* @return JSONMessage A JSON-encoded response
*/
function manage($args, $request) {
throw new Exception('Unhandled management action!');
}
/**
* Determine whether or not this plugin should be hidden from the
* management interface. Useful in the case of derivative plugins,
* i.e. when a generic plugin registers a feed plugin.
*
* @return boolean
*/
function getHideManagement() {
return false;
}
//
// Plugin Installation
//
/**
* @deprecated See https://github.com/pkp/pkp-lib/issues/2493
*/
final public function getInstallSchemaFile() {
}
/**
* Get the installation migration for this plugin.
* @return Illuminate\Database\Migrations\Migration?
*/
function getInstallMigration() {
return null;
}
/**
* Get the filename of the settings data for this plugin to install
* when the system is installed (i.e. site-level plugin settings).
* Subclasses using default settings should override this.
*
* @return string
*/
function getInstallSitePluginSettingsFile() {
return null;
}
/**
* Get the filename of the controlled vocabulary for this plugin to
* install when the system is installed. Null if none included.
* @return array|null Filename of controlled vocabs XML file.
*/
function getInstallControlledVocabFiles() {
return array();
}
/**
* Get the filename of the settings data for this plugin to install
* when a new application context (e.g. journal, conference or press)
* is installed.
*
* Subclasses using default settings should override this.
*
* @return string
*/
function getContextSpecificPluginSettingsFile() {
return null;
}
/**
* Get the filename of the email templates for this plugin.
* Subclasses using email templates should override this.
*
* @return string
*/
function getInstallEmailTemplatesFile() {
return null;
}
/**
* Get the filename of the email template data for this plugin.
* Subclasses using email templates should override this.
* @deprecated Starting with OJS/OMP 3.2, localized content should be
* specified via getInstallEmailTemplatesFile(). (pkp/pkp-lib#5461)
*
* @return string
*/
function getInstallEmailTemplateDataFile() {
return null;
}
/**
* Get the filename(s) of the filter configuration data for
* this plugin. Subclasses using filters can override this.
*
* The default implementation establishes "well known" locations
* for the filter configuration. If you keep your files in these
* locations then there's no need to override this method.
*
* @return string|array one or more file locations.
*/
function getInstallFilterConfigFiles() {
// Construct the well-known filter configuration file names.
$filterConfigFile = $this->getPluginPath().'/filter/'.PLUGIN_FILTER_DATAFILE;
$filterConfigFiles = array(
'./lib/pkp/'.$filterConfigFile,
'./'.$filterConfigFile
);
return $filterConfigFiles;
}
/*
* Protected helper methods (can be used by custom plugins but
* should not be overridden by custom plugins)
*/
/**
* Get the name of the category this plugin is registered to.
* @return String category
*/
function getCategory() {
return $this->pluginCategory;
}
/**
* Get the path this plugin's files are located in.
* @return String pathname
*/
function getPluginPath() {
return $this->pluginPath;
}
/**
* Get the directory name of the plugin
* @return String directory name
*/
function getDirName() {
return basename($this->pluginPath);
}
/**
* Return the Resource Name for templates in this plugin, or if specified, the full resource locator
* for a specific template.
* @param $template Template path/filename, if desired
* @param $inCore boolean True if a "core" template should be used.
* @return string
*/
public function getTemplateResource($template = null, $inCore = false) {
$pluginPath = $this->getPluginPath();
if ($inCore) {
$pluginPath = PKP_LIB_PATH . DIRECTORY_SEPARATOR . $pluginPath;
}
$plugin = basename($pluginPath);
$category = basename(dirname($pluginPath));
$contextId = CONTEXT_SITE;
if (Config::getVar('general', 'installed')) {
$context = Application::get()->getRequest()->getContext();
if (is_a($context, 'Context')) $contextId = $context->getId();
}
// Slash characters (/) are not allowed in resource names, so use dashes (-) instead.
$resourceName = strtr(join('/', array(PLUGIN_TEMPLATE_RESOURCE_PREFIX, $contextId, $pluginPath, $category, $plugin)),'/','-');
return $resourceName . ($template!==null?":$template":'');
}
/**
* Return the canonical template path of this plug-in
* @param $inCore Return the core template path if true.
* @return string|null
*/
function getTemplatePath($inCore = false) {
$templatePath = ($inCore?PKP_LIB_PATH . DIRECTORY_SEPARATOR:'') . $this->getPluginPath() . DIRECTORY_SEPARATOR . 'templates';
if (is_dir($templatePath)) return $templatePath;
return null;
}
/**
* Register this plugin's templates as a template resource
* @param $inCore boolean True iff this is a core resource.
*/
protected function _registerTemplateResource($inCore = false) {
if ($templatePath = $this->getTemplatePath($inCore)) {
$templateMgr = TemplateManager::getManager();
$pluginTemplateResource = new PKPTemplateResource($templatePath);
$templateMgr->registerResource($this->getTemplateResource(null, $inCore), $pluginTemplateResource);
}
}
/**
* Call this method when an enabled plugin is registered in order to override
* template files. Any plugin which calls this method can
* override template files by adding their own templates to:
* <overridingPlugin>/templates/plugins/<category>/<originalPlugin>/templates/<path>.tpl
*
* @param $hookName string TemplateResource::getFilename
* @param $args array [
* @option string File path to preferred template. Leave as-is to not
* override template.
* @option string Template file requested
* ]
* @return boolean
*/
public function _overridePluginTemplates($hookName, $args) {
$filePath =& $args[0];
$template = $args[1];
$checkFilePath = $filePath;
// If there's a templates/ prefix on the template, clean up the test path.
if (strpos($filePath, 'plugins/') === 0) $checkFilePath = 'templates/' . $checkFilePath;
// If there's a lib/pkp/ prefix on the template, test without it.
$libPkpPrefix = 'lib' . DIRECTORY_SEPARATOR . 'pkp' . DIRECTORY_SEPARATOR;
if (strpos($checkFilePath, $libPkpPrefix) === 0) $checkFilePath = substr($filePath, strlen($libPkpPrefix));
// Check if an overriding plugin exists in the plugin path.
if ($overriddenFilePath = $this->_findOverriddenTemplate($checkFilePath)) {
$filePath = $overriddenFilePath;
}
return false;
}
/**
* Recursive check for existing templates
* @param $path string
* @return string|null
*/
private function _findOverriddenTemplate($path) {
$fullPath = sprintf('%s/%s', $this->getPluginPath(), $path);
if (file_exists($fullPath)) {
return $fullPath;
}
// Backward compatibility for OJS prior to 3.1.2; changed path to templates for plugins.
if (($fullPath = preg_replace("/templates\/(?!.*templates\/)/", "", $fullPath)) && file_exists($fullPath)) {
if (Config::getVar('debug', 'deprecation_warnings')) {
trigger_error('Deprecated: The template at ' . $fullPath . ' has moved and will not be found in the future.');
}
return $fullPath;
}
// Recursive check for templates in ancestors of a current theme plugin
if (is_a($this, 'ThemePlugin')
&& $this->parent
&& $fullPath = $this->parent->_findOverriddenTemplate($path)) {
return $fullPath;
}
return null;
}
/**
* Load locale data for this plugin.
*
* @param $locale string|null
* @return boolean
*/
function addLocaleData($locale = null) {
$locale = $locale ?? AppLocale::getLocale();
if ($localeFilenames = $this->getLocaleFilename($locale)) {
foreach((array) $localeFilenames as $localeFilename) {
AppLocale::registerLocaleFile($locale, $localeFilename);
}
return true;
}
return false;
}
/**
* Retrieve a plugin setting within the given context
*
* @param $contextId int Context ID
* @param $name string Setting name
*/
function getSetting($contextId, $name) {
if (!defined('RUNNING_UPGRADE') && !Config::getVar('general', 'installed')) return null;
// Construct the argument list and call the plug-in settings DAO
$arguments = array(
$contextId,
$this->getName(),
$name,
);
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /* @var $pluginSettingsDao PluginSettingsDAO */
return call_user_func_array(array(&$pluginSettingsDao, 'getSetting'), $arguments);
}
/**
* Update a plugin setting within the given context.
*
* @param $contextId int Context ID
* @param $name string The name of the setting
* @param $value mixed Setting value
* @param $type string optional
*/
function updateSetting($contextId, $name, $value, $type = null) {
// Construct the argument list and call the plug-in settings DAO
$arguments = array(
$contextId,
$this->getName(),
$name,
$value,
$type,
);
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /* @var $pluginSettingsDao PluginSettingsDAO */
call_user_func_array(array(&$pluginSettingsDao, 'updateSetting'), $arguments);
}
/**
* Load a PHP file from this plugin's installation directory.
*
* @param $class string
*/
function import($class) {
require_once($this->getPluginPath() . '/' . str_replace('.', '/', $class) . '.inc.php');
}
/*
* Helper methods (for internal use only, should not
* be used by custom plug-ins)
*
* NB: These methods may change without notice in the future!
*/
/**
* Get the filename for the locale data for this plugin.
* (Warning: This function is used by the custom locale plugin)
*
* @param $locale string
* @return array The locale file names.
*/
public function getLocaleFilename($locale) {
$masterLocale = MASTER_LOCALE;
$baseLocaleFilename = $this->getPluginPath() . "/locale/$locale/locale.po";
$baseMasterLocaleFilename = $this->getPluginPath() . "/locale/$masterLocale/locale.po";
$libPkpFilename = "lib/pkp/$baseLocaleFilename";
$masterLibPkpFilename = "lib/pkp/$baseMasterLocaleFilename";
return array_filter([
file_exists($baseMasterLocaleFilename) ? $baseLocaleFilename : false,
file_exists($masterLibPkpFilename) ? $libPkpFilename : false,
]);
}
/**
* Callback used to install settings on system install.
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function installSiteSettings($hookName, $args) {
// All contexts are set to zero for site-wide plug-in settings
$application = Application::get();
$contextDepth = $application->getContextDepth();
if ($contextDepth >0) {
$arguments = array_fill(0, $contextDepth, 0);
} else {
$arguments = array();
}
$arguments[] = $this->getName();
$arguments[] = $this->getInstallSitePluginSettingsFile();
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /* @var $pluginSettingsDao PluginSettingsDAO */
call_user_func_array(array(&$pluginSettingsDao, 'installSettings'), $arguments);
return false;
}
/**
* Callback used to install controlled vocabularies on system install.
* @param $hookName string
* @param $args array
* @return boolean
*/
function installControlledVocabs($hookName, $args) {
// All contexts are set to zero for site-wide plug-in settings
$controlledVocabDao = DAORegistry::getDAO('ControlledVocabDAO'); /* @var $controlledVocabDao ControlledVocabDAO */
foreach ($this->getInstallControlledVocabFiles() as $file) {
$controlledVocabDao->installXML($file);
}
return false;
}
/**
* Callback used to install settings on new context
* (e.g. journal, conference or press) creation.
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function installContextSpecificSettings($hookName, $args) {
$context = $args[0];
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /* @var $pluginSettingsDao PluginSettingsDAO */
$pluginSettingsDao->installSettings($context->getId(), $this->getName(), $this->getContextSpecificPluginSettingsFile());
return false;
}
/**
* Callback used to install email templates.
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function installEmailTemplates($hookName, $args) {
$installer =& $args[0]; /* @var $installer Installer */
$result =& $args[1];
// Load email template data as required from the locale files.
$emailTemplateDao = DAORegistry::getDAO('EmailTemplateDAO'); /* @var $emailTemplateDao EmailTemplateDAO */
$emailTemplateLocales = [];
foreach ($installer->installedLocales as $locale) {
$emailFile = $this->getPluginPath() . "/locale/$locale/emails.po";
if (!file_exists($emailFile)) continue;
AppLocale::registerLocaleFile($locale, $emailFile);
$emailTemplateLocales[] = $locale;
}
$sql = $emailTemplateDao->installEmailTemplates($this->getInstallEmailTemplatesFile(), $emailTemplateLocales, true, null, true);
if ($sql === false) {
// The template file seems to be invalid.
$installer->setError(INSTALLER_ERROR_DB, str_replace('{$file}', $this->getInstallEmailTemplatesFile(), __('installer.installParseEmailTemplatesFileError')));
$result = false;
} else {
// Are there any yet uninstalled email templates?
assert(is_array($sql));
if (!empty($sql)) {
// Install templates.
$result = $installer->executeSQL($sql);
}
}
return false;
}
/**
* Callback used to install email template data.
* @deprecated Email template data should be installed via installEmailTemplates (pkp/pkp-lib#5461)
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function installEmailTemplateData($hookName, $args) {
$installer =& $args[0];
$result =& $args[1];
$emailTemplateDao = DAORegistry::getDAO('EmailTemplateDAO'); /* @var $emailTemplateDao EmailTemplateDAO */
foreach ($installer->installedLocales as $locale) {
$filename = str_replace('{$installedLocale}', $locale, $this->getInstallEmailTemplateDataFile());
if (!file_exists($filename)) continue;
$sql = $emailTemplateDao->installEmailTemplateData($filename, $locale, true);
if ($sql) {
$result = $installer->executeSQL($sql);
} else {
$installer->setError(INSTALLER_ERROR_DB, str_replace('{$file}', $filename, __('installer.installParseEmailTemplatesFileError')));
$result = false;
}
}
return false;
}
/**
* Callback used to install email template data on locale install.
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function installLocale($hookName, $args) {
$locale =& $args[0];
$filename = str_replace('{$installedLocale}', $locale, $this->getInstallEmailTemplateDataFile());
$emailTemplateDao = DAORegistry::getDAO('EmailTemplateDAO'); /* @var $emailTemplateDao EmailTemplateDAO */
// Since pkp/pkp-lib#5461, there are two ways to specify localized email data in plugins.
// Install locale data specified in the old form. (Deprecated!)
if ($this->getInstallEmailTemplateDataFile()) $emailTemplateDao->installEmailTemplateData($filename, $locale);
// Install locale data specified in the new form.
if (file_exists($emailFile = $this->getPluginPath() . "/locale/$locale/emails.po")) {
AppLocale::registerLocaleFile($locale, $emailFile);
$emailTemplateDao->installEmailTemplateLocaleData($this->getInstallEmailTemplatesFile(), [$locale]);
}
return false;
}
/**
* Callback used to install filters.
* @param $hookName string
* @param $args array
*/
function installFilters($hookName, $args) {
$installer =& $args[0]; /* @var $installer Installer */
$result =& $args[1]; /* @var $result boolean */
// Get the filter configuration file name(s).
$filterConfigFiles = $this->getInstallFilterConfigFiles();
if (is_scalar($filterConfigFiles)) $filterConfigFiles = array($filterConfigFiles);
// Run through the config file positions and see
// whether one of these exists and needs to be installed.
foreach($filterConfigFiles as $filterConfigFile) {
// Is there a filter configuration?
if (!file_exists($filterConfigFile)) continue;
// Install the filter configuration.
$result = $installer->installFilterConfig($filterConfigFile);
if (!$result) {
// The filter configuration file seems to be invalid.
$installer->setError(INSTALLER_ERROR_DB, str_replace('{$file}', $filterConfigFile, __('installer.installParseFilterConfigFileError')));
}
}
// Do not stop installation.
return false;
}
/**
* Called during the install process to install the plugin schema,
* if applicable.
*
* @param $hookName string
* @param $args array
* @return boolean
*/
function updateSchema($hookName, $args) {
$installer =& $args[0];
$result =& $args[1];
if ($migration = $this->getInstallMigration()) try {
$migration->up();
} catch (Exception $e) {
$installer->setError(INSTALLER_ERROR_DB, __('installer.installMigrationError', ['class' => get_class($migration)]));
$result = false;
}
return false;
}
/**
* Extend the {url ...} smarty to support plugins.
*
* @param $params array
* @param $smarty Smarty
* @return string
*/
function smartyPluginUrl($params, $smarty) {
$path = array($this->getCategory(), $this->getName());
if (is_array($params['path'])) {
$params['path'] = array_merge($path, $params['path']);
} elseif (!empty($params['path'])) {
$params['path'] = array_merge($path, array($params['path']));
} else {
$params['path'] = $path;
}
return $smarty->smartyUrl($params, $smarty);
}
/**
* Get the current version of this plugin
*
* @return Version
*/
function getCurrentVersion() {
$versionDao = DAORegistry::getDAO('VersionDAO'); /* @var $versionDao VersionDAO */
$pluginPath = $this->getPluginPath();
$product = basename($pluginPath);
$category = basename(dirname($pluginPath));
$installedPlugin = $versionDao->getCurrentVersion('plugins.'.$category, $product, true);
if ($installedPlugin) {
return $installedPlugin;
} else {
return false;
}
}
/**
* Get the current request object
* @return PKPRequest
*/
function &getRequest() {
if (!$this->request) {
$this->request =& Registry::get('request');
}
return $this->request;
}
/*
* Private helper methods
*/
/**
* Get a list of link actions for plugin management.
* @param request PKPRequest
* @param $actionArgs array The list of action args to be included in request URLs.
* @return array List of LinkActions
*/
function getActions($request, $actionArgs) {
return array();
}
/**
* Determine whether the plugin can be enabled.
* @return boolean
*/
function getCanEnable() {
return false;
}
/**
* Determine whether the plugin can be disabled.
* @return boolean
*/
function getCanDisable() {
return false;
}
/**
* Determine whether the plugin is enabled.
* @return boolean
*/
function getEnabled() {
return true;
}
}
|