<?php

# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
# All rights reserved.  See LICENSE file for licensing details

if (IN_serendipity !== true) {
    die ('Don\'t hack!');
}

// List of bundled core plugins
define('BUNDLED_PLUGINS',
    array(
    'serendipity_event_bbcode',
    'serendipity_event_creativecommons',
    'serendipity_event_emoticate',
    'serendipity_event_entryproperties',
    'serendipity_event_mailer',
    'serendipity_event_nl2br',
    'serendipity_event_responsiveimages',
    'serendipity_event_s9ymarkup',
    'serendipity_event_spamblock',
    'serendipity_event_spartacus',
    'serendipity_event_templatechooser',
    'serendipity_event_textile',
    'serendipity_event_xhtmlcleanup',
    'serendipity_plugin_archives',
    'serendipity_plugin_calendar',
    'serendipity_plugin_categories',
    'serendipity_plugin_comments',
    'serendipity_plugin_creativecommons',
    'serendipity_plugin_entrylinks',
    'serendipity_plugin_eventwrapper',
    'serendipity_plugin_history',
    'serendipity_plugin_html_nugget',
    'serendipity_plugin_plug',
    'serendipity_plugin_recententries',
    'serendipity_plugin_remoterss',
    'serendipity_plugin_superuser',
    'serendipity_plugin_syndication',
    'serendipity_plugin_templatedropdown'
    )
);

include_once S9Y_INCLUDE_PATH . 'include/functions.inc.php';

/* Core API function mappings
 * This allows the s9y Core to also execute internal core actions on plugin API hooks
 * Future use: Global variable can be customized/overriden by your own plugin on the frontend_configure event
 * or during runtime. The capabilities are theme or plugin based only.
 */
$serendipity['capabilities']['jquery'] = true;
$serendipity['capabilities']['jquery_backend'] = true;
$serendipity['capabilities']['jquery-noconflict'] = true; //set as being deprecated, while we should not need it anymore

$serendipity['core_events']['frontend_header']['jquery'] = 'serendipity_plugin_api_frontend_header';
$serendipity['core_events']['backend_header']['jquery']  = 'serendipity_plugin_api_backend_header';

// Add jquery to all frontend templates (in noConflict mode)
function serendipity_plugin_api_frontend_header($event_name, &$bag, &$eventData, $addData) {
    global $serendipity;

    // Only execute if current template (only) does not have its own jquery.js file
    // jquery can be disabled if a template's config.inc.php or a plugin sets
    // $serendipity['capabilities']['jquery'] = false

    $check = file_exists($serendipity['serendipityPath'] . $serendipity['templatePath'] . $serendipity['template'] . '/jquery.js');
    if (!$check && $serendipity['capabilities']['jquery']) {
?>
    <script src="<?php echo $serendipity['serendipityHTTPPath']; ?>templates/jquery.js"></script>
<?php
        if ($serendipity['capabilities']['jquery-noconflict']) {
?>
    <script>jQuery.noConflict();</script>
<?php
        }
    }
}

// Add jquery to all backend templates
function serendipity_plugin_api_backend_header($event_name, &$bag, &$eventData, $addData) {
    global $serendipity;

    // Only execute if current template does not have its own backend_jquery.js file
    // jquery can be disabled if a template's config.inc.php or a plugin sets
    // $serendipity['capabilities']['jquery'] = false

    $check = serendipity_getTemplateFile('jquery_backend.js', 'serendipityPath', true);
    if (!$check && $serendipity['capabilities']['jquery_backend']) {
?>
    <script src="<?php echo $serendipity['serendipityHTTPPath']; ?>templates/jquery.js"></script>
<?php
    }
}

// Add backend core (pre) hooks
function serendipity_plugin_api_core_event_hook($event, &$bag, &$eventData, &$addData) {
    global $serendipity;

    switch($event) {

        case 'js_backend':
        case 'js':
            // Add a global available (index.tpl; admin/index.tpl; preview_iframe.tpl) redirect error string function used by errorToExceptionHandler()
            // hardened by admin only - better have that here, to be reachable everywhere
            if( $serendipity['production'] === true && $serendipity['serendipityUserlevel'] >= USERLEVEL_ADMIN ) {
                echo "
function errorHandlerCreateDOM(htmlStr) {
    var frag = document.createDocumentFragment(),
        temp = document.createElement('div');
        temp.innerHTML = htmlStr;
    while (temp.firstChild) {
        frag.appendChild(temp.firstChild);
    }
    return frag;
} \n";
            }
            break;

        case 'external_plugin':
            if ($eventData == 'admin/serendipity_editor.js') {
                if (! headers_sent()) {
                    header('Content-Type: application/javascript');
                }

                echo serendipity_smarty_show('admin/serendipity_editor.js.tpl', null, 'JS', 'include/plugin_api.inc.php:external_plugin');
            }
            break;

        case 'backend_save':
        case 'backend_publish':
            // this is preview_iframe.tpl updertHooks [ NOT ONLY!! See freetags ]
            if ((($serendipity['GET']['adminAction'] ?? '') == 'save') && ($serendipity['GET']['preview'] ?? true) == 'false') {
                echo "\n".'<script>document.addEventListener("DOMContentLoaded", function() { window.parent.serendipity.eraseEntryEditorCache(); });</script>'."\n";
            }
            break;

    }
}


/* This file defines the plugin API for serendipity.
 * By extending these classes, you can add your own code
 * to appear in the sidebar(s) of serendipity.
 *
 *
 * The system defines a number of built-in plugins; these are
 * identified by @class_name.
 *
 * Third-party plugins are identified by the name of the folder into
 * which they were uploaded (so there is no @ sign at the start of
 * their class name.
 *
 * The user creates instances of plugins; an instance is assigned
 * an identifier like this:
 *   classname:uniqid()
 *
 * The user can configure instances of plugins.
 */

class serendipity_plugin_api
{

