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&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