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.

rcmail.php 81KB

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