    /**
     * Register the default list of plugins for installation.
     *
     * @access public
     * @return null
     */
    static function register_default_plugins()
    {
        /* Register default sidebar plugins, order matters */
        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_archives');
        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_categories');
        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_syndication');
        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_superuser');
        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_plug');

        /* Register default event plugins */
        serendipity_plugin_api::create_plugin_instance('serendipity_event_s9ymarkup', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_emoticate', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_nl2br', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_spamblock', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_spartacus', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_entryproperties', null, 'event');
        serendipity_plugin_api::create_plugin_instance('serendipity_event_responsiveimages', null, 'event');

        /* Register additional plugins? */
        if (file_exists(S9Y_INCLUDE_PATH . 'plugins/preload.txt')) {
            // Expects this format, one plugin per line:
            // serendipity_event_xxx:event
            // serendipity_plugin_xxx:left
            $plugins = file(S9Y_INCLUDE_PATH . 'plugins/preload.txt');
            foreach($plugins AS $plugin) {
                $plugin = trim($plugin);
                if (empty($plugin)) {
                    continue;
                }

                $plugin_info = explode(':', $plugin);
                serendipity_plugin_api::create_plugin_instance($plugin_info[0], null, $plugin_info[1]);
            }
        }
    }

    /**
     * Create an instance of a plugin.
     *
     * $plugin_class_id is of the form:
     *    @class_name        for a built-in plugin
     * or
     *    plugin_dir_name    for a third-party plugin
     * returns the instance identifier for the newly created plugin.
     *
     * TO BE IMPLEMENTED:
     * If $copy_from_instance is not null, and identifies another plugin
     * of the same class, then the persistent state will be copied.
     * This allows the user to clone a plugin.
     *
     * @access  public
     * @param   string  classname of the plugin to insert (see description above for details)
     * @param   boolean (reserved) variable to indicate a copy of an existing instance
     * @param   string  The type of the plugin to insert (event/left/right/hide/eventh)
     * @param   int     The authorid of the plugin owner
     * @param   string  The source path of the plugin file
     * @return  string  ID of the new plugin
     */
    static function create_plugin_instance($plugin_class_id, $copy_from_instance = null, $default_placement = 'right', $authorid = '0', $pluginPath = '')
    {
        global $serendipity;

        $id = bin2hex(random_bytes(16));

        $key = $plugin_class_id . ':' . $id;
        $key = serendipity_db_escape_string($key);

        // Secure Plugin path. No leading slashes, no backslashes, no "up" directories
        $pluginPath = preg_replace('@^(/)@', '', $pluginPath);
        $pluginPath = str_replace(array('..', "\\"), array('', '/'), serendipity_db_escape_string($pluginPath));

        if ($pluginPath == 'online_repository') {
            $pluginPath = $key;
        }

        $rs = serendipity_db_query("SELECT MAX(sort_order) as sort_order_max FROM {$serendipity['dbPrefix']}plugins WHERE placement = '$default_placement'", true, 'num');

        if (is_array($rs) && isset($rs[0]) && !empty($rs[0])) {
            $nextidx = intval($rs[0] + 1);
        } else {
            $nextidx = 0;
        }

        $serendipity['debug']['pluginload'][] = "Installing plugin: " . print_r(func_get_args(), true);

        $iq = "INSERT INTO {$serendipity['dbPrefix']}plugins (name, sort_order, placement, authorid, path) values ('" . serendipity_db_escape_string(serendipity_specialchars($key)) . "', $nextidx, '$default_placement', '$authorid', '" . serendipity_specialchars($pluginPath) . "')";
        $serendipity['debug']['pluginload'][] = $iq;
        serendipity_db_query($iq);
        serendipity_plugin_api::hook_event('backend_plugins_new_instance', $key, array('default_placement' => $default_placement));

        /* Check for multiple dependencies */
        $plugin =& serendipity_plugin_api::load_plugin($key, $authorid, $pluginPath);
        if (is_object($plugin)) {
            $bag    = new serendipity_property_bag();
            $plugin->introspect($bag);
            serendipity_plugin_api::get_event_plugins(false, true); // Refresh static list of plugins to allow execution of added plugin
            $plugin->register_dependencies(false, $authorid);
            $plugin->install();
        } else {
            $serendipity['debug']['pluginload'][] = "Loading plugin failed painfully. File not found?";
            echo '<span class="msg_error">' . ERROR . ': ' . serendipity_specialchars($key) . ' (' . serendipity_specialchars($pluginPath) . ')</span>';
        }

        return $key;
    }

    /**
     * Removes a plugin by it's instance name
     *
     * @access public
     * @param   string  The name of the plugin id ("serendipity_plugin_xxx:1232132fsdf")
     * @return null
     */
    static function remove_plugin_instance($plugin_instance_id)
    {
        global $serendipity;

        $plugin_instance_id = serendipity_db_escape_string($plugin_instance_id);

        $plugin =& serendipity_plugin_api::load_plugin($plugin_instance_id);
        if (is_object($plugin)) {
            $bag    = new serendipity_property_bag();
            $plugin->introspect($bag);
            $plugin->uninstall($bag);
        }

        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}plugins where name='$plugin_instance_id'");

        if (is_object($plugin)) {
            $plugin->register_dependencies(true);
        }

        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config  where name LIKE '$plugin_instance_id/%'");
        return;
    
    }

    /**
     * Removes an empty plugin configuration value
     *
     * @access public
     * @param   string  The name of the plugin id ("serendipity_plugin_xxx:1232132fsdf")
     * @param   array   An array of configuration item names
     * @return null
     */
    static function remove_plugin_value($plugin_instance_id, $where)
    {
        global $serendipity;
        $where_sql = array();
        foreach($where AS $key) {
            $where_sql[] = "(name LIKE '{$plugin_instance_id}/{$key}_%' AND value = '')";
        }

        $query = "DELETE FROM  {$serendipity['dbPrefix']}config
                                    WHERE  " . implode(' OR ', $where_sql);

        serendipity_db_query($query);
    }

    /**
     * Retrieve a list of available plugin classes
     *
     * This function searches through all directories and loaded internal files and tries
     * to detect the serendipity plugins.
     *
     * @access public
     * @param   boolean     If true, only event plugins will be searched. If false, sidebar plugins will be searched.
     * @return
     */
    static function &enum_plugin_classes($event_only = false)
    {
        global $serendipity;

        $classes = array();

        /* built-in classes first */
        $cls = get_declared_classes();
        foreach ($cls AS $class_name) {
            if (strncmp($class_name, 'serendipity_', 6)) {
                continue;
            }

            $p = get_parent_class($class_name);
            while ($p != 'serendipity_plugin' && $p != 'serendipity_event' && $p !== false) {
                $p = get_parent_class($p);
            }

            if ($p == 'serendipity_plugin' && $class_name != 'serendipity_event' && (!$event_only || is_null($event_only))) {
                $classes[$class_name] = array('name'       => '@' . $class_name,
                                              'type'       => 'internal_event',
                                              'true_name'  => $class_name,
                                              'pluginPath' => '');
            } elseif ($p == 'serendipity_event' && $class_name != 'serendipity_event' && ($event_only || is_null($event_only))) {
                $classes[$class_name] = array('name'       => '@' . $class_name,
                                              'type'       => 'internal_plugin',
                                              'true_name'  => $class_name,
                                              'pluginPath' => '');
            }
        }

        /* GLOBAL third-party classes next */
        $ppath = serendipity_getRealDir(__FILE__) . 'plugins';
        serendipity_plugin_api::traverse_plugin_dir($ppath, $classes, $event_only);

        /* LOCAL third-party classes next */
        $local_ppath = $serendipity['serendipityPath'] . 'plugins';
        if ($ppath != $local_ppath) {
            serendipity_plugin_api::traverse_plugin_dir($local_ppath, $classes, $event_only);
        }

        return $classes;
    }

    /**
     * Traverse a specific directory and search if a serendipity plugin exists there.
     *
     * @access public
     * @param   string      The path to start from (usually '.')
     * @param   array       A referenced array of currently found classes
     * @param   boolean     If true, only event plugins will be searched. If false, only sidebar plugins will be searched.
     * @param   string      The maindir where we started searching from [for recursive use]
     * @return
     */
    static function traverse_plugin_dir($ppath, &$classes, $event_only, $maindir = '')
    {
        $d = @opendir($ppath);
        if ($d) {
            while (($f = readdir($d)) !== false) {
                if ($f[0] == '.' || $f == 'CVS' || !is_dir($ppath . '/' . $f) || !is_readable($ppath . '/' .$f)) {
                    continue;
                }

                $subd = opendir($ppath . '/' . $f);
                if (!$subd) {
                    continue;
                }

                // Instead of only looking for directories, search for files within subdirectories
                $final_loop = false;
                while (($subf = readdir($subd)) !== false) {

                    if ($subf[0] == '.' || $subf == 'CVS') {
                        continue;
                    }

                    if (!$final_loop && is_dir($ppath . '/' . $f . '/' . $subf) && $maindir != $ppath . '/' . $f) {
                        // Search for another level of subdirectories
                        serendipity_plugin_api::traverse_plugin_dir($ppath . '/' . $f, $classes, $event_only, $f . '/');
                        // We can break after that operation because the current directory has been fully checked already.
                        $final_loop = true;
                    }

                    if (!preg_match('@^[^_]+_(event|plugin)_.+\.php$@i', $subf)) {
                        continue;
                    }

                    $class_name = str_replace('.php', '', $subf);
                    // If an external plugin/event already exists as internal, remove the internal reference because its redundant
                    if (isset($classes['@' . $class_name])) {
                        unset($classes['@' . $class_name]);
                    }

                    // A local plugin will be preferred over general plugins [used when calling this function the second time]
                    if (isset($classes[$class_name])) {
                        unset($classes[$class_name]);
                    }

                    if (!is_null($event_only) && $event_only && !serendipity_plugin_api::is_event_plugin($subf)) {
                        continue;
                    }

                    if (!is_null($event_only) && !$event_only && serendipity_plugin_api::is_event_plugin($subf)) {
                        continue;
                    }

                    $classes[$class_name] = array('name'       => $class_name,
                                                  'true_name'  => $class_name,
                                                  'type'       => 'additional_plugin',
                                                  'pluginPath' => $maindir . $f);
                }
                closedir($subd);
            }
            closedir($d);
        }
    }

