basic managed attachment support, tested with iCal from OS X mountain lion

This commit is contained in:
Ralf Becker 2013-09-23 10:21:31 +00:00
parent 3bdc5577d8
commit 1752f7defd
5 changed files with 323 additions and 18 deletions

View File

@ -7,7 +7,7 @@
* @package calendar * @package calendar
* @subpackage groupdav * @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007-12 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2007-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -1019,8 +1019,7 @@ class calendar_groupdav extends groupdav_handler
{ {
if ($this->debug) error_log(__METHOD__."() importVCal($eventId) returned false"); if ($this->debug) error_log(__METHOD__."() importVCal($eventId) returned false");
} }
// we should not return an etag here, as we never store the ical byte-by-byte header('ETag: "'.$this->get_etag($eventId).'"');
//header('ETag: "'.$this->get_etag($eventId).'"');
} }
} }
return true; return true;
@ -1318,6 +1317,18 @@ class calendar_groupdav extends groupdav_handler
return $event; return $event;
} }
/**
* Update etag, ctag and sync-token to reflect changed attachments
*
* @param array|string|int $entry array with entry data from read, or id
*/
public function update_tags($entry)
{
if (!is_array($entry)) $entry = $this->read($entry);
$this->bo->update($entry, true);
}
/** /**
* Query ctag for calendar * Query ctag for calendar
* *
@ -1453,6 +1464,7 @@ class calendar_groupdav extends groupdav_handler
{ {
$handler = new calendar_ical(); $handler = new calendar_ical();
$handler->setSupportedFields('GroupDAV',$this->agent); $handler->setSupportedFields('GroupDAV',$this->agent);
$handler->supportedFields['attachments'] = true; // enabling attachments
if ($this->debug > 1) error_log("ical Handler called: " . $this->agent); if ($this->debug > 1) error_log("ical Handler called: " . $this->agent);
return $handler; return $handler;
} }

View File

@ -221,6 +221,7 @@ class calendar_ical extends calendar_boupdate
'RECURRENCE-ID' => 'recurrence', 'RECURRENCE-ID' => 'recurrence',
'SEQUENCE' => 'etag', 'SEQUENCE' => 'etag',
'STATUS' => 'status', 'STATUS' => 'status',
'ATTACH' => 'attachments',
); );
if (!is_array($this->supportedFields)) $this->setSupportedFields(); if (!is_array($this->supportedFields)) $this->setSupportedFields();
@ -789,6 +790,31 @@ class calendar_ical extends calendar_boupdate
} }
break; break;
case 'ATTACH':
static $url_prefix;
if (!isset($url_prefix))
{
$url_prefix = '';
if ($GLOBALS['egw_info']['server']['webserver_url'][0] == '/')
{
$url_prefix = ($_SERVER['HTTPS'] ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST'];
}
}
foreach(egw_vfs::find(egw_link::vfs_path('calendar', $event['id'], '', true), array(
'type' => 'F',
'need_mime' => true,
), true) as $path => $stat)
{
$attributes['ATTACH'][] = $url_prefix.egw::link(egw_vfs::download_url($path));
$parameters['ATTACH'][] = array(
'MANAGED-ID' => groupdav::path2managed_id($path),
'FMTTYP' => $stat['mime'],
'SIZE' => $stat['size'],
'FILENAME' => egw_vfs::basename($path),
);
}
break;
default: default:
if (isset($this->clientProperties[$icalFieldName]['Size'])) if (isset($this->clientProperties[$icalFieldName]['Size']))
{ {

View File

@ -7,7 +7,7 @@
* @package infolog * @package infolog
* @subpackage groupdav * @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007-12 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2007-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -569,6 +569,18 @@ class infolog_groupdav extends groupdav_handler
return $retval; return $retval;
} }
/**
* Update etag, ctag and sync-token to reflect changed attachments
*
* @param array|string|int $entry array with entry data from read, or id
*/
public function update_tags($entry)
{
if (!is_array($entry)) $entry = $this->read($entry);
$this->bo->write($entry, true);
}
/** /**
* Callback for infolog_ical::importVTODO to implement infolog-cat-action * Callback for infolog_ical::importVTODO to implement infolog-cat-action
* *
@ -664,6 +676,19 @@ class infolog_groupdav extends groupdav_handler
return $this->bo->read(array(self::$path_attr => $id, "info_status!='deleted'"),false,'server'); return $this->bo->read(array(self::$path_attr => $id, "info_status!='deleted'"),false,'server');
} }
/**
* Get id from entry-array returned by read()
*
* Reimplemented because id uses key 'info_id'
*
* @param int|string|array $entry
* @return int|string
*/
function get_id($entry)
{
return is_array($entry) ? $entry['info_id'] : $entry;
}
/** /**
* Check if user has the neccessary rights on a task / infolog entry * Check if user has the neccessary rights on a task / infolog entry
* *

View File

@ -7,7 +7,7 @@
* @package api * @package api
* @subpackage groupdav * @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007-12 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2007-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -325,6 +325,8 @@ class groupdav extends HTTP_WebDAV_Server
$dav[] = 'calendarserver-principal-property-search'; $dav[] = 'calendarserver-principal-property-search';
// required by iOS & OS X iCal to show private checkbox (X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALENDAR) // required by iOS & OS X iCal to show private checkbox (X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALENDAR)
$dav[] = 'calendarserver-private-events'; $dav[] = 'calendarserver-private-events';
// managed attachments
$dav[] = 'calendar-managed-attachments';
// other capabilities calendarserver announces // other capabilities calendarserver announces
//$dav[] = 'calendar-schedule'; //$dav[] = 'calendar-schedule';
//$dav[] = 'calendar-availability'; //$dav[] = 'calendar-availability';
@ -1185,26 +1187,191 @@ class groupdav extends HTTP_WebDAV_Server
$_GET['add-member'] = ''; // otherwise we give no Location header $_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options); return $this->PUT($options);
} }
// read the content in a string, if a stream is given
if (isset($options['stream']))
{
$options['content'] = '';
while(!feof($options['stream']))
{
$options['content'] .= fread($options['stream'],8192);
}
}
if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
$this->_parse_path($options['path'],$id,$app,$user); $this->_parse_path($options['path'],$id,$app,$user);
if (($handler = self::app_handler($app)) && method_exists($handler, 'post')) if (($handler = self::app_handler($app)))
{ {
return $handler->post($options,$id,$user); // managed attachments
if (isset($_GET['action']) && substr($_GET['action'], 0, 11) === 'attachment-')
{
return $this->managed_attachements($options, $id, $handler, $_GET['action']);
}
if (method_exists($handler, 'post'))
{
// read the content in a string, if a stream is given
if (isset($options['stream']))
{
$options['content'] = '';
while(!feof($options['stream']))
{
$options['content'] .= fread($options['stream'],8192);
}
}
return $handler->post($options,$id,$user);
}
} }
return '501 Not Implemented'; return '501 Not Implemented';
} }
/**
* HTTP header containing managed id
*/
const MANAGED_ID_HEADER = 'Cal-Managed-ID';
/**
* Add, update or remove attachments
*
* @param array &$options
* @param string|int $id
* @param groupdav_handler $handler
* @param string $action 'attachment-add', 'attachment-update', 'attachment-remove'
* @return string http status
*
* @todo support for rid parameter
* @todo managed-id does NOT change on update
* @todo updates of attachments through vfs need to call $handler->update_tags($id) too
* @todo stripping attachments added via PUT direct and make them managed ones (urls are NOT yet supported too)
* @todo update of attachments via PUT on calendar resource (not sure if I want delete), allows to re-user managed-ids ...
*/
protected function managed_attachements(&$options, $id, groupdav_handler $handler, $action)
{
error_log(__METHOD__."(path=$options[path], id=$id, ..., action=$action) _GET=".array2string($_GET));
$entry = $handler->_common_get_put_delete('GET', $options, $id);
if (!is_array($entry))
{
return $entry ? $entry : "404 Not found";
}
if (!egw_link::file_access($handler->app, $entry['id'], EGW_ACL_EDIT))
{
return '403 Forbidden';
}
switch($action)
{
case 'attachment-add':
if (isset($this->_SERVER['HTTP_CONTENT_DISPOSITION']) &&
substr($this->_SERVER['HTTP_CONTENT_DISPOSITION'], 0, 10) === 'attachment' &&
preg_match('/filename="?([^";]+)/', $this->_SERVER['HTTP_CONTENT_DISPOSITION'], $matches))
{
$filename = $matches[1];
$parts = explode('.', $filename);
$ext = '.'.array_pop($parts);
$filename = implode('.', $parts);
}
else
{
$filename = 'attachment';
if (isset($options['content_type']) && ($ext = mime_magic::mime2ext($options['content_type'])))
{
$ext = '.'.$ext;
}
else
{
$ext = '';
}
}
for($i = 1; $i < 100; ++$i)
{
$path = egw_link::vfs_path($handler->app, $handler->get_id($entry), $filename.($i > 1 ? '-'.$i : '').$ext, true);
if (!egw_vfs::stat($path)) break;
}
if (!($to = egw_vfs::fopen($path, 'w')) ||
isset($options['stream']) && ($copied=stream_copy_to_stream($options['stream'], $to)) === false ||
isset($options['content']) && ($copied=fwrite($to, $options['content'])) === false)
{
return '403 Forbidden';
}
fclose($to);
error_log(__METHOD__."() content-type=$options[content_type], filename=$filename, ext=$ext: $path created $copied bytes copied");
$ret = '201 Created';
header(self::MANAGED_ID_HEADER.': '.self::path2managed_id($path));
break;
case 'attachment-remove':
case 'attachment-update':
if (empty($_GET['managed-id']) || !($path = self::managed_id2path($_GET['managed-id'], $app, $id)))
{
return '404 Not found';
}
if ($action == 'attachment-remove')
{
if (!egw_vfs::unlink($path))
{
return '403 Forbidden';
}
$ret = '204 No content';
}
else
{
if (!($to = egw_vfs::fopen($path, 'w')) ||
isset($options['stream']) && ($copied=stream_copy_to_stream($options['stream'], $to)) === false ||
isset($options['content']) && ($copied=fwrite($to, $options['content'])) === false)
{
return '403 Forbidden';
}
fclose($to);
error_log(__METHOD__."() content-type=$options[content_type], filename=$filename: $path updated $copied bytes copied");
$ret = '200 Ok';
header(self::MANAGED_ID_HEADER.': '.self::path2managed_id($path));
}
break;
default:
return '501 Unknown action parameter '.$action;
}
// update etag/ctag/sync-token by updating modification time
$handler->update_tags($entry);
// check/handle Prefer: return-representation
$handler->check_return_representation($options, $id, $user);
return $ret;
}
/**
* Return managed-id of a vfs-path
*
* @param string $path "/apps/$app/$id/something"
* @return string
*/
static public function path2managed_id($path)
{
return base64_encode($path);
}
/**
* Return vfs-path of a managed-id
*
* @param string $managed_id
* @param string $app=null app-name to check against path
* @param string|int $id=null id to check agains path
* @return string|boolean "/apps/$app/$id/something" or false if not found or not belonging to given $app/$id
*/
static public function managed_id2path($managed_id, $app=null, $id=null)
{
$path = base64_decode($managed_id);
if (!$path || substr($path, 0, 6) != '/apps/' || !egw_vfs::stat($path))
{
$path = false;
}
elseif (!empty($app) && !empty($id))
{
list(,,$a,$i) = explode('/', $path);
if ($a !== $app || $i !== (string)$id)
{
$path = false;
}
}
error_log(__METHOD__."('$managed_id', $app, $id) base64_decode('$managed_id')=".array2string(base64_decode($managed_id)).' returning '.array2string($path));
return $path;
}
/** /**
* Namespaces which need to be eplicitly named in self::$proppatch_props, * Namespaces which need to be eplicitly named in self::$proppatch_props,
* because we consider them protected, if not explicitly named * because we consider them protected, if not explicitly named
@ -1313,8 +1480,15 @@ class groupdav extends HTTP_WebDAV_Server
if (($handler = self::app_handler($app))) if (($handler = self::app_handler($app)))
{ {
$status = $handler->put($options,$id,$user,$prefix); $status = $handler->put($options,$id,$user,$prefix);
// set default stati: true --> 204 No Content, false --> should be already handled // set default stati: true --> 204 No Content, false --> should be already handled
if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong'; if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong';
// check/handle Prefer: return-representation
if ($status[0] === '2')
{
$handler->check_return_representation($options, $id, $user);
}
return $status; return $status;
} }
return '501 Not Implemented'; return '501 Not Implemented';
@ -1596,7 +1770,10 @@ class groupdav extends HTTP_WebDAV_Server
self::$log_level === 'f' || $this->debug) self::$log_level === 'f' || $this->debug)
{ {
self::$request_starttime = microtime(true); self::$request_starttime = microtime(true);
$this->store_request = true; // do NOT log non-text attachments
$this->store_request = $_SERVER['REQUEST_METHOD'] != 'POST' || !isset($_GET['action']) ||
!in_array($_GET['action'], array('attachment-add', 'attachment-update')) ||
substr($_SERVER['CONTENT_TYPE'], 0, 5) == 'text/';
ob_start(); ob_start();
} }
parent::ServeRequest(); parent::ServeRequest();

View File

@ -7,7 +7,7 @@
* @package api * @package api
* @subpackage groupdav * @subpackage groupdav
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2007-12 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2007-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -103,6 +103,13 @@ abstract class groupdav_handler
*/ */
static $path_attr = 'id'; static $path_attr = 'id';
/**
* New id of put/post stored here by put_response_headers for check_return_representation
*
* @var string
*/
var $new_id;
/** /**
* Constructor * Constructor
* *
@ -184,6 +191,17 @@ abstract class groupdav_handler
*/ */
abstract function read($id /*,$path=null*/); abstract function read($id /*,$path=null*/);
/**
* Get id from entry-array returned by read()
*
* @param int|string|array $entry
* @return int|string
*/
function get_id($entry)
{
return is_array($entry) ? $entry['id'] : $entry;
}
/** /**
* Check if user has the neccessary rights on an entry * Check if user has the neccessary rights on an entry
* *
@ -316,6 +334,49 @@ abstract class groupdav_handler
return $entry; return $entry;
} }
/**
* Return representation, if requested by HTTP Prefer header
*
* @param array $options
* @param int $id
* @param int $user=null account_id
* @return string|boolean http status of get or null if no representation was requested
*/
public function check_return_representation($options, $id, $user=null)
{
if (isset($_SERVER['HTTP_PREFER']) && in_array('return-representation', explode(',', $_SERVER['HTTP_PREFER'])))
{
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
$location = $this->groupdav->base_uri.$options['path'];
if ($location[0] == '/')
{
$location = (@$_SERVER['HTTPS'] === 'on' ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST'].$location;
}
header('Content-Location: '.$location);
}
if (($ret = $this->get($options, $id ? $id : $this->new_id, $user)) && !empty($options['data']))
{
header('Content-Length: '.$this->groupdav->bytes($options['data']));
header('Content-Type: '.$options['mimetype']);
echo $options['data'];
}
}
return $ret;
}
/**
* Update etag, ctag and sync-token to reflect changed attachments
*
* Not abstract, as not need to implement for apps not supporting managed attachments
*
* @param array|string|int $entry array with entry data from read, or id
*/
public function update_tags($entry)
{
}
/** /**
* Get the handler for the given app * Get the handler for the given app
* *
@ -498,6 +559,10 @@ abstract class groupdav_handler
if (is_null($etag)) $etag = $this->get_etag($entry); if (is_null($etag)) $etag = $this->get_etag($entry);
header('ETag: "'.$etag.'"'); header('ETag: "'.$etag.'"');
} }
// store (new) id for check_return_representation
$this->new_id = $this->get_path($entry);
// send Location header only on success AND if we dont use caldav_name as path-attribute or // send Location header only on success AND if we dont use caldav_name as path-attribute or
if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name || if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name ||
// POST with add-member query parameter // POST with add-member query parameter