2008-05-08 22:31:32 +02:00
< ? php
/**
2009-10-17 11:13:36 +02:00
* EGroupware : 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
* @ subpackage groupdav
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
2012-02-10 09:58:08 +01:00
* @ copyright ( c ) 2007 - 12 by Ralf Becker < RalfBecker - AT - outdoor - training . de >
2008-05-08 22:31:32 +02:00
* @ version $Id $
*/
/**
2009-10-17 11:13:36 +02:00
* EGroupware : GroupDAV access : addressbook handler
*
* Propfind now uses a groupdav_propfind_iterator with a callback to query huge addressbooks in chunk ,
* without getting into problems with memory_limit .
2011-11-06 10:40:33 +01:00
*
2012-02-10 12:44:52 +01:00
* @ todo check / fix contacts in LDAP ( no carddav_name column ! )
2008-05-08 22:31:32 +02:00
*/
class addressbook_groupdav extends groupdav_handler
{
/**
* bo class of the application
*
2010-01-06 00:25:17 +01:00
* @ var addressbook_bo
2008-05-08 22:31:32 +02:00
*/
var $bo ;
var $filter_prop2cal = array (
'UID' => 'uid' ,
//'NICKNAME',
'EMAIL' => 'email' ,
'FN' => 'n_fn' ,
2011-11-06 10:40:33 +01: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-10 12:44:52 +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'
2011-11-06 10:40:33 +01:00
* @ param groupdav $groupdav calling class
2008-05-17 15:11:46 +02:00
*/
2011-11-06 10:40:33 +01:00
function __construct ( $app , groupdav $groupdav )
2008-05-08 22:31:32 +02:00
{
2011-11-06 10:40:33 +01:00
parent :: __construct ( $app , $groupdav );
2008-05-08 22:31:32 +02:00
2009-06-08 18:21:14 +02:00
$this -> bo = new addressbook_bo ();
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-01-20 07:46:26 +01:00
if ( version_compare ( $GLOBALS [ 'egw_info' ][ 'apps' ][ 'phpgwapi' ][ 'version' ], '1.9.007' , '>=' ))
2011-04-05 22:39:13 +02:00
{
2011-11-06 10:40:33 +01:00
groupdav_handler :: $path_attr = 'carddav_name' ;
2011-04-05 22:39:13 +02:00
groupdav_handler :: $path_extension = '' ;
}
else
{
groupdav_handler :: $path_extension = '.vcf' ;
}
2012-02-10 12:44:52 +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
* @ param array $options
* @ param array & $files
* @ param int $user account_id
* @ param string $id = ''
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function propfind ( $path , $options , & $files , $user , $id = '' )
{
2008-05-19 10:01:28 +02:00
$filter = array ();
2012-02-10 12:44:52 +01:00
// If "Sync selected addressbooks into one" is set
if ( $user && $user == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && in_array ( 'O' , $this -> home_set_pref ))
{
$filter [ 'contact_owner' ] = array_keys ( $this -> get_shared ( true )); // true: ignore all-in-one pref
$filter [ 'contact_owner' ][] = $user ;
}
2008-05-19 10:01:28 +02:00
// show addressbook of a single user?
2012-02-10 12:44:52 +01:00
elseif ( $user && $path != '/addressbook/' || $user === 0 )
{
$filter [ 'contact_owner' ] = $user ;
}
2008-05-19 10:01:28 +02:00
// should we hide the accounts addressbook
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ]) $filter [ 'account_id' ] = null ;
2008-05-08 22:31:32 +02:00
// process REPORT filters or multiget href's
2011-11-06 10:40:33 +01: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-11-06 10:40:33 +01: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' &&
$options [ 'root' ][ 'ns' ] == groupdav :: 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 ;
}
}
}
2011-11-06 10:40:33 +01:00
if ( isset ( $nresults ))
{
$files [ 'files' ] = $this -> propfind_callback ( $path , $filter , array ( 0 , ( int ) $nresults ));
}
else
{
// return iterator, calling ourself to return result in chunks
$files [ 'files' ] = new groupdav_propfind_iterator ( $this , $path , $filter , $files [ 'files' ]);
}
2009-10-17 11:13:36 +02:00
return true ;
}
/**
* Callback for profind interator
*
2010-03-07 00:06:43 +01:00
* @ param string $path
2009-10-17 11:13:36 +02:00
* @ param array $filter
* @ param array | boolean $start = false false = return all or array ( start , num )
* @ return array with " files " array with values for keys path and props
*/
2010-03-07 00:06:43 +01:00
function & propfind_callback ( $path , array $filter , $start = false )
2009-10-17 11:13:36 +02:00
{
$starttime = microtime ( true );
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-10 12:44:52 +01:00
2011-11-06 10:40:33 +01:00
if ( isset ( $filter [ 'order' ]))
{
$order = $filter [ 'order' ];
unset ( $filter [ 'order' ]);
}
else
{
$order = 'egw_addressbook.contact_id' ;
}
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-02-10 12:02: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 ;
2011-11-06 10:40:33 +01: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
{
2009-04-02 14:31:44 +02:00
$props = array (
2011-11-06 10:40:33 +01:00
'getcontenttype' => HTTP_WebDAV_Server :: mkprop ( 'getcontenttype' , 'text/vcard' ),
2012-02-10 12:02: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-11-06 10:40:33 +01:00
$props [ 'getcontentlength' ] = bytes ( $content );
2010-03-07 00:06:43 +01:00
$props [] = HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'address-data' , $content , true );
2008-05-20 11:02:16 +02:00
}
2011-11-06 10:40:33 +01:00
$files [] = $this -> add_resource ( $path , $contact , $props );
2008-05-08 22:31:32 +02:00
}
}
2012-02-10 12:44:52 +01:00
// add groups after contacts, but only if enabled and NOT for '/addressbook/' (!isset($filter['contact_owner'])
if ( in_array ( 'D' , $this -> home_set_pref ) && ( ! $start || count ( $contacts ) < $start [ 1 ]) && isset ( $filter [ 'contact_owner' ]))
2012-02-10 12:11:38 +01:00
{
$where = array (
'list_owner' => isset ( $filter [ 'contact_owner' ]) ? $filter [ 'contact_owner' ] : array_keys ( $this -> bo -> grants )
);
if ( isset ( $filter [ self :: $path_attr ])) // multiget report?
{
$where [ 'list_' . self :: $path_attr ] = $filter [ self :: $path_attr ];
}
2012-02-10 12:44:52 +01:00
//error_log(__METHOD__."() filter=".array2string($filter).", do_groups=".in_array('D',$this->home_set_pref).", where=".array2string($where));
if (( $lists = $this -> bo -> read_lists ( $where , 'contact_uid' , $where [ 'list_owner' ]))) // limit to contacts in same AB!
2012-02-10 12:11:38 +01:00
{
foreach ( $lists as $list )
{
$list [ 'carddav_name' ] = $list [ 'list_carddav_name' ];
2012-02-10 12:44:52 +01:00
$etag = $list [ 'list_id' ] . ':' . $list [ 'list_etag' ];
// for all-in-one addressbook, add selected ABs to etag
if ( isset ( $filter [ 'contact_owner' ]) && is_array ( $filter [ 'contact_owner' ]))
{
$etag .= ':' . implode ( '-' , $filter [ 'contact_owner' ]);
}
2012-02-10 12:11:38 +01:00
$props = array (
'getcontenttype' => HTTP_WebDAV_Server :: mkprop ( 'getcontenttype' , 'text/vcard' ),
'getlastmodified' => egw_time :: to ( $list [ 'list_modified' ], 'ts' ),
'displayname' => $list [ 'list_name' ],
2012-02-10 12:44:52 +01:00
'getetag' => '"' . $etag . '"' ,
2012-02-10 12:11:38 +01:00
);
if ( $address_data )
{
$content = $handler -> getGroupVCard ( $list );
$props [ 'getcontentlength' ] = bytes ( $content );
$props [] = HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'address-data' , $content , true );
}
$files [] = $this -> add_resource ( $path , $list , $props );
}
}
}
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-11-06 10:40:33 +01: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-11-06 10:40:33 +01:00
function _report_filters ( $options , & $filters , $id , & $nresults )
2008-05-08 22:31:32 +02:00
{
if ( $options [ 'filters' ])
{
2011-11-06 10:40:33 +01: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 ();
foreach ( $options [ 'filters' ] as $n => $filter )
2008-05-08 22:31:32 +02:00
{
2011-11-06 10:40:33 +01: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-11-06 10:40:33 +01:00
case 'param-filter' :
error_log ( __METHOD__ . " (...) param-filter=' { $filter [ 'attrs' ][ 'name' ] } ' not (yet) implemented! " );
2008-05-08 22:31:32 +02:00
break ;
2011-11-06 10:40:33 +01:00
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 ' " );
break ;
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-11-06 10:40:33 +01: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-11-06 10:40:33 +01:00
switch ( $filter [ 'attrs' ][ 'collation' ]) // todo: which other collations allowed, we are allways unicode
2008-05-08 22:31:32 +02:00
{
2011-11-06 10:40:33 +01:00
case 'i;unicode-casemap' :
2008-05-08 22:31:32 +02:00
default :
2011-11-06 10:40:33 +01:00
$comp = ' ' . $GLOBALS [ 'egw' ] -> db -> capabilities [ egw_db :: CAPABILITY_CASE_INSENSITIV_LIKE ] . ' ' ;
2008-05-08 22:31:32 +02:00
break ;
}
2011-11-06 10:40:33 +01: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-11-06 10:40:33 +01: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 :
2011-11-06 10:40:33 +01:00
error_log ( __METHOD__ . " ( " . array2string ( $options ) . " ,, $id ) unknown filter= " . array2string ( $filter ) . ' --> ignored' );
2008-05-08 22:31:32 +02:00
break ;
}
}
2011-11-06 10:40:33 +01: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 ) . '))' ;
if ( $this -> debug ) error_log ( __METHOD__ . " ( $path ,...) sql-filter: $filter " );
}
}
// parse limit from $options['other']
/* Example limit
< B : limit >
< B : nresults > 10 </ B : nresults >
</ B : limit >
*/
2012-02-10 09:58:08 +01:00
foreach (( array ) $options [ 'other' ] as $option )
2011-11-06 10:40:33 +01: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:58:30 +01:00
case 'href' :
break ; // from addressbook-multiget, handled below
2011-11-06 10:40:33 +01:00
default :
error_log ( __METHOD__ . " (...) unknown xml: options[other]= " . array2string ( $options [ 'other' ]));
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' ]);
2011-04-05 22:39:13 +02:00
if (( $id = array_pop ( $parts )))
{
$ids [] = groupdav_handler :: $path_extension ? basename ( $id , groupdav_handler :: $path_extension ) : $id ;
}
2008-05-08 22:31:32 +02:00
}
}
2011-04-05 22:39:13 +02:00
if ( $ids ) $filters [ self :: $path_attr ] = $ids ;
2011-11-06 10:40:33 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " (...) addressbook-multiget: ids= " . implode ( ',' , $ids ));
2008-05-08 22:31:32 +02:00
}
elseif ( $id )
{
2011-04-05 22:39:13 +02:00
$filters [ self :: $path_attr ] = groupdav_handler :: $path_extension ? basename ( $id , groupdav_handler :: $path_extension ) : $id ;
2008-05-08 22:31:32 +02:00
}
return true ;
}
/**
* Handle get request for an event
*
* @ param array & $options
* @ param int $id
2011-03-05 11:21:32 +01: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
{
if ( ! is_array ( $contact = $this -> _common_get_put_delete ( 'GET' , $options , $id )))
{
return $contact ;
}
$handler = self :: _get_handler ();
2012-02-10 12:11:38 +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-11-06 10:40:33 +01:00
header ( 'ETag: "' . $this -> get_etag ( $contact ) . '"' );
2008-05-08 22:31:32 +02:00
return true ;
}
/**
2011-11-06 10:40:33 +01:00
* Handle put request for a contact
2008-05-08 22:31:32 +02:00
*
* @ param array & $options
* @ param int $id
* @ param int $user = null account_id of owner , default null
2010-10-20 17:47:30 +02:00
* @ 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-02-10 12:11:38 +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-03-07 00:06:43 +01:00
$vCard = htmlspecialchars_decode ( $options [ 'content' ]);
2010-10-29 10:45:40 +02:00
// Fix for Apple Addressbook
$vCard = preg_replace ( '/item\d\.(ADR|TEL|EMAIL|URL)/' , '\1' , $vCard );
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-02-10 12:11:38 +01:00
$is_group = $contact [ '##X-ADDRESSBOOKSERVER-KIND' ] == 'group' ;
if ( $oldContact && $is_group !== isset ( $oldContact [ 'list_id' ]))
{
throw new egw_exception_assertion_failed ( __METHOD__ . " (,' $id ', $user ,' $prefix ') can contact into group or visa-versa! " );
}
2010-03-07 00:06:43 +01:00
2012-02-10 12:11:38 +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' ];
}
else
{
$contact [ 'carddav_name' ] = $id ;
2008-05-08 22:31:32 +02:00
}
2010-10-20 17:47:30 +02:00
// only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/ !)
if ( $prefix )
{
2010-12-02 20:42:03 +01:00
// check for modified owners, if user has an add right for the new addressbook and
2010-10-20 17:47:30 +02:00
// delete rights for the old addressbook (_common_get_put_delete checks for PUT only EGW_ACL_EDIT)
if ( $oldContact && $user != $oldContact [ 'owner' ] && ! ( $this -> bo -> grants [ $user ] & EGW_ACL_ADD ) &&
( ! $this -> bo -> grants [ $oldContact [ 'owner' ]] & EGW_ACL_DELETE ))
{
2012-02-10 12:11:38 +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' ;
}
$contact [ 'owner' ] = $user ;
}
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-10 12:11:38 +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
{
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
2008-05-17 15:11:46 +02:00
if ( ! isset ( $contact [ 'etag' ]))
{
2012-02-10 12:44:52 +01:00
$contact = $this -> read ( $save_ok , $options [ 'path' ]);
2008-05-17 15:11:46 +02:00
}
2008-05-08 22:31:32 +02:00
2012-02-10 12:28:56 +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-02-10 12:11:38 +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-02-10 12:11:38 +01:00
/**
* Save distribition - list / group
*
* @ param array $contact
* @ param array | false $oldContact
* @ param int | boolean $list_id or false on error
*/
function save_group ( array $contact , $oldContact = null )
{
$data = array ( 'list_name' => $contact [ 'n_fn' ]);
foreach ( array ( 'id' , 'carddav_name' , 'uid' ) as $name )
{
if ( $name != self :: $path_attr ) $data [ 'list_' . $name ] = $contact [ $name ];
}
//error_log(__METHOD__.'('.array2string($contact).', '.array2string($oldContact).') data='.array2string($data));
if (( $list_id = $this -> bo -> add_list ( array ( 'list_' . self :: $path_attr => $contact [ self :: $path_attr ]),
$contact [ 'owner' ], null , $data )))
{
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
$new_members = $contact [ '##X-ADDRESSBOOKSERVER-MEMBER' ];
if ( $new_members [ 1 ] == ':' && ( $n = unserialize ( $new_members )))
{
$new_members = $n [ 'values' ];
}
else
{
$new_members = array ( $new_members );
}
foreach ( $new_members as & $uid ) $uid = substr ( $uid , 9 ); // cut off "urn:uuid:" prefix
if ( $oldContact )
{
$to_add = array_diff ( $new_members , $oldContact [ 'members' ]);
$to_delete = array_diff ( $oldContact [ 'members' ], $new_members );
}
else
{
$to_add = $new_members ;
}
//error_log('to_add='.array2string($to_add).', to_delete='.array2string($to_delete));
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 );
if (( $contacts =& $this -> bo -> search ( array (), 'id,uid' , '' , '' , '' , False , 'AND' , false , $filter )))
{
foreach ( $contacts as $contact )
{
if ( $to_delete && in_array ( $contact [ 'uid' ], $to_delete ))
{
$to_delete_ids [] = $contact [ 'id' ];
}
else
{
$to_add_ids [] = $contact [ 'id' ];
}
}
}
//error_log('to_add_ids='.array2string($to_add_ids).', to_delete_ids='.array2string($to_delete_ids));
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 );
}
}
if ( $this -> debug > 1 ) error_log ( __METHOD__ . '(' . array2string ( $contact ) . ', ' . array2string ( $oldContact ) . ') returning ' . array2string ( $list_id ));
return $list_id ;
}
2010-01-06 00:25:17 +01:00
/**
* Query ctag for addressbook
2010-03-07 00:06:43 +01:00
*
2012-02-10 12:44:52 +01:00
* @ param string $path
* @ param int $user
2010-01-06 00:25:17 +01:00
* @ return string
*/
public function getctag ( $path , $user )
{
2010-12-02 20:42:03 +01:00
// not showing addressbook of a single user?
if ( ! $user || $path == '/addressbook/' ) $user = null ;
2012-02-10 12:44:52 +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
}
$ctag = $this -> bo -> get_ctag ( $user );
// include lists-ctag, if enabled and NOT in /addressbook/ (we dont sync distribution-lists/groups there)
if ( in_array ( 'D' , $this -> home_set_pref ) && $path != '/addressbook/' )
{
$lists_ctag = $this -> bo -> lists_ctag ( $user );
}
//error_log(__METHOD__."('$path', ".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));
return max ( $ctag , $lists_ctag );
2010-01-06 00:25:17 +01: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
*
2010-01-07 03:04:09 +01:00
* @ param array $props = array () regular props by the groupdav handler
2010-03-07 00:06:43 +01:00
* @ param string $displayname
* @ param string $base_uri = null base url of handler
2011-11-06 10:40:33 +01:00
* @ param int $user = null account_id of owner of collection
2010-01-07 03:04:09 +01:00
* @ return array
*/
2011-11-06 10:40:33 +01:00
public function extra_properties ( array $props = array (), $displayname , $base_uri = null , $user = null )
2010-01-07 03:04:09 +01:00
{
2012-02-10 09:58:08 +01:00
if ( ! isset ( $props [ 'addressbook-description' ]))
{
// default addressbook description: can be overwritten via PROPPATCH, in which case it's already set
$props [ 'addressbook-description' ] = HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'addressbook-description' , $props [ 'displayname' ]);
}
2012-02-10 10:06:25 +01:00
// setting an max image size, so iOS scales the images before transmitting them
$props [ 'max-image-size' ] = HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'max-image-size' , 4096 );
2010-01-07 03:04:09 +01:00
// supported reports (required property for CardDAV)
2011-11-06 10:40:33 +01:00
$props [ 'supported-report-set' ] = HTTP_WebDAV_Server :: mkprop ( 'supported-report-set' , array (
2010-01-07 05:24:45 +01:00
HTTP_WebDAV_Server :: mkprop ( 'supported-report' , array (
2010-07-02 07:01:15 +02:00
HTTP_WebDAV_Server :: mkprop ( 'report' , array (
HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'addressbook-query' , '' ))))),
2010-01-07 05:24:45 +01:00
HTTP_WebDAV_Server :: mkprop ( 'supported-report' , array (
2010-07-02 07:01:15 +02:00
HTTP_WebDAV_Server :: mkprop ( 'report' , array (
HTTP_WebDAV_Server :: mkprop ( groupdav :: CARDDAV , 'addressbook-multiget' , '' ))))),
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-10 12:16:44 +01:00
$handler = new addressbook_vcal ( 'addressbook' , 'text/vcard' );
// Apple iOS or OS X addressbook
if ( $this -> agent = 'cfnetwork' || $this -> agent == 'dataaccess' )
2010-09-25 17:19:48 +02:00
{
2012-02-10 12:16:44 +01:00
$supportedFields = $handler -> supportedFields ;
2010-09-25 17:19:48 +02:00
// Apple Addressbook don't support CLASS
2012-02-10 12:16:44 +01:00
unset ( $supportedFields [ 'CLASS' ]);
unset ( $supportedFields [ 'CATEGORIES' ]);
// 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'),
$supportedFields [ 'TEL;CELL' ] = array ( 'tel_cell' );
unset ( $supportedFields [ 'TEL;CELL;WORK' ]);
$supportedFields [ 'TEL;IPHONE' ] = array ( 'tel_cell_private' );
unset ( $supportedFields [ 'TEL;CELL;HOME' ]);
}
$handler -> setSupportedFields ( 'GroupDAV' , $this -> agent , isset ( $supportedFields ) ?
$supportedFields : $handler -> supportedFields );
2008-11-03 10:36:20 +01:00
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-02-10 12:44:52 +01:00
if (( $Ok = isset ( $contact [ 'list_id' ]) ? $this -> bo -> delete_list ( $contact [ 'list_id' ]) !== false :
$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-02-10 12:44:52 +01:00
return $Ok ;
2008-05-08 22:31:32 +02:00
}
/**
* Read a contact
*
2011-11-06 10:40:33 +01: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-10 12:44:52 +01:00
* @ param string | int $id
* @ param string $path = null
* @ 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-10 12:44:52 +01:00
function read ( $id , $path = null )
2008-05-08 22:31:32 +02:00
{
2011-11-06 10:40:33 +01:00
static $non_deleted_tids ;
if ( is_null ( $non_deleted_tids ))
{
$non_deleted_tids = $this -> bo -> content_types ;
unset ( $non_deleted_tids [ addressbook_so :: DELETED_TYPE ]);
$non_deleted_tids = array_keys ( $non_deleted_tids );
}
$contact = $this -> bo -> read ( array ( self :: $path_attr => $id , 'tid' => $non_deleted_tids ));
2011-06-19 11:01:15 +02:00
2012-02-10 12:11:38 +01:00
// see if we have a distribution-list / group with that id
// 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-10 12:44:52 +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-02-10 12:11:38 +01:00
{
$contact = array_shift ( $contact );
$contact [ 'n_fn' ] = $contact [ 'n_family' ] = $contact [ 'list_name' ];
foreach ( array ( 'owner' , 'id' , 'carddav_name' , 'modified' , 'modifier' , 'created' , 'creator' , 'etag' , 'uid' ) as $name )
{
$contact [ $name ] = $contact [ 'list_' . $name ];
}
2012-02-10 12:44:52 +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-02-10 12:11:38 +01:00
}
elseif ( $contact === array ()) // not found from read_lists()
{
$contact = null ;
}
2011-06-19 11:01:15 +02:00
if ( $contact && $contact [ 'tid' ] == addressbook_so :: DELETED_TYPE )
{
$contact = null ; // handle deleted events, as not existing (404 Not Found)
}
2012-02-10 12:11:38 +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
*
* @ param int $acl EGW_ACL_READ , EGW_ACL_EDIT or EGW_ACL_DELETE
2012-02-10 12:44:52 +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 )
{
return $this -> bo -> check_perms ( $acl , $contact );
}
2012-02-10 12:27:26 +01:00
/**
* Return calendars / addressbooks shared from other users with the current one
*
2012-02-10 12:44:52 +01:00
* @ param boolean $ignore_all_in_one = false if true , return selected addressbooks and not array () for all - in - one
* @ return array account_id => account_lid pairs
2012-02-10 12:27:26 +01:00
*/
2012-02-10 12:44:52 +01:00
function get_shared ( $ignore_all_in_one = false )
2012-02-10 12:27:26 +01:00
{
$shared = array ();
2012-02-10 12:44:52 +01:00
// if "Sync all selected addressbook into one" is set --> no (additional) shared addressbooks
if ( ! $ignore_all_in_one && in_array ( 'O' , $this -> home_set_pref )) return array ();
2012-02-10 12:27:26 +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-10 12:44:52 +01:00
if (( $key = array_search ( $sym , $this -> home_set_pref )) !== false )
2012-02-10 12:27:26 +01:00
{
2012-02-10 12:44:52 +01:00
$this -> home_set_pref [ $key ] = $id ;
2012-02-10 12:27:26 +01:00
}
}
foreach ( $this -> bo -> get_addressbooks ( EGW_ACL_READ ) as $id => $label )
{
if (( $id || ! $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'addressbook' ][ 'hide_accounts' ]) &&
2012-02-10 12:44:52 +01:00
$GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] != $id && // no current user and no accounts, if disabled in ab prefs
( in_array ( 'A' , $this -> home_set_pref ) || in_array (( string ) $id , $this -> home_set_pref )) &&
2012-02-10 12:27:26 +01:00
is_numeric ( $id ) && ( $owner = $id ? $this -> accounts -> id2name ( $id ) : 'accounts' ))
{
$shared [ $id ] = $owner ;
}
}
return $shared ;
}
/**
* Return appliction specific settings
*
* return array of array with settings
*/
static function get_settings ()
{
if ( $hook_data [ 'setup' ])
{
$addressbooks = array ();
}
else
{
$user = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
$addressbook_bo = new addressbook_bo ();
$addressbooks = $addressbook_bo -> get_addressbooks ( EGW_ACL_READ );
unset ( $addressbooks [ $user ]); // allways synced
unset ( $addressbooks [ $user . 'p' ]); // ignore (optional) private addressbook for now
}
$addressbooks = array (
'A' => lang ( 'All' ),
'G' => lang ( 'Primary Group' ),
'U' => lang ( 'Accounts' ),
2012-02-10 12:44:52 +01:00
'O' => lang ( 'Sync all selected into one' ),
'D' => lang ( 'Distribution lists as groups' )
2012-02-10 12:27:26 +01:00
) + $addressbooks ;
// rewriting owner=0 to 'U', as 0 get's always selected by prefs
if ( ! isset ( $addressbooks [ 0 ]))
{
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' ,
2012-02-10 12:44:52 +01:00
'help' => lang ( 'Only supported by a few fully conformant clients (eg. from Apple). If you have to enter a URL, it will most likly not be suppored!' ) .
'<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-10 12:27:26 +01:00
'values' => $addressbooks ,
'xmlrpc' => True ,
'admin' => False ,
);
return $settings ;
}
2009-07-15 21:44:09 +02:00
}