This commit is contained in:
ralf 2024-02-06 16:39:12 +02:00
parent 59619f83a9
commit 087e969f9f
8 changed files with 87 additions and 30 deletions

View File

@ -652,7 +652,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
return $contact; return $contact;
} }
// jsContact or vCard // jsContact or vCard
if (($type=Api\CalDAV::isJSON())) if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
{ {
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) : $options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) :
JsContact::getJsCard($contact, $type); JsContact::getJsCard($contact, $type);

View File

@ -1043,7 +1043,7 @@ class CalDAV extends HTTP_WebDAV_Server
*/ */
function PATCH(array &$options) function PATCH(array &$options)
{ {
if (!preg_match('#^application/([^; +]+\+)?json#', $_SERVER['HTTP_CONTENT_TYPE'])) if (!self::isJSON())
{ {
return '501 Not implemented'; return '501 Not implemented';
} }
@ -1529,8 +1529,8 @@ class CalDAV extends HTTP_WebDAV_Server
// for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member") // for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member")
// POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys // POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys
if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork' || if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork' ||
// addressbook has not implemented a POST handler, therefore we have to call the PUT handler // REST API: all but mail have no POST handler, therefore we have to call the PUT handler
preg_match('#^(/[^/]+)?/(addressbook|calendar)(-[^/]+)?/$#', $options['path']) && self::isJSON()) !preg_match('#^(/[^/]+)?/mail/$#', $options['path']) && self::isJSON())
{ {
$_GET['add-member'] = ''; // otherwise we give no Location header $_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options, 'POST'); return $this->PUT($options, 'POST');
@ -2049,7 +2049,7 @@ class CalDAV extends HTTP_WebDAV_Server
return '404 Not Found'; return '404 Not Found';
} }
// REST API & PATCH only implemented for addressbook and calendar currently // REST API & PATCH only implemented for addressbook and calendar currently
if (!in_array($app, ['addressbook', 'calendar']) && $method === 'PATCH') if (!self::isJSON() && $method === 'PATCH')
{ {
return '501 Not implemented'; return '501 Not implemented';
} }

View File

