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.

717 lines
22 KiB

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