[3733] | 1 | <?php |
---|
| 2 | |
---|
| 3 | $need_expansion = false; |
---|
| 4 | function check_for_expansion( $calendar_data_node ) { |
---|
| 5 | global $need_expansion, $expand_range_start, $expand_range_end; |
---|
| 6 | |
---|
| 7 | if ( !class_exists('DateTime') ) return; /** We don't support expansion on PHP5.1 */ |
---|
| 8 | |
---|
| 9 | $expansion = $calendar_data_node->GetElements('urn:ietf:params:xml:ns:caldav:expand'); |
---|
| 10 | if ( isset($expansion[0]) ) { |
---|
| 11 | $need_expansion = true; |
---|
| 12 | $expand_range_start = $expansion[0]->GetAttribute('start'); |
---|
| 13 | $expand_range_end = $expansion[0]->GetAttribute('end'); |
---|
| 14 | if ( isset($expand_range_start) ) $expand_range_start = new RepeatRuleDateTime($expand_range_start); |
---|
| 15 | if ( isset($expand_range_end) ) $expand_range_end = new RepeatRuleDateTime($expand_range_end); |
---|
| 16 | } |
---|
| 17 | } |
---|
| 18 | |
---|
| 19 | /** |
---|
| 20 | * Build the array of properties to include in the report output |
---|
| 21 | */ |
---|
| 22 | $qry_content = $xmltree->GetContent('urn:ietf:params:xml:ns:caldav:calendar-query'); |
---|
| 23 | $proptype = $qry_content[0]->GetTag(); |
---|
| 24 | $properties = array(); |
---|
| 25 | switch( $proptype ) { |
---|
| 26 | case 'DAV::prop': |
---|
| 27 | $qry_props = $xmltree->GetPath('/urn:ietf:params:xml:ns:caldav:calendar-query/'.$proptype.'/*'); |
---|
| 28 | foreach( $qry_content[0]->GetElements() AS $k => $v ) { |
---|
| 29 | $propertyname = preg_replace( '/^.*:/', '', $v->GetTag() ); |
---|
| 30 | $properties[$propertyname] = 1; |
---|
| 31 | if ( $v->GetTag() == 'urn:ietf:params:xml:ns:caldav:calendar-data' ) check_for_expansion($v); |
---|
| 32 | } |
---|
| 33 | break; |
---|
| 34 | |
---|
| 35 | case 'DAV::allprop': |
---|
| 36 | $properties['allprop'] = 1; |
---|
| 37 | if ( $qry_content[1]->GetTag() == 'DAV::include' ) { |
---|
| 38 | foreach( $qry_content[1]->GetElements() AS $k => $v ) { |
---|
| 39 | $include_properties[] = $v->GetTag(); /** $include_properties is referenced in DAVResource where allprop is expanded */ |
---|
| 40 | if ( $v->GetTag() == 'urn:ietf:params:xml:ns:caldav:calendar-data' ) check_for_expansion($v); |
---|
| 41 | } |
---|
| 42 | } |
---|
| 43 | break; |
---|
| 44 | |
---|
| 45 | default: |
---|
| 46 | $propertyname = preg_replace( '/^.*:/', '', $proptype ); |
---|
| 47 | $properties[$propertyname] = 1; |
---|
| 48 | } |
---|
| 49 | |
---|
| 50 | /** |
---|
| 51 | * There can only be *one* FILTER element, and it must contain *one* COMP-FILTER |
---|
| 52 | * element. In every case I can see this contained COMP-FILTER element will be a |
---|
| 53 | * VCALENDAR, but perhaps there are others. In our case we strip it if that is |
---|
| 54 | * the case and leave it alone otherwise. |
---|
| 55 | */ |
---|
| 56 | $qry_filters = $xmltree->GetPath('/urn:ietf:params:xml:ns:caldav:calendar-query/urn:ietf:params:xml:ns:caldav:filter/*'); |
---|
| 57 | if ( count($qry_filters) == 1 ) { |
---|
| 58 | $qry_filters = $qry_filters[0]; // There can only be one FILTER element |
---|
| 59 | if ( $qry_filters->GetTag() == "urn:ietf:params:xml:ns:caldav:comp-filter" && $qry_filters->GetAttribute("name") == "VCALENDAR" ) |
---|
| 60 | $qry_filters = $qry_filters->GetContent(); // Everything is inside a VCALENDAR AFAICS |
---|
| 61 | else { |
---|
| 62 | dbg_error_log("calquery", "Got bizarre CALDAV:FILTER[%s=%s]] which does not contain comp-filter = VCALENDAR!!", $qry_filters->GetTag(), $qry_filters->GetAttribute("name") ); |
---|
| 63 | $qry_filters = false; |
---|
| 64 | } |
---|
| 65 | } |
---|
| 66 | else { |
---|
| 67 | $qry_filters = false; |
---|
| 68 | } |
---|
| 69 | |
---|
| 70 | |
---|
| 71 | /** |
---|
| 72 | * While we can construct our SQL to apply some filters in the query, other filters |
---|
| 73 | * need to be checked against the retrieved record. This is for handling those ones. |
---|
| 74 | * |
---|
| 75 | * @param array $filter An array of XMLElement which is the filter definition |
---|
| 76 | * @param string $item The database row retrieved for this calendar item |
---|
| 77 | * |
---|
| 78 | * @return boolean True if the check succeeded, false otherwise. |
---|
| 79 | */ |
---|
| 80 | function apply_filter( $filters, $item ) { |
---|
| 81 | global $session, $c, $request; |
---|
| 82 | |
---|
| 83 | if ( count($filters) == 0 ) return true; |
---|
| 84 | |
---|
| 85 | dbg_error_log("calquery","Applying filter for item '%s'", $item->dav_name ); |
---|
| 86 | $ical = new iCalendar( array( "icalendar" => $item->caldav_data) ); |
---|
| 87 | return $ical->TestFilter($filters); |
---|
| 88 | } |
---|
| 89 | |
---|
| 90 | |
---|
| 91 | /** |
---|
| 92 | * Process a filter fragment returning an SQL fragment |
---|
| 93 | */ |
---|
| 94 | $need_post_filter = false; |
---|
| 95 | function SqlFilterFragment( $filter, $components, $property = null, $parameter = null ) { |
---|
| 96 | global $need_post_filter, $target_collection; |
---|
| 97 | $sql = ""; |
---|
| 98 | $params = array(); |
---|
| 99 | if ( !is_array($filter) ) { |
---|
| 100 | dbg_error_log( "calquery", "Filter is of type '%s', but should be an array of XML Tags.", gettype($filter) ); |
---|
| 101 | } |
---|
| 102 | |
---|
| 103 | foreach( $filter AS $k => $v ) { |
---|
| 104 | $tag = $v->GetTag(); |
---|
| 105 | dbg_error_log("calquery", "Processing $tag into SQL - %d, '%s', %d\n", count($components), $property, isset($parameter) ); |
---|
| 106 | |
---|
| 107 | $not_defined = ""; |
---|
| 108 | switch( $tag ) { |
---|
| 109 | case 'urn:ietf:params:xml:ns:caldav:is-not-defined': |
---|
| 110 | $not_defined = "not-"; // then fall through to IS-DEFINED case |
---|
| 111 | case 'urn:ietf:params:xml:ns:caldav:is-defined': |
---|
| 112 | if ( isset( $parameter ) ) { |
---|
| 113 | $need_post_filter = true; |
---|
| 114 | dbg_error_log("calquery", "Could not handle 'is-%sdefined' on property %s, parameter %s in SQL", $not_defined, $property, $parameter ); |
---|
| 115 | return false; // Not handled in SQL |
---|
| 116 | } |
---|
| 117 | if ( isset( $property ) ) { |
---|
| 118 | switch( $property ) { |
---|
| 119 | case 'created': |
---|
| 120 | case 'completed': /** @todo when it can be handled in the SQL - see around line 200 below */ |
---|
| 121 | case 'dtend': |
---|
| 122 | case 'dtstamp': |
---|
| 123 | case 'dtstart': |
---|
| 124 | if ( ! $target_collection->IsSchedulingCollection() ) { |
---|
| 125 | $property_defined_match = "IS NOT NULL"; |
---|
| 126 | } |
---|
| 127 | break; |
---|
| 128 | |
---|
| 129 | case 'priority': |
---|
| 130 | $property_defined_match = "IS NOT NULL"; |
---|
| 131 | break; |
---|
| 132 | |
---|
| 133 | default: |
---|
| 134 | $property_defined_match = "LIKE '_%'"; // i.e. contains a single character or more |
---|
| 135 | } |
---|
| 136 | $sql .= sprintf( "AND %s %s%s ", $property, $not_defined, $property_defined_match ); |
---|
| 137 | } |
---|
| 138 | break; |
---|
| 139 | |
---|
| 140 | case 'urn:ietf:params:xml:ns:caldav:time-range': |
---|
| 141 | /** |
---|
| 142 | * @todo We should probably allow time range queries against other properties, since eventually some client may want to do this. |
---|
| 143 | */ |
---|
| 144 | $start_column = ($components[sizeof($components)-1] == 'VTODO' ? "due" : 'dtend'); // The column we compare against the START attribute |
---|
| 145 | $finish_column = 'dtstart'; // The column we compare against the END attribute |
---|
| 146 | $start = $v->GetAttribute("start"); |
---|
| 147 | $finish = $v->GetAttribute("end"); |
---|
| 148 | if ( isset($start) || isset($finish) ) { |
---|
| 149 | $sql .= ' AND (rrule_event_overlaps( dtstart, dtend, rrule, :time_range_start, :time_range_end ) OR event_has_exceptions(caldav_data.caldav_data) ) '; |
---|
| 150 | $params[':time_range_start'] = $start; |
---|
| 151 | $params[':time_range_end'] = $finish; |
---|
| 152 | } |
---|
| 153 | break; |
---|
| 154 | |
---|
| 155 | case 'urn:ietf:params:xml:ns:caldav:text-match': |
---|
| 156 | $search = $v->GetContent(); |
---|
| 157 | $negate = $v->GetAttribute("negate-condition"); |
---|
| 158 | $collation = $v->GetAttribute("collation"); |
---|
| 159 | switch( strtolower($collation) ) { |
---|
| 160 | case 'i;octet': |
---|
| 161 | $comparison = 'LIKE'; |
---|
| 162 | break; |
---|
| 163 | case 'i;ascii-casemap': |
---|
| 164 | default: |
---|
| 165 | $comparison = 'ILIKE'; |
---|
| 166 | break; |
---|
| 167 | } |
---|
| 168 | $params[':text_match'] = '%'.$search.'%'; |
---|
| 169 | dbg_error_log("calquery", " text-match: (%s IS NULL OR %s%s %s '%s') ", $property, (isset($negate) && strtolower($negate) == "yes" ? "NOT ": ""), |
---|
| 170 | $property, $comparison, $params[':text_match'] ); |
---|
| 171 | $sql .= sprintf( "AND (%s IS NULL OR %s%s %s :text_match) ", $property, (isset($negate) && strtolower($negate) == "yes" ? "NOT ": ""), |
---|
| 172 | $property, $comparison ); |
---|
| 173 | break; |
---|
| 174 | |
---|
| 175 | case 'urn:ietf:params:xml:ns:caldav:comp-filter': |
---|
| 176 | $comp_filter_name = $v->GetAttribute("name"); |
---|
| 177 | if ( count($components) == 0 ) { |
---|
| 178 | $sql .= "AND caldav_data.caldav_type = :component_name_filter "; |
---|
| 179 | $params[':component_name_filter'] = $comp_filter_name; |
---|
| 180 | } |
---|
| 181 | $components[] = $comp_filter_name; |
---|
| 182 | $subfilter = $v->GetContent(); |
---|
| 183 | if ( is_array( $subfilter ) ) { |
---|
| 184 | $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); |
---|
| 185 | if ( $success === false ) continue; else { |
---|
| 186 | $sql .= $success['sql']; |
---|
| 187 | $params = array_merge( $params, $success['params'] ); |
---|
| 188 | } |
---|
| 189 | } |
---|
| 190 | break; |
---|
| 191 | |
---|
| 192 | case 'urn:ietf:params:xml:ns:caldav:prop-filter': |
---|
| 193 | $propertyname = $v->GetAttribute("name"); |
---|
| 194 | switch( $propertyname ) { |
---|
| 195 | case 'PERCENT-COMPLETE': |
---|
| 196 | $property = 'percent_complete'; |
---|
| 197 | break; |
---|
| 198 | |
---|
| 199 | case 'UID': |
---|
| 200 | case 'SUMMARY': |
---|
| 201 | case 'LOCATION': |
---|
| 202 | case 'DESCRIPTION': |
---|
| 203 | case 'CLASS': |
---|
| 204 | case 'TRANSP': |
---|
| 205 | case 'RRULE': // Likely that this is not much use |
---|
| 206 | case 'URL': |
---|
| 207 | case 'STATUS': |
---|
| 208 | case 'CREATED': |
---|
| 209 | case 'DTSTAMP': |
---|
| 210 | case 'DTSTART': |
---|
| 211 | case 'DTEND': |
---|
| 212 | case 'DUE': |
---|
| 213 | case 'PRIORITY': |
---|
| 214 | $property = strtolower($propertyname); |
---|
| 215 | break; |
---|
| 216 | |
---|
| 217 | case 'COMPLETED': /** @todo this should be moved into the properties supported in SQL. */ |
---|
| 218 | default: |
---|
| 219 | $need_post_filter = true; |
---|
| 220 | dbg_error_log("calquery", "Could not handle 'prop-filter' on %s in SQL", $propertyname ); |
---|
| 221 | continue; |
---|
| 222 | } |
---|
| 223 | $subfilter = $v->GetContent(); |
---|
| 224 | $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); |
---|
| 225 | if ( $success === false ) continue; else { |
---|
| 226 | $sql .= $success['sql']; |
---|
| 227 | $params = array_merge( $params, $success['params'] ); |
---|
| 228 | } |
---|
| 229 | break; |
---|
| 230 | |
---|
| 231 | case 'urn:ietf:params:xml:ns:caldav:param-filter': |
---|
| 232 | $need_post_filter = true; |
---|
| 233 | return false; // Can't handle PARAM-FILTER conditions in the SQL |
---|
| 234 | $parameter = $v->GetAttribute("name"); |
---|
| 235 | $subfilter = $v->GetContent(); |
---|
| 236 | $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); |
---|
| 237 | if ( $success === false ) continue; else { |
---|
| 238 | $sql .= $success['sql']; |
---|
| 239 | $params = array_merge( $params, $success['params'] ); |
---|
| 240 | } |
---|
| 241 | break; |
---|
| 242 | |
---|
| 243 | default: |
---|
| 244 | dbg_error_log("calquery", "Could not handle unknown tag '%s' in calendar query report", $tag ); |
---|
| 245 | break; |
---|
| 246 | } |
---|
| 247 | } |
---|
| 248 | dbg_error_log("calquery", "Generated SQL was '%s'", $sql ); |
---|
| 249 | return array( 'sql' => $sql, 'params' => $params ); |
---|
| 250 | } |
---|
| 251 | |
---|
| 252 | /** |
---|
| 253 | * Build an SQL 'WHERE' clause which implements (parts of) the filter. The |
---|
| 254 | * elements of the filter which are implemented in the SQL will be removed. |
---|
| 255 | * |
---|
| 256 | * @param arrayref &$filter A reference to an array of XMLElement defining the filter |
---|
| 257 | * |
---|
| 258 | * @return string A string suitable for use as an SQL 'WHERE' clause selecting the desired records. |
---|
| 259 | */ |
---|
| 260 | function BuildSqlFilter( $filter ) { |
---|
| 261 | $components = array(); |
---|
| 262 | return SqlFilterFragment( $filter, $components ); |
---|
| 263 | } |
---|
| 264 | |
---|
| 265 | |
---|
| 266 | /** |
---|
| 267 | * Something that we can handle, at least roughly correctly. |
---|
| 268 | */ |
---|
| 269 | |
---|
| 270 | $responses = array(); |
---|
| 271 | $target_collection = new DAVResource($request->path); |
---|
| 272 | $bound_from = $target_collection->bound_from(); |
---|
| 273 | if ( !$target_collection->Exists() ) { |
---|
| 274 | $request->DoResponse( 404 ); |
---|
| 275 | } |
---|
| 276 | if ( ! ($target_collection->IsCalendar() || $target_collection->IsSchedulingCollection()) ) { |
---|
| 277 | $request->DoResponse( 403, translate('The calendar-query report must be run against a calendar or a scheduling collection') ); |
---|
| 278 | } |
---|
| 279 | |
---|
| 280 | /** |
---|
| 281 | * @todo Once we are past DB version 1.2.1 we can change this query more radically. The best performance to |
---|
| 282 | * date seems to be: |
---|
| 283 | * SELECT caldav_data.*,calendar_item.* FROM collection JOIN calendar_item USING (collection_id,user_no) |
---|
| 284 | * JOIN caldav_data USING (dav_id) WHERE collection.dav_name = '/user1/home/' |
---|
| 285 | * AND caldav_data.caldav_type = 'VEVENT' ORDER BY caldav_data.user_no, caldav_data.dav_name; |
---|
| 286 | */ |
---|
| 287 | |
---|
| 288 | $params = array(); |
---|
| 289 | //$where = ' WHERE caldav_data.collection_id = ' . $target_collection->resource_id(); |
---|
| 290 | //if ( is_array($qry_filters) ) { |
---|
| 291 | // dbg_log_array( "calquery", "qry_filters", $qry_filters, true ); |
---|
| 292 | // $components = array(); |
---|
| 293 | // $filter_fragment = SqlFilterFragment( $qry_filters, $components ); |
---|
| 294 | // if ( $filter_fragment !== false ) { |
---|
| 295 | // $where .= ' '.$filter_fragment['sql']; |
---|
| 296 | // $params = $filter_fragment['params']; |
---|
| 297 | // } |
---|
| 298 | //} |
---|
| 299 | //if ( $target_collection->Privileges() != privilege_to_bits('DAV::all') ) { |
---|
| 300 | // $where .= " AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) "; |
---|
| 301 | //} |
---|
| 302 | |
---|
| 303 | //if ( isset($c->hide_TODO) && $c->hide_TODO && ! $target_collection->HavePrivilegeTo('DAV::write-content') ) { |
---|
| 304 | // $where .= " AND caldav_data.caldav_type NOT IN ('VTODO') "; |
---|
| 305 | //} |
---|
| 306 | |
---|
| 307 | //if ( isset($c->hide_older_than) && intval($c->hide_older_than > 0) ) { |
---|
| 308 | // $where .= " AND calendar_item.dtstart > (now() - interval '".intval($c->hide_older_than)." days') "; |
---|
| 309 | //} |
---|
| 310 | |
---|
| 311 | //$sql = 'SELECT * FROM caldav_data INNER JOIN calendar_item USING(dav_id,user_no,dav_name)'. $where; |
---|
| 312 | //if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id"; |
---|
| 313 | //$qry = new AwlQuery( $sql, $params ); |
---|
| 314 | //if ( $qry->Exec("calquery",__LINE__,__FILE__) && $qry->rows() > 0 ) { |
---|
| 315 | // while( $calendar_object = $qry->Fetch() ) { |
---|
| 316 | // if ( !$need_post_filter || apply_filter( $qry_filters, $calendar_object ) ) { |
---|
| 317 | // if ( $bound_from != $target_collection->dav_name() ) { |
---|
| 318 | // $calendar_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $calendar_object->dav_name); |
---|
| 319 | // } |
---|
| 320 | // if ( $need_expansion ) { |
---|
| 321 | // $vResource = new vComponent($calendar_object->caldav_data); |
---|
| 322 | // $expanded = expand_event_instances($vResource, $expand_range_start, $expand_range_end); |
---|
| 323 | // $calendar_object->caldav_data = $expanded->Render(); |
---|
| 324 | // } |
---|
| 325 | // $responses[] = calendar_to_xml( $properties, $calendar_object ); |
---|
| 326 | // } |
---|
| 327 | // } |
---|
| 328 | //} |
---|
| 329 | $nome = $target_collection->GetProperty('user_no'); |
---|
| 330 | if ( $target_collection->Privileges() != privilege_to_bits('DAV::all') ) { |
---|
| 331 | $where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) "; |
---|
| 332 | } |
---|
| 333 | |
---|
| 334 | if ( isset($c->hide_TODO) && $c->hide_TODO && ! $target_collection->Privileges() == privilege_to_bits('all') ) { |
---|
| 335 | $where .= "AND caldav_data.caldav_type NOT IN ('VTODO') "; |
---|
| 336 | } |
---|
| 337 | $sqlp = "SELECT cal_id FROM phpgw_cal_user WHERE cal_type = 'u' AND cal_status = 'A' AND cal_login = :nome"; |
---|
| 338 | $qryp = new AwlQuery( $sqlp, array( ':nome' => $nome) ); |
---|
| 339 | if ( $qryp->Exec("REPORT-MULTIGET",__LINE__,__FILE__) && $qryp->rows() > 0 ) { |
---|
| 340 | while( $part = $qryp->Fetch() ) { |
---|
| 341 | $sql = "SELECT * FROM phpgw_cal WHERE cal_id = $part->cal_id AND cal_type = 'E'"; |
---|
| 342 | $qry = new AwlQuery( $sql); |
---|
| 343 | if ( $qry->Exec("REPORT-MULTIGET",__LINE__,__FILE__) && $qry->rows() > 0 ) { |
---|
| 344 | while( $calendar_object = $qry->Fetch() ) { |
---|
| 345 | if ( $bound_from != $target_collection->dav_name() ) { |
---|
| 346 | $calendar_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $calendar_object->dav_name); |
---|
| 347 | } |
---|
| 348 | if ( $need_expansion ) { |
---|
| 349 | $vResource = new vComponent($calendar_object->caldav_data); |
---|
| 350 | $expanded = expand_event_instances($vResource, $expand_range_start, $expand_range_end); |
---|
| 351 | $calendar_object->caldav_data = $expanded->Render(); |
---|
| 352 | } |
---|
| 353 | $responses[] = calendar_to_xml( $properties, $calendar_object ); |
---|
| 354 | } |
---|
| 355 | } |
---|
| 356 | $sql = "SELECT * FROM phpgw_cal INNER JOIN phpgw_cal_repeats USING(cal_id) WHERE cal_id = $part->cal_id"; |
---|
| 357 | $qry = new AwlQuery( $sql); |
---|
| 358 | if ( $qry->Exec("PROPFIND",__LINE__,__FILE__) && $qry->rows() > 0 ) { |
---|
| 359 | while( $calendar_object = $qry->Fetch() ) { |
---|
| 360 | if ( $bound_from != $target_collection->dav_name() ) { |
---|
| 361 | $calendar_object->dav_name = str_replace( $bound_from, $target_collection->dav_name(), $calendar_object->dav_name); |
---|
| 362 | } |
---|
| 363 | if ( $need_expansion ) { |
---|
| 364 | $vResource = new vComponent($calendar_object->caldav_data); |
---|
| 365 | $expanded = expand_event_instances($vResource, $expand_range_start, $expand_range_end); |
---|
| 366 | $calendar_object->caldav_data = $expanded->Render(); |
---|
| 367 | } |
---|
| 368 | $responses[] = calendar_to_xml( $properties, $calendar_object ); |
---|
| 369 | } |
---|
| 370 | } |
---|
| 371 | } |
---|
| 372 | } |
---|
| 373 | |
---|
| 374 | |
---|
| 375 | |
---|
| 376 | |
---|
| 377 | $multistatus = new XMLElement( "multistatus", $responses, $reply->GetXmlNsArray() ); |
---|
| 378 | |
---|
| 379 | $request->XMLResponse( 207, $multistatus ); |
---|