@ -61,6 +61,7 @@ abstract class Handler
'GET' => Api\Acl::READ, 'GET' => Api\Acl::READ,
'PUT' => Api\Acl::EDIT, 'PUT' => Api\Acl::EDIT,
'PATCH' => Api\Acl::EDIT, 'PATCH' => Api\Acl::EDIT,
'POST' => Api\Acl::ADD,
'DELETE' => Api\Acl::DELETE, 'DELETE' => Api\Acl::DELETE,
); );
/** /**

View File

@ -755,7 +755,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
} }
// jsEvent or iCal // jsEvent or iCal
if (($type=Api\CalDAV::isJSON())) if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
{ {
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null, $type); $options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null, $type);
$options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8'; $options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8';

View File

@ -44,10 +44,10 @@ curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accep
"quantity": 2.5, "quantity": 2.5,
"unitprice": 50, "unitprice": 50,
"category": { "other": true }, "category": { "other": true },
"owner": "ralf@boulder.egroupware.org", "owner": "ralf@example.org",
"created": "2005-12-16T23:00:00Z", "created": "2005-12-16T23:00:00Z",
"modified": "2011-06-08T10:51:20Z", "modified": "2011-06-08T10:51:20Z",
"modifier": "ralf@boulder.egroupware.org", "modifier": "ralf@example.org",
"status": "genehmigt", "status": "genehmigt",
"etag": "1:1307537480" "etag": "1:1307537480"
}, },
@ -58,10 +58,10 @@ curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accep
"start": "2016-08-22T12:12:00Z", "start": "2016-08-22T12:12:00Z",
"duration": 60, "duration": 60,
"quantity": 1, "quantity": 1,
"owner": "ralf@boulder.egroupware.org", "owner": "ralf@example.org",
"created": "2016-08-22T12:12:00Z", "created": "2016-08-22T12:12:00Z",
"modified": "2016-08-22T13:13:22Z", "modified": "2016-08-22T13:13:22Z",
"modifier": "ralf@boulder.egroupware.org", "modifier": "ralf@example.org",
"egroupware.org:customfields": { "egroupware.org:customfields": {
"auswahl": { "auswahl": {
"value": [ "value": [
@ -187,10 +187,13 @@ curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: app
"start": "2016-08-22T12:12:00Z", "start": "2016-08-22T12:12:00Z",
"duration": 60, "duration": 60,
"quantity": 1, "quantity": 1,
"owner": "ralf@boulder.egroupware.org", "project": "2024-0001: Test Project",
"unitprice": 100.0,
"pricelist": 123,
"owner": "ralf@example.org",
"created": "2016-08-22T12:12:00Z", "created": "2016-08-22T12:12:00Z",
"modified": "2016-08-22T13:13:22Z", "modified": "2016-08-22T13:13:22Z",
"modifier": "ralf@boulder.egroupware.org", "modifier": "ralf@example.org",
"egroupware.org:customfields": { "egroupware.org:customfields": {
"auswahl": { "auswahl": {
"value": [ "value": [
@ -216,14 +219,33 @@ curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: app
<summary>Example: POST request to create a new resource</summary> <summary>Example: POST request to create a new resource</summary>
``` ```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/' -X POST -d @- -H "Content-Type: application/json" --user <username> cat <<EOF | curl -i -X POST 'https://example.org/egroupware/groupdav.php/<username>/timesheet/' -d @- -H "Content-Type: application/json" -H 'Accept: application/pretty+json' -H 'Prefer: return=representation' --user <username>
{ {
TODO "@type": "timesheet",
"title": "5. Test Ralf",
"start": "2024-02-06T10:00:00Z",
"duration": 60
} }
EOF EOF
HTTP/1.1 201 Created HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234 Content-Type: application/json
Location: /egroupware/groupdav.php/ralf/timesheet/204
ETag: "204:1707233040"
{
"@type": "timesheet",
"id": 204,
"title": "5. Test Ralf",
"start": "2024-02-06T10:00:00Z",
"duration": 60,
"quantity": 1,
"owner": "ralf@example.org",
"created": "2024-02-06T14:24:05Z",
"modified": "2024-02-06T14:24:00Z",
"modifier": "ralf@example.org",
"etag": "204:1707233040"
}
``` ```
</details> </details>
@ -233,9 +255,17 @@ Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234
<summary>Example: PUT request to update a resource</summary> <summary>Example: PUT request to update a resource</summary>
``` ```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username> cat <<EOF | curl -i -X PUT 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -d @- -H "Content-Type: application/json" --user <username>
{ {
TODO "@type": "timesheet",
"title": "6. Test Ralf",
"start": "2024-02-06T10:00:00Z",
"duration": 60,
"quantity": 1,
"owner": "ralf@example.org",
"created": "2024-02-06T14:24:05Z",
"modified": "2024-02-06T14:24:00Z",
"modifier": "ralf@example.org",
} }
EOF EOF
@ -248,12 +278,12 @@ HTTP/1.1 204 No Content
* **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) * **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> <details>
<summary>Example: PATCH request to modify a contact with partial data</summary> <summary>Example: PATCH request to modify a timesheet with partial data</summary>
``` ```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username> cat <<EOF | curl -i -X PATCH 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -d @- -H "Content-Type: application/json" --user <username>
{ {
TODO "status": "invoiced"
} }
EOF EOF
@ -263,4 +293,14 @@ HTTP/1.1 204 No content
* **DELETE** requests delete single resources * **DELETE** requests delete single resources
<details>
<summary>Example: DELETE request to delete a timesheet</summary>
```
curl -i -X DELETE 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -H "Accept: application/json" --user <username>
HTTP/1.1 204 No content
```
</details>
> one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API > one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API

View File

@ -377,6 +377,10 @@ class timesheet_bo extends Api\Storage
{ {
$data =& $this->data; $data =& $this->data;
} }
if (!$data)
{
return null; // entry not found
}
if (!is_array($data)) if (!is_array($data))
{ {
$save_data = $this->data; $save_data = $this->data;

View File

@ -542,8 +542,8 @@ class ApiHandler extends Api\CalDAV\Handler
try try
{ {
// jsContact or vCard // only JsTimesheet, no *DAV
if (($type=Api\CalDAV::isJSON())) if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
{ {
$options['data'] = JsTimesheet::JsTimesheet($timesheet, $type); $options['data'] = JsTimesheet::JsTimesheet($timesheet, $type);
$options['mimetype'] = 'application/json'; $options['mimetype'] = 'application/json';
@ -646,19 +646,20 @@ class ApiHandler extends Api\CalDAV\Handler
} }
if ($this->http_if_match) $timesheet['etag'] = self::etag2value($this->http_if_match); if ($this->http_if_match) $timesheet['etag'] = self::etag2value($this->http_if_match);
if (!($save_ok = $this->bo->save($timesheet))) if (($err = $this->bo->save($timesheet)))
{ {
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($timesheet).") failed, Ok=$save_ok"); if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($timesheet).") failed, error=$err");
if ($save_ok === 0) if ($err !== true)
{ {
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload) // honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this->check_return_representation($options, $id, $user); $this->check_return_representation($options, $id, $user);
return '412 Precondition Failed'; return '412 Precondition Failed';
} }
return '403 Forbidden'; // happens when writing new entries in AB's without ADD rights return '403 Forbidden';
} }
$timesheet = Api\Db::strip_array_keys($this->bo->data, 'ts_');
// send evtl. necessary response headers: Location, etag, ... // send necessary response headers: Location, etag, ...
$this->put_response_headers($timesheet, $options['path'], $retval); $this->put_response_headers($timesheet, $options['path'], $retval);
if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval)); if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval));

View File

@ -92,16 +92,16 @@ class JsTimesheet extends Api\CalDAV\JsBase
if ($method === 'PATCH') if ($method === 'PATCH')
{ {
// apply patch on JsCard of contact // apply patch on JsCard of contact
$data = self::patch($data, $old ? self::getJsCalendar($old, false) : [], !$old); $data = self::patch($data, $old ? self::JsTimesheet($old, false) : [], !$old);
} }
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
// check required fields // check required fields
if (!$old || !$method === 'PATCH') if (!$old || !$method === 'PATCH')
{ {
static $required = ['title', 'start', 'duration']; static $required = ['title', 'start', 'duration'];
if (($missing = array_diff_key(array_filter(array_intersect_key($data), array_flip($required)), array_flip($required)))) if (($missing = array_diff_key(array_filter(array_intersect_key($data, array_flip($required))), array_flip($required))))
{ {
throw new Api\CalDAV\JsParseException("Required field(s) ".implode(', ', $missing)." missing"); throw new Api\CalDAV\JsParseException("Required field(s) ".implode(', ', $missing)." missing");
} }
@ -124,6 +124,15 @@ class JsTimesheet extends Api\CalDAV\JsBase
case 'duration': case 'duration':
$timesheet['ts_duration'] = self::parseInt($value); $timesheet['ts_duration'] = self::parseInt($value);
// set default quantity, if none explicitly given
if (!isset($timesheet['ts_quantity']))
{
$timesheet['ts_quantity'] = $timesheet['ts_duration'] / 60.0;
}
break;
case 'pricelist':
$timesheet['pl_id'] = self::parseInt($value);
break; break;
case 'quantity': case 'quantity':
@ -151,6 +160,8 @@ class JsTimesheet extends Api\CalDAV\JsBase
case 'created': case 'created':
case 'modified': case 'modified':
case 'modifier': case 'modifier':
case self::AT_TYPE:
case 'id':
break; break;
default: default: