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