RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

494 lines
15 KiB

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