PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/caldav-REPORT-cardquery.php

https://gitlab.com/tiggerben/davical
PHP | 337 lines | 256 code | 39 blank | 42 comment | 56 complexity | b94db1f513c2ed4908c2551da7287ed5 MD5 | raw file
  1. <?php
  2. require_once('vcard.php');
  3. $address_data_properties = array();
  4. function get_address_properties( $address_data_xml ) {
  5. global $address_data_properties;
  6. $expansion = $address_data_xml->GetElements();
  7. foreach( $expansion AS $k => $v ) {
  8. if ( $v instanceof XMLElement )
  9. $address_data_properties[strtoupper($v->GetAttribute('name'))] = true;
  10. }
  11. }
  12. /**
  13. * Build the array of properties to include in the report output
  14. */
  15. $qry_content = $xmltree->GetContent('urn:ietf:params:xml:ns:carddav:addressbook-query');
  16. $proptype = $qry_content[0]->GetNSTag();
  17. $properties = array();
  18. switch( $proptype ) {
  19. case 'DAV::prop':
  20. $qry_props = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/'.$proptype.'/*');
  21. foreach( $qry_content[0]->GetElements() AS $k => $v ) {
  22. $properties[$v->GetNSTag()] = 1;
  23. if ( $v->GetNSTag() == 'urn:ietf:params:xml:ns:carddav:address-data' ) get_address_properties($v);
  24. }
  25. break;
  26. case 'DAV::allprop':
  27. $properties['DAV::allprop'] = 1;
  28. if ( $qry_content[1]->GetNSTag() == 'DAV::include' ) {
  29. foreach( $qry_content[1]->GetElements() AS $k => $v ) {
  30. $include_properties[] = $v->GetNSTag(); /** $include_properties is referenced in DAVResource where allprop is expanded */
  31. if ( $v->GetNSTag() == 'urn:ietf:params:xml:ns:carddav:address-data' ) get_address_properties($v);
  32. }
  33. }
  34. break;
  35. default:
  36. $properties[$proptype] = 1;
  37. }
  38. if ( empty($properties) ) $properties['DAV::allprop'] = 1;
  39. /**
  40. * There can only be *one* FILTER element.
  41. */
  42. $qry_filters = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/urn:ietf:params:xml:ns:carddav:filter/*');
  43. if ( count($qry_filters) == 0 ) {
  44. $qry_filters = false;
  45. }
  46. $qry_limit = -1; // everything
  47. $qry_filters_combination='OR';
  48. if ( is_array($qry_filters) ) {
  49. $filters_parent = $xmltree->GetPath('/urn:ietf:params:xml:ns:carddav:addressbook-query/urn:ietf:params:xml:ns:carddav:filter');
  50. $filters_parent = $filters_parent[0];
  51. // only anyof (OR) or allof (AND) allowed, if missing anyof is default (RFC6352 10.5)
  52. if ( $filters_parent->GetAttribute("test") == 'allof' ) {
  53. $qry_filters_combination='AND';
  54. }
  55. $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');
  56. if ( count($limits) == 1) {
  57. $qry_limit = intval($limits[0]->GetContent());
  58. }
  59. }
  60. /**
  61. * While we can construct our SQL to apply some filters in the query, other filters
  62. * need to be checked against the retrieved record. This is for handling those ones.
  63. *
  64. * @param array $filter An array of XMLElement which is the filter definition
  65. * @param string $item The database row retrieved for this calendar item
  66. * @param string $filter_type possible values AND or OR (for OR only one filter fragment must match)
  67. *
  68. * @return boolean True if the check succeeded, false otherwise.
  69. */
  70. function apply_filter( $filters, $item, $filter_type) {
  71. global $session, $c, $request;
  72. if ( count($filters) == 0 ) return true;
  73. dbg_error_log("cardquery","Applying filter for item '%s'", $item->dav_name );
  74. $vcard = new vComponent( $item->caldav_data );
  75. if ( $filter_type === 'AND' ) {
  76. return $vcard->TestFilter($filters);
  77. } else {
  78. foreach($filters AS $filter) {
  79. $filter_fragment[0] = $filter;
  80. if ( $vcard->TestFilter($filter_fragment) ) {
  81. return true;
  82. }
  83. }
  84. return false;
  85. }
  86. }
  87. /**
  88. * Process a filter fragment returning an SQL fragment
  89. */
  90. $post_filters = array();
  91. $matchnum = 0;
  92. function SqlFilterCardDAV( $filter, $components, $property = null, $parameter = null ) {
  93. global $post_filters, $target_collection, $matchnum;
  94. $sql = "";
  95. $params = array();
  96. $tag = $filter->GetNSTag();
  97. dbg_error_log("cardquery", "Processing $tag into SQL - %d, '%s', %d\n", count($components), $property, isset($parameter) );
  98. $not_defined = "";
  99. switch( $tag ) {
  100. case 'urn:ietf:params:xml:ns:carddav:is-not-defined':
  101. $sql .= $property . 'IS NULL';
  102. break;
  103. case 'urn:ietf:params:xml:ns:carddav:text-match':
  104. if ( empty($property) ) {
  105. return false;
  106. }
  107. $collation = $filter->GetAttribute("collation");
  108. switch( strtolower($collation) ) {
  109. case 'i;octet':
  110. $comparison = 'LIKE';
  111. break;
  112. case 'i;ascii-casemap':
  113. case 'i;unicode-casemap':
  114. default:
  115. $comparison = 'ILIKE';
  116. break;
  117. }
  118. $search = $filter->GetContent();
  119. $match = $filter->GetAttribute("match-type");
  120. switch( strtolower($match) ) {
  121. case 'equals':
  122. break;
  123. case 'starts-with':
  124. $search = $search.'%';
  125. break;
  126. case 'ends-with':
  127. $search = $search.'%';
  128. break;
  129. case 'contains':
  130. default:
  131. $search = '%'.$search.'%';
  132. break;
  133. }
  134. $pname = ':text_match_'.$matchnum++;
  135. $params[$pname] = $search;
  136. $negate = $filter->GetAttribute("negate-condition");
  137. $negate = ( (isset($negate) && strtolower($negate) ) == "yes" ) ? "NOT " : "";
  138. dbg_error_log("cardquery", " text-match: (%s%s %s '%s') ", $negate, $property, $comparison, $search );
  139. $sql .= sprintf( "(%s%s %s $pname)", $negate, $property, $comparison );
  140. break;
  141. case 'urn:ietf:params:xml:ns:carddav:prop-filter':
  142. $propertyname = $filter->GetAttribute("name");
  143. switch( $propertyname ) {
  144. case 'VERSION':
  145. case 'UID':
  146. case 'NICKNAME':
  147. case 'FN':
  148. case 'NOTE':
  149. case 'ORG':
  150. case 'URL':
  151. case 'FBURL':
  152. case 'CALADRURI':
  153. case 'CALURI':
  154. $property = strtolower($propertyname);
  155. break;
  156. case 'N':
  157. $property = 'name';
  158. break;
  159. default:
  160. $post_filters[] = $filter;
  161. dbg_error_log("cardquery", "Could not handle 'prop-filter' on %s in SQL", $propertyname );
  162. return false;
  163. }
  164. $test_type = $filter->GetAttribute("test");
  165. switch( $test_type ) {
  166. case 'allOf':
  167. $test_type = 'AND';
  168. break;
  169. case 'anyOf':
  170. default:
  171. $test_type = 'OR';
  172. }
  173. $subfilters = $filter->GetContent();
  174. if (count($subfilters) <= 1) {
  175. $success = SqlFilterCardDAV( $subfilters[0], $components, $property, $parameter );
  176. if ( $success !== false ) {
  177. $sql .= $success['sql'];
  178. $params = array_merge( $params, $success['params'] );
  179. }
  180. } else {
  181. $subfilter_added_counter=0;
  182. foreach ($subfilters as $subfilter) {
  183. $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
  184. if ( $success === false ) continue; else {
  185. if ($subfilter_added_counter <= 0) {
  186. $sql .= '(' . $success['sql'];
  187. } else {
  188. $sql .= $test_type . ' ' . $success['sql'];
  189. }
  190. $params = array_merge( $params, $success['params'] );
  191. $subfilter_added_counter++;
  192. }
  193. }
  194. if ($subfilter_added_counter > 0) {
  195. $sql .= ')';
  196. }
  197. }
  198. break;
  199. case 'urn:ietf:params:xml:ns:carddav:param-filter':
  200. $post_filters[] = $filter;
  201. return false; /** Figure out how to handle PARAM-FILTER conditions in the SQL */
  202. /*
  203. $parameter = $filter->GetAttribute("name");
  204. $subfilter = $filter->GetContent();
  205. $success = SqlFilterCardDAV( $subfilter, $components, $property, $parameter );
  206. if ( $success === false ) continue; else {
  207. $sql .= $success['sql'];
  208. $params = array_merge( $params, $success['params'] );
  209. }
  210. break;
  211. */
  212. default:
  213. dbg_error_log("cardquery", "Could not handle unknown tag '%s' in calendar query report", $tag );
  214. break;
  215. }
  216. dbg_error_log("cardquery", "Generated SQL was '%s'", $sql );
  217. return array( 'sql' => $sql, 'params' => $params );
  218. }
  219. /**
  220. * Something that we can handle, at least roughly correctly.
  221. */
  222. $responses = array();
  223. $target_collection = new DAVResource($request->path);
  224. $bound_from = $target_collection->bound_from();
  225. if ( !$target_collection->Exists() ) {
  226. $request->DoResponse( 404 );
  227. }
  228. if ( ! $target_collection->IsAddressbook() ) {
  229. $request->DoResponse( 403, translate('The addressbook-query report must be run against an addressbook collection') );
  230. }
  231. /**
  232. * @todo Once we are past DB version 1.2.1 we can change this query more radically. The best performance to
  233. * date seems to be:
  234. * SELECT caldav_data.*,address_item.* FROM collection JOIN address_item USING (collection_id,user_no)
  235. * JOIN caldav_data USING (dav_id) WHERE collection.dav_name = '/user1/home/'
  236. * AND caldav_data.caldav_type = 'VEVENT' ORDER BY caldav_data.user_no, caldav_data.dav_name;
  237. */
  238. $params = array();
  239. $where = ' WHERE caldav_data.collection_id = ' . $target_collection->resource_id();
  240. if ( is_array($qry_filters) ) {
  241. dbg_log_array( 'cardquery', 'qry_filters', $qry_filters, true );
  242. $appended_where_counter=0;
  243. foreach ($qry_filters as $qry_filter) {
  244. $components = array();
  245. $filter_fragment = SqlFilterCardDAV( $qry_filter, $components );
  246. if ( $filter_fragment !== false ) {
  247. $filter_fragment_sql = $filter_fragment['sql'];
  248. if ( empty($filter_fragment_sql) ) {
  249. continue;
  250. }
  251. if ( $appended_where_counter == 0 ) {
  252. $where .= ' AND (' . $filter_fragment_sql;
  253. $params = $filter_fragment['params'];
  254. } else {
  255. $where .= ' ' . $qry_filters_combination . ' ' . $filter_fragment_sql;
  256. $params = array_merge( $params, $filter_fragment['params'] );
  257. }
  258. $appended_where_counter++;
  259. }
  260. }
  261. if ( $appended_where_counter > 0 ) {
  262. $where .= ')';
  263. }
  264. }
  265. else {
  266. dbg_error_log( 'cardquery', 'No query filters' );
  267. }
  268. $need_post_filter = !empty($post_filters);
  269. if ( $need_post_filter && ( $qry_filters_combination == 'OR' )) {
  270. // we need a post_filter step, and it should be sufficient, that only one
  271. // filter is enough to display the item => we can't prefilter values via SQL
  272. $where = '';
  273. $params = array();
  274. $post_filters = $qry_filters;
  275. }
  276. $sql = 'SELECT * FROM caldav_data INNER JOIN addressbook_resource USING(dav_id)'. $where;
  277. if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id";
  278. $qry = new AwlQuery( $sql, $params );
  279. if ( $qry->Exec("cardquery",__LINE__,__FILE__) && $qry->rows() > 0 ) {
  280. while( $address_object = $qry->Fetch() ) {
  281. if ( !$need_post_filter || apply_filter( $post_filters, $address_object, $qry_filters_combination ) ) {
  282. if ( $bound_from != $target_collection->dav_name() ) {
  283. $address_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $address_object->dav_name);
  284. }
  285. if ( count($address_data_properties) > 0 ) {
  286. $vcard = new VCard($address_object->caldav_data);
  287. $vcard->MaskProperties($address_data_properties);
  288. $address_object->caldav_data = $vcard->Render();
  289. }
  290. $responses[] = component_to_xml( $properties, $address_object );
  291. if ( ($qry_limit > 0) && ( count($responses) >= $qry_limit ) ) {
  292. break;
  293. }
  294. }
  295. }
  296. }
  297. $multistatus = new XMLElement( "multistatus", $responses, $reply->GetXmlNsArray() );
  298. $request->XMLResponse( 207, $multistatus );