forked from extern/egroupware
Merge remote-tracking branch 'origin/master' into web-components
This commit is contained in:
commit
25773a929f
@ -13,6 +13,7 @@
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Acl;
|
||||
use EGroupware\Api\Contacts\JsContact;
|
||||
|
||||
/**
|
||||
* CalDAV/CardDAV/GroupDAV access: Addressbook handler
|
||||
@ -60,6 +61,11 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
*/
|
||||
var $home_set_pref;
|
||||
|
||||
/**
|
||||
* Prefix for JsCardGroup id
|
||||
*/
|
||||
const JS_CARDGROUP_ID_PREFIX = 'list-';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -72,9 +78,14 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
|
||||
$this->bo = new Api\Contacts();
|
||||
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
self::$path_attr = 'id';
|
||||
self::$path_extension = '';
|
||||
}
|
||||
// since 1.9.007 we allow clients to specify the URL when creating a new contact, as specified by CardDAV
|
||||
// LDAP does NOT have a carddav_name attribute --> stick with id mapped to LDAP attribute uid
|
||||
if (version_compare($GLOBALS['egw_info']['apps']['api']['version'], '1.9.007', '<') ||
|
||||
elseif (version_compare($GLOBALS['egw_info']['apps']['api']['version'], '1.9.007', '<') ||
|
||||
$this->bo->contact_repository != 'sql' ||
|
||||
$this->bo->account_repository != 'sql' && strpos($_SERVER['REQUEST_URI'].'/','/addressbook-accounts/') !== false)
|
||||
{
|
||||
@ -172,12 +183,12 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
|
||||
{
|
||||
--$this->sync_collection_token;
|
||||
$files['sync-token-params'][] = true; // tel get_sync_collection_token that we have more entries
|
||||
$files['sync-token-params'][] = true; // tell get_sync_collection_token that we have more entries
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// return iterator, calling ourself to return result in chunks
|
||||
// return iterator, calling ourselves to return result in chunks
|
||||
$files['files'] = new Api\CalDAV\PropfindIterator($this,$path,$filter,$files['files']);
|
||||
}
|
||||
return true;
|
||||
@ -269,6 +280,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
}
|
||||
|
||||
$is_jscontact = Api\CalDAV::isJSON();
|
||||
foreach($contacts as &$contact)
|
||||
{
|
||||
// remove contact from requested multiget ids, to be able to report not found urls
|
||||
@ -283,15 +295,16 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
continue;
|
||||
}
|
||||
$props = array(
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'),
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', $is_jscontact ? JsContact::MIME_TYPE_JSCARD : 'text/vcard'),
|
||||
'getlastmodified' => $contact['modified'],
|
||||
'displayname' => $contact['n_fn'],
|
||||
);
|
||||
if ($address_data)
|
||||
{
|
||||
$content = $handler->getVCard($contact['id'],$this->charset,false);
|
||||
$content = $is_jscontact ? JsContact::getJsCard($contact['id'], false) :
|
||||
$handler->getVCard($contact['id'],$this->charset,false);
|
||||
$props['getcontentlength'] = bytes($content);
|
||||
$props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
}
|
||||
$files[] = $this->add_resource($path, $contact, $props);
|
||||
}
|
||||
@ -342,7 +355,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
foreach($lists as $list)
|
||||
{
|
||||
$list[self::$path_attr] = $list['list_carddav_name'];
|
||||
$list[self::$path_attr] = $is_jscontact ? self::JS_CARDGROUP_ID_PREFIX.$list['list_id'] : $list['list_carddav_name'];
|
||||
$etag = $list['list_id'].':'.$list['list_etag'];
|
||||
// for all-in-one addressbook, add selected ABs to etag
|
||||
if (isset($filter['owner']) && is_array($filter['owner']))
|
||||
@ -350,16 +363,16 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$etag .= ':'.implode('-',$filter['owner']);
|
||||
}
|
||||
$props = array(
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'),
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', $is_jscontact ? JsContact::MIME_TYPE_JSCARDGROUP : 'text/vcard'),
|
||||
'getlastmodified' => Api\DateTime::to($list['list_modified'],'ts'),
|
||||
'displayname' => $list['list_name'],
|
||||
'getetag' => '"'.$etag.'"',
|
||||
);
|
||||
if ($address_data)
|
||||
{
|
||||
$content = $handler->getGroupVCard($list);
|
||||
$content = $is_jscontact ? JsContact::getJsCardGroup($list, false) : $handler->getGroupVCard($list);
|
||||
$props['getcontentlength'] = bytes($content);
|
||||
$props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
}
|
||||
$files[] = $this->add_resource($path, $list, $props);
|
||||
|
||||
@ -451,7 +464,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
else
|
||||
{
|
||||
switch($filter['attrs']['collation']) // todo: which other collations allowed, we are allways unicode
|
||||
switch($filter['attrs']['collation']) // todo: which other collations allowed, we are always unicode
|
||||
{
|
||||
case 'i;unicode-casemap':
|
||||
default:
|
||||
@ -588,11 +601,22 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
return $contact;
|
||||
}
|
||||
$handler = self::_get_handler();
|
||||
$options['data'] = $contact['list_id'] ? $handler->getGroupVCard($contact) :
|
||||
$handler->getVCard($contact['id'],$this->charset,false);
|
||||
// e.g. Evolution does not understand 'text/vcard'
|
||||
$options['mimetype'] = 'text/x-vcard; charset='.$this->charset;
|
||||
// jsContact or vCard
|
||||
if (($type=Api\CalDAV::isJSON()))
|
||||
{
|
||||
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) :
|
||||
JsContact::getJsCard($contact, $type);
|
||||
$options['mimetype'] = ($contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP :
|
||||
JsContact::MIME_TYPE_JSCARD).';charset=utf-8';
|
||||
}
|
||||
else
|
||||
{
|
||||
$handler = self::_get_handler();
|
||||
$options['data'] = $contact['list_id'] ? $handler->getGroupVCard($contact) :
|
||||
$handler->getVCard($contact['id'], $this->charset, false);
|
||||
// e.g. Evolution does not understand 'text/vcard'
|
||||
$options['mimetype'] = 'text/x-vcard; charset=' . $this->charset;
|
||||
}
|
||||
header('Content-Encoding: identity');
|
||||
header('ETag: "'.$this->get_etag($contact).'"');
|
||||
return true;
|
||||
@ -618,31 +642,68 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
return $oldContact;
|
||||
}
|
||||
|
||||
$handler = self::_get_handler();
|
||||
// Fix for Apple Addressbook
|
||||
$vCard = preg_replace('/item\d\.(ADR|TEL|EMAIL|URL)/', '\1',
|
||||
htmlspecialchars_decode($options['content']));
|
||||
$charset = null;
|
||||
if (!empty($options['content_type']))
|
||||
$type = null;
|
||||
if (($is_json=Api\CalDAV::isJSON($type)))
|
||||
{
|
||||
$content_type = explode(';', $options['content_type']);
|
||||
if (count($content_type) > 1)
|
||||
if (strpos($type, JsContact::MIME_TYPE_JSCARD) === false && strpos($type, JsContact::MIME_TYPE_JSCARDGROUP) === false)
|
||||
{
|
||||
array_shift($content_type);
|
||||
foreach ($content_type as $attribute)
|
||||
if (!empty($id))
|
||||
{
|
||||
trim($attribute);
|
||||
list($key, $value) = explode('=', $attribute);
|
||||
switch (strtolower($key))
|
||||
$type = strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0 ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD;
|
||||
}
|
||||
else
|
||||
{
|
||||
$json = json_decode($options['content'], true);
|
||||
$type = is_array($json['members']) ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD;
|
||||
}
|
||||
}
|
||||
$contact = $type === JsContact::MIME_TYPE_JSCARD ?
|
||||
JsContact::parseJsCard($options['content']) : JsContact::parseJsCardGroup($options['content']);
|
||||
|
||||
if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0)
|
||||
{
|
||||
$id = substr($id, strlen(self::JS_CARDGROUP_ID_PREFIX));
|
||||
}
|
||||
elseif (empty($id))
|
||||
{
|
||||
$contact['cardav_name'] = $contact['uid'].'.vcf';
|
||||
$contact['owner'] = $user;
|
||||
}
|
||||
|
||||
/* uncomment to return parsed data for testing
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($contact, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
return "200 Ok";
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
$handler = self::_get_handler();
|
||||
// Fix for Apple Addressbook
|
||||
$vCard = preg_replace('/item\d\.(ADR|TEL|EMAIL|URL)/', '\1',
|
||||
htmlspecialchars_decode($options['content']));
|
||||
$charset = null;
|
||||
if (!empty($options['content_type']))
|
||||
{
|
||||
$content_type = explode(';', $options['content_type']);
|
||||
if (count($content_type) > 1)
|
||||
{
|
||||
array_shift($content_type);
|
||||
foreach ($content_type as $attribute)
|
||||
{
|
||||
case 'charset':
|
||||
$charset = strtoupper(substr($value,1,-1));
|
||||
trim($attribute);
|
||||
list($key, $value) = explode('=', $attribute);
|
||||
switch (strtolower($key))
|
||||
{
|
||||
case 'charset':
|
||||
$charset = strtoupper(substr($value,1,-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$contact = $handler->vcardtoegw($vCard, $charset);
|
||||
$contact = $handler->vcardtoegw($vCard, $charset);
|
||||
}
|
||||
|
||||
if (is_array($oldContact) || ($oldContact = $this->bo->read(array('contact_uid' => $contact['uid']))))
|
||||
{
|
||||
@ -655,7 +716,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$contactId = -1;
|
||||
$retval = '201 Created';
|
||||
}
|
||||
$is_group = $contact['##X-ADDRESSBOOKSERVER-KIND'] == 'group';
|
||||
$is_group = isset($type) && $type === JsContact::MIME_TYPE_JSCARDGROUP || $contact['##X-ADDRESSBOOKSERVER-KIND'] === 'group';
|
||||
if ($oldContact && $is_group !== isset($oldContact['list_id']))
|
||||
{
|
||||
throw new Api\Exception\AssertionFailed(__METHOD__."(,'$id',$user,'$prefix') can contact into group or visa-versa!");
|
||||
@ -723,7 +784,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
if ($this->http_if_match) $contact['etag'] = self::etag2value($this->http_if_match);
|
||||
|
||||
$contact['photo_unchanged'] = false; // photo needs saving
|
||||
// ignore photo for JSON/REST, it's not yet supported
|
||||
$contact['photo_unchanged'] = $is_json; //false; // photo needs saving
|
||||
if (!($save_ok = $is_group ? $this->save_group($contact, $oldContact) : $this->bo->save($contact)))
|
||||
{
|
||||
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($contact).") failed, Ok=$save_ok");
|
||||
@ -742,7 +804,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
if (($contact = $this->bo->read_list($save_ok)))
|
||||
{
|
||||
// re-read group to get correct etag (not dublicate etag code here)
|
||||
// re-read group to get correct etag (not duplicate etag code here)
|
||||
$contact = $this->read($contact['list_'.self::$path_attr], $options['path']);
|
||||
}
|
||||
}
|
||||
@ -753,15 +815,18 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
//error_log(__METHOD__."(, $id, '$user') read(_list)($save_ok) returned ".array2string($contact));
|
||||
}
|
||||
|
||||
// send evtl. necessary respose headers: Location, etag, ...
|
||||
$this->put_response_headers($contact, $options['path'], $retval, self::$path_attr != 'id');
|
||||
// send evtl. necessary response headers: Location, etag, ...
|
||||
$this->put_response_headers($contact, $options['path'], $retval,
|
||||
// JSON uses 'id', while CardDAV uses carddav_name !== 'id'
|
||||
(self::$path_attr !== 'id') === !$is_json, null,
|
||||
$is_group && $is_json ? self::JS_CARDGROUP_ID_PREFIX : '');
|
||||
|
||||
if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval));
|
||||
return $retval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save distribition-list / group
|
||||
* Save distribution-list / group
|
||||
*
|
||||
* @param array $contact
|
||||
* @param array|false $oldContact
|
||||
@ -780,18 +845,21 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$contact['owner'], null, $data)))
|
||||
{
|
||||
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
|
||||
$new_members = $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
||||
if ($new_members[1] == ':' && ($n = unserialize($new_members)))
|
||||
$new_members = $contact['members'] ?: $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
||||
if (is_string($new_members) && $new_members[1] === ':' && ($n = unserialize($new_members)))
|
||||
{
|
||||
$new_members = $n['values'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$new_members = array($new_members);
|
||||
$new_members = (array)$new_members;
|
||||
}
|
||||
foreach($new_members as &$uid)
|
||||
{
|
||||
$uid = substr($uid,9); // cut off "urn:uuid:" prefix
|
||||
if (substr($uid, 0, 9) === 'urn:uuid:')
|
||||
{
|
||||
$uid = substr($uid,9); // cut off "urn:uuid:" prefix
|
||||
}
|
||||
}
|
||||
if ($oldContact)
|
||||
{
|
||||
@ -828,7 +896,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
// reread as update of list-members updates etag and modified
|
||||
if (($contact = $this->bo->read_list($list_id)))
|
||||
{
|
||||
// re-read group to get correct etag (not dublicate etag code here)
|
||||
// re-read group to get correct etag (not duplicate etag code here)
|
||||
$contact = $this->read($contact['list_'.self::$path_attr]);
|
||||
}
|
||||
}
|
||||
@ -1023,7 +1091,25 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
unset($tids[Api\Contacts::DELETED_TYPE]);
|
||||
$non_deleted_tids = array_keys($tids);
|
||||
}
|
||||
$contact = $this->bo->read(array(self::$path_attr => $id, 'tid' => $non_deleted_tids));
|
||||
$keys = ['tid' => $non_deleted_tids];
|
||||
|
||||
// with REST/JSON we only use our id, but DELETE request has neither Accept nor Content-Type header to detect JSON request
|
||||
if (preg_match('/^('.self::JS_CARDGROUP_ID_PREFIX.')?(\d+)$/', $id, $matches))
|
||||
{
|
||||
if (!empty($matches[1]))
|
||||
{
|
||||
$keys = ['list_id' => $matches[2]];
|
||||
}
|
||||
else
|
||||
{
|
||||
$keys['id'] = $id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$keys[self::$path_attr] = $id;
|
||||
}
|
||||
$contact = isset($keys['list_id']) ? false: $this->bo->read($keys);
|
||||
|
||||
// if contact not found and accounts stored NOT like contacts, try reading it without path-extension as id
|
||||
if (is_null($contact) && $this->bo->so_accounts && ($c = $this->bo->read($test=basename($id, '.vcf'))))
|
||||
@ -1043,12 +1129,13 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$limit_in_ab[] = $GLOBALS['egw_info']['user']['account_id'];
|
||||
}
|
||||
/* we are currently not syncing distribution-lists/groups to /addressbook/ as
|
||||
* Apple clients use that only as directory gateway
|
||||
elseif ($account_lid == 'addressbook') // /addressbook/ contains all readably contacts
|
||||
* Apple clients use that only as directory gateway*/
|
||||
elseif (Api\CalDAV::isJSON() && $account_lid == 'addressbook') // /addressbook/ contains all readably contacts
|
||||
{
|
||||
$limit_in_ab = array_keys($this->bo->grants);
|
||||
}*/
|
||||
if (!$contact && ($contact = $this->bo->read_lists(array('list_'.self::$path_attr => $id),'contact_uid',$limit_in_ab)))
|
||||
}
|
||||
if (!$contact && ($contact = $this->bo->read_lists(isset($keys['list_id']) ? $keys :
|
||||
['list_'.self::$path_attr => $id],'contact_uid',$limit_in_ab)))
|
||||
{
|
||||
$contact = array_shift($contact);
|
||||
$contact['n_fn'] = $contact['n_family'] = $contact['list_name'];
|
||||
|
488
api/js/etemplate/et2_widget_placeholder.ts
Normal file
488
api/js/etemplate/et2_widget_placeholder.ts
Normal file
@ -0,0 +1,488 @@
|
||||
/**
|
||||
* EGroupware eTemplate2 - JS Placeholder widgets
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package etemplate
|
||||
* @subpackage api
|
||||
* @link https://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
* @copyright Nathan Gray 2021
|
||||
*/
|
||||
|
||||
/*egw:uses
|
||||
et2_core_inputWidget;
|
||||
et2_core_valueWidget;
|
||||
et2_widget_description;
|
||||
*/
|
||||
|
||||
import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget";
|
||||
import {ClassWithAttributes} from "./et2_core_inheritance";
|
||||
import {et2_dialog} from "./et2_widget_dialog";
|
||||
import {et2_inputWidget} from "./et2_core_inputWidget";
|
||||
import type {egw} from "../jsapi/egw_global";
|
||||
import {et2_selectbox} from "./et2_widget_selectbox";
|
||||
import {et2_description} from "./et2_widget_description";
|
||||
import {et2_link_entry} from "./et2_widget_link";
|
||||
import type {et2_button} from "./et2_widget_button";
|
||||
|
||||
|
||||
/**
|
||||
* Display a dialog to choose a placeholder
|
||||
*/
|
||||
export class et2_placeholder_select extends et2_inputWidget
|
||||
{
|
||||
static readonly _attributes : any = {
|
||||
insert_callback: {
|
||||
"name": "Insert callback",
|
||||
"description": "Method called with the selected placeholder text",
|
||||
"type": "js"
|
||||
},
|
||||
dialog_title: {
|
||||
"name": "Dialog title",
|
||||
"type": "string",
|
||||
"default": "Insert Placeholder"
|
||||
}
|
||||
};
|
||||
|
||||
static placeholders : Object | null = null;
|
||||
|
||||
button : JQuery;
|
||||
submit_callback : any;
|
||||
dialog : et2_dialog;
|
||||
protected value : any;
|
||||
|
||||
protected LIST_URL = 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders';
|
||||
protected TEMPLATE = '/api/templates/default/insert_merge_placeholder.xet?1';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param _parent
|
||||
* @param _attrs
|
||||
* @memberOf et2_vfsSelect
|
||||
*/
|
||||
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
|
||||
{
|
||||
// Call the inherited constructor
|
||||
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_placeholder_select._attributes, _child || {}));
|
||||
|
||||
// Allow no child widgets
|
||||
this.supportedWidgetClasses = [];
|
||||
}
|
||||
|
||||
_content(_content, _callback)
|
||||
{
|
||||
let self = this;
|
||||
if(this.dialog && this.dialog.div)
|
||||
{
|
||||
this.dialog.div.dialog('close');
|
||||
}
|
||||
|
||||
var callback = _callback || this._buildDialog;
|
||||
if(et2_placeholder_select.placeholders === null)
|
||||
{
|
||||
this.egw().loading_prompt('placeholder_select', true, '', 'body');
|
||||
this.egw().json(
|
||||
this.LIST_URL,
|
||||
[],
|
||||
function(_content)
|
||||
{
|
||||
this.egw().loading_prompt('placeholder_select', false);
|
||||
et2_placeholder_select.placeholders = _content;
|
||||
callback.apply(self, arguments);
|
||||
}.bind(this)
|
||||
).sendRequest(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._buildDialog(et2_placeholder_select.placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds placeholder selection dialog
|
||||
*
|
||||
* @param {object} _data content
|
||||
*/
|
||||
protected _buildDialog(_data)
|
||||
{
|
||||
|
||||
let self = this;
|
||||
let buttons = [
|
||||
{
|
||||
text: this.egw().lang("Insert"),
|
||||
id: "submit",
|
||||
image: "export"
|
||||
}
|
||||
];
|
||||
let extra_buttons_action = {};
|
||||
|
||||
if(this.options.extra_buttons && this.options.method)
|
||||
{
|
||||
for(let i = 0; i < this.options.extra_buttons.length; i++)
|
||||
{
|
||||
delete (this.options.extra_buttons[i]['click']);
|
||||
buttons.push(this.options.extra_buttons[i]);
|
||||
extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id'];
|
||||
}
|
||||
|
||||
}
|
||||
buttons.push({text: this.egw().lang("Cancel"), id: "cancel", image: "cancel"});
|
||||
|
||||
let data = {
|
||||
content: {app: '', group: '', entry: {}},
|
||||
sel_options: {app: [], group: []},
|
||||
modifications: {outer_box: {entry: {}}}
|
||||
};
|
||||
|
||||
Object.keys(_data).map((key) =>
|
||||
{
|
||||
data.sel_options.app.push(
|
||||
{
|
||||
value: key,
|
||||
label: this.egw().lang(key)
|
||||
});
|
||||
});
|
||||
data.sel_options.group = this._get_group_options(Object.keys(_data)[0]);
|
||||
data.content.app = data.sel_options.app[0].value;
|
||||
data.content.group = data.sel_options.group[0].value;
|
||||
data.content.entry = data.modifications.outer_box.entry.only_app = data.content.app;
|
||||
data.modifications.outer_box.entry.application_list = Object.keys(_data);
|
||||
|
||||
// callback for dialog
|
||||
this.submit_callback = function(submit_button_id, submit_value)
|
||||
{
|
||||
if((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value)
|
||||
{
|
||||
this._do_insert_callback(submit_value);
|
||||
return true;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.dialog = <et2_dialog>et2_createWidget("dialog",
|
||||
{
|
||||
callback: this.submit_callback,
|
||||
title: this.options.dialog_title || this.egw().lang("Insert Placeholder"),
|
||||
buttons: buttons,
|
||||
minWidth: 500,
|
||||
minHeight: 400,
|
||||
width: 400,
|
||||
value: data,
|
||||
template: this.egw().webserverUrl + this.TEMPLATE,
|
||||
resizable: true
|
||||
}, et2_dialog._create_parent('api'));
|
||||
this.dialog.template.uniqueId = 'api.insert_merge_placeholder';
|
||||
|
||||
|
||||
this.dialog.div.on('load', this._on_template_load.bind(this));
|
||||
}
|
||||
|
||||
doLoadingFinished()
|
||||
{
|
||||
this._content.call(this, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-load of the dialog
|
||||
* Bind internal events, set some things that are difficult to do in the template
|
||||
*/
|
||||
_on_template_load()
|
||||
{
|
||||
let app = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("app");
|
||||
let group = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("group");
|
||||
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
|
||||
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder");
|
||||
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
|
||||
|
||||
|
||||
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value()));
|
||||
|
||||
// Further setup / styling that can't be done in etemplate
|
||||
this.dialog.template.DOMContainer.style.display = "flex";
|
||||
this.dialog.template.DOMContainer.firstChild.style.display = "flex";
|
||||
group.getDOMNode().size = 5;
|
||||
placeholder_list.getDOMNode().size = 5;
|
||||
|
||||
// Bind some handlers
|
||||
app.onchange = (node, widget) =>
|
||||
{
|
||||
group.set_select_options(this._get_group_options(widget.get_value()));
|
||||
entry.set_value({app: widget.get_value()});
|
||||
}
|
||||
group.onchange = (select_node, select_widget) =>
|
||||
{
|
||||
console.log(this, arguments);
|
||||
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value()));
|
||||
preview.set_value("");
|
||||
}
|
||||
placeholder_list.onchange = this._on_placeholder_select.bind(this);
|
||||
entry.onchange = this._on_placeholder_select.bind(this);
|
||||
(<et2_button>this.dialog.template.widgetContainer.getDOMWidgetById("insert_placeholder")).onclick = () =>
|
||||
{
|
||||
this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder").getDOMNode().textContent);
|
||||
};
|
||||
(<et2_button>this.dialog.template.widgetContainer.getDOMWidgetById("insert_content")).onclick = () =>
|
||||
{
|
||||
this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_content").getDOMNode().textContent);
|
||||
};
|
||||
|
||||
this._on_placeholder_select();
|
||||
}
|
||||
|
||||
/**
|
||||
* User has selected a placeholder
|
||||
* Update the UI, and if they have an entry selected do the replacement and show that.
|
||||
*/
|
||||
_on_placeholder_select()
|
||||
{
|
||||
let app = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("app");
|
||||
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
|
||||
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
|
||||
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder");
|
||||
let preview_content = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_content");
|
||||
|
||||
// Show the selected placeholder
|
||||
this.set_value(placeholder_list.get_value());
|
||||
preview.set_value(placeholder_list.get_value());
|
||||
preview.getDOMNode().parentNode.style.visibility = placeholder_list.get_value().trim() ? null : 'hidden';
|
||||
|
||||
if(placeholder_list.get_value() && entry.get_value())
|
||||
{
|
||||
// Show the selected placeholder replaced with value from the selected entry
|
||||
this.egw().json(
|
||||
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
|
||||
[app.get_value(), placeholder_list.get_value(), entry.get_value()],
|
||||
function(_content)
|
||||
{
|
||||
preview_content.set_value(_content);
|
||||
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
|
||||
}.bind(this)
|
||||
).sendRequest(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No value, hide the row
|
||||
preview_content.getDOMNode().parentNode.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of placeholder groups under the selected application
|
||||
* @param appname
|
||||
* @returns {value:string, label:string}[]
|
||||
*/
|
||||
_get_group_options(appname : string)
|
||||
{
|
||||
let options = [];
|
||||
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
|
||||
{
|
||||
options.push(
|
||||
{
|
||||
value: key,
|
||||
label: this.egw().lang(key)
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of placeholders under the given application + group
|
||||
*
|
||||
* @param appname
|
||||
* @param group
|
||||
* @returns {value:string, label:string}[]
|
||||
*/
|
||||
_get_placeholders(appname : string, group : string)
|
||||
{
|
||||
let options = [];
|
||||
Object.keys(et2_placeholder_select.placeholders[appname][group]).map((key) =>
|
||||
{
|
||||
options.push(
|
||||
{
|
||||
value: key,
|
||||
label: et2_placeholder_select.placeholders[appname][group][key]
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct insert text call the insert callback with it
|
||||
*
|
||||
* @param dialog_values
|
||||
*/
|
||||
_do_insert_callback(dialog_values : Object)
|
||||
{
|
||||
this.options.insert_callback(this.get_value());
|
||||
}
|
||||
|
||||
set_value(value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
getValue()
|
||||
{
|
||||
return this.value;
|
||||
}
|
||||
};
|
||||
et2_register_widget(et2_placeholder_select, ["placeholder-select"]);
|
||||
|
||||
/**
|
||||
* Display a dialog to choose from a set list of placeholder snippets
|
||||
*/
|
||||
export class et2_placeholder_snippet_select extends et2_placeholder_select
|
||||
{
|
||||
static readonly _attributes : any = {
|
||||
dialog_title: {
|
||||
"default": "Insert address"
|
||||
}
|
||||
};
|
||||
static placeholders = {
|
||||
"addressbook": {
|
||||
"addresses": {
|
||||
"{{n_fn}}\n{{adr_one_street}}{{NELF adr_one_street2}}\n{{adr_one_formatted}}": "Work address",
|
||||
"{{n_fn}}\n{{adr_two_street}}{{NELF adr_two_street2}}\n{{adr_two_formatted}}": "Home address",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
button : JQuery;
|
||||
submit_callback : any;
|
||||
dialog : et2_dialog;
|
||||
protected value : any;
|
||||
|
||||
protected LIST_URL = 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders';
|
||||
protected TEMPLATE = '/api/templates/default/placeholder_snippet.xet?1';
|
||||
|
||||
/**
|
||||
* Post-load of the dialog
|
||||
* Bind internal events, set some things that are difficult to do in the template
|
||||
*/
|
||||
_on_template_load()
|
||||
{
|
||||
let app = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("app");
|
||||
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
|
||||
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_content");
|
||||
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
|
||||
|
||||
|
||||
placeholder_list.set_select_options(this._get_placeholders("addressbook", "addresses"));
|
||||
|
||||
// Further setup / styling that can't be done in etemplate
|
||||
app.getInputNode().setAttribute("readonly", true);
|
||||
this.dialog.template.DOMContainer.style.display = "flex";
|
||||
this.dialog.template.DOMContainer.firstChild.style.display = "flex";
|
||||
placeholder_list.getDOMNode().size = 5;
|
||||
|
||||
// Bind some handlers
|
||||
app.onchange = (node, widget) =>
|
||||
{
|
||||
entry.set_value({app: widget.get_value()});
|
||||
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), "addresses"));
|
||||
}
|
||||
placeholder_list.onchange = this._on_placeholder_select.bind(this);
|
||||
entry.onchange = this._on_placeholder_select.bind(this);
|
||||
|
||||
this._on_placeholder_select();
|
||||
}
|
||||
|
||||
/**
|
||||
* User has selected a placeholder
|
||||
* Update the UI, and if they have an entry selected do the replacement and show that.
|
||||
*/
|
||||
_on_placeholder_select()
|
||||
{
|
||||
let app = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("app");
|
||||
let entry = <et2_link_entry>this.dialog.template.widgetContainer.getDOMWidgetById("entry");
|
||||
let placeholder_list = <et2_selectbox>this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list");
|
||||
let preview = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder");
|
||||
let preview_content = <et2_description>this.dialog.template.widgetContainer.getDOMWidgetById("preview_content");
|
||||
|
||||
if(placeholder_list.get_value() && entry.get_value())
|
||||
{
|
||||
// Show the selected placeholder replaced with value from the selected entry
|
||||
this.egw().json(
|
||||
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
|
||||
[app.get_value(), placeholder_list.get_value(), entry.get_value()],
|
||||
function(_content)
|
||||
{
|
||||
this.set_value(_content);
|
||||
preview_content.set_value(_content);
|
||||
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
|
||||
}.bind(this)
|
||||
).sendRequest(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No value, hide the row
|
||||
preview_content.getDOMNode().parentNode.style.visibility = 'hidden';
|
||||
}
|
||||
if(!entry.get_value())
|
||||
{
|
||||
entry.search.get(0).focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of placeholder groups under the selected application
|
||||
* @param appname
|
||||
* @returns {value:string, label:string}[]
|
||||
*/
|
||||
_get_group_options(appname : string)
|
||||
{
|
||||
let options = [];
|
||||
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
|
||||
{
|
||||
options.push(
|
||||
{
|
||||
value: key,
|
||||
label: this.egw().lang(key)
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of placeholders under the given application + group
|
||||
*
|
||||
* @param appname
|
||||
* @param group
|
||||
* @returns {value:string, label:string}[]
|
||||
*/
|
||||
_get_placeholders(appname : string, group : string)
|
||||
{
|
||||
let options = [];
|
||||
Object.keys(et2_placeholder_snippet_select.placeholders[appname][group]).map((key) =>
|
||||
{
|
||||
options.push(
|
||||
{
|
||||
value: key,
|
||||
label: et2_placeholder_snippet_select.placeholders[appname][group][key]
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct insert text call the insert callback with it
|
||||
*
|
||||
* @param dialog_values
|
||||
*/
|
||||
_do_insert_callback(dialog_values : Object)
|
||||
{
|
||||
this.options.insert_callback(this.get_value());
|
||||
}
|
||||
|
||||
set_value(value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
getValue()
|
||||
{
|
||||
return this.value;
|
||||
}
|
||||
};
|
||||
et2_register_widget(et2_placeholder_snippet_select, ["placeholder-snippet"]);
|
@ -67,6 +67,7 @@ import './et2_widget_image';
|
||||
import './et2_widget_iframe';
|
||||
import './et2_widget_file';
|
||||
import './et2_widget_link';
|
||||
import './et2_widget_placeholder';
|
||||
import './et2_widget_progress';
|
||||
import './et2_widget_portlet';
|
||||
import './et2_widget_selectAccount';
|
||||
|
@ -837,6 +837,18 @@ window.fw_base = (function(){ "use strict"; return Class.extend(
|
||||
window.location = _url;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method only used for status app when it tries to broadcast data to users
|
||||
* avoiding throwing exceptions for users whom might have no status app access
|
||||
*
|
||||
* @param {type} _data
|
||||
* @returns {undefined}
|
||||
*/
|
||||
execPushBroadcastAppStatus: function(_data)
|
||||
{
|
||||
if (app.status) app.status.mergeContent(_data, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the active framework application to the application specified by _app
|
||||
*
|
||||
|
@ -519,18 +519,6 @@ import "sortablejs/Sortable.min.js";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method only used for status app when it tries to broadcast data to users
|
||||
* avoiding throwing exceptions for users whom might have no status app access
|
||||
*
|
||||
* @param {type} _data
|
||||
* @returns {undefined}
|
||||
*/
|
||||
execPushBroadcastAppStatus: function(_data)
|
||||
{
|
||||
if (app.status) app.status.mergeContent(_data, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get color scheme
|
||||
* @return {string|null} returns active color scheme mode or null in case browser not supporting it
|
||||
|
@ -14,7 +14,7 @@ $setup_info['api']['title'] = 'EGroupware API';
|
||||
$setup_info['api']['version'] = '21.1.001';
|
||||
$setup_info['api']['versions']['current_header'] = '1.29';
|
||||
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
|
||||
$setup_info['api']['versions']['maintenance_release'] = '21.1.20210723';
|
||||
$setup_info['api']['versions']['maintenance_release'] = '21.1.20210923';
|
||||
$setup_info['api']['enable'] = 3;
|
||||
$setup_info['api']['app_order'] = 1;
|
||||
$setup_info['api']['license'] = 'GPL';
|
||||
|
@ -18,15 +18,17 @@ use EGroupware\Api\CalDAV\Principals;
|
||||
|
||||
// explicit import non-namespaced classes
|
||||
require_once(__DIR__.'/WebDAV/Server.php');
|
||||
|
||||
use EGroupware\Api\Contacts\JsContactParseException;
|
||||
use HTTP_WebDAV_Server;
|
||||
use calendar_hooks;
|
||||
|
||||
/**
|
||||
* EGroupware: GroupDAV access
|
||||
* EGroupware: CalDAV/CardDAV server
|
||||
*
|
||||
* Using a modified PEAR HTTP/WebDAV/Server class from API!
|
||||
*
|
||||
* One can use the following url's releative (!) to http://domain.com/egroupware/groupdav.php
|
||||
* One can use the following URLs relative (!) to https://example.org/egroupware/groupdav.php
|
||||
*
|
||||
* - / base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here
|
||||
* - /principals/ principal-collection-set for WebDAV ACL
|
||||
@ -49,10 +51,25 @@ use calendar_hooks;
|
||||
* - /(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
|
||||
*
|
||||
* Shared addressbooks or calendars are only shown in in users home-set, if he subscribed to it via his CalDAV preferences!
|
||||
* Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
|
||||
*
|
||||
* Calling one of the above collections with a GET request / regular browser generates an automatic index
|
||||
* from the data of a allprop PROPFIND, allow to browse CalDAV/CardDAV/GroupDAV tree with a regular browser.
|
||||
* from the data of a allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
|
||||
*
|
||||
* Using EGroupware CalDAV/CardDAV as REST API: currently only for contacts
|
||||
* ===========================================
|
||||
* GET requests to collections with an "Accept: application/json" header return a JSON response similar to a PROPFIND
|
||||
* 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 addressbook 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
|
||||
* POST requests to collection with a "Content-Type: application/json" header add new entries in addressbook or calendar collections
|
||||
* (Location header in response gives URL of new resource)
|
||||
* GET requests with an "Accept: application/json" header can be used to retrieve single resources / JsContact or JsCalendar schema
|
||||
* PUT requests with a "Content-Type: application/json" header allow modifying single resources
|
||||
* DELETE requests delete single resources
|
||||
*
|
||||
* Permanent error_log() calls should use groupdav->log($str) instead, to be send to PHP error_log()
|
||||
* and our request-log (prefixed with "### " after request and response, like exceptions).
|
||||
@ -976,6 +993,23 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
parent::http_PROPFIND('REPORT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client want or sends JSON
|
||||
*
|
||||
* @param string &$type=null
|
||||
* @return bool|string false: no json, true: application/json, string: application/(string)+json
|
||||
*/
|
||||
public static function isJSON(string &$type=null)
|
||||
{
|
||||
if (!isset($type))
|
||||
{
|
||||
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ?
|
||||
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
|
||||
}
|
||||
return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ?
|
||||
(empty($matches[1]) ? true : $matches[2]) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET method handler
|
||||
*
|
||||
@ -989,6 +1023,10 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
$id = $app = $user = null;
|
||||
if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals')
|
||||
{
|
||||
if (($json = self::isJSON()))
|
||||
{
|
||||
return $this->jsonIndex($options, $json === 'pretty');
|
||||
}
|
||||
return $this->autoindex($options);
|
||||
}
|
||||
if (($handler = self::app_handler($app)))
|
||||
@ -999,6 +1037,173 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
return '501 Not Implemented';
|
||||
}
|
||||
|
||||
const JSON_OPTIONS = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR;
|
||||
const JSON_OPTIONS_PRETTY = JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* JSON encode incl. modified pretty-print
|
||||
*
|
||||
* @param $data
|
||||
* @return array|string|string[]|null
|
||||
*/
|
||||
public static function json_encode($data, $pretty = true)
|
||||
{
|
||||
if (!$pretty)
|
||||
{
|
||||
return json_encode($data, self::JSON_OPTIONS);
|
||||
}
|
||||
return preg_replace('/: {\n\s*(.*?)\n\s*(},?\n)/', ': { $1 $2',
|
||||
json_encode($data, self::JSON_OPTIONS_PRETTY));
|
||||
}
|
||||
|
||||
/**
|
||||
* PROPFIND/REPORT like output for GET request on collection with Accept: application/(.*+)?json
|
||||
*
|
||||
* For addressbook-collections we give a REST-like output without any other properties
|
||||
* {
|
||||
* "/addressbook/ID": {
|
||||
* JsContact-data
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @param array $options
|
||||
* @param bool $pretty =false true: pretty-print JSON
|
||||
* @return bool|string|void
|
||||
*/
|
||||
protected function jsonIndex(array $options, bool $pretty)
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$is_addressbook = strpos($options['path'], '/addressbook') !== false;
|
||||
$propfind_options = array(
|
||||
'path' => $options['path'],
|
||||
'depth' => 1,
|
||||
'props' => $is_addressbook ? [
|
||||
'address-data' => self::mkprop(self::CARDDAV, 'address-data', '')
|
||||
] : 'all',
|
||||
'other' => [],
|
||||
);
|
||||
|
||||
// sync-collection report via GET parameter sync-token
|
||||
if (isset($_GET['sync-token']))
|
||||
{
|
||||
$propfind_options['root'] = ['name' => 'sync-collection'];
|
||||
$propfind_options['other'][] = ['name' => 'sync-token', 'data' => $_GET['sync-token']];
|
||||
$propfind_options['other'][] = ['name' => 'sync-level', 'data' => $_GET['sync-level'] ?? 1];
|
||||
|
||||
// clients want's pagination
|
||||
if (isset($_GET['nresults']))
|
||||
{
|
||||
$propfind_options['other'][] = ['name' => 'nresults', 'data' => (int)$_GET['nresults']];
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: client want data filtered
|
||||
if (isset($_GET['filters']))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// properties to NOT get the default address-data for addressbook-collections and "all" for the rest
|
||||
if (isset($_GET['props']))
|
||||
{
|
||||
$propfind_options['props'] = [];
|
||||
foreach((array)$_GET['props'] as $value)
|
||||
{
|
||||
$parts = explode(':', $value);
|
||||
$name = array_pop($parts);
|
||||
$ns = $parts ? implode(':', $parts) : 'DAV:';
|
||||
$propfind_options['props'][$name] = self::mkprop($ns, $name, '');
|
||||
}
|
||||
}
|
||||
$files = array();
|
||||
if (($ret = $this->REPORT($propfind_options,$files)) !== true)
|
||||
{
|
||||
return $ret; // no collection
|
||||
}
|
||||
// set start as prefix, to no have it in front of exceptions
|
||||
$prefix = "{\n\t\"responses\": {\n";
|
||||
foreach($files['files'] as $resource)
|
||||
{
|
||||
$path = $resource['path'];
|
||||
echo $prefix.json_encode($path, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).': ';
|
||||
if (!isset($resource['props']))
|
||||
{
|
||||
echo 'null'; // deleted in sync-report
|
||||
}
|
||||
else
|
||||
{
|
||||
$props = $propfind_options['props'] === 'all' ? $resource['props'] :
|
||||
array_intersect_key($resource['props'], $propfind_options['props']);
|
||||
|
||||
if (count($props) > 1)
|
||||
{
|
||||
$props = self::jsonProps($props);
|
||||
}
|
||||
else
|
||||
{
|
||||
$props = current($props)['val'];
|
||||
}
|
||||
echo self::json_encode($props, $pretty);
|
||||
}
|
||||
$prefix = ",\n";
|
||||
}
|
||||
// happens with an empty response
|
||||
if ($prefix !== ",\n")
|
||||
{
|
||||
echo $prefix;
|
||||
$prefix = ",\n";
|
||||
}
|
||||
echo "\n\t}";
|
||||
// add sync-token and more-results to response
|
||||
if (isset($files['sync-token']))
|
||||
{
|
||||
echo $prefix."\t".'"sync-token": '.json_encode(!is_callable($files['sync-token']) ? $files['sync-token'] :
|
||||
call_user_func_array($files['sync-token'], (array)$files['sync-token-params']), JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
echo "\n}";
|
||||
|
||||
// exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nicer way to display/encode DAV properties
|
||||
*
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function jsonProps(array $props)
|
||||
{
|
||||
$json = [];
|
||||
foreach($props as $key => $prop)
|
||||
{
|
||||
if (is_scalar($prop['val']))
|
||||
{
|
||||
$value = is_int($key) && $prop['val'] === '' ?
|
||||
/*$prop['ns'].':'.*/$prop['name'] : $prop['val'];
|
||||
}
|
||||
// check if this is a property-object
|
||||
elseif (count($prop) === 3 && isset($prop['name']) && isset($prop['ns']) && isset($prop['val']))
|
||||
{
|
||||
$value = $prop['name'] === 'address-data' ? $prop['val'] : self::jsonProps($prop['val']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $prop;
|
||||
}
|
||||
if (is_int($key))
|
||||
{
|
||||
$json[] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$json[/*($prop['ns'] === 'DAV:' ? '' : $prop['ns'].':').*/$prop['name']] = $value;
|
||||
}
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an automatic index (listing and properties) for a collection
|
||||
*
|
||||
@ -1218,7 +1423,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')
|
||||
if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork' ||
|
||||
substr($options['path'], -1) === '/' && self::isJSON())
|
||||
{
|
||||
$_GET['add-member'] = ''; // otherwise we give no Location header
|
||||
return $this->PUT($options);
|
||||
@ -2236,16 +2442,37 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
$headline = null;
|
||||
_egw_log_exception($e,$headline);
|
||||
|
||||
// exception handler sending message back to the client as basic auth message
|
||||
$error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage());
|
||||
header('WWW-Authenticate: Basic realm="'.$headline.': '.$error.'"');
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
header('X-WebDAV-Status: 401 Unauthorized', true);
|
||||
|
||||
if (self::isJSON())
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (is_a($e, JsContactParseException::class))
|
||||
{
|
||||
$status = '422 Unprocessable Entity';
|
||||
}
|
||||
else
|
||||
{
|
||||
$status = '500 Internal Server Error';
|
||||
}
|
||||
http_response_code((int)$status);
|
||||
echo self::json_encode([
|
||||
'error' => $e->getCode() ?: (int)$status,
|
||||
'message' => $e->getMessage(),
|
||||
]+($e->getPrevious() ? [
|
||||
'original' => get_class($e->getPrevious()).': '.$e->getPrevious()->getMessage(),
|
||||
] : []), JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
else
|
||||
{
|
||||
// exception handler sending message back to the client as basic auth message
|
||||
$error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage());
|
||||
header('WWW-Authenticate: Basic realm="' . $headline . ': ' . $error . '"');
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
header('X-WebDAV-Status: 401 Unauthorized', true);
|
||||
}
|
||||
// if our own logging is active, log the request plus a trace, if enabled in server-config
|
||||
if (self::$request_starttime && isset(self::$instance))
|
||||
{
|
||||
self::$instance->_http_status = '401 Unauthorized'; // to correctly log it
|
||||
self::$instance->_http_status = self::isJSON() ? $status : '401 Unauthorized'; // to correctly log it
|
||||
if ($GLOBALS['egw_info']['server']['exception_show_trace'])
|
||||
{
|
||||
self::$instance->log_request("\n".$e->getTraceAsString()."\n");
|
||||
|
@ -570,8 +570,9 @@ abstract class Handler
|
||||
* @param int|string $retval
|
||||
* @param boolean $path_attr_is_name =true true: path_attr is ca(l|rd)dav_name, false: id (GroupDAV needs Location header)
|
||||
* @param string $etag =null etag, to not calculate it again (if != null)
|
||||
* @param string $prefix='' prefix for id
|
||||
*/
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null)
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null, string $prefix='')
|
||||
{
|
||||
//error_log(__METHOD__."(".array2string($entry).", '$path', ".array2string($retval).", path_attr_is_name=$path_attr_is_name, etag=".array2string($etag).")");
|
||||
// we should not return an etag here, as EGroupware never stores ical/vcard byte-by-byte
|
||||
@ -589,9 +590,9 @@ abstract class Handler
|
||||
// send Location header only on success AND if we dont use caldav_name as path-attribute or
|
||||
if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name ||
|
||||
// POST with add-member query parameter
|
||||
$_SERVER['REQUEST_METHOD'] == 'POST' && isset($_GET['add-member'])) ||
|
||||
// in case we choose to use a different name for the resourece, give the client a hint
|
||||
basename($path) !== $this->new_id)
|
||||
$_SERVER['REQUEST_METHOD'] == 'POST') ||
|
||||
// in case we choose to use a different name for the resource, give the client a hint
|
||||
basename($path) !== $prefix.$this->new_id)
|
||||
{
|
||||
$path = preg_replace('|(.*)/[^/]*|', '\1/', $path);
|
||||
header('Location: '.$this->base_uri.$path.$this->new_id);
|
||||
@ -712,16 +713,23 @@ abstract class Handler
|
||||
//error_log(__METHOD__."('$path', $user, more_results=$more_results) this->sync_collection_token=".$this->sync_collection_token);
|
||||
if ($more_results)
|
||||
{
|
||||
$error =
|
||||
' <D:response>
|
||||
<D:href>'.htmlspecialchars($this->caldav->base_uri.$this->caldav->path).'</D:href>
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
$error = ",\n\t".'"more-results": true';
|
||||
}
|
||||
else
|
||||
{
|
||||
$error =
|
||||
' <D:response>
|
||||
<D:href>' . htmlspecialchars($this->caldav->base_uri . $this->caldav->path) . '</D:href>
|
||||
<D:status>HTTP/1.1 507 Insufficient Storage</D:status>
|
||||
<D:error><D:number-of-matches-within-limits/></D:error>
|
||||
</D:response>
|
||||
';
|
||||
if ($this->caldav->crrnd)
|
||||
{
|
||||
$error = str_replace(array('<D:', '</D:'), array('<', '</'), $error);
|
||||
if ($this->caldav->crrnd)
|
||||
{
|
||||
$error = str_replace(array('<D:', '</D:'), array('<', '</'), $error);
|
||||
}
|
||||
}
|
||||
echo $error;
|
||||
}
|
||||
|
1301
api/src/Contacts/JsContact.php
Normal file
1301
api/src/Contacts/JsContact.php
Normal file
File diff suppressed because it is too large
Load Diff
27
api/src/Contacts/JsContactParseException.php
Normal file
27
api/src/Contacts/JsContactParseException.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* EGroupware API - JsContact
|
||||
*
|
||||
* @link https://www.egroupware.org
|
||||
* @author Ralf Becker <rb@egroupware.org>
|
||||
* @package addressbook
|
||||
* @copyright (c) 2021 by Ralf Becker <rb@egroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
*/
|
||||
|
||||
namespace EGroupware\Api\Contacts;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Error parsing JsContact format
|
||||
*
|
||||
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07
|
||||
*/
|
||||
class JsContactParseException extends \InvalidArgumentException
|
||||
{
|
||||
public function __construct($message = "", $code = 422, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code ?: 422, $previous);
|
||||
}
|
||||
}
|
@ -246,14 +246,21 @@ class Merge extends Api\Storage\Merge
|
||||
'owner' => lang('Owner'),
|
||||
) as $name => $label)
|
||||
{
|
||||
if (in_array($name,array('start','end')) && $n&1) // main values, which should be in the first column
|
||||
if(in_array($name, array('start',
|
||||
'end')) && $n & 1) // main values, which should be in the first column
|
||||
{
|
||||
echo "</tr>\n";
|
||||
$n++;
|
||||
}
|
||||
if (!($n&1)) echo '<tr>';
|
||||
echo '<td>{{calendar/#/'.$name.'}}</td><td>'.$label.'</td>';
|
||||
if ($n&1) echo "</tr>\n";
|
||||
if(!($n & 1))
|
||||
{
|
||||
echo '<tr>';
|
||||
}
|
||||
echo '<td>{{calendar/#/' . $name . '}}</td><td>' . $label . '</td>';
|
||||
if($n & 1)
|
||||
{
|
||||
echo "</tr>\n";
|
||||
}
|
||||
$n++;
|
||||
}
|
||||
echo "</table>\n";
|
||||
@ -261,6 +268,59 @@ class Merge extends Api\Storage\Merge
|
||||
$GLOBALS['egw']->framework->render(ob_get_clean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of placeholders provided.
|
||||
*
|
||||
* Placeholders are grouped logically. Group key should have a user-friendly translation.
|
||||
*/
|
||||
public function get_placeholder_list($prefix = '')
|
||||
{
|
||||
$placeholders = [];
|
||||
$group = 'contact';
|
||||
foreach($this->contacts->contact_fields as $name => $label)
|
||||
{
|
||||
if(in_array($name, array('tid', 'label', 'geo')))
|
||||
{
|
||||
continue;
|
||||
} // dont show them, as they are not used in the UI atm.
|
||||
|
||||
switch($name)
|
||||
{
|
||||
case 'adr_one_street':
|
||||
$group = 'business';
|
||||
break;
|
||||
case 'adr_two_street':
|
||||
$group = 'private';
|
||||
break;
|
||||
case 'tel_work':
|
||||
$group = 'phone';
|
||||
break;
|
||||
case 'email':
|
||||
case 'email_home':
|
||||
$group = 'email';
|
||||
break;
|
||||
case 'url':
|
||||
$group = 'details';
|
||||
}
|
||||
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $label;
|
||||
if($name == 'cat_id')
|
||||
{
|
||||
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = lang('Category path');
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly formatted address by country / preference
|
||||
$placeholders['business']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_one_formatted}}'] = "Formatted business address";
|
||||
$placeholders['private']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_two_formatted}}'] = "Formatted private address";
|
||||
|
||||
$group = 'customfields';
|
||||
foreach($this->contacts->customfields as $name => $field)
|
||||
{
|
||||
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $field['label'];
|
||||
}
|
||||
return $placeholders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insert-in-document action with optional default document on top
|
||||
*
|
||||
|
@ -810,14 +810,14 @@ class Sql extends Api\Storage
|
||||
{
|
||||
if ($limit_in_ab)
|
||||
{
|
||||
$in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND $this->lists_table.";
|
||||
$in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND ";
|
||||
if (!is_bool($limit_in_ab))
|
||||
{
|
||||
$in_ab_join .= $this->db->expression($this->lists_table, array('list_owner'=>$limit_in_ab));
|
||||
$in_ab_join .= $this->db->expression($this->table_name, $this->table_name.'.', ['contact_owner' => $limit_in_ab]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$in_ab_join .= "list_owner=$this->table_name.contact_owner";
|
||||
$in_ab_join .= "$this->lists_table.list_owner=$this->table_name.contact_owner";
|
||||
}
|
||||
}
|
||||
foreach($this->db->select($this->ab2list_table,"$this->ab2list_table.list_id,$this->table_name.$member_attr",
|
||||
|
@ -1209,7 +1209,8 @@ class Storage
|
||||
*
|
||||
* @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid)
|
||||
* @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute
|
||||
* @param boolean $limit_in_ab =false if true only return members from the same owners addressbook
|
||||
* @param boolean|int|array $limit_in_ab =false if true only return members from the same owners addressbook,
|
||||
* if int|array only return members from the given owners addressbook(s)
|
||||
* @return array with list_id => array(list_id,list_name,list_owner,...) pairs
|
||||
*/
|
||||
function read_lists($keys,$member_attr=null,$limit_in_ab=false)
|
||||
|
207
api/src/Etemplate/Widget/Placeholder.php
Normal file
207
api/src/Etemplate/Widget/Placeholder.php
Normal file
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/**
|
||||
* EGroupware - eTemplate serverside of linking widgets
|
||||
*
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @subpackage etemplate
|
||||
* @link http://www.egroupware.org
|
||||
* @author Nathan Gray
|
||||
* @copyright 2011 Nathan Gray
|
||||
* @version $Id$
|
||||
*/
|
||||
|
||||
namespace EGroupware\Api\Etemplate\Widget;
|
||||
|
||||
use EGroupware\Api\Etemplate;
|
||||
use EGroupware\Api;
|
||||
|
||||
/**
|
||||
* eTemplate Placeholder
|
||||
* Deals with listing & inserting placeholders, usually into Collabora
|
||||
*/
|
||||
class Placeholder extends Etemplate\Widget
|
||||
{
|
||||
|
||||
public $public_functions = array(
|
||||
'ajax_get_placeholders' => true,
|
||||
'ajax_fill_placeholder' => true
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|\XMLReader $xml string with xml or XMLReader positioned on the element to construct
|
||||
* @throws Api\Exception\WrongParameter
|
||||
*/
|
||||
public function __construct($xml = '')
|
||||
{
|
||||
if($xml)
|
||||
{
|
||||
parent::__construct($xml);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up what we know on the server side.
|
||||
*
|
||||
* Set the options for the application select.
|
||||
*
|
||||
* @param string $cname
|
||||
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
|
||||
*/
|
||||
public function beforeSendToClient($cname, array $expand = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the placeholders that match the given parameters.
|
||||
* Default options will get all placeholders in a single request.
|
||||
*/
|
||||
public static function ajax_get_placeholders($apps = null, $group = null)
|
||||
{
|
||||
$placeholders = [];
|
||||
|
||||
if(is_null($apps))
|
||||
{
|
||||
$apps = ['addressbook', 'user'];
|
||||
}
|
||||
|
||||
foreach($apps as $appname)
|
||||
{
|
||||
$merge = Api\Storage\Merge::get_app_class($appname);
|
||||
switch($appname)
|
||||
{
|
||||
case 'user':
|
||||
$list = $merge->get_user_placeholder_list();
|
||||
break;
|
||||
default:
|
||||
$list = $merge->get_placeholder_list();
|
||||
break;
|
||||
}
|
||||
if(!is_null($group))
|
||||
{
|
||||
$list = array_intersect_key($list, $group);
|
||||
}
|
||||
$placeholders[$appname] = $list;
|
||||
}
|
||||
|
||||
$response = Api\Json\Response::get();
|
||||
$response->data($placeholders);
|
||||
}
|
||||
|
||||
public function ajax_fill_placeholders($app, $content, $entry)
|
||||
{
|
||||
$merge = Api\Storage\Merge::get_app_class($app);
|
||||
$err = "";
|
||||
|
||||
switch($app)
|
||||
{
|
||||
case 'addressbook':
|
||||
default:
|
||||
$merged = $merge->merge_string($content, [$entry], $err, 'text/plain');
|
||||
}
|
||||
$response = Api\Json\Response::get();
|
||||
$response->data($merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input
|
||||
*
|
||||
* Following attributes get checked:
|
||||
* - needed: value must NOT be empty
|
||||
* - min, max: int and float widget only
|
||||
* - maxlength: maximum length of string (longer strings get truncated to allowed size)
|
||||
* - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
|
||||
* - int and float get casted to their type
|
||||
*
|
||||
* @param string $cname current namespace
|
||||
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
|
||||
* @param array $content
|
||||
* @param array &$validated =array() validated content
|
||||
*/
|
||||
public function validate($cname, array $expand, array $content, &$validated = array())
|
||||
{
|
||||
$form_name = self::form_name($cname, $this->id, $expand);
|
||||
|
||||
if(!$this->is_readonly($cname, $form_name))
|
||||
{
|
||||
$value = $value_in =& self::get_array($content, $form_name);
|
||||
|
||||
// keep values added into request by other ajax-functions, eg. files draged into htmlarea (Vfs)
|
||||
if((!$value || is_array($value) && !$value['to_id']) && is_array($expand['cont'][$this->id]) && !empty($expand['cont'][$this->id]['to_id']))
|
||||
{
|
||||
if(!is_array($value))
|
||||
{
|
||||
$value = array(
|
||||
'to_app' => $expand['cont'][$this->id]['to_app'],
|
||||
);
|
||||
}
|
||||
$value['to_id'] = $expand['cont'][$this->id]['to_id'];
|
||||
}
|
||||
|
||||
// Link widgets can share IDs, make sure to preserve values from others
|
||||
$already = self::get_array($validated, $form_name);
|
||||
if($already != null)
|
||||
{
|
||||
$value = array_merge($value, $already);
|
||||
}
|
||||
// Automatically do link if user selected entry but didn't click 'Link' button
|
||||
$link = self::get_array($content, self::form_name($cname, $this->id . '_link_entry'));
|
||||
if($this->type == 'link-to' && is_array($link) && $link['app'] && $link['id'])
|
||||
{
|
||||
// Do we have enough information to link automatically?
|
||||
if(is_array($value) && $value['to_id'])
|
||||
{
|
||||
Api\Link::link($value['to_app'], $value['to_id'], $link['app'], $link['id']);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not enough information, leave it to the application
|
||||
if(!is_array($value['to_id']))
|
||||
{
|
||||
$value['to_id'] = array();
|
||||
}
|
||||
$value['to_id'][] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for files - normally handled by ajax
|
||||
$files = self::get_array($content, self::form_name($cname, $this->id . '_file'));
|
||||
if(is_array($files) && !(is_array($value) && $value['to_id']))
|
||||
{
|
||||
$value = array();
|
||||
if(is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir']))
|
||||
{
|
||||
$path = $GLOBALS['egw_info']['server']['temp_dir'] . '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
$path = '';
|
||||
}
|
||||
foreach($files as $name => $attrs)
|
||||
{
|
||||
if(!is_array($value['to_id']))
|
||||
{
|
||||
$value['to_id'] = array();
|
||||
}
|
||||
$value['to_id'][] = array(
|
||||
'app' => Api\Link::VFS_APPNAME,
|
||||
'id' => array(
|
||||
'name' => $attrs['name'],
|
||||
'type' => $attrs['type'],
|
||||
'tmp_name' => $path . $name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
$valid =& self::get_array($validated, $form_name, true);
|
||||
if(true)
|
||||
{
|
||||
$valid = $value;
|
||||
}
|
||||
//error_log($this);
|
||||
//error_log(" " . array2string($valid));
|
||||
}
|
||||
}
|
||||
}
|
@ -603,6 +603,8 @@ class Sharing
|
||||
/**
|
||||
* Create a new share
|
||||
*
|
||||
* Only for shares with identical attributes AND recipients an existing share-token is returned.
|
||||
*
|
||||
* @param string $action_id Specific type of share being created, default ''
|
||||
* @param string $path either path in temp_dir or vfs with optional vfs scheme
|
||||
* @param string $mode self::LINK: copy file in users tmp-dir or self::READABLE share given vfs file,
|
||||
@ -626,35 +628,17 @@ class Sharing
|
||||
// Check if path is mounted somewhere that needs a password
|
||||
static::path_needs_password($path);
|
||||
|
||||
// check if file has been shared before, with identical attributes
|
||||
// check if file has been shared before, with identical attributes AND recipients
|
||||
if (($share = static::$db->select(static::TABLE, '*', $extra+array(
|
||||
'share_path' => $path,
|
||||
'share_owner' => Vfs::$user,
|
||||
'share_expires' => null,
|
||||
'share_passwd' => null,
|
||||
'share_writable'=> false,
|
||||
'share_with' => implode(',', (array)$recipients),
|
||||
), __LINE__, __FILE__, Db::API_APPNAME)->fetch()))
|
||||
{
|
||||
// if yes, just add additional recipients
|
||||
$share['share_with'] = $share['share_with'] ? explode(',', $share['share_with']) : array();
|
||||
$need_save = false;
|
||||
foreach((array)$recipients as $recipient)
|
||||
{
|
||||
if (!in_array($recipient, $share['share_with']))
|
||||
{
|
||||
$share['share_with'][] = $recipient;
|
||||
$need_save = true;
|
||||
}
|
||||
}
|
||||
$share['share_with'] = implode(',', $share['share_with']);
|
||||
if ($need_save)
|
||||
{
|
||||
static::$db->update(static::TABLE, array(
|
||||
'share_with' => $share['share_with'],
|
||||
), array(
|
||||
'share_id' => $share['share_id'],
|
||||
), __LINE__, __FILE__, Db::API_APPNAME);
|
||||
}
|
||||
// if yes, nothing to do
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -585,9 +585,9 @@ class Storage extends Storage\Base
|
||||
$col = $this->table_name .'.'.array_search($col, $this->db_cols).' AS '.$col;
|
||||
}
|
||||
// Check to make sure our order by doesn't have aliases that won't work
|
||||
else if (stripos($col, 'AS') !== false && $order_by)
|
||||
else if (stripos($col, ' AS ') !== false && $order_by)
|
||||
{
|
||||
list($value, $alias) = explode(' AS ', $col);
|
||||
list($value, $alias) = preg_split('/ AS /i', $col);
|
||||
if(stripos($order_by, $alias) !== FALSE && stripos($value, $this->table_name) === FALSE)
|
||||
{
|
||||
$order_by = str_replace($alias, $value, $order_by);
|
||||
|
@ -283,22 +283,43 @@ abstract class Merge
|
||||
}
|
||||
break;
|
||||
case 'account_id':
|
||||
if ($value)
|
||||
if($value)
|
||||
{
|
||||
$replacements['$$'.($prefix ? $prefix.'/':'').'account_lid$$'] = $GLOBALS['egw']->accounts->id2name($value);
|
||||
$replacements['$$' . ($prefix ? $prefix . '/' : '') . 'account_lid$$'] = $GLOBALS['egw']->accounts->id2name($value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($name != 'photo') $replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = $value;
|
||||
if($name != 'photo')
|
||||
{
|
||||
$replacements['$$' . ($prefix ? $prefix . '/' : '') . $name . '$$'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Formatted address, according to preference or country
|
||||
foreach(['one', 'two'] as $adr)
|
||||
{
|
||||
switch($this->contacts->addr_format_by_country($contact["adr_{$adr}_countryname"]))
|
||||
{
|
||||
case 'city_state_postcode':
|
||||
$formatted_placeholder = $contact["adr_{$adr}_locality"] . " " .
|
||||
$contact["adr_{$adr}_region"] . " " . $contact["adr_{$adr}_postalcode"];
|
||||
break;
|
||||
case 'postcode_city':
|
||||
default:
|
||||
$formatted_placeholder = $contact["adr_{$adr}_postalcode"] . ' ' . $contact["adr_{$adr}_locality"];
|
||||
break;
|
||||
}
|
||||
$replacements['$$adr_' . $adr . '_formatted$$'] = $formatted_placeholder;
|
||||
}
|
||||
|
||||
// set custom fields, should probably go to a general method all apps can use
|
||||
// need to load all cfs for $ignore_acl=true
|
||||
foreach($ignore_acl ? Customfields::get('addressbook', true) : $this->contacts->customfields as $name => $field)
|
||||
{
|
||||
$name = '#'.$name;
|
||||
$name = '#' . $name;
|
||||
if(!array_key_exists($name, $contact) || !$contact[$name])
|
||||
{
|
||||
$replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = '';
|
||||
$replacements['$$' . ($prefix ? $prefix . '/' : '') . $name . '$$'] = '';
|
||||
continue;
|
||||
}
|
||||
// Format date cfs per user Api\Preferences
|
||||
@ -1572,6 +1593,25 @@ abstract class Merge
|
||||
return $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct class for the given app
|
||||
*
|
||||
* @param $appname
|
||||
*/
|
||||
public static function get_app_class($appname)
|
||||
{
|
||||
if(class_exists($appname) && is_subclass_of($appname, 'EGroupware\\Api\\Storage\\Merge'))
|
||||
{
|
||||
$classname = "{$appname}_merge";
|
||||
$document_merge = new $classname();
|
||||
}
|
||||
else
|
||||
{
|
||||
$document_merge = new Api\Contacts\Merge();
|
||||
}
|
||||
return $document_merge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replacements for any entry specified by app & id
|
||||
*
|
||||
@ -1580,7 +1620,7 @@ abstract class Merge
|
||||
* @param string $content
|
||||
* @return array
|
||||
*/
|
||||
public function get_app_replacements($app, $id, $content, $prefix='')
|
||||
public function get_app_replacements($app, $id, $content, $prefix = '')
|
||||
{
|
||||
$replacements = array();
|
||||
if($app == 'addressbook')
|
||||
@ -2550,34 +2590,50 @@ abstract class Merge
|
||||
{
|
||||
return array(
|
||||
// Link to current entry
|
||||
'link' => lang('URL of current record'),
|
||||
'link/href' => lang('HTML link to the current record'),
|
||||
'link/title' => lang('Link title of current record'),
|
||||
'link' => lang('URL of current record'),
|
||||
'link/href' => lang('HTML link to the current record'),
|
||||
'link/title' => lang('Link title of current record'),
|
||||
|
||||
// Link system - linked entries
|
||||
'links' => lang('Titles of any entries linked to the current record, excluding attached files'),
|
||||
'links/href' => lang('HTML links to any entries linked to the current record, excluding attached files'),
|
||||
'links/url' => lang('URLs of any entries linked to the current record, excluding attached files'),
|
||||
'attachments' => lang('List of files linked to the current record'),
|
||||
'links_attachments' => lang('Links and attached files'),
|
||||
'links/[appname]' => lang('Links to specified application. Example: {{links/infolog}}'),
|
||||
'links' => lang('Titles of any entries linked to the current record, excluding attached files'),
|
||||
'links/href' => lang('HTML links to any entries linked to the current record, excluding attached files'),
|
||||
'links/url' => lang('URLs of any entries linked to the current record, excluding attached files'),
|
||||
'attachments' => lang('List of files linked to the current record'),
|
||||
'links_attachments' => lang('Links and attached files'),
|
||||
'links/[appname]' => lang('Links to specified application. Example: {{links/infolog}}'),
|
||||
|
||||
// General information
|
||||
'date' => lang('Date'),
|
||||
'user/n_fn' => lang('Name of current user, all other contact fields are valid too'),
|
||||
'user/account_lid' => lang('Username'),
|
||||
'date' => lang('Date'),
|
||||
'user/n_fn' => lang('Name of current user, all other contact fields are valid too'),
|
||||
'user/account_lid' => lang('Username'),
|
||||
|
||||
// Merge control
|
||||
'pagerepeat' => lang('For serial letter use this tag. Put the content, you want to repeat between two Tags.'),
|
||||
'label' => lang('Use this tag for addresslabels. Put the content, you want to repeat, between two tags.'),
|
||||
'labelplacement' => lang('Tag to mark positions for address labels'),
|
||||
'pagerepeat' => lang('For serial letter use this tag. Put the content, you want to repeat between two Tags.'),
|
||||
'label' => lang('Use this tag for addresslabels. Put the content, you want to repeat, between two tags.'),
|
||||
'labelplacement' => lang('Tag to mark positions for address labels'),
|
||||
|
||||
// Commands
|
||||
'IF fieldname' => lang('Example {{IF n_prefix~Mr~Hello Mr.~Hello Ms.}} - search the field "n_prefix", for "Mr", if found, write Hello Mr., else write Hello Ms.'),
|
||||
'NELF' => lang('Example {{NELF role}} - if field role is not empty, you will get a new line with the value of field role'),
|
||||
'NENVLF' => lang('Example {{NENVLF role}} - if field role is not empty, set a LF without any value of the field'),
|
||||
'LETTERPREFIX' => lang('Example {{LETTERPREFIX}} - Gives a letter prefix without double spaces, if the title is emty for example'),
|
||||
'IF fieldname' => lang('Example {{IF n_prefix~Mr~Hello Mr.~Hello Ms.}} - search the field "n_prefix", for "Mr", if found, write Hello Mr., else write Hello Ms.'),
|
||||
'NELF' => lang('Example {{NELF role}} - if field role is not empty, you will get a new line with the value of field role'),
|
||||
'NENVLF' => lang('Example {{NENVLF role}} - if field role is not empty, set a LF without any value of the field'),
|
||||
'LETTERPREFIX' => lang('Example {{LETTERPREFIX}} - Gives a letter prefix without double spaces, if the title is emty for example'),
|
||||
'LETTERPREFIXCUSTOM' => lang('Example {{LETTERPREFIXCUSTOM n_prefix title n_family}} - Example: Mr Dr. James Miller'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of placeholders for the current user
|
||||
*/
|
||||
public function get_user_placeholder_list($prefix = '')
|
||||
{
|
||||
$contacts = new Api\Contacts\Merge();
|
||||
$replacements = $contacts->get_placeholder_list(($prefix ? $prefix . '/' : '') . 'user');
|
||||
unset($replacements['details']['{{' . ($prefix ? $prefix . '/' : '') . 'user/account_id}}']);
|
||||
$replacements['account'] = [
|
||||
'{{' . ($prefix ? $prefix . '/' : '') . 'user/account_id}}' => 'Account ID',
|
||||
'{{' . ($prefix ? $prefix . '/' : '') . 'user/account_lid}}' => 'Login ID'
|
||||
];
|
||||
|
||||
return $replacements;
|
||||
}
|
||||
}
|
||||
|
@ -2315,6 +2315,10 @@ div.et2_toolbar_more h.ui-accordion-header.header_list-short {
|
||||
height: 24px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
div.et2_toolbar_more h.ui-accordion-header.header_list-short span.ui-accordion-header-icon.ui-icon.ui-icon-triangle-1-e {
|
||||
top: 0px;
|
||||
left: auto;
|
||||
}
|
||||
.et2_toolbar_more .ui-accordion-header-active.header_list-short.ui-state-active span.ui-accordion-header-icon {
|
||||
background-position: bottom !important;
|
||||
margin-top: 2px;
|
||||
|
81
api/templates/default/insert_merge_placeholder.xet
Normal file
81
api/templates/default/insert_merge_placeholder.xet
Normal file
@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
|
||||
<!-- $Id$ -->
|
||||
<overlay>
|
||||
<template id="etemplate.insert_merge_placeholder" template="" lang="" group="0" version="21.1.001">
|
||||
<vbox id="outer_box">
|
||||
<hbox id="selects">
|
||||
<vbox>
|
||||
<select id="app"/>
|
||||
<select id="group"/>
|
||||
|
||||
</vbox>
|
||||
<select id="placeholder_list"/>
|
||||
</hbox>
|
||||
<hbox class="preview">
|
||||
<description id="preview_placeholder"/>
|
||||
<button id="insert_placeholder" label="Insert" statustext="Insert placeholder" image="export"></button>
|
||||
</hbox>
|
||||
<hrule/>
|
||||
<link-entry id="entry" label="Preview with entry"/>
|
||||
<hbox class="preview">
|
||||
<description id="preview_content"/>
|
||||
<button id="insert_content" label="Insert" statustext="Insert merged content" image="export"></button>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<styles>
|
||||
|
||||
#api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects {
|
||||
flex: 1 1 80%;
|
||||
}
|
||||
#api\.insert_merge_placeholder_outer_box > label.et2_label {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
#api\.insert_merge_placeholder_outer_box .preview {
|
||||
flex: 1 1 2em;
|
||||
font-size: larger;
|
||||
}
|
||||
select#api\.insert_merge_placeholder_app {
|
||||
flex-grow: 0;
|
||||
}
|
||||
.ui-dialog-content, div.et2_box_widget, div.et2_box_widget > div.et2_box_widget {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
div.et2_hbox {
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
}
|
||||
div.et2_vbox {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
div.et2_box_widget > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
div.et2_link_entry {
|
||||
flex-grow: 0;
|
||||
}
|
||||
div.et2_link_entry input.ui-autocomplete-input {
|
||||
width: 75%
|
||||
}
|
||||
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button {
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button:hover, button#cancel:hover {
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
}
|
||||
.preview .et2_button {
|
||||
flex: 0 1 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
</styles>
|
||||
</template>
|
||||
</overlay>
|
72
api/templates/default/placeholder_snippet.xet
Normal file
72
api/templates/default/placeholder_snippet.xet
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
|
||||
<!-- $Id$ -->
|
||||
<overlay>
|
||||
<template id="etemplate.placeholder_snippet" template="" lang="" group="0" version="21.1.001">
|
||||
<vbox id="outer_box">
|
||||
<vbox id="selects">
|
||||
<select id="app"/>
|
||||
<select id="placeholder_list"/>
|
||||
</vbox>
|
||||
<hrule/>
|
||||
<link-entry id="entry" label="Select entry"/>
|
||||
<hbox class="preview">
|
||||
<description id="preview_content"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<styles>
|
||||
|
||||
#api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects {
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
#api\.insert_merge_placeholder_outer_box > label.et2_label {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
#api\.insert_merge_placeholder_outer_box .preview {
|
||||
flex: 1 1 50%;
|
||||
font-size: larger;
|
||||
}
|
||||
select#api\.insert_merge_placeholder_app {
|
||||
flex-grow: 0;
|
||||
}
|
||||
.ui-dialog-content, div.et2_box_widget, div.et2_box_widget > div.et2_box_widget {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
div.et2_hbox {
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
}
|
||||
div.et2_vbox {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
div.et2_box_widget > * {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
div.et2_link_entry {
|
||||
flex-grow: 0;
|
||||
}
|
||||
div.et2_link_entry input.ui-autocomplete-input {
|
||||
width: 75%
|
||||
}
|
||||
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button {
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button:hover, button#cancel:hover {
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
}
|
||||
.preview .et2_button {
|
||||
flex: 0 1 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
</styles>
|
||||
</template>
|
||||
</overlay>
|
@ -67,15 +67,15 @@ abstract class CalDAVTest extends TestCase
|
||||
/**
|
||||
* Get HTTP client for tests
|
||||
*
|
||||
* It will use by default the always existing user "demo" with password "guest" (use [] to NOT authenticate).
|
||||
* It will use by default the user configured in phpunit.xml: demo/guest (use [] to NOT authenticate).
|
||||
* Additional users need to be created with $this->createUser("name").
|
||||
*
|
||||
* @param string|array $user_or_options ='demo' string with account_lid of user for authentication or array of options
|
||||
* @param string|array $user_or_options =null string with account_lid of user for authentication or array of options
|
||||
* @return Client
|
||||
* @see http://docs.guzzlephp.org/en/v6/request-options.html
|
||||
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
|
||||
*/
|
||||
protected function getClient($user_or_options='demo')
|
||||
protected function getClient($user_or_options=null)
|
||||
{
|
||||
if (!is_array($user_or_options))
|
||||
{
|
||||
@ -160,14 +160,15 @@ abstract class CalDAVTest extends TestCase
|
||||
/**
|
||||
* Get authentication information for given user to use
|
||||
*
|
||||
* @param string $_account_lid ='demo'
|
||||
* @param string $_account_lid =null default EGW_USER configured in phpunit.xml
|
||||
* @return array
|
||||
*/
|
||||
protected function auth($_account_lid='demo')
|
||||
protected function auth($_account_lid=null)
|
||||
{
|
||||
if ($_account_lid === 'demo')
|
||||
if (!isset($_account_lid) || $_account_lid === $GLOBALS['EGW_USER'])
|
||||
{
|
||||
$password = 'guest';
|
||||
$_account_lid = $GLOBALS['EGW_USER'];
|
||||
$password = $GLOBALS['EGW_PASSWORD'];
|
||||
}
|
||||
elseif (!isset(self::$created_users[$_account_lid]))
|
||||
{
|
||||
|
@ -1550,8 +1550,9 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
* @param int|string $retval
|
||||
* @param boolean $path_attr_is_name =true true: path_attr is ca(l|rd)dav_name, false: id (GroupDAV needs Location header)
|
||||
* @param string $etag =null etag, to not calculate it again (if != null)
|
||||
* @param string $prefix =''
|
||||
*/
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null)
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null, $prefix='')
|
||||
{
|
||||
$schedule_tag = null;
|
||||
if (!isset($etag)) $etag = $this->get_etag($entry, $schedule_tag);
|
||||
@ -1560,7 +1561,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
header('Schedule-Tag: "'.$schedule_tag.'"');
|
||||
}
|
||||
parent::put_response_headers($entry, $path, $retval, $path_attr_is_name, $etag);
|
||||
parent::put_response_headers($entry, $path, $retval, $path_attr_is_name, $etag, $prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,6 +91,7 @@ calendar menu calendar it Menù Agenda
|
||||
calendar preferences calendar it Preferenze Agenda
|
||||
calendar settings admin it Impostazioni Agenda
|
||||
calendar-fieldname calendar it Campi-Agenda
|
||||
can not send any notifications because notifications app is not installed! calendar it Impossibile inviare notifiche poiché l'app notifiche non è installata!
|
||||
can't add alarms in the past !!! calendar it Non si può aggiungere sveglie nel passato !!!
|
||||
can't aquire lock! calendar it Impossibile acquisire lo sblocco!
|
||||
canceled calendar it Cancellato
|
||||
@ -109,7 +110,6 @@ close the window calendar it Chiudi la finestra
|
||||
compose a mail to all participants after the event is saved calendar it componi una email per tutti i partecipanti dopo che l'evento è stato salvato
|
||||
configuration settings calendar it Impostazioni di configurazione
|
||||
conflict calendar it Conflitto
|
||||
copy of: calendar it Copia di:
|
||||
copy this event calendar it Copia questo evento
|
||||
copy your changes to the clipboard, %1reload the entry%2 and merge them. calendar it Copia le tue modifiche negli appunti, %1aggiorna l'inserimento%2 e uniscili.
|
||||
countries calendar it Nazioni
|
||||
@ -159,6 +159,7 @@ delete series calendar it Cancella serie
|
||||
delete this alarm calendar it Cancella questa sveglia
|
||||
delete this event calendar it Cancella questo evento
|
||||
delete this exception calendar it Cancella questa eccezione
|
||||
delete this meeting for all participants calendar it Elimina questa riunione per tutti i partecipanti
|
||||
delete this recurrence calendar it Elimina la ricorrenza
|
||||
delete this series of recurring events calendar it Cancella questa serie di eventi ricorrenti
|
||||
deleted calendar it Cancellati
|
||||
@ -172,12 +173,15 @@ displays this calendar view on the home page (page you get when you enter egroup
|
||||
distribution list calendar it Lista di distribuzione
|
||||
do not import conflicting events calendar it Non importare eventi in conflitto
|
||||
do not include events of group members calendar it Non includere eventi di membri del gruppo
|
||||
do not notify externals (non-users) about this event calendar it NON notificare agli esterni (non-utenti) quest'evento
|
||||
do you really want to change the start of this series? if you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created. calendar it Confermi di voler modificare l'inizio di questa serie? Se confermi allora la serie originale terminerà da %1 e verrà creata una nuova serie nel futuro, che riflette queste modifiche.
|
||||
do you want a weekview with or without weekend? calendar it Vuoi una vista settimanale con o senza weekend?
|
||||
do you want non-egroupware participants of events you created to be automatically notified about new or changed appointments? calendar it Vuoi notificare a partecipanti che non sono utenti di Egroupware circa i nuovi eventi oppure le modifiche degli appuntamenti?
|
||||
do you want responses from events you created, but are not participating in? calendar it Vuoi ricevere risposte ad eventi che hai creato, ma ai quali non parteciperai?
|
||||
do you want to be notified about changes of appointments you modified? calendar it Vuoi ricevere notifiche sui cambiamenti in appuntamenti creati da te?
|
||||
do you want to be notified about new or changed appointments? you are not notified about changes you made yourself.<br>you can limit the notifications to certain changes only. each item includes all notifications listed above. all modifications include changes of title, description, participants, but no participant responses. if the owner of an event requested any notifcations, he will always get participant responses like acceptions or rejections too. calendar it Vuoi una notifica per i nuovi appuntamenti o per quelli cambiati? Sarai avvertito dei cambiamenti da te effettuati.<br>Puoi limitare la notifica su alcuni cambiamenti. TUTTE LE VOCI include tutte le notifiche elencate sopra di esso. TUTTE LE MODIFICHE include il cambiamento del titolo, della descrizione, dei partecipanti, ma non delle risposte dei partecipanti. Se il creatore dell'evento richiede ogni notifica, avrà sempre le risposte dei partecipanti sia gli accetti che i rifiuti.
|
||||
do you want to be notified about new or changed appointments? you be notified about changes you make yourself.<br>you can limit the notifications to certain changes only. each item includes all the notification listed above it. all modifications include changes of title, description, participants, but no participant responses. if the owner of an event requested any notifcations, he will always get the participant responses like acceptions and rejections too. calendar it Vuoi ricevere una notifica sugli appuntamenti nuovi o modificati? Sarai notificato anche delle modifiche che apporti tu. Puoi limitare le notifiche a solo alcuni cambiamenti. Ogni elemento include tutte le notifiche sopra di essa.Tutte le modifiche includono cambiamenti di titolo, descrizione, partecipanti, ma non le risposte dei partecipanti. Se il proprietario della scheda di un evento ha richiesto delle notifiche, riceverà sempre le risposte dei partecipanti.
|
||||
do you want to be notified about participant responses from events you created, but are not participating in? calendar it Vuoi ricevere una notifica sulla risposta dei partecipanti ad eventi che hai creato, ma ai quali non parteciperai?
|
||||
do you want to edit this event as an exception or the whole series? calendar it Vuoi modificare questo evento come eccezione oppure l'intera ricorrenza?
|
||||
do you want to keep the series exceptions in your calendar? calendar it Vuoi mantenere le modifiche alla serie nell'agenda?
|
||||
do you want to receive a regularly summary of your appointments via email?<br>the summary is sent to your standard email-address on the morning of that day or on monday for weekly summarys.<br>it is only sent when you have any appointments on that day or week. calendar it Vuoi ricevere regolarmente il resoconto dei tuoi appuntamenti via e-mail?<br>Il resoconto sarà mandato al tuo indirizzo e-mail standard ogni mattina o ogni Lunedì per il resoconto settimanale.<br>Sarà mandato solo se avrai un appuntamento per quel giorno o per quella settimana.
|
||||
@ -201,6 +205,7 @@ enddate / -time of the meeting, eg. for more then one day calendar it Data/ora f
|
||||
enddate of the export calendar it Data finale dell'esportazione
|
||||
ends calendar it finisce
|
||||
error adding the alarm calendar it Errore aggiungendo la sveglia
|
||||
error notifying %1 calendar it Errore notificando %1
|
||||
error saving the event! calendar it Errore durante il salvataggio dell'evento
|
||||
error: can't delete original series! calendar it Errore! Impossibile eliminare la serie originale!
|
||||
error: duration of event longer then recurrence interval! calendar it Errore! Durata dell'evento maggiore dell'intervallo di ricorrenza
|
||||
@ -369,6 +374,7 @@ name of current user, all other contact fields are valid too calendar it Nome de
|
||||
name of the day of the week (ex: monday) calendar it Nome del giorno della settimana
|
||||
name of the week (ex: monday), available for the first entry inside each day of week or daily table inside the selected range. calendar it Nome della settimana, p.es. lunedì, disponibile per la prima voce in ogni giorno della settimana oppure tabella giornaliera entro l'intervallo selezionato.
|
||||
needs action calendar it Richiede azione
|
||||
never notify externals (non-users) about events i create calendar it Non notificare mai agli esterni (non utenti) gli eventi che creo
|
||||
new calendar it Nuovo
|
||||
new event category calendar it Nuova categoria di evento
|
||||
new event participants calendar it Nuovi partecipanti all'evento
|
||||
@ -395,8 +401,12 @@ notification messages for uninvited participants calendar it Messaggi di notific
|
||||
notification messages for your alarms calendar it Messaggi di notifica per i tuoi allarmi
|
||||
notification messages for your responses calendar it Messaggi di notifica per le tue risposte
|
||||
notification settings calendar it Impostazioni di notifica
|
||||
notify calendar it Notifica
|
||||
notify all externals (non-users) about this event calendar it Notifica a tutti gli esterni (non-utenti) quest'evento
|
||||
notify externals calendar it Notifica agli esterni
|
||||
notify non-egroupware users about event updates calendar it Invia notifiche a utenti che non appartengono a EGroupware, circa gli aggiornamenti
|
||||
number of records to read (%1) calendar it Numero di record da leggere (%1)
|
||||
number of resources to be booked calendar it Numero di risorse da prenotare
|
||||
number of weeks to show calendar it Numero di settimane da mostrare
|
||||
observance rule calendar it Regola da osservare
|
||||
occurence calendar it Occorrenze
|
||||
@ -441,6 +451,7 @@ prevent deleting of entries admin it Previeni la eliminazione degli inserimenti
|
||||
previous calendar it precedente
|
||||
private and global public calendar it Privato e Pubblico Globale
|
||||
private and group public calendar it Private e Pubblico per il Gruppo
|
||||
private event calendar it Evento Privato
|
||||
private only calendar it Solo privato
|
||||
quantity calendar it Quantità
|
||||
quick add calendar it Immissione rapida
|
||||
@ -458,6 +469,7 @@ recurring event calendar it evento ricorrente
|
||||
regular edit calendar it Modifica regolare
|
||||
reject calendar it Rifiuta
|
||||
rejected calendar it Rifiutato
|
||||
removes the event from my calendar calendar it Rimuove l'evento dal mio calendario
|
||||
repeat days calendar it Giorni di ripetizione
|
||||
repeat the event until which date (empty means unlimited) calendar it ripeti l'evento fino a che data (vuoto significa illimitato)
|
||||
repeat type calendar it Tipo di ripetizione
|
||||
@ -499,6 +511,8 @@ select who should get the alarm calendar it Seleziona chi dovrà avere la svegli
|
||||
selected range calendar it Intervallo di selezione
|
||||
selected users/groups calendar it Utenti/Gruppi selezionati
|
||||
send meetingrequest to all participants after the event is saved calendar it Invia richiesta meeting a tutti i partecipanti a evento salvato.
|
||||
send notifications calendar it Invia notifiche
|
||||
send notifications to users right now calendar it Invia notifiche ai partecipanti adesso
|
||||
series deleted calendar it Serie cancellata
|
||||
set new events to private calendar it Imposta il nuovo evento come privato
|
||||
setting lock time calender admin it Impostazione di blocco di dati di orario per l'agenda (predefinito 1 sec.)
|
||||
@ -510,6 +524,7 @@ should the number of weeks be shown on top of the calendar calendar it Il numero
|
||||
should the number of weeks be shown on top of the calendar (only if offset = 0) calendar it Il numero delle settimane dovrebbe essere visualizzato in cima all'agenda (solo se offset=0)
|
||||
should the planner display an empty row for users or categories without any appointment. calendar it Il pianificatore dovrebbe mostrare una riga vuota per gli utenti o le categorie senza appuntamenti.
|
||||
should the status of the event-participants (accept, reject, ...) be shown in brackets after each participants name ? calendar it Lo stato dei partecipanti agli eventi (accettato, rifiutato, ...) sarà visualizzato tra parentesi dopo il nome?
|
||||
show %1 from %2 calendar it Mostra %1 da %2
|
||||
show a calendar title calendar it Mostra titolo dell'agenda
|
||||
show all events, as if they were private calendar it Mostra tutti gli eventi come se fossero privati
|
||||
show all status incl. rejected events calendar it Mostra tutti gli stati compresi gli eventi rifiutati
|
||||
@ -533,6 +548,7 @@ show this month calendar it mostra questo mese
|
||||
show this week calendar it mostra questa settimana
|
||||
show year and age calendar it Mostra anno e età
|
||||
single event calendar it singolo evento
|
||||
single participant calendar it Singolo partecipante
|
||||
specify where url of the day links to calendar it Specifica l'URL del collegamento del giorno
|
||||
start calendar it Inizio
|
||||
start date/time calendar it Data/Ora Inizio
|
||||
@ -542,6 +558,7 @@ startdate / -time calendar it Data/ora iniziale
|
||||
startdate and -time of the search calendar it Data/ora iniziale della ricerca
|
||||
startdate of the export calendar it Data iniziale dell'esportazione
|
||||
startrecord calendar it Primo Record
|
||||
status calendar it Stato
|
||||
status already applied calendar it Stato già applicato
|
||||
status changed calendar it Stato modificato
|
||||
status for all future scheduled days changed calendar it Stato per tutti gli eventi futuri modificato
|
||||
@ -610,6 +627,7 @@ update calendar view immediately when navigation calendar in sidebox is changed
|
||||
update timezones common it Aggiorna i fusi orari
|
||||
updated calendar it Aggiornato
|
||||
use end date calendar it Usa data di termine
|
||||
use event tz calendar it Usa fuso orario dell'evento
|
||||
use given criteria: calendar it Utilizzare i criteri indicati:
|
||||
use quick add or full edit dialog when creating a new event calendar it Utilizzare l'immissione rapida o completa quando si crea un nuovo evento
|
||||
use range-views to optimise calendar queries? calendar it Usare le viste a intervalli per ottimizzare le interrogazioni dell'agenda?
|
||||
|
268
doc/REST-CalDAV-CardDAV/README.md
Normal file
268
doc/REST-CalDAV-CardDAV/README.md
Normal file
@ -0,0 +1,268 @@
|
||||
# EGroupware CalDAV/CardDAV server and REST API
|
||||
|
||||
CalDAV/CardDAV is build on HTTP and WebDAV, implementing the following additional RFCs containing documentation of the protocol:
|
||||
* [rfc4791: CalDAV: Calendaring Extensions to WebDAV](https://datatracker.ietf.org/doc/html/rfc4791)
|
||||
* [rfc6638: Scheduling Extensions to CalDAV](https://datatracker.ietf.org/doc/html/rfc6638)
|
||||
* [rfc6352: CardDAV: vCard Extensions to WebDAV](https://datatracker.ietf.org/doc/html/rfc6352)
|
||||
* [rfc6578: Collection Synchronization for WebDAV](https://datatracker.ietf.org/doc/html/rfc6578)
|
||||
* many additional extensions from former Apple Calendaring Server used by Apple clients and others
|
||||
|
||||
## Path / URL layout for CalDAV/CardDAV and REST is identical
|
||||
|
||||
One can use the following URLs relative (!) to https://example.org/egroupware/groupdav.php
|
||||
|
||||
- ```/``` base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here
|
||||
- ```/principals/``` principal-collection-set for WebDAV ACL
|
||||
- ```/principals/users/<username>/```
|
||||
- ```/principals/groups/<groupname>/```
|
||||
- ```/<username>/``` users home-set with
|
||||
- ```/<username>/addressbook/``` addressbook of user or group <username> given the user has rights to view it
|
||||
- ```/<current-username>/addressbook-<other-username>/``` shared addressbooks from other user or group
|
||||
- ```/<current-username>/addressbook-accounts/``` all accounts current user has rights to see
|
||||
- ```/<username>/calendar/``` calendar of user <username> given the user has rights to view it
|
||||
- ```/<username>/calendar/?download``` download whole calendar as .ics file (GET request!)
|
||||
- ```/<current-username>/calendar-<other-username>/``` shared calendar from other user or group (only current <username>!)
|
||||
- ```/<username>/inbox/``` scheduling inbox of user <username>
|
||||
- ```/<username>/outbox/``` scheduling outbox of user <username>
|
||||
- ```/<username>/infolog/``` InfoLog's of user <username> given the user has rights to view it
|
||||
- ```/addressbook/``` all addressbooks current user has rights to, announced as directory-gateway now
|
||||
- ```/addressbook-accounts/``` all accounts current user has rights to see
|
||||
- ```/calendar/``` calendar of current user
|
||||
- ```/infolog/``` infologs of current user
|
||||
- ```/(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
|
||||
|
||||
Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
|
||||
|
||||
Calling one of the above collections with a GET request / regular browser generates an automatic index
|
||||
from the data of a allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
|
||||
|
||||
## REST API: using EGroupware CalDAV/CardDAV server with JSON
|
||||
> currently implemented only for contacts!
|
||||
|
||||
Following RFCs / drafts used/planned for JSON encoding of ressources
|
||||
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
|
||||
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
|
||||
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
|
||||
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
||||
|
||||
### Supported request methods and examples
|
||||
|
||||
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
|
||||
<details>
|
||||
<summary>Example: Getting all entries of a given users addessbook</summary>
|
||||
|
||||
```
|
||||
curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/<username>/addressbook/1833": {
|
||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||
"prodId": "EGroupware Addressbook 21.1.001",
|
||||
"created": "2010-10-21T09:55:42Z",
|
||||
"updated": "2014-06-02T14:45:24Z",
|
||||
"name": [
|
||||
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
||||
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": { "value": "Default Tester" },
|
||||
"organizations": {
|
||||
"org": {
|
||||
"@type": "Organization",
|
||||
"name": "default.org",
|
||||
"units": {
|
||||
"org_unit": "department.default.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"work": { "@type": "EmailAddress", "email": "test@test.com", "contexts": { "work": true }, "pref": 1 }
|
||||
},
|
||||
"phones": {
|
||||
"tel_work": { "@type": "Phone", "phone": "+49 123 4567890", "pref": 1, "features": { "voice": true }, "contexts": { "work": true } },
|
||||
"tel_cell": { "@type": "Phone", "phone": "012 3723567", "features": { "cell": true }, "contexts": { "work": true } }
|
||||
},
|
||||
"online": {
|
||||
"url": { "@type": "Resource", "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
|
||||
},
|
||||
"notes": [
|
||||
"Test test TEST\n\\server\\share\n\\\nother\nblah"
|
||||
],
|
||||
},
|
||||
"/<username>/addressbook/list-36": {
|
||||
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
||||
"name": "Example distribution list",
|
||||
"card": {
|
||||
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
||||
"prodId": "EGroupware Addressbook 21.1.001",
|
||||
"updated": "2018-04-11T14:46:43Z",
|
||||
"fullName": { "value": "Example distribution list" }
|
||||
},
|
||||
"members": {
|
||||
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</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 addressbook 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
|
||||
|
||||
<details>
|
||||
<summary>Example: Getting just ETAGs and displayname of all contacts in a given AB</summary>
|
||||
|
||||
```
|
||||
curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/addressbook/1833": {
|
||||
"displayname": "Default Tester",
|
||||
"getetag": "\"1833:24\""
|
||||
},
|
||||
"/addressbook/1838": {
|
||||
"displayname": "Test Tester",
|
||||
"getetag": "\"1838:19\""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</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/addressbook/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/addressbook/2050": "Frau Margot Test-Notifikation",
|
||||
"/addressbook/2384": "Test Tester",
|
||||
"/addressbook/5462": "Margot Testgedöns",
|
||||
"/addressbook/2380": "Frau Test Defaulterin",
|
||||
"/addressbook/5474": "Noch ein Neuer",
|
||||
"/addressbook/5575": "Mr New Name",
|
||||
"/addressbook/5461": "Herr Hugo Kurt Müller Senior",
|
||||
"/addressbook/5601": "Steve Jobs",
|
||||
"/addressbook/5603": "Ralf Becker",
|
||||
"/addressbook/1838": "Test Tester"
|
||||
},
|
||||
"more-results": true,
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1400867824"
|
||||
}
|
||||
```
|
||||
#### Requesting next chunk:
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/addressbook/1833": "Default Tester",
|
||||
"/addressbook/5597": "Neuer Testschnuffi",
|
||||
"/addressbook/5593": "Muster Max",
|
||||
"/addressbook/5628": "2. Test Contact",
|
||||
"/addressbook/5629": "Testen Tester",
|
||||
"/addressbook/5630": "Testen Tester",
|
||||
"/addressbook/5633": "Testen Tester",
|
||||
"/addressbook/5635": "Test4 Tester",
|
||||
"/addressbook/5638": "Test Kontakt",
|
||||
"/addressbook/5636": "Test Default"
|
||||
},
|
||||
"more-results": true,
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/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/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"responses": {
|
||||
"/addressbook/5597": null,
|
||||
"/addressbook/5593": {
|
||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||
"prodId": "EGroupware Addressbook 21.1.001",
|
||||
"created": "2010-10-21T09:55:42Z",
|
||||
"updated": "2014-06-02T14:45:24Z",
|
||||
"name": [
|
||||
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
||||
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": "Default Tester",
|
||||
....
|
||||
}
|
||||
},
|
||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
|
||||
<details>
|
||||
<summary>Example: GET request for a single resource</summary>
|
||||
|
||||
```
|
||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/5593' -H "Accept: application/pretty+json" --user <username>
|
||||
{
|
||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||
"prodId": "EGroupware Addressbook 21.1.001",
|
||||
"created": "2010-10-21T09:55:42Z",
|
||||
"updated": "2014-06-02T14:45:24Z",
|
||||
"name": [
|
||||
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
||||
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": "Default Tester",
|
||||
....
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar 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>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
||||
{
|
||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||
"prodId": "EGroupware Addressbook 21.1.001",
|
||||
"created": "2010-10-21T09:55:42Z",
|
||||
"updated": "2014-06-02T14:45:24Z",
|
||||
"name": [
|
||||
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
||||
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": { "value": "Default Tester" },
|
||||
....
|
||||
}
|
||||
EOF
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
||||
```
|
||||
</details>
|
||||
|
||||
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources
|
||||
|
||||
* **DELETE** requests delete single resources
|
||||
|
||||
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
||||
|
||||
#### Implemented [changes from JsContact draft 08](https://github.com/rsto/draft-stepanek-jscontact/compare/draft-ietf-jmap-jscontact-08):
|
||||
* localizedString type / object is removed in favor or regular String type and a [localizations object like in JsCalendar](https://datatracker.ietf.org/doc/html/rfc8984#section-4.6.1)
|
||||
* [Vendor-specific Property Extensions and Values](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-1.3)
|
||||
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```
|
@ -64,7 +64,7 @@ else \
|
||||
RESULT=$?; \
|
||||
fi; \
|
||||
rm composer-setup.php; \
|
||||
composer.phar self-update 1.10.22; \
|
||||
composer.phar self-update --1; \
|
||||
exit $RESULT' \
|
||||
# disable certificate checks for LDAP as most LDAP and AD servers have no "valid" cert
|
||||
&& echo "TLS_REQCERT never" >> /etc/ldap/ldap.conf
|
||||
|
@ -7,7 +7,7 @@ RECOMMENDED_PHP_VERSION=7.4
|
||||
|
||||
PHP_VERSION=${1:-7.4}
|
||||
|
||||
TAG=$(docker run --rm -i --entrypoint bash $REPO/$IMAGE:$PHP_VERSION -c "apt update && apt search php$PHP_VERSION-fpm" 2>/dev/null|grep php$PHP_VERSION-fpm|sed "s|^php$PHP_VERSION-fpm/focal.*\([78]\.[0-9]*\.[0-9]*\).*|\1|g")
|
||||
TAG=$(docker run --rm -i --entrypoint bash $REPO/$IMAGE:$PHP_VERSION -c "apt update && apt search php$PHP_VERSION-fpm" 2>/dev/null|grep php$PHP_VERSION-fpm|sed "s|^php$PHP_VERSION-fpm/focal[^ ]* \([78]\.[0-9]*\.[0-9]*\).*|\1|g")
|
||||
test -z "$TAG" && {
|
||||
echo "Can't get new tag of $REPO/$IMAGE container --> existing"
|
||||
exit 1
|
||||
|
@ -68,8 +68,8 @@ else \
|
||||
fi; \
|
||||
rm composer-setup.php; \
|
||||
exit $RESULT' \
|
||||
# build EGroupware
|
||||
&& composer.phar self-update 1.10.22 \
|
||||
# build EGroupware (Horde using a PEAR repo requires Composer v1)
|
||||
&& composer.phar self-update --1 \
|
||||
&& cd /usr/share \
|
||||
&& [ $PHP_VERSION = "8.0" ] && COMPOSER_EXTRA=--ignore-platform-reqs || true \
|
||||
&& composer.phar create-project $COMPOSER_EXTRA --prefer-dist --no-scripts --no-dev egroupware/egroupware:$VERSION \
|
||||
|
@ -359,11 +359,13 @@ function do_tag()
|
||||
update_composer_json_version($config['tag']);
|
||||
// might require more then one run, as pushed tags need to be picked up by packagist
|
||||
$output = $ret = null;
|
||||
$timeout = $retries = 10;
|
||||
$timeout = 30;
|
||||
$try = 0;
|
||||
$cmd = $config['composer'].' update --ignore-platform-reqs --no-dev egroupware/\*';
|
||||
for($try=1; $try <= $retries && run_cmd($cmd, $output, $try < $retries ? 2 : null); ++$try)
|
||||
while(run_cmd($cmd, $output, 2))
|
||||
{
|
||||
error_log("Retry $try/$retries in $timeout seconds ...");
|
||||
++$try;
|
||||
error_log("$try. retry in $timeout seconds ...");
|
||||
sleep($timeout);
|
||||
}
|
||||
run_cmd($config['git'].' commit -m '.escapeshellarg('Updating dependencies for '.$config['tag']).' composer.{json,lock}');
|
||||
|
@ -1,3 +1,42 @@
|
||||
egroupware-docker (21.1.20210923) hardy; urgency=low
|
||||
|
||||
* smallPART: many new features and improvements for the new semester:
|
||||
- push changes in course, videos, participants and comments instantly to all online users
|
||||
- new video-controls for speed, skip 10s forward/back, full width, speaker control
|
||||
- add staff rolls: tutor (readonly teacher access), teacher and co-admin (identical to owner)
|
||||
- split students in groups and limit visibility of comments to their group, staff can filter by group
|
||||
- allow students to pick a nickname displayed to fellow students, always show staff and students to staff with full name
|
||||
- videos are ordered now alphabetic, use eg. 1st, 2nd, ... as prefix to force videos to a desired order
|
||||
- record date and time student subscribes or unsubscribes a course
|
||||
- CSV comment export adds user retweeting in front of his comment
|
||||
- fix questions with same start-time got identical question-numbers
|
||||
- fix LTI automatic registration and interactive content-selection (LTI 1.3 eg. for Moodle 3.10+)
|
||||
+ content-selection shows all available courses, not just subscribed ones
|
||||
+ fix not working content selection if there is only a single 1.3 config (no 1.0 one)
|
||||
+ fix not working buttons to change between video, questions and scores
|
||||
* Filemanager: added user-interface to mount WebDAV or SMB shares
|
||||
* Filemanager/Sharing: create different share-token for different recipients (before recipients where added to the token)
|
||||
* Kanban: Boards now remember collapsed columns & swimlanes
|
||||
* Kanban: improve formatting for small columns
|
||||
* Kanban: Fix Infolog field "Projectmanager" did not load in board edit Column & Listen dialogs after first being set.
|
||||
* Calendar: Activate links in location & description in event tooltip
|
||||
* Knowledge Base: fix pasting/dragging image into htmlarea editor does not work
|
||||
* Addressbook/Mobile theme: fix opening contacts fails in mobile theme
|
||||
* Tracker: Add configuration for defaulting group (all queues and queue specific)
|
||||
* Mail: fix updating/deleting mail accounts does not refresh the mail tree no more
|
||||
* Api: Fix some merge files were opened in browser instead of downloaded
|
||||
* Api: Fix entry list stops scrolling if a row is updated while the tab is not visible
|
||||
* Api: Fix changes in history log had a hash instead of user if the change was made after a share was opened.
|
||||
* PostgreSQL/Addressbook: fix SQL error deleting a contact finally
|
||||
* PostgreSQL/Addressbook/All Apps: fix SQL error in history tab if there are attachments
|
||||
* PostgreSQL: fix SQL error when accessing eg. InfoLog
|
||||
* Calendar/Addressbook/InfoLog: no longer allow to immediately delete entries as it breaks CalDAV/CardDAV sync
|
||||
* Setup: support uninstalling automatic installed apps (no more reinstalling next update)
|
||||
* EPL/Univention: support permanent uninstalling EPL features / downgrade to CE
|
||||
* Chrome 94.0.4606.54: fix CSP error clicking on sidebox menu
|
||||
|
||||
-- Ralf Becker <rb@egroupware.org> Thu, 23 Sep 2021 09:59:41 +0200
|
||||
|
||||
egroupware-docker (21.1.20210723) hardy; urgency=low
|
||||
|
||||
* Security Update: all 21.1 users should upgrade ASAP, 20.1 and below is not affected
|
||||
|
@ -51,6 +51,7 @@ cannot create directory because it begins or ends in a space filemanager it Non
|
||||
cautiously rejecting to remove folder '%1'! filemanager it Eliminazione della cartella %1 rigettata per precauzione!
|
||||
check all filemanager it Seleziona tutto
|
||||
check virtual filesystem common it Controlla il filesystem virtuale
|
||||
classic filemanager it Barra degli strumenti predefinita
|
||||
clear search filemanager it Reimposta la ricerca
|
||||
clipboard is empty! filemanager it Gli appunti sono vuoti!
|
||||
collab editor settings filemanager it Impostazioni Collab Editor
|
||||
@ -80,8 +81,10 @@ current directory filemanager it Cartella corrente
|
||||
custom fields filemanager it Campi personalizzati
|
||||
cut filemanager it Taglia
|
||||
cut to clipboard filemanager it Taglia e metti negli appunti
|
||||
default action on double-click filemanager it Azione predefinita al doppio click
|
||||
default behavior is no. the link will not be shown, but you are still able to navigate to this location, or configure this paricular location as startfolder or folderlink. filemanager it Predefinito: No. Il collegamento non verrà mostrato, ma potrai comunque raggiungere questo percorso, oppure configurarlo come cartella di avvio o collegamento a cartella.
|
||||
default document to insert entries filemanager it Documento predefinito per l'inserimento
|
||||
defines how to handle double click action on a document file. images are always opened in the expose-view and emails with email application. all other mime-types are handled by the browser itself. filemanager it Definisce come gestire di doppio-click su un file. Le immagini sono sempre aperte in modalità visualizzazione e le email con l'applicazione E-Mail. Tutti gli altri file sono gestiti dal browser come oggetti MIME-type.
|
||||
defines how to open a merge print document filemanager it Definisce il modo di apertura di un documento per la stampa unione
|
||||
delete all older versions and deleted files older then %s days filemanager it Eliminare tutte le versioni passate e le cartelle più vecchie di %s giorni
|
||||
delete these files or directories? filemanager it Eliminare i file o le cartelle?
|
||||
@ -105,14 +108,17 @@ do you want more information about epl subscription? common it Si vorrebbero ric
|
||||
do you want to overwrite existing file %1 in directory %2? filemanager it Vuoi sovrascrivere il file esistente %1 nella cartella %2?
|
||||
do you want to overwrite the existing file %1? filemanager it Vuoi sovrascrivere il file esistente?
|
||||
download filemanager it Download
|
||||
download documents filemanager it scarica documenti
|
||||
edit comments filemanager it Modifica commenti
|
||||
edit settings filemanager it Modifica le impostazioni
|
||||
edit share filemanager it modifica condivisione
|
||||
enable filemanager it Abilita
|
||||
enable versioning for given mountpoint filemanager it Abilitare il controllo delle versioni per il mountpoint selezionato
|
||||
enter setup user and password filemanager it Inserisci il nome utente e la password dell'utente di setup.
|
||||
enter setup user and password to get root rights filemanager it Inserisci il nome utente e la password dell'utente di setup per ottenere diritti amministrativi (root)
|
||||
enter the complete vfs path to specify a fast access link to a folder filemanager it Inserisci il percorso VFS completo per specificare un link veloce verso una cartella
|
||||
enter the complete vfs path to specify your desired start folder. filemanager it Inserisci il percorso VFS completo per specificare la tua cartella di avvio
|
||||
enter your file name filemanager it inserisci il nome del file
|
||||
error adding the acl! filemanager it Errore nell'aggiunta della regola ACL!
|
||||
error creating symlink to target %1! filemanager it Errore di creazione del link simbolico verso %1!
|
||||
error deleting the acl entry! filemanager it Errore di eliminazione della regola ACL!
|
||||
@ -167,6 +173,8 @@ go home filemanager it Vai alla Home
|
||||
go to filemanager it Vai A
|
||||
go to your home directory filemanager it Vai alla tua directory Home
|
||||
go up filemanager it Vai su
|
||||
hidden upload filemanager it Caricamento nascosto
|
||||
hidden uploads filemanager it Caricamenti nascosti
|
||||
id filemanager it ID
|
||||
if you specify a directory (full vfs path) here, %1 displays an action for each document. that action allows to download the specified document with the %1 data inserted. filemanager it Se specifichi una directory (percorso vfs completo), %1 mostrerà l'azione per ogni documento. Quella azione permette di scaricare il documento specificato con i dati %1 inseriti
|
||||
if you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. that icon allows to download the specified document with the data inserted. filemanager it Se specifichi una directory (percorso vfs completo), %1 mostrerà un'icona in più per ogni voce. Quell'icona permette di scaricare il documento specificato con i dati inseriti
|
||||
@ -207,6 +215,8 @@ noone filemanager it Nessuno
|
||||
older versions or deleted files filemanager it Versioni vecchie o file eliminati
|
||||
only owner can rename or delete the content filemanager it Solo il proprietario può rinominare o eliminare il contenuto
|
||||
open filemanager it Apri
|
||||
open documents with collabora, if permissions are given filemanager it apri i documenti utilizzando Collabora, se hai i permessi
|
||||
open odt documents with collabeditor filemanager it Apri i file di testo (odt) con CollabEditor
|
||||
operation filemanager it Operazione
|
||||
paste link filemanager it Incolla collegamento
|
||||
path %1 not found or not a directory! filemanager it Il percorso %1 non è stato trovato, oppure non è una directory!
|
||||
@ -249,6 +259,7 @@ select file to upload in current directory filemanager it Seleziona il file da c
|
||||
select file(s) from vfs common it Seleziona file dal sistema virtuale di file (vfs)
|
||||
setting for document merge saved. filemanager it Impostazione stampa unione salvata.
|
||||
share files filemanager it Condividi file
|
||||
share link copied into clipboard filemanager it Condividi il collegamento copiato negli appunti
|
||||
shared files filemanager it File condivisi
|
||||
shared with filemanager it Condivisi con
|
||||
show filemanager it Mostra
|
||||
@ -278,6 +289,7 @@ there's already a file with that name! filemanager it Esiste già un file con qu
|
||||
tile view filemanager it Vista affiancata
|
||||
to overwrite the existing file store again. filemanager it Per sovrascrivere l'esistente salva di nuovo
|
||||
total files filemanager it File totali
|
||||
ui mode filemanager it Barra degli strumenti predefinita
|
||||
under directory filemanager it sotto la cartella
|
||||
unmount filemanager it Smonta
|
||||
unused space filemanager it Spazio inutilizzato
|
||||
@ -291,6 +303,7 @@ user color indicator filemanager it Indicatore colorato utente
|
||||
users and groups filemanager it Utenti e gruppo
|
||||
versioning filemanager it Registrazione Versioni di file
|
||||
vfs mounts and versioning common it Montaggi VFS e registrazione versioni
|
||||
view link filemanager it Mostra Indirizzo
|
||||
webdav link copied into clipboard filemanager it Link webDAV copiato negli appunti
|
||||
who should be allowed to finally delete deleted files or old versions of a file: filemanager it Chi dovrebbe poter eliminare definitivamente file o vecchie versioni di file:
|
||||
writable share link filemanager it Collegamento condiviso in scrittura
|
||||
|
@ -13,6 +13,7 @@
|
||||
addressbook csv import importexport it Importazione da CSV rubrica
|
||||
addressbook vcard import importexport it Importazione da vCard rubrica
|
||||
admin disabled exporting importexport it L'amministratore ha disabilitato l'esportazione
|
||||
all custom fields importexport it Tutti i campi personalizzati
|
||||
all encodings importexport it Tutte le codifiche
|
||||
all users importexport it Tutti gli utenti
|
||||
allowed users importexport it Utenti consentiti
|
||||
@ -33,6 +34,7 @@ column mismatch: %1 should be %2, not %3 importexport it Discrepanza di colonne:
|
||||
condition importexport it Condizione
|
||||
copied importexport it Copiato
|
||||
create a <a href="%1">new definition</a> for this file importexport it Crea una <a href="%1">nuova definizione</a> per questo file
|
||||
create a matching export definition based on this import definition importexport it Crea una definizione di esportazione in base a questa definizione di importazione
|
||||
create export importexport it Crea esportazione
|
||||
created importexport it Creato
|
||||
csv field importexport it Campo CSV
|
||||
@ -41,6 +43,7 @@ day of week importexport it Giorni della settimana
|
||||
define imports|exports common it Definisci Importazioni | Esportazioni
|
||||
definition importexport it Definizione
|
||||
delete all selected definitions importexport it Rimuovi TUTTE le definizioni selezionate
|
||||
delete files after import importexport it Elimina dopo l'importazione
|
||||
deleted importexport it Eliminato
|
||||
delimiter importexport it Separatore
|
||||
duplicate name, please choose another. importexport it Nome duplicato, selezionare un altro
|
||||
@ -66,6 +69,7 @@ fieldseperator importexport it Separatore campo
|
||||
filters importexport it Filtri
|
||||
finish importexport it Termina
|
||||
from definition importexport it Da definizione
|
||||
from file importexport it Da file
|
||||
general preferences it Generale
|
||||
general fields: preferences it Campi generici:
|
||||
header lines to skip importexport it Linee di intestazione da saltare
|
||||
@ -110,8 +114,11 @@ schedule import | export importexport it Pianifica importazione | esportazione
|
||||
schedule not found importexport it Pianificazione non trovata
|
||||
schedule times are server time! currently %s. importexport it Gli orari di pianificazine sono quelli del server! Attualmente sono le %s
|
||||
select definition importexport it Seleziona definizione
|
||||
select groups importexport it Seleziona gruppi
|
||||
select owner importexport it Seleziona proprietario
|
||||
select plugin importexport it Seleziona plugin
|
||||
select... importexport it Seleziona:
|
||||
set import values importexport it Imposta parametri di importazione
|
||||
skipped importexport it Saltati
|
||||
some nice text importexport it Un po' di testo
|
||||
some records may not have been imported importexport it Alcune voci potrebbero non essere state importate
|
||||
@ -137,6 +144,7 @@ user template folder importexport it Usa cartella modelli
|
||||
users allowed to create their own definitions importexport it Utenti a cui è consentito modificare le loro definizioni
|
||||
users allowed to share their own definitions importexport it Utenti a cui è consentito condividere le loro definizioni
|
||||
warnings importexport it Avvisi
|
||||
what should be done with unknown values? importexport it Cosa si dovrebbe fare con i valori sconosciuti?
|
||||
which useres are allowed for this definition importexport it A quali utenti è permesso di usare questa definizione
|
||||
which users are allowed to use this definition importexport it A quali utenti è permesso di usare questa definizione
|
||||
you may want to <a href="%1" target="_new">backup</a> first. importexport it Potresti effettuare un <a href="%1" target="_new">backup dei dati</a> prima di procedere.
|
||||
|
@ -82,6 +82,7 @@ change completed infolog it Modifica completata
|
||||
change completion infolog it Modifica la percentuale di completamento
|
||||
change history infolog it Cambia storico
|
||||
change owner when updating infolog it Modifica il proprietario quando aggiorni
|
||||
change responsible infolog it Cambia responsabile
|
||||
change the status of an entry, eg. close it infolog it Cambia lo stato di una voce, es. chiudi
|
||||
changed category to %1 infolog it Categoria modificata in %1
|
||||
changed completion to %1% infolog it Completamento modificato in %1%
|
||||
@ -170,6 +171,7 @@ done infolog it eseguita
|
||||
download infolog it Download
|
||||
download url for links infolog it URL di scaricamento per i collegamenti
|
||||
due %1 infolog it In scadenza %1
|
||||
due date infolog it Data di scadenza
|
||||
duration infolog it Durata
|
||||
e-mail: infolog it Email
|
||||
each value is a line like <id>[=<label>] infolog it ogni valore è una linea tipo <id>[=<label>]
|
||||
@ -248,10 +250,11 @@ infolog - new subproject infolog it Attività - Nuovo progetto secondario
|
||||
infolog - subprojects from infolog it Attività - progetto secondario da
|
||||
infolog csv export infolog it Esportazione CSV Attività
|
||||
infolog csv import infolog it Importazione CSV Attività
|
||||
infolog encryption requires epl subscription infolog it La cifratura delle attività richiede la sottoscrizione di EPL
|
||||
infolog entry deleted infolog it Voce Attività cancellata
|
||||
infolog entry saved infolog it Voce Attività salvata
|
||||
infolog fields: infolog it Campi Attività
|
||||
InfoLog filter for the home screen infolog it Filtro Attività per la schermata principale
|
||||
infolog filter for the home screen infolog it Filtro Attività per la schermata principale
|
||||
infolog ical export infolog it Esportazione iCal Attività
|
||||
infolog ical import infolog it importazione iCal Attività
|
||||
infolog id infolog it ID Attività
|
||||
@ -351,6 +354,7 @@ path to user and group files has to be outside of the webservers document-root!!
|
||||
pattern for search in addressbook infolog it stringa da ricercare nella rubrica
|
||||
pattern for search in projects infolog it stringa da ricercare nei progetti
|
||||
percent completed infolog it Percentuale completamento
|
||||
performance optimization for huge infolog tables admin it Ottimizza performance per tabelle Attività grandi
|
||||
permission denied infolog it Permesso negato
|
||||
permissions error - %1 could not %2 infolog it Errore di permessi - %1 non può %2
|
||||
phone infolog it Chiamata Telefonica
|
||||
@ -362,6 +366,7 @@ prefix for sub-entries (default: re:) infolog it Prefisso per sottovoci (default
|
||||
price infolog it Prezzo
|
||||
pricelist infolog it Listino
|
||||
primary link infolog it Collegamento primario
|
||||
print this infolog infolog it Stampa quest' Attività
|
||||
printing... infolog it In stampa...
|
||||
priority infolog it Priorità
|
||||
private infolog it Privata
|
||||
@ -432,7 +437,7 @@ sets the status of this entry and its subs to done infolog it Imposta lo stato d
|
||||
sets the status of this entry to done infolog it Imposta lo stato di questa voce in completato
|
||||
should infolog show subtasks, -calls or -notes in the normal view or not. you can always view the subs via there parent. infolog it Attività deve visualizzare ToDo-, chiamate- o note- secondarie nella vista normale oppure no. Potrai sempre visualizzare le secondarie attraverso le principali.
|
||||
should infolog show the links to other applications and/or the file-attachments in the infolog list (normal view when you enter infolog). infolog it Attività deve mostrare i link ad altre apllicazioni e/o gli allegati nell'elenco Attività (vista normale quando entri in Attività).
|
||||
Should InfoLog show up on the home screen and with which filter. Works only if you dont selected an application for the home screen (in your preferences). infolog it Attività deve apparire nella schermata principale e con quale filtro. Funziona solo se non hai selezionato un'applicazione per la schermata principale (nelle tue preferenze).
|
||||
should infolog show up on the home screen and with which filter. works only if you dont selected an application for the home screen (in your preferences). infolog it Attività deve apparire nella schermata principale e con quale filtro. Funziona solo se non hai selezionato un'applicazione per la schermata principale (nelle tue preferenze).
|
||||
should infolog use full names (surname and familyname) or just the loginnames. infolog it Attività deve usare i nomi completi (nome e cognome) o solo il nome utente.
|
||||
should the infolog list show a unique numerical id, which can be used eg. as ticket id. infolog it L'elenco Attività deve mostrare un ID numerico univoco, che può essere usato ad esempio come Id ticket.
|
||||
should the infolog list show the column "last modified". infolog it L'elenco Attività deve mostrare la colonna "ultima modifica".
|
||||
@ -457,8 +462,8 @@ startdate must be before enddate!!! infolog it La data di inizio deve essere ant
|
||||
starting %1 infolog it Avvio %1
|
||||
startrecord infolog it Record Iniziale
|
||||
status infolog it Stato
|
||||
status, percent and date completed are always allowed. infolog it Stato, percentuale e data completamento sono sempre permessi.
|
||||
status ... infolog it Stato ...
|
||||
status, percent and date completed are always allowed. infolog it Stato, percentuale e data completamento sono sempre permessi.
|
||||
sub infolog it Sub
|
||||
sub-entries become subs of the parent or main entries, if there's no parent infolog it Le sotto-voci diventano sotto- di voci superiori o voci principali, se non c'è più superiore
|
||||
sub-entries will not be closed infolog it Le sottovoci non verranno chiuse
|
||||
@ -514,7 +519,7 @@ view the parent of this entry and all his subs infolog it Visualizza i padri di
|
||||
view this linked entry in its application infolog it Visualizza le voci linkate nella sua applicazione
|
||||
when should the todo or phonecall be started, it shows up from that date in the filter open or own open (startpage) infolog it quando i ToDo e le Chiamate Telefoniche iniziano, vengono visualizzati dalla data impostata sul filtro aperti o propri aperti (pagina iniziale)
|
||||
which additional fields should the responsible be allowed to edit without having edit rights? infolog it Che campi aggiuntivi sarà permesso modificare al responsabile senza avere diritti di modifica?
|
||||
which implicit acl rights should the responsible get? infolog it Che diritti ACL impliciti otterrà il responsabile?
|
||||
which implicit acl rights should the responsible get infolog it Quale ACL implicita deve essere fornita al responsabile
|
||||
which participants should be preselected when scheduling an appointment. infolog it Quali partecipanti dovrebbero essere preselezionati quando si programma un appuntamento?
|
||||
which types should the calendar show infolog it Quali tipi di calendario mostrare?
|
||||
which types should the calendar show like events? infolog it Quali tipi dovrebbe mostrare l'agenda come eventi?
|
||||
@ -532,7 +537,7 @@ you can choose a categorie to be preselected, when you create a new infolog entr
|
||||
you can't delete one of the stock types !!! infolog it Non puoi cancellare uno dei tipi predefiniti !!!
|
||||
you have entered an invalid ending date infolog it E' stata inserita una data di fine non valida
|
||||
you have entered an invalid starting date infolog it E' stata inserita una data di inizio non valida
|
||||
you have to enter a name, to create a new typ!!! infolog it Per creare un nuovo tipo devi inserire un nome!!!
|
||||
you have to enter a name, to create a new type! infolog it Deve essere inserito un nome per creare un nuovo tipo!
|
||||
you must enter a subject or a description infolog it E' necessario inserire un oggetto o una descrizione
|
||||
you need to select an entry for linking. infolog it Devi selezionare una voce da collegare
|
||||
you need to select some entries first infolog it Devi prima selezionare alcune voci.
|
||||
|
@ -51,7 +51,7 @@ allways a new window mail en allways a new window
|
||||
always mail en Always
|
||||
always allow external sources from %1 mail en Always allow external sources from %1
|
||||
always show html emails mail en always show HTML emails
|
||||
always show notifiction mail en Always show notifiction
|
||||
always show notifiction mail en Always show notification
|
||||
an error happend while trying to remove acl rights from the account %1! mail en An error happend while trying to remove ACL rights from the account %1!
|
||||
and the rule with priority %1, now got the priority %2 mail en And the rule with priority %1, now got the priority %2
|
||||
any of mail en any of
|
||||
|
@ -143,14 +143,15 @@ import './slider.js';
|
||||
var href_regexp = /^javascript:([^\(]+)\((.*)?\);?$/;
|
||||
jQuery('#egw_fw_topmenu_items,#egw_fw_topmenu_info_items,#egw_fw_sidemenu,#egw_fw_footer').on('click','a[href^="javascript:"]',function(ev){
|
||||
ev.stopPropagation(); // do NOT execute regular event, as it will violate CSP, when handler does NOT return false
|
||||
var matches = this.href.match(href_regexp);
|
||||
// fix for Chrome 94.0.4606.54 returning all but first single quote "'" in href as "%27" :(
|
||||
var matches = this.href.replace(/%27/g, "'").match(href_regexp);
|
||||
var args = [];
|
||||
if (matches.length > 1 && matches[2] !== undefined)
|
||||
{
|
||||
try {
|
||||
args = JSON.parse('['+matches[2]+']');
|
||||
}
|
||||
catch(e) { // deal with '-encloded strings (JSON allows only ")
|
||||
catch(e) { // deal with '-enclosed strings (JSON allows only ")
|
||||
args = JSON.parse('['+matches[2].replace(/','/g, '","').replace(/((^|,)'|'(,|$))/g, '$2"$3')+']');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user