2008-05-08 22:31:32 +02:00
< ? php
/**
2016-04-02 10:40:34 +02:00
* EGroupware : CalDAV / CardDAV / GroupDAV access : Addressbook handler
2008-05-08 22:31:32 +02:00
*
* @ link http :// www . egroupware . org
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package addressbook
2016-04-02 12:44:17 +02:00
* @ subpackage carddav
2008-05-08 22:31:32 +02:00
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
2016-03-06 14:45:15 +01:00
* @ copyright ( c ) 2007 - 16 by Ralf Becker < RalfBecker - AT - outdoor - training . de >
2008-05-08 22:31:32 +02:00
* @ version $Id $
*/
2016-03-06 14:45:15 +01:00
use EGroupware\Api ;
2016-04-29 12:41:53 +02:00
use EGroupware\Api\Acl ;
2016-03-06 14:45:15 +01:00
2008-05-08 22:31:32 +02:00
/**
2016-04-02 12:44:17 +02:00
* CalDAV / CardDAV / GroupDAV access : Addressbook handler
2009-10-17 11:13:36 +02:00
*
2016-04-02 12:44:17 +02:00
* Propfind now uses a Api\CalDAV\PropfindIterator with a callback to query huge addressbooks in chunk ,
2009-10-17 11:13:36 +02:00
* without getting into problems with memory_limit .
2011-09-28 11:47:47 +02:00
*
2016-04-02 12:44:17 +02:00
* Permanent error_log () calls should use $this -> caldav -> log ( $str ) instead , to be send to PHP error_log ()
2012-02-21 21:04:45 +01:00
* and our request - log ( prefixed with " ### " after request and response , like exceptions ) .
2008-05-08 22:31:32 +02:00
*/
2016-04-02 12:44:17 +02:00
class addressbook_groupdav extends Api\CalDAV\Handler
2008-05-08 22:31:32 +02:00
{
/**
* bo class of the application
*
2016-04-02 12:44:17 +02:00
* @ var Api\Contacts
2008-05-08 22:31:32 +02:00
*/
var $bo ;
var $filter_prop2cal = array (
'UID' => 'uid' ,
//'NICKNAME',
'EMAIL' => 'email' ,
'FN' => 'n_fn' ,
2011-10-05 08:50:26 +02:00
'ORG' => 'org_name' ,
2008-05-08 22:31:32 +02:00
);
/**
* Charset for exporting data , as some clients ignore the headers specifying the charset
*
* @ var string
*/
var $charset = 'utf-8' ;
2009-08-07 09:15:37 +02:00
2012-02-09 21:09:49 +01:00
/**
* 'addressbook_home_set' preference already exploded as array
*
* A = all available addressbooks
* G = primary group
* D = distribution lists as groups
* O = sync all in one ( /< username >/ addressbook / )
* or nummerical account_id , but not user itself
*
* @ var array
*/
var $home_set_pref ;
2008-05-17 15:11:46 +02:00
/**
* Constructor
*
* @ param string $app 'calendar' , 'addressbook' or 'infolog'
2016-04-02 12:44:17 +02:00
* @ param Api\CalDAV $caldav calling class
2008-05-17 15:11:46 +02:00
*/
2016-04-02 12:44:17 +02:00
function __construct ( $app , Api\CalDAV $caldav )
2008-05-08 22:31:32 +02:00
{
2016-04-02 12:44:17 +02:00
parent :: __construct ( $app , $caldav );
2008-05-08 22:31:32 +02:00
2016-04-02 12:44:17 +02:00
$this -> bo = new Api\Contacts ();
2011-04-05 22:39:13 +02:00
// since 1.9.007 we allow clients to specify the URL when creating a new contact, as specified by CardDAV
2012-02-29 10:58:15 +01:00
// LDAP does NOT have a carddav_name attribute --> stick with id mapped to LDAP attribute uid
2018-03-23 19:58:35 +01:00
if ( version_compare ( $GLOBALS [ 'egw_info' ][ 'apps' ][ 'api' ][ 'version' ], '1.9.007' , '<' ) ||
2013-06-29 13:41:55 +02:00
$this -> bo -> contact_repository != 'sql' ||
$this -> bo -> account_repository != 'sql' && strpos ( $_SERVER [ 'REQUEST_URI' ] . '/' , '/addressbook-accounts/' ) !== false )
2011-04-05 22:39:13 +02:00
{
2016-04-02 12:44:17 +02:00
self :: $path_extension = '.vcf' ;
2011-04-05 22:39:13 +02:00
}
else
{
2016-04-02 12:44:17 +02:00
self :: $path_attr = 'carddav_name' ;
self :: $path_extension = '' ;
2011-04-05 22:39:13 +02:00
}
2012-02-29 10:58:15 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " () contact_repository= { $this -> bo -> contact_repository } , account_repository= { $this -> bo -> account_repository } , REQUEST_URI= $_SERVER[REQUEST_URI] --> path_attr= " . self :: $path_attr . " , path_extension= " . self :: $path_extension );
2012-02-09 21:09:49 +01:00
$this -> home_set_pref = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'addressbook-home-set' ];
$this -> home_set_pref = $this -> home_set_pref ? explode ( ',' , $this -> home_set_pref ) : array ();
// silently switch "Sync all into one" preference on for OS X addressbook, as it only supports one AB
// this restores behavior before Lion (10.7), where AB synced all ABs contained in addressbook-home-set
if ( substr ( self :: get_agent (), 0 , 9 ) == 'cfnetwork' && ! in_array ( 'O' , $this -> home_set_pref ))
{
$this -> home_set_pref [] = 'O' ;
}
2008-05-17 15:11:46 +02:00
}
2008-05-08 22:31:32 +02:00
/**
* Handle propfind in the addressbook folder
*
* @ param string $path
2012-06-27 22:08:56 +02:00
* @ param array & $options
2008-05-08 22:31:32 +02:00
* @ param array & $files
* @ param int $user account_id
2016-04-02 10:40:34 +02:00
* @ param string $id = ''
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2012-06-27 22:08:56 +02:00
function propfind ( $path , & $options , & $files , $user , $id = '' )
2008-05-08 22:31:32 +02:00
{
2008-05-19 10:01:28 +02:00
$filter = array ();
2012-02-04 22:47:28 +01:00
// If "Sync selected addressbooks into one" is set
2012-02-09 21:09:49 +01:00
if ( $user && $user == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && in_array ( 'O' , $this -> home_set_pref ))
2012-02-04 22:47:28 +01:00
{
2012-02-29 10:58:15 +01:00
$filter [ 'owner' ] = array_keys ( $this -> get_shared ( true )); // true: ignore all-in-one pref
$filter [ 'owner' ][] = $user ;
2012-02-04 22:47:28 +01:00
}
2008-05-19 10:01:28 +02:00
// show addressbook of a single user?
2012-02-04 22:47:28 +01:00
elseif ( $user && $path != '/addressbook/' || $user === 0 )
{
2012-02-29 10:58:15 +01:00
$filter [ 'owner' ] = $user ;
2012-02-04 22:47:28 +01:00
}
2008-05-19 10:01:28 +02:00
// should we hide the accounts addressbook
2017-12-01 14:58:44 +01:00
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] === '1' ) $filter [ 'account_id' ] = null ;
2008-05-08 22:31:32 +02:00
// process REPORT filters or multiget href's
2016-04-02 10:40:34 +02:00
$nresults = null ;
2011-10-05 08:50:26 +02:00
if (( $id || $options [ 'root' ][ 'name' ] != 'propfind' ) && ! $this -> _report_filters ( $options , $filter , $id , $nresults ))
2008-05-08 22:31:32 +02:00
{
return false ;
}
2011-10-04 14:16:03 +02:00
if ( $id ) $path = dirname ( $path ) . '/' ; // carddav_name get's added anyway in the callback
2008-05-17 15:11:46 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $path , " . array2string ( $options ) . " ,, $user , $id ) filter= " . array2string ( $filter ));
2010-03-07 00:06:43 +01:00
// check if we have to return the full contact data or just the etag's
if ( ! ( $filter [ 'address_data' ] = $options [ 'props' ] == 'all' &&
2016-04-02 12:44:17 +02:00
$options [ 'root' ][ 'ns' ] == Api\CalDAV :: CARDDAV ) && is_array ( $options [ 'props' ]))
2008-05-08 22:31:32 +02:00
{
foreach ( $options [ 'props' ] as $prop )
{
if ( $prop [ 'name' ] == 'address-data' )
{
2009-10-17 11:13:36 +02:00
$filter [ 'address_data' ] = true ;
2008-05-08 22:31:32 +02:00
break ;
}
}
}
2012-09-23 22:19:35 +02:00
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
if ( $options [ 'root' ][ 'name' ] == 'sync-collection' )
{
2012-09-26 16:30:47 +02:00
// callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token
$files [ 'sync-token' ] = array ( $this , 'get_sync_collection_token' );
$files [ 'sync-token-params' ] = array ( $path , $user );
$this -> sync_collection_token = null ;
2014-02-20 20:26:02 +01:00
$filter [ 'order' ] = 'contact_modified ASC' ; // return oldest modifications first
$filter [ 'sync-collection' ] = true ;
2012-09-23 22:19:35 +02:00
}
2011-10-05 08:50:26 +02:00
if ( isset ( $nresults ))
{
$files [ 'files' ] = $this -> propfind_callback ( $path , $filter , array ( 0 , ( int ) $nresults ));
2012-09-23 22:19:35 +02:00
2012-09-26 16:30:47 +02:00
// hack to support limit with sync-collection report: contacts are returned in modified ASC order (oldest first)
// if limit is smaller then full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ( $options [ 'root' ][ 'name' ] == 'sync-collection' && $this -> bo -> total > $nresults )
2012-09-23 22:19:35 +02:00
{
2012-09-26 16:30:47 +02:00
-- $this -> sync_collection_token ;
2014-02-20 18:46:15 +01:00
$files [ 'sync-token-params' ][] = true ; // tel get_sync_collection_token that we have more entries
2012-09-23 22:19:35 +02:00
}
2011-10-05 08:50:26 +02:00
}
else
{
// return iterator, calling ourself to return result in chunks
2016-04-02 12:44:17 +02:00
$files [ 'files' ] = new Api\CalDAV\PropfindIterator ( $this , $path , $filter , $files [ 'files' ]);
2011-10-05 08:50:26 +02:00
}
2009-10-17 11:13:36 +02:00
return true ;
}
/**
2016-04-02 12:44:17 +02:00
* Callback for profind iterator
2009-10-17 11:13:36 +02:00
*
2010-03-07 00:06:43 +01:00
* @ param string $path
2015-04-15 12:00:25 +02:00
* @ param array & $filter
2016-04-02 10:40:34 +02:00
* @ param array | boolean $start = false false = return all or array ( start , num )
2009-10-17 11:13:36 +02:00
* @ return array with " files " array with values for keys path and props
*/
2015-04-15 12:00:25 +02:00
function & propfind_callback ( $path , array & $filter , $start = false , $report_not_found_multiget_ids = true )
2009-10-17 11:13:36 +02:00
{
2015-04-18 15:20:56 +02:00
//error_log(__METHOD__."('$path', ".array2string($filter).", ".array2string($start).", $report_not_found_multiget_ids)");
2009-10-17 11:13:36 +02:00
$starttime = microtime ( true );
2013-08-22 18:39:21 +02:00
$filter_in = $filter ;
2009-10-17 11:13:36 +02:00
if (( $address_data = $filter [ 'address_data' ]))
2008-05-08 22:31:32 +02:00
{
$handler = self :: _get_handler ();
}
2009-10-17 11:13:36 +02:00
unset ( $filter [ 'address_data' ]);
2012-02-04 22:47:28 +01:00
2011-10-05 08:50:26 +02:00
if ( isset ( $filter [ 'order' ]))
{
$order = $filter [ 'order' ];
unset ( $filter [ 'order' ]);
}
else
{
$order = 'egw_addressbook.contact_id' ;
}
2012-09-26 16:30:47 +02:00
// detect sync-collection report
2014-02-20 20:26:02 +01:00
$sync_collection_report = $filter [ 'sync-collection' ];
2014-02-20 20:38:21 +01:00
unset ( $filter [ 'sync-collection' ]);
2012-09-26 16:30:47 +02:00
2015-04-18 15:20:56 +02:00
if ( isset ( $filter [ self :: $path_attr ]))
2013-10-01 13:38:34 +02:00
{
2015-04-18 15:20:56 +02:00
if ( ! is_array ( $filter [ self :: $path_attr ])) $filter [ self :: $path_attr ] = ( array ) $filter [ self :: $path_attr ];
$requested_multiget_ids =& $filter [ self :: $path_attr ];
2013-10-01 13:38:34 +02:00
}
2013-09-25 12:27:41 +02:00
2009-10-17 11:13:36 +02:00
$files = array ();
2008-05-17 15:11:46 +02:00
// we query etag and modified, as LDAP does not have the strong sql etag
2012-01-31 01:37:01 +01:00
$cols = array ( 'id' , 'uid' , 'etag' , 'modified' , 'n_fn' );
2011-04-05 22:39:13 +02:00
if ( ! in_array ( self :: $path_attr , $cols )) $cols [] = self :: $path_attr ;
2012-09-23 22:19:35 +02:00
// we need tid for sync-collection report
if ( array_key_exists ( 'tid' , $filter ) && ! isset ( $filter [ 'tid' ]) && ! in_array ( 'tid' , $cols )) $cols [] = 'tid' ;
2011-10-05 08:50:26 +02:00
if (( $contacts =& $this -> bo -> search ( array (), $cols , $order , '' , '' , False , 'AND' , $start , $filter )))
2008-05-08 22:31:32 +02:00
{
2009-10-16 10:01:28 +02:00
foreach ( $contacts as & $contact )
2008-05-08 22:31:32 +02:00
{
2013-09-25 12:27:41 +02:00
// remove contact from requested multiget ids, to be able to report not found urls
2013-10-08 09:41:11 +02:00
if ( $requested_multiget_ids && ( $k = array_search ( $contact [ self :: $path_attr ], $requested_multiget_ids )) !== false )
2013-09-25 12:27:41 +02:00
{
unset ( $requested_multiget_ids [ $k ]);
}
2012-09-23 22:19:35 +02:00
// sync-collection report: deleted entry need to be reported without properties
2016-04-29 12:41:53 +02:00
if ( $contact [ 'tid' ] == Api\Contacts :: DELETED_TYPE )
2012-09-23 22:19:35 +02:00
{
$files [] = array ( 'path' => $path . urldecode ( $this -> get_path ( $contact )));
continue ;
}
2009-04-02 14:31:44 +02:00
$props = array (
2016-04-02 12:44:17 +02:00
'getcontenttype' => Api\CalDAV :: mkprop ( 'getcontenttype' , 'text/vcard' ),
2012-01-31 01:37:01 +01:00
'getlastmodified' => $contact [ 'modified' ],
'displayname' => $contact [ 'n_fn' ],
2008-05-08 22:31:32 +02:00
);
2009-04-02 14:31:44 +02:00
if ( $address_data )
2008-05-08 22:31:32 +02:00
{
2010-03-07 00:06:43 +01:00
$content = $handler -> getVCard ( $contact [ 'id' ], $this -> charset , false );
2011-09-21 22:08:21 +02:00
$props [ 'getcontentlength' ] = bytes ( $content );
2016-04-02 12:44:17 +02:00
$props [] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'address-data' , $content , true );
2008-05-20 11:02:16 +02:00
}
2011-09-21 22:08:21 +02:00
$files [] = $this -> add_resource ( $path , $contact , $props );
2008-05-08 22:31:32 +02:00
}
2012-09-26 16:30:47 +02:00
// sync-collection report --> return modified of last contact as sync-token
if ( $sync_collection_report )
{
$this -> sync_collection_token = $contact [ 'modified' ];
}
2012-09-23 22:19:35 +02:00
}
2015-04-15 12:00:25 +02:00
// last chunk or no chunking: add accounts from different repo and report missing multiget urls
2013-08-22 18:39:21 +02:00
if ( ! $start || count ( $contacts ) < $start [ 1 ])
2012-01-31 10:57:59 +01:00
{
2015-04-15 12:00:25 +02:00
//error_log(__METHOD__."('$path', ".array2string($filter).", ".array2string($start)."; $report_not_found_multiget_ids) last chunk detected: count()=".count($contacts)." < $start[1]");
2013-08-22 18:39:21 +02:00
// add accounts after contacts, if enabled and stored in different repository
if ( $this -> bo -> so_accounts && is_array ( $filter [ 'owner' ]) && in_array ( '0' , $filter [ 'owner' ]))
2012-02-01 01:31:24 +01:00
{
2013-08-22 18:39:21 +02:00
$accounts_filter = $filter_in ;
$accounts_filter [ 'owner' ] = '0' ;
if ( $sync_collection_report ) $token_was = $this -> sync_collection_token ;
2016-04-02 12:44:17 +02:00
self :: $path_attr = 'id' ;
self :: $path_extension = '.vcf' ;
2013-09-25 12:27:41 +02:00
$files = array_merge ( $files , $this -> propfind_callback ( $path , $accounts_filter , false , false ));
2016-04-02 12:44:17 +02:00
self :: $path_attr = 'carddav_name' ;
self :: $path_extension = '' ;
2013-08-22 18:39:21 +02:00
if ( $sync_collection_report && $token_was > $this -> sync_collection_token )
{
$this -> sync_collection_token = $token_was ;
}
2012-02-01 01:31:24 +01:00
}
2013-08-22 18:39:21 +02:00
// add groups after contacts, but only if enabled and NOT for '/addressbook/' (!isset($filter['owner'])
if ( in_array ( 'D' , $this -> home_set_pref ) && ( string ) $filter [ 'owner' ] !== '0' )
2012-01-31 10:57:59 +01:00
{
2013-08-22 18:39:21 +02:00
$where = array (
'list_owner' => isset ( $filter [ 'owner' ]) ? $filter [ 'owner' ] : array_keys ( $this -> bo -> grants )
);
// add sync-token to support sync-collection report
if ( $sync_collection_report )
2012-01-31 10:57:59 +01:00
{
2013-08-22 18:39:21 +02:00
list (, $sync_token ) = explode ( '>' , $filter [ 0 ]);
2016-11-02 09:40:16 +01:00
if (( int ) $sync_token ) $where [] = 'list_modified>' . $GLOBALS [ 'egw' ] -> db -> from_unixtime (( int ) $sync_token );
2013-08-22 18:39:21 +02:00
}
if ( isset ( $filter [ self :: $path_attr ])) // multiget report?
{
$where [ 'list_' . self :: $path_attr ] = $filter [ self :: $path_attr ];
}
//error_log(__METHOD__."() filter=".array2string($filter).", do_groups=".in_array('D',$this->home_set_pref).", where=".array2string($where));
2018-04-12 13:01:56 +02:00
if (( $lists = $this -> bo -> read_lists ( $where , 'contact_uid' , $where [ 'list_owner' ]))) // limit to contacts in same AB!
2013-08-22 18:39:21 +02:00
{
foreach ( $lists as $list )
2012-01-31 10:57:59 +01:00
{
2013-09-25 12:27:41 +02:00
$list [ self :: $path_attr ] = $list [ 'list_carddav_name' ];
2013-08-22 18:39:21 +02:00
$etag = $list [ 'list_id' ] . ':' . $list [ 'list_etag' ];
// for all-in-one addressbook, add selected ABs to etag
if ( isset ( $filter [ 'owner' ]) && is_array ( $filter [ 'owner' ]))
{
$etag .= ':' . implode ( '-' , $filter [ 'owner' ]);
}
$props = array (
2016-04-02 12:44:17 +02:00
'getcontenttype' => Api\CalDAV :: mkprop ( 'getcontenttype' , 'text/vcard' ),
2016-04-29 12:41:53 +02:00
'getlastmodified' => Api\DateTime :: to ( $list [ 'list_modified' ], 'ts' ),
2013-08-22 18:39:21 +02:00
'displayname' => $list [ 'list_name' ],
'getetag' => '"' . $etag . '"' ,
);
if ( $address_data )
{
$content = $handler -> getGroupVCard ( $list );
$props [ 'getcontentlength' ] = bytes ( $content );
2016-04-02 12:44:17 +02:00
$props [] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'address-data' , $content , true );
2013-08-22 18:39:21 +02:00
}
$files [] = $this -> add_resource ( $path , $list , $props );
2012-09-26 16:30:47 +02:00
2013-09-25 12:27:41 +02:00
// remove list from requested multiget ids, to be able to report not found urls
if ( $requested_multiget_ids && ( $k = array_search ( $list [ self :: $path_attr ], $requested_multiget_ids )) !== false )
{
unset ( $requested_multiget_ids [ $k ]);
}
2013-08-22 18:39:21 +02:00
if ( $sync_collection_report && $this -> sync_collection_token < ( $ts = $GLOBALS [ 'egw' ] -> db -> from_timestamp ( $list [ 'list_modified' ])))
{
$this -> sync_collection_token = $ts ;
}
2012-09-26 16:30:47 +02:00
}
2012-01-31 10:57:59 +01:00
}
}
2015-04-15 12:00:25 +02:00
// report not found multiget urls
if ( $report_not_found_multiget_ids && $requested_multiget_ids )
2013-09-25 12:27:41 +02:00
{
2015-04-15 12:00:25 +02:00
foreach ( $requested_multiget_ids as $id )
{
$files [] = array ( 'path' => $path . $id . self :: $path_extension );
}
2013-09-25 12:27:41 +02:00
}
}
2010-03-07 00:06:43 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $path , " . array2string ( $filter ) . ',' . array2string ( $start ) . " ) took " . ( microtime ( true ) - $starttime ) . ' to return ' . count ( $files ) . ' items' );
2009-10-17 11:13:36 +02:00
return $files ;
2008-05-08 22:31:32 +02:00
}
/**
* Process the filters from the CalDAV REPORT request
*
* @ param array $options
* @ param array & $cal_filters
* @ param string $id
2011-10-05 08:50:26 +02:00
* @ param int & $nresult on return limit for number or results or unchanged / null
* @ return boolean true if filter could be processed
2008-05-08 22:31:32 +02:00
*/
2011-10-05 08:50:26 +02:00
function _report_filters ( $options , & $filters , $id , & $nresults )
2008-05-08 22:31:32 +02:00
{
if ( $options [ 'filters' ])
{
2011-10-05 08:50:26 +02:00
/* Example of a complex filter used by Mac Addressbook
< B : filter test = " anyof " >
< B : prop - filter name = " FN " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
< B : prop - filter name = " EMAIL " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
< B : prop - filter name = " NICKNAME " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
</ B : filter >
*/
$filter_test = isset ( $options [ 'filters' ][ 'attrs' ]) && isset ( $options [ 'filters' ][ 'attrs' ][ 'test' ]) ?
$options [ 'filters' ][ 'attrs' ][ 'test' ] : 'anyof' ;
$prop_filters = array ();
2016-04-02 10:40:34 +02:00
$matches = $prop_test = $column = null ;
2011-10-05 08:50:26 +02:00
foreach ( $options [ 'filters' ] as $n => $filter )
2008-05-08 22:31:32 +02:00
{
2011-10-05 08:50:26 +02:00
if ( ! is_int ( $n )) continue ; // eg. attributes of filter xml element
switch (( string ) $filter [ 'name' ])
2008-05-08 22:31:32 +02:00
{
2011-10-05 08:50:26 +02:00
case 'param-filter' :
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (...) param-filter=' { $filter [ 'attrs' ][ 'name' ] } ' not (yet) implemented! " );
2011-10-05 08:50:26 +02:00
break ;
case 'prop-filter' : // can be multiple prop-filter, see example
if ( $matches ) $prop_filters [] = implode ( $prop_test == 'allof' ? ' AND ' : ' OR ' , $matches );
$matches = array ();
$prop_filter = strtoupper ( $filter [ 'attrs' ][ 'name' ]);
$prop_test = isset ( $filter [ 'attrs' ][ 'test' ]) ? $filter [ 'attrs' ][ 'test' ] : 'anyof' ;
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (...) prop-filter=' $prop_filter ', test=' $prop_test ' " );
2008-05-08 22:31:32 +02:00
break ;
2011-10-05 08:50:26 +02:00
case 'is-not-defined' :
$matches [] = '(' . $column . " ='' OR " . $column . ' IS NULL)' ;
break ;
case 'text-match' : // prop-filter can have multiple text-match, see example
if ( ! isset ( $this -> filter_prop2cal [ $prop_filter ])) // eg. not existing NICKNAME in EGroupware
2008-05-08 22:31:32 +02:00
{
2011-10-05 08:50:26 +02:00
if ( $this -> debug || $prop_filter != 'NICKNAME' ) error_log ( __METHOD__ . " (...) text-match: $prop_filter { $filter [ 'attrs' ][ 'match-type' ] } ' { $filter [ 'data' ] } ' unknown property ' $prop_filter ' --> ignored " );
$column = false ; // to ignore following data too
2008-05-08 22:31:32 +02:00
}
else
{
2011-10-05 08:50:26 +02:00
switch ( $filter [ 'attrs' ][ 'collation' ]) // todo: which other collations allowed, we are allways unicode
2008-05-08 22:31:32 +02:00
{
2011-10-05 08:50:26 +02:00
case 'i;unicode-casemap' :
2008-05-08 22:31:32 +02:00
default :
2016-04-29 12:41:53 +02:00
$comp = ' ' . $GLOBALS [ 'egw' ] -> db -> capabilities [ Api\Db :: CAPABILITY_CASE_INSENSITIV_LIKE ] . ' ' ;
2008-05-08 22:31:32 +02:00
break ;
}
2011-10-05 08:50:26 +02:00
$column = $this -> filter_prop2cal [ strtoupper ( $prop_filter )];
if ( strpos ( $column , '_' ) === false ) $column = 'contact_' . $column ;
if ( ! isset ( $filters [ 'order' ])) $filters [ 'order' ] = $column ;
$match_type = $filter [ 'attrs' ][ 'match-type' ];
$negate_condition = isset ( $filter [ 'attrs' ][ 'negate-condition' ]) && $filter [ 'attrs' ][ 'negate-condition' ] == 'yes' ;
2008-05-08 22:31:32 +02:00
}
break ;
2011-10-05 08:50:26 +02:00
case '' : // data of text-match element
if ( isset ( $filter [ 'data' ]) && isset ( $column ))
{
if ( $column ) // false for properties not known to EGroupware
{
$value = str_replace ( array ( '%' , '_' ), array ( '\\%' , '\\_' ), $filter [ 'data' ]);
switch ( $match_type )
{
case 'equals' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( $value );
break ;
default :
case 'contains' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( '%' . $value . '%' );
break ;
case 'starts-with' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( $value . '%' );
break ;
case 'ends-with' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( '%' . $value );
break ;
}
$matches [] = ( $negate_condition ? 'NOT ' : '' ) . $sql_filter ;
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (...) text-match: $prop_filter $match_type ' ' { $filter [ 'data' ] } ' " );
}
unset ( $column );
break ;
}
// fall through
2008-05-08 22:31:32 +02:00
default :
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " ( " . array2string ( $options ) . " ,, $id ) unknown filter= " . array2string ( $filter ) . ' --> ignored' );
2008-05-08 22:31:32 +02:00
break ;
}
}
2011-10-05 08:50:26 +02:00
if ( $matches ) $prop_filters [] = implode ( $prop_test == 'allof' ? ' AND ' : ' OR ' , $matches );
if ( $prop_filters )
{
$filters [] = $filter = '((' . implode ( $filter_test == 'allof' ? ') AND (' : ') OR (' , $prop_filters ) . '))' ;
2016-04-02 10:40:34 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (path= $options[path] , ...) sql-filter: $filter " );
2011-10-05 08:50:26 +02:00
}
}
// parse limit from $options['other']
/* Example limit
< B : limit >
< B : nresults > 10 </ B : nresults >
</ B : limit >
*/
2012-01-30 06:11:05 +01:00
foreach (( array ) $options [ 'other' ] as $option )
2011-10-05 08:50:26 +02:00
{
switch ( $option [ 'name' ])
{
case 'nresults' :
$nresults = ( int ) $option [ 'data' ];
//error_log(__METHOD__."(...) options[other]=".array2string($options['other'])." --> nresults=$nresults");
break ;
case 'limit' :
break ;
2011-11-10 07:57:54 +01:00
case 'href' :
break ; // from addressbook-multiget, handled below
2012-09-23 22:19:35 +02:00
// rfc 6578 sync-report
case 'sync-token' :
if ( ! empty ( $option [ 'data' ]))
{
$parts = explode ( '/' , $option [ 'data' ]);
$sync_token = array_pop ( $parts );
$filters [] = 'contact_modified>' . ( int ) $sync_token ;
$filters [ 'tid' ] = null ; // to return deleted entries too
}
break ;
case 'sync-level' :
2012-09-24 12:26:29 +02:00
if ( $option [ 'data' ] != '1' )
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (...) only sync-level { $option [ 'data' ] } requested, but only 1 supported! options[other]= " . array2string ( $options [ 'other' ]));
2012-09-24 12:26:29 +02:00
}
2012-09-23 22:19:35 +02:00
break ;
2011-10-05 08:50:26 +02:00
default :
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (...) unknown xml tag ' { $option [ 'name' ] } ': options[other]= " . array2string ( $options [ 'other' ]));
2011-10-05 08:50:26 +02:00
break ;
}
2008-05-08 22:31:32 +02:00
}
// multiget --> fetch the url's
if ( $options [ 'root' ][ 'name' ] == 'addressbook-multiget' )
{
$ids = array ();
foreach ( $options [ 'other' ] as $option )
{
if ( $option [ 'name' ] == 'href' )
{
$parts = explode ( '/' , $option [ 'data' ]);
2014-02-20 16:11:27 +01:00
if (( $id = urldecode ( array_pop ( $parts ))))
2011-04-05 22:39:13 +02:00
{
2016-04-02 12:44:17 +02:00
$ids [] = self :: $path_extension ? basename ( $id , self :: $path_extension ) : $id ;
2011-04-05 22:39:13 +02:00
}
2008-05-08 22:31:32 +02:00
}
}
2011-04-05 22:39:13 +02:00
if ( $ids ) $filters [ self :: $path_attr ] = $ids ;
2011-10-05 08:50:26 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (...) addressbook-multiget: ids= " . implode ( ',' , $ids ));
2008-05-08 22:31:32 +02:00
}
elseif ( $id )
{
2016-04-02 12:44:17 +02:00
$filters [ self :: $path_attr ] = self :: $path_extension ? basename ( $id , self :: $path_extension ) : $id ;
2008-05-08 22:31:32 +02:00
}
2012-09-23 22:19:35 +02:00
//error_log(__METHOD__."() options[other]=".array2string($options['other'])." --> filters=".array2string($filters));
2008-05-08 22:31:32 +02:00
return true ;
}
/**
* Handle get request for an event
*
* @ param array & $options
* @ param int $id
2016-04-02 10:40:34 +02:00
* @ param int $user = null account_id
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2011-03-05 11:21:32 +01:00
function get ( & $options , $id , $user = null )
2008-05-08 22:31:32 +02:00
{
2016-04-02 10:40:34 +02:00
unset ( $user ); // not used, but required by function signature
2008-05-08 22:31:32 +02:00
if ( ! is_array ( $contact = $this -> _common_get_put_delete ( 'GET' , $options , $id )))
{
return $contact ;
}
$handler = self :: _get_handler ();
2012-01-31 10:57:59 +01:00
$options [ 'data' ] = $contact [ 'list_id' ] ? $handler -> getGroupVCard ( $contact ) :
$handler -> getVCard ( $contact [ 'id' ], $this -> charset , false );
2010-06-14 09:45:25 +02:00
// e.g. Evolution does not understand 'text/vcard'
2008-05-08 22:31:32 +02:00
$options [ 'mimetype' ] = 'text/x-vcard; charset=' . $this -> charset ;
header ( 'Content-Encoding: identity' );
2011-10-05 10:15:24 +02:00
header ( 'ETag: "' . $this -> get_etag ( $contact ) . '"' );
2008-05-08 22:31:32 +02:00
return true ;
}
/**
2011-10-05 08:50:26 +02:00
* Handle put request for a contact
2008-05-08 22:31:32 +02:00
*
* @ param array & $options
* @ param int $id
2016-04-02 10:40:34 +02:00
* @ param int $user = null account_id of owner , default null
* @ param string $prefix = null user prefix from path ( eg . / ralf from / ralf / addressbook )
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2010-10-20 17:47:30 +02:00
function put ( & $options , $id , $user = null , $prefix = null )
2008-05-08 22:31:32 +02:00
{
2010-03-07 00:06:43 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . '(' . array2string ( $options ) . " , $id , $user ) " );
$oldContact = $this -> _common_get_put_delete ( 'PUT' , $options , $id );
if ( ! is_null ( $oldContact ) && ! is_array ( $oldContact ))
2008-05-08 22:31:32 +02:00
{
2012-01-31 20:47:52 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " (,' $id ', $user , ' $prefix ') returning " . array2string ( $oldContact ));
2010-03-07 00:06:43 +01:00
return $oldContact ;
2008-05-08 22:31:32 +02:00
}
2010-03-07 00:06:43 +01:00
2008-05-08 22:31:32 +02:00
$handler = self :: _get_handler ();
2010-10-29 10:45:40 +02:00
// Fix for Apple Addressbook
2016-04-02 10:40:34 +02:00
$vCard = preg_replace ( '/item\d\.(ADR|TEL|EMAIL|URL)/' , '\1' ,
htmlspecialchars_decode ( $options [ 'content' ]));
2010-06-14 09:45:25 +02:00
$charset = null ;
if ( ! empty ( $options [ 'content_type' ]))
{
$content_type = explode ( ';' , $options [ 'content_type' ]);
if ( count ( $content_type ) > 1 )
{
array_shift ( $content_type );
foreach ( $content_type as $attribute )
{
trim ( $attribute );
list ( $key , $value ) = explode ( '=' , $attribute );
switch ( strtolower ( $key ))
{
case 'charset' :
$charset = strtoupper ( substr ( $value , 1 , - 1 ));
}
}
2010-12-02 20:42:03 +01:00
}
2010-06-14 09:45:25 +02:00
}
2008-05-17 15:11:46 +02:00
2011-04-05 22:39:13 +02:00
$contact = $handler -> vcardtoegw ( $vCard , $charset );
if ( is_array ( $oldContact ) || ( $oldContact = $this -> bo -> read ( array ( 'contact_uid' => $contact [ 'uid' ]))))
2010-03-07 00:06:43 +01:00
{
$contactId = $oldContact [ 'id' ];
$retval = true ;
}
else
{
2011-04-05 22:39:13 +02:00
// new entry
$contactId = - 1 ;
$retval = '201 Created' ;
2010-03-07 00:06:43 +01:00
}
2012-01-31 20:47:52 +01:00
$is_group = $contact [ '##X-ADDRESSBOOKSERVER-KIND' ] == 'group' ;
2012-01-31 10:57:59 +01:00
if ( $oldContact && $is_group !== isset ( $oldContact [ 'list_id' ]))
{
2016-04-29 12:41:53 +02:00
throw new Api\Exception\AssertionFailed ( __METHOD__ . " (,' $id ', $user ,' $prefix ') can contact into group or visa-versa! " );
2012-01-31 10:57:59 +01:00
}
2010-03-07 00:06:43 +01:00
2012-01-31 10:57:59 +01:00
if ( ! $is_group && is_array ( $contact [ 'cat_id' ]))
2010-03-07 00:06:43 +01:00
{
2010-06-14 09:45:25 +02:00
$contact [ 'cat_id' ] = implode ( ',' , $this -> bo -> find_or_add_categories ( $contact [ 'cat_id' ], $contactId ));
2010-03-07 00:06:43 +01:00
}
elseif ( $contactId > 0 )
2008-05-08 22:31:32 +02:00
{
2010-06-14 09:45:25 +02:00
$contact [ 'cat_id' ] = $oldContact [ 'cat_id' ];
2010-03-07 00:06:43 +01:00
}
if ( is_array ( $oldContact ))
{
$contact [ 'id' ] = $oldContact [ 'id' ];
2008-05-17 15:11:46 +02:00
// dont allow the client to overwrite certain values
2010-03-07 00:06:43 +01:00
$contact [ 'uid' ] = $oldContact [ 'uid' ];
2010-10-20 17:47:30 +02:00
$contact [ 'owner' ] = $oldContact [ 'owner' ];
2010-03-07 00:06:43 +01:00
$contact [ 'private' ] = $oldContact [ 'private' ];
2011-04-05 22:39:13 +02:00
$contact [ 'carddav_name' ] = $oldContact [ 'carddav_name' ];
2012-02-10 22:29:50 +01:00
$contact [ 'tid' ] = $oldContact [ 'tid' ];
$contact [ 'creator' ] = $oldContact [ 'creator' ];
2012-07-31 18:18:26 +02:00
$contact [ 'created' ] = $oldContact [ 'created' ];
2012-02-29 10:58:15 +01:00
$contact [ 'account_id' ] = $oldContact [ 'account_id' ];
2011-04-05 22:39:13 +02:00
}
else
{
$contact [ 'carddav_name' ] = $id ;
2012-02-15 14:04:27 +01:00
// only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!)
2015-07-23 19:40:48 +02:00
if ( $prefix && ! in_array ( 'O' , $this -> home_set_pref ) && $user )
2012-02-15 14:04:27 +01:00
{
$contact [ 'owner' ] = $user ;
}
2016-04-29 12:41:53 +02:00
// check if default addressbook is synced and not Api\Accounts, if not use (always synced) personal addressbook
2015-07-23 19:40:48 +02:00
elseif ( ! $this -> bo -> default_addressbook || ! in_array ( $this -> bo -> default_addressbook , $this -> home_set_pref ))
2012-02-15 14:04:27 +01:00
{
$contact [ 'owner' ] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
else
{
$contact [ 'owner' ] = $this -> bo -> default_addressbook ;
$contact [ 'private' ] = $this -> bo -> default_private ;
}
// check if user has add rights for addressbook
// done here again, as _common_get_put_delete knows nothing about default addressbooks...
2016-04-29 12:41:53 +02:00
if ( ! ( $this -> bo -> grants [ $contact [ 'owner' ]] & Acl :: ADD ))
2010-10-20 17:47:30 +02:00
{
2012-01-31 20:47:52 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " (,' $id ', $user , ' $prefix ') returning '403 Forbidden' " );
2010-10-20 17:47:30 +02:00
return '403 Forbidden' ;
}
}
2008-05-17 15:11:46 +02:00
if ( $this -> http_if_match ) $contact [ 'etag' ] = self :: etag2value ( $this -> http_if_match );
2008-05-08 22:31:32 +02:00
2012-02-01 01:59:25 +01:00
if ( ! ( $save_ok = $is_group ? $this -> save_group ( $contact , $oldContact ) : $this -> bo -> save ( $contact )))
2008-05-08 22:31:32 +02:00
{
2008-05-20 11:02:16 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (, $id ) save( " . array2string ( $contact ) . " ) failed, Ok= $save_ok " );
if ( $save_ok === 0 )
2008-05-08 22:31:32 +02:00
{
2015-10-16 09:50:43 +02:00
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this -> check_return_representation ( $options , $id , $user );
2008-05-08 22:31:32 +02:00
return '412 Precondition Failed' ;
}
2010-10-20 18:16:03 +02:00
return '403 Forbidden' ; // happens when writing new entries in AB's without ADD rights
2008-05-08 22:31:32 +02:00
}
2010-03-07 00:06:43 +01:00
2014-11-30 10:24:23 +01:00
if ( empty ( $contact [ 'etag' ]) || empty ( $contact [ 'cardav_name' ]))
2008-05-17 15:11:46 +02:00
{
2013-06-23 10:57:21 +02:00
if ( $is_group )
{
if (( $contact = $this -> bo -> read_list ( $save_ok )))
{
2018-04-12 13:01:56 +02:00
// re-read group to get correct etag (not dublicate etag code here)
$contact = $this -> read ( $contact [ 'list_' . self :: $path_attr ], $options [ 'path' ]);
2013-06-23 10:57:21 +02:00
}
}
else
{
$contact = $this -> bo -> read ( $save_ok );
}
//error_log(__METHOD__."(, $id, '$user') read(_list)($save_ok) returned ".array2string($contact));
2008-05-17 15:11:46 +02:00
}
2008-05-08 22:31:32 +02:00
2012-02-04 21:24:01 +01:00
// send evtl. necessary respose headers: Location, etag, ...
$this -> put_response_headers ( $contact , $options [ 'path' ], $retval , self :: $path_attr != 'id' );
2011-04-05 22:39:13 +02:00
2012-01-31 20:47:52 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (,' $id ', $user , ' $prefix ') returning " . array2string ( $retval ));
2011-04-05 22:39:13 +02:00
return $retval ;
2008-05-08 22:31:32 +02:00
}
2012-01-31 10:57:59 +01:00
/**
* Save distribition - list / group
*
* @ param array $contact
* @ param array | false $oldContact
2013-06-20 09:50:12 +02:00
* @ return int | boolean $list_id or false on error
2012-01-31 10:57:59 +01:00
*/
2012-02-18 11:22:59 +01:00
function save_group ( array & $contact , $oldContact = null )
2012-01-31 10:57:59 +01:00
{
$data = array ( 'list_name' => $contact [ 'n_fn' ]);
2012-02-20 12:25:41 +01:00
if ( ! isset ( $contact [ 'owner' ])) $contact [ 'owner' ] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
foreach ( array ( 'id' , 'carddav_name' , 'uid' , 'owner' ) as $name )
2012-01-31 10:57:59 +01:00
{
2013-08-02 18:31:54 +02:00
$data [ 'list_' . $name ] = $contact [ $name ];
2012-01-31 10:57:59 +01:00
}
2012-02-01 01:59:25 +01:00
//error_log(__METHOD__.'('.array2string($contact).', '.array2string($oldContact).') data='.array2string($data));
2018-04-12 13:01:56 +02:00
if (( $list_id = $this -> bo -> add_list ( empty ( $contact [ self :: $path_attr ]) ? null : array ( 'list_' . self :: $path_attr => $contact [ self :: $path_attr ]),
2012-01-31 10:57:59 +01:00
$contact [ 'owner' ], null , $data )))
{
2012-02-01 01:59:25 +01:00
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
$new_members = $contact [ '##X-ADDRESSBOOKSERVER-MEMBER' ];
2012-01-31 10:57:59 +01:00
if ( $new_members [ 1 ] == ':' && ( $n = unserialize ( $new_members )))
{
$new_members = $n [ 'values' ];
}
else
{
$new_members = array ( $new_members );
}
2016-04-02 10:40:34 +02:00
foreach ( $new_members as & $uid )
{
$uid = substr ( $uid , 9 ); // cut off "urn:uuid:" prefix
}
2012-01-31 10:57:59 +01:00
if ( $oldContact )
{
2012-02-01 01:59:25 +01:00
$to_add = array_diff ( $new_members , $oldContact [ 'members' ]);
$to_delete = array_diff ( $oldContact [ 'members' ], $new_members );
2012-01-31 10:57:59 +01:00
}
else
{
$to_add = $new_members ;
}
2012-02-01 01:59:25 +01:00
//error_log('to_add='.array2string($to_add).', to_delete='.array2string($to_delete));
2012-01-31 10:57:59 +01:00
if ( $to_add || $to_delete )
{
$to_add_ids = $to_delete_ids = array ();
$filter = array ( 'uid' => $to_delete ? array_merge ( $to_add , $to_delete ) : $to_add );
2013-08-06 12:18:12 +02:00
if (( $contacts =& $this -> bo -> search ( array (), array ( 'id' , 'uid' ), '' , '' , '' , False , 'AND' , false , $filter )))
2012-01-31 10:57:59 +01:00
{
2012-02-18 11:22:59 +01:00
foreach ( $contacts as $c )
2012-01-31 10:57:59 +01:00
{
2012-02-18 11:22:59 +01:00
if ( $to_delete && in_array ( $c [ 'uid' ], $to_delete ))
2012-01-31 10:57:59 +01:00
{
2012-02-18 11:22:59 +01:00
$to_delete_ids [] = $c [ 'id' ];
2012-01-31 10:57:59 +01:00
}
else
{
2012-02-18 11:22:59 +01:00
$to_add_ids [] = $c [ 'id' ];
2012-01-31 10:57:59 +01:00
}
}
}
2012-02-01 01:59:25 +01:00
//error_log('to_add_ids='.array2string($to_add_ids).', to_delete_ids='.array2string($to_delete_ids));
2012-01-31 10:57:59 +01:00
if ( $to_add_ids ) $this -> bo -> add2list ( $to_add_ids , $list_id , array ());
if ( $to_delete_ids ) $this -> bo -> remove_from_list ( $to_delete_ids , $list_id );
}
2013-08-02 18:31:54 +02:00
// reread as update of list-members updates etag and modified
2018-04-12 13:01:56 +02:00
if (( $contact = $this -> bo -> read_list ( $list_id )))
{
// re-read group to get correct etag (not dublicate etag code here)
$contact = $this -> read ( $contact [ 'list_' . self :: $path_attr ]);
}
2012-01-31 10:57:59 +01:00
}
2012-02-18 11:22:59 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . '(' . array2string ( $contact ) . ', ' . array2string ( $oldContact ) . ') on return contact=' . array2string ( $data ) . ' returning ' . array2string ( $list_id ));
2013-08-02 18:31:54 +02:00
return $list_id ;
2012-01-31 10:57:59 +01:00
}
2010-01-06 00:25:17 +01:00
/**
* Query ctag for addressbook
2010-03-07 00:06:43 +01:00
*
2012-02-09 21:09:49 +01:00
* @ param string $path
* @ param int $user
2010-01-06 00:25:17 +01:00
* @ return string
*/
public function getctag ( $path , $user )
{
2012-09-23 22:19:35 +02:00
static $ctags = array (); // a little per request caching, in case ctag and sync-token is both requested
if ( isset ( $ctags [ $path ])) return $ctags [ $path ];
2012-09-26 16:30:47 +02:00
$user_in = $user ;
2010-12-02 20:42:03 +01:00
// not showing addressbook of a single user?
2012-02-29 10:58:15 +01:00
if ( is_null ( $user ) || $user === '' || $path == '/addressbook/' ) $user = null ;
2010-12-02 20:42:03 +01:00
2012-02-09 21:09:49 +01:00
// If "Sync selected addressbooks into one" is set --> ctag need to take selected AB's into account too
if ( $user && $user == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && in_array ( 'O' , $this -> home_set_pref ))
{
$user = array_merge (( array ) $user , array_keys ( $this -> get_shared ( true ))); // true: ignore all-in-one pref
2013-08-22 18:39:21 +02:00
// include accounts ctag, if accounts stored different from contacts (eg.in LDAP or ADS)
if ( $this -> bo -> so_accounts && in_array ( '0' , $user ))
{
$accounts_ctag = $this -> bo -> get_ctag ( '0' );
}
2012-02-09 21:09:49 +01:00
}
$ctag = $this -> bo -> get_ctag ( $user );
2013-08-22 18:39:21 +02:00
2013-08-02 18:31:54 +02:00
// include lists-ctag, if enabled
if ( in_array ( 'D' , $this -> home_set_pref ))
2012-02-09 21:09:49 +01:00
{
$lists_ctag = $this -> bo -> lists_ctag ( $user );
}
2012-09-26 16:30:47 +02:00
//error_log(__METHOD__."('$path', ".array2string($user_in).") --> user=".array2string($user)." --> ctag=$ctag=".date('Y-m-d H:i:s',$ctag).", lists_ctag=".($lists_ctag ? $lists_ctag.'='.date('Y-m-d H:i:s',$lists_ctag) : '').' returning '.max($ctag,$lists_ctag));
2016-04-02 10:40:34 +02:00
unset ( $user_in );
2013-08-22 18:39:21 +02:00
return $ctags [ $path ] = max ( $ctag , $accounts_ctag , $lists_ctag );
2012-09-23 22:19:35 +02:00
}
2010-01-07 03:04:09 +01:00
/**
* Add extra properties for addressbook collections
*
2010-01-07 05:24:45 +01:00
* Example for supported - report - set syntax from Apples Calendarserver :
* < D : supported - report - set >
* < supported - report >
* < report >
* < addressbook - query xmlns = 'urn:ietf:params:xml:ns:carddav' />
* </ report >
* </ supported - report >
* < supported - report >
* < report >
* < addressbook - multiget xmlns = 'urn:ietf:params:xml:ns:carddav' />
* </ report >
* </ supported - report >
* </ D : supported - report - set >
* @ link http :// www . mail - archive . com / calendarserver - users @ lists . macosforge . org / msg01156 . html
2010-03-07 00:06:43 +01:00
*
2016-04-29 12:41:53 +02:00
* @ param array $props = array () regular props by the Api\CalDAV handler
2010-03-07 00:06:43 +01:00
* @ param string $displayname
2016-04-02 10:40:34 +02:00
* @ param string $base_uri = null base url of handler
* @ param int $user = null account_id of owner of collection
2010-01-07 03:04:09 +01:00
* @ return array
*/
2016-04-02 10:40:34 +02:00
public function extra_properties ( array $props , $displayname , $base_uri = null , $user = null )
2010-01-07 03:04:09 +01:00
{
2016-04-02 10:40:34 +02:00
unset ( $displayname , $base_uri , $user ); // not used, but required by function signature
2012-01-30 06:11:05 +01:00
if ( ! isset ( $props [ 'addressbook-description' ]))
{
// default addressbook description: can be overwritten via PROPPATCH, in which case it's already set
2016-04-02 12:44:17 +02:00
$props [ 'addressbook-description' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'addressbook-description' , $props [ 'displayname' ]);
2012-01-30 06:11:05 +01:00
}
2012-01-30 20:47:34 +01:00
// setting an max image size, so iOS scales the images before transmitting them
2014-01-28 12:24:47 +01:00
// we currently scale down to width of 240px, which tests shown to be ~20k
2016-04-02 12:44:17 +02:00
$props [ 'max-image-size' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'max-image-size' , 24 * 1024 );
2012-01-30 20:47:34 +01:00
2010-01-07 03:04:09 +01:00
// supported reports (required property for CardDAV)
2012-09-24 12:26:29 +02:00
$props [ 'supported-report-set' ] = array (
2016-04-02 12:44:17 +02:00
'addressbook-query' => Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'addressbook-query' , '' ))))),
'addressbook-multiget' => Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'addressbook-multiget' , '' ))))),
2012-09-24 12:26:29 +02:00
);
2014-01-07 12:10:51 +01:00
// only advertice rfc 6578 sync-collection report, if "delete-prevention" is switched on (deleted entries get marked deleted but not actualy deleted
if ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'history' ])
{
2016-04-02 12:44:17 +02:00
$props [ 'supported-report-set' ][ 'sync-collection' ] = Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( 'sync-collection' , '' )))));
2014-01-07 12:10:51 +01:00
}
2010-01-07 03:04:09 +01:00
return $props ;
}
2008-05-08 22:31:32 +02:00
/**
* Get the handler and set the supported fields
*
2008-05-10 14:02:49 +02:00
* @ return addressbook_vcal
2008-05-08 22:31:32 +02:00
*/
private function _get_handler ()
{
2012-02-01 20:52:19 +01:00
$handler = new addressbook_vcal ( 'addressbook' , 'text/vcard' );
2013-02-19 20:53:20 +01:00
$supportedFields = $handler -> supportedFields ;
2012-02-01 20:52:19 +01:00
// Apple iOS or OS X addressbook
2012-02-10 22:29:50 +01:00
if ( $this -> agent == 'cfnetwork' || $this -> agent == 'dataaccess' )
2010-09-25 17:19:48 +02:00
{
2012-06-17 14:28:16 +02:00
$databaseFields = $handler -> databaseFields ;
2012-02-01 20:52:19 +01:00
// use just CELL and IPHONE, CELL;WORK and CELL;HOME are NOT understood
//'TEL;CELL;WORK' => array('tel_cell'),
//'TEL;CELL;HOME' => array('tel_cell_private'),
2013-02-19 20:53:20 +01:00
$supportedFields [ 'TEL;CELL' ] = array ( 'tel_cell' );
unset ( $supportedFields [ 'TEL;CELL;WORK' ]);
$supportedFields [ 'TEL;IPHONE' ] = array ( 'tel_cell_private' );
unset ( $supportedFields [ 'TEL;CELL;HOME' ]);
2012-06-17 14:28:16 +02:00
$databaseFields [ 'X-ABSHOWAS' ] = $supportedFields [ 'X-ABSHOWAS' ] = array ( 'fileas_type' ); // Horde vCard class uses uppercase prop-names!
2012-07-31 18:18:26 +02:00
2012-02-14 16:14:41 +01:00
// Apple Addressbook pre Lion (OS X 10.7) messes up CLASS and CATEGORIES (Lion cant set them but leaves them alone)
2016-04-02 10:40:34 +02:00
$matches = null ;
2012-07-31 18:18:26 +02:00
if ( preg_match ( '|CFNetwork/([0-9]+)|i' , $_SERVER [ 'HTTP_USER_AGENT' ], $matches ) && $matches [ 1 ] < 520 ||
// iOS 5.1.1 does not display CLASS or CATEGORY, but wrongly escapes multiple, comma-separated categories
// and appends CLASS: PUBLIC to an empty NOTE: field --> leaving them out for iOS
$this -> agent == 'dataaccess' )
2012-02-14 16:14:41 +01:00
{
unset ( $supportedFields [ 'CLASS' ]);
2012-06-17 14:28:16 +02:00
unset ( $databaseFields [ 'CLASS' ]);
2012-02-14 16:14:41 +01:00
unset ( $supportedFields [ 'CATEGORIES' ]);
2012-06-17 14:28:16 +02:00
unset ( $databaseFields [ 'CATEGORIES' ]);
2012-07-31 18:18:26 +02:00
}
if ( preg_match ( '|CFNetwork/([0-9]+)|i' , $_SERVER [ 'HTTP_USER_AGENT' ], $matches ) && $matches [ 1 ] < 520 )
{
2012-02-14 16:14:41 +01:00
// gd cant parse or resize images stored from snow leopard addressbook: gd-jpeg:
// - JPEG library reports unrecoverable error
// - Passed data is not in 'JPEG' format
// - Couldn't create GD Image Stream out of Data
// FF (10), Safari (5.1.3) and Chrome (17) cant display it either --> ignore images
unset ( $supportedFields [ 'PHOTO' ]);
2012-06-17 14:28:16 +02:00
unset ( $databaseFields [ 'PHOTO' ]);
2012-02-14 16:14:41 +01:00
}
2012-06-17 14:28:16 +02:00
$handler -> setDatabaseFields ( $databaseFields );
2012-02-01 20:52:19 +01:00
}
2013-02-19 20:53:20 +01:00
$handler -> setSupportedFields ( 'GroupDAV' , $this -> agent , $supportedFields );
2008-05-08 22:31:32 +02:00
return $handler ;
}
/**
* Handle delete request for an event
*
* @ param array & $options
* @ param int $id
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function delete ( & $options , $id )
{
2008-05-17 15:11:46 +02:00
if ( ! is_array ( $contact = $this -> _common_get_put_delete ( 'DELETE' , $options , $id )))
2008-05-08 22:31:32 +02:00
{
2008-05-17 15:11:46 +02:00
return $contact ;
2008-05-08 22:31:32 +02:00
}
2012-10-01 19:23:44 +02:00
if ( isset ( $contact [ 'list_id' ]))
{
$ok = $this -> bo -> delete_list ( $contact [ 'list_id' ]) !== false ;
}
elseif (( $ok = $this -> bo -> delete ( $contact [ 'id' ], self :: etag2value ( $this -> http_if_match ))) === 0 )
2008-05-08 22:31:32 +02:00
{
return '412 Precondition Failed' ;
}
2012-10-01 19:23:44 +02:00
return $ok ;
2008-05-08 22:31:32 +02:00
}
/**
* Read a contact
*
2011-10-04 16:18:35 +02:00
* We have to make sure to not return or even consider in read deleted contacts , as the might have
* the same UID and / or carddav_name as not deleted contacts and would block access to valid entries
*
2012-02-09 21:09:49 +01:00
* @ param string | int $id
2016-04-02 10:40:34 +02:00
* @ param string $path = null
2012-02-09 21:09:49 +01:00
* @ return array | boolean array with entry , false if no read rights , null if $id does not exist
2008-05-08 22:31:32 +02:00
*/
2012-02-09 21:09:49 +01:00
function read ( $id , $path = null )
2008-05-08 22:31:32 +02:00
{
2016-04-02 10:40:34 +02:00
static $non_deleted_tids = null ;
2011-10-04 16:18:35 +02:00
if ( is_null ( $non_deleted_tids ))
{
2016-04-02 10:40:34 +02:00
$tids = $this -> bo -> content_types ;
unset ( $tids [ Api\Contacts :: DELETED_TYPE ]);
$non_deleted_tids = array_keys ( $tids );
2011-10-04 16:18:35 +02:00
}
$contact = $this -> bo -> read ( array ( self :: $path_attr => $id , 'tid' => $non_deleted_tids ));
2011-06-19 11:01:15 +02:00
2013-08-22 18:39:21 +02:00
// if contact not found and accounts stored NOT like contacts, try reading it without path-extension as id
if ( is_null ( $contact ) && $this -> bo -> so_accounts && ( $c = $this -> bo -> read ( $test = basename ( $id , '.vcf' ))))
{
$contact = $c ;
}
2012-01-31 10:57:59 +01:00
// see if we have a distribution-list / group with that id
2012-02-01 01:31:24 +01:00
// bo->read_list(..., true) limits returned uid to same owner's addressbook, as iOS and OS X addressbooks
// only understands/shows that and if return more, save_lists would delete the others ones on update!
2012-02-09 21:09:49 +01:00
$limit_in_ab = true ;
list (, $account_lid , $app ) = explode ( '/' , $path ); // eg. /<username>/addressbook/<id>
// /<username>/addressbook/ with home_set_prefs containing 'O'=all-in-one contains selected ab's
if ( $account_lid == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ] && $app == 'addressbook' && in_array ( 'O' , $this -> home_set_pref ))
{
$limit_in_ab = array_keys ( $this -> get_shared ( true ));
$limit_in_ab [] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
/* we are currently not syncing distribution - lists / groups to / addressbook / as
* Apple clients use that only as directory gateway
elseif ( $account_lid == 'addressbook' ) // /addressbook/ contains all readably contacts
{
$limit_in_ab = array_keys ( $this -> bo -> grants );
} */
if ( ! $contact && ( $contact = $this -> bo -> read_lists ( array ( 'list_' . self :: $path_attr => $id ), 'contact_uid' , $limit_in_ab )))
2012-01-31 10:57:59 +01:00
{
$contact = array_shift ( $contact );
$contact [ 'n_fn' ] = $contact [ 'n_family' ] = $contact [ 'list_name' ];
2012-02-01 01:59:25 +01:00
foreach ( array ( 'owner' , 'id' , 'carddav_name' , 'modified' , 'modifier' , 'created' , 'creator' , 'etag' , 'uid' ) as $name )
2012-01-31 10:57:59 +01:00
{
$contact [ $name ] = $contact [ 'list_' . $name ];
}
2012-02-09 21:09:49 +01:00
// if NOT limited to containing AB ($limit_in_ab === true), add that limit to etag
if ( $limit_in_ab !== true )
{
$contact [ 'etag' ] .= ':' . implode ( '-' , $limit_in_ab );
}
2012-01-31 10:57:59 +01:00
}
2012-01-31 20:47:52 +01:00
elseif ( $contact === array ()) // not found from read_lists()
{
$contact = null ;
}
2012-01-31 10:57:59 +01:00
2016-03-06 14:45:15 +01:00
if ( $contact && $contact [ 'tid' ] == Api\Contacts :: DELETED_TYPE )
2011-06-19 11:01:15 +02:00
{
$contact = null ; // handle deleted events, as not existing (404 Not Found)
}
2012-01-31 20:47:52 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (' $id ') returning " . array2string ( $contact ));
2011-06-19 11:01:15 +02:00
return $contact ;
2008-05-08 22:31:32 +02:00
}
/**
* Check if user has the neccessary rights on a contact
*
2016-04-29 12:41:53 +02:00
* @ param int $acl Acl :: READ , Acl :: EDIT or Acl :: DELETE
2012-02-09 21:09:49 +01:00
* @ param array | int $contact contact - array or id
2008-05-08 22:31:32 +02:00
* @ return boolean null if entry does not exist , false if no access , true if access permitted
*/
function check_access ( $acl , $contact )
{
2013-01-22 14:19:07 +01:00
return $this -> bo -> check_perms ( $acl , $contact , true ); // true = deny to delete accounts
2008-05-08 22:31:32 +02:00
}
2012-02-04 02:03:56 +01:00
2013-01-22 09:37:58 +01:00
/**
* Get grants of current user and app
*
* Reimplemented to account for static LDAP ACL and accounts ( owner = 0 )
*
* @ return array user - id => EGW_ACL_ADD | EGW_ACL_READ | EGW_ACL_EDIT | EGW_ACL_DELETE pairs
*/
public function get_grants ()
{
2015-07-23 19:40:48 +02:00
$grants = $this -> bo -> get_grants ( $this -> bo -> user );
// remove add and delete grants for accounts (for admins too)
// as accounts can not be created as contacts, they eg. need further data
// and admins might not recognice they delete an account incl. its data
if ( isset ( $grants [ 0 ])) $grants [ 0 ] &= ~ ( EGW_ACL_ADD | EGW_ACL_DELETE );
return $grants ;
2013-01-22 09:37:58 +01:00
}
2012-02-04 02:03:56 +01:00
/**
* Return calendars / addressbooks shared from other users with the current one
*
2016-04-02 10:40:34 +02:00
* @ param boolean $ignore_all_in_one = false if true , return selected addressbooks and not array () for all - in - one
2012-02-04 22:47:28 +01:00
* @ return array account_id => account_lid pairs
2012-02-04 02:03:56 +01:00
*/
2012-02-04 22:47:28 +01:00
function get_shared ( $ignore_all_in_one = false )
2012-02-04 02:03:56 +01:00
{
$shared = array ();
2012-02-04 22:47:28 +01:00
// if "Sync all selected addressbook into one" is set --> no (additional) shared addressbooks
2012-02-09 21:09:49 +01:00
if ( ! $ignore_all_in_one && in_array ( 'O' , $this -> home_set_pref )) return array ();
2012-02-04 22:47:28 +01:00
2012-02-04 02:03:56 +01:00
// replace symbolic id's with real nummeric id's
foreach ( array (
'G' => $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_primary_group' ],
'U' => '0' ,
) as $sym => $id )
{
2012-02-09 21:09:49 +01:00
if (( $key = array_search ( $sym , $this -> home_set_pref )) !== false )
2012-02-04 02:03:56 +01:00
{
2012-02-09 21:09:49 +01:00
$this -> home_set_pref [ $key ] = $id ;
2012-02-04 02:03:56 +01:00
}
}
2016-04-29 12:41:53 +02:00
foreach ( array_keys ( $this -> bo -> get_addressbooks ( Acl :: READ )) as $id )
2012-02-04 02:03:56 +01:00
{
2017-12-01 14:58:44 +01:00
if (( $id || $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] !== '1' ) &&
2012-02-04 22:47:28 +01:00
$GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] != $id && // no current user and no accounts, if disabled in ab prefs
2012-02-09 21:09:49 +01:00
( in_array ( 'A' , $this -> home_set_pref ) || in_array (( string ) $id , $this -> home_set_pref )) &&
2012-02-04 02:03:56 +01:00
is_numeric ( $id ) && ( $owner = $id ? $this -> accounts -> id2name ( $id ) : 'accounts' ))
{
2012-09-27 17:46:08 +02:00
$shared [ $id ] = 'addressbook-' . $owner ;
2012-02-04 02:03:56 +01:00
}
}
return $shared ;
}
2012-02-04 02:24:34 +01:00
2016-04-27 15:27:22 +02:00
/**
* Hook to add properties to CardDAV root
*
* OS X 10.11 . 4 addressbook does a propfind for " addressbook-home-set " and " directory-gateway "
* in the root and does not continue without it .
*
* @ param array $data
*/
public static function groupdav_root_props ( array $data )
{
$data [ 'props' ][ 'addressbook-home-set' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'addressbook-home-set' , array (
Api\CalDAV :: mkprop ( 'href' , $data [ 'caldav' ] -> base_uri . '/' . $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_lid' ] . '/' )));
2016-04-27 15:45:46 +02:00
$data [ 'props' ][ 'principal-address' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'principal-address' ,
2017-12-01 14:58:44 +01:00
$GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] === '1' ? '' : array (
2016-04-27 15:45:46 +02:00
Api\CalDAV :: mkprop ( 'href' , $data [ 'caldav' ] -> base_uri . '/addressbook-accounts/' . $GLOBALS [ 'egw_info' ][ 'user' ][ 'person_id' ] . '.vcf' )));
2016-04-27 15:27:22 +02:00
$data [ 'props' ][ 'directory-gateway' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'directory-gateway' , array (
Api\CalDAV :: mkprop ( 'href' , $data [ 'caldav' ] -> base_uri . '/addressbook/' )));
}
2012-02-04 02:24:34 +01:00
/**
* Return appliction specific settings
*
2014-07-23 16:00:20 +02:00
* @ param array $hook_data values for keys 'location' , 'type' and 'account_id'
2012-02-14 18:38:45 +01:00
* @ return array of array with settings
2012-02-04 02:24:34 +01:00
*/
2012-02-14 18:38:45 +01:00
static function get_settings ( $hook_data )
2012-02-04 02:24:34 +01:00
{
$addressbooks = array (
'A' => lang ( 'All' ),
'G' => lang ( 'Primary Group' ),
'U' => lang ( 'Accounts' ),
2012-02-04 22:47:28 +01:00
'O' => lang ( 'Sync all selected into one' ),
'D' => lang ( 'Distribution lists as groups' )
2014-07-23 16:00:20 +02:00
);
if ( ! isset ( $hook_data [ 'setup' ]) && in_array ( $hook_data [ 'type' ], array ( 'user' , 'group' )))
{
$user = $hook_data [ 'account_id' ];
2016-04-29 12:41:53 +02:00
$addressbook_bo = new Api\Contacts ();
$addressbooks += $addressbook_bo -> get_addressbooks ( Acl :: READ , null , $user );
2014-07-23 16:00:20 +02:00
if ( $user > 0 ) unset ( $addressbooks [ $user ]); // allways synced
unset ( $addressbooks [ $user . 'p' ]); // ignore (optional) private addressbook for now
}
2012-02-04 02:24:34 +01:00
2014-02-20 14:53:05 +01:00
// allow to force no other addressbooks
2014-07-23 16:00:20 +02:00
if ( $hook_data [ 'type' ] === 'forced' )
2014-02-20 14:53:05 +01:00
{
$addressbooks [ 'N' ] = lang ( 'None' );
}
2012-02-04 02:24:34 +01:00
// rewriting owner=0 to 'U', as 0 get's always selected by prefs
2014-07-23 16:00:20 +02:00
// not removing it for default or forced prefs based on current users pref
if ( ! isset ( $addressbooks [ 0 ]) && ( in_array ( $hook_data [ 'type' ], array ( 'user' , 'group' )) ||
2017-12-01 14:58:44 +01:00
$GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ] === '1' ))
2012-02-04 02:24:34 +01:00
{
unset ( $addressbooks [ 'U' ]);
}
else
{
unset ( $addressbooks [ 0 ]);
}
$settings = array ();
$settings [ 'addressbook-home-set' ] = array (
'type' => 'multiselect' ,
'label' => 'Addressbooks to sync in addition to personal addressbook' ,
'name' => 'addressbook-home-set' ,
2014-07-13 11:01:09 +02:00
'help' => lang ( 'Only supported by a few fully conformant clients (eg. from Apple). If you have to enter a URL, it will most likely not be supported!' ) .
2012-02-04 22:47:28 +01:00
'<br/>' . lang ( 'They will be sub-folders in users home (%1 attribute).' , 'CardDAV "addressbook-home-set"' ) .
'<br/>' . lang ( 'Select "%1", if your client does not support multiple addressbooks.' , lang ( 'Sync all selected into one' )) .
'<br/>' . lang ( 'Select "%1", if your client support groups, eg. OS X or iOS addressbook.' , lang ( 'Distribution lists as groups' )),
2012-02-04 02:24:34 +01:00
'values' => $addressbooks ,
'xmlrpc' => True ,
'admin' => False ,
);
return $settings ;
}
2009-07-15 21:44:09 +02:00
}