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.

rcube_addressbook.php 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  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+/', ' ', $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+/', ' ', $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+/', ' ', $result);
  572. $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $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. }