forked from extern/egroupware
basic managed attachment support, tested with iCal from OS X mountain lion
This commit is contained in:
parent
3bdc5577d8
commit
1752f7defd
@ -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;
|
||||
}
|
||||
|
@ -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']))
|
||||
{
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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,26 +1187,191 @@ class groupdav extends HTTP_WebDAV_Server
|
||||
$_GET['add-member'] = ''; // otherwise we give no Location header
|
||||
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).')');
|
||||
|
||||
$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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user