1<?php
2
3# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
4# All rights reserved.  See LICENSE file for licensing details
5
6if (IN_serendipity !== true) {
7    die ('Don\'t hack!');
8}
9
10// List of bundled core plugins
11define('BUNDLED_PLUGINS',
12    array(
13    'serendipity_event_bbcode',
14    'serendipity_event_creativecommons',
15    'serendipity_event_emoticate',
16    'serendipity_event_entryproperties',
17    'serendipity_event_mailer',
18    'serendipity_event_nl2br',
19    'serendipity_event_responsiveimages',
20    'serendipity_event_s9ymarkup',
21    'serendipity_event_spamblock',
22    'serendipity_event_spartacus',
23    'serendipity_event_templatechooser',
24    'serendipity_event_textile',
25    'serendipity_event_xhtmlcleanup',
26    'serendipity_plugin_archives',
27    'serendipity_plugin_calendar',
28    'serendipity_plugin_categories',
29    'serendipity_plugin_comments',
30    'serendipity_plugin_creativecommons',
31    'serendipity_plugin_entrylinks',
32    'serendipity_plugin_eventwrapper',
33    'serendipity_plugin_history',
34    'serendipity_plugin_html_nugget',
35    'serendipity_plugin_plug',
36    'serendipity_plugin_recententries',
37    'serendipity_plugin_remoterss',
38    'serendipity_plugin_superuser',
39    'serendipity_plugin_syndication',
40    'serendipity_plugin_templatedropdown'
41    )
42);
43
44include_once S9Y_INCLUDE_PATH . 'include/functions.inc.php';
45
46/* Core API function mappings
47 * This allows the s9y Core to also execute internal core actions on plugin API hooks
48 * Future use: Global variable can be customized/overriden by your own plugin on the frontend_configure event
49 * or during runtime. The capabilities are theme or plugin based only.
50 */
51$serendipity['capabilities']['jquery'] = true;
52$serendipity['capabilities']['jquery_backend'] = true;
53$serendipity['capabilities']['jquery-noconflict'] = true; //set as being deprecated, while we should not need it anymore
54
55$serendipity['core_events']['frontend_header']['jquery'] = 'serendipity_plugin_api_frontend_header';
56$serendipity['core_events']['backend_header']['jquery']  = 'serendipity_plugin_api_backend_header';
57
58// Add jquery to all frontend templates (in noConflict mode)
59function serendipity_plugin_api_frontend_header($event_name, &$bag, &$eventData, $addData) {
60    global $serendipity;
61
62    // Only execute if current template (only) does not have its own jquery.js file
63    // jquery can be disabled if a template's config.inc.php or a plugin sets
64    // $serendipity['capabilities']['jquery'] = false
65
66    $check = file_exists($serendipity['serendipityPath'] . $serendipity['templatePath'] . $serendipity['template'] . '/jquery.js');
67    if (!$check && $serendipity['capabilities']['jquery']) {
68?>
69    <script src="<?php echo $serendipity['serendipityHTTPPath']; ?>templates/jquery.js"></script>
70<?php
71        if ($serendipity['capabilities']['jquery-noconflict']) {
72?>
73    <script>jQuery.noConflict();</script>
74<?php
75        }
76    }
77}
78
79// Add jquery to all backend templates
80function serendipity_plugin_api_backend_header($event_name, &$bag, &$eventData, $addData) {
81    global $serendipity;
82
83    // Only execute if current template does not have its own backend_jquery.js file
84    // jquery can be disabled if a template's config.inc.php or a plugin sets
85    // $serendipity['capabilities']['jquery'] = false
86
87    $check = serendipity_getTemplateFile('jquery_backend.js', 'serendipityPath', true);
88    if (!$check && $serendipity['capabilities']['jquery_backend']) {
89?>
90    <script src="<?php echo $serendipity['serendipityHTTPPath']; ?>templates/jquery.js"></script>
91<?php
92    }
93}
94
95// Add backend core (pre) hooks
96function serendipity_plugin_api_core_event_hook($event, &$bag, &$eventData, &$addData) {
97    global $serendipity;
98
99    switch($event) {
100
101        case 'js_backend':
102        case 'js':
103            // Add a global available (index.tpl; admin/index.tpl; preview_iframe.tpl) redirect error string function used by errorToExceptionHandler()
104            // hardened by admin only - better have that here, to be reachable everywhere
105            if( $serendipity['production'] === true && $serendipity['serendipityUserlevel'] >= USERLEVEL_ADMIN ) {
106                echo "
107function errorHandlerCreateDOM(htmlStr) {
108    var frag = document.createDocumentFragment(),
109        temp = document.createElement('div');
110        temp.innerHTML = htmlStr;
111    while (temp.firstChild) {
112        frag.appendChild(temp.firstChild);
113    }
114    return frag;
115} \n";
116            }
117            break;
118
119        case 'external_plugin':
120            if ($eventData == 'admin/serendipity_editor.js') {
121                header('Content-Type: application/javascript');
122
123                echo serendipity_smarty_show('admin/serendipity_editor.js.tpl', null, 'JS', 'include/plugin_api.inc.php:external_plugin');
124            }
125            break;
126
127        case 'backend_save':
128        case 'backend_publish':
129            // this is preview_iframe.tpl updertHooks [ NOT ONLY!! See freetags ]
130            if ($_GET['serendipity']['is_iframe'] == 'true' && $_GET['serendipity']['iframe_mode'] == 'save') {
131                echo "\n".'<script>document.addEventListener("DOMContentLoaded", function() { window.parent.serendipity.eraseEntryEditorCache(); });</script>'."\n";
132            }
133            break;
134
135    }
136}
137
138
139/* This file defines the plugin API for serendipity.
140 * By extending these classes, you can add your own code
141 * to appear in the sidebar(s) of serendipity.
142 *
143 *
144 * The system defines a number of built-in plugins; these are
145 * identified by @class_name.
146 *
147 * Third-party plugins are identified by the name of the folder into
148 * which they were uploaded (so there is no @ sign at the start of
149 * their class name.
150 *
151 * The user creates instances of plugins; an instance is assigned
152 * an identifier like this:
153 *   classname:uniqid()
154 *
155 * The user can configure instances of plugins.
156 */
157
158class serendipity_plugin_api
159{
160
161    /**
162     * Register the default list of plugins for installation.
163     *
164     * @access public
165     * @return null
166     */
167    static function register_default_plugins()
168    {
169        /* Register default sidebar plugins, order matters */
170        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_archives');
171        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_categories');
172        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_syndication');
173        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_superuser');
174        serendipity_plugin_api::create_plugin_instance('@serendipity_plugin_plug');
175
176        /* Register default event plugins */
177        serendipity_plugin_api::create_plugin_instance('serendipity_event_s9ymarkup', null, 'event');
178        serendipity_plugin_api::create_plugin_instance('serendipity_event_emoticate', null, 'event');
179        serendipity_plugin_api::create_plugin_instance('serendipity_event_nl2br', null, 'event');
180        serendipity_plugin_api::create_plugin_instance('serendipity_event_spamblock', null, 'event');
181        serendipity_plugin_api::create_plugin_instance('serendipity_event_spartacus', null, 'event');
182        serendipity_plugin_api::create_plugin_instance('serendipity_event_entryproperties', null, 'event');
183        serendipity_plugin_api::create_plugin_instance('serendipity_event_responsiveimages', null, 'event');
184
185        /* Register additional plugins? */
186        if (file_exists(S9Y_INCLUDE_PATH . 'plugins/preload.txt')) {
187            // Expects this format, one plugin per line:
188            // serendipity_event_xxx:event
189            // serendipity_plugin_xxx:left
190            $plugins = file(S9Y_INCLUDE_PATH . 'plugins/preload.txt');
191            foreach($plugins AS $plugin) {
192                $plugin = trim($plugin);
193                if (empty($plugin)) {
194                    continue;
195                }
196
197                $plugin_info = explode(':', $plugin);
198                serendipity_plugin_api::create_plugin_instance($plugin_info[0], null, $plugin_info[1]);
199            }
200        }
201    }
202
203    /**
204     * Create an instance of a plugin.
205     *
206     * $plugin_class_id is of the form:
207     *    @class_name        for a built-in plugin
208     * or
209     *    plugin_dir_name    for a third-party plugin
210     * returns the instance identifier for the newly created plugin.
211     *
212     * TO BE IMPLEMENTED:
213     * If $copy_from_instance is not null, and identifies another plugin
214     * of the same class, then the persistent state will be copied.
215     * This allows the user to clone a plugin.
216     *
217     * @access  public
218     * @param   string  classname of the plugin to insert (see description above for details)
219     * @param   boolean (reserved) variable to indicate a copy of an existing instance
220     * @param   string  The type of the plugin to insert (event/left/right/hide/eventh)
221     * @param   int     The authorid of the plugin owner
222     * @param   string  The source path of the plugin file
223     * @return  string  ID of the new plugin
224     */
225    static function create_plugin_instance($plugin_class_id, $copy_from_instance = null, $default_placement = 'right', $authorid = '0', $pluginPath = '')
226    {
227        global $serendipity;
228
229        $id = md5(uniqid(''));
230
231        $key = $plugin_class_id . ':' . $id;
232        $key = serendipity_db_escape_string($key);
233
234        // Secure Plugin path. No leading slashes, no backslashes, no "up" directories
235        $pluginPath = preg_replace('@^(/)@', '', $pluginPath);
236        $pluginPath = str_replace(array('..', "\\"), array('', '/'), serendipity_db_escape_string($pluginPath));
237
238        if ($pluginPath == 'online_repository') {
239            $pluginPath = $key;
240        }
241
242        $rs = serendipity_db_query("SELECT MAX(sort_order) as sort_order_max FROM {$serendipity['dbPrefix']}plugins WHERE placement = '$default_placement'", true, 'num');
243
244        if (is_array($rs) && isset($rs[0]) && !empty($rs[0])) {
245            $nextidx = intval($rs[0] + 1);
246        } else {
247            $nextidx = 0;
248        }
249
250        $serendipity['debug']['pluginload'][] = "Installing plugin: " . print_r(func_get_args(), true);
251
252        $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) . "')";
253        $serendipity['debug']['pluginload'][] = $iq;
254        serendipity_db_query($iq);
255        serendipity_plugin_api::hook_event('backend_plugins_new_instance', $key, array('default_placement' => $default_placement));
256
257        /* Check for multiple dependencies */
258        $plugin =& serendipity_plugin_api::load_plugin($key, $authorid, $pluginPath);
259        if (is_object($plugin)) {
260            $bag    = new serendipity_property_bag();
261            $plugin->introspect($bag);
262            serendipity_plugin_api::get_event_plugins(false, true); // Refresh static list of plugins to allow execution of added plugin
263            $plugin->register_dependencies(false, $authorid);
264            $plugin->install();
265        } else {
266            $serendipity['debug']['pluginload'][] = "Loading plugin failed painfully. File not found?";
267            echo '<span class="msg_error">' . ERROR . ': ' . serendipity_specialchars($key) . ' (' . serendipity_specialchars($pluginPath) . ')</span>';
268        }
269
270        return $key;
271    }
272
273    /**
274     * Removes a plugin by it's instance name
275     *
276     * @access public
277     * @param   string  The name of the plugin id ("serendipity_plugin_xxx:1232132fsdf")
278     * @return null
279     */
280    static function remove_plugin_instance($plugin_instance_id)
281    {
282        global $serendipity;
283
284        $plugin_instance_id = serendipity_db_escape_string($plugin_instance_id);
285
286        $plugin =& serendipity_plugin_api::load_plugin($plugin_instance_id);
287        if (is_object($plugin)) {
288            $bag    = new serendipity_property_bag();
289            $plugin->introspect($bag);
290            $plugin->uninstall($bag);
291        }
292
293        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}plugins where name='$plugin_instance_id'");
294
295        if (is_object($plugin)) {
296            $plugin->register_dependencies(true);
297        }
298
299        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config  where name LIKE '$plugin_instance_id/%'");
300    }
301
302    /**
303     * Removes an empty plugin configuration value
304     *
305     * @access public
306     * @param   string  The name of the plugin id ("serendipity_plugin_xxx:1232132fsdf")
307     * @param   array   An array of configuration item names
308     * @return null
309     */
310    static function remove_plugin_value($plugin_instance_id, $where)
311    {
312        global $serendipity;
313        $where_sql = array();
314        foreach($where AS $key) {
315            $where_sql[] = "(name LIKE '{$plugin_instance_id}/{$key}_%' AND value = '')";
316        }
317
318        $query = "DELETE FROM  {$serendipity['dbPrefix']}config
319                                    WHERE  " . implode(' OR ', $where_sql);
320
321        serendipity_db_query($query);
322    }
323
324    /**
325     * Retrieve a list of available plugin classes
326     *
327     * This function searches through all directories and loaded internal files and tries
328     * to detect the serendipity plugins.
329     *
330     * @access public
331     * @param   boolean     If true, only event plugins will be searched. If false, sidebar plugins will be searched.
332     * @return
333     */
334    static function &enum_plugin_classes($event_only = false)
335    {
336        global $serendipity;
337
338        $classes = array();
339
340        /* built-in classes first */
341        $cls = get_declared_classes();
342        foreach ($cls AS $class_name) {
343            if (strncmp($class_name, 'serendipity_', 6)) {
344                continue;
345            }
346
347            $p = get_parent_class($class_name);
348            while ($p != 'serendipity_plugin' && $p != 'serendipity_event' && $p !== false) {
349                $p = get_parent_class($p);
350            }
351
352            if ($p == 'serendipity_plugin' && $class_name != 'serendipity_event' && (!$event_only || is_null($event_only))) {
353                $classes[$class_name] = array('name'       => '@' . $class_name,
354                                              'type'       => 'internal_event',
355                                              'true_name'  => $class_name,
356                                              'pluginPath' => '');
357            } elseif ($p == 'serendipity_event' && $class_name != 'serendipity_event' && ($event_only || is_null($event_only))) {
358                $classes[$class_name] = array('name'       => '@' . $class_name,
359                                              'type'       => 'internal_plugin',
360                                              'true_name'  => $class_name,
361                                              'pluginPath' => '');
362            }
363        }
364
365        /* GLOBAL third-party classes next */
366        $ppath = serendipity_getRealDir(__FILE__) . 'plugins';
367        serendipity_plugin_api::traverse_plugin_dir($ppath, $classes, $event_only);
368
369        /* LOCAL third-party classes next */
370        $local_ppath = $serendipity['serendipityPath'] . 'plugins';
371        if ($ppath != $local_ppath) {
372            serendipity_plugin_api::traverse_plugin_dir($local_ppath, $classes, $event_only);
373        }
374
375        return $classes;
376    }
377
378    /**
379     * Traverse a specific directory and search if a serendipity plugin exists there.
380     *
381     * @access public
382     * @param   string      The path to start from (usually '.')
383     * @param   array       A referenced array of currently found classes
384     * @param   boolean     If true, only event plugins will be searched. If false, only sidebar plugins will be searched.
385     * @param   string      The maindir where we started searching from [for recursive use]
386     * @return
387     */
388    static function traverse_plugin_dir($ppath, &$classes, $event_only, $maindir = '')
389    {
390        $d = @opendir($ppath);
391        if ($d) {
392            while (($f = readdir($d)) !== false) {
393                if ($f[0] == '.' || $f == 'CVS' || !is_dir($ppath . '/' . $f) || !is_readable($ppath . '/' .$f)) {
394                    continue;
395                }
396
397                $subd = opendir($ppath . '/' . $f);
398                if (!$subd) {
399                    continue;
400                }
401
402                // Instead of only looking for directories, search for files within subdirectories
403                $final_loop = false;
404                while (($subf = readdir($subd)) !== false) {
405
406                    if ($subf[0] == '.' || $subf == 'CVS') {
407                        continue;
408                    }
409
410                    if (!$final_loop && is_dir($ppath . '/' . $f . '/' . $subf) && $maindir != $ppath . '/' . $f) {
411                        // Search for another level of subdirectories
412                        serendipity_plugin_api::traverse_plugin_dir($ppath . '/' . $f, $classes, $event_only, $f . '/');
413                        // We can break after that operation because the current directory has been fully checked already.
414                        $final_loop = true;
415                    }
416
417                    if (!preg_match('@^[^_]+_(event|plugin)_.+\.php$@i', $subf)) {
418                        continue;
419                    }
420
421                    $class_name = str_replace('.php', '', $subf);
422                    // If an external plugin/event already exists as internal, remove the internal reference because its redundant
423                    if (isset($classes['@' . $class_name])) {
424                        unset($classes['@' . $class_name]);
425                    }
426
427                    // A local plugin will be preferred over general plugins [used when calling this function the second time]
428                    if (isset($classes[$class_name])) {
429                        unset($classes[$class_name]);
430                    }
431
432                    if (!is_null($event_only) && $event_only && !serendipity_plugin_api::is_event_plugin($subf)) {
433                        continue;
434                    }
435
436                    if (!is_null($event_only) && !$event_only && serendipity_plugin_api::is_event_plugin($subf)) {
437                        continue;
438                    }
439
440                    $classes[$class_name] = array('name'       => $class_name,
441                                                  'true_name'  => $class_name,
442                                                  'type'       => 'additional_plugin',
443                                                  'pluginPath' => $maindir . $f);
444                }
445                closedir($subd);
446            }
447            closedir($d);
448        }
449    }
450
451    /**
452     * Returns a list of currently installed plugins
453     *
454     * @access public
455     * @param   string  The filter for plugins (left|right|hide|event|eventh)
456     * @return  array   The list of plugins
457     */
458    static function get_installed_plugins($filter = '*')
459    {
460        $plugins = serendipity_plugin_api::enum_plugins($filter);
461        $res = array();
462        foreach ( (array)$plugins AS $plugin ) {
463            list($class_name) = explode(':', $plugin['name']);
464            $class_name = ltrim($class_name, '@');
465            $res[] = $class_name;
466        }
467        return $res;
468    }
469
470    /**
471     * Searches for installed plugins based on specific conditions
472     *
473     * @access public
474     * @param   string  The filter for plugins (left|right|hide|event|eventh)
475     * @param   boolean If true, the filtering logic will be reversed and all plugins that are NOT part of the filter will be returned
476     * @param   string  Filter by a specific classname (like 'serendipity_plugin_archives'). Can take SQL wildcards.
477     * @param   string  Filter by a specific plugin instance id
478     * @return  array   Returns the associative array of found plugins in the database
479     */
480    static function enum_plugins($filter = '*', $negate = false, $classname = null, $id = null)
481    {
482        global $serendipity;
483
484        $sql   = "SELECT * from {$serendipity['dbPrefix']}plugins ";
485        $where = array();
486
487        if ($filter !== '*') {
488            if ($negate) {
489                $where[] = " placement != '" . serendipity_db_escape_string($filter) . "' ";
490            } else {
491                $where[] = " placement =  '" . serendipity_db_escape_string($filter) . "' ";
492            }
493        }
494
495        if (!empty($classname)) {
496            $where[] = " (name LIKE '@" . serendipity_db_escape_string($classname) . "%' OR name LIKE '" . serendipity_db_escape_string($classname) . "%') ";
497        }
498
499        if (!empty($id)) {
500            $where[] = " name = '" . serendipity_db_escape_string($id) . "' ";
501        }
502
503        if (count($where) > 0) {
504            $sql .= ' WHERE ' . implode(' AND ', $where);
505        }
506
507        $sql .= ' ORDER BY placement, sort_order';
508
509        return serendipity_db_query($sql);
510    }
511
512    /**
513     * Count the number of plugins to which the filter criteria matches
514     *
515     * @access public
516     * @param   string  The filter for plugins (left|right|hide|event|eventh)
517     * @param   boolean If true, the filtering logic will be reversed and all plugins that are NOT part of the filter will be evaluated
518     * @return  int     Number of plugins that were found.
519     */
520    static function count_plugins($filter = '*', $negate = false)
521    {
522        global $serendipity;
523
524        // Can be shortcircuited via a $serendipity['prevent_sidebar_plugins_(left|right|event)'] variable!
525        if (!$negate && $serendipity['prevent_sidebar_plugins_' . $filter] == true) {
526            return 0;
527        }
528
529
530        $sql = "SELECT COUNT(placement) AS count from {$serendipity['dbPrefix']}plugins ";
531
532        if ($filter !== '*') {
533            if ($negate) {
534                $sql .= "WHERE placement != '$filter' ";
535            } else {
536                $sql .= "WHERE placement='$filter' ";
537            }
538        }
539
540        $count = serendipity_db_query($sql, true);
541        if (is_array($count) && isset($count[0])) {
542            return (int) $count[0];
543        }
544
545        return 0;
546    }
547
548    /**
549     * Detect the filename to use for a specific plugin
550     *
551     * @access public
552     * @param   string  The name of the plugin ('serendipity_event_archive')
553     * @param   string  The path to the plugin file (if empty, the current path structure will be used.)
554     * @param   string  If an instance ID is passed this means, the plugin to be loaded is internally available
555     * @return  string  Returns the filename to include for a specific plugin
556     */
557    static function includePlugin($name, $pluginPath = '', $instance_id = '')
558    {
559        global $serendipity;
560
561        if (empty($pluginPath)) {
562            $pluginPath = $name;
563        }
564
565        $file = false;
566
567        // Security constraint
568        $pluginFile = 'plugins/' . $pluginPath . '/' . $name . '.php';
569        $pluginFile = preg_replace('@([\r\n\t\0\\\]+|\.\.+)@', '', $pluginFile);
570
571        // First try the local path, and then (if existing) a shared library repository ...
572        // Internal plugins ignored.
573        if (file_exists($serendipity['serendipityPath'] . $pluginFile)) {
574            $file = $serendipity['serendipityPath'] . $pluginFile;
575        } elseif (file_exists(S9Y_INCLUDE_PATH . $pluginFile)) {
576            $file = S9Y_INCLUDE_PATH . $pluginFile;
577        }
578
579        return $file;
580    }
581
582    /**
583     * Returns the plugin class name by a plugin instance ID
584     *
585     * @access public
586     * @param   string      The ID of a plugin
587     * @param   boolean     If true, the plugin is a internal plugin (prefixed with '@'). (Unused, keep for compat.)
588     * @return  string      The classname of the plugin
589     */
590    static function getClassByInstanceID($instance_id, &$is_internal)
591    {
592        $instance   = explode(':', $instance_id);
593        $class_name = ltrim($instance[0], '@');
594        return $class_name;
595    }
596
597    /**
598     * Auto-detect a plugin and see if the file information is given, and if not, detect it.
599     *
600     * @access public
601     * @param   string      The ID of a plugin to load
602     * @param   string      A reference variable that will hold the class name of the plugin (do not pass manually)
603     * @param   string      A reference variable that will hold the path to the plugin (do not pass manually)
604     * @return  string      Returns the filename of a plugin to load
605     */
606    /* Probes for the plugin filename */
607    static function probePlugin($instance_id, &$class_name, &$pluginPath)
608    {
609        global $serendipity;
610
611        $filename    = false;
612        $is_internal = false;
613
614        $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);
615
616        if (!$is_internal) {
617            /* plugin from the plugins/ dir */
618            // $serendipity['debug']['pluginload'][] = "Including plugin $class_name, $pluginPath";
619            $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath, $instance_id);
620            if (empty($filename) && !empty($instance_id)) {
621                // $serendipity['debug']['pluginload'][] = "No valid path/filename found.";
622                $sql = "SELECT path from {$serendipity['dbPrefix']}plugins WHERE name = '" . serendipity_db_escape_string($instance_id) . "'";
623                $plugdata = serendipity_db_query($sql, true, 'both', false, false, false, true);
624                if (is_array($plugdata) && isset($plugdata[0])) {
625                    $pluginPath = $plugdata[0];
626                }
627
628                if (empty($pluginPath)) {
629                    $pluginPath = $class_name;
630                }
631
632                // $serendipity['debug']['pluginload'][] = "Including plugin(2) $class_name, $pluginPath";
633                $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath);
634            }
635
636            if (empty($filename)) {
637                $serendipity['debug']['pluginload'][] = "No valid path/filename found. Aborting.";
638                $retval = false;
639                return $retval;
640            }
641        }
642
643        // $serendipity['debug']['pluginload'][] = "Found plugin file $filename";
644        return $filename;
645    }
646
647    /**
648     * Instantiates a plugin class
649     *
650     * @access public
651     * @param   string      The ID of the plugin to load
652     * @param   int         The owner of the plugin (can be autodetected)
653     * @param   string      The path to a plugin (can be autodetected)
654     * @param   string      The filename of a plugin (can be autodetected)
655     * @return
656     */
657    static function &load_plugin($instance_id, $authorid = null, $pluginPath = '', $pluginFile = null)
658    {
659        global $serendipity;
660
661        if ($pluginFile === null) {
662            $class_name = '';
663            // $serendipity['debug']['pluginload'][] = "Init probe for plugin $instance_id, $class_name, $pluginPath";
664            $pluginFile = serendipity_plugin_api::probePlugin($instance_id, $class_name, $pluginPath);
665        } else {
666            $is_internal = false;
667            // $serendipity['debug']['pluginload'][] = "getClassByInstanceID $instance_id, $is_internal";
668            $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);
669        }
670
671        if (!class_exists($class_name) && !empty($pluginFile)) {
672            // $serendipity['debug']['pluginload'][] = "Classname does not exist. Including $pluginFile.";
673            include($pluginFile);
674        }
675
676        if (!class_exists($class_name)) {
677            $serendipity['debug']['pluginload'][] = "Classname $class_name still does not exist. Aborting.";
678            return false;
679        }
680
681        // $serendipity['debug']['pluginload'][] = "Returning new $class_name($instance_id)";
682        $p = new $class_name($instance_id);
683        if (!is_null($authorid)) {
684            $p->serendipity_owner = $authorid;
685        } else {
686            $sql = "SELECT authorid from {$serendipity['dbPrefix']}plugins WHERE name = '" . serendipity_db_escape_string($instance_id) . "'";
687            $owner = serendipity_db_query($sql, true);
688            if (is_array($owner) && isset($owner[0])) {
689                $p->serendipity_owner = $owner[0];
690            }
691        }
692
693        $p->pluginPath = $p->act_pluginPath = $pluginPath;
694        if (empty($p->act_pluginPath)) {
695            $p->act_pluginPath = $class_name;
696        }
697        $p->pluginFile = $pluginFile;
698
699        return $p;
700    }
701
702    /**
703     * Gets cached properties/information about a specific plugin, auto-loads a cache of all plugins
704     *
705     * @access public
706     * @param   string      The filename of the plugin to get information about
707     * @param   array       A referenced array that holds information about the plugin instance (self::load_plugin() response)
708     * @param   type        The type of the plugin (sidebar|event)
709     * @return  array       Information about the plugin
710     */
711    static function &getPluginInfo(&$pluginFile, &$class_data, $type)
712    {
713        global $serendipity;
714
715        static $pluginlist = null;
716
717        if ($pluginlist === null) {
718            $data = serendipity_db_query("SELECT p.*,
719                                                 pc.category
720                                            FROM {$serendipity['dbPrefix']}pluginlist AS p
721                                 LEFT OUTER JOIN {$serendipity['dbPrefix']}plugincategories AS pc
722                                              ON pc.class_name = p.class_name
723                                           WHERE p.pluginlocation = 'local' AND
724                                                 p.plugintype     = '" . serendipity_db_escape_string($type) . "'");
725            if (is_array($data)) {
726                foreach($data AS $p) {
727                    if (isset($p['pluginpath'])) {
728                        $p['pluginPath'] = $p['pluginpath'];
729                    }
730                    if (!isset($pluginlist[$p['plugin_file']])) {
731                        $pluginlist[$p['plugin_file']] = $p;
732                    }
733
734                    $pluginlist[$p['plugin_file']]['groups'][] = $p['category'];
735                }
736            }
737        }
738
739        if (is_array($pluginlist[$pluginFile]) && !preg_match('@plugin_internal\.inc\.php@', $pluginFile)) {
740            $data = $pluginlist[$pluginFile];
741            if ((int) filemtime($pluginFile) == (int) $data['last_modified']) {
742                $data['stackable'] = serendipity_db_bool($data['stackable']);
743                $plugin = $data;
744                return $plugin;
745            }
746        }
747
748        $plugin =& serendipity_plugin_api::load_plugin($class_data['name'], null, $class_data['pluginPath'], $pluginFile);
749
750        return $plugin;
751    }
752
753    /**
754     * Set cache information about a plugin
755     *
756     * @access public
757     * @param   mixed       Either an plugin object or a plugin information array that holds the information about the plugin
758     * @param   string      The filename of the plugin
759     * @param   object      The property bag object bundled with the plugin
760     * @param   array       Previous/additional information about the plugin
761     * @param   string      The location/type of a plugin (local|spartacus)
762     * @return
763     */
764    static function &setPluginInfo(&$plugin, &$pluginFile, &$bag, &$class_data, $pluginlocation = 'local')
765    {
766        global $serendipity;
767
768        static $dbfields = array(
769            'plugin_file',
770            'class_name',
771            'plugin_class',
772            'pluginPath',
773            'name',
774            'description',
775            'version',
776            'upgrade_version',
777            'plugintype',
778            'pluginlocation',
779            'stackable',
780            'author',
781            'requirements',
782            'website',
783            'last_modified'
784        );
785
786        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}pluginlist WHERE plugin_file = '" . serendipity_db_escape_string($pluginFile) . "' AND pluginlocation = '" . serendipity_db_escape_string($pluginlocation) . "'");
787
788        if (!empty($pluginFile) && file_exists($pluginFile)) {
789            $lastModified = filemtime($pluginFile);
790        } else {
791            $lastModified = 0;
792        }
793
794        if (is_object($plugin)) {
795            $data = array(
796                'class_name'      => get_class($plugin),
797                'stackable'       => $bag->get('stackable'),
798                'name'            => $bag->get('name'),
799                'description'     => $bag->get('description'),
800                'author'          => $bag->get('author'),
801                'version'         => $bag->get('version'),
802                'upgrade_version' => isset($class_data['upgrade_version']) ? $class_data['upgrade_version'] : $bag->get('version'),
803                'requirements'    => serialize($bag->get('requirements')),
804                'website'         => $bag->get('website'),
805                'plugin_class'    => $class_data['name'],
806                'pluginPath'      => $class_data['pluginPath'],
807                'plugin_file'     => $pluginFile,
808                'pluginlocation'  => $pluginlocation,
809                'plugintype'      => $serendipity['GET']['type'],
810                'last_modified'   => $lastModified
811            );
812            $groups = $bag->get('groups');
813        } elseif (is_array($plugin)) {
814            $data = $plugin;
815            $groups = $data['groups'];
816            unset($data['installable']);
817            unset($data['true_name']);
818            unset($data['customURI']);
819            unset($data['groups']);
820            if (isset($data['pluginpath'])) {
821                $data['pluginPath'] = $data['pluginpath'];
822            }
823            $data['requirements'] = serialize($data['requirements']);
824        }
825
826        if (!isset($data['stackable']) || empty($data['stackable'])) {
827            $data['stackable'] = '0';
828        }
829
830        if (!isset($data['last_modified'])) {
831            $data['last_modified'] = $lastModified;
832        }
833
834        // Only insert data keys that exist in the DB.
835        $insertdata = array();
836        foreach($dbfields AS $field) {
837            $insertdata[$field] = $data[$field];
838        }
839
840        if ($data['upgradable']) {
841            serendipity_db_query("UPDATE {$serendipity['dbPrefix']}pluginlist
842                                     SET upgrade_version = '" . serendipity_db_escape_string($data['upgrade_version']) . "'
843                                   WHERE plugin_class    = '" . serendipity_db_escape_string($data['plugin_class']) . "'");
844        }
845        serendipity_db_insert('pluginlist', $insertdata);
846
847        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}plugincategories WHERE class_name = '" . serendipity_db_escape_string($data['class_name']) . "'");
848        foreach((array)$groups AS $group) {
849            if (empty($group)) {
850                continue;
851            }
852
853            $cat = array(
854                'class_name'  => $data['class_name'],
855                'category'    => $group
856            );
857            serendipity_db_insert('plugincategories', $cat);
858        }
859
860        $data['groups'] = $groups;
861
862        return $data;
863    }
864
865    /**
866     * Moves a sidebar plugin to a different side or up/down
867     *
868     * @access public
869     * @param   string  The instance ID of a plugin
870     * @param   string  The new placement of a plugin (left|right|hide|event|eventh)
871     * @param   string  A new sort order for the plugin
872     * @return
873     */
874    static function update_plugin_placement($name, $placement, $order = null)
875    {
876        global $serendipity;
877
878        $admin = '';
879        if (!serendipity_checkPermission('adminPlugins') && $placement == 'hide') {
880            // Only administrators can set plugins to 'hide' if they are not the owners.
881            $admin = " AND (authorid = 0 OR authorid = {$serendipity['authorid']})";
882        }
883
884        $sql = "UPDATE {$serendipity['dbPrefix']}plugins set placement='$placement' ";
885
886        if ($order !== null) {
887            $sql .= ", sort_order=$order ";
888        }
889
890        $sql .= "WHERE name='$name' $admin";
891
892        return serendipity_db_query($sql);
893    }
894
895    /**
896     * Updates the ownership information about a plugin
897     *
898     * @access public
899     * @param   string  The instance ID of the plugin
900     * @param   int     The ID of the new author owner of the plugin
901     * @return
902     */
903    static function update_plugin_owner($name, $authorid)
904    {
905        global $serendipity;
906
907        if (empty($authorid) && $authorid != '0') {
908            return;
909        }
910
911        $admin = '';
912        if (!serendipity_checkPermission('adminPlugins')) {
913            $admin = " AND (authorid = 0 OR authorid = {$serendipity['authorid']})";
914        }
915
916        $sql = "UPDATE {$serendipity['dbPrefix']}plugins SET authorid='$authorid' WHERE name='$name' $admin";
917
918        return serendipity_db_query($sql);
919    }
920
921    /**
922     * Get a list of Sidebar plugins and pass them to Smarty
923     *
924     * @access public
925     * @param   string      The side of plugins to show (left/right/hide/event/eventh)
926     * @param   string      deprecated: Indicated which wrapping HTML element to use for plugins
927     * @param   boolean     Indicates whether only all plugins should be shown that are not in the $side list
928     * @param   string      Only show plugins of this plugin class
929     * @param   string      Only show a plugin with this instance ID
930     * @return  string      Smarty HTML output
931     */
932    static function generate_plugins($side, $negate = false, $class = null, $id = null, $tpl = 'sidebar.tpl')
933    {
934        global $serendipity;
935        $plugins = serendipity_plugin_api::enum_plugins($side, $negate, $class, $id);
936
937        if (!is_array($plugins)) {
938            return;
939        }
940
941        if (!isset($serendipity['smarty'])) {
942            $serendipity['smarty_raw_mode'] = true;
943            serendipity_smarty_init();
944        }
945
946        $pluginData = array();
947        $addData    = func_get_args();
948        serendipity_plugin_api::hook_event('frontend_generate_plugins', $plugins, $addData);
949
950        if (count($plugins) == 0) {
951            $serendipity['prevent_sidebar_plugins_' . $side] = true;
952        }
953
954        $loggedin = false;
955        if (serendipity_userLoggedIn() && serendipity_checkPermission('adminPlugins')) {
956            $loggedin = true;
957        }
958
959        foreach ($plugins AS $plugin_data) {
960            $plugin =& serendipity_plugin_api::load_plugin($plugin_data['name'], $plugin_data['authorid'], $plugin_data['path']);
961            if (is_object($plugin)) {
962                $class  = get_class($plugin);
963                $title  = '';
964
965                /* TODO: make generate_content NOT echo its output */
966                ob_start();
967                $show_plugin = $plugin->generate_content($title);
968                $content = ob_get_contents();
969                ob_end_clean();
970
971                if ($loggedin) {
972                    $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>';
973                }
974
975                if ($show_plugin !== false) {
976                    $pluginData[] = array('side'    => $side,
977                                          'class'   => $class,
978                                          'title'   => $title,
979                                          'content' => $content,
980                                          'id'      => $plugin->instance);
981                }
982            } else {
983                    $pluginData[] = array('side'          => $side,
984                                          'title'         => ERROR,
985                                          'class'         => $class,
986                                          'content'       => sprintf(INCLUDE_ERROR, $plugin_data['name']));
987            }
988        }
989
990        serendipity_plugin_api::hook_event('frontend_sidebar_plugins', $pluginData, $addData);
991
992        $serendipity['smarty']->assignByRef('plugindata', $pluginData);
993        $serendipity['smarty']->assign('pluginside', ucfirst($side));
994
995        return serendipity_smarty_fetch('sidebar_'. $side, $tpl, true);
996    }
997
998    /**
999     * Gets the title of a plugin to be shown in plugin overview
1000     *
1001     * @access public
1002     * @param   object      The plugin object
1003     * @param   string      The default title, if none was configured
1004     * @return  string      The title of the plugin
1005     */
1006    static function get_plugin_title(&$plugin, $default_title = '')
1007    {
1008        global $serendipity;
1009
1010        // Generate plugin output. Make sure that by probing the plugin, no events are actually called. After that,
1011        // restore setting of 'no_events'.
1012
1013        if (!is_null($plugin->title)) {
1014            // Preferred way of fetching a plugins title
1015            $title = &$plugin->title;
1016        } else {
1017            $ne = (isset($serendipity['no_events']) && $serendipity['no_events'] ? true : false);
1018            $serendipity['no_events'] = true;
1019            ob_start();
1020            $plugin->generate_content($title);
1021            ob_end_clean();
1022            $serendipity['no_events'] = $ne;
1023        }
1024
1025        if (strlen(trim($title)) == 0) {
1026            if (!empty($default_title)) {
1027                $title = $default_title;
1028            } else {
1029                $title = $plugin->instance;
1030            }
1031        }
1032
1033        return $title;
1034    }
1035
1036    /**
1037     * Check if a plugin is bundled with s9y core
1038     *
1039     * @access public
1040     * @param   string  Name of a plugin
1041     * @return  boolean
1042     */
1043    static function is_bundled_plugin($name)
1044    {
1045        return in_array ($name, BUNDLED_PLUGINS);
1046    }
1047
1048    /**
1049     * Check if a plugin is an event plugin
1050     *
1051     * Refactoring: decompose conditional
1052     *
1053     * @access public
1054     * @param   string  Name of a plugin
1055     * @return  boolean
1056     */
1057    static function is_event_plugin($name)
1058    {
1059        return (strstr($name, '_event_'));
1060    }
1061
1062    /**
1063     * Prepares a cache of all event plugins and load them in queue so that they can be fetched
1064     *
1065     * @access public
1066     * @param  mixed    If set to a string, a certain event plugin cache object will be returned by this function
1067     * @param  boolean  If set to true, the list of cached event plugins will be refreshed
1068     * @return mixed    Either returns the whole list of event plugins, or only a specific instance
1069     */
1070    static function &get_event_plugins($getInstance = false, $refresh = false)
1071    {
1072        static $event_plugins;
1073        static $false = false;
1074
1075        if (!$refresh && isset($event_plugins) && is_array($event_plugins)) {
1076            if ($getInstance) {
1077                if (isset($event_plugins[$getInstance]['p'])) {
1078                    return $event_plugins[$getInstance]['p'];
1079                }
1080                return $false;
1081            }
1082            return $event_plugins;
1083        }
1084
1085        $plugins = serendipity_plugin_api::enum_plugins('event');
1086        if (!is_array($plugins)) {
1087            return $false;
1088        }
1089
1090        $event_plugins = array();
1091        foreach($plugins AS $plugin_data) {
1092            if ($event_plugins[$plugin_data['name']]['p'] = &serendipity_plugin_api::load_plugin($plugin_data['name'], $plugin_data['authorid'], $plugin_data['path'])) {
1093                /* query for its name, description and configuration data */
1094                $event_plugins[$plugin_data['name']]['b'] = new serendipity_property_bag;
1095                $event_plugins[$plugin_data['name']]['p']->introspect($event_plugins[$plugin_data['name']]['b']);
1096                $event_plugins[$plugin_data['name']]['t'] = serendipity_plugin_api::get_plugin_title($event_plugins[$plugin_data['name']]['p']);
1097            } else {
1098                unset($event_plugins[$plugin_data['name']]); // Unset failed plugins
1099            }
1100        }
1101
1102        if ($getInstance) {
1103            if (isset($event_plugins[$getInstance]['p'])) {
1104                return $event_plugins[$getInstance]['p'];
1105            }
1106            return $false;
1107        }
1108
1109        return $event_plugins;
1110    }
1111
1112    /**
1113     * Executes a specific Eventhook
1114     *
1115     * If you want to temporarily block any event plugins, you can set $serendipity['no_events'] before
1116     * this method call.
1117     *
1118     * @access public
1119     * @param   string      The name of the event to hook on to
1120     * @param   mixed       May contain any type of variables that are passed by reference to an event plugin
1121     * @param   mixed       May contain any type of variables that are passed to an event plugin
1122     * @return true
1123     */
1124    static function hook_event($event_name, &$eventData, $addData = null)
1125    {
1126        global $serendipity;
1127
1128        // Can be bypassed globally by setting $serendipity['no_events'] = TRUE;
1129        if (isset($serendipity['no_events']) && $serendipity['no_events'] == true) {
1130            return false;
1131        }
1132
1133        if ($serendipity['enablePluginACL'] && !serendipity_hasPluginPermissions($event_name)) {
1134            return false;
1135        }
1136
1137        // We can NOT use a "return by reference" here, because then when
1138        // a plugin executes another event_hook, the referenced variable within
1139        // that call will overwrite the previous original plugin listing and
1140        // skip the execution of any follow-up plugins.
1141        $plugins = serendipity_plugin_api::get_event_plugins();
1142
1143        if ($serendipity['core_events'][$event_name]) {
1144            foreach($serendipity['core_events'][$event_name] as $apifunc_key => $apifunc) {
1145                $apifunc($event_name, $bag, $eventData, $addData);
1146            }
1147        }
1148
1149        // execute backend needed core hooks
1150        serendipity_plugin_api_core_event_hook($event_name, $bag, $eventData, $addData);
1151
1152        if (function_exists('serendipity_plugin_api_pre_event_hook')) {
1153            $apifunc = 'serendipity_plugin_api_pre_event_hook';
1154            $apifunc($event_name, $bag, $eventData, $addData);
1155        }
1156
1157        // Function names cannot contain ":" etc, so if we ever have event looks like "backend:js" this
1158        // needs to be replaced to "backend_js". The real event name is passed as a function argument
1159        // These specific per-hook functions are utilized for theme's config.inc.php files
1160        // that act as an engine for other themes.
1161        $safe_event_name = preg_replace('@[^a-z0-9_]+@i', '_', $event_name);
1162        if (function_exists('serendipity_plugin_api_pre_event_hook_' . $safe_event_name)) {
1163            $apifunc = 'serendipity_plugin_api_pre_event_hook_' . $safe_event_name;
1164            $apifunc($event_name, $bag, $eventData, $addData);
1165        }
1166
1167        if (is_array($plugins)) {
1168            foreach($plugins as $plugin => $plugin_data) {
1169                $bag    = &$plugin_data['b'];
1170                $phooks = &$bag->get('event_hooks');
1171                if (isset($phooks[$event_name])) {
1172
1173                    // Check for cachable events.
1174                    if (isset($eventData['is_cached']) && $eventData['is_cached']) {
1175                        $chooks = &$bag->get('cachable_events');
1176                        if (is_array($chooks) && isset($chooks[$event_name])) {
1177                            continue;
1178                        }
1179                    }
1180
1181                    if ($serendipity['enablePluginACL'] && !serendipity_hasPluginPermissions($plugin)) {
1182                        continue;
1183                    }
1184                    $plugins[$plugin]['p']->event_hook($event_name, $bag, $eventData, $addData);
1185                }
1186            }
1187
1188            if (function_exists('serendipity_plugin_api_event_hook')) {
1189                $apifunc = 'serendipity_plugin_api_event_hook';
1190                $apifunc($event_name, $bag, $eventData, $addData);
1191            }
1192
1193            if (function_exists('serendipity_plugin_api_event_hook_' . $safe_event_name)) {
1194                $apifunc = 'serendipity_plugin_api_event_hook_' . $safe_event_name;
1195                $apifunc($event_name, $bag, $eventData, $addData);
1196            }
1197
1198        }
1199
1200        return true;
1201    }
1202
1203    /**
1204     * Checks if a specific plugin instance is already installed
1205     *
1206     * @access public
1207     * @param   string      A name (may contain wildcards) of a plugin class to check
1208     * @return  boolean     True if a plugin was found
1209     */
1210    static function exists($instance_id)
1211    {
1212        global $serendipity;
1213
1214        if (!strstr($instance_id, ':')) {
1215            $instance_id .= ':';
1216        }
1217
1218        $existing = serendipity_db_query("SELECT name FROM {$serendipity['dbPrefix']}plugins WHERE name LIKE '%" . serendipity_db_escape_string($instance_id) . "%'");
1219
1220        if (is_array($existing) && !empty($existing[0][0])) {
1221            return $existing[0][0];
1222        }
1223
1224        return false;
1225    }
1226
1227    /**
1228     * Install a new plugin by ensuring that it does not already exist
1229     *
1230     * @access public
1231     * @param   string      The classname of the plugin
1232     * @param   int         The new owner author
1233     * @param   boolean     Indicates if the plugin is an event plugin
1234     * @return  object      Returns the plugin object or false, if failure
1235     */
1236    static function &autodetect_instance($plugin_name, $authorid, $is_event_plugin = false)
1237    {
1238        if ($is_event_plugin) {
1239            $side = 'event';
1240        } else {
1241            $side = 'right';
1242        }
1243
1244        $classes = serendipity_plugin_api::enum_plugin_classes(null);
1245        if (isset($classes[$plugin_name])) {
1246            $instance = serendipity_plugin_api::create_plugin_instance($plugin_name, null, $side, $authorid, $classes[$plugin_name]['pluginPath']);
1247        } else {
1248            $instance = false;
1249        }
1250
1251        return $instance;
1252    }
1253
1254    /**
1255     * Probe for a language include with constants. Still include defines later on, if some constants were missing
1256     *
1257     * @access public
1258     * @param current plugin's path
1259     * @return  object      Returns the plugin object or false, if failure
1260     */
1261    static function load_language($path) {
1262        global $serendipity;
1263
1264        $probelang = $path . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php';
1265        if (file_exists($probelang)) {
1266            include $probelang;
1267        }
1268
1269        include $path . '/lang_en.inc.php';
1270    }
1271}
1272
1273/**
1274 * holds a bunch of properties; since serendipity 0.8 only one value per key is
1275 * allowed [was never really useful]
1276 */
1277class serendipity_property_bag
1278{
1279    /**
1280     * @access  private
1281     * @var array   property storage container.
1282     */
1283    var $properties = array();
1284
1285    /**
1286     * @access private
1287     * @var    string   Name of the property bag
1288     */
1289    var $name = null;
1290
1291    /**
1292     * Adds a property value to the bag
1293     *
1294     * @access public
1295     * @param   string  The name of the property
1296     * @param   mixed   The value of a property
1297     * @return null
1298     */
1299    function add($name, $value)
1300    {
1301        $this->properties[$name] = $value;
1302    }
1303
1304    /**
1305     * Returns a property value of a bag
1306     *
1307     * @access public
1308     * @param   string  Name of property to fetch
1309     * @return  mixed   The value of the property
1310     */
1311    function &get($name)
1312    {
1313        return $this->properties[$name];
1314    }
1315
1316    /**
1317     * Check if a specific property name is already set
1318     *
1319     * @access public
1320     * @param   string  Name of the property to check
1321     * @return  boolean True, if already set.
1322     */
1323    function is_set($name)
1324    {
1325        return isset($this->properties[$name]);
1326    }
1327
1328}
1329
1330/**
1331 * A core plugin, with methods that both event and sidebar plugins share
1332 */
1333class serendipity_plugin
1334{
1335    var $instance          = null;
1336    var $protected         = false;
1337    var $wrap_class        = 'serendipitySideBarItem';
1338    var $title_class       = 'serendipitySideBarTitle';
1339    var $content_class     = 'serendipitySideBarContent';
1340    var $title             = null;
1341    var $pluginPath        = null;
1342    var $act_pluginPath    = null;
1343    var $pluginFile        = null;
1344    var $serendipity_owner = null;
1345
1346    /**
1347     * The constructor of a plugin
1348     *
1349     * Needs to be implemented by your own class.
1350     * Be sure to call this method from your derived classes constructors,
1351     * otherwise your config data will not be stored or retrieved correctly
1352     *
1353     * @access public
1354     * @return true
1355     */
1356    function __construct($instance)
1357    {
1358        $this->instance = $instance;
1359    }
1360
1361    /**
1362     * Perform configuration routines
1363     *
1364     * Called by Serendipity when the plugin is being configured.
1365     * Can be used to query the database for configuration values that
1366     * only need to be available for the global configuration and not
1367     * on each page request.
1368     *
1369     * @access public
1370     * @return true
1371     */
1372    function performConfig(&$bag)
1373    {
1374        return true;
1375    }
1376
1377    /**
1378     * Perform install routines
1379     *
1380     * Called by Serendipity when the plugin is first installed.
1381     * Can be used to install database tables etc.
1382     *
1383     * @access public
1384     * @return true
1385     */
1386    function install()
1387    {
1388        return true;
1389    }
1390
1391    /**
1392     * Perform uninstall routines
1393     *
1394     * Called by Serendipity when the plugin is removed/uninstalled.
1395     * Can be used to drop installed database tables etc.
1396     *
1397     * @access public
1398     * @param  object   A property bag object
1399     * @return true
1400     */
1401    function uninstall(&$propbag)
1402    {
1403        return true;
1404    }
1405
1406    /**
1407     * The introspection function of a plugin, to setup properties
1408     *
1409     * Called by serendipity when it wants to display information
1410     * about your plugin.
1411     * You need to override this method in your child class.
1412     *
1413     * @access public
1414     * @param   object  A property bag object you can manipulate
1415     * @return true
1416     */
1417    function introspect(&$propbag)
1418    {
1419        $propbag->add('copyright', 'MIT License');
1420        $propbag->add('name'     , get_class($this));
1421
1422        // $propbag->add(
1423        //   'configuration',
1424        //   array(
1425        //     'text field',
1426        //     'foo bar'
1427        //   )
1428        // );
1429
1430        $this->protected = false; // If set to TRUE, only allows the owner of the plugin to modify its configuration
1431
1432        return true;
1433    }
1434
1435    /**
1436     * Introspection of a plugin configuration item
1437     *
1438     * Called by serendipity when it wants to display the configuration
1439     * editor for your plugin.
1440     * $name is the name of a configuration item you added in
1441     * your instrospect method.
1442     * You need to fill the property bag with appropriate items
1443     * that describe the type and value(s) for that particular
1444     * configuration option.
1445     * You need to override this method in your child class if
1446     * you have configuration options.
1447     *
1448     * @access public
1449     * @param   string      Name of the config item
1450     * @param   object      A property bag object you can store the configuration in
1451     * @return
1452     */
1453    function introspect_config_item($name, &$propbag)
1454    {
1455        return false;
1456    }
1457
1458    /**
1459     * Validate plugin configuration options.
1460     *
1461     * Called from Plugin Configuration manager. Can be extended by your own plugin, if you need.
1462     *
1463     * @access public
1464     * @param   string      Name of the config item to validate
1465     * @param   object      Property bag of the config item
1466     * @param   value       The value of a config item
1467     * @return
1468     */
1469    function validate($config_item, &$cbag, &$value)
1470    {
1471        static $pattern_mail  = '([\.\-\+~@_0-9a-z]+?)';
1472        static $pattern_url   = '([@!=~\?:&;0-9a-z#\.\-_\/]+?)';
1473
1474        $validate = $cbag->get('validate');
1475        $valid    = true;
1476
1477        if (!empty($validate)) {
1478            switch ($validate) {
1479                case 'string':
1480                    if (!preg_match('@^\w*$@i', $value)) {
1481                        $valid = false;
1482                    }
1483                    break;
1484
1485                case 'words':
1486                    if (!preg_match('@^[\w\s\r\n,\.\-!\?:;&_/=%\$]*$@i', $value)) {
1487                        $valid = false;
1488                    }
1489                    break;
1490
1491                case 'number':
1492                    if (!preg_match('@^[\d]*$@', $value)) {
1493                        $valid = false;
1494                    }
1495                    break;
1496
1497                case 'url':
1498                    if (!preg_match('�^' . $pattern_url . '$�', $value)) {
1499                        $valid = false;
1500                    }
1501                    break;
1502
1503                case 'mail':
1504                    if (!preg_match('�^' . $pattern_mail . '$�', $value)) {
1505                        $valid = false;
1506                    }
1507                    break;
1508
1509                case 'path':
1510                    if (!preg_match('@^[\w/_.\-~]$@', $value)) {
1511                        $valid = false;
1512                    }
1513                    break;
1514
1515                default:
1516                    if (!preg_match($validate, $value)) {
1517                        $valid = false;
1518                    }
1519                    break;
1520            }
1521
1522            $error = $cbag->get('validate_error');
1523            if ($valid) {
1524                return true;
1525            } elseif (!empty($error)) {
1526                return $error;
1527            } else {
1528                return sprintf(PLUGIN_API_VALIDATE_ERROR, $config_item, $validate);
1529            }
1530        }
1531
1532        return true;
1533    }
1534
1535    /**
1536     * Output plugin's contents (Sidebar plugins)
1537     *
1538     * Called by serendipity when it wants your plugin to display itself.
1539     * You need to set $title to be whatever text you want want to
1540     * appear in the item caption space.
1541     * Simply echo/print your content to the output; serendipity will
1542     * capture it and make things work.
1543     * You need to override this method in your child class.
1544     *
1545     * @access public
1546     * @param   string       The referenced variable that holds the sidebar title of your plugin.
1547     * @return null
1548     */
1549    function generate_content(&$title)
1550    {
1551        $title = 'Sample!';
1552        echo     'This is a sample!';
1553    }
1554
1555    /**
1556     * Get a config value of the plugin
1557     *
1558     * @access public
1559     * @param   string  Name of the config value to fetch
1560     * @param   mixed   The default value of a configuration item, if not set
1561     * @param   boolean If true, the default value will only be set if the plugin config item was not set.
1562     * @return  mixed   The value of the config item
1563     */
1564    function get_config($name, $defaultvalue = null, $empty = true)
1565    {
1566        $_res = serendipity_get_config_var($this->instance . '/' . $name, $defaultvalue, $empty);
1567
1568        if (is_null($_res)) {
1569            // A protected plugin by a specific owner may not have its values stored in $serendipity
1570            // because of the special authorid. To display such contents, we need to fetch it
1571            // separately from the DB.
1572            $_res = serendipity_get_user_config_var($this->instance . '/' . $name, null, $defaultvalue);
1573        }
1574
1575        if (is_null($_res)) {
1576            $cbag = new serendipity_property_bag();
1577            $this->introspect_config_item($name, $cbag);
1578            $_res = $cbag->get('default');
1579            unset($cbag);
1580            // Set the fetched value, so the default will not be fetched the next config call time
1581            $this->set_config($name, $_res);
1582        }
1583
1584        return $_res;
1585    }
1586
1587    /**
1588     * Sets a configuration value for a plugin
1589     *
1590     * @access public
1591     * @param   string  Name of the plugin configuration item
1592     * @param   string  Value of the plugin configuration item
1593     * @param   string  A concatenation key for imploding arrays
1594     * @return
1595     */
1596    function set_config($name, $value, $implodekey = '^')
1597    {
1598        $name = $this->instance . '/' . $name;
1599
1600        if (is_array($value)) {
1601            $dbvalue = implode($implodekey, $value);
1602            $_POST['serendipity']['plugin'][$name] = $dbvalue;
1603        } else {
1604            $dbvalue = $value;
1605        }
1606
1607        return serendipity_set_config_var($name, $dbvalue);
1608    }
1609
1610    /**
1611     * Garbage Collection
1612     *
1613     * Called by serendipity after insertion of a config item. If you want to kick out certain
1614     * elements based on contents, create the corresponding function here.
1615     *
1616     * @access public
1617     * @return true
1618     */
1619    function cleanup()
1620    {
1621        // Cleanup. Remove all empty configs on SAVECONF-Submit.
1622        // serendipity_plugin_api::remove_plugin_value($this->instance, array('configname1', 'configname2'));
1623        return true;
1624    }
1625
1626    /**
1627     * Auto-Register dependencies of a plugin
1628     *
1629     * This method evaluates the "dependencies" member variable to check which plugins need to be installed.
1630     *
1631     * @access public
1632     * @param   boolean     If true, a depending plugin will be removed when this plugin is uninstalled
1633     * @param   int         The owner id of the current plugin
1634     * @return true
1635     */
1636    function register_dependencies($remove = false, $authorid = '0')
1637    {
1638        global $serendipity;
1639
1640        if (isset($this->dependencies) && is_array($this->dependencies)) {
1641
1642            if ($remove) {
1643                $dependencies = @explode(';', $this->get_config('dependencies'));
1644                $modes        = @explode(';', $this->get_config('dependency_modes'));
1645
1646                if (!empty($dependencies) && is_array($dependencies)) {
1647                    foreach($dependencies AS $idx => $dependency) {
1648                        if ($modes[$idx] == 'remove' && serendipity_plugin_api::exists($dependency)) {
1649                            serendipity_plugin_api::remove_plugin_instance($dependency);
1650                        }
1651                    }
1652                }
1653            } else {
1654                $keys  = array();
1655                $modes = array();
1656                foreach($this->dependencies AS $dependency => $mode) {
1657                    $exists = serendipity_plugin_api::exists($dependency);
1658                    if (!$exists) {
1659                        if (serendipity_plugin_api::is_event_plugin($dependency)) {
1660                            $keys[] = serendipity_plugin_api::autodetect_instance($dependency, $authorid, true);
1661                        } else {
1662                            $keys[] = serendipity_plugin_api::autodetect_instance($dependency, $authorid, false);
1663                        }
1664                    } else {
1665                        $keys[] = $exists;
1666                    }
1667
1668                    $modes[] = $mode;
1669                }
1670
1671                $this->set_config('dependencies',     implode(';', $keys));
1672                $this->set_config('dependency_modes', implode(';', $modes));
1673            }
1674        }
1675
1676        return true;
1677    }
1678
1679    /**
1680     * Parses a smarty template file (which can be stored in either the plugin directory, the user template directory
1681     * or the default template directory, and return the parsed output.
1682     *
1683     * @access public
1684     * @param  string   template filename (no directory!)
1685     * @return string   Parsed Smarty return
1686     */
1687    function &parseTemplate($filename)
1688    {
1689        global $serendipity;
1690
1691        $filename = basename($filename);
1692        $tfile    = serendipity_getTemplateFile($filename, 'serendipityPath', true);
1693        if (!$tfile || $tfile == $filename) {
1694            $tfile = dirname($this->pluginFile) . '/' . $filename;
1695        }
1696
1697        return $serendipity['smarty']->fetch('file:'. $tfile);
1698    }
1699
1700    /**
1701     * Get full path for a filename. Will first look into themes and then in the plugins directory
1702     * @param   string  relative path to file
1703     * @param   string  The path selector that tells whether to return a HTTP or realpath
1704     * @return  string  The full path+filename to the requested file
1705     * */
1706    function &getFile($filename, $key = 'serendipityPath')
1707    {
1708        global $serendipity;
1709
1710        $path = serendipity_getTemplateFile($filename, $key, true);
1711        if (!$path) {
1712            if (file_exists(dirname($this->pluginFile) . '/' . $filename)) {
1713                return $serendipity[$key] . 'plugins/' . basename(dirname($this->pluginFile)) . '/' . $filename;
1714            }
1715        }
1716
1717        return $path;
1718    }
1719
1720}
1721
1722/**
1723 * Events can be called on several occasions when s9y performs an action.
1724 * One or multiple plugin can be registered for each of those hooks.
1725 */
1726class serendipity_event extends serendipity_plugin
1727{
1728
1729    /**
1730     * The class constructor
1731     *
1732     * Be sure to call this method from your derived classes constructors,
1733     * otherwise your config data will not be stored or retrieved correctly
1734     *
1735     * @access public
1736     * @param   string      The instance name
1737     * @return
1738     */
1739    function __construct($instance)
1740    {
1741        $this->instance = $instance;
1742    }
1743
1744    /**
1745     * Gets a reference to an $entry / $eventData array pointer, interacting with Cache-Options
1746     *
1747     * This function is used by specific event plugins that require to properly get a reference
1748     * to the 'extended' or 'body' field of an entry superarray. If they would immediately operate
1749     * on the 'body' field, it might get overwritten by other plugins later on.
1750     *
1751     * @access public
1752     * @param   string      The fieldname to get a reference for
1753     * @param   array       The entry superarray to get the reference from
1754     * @return  array       The value of the array for the fieldname (reference)
1755     */
1756    function &getFieldReference($fieldname = 'body', &$eventData)
1757    {
1758        // Get a reference to a content field (body/extended) of
1759        // $entries input data. This is a unifying function because
1760        // several plugins are using similar fields.
1761
1762        if (is_array($eventData) && isset($eventData[0]) && is_array($eventData[0]) && is_array($eventData[0]['properties'])) {
1763            if (!empty($eventData[0]['properties']['ep_cache_' . $fieldname])) {
1764
1765                // It may happen that there is no extended entry to concatenate to. In that case,
1766                // create a dummy extended entry.
1767                if (!isset($eventData[0]['properties']['ep_cache_' . $fieldname])) {
1768                    $eventData[0]['properties']['ep_cache_' . $fieldname] = '';
1769                }
1770
1771                $key = &$eventData[0]['properties']['ep_cache_' . $fieldname];
1772            } else {
1773                $key = &$eventData[0][$fieldname];
1774            }
1775        } elseif (is_array($eventData) && is_array($eventData['properties'])) {
1776            if (!empty($eventData['properties']['ep_cache_' . $fieldname])) {
1777                $key = &$eventData['properties']['ep_cache_' . $fieldname];
1778            } else {
1779                $key = &$eventData[$fieldname];
1780            }
1781        } elseif (is_array($eventData[0]) && isset($eventData[0][$fieldname])) {
1782            $key = &$eventData[0][$fieldname];
1783        } elseif (isset($eventData[$fieldname])) {
1784            $key = &$eventData[$fieldname];
1785        } else {
1786            $key = '';
1787        }
1788
1789        return $key;
1790    }
1791
1792    /**
1793     * Main logic for making a plugin "listen" to an event
1794     *
1795     * This method is called by the main plugin API for every event, that is executed.
1796     * You need to implement each actions that shall be performed by your plugin here.
1797     *
1798     * @access public
1799     * @param   string      The name of the executed event
1800     * @param   object      A property bag for the current plugin
1801     * @param   mixed       Any referenced event data from the serendipity_plugin_api::hook_event() function
1802     * @param   mixed       Any additional data from the hook_event call
1803     * @return true
1804     */
1805    function event_hook($event, &$bag, &$eventData, $addData = null)
1806    {
1807        // Define event hooks here, if you want your plugin to execute those instead of being a sidebar item.
1808        // Look at in/external plugins 'serendipity_event_mailer' or 'serendipity_event_weblogping' for usage.
1809        // Currently available events:
1810        //   backend_publish [after insertion of a new article in your s9y-backend]
1811        //   backend_display [after displaying an article in your s9y-backend]
1812        //   frontend_display [before displaying an article in your s9y-frontend]
1813        //   frontend_comment [after displaying the "enter comment" dialog]
1814        //   ...and some more in the meanwhile...! :)
1815        return true;
1816    }
1817
1818}
1819
1820/* vim: set sts=4 ts=4 expandtab : */
1821