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
* @subpackage groupdav
* @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$
*/
@ -1019,8 +1019,7 @@ class calendar_groupdav extends groupdav_handler
{
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;
@ -1318,6 +1317,18 @@ class calendar_groupdav extends groupdav_handler
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
*
@ -1453,6 +1464,7 @@ class calendar_groupdav extends groupdav_handler
{
$handler = new calendar_ical();
$handler->setSupportedFields('GroupDAV',$this->agent);
$handler->supportedFields['attachments'] = true; // enabling attachments
if ($this->debug > 1) error_log("ical Handler called: " . $this->agent);
return $handler;
}

View File

@ -221,6 +221,7 @@ class calendar_ical extends calendar_boupdate
'RECURRENCE-ID' => 'recurrence',
'SEQUENCE' => 'etag',
'STATUS' => 'status',
'ATTACH' => 'attachments',
);
if (!is_array($this->supportedFields)) $this->setSupportedFields();
@ -789,6 +790,31 @@ class calendar_ical extends calendar_boupdate
}
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:
if (isset($this->clientProperties[$icalFieldName]['Size']))
{

View File

@ -7,7 +7,7 @@
* @package infolog
* @subpackage groupdav
* @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$
*/
@ -569,6 +569,18 @@ class infolog_groupdav extends groupdav_handler
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
*
@ -664,6 +676,19 @@ class infolog_groupdav extends groupdav_handler
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
*

View File

@ -7,7 +7,7 @@
* @package api
* @subpackage groupdav
* @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$
*/
@ -325,6 +325,8 @@ class groupdav extends HTTP_WebDAV_Server
$dav[] = 'calendarserver-principal-property-search';
// required by iOS & OS X iCal to show private checkbox (X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALENDAR)
$dav[] = 'calendarserver-private-events';
// managed attachments
$dav[] = 'calendar-managed-attachments';
// other capabilities calendarserver announces
//$dav[] = 'calendar-schedule';
//$dav[] = 'calendar-availability';
@ -1185,6 +1187,20 @@ class groupdav extends HTTP_WebDAV_Server
$_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options);
}
if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
$this->_parse_path($options['path'],$id,$app,$user);
if (($handler = self::app_handler($app)))
{
// 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']))
{
@ -1194,17 +1210,168 @@ class groupdav extends HTTP_WebDAV_Server
$options['content'] .= fread($options['stream'],8192);
}
}
if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
$this->_parse_path($options['path'],$id,$app,$user);
if (($handler = self::app_handler($app)) && method_exists($handler, 'post'))
{
return $handler->post($options,$id,$user);
}
}
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,
* 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)))
{
$status = $handler->put($options,$id,$user,$prefix);
// 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';
// check/handle Prefer: return-representation
if ($status[0] === '2')
{
$handler->check_return_representation($options, $id, $user);
}
return $status;
}
return '501 Not Implemented';
@ -1596,7 +1770,10 @@ class groupdav extends HTTP_WebDAV_Server
self::$log_level === 'f' || $this->debug)
{
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();
}
parent::ServeRequest();

View File

@ -7,7 +7,7 @@
* @package api
* @subpackage groupdav
* @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$
*/
@ -103,6 +103,13 @@ abstract class groupdav_handler
*/
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
*
@ -184,6 +191,17 @@ abstract class groupdav_handler
*/
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
*
@ -316,6 +334,49 @@ abstract class groupdav_handler
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
*
@ -498,6 +559,10 @@ abstract class groupdav_handler
if (is_null($etag)) $etag = $this->get_etag($entry);
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
if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name ||
// POST with add-member query parameter