2461 lines
75 KiB
PHP
2461 lines
75 KiB
PHP
<?php
|
|
/*
|
|
RCM CardDAV Plugin
|
|
Copyright (C) 2011-2016 Benjamin Schieder <rcmcarddav@wegwerf.anderdonau.de>,
|
|
Michael Stilkerich <ms@mike2k.de>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
require_once("carddav_common.php");
|
|
|
|
use \Sabre\VObject;
|
|
|
|
class carddav_backend extends rcube_addressbook
|
|
{
|
|
private static $helper;
|
|
|
|
// database primary key, used by RC to search by ID
|
|
public $primary_key = 'id';
|
|
public $coltypes;
|
|
private $fallbacktypes = array( 'email' => array('internet') );
|
|
|
|
// database ID of the addressbook
|
|
private $id;
|
|
// currently active search filter
|
|
private $filter;
|
|
|
|
private $result;
|
|
// configuration of the addressbook
|
|
private $config;
|
|
// The value of the global "sync_collection_workaround" preference.
|
|
// Defaults to false if the user comments it out.
|
|
private $sync_collection_workaround = false;
|
|
// custom labels defined in the addressbook
|
|
private $xlabels;
|
|
|
|
const SEPARATOR = ',';
|
|
|
|
// contains a the URIs, db ids and etags of the locally stored cards whenever
|
|
// a refresh from the server is attempted. This is used to avoid a separate
|
|
// "exists?" DB query for each card retrieved from the server and also allows
|
|
// to detect whether cards were deleted on the server
|
|
private $existing_card_cache = array();
|
|
// same thing for groups
|
|
private $existing_grpcard_cache = array();
|
|
// used in refresh DB to record group memberships for the delayed
|
|
// creation in the database (after all contacts have been loaded and
|
|
// stored from the server)
|
|
private $users_to_add;
|
|
|
|
// total number of contacts in address book
|
|
private $total_cards = -1;
|
|
// attributes that are redundantly stored in the contact table and need
|
|
// not be parsed from the vcard
|
|
private $table_cols = array('id', 'name', 'email', 'firstname', 'surname');
|
|
|
|
// maps VCard property names to roundcube keys
|
|
private $vcf2rc = array(
|
|
'simple' => array(
|
|
'BDAY' => 'birthday',
|
|
'FN' => 'name',
|
|
'NICKNAME' => 'nickname',
|
|
'NOTE' => 'notes',
|
|
'PHOTO' => 'photo',
|
|
'TITLE' => 'jobtitle',
|
|
'UID' => 'cuid',
|
|
'X-ABShowAs' => 'showas',
|
|
'X-ANNIVERSARY' => 'anniversary',
|
|
'X-ASSISTANT' => 'assistant',
|
|
'X-GENDER' => 'gender',
|
|
'X-MANAGER' => 'manager',
|
|
'X-SPOUSE' => 'spouse',
|
|
// the two kind attributes should not occur both in the same vcard
|
|
//'KIND' => 'kind', // VCard v4
|
|
'X-ADDRESSBOOKSERVER-KIND' => 'kind', // Apple Addressbook extension
|
|
),
|
|
'multi' => array(
|
|
'EMAIL' => 'email',
|
|
'TEL' => 'phone',
|
|
'URL' => 'website',
|
|
),
|
|
);
|
|
|
|
// array with list of potential date fields for formatting
|
|
private $datefields = array('birthday', 'anniversary');
|
|
|
|
public function __construct($dbid)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
$this->ready = $dbh && !$dbh->is_error();
|
|
$this->groups = true;
|
|
$this->readonly = false;
|
|
$this->id = $dbid;
|
|
|
|
$this->config = self::carddavconfig($dbid);
|
|
|
|
if ($this->config["needs_update"]){
|
|
$this->refreshdb_from_server();
|
|
}
|
|
|
|
$prefs = carddav_common::get_adminsettings();
|
|
if($this->config['presetname']) {
|
|
if($prefs[$this->config['presetname']]['readonly'])
|
|
$this->readonly = true;
|
|
}
|
|
|
|
if (isset($prefs['_GLOBAL']['sync_collection_workaround'])) {
|
|
$this->sync_collection_workaround =
|
|
$prefs['_GLOBAL']['sync_collection_workaround'];
|
|
}
|
|
|
|
$rc = rcmail::get_instance();
|
|
$this->coltypes = array( /* {{{ */
|
|
'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('name'), 'category' => 'main'),
|
|
'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('firstname'), 'category' => 'main'),
|
|
'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('surname'), 'category' => 'main'),
|
|
'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('email'), 'subtypes' => array('home','work','other','internet'), 'category' => 'main'),
|
|
'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('middlename'), 'category' => 'main'),
|
|
'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $rc->gettext('nameprefix'), 'category' => 'main'),
|
|
'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $rc->gettext('namesuffix'), 'category' => 'main'),
|
|
'nickname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('nickname'), 'category' => 'main'),
|
|
'jobtitle' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('jobtitle'), 'category' => 'main'),
|
|
'organization' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('organization'), 'category' => 'main'),
|
|
'department' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('department'), 'category' => 'main'),
|
|
'gender' => array('type' => 'select', 'limit' => 1, 'label' => $rc->gettext('gender'), 'options' => array('male' => $rc->gettext('male'), 'female' => $rc->gettext('female')), 'category' => 'personal'),
|
|
'phone' => array('type' => 'text', 'size' => 40, 'maxlength' => 20, 'label' => $rc->gettext('phone'), 'subtypes' => array('home','home2','work','work2','mobile','cell','main','homefax','workfax','car','pager','video','assistant','other'), 'category' => 'main'),
|
|
'address' => array('type' => 'composite', 'label' => $rc->gettext('address'), 'subtypes' => array('home','work','other'), 'childs' => array(
|
|
'street' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('street'), 'category' => 'main'),
|
|
'locality' => array('type' => 'text', 'size' => 28, 'maxlength' => 50, 'label' => $rc->gettext('locality'), 'category' => 'main'),
|
|
'zipcode' => array('type' => 'text', 'size' => 8, 'maxlength' => 15, 'label' => $rc->gettext('zipcode'), 'category' => 'main'),
|
|
'region' => array('type' => 'text', 'size' => 12, 'maxlength' => 50, 'label' => $rc->gettext('region'), 'category' => 'main'),
|
|
'country' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('country'), 'category' => 'main'),), 'category' => 'main'),
|
|
'birthday' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $rc->gettext('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
|
|
'anniversary' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $rc->gettext('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
|
|
'website' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('website'), 'subtypes' => array('homepage','work','blog','profile','other'), 'category' => 'main'),
|
|
'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'maxlength' => 500, 'label' => $rc->gettext('notes'), 'limit' => 1),
|
|
'photo' => array('type' => 'image', 'limit' => 1, 'category' => 'main'),
|
|
'assistant' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('assistant'), 'category' => 'personal'),
|
|
'manager' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('manager'), 'category' => 'personal'),
|
|
'spouse' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('spouse'), 'category' => 'personal'),
|
|
// TODO: define fields for vcards like GEO, KEY
|
|
); /* }}} */
|
|
$this->addextrasubtypes();
|
|
}}}
|
|
|
|
/**
|
|
* Stores a custom label in the database (X-ABLabel extension).
|
|
*
|
|
* @param string Name of the type/category (phone,address,email)
|
|
* @param string Name of the custom label to store for the type
|
|
*/
|
|
private function storeextrasubtype($typename, $subtype)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
$sql_result = $dbh->query('INSERT INTO ' .
|
|
$dbh->table_name('carddav_xsubtypes') .
|
|
' (typename,subtype,abook_id) VALUES (?,?,?)',
|
|
$typename, $subtype, $this->id);
|
|
}}}
|
|
|
|
/**
|
|
* Adds known custom labels to the roundcube subtype list (X-ABLabel extension).
|
|
*
|
|
* Reads the previously seen custom labels from the database and adds them to the
|
|
* roundcube subtype list in #coltypes and additionally stores them in the #xlabels
|
|
* list.
|
|
*/
|
|
private function addextrasubtypes()
|
|
{{{
|
|
$this->xlabels = array();
|
|
|
|
foreach($this->coltypes as $k => $v) {
|
|
if(array_key_exists('subtypes', $v)) {
|
|
$this->xlabels[$k] = array();
|
|
} }
|
|
|
|
// read extra subtypes
|
|
$xtypes = self::get_dbrecord($this->id,'typename,subtype','xsubtypes',false,'abook_id');
|
|
|
|
foreach ($xtypes as $row) {
|
|
$this->coltypes[$row['typename']]['subtypes'][] = $row['subtype'];
|
|
$this->xlabels[$row['typename']][] = $row['subtype'];
|
|
}
|
|
}}}
|
|
|
|
/**
|
|
* Returns addressbook name (e.g. for addressbooks listing).
|
|
*
|
|
* @return string name of this addressbook
|
|
*/
|
|
public function get_name()
|
|
{{{
|
|
return $this->config['name'];
|
|
}}}
|
|
|
|
/**
|
|
* Save a search string for future listings.
|
|
*
|
|
* @param mixed Search params to use in listing method, obtained by get_search_set()
|
|
*/
|
|
public function set_search_set($filter)
|
|
{{{
|
|
$this->filter = $filter;
|
|
$this->total_cards = -1;
|
|
}}}
|
|
|
|
/**
|
|
* Getter for saved search properties
|
|
*
|
|
* @return mixed Search properties used by this class
|
|
*/
|
|
public function get_search_set()
|
|
{{{
|
|
return $this->filter;
|
|
}}}
|
|
|
|
/**
|
|
* Reset saved results and search parameters
|
|
*/
|
|
public function reset()
|
|
{{{
|
|
$this->result = null;
|
|
$this->filter = null;
|
|
$this->total_cards = -1;
|
|
}}}
|
|
|
|
/**
|
|
* Determines the name to be displayed for a contact. The routine
|
|
* distinguishes contact cards for individuals from organizations.
|
|
*/
|
|
private function set_displayname(&$save_data)
|
|
{{{
|
|
if(strcasecmp($save_data['showas'], 'COMPANY') == 0 && strlen($save_data['organization'])>0) {
|
|
$save_data['name'] = $save_data['organization'];
|
|
}
|
|
|
|
// we need a displayname; if we do not have one, try to make one up
|
|
if(strlen($save_data['name']) == 0) {
|
|
$dname = array();
|
|
if(strlen($save_data['firstname'])>0)
|
|
$dname[] = $save_data['firstname'];
|
|
if(strlen($save_data['surname'])>0)
|
|
$dname[] = $save_data['surname'];
|
|
|
|
if(count($dname) > 0) {
|
|
$save_data['name'] = implode(' ', $dname);
|
|
|
|
} else { // no name? try email and phone
|
|
$ep_keys = array_keys($save_data);
|
|
$ep_keys = preg_grep(";^(email|phone):;", $ep_keys);
|
|
sort($ep_keys, SORT_STRING);
|
|
foreach($ep_keys as $ep_key) {
|
|
$ep_vals = $save_data[$ep_key];
|
|
if(!is_array($ep_vals)) $ep_vals = array($ep_vals);
|
|
|
|
foreach($ep_vals as $ep_val) {
|
|
if(strlen($ep_val)>0) {
|
|
$save_data['name'] = $ep_val;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// still no name? set to unknown and hope the user will fix it
|
|
if(strlen($save_data['name']) == 0)
|
|
$save_data['name'] = 'Unset Displayname';
|
|
}
|
|
}}}
|
|
|
|
/**
|
|
* Stores a group vcard in the database.
|
|
*
|
|
* @param string etag of the VCard in the given version on the CardDAV server
|
|
* @param string path to the VCard on the CardDAV server
|
|
* @param string string representation of the VCard
|
|
* @param array associative array containing at least name and cuid (card UID)
|
|
* @param int optionally, database id of the group if the store operation is an update
|
|
*
|
|
* @return int The database id of the created or updated card, false on error.
|
|
*/
|
|
private function dbstore_group($etag, $uri, $vcfstr, $save_data, $dbid=0)
|
|
{{{
|
|
return $this->dbstore_base('groups',$etag,$uri,$vcfstr,$save_data,$dbid);
|
|
}}}
|
|
|
|
private function dbstore_base($table, $etag, $uri, $vcfstr, $save_data, $dbid=0, $xcol=array(), $xval=array())
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
// get rid of the %u placeholder in the URI, otherwise the refresh operation
|
|
// will not be able to match local cards with those provided by the server
|
|
$username = $this->config['username'];
|
|
if($username === "%u")
|
|
$username = $_SESSION['username'];
|
|
$uri = str_replace("%u", $username, $uri);
|
|
|
|
$xcol[]='name'; $xval[]=$save_data['name'];
|
|
$xcol[]='etag'; $xval[]=$etag;
|
|
$xcol[]='vcard'; $xval[]=$vcfstr;
|
|
|
|
if($dbid) {
|
|
self::$helper->debug("UPDATE card $uri");
|
|
$xval[]=$dbid;
|
|
$sql_result = $dbh->query('UPDATE ' .
|
|
$dbh->table_name("carddav_$table") .
|
|
' SET ' . implode('=?,', $xcol) . '=?' .
|
|
' WHERE id=?', $xval);
|
|
|
|
} else {
|
|
self::$helper->debug("INSERT card $uri");
|
|
if ("x".$save_data['cuid'] == "x"){
|
|
// There is no contact UID in the VCARD, try to create one
|
|
$cuid = $uri;
|
|
$cuid = preg_replace(';^.*/;', "", $cuid);
|
|
$cuid = preg_replace(';\.vcf$;', "", $cuid);
|
|
$save_data['cuid'] = $cuid;
|
|
}
|
|
$xcol[]='abook_id'; $xval[]=$this->id;
|
|
$xcol[]='uri'; $xval[]=$uri;
|
|
$xcol[]='cuid'; $xval[]=$save_data['cuid'];
|
|
|
|
$sql_result = $dbh->query('INSERT INTO ' .
|
|
$dbh->table_name("carddav_$table") .
|
|
' (' . implode(',',$xcol) . ') VALUES (?' . str_repeat(',?', count($xcol)-1) .')',
|
|
$xval);
|
|
|
|
$dbid = $dbh->insert_id("carddav_$table");
|
|
}
|
|
|
|
if($dbh->is_error()) {
|
|
self::$helper->warn($dbh->is_error());
|
|
$this->set_error(self::ERROR_SAVING, $dbh->is_error());
|
|
return false;
|
|
}
|
|
|
|
return $dbid;
|
|
}}}
|
|
|
|
/**
|
|
* Stores a contact to the local database.
|
|
*
|
|
* @param string etag of the VCard in the given version on the CardDAV server
|
|
* @param string path to the VCard on the CardDAV server
|
|
* @param string string representation of the VCard
|
|
* @param array associative array containing the roundcube save data for the contact
|
|
* @param int optionally, database id of the contact if the store operation is an update
|
|
*
|
|
* @return int The database id of the created or updated card, false on error.
|
|
*/
|
|
private function dbstore_contact($etag, $uri, $vcfstr, $save_data, $dbid=0)
|
|
{{{
|
|
$this->preprocess_rc_savedata($save_data);
|
|
// build email search string
|
|
$email_keys = preg_grep('/^email(:|$)/', array_keys($save_data));
|
|
$email_addrs = array();
|
|
foreach($email_keys as $email_key) {
|
|
$email_addrs = array_merge($email_addrs, (array) $save_data[$email_key]);
|
|
}
|
|
$save_data['email'] = implode(', ', $email_addrs);
|
|
|
|
// extra columns for the contacts table
|
|
$xcol_all=array('firstname','surname','organization','showas','email');
|
|
$xcol=array();
|
|
$xval=array();
|
|
foreach($xcol_all as $k) {
|
|
if(array_key_exists($k,$save_data)) {
|
|
$xcol[] = $k;
|
|
$xval[] = $save_data[$k];
|
|
} }
|
|
|
|
return $this->dbstore_base('contacts',$etag,$uri,$vcfstr,$save_data,$dbid,$xcol,$xval);
|
|
}}}
|
|
|
|
/**
|
|
* Checks if the given local card cache (for contacts or groups) contains
|
|
* a card with the given URI. If not, the function returns false.
|
|
* If yes, the card is marked seen in the cache, and the cached etag is
|
|
* compared with the given one. The function returns an associative array
|
|
* with the database id of the existing card (key dbid) and a boolean that
|
|
* indicates whether the card needs a server refresh as determined by the
|
|
* etag comparison (key needs_update).
|
|
*/
|
|
private static function checkcache(&$cache, $uri, $etag)
|
|
{{{
|
|
if(!array_key_exists($uri, $cache))
|
|
return false;
|
|
|
|
$cache[$uri]['seen'] = true;
|
|
|
|
$dbrec = $cache[$uri];
|
|
$dbid = $dbrec['id'];
|
|
|
|
$needsupd = true;
|
|
|
|
// abort if card has not changed
|
|
if($etag === $dbrec['etag']) {
|
|
self::$helper->debug("UNCHANGED card $uri");
|
|
$needsupd = false;
|
|
}
|
|
return array('needs_update'=>$needsupd, 'dbid'=>$dbid);
|
|
}}}
|
|
|
|
/**
|
|
* Synchronizes the local card store with the CardDAV server.
|
|
*/
|
|
private function refreshdb_from_server()
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
$duration = time();
|
|
|
|
// determine existing local contact URIs and ETAGs
|
|
$contacts = self::get_dbrecord($this->id,'id,uri,etag','contacts',false,'abook_id');
|
|
foreach($contacts as $contact) {
|
|
$this->existing_card_cache[$contact['uri']] = $contact;
|
|
}
|
|
|
|
if(!$this->config['use_categories']) {
|
|
// determine existing local group URIs and ETAGs
|
|
$groups = self::get_dbrecord($this->id,'id,uri,etag','groups',false,'abook_id');
|
|
foreach($groups as $group) {
|
|
$this->existing_grpcard_cache[$group['uri']] = $group;
|
|
}
|
|
}
|
|
|
|
// used to record which users need to be added to which groups
|
|
$this->users_to_add = array();
|
|
|
|
// Check for supported-report-set and only use sync-collection if server advertises it.
|
|
// This suppresses 501 Not implemented errors with ownCloud.
|
|
$opts = array(
|
|
'method'=>"PROPFIND",
|
|
'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'),
|
|
'content'=> <<<EOF
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<propfind xmlns="DAV:"> <prop>
|
|
<supported-report-set/>
|
|
</prop> </propfind>
|
|
EOF
|
|
);
|
|
$reply = self::$helper->cdfopen($this->config['url'], $opts, $this->config);
|
|
|
|
$records = -1;
|
|
|
|
$xml = self::$helper->checkAndParseXML($reply);
|
|
if($xml !== false) {
|
|
$xpresult = $xml->xpath('//RCMCD:supported-report/RCMCD:report/RCMCD:sync-collection');
|
|
// To avoid sync-collection, we can simply skip the next line
|
|
// leaving $records = -1 which will trigger a call to
|
|
// list_records_propfind() below.
|
|
if(count($xpresult) > 0 && !$this->sync_collection_workaround) {
|
|
$records = $this->list_records_sync_collection();
|
|
}
|
|
}
|
|
|
|
// sync-collection not supported or returned error
|
|
if ($records < 0){
|
|
$records = $this->list_records_propfind();
|
|
}
|
|
|
|
foreach($this->users_to_add as $dbid => $cuids) {
|
|
if(count($cuids)<=0) continue;
|
|
$sql_result = $dbh->query('INSERT INTO '.
|
|
$dbh->table_name('carddav_group_user') .
|
|
' (group_id,contact_id) SELECT ?,id from ' .
|
|
$dbh->table_name('carddav_contacts') .
|
|
' WHERE abook_id=? AND cuid IN (' . implode(',', $cuids) . ')', $dbid, $this->id);
|
|
self::$helper->debug("Added " . $dbh->affected_rows($sql_result) . " contacts to group $dbid");
|
|
}
|
|
|
|
unset($this->users_to_add);
|
|
$this->existing_card_cache = array();
|
|
$this->existing_grpcard_cache = array();
|
|
|
|
// set last_updated timestamp
|
|
$dbh->query('UPDATE ' .
|
|
$dbh->table_name('carddav_addressbooks') .
|
|
' SET last_updated=' . $dbh->now() .' WHERE id=?',
|
|
$this->id);
|
|
|
|
$duration = time() - $duration;
|
|
self::$helper->debug("server refresh took $duration seconds");
|
|
if($records < 0) {
|
|
self::$helper->warn("Errors occurred during the refresh of addressbook " . $this->id);
|
|
}
|
|
}}}
|
|
|
|
/**
|
|
* List the current set of contact records
|
|
*
|
|
* @param array List of cols to show, Null means all
|
|
* @param int Only return this number of records, use negative values for tail
|
|
* @param boolean True to skip the count query (select only)
|
|
* @return array Indexed list of contact records, each a hash array
|
|
*/
|
|
public function list_records($cols=array(), $subset=0, $nocount=false)
|
|
{{{
|
|
// refresh from server if refresh interval passed
|
|
if ( $this->config['needs_update'] == 1 )
|
|
$this->refreshdb_from_server();
|
|
|
|
// XXX workaround for a roundcube bug to support roundcube's displayname setting
|
|
// Reported as Roundcube Ticket #1488394
|
|
if(count($cols)>0) {
|
|
if(!in_array('firstname', $cols)) {
|
|
$cols[] = 'firstname';
|
|
}
|
|
if(!in_array('surname', $cols)) {
|
|
$cols[] = 'surname';
|
|
}
|
|
}
|
|
// XXX workaround for a roundcube bug to support roundcube's displayname setting
|
|
|
|
// if the count is not requested we can save one query
|
|
if($nocount)
|
|
$this->result = new rcube_result_set();
|
|
else
|
|
$this->result = $this->count();
|
|
|
|
$records = $this->list_records_readdb($cols,$subset);
|
|
if($nocount) {
|
|
$this->result->count = $records;
|
|
|
|
} else if ($this->list_page <= 1) {
|
|
if ($records < $this->page_size && $subset == 0)
|
|
$this->result->count = $records;
|
|
else
|
|
$this->result->count = $this->_count($cols);
|
|
}
|
|
|
|
if ($records > 0){
|
|
return $this->result;
|
|
}
|
|
|
|
return false;
|
|
}}}
|
|
|
|
/**
|
|
* Retrieves the Card URIs from the CardDAV server
|
|
*
|
|
* @return int number of cards in collection, -1 on error
|
|
*/
|
|
private function list_records_sync_collection()
|
|
{{{
|
|
$sync_token = $this->config['sync_token'];
|
|
|
|
while(true) {
|
|
$opts = array(
|
|
'method'=>"REPORT",
|
|
'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'),
|
|
'content'=> <<<EOF
|
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
<D:sync-collection xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
|
<D:sync-token>$sync_token</D:sync-token>
|
|
<D:sync-level>1</D:sync-level>
|
|
<D:prop>
|
|
<D:getcontenttype/>
|
|
<D:getetag/>
|
|
</D:prop>
|
|
</D:sync-collection>
|
|
EOF
|
|
);
|
|
|
|
$reply = self::$helper->cdfopen($this->config['url'], $opts, $this->config);
|
|
|
|
$xml = self::$helper->checkAndParseXML($reply);
|
|
|
|
if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) {
|
|
// a server may invalidate old sync-tokens, in which case we need to do a full resync
|
|
if (strlen($sync_token)>0 && ($reply == 412 || (is_array($reply) && $reply["status"] == 412))){
|
|
self::$helper->warn("Server reported invalid sync-token in sync of addressbook " . $this->config['abookid'] . ". Resorting to full resync.");
|
|
$sync_token = '';
|
|
continue;
|
|
} else {
|
|
$errorstatus = is_array($reply) ? $reply["status"] : $reply;
|
|
self::$helper->warn("An error (status " . $errorstatus . ") occured while retrieving the sync-token of addressbook " . $this->config['abookid'] . ". Sync-collection synchronization aborted. Will use propfind synchronization instead.");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
list($new_sync_token) = $xml->xpath('//RCMCD:sync-token');
|
|
|
|
$records = $this->addvcards($xml);
|
|
|
|
if(strlen($sync_token) == 0) {
|
|
if($records>=0) {
|
|
$this->delete_unseen();
|
|
}
|
|
} else {
|
|
$this->delete_synccoll($xml);
|
|
}
|
|
|
|
if($records >= 0) {
|
|
carddav::update_abook($this->config['abookid'], array('sync_token' => "$new_sync_token"));
|
|
|
|
// if we got a truncated result set continue sync
|
|
$xpresult = $xml->xpath('//RCMCD:response[contains(child::RCMCD:status, " 507 Insufficient Storage")]');
|
|
if(count($xpresult) > 0) {
|
|
$sync_token = "$new_sync_token";
|
|
continue;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
return $records;
|
|
}}}
|
|
|
|
private function list_records_readdb($cols, $subset=0, $count_only=false)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
// true if we can use DB filtering or no filtering is requested
|
|
$filter = $this->get_search_set();
|
|
$this->determine_filter_params($cols,$subset, $firstrow, $numrows, $read_vcard);
|
|
|
|
$dbattr = $read_vcard ? 'vcard' : 'firstname,surname,email';
|
|
|
|
$limit_index = $firstrow;
|
|
$limit_rows = $numrows;
|
|
|
|
$xfrom = '';
|
|
$xwhere = '';
|
|
if($this->group_id) {
|
|
$xfrom = ',' . $dbh->table_name('carddav_group_user');
|
|
$xwhere = ' AND id=contact_id AND group_id=' . $dbh->quote($this->group_id) . ' ';
|
|
}
|
|
|
|
if ($this->config['presetname']){
|
|
$prefs = carddav_common::get_adminsettings();
|
|
if (array_key_exists("require_always", $prefs[$this->config['presetname']])){
|
|
foreach ($prefs[$this->config['presetname']]["require_always"] as $col){
|
|
$xwhere .= " AND $col <> ".$dbh->quote('')." ";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Workaround for Roundcube versions < 0.7.2
|
|
$sort_column = $this->sort_col ? $this->sort_col : 'surname';
|
|
$sort_order = $this->sort_order ? $this->sort_order : 'ASC';
|
|
|
|
$sql_result = $dbh->limitquery("SELECT id,name,$dbattr FROM " .
|
|
$dbh->table_name('carddav_contacts') . $xfrom .
|
|
' WHERE abook_id=? ' . $xwhere .
|
|
($this->filter ? " AND (".$this->filter.")" : "") .
|
|
" ORDER BY (CASE WHEN showas='COMPANY' THEN organization ELSE " . $sort_column . " END) "
|
|
. $sort_order,
|
|
$limit_index,
|
|
$limit_rows,
|
|
$this->id
|
|
);
|
|
|
|
$addresses = array();
|
|
while($contact = $dbh->fetch_assoc($sql_result)) {
|
|
if($read_vcard) {
|
|
$save_data = $this->create_save_data_from_vcard($contact['vcard']);
|
|
if (!$save_data){
|
|
self::$helper->warn("Couldn't parse vcard ".$contact['vcard']);
|
|
continue;
|
|
}
|
|
|
|
// needed by the calendar plugin
|
|
if(is_array($cols) && in_array('vcard', $cols)) {
|
|
$save_data['save_data']['vcard'] = $contact['vcard'];
|
|
}
|
|
|
|
$save_data = $save_data['save_data'];
|
|
} else {
|
|
$save_data = array();
|
|
foreach ($cols as $col) {
|
|
if(strcmp($col,'email')==0)
|
|
$save_data[$col] = preg_split('/,\s*/', $contact[$col]);
|
|
else
|
|
$save_data[$col] = $contact[$col];
|
|
}
|
|
}
|
|
$addresses[] = array('ID' => $contact['id'], 'name' => $contact['name'], 'save_data' => $save_data);
|
|
}
|
|
|
|
if(!$count_only) {
|
|
// create results for roundcube
|
|
foreach($addresses as $a) {
|
|
$a['save_data']['ID'] = $a['ID'];
|
|
$this->result->add($a['save_data']);
|
|
}
|
|
}
|
|
return count($addresses);
|
|
}}}
|
|
|
|
private function query_addressbook_multiget($hrefs)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
$hrefstr = '';
|
|
foreach ($hrefs as $href) {
|
|
$hrefstr .= "<D:href>$href</D:href>\n";
|
|
}
|
|
|
|
$optsREPORT = array(
|
|
'method'=>"REPORT",
|
|
'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'),
|
|
'content'=> <<<EOF
|
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
|
<D:prop>
|
|
<D:getetag/>
|
|
<C:address-data>
|
|
<C:allprop/>
|
|
</C:address-data>
|
|
</D:prop>
|
|
$hrefstr
|
|
</C:addressbook-multiget>
|
|
EOF
|
|
);
|
|
|
|
$reply = self::$helper->cdfopen($this->config['url'], $optsREPORT, $this->config);
|
|
$xml = self::$helper->checkAndParseXML($reply);
|
|
if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) {
|
|
$errorstatus = is_array($reply) ? $reply["status"] : $reply;
|
|
rcmail::write_log("carddav", "An error (status " . $errorstatus . ") occured while retrieving vcards for addressbook " . $this->config['abookid'] . ". Synchronization aborted.");
|
|
return -1;
|
|
}
|
|
|
|
$xpresult = $xml->xpath('//RCMCD:response[descendant::RCMCC:address-data]');
|
|
|
|
$numcards = 0;
|
|
foreach ($xpresult as $vcard) {
|
|
self::$helper->registerNamespaces($vcard);
|
|
list($href) = $vcard->xpath('child::RCMCD:href');
|
|
list($etag) = $vcard->xpath('descendant::RCMCD:getetag');
|
|
list($vcf) = $vcard->xpath('descendant::RCMCC:address-data');
|
|
|
|
// determine database ID of existing cards by checking the cache
|
|
$dbid = 0;
|
|
if( ($ret = self::checkcache($this->existing_card_cache,"$href","$etag"))
|
|
|| ($ret = self::checkcache($this->existing_grpcard_cache,"$href","$etag")) ) {
|
|
$dbid = $ret['dbid'];
|
|
}
|
|
|
|
// changed on server, parse VCF
|
|
$save_data = $this->create_save_data_from_vcard("$vcf");
|
|
$vcfobj = $save_data['vcf'];
|
|
if($save_data['needs_update'])
|
|
$vcf = $vcfobj->serialize();
|
|
$save_data = $save_data['save_data'];
|
|
|
|
if($save_data['kind'] === 'group') {
|
|
if(!$this->config['use_categories']) {
|
|
self::$helper->debug('Processing Group ' . $save_data['name']);
|
|
// delete current group members (will be reinserted if needed below)
|
|
if($dbid) self::delete_dbrecord($dbid,'group_user','group_id');
|
|
|
|
// store group card
|
|
if(!($dbid = $this->dbstore_group("$etag","$href","$vcf",$save_data,$dbid)))
|
|
return -1;
|
|
|
|
// record group members for deferred store
|
|
$this->users_to_add[$dbid] = array();
|
|
$members = $vcfobj->{'X-ADDRESSBOOKSERVER-MEMBER'};
|
|
if ($members === null) {
|
|
$members = array();
|
|
}
|
|
|
|
self::$helper->debug("Group $dbid has " . count($members) . " members");
|
|
foreach($members as $mbr) {
|
|
$mbr = preg_split('/:/', $mbr);
|
|
if(!$mbr) continue;
|
|
if(count($mbr)!=3 || $mbr[0] !== 'urn' || $mbr[1] !== 'uuid') {
|
|
self::$helper->warn("don't know how to interpret group membership: " . implode(':', $mbr));
|
|
continue;
|
|
}
|
|
$this->users_to_add[$dbid][] = $dbh->quote($mbr[2]);
|
|
}
|
|
}
|
|
} else { // individual/other
|
|
if (trim($save_data['name']) == '') { // roundcube display fix for contacts that don't have first/last names
|
|
if ($save_data['nickname'] !== NULL && trim($save_data['nickname'] !== '')) {
|
|
$save_data['name'] = $save_data['nickname'];
|
|
} else {
|
|
foreach ($save_data as $key=>$val) {
|
|
if (strpos($key,'email') !== false) {
|
|
$save_data['name'] = $val[0];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if($this->config['use_categories']) {
|
|
// delete current member from groups (will be reinserted if needed below)
|
|
self::delete_dbrecord($dbid,'group_user','contact_id');
|
|
foreach ($this->getCategories($vcfobj) as $category) {
|
|
if($category !== "All" && $category !== "Unfiled") {
|
|
$record = self::get_dbrecord($category, 'id', 'groups', true, 'name', array('abook_id' => $this->config['abookid']));
|
|
if(!$record) {
|
|
$cuid = $this->find_free_uid();
|
|
$uri = "$cuid.vcf";
|
|
|
|
$gsave_data = array(
|
|
'name' => $category,
|
|
'kind' => 'group',
|
|
'cuid' => $cuid,
|
|
);
|
|
$url = carddav_common::concaturl($this->config['url'], $uri);
|
|
$url = preg_replace(';https?://[^/]+;', '', $url);
|
|
// store group card
|
|
$vcfg = $this->create_vcard_from_save_data($gsave_data);
|
|
$vcfgstr = $vcfg->serialize();
|
|
if(!($database = $this->dbstore_group("dummy",$url,$vcfgstr,$gsave_data)))
|
|
return -1;
|
|
} else {
|
|
$database = $record['id'];
|
|
}
|
|
if(!isset($this->users_to_add[$database])) {
|
|
$this->users_to_add[$database] = array();
|
|
}
|
|
$uid = $save_data['cuid'];
|
|
$this->users_to_add[$database][] = $dbh->quote($uid);
|
|
}
|
|
}
|
|
}
|
|
if(!$this->dbstore_contact("$etag","$href","$vcf",$save_data,$dbid))
|
|
return -1;
|
|
}
|
|
$numcards++;
|
|
}
|
|
return $numcards;
|
|
}}}
|
|
|
|
private function list_records_propfind()
|
|
{{{
|
|
$opts = array(
|
|
'method'=>"PROPFIND",
|
|
'header'=>array("Depth: 1", 'Content-Type: application/xml; charset="utf-8"'),
|
|
'content'=> <<<EOF
|
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
<a:propfind xmlns:a="DAV:"> <a:prop>
|
|
<a:getcontenttype/>
|
|
<a:getetag/>
|
|
</a:prop> </a:propfind>
|
|
EOF
|
|
);
|
|
|
|
$reply = self::$helper->cdfopen("", $opts, $this->config);
|
|
$xml = self::$helper->checkAndParseXML($reply);
|
|
if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) {
|
|
$errorstatus = is_array($reply) ? $reply["status"] : $reply;
|
|
rcmail::write_log("carddav", "An error (status " . $errorstatus . ") occured while retrieving the vcard list for addressbook " . $this->config['abookid'] . ". Synchronization aborted.");
|
|
return -1;
|
|
}
|
|
$records = $this->addvcards($xml);
|
|
if($records>=0) {
|
|
$this->delete_unseen();
|
|
}
|
|
|
|
return $records;
|
|
}}}
|
|
|
|
private function addvcards($xml)
|
|
{{{
|
|
$records = 0;
|
|
$urls = array();
|
|
$xpresult = $xml->xpath('//RCMCD:response[starts-with(translate(child::RCMCD:propstat/RCMCD:status, "ABCDEFGHJIKLMNOPQRSTUVWXYZ", "abcdefghjiklmnopqrstuvwxyz"), "http/1.1 200 ") and child::RCMCD:propstat/RCMCD:prop/RCMCD:getetag]');
|
|
foreach ($xpresult as $r) {
|
|
self::$helper->registerNamespaces($r);
|
|
|
|
list($href) = $r->xpath('child::RCMCD:href');
|
|
if(preg_match('/\/$/', $href)) continue;
|
|
|
|
list($etag) = $r->xpath('descendant::RCMCD:getetag');
|
|
|
|
$ret = self::checkcache($this->existing_card_cache,"$href","$etag");
|
|
$retgrp = self::checkcache($this->existing_grpcard_cache,"$href","$etag");
|
|
|
|
if( ($ret===false && $retgrp===false)
|
|
|| (is_array($ret) && $ret['needs_update'])
|
|
|| (is_array($retgrp) && $retgrp['needs_update']) )
|
|
{
|
|
$urls[] = "$href";
|
|
}
|
|
}
|
|
|
|
if (count($urls) > 0) {
|
|
$records = $this->query_addressbook_multiget($urls);
|
|
}
|
|
|
|
return $records;
|
|
}}}
|
|
|
|
/** delete cards not present on the server anymore */
|
|
private function delete_unseen()
|
|
{{{
|
|
$delids = array();
|
|
foreach($this->existing_card_cache as $value) {
|
|
if(!array_key_exists('seen', $value) || !$value['seen']) {
|
|
$delids[] = $value['id'];
|
|
}
|
|
}
|
|
$del = self::delete_dbrecord($delids);
|
|
self::$helper->debug("deleted $del contacts during server refresh");
|
|
|
|
$delids = array();
|
|
foreach($this->existing_grpcard_cache as $value) {
|
|
if(!array_key_exists('seen', $value) || !$value['seen']) {
|
|
$delids[] = $value['id'];
|
|
}
|
|
}
|
|
$del = self::delete_dbrecord($delids,'groups');
|
|
self::$helper->debug("deleted $del groups during server refresh");
|
|
}}}
|
|
|
|
/** delete cards reported deleted by the server */
|
|
private function delete_synccoll($xml)
|
|
{{{
|
|
$xpresult = $xml->xpath('//RCMCD:response[contains(child::RCMCD:status, " 404 Not Found")]');
|
|
$del_contacts = array();
|
|
$del_groups = array();
|
|
|
|
foreach ($xpresult as $r) {
|
|
self::$helper->registerNamespaces($r);
|
|
|
|
list($href) = $r->xpath('child::RCMCD:href');
|
|
if(preg_match('/\/$/', $href)) continue;
|
|
|
|
if(isset($this->existing_card_cache["$href"])) {
|
|
$del_contacts[] = $this->existing_card_cache["$href"]['id'];
|
|
} else if(isset($this->existing_grpcard_cache["$href"])) {
|
|
$del_groups[] = $this->existing_grpcard_cache["$href"]['id'];
|
|
}
|
|
}
|
|
$del = self::delete_dbrecord($del_contacts);
|
|
self::$helper->debug("deleted $del contacts during incremental server refresh");
|
|
$del = self::delete_dbrecord($del_groups,'groups');
|
|
self::$helper->debug("deleted $del groups during incremental server refresh");
|
|
}}}
|
|
|
|
/**
|
|
* Search contacts
|
|
*
|
|
* @param mixed $fields The field name of array of field names to search in
|
|
* @param mixed $value Search value (or array of values when $fields is array)
|
|
* @param int $mode Matching mode:
|
|
* 0 - partial (*abc*),
|
|
* 1 - strict (=),
|
|
* 2 - prefix (abc*)
|
|
* @param boolean $select True if results are requested, False if count only
|
|
* @param boolean $nocount True to skip the count query (select only)
|
|
* @param array $required List of fields that cannot be empty
|
|
*
|
|
* @return object rcube_result_set Contact records and 'count' value
|
|
*/
|
|
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
if (!is_array($fields))
|
|
$fields = array($fields);
|
|
if (!is_array($required) && !empty($required))
|
|
$required = array($required);
|
|
|
|
$where = $and_where = array();
|
|
$mode = intval($mode);
|
|
$WS = ' ';
|
|
$AS = self::SEPARATOR;
|
|
|
|
// build the $where array; each of its entries is an SQL search condition
|
|
foreach ($fields as $idx => $col) {
|
|
// direct ID search
|
|
if ($col == 'ID' || $col == $this->primary_key) {
|
|
$ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
|
|
$ids = $dbh->array2list($ids, 'integer');
|
|
$where[] = $this->primary_key.' IN ('.$ids.')';
|
|
continue;
|
|
}
|
|
|
|
$val = is_array($value) ? $value[$idx] : $value;
|
|
// table column
|
|
if (in_array($col, $this->table_cols)) {
|
|
if ($mode & 1) {
|
|
// strict
|
|
$where[] =
|
|
// exact match 'name@domain.com'
|
|
'(' . $dbh->ilike($col, $val)
|
|
// line beginning match 'name@domain.com,%'
|
|
. ' OR ' . $dbh->ilike($col, $val . $AS . '%')
|
|
// middle match '%, name@domain.com,%'
|
|
. ' OR ' . $dbh->ilike($col, '%' . $AS . $WS . $val . $AS . '%')
|
|
// line end match '%, name@domain.com'
|
|
. ' OR ' . $dbh->ilike($col, '%' . $AS . $WS . $val) . ')';
|
|
} elseif ($mode & 2) {
|
|
// prefix
|
|
$where[] = '(' . $dbh->ilike($col, $val . '%')
|
|
. ' OR ' . $dbh->ilike($col, $AS . $WS . $val . '%') . ')';
|
|
} else {
|
|
// partial
|
|
$where[] = $dbh->ilike($col, '%' . $val . '%');
|
|
}
|
|
}
|
|
// vCard field
|
|
else {
|
|
foreach (explode(" ", self::normalize_string($val)) as $word) {
|
|
if ($mode & 1) {
|
|
// strict
|
|
$words[] = '(' . $dbh->ilike('vcard', $word . $WS . '%')
|
|
. ' OR ' . $dbh->ilike('vcard', '%' . $AS . $WS . $word . $WS .'%')
|
|
. ' OR ' . $dbh->ilike('vcard', '%' . $AS . $WS . $word) . ')';
|
|
} elseif ($mode & 2) {
|
|
// prefix
|
|
$words[] = '(' . $dbh->ilike('vcard', $word . '%')
|
|
. ' OR ' . $dbh->ilike('vcard', $AS . $WS . $word . '%') . ')';
|
|
} else {
|
|
// partial
|
|
$words[] = $dbh->ilike('vcard', '%' . $word . '%');
|
|
}
|
|
}
|
|
$where[] = '(' . join(' AND ', $words) . ')';
|
|
if (is_array($value))
|
|
$post_search[$col] = mb_strtolower($val);
|
|
}
|
|
}
|
|
|
|
if ($this->config['presetname']){
|
|
$prefs = carddav_common::get_adminsettings();
|
|
if (array_key_exists("require_always", $prefs[$this->config['presetname']])){
|
|
$required = array_merge($prefs[$this->config['presetname']]["require_always"], $required);
|
|
}
|
|
}
|
|
|
|
foreach (array_intersect($required, $this->table_cols) as $col) {
|
|
$and_where[] = $dbh->quoteIdentifier($col).' <> '.$dbh->quote('');
|
|
}
|
|
|
|
if (!empty($where)) {
|
|
// use AND operator for advanced searches
|
|
$where = join(is_array($value) ? ' AND ' : ' OR ', $where);
|
|
}
|
|
|
|
if (!empty($and_where))
|
|
$where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
|
|
|
|
// Post-searching in vCard data fields
|
|
// we will search in all records and then build a where clause for their IDs
|
|
if (!empty($post_search)) {
|
|
$ids = array(0);
|
|
// build key name regexp
|
|
$regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
|
|
// use initial WHERE clause, to limit records number if possible
|
|
if (!empty($where))
|
|
$this->set_search_set($where);
|
|
|
|
// count result pages
|
|
$cnt = $this->count();
|
|
$pages = ceil($cnt / $this->page_size);
|
|
$scnt = count($post_search);
|
|
|
|
// get (paged) result
|
|
for ($i=0; $i<$pages; $i++) {
|
|
$this->list_records(null, $i, true);
|
|
while ($row = $this->result->next()) {
|
|
$id = $row[$this->primary_key];
|
|
$found = array();
|
|
foreach (preg_grep($regexp, array_keys($row)) as $col) {
|
|
$pos = strpos($col, ':');
|
|
$colname = $pos ? substr($col, 0, $pos) : $col;
|
|
$search = $post_search[$colname];
|
|
foreach ((array)$row[$col] as $value) {
|
|
// composite field, e.g. address
|
|
foreach ((array)$value as $val) {
|
|
$val = mb_strtolower($val);
|
|
if ($mode & 1) {
|
|
$got = ($val == $search);
|
|
} elseif ($mode & 2) {
|
|
$got = ($search == substr($val, 0, strlen($search)));
|
|
} else {
|
|
$got = (strpos($val, $search) !== false);
|
|
}
|
|
|
|
if ($got) {
|
|
$found[$colname] = true;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// all fields match
|
|
if (count($found) >= $scnt) {
|
|
$ids[] = $id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// build WHERE clause
|
|
$ids = $dbh->array2list($ids, 'integer');
|
|
$where = $this->primary_key.' IN ('.$ids.')';
|
|
|
|
// when we know we have an empty result
|
|
if ($ids == '0') {
|
|
$this->set_search_set($where);
|
|
return ($this->result = new rcube_result_set(0, 0));
|
|
}
|
|
}
|
|
|
|
if (!empty($where)) {
|
|
$this->set_search_set($where);
|
|
if ($select)
|
|
$this->list_records(null, 0, $nocount);
|
|
else
|
|
$this->result = $this->count();
|
|
}
|
|
|
|
return $this->result;
|
|
}}}
|
|
|
|
/**
|
|
* Count number of available contacts in database
|
|
*
|
|
* @return rcube_result_set Result set with values for 'count' and 'first'
|
|
*/
|
|
public function count()
|
|
{{{
|
|
if($this->total_cards < 0) {
|
|
$this->_count();
|
|
}
|
|
return new rcube_result_set($this->total_cards, ($this->list_page-1) * $this->page_size);
|
|
}}}
|
|
|
|
// Determines and returns the number of cards matching the current search criteria
|
|
private function _count($cols=array())
|
|
{{{
|
|
if($this->total_cards < 0) {
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
$sql_result = $dbh->query('SELECT COUNT(id) as total_cards FROM ' .
|
|
$dbh->table_name('carddav_contacts') .
|
|
' WHERE abook_id=?' .
|
|
($this->filter ? " AND (".$this->filter.")" : ""),
|
|
$this->id
|
|
);
|
|
|
|
$resultrow = $dbh->fetch_assoc($sql_result);
|
|
$this->total_cards = $resultrow['total_cards'];
|
|
}
|
|
return $this->total_cards;
|
|
}}}
|
|
|
|
private function determine_filter_params($cols, $subset, &$firstrow, &$numrows, &$read_vcard)
|
|
{{{
|
|
// determine whether we have to parse the vcard or if only db cols are requested
|
|
$read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
|
|
|
|
// determine result subset needed
|
|
$firstrow = ($subset>=0) ?
|
|
$this->result->first : ($this->result->first+$this->page_size+$subset);
|
|
$numrows = $subset ? abs($subset) : $this->page_size;
|
|
}}}
|
|
|
|
/**
|
|
* Return the last result set
|
|
*
|
|
* @return rcube_result_set Current result set or NULL if nothing selected yet
|
|
*/
|
|
public function get_result()
|
|
{{{
|
|
return $this->result;
|
|
}}}
|
|
|
|
/**
|
|
* Return the last result set
|
|
*
|
|
* @return rcube_result_set Current result set or NULL if nothing selected yet
|
|
*/
|
|
private function get_record_from_carddav($uid)
|
|
{{{
|
|
$opts = array( 'method'=>"GET" );
|
|
$reply = self::$helper->cdfopen($uid, $opts, $this->config);
|
|
if (!is_array($reply) || strlen($reply["body"])==0) { return false; }
|
|
if ($reply["status"] == 404){
|
|
self::$helper->warn("Request for VCF '$uid' which doesn't exist on the server.");
|
|
return false;
|
|
}
|
|
|
|
return array(
|
|
'vcf' => $reply["body"],
|
|
'etag' => $reply['headers']['etag'],
|
|
);
|
|
}}}
|
|
|
|
/**
|
|
* Get a specific contact record
|
|
*
|
|
* @param mixed record identifier(s)
|
|
* @param boolean True to return record as associative array, otherwise a result set is returned
|
|
*
|
|
* @return mixed Result object with all record fields or False if not found
|
|
*/
|
|
public function get_record($oid, $assoc_return=false)
|
|
{{{
|
|
$this->result = $this->count();
|
|
|
|
$contact = self::get_dbrecord($oid, 'vcard');
|
|
if(!$contact) return false;
|
|
|
|
$retval = $this->create_save_data_from_vcard($contact['vcard']);
|
|
if(!$retval) {
|
|
return false;
|
|
}
|
|
$vcfobj = $retval['vcf'];
|
|
$retval = $retval['save_data'];
|
|
$retval['__vcf'] = $vcfobj;
|
|
|
|
$retval['ID'] = $oid;
|
|
$this->result->add($retval);
|
|
$sql_arr = $assoc_return && $this->result ? $this->result->first() : null;
|
|
return $assoc_return && $sql_arr ? $sql_arr : $this->result;
|
|
}}}
|
|
|
|
private function put_record_to_carddav($id, $vcf, $etag='')
|
|
{{{
|
|
$this->result = $this->count();
|
|
$matchhdr = $etag ?
|
|
"If-Match: $etag" :
|
|
"If-None-Match: *";
|
|
|
|
$opts = array(
|
|
'method'=>"PUT",
|
|
'content'=>$vcf,
|
|
'header'=> array(
|
|
"Content-Type: text/vcard; charset=\"utf-8\"",
|
|
$matchhdr,
|
|
),
|
|
);
|
|
$reply = self::$helper->cdfopen($id, $opts, $this->config);
|
|
if (is_array($reply) && $reply["status"] >= 200 && $reply["status"] < 300) {
|
|
$etag = $reply["headers"]["etag"];
|
|
if ("$etag" == ""){
|
|
// Server did not reply an etag
|
|
$retval = $this->get_record_from_carddav($id);
|
|
self::$helper->debug(var_export($retval, true));
|
|
$etag = $retval["etag"];
|
|
}
|
|
return $etag;
|
|
}
|
|
|
|
return false;
|
|
}}}
|
|
|
|
private function delete_record_from_carddav($id)
|
|
{{{
|
|
$this->result = $this->count();
|
|
$opts = array( 'method'=>"DELETE" );
|
|
$reply = self::$helper->cdfopen($id, $opts, $this->config);
|
|
if (is_array($reply) && ($reply["status"] == 204 || $reply["status"] == 200)){
|
|
return true;
|
|
}
|
|
return false;
|
|
}}}
|
|
|
|
private function guid()
|
|
{{{
|
|
return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535));
|
|
}}}
|
|
|
|
/**
|
|
* Creates a new or updates an existing vcard from save data.
|
|
*/
|
|
private function create_vcard_from_save_data($save_data, $vcf=null)
|
|
{{{
|
|
unset($save_data['vcard']);
|
|
if(!$vcf) { // create fresh minimal vcard
|
|
$vcf = new VObject\Component\VCard(
|
|
array(
|
|
'UID' => $save_data['cuid'],
|
|
'REV' => gmdate("Y-m-d\TH:i:s\Z")
|
|
)
|
|
);
|
|
} else { // update revision
|
|
$vcf->REV = gmdate("Y-m-d\TH:i:s\Z");
|
|
}
|
|
|
|
// N is mandatory
|
|
if(array_key_exists('kind',$save_data) && $save_data['kind'] === 'group') {
|
|
$vcf->N = $save_data['name'];
|
|
} else {
|
|
$vcf->N = array(
|
|
$save_data['surname'],
|
|
$save_data['firstname'],
|
|
$save_data['middlename'],
|
|
$save_data['prefix'],
|
|
$save_data['suffix'],
|
|
);
|
|
}
|
|
|
|
$new_org_value = array();
|
|
if (array_key_exists("organization", $save_data) &&
|
|
strlen($save_data['organization']) > 0 ){
|
|
$new_org_value[] = $save_data['organization'];
|
|
}
|
|
|
|
if (array_key_exists("department", $save_data)){
|
|
if (is_array($save_data['department'])){
|
|
foreach ($save_data['department'] as $key => $value) {
|
|
$new_org_value[] = $value;
|
|
}
|
|
} else if (strlen($save_data['department']) > 0){
|
|
$new_org_value[] = $save_data['department'];
|
|
}
|
|
}
|
|
|
|
if (count($new_org_value) > 0) {
|
|
$vcf->ORG = $new_org_value;
|
|
} else {
|
|
unset($vcf->ORG);
|
|
}
|
|
|
|
// normalize date fields to RFC2425 YYYY-MM-DD date values
|
|
foreach ($this->datefields as $key) {
|
|
if (array_key_exists($key, $save_data)) {
|
|
$data = (is_array($save_data[$key])) ? $save_data[$key][0] : $save_data[$key];
|
|
if (strlen($data) > 0) {
|
|
$val = rcube_utils::strtotime($data);
|
|
$save_data[$key] = date('Y-m-d',$val);
|
|
}
|
|
}
|
|
}
|
|
|
|
// due to a bug in earlier versions of RCMCardDAV the PHOTO field was encoded base64 TWICE
|
|
// This was recognized and fixed on 2013-01-09 and should be kept here until reasonable
|
|
// certain that it's been fixed on users data, too.
|
|
if (!array_key_exists('photo', $save_data) && strlen($vcf->PHOTO) > 0){
|
|
$save_data['photo']= $vcf->PHOTO;
|
|
}
|
|
if (array_key_exists('photo', $save_data) && strlen($save_data['photo']) > 0 && base64_decode($save_data['photo'], true) !== FALSE){
|
|
self::$helper->debug("photo is base64 encoded. Decoding...");
|
|
$i=0;
|
|
while(base64_decode($save_data['photo'], true)!==FALSE && $i++ < 10){
|
|
self::$helper->debug("Decoding $i...");
|
|
$save_data['photo'] = base64_decode($save_data['photo'], true);
|
|
}
|
|
if ($i >= 10){
|
|
lef::$helper->warn("PHOTO of ".$save_data['uid']." does not decode after 10 attempts...");
|
|
}
|
|
}
|
|
|
|
// process all simple attributes
|
|
foreach ($this->vcf2rc['simple'] as $vkey => $rckey){
|
|
if (array_key_exists($rckey, $save_data)) {
|
|
$data = (is_array($save_data[$rckey])) ? $save_data[$rckey][0] : $save_data[$rckey];
|
|
if (strlen($data) > 0) {
|
|
$vcf->{$vkey} = $data;
|
|
} else { // delete the field
|
|
unset($vcf->{$vkey});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special handling for PHOTO
|
|
if ($property = $vcf->PHOTO) {
|
|
$property['ENCODING'] = 'B';
|
|
$property['VALUE'] = 'BINARY';
|
|
}
|
|
|
|
// process all multi-value attributes
|
|
foreach ($this->vcf2rc['multi'] as $vkey => $rckey){
|
|
// delete and fully recreate all entries
|
|
// there is no easy way of mapping an address in the existing card
|
|
// to an address in the save data, as subtypes may have changed
|
|
unset($vcf->{$vkey});
|
|
|
|
$stmap = array( $rckey => 'other' );
|
|
foreach ($this->coltypes[$rckey]['subtypes'] AS $subtype){
|
|
$stmap[ $rckey.':'.$subtype ] = $subtype;
|
|
}
|
|
|
|
foreach ($stmap as $rcqkey => $subtype){
|
|
if(array_key_exists($rcqkey, $save_data)) {
|
|
$avalues = is_array($save_data[$rcqkey]) ? $save_data[$rcqkey] : array($save_data[$rcqkey]);
|
|
foreach($avalues as $evalue) {
|
|
if (strlen($evalue) > 0){
|
|
$prop = $vcf->add($vkey, $evalue);
|
|
$this->set_attr_label($vcf, $prop, $rckey, $subtype); // set label
|
|
}
|
|
}}
|
|
}
|
|
}
|
|
|
|
// process address entries
|
|
unset($vcf->ADR);
|
|
foreach ($this->coltypes['address']['subtypes'] AS $subtype){
|
|
$rcqkey = 'address:'.$subtype;
|
|
|
|
if(array_key_exists($rcqkey, $save_data)) {
|
|
foreach($save_data[$rcqkey] as $avalue) {
|
|
if ( strlen($avalue['street'])
|
|
|| strlen($avalue['locality'])
|
|
|| strlen($avalue['region'])
|
|
|| strlen($avalue['zipcode'])
|
|
|| strlen($avalue['country'])) {
|
|
|
|
$prop = $vcf->add('ADR', array(
|
|
'',
|
|
'',
|
|
$avalue['street'],
|
|
$avalue['locality'],
|
|
$avalue['region'],
|
|
$avalue['zipcode'],
|
|
$avalue['country'],
|
|
));
|
|
$this->set_attr_label($vcf, $prop, 'address', $subtype); // set label
|
|
}
|
|
} }
|
|
}
|
|
|
|
return $vcf;
|
|
}}}
|
|
|
|
private function set_attr_label($vcard, $pvalue, $attrname, $newlabel)
|
|
{{{
|
|
$group = $pvalue->group;
|
|
|
|
// X-ABLabel?
|
|
if(in_array($newlabel, $this->xlabels[$attrname])) {
|
|
if(!$group) {
|
|
do {
|
|
$group = $this->guid();
|
|
} while (null !== $vcard->{$group . '.X-ABLabel'});
|
|
|
|
$pvalue->group = $group;
|
|
|
|
// delete standard label if we had one
|
|
$oldlabel = $pvalue['TYPE'];
|
|
if(strlen($oldlabel)>0 &&
|
|
in_array($oldlabel, $this->coltypes[$attrname]['subtypes'])) {
|
|
unset($pvalue['TYPE']);
|
|
}
|
|
}
|
|
|
|
$vcard->{$group . '.X-ABLabel'} = $newlabel;
|
|
return true;
|
|
}
|
|
|
|
// Standard Label
|
|
$had_xlabel = false;
|
|
if($group) { // delete group label property if present
|
|
$had_xlabel = isset($vcard->{$group . '.X-ABLabel'});
|
|
unset($vcard->{$group . '.X-ABLabel'});
|
|
}
|
|
|
|
// add or replace?
|
|
$oldlabel = $pvalue['TYPE'];
|
|
if(strlen($oldlabel)>0 &&
|
|
in_array($oldlabel, $this->coltypes[$attrname]['subtypes'])) {
|
|
$had_xlabel = false; // replace
|
|
}
|
|
|
|
if($had_xlabel &&is_array($pvalue['TYPE'])) {
|
|
$new_type = $pvalue['TYPE'];
|
|
array_unshift($new_type, $newlabel);
|
|
} else {
|
|
$new_type = $newlabel;
|
|
}
|
|
$pvalue['TYPE'] = $new_type;
|
|
|
|
return false;
|
|
}}}
|
|
|
|
private function get_attr_label($vcard, $pvalue, $attrname)
|
|
{{{
|
|
// prefer a known standard label if available
|
|
$xlabel = '';
|
|
$fallback = null;
|
|
|
|
if(isset($pvalue['TYPE'])) {
|
|
foreach($pvalue['TYPE'] as $type)
|
|
{
|
|
$type = strtolower($type);
|
|
if(is_array($this->coltypes[$attrname]['subtypes']) && in_array($type, $this->coltypes[$attrname]['subtypes']) )
|
|
{
|
|
$fallback = $type;
|
|
if(!(is_array($this->fallbacktypes[$attrname])
|
|
&& in_array($type, $this->fallbacktypes[$attrname])))
|
|
{
|
|
return $type;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if($fallback) { return $fallback; }
|
|
|
|
// check for a custom label using Apple's X-ABLabel extension
|
|
$group = $pvalue->group;
|
|
if($group) {
|
|
$xlabel = $vcard->{$group . '.X-ABLabel'};
|
|
if($xlabel) {
|
|
$xlabel = $xlabel->getParts();
|
|
if($xlabel)
|
|
$xlabel = $xlabel[0];
|
|
}
|
|
|
|
// strange Apple label that I don't know to interpret
|
|
if(strlen($xlabel)<=0) {
|
|
return 'other';
|
|
}
|
|
|
|
if(preg_match(';_\$!<(.*)>!\$_;', $xlabel, $matches)) {
|
|
$match = strtolower($matches[1]);
|
|
if(in_array($match, $this->coltypes[$attrname]['subtypes']))
|
|
return $match;
|
|
return 'other';
|
|
}
|
|
|
|
// add to known types if new
|
|
if(!in_array($xlabel, $this->coltypes[$attrname]['subtypes'])) {
|
|
$this->storeextrasubtype($attrname, $xlabel);
|
|
$this->coltypes[$attrname]['subtypes'][] = $xlabel;
|
|
}
|
|
return $xlabel;
|
|
}
|
|
|
|
return 'other';
|
|
}}}
|
|
|
|
private function download_photo(&$save_data)
|
|
{{{
|
|
$opts = array( 'method'=>"GET" );
|
|
$uri = $save_data['photo'];
|
|
$reply = self::$helper->cdfopen($uri, $opts, $this->config);
|
|
if (is_array($reply) && $reply["status"] == 200){
|
|
$save_data['photo'] = $reply['body'];
|
|
return true;
|
|
}
|
|
self::$helper->warn("Downloading $uri failed: " . (is_array($reply) ? $reply["status"] : $reply) );
|
|
return false;
|
|
}}}
|
|
|
|
/**
|
|
* Creates the roundcube representation of a contact from a VCard.
|
|
*
|
|
* If the card contains a URI referencing an external photo, this
|
|
* function will download the photo and inline it into the VCard.
|
|
* The returned array contains a boolean that indicates that the
|
|
* VCard was modified and should be stored to avoid repeated
|
|
* redownloads of the photo in the future. The returned VCard
|
|
* object contains the modified representation and can be used
|
|
* for storage.
|
|
*
|
|
* @param string Textual representation of a VCard.
|
|
* @return mixed false on failure, otherwise associative array with keys:
|
|
* - save_data: Roundcube representation of the VCard
|
|
* - vcf: VCard object created from the given VCard
|
|
* - needs_update: boolean that indicates whether the card was modified
|
|
*/
|
|
private function create_save_data_from_vcard($vcfstr)
|
|
{{{
|
|
try {
|
|
$vcf = VObject\Reader::read($vcfstr, VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Couldn't parse vcard: $vcfstr");
|
|
return false;
|
|
}
|
|
|
|
$needs_update=false;
|
|
$save_data = array(
|
|
// DEFAULTS
|
|
'kind' => 'individual',
|
|
);
|
|
|
|
foreach ($this->vcf2rc['simple'] as $vkey => $rckey){
|
|
$property = $vcf->{$vkey};
|
|
if ($property !== null){
|
|
$p = $property->getParts();
|
|
$save_data[$rckey] = $p[0];
|
|
}
|
|
}
|
|
|
|
// inline photo if external reference
|
|
if(array_key_exists('photo', $save_data)) {
|
|
$kind = $vcf->PHOTO['VALUE'];
|
|
if($kind && strcasecmp('uri', $kind)==0) {
|
|
if($this->download_photo($save_data)) {
|
|
unset($vcf->PHOTO['VALUE']);
|
|
$vcf->PHOTO['ENCODING'] = 'b';
|
|
$vcf->PHOTO = $save_data['photo'];
|
|
$needs_update=true;
|
|
}
|
|
}
|
|
self::xabcropphoto($vcf, $save_data);
|
|
}
|
|
|
|
$property = $vcf->N;
|
|
if ($property !== null){
|
|
$N = $property->getParts();
|
|
switch(count($N)){
|
|
case 5:
|
|
$save_data['suffix'] = $N[4];
|
|
case 4:
|
|
$save_data['prefix'] = $N[3];
|
|
case 3:
|
|
$save_data['middlename'] = $N[2];
|
|
case 2:
|
|
$save_data['firstname'] = $N[1];
|
|
case 1:
|
|
$save_data['surname'] = $N[0];
|
|
}
|
|
}
|
|
|
|
$property = $vcf->ORG;
|
|
if ($property){
|
|
$ORG = $property->getParts();
|
|
$save_data['organization'] = $ORG[0];
|
|
for ($i = 1; $i <= count($ORG); $i++){
|
|
$save_data['department'][] = $ORG[$i];
|
|
}
|
|
}
|
|
|
|
foreach ($this->vcf2rc['multi'] as $key => $value){
|
|
$property = $vcf->{$key};
|
|
if ($property !== null) {
|
|
foreach ($property as $property_instance){
|
|
$p = $property_instance->getParts();
|
|
$label = $this->get_attr_label($vcf, $property_instance, $value);
|
|
$save_data[$value.':'.$label][] = $p[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
$property = ($vcf->ADR) ? $vcf->ADR : array();
|
|
foreach ($property as $property_instance){
|
|
$p = $property_instance->getParts();
|
|
$label = $this->get_attr_label($vcf, $property_instance, 'address');
|
|
$adr = array(
|
|
'pobox' => $p[0], // post office box
|
|
'extended' => $p[1], // extended address
|
|
'street' => $p[2], // street address
|
|
'locality' => $p[3], // locality (e.g., city)
|
|
'region' => $p[4], // region (e.g., state or province)
|
|
'zipcode' => $p[5], // postal code
|
|
'country' => $p[6], // country name
|
|
);
|
|
$save_data['address:'.$label][] = $adr;
|
|
}
|
|
|
|
// set displayname according to settings
|
|
$this->set_displayname($save_data);
|
|
|
|
return array(
|
|
'save_data' => $save_data,
|
|
'vcf' => $vcf,
|
|
'needs_update' => $needs_update,
|
|
);
|
|
}}}
|
|
|
|
|
|
const MAX_PHOTO_SIZE = 256;
|
|
|
|
public function xabcropphoto($vcard, &$save_data)
|
|
{{{
|
|
if (!function_exists('gd_info') || $vcard == null) {
|
|
return $vcard;
|
|
}
|
|
$photo = $vcard->PHOTO;
|
|
if ($photo == null) {
|
|
return $vcard;
|
|
}
|
|
$abcrop = $vcard['X-ABCROP-RECTANGLE'];
|
|
if ($abcrop == null) {
|
|
return $vcard;
|
|
}
|
|
|
|
$parts = explode('&', $abcrop);
|
|
$x = intval($parts[1]);
|
|
$y = intval($parts[2]);
|
|
$w = intval($parts[3]);
|
|
$h = intval($parts[4]);
|
|
$dw = min($w, self::MAX_PHOTO_SIZE);
|
|
$dh = min($h, self::MAX_PHOTO_SIZE);
|
|
|
|
$src = imagecreatefromstring($photo);
|
|
$dst = imagecreatetruecolor($dw, $dh);
|
|
imagecopyresampled($dst, $src, 0, 0, $x, imagesy($src) - $y - $h, $dw, $dh, $w, $h);
|
|
|
|
ob_start();
|
|
imagepng($dst);
|
|
$data = ob_get_contents();
|
|
ob_end_clean();
|
|
$save_data['photo'] = $data;
|
|
|
|
return $vcard;
|
|
}}}
|
|
private function find_free_uid()
|
|
{{{
|
|
// find an unused UID
|
|
$cuid = $this->guid();
|
|
while ($this->get_record_from_carddav("$cuid.vcf")){
|
|
$cuid = $this->guid();
|
|
}
|
|
return $cuid;
|
|
}}}
|
|
|
|
/**
|
|
* Create a new contact record
|
|
*
|
|
* @param array Assoziative array with save data
|
|
* Keys: Field name with optional section in the form FIELD:SECTION
|
|
* Values: Field value. Can be either a string or an array of strings for multiple values
|
|
* @param boolean True to check for duplicates first
|
|
* @return mixed The created record ID on success, False on error
|
|
*/
|
|
public function insert($save_data, $check=false)
|
|
{{{
|
|
$this->preprocess_rc_savedata($save_data);
|
|
|
|
// find an unused UID
|
|
$save_data['cuid'] = $this->find_free_uid();
|
|
|
|
$vcf = $this->create_vcard_from_save_data($save_data);
|
|
if(!$vcf) return false;
|
|
$vcfstr = $vcf->serialize();
|
|
|
|
$uri = $save_data['cuid'] . '.vcf';
|
|
if(!($etag = $this->put_record_to_carddav($uri, $vcfstr)))
|
|
return false;
|
|
|
|
$url = carddav_common::concaturl($this->config['url'], $uri);
|
|
$url = preg_replace(';https?://[^/]+;', '', $url);
|
|
$dbid = $this->dbstore_contact($etag,$url,$vcfstr,$save_data);
|
|
if(!$dbid) return false;
|
|
|
|
# Done by save.inc
|
|
#if ($this->groupd != -1)
|
|
# $this->add_to_group($this->group_id, $dbid);
|
|
|
|
if($this->total_cards != -1)
|
|
$this->total_cards++;
|
|
return $dbid;
|
|
}}}
|
|
|
|
/**
|
|
* Does some common preprocessing with save data created by roundcube.
|
|
*/
|
|
private function preprocess_rc_savedata(&$save_data)
|
|
{{{
|
|
// heuristic to determine X-ABShowAs setting
|
|
// organization set but neither first nor surname => showas company
|
|
if(!$save_data['surname'] && !$save_data['firstname']
|
|
&& $save_data['organization'] && !array_key_exists('showas',$save_data)) {
|
|
$save_data['showas'] = 'COMPANY';
|
|
}
|
|
if(!array_key_exists('showas',$save_data)) {
|
|
$save_data['showas'] = 'INDIVIDUAL';
|
|
}
|
|
// organization not set but showas==company => show as regular
|
|
if(!$save_data['organization'] && $save_data['showas']==='COMPANY') {
|
|
$save_data['showas'] = 'INDIVIDUAL';
|
|
}
|
|
|
|
// generate display name according to display order setting
|
|
$this->set_displayname($save_data);
|
|
}}}
|
|
|
|
/**
|
|
* Update a specific contact record
|
|
*
|
|
* @param mixed Record identifier
|
|
* @param array Assoziative array with save data
|
|
* Keys: Field name with optional section in the form FIELD:SECTION
|
|
* Values: Field value. Can be either a string or an array of strings for multiple values
|
|
* @return boolean True on success, False on error
|
|
*/
|
|
public function update($id, $save_data)
|
|
{{{
|
|
// get current DB data
|
|
$contact = self::get_dbrecord($id,'id,cuid,uri,etag,vcard,showas');
|
|
if(!$contact) return false;
|
|
|
|
// complete save_data
|
|
$save_data['showas'] = $contact['showas'];
|
|
$this->preprocess_rc_savedata($save_data);
|
|
|
|
// create vcard from current DB data to be updated with the new data
|
|
try {
|
|
$vcf = VObject\Reader::read($contact['vcard'], VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Update: Couldn't parse local vcard: ".$contact['vcard']);
|
|
return false;
|
|
}
|
|
|
|
$vcf = $this->create_vcard_from_save_data($save_data, $vcf);
|
|
if(!$vcf) {
|
|
self::$helper->warn("Update: Couldn't adopt local vcard to new settings");
|
|
return false;
|
|
}
|
|
|
|
$vcfstr = $vcf->serialize();
|
|
if(!($etag=$this->put_record_to_carddav($contact['uri'], $vcfstr, $contact['etag']))) {
|
|
self::$helper->warn("Updating card on server failed");
|
|
return false;
|
|
}
|
|
$id = $this->dbstore_contact($etag,$contact['uri'],$vcfstr,$save_data,$id);
|
|
return ($id!=0);
|
|
}}}
|
|
|
|
/**
|
|
* Mark one or more contact records as deleted
|
|
*
|
|
* @param array Record identifiers
|
|
* @param bool Remove records irreversible (see self::undelete)
|
|
*/
|
|
public function delete($ids, $force = true)
|
|
{{{
|
|
$deleted = 0;
|
|
foreach ($ids as $dbid) {
|
|
$contact = self::get_dbrecord($dbid,'uri');
|
|
if(!$contact) continue;
|
|
|
|
// delete contact from all groups it is contained in
|
|
$groups = $this->get_record_groups($dbid);
|
|
foreach($groups as $group_id => $grpname)
|
|
$this->remove_from_group($group_id, $dbid);
|
|
|
|
if($this->delete_record_from_carddav($contact['uri'])) {
|
|
$deleted += self::delete_dbrecord($dbid);
|
|
}
|
|
}
|
|
|
|
if($this->total_cards != -1)
|
|
$this->total_cards -= $deleted;
|
|
return $deleted;
|
|
}}}
|
|
|
|
private function update_contact_categories($id,$vcf) {
|
|
$groups = $this->get_record_groups($id);
|
|
|
|
if($vcf->{'CATEGORY'}) {
|
|
$cat_name = "CATEGORY";
|
|
} else {
|
|
$cat_name = "CATEGORIES";
|
|
}
|
|
unset($vcf->{$cat_name});
|
|
$categories = array();
|
|
foreach($groups as $group_id => $grpname) {
|
|
$categories[] = $grpname;
|
|
}
|
|
$vcf->{$cat_name} = $categories;
|
|
}
|
|
|
|
private function update_contacts($ids) {
|
|
foreach ($ids as $id) {
|
|
$contact = self::get_dbrecord($id,'id,cuid,uri,etag,vcard,showas');
|
|
if(!$contact) return false;
|
|
|
|
try {
|
|
$vcf = VObject\Reader::read($contact['vcard'], VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Update: Couldn't parse local vcard: ".$contact['vcard']);
|
|
return false;
|
|
}
|
|
|
|
$this->update_contact_categories($id,$vcf);
|
|
|
|
$vcfstr = $vcf->serialize();
|
|
|
|
$save_data_arr = $this->create_save_data_from_vcard("$vcfstr");
|
|
$save_data = $save_data_arr['save_data'];
|
|
|
|
// complete save_data
|
|
$save_data['showas'] = $contact['showas'];
|
|
$this->preprocess_rc_savedata($save_data);
|
|
|
|
if(!($etag=$this->put_record_to_carddav($contact['uri'], $vcfstr, $contact['etag']))) {
|
|
self::$helper->warn("Updating card on server failed");
|
|
return false;
|
|
}
|
|
$id = $this->dbstore_contact($etag,$contact['uri'],$vcfstr,$save_data,$id);
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add the given contact records the a certain group
|
|
*
|
|
* @param string Group identifier
|
|
* @param array List of contact identifiers to be added
|
|
* @return int Number of contacts added
|
|
*/
|
|
public function add_to_group($group_id, $ids)
|
|
{{{
|
|
if (!is_array($ids)) {
|
|
$ids = explode(',', $ids);
|
|
}
|
|
|
|
if(!$this->config['use_categories']) {
|
|
// get current DB data
|
|
$group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups');
|
|
if(!$group) return false;
|
|
|
|
// get current DB data
|
|
$group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups');
|
|
if(!$group) return false;
|
|
|
|
// create vcard from current DB data to be updated with the new data
|
|
try {
|
|
$vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']);
|
|
return false;
|
|
}
|
|
|
|
foreach ($ids as $cid) {
|
|
$contact = self::get_dbrecord($cid,'cuid');
|
|
if(!$contact) return false;
|
|
|
|
$vcf->add('X-ADDRESSBOOKSERVER-MEMBER', "urn:uuid:" . $contact['cuid']);
|
|
}
|
|
|
|
$vcfstr = $vcf->serialize();
|
|
if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag'])))
|
|
return false;
|
|
|
|
if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id))
|
|
return false;
|
|
}
|
|
|
|
$dbh = rcmail::get_instance()->db;
|
|
foreach ($ids as $cid) {
|
|
$dbh->query('INSERT INTO ' .
|
|
$dbh->table_name('carddav_group_user') .
|
|
' (group_id,contact_id) VALUES (?,?)',
|
|
$group_id, $cid);
|
|
}
|
|
|
|
if($this->config['use_categories']) {
|
|
if(!$this->update_contacts($ids))
|
|
return false;
|
|
$added = count($ids);
|
|
}
|
|
return $added;
|
|
}}}
|
|
|
|
/**
|
|
* Remove the given contact records from a certain group
|
|
*
|
|
* @param string Group identifier
|
|
* @param array List of contact identifiers to be removed
|
|
* @return int Number of deleted group members
|
|
*/
|
|
public function remove_from_group($group_id, $ids)
|
|
{{{
|
|
if (!is_array($ids))
|
|
$ids = explode(',', $ids);
|
|
if(!$this->config['use_categories']) {
|
|
// get current DB data
|
|
$group = self::get_dbrecord($group_id,'name,cuid,uri,etag,vcard','groups');
|
|
if(!$group) return false;
|
|
|
|
// create vcard from current DB data to be updated with the new data
|
|
try {
|
|
$vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']);
|
|
return false;
|
|
}
|
|
|
|
$deleted = 0;
|
|
foreach ($ids as $cid) {
|
|
$contact = self::get_dbrecord($cid,'cuid');
|
|
if(!$contact) return false;
|
|
|
|
$search_for = 'urn:uuid:' . $contact['cuid'];
|
|
foreach ($vcf->{'X-ADDRESSBOOKSERVER-MEMBER'} as $member) {
|
|
if ($member == $search_for) {
|
|
$vcf->remove($member);
|
|
break;
|
|
}
|
|
}
|
|
$deleted++;
|
|
}
|
|
|
|
$vcfstr = $vcf->serialize();
|
|
if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag'])))
|
|
return false;
|
|
if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id))
|
|
return false;
|
|
}
|
|
|
|
$deleted = self::delete_dbrecord($ids,'group_user','contact_id', array('group_id' => $group_id));
|
|
return $deleted;
|
|
}}}
|
|
|
|
/**
|
|
* Get group assignments of a specific contact record
|
|
*
|
|
* @param mixed Record identifier
|
|
*
|
|
* @return array List of assigned groups as ID=>Name pairs
|
|
* @since 0.5-beta
|
|
*/
|
|
public function get_record_groups($id)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
$sql_result = $dbh->query('SELECT id,name FROM '.
|
|
$dbh->table_name('carddav_groups') . ',' .
|
|
$dbh->table_name('carddav_group_user') .
|
|
' WHERE id=group_id AND contact_id=?', $id);
|
|
|
|
$res = array();
|
|
while ($row = $dbh->fetch_assoc($sql_result)) {
|
|
$res[$row['id']] = $row['name'];
|
|
}
|
|
|
|
return $res;
|
|
}}}
|
|
|
|
/**
|
|
* Setter for the current group
|
|
*/
|
|
public function set_group($gid)
|
|
{{{
|
|
$this->group_id = $gid;
|
|
$this->total_cards = -1;
|
|
if ($gid) {
|
|
$dbh = rcmail::get_instance()->db;
|
|
$this->filter = "EXISTS(SELECT * FROM ".$dbh->table_name("carddav_group_user")."
|
|
WHERE group_id = '{$gid}' AND contact_id = ".$dbh->table_name("carddav_contacts").".id)";
|
|
} else {
|
|
$this->filter = '';
|
|
}
|
|
}}}
|
|
|
|
/**
|
|
* Get group properties such as name and email address(es)
|
|
*
|
|
* @param string Group identifier
|
|
* @return array Group properties as hash array
|
|
*/
|
|
function get_group($group_id)
|
|
{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
$sql_result = $dbh->query('SELECT * FROM '.
|
|
$dbh->table_name('carddav_groups').
|
|
' WHERE id = ?', $group_id);
|
|
|
|
if ($sql_result && ($sql_arr = $dbh->fetch_assoc($sql_result))) {
|
|
return $sql_arr;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* List all active contact groups of this source
|
|
*
|
|
* @param string Optional search string to match group name
|
|
* @param int Search mode. Sum of self::SEARCH_* (>= 1.2.3)
|
|
* 0 - partial (*abc*),
|
|
* 1 - strict (=),
|
|
* 2 - prefix (abc*)
|
|
*
|
|
* @return array Indexed list of contact groups, each a hash array
|
|
*/
|
|
public function list_groups($search = null, $mode = 0)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
$searchextra = "";
|
|
if ($search !== null){
|
|
if ($mode & 1) {
|
|
$searchextra = $dbh->ilike('name', $search);
|
|
} elseif ($mode & 2) {
|
|
$searchextra = $dbh->ilike('name',"$search%");
|
|
} else {
|
|
$searchextra = $dbh->ilike('name',"%$search%");
|
|
}
|
|
$searchextra = ' AND ' . $searchextra;
|
|
}
|
|
|
|
$sql_result = $dbh->query('SELECT id,name from ' .
|
|
$dbh->table_name('carddav_groups') .
|
|
' WHERE abook_id=?' .
|
|
$searchextra .
|
|
' ORDER BY name ASC',
|
|
$this->id);
|
|
|
|
$groups = array();
|
|
|
|
while ($row = $dbh->fetch_assoc($sql_result)) {
|
|
$row['ID'] = $row['id'];
|
|
$groups[] = $row;
|
|
}
|
|
|
|
return $groups;
|
|
}}}
|
|
|
|
/**
|
|
* Create a contact group with the given name
|
|
*
|
|
* @param string The group name
|
|
* @return mixed False on error, array with record props in success
|
|
*/
|
|
public function create_group($name)
|
|
{{{
|
|
$cuid = $this->find_free_uid();
|
|
$uri = "$cuid.vcf";
|
|
|
|
$save_data = array(
|
|
'name' => $name,
|
|
'kind' => 'group',
|
|
'cuid' => $cuid,
|
|
);
|
|
|
|
$vcf = $this->create_vcard_from_save_data($save_data);
|
|
if (!$vcf) return false;
|
|
$vcfstr = $vcf->serialize();
|
|
if(!$this->config['use_categories']) {
|
|
if (!($etag = $this->put_record_to_carddav($uri, $vcfstr)))
|
|
return false;
|
|
|
|
$url = carddav_common::concaturl($this->config['url'], $uri);
|
|
$url = preg_replace(';https?://[^/]+;', '', $url);
|
|
} else {
|
|
$etag="dummy".$name;
|
|
$url="dummy".$name;
|
|
}
|
|
if(!($dbid = $this->dbstore_group($etag,$url,$vcfstr,$save_data)))
|
|
return false;
|
|
|
|
return array('id'=>$dbid, 'name'=>$name);
|
|
}}}
|
|
|
|
/**
|
|
* Delete the given group and all linked group members
|
|
*
|
|
* @param string Group identifier
|
|
* @return boolean True on success, false if no data was changed
|
|
*/
|
|
public function delete_group($group_id)
|
|
{{{
|
|
$ids = null;
|
|
// get current DB data
|
|
$group = self::get_dbrecord($group_id,'uri','groups');
|
|
if(!$group) return false;
|
|
|
|
if($this->config['use_categories']) {
|
|
$contacts = self::get_dbrecord($group_id, 'contact_id as id', 'group_user', false, 'group_id');
|
|
$ids = array();
|
|
foreach($contacts as $contact) {
|
|
$ids[]=$contact['id'];
|
|
}
|
|
}
|
|
if(!$this->config['use_categories']) {
|
|
if($this->delete_record_from_carddav($group['uri'])) {
|
|
self::delete_dbrecord($group_id, 'groups');
|
|
self::delete_dbrecord($group_id, 'group_user', 'group_id');
|
|
return true;
|
|
}
|
|
} else {
|
|
self::delete_dbrecord($group_id, 'groups');
|
|
self::delete_dbrecord($group_id, 'group_user', 'group_id');
|
|
}
|
|
if($this->config['use_categories']) {
|
|
$this->update_contacts($ids);
|
|
return true;
|
|
}
|
|
return false;
|
|
}}}
|
|
|
|
/**
|
|
* Rename a specific contact group
|
|
*
|
|
* @param string Group identifier
|
|
* @param string New name to set for this group
|
|
* @param string New group identifier (if changed, otherwise don't set)
|
|
* @return boolean New name on success, false if no data was changed
|
|
*/
|
|
public function rename_group($group_id, $newname, &$newid)
|
|
{{{
|
|
// get current DB data
|
|
$group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups');
|
|
if(!$group) return false;
|
|
$group['name'] = $newname;
|
|
// create vcard from current DB data to be updated with the new data
|
|
|
|
if(!$this->config['use_categories']) {
|
|
// create vcard from current DB data to be updated with the new data
|
|
try {
|
|
$vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING);
|
|
} catch (Exception $e) {
|
|
self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']);
|
|
return false;
|
|
}
|
|
|
|
$vcf->FN = $newname;
|
|
$vcf->N = $newname;
|
|
$vcfstr = $vcf->serialize();
|
|
|
|
if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag'])))
|
|
return false;
|
|
}
|
|
if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id))
|
|
return false;
|
|
|
|
if($this->config['use_categories']) {
|
|
$contacts = self::get_dbrecord($group_id, 'contact_id as id', 'group_user', false, 'group_id');
|
|
$ids = array();
|
|
foreach($contacts as $contact) {
|
|
$ids[]=$contact['id'];
|
|
}
|
|
$this->update_contacts($ids);
|
|
}
|
|
|
|
return $newname;
|
|
}}}
|
|
|
|
|
|
/**
|
|
* Returns an array of categories for this card or a one-element array with
|
|
* the value 'Unfiled' if no CATEGORIES property is found.
|
|
*/
|
|
function getCategories(&$vcard)
|
|
{
|
|
$property = $vcard->{'CATEGORIES'};
|
|
// The Mac OS X Address Book application uses the CATEGORY property
|
|
// instead of the CATEGORIES property.
|
|
if (!$property) {
|
|
$property = $vcard->{'CATEGORY'};
|
|
}
|
|
if ($property) {
|
|
return $property->getParts();
|
|
}
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Returns true if the card belongs to at least one of the categories.
|
|
*/
|
|
function inCategories(&$vcard, &$categories)
|
|
{
|
|
$our_categories = $vcard->getCategories();
|
|
foreach ($categories as $category) {
|
|
if (in_array_case($category, $our_categories)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static function get_dbrecord($id, $cols='*', $table='contacts', $retsingle=true, $idfield='id', $other_conditions = array())
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
$idfield = $dbh->quoteIdentifier($idfield);
|
|
$id = $dbh->quote($id);
|
|
$sql = "SELECT $cols FROM " . $dbh->table_name("carddav_$table") . ' WHERE ' . $idfield . '=' . $id;
|
|
|
|
// Append additional conditions
|
|
foreach ($other_conditions as $field => $value) {
|
|
$sql .= ' AND ' . $dbh->quoteIdentifier($field) . ' = ' . $dbh->quote($value);
|
|
}
|
|
|
|
$sql_result = $dbh->query($sql);
|
|
|
|
// single row requested?
|
|
if($retsingle)
|
|
return $dbh->fetch_assoc($sql_result);
|
|
|
|
// multiple rows requested
|
|
$ret = array();
|
|
while($row = $dbh->fetch_assoc($sql_result))
|
|
$ret[] = $row;
|
|
return $ret;
|
|
}}}
|
|
|
|
public static function delete_dbrecord($ids, $table='contacts', $idfield='id', $other_conditions = array())
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
if(is_array($ids)) {
|
|
if(count($ids) <= 0) return 0;
|
|
foreach($ids as &$id)
|
|
$id = $dbh->quote(is_array($id)?$id[$idfield]:$id);
|
|
$dspec = ' IN ('. implode(',',$ids) .')';
|
|
} else {
|
|
$dspec = ' = ' . $dbh->quote($ids);
|
|
}
|
|
|
|
$idfield = $dbh->quoteIdentifier($idfield);
|
|
$sql = "DELETE FROM " . $dbh->table_name("carddav_$table") . " WHERE $idfield $dspec";
|
|
|
|
// Append additional conditions
|
|
foreach ($other_conditions as $field => $value) {
|
|
$sql .= ' AND ' . $dbh->quoteIdentifier($field) . ' = ' . $dbh->quote($value);
|
|
}
|
|
|
|
$sql_result = $dbh->query($sql);
|
|
return $dbh->affected_rows($sql_result);
|
|
}}}
|
|
|
|
public static function carddavconfig($abookid)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
// cludge, agreed, but the MDB abstraction seems to have no way of
|
|
// doing time calculations...
|
|
$timequery = '('. $dbh->now() . ' > ';
|
|
if ($dbh->db_provider === 'sqlite') {
|
|
$timequery .= ' datetime(last_updated,refresh_time))';
|
|
} elseif ($dbh->db_provider === 'mysql') {
|
|
$timequery .= ' date_add(last_updated, INTERVAL refresh_time HOUR_SECOND))';
|
|
} else {
|
|
$timequery .= ' last_updated+refresh_time)';
|
|
}
|
|
|
|
$abookrow = self::get_dbrecord($abookid,
|
|
'id as abookid,name,username,use_categories,password,url,presetname,sync_token,authentication_scheme,'
|
|
. $timequery . ' as needs_update', 'addressbooks');
|
|
|
|
if(! $abookrow) {
|
|
self::$helper->warn("FATAL! Request for non-existent configuration $abookid");
|
|
return false;
|
|
}
|
|
|
|
if ($dbh->db_provider === 'postgres') {
|
|
// postgres will return 't'/'f' here for true/false, normalize it to 1/0
|
|
$nu = $abookrow['needs_update'];
|
|
$nu = ($nu==1 || $nu=='t')?1:0;
|
|
$abookrow['needs_update'] = $nu;
|
|
}
|
|
|
|
return $abookrow;
|
|
}}}
|
|
|
|
public static function update_addressbook($dbid=0, $xcol=array(), $xval=array())
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
|
|
self::$helper->debug("UPDATE addressbook $dbid");
|
|
$xval[]=$dbid;
|
|
$sql_result = $dbh->query('UPDATE ' .
|
|
$dbh->table_name("carddav_addressbooks") .
|
|
' SET ' . implode('=?,', $xcol) . '=?' .
|
|
' WHERE id=?', $xval);
|
|
|
|
if($dbh->is_error()) {
|
|
self::$helper->warn($dbh->is_error());
|
|
$this->set_error(self::ERROR_SAVING, $dbh->is_error());
|
|
return false;
|
|
}
|
|
|
|
return $dbid;
|
|
}}}
|
|
/**
|
|
* Migrates settings to a separate addressbook table.
|
|
*/
|
|
public static function migrateconfig($sub = 'CardDAV')
|
|
{{{
|
|
$rcmail = rcmail::get_instance();
|
|
$prefs_all = $rcmail->config->get('carddav', 0);
|
|
$dbh = $rcmail->db;
|
|
|
|
// adopt password storing scheme if stored password differs from configured scheme
|
|
$sql_result = $dbh->query('SELECT id,password FROM ' .
|
|
$dbh->table_name('carddav_addressbooks') .
|
|
' WHERE user_id=?', $_SESSION['user_id']);
|
|
|
|
while ($abookrow = $dbh->fetch_assoc($sql_result)) {
|
|
$pw_scheme = self::$helper->password_scheme($abookrow['password']);
|
|
if(strcasecmp($pw_scheme, carddav_common::$pwstore_scheme) !== 0) {
|
|
$abookrow['password'] = self::$helper->decrypt_password($abookrow['password']);
|
|
$abookrow['password'] = self::$helper->encrypt_password($abookrow['password']);
|
|
$dbh->query('UPDATE ' .
|
|
$dbh->table_name('carddav_addressbooks') .
|
|
' SET password=? WHERE id=?',
|
|
$abookrow['password'],
|
|
$abookrow['id']);
|
|
}
|
|
}
|
|
|
|
// any old (Pre-DB) settings to migrate?
|
|
if(!$prefs_all) {
|
|
return;
|
|
}
|
|
|
|
// migrate to the multiple addressbook schema first if needed
|
|
if ($prefs_all['db_version'] == 1 || !array_key_exists('db_version', $prefs_all)){
|
|
self::$helper->debug("migrating DB1 to DB2");
|
|
unset($prefs_all['db_version']);
|
|
$p = array();
|
|
$p['CardDAV'] = $prefs_all;
|
|
$p['db_version'] = 2;
|
|
$prefs_all = $p;
|
|
}
|
|
|
|
// migrate settings to database
|
|
foreach ($prefs_all as $desc => $prefs){
|
|
// skip non address book attributes
|
|
if (!is_array($prefs)){
|
|
continue;
|
|
}
|
|
|
|
$crypt_password = self::$helper->encrypt_password($prefs['password']);
|
|
|
|
self::$helper->debug("move addressbook $desc");
|
|
$dbh->query('INSERT INTO ' .
|
|
$dbh->table_name('carddav_addressbooks') .
|
|
'(name,username,password,url,active,user_id) ' .
|
|
'VALUES (?, ?, ?, ?, ?, ?)',
|
|
$desc, $prefs['username'], $crypt_password, $prefs['url'],
|
|
$prefs['use_carddav'], $_SESSION['user_id']);
|
|
}
|
|
|
|
// delete old settings
|
|
$usettings = $rcmail->user->get_prefs();
|
|
$usettings['carddav'] = array();
|
|
self::$helper->debug("delete old prefs: " . $rcmail->user->save_prefs($usettings));
|
|
}}}
|
|
|
|
public function delete_all($with_groups = false)
|
|
{{{
|
|
$dbh = rcmail::get_instance()->db;
|
|
$abook_id = $this->id;
|
|
$res1 = $dbh->query('SELECT id FROM '.
|
|
$dbh->table_name('carddav_contacts').
|
|
' WHERE abook_id=?',$abook_id);
|
|
$contact_ids = array();
|
|
while($row = $dbh->fetch_assoc($res1)) {
|
|
$contact_ids[] = $row['id'];
|
|
}
|
|
$this->delete($contact_ids);
|
|
|
|
if ($with_groups != false) {
|
|
$res2 = $dbh->query('SELECT id FROM '.
|
|
$dbh->table_name('carddav_groups').
|
|
' WHERE abook_id=?',$abook_id);
|
|
while($row = $dbh->fetch_assoc($res2)) {
|
|
$this->delete_group($row['id']);
|
|
}
|
|
}
|
|
}}}
|
|
|
|
public static function initClass()
|
|
{{{
|
|
self::$helper = new carddav_common('BACKEND: ');
|
|
}}}
|
|
}
|
|
|
|
carddav_backend::initClass();
|
|
|
|
?>
|