Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

rcmail.php 85KB

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