implement and document PATCH

This commit is contained in:
Ralf Becker 2021-09-25 12:20:31 +02:00
parent 392b8036f4
commit 3e035a70a4
6 changed files with 99 additions and 18 deletions

View File

@ -629,13 +629,15 @@ class addressbook_groupdav extends Api\CalDAV\Handler
* @param int $id * @param int $id
* @param int $user =null account_id of owner, default null * @param int $user =null account_id of owner, default null
* @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook) * @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook)
* @param string $method='PUT' also called for POST and PATCH
* @param string $content_type=null
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/ */
function put(&$options,$id,$user=null,$prefix=null) function put(&$options, $id, $user=null, $prefix=null, string $method='PUT', string $content_type=null)
{ {
if ($this->debug) error_log(__METHOD__.'('.array2string($options).",$id,$user)"); if ($this->debug) error_log(__METHOD__.'('.array2string($options).",$id,$user)");
$oldContact = $this->_common_get_put_delete('PUT',$options,$id); $oldContact = $this->_common_get_put_delete($method,$options,$id);
if (!is_null($oldContact) && !is_array($oldContact)) if (!is_null($oldContact) && !is_array($oldContact))
{ {
if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($oldContact)); if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($oldContact));
@ -658,7 +660,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
} }
} }
$contact = $type === JsContact::MIME_TYPE_JSCARD ? $contact = $type === JsContact::MIME_TYPE_JSCARD ?
JsContact::parseJsCard($options['content'], $oldContact ?: []) : JsContact::parseJsCardGroup($options['content']); JsContact::parseJsCard($options['content'], $oldContact ?: [], $content_type, $method) :
JsContact::parseJsCardGroup($options['content']);
if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0) if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0)
{ {

View File

@ -993,6 +993,34 @@ class CalDAV extends HTTP_WebDAV_Server
parent::http_PROPFIND('REPORT'); parent::http_PROPFIND('REPORT');
} }
/**
* REST API PATCH handler
*
* Currently, only implemented for REST not CalDAV/CardDAV
*
* @param $options
* @param $files
* @return string|void
*/
function PATCH(array &$options)
{
if (!preg_match('#^application/([^; +]+\+)?json#', $_SERVER['HTTP_CONTENT_TYPE']))
{
return '501 Not implemented';
}
return $this->PUT($options, 'PATCH');
}
/**
* REST API PATCH handler
*
* Just calls http_PUT()
*/
function http_PATCH()
{
return parent::http_PUT('PATCH');
}
/** /**
* Check if client want or sends JSON * Check if client want or sends JSON
* *
@ -1003,7 +1031,7 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
if (!isset($type)) if (!isset($type))
{ {
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ? $type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PATCH', 'PROPPATCH']) ?
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT']; $_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
} }
return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ? return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ?
@ -1427,7 +1455,7 @@ class CalDAV extends HTTP_WebDAV_Server
substr($options['path'], -1) === '/' && self::isJSON()) substr($options['path'], -1) === '/' && self::isJSON())
{ {
$_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, 'POST');
} }
if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
@ -1915,7 +1943,7 @@ class CalDAV extends HTTP_WebDAV_Server
* @param array parameter passing array * @param array parameter passing array
* @return bool true on success * @return bool true on success
*/ */
function PUT(&$options) function PUT(&$options, $method='PUT')
{ {
// read the content in a string, if a stream is given // read the content in a string, if a stream is given
if (isset($options['stream'])) if (isset($options['stream']))
@ -1934,9 +1962,14 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
return '404 Not Found'; return '404 Not Found';
} }
// REST API & PATCH only implemented for addressbook currently
if ($app !== 'addressbook' && $method === 'PATCH')
{
return '501 Not implemented';
}
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, $method, $_SERVER['HTTP_CONTENT_TYPE']);
// 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';

View File