    /**
     * Returns a list of currently installed plugins
     *
     * @access public
     * @param   string  The filter for plugins (left|right|hide|event|eventh)
     * @return  array   The list of plugins
     */
    static function get_installed_plugins($filter = '*')
    {
        $plugins = serendipity_plugin_api::enum_plugins($filter);
        $res = array();
        foreach ( (array)$plugins AS $plugin ) {
            list($class_name) = explode(':', $plugin['name']);
            $class_name = ltrim($class_name, '@');
            $res[] = $class_name;
        }
        return $res;
    }

    /**
     * Searches for installed plugins based on specific conditions
     *
     * @access public
     * @param   string  The filter for plugins (left|right|hide|event|eventh)
     * @param   boolean If true, the filtering logic will be reversed and all plugins that are NOT part of the filter will be returned
     * @param   string  Filter by a specific classname (like 'serendipity_plugin_archives'). Can take SQL wildcards.
     * @param   string  Filter by a specific plugin instance id
     * @return  array   Returns the associative array of found plugins in the database
     */
    static function enum_plugins($filter = '*', $negate = false, $classname = null, $id = null)
    {
        global $serendipity;

        $sql   = "SELECT * from {$serendipity['dbPrefix']}plugins ";
        $where = array();

        if ($filter !== '*') {
            if ($negate) {
                $where[] = " placement != '" . serendipity_db_escape_string($filter) . "' ";
            } else {
                $where[] = " placement =  '" . serendipity_db_escape_string($filter) . "' ";
            }
        }

        if (!empty($classname)) {
            $where[] = " (name LIKE '@" . serendipity_db_escape_string($classname) . "%' OR name LIKE '" . serendipity_db_escape_string($classname) . "%') ";
        }

        if (!empty($id)) {
            $where[] = " name = '" . serendipity_db_escape_string($id) . "' ";
        }

        if (count($where) > 0) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }

        $sql .= ' ORDER BY placement, sort_order';

