diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index 32714f93db..464085e7e1 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -17,7 +17,7 @@ * Propfind now uses a groupdav_propfind_iterator with a callback to query huge addressbooks in chunk, * without getting into problems with memory_limit. * - * @todo create extra addressbook eg. "/accounts/" which shows accounts, even if they are in LDAP (no carddav_name column!) + * @todo check/fix contacts in LDAP (no carddav_name column!) */ class addressbook_groupdav extends groupdav_handler { @@ -43,6 +43,19 @@ class addressbook_groupdav extends groupdav_handler */ var $charset = 'utf-8'; + /** + * '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 (//addressbook/) + * or nummerical account_id, but not user itself + * + * @var array + */ + var $home_set_pref; + /** * Constructor * @@ -65,6 +78,15 @@ class addressbook_groupdav extends groupdav_handler { groupdav_handler::$path_extension = '.vcf'; } + $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'; + } } /** @@ -80,8 +102,17 @@ class addressbook_groupdav extends groupdav_handler function propfind($path,$options,&$files,$user,$id='') { $filter = array(); + // 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; + } // show addressbook of a single user? - if ($user && $path != '/addressbook/' || $user === 0) $filter['contact_owner'] = $user; + elseif ($user && $path != '/addressbook/' || $user === 0) + { + $filter['contact_owner'] = $user; + } // should we hide the accounts addressbook if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) $filter['account_id'] = null; @@ -136,6 +167,7 @@ class addressbook_groupdav extends groupdav_handler $handler = self::_get_handler(); } unset($filter['address_data']); + if (isset($filter['order'])) { $order = $filter['order']; @@ -167,8 +199,8 @@ class addressbook_groupdav extends groupdav_handler $files[] = $this->add_resource($path, $contact, $props); } } - // add groups after contacts - if (!$start || count($contacts) < $start[1]) + // 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'])) { $where = array( 'list_owner' => isset($filter['contact_owner'])?$filter['contact_owner']:array_keys($this->bo->grants) @@ -177,18 +209,23 @@ class addressbook_groupdav extends groupdav_handler { $where['list_'.self::$path_attr] = $filter[self::$path_attr]; } - //error_log(__METHOD__."() filter=".array2string($filter).', where='.array2string($where)); - if (($lists = $this->bo->read_lists($where,'contact_uid',true))) // true = limit to contacts in same AB! + //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! { - //_debug_array($lists); foreach($lists as $list) { $list['carddav_name'] = $list['list_carddav_name']; + $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']); + } $props = array( 'getcontenttype' => HTTP_WebDAV_Server::mkprop('getcontenttype', 'text/vcard'), 'getlastmodified' => egw_time::to($list['list_modified'],'ts'), 'displayname' => $list['list_name'], - 'getetag' => '"'.$list['list_id'].':'.$list['list_etag'].'"', + 'getetag' => '"'.$etag.'"', ); if ($address_data) { @@ -503,7 +540,7 @@ class addressbook_groupdav extends groupdav_handler if (!isset($contact['etag'])) { - $contact = $this->read($save_ok); + $contact = $this->read($save_ok,$options['path']); } // send evtl. necessary respose headers: Location, etag, ... @@ -583,6 +620,8 @@ class addressbook_groupdav extends groupdav_handler /** * Query ctag for addressbook * + * @param string $path + * @param int $user * @return string */ public function getctag($path,$user) @@ -590,36 +629,19 @@ class addressbook_groupdav extends groupdav_handler // not showing addressbook of a single user? if (!$user || $path == '/addressbook/') $user = null; - return max($this->bo->get_ctag($user),$this->bo->lists_ctag($user)); - } - - /** - * Add the privileges of the current user - * - * @param array $props=array() regular props by the groupdav handler - * @return array - */ - static function current_user_privilege_set(array $props=array()) - { - $props[] = HTTP_WebDAV_Server::mkprop(groupdav::DAV,'current-user-privilege-set', - array(HTTP_WebDAV_Server::mkprop(groupdav::DAV,'privilege', - array( - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read',''), - HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'read-free-busy',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'read-current-user-privilege-set',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'bind',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'unbind',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-post-vevent',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-respond-vevent',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'schedule-deliver-vevent',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-properties',''), - HTTP_WebDAV_Server::mkprop(groupdav::DAV,'write-content',''), - )))); - return $props; + // 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); } /** @@ -710,11 +732,12 @@ class addressbook_groupdav extends groupdav_handler { return $contact; } - if (($Ok = $this->bo->delete($contact['id'],self::etag2value($this->http_if_match))) === 0) + 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) { return '412 Precondition Failed'; } - return true; + return $Ok; } /** @@ -723,10 +746,11 @@ class addressbook_groupdav extends groupdav_handler * 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 * - * @param string|id $id - * @return array/boolean array with entry, false if no read rights, null if $id does not exist + * @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 */ - function read($id) + function read($id, $path=null) { static $non_deleted_tids; if (is_null($non_deleted_tids)) @@ -740,7 +764,21 @@ class addressbook_groupdav extends groupdav_handler // 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! - if (!$contact && ($contact = $this->bo->read_lists(array('list_'.self::$path_attr => $id),'contact_uid',true))) + $limit_in_ab = true; + list(,$account_lid,$app) = explode('/',$path); // eg. //addressbook/ + // //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))) { $contact = array_shift($contact); $contact['n_fn'] = $contact['n_family'] = $contact['list_name']; @@ -748,6 +786,11 @@ class addressbook_groupdav extends groupdav_handler { $contact[$name] = $contact['list_'.$name]; } + // 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); + } } elseif($contact === array()) // not found from read_lists() { @@ -766,7 +809,7 @@ class addressbook_groupdav extends groupdav_handler * Check if user has the neccessary rights on a contact * * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE - * @param array/int $contact contact-array or id + * @param array|int $contact contact-array or id * @return boolean null if entry does not exist, false if no access, true if access permitted */ function check_access($acl,$contact) @@ -777,30 +820,32 @@ class addressbook_groupdav extends groupdav_handler /** * Return calendars/addressbooks shared from other users with the current one * - * return array account_id => account_lid pairs + * @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 */ - function get_shared() + function get_shared($ignore_all_in_one=false) { $shared = array(); - $addressbook_home_set = $GLOBALS['egw_info']['user']['preferences']['groupdav']['addressbook-home-set']; - if (empty($addressbook_home_set)) $addressbook_home_set = 'P'; // personal addressbook - $addressbook_home_set = explode(',',$addressbook_home_set); + + // 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(); + // replace symbolic id's with real nummeric id's foreach(array( 'G' => $GLOBALS['egw_info']['user']['account_primary_group'], 'U' => '0', ) as $sym => $id) { - if (($key = array_search($sym, $addressbook_home_set)) !== false) + if (($key = array_search($sym, $this->home_set_pref)) !== false) { - $addressbook_home_set[$key] = $id; + $this->home_set_pref[$key] = $id; } } foreach($this->bo->get_addressbooks(EGW_ACL_READ) as $id => $label) { if (($id || !$GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) && - $user != $id && // no current user and no accounts, if disabled in ab prefs - (in_array('A',$addressbook_home_set) || in_array((string)$id,$addressbook_home_set)) && + $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)) && is_numeric($id) && ($owner = $id ? $this->accounts->id2name($id) : 'accounts')) { $shared[$id] = $owner; @@ -832,6 +877,8 @@ class addressbook_groupdav extends groupdav_handler 'A' => lang('All'), 'G' => lang('Primary Group'), 'U' => lang('Accounts'), + 'O' => lang('Sync all selected into one'), + 'D' => lang('Distribution lists as groups') ) + $addressbooks; // rewriting owner=0 to 'U', as 0 get's always selected by prefs @@ -849,7 +896,10 @@ class addressbook_groupdav extends groupdav_handler 'type' => 'multiselect', 'label' => 'Addressbooks to sync in addition to personal addressbook', 'name' => 'addressbook-home-set', - '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!').'
'.lang('They will be sub-folders in users home (%1 attribute).','CardDAV "addressbook-home-set"'), + '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!'). + '
'.lang('They will be sub-folders in users home (%1 attribute).','CardDAV "addressbook-home-set"'). + '
'.lang('Select "%1", if your client does not support multiple addressbooks.',lang('Sync all selected into one')). + '
'.lang('Select "%1", if your client support groups, eg. OS X or iOS addressbook.',lang('Distribution lists as groups')), 'values' => $addressbooks, 'xmlrpc' => True, 'admin' => False, diff --git a/addressbook/inc/class.addressbook_sql.inc.php b/addressbook/inc/class.addressbook_sql.inc.php index f03d7da5ed..9fc0d6a72f 100644 --- a/addressbook/inc/class.addressbook_sql.inc.php +++ b/addressbook/inc/class.addressbook_sql.inc.php @@ -462,7 +462,8 @@ class addressbook_sql extends so_sql_cf * or whole where array: column-name => value(s) pairs * @param string $uid_column='list_owner' column-name or null to use $uids as where array * @param string $member_attr=null null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute - * @param boolean $limit_in_ab=false if true only return members from the same owners addressbook + * @param boolean|int|array $limit_in_ab=false if true only return members from the same owners addressbook, + * if int|array only return members from the given owners addressbook(s) * @return array with list_id => array(list_id,list_name,list_owner,...) pairs */ function get_lists($uids,$uid_column='list_owner',$member_attr=null,$limit_in_ab=false) @@ -476,11 +477,22 @@ class addressbook_sql extends so_sql_cf } if ($lists && $member_attr && in_array($member_attr,array('contact_id','contact_uid','caldav_name'))) { + if ($limit_in_ab) + { + $in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND $this->lists_table."; + if (!is_bool($limit_in_ab)) + { + $in_ab_join .= $this->db->expression($this->lists_table, array('list_owner'=>$limit_in_ab)); + } + else + { + $in_ab_join .= "list_owner=$this->table_name.contact_owner"; + } + } foreach($this->db->select($this->ab2list_table,"$this->ab2list_table.list_id,$this->table_name.$member_attr", $this->db->expression($this->ab2list_table, $this->ab2list_table.'.', array('list_id'=>array_keys($lists))), __LINE__,__FILE__,false,$member_attr=='contact_id' ? '' : - '',false,0,"JOIN $this->table_name ON $this->ab2list_table.contact_id=$this->table_name.contact_id". - ($limit_in_ab?" JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND $this->lists_table.list_owner=$this->table_name.contact_owner":'')) as $row) + '',false,0,"JOIN $this->table_name ON $this->ab2list_table.contact_id=$this->table_name.contact_id".$in_ab_join) as $row) { $lists[$row['list_id']]['members'][] = $row[$member_attr]; } diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index a32b5684bc..64cd5d3a8b 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -736,6 +736,12 @@ class addressbook_vcal extends addressbook_bo $rowName = 'URL;X-egw-Ref' . $url++; } + // current algorithm cant cope with multiple attributes of same name + // --> cumulate them in values, so they can be used later (works only for values, not for parameters!) + if (($k = array_search($rowName, $rowNames)) != false) + { + $vcardValues[$k]['values'] = array_merge($vcardValues[$k]['values'],$vcardValues[$key]['values']); + } $rowNames[$key] = $rowName; } @@ -976,7 +982,7 @@ class addressbook_vcal extends addressbook_bo // for attributes with multiple values in multiple lines, merge the values if (isset($contact['##'.$attribute['name']])) { - error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($contact['##'.$attribute['name']])); + error_log(__METHOD__."() contact['##$attribute[name]'] = ".array2string($contact['##'.$attribute['name']])); $attribute['values'] = array_merge( is_array($contact['##'.$attribute['name']]) ? $contact['##'.$attribute['name']]['values'] : (array)$contact['##'.$attribute['name']], $attribute['values']); diff --git a/phpgwapi/inc/class.groupdav.inc.php b/phpgwapi/inc/class.groupdav.inc.php index 51a5e2ff5a..f4a9e8eb44 100644 --- a/phpgwapi/inc/class.groupdav.inc.php +++ b/phpgwapi/inc/class.groupdav.inc.php @@ -26,7 +26,9 @@ require_once('HTTP/WebDAV/Server.php'); * - /principals/groups// * - // users home-set with * - //addressbook/ addressbook of user or group given the user has rights to view it + * - //addressbook-/ shared addressbooks from other user or group * - //calendar/ calendar of user given the user has rights to view it + * - //calendar-/ shared calendar from other user or group * - //inbox/ scheduling inbox of user * - //outbox/ scheduling outbox of user * - //infolog/ InfoLog's of user given the user has rights to view it @@ -289,7 +291,10 @@ class groupdav extends HTTP_WebDAV_Server */ function OPTIONS($path, &$dav, &$allow) { - if (preg_match('#/(calendar|inbox|outbox)/#', $path)) + // locking support + if (!in_array('2', $dav)) $dav[] = '2'; + + if (preg_match('#/(calendar(-[^/]+)?|inbox|outbox)/#', $path)) // eg. //calendar-/ { $app = 'calendar'; } @@ -315,7 +320,6 @@ class groupdav extends HTTP_WebDAV_Server //$dav[] = 'calendarserver-private-comments'; //$dav[] = 'calendarserver-sharing'; //$dav[] = 'calendarserver-sharing-no-scheduling'; - } if ($app !== 'calendar') // CardDAV { @@ -662,7 +666,8 @@ class groupdav extends HTTP_WebDAV_Server foreach($shared as $id => $owner) { $file = $this->add_app($app,false,$id,$path.$app.'-'.$owner.'/'); - $file['props']['resourcetype']['val'][] = self::mkprop(self::CALENDARSERVER,'shared',''); + // mark other users calendar as shared (iOS 5.0.1 AB does NOT display AB marked as shared!) + if ($app == 'calendar') $file['props']['resourcetype']['val'][] = self::mkprop(self::CALENDARSERVER,'shared',''); $files[] = $file; } } @@ -1477,31 +1482,6 @@ class groupdav extends HTTP_WebDAV_Server } return $ok; } - /** - * Add the privileges of the current user - * - * @return array self::mkprop('privilege',array(...)) - */ - static function current_user_privilege_set() - { - return array(self::mkprop('privilege', - array(//self::mkprop('all',''), - self::mkprop('read',''), - self::mkprop('read-free-busy',''), - //self::mkprop('read-current-user-privilege-set',''), - self::mkprop('bind',''), - self::mkprop('unbind',''), - self::mkprop('schedule-post',''), - self::mkprop('schedule-post-vevent',''), - self::mkprop('schedule-respond',''), - self::mkprop('schedule-respond-vevent',''), - self::mkprop('schedule-deliver',''), - self::mkprop('schedule-deliver-vevent',''), - self::mkprop('write',''), - self::mkprop('write-properties',''), - self::mkprop('write-content',''), - ))); - } /** * Serve WebDAV HTTP request diff --git a/phpgwapi/inc/class.groupdav_handler.inc.php b/phpgwapi/inc/class.groupdav_handler.inc.php index db8e91a516..705bc50e81 100644 --- a/phpgwapi/inc/class.groupdav_handler.inc.php +++ b/phpgwapi/inc/class.groupdav_handler.inc.php @@ -173,16 +173,17 @@ abstract class groupdav_handler /** * Read an entry * - * @param string/int $id - * @return array/boolean array with entry, false if no read rights, null if $id does not exist + * @param string|int $id + * @param string $path=null implementation can use it, used in call from _common_get_put_delete + * @return array|boolean array with entry, false if no read rights, null if $id does not exist */ - abstract function read($id); + abstract function read($id /*,$path=null*/); /** * Check if user has the neccessary rights on an entry * * @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE - * @param array/int $entry entry-array or id + * @param array|int $entry entry-array or id * @return boolean null if entry does not exist, false if no access, true if access permitted */ abstract function check_access($acl,$entry); @@ -204,8 +205,8 @@ abstract class groupdav_handler /** * Get the etag for an entry, can be reimplemented for other algorithm or field names * - * @param array/int $event array with event or cal_id - * @return string/boolean string with etag or false + * @param array|int $event array with event or cal_id + * @return string|boolean string with etag or false */ function get_etag($entry) { @@ -257,7 +258,7 @@ abstract class groupdav_handler return '403 Forbidden'; // no app rights } $extra_acl = $this->method2acl[$method]; - if (!($entry = $this->read($id)) && ($method != 'PUT' || $entry === false) || + if (!($entry = $this->read($id, $options['path'])) && ($method != 'PUT' || $entry === false) || ($extra_acl != EGW_ACL_READ && $this->check_access($extra_acl,$entry) === false)) { if ($return_no_access && !is_null($entry)) @@ -284,7 +285,7 @@ abstract class groupdav_handler if ($this->http_if_match !== $etag) { - if ($this->debug) error_log(__METHOD__."($method,,$id) HTTP_IF_MATCH='$_SERVER[HTTP_IF_MATCH]', etag='$etag': 412 Precondition failed"); + if ($this->debug) error_log(__METHOD__."($method,path=$options[path],$id) HTTP_IF_MATCH='$_SERVER[HTTP_IF_MATCH]', etag='$etag': 412 Precondition failed".array2string($entry)); return '412 Precondition Failed'; } }