@ -60,6 +60,7 @@ abstract class Handler
var $method2acl = array( var $method2acl = array(
'GET' => Api\Acl::READ, 'GET' => Api\Acl::READ,
'PUT' => Api\Acl::EDIT, 'PUT' => Api\Acl::EDIT,
'PATCH' => Api\Acl::EDIT,
'DELETE' => Api\Acl::DELETE, 'DELETE' => Api\Acl::DELETE,
); );
/** /**

View File

@ -78,26 +78,32 @@ class JsContact
/** /**
* Parse JsCard * Parse JsCard
* *
* We use strict parsing for "application/jscontact+json" content-type, not for "application/json".
* Strict parsing checks objects for proper @type attributes and value attributes, non-strict allows scalar values.
*
* Non-strict parsing also automatic detects patch for POST requests.
*
* @param string $json * @param string $json
* @param array $old=[] existing contact * @param array $old=[] existing contact for patch
* @param bool $strict true: check if objects have their proper @type attribute * @param ?string $content_type=null application/json no strict parsing and automatic patch detection, if method not 'PATCH' or 'PUT'
* @param string $method='PUT' 'PUT', 'POST' or 'PATCH'
* @return array * @return array
*/ */
public static function parseJsCard(string $json, array $old=[], bool $strict=true) public static function parseJsCard(string $json, array $old=[], string $content_type=null, $method='PUT')
{ {
try try
{ {
$strict = !isset($content_type) || !preg_match('#^application/json#', $content_type);
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR); $data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
// check if we have a patch: keys contain slashes // check if we use patch: method is PATCH or method is POST AND keys contain slashes
if (array_filter(array_keys($data), static function ($key) if ($method === 'PATCH' || !$strict && $method === 'POST' && array_filter(array_keys($data), static function ($key)
{ {
return strpos($key, '/') !== false; return strpos($key, '/') !== false;
})) }))
{ {
// apply patch on JsCard of contact // apply patch on JsCard of contact
$data = self::patch($data, $old ? self::getJsCard($old, false) : [], !$old); $data = self::patch($data, $old ? self::getJsCard($old, false) : [], !$old);
$strict = false;
} }
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
@ -951,7 +957,7 @@ class JsContact
{ {
throw new \InvalidArgumentException("Invalid email object (requires email attribute): ".json_encode($value, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid email object (requires email attribute): ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
if (!isset($contact['email']) && $id === 'work' && empty($value['context']['private'])) if (!isset($contact['email']) && ($id === 'work' || empty($value['contexts']['private']) || isset($contact['email_home'])))
{ {
$contact['email'] = $value['email']; $contact['email'] = $value['email'];
} }

View File

@ -1701,10 +1701,10 @@ class HTTP_WebDAV_Server
/** /**
* PUT method handler * PUT method handler
* *
* @param void * @param string $method='PUT'
* @return void * @return void
*/ */
function http_PUT() function http_PUT(string $method='PUT')
{ {
if ($this->_check_lock_status($this->path)) { if ($this->_check_lock_status($this->path)) {
$options = Array(); $options = Array();
@ -1839,7 +1839,7 @@ class HTTP_WebDAV_Server
} }
} }
$stat = $this->PUT($options); $stat = $this->$method($options);
if ($stat === false) { if ($stat === false) {
$stat = "403 Forbidden"; $stat = "403 Forbidden";

View File

@ -276,7 +276,37 @@ Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/123
``` ```
</details> </details>
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources * **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
<details>
<summary>Example: PATCH request to modify a contact with partial data</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
{
"name": [
{
"@type": "NameComponent",
"type": "personal",
"value": "Testfirst"
},
{
"@type": "NameComponent",
"type": "surname",
"value": "Username"
}
],
"fullName": "Testfirst Username",
"organizations/org/name": "Test-User.org",
"emails/work/email": "test.user@test-user.org"
}
EOF
HTTP/1.1 204 No content
```
</details>
* **DELETE** requests delete single resources * **DELETE** requests delete single resources
@ -289,3 +319,11 @@ use ```<domain-name>:<name>``` like in JsCalendar
* top-level objects need a ```@type``` attribute with one of the following values: * top-level objects need a ```@type``` attribute with one of the following values:
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```, ```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation``` ```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
### ToDos
- [x] Addressbook
- [ ] update of photos, keys, attachments
- [ ] InfoLog
- [ ] Calendar
- [ ] relatedTo / links
- [ ] storing not native supported attributes eg. localization