Aleksander Machniak
2014-10-28 d93019125cca783e8acfaa68467024375321e55f
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;
70c7df 38     public $handlers              = array();
AM 39     public $allowed_prefs         = array();
40     public $allowed_session_prefs = array();
65baa0 41     public $active_plugins        = array();
413df0 42
c47813 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();
64d49e 49     protected $exec_stack = array();
cc97ea 50
c47813 51     // Deprecated names of hooks, will be removed after 0.5-stable release
AM 52     protected $deprecated_hooks = array(
53         'create_user'       => 'user_create',
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',
66         'save_contact'      => 'contact_update',
67         'contact_save'      => 'contact_update',
68         'delete_contact'    => 'contact_delete',
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',
76         'delete_identity'   => 'identity_delete',
77         'save_identity'     => 'identity_update',
78         'identity_save'     => 'identity_update',
79         // to be removed after 0.8
80         'imap_init'         => 'storage_init',
81         'mailboxes_list'    => 'storage_folders',
82         'imap_connect'      => 'storage_connect',
83     );
e6ce00 84
c47813 85     /**
AM 86      * This implements the 'singleton' design pattern
87      *
88      * @return rcube_plugin_api The one and only instance if this class
89      */
90     static function get_instance()
91     {
92         if (!self::$instance) {
93             self::$instance = new rcube_plugin_api();
cc97ea 94         }
8ec1b9 95
c47813 96         return self::$instance;
0501b6 97     }
8ec1b9 98
c47813 99     /**
AM 100      * Private constructor
101      */
102     protected function __construct()
103     {
104         $this->dir = slashify(RCUBE_PLUGINS_DIR);
105     }
8ec1b9 106
c47813 107     /**
AM 108      * Initialize plugin engine
109      *
110      * This has to be done after rcmail::load_gui() or rcmail::json_init()
111      * was called because plugins need to have access to rcmail->output
112      *
113      * @param object rcube Instance of the rcube base class
114      * @param string Current application task (used for conditional plugin loading)
115      */
116     public function init($app, $task = '')
117     {
118         $this->task   = $task;
119         $this->output = $app->output;
0501b6 120
c47813 121         // register an internal hook
AM 122         $this->register_hook('template_container', array($this, 'template_container_hook'));
0501b6 123
c47813 124         // maybe also register a shudown function which triggers
AM 125         // shutdown functions of all plugin objects
126     }
127
128     /**
129      * Load and init all enabled plugins
130      *
131      * This has to be done after rcmail::load_gui() or rcmail::json_init()
132      * was called because plugins need to have access to rcmail->output
133      *
134      * @param array List of configured plugins to load
135      * @param array List of plugins required by the application
136      */
137     public function load_plugins($plugins_enabled, $required_plugins = array())
138     {
139         foreach ($plugins_enabled as $plugin_name) {
140             $this->load_plugin($plugin_name);
0501b6 141         }
c47813 142
AM 143         // check existance of all required core plugins
144         foreach ($required_plugins as $plugin_name) {
145             $loaded = false;
146             foreach ($this->plugins as $plugin) {
147                 if ($plugin instanceof $plugin_name) {
148                     $loaded = true;
149                     break;
150                 }
151             }
152
153             // load required core plugin if no derivate was found
154             if (!$loaded) {
155                 $loaded = $this->load_plugin($plugin_name);
156             }
157
158             // trigger fatal error if still not loaded
159             if (!$loaded) {
160                 rcube::raise_error(array(
161                     'code' => 520, 'type' => 'php',
162                     'file' => __FILE__, 'line' => __LINE__,
163                     'message' => "Requried plugin $plugin_name was not loaded"), true, true);
164             }
165         }
0501b6 166     }
8ec1b9 167
c47813 168     /**
AM 169      * Load the specified plugin
170      *
171      * @param string Plugin name
cf3195 172      * @param boolean Force loading of the plugin even if it doesn't match the filter
0b799b 173      * @param boolean Require loading of the plugin, error if it doesn't exist
c47813 174      *
AM 175      * @return boolean True on success, false if not loaded or failure
176      */
0b799b 177     public function load_plugin($plugin_name, $force = false, $require = true)
c47813 178     {
AM 179         static $plugins_dir;
8ec1b9 180
c47813 181         if (!$plugins_dir) {
AM 182             $dir         = dir($this->dir);
183             $plugins_dir = unslashify($dir->path);
184         }
8ec1b9 185
c47813 186         // plugin already loaded
509a6b 187         if ($this->plugins[$plugin_name]) {
c47813 188             return true;
AM 189         }
8ec1b9 190
29c24e 191         $fn = "$plugins_dir/$plugin_name/$plugin_name.php";
479af9 192
65baa0 193         if (is_readable($fn)) {
509a6b 194             if (!class_exists($plugin_name, false)) {
TB 195                 include $fn;
196             }
8ec1b9 197
c47813 198             // instantiate class if exists
AM 199             if (class_exists($plugin_name, false)) {
200                 $plugin = new $plugin_name($this);
65baa0 201                 $this->active_plugins[] = $plugin_name;
TB 202
c47813 203                 // check inheritance...
AM 204                 if (is_subclass_of($plugin, 'rcube_plugin')) {
205                     // ... task, request type and framed mode
fa635d 206                     if (($force || !$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
c47813 207                         && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html'))
AM 208                         && (!$plugin->noframe || empty($_REQUEST['_framed']))
209                     ) {
210                         $plugin->init();
211                         $this->plugins[$plugin_name] = $plugin;
212                     }
648fcf 213
AM 214                     if (!empty($plugin->allowed_prefs)) {
215                         $this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs);
216                     }
217
c47813 218                     return true;
AM 219                 }
220             }
221             else {
222                 rcube::raise_error(array('code' => 520, 'type' => 'php',
223                     'file' => __FILE__, 'line' => __LINE__,
224                     'message' => "No plugin class $plugin_name found in $fn"),
225                     true, false);
226             }
227         }
0b799b 228         elseif ($require) {
c47813 229             rcube::raise_error(array('code' => 520, 'type' => 'php',
AM 230                 'file' => __FILE__, 'line' => __LINE__,
231                 'message' => "Failed to load plugin file $fn"), true, false);
232         }
463a03 233
c47813 234         return false;
cc97ea 235     }
8ec1b9 236
c47813 237     /**
378d6c 238      * Get information about a specific plugin.
TB 239      * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
240      *
241      * @param string Plugin name
242      * @return array Meta information about a plugin or False if plugin was not found
243      */
244     public function get_info($plugin_name)
245     {
246       static $composer_lock, $license_uris = array(
247         'Apache'     => 'http://www.apache.org/licenses/LICENSE-2.0.html',
248         'Apache-2'   => 'http://www.apache.org/licenses/LICENSE-2.0.html',
249         'Apache-1'   => 'http://www.apache.org/licenses/LICENSE-1.0',
250         'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1',
251         'GPL'        => 'http://www.gnu.org/licenses/gpl.html',
252         'GPLv2'      => 'http://www.gnu.org/licenses/gpl-2.0.html',
253         'GPL-2.0'    => 'http://www.gnu.org/licenses/gpl-2.0.html',
254         'GPLv3'      => 'http://www.gnu.org/licenses/gpl-3.0.html',
255         'GPL-3.0'    => 'http://www.gnu.org/licenses/gpl-3.0.html',
256         'GPL-3.0+'   => 'http://www.gnu.org/licenses/gpl.html',
257         'GPL-2.0+'   => 'http://www.gnu.org/licenses/gpl.html',
65baa0 258         'AGPLv3'     => 'http://www.gnu.org/licenses/agpl.html',
TB 259         'AGPLv3+'    => 'http://www.gnu.org/licenses/agpl.html',
260         'AGPL-3.0'   => 'http://www.gnu.org/licenses/agpl.html',
378d6c 261         'LGPL'       => 'http://www.gnu.org/licenses/lgpl.html',
TB 262         'LGPLv2'     => 'http://www.gnu.org/licenses/lgpl-2.0.html',
263         'LGPLv2.1'   => 'http://www.gnu.org/licenses/lgpl-2.1.html',
264         'LGPLv3'     => 'http://www.gnu.org/licenses/lgpl.html',
265         'LGPL-2.0'   => 'http://www.gnu.org/licenses/lgpl-2.0.html',
266         'LGPL-2.1'   => 'http://www.gnu.org/licenses/lgpl-2.1.html',
267         'LGPL-3.0'   => 'http://www.gnu.org/licenses/lgpl.html',
268         'LGPL-3.0+'  => 'http://www.gnu.org/licenses/lgpl.html',
269         'BSD'          => 'http://opensource.org/licenses/bsd-license.html',
270         'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause',
271         'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause',
272         'FreeBSD'      => 'http://opensource.org/licenses/BSD-2-Clause',
273         'MIT'          => 'http://www.opensource.org/licenses/mit-license.php',
274         'PHP'          => 'http://opensource.org/licenses/PHP-3.0',
275         'PHP-3'        => 'http://www.php.net/license/3_01.txt',
276         'PHP-3.0'      => 'http://www.php.net/license/3_0.txt',
277         'PHP-3.01'     => 'http://www.php.net/license/3_01.txt',
278       );
279
280       $dir = dir($this->dir);
29c24e 281       $fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
378d6c 282       $info = false;
TB 283
65baa0 284       if (!class_exists($plugin_name, false)) {
TB 285         if (is_readable($fn))
286           include($fn);
287         else
288           return false;
289       }
378d6c 290
TB 291       if (class_exists($plugin_name))
292         $info = $plugin_name::info();
293
294       // fall back to composer.json file
295       if (!$info) {
296         $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
65baa0 297         if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
378d6c 298           list($info['vendor'], $info['name']) = explode('/', $json['name']);
ce6050 299           $info['version'] = $json['version'];
378d6c 300           $info['license'] = $json['license'];
65baa0 301           $info['uri'] = $json['homepage'];
TB 302           $info['require'] = array_filter(array_keys((array)$json['require']), function($pname) {
303             if (strpos($pname, '/') == false)
304               return false;
305             list($vendor, $name) = explode('/', $pname);
306             return !($name == 'plugin-installer' || $vendor == 'pear-pear');
307           });
378d6c 308         }
TB 309
310         // read local composer.lock file (once)
311         if (!isset($composer_lock)) {
312           $composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true);
313           if ($composer_lock['packages']) {
314             foreach ($composer_lock['packages'] as $i => $package) {
315               $composer_lock['installed'][$package['name']] = $package;
316             }
317           }
318         }
319
320         // load additional information from local composer.lock file
321         if ($lock = $composer_lock['installed'][$json['name']]) {
322           $info['version'] = $lock['version'];
323           $info['uri']     = $lock['homepage'] ? $lock['homepage'] : $lock['source']['uri'];
324           $info['src_uri'] = $lock['dist']['uri'] ? $lock['dist']['uri'] : $lock['source']['uri'];
325         }
326       }
327
328       // fall back to package.xml file
329       if (!$info) {
330         $package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
65baa0 331         if (is_readable($package) && ($file = file_get_contents($package))) {
378d6c 332           $doc = new DOMDocument();
TB 333           $doc->loadXML($file);
334           $xpath = new DOMXPath($doc);
335           $xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0");
336
337           // XPaths of plugin metadata elements
338           $metadata = array(
339             'name'    => 'string(//rc:package/rc:name)',
340             'version' => 'string(//rc:package/rc:version/rc:release)',
341             'license' => 'string(//rc:package/rc:license)',
342             'license_uri' => 'string(//rc:package/rc:license/@uri)',
343             'src_uri' => 'string(//rc:package/rc:srcuri)',
344             'uri' => 'string(//rc:package/rc:uri)',
345           );
346
347           foreach ($metadata as $key => $path) {
348             $info[$key] = $xpath->evaluate($path);
349           }
350
351           // dependent required plugins (can be used, but not included in config)
352           $deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
353           for ($i = 0; $i < $deps->length; $i++) {
354             $dn = $deps->item($i)->nodeValue;
65baa0 355             $info['require'][] = $dn;
378d6c 356           }
TB 357         }
358       }
359
65baa0 360       // At least provide the name
TB 361       if (!$info && class_exists($plugin_name)) {
362         $info = array('name' => $plugin_name, 'version' => '--');
363       }
364       else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) {
365         $info['license_uri'] = $license_uri;
366       }
367
378d6c 368       return $info;
TB 369     }
370
371     /**
c47813 372      * Allows a plugin object to register a callback for a certain hook
AM 373      *
374      * @param string $hook Hook name
375      * @param mixed  $callback String with global function name or array($obj, 'methodname')
376      */
377     public function register_hook($hook, $callback)
378     {
379         if (is_callable($callback)) {
380             if (isset($this->deprecated_hooks[$hook])) {
381                 rcube::raise_error(array('code' => 522, 'type' => 'php',
382                     'file' => __FILE__, 'line' => __LINE__,
383                     'message' => "Deprecated hook name. "
384                         . $hook . ' -> ' . $this->deprecated_hooks[$hook]), true, false);
385                 $hook = $this->deprecated_hooks[$hook];
386             }
387             $this->handlers[$hook][] = $callback;
388         }
389         else {
390             rcube::raise_error(array('code' => 521, 'type' => 'php',
391                 'file' => __FILE__, 'line' => __LINE__,
392                 'message' => "Invalid callback function for $hook"), true, false);
393         }
05a631 394     }
8ec1b9 395
c47813 396     /**
AM 397      * Allow a plugin object to unregister a callback.
398      *
399      * @param string $hook Hook name
400      * @param mixed  $callback String with global function name or array($obj, 'methodname')
401      */
402     public function unregister_hook($hook, $callback)
403     {
404         $callback_id = array_search($callback, $this->handlers[$hook]);
405         if ($callback_id !== false) {
406             unset($this->handlers[$hook][$callback_id]);
407         }
cc97ea 408     }
T 409
c47813 410     /**
AM 411      * Triggers a plugin hook.
412      * This is called from the application and executes all registered handlers
413      *
414      * @param string $hook Hook name
415      * @param array $args Named arguments (key->value pairs)
416      *
417      * @return array The (probably) altered hook arguments
418      */
419     public function exec_hook($hook, $args = array())
420     {
421         if (!is_array($args)) {
422             $args = array('arg' => $args);
423         }
8ec1b9 424
64d49e 425         // TODO: avoid recusion by checking in_array($hook, $this->exec_stack) ?
TB 426
c47813 427         $args += array('abort' => false);
64d49e 428         array_push($this->exec_stack, $hook);
c47813 429
AM 430         foreach ((array)$this->handlers[$hook] as $callback) {
431             $ret = call_user_func($callback, $args);
432             if ($ret && is_array($ret)) {
433                 $args = $ret + $args;
434             }
435
c1bc8f 436             if ($args['break']) {
c47813 437                 break;
AM 438             }
439         }
440
64d49e 441         array_pop($this->exec_stack);
c47813 442         return $args;
cc97ea 443     }
8ec1b9 444
c47813 445     /**
AM 446      * Let a plugin register a handler for a specific request
447      *
448      * @param string $action   Action name (_task=mail&_action=plugin.foo)
449      * @param string $owner    Plugin name that registers this action
450      * @param mixed  $callback Callback: string with global function name or array($obj, 'methodname')
451      * @param string $task     Task name registered by this plugin
452      */
453     public function register_action($action, $owner, $callback, $task = null)
454     {
455         // check action name
456         if ($task)
457             $action = $task.'.'.$action;
458         else if (strpos($action, 'plugin.') !== 0)
459             $action = 'plugin.'.$action;
8ec1b9 460
c47813 461         // can register action only if it's not taken or registered by myself
AM 462         if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) {
463             $this->actions[$action] = $callback;
464             $this->actionmap[$action] = $owner;
465         }
466         else {
467             rcube::raise_error(array('code' => 523, 'type' => 'php',
468                 'file' => __FILE__, 'line' => __LINE__,
469                 'message' => "Cannot register action $action;"
470                     ." already taken by another plugin"), true, false);
471         }
472     }
8ec1b9 473
c47813 474     /**
AM 475      * This method handles requests like _task=mail&_action=plugin.foo
476      * It executes the callback function that was registered with the given action.
477      *
478      * @param string $action Action name
479      */
480     public function exec_action($action)
481     {
482         if (isset($this->actions[$action])) {
483             call_user_func($this->actions[$action]);
484         }
485         else if (rcube::get_instance()->action != 'refresh') {
486             rcube::raise_error(array('code' => 524, 'type' => 'php',
487                 'file' => __FILE__, 'line' => __LINE__,
488                 'message' => "No handler found for action $action"), true, true);
489         }
490     }
8ec1b9 491
c47813 492     /**
AM 493      * Register a handler function for template objects
494      *
495      * @param string $name Object name
496      * @param string $owner Plugin name that registers this action
497      * @param mixed  $callback Callback: string with global function name or array($obj, 'methodname')
498      */
499     public function register_handler($name, $owner, $callback)
500     {
501         // check name
502         if (strpos($name, 'plugin.') !== 0) {
503             $name = 'plugin.' . $name;
504         }
8703b0 505
c47813 506         // can register handler only if it's not taken or registered by myself
AM 507         if (is_object($this->output)
508             && (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)
509         ) {
510             $this->output->add_handler($name, $callback);
511             $this->objectsmap[$name] = $owner;
512         }
513         else {
514             rcube::raise_error(array('code' => 525, 'type' => 'php',
515                 'file' => __FILE__, 'line' => __LINE__,
516                 'message' => "Cannot register template handler $name;"
517                     ." already taken by another plugin or no output object available"), true, false);
518         }
519     }
8703b0 520
c47813 521     /**
AM 522      * Register this plugin to be responsible for a specific task
523      *
8b7716 524      * @param string $task Task name (only characters [a-z0-9_-] are allowed)
c47813 525      * @param string $owner Plugin name that registers this action
AM 526      */
527     public function register_task($task, $owner)
528     {
529         // tasks are irrelevant in framework mode
530         if (!class_exists('rcmail', false)) {
531             return true;
532         }
8ec1b9 533
8b7716 534         if ($task != asciiwords($task, true)) {
c47813 535             rcube::raise_error(array('code' => 526, 'type' => 'php',
AM 536                 'file' => __FILE__, 'line' => __LINE__,
537                 'message' => "Invalid task name: $task."
538                     ." Only characters [a-z0-9_.-] are allowed"), true, false);
539         }
540         else if (in_array($task, rcmail::$main_tasks)) {
541             rcube::raise_error(array('code' => 526, 'type' => 'php',
542                 'file' => __FILE__, 'line' => __LINE__,
543                 'message' => "Cannot register taks $task;"
544                     ." already taken by another plugin or the application itself"), true, false);
545         }
546         else {
547             $this->tasks[$task] = $owner;
548             rcmail::$main_tasks[] = $task;
549             return true;
550         }
8ec1b9 551
c47813 552         return false;
AM 553     }
cc97ea 554
c47813 555     /**
AM 556      * Checks whether the given task is registered by a plugin
557      *
558      * @param string $task Task name
559      *
560      * @return boolean True if registered, otherwise false
561      */
562     public function is_plugin_task($task)
563     {
564         return $this->tasks[$task] ? true : false;
565     }
566
567     /**
568      * Check if a plugin hook is currently processing.
569      * Mainly used to prevent loops and recursion.
570      *
571      * @param string $hook Hook to check (optional)
572      *
573      * @return boolean True if any/the given hook is currently processed, otherwise false
574      */
575     public function is_processing($hook = null)
576     {
64d49e 577         return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack));
c47813 578     }
AM 579
580     /**
581      * Include a plugin script file in the current HTML page
582      *
583      * @param string $fn Path to script
584      */
585     public function include_script($fn)
586     {
587         if (is_object($this->output) && $this->output->type == 'html') {
588             $src = $this->resource_url($fn);
589             $this->output->add_header(html::tag('script',
590                 array('type' => "text/javascript", 'src' => $src)));
591         }
592     }
593
594     /**
595      * Include a plugin stylesheet in the current HTML page
596      *
597      * @param string $fn Path to stylesheet
598      */
599     public function include_stylesheet($fn)
600     {
601         if (is_object($this->output) && $this->output->type == 'html') {
602             $src = $this->resource_url($fn);
603             $this->output->include_css($src);
604         }
605     }
606
607     /**
608      * Save the given HTML content to be added to a template container
609      *
610      * @param string $html HTML content
611      * @param string $container Template container identifier
612      */
613     public function add_content($html, $container)
614     {
615         $this->template_contents[$container] .= $html . "\n";
616     }
617
618     /**
619      * Returns list of loaded plugins names
620      *
621      * @return array List of plugin names
622      */
623     public function loaded_plugins()
624     {
625         return array_keys($this->plugins);
626     }
627
628     /**
a1d042 629      * Returns loaded plugin
AM 630      *
631      * @return rcube_plugin Plugin instance
632      */
633     public function get_plugin($name)
634     {
635         return $this->plugins[$name];
636     }
637
638     /**
c47813 639      * Callback for template_container hooks
AM 640      *
641      * @param array $attrib
642      * @return array
643      */
644     protected function template_container_hook($attrib)
645     {
646         $container = $attrib['name'];
647         return array('content' => $attrib['content'] . $this->template_contents[$container]);
648     }
649
650     /**
651      * Make the given file name link into the plugins directory
652      *
653      * @param string $fn Filename
654      * @return string
655      */
656     protected function resource_url($fn)
657     {
658         if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
659             return $this->url . $fn;
660         else
661             return $fn;
662     }
cc97ea 663 }