<?php

/**
 * @file classes/plugins/PluginSettingsDAO.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2003-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class PluginSettingsDAO
 *
 * @ingroup plugins
 *
 * @see Plugin
 *
 * @brief Operations for retrieving and modifying plugin settings.
 */

namespace PKP\plugins;

use Illuminate\Support\Facades\DB;
use PKP\cache\CacheManager;
use PKP\cache\GenericCache;
use PKP\xml\PKPXMLParser;

class PluginSettingsDAO extends \PKP\db\DAO
{
    /**
     * Get the cache for plugin settings.
     *
     * @param int $contextId Context ID
     * @param string $pluginName Plugin symbolic name
     *
     * @return GenericCache
     */
    public function _getCache($contextId, $pluginName)
    {
        static $settingCache = [];
        return $settingCache[(int) $contextId][$pluginName] ??= CacheManager::getManager()->getCache(
            'pluginSettings-' . $contextId,
            $pluginName,
            [$this, '_cacheMiss']
        );
    }

    /**
     * Retrieve a plugin setting value.
     *
     * @param int $contextId Context ID
     * @param string $pluginName Plugin symbolic name
     * @param string $name Setting name
     */
    public function getSetting($contextId, $pluginName, $name)
    {
        // Normalize the plug-in name to lower case.
        $pluginName = strtolower_codesafe($pluginName);

        // Retrieve the setting.
        $cache = $this->_getCache($contextId, $pluginName);
        return $cache->get($name);
    }

    /**
     * Does the plugin setting exist.
     *
     * @param int $contextId Context ID
     * @param string $pluginName Plugin symbolic name
     * @param string $name Setting name
     *
     * @return bool
     */
    public function settingExists($contextId, $pluginName, $name)
    {
        $pluginName = strtolower_codesafe($pluginName);
        $result = $this->retrieve(
            'SELECT COUNT(*) AS row_count FROM plugin_settings WHERE plugin_name = ? AND context_id = ? AND setting_name = ?',
            [$pluginName, (int) $contextId, $name]
        );
        $row = $result->current();
        return $row ? (bool) $row->row_count : false;
    }

    /**
     * Callback for a cache miss.
     *
     * @param object $cache Cache object
     * @param string $id Identifier to look up in cache
     */
    public function _cacheMiss($cache, $id)
    {
        $contextParts = explode('-', $cache->getContext());
        $contextId = array_pop($contextParts);
        $settings = $this->getPluginSettings($contextId, $cache->getCacheId());
        if (!isset($settings[$id])) {
            // Make sure that even null values are cached
            $cache->setCache($id, null);
            return null;
        }
        return $settings[$id];
    }

    /**
     * Retrieve and cache all settings for a plugin.
     *
     * @param int $contextId Context ID
     * @param string $pluginName Plugin symbolic name
     *
     * @return array
     */
    public function getPluginSettings($contextId, $pluginName)
    {
        // Normalize plug-in name to lower case.
        $pluginName = strtolower_codesafe($pluginName);

        $result = $this->retrieve(
            'SELECT setting_name, setting_value, setting_type FROM plugin_settings WHERE plugin_name = ? AND context_id = ?',
            [$pluginName, (int) $contextId]
        );

        $pluginSettings = [];
        foreach ($result as $row) {
            $pluginSettings[$row->setting_name] = $this->convertFromDB($row->setting_value, $row->setting_type);
        }

        $cache = $this->_getCache($contextId, $pluginName);
        $cache->setEntireCache($pluginSettings);

        return $pluginSettings;
    }

    /**
     * Add/update a plugin setting.
     *
     * @param int $contextId Context ID
     * @param string $pluginName Symbolic plugin name
     * @param string $name Setting name
     * @param mixed $value Setting value
     * @param string $type data type of the setting. If omitted, type will be guessed
     */
    public function updateSetting($contextId, $pluginName, $name, $value, $type = null)
    {
        // Normalize the plug-in name to lower case.
        $pluginName = strtolower_codesafe($pluginName);

        $cache = $this->_getCache($contextId, $pluginName);
        $cache->setCache($name, $value);

        $value = $this->convertToDB($value, $type);

        DB::table('plugin_settings')->updateOrInsert(
            ['context_id' => (int) $contextId, 'plugin_name' => $pluginName, 'setting_name' => $name],
            ['setting_value' => $value, 'setting_type' => $type]
        );
    }

