Merge remote-tracking branch 'origin/master' into web-components

This commit is contained in:
nathan 2021-09-23 10:22:18 -06:00
commit 25773a929f
34 changed files with 3125 additions and 165 deletions

View File

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

View 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"]);

View File

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

View File

@ -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
*

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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);
}
}

View File

@ -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
*

View File

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

View File

@ -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)

View 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));
}
}
}

View File

@ -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
{

View File

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

View File

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

View File

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

View 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>

View 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>

View File

@ -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]))
{

View File

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

View File

@ -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?

View 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```

View File

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

View File

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

View File

@ -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 \

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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.

View File

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

View File

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