        return serendipity_db_query($sql);
    }

    /**
     * Count the number of plugins to which the filter criteria matches
     *
     * @access public
     * @param   string  The filter for plugins (left|right|hide|event|eventh)
     * @param   boolean If true, the filtering logic will be reversed and all plugins that are NOT part of the filter will be evaluated
     * @return  int     Number of plugins that were found.
     */
    static function count_plugins($filter = '*', $negate = false)
    {
        global $serendipity;

        // Can be shortcircuited via a $serendipity['prevent_sidebar_plugins_(left|right|event)'] variable!
        if (!$negate && ($serendipity['prevent_sidebar_plugins_' . $filter] ?? false) == true) {
            return 0;
        }


        $sql = "SELECT COUNT(placement) AS count from {$serendipity['dbPrefix']}plugins ";

        if ($filter !== '*') {
            if ($negate) {
                $sql .= "WHERE placement != '$filter' ";
            } else {
                $sql .= "WHERE placement='$filter' ";
            }
        }

        $count = serendipity_db_query($sql, true);
        if (is_array($count) && isset($count[0])) {
            return (int) $count[0];
        }

        return 0;
    }

    /**
     * Detect the filename to use for a specific plugin
     *
     * @access public
     * @param   string  The name of the plugin ('serendipity_event_archive')
     * @param   string  The path to the plugin file (if empty, the current path structure will be used.)
     * @param   string  If an instance ID is passed this means, the plugin to be loaded is internally available
     * @return  string  Returns the filename to include for a specific plugin
     */
    static function includePlugin($name, $pluginPath = '', $instance_id = '')
    {
        global $serendipity;

        if (empty($pluginPath)) {
            $pluginPath = $name;
        }

        $file = false;

        // Security constraint
        $pluginFile = 'plugins/' . $pluginPath . '/' . $name . '.php';
        $pluginFile = preg_replace('@([\r\n\t\0\\\]+|\.\.+)@', '', $pluginFile);

        // First try the local path, and then (if existing) a shared library repository ...
        // Internal plugins ignored.
        if (file_exists($serendipity['serendipityPath'] . $pluginFile)) {
            $file = $serendipity['serendipityPath'] . $pluginFile;
        } elseif (file_exists(S9Y_INCLUDE_PATH . $pluginFile)) {
            $file = S9Y_INCLUDE_PATH . $pluginFile;
        }

        return $file;
    }

    /**
     * Returns the plugin class name by a plugin instance ID
     *
     * @access public
     * @param   string      The ID of a plugin
     * @param   boolean     If true, the plugin is a internal plugin (prefixed with '@'). (Unused, keep for compat.)
     * @return  string      The classname of the plugin
     */
    static function getClassByInstanceID($instance_id, &$is_internal)
    {
        $instance   = explode(':', $instance_id);
        $class_name = ltrim($instance[0], '@');
        return $class_name;
    }

    /**
     * Auto-detect a plugin and see if the file information is given, and if not, detect it.
     *
     * @access public
     * @param   string      The ID of a plugin to load
     * @param   string      A reference variable that will hold the class name of the plugin (do not pass manually)
     * @param   string      A reference variable that will hold the path to the plugin (do not pass manually)
     * @return  string      Returns the filename of a plugin to load
     */
    /* Probes for the plugin filename */
    static function probePlugin($instance_id, &$class_name, &$pluginPath)
    {
        global $serendipity;

        $filename    = false;
        $is_internal = false;

        $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);

        if (!$is_internal) {
            /* plugin from the plugins/ dir */
            // $serendipity['debug']['pluginload'][] = "Including plugin $class_name, $pluginPath";
            $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath, $instance_id);
            if (empty($filename) && !empty($instance_id)) {
                // $serendipity['debug']['pluginload'][] = "No valid path/filename found.";
                $sql = "SELECT path from {$serendipity['dbPrefix']}plugins WHERE name = '" . serendipity_db_escape_string($instance_id) . "'";
                $plugdata = serendipity_db_query($sql, true, 'both', false, false, false, true);
                if (is_array($plugdata) && isset($plugdata[0])) {
                    $pluginPath = $plugdata[0];
                }

                if (empty($pluginPath)) {
                    $pluginPath = $class_name;
                }

                // $serendipity['debug']['pluginload'][] = "Including plugin(2) $class_name, $pluginPath";
                $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath);
            }

            if (empty($filename)) {
                $serendipity['debug']['pluginload'][] = "No valid path/filename found. Aborting.";
                $retval = false;
                return $retval;
            }
        }

        // $serendipity['debug']['pluginload'][] = "Found plugin file $filename";
        return $filename;
    }

    /**
     * Instantiates a plugin class
     *
     * @access public
     * @param   string      The ID of the plugin to load
     * @param   int         The owner of the plugin (can be autodetected)
     * @param   string      The path to a plugin (can be autodetected)
     * @param   string      The filename of a plugin (can be autodetected)
     * @return
     */
    static function &load_plugin($instance_id, $authorid = null, $pluginPath = '', $pluginFile = null)
    {
        global $serendipity;

        if ($pluginFile === null) {
            $class_name = '';
            // $serendipity['debug']['pluginload'][] = "Init probe for plugin $instance_id, $class_name, $pluginPath";
            $pluginFile = serendipity_plugin_api::probePlugin($instance_id, $class_name, $pluginPath);
        } else {
            $is_internal = false;
            // $serendipity['debug']['pluginload'][] = "getClassByInstanceID $instance_id, $is_internal";
            $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);
        }

        if (!class_exists($class_name) && !empty($pluginFile)) {
            // $serendipity['debug']['pluginload'][] = "Classname does not exist. Including $pluginFile.";
            include($pluginFile);
        }

        if (!class_exists($class_name)) {
            $serendipity['debug']['pluginload'][] = "Classname $class_name still does not exist. Aborting.";
            return false;
        }

        // $serendipity['debug']['pluginload'][] = "Returning new $class_name($instance_id)";
        $p = new $class_name($instance_id);
        if (!is_null($authorid)) {
            $p->serendipity_owner = $authorid;
        } else {
            $sql = "SELECT authorid from {$serendipity['dbPrefix']}plugins WHERE name = '" . serendipity_db_escape_string($instance_id) . "'";
            $owner = serendipity_db_query($sql, true);
            if (is_array($owner) && isset($owner[0])) {
                $p->serendipity_owner = $owner[0];
            }
        }

        $p->pluginPath = $p->act_pluginPath = $pluginPath;
        if (empty($p->act_pluginPath)) {
            $p->act_pluginPath = $class_name;
        }
        $p->pluginFile = $pluginFile;

        return $p;
    }

    /**
     * Gets cached properties/information about a specific plugin, auto-loads a cache of all plugins
     *
     * @access public
     * @param   string      The filename of the plugin to get information about
     * @param   array       A referenced array that holds information about the plugin instance (self::load_plugin() response)
     * @param   type        The type of the plugin (sidebar|event)
     * @return  array       Information about the plugin
     */
    static function &getPluginInfo(&$pluginFile, &$class_data, $type)
    {
        global $serendipity;

        static $pluginlist = null;

        if ($pluginlist === null) {
            $data = serendipity_db_query("SELECT p.*,
                                                 pc.category
                                            FROM {$serendipity['dbPrefix']}pluginlist AS p
                                 LEFT OUTER JOIN {$serendipity['dbPrefix']}plugincategories AS pc
                                              ON pc.class_name = p.class_name
                                           WHERE p.pluginlocation = 'local' AND
                                                 p.plugintype     = '" . serendipity_db_escape_string($type) . "'");
            if (is_array($data)) {
                foreach($data AS $p) {
                    if (isset($p['pluginpath'])) {
                        $p['pluginPath'] = $p['pluginpath'];
                    }
                    if (!isset($pluginlist[$p['plugin_file']])) {
                        $pluginlist[$p['plugin_file']] = $p;
                    }

                    $pluginlist[$p['plugin_file']]['groups'][] = $p['category'];
                }
            }
        }

        if (is_array($pluginlist[$pluginFile] ?? null) && !preg_match('@plugin_internal\.inc\.php@', $pluginFile)) {
            $data = $pluginlist[$pluginFile];
            if ((int) filemtime($pluginFile) == (int) $data['last_modified']) {
                $data['stackable'] = serendipity_db_bool($data['stackable']);
                $plugin = $data;
                return $plugin;
            }
        }

        $plugin =& serendipity_plugin_api::load_plugin($class_data['name'], null, $class_data['pluginPath'], $pluginFile);

        return $plugin;
    }

    /**
     * Set cache information about a plugin
     *
     * @access public
     * @param   mixed       Either an plugin object or a plugin information array that holds the information about the plugin
     * @param   string      The filename of the plugin
     * @param   object      The property bag object bundled with the plugin
     * @param   array       Previous/additional information about the plugin
     * @param   string      The location/type of a plugin (local|spartacus)
     * @return
     */
    static function &setPluginInfo(&$plugin, &$pluginFile, &$bag, &$class_data, $pluginlocation = 'local')
    {
        global $serendipity;

        static $dbfields = array(
            'plugin_file',
            'class_name',
            'plugin_class',
            'pluginPath',
            'name',
            'description',
            'version',
            'upgrade_version',
            'plugintype',
            'pluginlocation',
            'stackable',
            'author',
            'requirements',
            'website',
            'last_modified'
        );

        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}pluginlist WHERE plugin_file = '" . serendipity_db_escape_string($pluginFile) . "' AND pluginlocation = '" . serendipity_db_escape_string($pluginlocation) . "'");

        if (!empty($pluginFile) && file_exists($pluginFile)) {
            $lastModified = filemtime($pluginFile);
        } else {
            $lastModified = 0;
        }

        if (is_object($plugin)) {
            $data = array(
                'class_name'      => get_class($plugin),
                'stackable'       => $bag->get('stackable'),
                'name'            => $bag->get('name'),
                'description'     => $bag->get('description'),
                'author'          => $bag->get('author'),
                'version'         => $bag->get('version'),
                'upgrade_version' => isset($class_data['upgrade_version']) ? $class_data['upgrade_version'] : $bag->get('version'),
                'requirements'    => serialize($bag->get('requirements')),
                'website'         => $bag->get('website'),
                'plugin_class'    => $class_data['name'],
                'pluginPath'      => $class_data['pluginPath'],
                'plugin_file'     => $pluginFile,
                'pluginlocation'  => $pluginlocation,
                'plugintype'      => $serendipity['GET']['type'],
                'last_modified'   => $lastModified
            );
            $groups = $bag->get('groups');
        } elseif (is_array($plugin)) {
            $data = $plugin;
            $groups = $data['groups'];
            unset($data['installable']);
            unset($data['true_name']);
            unset($data['customURI']);
            unset($data['groups']);
            if (isset($data['pluginpath'])) {
                $data['pluginPath'] = $data['pluginpath'];
            }
            $data['requirements'] = serialize($data['requirements']);
        }

        if (!isset($data['stackable']) || empty($data['stackable'])) {
            $data['stackable'] = '0';
        }

        if (!isset($data['last_modified'])) {
            $data['last_modified'] = $lastModified;
        }

        // Only insert data keys that exist in the DB.
        $insertdata = array();
        foreach($dbfields AS $field) {
            $insertdata[$field] = $data[$field] ?? null;
        }

        if ($data['upgradable'] ?? false) {
            serendipity_db_query("UPDATE {$serendipity['dbPrefix']}pluginlist
                                     SET upgrade_version = '" . serendipity_db_escape_string($data['upgrade_version']) . "'
                                   WHERE plugin_class    = '" . serendipity_db_escape_string($data['plugin_class']) . "'");
        }
        serendipity_db_insert('pluginlist', $insertdata);

        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}plugincategories WHERE class_name = '" . serendipity_db_escape_string($data['class_name']) . "'");
        foreach((array)$groups AS $group) {
            if (empty($group)) {
                continue;
            }

            $cat = array(
                'class_name'  => $data['class_name'],
                'category'    => $group
            );
            serendipity_db_insert('plugincategories', $cat);
        }

        $data['groups'] = $groups;

        return $data;
    }

    /**
     * Moves a sidebar plugin to a different side or up/down
     *
     * @access public
     * @param   string  The instance ID of a plugin
     * @param   string  The new placement of a plugin (left|right|hide|event|eventh)
     * @param   string  A new sort order for the plugin
     * @return
     */
    static function update_plugin_placement($name, $placement, $order = null)
    {
        global $serendipity;

        $admin = '';
        if (!serendipity_checkPermission('adminPlugins') && $placement == 'hide') {
            // Only administrators can set plugins to 'hide' if they are not the owners.
            $admin = " AND (authorid = 0 OR authorid = {$serendipity['authorid']})";
        }

        $sql = "UPDATE {$serendipity['dbPrefix']}plugins set placement='$placement' ";

        if ($order !== null) {
            $sql .= ", sort_order=$order ";
        }

        $sql .= "WHERE name='$name' $admin";

        return serendipity_db_query($sql);
    }

    /**
     * Updates the ownership information about a plugin
     *
     * @access public
     * @param   string  The instance ID of the plugin
     * @param   int     The ID of the new author owner of the plugin
     * @return
     */
    static function update_plugin_owner($name, $authorid)
    {
        global $serendipity;

        if (empty($authorid) && $authorid != '0') {
            return;
        }

        $admin = '';
        if (!serendipity_checkPermission('adminPlugins')) {
            $admin = " AND (authorid = 0 OR authorid = {$serendipity['authorid']})";
        }

        $sql = "UPDATE {$serendipity['dbPrefix']}plugins SET authorid='$authorid' WHERE name='$name' $admin";

        return serendipity_db_query($sql);
    }

    /**
     * Get a list of Sidebar plugins and pass them to Smarty
     *
     * @access public
     * @param   string      The side of plugins to show (left/right/hide/event/eventh)
     * @param   string      deprecated: Indicated which wrapping HTML element to use for plugins
     * @param   boolean     Indicates whether only all plugins should be shown that are not in the $side list
     * @param   string      Only show plugins of this plugin class
     * @param   string      Only show a plugin with this instance ID
     * @return  string      Smarty HTML output
     */
    static function generate_plugins($side, $negate = false, $class = null, $id = null, $tpl = 'sidebar.tpl')
    {
        global $serendipity;
        $plugins = serendipity_plugin_api::enum_plugins($side, $negate, $class, $id);

        if (!is_array($plugins)) {
            return;
        }

        if (!isset($serendipity['smarty'])) {
            $serendipity['smarty_raw_mode'] = true;
            serendipity_smarty_init();
        }

        $pluginData = array();
        $addData    = func_get_args();
        serendipity_plugin_api::hook_event('frontend_generate_plugins', $plugins, $addData);

        if (count($plugins) == 0) {
            $serendipity['prevent_sidebar_plugins_' . $side] = true;
        }

        $loggedin = false;
        if (serendipity_userLoggedIn() && serendipity_checkPermission('adminPlugins')) {
            $loggedin = true;
        }

        foreach ($plugins AS $plugin_data) {
            $plugin =& serendipity_plugin_api::load_plugin($plugin_data['name'], $plugin_data['authorid'], $plugin_data['path']);
            if (is_object($plugin)) {
                $class  = get_class($plugin);
                $title  = '';

                /* TODO: make generate_content NOT echo its output */
                ob_start();
                $show_plugin = $plugin->generate_content($title);
                $content = ob_get_contents();
                ob_end_clean();

                if ($loggedin) {
                    $content .= '<div class="serendipity_edit_nugget"><a href="' . $serendipity['serendipityHTTPPath'] . 'serendipity_admin.php?serendipity[adminModule]=plugins&amp;serendipity[plugin_to_conf]=' . serendipity_entities($plugin->instance) . '">' . EDIT . '</a></div>';
                }

                if ($show_plugin !== false) {
                    $pluginData[] = array('side'    => $side,
                                          'class'   => $class,
                                          'title'   => $title,
                                          'content' => $content,
                                          'id'      => $plugin->instance);
                }
            } else {
                    $pluginData[] = array('side'          => $side,
                                          'title'         => ERROR,
                                          'class'         => $class,
                                          'content'       => sprintf(INCLUDE_ERROR, $plugin_data['name']));
            }
        }

        serendipity_plugin_api::hook_event('frontend_sidebar_plugins', $pluginData, $addData);

        $serendipity['smarty']->assignByRef('plugindata', $pluginData);
        $serendipity['smarty']->assign('pluginside', ucfirst($side));

        return serendipity_smarty_fetch('sidebar_'. $side, $tpl, true);
    }

    /**
     * Gets the title of a plugin to be shown in plugin overview
     *
     * @access public
     * @param   object      The plugin object
     * @param   string      The default title, if none was configured
     * @return  string      The title of the plugin
     */
    static function get_plugin_title(&$plugin, $default_title = '')
    {
        global $serendipity;

        // Generate plugin output. Make sure that by probing the plugin, no events are actually called. After that,
        // restore setting of 'no_events'.

        if (!is_null($plugin->title)) {
            // Preferred way of fetching a plugins title
            $title = &$plugin->title;
        } else {
            $ne = (isset($serendipity['no_events']) && $serendipity['no_events'] ? true : false);
            $serendipity['no_events'] = true;
            ob_start();
            $plugin->generate_content($title);
            ob_end_clean();
            $serendipity['no_events'] = $ne;
        }

        if (strlen(trim($title ?? '')) == 0) {
            if (!empty($default_title)) {
                $title = $default_title;
            } else {
                $title = $plugin->instance;
            }
        }

        return $title;
    }

    /**
     * Check if a plugin is bundled with s9y core
     *
     * @access public
     * @param   string  Name of a plugin
     * @return  boolean
     */
    static function is_bundled_plugin($name)
    {   
        return in_array ($name, BUNDLED_PLUGINS);
    }

    /**
     * Check if a plugin is an event plugin
     *
     * Refactoring: decompose conditional
     *
     * @access public
     * @param   string  Name of a plugin
     * @return  boolean
     */
    static function is_event_plugin($name)
    {
        return (strstr($name, '_event_'));
    }

    /**
     * Prepares a cache of all event plugins and load them in queue so that they can be fetched
     *
     * @access public
     * @param  mixed    If set to a string, a certain event plugin cache object will be returned by this function
     * @param  boolean  If set to true, the list of cached event plugins will be refreshed
     * @return mixed    Either returns the whole list of event plugins, or only a specific instance
     */
    static function &get_event_plugins($getInstance = false, $refresh = false)
    {
        static $event_plugins;
        static $false = false;

        if (!$refresh && isset($event_plugins) && is_array($event_plugins)) {
            if ($getInstance) {
                if (isset($event_plugins[$getInstance]['p'])) {
                    return $event_plugins[$getInstance]['p'];
                }
                return $false;
            }
            return $event_plugins;
        }

        $plugins = serendipity_plugin_api::enum_plugins('event');
        if (!is_array($plugins)) {
            return $false;
        }

        $event_plugins = array();
        foreach($plugins AS $plugin_data) {
            if ($event_plugins[$plugin_data['name']]['p'] = &serendipity_plugin_api::load_plugin($plugin_data['name'], $plugin_data['authorid'], $plugin_data['path'])) {
                /* query for its name, description and configuration data */
                $event_plugins[$plugin_data['name']]['b'] = new serendipity_property_bag;
                $event_plugins[$plugin_data['name']]['p']->introspect($event_plugins[$plugin_data['name']]['b']);
                $event_plugins[$plugin_data['name']]['t'] = serendipity_plugin_api::get_plugin_title($event_plugins[$plugin_data['name']]['p']);
            } else {
                unset($event_plugins[$plugin_data['name']]); // Unset failed plugins
            }
        }

        if ($getInstance) {
            if (isset($event_plugins[$getInstance]['p'])) {
                return $event_plugins[$getInstance]['p'];
            }
            return $false;
        }

        return $event_plugins;
    }

    /**
     * Executes a specific Eventhook
     *
     * If you want to temporarily block any event plugins, you can set $serendipity['no_events'] before
     * this method call.
     *
     * @access public
     * @param   string      The name of the event to hook on to
     * @param   mixed       May contain any type of variables that are passed by reference to an event plugin
     * @param   mixed       May contain any type of variables that are passed to an event plugin
     * @return true
     */
    static function hook_event($event_name, &$eventData, $addData = null)
    {
        global $serendipity;

        // Can be bypassed globally by setting $serendipity['no_events'] = TRUE;
        if (isset($serendipity['no_events']) && $serendipity['no_events'] == true) {
            return false;
        }

        if (($serendipity['enablePluginACL'] ?? false) && !serendipity_hasPluginPermissions($event_name)) {
            return false;
        }

        // We can NOT use a "return by reference" here, because then when
        // a plugin executes another event_hook, the referenced variable within
        // that call will overwrite the previous original plugin listing and
        // skip the execution of any follow-up plugins.
        $plugins = serendipity_plugin_api::get_event_plugins();

        if (array_key_exists($event_name, $serendipity['core_events']) && $serendipity['core_events'][$event_name]) {
            foreach($serendipity['core_events'][$event_name] as $apifunc_key => $apifunc) {
                $apifunc($event_name, $bag, $eventData, $addData);
            }
        }

        // execute backend needed core hooks
        serendipity_plugin_api_core_event_hook($event_name, $bag, $eventData, $addData);

        if (function_exists('serendipity_plugin_api_pre_event_hook')) {
            $apifunc = 'serendipity_plugin_api_pre_event_hook';
            $apifunc($event_name, $bag, $eventData, $addData);
        }

        // Function names cannot contain ":" etc, so if we ever have event looks like "backend:js" this
        // needs to be replaced to "backend_js". The real event name is passed as a function argument
        // These specific per-hook functions are utilized for theme's config.inc.php files
        // that act as an engine for other themes.
        $safe_event_name = preg_replace('@[^a-z0-9_]+@i', '_', $event_name);
        if (function_exists('serendipity_plugin_api_pre_event_hook_' . $safe_event_name)) {
            $apifunc = 'serendipity_plugin_api_pre_event_hook_' . $safe_event_name;
            $apifunc($event_name, $bag, $eventData, $addData);
        }

        if (is_array($plugins)) {
            foreach($plugins as $plugin => $plugin_data) {
                $bag    = &$plugin_data['b'];
                $phooks = &$bag->get('event_hooks');
                if (isset($phooks[$event_name])) {

                    // Check for cachable events.
                    if (isset($eventData['is_cached']) && $eventData['is_cached']) {
                        $chooks = &$bag->get('cachable_events');
                        if (is_array($chooks) && isset($chooks[$event_name])) {
                            continue;
                        }
                    }

                    if (($serendipity['enablePluginACL'] ?? false) && !serendipity_hasPluginPermissions($plugin)) {
                        continue;
                    }
                    $plugins[$plugin]['p']->event_hook($event_name, $bag, $eventData, $addData);
                }
            }

            if (function_exists('serendipity_plugin_api_event_hook')) {
                $apifunc = 'serendipity_plugin_api_event_hook';
                $apifunc($event_name, $bag, $eventData, $addData);
            }

            if (function_exists('serendipity_plugin_api_event_hook_' . $safe_event_name)) {
                $apifunc = 'serendipity_plugin_api_event_hook_' . $safe_event_name;
                $apifunc($event_name, $bag, $eventData, $addData);
            }

        }

        return true;
    }

    /**
     * Checks if a specific plugin instance is already installed
     *
     * @access public
     * @param   string      A name (may contain wildcards) of a plugin class to check
     * @return  boolean     True if a plugin was found
     */
    static function exists($instance_id)
    {
        global $serendipity;

        if (!strstr($instance_id, ':')) {
            $instance_id .= ':';
        }

        $existing = serendipity_db_query("SELECT name FROM {$serendipity['dbPrefix']}plugins WHERE name LIKE '%" . serendipity_db_escape_string($instance_id) . "%'");

        if (is_array($existing) && !empty($existing[0][0])) {
            return $existing[0][0];
        }

        return false;
    }

    /**
     * Install a new plugin by ensuring that it does not already exist
     *
     * @access public
     * @param   string      The classname of the plugin
     * @param   int         The new owner author
     * @param   boolean     Indicates if the plugin is an event plugin
     * @return  object      Returns the plugin object or false, if failure
     */
    static function &autodetect_instance($plugin_name, $authorid, $is_event_plugin = false)
    {
        if ($is_event_plugin) {
            $side = 'event';
        } else {
            $side = 'right';
        }

        $classes = serendipity_plugin_api::enum_plugin_classes(null);
        if (isset($classes[$plugin_name])) {
            $instance = serendipity_plugin_api::create_plugin_instance($plugin_name, null, $side, $authorid, $classes[$plugin_name]['pluginPath']);
        } else {
            $instance = false;
        }

        return $instance;
    }
    
    /**
     * Find the plugin instances for a given classname
     * 
     * @access public
     * @param   string      The classname of the plugin
     * @param   int         The owner author
     * @param   boolean     ignore hidden (deactivated) plugins
     * @return  object      Array with ids of the installed plugin
     */
    static function find_plugin_id($plugin_name,$authorid = 0, $ignore_hidden = true) {
        global $serendipity;

        $sql   = "SELECT * from {$serendipity['dbPrefix']}plugins ";
        $where = array();

        $where[] = "(name LIKE '@" . serendipity_db_escape_string($plugin_name) . "%' OR name LIKE '" . serendipity_db_escape_string($plugin_name) . "%') ";
        $where[] = "authorid='" . serendipity_db_escape_string($authorid) . "' ";
        
        if ($ignore_hidden) $where[] = "NOT ( placement = 'hidden' OR placement = 'eventh') ";
        
        if (count($where) > 0) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        
        $rs = serendipity_db_query($sql,false,'assoc');
        $ids = array();
        
        if (!empty($rs) & is_Array($rs)) { 
            foreach($rs as $line) {
                $ex = explode(':',$line['name']);
                $ids[] = $ex[1];
            }
            return $ids;
        } else {
            return null;
        }

    }


    /**
     * Probe for a language include with constants. Still include defines later on, if some constants were missing
     *
     * @access public
     * @param current plugin's path
     * @return  object      Returns the plugin object or false, if failure
     */
    static function load_language($path) {
        global $serendipity;

        // We deactivate error reporting here, because in PHP 8 setting constants twice always throws a warning.
        // However, the language files of some plugins sometimes rely on constants being set only in the fallback
        // language file, so we do have to set the other constants twice.
        $oldReportingLevel = error_reporting();
        error_reporting(0);
        
        $probelang = $path . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php';
        if (file_exists($probelang)) {
            include $probelang;
        }

        include $path . '/lang_en.inc.php';
        error_reporting($oldReportingLevel);
    }
}

