From 6331c50003d6176f06f2bfbe7763173999ea95cb Mon Sep 17 00:00:00 2001 From: Rob Ostensen Date: Mon, 27 Jun 2011 18:39:40 -0500 Subject: [PATCH] initial support for remote url BINDing --- config/example-config.php | 10 ++++ inc/DAVResource.php | 28 ++++++++- inc/caldav-BIND.php | 123 +++++++++++++++++++++++++------------- inc/caldav-GET.php | 3 + inc/caldav-REPORT.php | 10 ++++ inc/external-fetch.php | 73 ++++++++++++++++++++++ 6 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 inc/external-fetch.php diff --git a/config/example-config.php b/config/example-config.php index 22fb8e97..f0c2e851 100644 --- a/config/example-config.php +++ b/config/example-config.php @@ -188,6 +188,16 @@ $c->collections_always_exist = false; $c->schedule_private_key = 'PRIVATE-KEY-BASE-64-DATA'; */ +/* +* External subscription (BIND) minimum refresh interval +* Required if you want to enable remote binding ( webcal subscriptions ) +* Default: none +*/ +/* +$c->external_refresh = 60; +*/ + + /*************************************************************************** diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 2b445b72..7c7834d4 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -97,6 +97,11 @@ class DAVResource */ private $_is_binding; + /** + * @var True if this resource is a binding to an external resource + */ + private $_is_external; + /** * @var True if this resource is an addressbook collection */ @@ -158,6 +163,7 @@ class DAVResource $this->_is_principal = false; $this->_is_calendar = false; $this->_is_binding = false; + $this->_is_external = false; $this->_is_addressbook = false; $this->_is_proxy_request = false; if ( isset($parameters) && is_object($parameters) ) { @@ -400,7 +406,8 @@ EOSQL; SELECT collection.*, path_privs(:session_principal::int8, collection.dav_name,:scan_depth::int), p.principal_id, p.type_id AS principal_type_id, p.displayname AS principal_displayname, p.default_privileges AS principal_default_privileges, time_zone.tz_spec, dav_binding.access_ticket_id, dav_binding.parent_container AS bind_parent_container, - dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to + dav_binding.dav_displayname, owner.dav_name AS bind_owner_url, dav_binding.dav_name AS bound_to, + dav_binding.external_url AS external_url, dav_binding.type AS external_type, dav_binding.bind_id AS bind_id FROM dav_binding LEFT JOIN collection ON (collection.collection_id=bound_source_id) LEFT JOIN principal p USING (user_no) @@ -431,7 +438,16 @@ EOSQL; $this->collection->type = 'schedule-'. $matches[3]. 'box'; else $this->collection->type = 'collection'; - + if ( strlen($row->external_url) > 8 ) { + $this->collection->bound_from = $row->external_url; + $this->_is_external = true; + if ( $row->external_type == 'calendar' ) + $this->collection->type = 'calendar'; + else if ( $row->external_type == 'addressbook' ) + $this->collection->type = 'addressbook'; + 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) ) { @@ -1047,6 +1063,14 @@ EOQRY; } + /** + * Checks whether this resource is a bind to an external resource + */ + function IsExternal() { + return $this->_is_external; + } + + /** * Checks whether this resource actually exists, in the virtual sense, within the hierarchy */ diff --git a/inc/caldav-BIND.php b/inc/caldav-BIND.php index fc82f15b..674451bd 100644 --- a/inc/caldav-BIND.php +++ b/inc/caldav-BIND.php @@ -47,45 +47,88 @@ if ( $destination->Exists() ) { $request->PreconditionFailed(403,'DAV::can-overwrite',translate('A resource already exists at the destination.')); } -$source = new DAVResource( $href ); -if ( !$source->Exists() ) { - $request->PreconditionFailed(403,'DAV::bind-source-exists',translate('The BIND Request MUST identify an existing resource.')); -} +if ( preg_match ( '{^https?://[A-Za-z][^/]*/.+$}', $href ) ) { + require_once('external-fetch.php'); + $qry = new AwlQuery( ); + $qry->QDo('SELECT collection_id FROM collection WHERE dav_name = :dav_name ', array( ':dav_name' => '/.external/'. md5($href) )); + if ( $qry->rows() == 1 && ($row = $qry->Fetch()) ) { + $dav_id = $row->dav_id; + } + else { + create_external ( '/.external/'. md5($href) ,true,false ); + $qry->QDo('SELECT collection_id FROM collection WHERE dav_name = :dav_name ', array( ':dav_name' => '/.external/'. md5($href) )); + if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) + $request->DoResponse(500,translate('Database Error1')); + $dav_id = $row->collection_id; + } -if ( $source->IsPrincipal() || !$source->IsCollection() ) { - $request->PreconditionFailed(403,'DAV::binding-allowed',translate('DAViCal only allows BIND requests for collections at present.')); -} - -/* - bind_id INT8 DEFAULT nextval('dav_id_seq') PRIMARY KEY, - bound_source_id INT8 REFERENCES collection(collection_id) ON UPDATE CASCADE ON DELETE CASCADE, - access_ticket_id TEXT REFERENCES access_ticket(ticket_id) ON UPDATE CASCADE ON DELETE SET NULL, - parent_container TEXT NOT NULL, - dav_name TEXT UNIQUE NOT NULL, - dav_displayname TEXT -*/ - -$sql = 'INSERT INTO dav_binding ( bound_source_id, access_ticket_id, dav_owner_id, parent_container, dav_name, dav_displayname ) -VALUES( :target_id, :ticket_id, :session_principal, :parent_container, :dav_name, :displayname )'; -$params = array( - ':target_id' => $source->GetProperty('collection_id'), - ':ticket_id' => (isset($request->ticket) ? $request->ticket->id() : null), - ':parent_container' => $parent->dav_name(), - ':session_principal' => $session->principal_id, - ':dav_name' => $destination_path, - ':displayname' => $source->GetProperty('displayname') -); -$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); -} + $sql = 'INSERT INTO dav_binding ( bound_source_id, access_ticket_id, dav_owner_id, parent_container, dav_name, dav_displayname, external_url, type ) + VALUES( :target_id, :ticket_id, :session_principal, :parent_container, :dav_name, :displayname, :external_url, :external_type )'; + $params = array( + ':target_id' => $dav_id, + ':ticket_id' => null, + ':parent_container' => $parent->dav_name(), + ':session_principal' => $session->principal_id, + ':dav_name' => $destination_path, + ':displayname' => $segment, + ':external_url' => $href, + ':external_type' => 'calendar' + ); + $qry = new AwlQuery( $sql, $params ); + if ( $qry->Exec('BIND',__LINE__,__FILE__) ) { + $qry = new AwlQuery( 'SELECT bind_id from dav_binding where dav_name = :dav_name', array( ':dav_name' => $destination_path ) ); + if ( ! $qry->Exec('BIND',__LINE__,__FILE__) || $qry->rows() != 1 || !($row = $qry->Fetch()) ) + $request->DoResponse(500,translate('Database Error1')); + fetch_external ( $row->bind_id, '' ); + $request->DoResponse(201); + } + else { + $request->DoResponse(500,translate('Database Error2')); + } +} else { - $request->DoResponse(500,translate('Database Error')); -} \ No newline at end of file + $source = new DAVResource( $href ); + if ( !$source->Exists() ) { + $request->PreconditionFailed(403,'DAV::bind-source-exists',translate('The BIND Request MUST identify an existing resource.')); + } + + if ( $source->IsPrincipal() || !$source->IsCollection() ) { + $request->PreconditionFailed(403,'DAV::binding-allowed',translate('DAViCal only allows BIND requests for collections at present.')); + } + + /* + bind_id INT8 DEFAULT nextval('dav_id_seq') PRIMARY KEY, + bound_source_id INT8 REFERENCES collection(collection_id) ON UPDATE CASCADE ON DELETE CASCADE, + access_ticket_id TEXT REFERENCES access_ticket(ticket_id) ON UPDATE CASCADE ON DELETE SET NULL, + parent_container TEXT NOT NULL, + dav_name TEXT UNIQUE NOT NULL, + dav_displayname TEXT, + external_url TEXT, + type TEXT + */ + + $sql = 'INSERT INTO dav_binding ( bound_source_id, access_ticket_id, dav_owner_id, parent_container, dav_name, dav_displayname ) + VALUES( :target_id, :ticket_id, :session_principal, :parent_container, :dav_name, :displayname )'; + $params = array( + ':target_id' => $source->GetProperty('collection_id'), + ':ticket_id' => (isset($request->ticket) ? $request->ticket->id() : null), + ':parent_container' => $parent->dav_name(), + ':session_principal' => $session->principal_id, + ':dav_name' => $destination_path, + ':displayname' => $source->GetProperty('displayname') + ); + $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 { + $request->DoResponse(500,translate('Database Error')); + } +} diff --git a/inc/caldav-GET.php b/inc/caldav-GET.php index 685d2604..d6885b18 100644 --- a/inc/caldav-GET.php +++ b/inc/caldav-GET.php @@ -15,6 +15,9 @@ require_once("DAVResource.php"); $dav_resource = new DAVResource($request->path); $dav_resource->NeedPrivilege( array('urn:ietf:params:xml:ns:caldav:read-free-busy','DAV::read') ); +if ( $dav_resource->IsExternal() ) { + update_external ( $dav_resource ); +} if ( ! $dav_resource->Exists() ) { $request->DoResponse( 404, translate("Resource Not Found.") ); diff --git a/inc/caldav-REPORT.php b/inc/caldav-REPORT.php index e2a6d633..c2d77b2f 100644 --- a/inc/caldav-REPORT.php +++ b/inc/caldav-REPORT.php @@ -61,9 +61,15 @@ switch( $xmltree->GetTag() ) { include("caldav-REPORT-pps-set.php"); exit; // Not that it should return anyway. case 'DAV::sync-collection': + require_once("external-fetch.php"); + if ( $target->IsExternal() ) + update_external ( $target ); include("caldav-REPORT-sync-collection.php"); exit; // Not that it should return anyway. case 'DAV::expand-property': + require_once("external-fetch.php"); + if ( $target->IsExternal() ) + update_external ( $target ); include("caldav-REPORT-expand-property.php"); exit; // Not that it should return anyway. case 'DAV::principal-match': @@ -313,6 +319,10 @@ function component_to_xml( $properties, $item ) { return $response; } +require_once("external-fetch.php"); +if ( $target->IsExternal() ) + update_external ( $target ); + if ( $xmltree->GetTag() == "urn:ietf:params:xml:ns:caldav:calendar-query" ) { $calquery = $xmltree->GetPath("/urn:ietf:params:xml:ns:caldav:calendar-query/*"); include("caldav-REPORT-calquery.php"); diff --git a/inc/external-fetch.php b/inc/external-fetch.php new file mode 100644 index 00000000..731f0089 --- /dev/null +++ b/inc/external-fetch.php @@ -0,0 +1,73 @@ + +* @copyright Rob Ostensen +* @license http://gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +function create_external ( $path,$is_calendar,$is_addressbook ) +{ + global $request; + $resourcetypes = ''; + if ($is_calendar) $resourcetypes .= ''; + $qry = new AwlQuery(); + if ( ! $qry->QDo( 'INSERT INTO collection ( user_no, parent_container, dav_name, dav_etag, dav_displayname, + is_calendar, is_addressbook, resourcetypes, created, modified ) + VALUES( :user_no, :parent_container, :dav_name, :dav_etag, :dav_displayname, + :is_calendar, :is_addressbook, :resourcetypes, current_timestamp, current_timestamp )', + array( + ':user_no' => $request->user_no, + ':parent_container' => '/.external/', + ':dav_name' => $path, + ':dav_etag' => md5($request->user_no. $path), + ':dav_displayname' => $path, + ':is_calendar' => ($is_calendar ? 't' : 'f'), + ':is_addressbook' => ($is_addressbook ? 't' : 'f'), + ':resourcetypes' => $resourcetypes + ) ) ) { + $request->DoResponse( 500, translate('Error writing calendar details to database.') ); + } +} + +function fetch_external ( $bind_id, $min_age ) +{ + $sql = 'SELECT collection.*, collection.dav_name as path, dav_binding.external_url as external_url FROM dav_binding LEFT JOIN collection ON (collection.collection_id=bound_source_id) WHERE bind_id = :bind_id'; + $params = array( ':bind_id' => $bind_id ); + if ( strlen ( $min_age ) > 2 ) { + $sql .= ' and collection.modified + interval :interval > NOW()'; + $params[':interval'] = $min_age; + } + $qry = new AwlQuery( $sql, $params ); + if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $row = $qry->Fetch() ) { + $curl = curl_init ( $row->external_url ); + curl_setopt ( $curl, CURLOPT_RETURNTRANSFER, true ); + $ics = curl_exec ( $curl ); + curl_close ( $curl ); + if ( is_string ( $ics ) && strlen ( $ics ) > 20 ) { + $qry = new AwlQuery( 'UPDATE collection SET modified=NOW() where collection_id = :cid', array ( ':cid' => $row->collection_id ) ); + $qry->Exec('DAVResource'); + require_once ( 'caldav-PUT-functions.php'); + import_collection ( $ics , $row->user_no, $row->path, 'External Fetch' , false ) ; + return true; + } + } + return false; +} + +function update_external ( $request ) +{ + global $c; + if ( $c->external_refresh < 1 ) + return ; + $sql = 'SELECT bind_id from dav_binding LEFT JOIN collection ON (collection.collection_id=bound_source_id) collection where dav_name = :dav_name and collection.modified + interval :interval < NOW()'; + $qry = new AwlQuery( $sql, array ( ':dav_name' => $request->dav_name, ':interval' => $c->external_refresh + ' minutes' ) ); + if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $row = $qry->Fetch() ) { + if ( $row->bind_id != 0 ) + fetch_external ( $row->bind_id ); + } +}