From 3e035a70a40daa590279080bc71de81269fe7e1a Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 25 Sep 2021 12:20:31 +0200 Subject: [PATCH] implement and document PATCH --- .../inc/class.addressbook_groupdav.inc.php | 9 ++-- api/src/CalDAV.php | 41 +++++++++++++++++-- api/src/CalDAV/Handler.php | 1 + api/src/Contacts/JsContact.php | 20 +++++---- api/src/WebDAV/Server.php | 6 +-- doc/REST-CalDAV-CardDAV/README.md | 40 +++++++++++++++++- 6 files changed, 99 insertions(+), 18 deletions(-) diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index 56dff82837..9bd7c7dec2 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -629,13 +629,15 @@ class addressbook_groupdav extends Api\CalDAV\Handler * @param int $id * @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 $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') */ - 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)"); - $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 ($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 ? - 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) { diff --git a/api/src/CalDAV.php b/api/src/CalDAV.php index 5a0f0dc62d..2130dbbcd1 100644 --- a/api/src/CalDAV.php +++ b/api/src/CalDAV.php @@ -993,6 +993,34 @@ class CalDAV extends HTTP_WebDAV_Server 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 * @@ -1003,7 +1031,7 @@ class CalDAV extends HTTP_WebDAV_Server { 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']; } return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ? @@ -1427,7 +1455,7 @@ class CalDAV extends HTTP_WebDAV_Server substr($options['path'], -1) === '/' && self::isJSON()) { $_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).')'); @@ -1915,7 +1943,7 @@ class CalDAV extends HTTP_WebDAV_Server * @param array parameter passing array * @return bool true on success */ - function PUT(&$options) + function PUT(&$options, $method='PUT') { // read the content in a string, if a stream is given if (isset($options['stream'])) @@ -1934,9 +1962,14 @@ class CalDAV extends HTTP_WebDAV_Server { 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))) { - $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 if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong'; diff --git a/api/src/CalDAV/Handler.php b/api/src/CalDAV/Handler.php index 6c360a769a..ca71dcc59f 100644 --- a/api/src/CalDAV/Handler.php +++ b/api/src/CalDAV/Handler.php @@ -60,6 +60,7 @@ abstract class Handler var $method2acl = array( 'GET' => Api\Acl::READ, 'PUT' => Api\Acl::EDIT, + 'PATCH' => Api\Acl::EDIT, 'DELETE' => Api\Acl::DELETE, ); /** diff --git a/api/src/Contacts/JsContact.php b/api/src/Contacts/JsContact.php index 6e3038cf0e..bb3b7338a2 100644 --- a/api/src/Contacts/JsContact.php +++ b/api/src/Contacts/JsContact.php @@ -78,26 +78,32 @@ class JsContact /** * 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 array $old=[] existing contact - * @param bool $strict true: check if objects have their proper @type attribute + * @param array $old=[] existing contact for patch + * @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 */ - 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 { + $strict = !isset($content_type) || !preg_match('#^application/json#', $content_type); $data = json_decode($json, true, 10, JSON_THROW_ON_ERROR); - // check if we have a patch: keys contain slashes - if (array_filter(array_keys($data), static function ($key) + // check if we use patch: method is PATCH or method is POST AND keys contain slashes + if ($method === 'PATCH' || !$strict && $method === 'POST' && array_filter(array_keys($data), static function ($key) { return strpos($key, '/') !== false; })) { // apply patch on JsCard of contact $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 @@ -951,7 +957,7 @@ class JsContact { 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']; } diff --git a/api/src/WebDAV/Server.php b/api/src/WebDAV/Server.php index 1fcd32416a..2352b31b53 100644 --- a/api/src/WebDAV/Server.php +++ b/api/src/WebDAV/Server.php @@ -1701,10 +1701,10 @@ class HTTP_WebDAV_Server /** * PUT method handler * - * @param void + * @param string $method='PUT' * @return void */ - function http_PUT() + function http_PUT(string $method='PUT') { if ($this->_check_lock_status($this->path)) { $options = Array(); @@ -1839,7 +1839,7 @@ class HTTP_WebDAV_Server } } - $stat = $this->PUT($options); + $stat = $this->$method($options); if ($stat === false) { $stat = "403 Forbidden"; diff --git a/doc/REST-CalDAV-CardDAV/README.md b/doc/REST-CalDAV-CardDAV/README.md index b173c4942d..d203c94df5 100644 --- a/doc/REST-CalDAV-CardDAV/README.md +++ b/doc/REST-CalDAV-CardDAV/README.md @@ -276,7 +276,37 @@ Location: https://example.org/egroupware/groupdav.php//addressbook/123 ``` -* **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) + +
+ Example: PATCH request to modify a contact with partial data + +``` +cat </addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user +{ + "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 +``` +
* **DELETE** requests delete single resources @@ -289,3 +319,11 @@ use ```:``` like in JsCalendar * top-level objects need a ```@type``` attribute with one of the following values: ```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```, ```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation``` + +### ToDos +- [x] Addressbook + - [ ] update of photos, keys, attachments +- [ ] InfoLog +- [ ] Calendar +- [ ] relatedTo / links +- [ ] storing not native supported attributes eg. localization \ No newline at end of file