/**
 * holds a bunch of properties; since serendipity 0.8 only one value per key is
 * allowed [was never really useful]
 */
class serendipity_property_bag
{
    /**
     * @access  private
     * @var array   property storage container.
     */
    var $properties = array();

    /**
     * @access private
     * @var    string   Name of the property bag
     */
    var $name = null;

    /**
     * Adds a property value to the bag
     *
     * @access public
     * @param   string  The name of the property
     * @param   mixed   The value of a property
     * @return null
     */
    function add($name, $value)
    {
        $this->properties[$name] = $value;
    }

    /**
     * Returns a property value of a bag
     *
     * @access public
     * @param   string  Name of property to fetch
     * @return  mixed   The value of the property
     */
    function &get($name)
    {
        return $this->properties[$name];
    }

    /**
     * Check if a specific property name is already set
     *
     * @access public
     * @param   string  Name of the property to check
     * @return  boolean True, if already set.
     */
    function is_set($name)
    {
        return isset($this->properties[$name]);
    }

}

/**
 * A core plugin, with methods that both event and sidebar plugins share
 */
class serendipity_plugin
{
    var $instance          = null;
    var $protected         = false;
    var $wrap_class        = 'serendipitySideBarItem';
    var $title_class       = 'serendipitySideBarTitle';
    var $content_class     = 'serendipitySideBarContent';
    var $title             = null;
    var $pluginPath        = null;
    var $act_pluginPath    = null;
    var $pluginFile        = null;
    var $serendipity_owner = null;

