1<?php
2
3/**
4 +-----------------------------------------------------------------------+
5 | This file is part of the Roundcube Webmail client                     |
6 |                                                                       |
7 | Copyright (C) The Roundcube Dev Team                                  |
8 |                                                                       |
9 | Licensed under the GNU General Public License version 3 or            |
10 | any later version with exceptions for skins & plugins.                |
11 | See the README file for a full license statement.                     |
12 |                                                                       |
13 | PURPOSE:                                                              |
14 |  Abstract plugins interface/class                                     |
15 |  All plugins need to extend this class                                |
16 +-----------------------------------------------------------------------+
17 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18 +-----------------------------------------------------------------------+
19*/
20
21/**
22 * Plugin interface class
23 *
24 * @package    Framework
25 * @subpackage PluginAPI
26 */
27abstract class rcube_plugin
28{
29    /**
30     * Class name of the plugin instance
31     *
32     * @var string
33     */
34    public $ID;
35
36    /**
37     * Instance of Plugin API
38     *
39     * @var rcube_plugin_api
40     */
41    public $api;
42
43    /**
44     * Regular expression defining task(s) to bind with
45     *
46     * @var string
47     */
48    public $task;
49
50    /**
51     * Disables plugin in AJAX requests
52     *
53     * @var boolean
54     */
55    public $noajax = false;
56
57    /**
58     * Disables plugin in framed mode
59     *
60     * @var boolean
61     */
62    public $noframe = false;
63
64    /**
65     * A list of config option names that can be modified
66     * by the user via user interface (with save-prefs command)
67     *
68     * @var array
69     */
70    public $allowed_prefs;
71
72    /** @var string Plugin directory location */
73    protected $home;
74
75    /** @var string Base URL to the plugin directory */
76    protected $urlbase;
77
78    /** @var string Plugin task name (if registered) */
79    private $mytask;
80
81    /** @var array List of plugin configuration files already loaded */
82    private $loaded_config = [];
83
84
85    /**
86     * Default constructor.
87     *
88     * @param rcube_plugin_api $api Plugin API
89     */
90    public function __construct($api)
91    {
92        $this->ID      = get_class($this);
93        $this->api     = $api;
94        $this->home    = $api->dir . $this->ID;
95        $this->urlbase = $api->url . $this->ID . '/';
96    }
97
98    /**
99     * Initialization method, needs to be implemented by the plugin itself
100     */
101    abstract function init();
102
103    /**
104     * Provide information about this
105     *
106     * @return array Meta information about a plugin or false if not implemented.
107     * As hash array with the following keys:
108     *      name: The plugin name
109     *    vendor: Name of the plugin developer
110     *   version: Plugin version name
111     *   license: License name (short form according to http://spdx.org/licenses/)
112     *       uri: The URL to the plugin homepage or source repository
113     *   src_uri: Direct download URL to the source code of this plugin
114     *   require: List of plugins required for this one (as array of plugin names)
115     */
116    public static function info()
117    {
118        return false;
119    }
120
121    /**
122     * Attempt to load the given plugin which is required for the current plugin
123     *
124     * @param string Plugin name
125     *
126     * @return bool True on success, false on failure
127     */
128    public function require_plugin($plugin_name)
129    {
130        return $this->api->load_plugin($plugin_name, true);
131    }
132
133    /**
134     * Attempt to load the given plugin which is optional for the current plugin
135     *
136     * @param string Plugin name
137     *
138     * @return bool True on success, false on failure
139     */
140    public function include_plugin($plugin_name)
141    {
142        return $this->api->load_plugin($plugin_name, true, false);
143    }
144
145    /**
146     * Load local config file from plugins directory.
147     * The loaded values are patched over the global configuration.
148     *
149     * @param string $fname Config file name relative to the plugin's folder
150     *
151     * @return bool True on success, false on failure
152     */
153    public function load_config($fname = 'config.inc.php')
154    {
155        if (in_array($fname, $this->loaded_config)) {
156            return true;
157        }
158
159        $this->loaded_config[] = $fname;
160
161        $fpath = slashify($this->home) . $fname;
162        $rcube = rcube::get_instance();
163
164        if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
165            rcube::raise_error([
166                    'code' => 527, 'file' => __FILE__, 'line' => __LINE__,
167                    'message' => "Failed to load config from $fpath"
168                ], true, false
169            );
170            return false;
171        }
172        else if (!$is_local) {
173            // Search plugin_name.inc.php file in any configured path
174            return $rcube->config->load_from_file($this->ID . '.inc.php');
175        }
176
177        return true;
178    }
179
180    /**
181     * Register a callback function for a specific (server-side) hook
182     *
183     * @param string $hook     Hook name
184     * @param mixed  $callback Callback function as string or array
185     *                         with object reference and method name
186     */
187    public function add_hook($hook, $callback)
188    {
189        $this->api->register_hook($hook, $callback);
190    }
191
192    /**
193     * Unregister a callback function for a specific (server-side) hook.
194     *
195     * @param string $hook     Hook name
196     * @param mixed  $callback Callback function as string or array
197     *                         with object reference and method name
198     */
199    public function remove_hook($hook, $callback)
200    {
201        $this->api->unregister_hook($hook, $callback);
202    }
203
204    /**
205     * Load localized texts from the plugins dir
206     *
207     * @param string $dir        Directory to search in
208     * @param mixed  $add2client Make texts also available on the client
209     *                           (array with list or true for all)
210     */
211    public function add_texts($dir, $add2client = false)
212    {
213        $rcube = rcube::get_instance();
214        $texts = $rcube->read_localization(realpath(slashify($this->home) . $dir));
215
216        // prepend domain to text keys and add to the application texts repository
217        if (!empty($texts)) {
218            $domain = $this->ID;
219            $add    = [];
220
221            foreach ($texts as $key => $value) {
222                $add[$domain.'.'.$key] = $value;
223            }
224
225            $rcube->load_language($_SESSION['language'], $add);
226
227            // add labels to client
228            if ($add2client && method_exists($rcube->output, 'add_label')) {
229                if (is_array($add2client)) {
230                    $js_labels = array_map([$this, 'label_map_callback'], $add2client);
231                }
232                else {
233                    $js_labels = array_keys($add);
234                }
235
236                $rcube->output->add_label($js_labels);
237            }
238        }
239    }
240
241    /**
242     * Wrapper for add_label() adding the plugin ID as domain
243     */
244    public function add_label()
245    {
246        $rcube = rcube::get_instance();
247
248        if (method_exists($rcube->output, 'add_label')) {
249            $args = func_get_args();
250            if (count($args) == 1 && is_array($args[0])) {
251                $args = $args[0];
252            }
253
254            $args = array_map([$this, 'label_map_callback'], $args);
255            $rcube->output->add_label($args);
256        }
257    }
258
259    /**
260     * Wrapper for rcube::gettext() adding the plugin ID as domain
261     *
262     * @param string|array $p Named parameters array or label name
263     *
264     * @return string Localized text
265     * @see rcube::gettext()
266     */
267    public function gettext($p)
268    {
269        return rcube::get_instance()->gettext($p, $this->ID);
270    }
271
272    /**
273     * Register this plugin to be responsible for a specific task
274     *
275     * @param string $task Task name (only characters [a-z0-9_-] are allowed)
276     */
277    public function register_task($task)
278    {
279        if ($this->api->register_task($task, $this->ID)) {
280            $this->mytask = $task;
281        }
282    }
283
284    /**
285     * Register a handler for a specific client-request action
286     *
287     * The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction
288     *
289     * @param string $action   Action name (should be unique)
290     * @param mixed  $callback Callback function as string
291     *                         or array with object reference and method name
292     */
293    public function register_action($action, $callback)
294    {
295        $this->api->register_action($action, $this->ID, $callback, $this->mytask);
296    }
297
298    /**
299     * Register a handler function for a template object
300     *
301     * When parsing a template for display, tags like <roundcube:object name="plugin.myobject" />
302     * will be replaced by the return value if the registered callback function.
303     *
304     * @param string $name     Object name (should be unique and start with 'plugin.')
305     * @param mixed  $callback Callback function as string or array with object reference
306     *                         and method name
307     */
308    public function register_handler($name, $callback)
309    {
310        $this->api->register_handler($name, $this->ID, $callback);
311    }
312
313    /**
314     * Make this javascript file available on the client
315     *
316     * @param string $fn File path; absolute or relative to the plugin directory
317     */
318    public function include_script($fn)
319    {
320        $this->api->include_script($this->resource_url($fn));
321    }
322
323    /**
324     * Make this stylesheet available on the client
325     *
326     * @param string $fn File path; absolute or relative to the plugin directory
327     */
328    public function include_stylesheet($fn)
329    {
330        $this->api->include_stylesheet($this->resource_url($fn));
331    }
332
333    /**
334     * Append a button to a certain container
335     *
336     * @param array  $p         Hash array with named parameters (as used in skin templates)
337     * @param string $container Container name where the buttons should be added to
338     *
339     * @see rcube_template::button()
340     */
341    public function add_button($p, $container)
342    {
343        if ($this->api->output->type == 'html') {
344            // fix relative paths
345            foreach (['imagepas', 'imageact', 'imagesel'] as $key) {
346                if (!empty($p[$key])) {
347                    $p[$key] = $this->api->url . $this->resource_url($p[$key]);
348                }
349            }
350
351            $this->api->add_content($this->api->output->button($p), $container);
352        }
353    }
354
355    /**
356     * Generate an absolute URL to the given resource within the current
357     * plugin directory
358     *
359     * @param string $fn The file name
360     *
361     * @return string Absolute URL to the given resource
362     */
363    public function url($fn)
364    {
365        return $this->api->url . $this->resource_url($fn);
366    }
367
368    /**
369     * Make the given file name link into the plugin directory
370     *
371     * @param string $fn Filename
372     */
373    private function resource_url($fn)
374    {
375        // pattern "skins/[a-z0-9-_]+/plugins/$this->ID/" used to identify plugin resources loaded from the core skin folder
376        if ($fn[0] != '/' && !preg_match("#^(https?://|skins/[a-z0-9-_]+/plugins/$this->ID/)#i", $fn)) {
377            return $this->ID . '/' . $fn;
378        }
379        else {
380            return $fn;
381        }
382    }
383
384    /**
385     * Provide path to the currently selected skin folder within the plugin directory
386     * with a fallback to the default skin folder.
387     *
388     * @param  string $extra_dir Additional directory to search in (optional)
389     * @param  mixed  $skin_name Specific skin name(s) to look for, string or array (optional)
390     * @return string            Skin path relative to plugins directory
391     */
392    public function local_skin_path($extra_dir = null, $skin_name = null)
393    {
394        $rcube     = rcube::get_instance();
395        $skins     = array_keys((array)$rcube->output->skins);
396        $skin_path = '';
397
398        if (empty($skins)) {
399            $skins = (array) $rcube->config->get('skin');
400        }
401
402        $dirs = ['skins'];
403        if (!empty($extra_dir)) {
404            array_unshift($dirs, $extra_dir);
405        }
406
407        if (!empty($skin_name)) {
408            $skins = (array) $skin_name;
409        }
410
411        foreach ($skins as $skin) {
412            foreach ($dirs as $dir) {
413                // skins folder in the plugins dir
414                $skin_path = $dir . '/' . $skin;
415
416                if (!is_dir(realpath(slashify($this->home) . $skin_path))) {
417                    // plugins folder in the skins dir
418                    $skin_path .= '/plugins/' . $this->ID;
419                    if (is_dir(realpath(slashify(RCUBE_INSTALL_PATH) . $skin_path))) {
420                        break 2;
421                    }
422                }
423                else {
424                    break 2;
425                }
426            }
427        }
428
429        return $skin_path;
430    }
431
432    /**
433     * Callback function for array_map
434     *
435     * @param string $key Array key.
436     *
437     * @return string
438     */
439    private function label_map_callback($key)
440    {
441        if (strpos($key, $this->ID.'.') === 0) {
442            return $key;
443        }
444
445        return $this->ID.'.'.$key;
446    }
447}
448