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

691 lines
21 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
13 years ago
10 years ago
13 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
12 years ago
12 years ago
13 years ago
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2006-2013, The Roundcube Dev Team |
  6. | |
  7. | Licensed under the GNU General Public License version 3 or |
  8. | any later version with exceptions for skins & plugins. |
  9. | See the README file for a full license statement. |
  10. | |
  11. | PURPOSE: |
  12. | Interface to the local address book database |
  13. +-----------------------------------------------------------------------+
  14. | Author: Thomas Bruederli <roundcube@gmail.com> |
  15. +-----------------------------------------------------------------------+
  16. */
  17. /**
  18. * Abstract skeleton of an address book/repository
  19. *
  20. * @package Framework
  21. * @subpackage Addressbook
  22. */
  23. abstract class rcube_addressbook
  24. {
  25. /** constants for error reporting **/
  26. const ERROR_READ_ONLY = 1;
  27. const ERROR_NO_CONNECTION = 2;
  28. const ERROR_VALIDATE = 3;
  29. const ERROR_SAVING = 4;
  30. const ERROR_SEARCH = 5;
  31. /** public properties (mandatory) */
  32. public $primary_key;
  33. public $groups = false;
  34. public $export_groups = true;
  35. public $readonly = true;
  36. public $searchonly = false;
  37. public $undelete = false;
  38. public $ready = false;
  39. public $group_id = null;
  40. public $list_page = 1;
  41. public $page_size = 10;
  42. public $sort_col = 'name';
  43. public $sort_order = 'ASC';
  44. public $date_cols = array();
  45. public $coltypes = array(
  46. 'name' => array('limit'=>1),
  47. 'firstname' => array('limit'=>1),
  48. 'surname' => array('limit'=>1),
  49. 'email' => array('limit'=>1)
  50. );
  51. protected $error;
  52. /**
  53. * Returns addressbook name (e.g. for addressbooks listing)
  54. */
  55. abstract function get_name();
  56. /**
  57. * Save a search string for future listings
  58. *
  59. * @param mixed Search params to use in listing method, obtained by get_search_set()
  60. */
  61. abstract function set_search_set($filter);
  62. /**
  63. * Getter for saved search properties
  64. *
  65. * @return mixed Search properties used by this class
  66. */
  67. abstract function get_search_set();
  68. /**
  69. * Reset saved results and search parameters
  70. */
  71. abstract function reset();
  72. /**
  73. * Refresh saved search set after data has changed
  74. *
  75. * @return mixed New search set
  76. */
  77. function refresh_search()
  78. {
  79. return $this->get_search_set();
  80. }
  81. /**
  82. * List the current set of contact records
  83. *
  84. * @param array List of cols to show
  85. * @param int Only return this number of records, use negative values for tail
  86. * @return array Indexed list of contact records, each a hash array
  87. */
  88. abstract function list_records($cols=null, $subset=0);
  89. /**
  90. * Search records
  91. *
  92. * @param array List of fields to search in
  93. * @param string Search value
  94. * @param int Matching mode:
  95. * 0 - partial (*abc*),
  96. * 1 - strict (=),
  97. * 2 - prefix (abc*)
  98. * @param boolean True if results are requested, False if count only
  99. * @param boolean True to skip the count query (select only)
  100. * @param array List of fields that cannot be empty
  101. * @return object rcube_result_set List of contact records and 'count' value
  102. */
  103. abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array());
  104. /**
  105. * Count number of available contacts in database
  106. *
  107. * @return rcube_result_set Result set with values for 'count' and 'first'
  108. */
  109. abstract function count();
  110. /**
  111. * Return the last result set
  112. *
  113. * @return rcube_result_set Current result set or NULL if nothing selected yet
  114. */
  115. abstract function get_result();
  116. /**
  117. * Get a specific contact record
  118. *
  119. * @param mixed record identifier(s)
  120. * @param boolean True to return record as associative array, otherwise a result set is returned
  121. *
  122. * @return mixed Result object with all record fields or False if not found
  123. */
  124. abstract function get_record($id, $assoc=false);
  125. /**
  126. * Returns the last error occurred (e.g. when updating/inserting failed)
  127. *
  128. * @return array Hash array with the following fields: type, message
  129. */
  130. function get_error()
  131. {
  132. return $this->error;
  133. }
  134. /**
  135. * Setter for errors for internal use
  136. *
  137. * @param int Error type (one of this class' error constants)
  138. * @param string Error message (name of a text label)
  139. */
  140. protected function set_error($type, $message)
  141. {
  142. $this->error = array('type' => $type, 'message' => $message);
  143. }
  144. /**
  145. * Close connection to source
  146. * Called on script shutdown
  147. */
  148. function close() { }
  149. /**
  150. * Set internal list page
  151. *
  152. * @param number Page number to list
  153. */
  154. function set_page($page)
  155. {
  156. $this->list_page = (int)$page;
  157. }
  158. /**
  159. * Set internal page size
  160. *
  161. * @param number Number of messages to display on one page
  162. */
  163. function set_pagesize($size)
  164. {
  165. $this->page_size = (int)$size;
  166. }
  167. /**
  168. * Set internal sort settings
  169. *
  170. * @param string $sort_col Sort column
  171. * @param string $sort_order Sort order
  172. */
  173. function set_sort_order($sort_col, $sort_order = null)
  174. {
  175. if ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) {
  176. $this->sort_col = $sort_col;
  177. }
  178. if ($sort_order != null) {
  179. $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
  180. }
  181. }
  182. /**
  183. * Check the given data before saving.
  184. * If input isn't valid, the message to display can be fetched using get_error()
  185. *
  186. * @param array Assoziative array with data to save
  187. * @param boolean Attempt to fix/complete record automatically
  188. *
  189. * @return boolean True if input is valid, False if not.
  190. */
  191. public function validate(&$save_data, $autofix = false)
  192. {
  193. $rcube = rcube::get_instance();
  194. $valid = true;
  195. // check validity of email addresses
  196. foreach ($this->get_col_values('email', $save_data, true) as $email) {
  197. if (strlen($email)) {
  198. if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
  199. $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
  200. $this->set_error(self::ERROR_VALIDATE, $error);
  201. $valid = false;
  202. break;
  203. }
  204. }
  205. }
  206. // allow plugins to do contact validation and auto-fixing
  207. $plugin = $rcube->plugins->exec_hook('contact_validate', array(
  208. 'record' => $save_data,
  209. 'autofix' => $autofix,
  210. 'valid' => $valid,
  211. ));
  212. if ($valid && !$plugin['valid']) {
  213. $this->set_error(self::ERROR_VALIDATE, $plugin['error']);
  214. }
  215. if (is_array($plugin['record'])) {
  216. $save_data = $plugin['record'];
  217. }
  218. return $plugin['valid'];
  219. }
  220. /**
  221. * Create a new contact record
  222. *
  223. * @param array Assoziative array with save data
  224. * Keys: Field name with optional section in the form FIELD:SECTION
  225. * Values: Field value. Can be either a string or an array of strings for multiple values
  226. * @param boolean True to check for duplicates first
  227. * @return mixed The created record ID on success, False on error
  228. */
  229. function insert($save_data, $check=false)
  230. {
  231. /* empty for read-only address books */
  232. }
  233. /**
  234. * Create new contact records for every item in the record set
  235. *
  236. * @param object rcube_result_set Recordset to insert
  237. * @param boolean True to check for duplicates first
  238. * @return array List of created record IDs
  239. */
  240. function insertMultiple($recset, $check=false)
  241. {
  242. $ids = array();
  243. if (is_object($recset) && is_a($recset, rcube_result_set)) {
  244. while ($row = $recset->next()) {
  245. if ($insert = $this->insert($row, $check))
  246. $ids[] = $insert;
  247. }
  248. }
  249. return $ids;
  250. }
  251. /**
  252. * Update a specific contact record
  253. *
  254. * @param mixed Record identifier
  255. * @param array Assoziative array with save data
  256. * Keys: Field name with optional section in the form FIELD:SECTION
  257. * Values: Field value. Can be either a string or an array of strings for multiple values
  258. *
  259. * @return mixed On success if ID has been changed returns ID, otherwise True, False on error
  260. */
  261. function update($id, $save_cols)
  262. {
  263. /* empty for read-only address books */
  264. }
  265. /**
  266. * Mark one or more contact records as deleted
  267. *
  268. * @param array Record identifiers
  269. * @param bool Remove records irreversible (see self::undelete)
  270. */
  271. function delete($ids, $force = true)
  272. {
  273. /* empty for read-only address books */
  274. }
  275. /**
  276. * Unmark delete flag on contact record(s)
  277. *
  278. * @param array Record identifiers
  279. */
  280. function undelete($ids)
  281. {
  282. /* empty for read-only address books */
  283. }
  284. /**
  285. * Mark all records in database as deleted
  286. *
  287. * @param bool $with_groups Remove also groups
  288. */
  289. function delete_all($with_groups = false)
  290. {
  291. /* empty for read-only address books */
  292. }
  293. /**
  294. * Setter for the current group
  295. * (empty, has to be re-implemented by extending class)
  296. */
  297. function set_group($gid) { }
  298. /**
  299. * List all active contact groups of this source
  300. *
  301. * @param string Optional search string to match group name
  302. * @param int Matching mode:
  303. * 0 - partial (*abc*),
  304. * 1 - strict (=),
  305. * 2 - prefix (abc*)
  306. *
  307. * @return array Indexed list of contact groups, each a hash array
  308. */
  309. function list_groups($search = null, $mode = 0)
  310. {
  311. /* empty for address books don't supporting groups */
  312. return array();
  313. }
  314. /**
  315. * Get group properties such as name and email address(es)
  316. *
  317. * @param string Group identifier
  318. * @return array Group properties as hash array
  319. */
  320. function get_group($group_id)
  321. {
  322. /* empty for address books don't supporting groups */
  323. return null;
  324. }
  325. /**
  326. * Create a contact group with the given name
  327. *
  328. * @param string The group name
  329. * @return mixed False on error, array with record props in success
  330. */
  331. function create_group($name)
  332. {
  333. /* empty for address books don't supporting groups */
  334. return false;
  335. }
  336. /**
  337. * Delete the given group and all linked group members
  338. *
  339. * @param string Group identifier
  340. * @return boolean True on success, false if no data was changed
  341. */
  342. function delete_group($gid)
  343. {
  344. /* empty for address books don't supporting groups */
  345. return false;
  346. }
  347. /**
  348. * Rename a specific contact group
  349. *
  350. * @param string Group identifier
  351. * @param string New name to set for this group
  352. * @param string New group identifier (if changed, otherwise don't set)
  353. * @return boolean New name on success, false if no data was changed
  354. */
  355. function rename_group($gid, $newname, &$newid)
  356. {
  357. /* empty for address books don't supporting groups */
  358. return false;
  359. }
  360. /**
  361. * Add the given contact records the a certain group
  362. *
  363. * @param string Group identifier
  364. * @param array|string List of contact identifiers to be added
  365. *
  366. * @return int Number of contacts added
  367. */
  368. function add_to_group($group_id, $ids)
  369. {
  370. /* empty for address books don't supporting groups */
  371. return 0;
  372. }
  373. /**
  374. * Remove the given contact records from a certain group
  375. *
  376. * @param string Group identifier
  377. * @param array|string List of contact identifiers to be removed
  378. *
  379. * @return int Number of deleted group members
  380. */
  381. function remove_from_group($group_id, $ids)
  382. {
  383. /* empty for address books don't supporting groups */
  384. return 0;
  385. }
  386. /**
  387. * Get group assignments of a specific contact record
  388. *
  389. * @param mixed Record identifier
  390. *
  391. * @return array List of assigned groups as ID=>Name pairs
  392. * @since 0.5-beta
  393. */
  394. function get_record_groups($id)
  395. {
  396. /* empty for address books don't supporting groups */
  397. return array();
  398. }
  399. /**
  400. * Utility function to return all values of a certain data column
  401. * either as flat list or grouped by subtype
  402. *
  403. * @param string Col name
  404. * @param array Record data array as used for saving
  405. * @param boolean True to return one array with all values, False for hash array with values grouped by type
  406. * @return array List of column values
  407. */
  408. public static function get_col_values($col, $data, $flat = false)
  409. {
  410. $out = array();
  411. foreach ((array)$data as $c => $values) {
  412. if ($c === $col || strpos($c, $col.':') === 0) {
  413. if ($flat) {
  414. $out = array_merge($out, (array)$values);
  415. }
  416. else {
  417. list(, $type) = explode(':', $c);
  418. $out[$type] = array_merge((array)$out[$type], (array)$values);
  419. }
  420. }
  421. }
  422. // remove duplicates
  423. if ($flat && !empty($out)) {
  424. $out = array_unique($out);
  425. }
  426. return $out;
  427. }
  428. /**
  429. * Normalize the given string for fulltext search.
  430. * Currently only optimized for Latin-1 characters; to be extended
  431. *
  432. * @param string Input string (UTF-8)
  433. * @return string Normalized string
  434. * @deprecated since 0.9-beta
  435. */
  436. protected static function normalize_string($str)
  437. {
  438. return rcube_utils::normalize_string($str);
  439. }
  440. /**
  441. * Compose a valid display name from the given structured contact data
  442. *
  443. * @param array Hash array with contact data as key-value pairs
  444. * @param bool Don't attempt to extract components from the email address
  445. *
  446. * @return string Display name
  447. */
  448. public static function compose_display_name($contact, $full_email = false)
  449. {
  450. $contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
  451. $fn = $contact['name'];
  452. if (!$fn) // default display name composition according to vcard standard
  453. $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
  454. // use email address part for name
  455. $email = self::get_col_values('email', $contact, true);
  456. $email = $email[0];
  457. if ($email && (empty($fn) || $fn == $email)) {
  458. // return full email
  459. if ($full_email)
  460. return $email;
  461. list($emailname) = explode('@', $email);
  462. if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
  463. $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
  464. else
  465. $fn = ucfirst($emailname);
  466. }
  467. return $fn;
  468. }
  469. /**
  470. * Compose the name to display in the contacts list for the given contact record.
  471. * This respects the settings parameter how to list conacts.
  472. *
  473. * @param array Hash array with contact data as key-value pairs
  474. * @return string List name
  475. */
  476. public static function compose_list_name($contact)
  477. {
  478. static $compose_mode;
  479. if (!isset($compose_mode)) // cache this
  480. $compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
  481. if ($compose_mode == 3)
  482. $fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
  483. else if ($compose_mode == 2)
  484. $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
  485. else if ($compose_mode == 1)
  486. $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
  487. else if ($compose_mode == 0)
  488. $fn = $contact['name'] ?: join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
  489. else {
  490. $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
  491. $fn = $plugin['fn'];
  492. }
  493. $fn = trim($fn, ', ');
  494. // fallbacks...
  495. if ($fn === '') {
  496. // ... display name
  497. if ($name = trim($contact['name'])) {
  498. $fn = $name;
  499. }
  500. // ... organization
  501. else if ($org = trim($contact['organization'])) {
  502. $fn = $org;
  503. }
  504. // ... email address
  505. else if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
  506. $fn = $email[0];
  507. }
  508. }
  509. return $fn;
  510. }
  511. /**
  512. * Build contact display name for autocomplete listing
  513. *
  514. * @param array Hash array with contact data as key-value pairs
  515. * @param string Optional email address
  516. * @param string Optional name (self::compose_list_name() result)
  517. * @param string Optional template to use (defaults to the 'contact_search_name' config option)
  518. *
  519. * @return string Display name
  520. */
  521. public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
  522. {
  523. static $template;
  524. if (empty($templ) && !isset($template)) { // cache this
  525. $template = rcube::get_instance()->config->get('contact_search_name');
  526. if (empty($template)) {
  527. $template = '{name} <{email}>';
  528. }
  529. }
  530. $result = $templ ?: $template;
  531. if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
  532. foreach ($matches[0] as $key) {
  533. $key = trim($key, '{}');
  534. $value = '';
  535. switch ($key) {
  536. case 'name':
  537. $value = $name ?: self::compose_list_name($contact);
  538. // If name(s) are undefined compose_list_name() may return an email address
  539. // here we prevent from returning the same name and email
  540. if ($name === $email && strpos($result, '{email}') !== false) {
  541. $value = '';
  542. }
  543. break;
  544. case 'email':
  545. $value = $email;
  546. break;
  547. }
  548. if (empty($value)) {
  549. $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
  550. if (is_array($value)) {
  551. $value = $value[0];
  552. }
  553. }
  554. $result = str_replace('{' . $key . '}', $value, $result);
  555. }
  556. }
  557. $result = preg_replace('/\s+/', ' ', $result);
  558. $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $result);
  559. $result = trim($result, '/ ');
  560. return $result;
  561. }
  562. /**
  563. * Create a unique key for sorting contacts
  564. */
  565. public static function compose_contact_key($contact, $sort_col)
  566. {
  567. $key = $contact[$sort_col] . ':' . $contact['sourceid'];
  568. // add email to a key to not skip contacts with the same name (#1488375)
  569. if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
  570. $key .= ':' . implode(':', (array)$email);
  571. }
  572. return $key;
  573. }
  574. /**
  575. * Compare search value with contact data
  576. *
  577. * @param string $colname Data name
  578. * @param string|array $value Data value
  579. * @param string $search Search value
  580. * @param int $mode Search mode
  581. *
  582. * @return bool Comparision result
  583. */
  584. protected function compare_search_value($colname, $value, $search, $mode)
  585. {
  586. // The value is a date string, for date we'll
  587. // use only strict comparison (mode = 1)
  588. // @TODO: partial search, e.g. match only day and month
  589. if (in_array($colname, $this->date_cols)) {
  590. return (($value = rcube_utils::anytodatetime($value))
  591. && ($search = rcube_utils::anytodatetime($search))
  592. && $value->format('Ymd') == $search->format('Ymd'));
  593. }
  594. // composite field, e.g. address
  595. foreach ((array)$value as $val) {
  596. $val = mb_strtolower($val);
  597. switch ($mode) {
  598. case 1:
  599. $got = ($val == $search);
  600. break;
  601. case 2:
  602. $got = ($search == substr($val, 0, strlen($search)));
  603. break;
  604. default:
  605. $got = (strpos($val, $search) !== false);
  606. }
  607. if ($got) {
  608. return true;
  609. }
  610. }
  611. return false;
  612. }
  613. }