    /**
     * The constructor of a plugin
     *
     * Needs to be implemented by your own class.
     * Be sure to call this method from your derived classes constructors,
     * otherwise your config data will not be stored or retrieved correctly
     *
     * @access public
     * @return true
     */
    function __construct($instance)
    {
        $this->instance = $instance;
    }

    /**
     * Perform configuration routines
     *
     * Called by Serendipity when the plugin is being configured.
     * Can be used to query the database for configuration values that
     * only need to be available for the global configuration and not
     * on each page request.
     *
     * @access public
     * @return true
     */
    function performConfig(&$bag)
    {
        return true;
    }

    /**
     * Perform install routines
     *
     * Called by Serendipity when the plugin is first installed.
     * Can be used to install database tables etc.
     *
     * @access public
     * @return true
     */
    function install()
    {
        return true;
    }

    /**
     * Perform uninstall routines
     *
     * Called by Serendipity when the plugin is removed/uninstalled.
     * Can be used to drop installed database tables etc.
     *
     * @access public
     * @param  object   A property bag object
     * @return true
     */
    function uninstall(&$propbag)
    {
        return true;
    }

    /**
     * The introspection function of a plugin, to setup properties
     *
     * Called by serendipity when it wants to display information
     * about your plugin.
     * You need to override this method in your child class.
     *
     * @access public
     * @param   object  A property bag object you can manipulate
     * @return true
     */
    function introspect(&$propbag)
    {
        $propbag->add('copyright', 'MIT License');
        $propbag->add('name'     , get_class($this));

        // $propbag->add(
        //   'configuration',
        //   array(
        //     'text field',
        //     'foo bar'
        //   )
        // );

        $this->protected = false; // If set to TRUE, only allows the owner of the plugin to modify its configuration

        return true;
    }

