From 4178ab425420ae570432f47193443e3470b9203a Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Sat, 1 Jan 2011 21:25:46 +1300 Subject: [PATCH] Add caching of collection reads Also a special header is added for telling DAViCal to flush the cache during regression testing etc. Signed-off-by: Andrew McMillan --- inc/CalDAVRequest.php | 6 ++ inc/DAVResource.php | 93 ++++++++++++++----- inc/caldav-BIND.php | 6 ++ inc/caldav-DELETE.php | 5 + inc/caldav-MOVE.php | 16 ++++ inc/caldav-PROPPATCH.php | 1 - inc/caldav-PUT-functions.php | 9 +- testing/tests/binding/1000-BIND.test | 1 + .../tests/carddav/2000-MKCOL-addressbook.test | 2 + .../tests/scheduling/3001-POST-CANCEL.test | 2 + 10 files changed, 111 insertions(+), 30 deletions(-) diff --git a/inc/CalDAVRequest.php b/inc/CalDAVRequest.php index 5d1ec9ea..84adcb89 100644 --- a/inc/CalDAVRequest.php +++ b/inc/CalDAVRequest.php @@ -14,6 +14,7 @@ * @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once("AwlCache.php"); require_once("XMLDocument.php"); require_once("CalDAVPrincipal.php"); include("DAVTicket.php"); @@ -343,6 +344,11 @@ EOSQL; $qry->Exec('caldav',__LINE__,__FILE__); dbg_error_log( 'caldav', 'Created new collection as "%s".', trim($params[':boxname']) ); + // Uncache anything to do with the collection + $cache = getCacheInstance(); + $cache->delete( 'collection-'.$params[':dav_name'], null ); + $cache->delete( 'principal-'.$params[':parent_container'], null ); + $qry = new AwlQuery( "SELECT * FROM collection WHERE dav_name = :dav_name", array( ':dav_name' => $matches[1] ) ); if ( $qry->Exec('caldav',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { $this->collection_id = $row->collection_id; diff --git a/inc/DAVResource.php b/inc/DAVResource.php index d76bec14..e116b606 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -9,6 +9,7 @@ * @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once('AwlCache.php'); require_once('AwlQuery.php'); require_once('DAVTicket.php'); @@ -295,23 +296,8 @@ class DAVResource } - /** - * Find the collection associated with this resource. - */ - function FetchCollection() { - global $c, $session, $request; - /** - * RFC4918, 8.3: Identifiers for collections SHOULD end in '/' - * - also discussed at more length in 5.2 - * - * So we look for a collection which matches one of the following URLs: - * - The exact request. - * - If the exact request, doesn't end in '/', then the request URL with a '/' appended - * - The request URL truncated to the last '/' - * The collection URL for this request is therefore the longest row in the result, so we - * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1" - */ - dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name ); + private function ReadCollectionFromDatabase() { + global $c, $session; $this->collection = (object) array( 'collection_id' => -1, @@ -385,7 +371,9 @@ EOSQL; || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) { $this->_is_principal = true; $this->FetchPrincipal(); - } + $this->collection->is_principal = true; + $this->collection->type = 'principal'; + } else if ( $this->dav_name == '/' ) { $this->collection->dav_name = '/'; $this->collection->type = 'root'; @@ -418,16 +406,10 @@ EOSQL; if ( $qry->Exec('DAVResource',__LINE__,__FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { $this->collection = $row; $this->collection->exists = true; - $this->_is_binding = true; $this->collection->parent_set = $row->parent_container; $this->collection->parent_container = $row->bind_parent_container; - $this->bound_from = str_replace( $row->bound_to, $row->dav_name, $this->dav_name); $this->collection->bound_from = $row->dav_name; $this->collection->dav_name = $row->bound_to; - if ( isset($row->access_ticket_id) ) { - if ( !isset($this->tickets) ) $this->tickets = array(); - $this->tickets[] = new DAVTicket($row->access_ticket_id); - } if ( $row->is_calendar == 't' ) $this->collection->type = 'calendar'; else if ( $row->is_addressbook == 't' ) @@ -436,6 +418,13 @@ EOSQL; $this->collection->type = 'schedule-'. $matches[3]. 'box'; else $this->collection->type = 'collection'; + + $this->_is_binding = true; + $this->bound_from = str_replace( $row->bound_to, $row->dav_name, $this->dav_name); + if ( isset($row->access_ticket_id) ) { + if ( !isset($this->tickets) ) $this->tickets = array(); + $this->tickets[] = new DAVTicket($row->access_ticket_id); + } } else { dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name ); @@ -443,9 +432,63 @@ EOSQL; $this->collection->dav_name = preg_replace('{/[^/]*$}', '/', $this->dav_name); } } + + } + + /** + * Find the collection associated with this resource. + */ + function FetchCollection() { + global $session; + + /** + * RFC4918, 8.3: Identifiers for collections SHOULD end in '/' + * - also discussed at more length in 5.2 + * + * So we look for a collection which matches one of the following URLs: + * - The exact request. + * - If the exact request, doesn't end in '/', then the request URL with a '/' appended + * - The request URL truncated to the last '/' + * The collection URL for this request is therefore the longest row in the result, so we + * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1" + */ + dbg_error_log( 'DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name ); - @dbg_error_log( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type ); + // Try and pull the answer out of a hat + $cache = getCacheInstance(); + $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $this->dav_name); + $cache_key = 'dav_resource'.$session->user_no; + $this->collection = $cache->get( $cache_ns, $cache_key ); + if ( $this->collection === false ) { + $this->ReadCollectionFromDatabase(); + if ( $this->collection->type != 'principal' ) { + $cache_ns = 'collection-'.$this->collection->dav_name; + @dbg_error_log( 'Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type ); + $cache->set( $cache_ns, $cache_key, $this->collection ); + } + @dbg_error_log( 'DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type ); + } + else { + @dbg_error_log( 'Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type ); + if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches) + || preg_match( '#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches) ) { + $this->_is_principal = true; + $this->FetchPrincipal(); + $this->collection->is_principal = true; + $this->collection->type = 'principal'; + } + @dbg_error_log( 'DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type ); + } + if ( isset($this->collection->bound_from) ) { + $this->_is_binding = true; + $this->bound_from = str_replace( $this->collection->bound_to, $this->collection->bound_from, $this->dav_name); + if ( isset($this->collection->access_ticket_id) ) { + if ( !isset($this->tickets) ) $this->tickets = array(); + $this->tickets[] = new DAVTicket($this->collection->access_ticket_id); + } + } + $this->_is_collection = ( $this->_is_principal || $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name.'/' ); if ( $this->_is_collection ) { $this->dav_name = $this->collection->dav_name; diff --git a/inc/caldav-BIND.php b/inc/caldav-BIND.php index cb6363a3..fc82f15b 100644 --- a/inc/caldav-BIND.php +++ b/inc/caldav-BIND.php @@ -78,6 +78,12 @@ $params = array( $qry = new AwlQuery( $sql, $params ); if ( $qry->Exec('BIND',__LINE__,__FILE__) ) { header('Location: '. ConstructURL($destination_path) ); + + // Uncache anything to do with the target + $cache = getCacheInstance(); + $cache_ns = 'collection-'.$destination_path; + $cache->delete( $cache_ns, null ); + $request->DoResponse(201); } else { diff --git a/inc/caldav-DELETE.php b/inc/caldav-DELETE.php index 0f46e5d1..aee027f5 100644 --- a/inc/caldav-DELETE.php +++ b/inc/caldav-DELETE.php @@ -59,6 +59,9 @@ if ( $dav_resource->IsBinding() ) { } else if ( $dav_resource->IsCollection() ) { if ( delete_collection( $dav_resource->resource_id() ) && $qry->Commit() ) { + // Uncache anything to do with the collection + $cache = getCacheInstance(); + $cache->delete( 'collection-'.$dav_resource->dav_name(), null ); $request->DoResponse( 204 ); } } @@ -78,6 +81,8 @@ else { if ( function_exists('log_caldav_action') ) { log_caldav_action( 'DELETE', $dav_resource->GetProperty('uid'), $dav_resource->GetProperty('user_no'), $dav_resource->GetProperty('collection_id'), $request->path ); } + $cache = getCacheInstance(); + $cache->delete( 'collection-'.$dav_resource->parent_path(), null ); $request->DoResponse( 204 ); } } diff --git a/inc/caldav-MOVE.php b/inc/caldav-MOVE.php index 506879f0..e1bd49ac 100644 --- a/inc/caldav-MOVE.php +++ b/inc/caldav-MOVE.php @@ -108,8 +108,13 @@ $dst_collection = $dest->GetProperty('collection_id'); $src_user_no = $src->GetProperty('user_no'); $dst_user_no = $dest->GetProperty('user_no'); +$cache = getCacheInstance(); +$cachekeys = array(); if ( $src->IsCollection() ) { + $cachekeys[] = ($src->ContainerType() == 'principal' ? 'principal' : 'collection').'-'.$src->parent_path(); + $cachekeys[] = ($src->IsPrincipal() == 'principal' ? 'principal' : 'collection').'-'.$src->dav_name(); + $cachekeys[] = ($src->IsPrincipal() ? 'principal' : 'collection').'-'.$dest->dav_name(); if ( $dest->Exists() ) { $qry = new AwlQuery( 'DELETE FROM collection WHERE dav_name = :dst_name', array( ':dst_name' => $dst_name ) ); if ( !$qry->Exec('move') ) rollback(500); @@ -121,6 +126,11 @@ if ( $src->IsCollection() ) { $sql .= ', user_no = :dst_user_no '; $params[':dst_user_no'] = $dst_user_no; } + if ( $src->parent_path() != $dest->parent_path() ) { + $sql .= ', parent_container=:parent '; + $params[':parent'] = $dest->parent_path(); + $cachekeys[] = ($dest->ContainerType() == 'principal' ? 'principal' : 'collection').'-'.$dest->parent_path(); + } $sql .= 'WHERE collection_id = :src_collection'; $params[':src_collection'] = $src_collection; $qry = new AwlQuery( $sql, $params ); @@ -131,6 +141,8 @@ else { $qry = new AwlQuery( 'DELETE FROM caldav_data WHERE dav_name = :dst_name', array( ':dst_name' => $dst_name) ); if ( !$qry->Exec('move') ) rollback(500); } + $cachekeys[] = ($src->ContainerType() == 'principal' ? 'principal' : 'collection').'-'.$src->parent_path(); + $sql = 'UPDATE caldav_data SET dav_name = :dst_name'; $params = array( ':dst_name' => $dst_name ); if ( $src_user_no != $dst_user_no ) { @@ -140,6 +152,7 @@ else { if ( $src_collection != $dst_collection ) { $sql .= ', collection_id = :dst_collection'; $params[':dst_collection'] = $dst_collection; + $cachekeys[] = ($dest->ContainerType() == 'principal' ? 'principal' : 'collection').'-'.$dest->parent_path(); } $sql .=' WHERE dav_name = :src_name'; $params[':src_name'] = $src_name; @@ -170,4 +183,7 @@ else { $qry = new AwlQuery('COMMIT'); if ( !$qry->Exec('move') ) rollback(500); +// We need to delete from the cache *after* we commit the transaction :-) +foreach( $cachekeys AS $cache_ns ) $cache->delete( $cache_ns, null ); + $request->DoResponse( 200 ); diff --git a/inc/caldav-PROPPATCH.php b/inc/caldav-PROPPATCH.php index 690c2f00..fb82266b 100644 --- a/inc/caldav-PROPPATCH.php +++ b/inc/caldav-PROPPATCH.php @@ -10,7 +10,6 @@ */ dbg_error_log("PROPPATCH", "method handler"); -require_once('AWLCache.php'); require_once('iCalendar.php'); require_once('DAVResource.php'); diff --git a/inc/caldav-PUT-functions.php b/inc/caldav-PUT-functions.php index 5543f0bc..70eba6d5 100644 --- a/inc/caldav-PUT-functions.php +++ b/inc/caldav-PUT-functions.php @@ -15,7 +15,7 @@ * return true if it's a whole calendar */ -require_once('AWLCache.php'); +require_once('AwlCache.php'); require_once('iCalendar.php'); require_once('WritableCollection.php'); @@ -121,7 +121,7 @@ VALUES( :user_no, :parent_container, :dav_name, :dav_etag, :dav_displayname, TRU ); $qry->QDo( $sql, $params ); } - else if ( isset($public) ) { + else if ( isset($public) && $collection->is_public == ($public?'t':'f') ) { $collection = $qry->Fetch(); $sql = 'UPDATE collection SET publicly_readable = :is_public::boolean WHERE collection_id = :collection_id'; $params = array( ':is_public' => ($public?'t':'f'), ':collection_id' => $collection->collection_id ); @@ -129,6 +129,7 @@ VALUES( :user_no, :parent_container, :dav_name, :dav_etag, :dav_displayname, TRU rollback_on_error( $caldav_context, $user_no, $path ); } } + } } @@ -631,7 +632,7 @@ EOSQL; // Uncache anything to do with the collection $cache = getCacheInstance(); - $cache_ns = 'collection-'.preg_replace( '{/.*$}', '/', $path); + $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $path); $cache->delete( $cache_ns, null ); } @@ -995,7 +996,7 @@ EOSQL; // Uncache anything to do with the collection $cache = getCacheInstance(); - $cache_ns = 'collection-'.preg_replace( '{/.*$}', '/', $path); + $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $path); $cache->delete( $cache_ns, null ); dbg_error_log( 'PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path); diff --git a/testing/tests/binding/1000-BIND.test b/testing/tests/binding/1000-BIND.test index 1da2acf0..25a9aab3 100644 --- a/testing/tests/binding/1000-BIND.test +++ b/testing/tests/binding/1000-BIND.test @@ -12,6 +12,7 @@ SELECT ticket_id FROM access_ticket WHERE target_collection_id = 11 AND target_resource_id is null; ENDSQL +HEADER=X-DAViCal-Flush-Cache: true HEADER=User-Agent: Ticket Spec Tests HEADER=Content-Type: text/xml; charset="UTF-8" HEADER=Ticket: ##ticket## diff --git a/testing/tests/carddav/2000-MKCOL-addressbook.test b/testing/tests/carddav/2000-MKCOL-addressbook.test index 17d1b5cd..90438021 100644 --- a/testing/tests/carddav/2000-MKCOL-addressbook.test +++ b/testing/tests/carddav/2000-MKCOL-addressbook.test @@ -5,6 +5,8 @@ TYPE=MKCOL URL=http://mycaldav/caldav.php/user1/addressbook/ HEADER=User-Agent: DAViCalTester/public HEADER=Content-Type: text/xml +HEADER=X-DAViCal-Flush-Cache: true + HEAD diff --git a/testing/tests/scheduling/3001-POST-CANCEL.test b/testing/tests/scheduling/3001-POST-CANCEL.test index d1bf5f20..d1862dc5 100644 --- a/testing/tests/scheduling/3001-POST-CANCEL.test +++ b/testing/tests/scheduling/3001-POST-CANCEL.test @@ -3,6 +3,8 @@ # TYPE=POST URL=http://mycaldav/caldav.php/user1/.out/ + +HEADER=X-DAViCal-Flush-Cache: true HEADER=User-Agent: DAVKit/4.0.3 (732); CalendarStore/4.0.4 (997); iCal/4.0.4 (1395); Mac OS X/10.6.5 (10H574) HEADER=Recipient: mailto:user2@example.net HEADER=Content-Type: text/calendar