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;
}
// 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) :
JsContact::getJsCard($contact, $type);

View File

@ -1043,7 +1043,7 @@ class CalDAV extends HTTP_WebDAV_Server
*/
function PATCH(array &$options)
{
if (!preg_match('#^application/([^; +]+\+)?json#', $_SERVER['HTTP_CONTENT_TYPE']))
if (!self::isJSON())
{
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")
// 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' ||
// addressbook has not implemented a POST handler, therefore we have to call the PUT handler
preg_match('#^(/[^/]+)?/(addressbook|calendar)(-[^/]+)?/$#', $options['path']) && self::isJSON())
// REST API: all but mail have no POST handler, therefore we have to call the PUT handler
!preg_match('#^(/[^/]+)?/mail/$#', $options['path']) && self::isJSON())
{
$_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options, 'POST');
@ -2049,7 +2049,7 @@ class CalDAV extends HTTP_WebDAV_Server
return '404 Not Found';
}
// 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';
}

View File

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

View File

@ -755,7 +755,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
}
// 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['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,
"unitprice": 50,
"category": { "other": true },
"owner": "ralf@boulder.egroupware.org",
"owner": "ralf@example.org",
"created": "2005-12-16T23:00:00Z",
"modified": "2011-06-08T10:51:20Z",
"modifier": "ralf@boulder.egroupware.org",
"modifier": "ralf@example.org",
"status": "genehmigt",
"etag": "1:1307537480"
},
@ -58,10 +58,10 @@ curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accep
"start": "2016-08-22T12:12:00Z",
"duration": 60,
"quantity": 1,
"owner": "ralf@boulder.egroupware.org",
"owner": "ralf@example.org",
"created": "2016-08-22T12:12:00Z",
"modified": "2016-08-22T13:13:22Z",
"modifier": "ralf@boulder.egroupware.org",
"modifier": "ralf@example.org",
"egroupware.org:customfields": {
"auswahl": {
"value": [
@ -187,10 +187,13 @@ curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: app
"start": "2016-08-22T12:12:00Z",
"duration": 60,
"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",
"modified": "2016-08-22T13:13:22Z",
"modifier": "ralf@boulder.egroupware.org",
"modifier": "ralf@example.org",
"egroupware.org:customfields": {
"auswahl": {
"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>
```
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
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>
@ -233,9 +255,17 @@ Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234
<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
@ -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)
<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
@ -263,4 +293,14 @@ HTTP/1.1 204 No content
* **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

View File

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

View File

@ -542,8 +542,8 @@ class ApiHandler extends Api\CalDAV\Handler
try
{
// jsContact or vCard
if (($type=Api\CalDAV::isJSON()))
// only JsTimesheet, no *DAV
if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
{
$options['data'] = JsTimesheet::JsTimesheet($timesheet, $type);
$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 (!($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 ($save_ok === 0)
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($timesheet).") failed, error=$err");
if ($err !== true)
{
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this->check_return_representation($options, $id, $user);
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);
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')
{
// 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
if (!$old || !$method === 'PATCH')
{
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");
}
@ -124,6 +124,15 @@ class JsTimesheet extends Api\CalDAV\JsBase
case 'duration':
$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;
case 'quantity':
@ -151,6 +160,8 @@ class JsTimesheet extends Api\CalDAV\JsBase
case 'created':
case 'modified':
case 'modifier':
case self::AT_TYPE:
case 'id':
break;
default: