| Total Complexity | 254 |
| Total Lines | 1150 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like addressbook_groupdav often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use addressbook_groupdav, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 26 | class addressbook_groupdav extends Api\CalDAV\Handler |
||
| 27 | { |
||
| 28 | /** |
||
| 29 | * bo class of the application |
||
| 30 | * |
||
| 31 | * @var Api\Contacts |
||
| 32 | */ |
||
| 33 | var $bo; |
||
| 34 | |||
| 35 | var $filter_prop2cal = array( |
||
| 36 | 'UID' => 'uid', |
||
| 37 | //'NICKNAME', |
||
| 38 | 'EMAIL' => 'email', |
||
| 39 | 'FN' => 'n_fn', |
||
| 40 | 'ORG' => 'org_name', |
||
| 41 | ); |
||
| 42 | |||
| 43 | /** |
||
| 44 | * Charset for exporting data, as some clients ignore the headers specifying the charset |
||
| 45 | * |
||
| 46 | * @var string |
||
| 47 | */ |
||
| 48 | var $charset = 'utf-8'; |
||
| 49 | |||
| 50 | /** |
||
| 51 | * 'addressbook_home_set' preference already exploded as array |
||
| 52 | * |
||
| 53 | * A = all available addressbooks |
||
| 54 | * G = primary group |
||
| 55 | * D = distribution lists as groups |
||
| 56 | * O = sync all in one (/<username>/addressbook/) |
||
| 57 | * or nummerical account_id, but not user itself |
||
| 58 | * |
||
| 59 | * @var array |
||
| 60 | */ |
||
| 61 | var $home_set_pref; |
||
| 62 | |||
| 63 | /** |
||
| 64 | * Constructor |
||
| 65 | * |
||
| 66 | * @param string $app 'calendar', 'addressbook' or 'infolog' |
||
| 67 | * @param Api\CalDAV $caldav calling class |
||
| 68 | */ |
||
| 69 | function __construct($app, Api\CalDAV $caldav) |
||
| 70 | { |
||
| 71 | parent::__construct($app, $caldav); |
||
| 72 | |||
| 73 | $this->bo = new Api\Contacts(); |
||
| 74 | |||
| 75 | // since 1.9.007 we allow clients to specify the URL when creating a new contact, as specified by CardDAV |
||
| 76 | // LDAP does NOT have a carddav_name attribute --> stick with id mapped to LDAP attribute uid |
||
| 77 | if (version_compare($GLOBALS['egw_info']['apps']['api']['version'], '1.9.007', '<') || |
||
| 78 | $this->bo->contact_repository != 'sql' || |
||
| 79 | $this->bo->account_repository != 'sql' && strpos($_SERVER['REQUEST_URI'].'/','/addressbook-accounts/') !== false) |
||
| 80 | { |
||
| 81 | self::$path_extension = '.vcf'; |
||
| 82 | } |
||
| 83 | else |
||
| 84 | { |
||
| 85 | self::$path_attr = 'carddav_name'; |
||
| 86 | self::$path_extension = ''; |
||
| 87 | } |
||
| 88 | 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); |
||
| 89 | |||
| 90 | $this->home_set_pref = $GLOBALS['egw_info']['user']['preferences']['groupdav']['addressbook-home-set']; |
||
| 91 | $this->home_set_pref = $this->home_set_pref ? explode(',',$this->home_set_pref) : array(); |
||
| 92 | |||
| 93 | // silently switch "Sync all into one" preference on for OS X addressbook, as it only supports one AB |
||
| 94 | // this restores behavior before Lion (10.7), where AB synced all ABs contained in addressbook-home-set |
||
| 95 | if (substr(self::get_agent(),0,9) == 'cfnetwork' && !in_array('O',$this->home_set_pref)) |
||
| 96 | { |
||
| 97 | $this->home_set_pref[] = 'O'; |
||
| 98 | } |
||
| 99 | } |
||
| 100 | |||
| 101 | /** |
||
| 102 | * Handle propfind in the addressbook folder |
||
| 103 | * |
||
| 104 | * @param string $path |
||
| 105 | * @param array &$options |
||
| 106 | * @param array &$files |
||
| 107 | * @param int $user account_id |
||
| 108 | * @param string $id ='' |
||
| 109 | * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') |
||
| 110 | */ |
||
| 111 | function propfind($path,&$options,&$files,$user,$id='') |
||
| 112 | { |
||
| 113 | $filter = array(); |
||
| 114 | // If "Sync selected addressbooks into one" is set |
||
| 115 | if ($user && $user == $GLOBALS['egw_info']['user']['account_id'] && in_array('O',$this->home_set_pref)) |
||
| 116 | { |
||
| 117 | $filter['owner'] = array_keys($this->get_shared(true)); // true: ignore all-in-one pref |
||
| 118 | $filter['owner'][] = $user; |
||
| 119 | } |
||
| 120 | // show addressbook of a single user? |
||
| 121 | elseif ($user && $path != '/addressbook/' || $user === 0) |
||
|
|
|||
| 122 | { |
||
| 123 | $filter['owner'] = $user; |
||
| 124 | } |
||
| 125 | // should we hide the accounts addressbook |
||
| 126 | if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1') $filter['account_id'] = null; |
||
| 127 | |||
| 128 | // process REPORT filters or multiget href's |
||
| 129 | $nresults = null; |
||
| 130 | if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$filter,$id, $nresults)) |
||
| 131 | { |
||
| 132 | return false; |
||
| 133 | } |
||
| 134 | if ($id) $path = dirname($path).'/'; // carddav_name get's added anyway in the callback |
||
| 135 | |||
| 136 | if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user,$id) filter=".array2string($filter)); |
||
| 137 | |||
| 138 | // check if we have to return the full contact data or just the etag's |
||
| 139 | if (!($filter['address_data'] = $options['props'] == 'all' && |
||
| 140 | $options['root']['ns'] == Api\CalDAV::CARDDAV) && is_array($options['props'])) |
||
| 141 | { |
||
| 142 | foreach($options['props'] as $prop) |
||
| 143 | { |
||
| 144 | if ($prop['name'] == 'address-data') |
||
| 145 | { |
||
| 146 | $filter['address_data'] = true; |
||
| 147 | break; |
||
| 148 | } |
||
| 149 | } |
||
| 150 | } |
||
| 151 | // rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters |
||
| 152 | if ($options['root']['name'] == 'sync-collection') |
||
| 153 | { |
||
| 154 | // callback to query sync-token, after propfind_callbacks / iterator is run and |
||
| 155 | // stored max. modification-time in $this->sync_collection_token |
||
| 156 | $files['sync-token'] = array($this, 'get_sync_collection_token'); |
||
| 157 | $files['sync-token-params'] = array($path, $user); |
||
| 158 | |||
| 159 | $this->sync_collection_token = null; |
||
| 160 | |||
| 161 | $filter['order'] = 'contact_modified ASC'; // return oldest modifications first |
||
| 162 | $filter['sync-collection'] = true; |
||
| 163 | } |
||
| 164 | |||
| 165 | if (isset($nresults)) |
||
| 166 | { |
||
| 167 | $files['files'] = $this->propfind_callback($path, $filter, array(0, (int)$nresults)); |
||
| 168 | |||
| 169 | // hack to support limit with sync-collection report: contacts are returned in modified ASC order (oldest first) |
||
| 170 | // if limit is smaller then full result, return modified-1 as sync-token, so client requests next chunk incl. modified |
||
| 171 | // (which might contain further entries with identical modification time) |
||
| 172 | if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults) |
||
| 173 | { |
||
| 174 | --$this->sync_collection_token; |
||
| 175 | $files['sync-token-params'][] = true; // tel get_sync_collection_token that we have more entries |
||
| 176 | } |
||
| 177 | } |
||
| 178 | else |
||
| 179 | { |
||
| 180 | // return iterator, calling ourself to return result in chunks |
||
| 181 | $files['files'] = new Api\CalDAV\PropfindIterator($this,$path,$filter,$files['files']); |
||
| 182 | } |
||
| 183 | return true; |
||
| 184 | } |
||
| 185 | |||
| 186 | /** |
||
| 187 | * Callback for profind iterator |
||
| 188 | * |
||
| 189 | * @param string $path |
||
| 190 | * @param array& $filter |
||
| 191 | * @param array|boolean $start =false false=return all or array(start,num) |
||
| 192 | * @return array with "files" array with values for keys path and props |
||
| 193 | */ |
||
| 194 | function &propfind_callback($path,array &$filter,$start=false,$report_not_found_multiget_ids=true) |
||
| 195 | { |
||
| 196 | //error_log(__METHOD__."('$path', ".array2string($filter).", ".array2string($start).", $report_not_found_multiget_ids)"); |
||
| 197 | $starttime = microtime(true); |
||
| 198 | $filter_in = $filter; |
||
| 199 | |||
| 200 | if (($address_data = $filter['address_data'])) |
||
| 201 | { |
||
| 202 | $handler = self::_get_handler(); |
||
| 203 | } |
||
| 204 | unset($filter['address_data']); |
||
| 205 | |||
| 206 | if (isset($filter['order'])) |
||
| 207 | { |
||
| 208 | $order = $filter['order']; |
||
| 209 | unset($filter['order']); |
||
| 210 | } |
||
| 211 | else |
||
| 212 | { |
||
| 213 | $order = 'egw_addressbook.contact_id'; |
||
| 214 | } |
||
| 215 | // detect sync-collection report |
||
| 216 | $sync_collection_report = $filter['sync-collection']; |
||
| 217 | unset($filter['sync-collection']); |
||
| 218 | |||
| 219 | if (isset($filter[self::$path_attr])) |
||
| 220 | { |
||
| 221 | if (!is_array($filter[self::$path_attr])) $filter[self::$path_attr] = (array)$filter[self::$path_attr]; |
||
| 222 | $requested_multiget_ids =& $filter[self::$path_attr]; |
||
| 223 | } |
||
| 224 | |||
| 225 | $files = array(); |
||
| 226 | // we query etag and modified, as LDAP does not have the strong sql etag |
||
| 227 | $cols = array('id','uid','etag','modified','n_fn'); |
||
| 228 | if (!in_array(self::$path_attr,$cols)) $cols[] = self::$path_attr; |
||
| 229 | // we need tid for sync-collection report |
||
| 230 | if (array_key_exists('tid', $filter) && !isset($filter['tid']) && !in_array('tid', $cols)) $cols[] = 'tid'; |
||
| 231 | if (($contacts =& $this->bo->search(array(),$cols,$order,'','',False,'AND',$start,$filter))) |
||
| 232 | { |
||
| 233 | foreach($contacts as &$contact) |
||
| 234 | { |
||
| 235 | // remove contact from requested multiget ids, to be able to report not found urls |
||
| 236 | if ($requested_multiget_ids && ($k = array_search($contact[self::$path_attr], $requested_multiget_ids)) !== false) |
||
| 237 | { |
||
| 238 | unset($requested_multiget_ids[$k]); |
||
| 239 | } |
||
| 240 | // sync-collection report: deleted entry need to be reported without properties |
||
| 241 | if ($contact['tid'] == Api\Contacts::DELETED_TYPE) |
||
| 242 | { |
||
| 243 | $files[] = array('path' => $path.urldecode($this->get_path($contact))); |
||
| 244 | continue; |
||
| 245 | } |
||
| 246 | $props = array( |
||
| 247 | 'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'), |
||
| 248 | 'getlastmodified' => $contact['modified'], |
||
| 249 | 'displayname' => $contact['n_fn'], |
||
| 250 | ); |
||
| 251 | if ($address_data) |
||
| 252 | { |
||
| 253 | $content = $handler->getVCard($contact['id'],$this->charset,false); |
||
| 254 | $props['getcontentlength'] = bytes($content); |
||
| 255 | $props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content); |
||
| 256 | } |
||
| 257 | $files[] = $this->add_resource($path, $contact, $props); |
||
| 258 | } |
||
| 259 | // sync-collection report --> return modified of last contact as sync-token |
||
| 260 | if ($sync_collection_report) |
||
| 261 | { |
||
| 262 | $this->sync_collection_token = $contact['modified']; |
||
| 263 | } |
||
| 264 | } |
||
| 265 | // last chunk or no chunking: add accounts from different repo and report missing multiget urls |
||
| 266 | if (!$start || (empty($contact)?0:count($contacts)) < $start[1]) |
||
| 267 | { |
||
| 268 | //error_log(__METHOD__."('$path', ".array2string($filter).", ".array2string($start)."; $report_not_found_multiget_ids) last chunk detected: count()=".count($contacts)." < $start[1]"); |
||
| 269 | // add accounts after contacts, if enabled and stored in different repository |
||
| 270 | if ($this->bo->so_accounts && is_array($filter['owner']) && in_array('0', $filter['owner'])) |
||
| 271 | { |
||
| 272 | $accounts_filter = $filter_in; |
||
| 273 | $accounts_filter['owner'] = '0'; |
||
| 274 | if ($sync_collection_report) $token_was = $this->sync_collection_token; |
||
| 275 | self::$path_attr = 'id'; |
||
| 276 | self::$path_extension = '.vcf'; |
||
| 277 | $files = array_merge($files, $this->propfind_callback($path, $accounts_filter, false, false)); |
||
| 278 | self::$path_attr = 'carddav_name'; |
||
| 279 | self::$path_extension = ''; |
||
| 280 | if ($sync_collection_report && $token_was > $this->sync_collection_token) |
||
| 281 | { |
||
| 282 | $this->sync_collection_token = $token_was; |
||
| 283 | } |
||
| 284 | } |
||
| 285 | // add groups after contacts, but only if enabled and NOT for '/addressbook/' (!isset($filter['owner']) |
||
| 286 | if (in_array('D',$this->home_set_pref) && (string)$filter['owner'] !== '0') |
||
| 287 | { |
||
| 288 | $where = array( |
||
| 289 | 'list_owner' => isset($filter['owner'])?$filter['owner']:array_keys($this->bo->grants) |
||
| 290 | ); |
||
| 291 | // add sync-token to support sync-collection report |
||
| 292 | if ($sync_collection_report) |
||
| 293 | { |
||
| 294 | list(,$sync_token) = explode('>', $filter[0]); |
||
| 295 | if ((int)$sync_token) $where[] = 'list_modified>'.$GLOBALS['egw']->db->from_unixtime((int)$sync_token); |
||
| 296 | } |
||
| 297 | if (isset($filter[self::$path_attr])) // multiget report? |
||
| 298 | { |
||
| 299 | $where['list_'.self::$path_attr] = $filter[self::$path_attr]; |
||
| 300 | } |
||
| 301 | //error_log(__METHOD__."() filter=".array2string($filter).", do_groups=".in_array('D',$this->home_set_pref).", where=".array2string($where)); |
||
| 302 | if (($lists = $this->bo->read_lists($where,'contact_uid',$where['list_owner']))) // limit to contacts in same AB! |
||
| 303 | { |
||
| 304 | foreach($lists as $list) |
||
| 305 | { |
||
| 306 | $list[self::$path_attr] = $list['list_carddav_name']; |
||
| 307 | $etag = $list['list_id'].':'.$list['list_etag']; |
||
| 308 | // for all-in-one addressbook, add selected ABs to etag |
||
| 309 | if (isset($filter['owner']) && is_array($filter['owner'])) |
||
| 310 | { |
||
| 311 | $etag .= ':'.implode('-',$filter['owner']); |
||
| 312 | } |
||
| 313 | $props = array( |
||
| 314 | 'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'), |
||
| 315 | 'getlastmodified' => Api\DateTime::to($list['list_modified'],'ts'), |
||
| 316 | 'displayname' => $list['list_name'], |
||
| 317 | 'getetag' => '"'.$etag.'"', |
||
| 318 | ); |
||
| 319 | if ($address_data) |
||
| 320 | { |
||
| 321 | $content = $handler->getGroupVCard($list); |
||
| 322 | $props['getcontentlength'] = bytes($content); |
||
| 323 | $props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content); |
||
| 324 | } |
||
| 325 | $files[] = $this->add_resource($path, $list, $props); |
||
| 326 | |||
| 327 | // remove list from requested multiget ids, to be able to report not found urls |
||
| 328 | if ($requested_multiget_ids && ($k = array_search($list[self::$path_attr], $requested_multiget_ids)) !== false) |
||
| 329 | { |
||
| 330 | unset($requested_multiget_ids[$k]); |
||
| 331 | } |
||
| 332 | |||
| 333 | if ($sync_collection_report && $this->sync_collection_token < ($ts=$GLOBALS['egw']->db->from_timestamp($list['list_modified']))) |
||
| 334 | { |
||
| 335 | $this->sync_collection_token = $ts; |
||
| 336 | } |
||
| 337 | } |
||
| 338 | } |
||
| 339 | } |
||
| 340 | // report not found multiget urls |
||
| 341 | if ($report_not_found_multiget_ids && $requested_multiget_ids) |
||
| 342 | { |
||
| 343 | foreach($requested_multiget_ids as $id) |
||
| 344 | { |
||
| 345 | $files[] = array('path' => $path.$id.self::$path_extension); |
||
| 346 | } |
||
| 347 | } |
||
| 348 | } |
||
| 349 | |||
| 350 | if ($this->debug) error_log(__METHOD__."($path,".array2string($filter).','.array2string($start).") took ".(microtime(true) - $starttime).' to return '.count($files).' items'); |
||
| 351 | return $files; |
||
| 352 | } |
||
| 353 | |||
| 354 | /** |
||
| 355 | * Process the filters from the CalDAV REPORT request |
||
| 356 | * |
||
| 357 | * @param array $options |
||
| 358 | * @param array &$cal_filters |
||
| 359 | * @param string $id |
||
| 360 | * @param int &$nresult on return limit for number or results or unchanged/null |
||
| 361 | * @return boolean true if filter could be processed |
||
| 362 | */ |
||
| 363 | function _report_filters($options,&$filters,$id, &$nresults) |
||
| 364 | { |
||
| 365 | if ($options['filters']) |
||
| 366 | { |
||
| 367 | /* Example of a complex filter used by Mac Addressbook |
||
| 368 | <B:filter test="anyof"> |
||
| 369 | <B:prop-filter name="FN" test="allof"> |
||
| 370 | <B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match> |
||
| 371 | <B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match> |
||
| 372 | </B:prop-filter> |
||
| 373 | <B:prop-filter name="EMAIL" test="allof"> |
||
| 374 | <B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match> |
||
| 375 | <B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match> |
||
| 376 | </B:prop-filter> |
||
| 377 | <B:prop-filter name="NICKNAME" test="allof"> |
||
| 378 | <B:text-match collation="i;unicode-casemap" match-type="contains">becker</B:text-match> |
||
| 379 | <B:text-match collation="i;unicode-casemap" match-type="contains">ralf</B:text-match> |
||
| 380 | </B:prop-filter> |
||
| 381 | </B:filter> |
||
| 382 | */ |
||
| 383 | $filter_test = isset($options['filters']['attrs']) && isset($options['filters']['attrs']['test']) ? |
||
| 384 | $options['filters']['attrs']['test'] : 'anyof'; |
||
| 385 | $prop_filters = array(); |
||
| 386 | |||
| 387 | $matches = $prop_test = $column = null; |
||
| 388 | foreach($options['filters'] as $n => $filter) |
||
| 389 | { |
||
| 390 | if (!is_int($n)) continue; // eg. attributes of filter xml element |
||
| 391 | |||
| 392 | switch((string)$filter['name']) |
||
| 393 | { |
||
| 394 | case 'param-filter': |
||
| 395 | $this->caldav->log(__METHOD__."(...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!"); |
||
| 396 | break; |
||
| 397 | case 'prop-filter': // can be multiple prop-filter, see example |
||
| 398 | if ($matches) $prop_filters[] = implode($prop_test=='allof'?' AND ':' OR ',$matches); |
||
| 399 | $matches = array(); |
||
| 400 | $prop_filter = strtoupper($filter['attrs']['name']); |
||
| 401 | $prop_test = isset($filter['attrs']['test']) ? $filter['attrs']['test'] : 'anyof'; |
||
| 402 | if ($this->debug > 1) error_log(__METHOD__."(...) prop-filter='$prop_filter', test='$prop_test'"); |
||
| 403 | break; |
||
| 404 | case 'is-not-defined': |
||
| 405 | $matches[] = '('.$column."='' OR ".$column.' IS NULL)'; |
||
| 406 | break; |
||
| 407 | case 'text-match': // prop-filter can have multiple text-match, see example |
||
| 408 | if (!isset($this->filter_prop2cal[$prop_filter])) // eg. not existing NICKNAME in EGroupware |
||
| 409 | { |
||
| 410 | if ($this->debug || $prop_filter != 'NICKNAME') error_log(__METHOD__."(...) text-match: $prop_filter {$filter['attrs']['match-type']} '{$filter['data']}' unknown property '$prop_filter' --> ignored"); |
||
| 411 | $column = false; // to ignore following data too |
||
| 412 | } |
||
| 413 | else |
||
| 414 | { |
||
| 415 | switch($filter['attrs']['collation']) // todo: which other collations allowed, we are allways unicode |
||
| 416 | { |
||
| 417 | case 'i;unicode-casemap': |
||
| 418 | default: |
||
| 419 | $comp = ' '.$GLOBALS['egw']->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '; |
||
| 420 | break; |
||
| 421 | } |
||
| 422 | $column = $this->filter_prop2cal[strtoupper($prop_filter)]; |
||
| 423 | if (strpos($column, '_') === false) $column = 'contact_'.$column; |
||
| 424 | if (!isset($filters['order'])) $filters['order'] = $column; |
||
| 425 | $match_type = $filter['attrs']['match-type']; |
||
| 426 | $negate_condition = isset($filter['attrs']['negate-condition']) && $filter['attrs']['negate-condition'] == 'yes'; |
||
| 427 | } |
||
| 428 | break; |
||
| 429 | case '': // data of text-match element |
||
| 430 | if (isset($filter['data']) && isset($column)) |
||
| 431 | { |
||
| 432 | if ($column) // false for properties not known to EGroupware |
||
| 433 | { |
||
| 434 | $value = str_replace(array('%', '_'), array('\\%', '\\_'), $filter['data']); |
||
| 435 | switch($match_type) |
||
| 436 | { |
||
| 437 | case 'equals': |
||
| 438 | $sql_filter = $column . $comp . $GLOBALS['egw']->db->quote($value); |
||
| 439 | break; |
||
| 440 | default: |
||
| 441 | case 'contains': |
||
| 442 | $sql_filter = $column . $comp . $GLOBALS['egw']->db->quote('%'.$value.'%'); |
||
| 443 | break; |
||
| 444 | case 'starts-with': |
||
| 445 | $sql_filter = $column . $comp . $GLOBALS['egw']->db->quote($value.'%'); |
||
| 446 | break; |
||
| 447 | case 'ends-with': |
||
| 448 | $sql_filter = $column . $comp . $GLOBALS['egw']->db->quote('%'.$value); |
||
| 449 | break; |
||
| 450 | } |
||
| 451 | $matches[] = ($negate_condition ? 'NOT ' : '').$sql_filter; |
||
| 452 | |||
| 453 | if ($this->debug > 1) error_log(__METHOD__."(...) text-match: $prop_filter $match_type' '{$filter['data']}'"); |
||
| 454 | } |
||
| 455 | unset($column); |
||
| 456 | break; |
||
| 457 | } |
||
| 458 | // fall through |
||
| 459 | default: |
||
| 460 | $this->caldav->log(__METHOD__."(".array2string($options).",,$id) unknown filter=".array2string($filter).' --> ignored'); |
||
| 461 | break; |
||
| 462 | } |
||
| 463 | } |
||
| 464 | if ($matches) $prop_filters[] = implode($prop_test=='allof'?' AND ':' OR ',$matches); |
||
| 465 | if ($prop_filters) |
||
| 466 | { |
||
| 467 | $filters[] = $filter = '(('.implode($filter_test=='allof'?') AND (':') OR (', $prop_filters).'))'; |
||
| 468 | if ($this->debug) error_log(__METHOD__."(path=$options[path], ...) sql-filter: $filter"); |
||
| 469 | } |
||
| 470 | } |
||
| 471 | // parse limit from $options['other'] |
||
| 472 | /* Example limit |
||
| 473 | <B:limit> |
||
| 474 | <B:nresults>10</B:nresults> |
||
| 475 | </B:limit> |
||
| 476 | */ |
||
| 477 | foreach((array)$options['other'] as $option) |
||
| 478 | { |
||
| 479 | switch($option['name']) |
||
| 480 | { |
||
| 481 | case 'nresults': |
||
| 482 | $nresults = (int)$option['data']; |
||
| 483 | //error_log(__METHOD__."(...) options[other]=".array2string($options['other'])." --> nresults=$nresults"); |
||
| 484 | break; |
||
| 485 | case 'limit': |
||
| 486 | break; |
||
| 487 | case 'href': |
||
| 488 | break; // from addressbook-multiget, handled below |
||
| 489 | // rfc 6578 sync-report |
||
| 490 | case 'sync-token': |
||
| 491 | if (!empty($option['data'])) |
||
| 492 | { |
||
| 493 | $parts = explode('/', $option['data']); |
||
| 494 | $sync_token = array_pop($parts); |
||
| 495 | $filters[] = 'contact_modified>'.(int)$sync_token; |
||
| 496 | $filters['tid'] = null; // to return deleted entries too |
||
| 497 | } |
||
| 498 | break; |
||
| 499 | case 'sync-level': |
||
| 500 | if ($option['data'] != '1') |
||
| 501 | { |
||
| 502 | $this->caldav->log(__METHOD__."(...) only sync-level {$option['data']} requested, but only 1 supported! options[other]=".array2string($options['other'])); |
||
| 503 | } |
||
| 504 | break; |
||
| 505 | default: |
||
| 506 | $this->caldav->log(__METHOD__."(...) unknown xml tag '{$option['name']}': options[other]=".array2string($options['other'])); |
||
| 507 | break; |
||
| 508 | } |
||
| 509 | } |
||
| 510 | // multiget --> fetch the url's |
||
| 511 | if ($options['root']['name'] == 'addressbook-multiget') |
||
| 512 | { |
||
| 513 | $ids = array(); |
||
| 514 | foreach($options['other'] as $option) |
||
| 515 | { |
||
| 516 | if ($option['name'] == 'href') |
||
| 517 | { |
||
| 518 | $parts = explode('/',$option['data']); |
||
| 519 | if (($id = urldecode(array_pop($parts)))) |
||
| 520 | { |
||
| 521 | $ids[] = self::$path_extension ? basename($id,self::$path_extension) : $id; |
||
| 522 | } |
||
| 523 | } |
||
| 524 | } |
||
| 525 | if ($ids) $filters[self::$path_attr] = $ids; |
||
| 526 | if ($this->debug) error_log(__METHOD__."(...) addressbook-multiget: ids=".implode(',',$ids)); |
||
| 527 | } |
||
| 528 | elseif ($id) |
||
| 529 | { |
||
| 530 | $filters[self::$path_attr] = self::$path_extension ? basename($id,self::$path_extension) : $id; |
||
| 531 | } |
||
| 532 | //error_log(__METHOD__."() options[other]=".array2string($options['other'])." --> filters=".array2string($filters)); |
||
| 533 | return true; |
||
| 534 | } |
||
| 535 | |||
| 536 | /** |
||
| 537 | * Handle get request for an event |
||
| 538 | * |
||
| 539 | * @param array &$options |
||
| 540 | * @param int $id |
||
| 541 | * @param int $user =null account_id |
||
| 542 | * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') |
||
| 543 | */ |
||
| 544 | function get(&$options,$id,$user=null) |
||
| 545 | { |
||
| 546 | unset($user); // not used, but required by function signature |
||
| 547 | |||
| 548 | if (!is_array($contact = $this->_common_get_put_delete('GET',$options,$id))) |
||
| 549 | { |
||
| 550 | return $contact; |
||
| 551 | } |
||
| 552 | $handler = self::_get_handler(); |
||
| 553 | $options['data'] = $contact['list_id'] ? $handler->getGroupVCard($contact) : |
||
| 554 | $handler->getVCard($contact['id'],$this->charset,false); |
||
| 555 | // e.g. Evolution does not understand 'text/vcard' |
||
| 556 | $options['mimetype'] = 'text/x-vcard; charset='.$this->charset; |
||
| 557 | header('Content-Encoding: identity'); |
||
| 558 | header('ETag: "'.$this->get_etag($contact).'"'); |
||
| 559 | return true; |
||
| 560 | } |
||
| 561 | |||
| 562 | /** |
||
| 563 | * Handle put request for a contact |
||
| 564 | * |
||
| 565 | * @param array &$options |
||
| 566 | * @param int $id |
||
| 567 | * @param int $user =null account_id of owner, default null |
||
| 568 | * @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook) |
||
| 569 | * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') |
||
| 570 | */ |
||
| 571 | function put(&$options,$id,$user=null,$prefix=null) |
||
| 572 | { |
||
| 573 | if ($this->debug) error_log(__METHOD__.'('.array2string($options).",$id,$user)"); |
||
| 574 | |||
| 575 | $oldContact = $this->_common_get_put_delete('PUT',$options,$id); |
||
| 576 | if (!is_null($oldContact) && !is_array($oldContact)) |
||
| 577 | { |
||
| 578 | if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($oldContact)); |
||
| 579 | return $oldContact; |
||
| 580 | } |
||
| 581 | |||
| 582 | $handler = self::_get_handler(); |
||
| 583 | // Fix for Apple Addressbook |
||
| 584 | $vCard = preg_replace('/item\d\.(ADR|TEL|EMAIL|URL)/', '\1', |
||
| 585 | htmlspecialchars_decode($options['content'])); |
||
| 586 | $charset = null; |
||
| 587 | if (!empty($options['content_type'])) |
||
| 588 | { |
||
| 589 | $content_type = explode(';', $options['content_type']); |
||
| 590 | if (count($content_type) > 1) |
||
| 591 | { |
||
| 592 | array_shift($content_type); |
||
| 593 | foreach ($content_type as $attribute) |
||
| 594 | { |
||
| 595 | trim($attribute); |
||
| 596 | list($key, $value) = explode('=', $attribute); |
||
| 597 | switch (strtolower($key)) |
||
| 598 | { |
||
| 599 | case 'charset': |
||
| 600 | $charset = strtoupper(substr($value,1,-1)); |
||
| 601 | } |
||
| 602 | } |
||
| 603 | } |
||
| 604 | } |
||
| 605 | |||
| 606 | $contact = $handler->vcardtoegw($vCard, $charset); |
||
| 607 | |||
| 608 | if (is_array($oldContact) || ($oldContact = $this->bo->read(array('contact_uid' => $contact['uid'])))) |
||
| 609 | { |
||
| 610 | $contactId = $oldContact['id']; |
||
| 611 | $retval = true; |
||
| 612 | } |
||
| 613 | else |
||
| 614 | { |
||
| 615 | // new entry |
||
| 616 | $contactId = -1; |
||
| 617 | $retval = '201 Created'; |
||
| 618 | } |
||
| 619 | $is_group = $contact['##X-ADDRESSBOOKSERVER-KIND'] == 'group'; |
||
| 620 | if ($oldContact && $is_group !== isset($oldContact['list_id'])) |
||
| 621 | { |
||
| 622 | throw new Api\Exception\AssertionFailed(__METHOD__."(,'$id',$user,'$prefix') can contact into group or visa-versa!"); |
||
| 623 | } |
||
| 624 | |||
| 625 | if (!$is_group && is_array($contact['cat_id'])) |
||
| 626 | { |
||
| 627 | $contact['cat_id'] = implode(',',$this->bo->find_or_add_categories($contact['cat_id'], $contactId)); |
||
| 628 | } |
||
| 629 | elseif ($contactId > 0) |
||
| 630 | { |
||
| 631 | $contact['cat_id'] = null; |
||
| 632 | } |
||
| 633 | if (is_array($oldContact)) |
||
| 634 | { |
||
| 635 | $contact['id'] = $oldContact['id']; |
||
| 636 | // dont allow the client to overwrite certain values |
||
| 637 | $contact['uid'] = $oldContact['uid']; |
||
| 638 | $contact['owner'] = $oldContact['owner']; |
||
| 639 | $contact['private'] = $oldContact['private']; |
||
| 640 | $contact['carddav_name'] = $oldContact['carddav_name']; |
||
| 641 | $contact['tid'] = $oldContact['tid']; |
||
| 642 | $contact['creator'] = $oldContact['creator']; |
||
| 643 | $contact['created'] = $oldContact['created']; |
||
| 644 | $contact['account_id'] = $oldContact['account_id']; |
||
| 645 | } |
||
| 646 | else |
||
| 647 | { |
||
| 648 | $contact['carddav_name'] = $id; |
||
| 649 | |||
| 650 | // only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!) |
||
| 651 | if ($prefix && !in_array('O',$this->home_set_pref) && $user) |
||
| 652 | { |
||
| 653 | $contact['owner'] = $user; |
||
| 654 | } |
||
| 655 | // check if default addressbook is synced and not Api\Accounts, if not use (always synced) personal addressbook |
||
| 656 | elseif(!$this->bo->default_addressbook || !in_array($this->bo->default_addressbook,$this->home_set_pref)) |
||
| 657 | { |
||
| 658 | $contact['owner'] = $GLOBALS['egw_info']['user']['account_id']; |
||
| 659 | } |
||
| 660 | else |
||
| 661 | { |
||
| 662 | $contact['owner'] = $this->bo->default_addressbook; |
||
| 663 | $contact['private'] = $this->bo->default_private; |
||
| 664 | } |
||
| 665 | // check if user has add rights for addressbook |
||
| 666 | // done here again, as _common_get_put_delete knows nothing about default addressbooks... |
||
| 667 | if (!($this->bo->grants[$contact['owner']] & Acl::ADD)) |
||
| 668 | { |
||
| 669 | if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning '403 Forbidden'"); |
||
| 670 | return '403 Forbidden'; |
||
| 671 | } |
||
| 672 | } |
||
| 673 | if ($this->http_if_match) $contact['etag'] = self::etag2value($this->http_if_match); |
||
| 674 | |||
| 675 | $contact['photo_unchanged'] = false; // photo needs saving |
||
| 676 | if (!($save_ok = $is_group ? $this->save_group($contact, $oldContact) : $this->bo->save($contact))) |
||
| 677 | { |
||
| 678 | if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($contact).") failed, Ok=$save_ok"); |
||
| 679 | if ($save_ok === 0) |
||
| 680 | { |
||
| 681 | // honor Prefer: return=representation for 412 too (no need for client to explicitly reload) |
||
| 682 | $this->check_return_representation($options, $id, $user); |
||
| 683 | return '412 Precondition Failed'; |
||
| 684 | } |
||
| 685 | return '403 Forbidden'; // happens when writing new entries in AB's without ADD rights |
||
| 686 | } |
||
| 687 | |||
| 688 | if (empty($contact['etag']) || empty($contact['cardav_name'])) |
||
| 689 | { |
||
| 690 | if ($is_group) |
||
| 691 | { |
||
| 692 | if (($contact = $this->bo->read_list($save_ok))) |
||
| 693 | { |
||
| 694 | // re-read group to get correct etag (not dublicate etag code here) |
||
| 695 | $contact = $this->read($contact['list_'.self::$path_attr], $options['path']); |
||
| 696 | } |
||
| 697 | } |
||
| 698 | else |
||
| 699 | { |
||
| 700 | $contact = $this->bo->read($save_ok); |
||
| 701 | } |
||
| 702 | //error_log(__METHOD__."(, $id, '$user') read(_list)($save_ok) returned ".array2string($contact)); |
||
| 703 | } |
||
| 704 | |||
| 705 | // send evtl. necessary respose headers: Location, etag, ... |
||
| 706 | $this->put_response_headers($contact, $options['path'], $retval, self::$path_attr != 'id'); |
||
| 707 | |||
| 708 | if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval)); |
||
| 709 | return $retval; |
||
| 710 | } |
||
| 711 | |||
| 712 | /** |
||
| 713 | * Save distribition-list / group |
||
| 714 | * |
||
| 715 | * @param array $contact |
||
| 716 | * @param array|false $oldContact |
||
| 717 | * @return int|boolean $list_id or false on error |
||
| 718 | */ |
||
| 719 | function save_group(array &$contact, $oldContact=null) |
||
| 720 | { |
||
| 721 | $data = array('list_name' => $contact['n_fn']); |
||
| 722 | if (!isset($contact['owner'])) $contact['owner'] = $GLOBALS['egw_info']['user']['account_id']; |
||
| 723 | foreach(array('id','carddav_name','uid','owner') as $name) |
||
| 724 | { |
||
| 725 | $data['list_'.$name] = $contact[$name]; |
||
| 726 | } |
||
| 727 | //error_log(__METHOD__.'('.array2string($contact).', '.array2string($oldContact).') data='.array2string($data)); |
||
| 728 | if (($list_id=$this->bo->add_list(empty($contact[self::$path_attr]) ? null : array('list_'.self::$path_attr => $contact[self::$path_attr]), |
||
| 729 | $contact['owner'], null, $data))) |
||
| 730 | { |
||
| 731 | // update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER'] |
||
| 732 | $new_members = $contact['##X-ADDRESSBOOKSERVER-MEMBER']; |
||
| 733 | if ($new_members[1] == ':' && ($n = unserialize($new_members))) |
||
| 734 | { |
||
| 735 | $new_members = $n['values']; |
||
| 736 | } |
||
| 737 | else |
||
| 738 | { |
||
| 739 | $new_members = array($new_members); |
||
| 740 | } |
||
| 741 | foreach($new_members as &$uid) |
||
| 742 | { |
||
| 743 | $uid = substr($uid,9); // cut off "urn:uuid:" prefix |
||
| 744 | } |
||
| 745 | if ($oldContact) |
||
| 746 | { |
||
| 747 | $to_add = array_diff($new_members,$oldContact['members']); |
||
| 748 | $to_delete = array_diff($oldContact['members'],$new_members); |
||
| 749 | } |
||
| 750 | else |
||
| 751 | { |
||
| 752 | $to_add = $new_members; |
||
| 753 | } |
||
| 754 | //error_log('to_add='.array2string($to_add).', to_delete='.array2string($to_delete)); |
||
| 755 | if ($to_add || $to_delete) |
||
| 756 | { |
||
| 757 | $to_add_ids = $to_delete_ids = array(); |
||
| 758 | $filter = array('uid' => $to_delete ? array_merge($to_add, $to_delete) : $to_add); |
||
| 759 | if (($contacts =& $this->bo->search(array(), array('id', 'uid'),'','','',False,'AND',false,$filter))) |
||
| 760 | { |
||
| 761 | foreach($contacts as $c) |
||
| 762 | { |
||
| 763 | if ($to_delete && in_array($c['uid'], $to_delete)) |
||
| 764 | { |
||
| 765 | $to_delete_ids[] = $c['id']; |
||
| 766 | } |
||
| 767 | else |
||
| 768 | { |
||
| 769 | $to_add_ids[] = $c['id']; |
||
| 770 | } |
||
| 771 | } |
||
| 772 | } |
||
| 773 | //error_log('to_add_ids='.array2string($to_add_ids).', to_delete_ids='.array2string($to_delete_ids)); |
||
| 774 | if ($to_add_ids) $this->bo->add2list($to_add_ids, $list_id, array()); |
||
| 775 | if ($to_delete_ids) $this->bo->remove_from_list($to_delete_ids, $list_id); |
||
| 776 | } |
||
| 777 | // reread as update of list-members updates etag and modified |
||
| 778 | if (($contact = $this->bo->read_list($list_id))) |
||
| 779 | { |
||
| 780 | // re-read group to get correct etag (not dublicate etag code here) |
||
| 781 | $contact = $this->read($contact['list_'.self::$path_attr]); |
||
| 782 | } |
||
| 783 | } |
||
| 784 | if ($this->debug > 1) error_log(__METHOD__.'('.array2string($contact).', '.array2string($oldContact).') on return contact='.array2string($data).' returning '.array2string($list_id)); |
||
| 785 | return $list_id; |
||
| 786 | } |
||
| 787 | |||
| 788 | /** |
||
| 789 | * Query ctag for addressbook |
||
| 790 | * |
||
| 791 | * @param string $path |
||
| 792 | * @param int $user |
||
| 793 | * @return string |
||
| 794 | */ |
||
| 795 | public function getctag($path,$user) |
||
| 796 | { |
||
| 797 | static $ctags = array(); // a little per request caching, in case ctag and sync-token is both requested |
||
| 798 | if (isset($ctags[$path])) return $ctags[$path]; |
||
| 799 | |||
| 800 | $user_in = $user; |
||
| 801 | // not showing addressbook of a single user? |
||
| 802 | if (is_null($user) || $user === '' || $path == '/addressbook/') $user = null; |
||
| 803 | |||
| 804 | // If "Sync selected addressbooks into one" is set --> ctag need to take selected AB's into account too |
||
| 805 | if ($user && $user == $GLOBALS['egw_info']['user']['account_id'] && in_array('O',$this->home_set_pref)) |
||
| 806 | { |
||
| 807 | $user = array_merge((array)$user,array_keys($this->get_shared(true))); // true: ignore all-in-one pref |
||
| 808 | |||
| 809 | // include accounts ctag, if accounts stored different from contacts (eg.in LDAP or ADS) |
||
| 810 | if ($this->bo->so_accounts && in_array('0', $user)) |
||
| 811 | { |
||
| 812 | $accounts_ctag = $this->bo->get_ctag('0'); |
||
| 813 | } |
||
| 814 | } |
||
| 815 | $ctag = $this->bo->get_ctag($user); |
||
| 816 | |||
| 817 | // include lists-ctag, if enabled |
||
| 818 | if (in_array('D',$this->home_set_pref)) |
||
| 819 | { |
||
| 820 | $lists_ctag = $this->bo->lists_ctag($user); |
||
| 821 | } |
||
| 822 | //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)); |
||
| 823 | unset($user_in); |
||
| 824 | return $ctags[$path] = max($ctag, $accounts_ctag, $lists_ctag); |
||
| 825 | } |
||
| 826 | |||
| 827 | /** |
||
| 828 | * Add extra properties for addressbook collections |
||
| 829 | * |
||
| 830 | * Example for supported-report-set syntax from Apples Calendarserver: |
||
| 831 | * <D:supported-report-set> |
||
| 832 | * <supported-report> |
||
| 833 | * <report> |
||
| 834 | * <addressbook-query xmlns='urn:ietf:params:xml:ns:carddav'/> |
||
| 835 | * </report> |
||
| 836 | * </supported-report> |
||
| 837 | * <supported-report> |
||
| 838 | * <report> |
||
| 839 | * <addressbook-multiget xmlns='urn:ietf:params:xml:ns:carddav'/> |
||
| 840 | * </report> |
||
| 841 | * </supported-report> |
||
| 842 | * </D:supported-report-set> |
||
| 843 | * @link http://www.mail-archive.com/[email protected]/msg01156.html |
||
| 844 | * |
||
| 845 | * @param array $props =array() regular props by the Api\CalDAV handler |
||
| 846 | * @param string $displayname |
||
| 847 | * @param string $base_uri =null base url of handler |
||
| 848 | * @param int $user =null account_id of owner of collection |
||
| 849 | * @return array |
||
| 850 | */ |
||
| 851 | public function extra_properties(array $props, $displayname, $base_uri=null, $user=null) |
||
| 852 | { |
||
| 853 | unset($displayname, $base_uri, $user); // not used, but required by function signature |
||
| 854 | |||
| 855 | if (!isset($props['addressbook-description'])) |
||
| 856 | { |
||
| 857 | // default addressbook description: can be overwritten via PROPPATCH, in which case it's already set |
||
| 858 | $props['addressbook-description'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV,'addressbook-description',$props['displayname']); |
||
| 859 | } |
||
| 860 | // setting an max image size, so iOS scales the images before transmitting them |
||
| 861 | // we currently scale down to width of 240px, which tests shown to be ~20k |
||
| 862 | $props['max-image-size'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV,'max-image-size',24*1024); |
||
| 863 | |||
| 864 | // supported reports (required property for CardDAV) |
||
| 865 | $props['supported-report-set'] = array( |
||
| 866 | 'addressbook-query' => Api\CalDAV::mkprop('supported-report',array( |
||
| 867 | Api\CalDAV::mkprop('report',array( |
||
| 868 | Api\CalDAV::mkprop(Api\CalDAV::CARDDAV,'addressbook-query',''))))), |
||
| 869 | 'addressbook-multiget' => Api\CalDAV::mkprop('supported-report',array( |
||
| 870 | Api\CalDAV::mkprop('report',array( |
||
| 871 | Api\CalDAV::mkprop(Api\CalDAV::CARDDAV,'addressbook-multiget',''))))), |
||
| 872 | ); |
||
| 873 | // only advertice rfc 6578 sync-collection report, if "delete-prevention" is switched on (deleted entries get marked deleted but not actualy deleted |
||
| 874 | if ($GLOBALS['egw_info']['server']['history']) |
||
| 875 | { |
||
| 876 | $props['supported-report-set']['sync-collection'] = Api\CalDAV::mkprop('supported-report',array( |
||
| 877 | Api\CalDAV::mkprop('report',array( |
||
| 878 | Api\CalDAV::mkprop('sync-collection',''))))); |
||
| 879 | } |
||
| 880 | return $props; |
||
| 881 | } |
||
| 882 | |||
| 883 | /** |
||
| 884 | * Get the handler and set the supported fields |
||
| 885 | * |
||
| 886 | * @return addressbook_vcal |
||
| 887 | */ |
||
| 888 | private function _get_handler() |
||
| 889 | { |
||
| 890 | $handler = new addressbook_vcal('addressbook','text/vcard'); |
||
| 891 | $supportedFields = $handler->supportedFields; |
||
| 892 | // Apple iOS or OS X addressbook |
||
| 893 | if ($this->agent == 'cfnetwork' || $this->agent == 'dataaccess') |
||
| 894 | { |
||
| 895 | $databaseFields = $handler->databaseFields; |
||
| 896 | // use just CELL and IPHONE, CELL;WORK and CELL;HOME are NOT understood |
||
| 897 | //'TEL;CELL;WORK' => array('tel_cell'), |
||
| 898 | //'TEL;CELL;HOME' => array('tel_cell_private'), |
||
| 899 | $supportedFields['TEL;CELL'] = array('tel_cell'); |
||
| 900 | unset($supportedFields['TEL;CELL;WORK']); |
||
| 901 | $supportedFields['TEL;IPHONE'] = array('tel_cell_private'); |
||
| 902 | unset($supportedFields['TEL;CELL;HOME']); |
||
| 903 | $databaseFields['X-ABSHOWAS'] = $supportedFields['X-ABSHOWAS'] = array('fileas_type'); // Horde vCard class uses uppercase prop-names! |
||
| 904 | |||
| 905 | // Apple Addressbook pre Lion (OS X 10.7) messes up CLASS and CATEGORIES (Lion cant set them but leaves them alone) |
||
| 906 | $matches = null; |
||
| 907 | if (preg_match('|CFNetwork/([0-9]+)|i', $_SERVER['HTTP_USER_AGENT'],$matches) && $matches[1] < 520 || |
||
| 908 | // iOS 5.1.1 does not display CLASS or CATEGORY, but wrongly escapes multiple, comma-separated categories |
||
| 909 | // and appends CLASS: PUBLIC to an empty NOTE: field --> leaving them out for iOS |
||
| 910 | $this->agent == 'dataaccess') |
||
| 911 | { |
||
| 912 | unset($supportedFields['CLASS']); |
||
| 913 | unset($databaseFields['CLASS']); |
||
| 914 | unset($supportedFields['CATEGORIES']); |
||
| 915 | unset($databaseFields['CATEGORIES']); |
||
| 916 | } |
||
| 917 | if (preg_match('|CFNetwork/([0-9]+)|i', $_SERVER['HTTP_USER_AGENT'],$matches) && $matches[1] < 520) |
||
| 918 | { |
||
| 919 | // gd cant parse or resize images stored from snow leopard addressbook: gd-jpeg: |
||
| 920 | // - JPEG library reports unrecoverable error |
||
| 921 | // - Passed data is not in 'JPEG' format |
||
| 922 | // - Couldn't create GD Image Stream out of Data |
||
| 923 | // FF (10), Safari (5.1.3) and Chrome (17) cant display it either --> ignore images |
||
| 924 | unset($supportedFields['PHOTO']); |
||
| 925 | unset($databaseFields['PHOTO']); |
||
| 926 | } |
||
| 927 | $handler->setDatabaseFields($databaseFields); |
||
| 928 | } |
||
| 929 | $handler->setSupportedFields('GroupDAV',$this->agent,$supportedFields); |
||
| 930 | return $handler; |
||
| 931 | } |
||
| 932 | |||
| 933 | /** |
||
| 934 | * Handle delete request for an event |
||
| 935 | * |
||
| 936 | * @param array &$options |
||
| 937 | * @param int $id |
||
| 938 | * @param int $user account_id of collection owner |
||
| 939 | * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') |
||
| 940 | */ |
||
| 941 | function delete(&$options,$id,$user) |
||
| 942 | { |
||
| 943 | unset($user); // not used, but required by function signature |
||
| 944 | |||
| 945 | if (!is_array($contact = $this->_common_get_put_delete('DELETE',$options,$id))) |
||
| 946 | { |
||
| 947 | return $contact; |
||
| 948 | } |
||
| 949 | if (isset($contact['list_id'])) |
||
| 950 | { |
||
| 951 | $ok = $this->bo->delete_list($contact['list_id']) !== false; |
||
| 952 | } |
||
| 953 | elseif (($ok = $this->bo->delete($contact['id'],self::etag2value($this->http_if_match))) === 0) |
||
| 954 | { |
||
| 955 | return '412 Precondition Failed'; |
||
| 956 | } |
||
| 957 | return $ok; |
||
| 958 | } |
||
| 959 | |||
| 960 | /** |
||
| 961 | * Read a contact |
||
| 962 | * |
||
| 963 | * We have to make sure to not return or even consider in read deleted contacts, as the might have |
||
| 964 | * the same UID and/or carddav_name as not deleted contacts and would block access to valid entries |
||
| 965 | * |
||
| 966 | * @param string|int $id |
||
| 967 | * @param string $path =null |
||
| 968 | * @return array|boolean array with entry, false if no read rights, null if $id does not exist |
||
| 969 | */ |
||
| 970 | function read($id, $path=null) |
||
| 971 | { |
||
| 972 | static $non_deleted_tids=null; |
||
| 973 | if (is_null($non_deleted_tids)) |
||
| 974 | { |
||
| 975 | $tids = $this->bo->content_types; |
||
| 976 | unset($tids[Api\Contacts::DELETED_TYPE]); |
||
| 977 | $non_deleted_tids = array_keys($tids); |
||
| 978 | } |
||
| 979 | $contact = $this->bo->read(array(self::$path_attr => $id, 'tid' => $non_deleted_tids)); |
||
| 980 | |||
| 981 | // if contact not found and accounts stored NOT like contacts, try reading it without path-extension as id |
||
| 982 | if (is_null($contact) && $this->bo->so_accounts && ($c = $this->bo->read($test=basename($id, '.vcf')))) |
||
| 983 | { |
||
| 984 | $contact = $c; |
||
| 985 | } |
||
| 986 | |||
| 987 | // see if we have a distribution-list / group with that id |
||
| 988 | // bo->read_list(..., true) limits returned uid to same owner's addressbook, as iOS and OS X addressbooks |
||
| 989 | // only understands/shows that and if return more, save_lists would delete the others ones on update! |
||
| 990 | $limit_in_ab = true; |
||
| 991 | list(,$account_lid,$app) = explode('/',$path); // eg. /<username>/addressbook/<id> |
||
| 992 | // /<username>/addressbook/ with home_set_prefs containing 'O'=all-in-one contains selected ab's |
||
| 993 | if($account_lid == $GLOBALS['egw_info']['user']['account_lid'] && $app == 'addressbook' && in_array('O',$this->home_set_pref)) |
||
| 994 | { |
||
| 995 | $limit_in_ab = array_keys($this->get_shared(true)); |
||
| 996 | $limit_in_ab[] = $GLOBALS['egw_info']['user']['account_id']; |
||
| 997 | } |
||
| 998 | /* we are currently not syncing distribution-lists/groups to /addressbook/ as |
||
| 999 | * Apple clients use that only as directory gateway |
||
| 1000 | elseif ($account_lid == 'addressbook') // /addressbook/ contains all readably contacts |
||
| 1001 | { |
||
| 1002 | $limit_in_ab = array_keys($this->bo->grants); |
||
| 1003 | }*/ |
||
| 1004 | if (!$contact && ($contact = $this->bo->read_lists(array('list_'.self::$path_attr => $id),'contact_uid',$limit_in_ab))) |
||
| 1005 | { |
||
| 1006 | $contact = array_shift($contact); |
||
| 1007 | $contact['n_fn'] = $contact['n_family'] = $contact['list_name']; |
||
| 1008 | foreach(array('owner','id','carddav_name','modified','modifier','created','creator','etag','uid') as $name) |
||
| 1009 | { |
||
| 1010 | $contact[$name] = $contact['list_'.$name]; |
||
| 1011 | } |
||
| 1012 | // if NOT limited to containing AB ($limit_in_ab === true), add that limit to etag |
||
| 1013 | if ($limit_in_ab !== true) |
||
| 1014 | { |
||
| 1015 | $contact['etag'] .= ':'.implode('-',$limit_in_ab); |
||
| 1016 | } |
||
| 1017 | } |
||
| 1018 | elseif($contact === array()) // not found from read_lists() |
||
| 1019 | { |
||
| 1020 | $contact = null; |
||
| 1021 | } |
||
| 1022 | |||
| 1023 | if ($contact && $contact['tid'] == Api\Contacts::DELETED_TYPE) |
||
| 1024 | { |
||
| 1025 | $contact = null; // handle deleted events, as not existing (404 Not Found) |
||
| 1026 | } |
||
| 1027 | if ($this->debug > 1) error_log(__METHOD__."('$id') returning ".array2string($contact)); |
||
| 1028 | return $contact; |
||
| 1029 | } |
||
| 1030 | |||
| 1031 | /** |
||
| 1032 | * Check if user has the neccessary rights on a contact |
||
| 1033 | * |
||
| 1034 | * @param int $acl Acl::READ, Acl::EDIT or Acl::DELETE |
||
| 1035 | * @param array|int $contact contact-array or id |
||
| 1036 | * @return boolean null if entry does not exist, false if no access, true if access permitted |
||
| 1037 | */ |
||
| 1038 | function check_access($acl,$contact) |
||
| 1041 | } |
||
| 1042 | |||
| 1043 | /** |
||
| 1044 | * Get grants of current user and app |
||
| 1045 | * |
||
| 1046 | * Reimplemented to account for static LDAP ACL and accounts (owner=0) |
||
| 1047 | * |
||
| 1048 | * @return array user-id => EGW_ACL_ADD|EGW_ACL_READ|EGW_ACL_EDIT|EGW_ACL_DELETE pairs |
||
| 1049 | */ |
||
| 1050 | public function get_grants() |
||
| 1051 | { |
||
| 1052 | $grants = $this->bo->get_grants($this->bo->user); |
||
| 1053 | |||
| 1054 | // remove add and delete grants for accounts (for admins too) |
||
| 1055 | // as accounts can not be created as contacts, they eg. need further data |
||
| 1056 | // and admins might not recognice they delete an account incl. its data |
||
| 1057 | if (isset($grants[0])) $grants[0] &= ~(EGW_ACL_ADD|EGW_ACL_DELETE); |
||
| 1058 | |||
| 1059 | return $grants; |
||
| 1060 | } |
||
| 1061 | |||
| 1062 | /** |
||
| 1063 | * Return calendars/addressbooks shared from other users with the current one |
||
| 1064 | * |
||
| 1065 | * @param boolean $ignore_all_in_one =false if true, return selected addressbooks and not array() for all-in-one |
||
| 1066 | * @return array account_id => account_lid pairs |
||
| 1067 | */ |
||
| 1068 | function get_shared($ignore_all_in_one=false) |
||
| 1069 | { |
||
| 1070 | $shared = array(); |
||
| 1071 | |||
| 1072 | // if "Sync all selected addressbook into one" is set --> no (additional) shared addressbooks |
||
| 1073 | if (!$ignore_all_in_one && in_array('O',$this->home_set_pref)) return array(); |
||
| 1074 | |||
| 1075 | // replace symbolic id's with real nummeric id's |
||
| 1076 | foreach(array( |
||
| 1077 | 'G' => $GLOBALS['egw_info']['user']['account_primary_group'], |
||
| 1078 | 'U' => '0', |
||
| 1079 | ) as $sym => $id) |
||
| 1080 | { |
||
| 1081 | if (($key = array_search($sym, $this->home_set_pref)) !== false) |
||
| 1082 | { |
||
| 1083 | $this->home_set_pref[$key] = $id; |
||
| 1084 | } |
||
| 1085 | } |
||
| 1086 | foreach(array_keys($this->bo->get_addressbooks(Acl::READ)) as $id) |
||
| 1087 | { |
||
| 1088 | if (($id || $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1') && |
||
| 1089 | $GLOBALS['egw_info']['user']['account_id'] != $id && // no current user and no accounts, if disabled in ab prefs |
||
| 1090 | (in_array('A',$this->home_set_pref) || in_array((string)$id,$this->home_set_pref)) && |
||
| 1091 | is_numeric($id) && ($owner = $id ? $this->accounts->id2name($id) : 'accounts')) |
||
| 1092 | { |
||
| 1093 | $shared[$id] = 'addressbook-'.$owner; |
||
| 1094 | } |
||
| 1095 | } |
||
| 1096 | return $shared; |
||
| 1097 | } |
||
| 1098 | |||
| 1099 | /** |
||
| 1100 | * Hook to add properties to CardDAV root |
||
| 1101 | * |
||
| 1102 | * OS X 10.11.4 addressbook does a propfind for "addressbook-home-set" and "directory-gateway" |
||
| 1103 | * in the root and does not continue without it. |
||
| 1104 | * |
||
| 1105 | * @param array $data |
||
| 1106 | */ |
||
| 1107 | public static function groupdav_root_props(array $data) |
||
| 1108 | { |
||
| 1109 | $data['props']['addressbook-home-set'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'addressbook-home-set', array( |
||
| 1110 | Api\CalDAV::mkprop('href',$data['caldav']->base_uri.'/'.$GLOBALS['egw_info']['user']['account_lid'].'/'))); |
||
| 1111 | |||
| 1112 | $data['props']['principal-address'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'principal-address', |
||
| 1113 | $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1' ? '' : array( |
||
| 1114 | Api\CalDAV::mkprop('href',$data['caldav']->base_uri.'/addressbook-accounts/'.$GLOBALS['egw_info']['user']['person_id'].'.vcf'))); |
||
| 1115 | |||
| 1116 | $data['props']['directory-gateway'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'directory-gateway', array( |
||
| 1117 | Api\CalDAV::mkprop('href',$data['caldav']->base_uri.'/addressbook/'))); |
||
| 1118 | } |
||
| 1119 | |||
| 1120 | /** |
||
| 1121 | * Return appliction specific settings |
||
| 1122 | * |
||
| 1123 | * @param array $hook_data values for keys 'location', 'type' and 'account_id' |
||
| 1124 | * @return array of array with settings |
||
| 1125 | */ |
||
| 1126 | static function get_settings($hook_data) |
||
| 1176 | } |
||
| 1177 | } |
||
| 1178 |