mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-21 23:43:17 +01:00
WIP timesheet REST API
This commit is contained in:
parent
4fcd761f0c
commit
ca443060f4
@ -254,6 +254,17 @@ abstract class Handler
|
||||
{
|
||||
$entry = $this->read($entry);
|
||||
}
|
||||
return static::etag($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the etag for an entry-array, can be reimplemented for other algorithm or field names
|
||||
*
|
||||
* @param array $entry
|
||||
* @return false|string
|
||||
*/
|
||||
public static function etag(array $entry)
|
||||
{
|
||||
if (!is_array($entry) || !isset($entry['id']) || !(isset($entry['modified']) || isset($entry['etag'])))
|
||||
{
|
||||
// error_log(__METHOD__."(".array2string($entry).") Cant create etag!");
|
||||
|
@ -1257,13 +1257,12 @@ class Base
|
||||
*/
|
||||
public function search2criteria($_pattern,&$wildcard='',&$op='AND',$extra_col=null, $search_cols=[],$search_cfs=null)
|
||||
{
|
||||
$pattern = trim($_pattern);
|
||||
// This function can get called multiple times. Make sure it doesn't re-process.
|
||||
if (empty($pattern) || is_array($pattern)) return $pattern;
|
||||
if(strpos($pattern, 'CAST(COALESCE(') !== false)
|
||||
if (empty($_pattern) || is_array($_pattern) || strpos($_pattern, 'CAST(COALESCE(') !== false)
|
||||
{
|
||||
return $pattern;
|
||||
return $_pattern;
|
||||
}
|
||||
$pattern = trim($_pattern);
|
||||
|
||||
$criteria = array();
|
||||
$filter = array();
|
||||
|
@ -3,7 +3,7 @@
|
||||
Authentication is via Basic Auth with username and a password, or a token valid for:
|
||||
- either just the given user or all users
|
||||
- CalDAV/CardDAV Sync (REST API)
|
||||
- Calendar application
|
||||
- Addressbook application
|
||||
|
||||
Following RFCs / drafts used/planned for JSON encoding of resources
|
||||
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
|
||||
|
@ -32,6 +32,7 @@ One can use the following URLs relative (!) to https://example.org/egroupware/gr
|
||||
- ```/(resources|locations)/<resource-name>/calendar``` calendar of a resource/location, if user has rights to view
|
||||
- ```/<current-username>/(resource|location)-<resource-name>``` shared calendar from a resource/location
|
||||
- ```/mail/``` REST API only
|
||||
- ```/timesheet/``` REST API only
|
||||
|
||||
Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
|
||||
|
||||
@ -40,8 +41,14 @@ from the data of an ```allprop``` PROPFIND, allow browsing CalDAV/CardDAV tree w
|
||||
|
||||
## REST API: using EGroupware CalDAV/CardDAV server with JSON
|
||||
- [Addressbook](Addressbook.md)
|
||||
- [Calendar](Calendar.md) (currently recurring events are readonly, they are returned but can not be created or modified)
|
||||
- [Mail](Mail.md) (currently only sending mails, opening interactive compose windows and vacation handling)
|
||||
- [Calendar](Calendar.md)
|
||||
* currently recurring events are readonly, they are returned but can not be created or modified
|
||||
- [Mail](Mail.md)
|
||||
* currently only sending mails,
|
||||
* opening interactive compose windows,
|
||||
* view and reply to eml files and
|
||||
* vacation handling
|
||||
- [Timesheet](Timesheet.md)
|
||||
|
||||
> For the REST API you always have to send an "Accept: application/json" header and for POST & PUT requests additionally
|
||||
> a "Content-Type: application/json" header, otherwise you talk to the CalDAV/CardDAV server and don't get the response you expect!
|
||||
|
265
doc/REST-CalDAV-CardDAV/Timesheet.md
Normal file
265
doc/REST-CalDAV-CardDAV/Timesheet.md
Normal file
@ -0,0 +1,265 @@
|
||||
# EGroupware REST API for Timesheet
|
||||
|
||||
Authentication is via Basic Auth with username and a password, or a token valid for:
|
||||
- either just the given user or all users
|
||||
- CalDAV/CardDAV Sync (REST API)
|
||||
- Timesheet application
|
||||
|
||||
Following schema is used for JSON encoding of timesheets
|
||||
* @type: `timesheet`
|
||||
* id: integer ID
|
||||
* title: string
|
||||
* description: string (multiple lines)
|
||||
* start: UTCDateTime e.g. `2020-02-03T14:35:37Z`
|
||||
* duration: integer in minutes
|
||||
* quantity: double
|
||||
* unitprice: double
|
||||
* category: category object with a single(!) category-name e.g. `{"category name": true}`
|
||||
* owner: string with either email or username or integer ID
|
||||
* created: UTCDateTime e.g. `2020-02-03T14:35:37Z`
|
||||
* modified: UTCDateTime e.g. `2020-02-03T14:35:37Z`
|
||||
* modifier: string with either email or username or integer ID
|
||||
* pricelist: integer ID of projectmanager pricelist item
|
||||
* status: string
|
||||
* egroupware.org:customfields: custom-fields object, see other types
|
||||
* etag: string `"<id>:<modified-timestamp>"` (double quotes are part of the etag!)
|
||||
|
||||
### Supported request methods and examples
|
||||
|
||||
* **GET** to collections with an ```Accept: application/json``` header return all timesheets (similar to WebDAV PROPFIND)
|
||||
<details>
|
||||
<summary>Example: Getting all timesheets of a given user</summary>
|
||||
|
||||
```
|
||||
curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/<username>/timesheet/1": {
|
||||
"@type": "timesheet",
|
||||
"id": 1,
|
||||
"title": "Test",
|
||||
"start": "2005-12-16T23:00:00Z",
|
||||
"duration": 150,
|
||||
"quantity": 2.5,
|
||||
"unitprice": 50,
|
||||
"category": { "other": true },
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"created": "2005-12-16T23:00:00Z",
|
||||
"modified": "2011-06-08T10:51:20Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"status": "genehmigt",
|
||||
"etag": "1:1307537480"
|
||||
},
|
||||
"/<username>/timesheet/140": {
|
||||
"@type": "timesheet",
|
||||
"id": 140,
|
||||
"title": "Test Ralf aus PM",
|
||||
"start": "2016-08-22T12:12:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"created": "2016-08-22T12:12:00Z",
|
||||
"modified": "2016-08-22T13:13:22Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"egroupware.org:customfields": {
|
||||
"auswahl": {
|
||||
"value": [
|
||||
"3"
|
||||
],
|
||||
"type": "select",
|
||||
"label": "Auswählen",
|
||||
"values": {
|
||||
"3": "Three",
|
||||
"2": "Two",
|
||||
"1": "One"
|
||||
}
|
||||
}
|
||||
},
|
||||
"etag": "140:1471878802"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
Following GET parameters are supported to customize the returned properties:
|
||||
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
|
||||
Default for timesheet collections is to only return address-data (JsContact), other collections return all props.
|
||||
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
|
||||
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
|
||||
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
|
||||
|
||||
The GET parameter `filters` allows to filter or search for a pattern in timesheets of a user:
|
||||
- `filters[search]=<pattern>` searches for `<pattern>` in the whole timesheet like the search in the GUI
|
||||
- `filters[search][%23<custom-field-name>]=<custom-field-value>` filters by a custom-field value
|
||||
- `filters[<attribute-name>]=<value>` filters by a DB-column name and value
|
||||
|
||||
<details>
|
||||
<summary>Example: Getting just ETAGs and displayname of all timesheets of a user</summary>
|
||||
|
||||
```
|
||||
curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
|
||||
{
|
||||
"responses": {
|
||||
"/ralf/timesheet/1": {"displayname":"Test","getetag":"\"1:1307537480\""},
|
||||
"/ralf/timesheet/140": {"displayname":"Test Ralf aus PM","getetag":"\"140:1471878802\""},
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example: Start using a sync-token to get only changed entries since last sync</summary>
|
||||
|
||||
#### Initial request with empty sync-token and only requesting 10 entries per chunk:
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/timesheet/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/timesheet/2050": "Frau Margot Test-Notifikation",
|
||||
"/timesheet/2384": "Test Tester",
|
||||
"/timesheet/5462": "Margot Testgedöns",
|
||||
"/timesheet/2380": "Frau Test Defaulterin",
|
||||
"/timesheet/5474": "Noch ein Neuer",
|
||||
"/timesheet/5575": "Mr New Name",
|
||||
"/timesheet/5461": "Herr Hugo Kurt Müller Senior",
|
||||
"/timesheet/5601": "Steve Jobs",
|
||||
"/timesheet/5603": "Ralf Becker",
|
||||
"/timesheet/1838": "Test Tester"
|
||||
},
|
||||
"more-results": true,
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/timesheet/1400867824"
|
||||
}
|
||||
```
|
||||
#### Requesting next chunk:
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/timesheet/?sync-token=https://example.org/egroupware/groupdav.php/timesheet/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/timesheet/1833": "Default Tester",
|
||||
"/timesheet/5597": "Neuer Testschnuffi",
|
||||
"/timesheet/5593": "Muster Max",
|
||||
"/timesheet/5628": "2. Test Contact",
|
||||
"/timesheet/5629": "Testen Tester",
|
||||
"/timesheet/5630": "Testen Tester",
|
||||
"/timesheet/5633": "Testen Tester",
|
||||
"/timesheet/5635": "Test4 Tester",
|
||||
"/timesheet/5638": "Test Kontakt",
|
||||
"/timesheet/5636": "Test Default"
|
||||
},
|
||||
"more-results": true,
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/timesheet/1427103057"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example: Requesting only changes since last sync</summary>
|
||||
|
||||
#### ```sync-token``` from last sync need to be specified (note the null for a deleted resource!)
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/timesheet/?sync-token=https://example.org/egroupware/groupdav.php/timesheet/1400867824' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/timesheet/5597": null,
|
||||
"/timesheet/5593": {
|
||||
TODO
|
||||
....
|
||||
}
|
||||
},
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/timesheet/1427103057"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsTimesheet schema
|
||||
<details>
|
||||
<summary>Example: GET request for a single resource showcasing available fieldes</summary>
|
||||
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"@type": "timesheet",
|
||||
"id": 140,
|
||||
"title": "Test Ralf aus PM",
|
||||
"start": "2016-08-22T12:12:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"created": "2016-08-22T12:12:00Z",
|
||||
"modified": "2016-08-22T13:13:22Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"egroupware.org:customfields": {
|
||||
"auswahl": {
|
||||
"value": [
|
||||
"3"
|
||||
],
|
||||
"type": "select",
|
||||
"label": "Auswählen",
|
||||
"values": {
|
||||
"3": "Three",
|
||||
"2": "Two",
|
||||
"1": "One"
|
||||
}
|
||||
}
|
||||
},
|
||||
"etag": "140:1471878802"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in timesheet collections
|
||||
(Location header in response gives URL of new resource)
|
||||
<details>
|
||||
<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>
|
||||
{
|
||||
TODO
|
||||
}
|
||||
EOF
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234
|
||||
```
|
||||
</details>
|
||||
|
||||
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
|
||||
|
||||
<details>
|
||||
<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>
|
||||
{
|
||||
TODO
|
||||
}
|
||||
EOF
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
* **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>/timesheet/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
|
||||
{
|
||||
TODO
|
||||
}
|
||||
EOF
|
||||
|
||||
HTTP/1.1 204 No content
|
||||
```
|
||||
</details>
|
||||
|
||||
* **DELETE** requests delete single resources
|
||||
|
||||
> one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
@ -151,6 +151,7 @@ class timesheet_bo extends Api\Storage
|
||||
* Name of the timesheet table storing custom fields
|
||||
*/
|
||||
const EXTRA_TABLE = 'egw_timesheet_extra';
|
||||
const TABLE = 'egw_timesheet';
|
||||
|
||||
/**
|
||||
* Columns to search when user does a text search
|
||||
@ -166,7 +167,7 @@ class timesheet_bo extends Api\Storage
|
||||
|
||||
function __construct()
|
||||
{
|
||||
parent::__construct(TIMESHEET_APP,'egw_timesheet',self::EXTRA_TABLE,'','ts_extra_name','ts_extra_value','ts_id');
|
||||
parent::__construct(TIMESHEET_APP,self::TABLE,self::EXTRA_TABLE,'','ts_extra_name','ts_extra_value','ts_id');
|
||||
|
||||
$this->config_data = Api\Config::read(TIMESHEET_APP);
|
||||
$this->quantity_sum = $this->config_data['quantity_sum'] == 'true';
|
||||
|
@ -37,7 +37,7 @@ class ApiHandler extends Api\CalDAV\Handler
|
||||
*/
|
||||
function __construct($app, Api\CalDAV $caldav)
|
||||
{
|
||||
parent::__construct($app, $caldav);
|
||||
parent::__construct('timesheet', $caldav);
|
||||
self::$path_extension = '';
|
||||
|
||||
$this->bo = new \timesheet_bo();
|
||||
@ -64,415 +64,16 @@ class ApiHandler extends Api\CalDAV\Handler
|
||||
{
|
||||
$user = $GLOBALS['egw_info']['user']['account_id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$prefix = '/'.Api\Accounts::id2name($user);
|
||||
if (str_starts_with($path, $prefix)) $path = substr($path, strlen($prefix));
|
||||
if ($user != $GLOBALS['egw_info']['user']['account_id'])
|
||||
{
|
||||
throw new \Exception("/mail is NOT available for users other than the one you authenticated!", 403);
|
||||
}
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
if (str_starts_with($path, '/mail/attachments/'))
|
||||
{
|
||||
return self::storeAttachment($path, $options['stream'] ?? $options['content']);
|
||||
}
|
||||
elseif (preg_match('#^/mail(/(\d+))?/vacation/?$#', $path, $matches))
|
||||
{
|
||||
return self::updateVacation($user, $options['content'], $matches[2]);
|
||||
}
|
||||
elseif (preg_match('#^/mail(/(\d+))?/view/?$#', $path, $matches))
|
||||
{
|
||||
return self::viewEml($user, $options['stream'] ?? $options['content'], $matches[2]);
|
||||
}
|
||||
elseif (preg_match('#^/mail(/(\d+))?(/compose)?#', $path, $matches))
|
||||
{
|
||||
$ident_id = $matches[2] ?? self::defaultIdentity($user);
|
||||
$do_compose = (bool)($matches[3] ?? false);
|
||||
if (!($data = json_decode($options['content'], true)))
|
||||
{
|
||||
throw new \Exception('Error decoding JSON: '.json_last_error_msg(), 422);
|
||||
}
|
||||
// ToDo: check required attributes
|
||||
|
||||
$preset = array_filter(array_intersect_key($data, array_flip(['to', 'cc', 'bcc', 'replyto', 'subject', 'priority']))+[
|
||||
'body' => $data['bodyHtml'] ?? null ?: $data['body'] ?? '',
|
||||
'mimeType' => !empty($data['bodyHtml']) ? 'html' : 'plain',
|
||||
'identity' => $ident_id,
|
||||
]+self::prepareAttachments($data['attachments'] ?? [], $data['attachmentType'] ?? 'attach',
|
||||
$data['shareExpiration'], $data['sharePassword'], $do_compose));
|
||||
|
||||
// for compose we need to construct a URL and push it to the client (or give an error if the client is not online)
|
||||
if ($do_compose)
|
||||
{
|
||||
if (!Api\Json\Push::isOnline($user))
|
||||
{
|
||||
$account_lid = Api\Accounts::id2name($user);
|
||||
throw new \Exception("User '$account_lid' (#$user) is NOT online", 404);
|
||||
}
|
||||
$push = new Api\Json\Push($user);
|
||||
$push->call('egw.open', '', 'mail', 'add', ['preset' => $preset], '_blank', 'mail');
|
||||
echo json_encode([
|
||||
'status' => 200,
|
||||
'message' => 'Request to open compose window sent',
|
||||
//'data' => $preset,
|
||||
], self::JSON_RESPONSE_OPTIONS);
|
||||
return true;
|
||||
}
|
||||
$acc_id = Api\Mail\Account::read_identity($ident_id)['acc_id'];
|
||||
$mail_account = Api\Mail\Account::read($acc_id);
|
||||
// check if the mail-account requires a user-context / password and then just send the mail with an smtp-only account NOT saving to Sent folder
|
||||
if (empty($mail_account->acc_imap_password) || $mail_account->acc_smtp_auth_session && empty($mail_account->acc_smtp_password))
|
||||
{
|
||||
$acc_id = Api\Mail\Account::get_default(true, true, true, false);
|
||||
$compose = new \mail_compose($acc_id);
|
||||
$compose->mailPreferences['sendOptions'] = 'send_only';
|
||||
$warning = 'Mail NOT saved to Sent folder, as no user password';
|
||||
}
|
||||
else
|
||||
{
|
||||
$compose = new \mail_compose($acc_id);
|
||||
}
|
||||
$preset = array_filter([
|
||||
'mailaccount' => $acc_id,
|
||||
'mailidentity' => $ident_id,
|
||||
'identity' => null,
|
||||
'add_signature' => true, // add signature in send, independent what preference says
|
||||
]+$preset);
|
||||
if ($compose->send($preset, $acc_id))
|
||||
{
|
||||
echo json_encode(array_filter([
|
||||
'status' => 200,
|
||||
'warning' => $warning ?? null,
|
||||
'message' => 'Mail successful sent',
|
||||
//'data' => $preset,
|
||||
]), self::JSON_RESPONSE_OPTIONS);
|
||||
return true;
|
||||
}
|
||||
throw new \Exception($compose->error_info);
|
||||
}
|
||||
|
||||
throw new \Exception('Not Found', 404);
|
||||
throw new \Exception('Not Implemented', 501);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
return self::handleException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vacation array from server
|
||||
*
|
||||
* @param Api\Mail\Imap $imap
|
||||
* @param ?int $user
|
||||
* @return array
|
||||
*/
|
||||
protected static function getVacation(Api\Mail\Imap $imap, int $user=null)
|
||||
{
|
||||
if ($GLOBALS['egw']->session->token_auth)
|
||||
{
|
||||
return $imap->getVacationUser($user ?: $GLOBALS['egw_info']['user']['account_id']);
|
||||
}
|
||||
$sieve = new Api\Mail\Sieve($imap);
|
||||
return $sieve->getVacation()+['script' => $sieve->script];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update vacation message/handling with JSON data given in $content
|
||||
*
|
||||
* @param int $user
|
||||
* @param array $content
|
||||
* @param int|null $identity
|
||||
* @return bool
|
||||
* @throws Api\Exception\AssertionFailed
|
||||
* @throws Api\Exception\NotFound
|
||||
*/
|
||||
protected static function updateVacation(int $user, string $content, int $identity=null)
|
||||
{
|
||||
$account = self::getMailAccount($user, $identity);
|
||||
$vacation = $account->imapServer()->getVacationUser($user);
|
||||
if (!($update = json_decode($content, true, 3, JSON_THROW_ON_ERROR)))
|
||||
{
|
||||
throw new \Exeception('Invalid request: no content', 400);
|
||||
}
|
||||
// Sieve class stores them as timestamps
|
||||
foreach(['start', 'end'] as $name)
|
||||
{
|
||||
if (isset($update[$name]))
|
||||
{
|
||||
$vacation[$name.'_date'] = (new Api\DateTime($update[$name]))->format('ts');
|
||||
if (empty($update['status'])) $update['status'] = 'by_date';
|
||||
}
|
||||
elseif (array_key_exists($name, $update))
|
||||
{
|
||||
$vacation[$name.'_date'] = null;
|
||||
if (empty($update['status'])) $update['status'] = 'off';
|
||||
}
|
||||
unset($update[$name]);
|
||||
}
|
||||
// Sieve class stores them as comma-separated string
|
||||
if (array_key_exists('forwards', $update))
|
||||
{
|
||||
$vacation['forwards'] = implode(',', self::parseAddressList($update['forwards'] ?? [], 'forwards'));
|
||||
unset($update['forwards']);
|
||||
}
|
||||
if (array_key_exists('addresses', $update))
|
||||
{
|
||||
$update['addresses'] = self::parseAddressList($update['addresses'] ?? [], 'addresses');
|
||||
}
|
||||
static $modi = ['notice+store', 'notice', 'store'];
|
||||
if (isset($update['modus']) && !in_array($update['modus'], $modi))
|
||||
{
|
||||
throw new \Exception("Invalid value '$update[modus]' for attribute modus, allowed values are: '".implode("', '", $modi)."'", 400);
|
||||
}
|
||||
if (($invalid=array_diff(array_keys($update), ['start','end','status','modus','text','addresses','forwards','days'])))
|
||||
{
|
||||
throw new \Exception("Invalid attribute: ".implode(', ', $invalid), 400);
|
||||
}
|
||||
$vacation_rule = null;
|
||||
$vacation = array_merge([ // some defaults
|
||||
'status' => 'on',
|
||||
'addresses' => [Api\Accounts::id2name($user, 'account_email')],
|
||||
'days' => 3,
|
||||
], $vacation, $update);
|
||||
// for token-auth we have to use the admin connection
|
||||
if ($GLOBALS['egw']->session->token_auth)
|
||||
{
|
||||
if (!$account->imapServer()->setVacationUser($user, $vacation))
|
||||
{
|
||||
throw new \Exception($account->imapServer()->error ?: 'Error updating sieve-script');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$sieve = new Api\Mail\Sieve($account->imapServer());
|
||||
$sieve->setVacation($vacation, null, $vacation_rule, true);
|
||||
}
|
||||
echo json_encode(array_filter([
|
||||
'status' => 200,
|
||||
'message' => 'Vacation handling updated',
|
||||
'vacation_rule' => $vacation_rule,
|
||||
'vacation' => self::returnVacation(self::getVacation($account->imapServer(), $user)),
|
||||
]), self::JSON_RESPONSE_OPTIONS);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse array of email addresses
|
||||
*
|
||||
* @param string[] $_addresses
|
||||
* @param string $name attribute name for exception
|
||||
* @return string[]
|
||||
* @throws \Exception if there is an invalid email address
|
||||
*/
|
||||
protected static function parseAddressList(array $_addresses, $name=null)
|
||||
{
|
||||
$parsed = iterator_to_array(Api\Mail::parseAddressList($_addresses));
|
||||
|
||||
if (count($parsed) !== count($_addresses) ||
|
||||
array_filter($parsed, static function ($addr)
|
||||
{
|
||||
return !$addr->valid;
|
||||
}))
|
||||
{
|
||||
throw new \Exception("Error parsing email-addresses in attribute $name: ".json_encode($_addresses));
|
||||
}
|
||||
return array_map(static function($addr)
|
||||
{
|
||||
return $addr->mailbox.'@'.$addr->host;
|
||||
}, $parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store uploaded attachment and return token
|
||||
*
|
||||
* @param string $path
|
||||
* @param string|stream $content
|
||||
* @return string HTTP status
|
||||
* @throws \Exception on error
|
||||
*/
|
||||
protected static function storeAttachment(string $path, $content)
|
||||
{
|
||||
$attachment_path = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'attach--'.
|
||||
(str_replace('/', '-', substr($path, 18)) ?: 'no-name').'--');
|
||||
if (is_resource($content) ?
|
||||
stream_copy_to_stream($content, $fp=fopen($attachment_path, 'w')) :
|
||||
file_put_contents($attachment_path, $content))
|
||||
{
|
||||
if (isset($fp)) fclose($fp);
|
||||
$location = '/mail/attachments/'.substr(basename($attachment_path), 8);
|
||||
// allow to suppress location header with an "X-No-Location: true" header
|
||||
if (($location_header = empty($_SERVER['HTTP_X_NO_LOCATION'])))
|
||||
{
|
||||
header('Location: '.Api\Framework::getUrl(Api\Framework::link('/groupdav.php'.$location)));
|
||||
}
|
||||
$ret = $location_header ? '201 Created' : '200 Ok';
|
||||
echo json_encode([
|
||||
'status' => (int)$ret,
|
||||
'message' => 'Attachment stored',
|
||||
'location' => $location,
|
||||
], self::JSON_RESPONSE_OPTIONS);
|
||||
return $ret;
|
||||
}
|
||||
throw new \Exception('Error storing attachment');
|
||||
}
|
||||
|
||||
/**
|
||||
* View posted eml file
|
||||
*
|
||||
* @param int $user
|
||||
* @param string|stream $content
|
||||
* @param ?int $acc_id mail account to import in Drafts folder
|
||||
* @return string HTTP status
|
||||
* @throws \Exception on error
|
||||
*/
|
||||
protected static function viewEml(int $user, $content, int $acc_id=null)
|
||||
{
|
||||
if (empty($acc_id))
|
||||
{
|
||||
$acc_id = self::defaultIdentity($user);
|
||||
}
|
||||
|
||||
// check and bail, if user is not online
|
||||
if (!Api\Json\Push::isOnline($user))
|
||||
{
|
||||
$account_lid = Api\Accounts::id2name($user);
|
||||
throw new \Exception("User '$account_lid' (#$user) is NOT online", 404);
|
||||
}
|
||||
|
||||
// save posted eml to a temp-dir
|
||||
$eml = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'view-eml-');
|
||||
if (!(is_resource($content) ?
|
||||
stream_copy_to_stream($content, $fp = fopen($eml, 'w')) :
|
||||
file_put_contents($eml, $content)))
|
||||
{
|
||||
throw new \Exception('Error storing attachment');
|
||||
}
|
||||
if (isset($fp)) fclose($fp);
|
||||
|
||||
// import mail into drafts folder
|
||||
$mail = Api\Mail::getInstance(false, $acc_id);
|
||||
$folder = $mail->getDraftFolder();
|
||||
$mailer = new Api\Mailer();
|
||||
$mail->parseFileIntoMailObject($mailer, $eml);
|
||||
$mail->openConnection();
|
||||
$message_uid = $mail->appendMessage($folder, $mailer->getRaw(), null, '\\Seen');
|
||||
|
||||
// tell browser to view eml from drafts folder
|
||||
$push = new Api\Json\Push($user);
|
||||
$push->call('egw.open', \mail_ui::generateRowID($acc_id, $folder, $message_uid, true),
|
||||
'mail', 'view', ['mode' => 'display'], '_blank', 'mail');
|
||||
|
||||
// respond with success message
|
||||
echo json_encode([
|
||||
'status' => 200,
|
||||
'message' => 'Request to open view window sent',
|
||||
], self::JSON_RESPONSE_OPTIONS);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default identity of user
|
||||
*
|
||||
* @param int $user
|
||||
* @return int ident_id
|
||||
* @throws Api\Exception\WrongParameter
|
||||
* @throws \Exception (404) if user has no IMAP account
|
||||
*/
|
||||
protected static function defaultIdentity(int $user)
|
||||
{
|
||||
foreach(Api\Mail\Account::search($user,false) as $acc_id => $account)
|
||||
{
|
||||
// do NOT add SMTP only accounts as identities
|
||||
if (!$account->is_imap(false)) continue;
|
||||
|
||||
foreach($account->identities($acc_id) as $ident_id => $identity)
|
||||
{
|
||||
return $ident_id;
|
||||
}
|
||||
}
|
||||
throw new \Exception("No IMAP account found for user #$user", 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an attachment name into an upload array for mail_compose::compose
|
||||
*
|
||||
* @param string[] $attachments either "/mail/attachments/<token>" / file in temp_dir or VFS path
|
||||
* @param ?string $attachmentType "attach" (default), "link", "share_ro", "share_rw"
|
||||
* @param ?string $expiration "YYYY-mm-dd" or e.g. "+2days"
|
||||
* @param ?string $password optional password for the share
|
||||
* @param bool $compose true: for compose window, false: to send
|
||||
* @return array with values for keys "file", "name", "filemode", "expiration" and "password"
|
||||
* @throws Exception if file not found or unreadable
|
||||
*/
|
||||
protected static function prepareAttachments(array $attachments, string $attachmentType=null, string $expiration=null, string $password=null, bool $compose=true)
|
||||
{
|
||||
$ret = [];
|
||||
foreach($attachments as $attachment)
|
||||
{
|
||||
if (preg_match('#^/mail/attachments/(([^/]+)--[^/.-]{6,})$#', $attachment, $matches))
|
||||
{
|
||||
if (!file_exists($path=$GLOBALS['egw_info']['server']['temp_dir'].'/attach--'.$matches[1]))
|
||||
{
|
||||
throw new \Exception("Attachment $attachment NOT found", 400);
|
||||
}
|
||||
if ($compose)
|
||||
{
|
||||
$ret['file'][] = $path;
|
||||
$ret['name'][] = $matches[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
$ret['attachments'][] = [
|
||||
'name' => $matches[2],
|
||||
'type' => Api\Vfs::mime_content_type($path),
|
||||
'file' => $path,
|
||||
'size' => filesize($path),
|
||||
];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Api\Vfs::is_readable($attachment))
|
||||
{
|
||||
throw new \Exception("Attachment $attachment NOT found", 400);
|
||||
}
|
||||
if ($compose)
|
||||
{
|
||||
$ret['file'][] = Api\Vfs::PREFIX.$attachment;
|
||||
$ret['name'][] = Api\Vfs::basename($attachment);
|
||||
}
|
||||
else
|
||||
{
|
||||
$ret['attachments'][] = [
|
||||
'name' => Api\Vfs::basename($attachment),
|
||||
'type' => Api\Vfs::mime_content_type($attachment),
|
||||
'file' => Api\Vfs::PREFIX.$attachment,
|
||||
'size' => filesize(Api\Vfs::PREFIX.$attachment),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ret)
|
||||
{
|
||||
$ret['filemode'] = $attachmentType ?? 'attach';
|
||||
if (!in_array($ret['filemode'], $valid=['attach', 'link', 'share_ro', 'share_rw']))
|
||||
{
|
||||
throw new \Exception("Invalid value '$ret[filemode]' for attachmentType, must be one of: '".implode("', '", $valid)."'", 422);
|
||||
}
|
||||
// EPL share password and expiration
|
||||
$ret['password'] = $password ?: null;
|
||||
if (!empty($expiration))
|
||||
{
|
||||
$ret['expiration'] = (new Api\DateTime($expiration))->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle propfind in the timesheet folder / get request on the collection itself
|
||||
*
|
||||
@ -671,6 +272,69 @@ class ApiHandler extends Api\CalDAV\Handler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process filter GET parameter:
|
||||
* - filter[<json-attribute-name>]=<value>
|
||||
* - filter[%23<custom-field-name]=<value>
|
||||
* - filter[search]=<pattern> with string pattern like for search in the UI
|
||||
* - filter[search][%23<custom-field-name]=<value>
|
||||
* - filter[search][<db-column>]=<value>
|
||||
*
|
||||
* @param array $filter
|
||||
* @return array
|
||||
*/
|
||||
protected function filter2col_filter(array $filter)
|
||||
{
|
||||
$cols = [];
|
||||
foreach($filter as $name => $value)
|
||||
{
|
||||
switch($name)
|
||||
{
|
||||
case 'search':
|
||||
$cols = array_merge($cols, $this->bo->search2criteria($value));
|
||||
break;
|
||||
case 'category':
|
||||
case 'pricelist':
|
||||
$cols[$name === 'pricelist' ? 'pl_id' : 'cat_id'] = $value;
|
||||
break;
|
||||
case 'status':
|
||||
$value = array_map(function ($val) use ($value)
|
||||
{
|
||||
if (!is_numeric($val) || (string)(int)$val !== $val)
|
||||
{
|
||||
$val = array_search($val, $this->bo->status_labels, true);
|
||||
}
|
||||
elseif (isset($this->status_labels[$val]))
|
||||
{
|
||||
$val = (int)$val;
|
||||
}
|
||||
else
|
||||
{
|
||||
$val = false;
|
||||
}
|
||||
if ($val === false)
|
||||
{
|
||||
throw new Api\CalDAV\JsParseException("Invalid status filter value ".json_encode($value));
|
||||
}
|
||||
return (int)$val;
|
||||
}, (array)$value);
|
||||
$cols['ts_status'] = count($value) <= 1 ? array_pop($value) : $value;
|
||||
break;
|
||||
default:
|
||||
if ($name[0] === '#')
|
||||
{
|
||||
$cols[$name] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$cols['ts_'.$name] = $value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the filters from the CalDAV REPORT request
|
||||
*
|
||||
@ -685,7 +349,7 @@ class ApiHandler extends Api\CalDAV\Handler
|
||||
// in case of JSON/REST API pass filters to report
|
||||
if (Api\CalDAV::isJSON() && !empty($options['filters']) && is_array($options['filters']))
|
||||
{
|
||||
$filters += $options['filters']; // using += to no allow overwriting existing filters
|
||||
$filters += $this->filter2col_filter($options['filters']); // using += to not allow overwriting existing filters
|
||||
}
|
||||
elseif (!empty($options['filters']))
|
||||
{
|
||||
|
@ -62,6 +62,7 @@ class JsTimesheet extends Api\CalDAV\JsBase
|
||||
'pricelist' => (int)$timesheet['pl_id'] ?: null,
|
||||
'status' => $bo->status_labels[$timesheet['status']] ?? null,
|
||||
'egroupware.org:customfields' => self::customfields($timesheet),
|
||||
'etag' => ApiHandler::etag($timesheet)
|
||||
]);
|
||||
|
||||
if ($encode)
|
||||
|
Loading…
Reference in New Issue
Block a user