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  if (! isset($collation) ) $collation = '';
135  switch( strtolower($collation) ) {
136  case 'i;octet':
137  $comparison = 'LIKE';
138  break;
139  case 'i;ascii-casemap':
140  case 'i;unicode-casemap':
141  default:
142  $comparison = 'ILIKE';
143  break;
144  }
145 
146  $search = $filter->GetContent();
147  $match = $filter->GetAttribute("match-type");
148  if (! isset($match) ) $match = '';
149  switch( strtolower($match) ) {
150  case 'equals':
151  break;
152  case 'starts-with':
153  $search = $search.'%';
154  break;
155  case 'ends-with':
156  $search = '%'.$search;
157  break;
158  case 'contains':
159  default:
160  $search = '%'.$search.'%';
161  break;
162  }
163 
164  $pname = ':text_match_'.$matchnum++;
165  $params[$pname] = $search;
166 
167  $negate = $filter->GetAttribute("negate-condition");
168  $negate = ( isset($negate) && strtolower($negate) == "yes" ) ? "NOT " : "";
169  dbg_error_log("cardquery", " text-match: (%s%s %s '%s') ", $negate, $property, $comparison, $search );
170  $sql .= sprintf( "(%s%s %s $pname)", $negate, $property, $comparison );
171  break;
172 
173  case 'urn:ietf:params:xml:ns:carddav:prop-filter':
174  $propertyname = $filter->GetAttribute("name");
175  switch( $propertyname ) {
176  case 'VERSION':
177  case 'UID':
178  case 'NICKNAME':
179  case 'FN':
180  case 'NOTE':
181  case 'ORG':
182  case 'URL':
183  case 'FBURL':
184  case 'CALADRURI':
185  case 'CALURI':
186  $property = strtolower($propertyname);
187  break;
188 
189  case 'N':
190  $property = 'name';
191  break;
192 
193  default:
194  $post_filters[] = $filter;
195  dbg_error_log("cardquery", "Could not handle 'prop-filter' on %s in SQL", $propertyname );
196  return false;
197  }
198 
199  $test_type = $filter->GetAttribute("test");
200  switch( $test_type ) {
201  case 'allOf':
202  $test_type = 'AND';
203  break;
204  case 'anyOf':
205  default:
206  $test_type = 'OR';
207  }
208 
209  $subfilters = $filter->GetContent();
210  if (count($subfilters) <= 1) {
211  $success = SqlFilterCardDAV( $subfilters[0], $components, $property, $parameter );
212  if ( $success !== false ) {
213  $sql .= $success['sql'];
214  $params = array_merge( $params, $success['params'] );
215  }
216  } else {
217  $subfilter_added_counter=0;
218  foreach ($subfilters as $subfilter) {
219  $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
220  if ( $success === false ) continue; else {
221  if ($subfilter_added_counter <= 0) {
222  $sql .= '(' . $success['sql'];
223  } else {
224  $sql .= $test_type . ' ' . $success['sql'];
225  }
226  $params = array_merge( $params, $success['params'] );
227  $subfilter_added_counter++;
228  }
229  }
230  if ($subfilter_added_counter > 0) {
231  $sql .= ')';
232  }
233  }
234  break;
235 
236  case 'urn:ietf:params:xml:ns:carddav:param-filter':
237  $post_filters[] = $filter;
238  return false;
239  /*
240  $parameter = $filter->GetAttribute("name");
241  $subfilter = $filter->GetContent();
242  $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
243  if ( $success === false ) continue; else {
244  $sql .= $success['sql'];
245  $params = array_merge( $params, $success['params'] );
246  }
247  break;
248  */
249 
250  default:
251  dbg_error_log("cardquery", "Could not handle unknown tag '%s' in calendar query report", $tag );
252  break;
253  }
254  dbg_error_log("cardquery", "Generated SQL was '%s'", $sql );
255  return array( 'sql' => $sql, 'params' => $params );
256 }
257 
258 
263 $responses = array();
264 $target_collection = new DAVResource($request->path);
265 $bound_from = $target_collection->bound_from();
266 if ( !$target_collection->Exists() ) {
267  $request->DoResponse( 404 );
268 }
269 if ( ! $target_collection->IsAddressbook() ) {
270  $request->DoResponse( 403, translate('The addressbook-query report must be run against an addressbook collection') );
271 }
272 
281 $params = array();
282 $where_collection = ' WHERE caldav_data.collection_id = ' . $target_collection->resource_id();
283 $where = '';
284 if ( is_array($qry_filters) ) {
285  dbg_log_array( 'cardquery', 'qry_filters', $qry_filters, true );
286 
287  $appended_where_counter=0;
288  foreach ($qry_filters as $qry_filter) {
289  $components = array();
290  $filter_fragment = SqlFilterCardDAV( $qry_filter, $components );
291  if ( $filter_fragment !== false ) {
292  $filter_fragment_sql = $filter_fragment['sql'];
293  if ( empty($filter_fragment_sql) ) {
294  continue;
295  }
296 
297  if ( $appended_where_counter == 0 ) {
298  $where .= ' AND (' . $filter_fragment_sql;
299  $params = $filter_fragment['params'];
300  } else {
301  $where .= ' ' . $qry_filters_combination . ' ' . $filter_fragment_sql;
302  $params = array_merge( $params, $filter_fragment['params'] );
303  }
304  $appended_where_counter++;
305  }
306  }
307  if ( $appended_where_counter > 0 ) {
308  $where .= ')';
309  }
310 }
311 else {
312  dbg_error_log( 'cardquery', 'No query filters' );
313 }
314 
315 $need_post_filter = !empty($post_filters);
316 if ( $need_post_filter && ( $qry_filters_combination == 'OR' )) {
317  // we need a post_filter step, and it should be sufficient, that only one
318  // filter is enough to display the item => we can't prefilter values via SQL
319  $where = '';
320  $params = array();
321  $post_filters = $qry_filters;
322 }
323 
324 $sql = 'SELECT * FROM caldav_data INNER JOIN addressbook_resource USING(dav_id)'. $where_collection . $where;
325 if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id";
326 $qry = new AwlQuery( $sql, $params );
327 if ( $qry->Exec("cardquery",__LINE__,__FILE__) && $qry->rows() > 0 ) {
328  while( $address_object = $qry->Fetch() ) {
329  if ( !$need_post_filter || cardquery_apply_filter( $post_filters, $address_object, $qry_filters_combination ) ) {
330  if ( $bound_from != $target_collection->dav_name() ) {
331  $address_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $address_object->dav_name);
332  }
333  if ( count($address_data_properties) > 0 ) {
334  $vcard = new VCard($address_object->caldav_data);
335  $vcard->MaskProperties($address_data_properties);
336  $address_object->caldav_data = $vcard->Render();
337  }
338  $responses[] = component_to_xml( $properties, $address_object );
339  if ( ($qry_limit > 0) && ( count($responses) >= $qry_limit ) ) {
340  break;
341  }
342  }
343  }
344 }
345 $multistatus = new XMLElement( "multistatus", $responses, $reply->GetXmlNsArray() );
346 
347 $request->XMLResponse( 207, $multistatus );
Definition: vcard.php:9