From e580fea2268e427798be25b66f33403e96f1e4a0 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Thu, 14 Jun 2007 17:28:35 +0100 Subject: [PATCH] Final completion of refactoring of caldav-REPORT. --- inc/caldav-REPORT-calquery.php | 357 +++++++++++++++------------------ 1 file changed, 162 insertions(+), 195 deletions(-) diff --git a/inc/caldav-REPORT-calquery.php b/inc/caldav-REPORT-calquery.php index 7fccbc0f..96562820 100644 --- a/inc/caldav-REPORT-calquery.php +++ b/inc/caldav-REPORT-calquery.php @@ -1,152 +1,15 @@ xml_tags AS $k => $v ) { - - $fulltag = $v['tag']; - if ( preg_match('/^(.*):([^:]+)$/', $fulltag, $matches) ) { - $xmlns = $matches[1]; - $xmltag = $matches[2]; - } - else { - $xmlns = 'DAV:'; - $xmltag = $tag; - } - - switch ( $fulltag ) { - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY': - dbg_error_log( "REPORT", ":Request: %s -> %s", $v['type'], $xmltag ); - if ( $v['type'] == "open" ) { - $reportnum++; - $report[$reportnum]['type'] = $xmltag; - $report[$reportnum]['include_href'] = 1; - $report[$reportnum]['include_data'] = 1; - } - else { - unset($report_type); - } - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:FILTER': - dbg_error_log( "REPORT", ":Request: %s -> %s", $v['type'], $xmltag ); - if ( $v['type'] == "open" ) { - $filters = array(); - } - else if ( $v['type'] == "close" ) { - $report[$reportnum]['filters'] = $filters; - unset($filters); - } - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:IS-DEFINED': - case 'URN:IETF:PARAMS:XML:NS:CALDAV:COMP-FILTER': - dbg_error_log( "REPORT", ":Request: %s -> %s", $v['type'], $xmltag ); - if ( $v['type'] == "close" ) { - break; - } - $filter_name = $v['attributes']['NAME']; - dbg_log_array( "REPORT", "COMP-FILTER", $v, true ); - if ( isset($filters) ) { - dbg_error_log( "REPORT", "Adding filter '%s'", $filter_name ); - $filters[$filter_name] = 1; - } - else { - dbg_error_log( "ERROR", "Not using COMP-FILTER '%s' outside of defined FILTER!", $filter_name ); - } - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:TIME-RANGE': - dbg_log_array( "REPORT", "TIME-RANGE", $v, true ); - if ( isset($v['attributes']['START']) ) { - $report[$reportnum]['start'] = $v['attributes']['START']; - } - if ( isset($v['attributes']['END']) ) { - $report[$reportnum]['end'] = $v['attributes']['END']; - } - break; - - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:FILTER': - dbg_error_log( "REPORT", "Not using %s information which follows...", $v['tag'] ); - dbg_log_array( "REPORT", "FILTER", $v, true ); - break; - - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:PROP-FILTER': - dbg_log_array( "REPORT", "PROP-FILTER", $v, true ); - if ( $v['type'] == "open" ) { - $prop_filter = array( "name" => $v['attributes']['NAME'] ); - } - elseif ( $v['type'] == "close" ) { - $report[$reportnum]['propfilter'] = $prop_filter; - unset($prop_filter); - } - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:TEXT-MATCH': - dbg_log_array( "REPORT", "TEXT-MATCH", $v, true ); - $prop_filter["text-match"] = $v['value']; - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:IS-NOT-DEFINED': - dbg_log_array( "REPORT", "TEXT-MATCH", $v, true ); - $prop_filter["is-not-defined"] = 1; - break; - - - case 'DAV::PROP': - dbg_log_array( "REPORT", "DAV::PROP", $v, true ); - if ( isset($report[$reportnum]['type']) ) { - if ( $v['type'] == "open" ) { - $report_properties = array(); - } - else if ( $v['type'] == "close" ) { - $report[$reportnum]['properties'] = $report_properties; - unset($report_properties); - } - else { - dbg_error_log( "REPORT", "Unexpected DAV::PROP type of ".$v['type'] ); - } - } - else { - dbg_error_log( "REPORT", "Unexpected DAV::PROP type of ".$v['type']." when no active report type."); - } - break; - - case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA': - case 'DAV::HREF': - case 'DAV::GETETAG': - case 'DAV::GETCONTENTLENGTH': - case 'DAV::GETCONTENTTYPE': - case 'DAV::RESOURCETYPE': - if ( isset($report_properties) ) { - dbg_error_log( "REPORT", "Adding property '%s'", $xmltag ); - $report_properties[$xmltag] = 1; - } - else { - dbg_error_log( "ERROR", "Not using property '%s' outside of defined report!", $xmltag ); - } - break; - - default: - $unsupported[$xmltag] = $xmlns; - dbg_error_log( "REPORT", "Unhandled tag >>%s<<", $fulltag ); - } -} - -$request->UnsupportedRequest($unsupported); // Won't return if there was unsupported stuff. - /** * Build the array of properties to include in the report output */ -$mg_content = $xmltree->GetContent('URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-MULTIGET'); -$proptype = $mg_content[0]->GetTag(); +$qry_content = $xmltree->GetContent('URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY'); +$proptype = $qry_content[0]->GetTag(); $properties = array(); switch( $proptype ) { case 'DAV::PROP': - $mg_props = $xmltree->GetPath('/URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-MULTIGET/DAV::PROP/*'); - foreach( $mg_props AS $k => $v ) { + $qry_props = $xmltree->GetPath('/URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY/DAV::PROP/*'); + foreach( $qry_props AS $k => $v ) { $propertyname = preg_replace( '/^.*:/', '', $v->GetTag() ); $properties[$propertyname] = 1; } @@ -162,27 +25,161 @@ switch( $proptype ) { } /** -* Return XML for a single calendar (or todo) entry from the DB -* -* @param array $filter The definition of the prop-filter -* @param string $item The SQL calendar row for this calendar -* + * There can only be *one* FILTER element, and it must contain *one* COMP-FILTER + * element. In every case I can see this contained COMP-FILTER element will be a + * VCALENDAR, but perhaps there are others. In our case we strip it if that is + * the case and leave it alone otherwise. + */ +$qry_filters = $xmltree->GetPath('/URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY/URN:IETF:PARAMS:XML:NS:CALDAV:FILTER/*'); +$qry_filters = $qry_filters[0]; // There can only be one FILTER element +if ( $qry_filters->GetTag() == "URN:IETF:PARAMS:XML:NS:CALDAV:COMP-FILTER" && $qry_filters->GetAttribute("NAME") == "VCALENDAR" ) + $qry_filters = $qry_filters->GetContent(); // Everything is inside a VCALENDAR AFAICS + +/** +* While we can construct our SQL to apply some filters in the query, other filters +* need to be checked against the retrieved record. This is for handling those ones. +* +* @param array $filter An array of XMLElement which is the filter definition +* @param string $item The database row retrieved for this calendar item +* * @return boolean True if the check succeeded, false otherwise. */ -function check_prop_filter( $filter, $item ) { +function apply_filter( $filter, $item ) { global $session, $c, $request; - dbg_error_log("REPORT","Checking property filter for item '%s'", $item->dav_name ); + if ( count($filter) == 0 ) return true; + + dbg_error_log("REPORT","Applying filter for item '%s'", $item->dav_name ); $ical = new iCalendar( array( "icalendar" => $item->caldav_data) ); - $property = $ical->Get($filter["name"]); - if ( $property == "" && isset($filter["is-not-defined"]) ) return true; - - foreach( $filter AS $k => $v ) { - if ( $k == 'name' || $k == 'is-not-defined' ) continue; - } + return $ical->TestFilter($filter); } +/** + * Process a filter fragment returning an SQL fragment + */ +$need_post_filter = false; +function SqlFilterFragment( $filter, $components, $property = null, $parameter = null ) { + $sql = ""; + foreach( $filter AS $k => $v ) { + $tag = $v->GetTag(); + dbg_error_log("REPORT", "Processing $tag into SQL - %d, '%s', %d\n", count($components), $property, isset($parameter) ); + + $not_defined = ""; + switch( $tag ) { + case 'URN:IETF:PARAMS:XML:NS:CALDAV:IS-NOT-DEFINED': + $not_defined = "NOT "; // then fall through to IS-DEFINED case + case 'URN:IETF:PARAMS:XML:NS:CALDAV:IS-DEFINED': + if ( isset( $parameter ) ) { + $need_post_filter = true; + return false; // Not handled in SQL + } + if ( isset( $property ) ) { + switch( $property ) { + case "xxx": + default: + $need_post_filter = true; + return false; // Not handled in SQL + } + } + elseif ( count($components) > 0 ) { + $filters = ""; + foreach( $components AS $ck => $cv ) { + switch ( $cv ) { + case 'VCALENDAR': + continue; + default: + $filters .= ($filters == "" ? "" : ", ") . qpg($cv); + } + } + if ( $filters != "" ) { + $sql .= "AND caldav_data.caldav_type ".$not_defined."IN ( $filters ) "; + } + } + break; + + case 'URN:IETF:PARAMS:XML:NS:CALDAV:TIME-RANGE': + /** + * TODO: We should probably allow time range queries against other properties, since eventually some client may want to do this. + */ + $start_column = 'dtend'; // The column we compare against the START attribute + $finish_column = 'dtstart'; // The column we compare against the END attribute + $start = $v->GetAttribute("START"); + $finish = $v->GetAttribute("END"); + if ( isset($start) ) { + $sql .= sprintf( "AND (%s >= %s::timestamp with time zone ", $start_column, qpg($start)); + $sql .= sprintf( "OR calculate_later_timestamp(%s::timestamp with time zone,%s,rrule) >= %s::timestamp with time zone) ", qpg($start), $start_column, qpg($start) ); + } + if ( isset( $report[$i]['end'] ) ) { + $sql .= sprintf( "AND %s <= %s::timestamp with time zone ", $finish_column, qpg($finish) ); + } + break; + + case 'URN:IETF:PARAMS:XML:NS:CALDAV:TEXT-MATCH': + $search = $v->GetContent(); + $negate = $v->GetAttribute("NEGATE-CONDITION"); + if ( isset($negate) && strtolower($negate) == "yes" ) $match = !$match; + $sql .= sprintf( "AND %s LIKE %s ", $property, qpg("%".$search."%") ); + break; + + case 'URN:IETF:PARAMS:XML:NS:CALDAV:COMP-FILTER': + $components[] = $v->GetAttribute("NAME"); + $subfilter = $v->GetContent(); + $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); + if ( $success === false ) continue; else $sql .= $success; + break; + + case 'URN:IETF:PARAMS:XML:NS:CALDAV:PROP-FILTER': + $propertyname = $v->GetAttribute("NAME"); + switch( $propertyname ) { + case 'UID': + case 'SUMMARY': + case 'LOCATION': + case 'DESCRIPTION': + case 'CLASS': + case 'TRANSP': + case 'URL': + case 'TZID': // May not work as expected. Perhaps this should be checked later. + case 'STATUS': + $property = strtolower($propertyname); + break; + default: + $need_post_filter = true; + return false; // Can't handle PARAM-FILTER conditions in the SQL + } + $subfilter = $v->GetContent(); + $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); + if ( $success === false ) continue; else $sql .= $success; + break; + + case 'URN:IETF:PARAMS:XML:NS:CALDAV:PARAM-FILTER': + $need_post_filter = true; + return false; // Can't handle PARAM-FILTER conditions in the SQL + $parameter = $v->GetAttribute("NAME"); + $subfilter = $v->GetContent(); + $success = SqlFilterFragment( $subfilter, $components, $property, $parameter ); + if ( $success === false ) continue; else $sql .= $success; + break; + } + } + return $sql; +} + +/** + * Build an SQL 'WHERE' clause which implements (parts of) the filter. The + * elements of the filter which are implemented in the SQL will be removed. + * + * @param arrayref &$filter A reference to an array of XMLElement defining the filter + * + * @return string A string suitable for use as an SQL 'WHERE' clause selecting the desired records. + */ +function BuildSqlFilter( $filter ) { + $components = array(); + $sql = SqlFilterFragment( $filter, $components ); + if ( $sql === false ) return ""; + return $sql; +} + /** * Something that we can handle, at least roughly correctly. @@ -190,48 +187,18 @@ function check_prop_filter( $filter, $item ) { $responses = array(); -for ( $i=0; $i <= $reportnum; $i++ ) { +$where = " WHERE caldav_data.dav_name ~ ".qpg("^".$request->path)." "; +$where .= BuildSqlFilter( $qry_filters); - $where = " WHERE caldav_data.dav_name ~ ".qpg("^".$request->path)." "; - switch( $report[$i]['type'] ) { - case 'CALENDAR-QUERY': - if ( ! ($request->AllowedTo('read') ) ) $request->DoResponse( 403, translate("You may not access that calendar") ); - if ( isset( $report[$i]['start'] ) ) { - $where .= "AND (dtend >= ".qpg($report[$i]['start'])."::timestamp with time zone "; - $where .= "OR calculate_later_timestamp(".qpg($report[$i]['start'])."::timestamp with time zone,dtend,rrule) >= ".qpg($report[$i]['start'])."::timestamp with time zone) "; - } - if ( isset( $report[$i]['end'] ) ) { - $where .= "AND dtstart <= ".qpg($report[$i]['end'])."::timestamp with time zone "; - } - break; - - default: - dbg_error_log("REPORT", "Unhandled report type of '%s'", $report[$i]['type'] ); - } - - if ( isset( $report[$i]['filters'] ) ) { - /** - * Only report on the filtered types that were specified - */ - $filters = ""; - foreach( $report[$i]['filters'] AS $k => $v ) { - $filters .= ($filters == "" ? "" : ", ") . qpg($k); - } - if ( $filters != "" ) { - $where .= "AND caldav_data.caldav_type IN ( $filters ) "; - } - } - - $where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL OR get_permissions($session->user_no,caldav_data.user_no) ~ 'A') "; // Must have 'all' permissions to see confidential items - if ( isset($c->hide_TODO) && $c->hide_TODO ) { - $where .= "AND (caldav_data.caldav_type NOT IN ('VTODO') OR get_permissions($session->user_no,caldav_data.user_no) ~ 'A') "; - } - $qry = new PgQuery( "SELECT * , get_permissions($session->user_no,caldav_data.user_no) as permissions FROM caldav_data INNER JOIN calendar_item USING(user_no, dav_name)". $where ); - if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows > 0 ) { - while( $calendar_object = $qry->Fetch() ) { - if ( !isset($report[$reportnum]['propfilter']) || check_prop_filter( $report[$reportnum]['propfilter'], $calendar_object ) ) { - $responses[] = calendar_to_xml( $report[$i]['properties'], $calendar_object ); - } +$where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL OR get_permissions($session->user_no,caldav_data.user_no) ~ 'A') "; // Must have 'all' permissions to see confidential items +if ( isset($c->hide_TODO) && $c->hide_TODO ) { + $where .= "AND (caldav_data.caldav_type NOT IN ('VTODO') OR get_permissions($session->user_no,caldav_data.user_no) ~ 'A') "; +} +$qry = new PgQuery( "SELECT * , get_permissions($session->user_no,caldav_data.user_no) as permissions FROM caldav_data INNER JOIN calendar_item USING(user_no, dav_name)". $where ); +if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows > 0 ) { + while( $calendar_object = $qry->Fetch() ) { + if ( !$need_post_filter || apply_filter( $qry_filters, $calendar_object ) ) { + $responses[] = calendar_to_xml( $properties, $calendar_object ); } } }