PageRenderTime 32ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/etc/apps/webmail/program/include/rcmail.php

https://github.com/raiman264/zpanelx
PHP | 2077 lines | 1420 code | 282 blank | 375 comment | 350 complexity | e9320e4ae695e98b2774adbfb7b9e23b MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, LGPL-2.1, CC-BY-SA-4.0, GPL-3.0
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcmail.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2008-2013, The Roundcube Dev Team |
  8. | Copyright (C) 2011-2013, Kolab Systems AG |
  9. | |
  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. |
  13. | |
  14. | PURPOSE: |
  15. | Application class providing core functions and holding |
  16. | instances of all 'global' objects like db- and imap-connections |
  17. +-----------------------------------------------------------------------+
  18. | Author: Thomas Bruederli <roundcube@gmail.com> |
  19. | Author: Aleksander Machniak <alec@alec.pl> |
  20. +-----------------------------------------------------------------------+
  21. */
  22. /**
  23. * Application class of Roundcube Webmail
  24. * implemented as singleton
  25. *
  26. * @package Core
  27. */
  28. class rcmail extends rcube
  29. {
  30. /**
  31. * Main tasks.
  32. *
  33. * @var array
  34. */
  35. static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
  36. /**
  37. * Current task.
  38. *
  39. * @var string
  40. */
  41. public $task;
  42. /**
  43. * Current action.
  44. *
  45. * @var string
  46. */
  47. public $action = '';
  48. public $comm_path = './';
  49. public $filename = '';
  50. private $address_books = array();
  51. private $action_map = array();
  52. const ERROR_STORAGE = -2;
  53. const ERROR_INVALID_REQUEST = 1;
  54. const ERROR_INVALID_HOST = 2;
  55. const ERROR_COOKIES_DISABLED = 3;
  56. /**
  57. * This implements the 'singleton' design pattern
  58. *
  59. * @param string Environment name to run (e.g. live, dev, test)
  60. *
  61. * @return rcmail The one and only instance
  62. */
  63. static function get_instance($env = '')
  64. {
  65. if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
  66. self::$instance = new rcmail($env);
  67. // init AFTER object was linked with self::$instance
  68. self::$instance->startup();
  69. }
  70. return self::$instance;
  71. }
  72. /**
  73. * Initial startup function
  74. * to register session, create database and imap connections
  75. */
  76. protected function startup()
  77. {
  78. $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
  79. // set filename if not index.php
  80. if (($basename = basename($_SERVER['SCRIPT_FILENAME'])) && $basename != 'index.php') {
  81. $this->filename = $basename;
  82. }
  83. // start session
  84. $this->session_init();
  85. // create user object
  86. $this->set_user(new rcube_user($_SESSION['user_id']));
  87. // set task and action properties
  88. $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
  89. $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
  90. // reset some session parameters when changing task
  91. if ($this->task != 'utils') {
  92. // we reset list page when switching to another task
  93. // but only to the main task interface - empty action (#1489076)
  94. // this will prevent from unintentional page reset on cross-task requests
  95. if ($this->session && $_SESSION['task'] != $this->task && empty($this->action)) {
  96. $this->session->remove('page');
  97. }
  98. // set current task to session
  99. $_SESSION['task'] = $this->task;
  100. }
  101. // init output class
  102. if (!empty($_REQUEST['_remote']))
  103. $GLOBALS['OUTPUT'] = $this->json_init();
  104. else
  105. $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
  106. // load plugins
  107. $this->plugins->init($this, $this->task);
  108. $this->plugins->load_plugins((array)$this->config->get('plugins', array()),
  109. array('filesystem_attachments', 'jqueryui'));
  110. }
  111. /**
  112. * Setter for application task
  113. *
  114. * @param string Task to set
  115. */
  116. public function set_task($task)
  117. {
  118. $task = asciiwords($task, true);
  119. if ($this->user && $this->user->ID)
  120. $task = !$task ? 'mail' : $task;
  121. else
  122. $task = 'login';
  123. $this->task = $task;
  124. $this->comm_path = $this->url(array('task' => $this->task));
  125. if ($this->output) {
  126. $this->output->set_env('task', $this->task);
  127. }
  128. }
  129. /**
  130. * Setter for system user object
  131. *
  132. * @param rcube_user Current user instance
  133. */
  134. public function set_user($user)
  135. {
  136. if (is_object($user)) {
  137. $this->user = $user;
  138. // overwrite config with user preferences
  139. $this->config->set_user_prefs((array)$this->user->get_prefs());
  140. }
  141. $lang = $this->language_prop($this->config->get('language', $_SESSION['language']));
  142. $_SESSION['language'] = $this->user->language = $lang;
  143. // set localization
  144. setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');
  145. // workaround for http://bugs.php.net/bug.php?id=18556
  146. if (version_compare(PHP_VERSION, '5.5.0', '<') && in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
  147. setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8');
  148. }
  149. }
  150. /**
  151. * Return instance of the internal address book class
  152. *
  153. * @param string Address book identifier (-1 for default addressbook)
  154. * @param boolean True if the address book needs to be writeable
  155. *
  156. * @return rcube_contacts Address book object
  157. */
  158. public function get_address_book($id, $writeable = false)
  159. {
  160. $contacts = null;
  161. $ldap_config = (array)$this->config->get('ldap_public');
  162. // 'sql' is the alias for '0' used by autocomplete
  163. if ($id == 'sql')
  164. $id = '0';
  165. else if ($id == -1) {
  166. $id = $this->config->get('default_addressbook');
  167. $default = true;
  168. }
  169. // use existing instance
  170. if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
  171. $contacts = $this->address_books[$id];
  172. }
  173. else if ($id && $ldap_config[$id]) {
  174. $domain = $this->config->mail_domain($_SESSION['storage_host']);
  175. $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $domain);
  176. }
  177. else if ($id === '0') {
  178. $contacts = new rcube_contacts($this->db, $this->get_user_id());
  179. }
  180. else {
  181. $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
  182. // plugin returned instance of a rcube_addressbook
  183. if ($plugin['instance'] instanceof rcube_addressbook) {
  184. $contacts = $plugin['instance'];
  185. }
  186. }
  187. // when user requested default writeable addressbook
  188. // we need to check if default is writeable, if not we
  189. // will return first writeable book (if any exist)
  190. if ($contacts && $default && $contacts->readonly && $writeable) {
  191. $contacts = null;
  192. }
  193. // Get first addressbook from the list if configured default doesn't exist
  194. // This can happen when user deleted the addressbook (e.g. Kolab folder)
  195. if (!$contacts && (!$id || $default)) {
  196. $source = reset($this->get_address_sources($writeable, !$default));
  197. if (!empty($source)) {
  198. $contacts = $this->get_address_book($source['id']);
  199. if ($contacts) {
  200. $id = $source['id'];
  201. }
  202. }
  203. }
  204. if (!$contacts) {
  205. // there's no default, just return
  206. if ($default) {
  207. return null;
  208. }
  209. self::raise_error(array(
  210. 'code' => 700,
  211. 'file' => __FILE__,
  212. 'line' => __LINE__,
  213. 'message' => "Addressbook source ($id) not found!"
  214. ),
  215. true, true);
  216. }
  217. // add to the 'books' array for shutdown function
  218. $this->address_books[$id] = $contacts;
  219. if ($writeable && $contacts->readonly) {
  220. return null;
  221. }
  222. // set configured sort order
  223. if ($sort_col = $this->config->get('addressbook_sort_col')) {
  224. $contacts->set_sort_order($sort_col);
  225. }
  226. return $contacts;
  227. }
  228. /**
  229. * Return identifier of the address book object
  230. *
  231. * @param rcube_addressbook Addressbook source object
  232. *
  233. * @return string Source identifier
  234. */
  235. public function get_address_book_id($object)
  236. {
  237. foreach ($this->address_books as $index => $book) {
  238. if ($book === $object) {
  239. return $index;
  240. }
  241. }
  242. }
  243. /**
  244. * Return address books list
  245. *
  246. * @param boolean True if the address book needs to be writeable
  247. * @param boolean True if the address book needs to be not hidden
  248. *
  249. * @return array Address books array
  250. */
  251. public function get_address_sources($writeable = false, $skip_hidden = false)
  252. {
  253. $abook_type = (string) $this->config->get('address_book_type');
  254. $ldap_config = (array) $this->config->get('ldap_public');
  255. $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
  256. $list = array();
  257. // We are using the DB address book or a plugin address book
  258. if (!empty($abook_type) && strtolower($abook_type) != 'ldap') {
  259. if (!isset($this->address_books['0'])) {
  260. $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
  261. }
  262. $list['0'] = array(
  263. 'id' => '0',
  264. 'name' => $this->gettext('personaladrbook'),
  265. 'groups' => $this->address_books['0']->groups,
  266. 'readonly' => $this->address_books['0']->readonly,
  267. 'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
  268. 'autocomplete' => in_array('sql', $autocomplete),
  269. );
  270. }
  271. if (!empty($ldap_config)) {
  272. foreach ($ldap_config as $id => $prop) {
  273. // handle misconfiguration
  274. if (empty($prop) || !is_array($prop)) {
  275. continue;
  276. }
  277. $list[$id] = array(
  278. 'id' => $id,
  279. 'name' => html::quote($prop['name']),
  280. 'groups' => !empty($prop['groups']) || !empty($prop['group_filters']),
  281. 'readonly' => !$prop['writable'],
  282. 'hidden' => $prop['hidden'],
  283. 'autocomplete' => in_array($id, $autocomplete)
  284. );
  285. }
  286. }
  287. $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
  288. $list = $plugin['sources'];
  289. foreach ($list as $idx => $item) {
  290. // register source for shutdown function
  291. if (!is_object($this->address_books[$item['id']])) {
  292. $this->address_books[$item['id']] = $item;
  293. }
  294. // remove from list if not writeable as requested
  295. if ($writeable && $item['readonly']) {
  296. unset($list[$idx]);
  297. }
  298. // remove from list if hidden as requested
  299. else if ($skip_hidden && $item['hidden']) {
  300. unset($list[$idx]);
  301. }
  302. }
  303. return $list;
  304. }
  305. /**
  306. * Getter for compose responses.
  307. * These are stored in local config and user preferences.
  308. *
  309. * @param boolean True to sort the list alphabetically
  310. * @param boolean True if only this user's responses shall be listed
  311. *
  312. * @return array List of the current user's stored responses
  313. */
  314. public function get_compose_responses($sorted = false, $user_only = false)
  315. {
  316. $responses = array();
  317. if (!$user_only) {
  318. foreach ($this->config->get('compose_responses_static', array()) as $response) {
  319. if (empty($response['key'])) {
  320. $response['key'] = substr(md5($response['name']), 0, 16);
  321. }
  322. $response['static'] = true;
  323. $response['class'] = 'readonly';
  324. $k = $sorted ? '0000-' . strtolower($response['name']) : $response['key'];
  325. $responses[$k] = $response;
  326. }
  327. }
  328. foreach ($this->config->get('compose_responses', array()) as $response) {
  329. if (empty($response['key'])) {
  330. $response['key'] = substr(md5($response['name']), 0, 16);
  331. }
  332. $k = $sorted ? strtolower($response['name']) : $response['key'];
  333. $responses[$k] = $response;
  334. }
  335. // sort list by name
  336. if ($sorted) {
  337. ksort($responses, SORT_LOCALE_STRING);
  338. }
  339. return array_values($responses);
  340. }
  341. /**
  342. * Init output object for GUI and add common scripts.
  343. * This will instantiate a rcmail_output_html object and set
  344. * environment vars according to the current session and configuration
  345. *
  346. * @param boolean True if this request is loaded in a (i)frame
  347. *
  348. * @return rcube_output Reference to HTML output object
  349. */
  350. public function load_gui($framed = false)
  351. {
  352. // init output page
  353. if (!($this->output instanceof rcmail_output_html)) {
  354. $this->output = new rcmail_output_html($this->task, $framed);
  355. }
  356. // set refresh interval
  357. $this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0));
  358. $this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60);
  359. if ($framed) {
  360. $this->comm_path .= '&_framed=1';
  361. $this->output->set_env('framed', true);
  362. }
  363. $this->output->set_env('task', $this->task);
  364. $this->output->set_env('action', $this->action);
  365. $this->output->set_env('comm_path', $this->comm_path);
  366. $this->output->set_charset(RCUBE_CHARSET);
  367. if ($this->user && $this->user->ID) {
  368. $this->output->set_env('user_id', $this->user->get_hash());
  369. }
  370. // add some basic labels to client
  371. $this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout', 'refreshing');
  372. return $this->output;
  373. }
  374. /**
  375. * Create an output object for JSON responses
  376. *
  377. * @return rcube_output Reference to JSON output object
  378. */
  379. public function json_init()
  380. {
  381. if (!($this->output instanceof rcmail_output_json)) {
  382. $this->output = new rcmail_output_json($this->task);
  383. }
  384. return $this->output;
  385. }
  386. /**
  387. * Create session object and start the session.
  388. */
  389. public function session_init()
  390. {
  391. parent::session_init();
  392. // set initial session vars
  393. if (!$_SESSION['user_id']) {
  394. $_SESSION['temp'] = true;
  395. }
  396. // restore skin selection after logout
  397. if ($_SESSION['temp'] && !empty($_SESSION['skin'])) {
  398. $this->config->set('skin', $_SESSION['skin']);
  399. }
  400. }
  401. /**
  402. * Perfom login to the mail server and to the webmail service.
  403. * This will also create a new user entry if auto_create_user is configured.
  404. *
  405. * @param string Mail storage (IMAP) user name
  406. * @param string Mail storage (IMAP) password
  407. * @param string Mail storage (IMAP) host
  408. * @param bool Enables cookie check
  409. *
  410. * @return boolean True on success, False on failure
  411. */
  412. function login($username, $pass, $host = null, $cookiecheck = false)
  413. {
  414. $this->login_error = null;
  415. if (empty($username)) {
  416. return false;
  417. }
  418. if ($cookiecheck && empty($_COOKIE)) {
  419. $this->login_error = self::ERROR_COOKIES_DISABLED;
  420. return false;
  421. }
  422. $default_host = $this->config->get('default_host');
  423. $default_port = $this->config->get('default_port');
  424. $username_domain = $this->config->get('username_domain');
  425. $login_lc = $this->config->get('login_lc', 2);
  426. if (!$host) {
  427. $host = $default_host;
  428. }
  429. // Validate that selected host is in the list of configured hosts
  430. if (is_array($default_host)) {
  431. $allowed = false;
  432. foreach ($default_host as $key => $host_allowed) {
  433. if (!is_numeric($key)) {
  434. $host_allowed = $key;
  435. }
  436. if ($host == $host_allowed) {
  437. $allowed = true;
  438. break;
  439. }
  440. }
  441. if (!$allowed) {
  442. $host = null;
  443. }
  444. }
  445. else if (!empty($default_host) && $host != rcube_utils::parse_host($default_host)) {
  446. $host = null;
  447. }
  448. if (!$host) {
  449. $this->login_error = self::ERROR_INVALID_HOST;
  450. return false;
  451. }
  452. // parse $host URL
  453. $a_host = parse_url($host);
  454. if ($a_host['host']) {
  455. $host = $a_host['host'];
  456. $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
  457. if (!empty($a_host['port']))
  458. $port = $a_host['port'];
  459. else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143))
  460. $port = 993;
  461. }
  462. if (!$port) {
  463. $port = $default_port;
  464. }
  465. // Check if we need to add/force domain to username
  466. if (!empty($username_domain)) {
  467. $domain = is_array($username_domain) ? $username_domain[$host] : $username_domain;
  468. if ($domain = rcube_utils::parse_host((string)$domain, $host)) {
  469. $pos = strpos($username, '@');
  470. // force configured domains
  471. if ($pos !== false && $this->config->get('username_domain_forced')) {
  472. $username = substr($username, 0, $pos) . '@' . $domain;
  473. }
  474. // just add domain if not specified
  475. else if ($pos === false) {
  476. $username .= '@' . $domain;
  477. }
  478. }
  479. }
  480. // Convert username to lowercase. If storage backend
  481. // is case-insensitive we need to store always the same username (#1487113)
  482. if ($login_lc) {
  483. if ($login_lc == 2 || $login_lc === true) {
  484. $username = mb_strtolower($username);
  485. }
  486. else if (strpos($username, '@')) {
  487. // lowercase domain name
  488. list($local, $domain) = explode('@', $username);
  489. $username = $local . '@' . mb_strtolower($domain);
  490. }
  491. }
  492. // try to resolve email address from virtuser table
  493. if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
  494. $username = $virtuser;
  495. }
  496. // Here we need IDNA ASCII
  497. // Only rcube_contacts class is using domain names in Unicode
  498. $host = rcube_utils::idn_to_ascii($host);
  499. $username = rcube_utils::idn_to_ascii($username);
  500. // user already registered -> overwrite username
  501. if ($user = rcube_user::query($username, $host)) {
  502. $username = $user->data['username'];
  503. }
  504. $storage = $this->get_storage();
  505. // try to log in
  506. if (!$storage->connect($host, $username, $pass, $port, $ssl)) {
  507. return false;
  508. }
  509. // user already registered -> update user's record
  510. if (is_object($user)) {
  511. // update last login timestamp
  512. $user->touch();
  513. }
  514. // create new system user
  515. else if ($this->config->get('auto_create_user')) {
  516. if ($created = rcube_user::create($username, $host)) {
  517. $user = $created;
  518. }
  519. else {
  520. self::raise_error(array(
  521. 'code' => 620,
  522. 'file' => __FILE__,
  523. 'line' => __LINE__,
  524. 'message' => "Failed to create a user record. Maybe aborted by a plugin?"
  525. ),
  526. true, false);
  527. }
  528. }
  529. else {
  530. self::raise_error(array(
  531. 'code' => 621,
  532. 'file' => __FILE__,
  533. 'line' => __LINE__,
  534. 'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
  535. ),
  536. true, false);
  537. }
  538. // login succeeded
  539. if (is_object($user) && $user->ID) {
  540. // Configure environment
  541. $this->set_user($user);
  542. $this->set_storage_prop();
  543. // set session vars
  544. $_SESSION['user_id'] = $user->ID;
  545. $_SESSION['username'] = $user->data['username'];
  546. $_SESSION['storage_host'] = $host;
  547. $_SESSION['storage_port'] = $port;
  548. $_SESSION['storage_ssl'] = $ssl;
  549. $_SESSION['password'] = $this->encrypt($pass);
  550. $_SESSION['login_time'] = time();
  551. if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_') {
  552. $_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
  553. }
  554. // fix some old settings according to namespace prefix
  555. $this->fix_namespace_settings($user);
  556. // create default folders on login
  557. if ($this->config->get('create_default_folders')) {
  558. $storage->create_default_folders();
  559. }
  560. // clear all mailboxes related cache(s)
  561. $storage->clear_cache('mailboxes', true);
  562. return true;
  563. }
  564. return false;
  565. }
  566. /**
  567. * Returns error code of last login operation
  568. *
  569. * @return int Error code
  570. */
  571. public function login_error()
  572. {
  573. if ($this->login_error) {
  574. return $this->login_error;
  575. }
  576. if ($this->storage && $this->storage->get_error_code() < -1) {
  577. return self::ERROR_STORAGE;
  578. }
  579. }
  580. /**
  581. * Auto-select IMAP host based on the posted login information
  582. *
  583. * @return string Selected IMAP host
  584. */
  585. public function autoselect_host()
  586. {
  587. $default_host = $this->config->get('default_host');
  588. $host = null;
  589. if (is_array($default_host)) {
  590. $post_host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
  591. $post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST);
  592. list(, $domain) = explode('@', $post_user);
  593. // direct match in default_host array
  594. if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
  595. $host = $post_host;
  596. }
  597. // try to select host by mail domain
  598. else if (!empty($domain)) {
  599. foreach ($default_host as $storage_host => $mail_domains) {
  600. if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
  601. $host = $storage_host;
  602. break;
  603. }
  604. else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
  605. $host = is_numeric($storage_host) ? $mail_domains : $storage_host;
  606. break;
  607. }
  608. }
  609. }
  610. // take the first entry if $host is still not set
  611. if (empty($host)) {
  612. list($key, $val) = each($default_host);
  613. $host = is_numeric($key) ? $val : $key;
  614. }
  615. }
  616. else if (empty($default_host)) {
  617. $host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
  618. }
  619. else {
  620. $host = rcube_utils::parse_host($default_host);
  621. }
  622. return $host;
  623. }
  624. /**
  625. * Destroy session data and remove cookie
  626. */
  627. public function kill_session()
  628. {
  629. $this->plugins->exec_hook('session_destroy');
  630. $this->session->kill();
  631. $_SESSION = array('language' => $this->user->language, 'temp' => true, 'skin' => $this->config->get('skin'));
  632. $this->user->reset();
  633. }
  634. /**
  635. * Do server side actions on logout
  636. */
  637. public function logout_actions()
  638. {
  639. $config = $this->config->all();
  640. $storage = $this->get_storage();
  641. if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
  642. $storage->clear_folder($config['trash_mbox']);
  643. }
  644. if ($config['logout_expunge']) {
  645. $storage->expunge_folder('INBOX');
  646. }
  647. // Try to save unsaved user preferences
  648. if (!empty($_SESSION['preferences'])) {
  649. $this->user->save_prefs(unserialize($_SESSION['preferences']));
  650. }
  651. }
  652. /**
  653. * Generate a unique token to be used in a form request
  654. *
  655. * @return string The request token
  656. */
  657. public function get_request_token()
  658. {
  659. $sess_id = $_COOKIE[ini_get('session.name')];
  660. if (!$sess_id) {
  661. $sess_id = session_id();
  662. }
  663. $plugin = $this->plugins->exec_hook('request_token', array(
  664. 'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
  665. return $plugin['value'];
  666. }
  667. /**
  668. * Check if the current request contains a valid token
  669. *
  670. * @param int Request method
  671. *
  672. * @return boolean True if request token is valid false if not
  673. */
  674. public function check_request($mode = rcube_utils::INPUT_POST)
  675. {
  676. $token = rcube_utils::get_input_value('_token', $mode);
  677. $sess_id = $_COOKIE[ini_get('session.name')];
  678. return !empty($sess_id) && $token == $this->get_request_token();
  679. }
  680. /**
  681. * Build a valid URL to this instance of Roundcube
  682. *
  683. * @param mixed Either a string with the action or url parameters as key-value pairs
  684. *
  685. * @return string Valid application URL
  686. */
  687. public function url($p)
  688. {
  689. if (!is_array($p)) {
  690. if (strpos($p, 'http') === 0) {
  691. return $p;
  692. }
  693. $p = array('_action' => @func_get_arg(0));
  694. }
  695. $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
  696. $p['_task'] = $task;
  697. unset($p['task']);
  698. $url = './' . $this->filename;
  699. $delm = '?';
  700. foreach (array_reverse($p) as $key => $val) {
  701. if ($val !== '' && $val !== null) {
  702. $par = $key[0] == '_' ? $key : '_'.$key;
  703. $url .= $delm.urlencode($par).'='.urlencode($val);
  704. $delm = '&';
  705. }
  706. }
  707. return $url;
  708. }
  709. /**
  710. * Function to be executed in script shutdown
  711. */
  712. public function shutdown()
  713. {
  714. parent::shutdown();
  715. foreach ($this->address_books as $book) {
  716. if (is_object($book) && is_a($book, 'rcube_addressbook'))
  717. $book->close();
  718. }
  719. // write performance stats to logs/console
  720. if ($this->config->get('devel_mode')) {
  721. if (function_exists('memory_get_usage'))
  722. $mem = $this->show_bytes(memory_get_usage());
  723. if (function_exists('memory_get_peak_usage'))
  724. $mem .= '/'.$this->show_bytes(memory_get_peak_usage());
  725. $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
  726. if (defined('RCMAIL_START'))
  727. self::print_timer(RCMAIL_START, $log);
  728. else
  729. self::console($log);
  730. }
  731. }
  732. /**
  733. * Registers action aliases for current task
  734. *
  735. * @param array $map Alias-to-filename hash array
  736. */
  737. public function register_action_map($map)
  738. {
  739. if (is_array($map)) {
  740. foreach ($map as $idx => $val) {
  741. $this->action_map[$idx] = $val;
  742. }
  743. }
  744. }
  745. /**
  746. * Returns current action filename
  747. *
  748. * @param array $map Alias-to-filename hash array
  749. */
  750. public function get_action_file()
  751. {
  752. if (!empty($this->action_map[$this->action])) {
  753. return $this->action_map[$this->action];
  754. }
  755. return strtr($this->action, '-', '_') . '.inc';
  756. }
  757. /**
  758. * Fixes some user preferences according to namespace handling change.
  759. * Old Roundcube versions were using folder names with removed namespace prefix.
  760. * Now we need to add the prefix on servers where personal namespace has prefix.
  761. *
  762. * @param rcube_user $user User object
  763. */
  764. private function fix_namespace_settings($user)
  765. {
  766. $prefix = $this->storage->get_namespace('prefix');
  767. $prefix_len = strlen($prefix);
  768. if (!$prefix_len)
  769. return;
  770. $prefs = $this->config->all();
  771. if (!empty($prefs['namespace_fixed']))
  772. return;
  773. // Build namespace prefix regexp
  774. $ns = $this->storage->get_namespace();
  775. $regexp = array();
  776. foreach ($ns as $entry) {
  777. if (!empty($entry)) {
  778. foreach ($entry as $item) {
  779. if (strlen($item[0])) {
  780. $regexp[] = preg_quote($item[0], '/');
  781. }
  782. }
  783. }
  784. }
  785. $regexp = '/^('. implode('|', $regexp).')/';
  786. // Fix preferences
  787. $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
  788. foreach ($opts as $opt) {
  789. if ($value = $prefs[$opt]) {
  790. if ($value != 'INBOX' && !preg_match($regexp, $value)) {
  791. $prefs[$opt] = $prefix.$value;
  792. }
  793. }
  794. }
  795. if (!empty($prefs['default_folders'])) {
  796. foreach ($prefs['default_folders'] as $idx => $name) {
  797. if ($name != 'INBOX' && !preg_match($regexp, $name)) {
  798. $prefs['default_folders'][$idx] = $prefix.$name;
  799. }
  800. }
  801. }
  802. if (!empty($prefs['search_mods'])) {
  803. $folders = array();
  804. foreach ($prefs['search_mods'] as $idx => $value) {
  805. if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
  806. $idx = $prefix.$idx;
  807. }
  808. $folders[$idx] = $value;
  809. }
  810. $prefs['search_mods'] = $folders;
  811. }
  812. if (!empty($prefs['message_threading'])) {
  813. $folders = array();
  814. foreach ($prefs['message_threading'] as $idx => $value) {
  815. if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
  816. $idx = $prefix.$idx;
  817. }
  818. $folders[$prefix.$idx] = $value;
  819. }
  820. $prefs['message_threading'] = $folders;
  821. }
  822. if (!empty($prefs['collapsed_folders'])) {
  823. $folders = explode('&&', $prefs['collapsed_folders']);
  824. $count = count($folders);
  825. $folders_str = '';
  826. if ($count) {
  827. $folders[0] = substr($folders[0], 1);
  828. $folders[$count-1] = substr($folders[$count-1], 0, -1);
  829. }
  830. foreach ($folders as $value) {
  831. if ($value != 'INBOX' && !preg_match($regexp, $value)) {
  832. $value = $prefix.$value;
  833. }
  834. $folders_str .= '&'.$value.'&';
  835. }
  836. $prefs['collapsed_folders'] = $folders_str;
  837. }
  838. $prefs['namespace_fixed'] = true;
  839. // save updated preferences and reset imap settings (default folders)
  840. $user->save_prefs($prefs);
  841. $this->set_storage_prop();
  842. }
  843. /**
  844. * Overwrite action variable
  845. *
  846. * @param string New action value
  847. */
  848. public function overwrite_action($action)
  849. {
  850. $this->action = $action;
  851. $this->output->set_env('action', $action);
  852. }
  853. /**
  854. * Set environment variables for specified config options
  855. */
  856. public function set_env_config($options)
  857. {
  858. foreach ((array) $options as $option) {
  859. if ($this->config->get($option)) {
  860. $this->output->set_env($option, true);
  861. }
  862. }
  863. }
  864. /**
  865. * Returns RFC2822 formatted current date in user's timezone
  866. *
  867. * @return string Date
  868. */
  869. public function user_date()
  870. {
  871. // get user's timezone
  872. try {
  873. $tz = new DateTimeZone($this->config->get('timezone'));
  874. $date = new DateTime('now', $tz);
  875. }
  876. catch (Exception $e) {
  877. $date = new DateTime();
  878. }
  879. return $date->format('r');
  880. }
  881. /**
  882. * Write login data (name, ID, IP address) to the 'userlogins' log file.
  883. */
  884. public function log_login($user = null, $failed_login = false, $error_code = 0)
  885. {
  886. if (!$this->config->get('log_logins')) {
  887. return;
  888. }
  889. // failed login
  890. if ($failed_login) {
  891. $message = sprintf('Failed login for %s from %s in session %s (error: %d)',
  892. $user, rcube_utils::remote_ip(), session_id(), $error_code);
  893. }
  894. // successful login
  895. else {
  896. $user_name = $this->get_user_name();
  897. $user_id = $this->get_user_id();
  898. if (!$user_id) {
  899. return;
  900. }
  901. $message = sprintf('Successful login for %s (ID: %d) from %s in session %s',
  902. $user_name, $user_id, rcube_utils::remote_ip(), session_id());
  903. }
  904. // log login
  905. self::write_log('userlogins', $message);
  906. }
  907. /**
  908. * Create a HTML table based on the given data
  909. *
  910. * @param array Named table attributes
  911. * @param mixed Table row data. Either a two-dimensional array or a valid SQL result set
  912. * @param array List of cols to show
  913. * @param string Name of the identifier col
  914. *
  915. * @return string HTML table code
  916. */
  917. public function table_output($attrib, $table_data, $a_show_cols, $id_col)
  918. {
  919. $table = new html_table($attrib);
  920. // add table header
  921. if (!$attrib['noheader']) {
  922. foreach ($a_show_cols as $col) {
  923. $table->add_header($col, $this->Q($this->gettext($col)));
  924. }
  925. }
  926. if (!is_array($table_data)) {
  927. $db = $this->get_dbh();
  928. while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
  929. $table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
  930. // format each col
  931. foreach ($a_show_cols as $col) {
  932. $table->add($col, $this->Q($sql_arr[$col]));
  933. }
  934. }
  935. }
  936. else {
  937. foreach ($table_data as $row_data) {
  938. $class = !empty($row_data['class']) ? $row_data['class'] : '';
  939. $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
  940. $table->add_row(array('id' => $rowid, 'class' => $class));
  941. // format each col
  942. foreach ($a_show_cols as $col) {
  943. $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
  944. }
  945. }
  946. }
  947. return $table->show($attrib);
  948. }
  949. /**
  950. * Convert the given date to a human readable form
  951. * This uses the date formatting properties from config
  952. *
  953. * @param mixed Date representation (string, timestamp or DateTime object)
  954. * @param string Date format to use
  955. * @param bool Enables date convertion according to user timezone
  956. *
  957. * @return string Formatted date string
  958. */
  959. public function format_date($date, $format = null, $convert = true)
  960. {
  961. if (is_object($date) && is_a($date, 'DateTime')) {
  962. $timestamp = $date->format('U');
  963. }
  964. else {
  965. if (!empty($date)) {
  966. $timestamp = rcube_utils::strtotime($date);
  967. }
  968. if (empty($timestamp)) {
  969. return '';
  970. }
  971. try {
  972. $date = new DateTime("@".$timestamp);
  973. }
  974. catch (Exception $e) {
  975. return '';
  976. }
  977. }
  978. if ($convert) {
  979. try {
  980. // convert to the right timezone
  981. $stz = date_default_timezone_get();
  982. $tz = new DateTimeZone($this->config->get('timezone'));
  983. $date->setTimezone($tz);
  984. date_default_timezone_set($tz->getName());
  985. $timestamp = $date->format('U');
  986. }
  987. catch (Exception $e) {
  988. }
  989. }
  990. // define date format depending on current time
  991. if (!$format) {
  992. $now = time();
  993. $now_date = getdate($now);
  994. $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
  995. $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
  996. $pretty_date = $this->config->get('prettydate');
  997. if ($pretty_date && $timestamp > $today_limit && $timestamp < $now) {
  998. $format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
  999. $today = true;
  1000. }
  1001. else if ($pretty_date && $timestamp > $week_limit && $timestamp < $now) {
  1002. $format = $this->config->get('date_short', 'D H:i');
  1003. }
  1004. else {
  1005. $format = $this->config->get('date_long', 'Y-m-d H:i');
  1006. }
  1007. }
  1008. // strftime() format
  1009. if (preg_match('/%[a-z]+/i', $format)) {
  1010. $format = strftime($format, $timestamp);
  1011. if ($stz) {
  1012. date_default_timezone_set($stz);
  1013. }
  1014. return $today ? ($this->gettext('today') . ' ' . $format) : $format;
  1015. }
  1016. // parse format string manually in order to provide localized weekday and month names
  1017. // an alternative would be to convert the date() format string to fit with strftime()
  1018. $out = '';
  1019. for ($i=0; $i<strlen($format); $i++) {
  1020. if ($format[$i] == "\\") { // skip escape chars
  1021. continue;
  1022. }
  1023. // write char "as-is"
  1024. if ($format[$i] == ' ' || $format[$i-1] == "\\") {
  1025. $out .= $format[$i];
  1026. }
  1027. // weekday (short)
  1028. else if ($format[$i] == 'D') {
  1029. $out .= $this->gettext(strtolower(date('D', $timestamp)));
  1030. }
  1031. // weekday long
  1032. else if ($format[$i] == 'l') {
  1033. $out .= $this->gettext(strtolower(date('l', $timestamp)));
  1034. }
  1035. // month name (short)
  1036. else if ($format[$i] == 'M') {
  1037. $out .= $this->gettext(strtolower(date('M', $timestamp)));
  1038. }
  1039. // month name (long)
  1040. else if ($format[$i] == 'F') {
  1041. $out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
  1042. }
  1043. else if ($format[$i] == 'x') {
  1044. $out .= strftime('%x %X', $timestamp);
  1045. }
  1046. else {
  1047. $out .= date($format[$i], $timestamp);
  1048. }
  1049. }
  1050. if ($today) {
  1051. $label = $this->gettext('today');
  1052. // replcae $ character with "Today" label (#1486120)
  1053. if (strpos($out, '$') !== false) {
  1054. $out = preg_replace('/\$/', $label, $out, 1);
  1055. }
  1056. else {
  1057. $out = $label . ' ' . $out;
  1058. }
  1059. }
  1060. if ($stz) {
  1061. date_default_timezone_set($stz);
  1062. }
  1063. return $out;
  1064. }
  1065. /**
  1066. * Return folders list in HTML
  1067. *
  1068. * @param array $attrib Named parameters
  1069. *
  1070. * @return string HTML code for the gui object
  1071. */
  1072. public function folder_list($attrib)
  1073. {
  1074. static $a_mailboxes;
  1075. $attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
  1076. $rcmail = rcmail::get_instance();
  1077. $storage = $rcmail->get_storage();
  1078. // add some labels to client
  1079. $rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
  1080. $type = $attrib['type'] ? $attrib['type'] : 'ul';
  1081. unset($attrib['type']);
  1082. if ($type == 'ul' && !$attrib['id']) {
  1083. $attrib['id'] = 'rcmboxlist';
  1084. }
  1085. if (empty($attrib['folder_name'])) {
  1086. $attrib['folder_name'] = '*';
  1087. }
  1088. // get current folder
  1089. $mbox_name = $storage->get_folder();
  1090. // build the folders tree
  1091. if (empty($a_mailboxes)) {
  1092. // get mailbox list
  1093. $a_folders = $storage->list_folders_subscribed(
  1094. '', $attrib['folder_name'], $attrib['folder_filter']);
  1095. $delimiter = $storage->get_hierarchy_delimiter();
  1096. $a_mailboxes = array();
  1097. foreach ($a_folders as $folder) {
  1098. $rcmail->build_folder_tree($a_mailboxes, $folder, $delimiter);
  1099. }
  1100. }
  1101. // allow plugins to alter the folder tree or to localize folder names
  1102. $hook = $rcmail->plugins->exec_hook('render_mailboxlist', array(
  1103. 'list' => $a_mailboxes,
  1104. 'delimiter' => $delimiter,
  1105. 'type' => $type,
  1106. 'attribs' => $attrib,
  1107. ));
  1108. $a_mailboxes = $hook['list'];
  1109. $attrib = $hook['attribs'];
  1110. if ($type == 'select') {
  1111. $attrib['is_escaped'] = true;
  1112. $select = new html_select($attrib);
  1113. // add no-selection option
  1114. if ($attrib['noselection']) {
  1115. $select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
  1116. }
  1117. $rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
  1118. $out = $select->show($attrib['default']);
  1119. }
  1120. else {
  1121. $js_mailboxlist = array();
  1122. $tree = $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
  1123. if ($type != 'js') {
  1124. $out = html::tag('ul', $attrib, $tree, html::$common_attrib);
  1125. $rcmail->output->include_script('treelist.js');
  1126. $rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
  1127. $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
  1128. $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
  1129. }
  1130. $rcmail->output->set_env('mailboxes', $js_mailboxlist);
  1131. // we can't use object keys in javascript because they are unordered
  1132. // we need sorted folders list for folder-selector widget
  1133. $rcmail->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
  1134. }
  1135. return $out;
  1136. }
  1137. /**
  1138. * Return folders list as html_select object
  1139. *
  1140. * @param array $p Named parameters
  1141. *
  1142. * @return html_select HTML drop-down object
  1143. */
  1144. public function folder_selector($p = array())
  1145. {
  1146. $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
  1147. $a_mailboxes = array();
  1148. $storage = $this->get_storage();
  1149. if (empty($p['folder_name'])) {
  1150. $p['folder_name'] = '*';
  1151. }
  1152. if ($p['unsubscribed']) {
  1153. $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
  1154. }
  1155. else {
  1156. $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
  1157. }
  1158. $delimiter = $storage->get_hierarchy_delimiter();
  1159. if (!empty($p['exceptions'])) {
  1160. $list = array_diff($list, (array) $p['exceptions']);
  1161. }
  1162. if (!empty($p['additional'])) {
  1163. foreach ($p['additional'] as $add_folder) {
  1164. $add_items = explode($delimiter, $add_folder);
  1165. $folder = '';
  1166. while (count($add_items)) {
  1167. $folder .= array_shift($add_items);
  1168. // @TODO: sorting
  1169. if (!in_array($folder, $list)) {
  1170. $list[] = $folder;
  1171. }
  1172. $folder .= $delimiter;
  1173. }
  1174. }
  1175. }
  1176. foreach ($list as $folder) {
  1177. $this->build_folder_tree($a_mailboxes, $folder, $delimiter);
  1178. }
  1179. $select = new html_select($p);
  1180. if ($p['noselection']) {
  1181. $select->add(html::quote($p['noselection']), '');
  1182. }
  1183. $this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
  1184. return $select;
  1185. }
  1186. /**
  1187. * Create a hierarchical array of the mailbox list
  1188. */
  1189. public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
  1190. {
  1191. // Handle namespace prefix
  1192. $prefix = '';
  1193. if (!$path) {
  1194. $n_folder = $folder;
  1195. $folder = $this->storage->mod_folder($folder);
  1196. if ($n_folder != $folder) {
  1197. $prefix = substr($n_folder, 0, -strlen($folder));
  1198. }
  1199. }
  1200. $pos = strpos($folder, $delm);
  1201. if ($pos !== false) {
  1202. $subFolders = substr($folder, $pos+1);
  1203. $currentFolder = substr($folder, 0, $pos);
  1204. // sometimes folder has a delimiter as the last character
  1205. if (!strlen($subFolders)) {
  1206. $virtual = false;
  1207. }
  1208. else if (!isset($arrFolders[$currentFolder])) {
  1209. $virtual = true;
  1210. }
  1211. else {
  1212. $virtual = $arrFolders[$currentFolder]['virtual'];
  1213. }
  1214. }
  1215. else {
  1216. $subFolders = false;
  1217. $currentFolder = $folder;
  1218. $virtual = false;
  1219. }
  1220. $path .= $prefix . $currentFolder;
  1221. if (!isset($arrFolders[$currentFolder])) {
  1222. $arrFolders[$currentFolder] = array(
  1223. 'id' => $path,
  1224. 'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
  1225. 'virtual' => $virtual,
  1226. 'folders' => array());
  1227. }
  1228. else {
  1229. $arrFolders[$currentFolder]['virtual'] = $virtual;
  1230. }
  1231. if (strlen($subFolders)) {
  1232. $this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
  1233. }
  1234. }
  1235. /**
  1236. * Return html for a structured list &lt;ul&gt; for the mailbox tree
  1237. */
  1238. public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
  1239. {
  1240. $maxlength = intval($attrib['maxlength']);
  1241. $realnames = (bool)$attrib['realnames'];
  1242. $msgcounts = $this->storage->get_cache('messagecount');
  1243. $collapsed = $this->config->get('collapsed_folders');
  1244. $realnames = $this->config->get('show_real_foldernames');
  1245. $out = '';
  1246. foreach ($arrFolders as $folder) {
  1247. $title = null;
  1248. $folder_class = $this->folder_classname($folder['id']);
  1249. $is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
  1250. $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
  1251. if ($folder_class && !$realnames) {
  1252. $foldername = $this->gettext($folder_class);
  1253. }
  1254. else {
  1255. $foldername = $folder['name'];
  1256. // shorten the folder name to a given length
  1257. if ($maxlength && $maxlength > 1) {
  1258. $fname = abbreviate_string($foldername, $maxlength);
  1259. if ($fname != $foldername) {
  1260. $title = $foldername;
  1261. }
  1262. $foldername = $fname;
  1263. }
  1264. }
  1265. // make folder name safe for ids and class names
  1266. $folder_id = rcube_utils::html_identifier($folder['id'], true);
  1267. $classes = array('mailbox');
  1268. // set special class for Sent, Drafts, Trash and Junk
  1269. if ($folder_class) {
  1270. $classes[] = $folder_class;
  1271. }
  1272. if ($folder['id'] == $mbox_name) {
  1273. $classes[] = 'selected';
  1274. }
  1275. if ($folder['virtual']) {
  1276. $classes[] = 'virtual';
  1277. }
  1278. else if ($unread) {
  1279. $classes[] = 'unread';
  1280. }
  1281. $js_name = $this->JQ($folder['id']);
  1282. $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
  1283. $link_attrib = $folder['virtual'] ? array() : array(
  1284. 'href' => $this->url(array('_mbox' => $folder['id'])),
  1285. 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name),
  1286. 'rel' => $folder['id'],
  1287. 'title' => $title,
  1288. );
  1289. $out .= html::tag('li', array(
  1290. 'id' => "rcmli".$folder_id,
  1291. 'class' => join(' ', $classes),
  1292. 'noclose' => true),
  1293. html::a($link_attrib, $html_name));
  1294. if (!empty($folder['folders'])) {
  1295. $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
  1296. }
  1297. $jslist[$folder['id']] = array(
  1298. 'id' => $folder['id'],
  1299. 'name' => $foldername,
  1300. 'virtual' => $folder['virtual'],
  1301. );
  1302. if (!empty($folder_class)) {
  1303. $jslist[$folder['id']]['class'] = $folder_class;
  1304. }
  1305. if (!empty($folder['folders'])) {
  1306. $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
  1307. $this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
  1308. }
  1309. $out .= "</li>\n";
  1310. }
  1311. return $out;
  1312. }
  1313. /**
  1314. * Return html for a flat list <select> for the mailbox tree
  1315. */
  1316. public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
  1317. {
  1318. $out = '';
  1319. foreach ($arrFolders as $folder) {
  1320. // skip exceptions (and its subfolders)
  1321. if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
  1322. continue;
  1323. }
  1324. // skip folders in which it isn't possible to create subfolders
  1325. if (!empty($opts['skip_noinferiors'])) {
  1326. $attrs = $this->storage->folder_attributes($folder['id']);
  1327. if ($attrs && in_array('\\Noinferiors', $attrs)) {
  1328. continue;
  1329. }
  1330. }
  1331. if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
  1332. $foldername = $this->gettext($folder_class);
  1333. }
  1334. else {
  1335. $foldername = $folder['name'];
  1336. // shorten the folder name to a given length
  1337. if ($maxlength && $maxlength > 1) {
  1338. $foldername = abbreviate_string($foldername, $maxlength);
  1339. }
  1340. }
  1341. $select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
  1342. if (!empty($folder['folders'])) {
  1343. $out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
  1344. $select, $realnames, $nestLevel+1, $opts);
  1345. }
  1346. }
  1347. return $out;
  1348. }
  1349. /**
  1350. * Return internal name for the given folder if it matches the configured special folders
  1351. */
  1352. public function folder_classname($folder_id)
  1353. {
  1354. if ($folder_id == 'INBOX') {
  1355. return 'inbox';
  1356. }
  1357. // for these mailboxes we have localized labels and css classes
  1358. foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
  1359. {
  1360. if ($folder_id === $this->config->get($smbx.'_mbox')) {
  1361. return $smbx;
  1362. }
  1363. }
  1364. }
  1365. /**
  1366. * Try to localize the given IMAP folder name.
  1367. * UTF-7 decode it in case no localized text was found
  1368. *
  1369. * @param string $name Folder name
  1370. * @param bool $with_path Enable path localization
  1371. *
  1372. * @return string Localized folder name in UTF-8 encoding
  1373. */
  1374. public function localize_foldername($name, $with_path = true)
  1375. {
  1376. $realnames = $this->config->get('show_real_foldernames');
  1377. // try to localize path of the folder
  1378. if ($with_path && !$realnames) {
  1379. $storage = $this->get_storage();
  1380. $delimiter = $storage->get_hierarchy_delimiter();
  1381. $path = explode($delimiter, $name);
  1382. $count = count($path);
  1383. if ($count > 1) {
  1384. for ($i = 0; $i < $count; $i++) {
  1385. $folder = implode($delimiter, array_slice($path, 0, -$i));
  1386. if ($folder_class = $this->folder_classname($folder)) {
  1387. $name = implode($delimiter, array_slice($path, $count - $i));
  1388. return $this->gettext($folder_class) . $delimiter . rcube_charset::convert($name, 'UTF7-IMAP');
  1389. }
  1390. }
  1391. }
  1392. }
  1393. if (!$realnames && ($folder_class = $this->folder_classname($name))) {
  1394. return $this->gettext($folder_class);
  1395. }
  1396. return rcube_charset::convert($name, 'UTF7-IMAP');
  1397. }
  1398. public function localize_folderpath($path)
  1399. {
  1400. $protect_folders = $this->config->get('protect_default_folders');
  1401. $default_folders = (array) $this->config->get('default_folders');
  1402. $delimiter = $this->storage->get_hierarchy_delimiter();
  1403. $path = explode($delimiter, $path);
  1404. $result = array();
  1405. foreach ($path as $idx => $dir) {
  1406. $directory = implode($delimiter, array_slice($path, 0, $idx+1));
  1407. if ($protect_folders && in_array($directory, $default_folders)) {
  1408. unset($result);
  1409. $result[] = $this->localize_foldername($directory);
  1410. }
  1411. else {
  1412. $result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
  1413. }
  1414. }
  1415. return implode($delimiter, $result);
  1416. }
  1417. public static function quota_display($attrib)
  1418. {
  1419. $rcmail = rcmail::get_instance();
  1420. if (!$attrib['id']) {
  1421. $attrib['id'] = 'rcmquotadisplay';
  1422. }
  1423. $_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
  1424. $rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
  1425. $quota = $rcmail->quota_content($attrib);
  1426. $rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
  1427. return html::span($attrib, '&nbsp;');
  1428. }
  1429. public function quota_content($attrib = null)
  1430. {
  1431. $quota = $this->storage->get_quota();
  1432. $quota = $this->plugins->exec_hook('quota', $quota);
  1433. $quota_result = (array) $quota;
  1434. $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
  1435. if ($quota['total'] > 0) {
  1436. if (!isset($quota['percent'])) {
  1437. $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
  1438. }
  1439. $title = sprintf('%s / %s (%.0f%%)',
  1440. $this->show_bytes($quota['used'] * 1024), $this->show_bytes($quota['total'] * 1024),
  1441. $quota_result['percent']);
  1442. $quota_result['title'] = $title;
  1443. if ($attrib['width']) {
  1444. $quota_result['width'] = $attrib['width'];
  1445. }
  1446. if ($attrib['height']) {
  1447. $quota_result['height'] = $attrib['height'];
  1448. }
  1449. }
  1450. else {
  1451. $unlimited = $this->config->get('quota_zero_as_unlimited');
  1452. $quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
  1453. $quota_result['percent'] = 0;
  1454. }
  1455. return $quota_result;
  1456. }
  1457. /**
  1458. * Outputs error message according to server error/response codes
  1459. *
  1460. * @param string $fallback Fallback message label
  1461. * @param array $fallback_args Fallback message label arguments
  1462. * @param string $suffix Message label suffix
  1463. */
  1464. public function display_server_error($fallback = null, $fallback_args = null, $suffix = '')
  1465. {
  1466. $err_code = $this->storage->get_error_code();
  1467. $res_code = $this->storage->get_response_code();
  1468. $args = array();
  1469. if ($res_code == rcube_storage::NOPERM) {
  1470. $error = 'errornoperm';
  1471. }
  1472. else if ($res_code == rcube_storage::READONLY) {
  1473. $error = 'errorreadonly';
  1474. }
  1475. else if ($res_code == rcube_storage::OVERQUOTA) {
  1476. $error = 'errorroverquota';
  1477. }
  1478. else if ($err_code && ($err_str = $this->storage->get_error_str())) {
  1479. // try to detect access rights problem and display appropriate message
  1480. if (stripos($err_str, 'Permission denied') !== false) {
  1481. $error = 'errornoperm';
  1482. }
  1483. // try to detect full mailbox problem and display appropriate message
  1484. // there can be e.g. "Quota exceeded" or "quotum would exceed"
  1485. else if (stripos($err_str, 'quot') !== false && stripos($err_str, 'exceed') !== false) {
  1486. $error = 'erroroverquota';
  1487. }
  1488. else {
  1489. $error = 'servererrormsg';
  1490. $args = array('msg' => $err_str);
  1491. }
  1492. }
  1493. else if ($err_code < 0) {
  1494. $error = 'storageerror';
  1495. }
  1496. else if ($fallback) {
  1497. $error = $fallback;
  1498. $args = $fallback_args;
  1499. }
  1500. if ($error) {
  1501. if ($suffix && $this->text_exists($error . $suffix)) {
  1502. $error .= $suffix;
  1503. }
  1504. $this->output->show_message($error, 'error', $args);
  1505. }
  1506. }
  1507. /**
  1508. * Output HTML editor scripts
  1509. *
  1510. * @param string $mode Editor mode
  1511. */
  1512. public function html_editor($mode = '')
  1513. {
  1514. $hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
  1515. if ($hook['abort']) {
  1516. return;
  1517. }
  1518. $lang = strtolower($_SESSION['language']);
  1519. // TinyMCE uses two-letter lang codes, with exception of Chinese
  1520. if (strpos($lang, 'zh_') === 0) {
  1521. $lang = str_replace('_', '-', $lang);
  1522. }
  1523. else {
  1524. $lang = substr($lang, 0, 2);
  1525. }
  1526. if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
  1527. $lang = 'en';
  1528. }
  1529. $script = json_encode(array(
  1530. 'mode' => $mode,
  1531. 'lang' => $lang,
  1532. 'skin_path' => $this->output->get_skin_path(),
  1533. 'spellcheck' => intval($this->config->get('enable_spellcheck')),
  1534. 'spelldict' => intval($this->config->get('spellcheck_dictionary'))
  1535. ));
  1536. $this->output->include_script('tiny_mce/tiny_mce.js');
  1537. $this->output->include_script('editor.js');
  1538. $this->output->add_script("rcmail_editor_init($script)", 'docready');
  1539. }
  1540. /**
  1541. * Replaces TinyMCE's emoticon images with plain-text representation
  1542. *
  1543. * @param string $html HTML content
  1544. *
  1545. * @return string HTML content
  1546. */
  1547. public static function replace_emoticons($html)
  1548. {
  1549. $emoticons = array(
  1550. '8-)' => 'smiley-cool',
  1551. ':-#' => 'smiley-foot-in-mouth',
  1552. ':-*' => 'smiley-kiss',
  1553. ':-X' => 'smiley-sealed',
  1554. ':-P' => 'smiley-tongue-out',
  1555. ':-@' => 'smiley-yell',
  1556. ":'(" => 'smiley-cry',
  1557. ':-(' => 'smiley-frown',
  1558. ':-D' => 'smiley-laughing',
  1559. ':-)' => 'smiley-smile',
  1560. ':-S' => 'smiley-undecided',
  1561. ':-$' => 'smiley-embarassed',
  1562. 'O:-)' => 'smiley-innocent',
  1563. ':-|' => 'smiley-money-mouth',
  1564. ':-O' => 'smiley-surprised',
  1565. ';-)' => 'smiley-wink',
  1566. );
  1567. foreach ($emoticons as $idx => $file) {
  1568. // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
  1569. $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
  1570. $replace[] = $idx;
  1571. }
  1572. return preg_replace($search, $replace, $html);
  1573. }
  1574. /**
  1575. * File upload progress handler.
  1576. */
  1577. public function upload_progress()
  1578. {
  1579. $prefix = ini_get('apc.rfc1867_prefix');
  1580. $params = array(
  1581. 'action' => $this->action,
  1582. 'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET),
  1583. );
  1584. if (function_exists('apc_fetch')) {
  1585. $status = apc_fetch($prefix . $params['name']);
  1586. if (!empty($status)) {
  1587. $status['percent'] = round($status['current']/$status['total']*100);
  1588. $params = array_merge($status, $params);
  1589. }
  1590. }
  1591. if (isset($params['percent']))
  1592. $params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
  1593. 'percent' => $params['percent'] . '%',
  1594. 'current' => $this->show_bytes($params['current']),
  1595. 'total' => $this->show_bytes($params['total'])
  1596. )));
  1597. $this->output->command('upload_progress_update', $params);
  1598. $this->output->send();
  1599. }
  1600. /**
  1601. * Initializes file uploading interface.
  1602. */
  1603. public function upload_init()
  1604. {
  1605. // Enable upload progress bar
  1606. $rfc1867 = filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN);
  1607. if ($rfc1867 && ($seconds = $this->config->get('upload_progress'))) {
  1608. if ($field_name = ini_get('apc.rfc1867_name')) {
  1609. $this->output->set_env('upload_progress_name', $field_name);
  1610. $this->output->set_env('upload_progress_time', (int) $seconds);
  1611. }
  1612. }
  1613. // find max filesize value
  1614. $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
  1615. $max_postsize = parse_bytes(ini_get('post_max_size'));
  1616. if ($max_postsize && $max_postsize < $max_filesize) {
  1617. $max_filesize = $max_postsize;
  1618. }
  1619. $this->output->set_env('max_filesize', $max_filesize);
  1620. $max_filesize = $this->show_bytes($max_filesize);
  1621. $this->output->set_env('filesizeerror', $this->gettext(array(
  1622. 'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
  1623. return $max_filesize;
  1624. }
  1625. /**
  1626. * Initializes client-side autocompletion.
  1627. */
  1628. public function autocomplete_init()
  1629. {
  1630. static $init;
  1631. if ($init) {
  1632. return;
  1633. }
  1634. $init = 1;
  1635. if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
  1636. $book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
  1637. if (count($book_types) > 1) {
  1638. $this->output->set_env('autocomplete_threads', $threads);
  1639. $this->output->set_env('autocomplete_sources', $book_types);
  1640. }
  1641. }
  1642. $this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
  1643. $this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
  1644. $this->output->add_label('autocompletechars', 'autocompletemore');
  1645. }
  1646. /**
  1647. * Returns supported font-family specifications
  1648. *
  1649. * @param string $font Font name
  1650. *
  1651. * @param string|array Font-family specification array or string (if $font is used)
  1652. */
  1653. public static function font_defs($font = null)
  1654. {
  1655. $fonts = array(
  1656. 'Andale Mono' => '"Andale Mono",Times,monospace',
  1657. 'Arial' => 'Arial,Helvetica,sans-serif',
  1658. 'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
  1659. 'Book Antiqua' => '"Book Antiqua",Palatino,serif',
  1660. 'Courier New' => '"Courier New",Courier,monospace',
  1661. 'Georgia' => 'Georgia,Palatino,serif',
  1662. 'Helvetica' => 'Helvetica,Arial,sans-serif',
  1663. 'Impact' => 'Impact,Chicago,sans-serif',
  1664. 'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
  1665. 'Terminal' => 'Terminal,Monaco,monospace',
  1666. 'Times New Roman' => '"Times New Roman",Times,serif',
  1667. 'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
  1668. 'Verdana' => 'Verdana,Geneva,sans-serif',
  1669. );
  1670. if ($font) {
  1671. return $fonts[$font];
  1672. }
  1673. return $fonts;
  1674. }
  1675. /**
  1676. * Create a human readable string for a number of bytes
  1677. *
  1678. * @param int Number of bytes
  1679. *
  1680. * @return string Byte string
  1681. */
  1682. public function show_bytes($bytes)
  1683. {
  1684. if ($bytes >= 1073741824) {
  1685. $gb = $bytes/1073741824;
  1686. $str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
  1687. }
  1688. else if ($bytes >= 1048576) {
  1689. $mb = $bytes/1048576;
  1690. $str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
  1691. }
  1692. else if ($bytes >= 1024) {
  1693. $str = sprintf("%d ", round($bytes/1024)) . $this->gettext('KB');
  1694. }
  1695. else {
  1696. $str = sprintf('%d ', $bytes) . $this->gettext('B');
  1697. }
  1698. return $str;
  1699. }
  1700. /**
  1701. * Returns real size (calculated) of the message part
  1702. *
  1703. * @param rcube_message_part Message part
  1704. *
  1705. * @return string Part size (and unit)
  1706. */
  1707. public function message_part_size($part)
  1708. {
  1709. if (isset($part->d_parameters['size'])) {
  1710. $size = $this->show_bytes((int)$part->d_parameters['size']);
  1711. }
  1712. else {
  1713. $size = $part->size;
  1714. if ($part->encoding == 'base64') {
  1715. $size = $size / 1.33;
  1716. }
  1717. $size = '~' . $this->show_bytes($size);
  1718. }
  1719. return $size;
  1720. }
  1721. /************************************************************************
  1722. ********* Deprecated methods (to be removed) *********
  1723. ***********************************************************************/
  1724. public static function setcookie($name, $value, $exp = 0)
  1725. {
  1726. rcube_utils::setcookie($name, $value, $exp);
  1727. }
  1728. public function imap_connect()
  1729. {
  1730. return $this->storage_connect();
  1731. }
  1732. public function imap_init()
  1733. {
  1734. return $this->storage_init();
  1735. }
  1736. /**
  1737. * Connect to the mail storage server with stored session data
  1738. *
  1739. * @return bool True on success, False on error
  1740. */
  1741. public function storage_connect()
  1742. {
  1743. $storage = $this->get_storage();
  1744. if ($_SESSION['storage_host'] && !$storage->is_connected()) {
  1745. $host = $_SESSION['storage_host'];
  1746. $user = $_SESSION['username'];
  1747. $port = $_SESSION['storage_port'];
  1748. $ssl = $_SESSION['storage_ssl'];
  1749. $pass = $this->decrypt($_SESSION['password']);
  1750. if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
  1751. if (is_object($this->output)) {
  1752. $this->output->show_message('storageerror', 'error');
  1753. }
  1754. }
  1755. else {
  1756. $this->set_storage_prop();
  1757. }
  1758. }
  1759. return $storage->is_connected();
  1760. }
  1761. }