DAViCal
caldav-REPORT-cardquery.php
1 <?php
2 
3 require_once('vcard.php');
4 
5 $address_data_properties = array();
6 function get_address_properties( $address_data_xml ) {
7  global $address_data_properties;
8  $expansion = $address_data_xml->GetElements();
9  foreach( $expansion AS $k => $v ) {
10  if ( $v instanceof XMLElement )
11  $address_data_properties[strtoupper($v->GetAttribute('name'))] = true;
12  }
13 }
14 
15 
19 $qry_content = $xmltree->GetContent('urn:ietf:params:xml:ns:carddav:addressbook-query');
20 $proptype = $qry_content[0]->GetNSTag();
21 $properties = array();
22 switch( $proptype ) {
23  case 'DAV::prop':
24  $qry_props = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/'.$proptype.'/*');
25  foreach( $qry_content[0]->GetElements() AS $k => $v ) {
26  $properties[$v->GetNSTag()] = 1;
27  if ( $v->GetNSTag() == 'urn:ietf:params:xml:ns:carddav:address-data' ) get_address_properties($v);
28  }
29  break;
30 
31  case 'DAV::allprop':
32  $properties['DAV::allprop'] = 1;
33  if ( $qry_content[1]->GetNSTag() == 'DAV::include' ) {
34  foreach( $qry_content[1]->GetElements() AS $k => $v ) {
35  $include_properties[] = $v->GetNSTag();
36  if ( $v->GetNSTag() == 'urn:ietf:params:xml:ns:carddav:address-data' ) get_address_properties($v);
37  }
38  }
39  break;
40 
41  default:
42  $properties[$proptype] = 1;
43 }
44 if ( empty($properties) ) $properties['DAV::allprop'] = 1;
45 
49 $qry_filters = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/urn:ietf:params:xml:ns:carddav:filter/*');
50 if ( count($qry_filters) == 0 ) {
51  $qry_filters = false;
52 }
53 
54 $qry_filters_combination='OR';
55 if ( is_array($qry_filters) ) {
56  $filters_parent = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/urn:ietf:params:xml:ns:carddav:filter');
57  $filters_parent = $filters_parent[0];
58  // only anyof (OR) or allof (AND) allowed, if missing anyof is default (RFC6352 10.5)
59  if ( $filters_parent->GetAttribute("test") == 'allof' ) {
60  $qry_filters_combination='AND';
61  }
62 }
63 
64 $qry_limit = -1; // everything
65 $limits = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/urn:ietf:params:xml:ns:carddav:limit/urn:ietf:params:xml:ns:carddav:nresults');
66 if ( count($limits) == 1) {
67  $qry_limit = intval($limits[0]->GetContent());
68 }
69 
80 function cardquery_apply_filter( $filters, $item, $filter_type) {
81  global $session, $c, $request;
82 
83  if ( count($filters) == 0 ) return true;
84 
85  dbg_error_log("cardquery","Applying filter for item '%s'", $item->dav_name );
86  $vcard = new vComponent( $item->caldav_data );
87 
88  if ( $filter_type === 'AND' ) {
89  return $vcard->TestFilter($filters);
90  } else {
91  foreach($filters AS $filter) {
92  $filter_fragment[0] = $filter;
93  if ( $vcard->TestFilter($filter_fragment) ) {
94  return true;
95  }
96  }
97  return false;
98  }
99 }
100 
101 
105 $post_filters = array();
106 $matchnum = 0;
107 function SqlFilterCardDAV( $filter, $components, $property = null, $parameter = null ) {
108  global $post_filters, $target_collection, $matchnum;
109  $sql = "";
110  $params = array();
111 
112  // a prop-filter without an actual filter rule means we simply need to ensure the property exists
113  if ( !is_object($filter) ) {
114  if ( empty($property) ) return false;
115  $sql .= $property . ' IS NOT NULL';
116  return array( 'sql' => $sql, 'params' => $params );
117  }
118 
119  $tag = $filter->GetNSTag();
120  dbg_error_log("cardquery", "Processing $tag into SQL - %d, '%s', %d\n", count($components), $property, isset($parameter) );
121 
122  $not_defined = "";
123  switch( $tag ) {
124  case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
125  $sql .= $property . ' IS NULL';
126  break;
127 
128  case 'urn:ietf:params:xml:ns:carddav:text-match':
129  if ( empty($property) ) {
130  return false;
131  }
132 
133  $collation = $filter->GetAttribute("collation");
134  switch( strtolower($collation) ) {
135  case 'i;octet':
136  $comparison = 'LIKE';
137  break;
138  case 'i;ascii-casemap':
139  case 'i;unicode-casemap':
140  default:
141  $comparison = 'ILIKE';
142  break;
143  }
144 
145  $search = $filter->GetContent();
146  $match = $filter->GetAttribute("match-type");
147  switch( strtolower($match) ) {
148  case 'equals':
149  break;
150  case 'starts-with':
151  $search = $search.'%';
152  break;
153  case 'ends-with':
154  $search = '%'.$search;
155  break;
156  case 'contains':
157  default:
158  $search = '%'.$search.'%';
159  break;
160  }
161 
162  $pname = ':text_match_'.$matchnum++;
163  $params[$pname] = $search;
164 
165  $negate = $filter->GetAttribute("negate-condition");
166  $negate = ( isset($negate) && strtolower($negate) == "yes" ) ? "NOT " : "";
167  dbg_error_log("cardquery", " text-match: (%s%s %s '%s') ", $negate, $property, $comparison, $search );
168  $sql .= sprintf( "(%s%s %s $pname)", $negate, $property, $comparison );
169  break;
170 
171  case 'urn:ietf:params:xml:ns:carddav:prop-filter':
172  $propertyname = $filter->GetAttribute("name");
173  switch( $propertyname ) {
174  case 'VERSION':
175  case 'UID':
176  case 'NICKNAME':
177  case 'FN':
178  case 'NOTE':
179  case 'ORG':
180  case 'URL':
181  case 'FBURL':
182  case 'CALADRURI':
183  case 'CALURI':
184  $property = strtolower($propertyname);
185  break;
186 
187  case 'N':
188  $property = 'name';
189  break;
190 
191  default:
192  $post_filters[] = $filter;
193  dbg_error_log("cardquery", "Could not handle 'prop-filter' on %s in SQL", $propertyname );
194  return false;
195  }
196 
197  $test_type = $filter->GetAttribute("test");
198  switch( $test_type ) {
199  case 'allOf':
200  $test_type = 'AND';
201  break;
202  case 'anyOf':
203  default:
204  $test_type = 'OR';
205  }
206 
207  $subfilters = $filter->GetContent();
208  if (count($subfilters) <= 1) {
209  $success = SqlFilterCardDAV( $subfilters[0], $components, $property, $parameter );
210  if ( $success !== false ) {
211  $sql .= $success['sql'];
212  $params = array_merge( $params, $success['params'] );
213  }
214  } else {
215  $subfilter_added_counter=0;
216  foreach ($subfilters as $subfilter) {
217  $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
218  if ( $success === false ) continue; else {
219  if ($subfilter_added_counter <= 0) {
220  $sql .= '(' . $success['sql'];
221  } else {
222  $sql .= $test_type . ' ' . $success['sql'];
223  }
224  $params = array_merge( $params, $success['params'] );
225  $subfilter_added_counter++;
226  }
227  }
228  if ($subfilter_added_counter > 0) {
229  $sql .= ')';
230  }
231  }
232  break;
233 
234  case 'urn:ietf:params:xml:ns:carddav:param-filter':
235  $post_filters[] = $filter;
236  return false;
237  /*
238  $parameter = $filter->GetAttribute("name");
239  $subfilter = $filter->GetContent();
240  $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
241  if ( $success === false ) continue; else {
242  $sql .= $success['sql'];
243  $params = array_merge( $params, $success['params'] );
244  }
245  break;
246  */
247 
248  default:
249  dbg_error_log("cardquery", "Could not handle unknown tag '%s' in calendar query report", $tag );
250  break;
251  }
252  dbg_error_log("cardquery", "Generated SQL was '%s'", $sql );
253  return array( 'sql' => $sql, 'params' => $params );
254 }
255 
256 
261 $responses = array();
262 $target_collection = new DAVResource($request->path);
263 $bound_from = $target_collection->bound_from();
264 if ( !$target_collection->Exists() ) {
265  $request->DoResponse( 404 );
266 }
267 if ( ! $target_collection->IsAddressbook() ) {
268  $request->DoResponse( 403, translate('The addressbook-query report must be run against an addressbook collection') );
269 }
270 
279 $params = array();
280 $where = ' WHERE caldav_data.collection_id = ' . $target_collection->resource_id();
281 if ( is_array($qry_filters) ) {
282  dbg_log_array( 'cardquery', 'qry_filters', $qry_filters, true );
283 
284  $appended_where_counter=0;
285  foreach ($qry_filters as $qry_filter) {
286  $components = array();
287  $filter_fragment = SqlFilterCardDAV( $qry_filter, $components );
288  if ( $filter_fragment !== false ) {
289  $filter_fragment_sql = $filter_fragment['sql'];
290  if ( empty($filter_fragment_sql) ) {
291  continue;
292  }
293 
294  if ( $appended_where_counter == 0 ) {
295  $where .= ' AND (' . $filter_fragment_sql;
296  $params = $filter_fragment['params'];
297  } else {
298  $where .= ' ' . $qry_filters_combination . ' ' . $filter_fragment_sql;
299  $params = array_merge( $params, $filter_fragment['params'] );
300  }
301  $appended_where_counter++;
302  }
303  }
304  if ( $appended_where_counter > 0 ) {
305  $where .= ')';
306  }
307 }
308 else {
309  dbg_error_log( 'cardquery', 'No query filters' );
310 }
311 
312 $need_post_filter = !empty($post_filters);
313 if ( $need_post_filter && ( $qry_filters_combination == 'OR' )) {
314  // we need a post_filter step, and it should be sufficient, that only one
315  // filter is enough to display the item => we can't prefilter values via SQL
316  $where = '';
317  $params = array();
318  $post_filters = $qry_filters;
319 }
320 
321 $sql = 'SELECT * FROM caldav_data INNER JOIN addressbook_resource USING(dav_id)'. $where;
322 if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id";
323 $qry = new AwlQuery( $sql, $params );
324 if ( $qry->Exec("cardquery",__LINE__,__FILE__) && $qry->rows() > 0 ) {
325  while( $address_object = $qry->Fetch() ) {
326  if ( !$need_post_filter || cardquery_apply_filter( $post_filters, $address_object, $qry_filters_combination ) ) {
327  if ( $bound_from != $target_collection->dav_name() ) {
328  $address_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $address_object->dav_name);
329  }
330  if ( count($address_data_properties) > 0 ) {
331  $vcard = new VCard($address_object->caldav_data);
332  $vcard->MaskProperties($address_data_properties);
333  $address_object->caldav_data = $vcard->Render();
334  }
335  $responses[] = component_to_xml( $properties, $address_object );
336  if ( ($qry_limit > 0) && ( count($responses) >= $qry_limit ) ) {
337  break;
338  }
339  }
340  }
341 }
342 $multistatus = new XMLElement( "multistatus", $responses, $reply->GetXmlNsArray() );
343 
344 $request->XMLResponse( 207, $multistatus );
VCard
Definition: vcard.php:9
DAVResource
Definition: DAVResource.php:24