implement and document PATCH

This commit is contained in:
Ralf Becker 2021-09-25 12:20:31 +02:00
parent 1280de46d6
commit e640873fc0
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 $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)
{

View File

@ -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';

View File

@ -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,
);
/**

View File

@ -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'];
}

View File

@ -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";

View File

@ -276,7 +276,37 @@ Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/123
```
</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
@ -289,3 +319,11 @@ use ```<domain-name>:<name>``` 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