Thomas Bruederli
2012-11-25 9be2f43d02cd54340fc64c9b2cee9cfa94b49034
commit | author | age
cc97ea 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_plugin_api.php                                  |
6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
3a7dec 8  | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
7fe381 9  |                                                                       |
T 10  | Licensed under the GNU General Public License version 3 or            |
11  | any later version with exceptions for skins & plugins.                |
12  | See the README file for a full license statement.                     |
cc97ea 13  |                                                                       |
T 14  | PURPOSE:                                                              |
15  |   Plugins repository                                                  |
16  |                                                                       |
17  +-----------------------------------------------------------------------+
18  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
19  +-----------------------------------------------------------------------+
20 */
21
0c2596 22 // location where plugins are loade from
9be2f4 23 if (!defined('RCUBE_PLUGINS_DIR'))
TB 24   define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
0c2596 25
A 26
cc97ea 27 /**
T 28  * The plugin loader and global API
29  *
9ab346 30  * @package    Framework
AM 31  * @subpackage PluginAPI
cc97ea 32  */
T 33 class rcube_plugin_api
34 {
53f4b9 35   static protected $instance;
413df0 36
cc97ea 37   public $dir;
T 38   public $url = 'plugins/';
0c2596 39   public $task = '';
cc97ea 40   public $output;
413df0 41
cc97ea 42   public $handlers = array();
53f4b9 43   protected $plugins = array();
AM 44   protected $tasks = array();
45   protected $actions = array();
46   protected $actionmap = array();
47   protected $objectsmap = array();
48   protected $template_contents = array();
49   protected $active_hook = false;
cc97ea 50
e6ce00 51   // Deprecated names of hooks, will be removed after 0.5-stable release
53f4b9 52   protected $deprecated_hooks = array(
e6ce00 53     'create_user'       => 'user_create',
A 54     'kill_session'      => 'session_destroy',
55     'upload_attachment' => 'attachment_upload',
56     'save_attachment'   => 'attachment_save',
57     'get_attachment'    => 'attachment_get',
58     'cleanup_attachments' => 'attachments_cleanup',
59     'display_attachment' => 'attachment_display',
60     'remove_attachment' => 'attachment_delete',
61     'outgoing_message_headers' => 'message_outgoing_headers',
62     'outgoing_message_body' => 'message_outgoing_body',
63     'address_sources'   => 'addressbooks_list',
64     'get_address_book'  => 'addressbook_get',
65     'create_contact'    => 'contact_create',
119ad1 66     'save_contact'      => 'contact_update',
A 67     'contact_save'      => 'contact_update',
e6ce00 68     'delete_contact'    => 'contact_delete',
A 69     'manage_folders'    => 'folders_list',
70     'list_mailboxes'    => 'mailboxes_list',
71     'save_preferences'  => 'preferences_save',
72     'user_preferences'  => 'preferences_list',
73     'list_prefs_sections' => 'preferences_sections_list',
74     'list_identities'   => 'identities_list',
75     'create_identity'   => 'identity_create',
119ad1 76     'delete_identity'   => 'identity_delete',
A 77     'save_identity'     => 'identity_update',
78     'identity_save'     => 'identity_update',
b3ad48 79     // to be removed after 0.8
a2c495 80     'imap_init'         => 'storage_init',
b3ad48 81     'mailboxes_list'    => 'storage_folders', 
e6ce00 82   );
A 83
cc97ea 84   /**
T 85    * This implements the 'singleton' design pattern
86    *
5c461b 87    * @return rcube_plugin_api The one and only instance if this class
cc97ea 88    */
T 89   static function get_instance()
90   {
91     if (!self::$instance) {
92       self::$instance = new rcube_plugin_api();
93     }
94
95     return self::$instance;
96   }
8ec1b9 97
A 98
cc97ea 99   /**
T 100    * Private constructor
101    */
53f4b9 102   protected function __construct()
cc97ea 103   {
9be2f4 104     $this->dir = slashify(RCUBE_PLUGINS_DIR);
0c2596 105   }
A 106
107
108   /**
109    * Initialize plugin engine
110    *
111    * This has to be done after rcmail::load_gui() or rcmail::json_init()
112    * was called because plugins need to have access to rcmail->output
113    *
114    * @param object rcube Instance of the rcube base class
115    * @param string Current application task (used for conditional plugin loading)
116    */
117   public function init($app, $task = '')
118   {
119     $this->task = $task;
120     $this->output = $app->output;
121
122     // register an internal hook
123     $this->register_hook('template_container', array($this, 'template_container_hook'));
124
125     // maybe also register a shudown function which triggers shutdown functions of all plugin objects
cc97ea 126   }
8ec1b9 127
A 128
cc97ea 129   /**
T 130    * Load and init all enabled plugins
131    *
929a50 132    * This has to be done after rcmail::load_gui() or rcmail::json_init()
cc97ea 133    * was called because plugins need to have access to rcmail->output
0c2596 134    *
A 135    * @param array List of configured plugins to load
136    * @param array List of plugins required by the application
cc97ea 137    */
0c2596 138   public function load_plugins($plugins_enabled, $required_plugins = array())
cc97ea 139   {
T 140     foreach ($plugins_enabled as $plugin_name) {
0501b6 141       $this->load_plugin($plugin_name);
cc97ea 142     }
8ec1b9 143
cc97ea 144     // check existance of all required core plugins
0c2596 145     foreach ($required_plugins as $plugin_name) {
cc97ea 146       $loaded = false;
T 147       foreach ($this->plugins as $plugin) {
148         if ($plugin instanceof $plugin_name) {
149           $loaded = true;
150           break;
151         }
152       }
8ec1b9 153
cc97ea 154       // load required core plugin if no derivate was found
0501b6 155       if (!$loaded)
T 156         $loaded = $this->load_plugin($plugin_name);
d5d968 157
cc97ea 158       // trigger fatal error if still not loaded
T 159       if (!$loaded) {
0c2596 160         rcube::raise_error(array('code' => 520, 'type' => 'php',
0501b6 161           'file' => __FILE__, 'line' => __LINE__,
T 162           'message' => "Requried plugin $plugin_name was not loaded"), true, true);
cc97ea 163       }
T 164     }
165   }
0501b6 166
T 167   /**
168    * Load the specified plugin
169    *
170    * @param string Plugin name
171    * @return boolean True on success, false if not loaded or failure
172    */
173   public function load_plugin($plugin_name)
174   {
175     static $plugins_dir;
8ec1b9 176
0501b6 177     if (!$plugins_dir) {
T 178       $dir = dir($this->dir);
179       $plugins_dir = unslashify($dir->path);
180     }
8ec1b9 181
0501b6 182     // plugin already loaded
T 183     if ($this->plugins[$plugin_name] || class_exists($plugin_name, false))
184       return true;
8ec1b9 185
0501b6 186     $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
T 187
188     if (file_exists($fn)) {
189       include($fn);
190
191       // instantiate class if exists
192       if (class_exists($plugin_name, false)) {
193         $plugin = new $plugin_name($this);
194         // check inheritance...
195         if (is_subclass_of($plugin, 'rcube_plugin')) {
196           // ... task, request type and framed mode
0c2596 197           if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
A 198               && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html'))
8ec1b9 199               && (!$plugin->noframe || empty($_REQUEST['_framed']))
0501b6 200           ) {
T 201             $plugin->init();
202             $this->plugins[$plugin_name] = $plugin;
203           }
204           return true;
205         }
206       }
207       else {
0c2596 208         rcube::raise_error(array('code' => 520, 'type' => 'php',
0501b6 209           'file' => __FILE__, 'line' => __LINE__,
T 210           'message' => "No plugin class $plugin_name found in $fn"), true, false);
211       }
212     }
213     else {
0c2596 214       rcube::raise_error(array('code' => 520, 'type' => 'php',
0501b6 215         'file' => __FILE__, 'line' => __LINE__,
T 216         'message' => "Failed to load plugin file $fn"), true, false);
217     }
8ec1b9 218
0501b6 219     return false;
T 220   }
8ec1b9 221
A 222
cc97ea 223   /**
T 224    * Allows a plugin object to register a callback for a certain hook
225    *
5c461b 226    * @param string $hook Hook name
A 227    * @param mixed  $callback String with global function name or array($obj, 'methodname')
cc97ea 228    */
T 229   public function register_hook($hook, $callback)
230   {
e6ce00 231     if (is_callable($callback)) {
A 232       if (isset($this->deprecated_hooks[$hook])) {
0c2596 233         rcube::raise_error(array('code' => 522, 'type' => 'php',
e6ce00 234           'file' => __FILE__, 'line' => __LINE__,
A 235           'message' => "Deprecated hook name. ".$hook.' -> '.$this->deprecated_hooks[$hook]), true, false);
236         $hook = $this->deprecated_hooks[$hook];
237       }
cc97ea 238       $this->handlers[$hook][] = $callback;
e6ce00 239     }
cc97ea 240     else
0c2596 241       rcube::raise_error(array('code' => 521, 'type' => 'php',
10eedb 242         'file' => __FILE__, 'line' => __LINE__,
A 243         'message' => "Invalid callback function for $hook"), true, false);
cc97ea 244   }
8ec1b9 245
479af9 246   /**
A 247    * Allow a plugin object to unregister a callback.
248    *
249    * @param string $hook Hook name
250    * @param mixed  $callback String with global function name or array($obj, 'methodname')
251    */
252   public function unregister_hook($hook, $callback)
253   {
254     $callback_id = array_search($callback, $this->handlers[$hook]);
255     if ($callback_id !== false) {
256       unset($this->handlers[$hook][$callback_id]);
257     }
258   }
259
8ec1b9 260
cc97ea 261   /**
T 262    * Triggers a plugin hook.
263    * This is called from the application and executes all registered handlers
264    *
5c461b 265    * @param string $hook Hook name
A 266    * @param array $args Named arguments (key->value pairs)
cc97ea 267    * @return array The (probably) altered hook arguments
T 268    */
269   public function exec_hook($hook, $args = array())
270   {
463a03 271     if (!is_array($args))
A 272       $args = array('arg' => $args);
273
cc97ea 274     $args += array('abort' => false);
a366a3 275     $this->active_hook = $hook;
8ec1b9 276
cc97ea 277     foreach ((array)$this->handlers[$hook] as $callback) {
T 278       $ret = call_user_func($callback, $args);
279       if ($ret && is_array($ret))
280         $args = $ret + $args;
8ec1b9 281
cc97ea 282       if ($args['abort'])
T 283         break;
284     }
8ec1b9 285
a366a3 286     $this->active_hook = false;
cc97ea 287     return $args;
T 288   }
289
290
291   /**
292    * Let a plugin register a handler for a specific request
293    *
5c461b 294    * @param string $action Action name (_task=mail&_action=plugin.foo)
A 295    * @param string $owner Plugin name that registers this action
296    * @param mixed  $callback Callback: string with global function name or array($obj, 'methodname')
297    * @param string $task Task name registered by this plugin
cc97ea 298    */
05a631 299   public function register_action($action, $owner, $callback, $task = null)
cc97ea 300   {
T 301     // check action name
05a631 302     if ($task)
T 303       $action = $task.'.'.$action;
304     else if (strpos($action, 'plugin.') !== 0)
cc97ea 305       $action = 'plugin.'.$action;
2cd443 306
cc97ea 307     // can register action only if it's not taken or registered by myself
T 308     if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) {
309       $this->actions[$action] = $callback;
310       $this->actionmap[$action] = $owner;
311     }
312     else {
0c2596 313       rcube::raise_error(array('code' => 523, 'type' => 'php',
10eedb 314         'file' => __FILE__, 'line' => __LINE__,
A 315         'message' => "Cannot register action $action; already taken by another plugin"), true, false);
cc97ea 316     }
T 317   }
318
319
320   /**
321    * This method handles requests like _task=mail&_action=plugin.foo
322    * It executes the callback function that was registered with the given action.
323    *
5c461b 324    * @param string $action Action name
cc97ea 325    */
T 326   public function exec_action($action)
327   {
328     if (isset($this->actions[$action])) {
329       call_user_func($this->actions[$action]);
330     }
92eb10 331     else if (rcube::get_instance()->action != 'refresh') {
0c2596 332       rcube::raise_error(array('code' => 524, 'type' => 'php',
10eedb 333         'file' => __FILE__, 'line' => __LINE__,
A 334         'message' => "No handler found for action $action"), true, true);
cc97ea 335     }
T 336   }
337
338
339   /**
340    * Register a handler function for template objects
341    *
5c461b 342    * @param string $name Object name
A 343    * @param string $owner Plugin name that registers this action
344    * @param mixed  $callback Callback: string with global function name or array($obj, 'methodname')
cc97ea 345    */
T 346   public function register_handler($name, $owner, $callback)
347   {
348     // check name
349     if (strpos($name, 'plugin.') !== 0)
350       $name = 'plugin.'.$name;
8ec1b9 351
cc97ea 352     // can register handler only if it's not taken or registered by myself
0c2596 353     if (is_object($this->output) && (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)) {
cc97ea 354       $this->output->add_handler($name, $callback);
T 355       $this->objectsmap[$name] = $owner;
356     }
357     else {
0c2596 358       rcube::raise_error(array('code' => 525, 'type' => 'php',
10eedb 359         'file' => __FILE__, 'line' => __LINE__,
0c2596 360         'message' => "Cannot register template handler $name; already taken by another plugin or no output object available"), true, false);
cc97ea 361     }
T 362   }
8ec1b9 363
A 364
a366a3 365   /**
05a631 366    * Register this plugin to be responsible for a specific task
T 367    *
5c461b 368    * @param string $task Task name (only characters [a-z0-9_.-] are allowed)
A 369    * @param string $owner Plugin name that registers this action
05a631 370    */
T 371   public function register_task($task, $owner)
372   {
373     if ($task != asciiwords($task)) {
0c2596 374       rcube::raise_error(array('code' => 526, 'type' => 'php',
05a631 375         'file' => __FILE__, 'line' => __LINE__,
T 376         'message' => "Invalid task name: $task. Only characters [a-z0-9_.-] are allowed"), true, false);
377     }
378     else if (in_array($task, rcmail::$main_tasks)) {
0c2596 379       rcube::raise_error(array('code' => 526, 'type' => 'php',
05a631 380         'file' => __FILE__, 'line' => __LINE__,
T 381         'message' => "Cannot register taks $task; already taken by another plugin or the application itself"), true, false);
382     }
383     else {
384       $this->tasks[$task] = $owner;
385       rcmail::$main_tasks[] = $task;
386       return true;
387     }
8ec1b9 388
05a631 389     return false;
T 390   }
391
392
393   /**
394    * Checks whether the given task is registered by a plugin
395    *
5c461b 396    * @param string $task Task name
05a631 397    * @return boolean True if registered, otherwise false
T 398    */
399   public function is_plugin_task($task)
400   {
401     return $this->tasks[$task] ? true : false;
402   }
403
404
405   /**
a366a3 406    * Check if a plugin hook is currently processing.
T 407    * Mainly used to prevent loops and recursion.
408    *
5c461b 409    * @param string $hook Hook to check (optional)
a366a3 410    * @return boolean True if any/the given hook is currently processed, otherwise false
T 411    */
412   public function is_processing($hook = null)
413   {
414     return $this->active_hook && (!$hook || $this->active_hook == $hook);
415   }
8ec1b9 416
cc97ea 417   /**
T 418    * Include a plugin script file in the current HTML page
5c461b 419    *
A 420    * @param string $fn Path to script
cc97ea 421    */
T 422   public function include_script($fn)
423   {
0c2596 424     if (is_object($this->output) && $this->output->type == 'html') {
eb6f19 425       $src = $this->resource_url($fn);
cc97ea 426       $this->output->add_header(html::tag('script', array('type' => "text/javascript", 'src' => $src)));
T 427     }
428   }
429
8ec1b9 430
cc97ea 431   /**
T 432    * Include a plugin stylesheet in the current HTML page
5c461b 433    *
A 434    * @param string $fn Path to stylesheet
cc97ea 435    */
T 436   public function include_stylesheet($fn)
437   {
0c2596 438     if (is_object($this->output) && $this->output->type == 'html') {
eb6f19 439       $src = $this->resource_url($fn);
ba3377 440       $this->output->include_css($src);
cc97ea 441     }
T 442   }
8ec1b9 443
A 444
cc97ea 445   /**
T 446    * Save the given HTML content to be added to a template container
5c461b 447    *
A 448    * @param string $html HTML content
449    * @param string $container Template container identifier
cc97ea 450    */
T 451   public function add_content($html, $container)
452   {
453     $this->template_contents[$container] .= $html . "\n";
454   }
8ec1b9 455
A 456
cc97ea 457   /**
8703b0 458    * Returns list of loaded plugins names
A 459    *
460    * @return array List of plugin names
461    */
462   public function loaded_plugins()
463   {
464     return array_keys($this->plugins);
465   }
466
467
468   /**
cc97ea 469    * Callback for template_container hooks
5c461b 470    *
A 471    * @param array $attrib
472    * @return array
cc97ea 473    */
53f4b9 474   protected function template_container_hook($attrib)
cc97ea 475   {
T 476     $container = $attrib['name'];
8448fc 477     return array('content' => $attrib['content'] . $this->template_contents[$container]);
cc97ea 478   }
8ec1b9 479
A 480
cc97ea 481   /**
T 482    * Make the given file name link into the plugins directory
5c461b 483    *
A 484    * @param string $fn Filename
485    * @return string 
cc97ea 486    */
53f4b9 487   protected function resource_url($fn)
cc97ea 488   {
23a2ee 489     if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
cc97ea 490       return $this->url . $fn;
T 491     else
492       return $fn;
493   }
494
495 }