<?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.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>
 */

namespace PKP\plugins;

use APP\core\Application;
use APP\template\TemplateManager;
use Exception;
use Illuminate\Database\Migrations\Migration;
use PKP\config\Config;
use PKP\core\JSONMessage;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\core\Registry;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\facades\Repo;
use PKP\install\Installer;
use PKP\observers\events\PluginSettingChanged;
use PKP\site\Version;
use PKP\site\VersionDAO;
use PKP\template\PKPTemplateResource;

// 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 */
    public $pluginPath;

    /** @var string Category name this plugin is registered to*/
    public $pluginCategory;

    /** @var PKPRequest the current request object */
    public $request;

    /**
     * Constructor
     */
    public 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 string $category Name of category plugin was registered to
     * @param string $path The path the plugin was found in
     * @param int $mainContextId 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 bool True iff plugin registered successfully; if false,
     * 	the plugin will not be executed.
     */
    public function register($category, $path, $mainContextId = null)
    {
        $this->pluginPath = $path;
        $this->pluginCategory = $category;
        if ($this->getInstallMigration()) {
            Hook::add('Installer::postInstall', [$this, 'updateSchema']);
        }
        if ($this->getInstallSitePluginSettingsFile()) {
            Hook::add('Installer::postInstall', [$this, 'installSiteSettings']);
        }
        if ($this->getInstallEmailTemplatesFile()) {
            Hook::add('Installer::postInstall', [$this, 'installEmailTemplates']);
            Hook::add('Locale::installLocale', [$this, 'installLocale']);
        }
        if ($this->getInstallEmailTemplateDataFile()) {
            Hook::add('Installer::postInstall', [$this, 'installEmailTemplateData']);
        }
        if ($this->getContextSpecificPluginSettingsFile()) {
            Hook::add('Context::add', [$this, 'installContextSpecificSettings']);
        }

        Hook::add('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 public function getName();

    /**
     * Get the display name for this plugin.
     *
     * @return string
     */
    abstract public function getDisplayName();

    /**
     * Get a description of this plugin.
     *
     * @return string
     */
    abstract public 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 int
     */
    public function getSeq()
    {
        return 0;
    }

    /**
     * Site-wide plugins should override this function to return true.
     *
     * @return bool
     */
    public function isSitePlugin()
    {
        return false;
    }

    /**
     * Perform a management function.
     *
     * @param array $args
     * @param PKPRequest $request
     *
     * @return JSONMessage A JSON-encoded response
     */
    public 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 bool
     */
    public 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 ?Migration
     */
    public 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
     */
    public function getInstallSitePluginSettingsFile()
    {
        return null;
    }

    /**
     * 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
     */
    public function getContextSpecificPluginSettingsFile()
    {
        return null;
    }

    /**
     * Get the filename of the email templates for this plugin.
     * Subclasses using email templates should override this.
     *
     * @return string
     */
    public 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
     */
    public 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.
     */
    public function getInstallFilterConfigFiles()
    {
        // Construct the well-known filter configuration file names.
        $filterConfigFile = $this->getPluginPath() . '/filter/' . PLUGIN_FILTER_DATAFILE;
        $filterConfigFiles = [
            './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
     */
    public function getCategory()
    {
        return $this->pluginCategory;
    }

    /**
     * Get the path this plugin's files are located in.
     *
     * @return string pathname
     */
    public function getPluginPath()
    {
        return $this->pluginPath;
    }

    /**
     * Get the directory name of the plugin
     *
     * @return string directory name
     */
    public 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 string $template path/filename, if desired
     * @param bool $inCore 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 . "/{$pluginPath}";
        }
        $plugin = basename($pluginPath);
        $category = basename(dirname($pluginPath));

        $contextId = PKPApplication::CONTEXT_SITE;
        if (Application::isInstalled()) {
            $context = Application::get()->getRequest()->getContext();
            if ($context instanceof \PKP\context\Context) {
                $contextId = $context->getId();
            }
        }

        // Slash characters (/) are not allowed in resource names, so use dashes (-) instead.
        $resourceName = strtr(join('/', [PLUGIN_TEMPLATE_RESOURCE_PREFIX, $contextId, $pluginPath, $category, $plugin]), '/', '-');
        return $resourceName . ($template !== null ? ":{$template}" : '');
    }

    /**
     * Return the canonical template path of this plug-in
     *
     * @param bool $inCore Return the core template path if true.
     *
     * @return string|null
     */
    public function getTemplatePath($inCore = false)
    {
        $templatePath = ($inCore ? PKP_LIB_PATH . '/' : '') . "{$this->getPluginPath()}/templates";
        if (is_dir($templatePath)) {
            return $templatePath;
        }
        return null;
    }

    /**
     * Register this plugin's templates as a template resource
     *
     * @param bool $inCore True iff this is a core resource.
     */
    protected function _registerTemplateResource($inCore = false)
    {
        if ($templatePath = $this->getTemplatePath($inCore)) {
            $templateMgr = TemplateManager::getManager(Application::get()->getRequest());
            $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 string $hookName TemplateResource::getFilename
     * @param array $args [
     *
     *		@option string File path to preferred template. Leave as-is to not
     *			override template.
     *		@option string Template file requested
     * ]
     *
     * @return bool
     */
    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/pkp/';
        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 string $path
     *
     * @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 ($this instanceof ThemePlugin
            && $this->parent
            && $fullPath = $this->parent->_findOverriddenTemplate($path)) {
            return $fullPath;
        }

        return null;
    }

    /**
     * Load locale data for this plugin.
     */
    public function addLocaleData(): void
    {
        $basePath = $this->getPluginPath() . '/locale';
        foreach ([$basePath, "lib/pkp/{$basePath}"] as $path) {
            if (is_dir($path)) {
                Locale::registerPath($path);
            }
        }
    }

    /**
     * Retrieve a plugin setting within the given context
     *
     * @param int $contextId Context ID
     * @param string $name Setting name
     */
    public function getSetting($contextId, $name)
    {
        if (!Application::isUpgrading() && !Application::isInstalled()) {
            return null;
        }

        $pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */
        return $pluginSettingsDao->getSetting($contextId, $this->getName(), $name);
    }

    /**
     * Update a plugin setting within the given context.
     *
     * @param int $contextId Context ID
     * @param string $name The name of the setting
     * @param mixed $value Setting value
     * @param string $type optional
     */
    public function updateSetting($contextId, $name, $value, $type = null)
    {
        $pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */
        $pluginSettingsDao->updateSetting($contextId, $this->getName(), $name, $value, $type);

        event(new PluginSettingChanged($this, $name, $value, $contextId));
    }

    /**
     * Load a PHP file from this plugin's installation directory.
     *
     * @deprecated 3.4.0 pkp/pkp-lib#8186
     *
     * @param string $class
     */
    public 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!
     */
    /**
     * Callback used to install settings on system install.
     *
     * @param string $hookName
     * @param array $args
     *
     * @return bool
     */
    public function installSiteSettings($hookName, $args)
    {
        $pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */
        $pluginSettingsDao->installSettings(PKPApplication::CONTEXT_SITE, $this->getName(), $this->getInstallSitePluginSettingsFile());

        return false;
    }

    /**
     * Callback used to install settings on new context
     * (e.g. journal, conference or press) creation.
     *
     * @param string $hookName
     * @param array $args
     *
     * @return bool
     */
    public 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 string $hookName
     * @param array $args
     *
     * @return bool
     */
    public function installEmailTemplates($hookName, $args)
    {
        $installer = & $args[0]; /** @var Installer $installer */
        $result = & $args[1];

        // Load email template data as required from the locale files.
        $locales = [];
        foreach ($installer->installedLocales as $locale) {
            if (file_exists($this->getPluginPath() . "/locale/{$locale}/emails.po")) {
                $locales[] = $locale;
            }
        }
        // Localized data is needed by the email installation
        $this->addLocaleData();
        $status = Repo::emailTemplate()->dao->installEmailTemplates($this->getInstallEmailTemplatesFile(), $locales, null, true);

        if ($status === false) {
            // The template file seems to be invalid.
            $installer->setError(Installer::INSTALLER_ERROR_DB, str_replace('{$file}', $this->getInstallEmailTemplatesFile(), __('installer.installParseEmailTemplatesFileError')));
            $result = false;
        }
        return false;
    }

    /**
     * Callback used to install email template data.
     *
     * @deprecated Email template data should be installed via installEmailTemplates (pkp/pkp-lib#5461)
     *
     * @param string $hookName
     * @param array $args
     *
     * @return bool
     */
    public function installEmailTemplateData($hookName, $args)
    {
        $installer = & $args[0];
        $result = & $args[1];

        foreach ($installer->installedLocales as $locale) {
            $filename = str_replace('{$installedLocale}', $locale, $this->getInstallEmailTemplateDataFile());
            if (!file_exists($filename)) {
                continue;
            }
            $sql = Repo::emailTemplate()->dao->installEmailTemplateData($filename, $locale, true);
            if ($sql) {
                $result = $installer->executeSQL($sql);
            } else {
                $installer->setError(Installer::INSTALLER_ERROR_DB, str_replace('{$file}', $filename, __('installer.installParseEmailTemplatesFileError')));
                $result = false;
            }
        }
        return false;
    }

    /**
     * Callback used to install email template data on locale install.
     *
     * @param string $hookName
     * @param array $args
     *
     * @return bool
     */
    public function installLocale($hookName, $args)
    {
        $locale = & $args[0];
        $filename = str_replace('{$installedLocale}', $locale, $this->getInstallEmailTemplateDataFile());

        // 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()) {
            Repo::emailTemplate()->dao->installEmailTemplateData($filename, $locale);
        }

        // Install locale data specified in the new form.
        if (file_exists($this->getPluginPath() . "/locale/{$locale}/emails.po")) {
            $this->addLocaleData();
            Repo::emailTemplate()->dao->installEmailTemplateLocaleData($this->getInstallEmailTemplatesFile(), [$locale]);
        }
        return false;
    }

    /**
     * Callback used to install filters.
     *
     * @param string $hookName
     * @param array $args
     */
    public function installFilters($hookName, $args)
    {
        $installer = & $args[0]; /** @var Installer $installer */
        $result = & $args[1]; /** @var bool $result */

        // Get the filter configuration file name(s).
        $filterConfigFiles = $this->getInstallFilterConfigFiles();
        if (is_scalar($filterConfigFiles)) {
            $filterConfigFiles = [$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::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 string $hookName
     * @param array $args
     *
     * @return bool
     */
    public function updateSchema($hookName, $args)
    {
        $installer = & $args[0];
        $result = & $args[1];

        if ($migration = $this->getInstallMigration()) {
            try {
                $migration->up();
            } catch (Exception $e) {
                $installer->setError(Installer::INSTALLER_ERROR_DB, __('installer.installMigrationError', ['class' => get_class($migration), 'message' => $e->getMessage()]));
                $result = false;
            }
        }
        return false;
    }

    /**
     * Extend the {url ...} smarty to support plugins.
     *
     * @param array $params
     * @param \Smarty $smarty
     *
     * @return string
     */
    public function smartyPluginUrl($params, $smarty)
    {
        $path = [$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, [$params['path']]);
        } else {
            $params['path'] = $path;
        }
        return $smarty->smartyUrl($params, $smarty);
    }

    /**
     * Get the current version of this plugin
     *
     * @return ?Version
     */
    public 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);

        if ($installedPlugin) {
            return $installedPlugin;
        } else {
            return false;
        }
    }

    /**
     * Get the current request object
     *
     * @return PKPRequest
     */
    public 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 PKPRequest $request
     * @param array $actionArgs The list of action args to be included in request URLs.
     *
     * @return array List of LinkActions
     */
    public function getActions($request, $actionArgs)
    {
        return [];
    }

    /**
     * Determine whether the plugin can be enabled.
     *
     * @return bool
     */
    public function getCanEnable()
    {
        return false;
    }

    /**
     * Determine whether the plugin can be disabled.
     *
     * @return bool
     */
    public function getCanDisable()
    {
        return false;
    }

    /**
     * Determine whether the plugin is enabled.
     *
     * @return bool
     */
    public function getEnabled()
    {
        return true;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\plugins\Plugin', '\Plugin');
}