    /**
     * Delete a plugin setting.
     *
     * @param int $contextId
     * @param int $pluginName
     * @param string $name
     */
    public function deleteSetting($contextId, $pluginName, $name)
    {
        // Normalize the plug-in name to lower case.
        $pluginName = strtolower_codesafe($pluginName);

        $cache = $this->_getCache($contextId, $pluginName);
        $cache->setCache($name, null);

        return $this->update(
            'DELETE FROM plugin_settings WHERE plugin_name = ? AND setting_name = ? AND context_id = ?',
            [$pluginName, $name, (int) $contextId]
        );
    }

    /**
     * Delete all settings for a plugin.
     *
     * @param int $contextId
     * @param string $pluginName
     */
    public function deleteSettingsByPlugin($contextId, $pluginName)
    {
        // Normalize the plug-in name to lower case.
        $pluginName = strtolower_codesafe($pluginName);

        $cache = $this->_getCache($contextId, $pluginName);
        $cache->flush();

        return $this->update(
            'DELETE FROM plugin_settings WHERE context_id = ? AND plugin_name = ?',
            [(int) $contextId, $pluginName]
        );
    }

    /**
     * Delete all settings for a context.
     *
     * @param int $contextId
     */
    public function deleteByContextId($contextId)
    {
        return $this->update(
            'DELETE FROM plugin_settings WHERE context_id = ?',
            [(int) $contextId]
        );
    }

    /**
     * Used internally by installSettings to perform variable and translation replacements.
     *
     * @param string $rawInput contains text including variable and/or translate replacements.
     * @param array $paramArray contains variables for replacement
     *
     * @return string
     */
    public function _performReplacement($rawInput, $paramArray = [])
    {
        $value = preg_replace_callback('{{translate key="([^"]+)"}}', fn ($matches) => __($matches[1]), (string) $rawInput);
        foreach ($paramArray as $pKey => $pValue) {
            $value = str_replace('{$' . $pKey . '}', $pValue, $value);
        }
        return $value;
    }

    /**
     * Used internally by installSettings to recursively build nested arrays.
     * Deals with translation and variable replacement calls.
     *
     * @param object $node XMLNode <array> tag
     * @param array $paramArray Parameters to be replaced in key/value contents
     */
    public function _buildObject($node, $paramArray = [])
    {
        $value = [];
        foreach ($node->getChildren() as $element) {
            $key = $element->getAttribute('key');
            $childArray = $element->getChildByName('array');
            if (isset($childArray)) {
                $content = $this->_buildObject($childArray, $paramArray);
            } else {
                $content = $this->_performReplacement($element->getValue(), $paramArray);
            }
            if (!empty($key)) {
                $key = $this->_performReplacement($key, $paramArray);
                $value[$key] = $content;
            } else {
                $value[] = $content;
            }
        }
        return $value;
    }

    /**
     * Install plugin settings from an XML file.
     *
     * @param string $pluginName name of plugin for settings to apply to
     * @param string $filename Name of XML file to parse and install
     * @param array $paramArray Optional parameters for variable replacement in settings
     */
    public function installSettings($contextId, $pluginName, $filename, $paramArray = [])
    {
        $xmlParser = new PKPXMLParser();
        $tree = $xmlParser->parse($filename);

        if (!$tree) {
            return false;
        }

        // Check for existing settings and leave them if they are already in place.
        $currentSettings = $this->getPluginSettings($contextId, $pluginName);

        foreach ($tree->getChildren() as $setting) {
            $nameNode = $setting->getChildByName('name');
            $valueNode = $setting->getChildByName('value');

            if (isset($nameNode) && isset($valueNode)) {
                $type = $setting->getAttribute('type');
                $name = $nameNode->getValue();

                // If the setting already exists, respect it.
                if (isset($currentSettings[$name])) {
                    continue;
                }

                if ($type == 'object') {
                    $arrayNode = $valueNode->getChildByName('array');
                    $value = $this->_buildObject($arrayNode, $paramArray);
                } else {
                    $value = $this->_performReplacement($valueNode->getValue(), $paramArray);
                }

                // Replace translate calls with translated content
                $this->updateSetting($contextId, $pluginName, $name, $value, $type);
            }
        }
    }
}

/**
 * Used internally by plugin setting installation code to perform translation
 * function.
 *
 * @param array $matches
 *
 * @return string
 */
function _installer_plugin_regexp_callback($matches)
{
    return __($matches[1]);
}

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