    /**
     * Introspection of a plugin configuration item
     *
     * Called by serendipity when it wants to display the configuration
     * editor for your plugin.
     * $name is the name of a configuration item you added in
     * your instrospect method.
     * You need to fill the property bag with appropriate items
     * that describe the type and value(s) for that particular
     * configuration option.
     * You need to override this method in your child class if
     * you have configuration options.
     *
     * @access public
     * @param   string      Name of the config item
     * @param   object      A property bag object you can store the configuration in
     * @return
     */
    function introspect_config_item($name, &$propbag)
    {
        return false;
    }

    /**
     * Validate plugin configuration options.
     *
     * Called from Plugin Configuration manager. Can be extended by your own plugin, if you need.
     *
     * @access public
     * @param   string      Name of the config item to validate
     * @param   object      Property bag of the config item
     * @param   value       The value of a config item
     * @return
     */
    function validate($config_item, &$cbag, &$value)
    {
        static $pattern_mail  = '([\.\-\+~@_0-9a-z]+?)';
        static $pattern_url   = '([@!=~\?:&;0-9a-z#\.\-_\/]+?)';

        $validate = $cbag->get('validate');
        $valid    = true;

        if (!empty($validate)) {
            switch ($validate) {
                case 'string':
                    if (!preg_match('@^\w*$@i', $value)) {
                        $valid = false;
                    }
                    break;

                case 'words':
                    if (!preg_match('@^[\w\s\r\n,\.\-!\?:;&_/=%\$]*$@i', $value)) {
                        $valid = false;
                    }
                    break;

                case 'number':
                    if (!preg_match('@^[\d]*$@', $value)) {
                        $valid = false;
                    }
                    break;

                case 'url':
                    if (!preg_match('§^' . $pattern_url . '$§', $value)) {
                        $valid = false;
                    }
                    break;

                case 'mail':
                    if (!preg_match('§^' . $pattern_mail . '$§', $value)) {
                        $valid = false;
                    }
                    break;

                case 'path':
                    if (!preg_match('@^[\w/_.\-~]$@', $value)) {
                        $valid = false;
                    }
                    break;

                default:
                    if (!preg_match($validate, $value)) {
                        $valid = false;
                    }
                    break;
            }

            $error = $cbag->get('validate_error');
            if ($valid) {
                return true;
            } elseif (!empty($error)) {
                return $error;
            } else {
                return sprintf(PLUGIN_API_VALIDATE_ERROR, $config_item, $validate);
            }
        }

        return true;
    }

    /**
     * Output plugin's contents (Sidebar plugins)
     *
     * Called by serendipity when it wants your plugin to display itself.
     * You need to set $title to be whatever text you want want to
     * appear in the item caption space.
     * Simply echo/print your content to the output; serendipity will
     * capture it and make things work.
     * You need to override this method in your child class.
     *
     * @access public
     * @param   string       The referenced variable that holds the sidebar title of your plugin.
     * @return null
     */
    function generate_content(&$title)
    {
        $title = 'Sample!';
        echo     'This is a sample!';
    }

    /**
     * Get a config value of the plugin
     *
     * @access public
     * @param   string  Name of the config value to fetch
     * @param   mixed   The default value of a configuration item, if not set
     * @param   boolean If true, the default value will only be set if the plugin config item was not set.
     * @return  mixed   The value of the config item
     */
    function get_config($name, $defaultvalue = null, $empty = true)
    {
        $_res = serendipity_get_config_var($this->instance . '/' . $name, $defaultvalue, $empty);

        if (is_null($_res)) {
            // A protected plugin by a specific owner may not have its values stored in $serendipity
            // because of the special authorid. To display such contents, we need to fetch it
            // separately from the DB.
            $_res = serendipity_get_user_config_var($this->instance . '/' . $name, null, $defaultvalue);
        }

        if (is_null($_res)) {
            $cbag = new serendipity_property_bag();
            $this->introspect_config_item($name, $cbag);
            $_res = $cbag->get('default');
            unset($cbag);
            // Set the fetched value, so the default will not be fetched the next config call time
            $this->set_config($name, $_res);
        }

        return $_res;
    }

    /**
     * Sets a configuration value for a plugin
     *
     * @access public
     * @param   string  Name of the plugin configuration item
     * @param   string  Value of the plugin configuration item
     * @param   string  A concatenation key for imploding arrays
     * @return
     */
    function set_config($name, $value, $implodekey = '^')
    {
        $name = $this->instance . '/' . $name;

        if (is_array($value)) {
            $dbvalue = implode($implodekey, $value);
            $_POST['serendipity']['plugin'][$name] = $dbvalue;
        } else {
            $dbvalue = $value;
        }

        return serendipity_set_config_var($name, $dbvalue);
    }

    /**
     * Garbage Collection
     *
     * Called by serendipity after insertion of a config item. If you want to kick out certain
     * elements based on contents, create the corresponding function here.
     *
     * @access public
     * @return true
     */
    function cleanup()
    {
        // Cleanup. Remove all empty configs on SAVECONF-Submit.
        // serendipity_plugin_api::remove_plugin_value($this->instance, array('configname1', 'configname2'));
        return true;
    }

    /**
     * Auto-Register dependencies of a plugin
     *
     * This method evaluates the "dependencies" member variable to check which plugins need to be installed.
     *
     * @access public
     * @param   boolean     If true, a depending plugin will be removed when this plugin is uninstalled
     * @param   int         The owner id of the current plugin
     * @return true
     */
    function register_dependencies($remove = false, $authorid = '0')
    {
        global $serendipity;

        if (isset($this->dependencies) && is_array($this->dependencies)) {

            if ($remove) {
                $dependencies = @explode(';', $this->get_config('dependencies'));
                $modes        = @explode(';', $this->get_config('dependency_modes'));

                if (!empty($dependencies) && is_array($dependencies)) {
                    foreach($dependencies AS $idx => $dependency) {
                        if ($modes[$idx] == 'remove' && serendipity_plugin_api::exists($dependency)) {
                            serendipity_plugin_api::remove_plugin_instance($dependency);
                        }
                    }
                }
            } else {
                $keys  = array();
                $modes = array();
                foreach($this->dependencies AS $dependency => $mode) {
                    $exists = serendipity_plugin_api::exists($dependency);
                    if (!$exists) {
                        if (serendipity_plugin_api::is_event_plugin($dependency)) {
                            $keys[] = serendipity_plugin_api::autodetect_instance($dependency, $authorid, true);
                        } else {
                            $keys[] = serendipity_plugin_api::autodetect_instance($dependency, $authorid, false);
                        }
                    } else {
                        $keys[] = $exists;
                    }

                    $modes[] = $mode;
                }

                $this->set_config('dependencies',     implode(';', $keys));
                $this->set_config('dependency_modes', implode(';', $modes));
            }
        }

        return true;
    }

    /**
     * Parses a smarty template file (which can be stored in either the plugin directory, the user template directory
     * or the default template directory, and return the parsed output.
     *
     * @access public
     * @param  string   template filename (no directory!)
     * @return string   Parsed Smarty return
     */
    function &parseTemplate($filename)
    {
        global $serendipity;

        $filename = basename($filename);
        $tfile    = serendipity_getTemplateFile($filename, 'serendipityPath', true);
        if (!$tfile || $tfile == $filename) {
            $tfile = dirname($this->pluginFile) . '/' . $filename;
        }

        $output = $serendipity['smarty']->fetch('file:'. $tfile);
        return $output;
    }

    /**
     * Get full path for a filename. Will first look into themes and then in the plugins directory
     * @param   string  relative path to file
     * @param   string  The path selector that tells whether to return a HTTP or realpath
     * @return  string  The full path+filename to the requested file
     * */
    function getFile($filename, $key = 'serendipityPath')
    {
        global $serendipity;
        
        $path = serendipity_getTemplateFile($filename, $key, true);
        if (!$path) {
            if (file_exists(dirname($this->pluginFile) . '/' . $filename)) {
                return $serendipity[$key] . 'plugins/' . basename(dirname($this->pluginFile)) . '/' . $filename;
            }
        }

        return $path;
    }

}

/**
 * Events can be called on several occasions when s9y performs an action.
 * One or multiple plugin can be registered for each of those hooks.
 */
class serendipity_event extends serendipity_plugin
{

    /**
     * The class constructor
     *
     * Be sure to call this method from your derived classes constructors,
     * otherwise your config data will not be stored or retrieved correctly
     *
     * @access public
     * @param   string      The instance name
     * @return
     */
    function __construct($instance)
    {
        $this->instance = $instance;
    }

    /**
     * Gets a reference to an $entry / $eventData array pointer, interacting with Cache-Options
     *
     * This function is used by specific event plugins that require to properly get a reference
     * to the 'extended' or 'body' field of an entry superarray. If they would immediately operate
     * on the 'body' field, it might get overwritten by other plugins later on.
     *
     * @access public
     * @param   string      The fieldname to get a reference for
     * @param   array       The entry superarray to get the reference from
     * @return  array       The value of the array for the fieldname (reference)
     */
    function &getFieldReference($fieldname = 'body', &$eventData = [])
    {
        // Get a reference to a content field (body/extended) of
        // $entries input data. This is a unifying function because
        // several plugins are using similar fields.

        if (is_array($eventData) && isset($eventData[0]) && is_array($eventData[0]) && is_array($eventData[0]['properties'])) {
            if (!empty($eventData[0]['properties']['ep_cache_' . $fieldname])) {

                // It may happen that there is no extended entry to concatenate to. In that case,
                // create a dummy extended entry.
                if (!isset($eventData[0]['properties']['ep_cache_' . $fieldname])) {
                    $eventData[0]['properties']['ep_cache_' . $fieldname] = '';
                }

                $key = &$eventData[0]['properties']['ep_cache_' . $fieldname];
            } else {
                $key = &$eventData[0][$fieldname];
            }
        } elseif (is_array($eventData) && is_array($eventData['properties'] ?? null)) {
            if (!empty($eventData['properties']['ep_cache_' . $fieldname])) {
                $key = &$eventData['properties']['ep_cache_' . $fieldname];
            } else {
                $key = &$eventData[$fieldname];
            }
        } elseif (isset($eventData[0]) && isset($eventData[0][$fieldname])) {
            $key = &$eventData[0][$fieldname];
        } elseif (isset($eventData[$fieldname])) {
            $key = &$eventData[$fieldname];
        } else {
            $key = '';
        }

        return $key;
    }

    /**
     * Main logic for making a plugin "listen" to an event
     *
     * This method is called by the main plugin API for every event, that is executed.
     * You need to implement each actions that shall be performed by your plugin here.
     *
     * @access public
     * @param   string      The name of the executed event
     * @param   object      A property bag for the current plugin
     * @param   mixed       Any referenced event data from the serendipity_plugin_api::hook_event() function
     * @param   mixed       Any additional data from the hook_event call
     * @return true
     */
    function event_hook($event, &$bag, &$eventData, $addData = null)
    {
        // Define event hooks here, if you want your plugin to execute those instead of being a sidebar item.
        // Look at in/external plugins 'serendipity_event_mailer' or 'serendipity_event_weblogping' for usage.
        // Currently available events:
        //   backend_publish [after insertion of a new article in your s9y-backend]
        //   backend_display [after displaying an article in your s9y-backend]
        //   frontend_display [before displaying an article in your s9y-frontend]
        //   frontend_comment [after displaying the "enter comment" dialog]
        //   ...and some more in the meanwhile...! :)
        return true;
    }

}

/* vim: set sts=4 ts=4 expandtab : */
