Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-10-06 14:02:22 -06:00
commit 02dce82010
79 changed files with 2304 additions and 925 deletions

View File

@ -629,13 +629,15 @@ class addressbook_groupdav extends Api\CalDAV\Handler
* @param int $id * @param int $id
* @param int $user =null account_id of owner, default null * @param int $user =null account_id of owner, default null
* @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook) * @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook)
* @param string $method='PUT' also called for POST and PATCH
* @param string $content_type=null
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/ */
function put(&$options,$id,$user=null,$prefix=null) function put(&$options, $id, $user=null, $prefix=null, string $method='PUT', string $content_type=null)
{ {
if ($this->debug) error_log(__METHOD__.'('.array2string($options).",$id,$user)"); if ($this->debug) error_log(__METHOD__.'('.array2string($options).",$id,$user)");
$oldContact = $this->_common_get_put_delete('PUT',$options,$id); $oldContact = $this->_common_get_put_delete($method,$options,$id);
if (!is_null($oldContact) && !is_array($oldContact)) if (!is_null($oldContact) && !is_array($oldContact))
{ {
if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($oldContact)); if ($this->debug) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($oldContact));
@ -658,7 +660,8 @@ class addressbook_groupdav extends Api\CalDAV\Handler
} }
} }
$contact = $type === JsContact::MIME_TYPE_JSCARD ? $contact = $type === JsContact::MIME_TYPE_JSCARD ?
JsContact::parseJsCard($options['content']) : JsContact::parseJsCardGroup($options['content']); JsContact::parseJsCard($options['content'], $oldContact ?: [], $content_type, $method) :
JsContact::parseJsCardGroup($options['content']);
if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0) if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0)
{ {
@ -757,10 +760,10 @@ class addressbook_groupdav extends Api\CalDAV\Handler
} }
else else
{ {
$contact['carddav_name'] = $id; $contact['carddav_name'] = (!empty($id) ? basename($id, '.vcf') : $contact['uid']).'.vcf';
// only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!) // only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!)
if ($prefix && !in_array('O',$this->home_set_pref) && $user) if ($prefix && ($is_json || !in_array('O',$this->home_set_pref)) && $user)
{ {
$contact['owner'] = $user; $contact['owner'] = $user;
} }
@ -1105,6 +1108,11 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$keys['id'] = $id; $keys['id'] = $id;
} }
} }
// json with uid
elseif (empty(self::$path_extension) && (string)$id !== (string)(int)$id)
{
$keys['uid'] = $id;
}
else else
{ {
$keys[self::$path_attr] = $id; $keys[self::$path_attr] = $id;

View File

@ -316,6 +316,16 @@ class addressbook_hooks
'admin' => False, 'admin' => False,
'default' => '/templates/addressbook', 'default' => '/templates/addressbook',
); );
$settings[Api\Storage\Merge::PREF_DOCUMENT_FILENAME] = array(
'type' => 'taglist',
'label' => 'Document download filename',
'name' => 'document_download_name',
'values' => Api\Storage\Merge::DOCUMENT_FILENAME_OPTIONS,
'help' => 'Choose the default filename for downloaded documents.',
'xmlrpc' => True,
'admin' => False,
'default' => 'document',
);
} }
if ($GLOBALS['egw_info']['user']['apps']['felamimail'] || $GLOBALS['egw_info']['user']['apps']['mail']) if ($GLOBALS['egw_info']['user']['apps']['felamimail'] || $GLOBALS['egw_info']['user']['apps']['mail'])

View File

@ -183,12 +183,12 @@ class addressbook_ui extends addressbook_bo
$msg = ''; $msg = '';
} }
} }
if ($_content['nm']['rows']['infolog']) if (!empty($_content['nm']['rows']['infolog']))
{ {
$org = key($_content['nm']['rows']['infolog']); $org = key($_content['nm']['rows']['infolog']);
return $this->infolog_org_view($org); return $this->infolog_org_view($org);
} }
if ($_content['nm']['rows']['view']) // show all contacts of an organisation if (!empty($_content['nm']['rows']['view'])) // show all contacts of an organisation
{ {
$grouped_view = key($_content['nm']['rows']['view']); $grouped_view = key($_content['nm']['rows']['view']);
} }
@ -1736,9 +1736,13 @@ class addressbook_ui extends addressbook_bo
if (isset($this->grouped_views[(string) $query['grouped_view']])) if (isset($this->grouped_views[(string) $query['grouped_view']]))
{ {
// we have a grouped view, reset the advanced search // we have a grouped view, reset the advanced search
if(!$query['search'] && $old_state['advanced_search']) $query['advanced_search'] = $old_state['advanced_search']; if (empty($query['search']) && !empty($old_state['advanced_search']))
{
$query['advanced_search'] = $old_state['advanced_search'];
} }
elseif(!$query['search'] && array_key_exists('advanced_search',$old_state)) // eg. paging in an advanced search }
// eg. paging in an advanced search
elseif(empty($query['search']) && is_array($old_state) && array_key_exists('advanced_search', $old_state))
{ {
$query['advanced_search'] = $old_state['advanced_search']; $query['advanced_search'] = $old_state['advanced_search'];
} }
@ -2256,7 +2260,7 @@ class addressbook_ui extends addressbook_bo
// remove invalid shared-with entries (should not happen, as we validate already on client-side) // remove invalid shared-with entries (should not happen, as we validate already on client-side)
$this->check_shared_with($content['shared']); $this->check_shared_with($content['shared']);
$button = @key($content['button']); $button = @key($content['button'] ?? []);
unset($content['button']); unset($content['button']);
$content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p'); $content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p');
$content['owner'] = (string) (int) $content['owner']; $content['owner'] = (string) (int) $content['owner'];
@ -2984,7 +2988,7 @@ class addressbook_ui extends addressbook_bo
if(is_array($content)) if(is_array($content))
{ {
$button = is_array($content['button']) ? key($content['button']) : ""; $button = key($content['button'] ?? []);
switch ($button) switch ($button)
{ {
case 'vcard': case 'vcard':
@ -3067,7 +3071,7 @@ class addressbook_ui extends addressbook_bo
$_GET['contact_id'] = array_shift($rows); $_GET['contact_id'] = array_shift($rows);
$_GET['index'] = 0; $_GET['index'] = 0;
} }
$contact_id = $_GET['contact_id'] ? $_GET['contact_id'] : ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0); $contact_id = $_GET['contact_id'] ?? ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0);
if(!$contact_id || !is_array($content = $this->read($contact_id))) if(!$contact_id || !is_array($content = $this->read($contact_id)))
{ {
Egw::redirect_link('/index.php',array( Egw::redirect_link('/index.php',array(

View File

@ -139,7 +139,7 @@ class admin_categories
$button = 'delete'; $button = 'delete';
$delete_subs = $content['delete']['subs']?true:false; $delete_subs = $content['delete']['subs']?true:false;
} }
else elseif (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -564,7 +564,7 @@ class admin_categories
{ {
$content = array_merge($content,$content[$action.'_popup']); $content = array_merge($content,$content[$action.'_popup']);
} }
$content['nm']['action'] .= '_' . key($content[$action . '_action']); $content['nm']['action'] .= '_' . key($content[$action . '_action'] ?? []);
if(is_array($content[$action])) if(is_array($content[$action]))
{ {
@ -680,7 +680,7 @@ class admin_categories
{ {
$cmd = new admin_cmd_delete_category( $cmd = new admin_cmd_delete_category(
$cat_id, $cat_id,
key($content['button']) == 'delete_sub', key($content['button'] ?? []) == 'delete_sub',
$content['admin_cmd'] $content['admin_cmd']
); );
$cmd->run(); $cmd->run();

View File

@ -70,7 +70,7 @@ class admin_cmds
} }
$content['nm']['actions'] = self::cmd_actions(); $content['nm']['actions'] = self::cmd_actions();
} }
elseif ($content['nm']['rows']['delete']) elseif (!empty($content['nm']['rows']['delete']))
{ {
$id = key($content['nm']['rows']['delete']); $id = key($content['nm']['rows']['delete']);
unset($content['nm']['rows']); unset($content['nm']['rows']);

View File

@ -212,7 +212,7 @@ class admin_mail
public function autoconfig(array $content) public function autoconfig(array $content)
{ {
// user pressed [Skip IMAP] --> jump to SMTP config // user pressed [Skip IMAP] --> jump to SMTP config
if ($content['button'] && key($content['button']) == 'skip_imap') if (!empty($content['button']) && key($content['button']) === 'skip_imap')
{ {
unset($content['button']); unset($content['button']);
if (!isset($content['acc_smtp_host'])) $content['acc_smtp_host'] = ''; // do manual mode right away if (!isset($content['acc_smtp_host'])) $content['acc_smtp_host'] = ''; // do manual mode right away
@ -361,7 +361,7 @@ class admin_mail
*/ */
public function folder(array $content, $msg='', Horde_Imap_Client_Socket $imap=null) public function folder(array $content, $msg='', Horde_Imap_Client_Socket $imap=null)
{ {
if (isset($content['button'])) if (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -482,7 +482,7 @@ class admin_mail
); );
$content['msg'] = $msg; $content['msg'] = $msg;
if (isset($content['button'])) if (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -619,7 +619,7 @@ class admin_mail
); );
$content['msg'] = $msg; $content['msg'] = $msg;
if (isset($content['button'])) if (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -835,7 +835,7 @@ class admin_mail
{ {
$content['called_for'] = (int)$_GET['account_id']; $content['called_for'] = (int)$_GET['account_id'];
$content['accounts'] = iterator_to_array(Mail\Account::search($content['called_for'])); $content['accounts'] = iterator_to_array(Mail\Account::search($content['called_for']));
if ($content['accounts']) if (!empty($content['accounts']))
{ {
$content['acc_id'] = key($content['accounts']); $content['acc_id'] = key($content['accounts']);
//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id'])); //error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
@ -949,7 +949,7 @@ class admin_mail
$tpl->disableElement('notify_save_default', !$is_multiple || !$edit_access); $tpl->disableElement('notify_save_default', !$is_multiple || !$edit_access);
$tpl->disableElement('notify_use_default', !$is_multiple); $tpl->disableElement('notify_use_default', !$is_multiple);
if (isset($content['button'])) if (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -1031,7 +1031,7 @@ class admin_mail
unset($content['smimeKeyUpload']); unset($content['smimeKeyUpload']);
} }
self::fix_account_id_0($content['account_id'], true); self::fix_account_id_0($content['account_id'], true);
$content = Mail\Account::write($content, $content['called_for'] || !$this->is_admin ? $content = Mail\Account::write($content, !empty($content['called_for']) && $this->is_admin ?
$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']); $content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
self::fix_account_id_0($content['account_id']); self::fix_account_id_0($content['account_id']);
$msg = lang('Account saved.'); $msg = lang('Account saved.');

View File

@ -204,7 +204,7 @@ class admin_ui
$item['id'] = substr($item['extradata'], 11); $item['id'] = substr($item['extradata'], 11);
unset($item['extradata']); unset($item['extradata']);
$matches = null; $matches = null;
if ($item['options'] && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches)) if (!empty($item['options']) && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches))
{ {
$item['popup'] = $matches[2].'x'.$matches[3]; $item['popup'] = $matches[2].'x'.$matches[3];
if (isset($matches[5])) $item['tooltip'] = $matches[5]; if (isset($matches[5])) $item['tooltip'] = $matches[5];
@ -213,7 +213,7 @@ class admin_ui
} }
if (empty($item['icon'])) $item['icon'] = $app.'/navbar'; if (empty($item['icon'])) $item['icon'] = $app.'/navbar';
if (empty($item['group'])) $item['group'] = $group; if (empty($item['group'])) $item['group'] = $group;
if (empty($item['onExecute'])) $item['onExecute'] = $item['popup'] ? if (empty($item['onExecute'])) $item['onExecute'] = !empty($item['popup']) ?
'javaScript:nm_action' : 'javaScript:app.admin.iframe_location'; 'javaScript:nm_action' : 'javaScript:app.admin.iframe_location';
if (!isset($item['allowOnMultiple'])) $item['allowOnMultiple'] = false; if (!isset($item['allowOnMultiple'])) $item['allowOnMultiple'] = false;
@ -297,7 +297,7 @@ class admin_ui
$item['id'] = substr($item['extradata'], 11); $item['id'] = substr($item['extradata'], 11);
unset($item['extradata']); unset($item['extradata']);
$matches = null; $matches = null;
if ($item['options'] && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches)) if (!empty($item['options']) && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches))
{ {
$item['popup'] = $matches[2].'x'.$matches[3]; $item['popup'] = $matches[2].'x'.$matches[3];
$item['onExecute'] = 'javaScript:nm_action'; $item['onExecute'] = 'javaScript:nm_action';
@ -326,7 +326,7 @@ class admin_ui
public static function get_users(array $query, array &$rows=null) public static function get_users(array $query, array &$rows=null)
{ {
$params = array( $params = array(
'type' => (int)$query['filter'] ? (int)$query['filter'] : 'accounts', 'type' => (int)($query['filter'] ?? 0) ?: 'accounts',
'start' => $query['start'], 'start' => $query['start'],
'offset' => $query['num_rows'], 'offset' => $query['num_rows'],
'order' => $query['order'], 'order' => $query['order'],
@ -334,7 +334,7 @@ class admin_ui
'active' => !empty($query['active']) ? $query['active'] : false, 'active' => !empty($query['active']) ? $query['active'] : false,
); );
// Make sure active filter give status what it needs // Make sure active filter give status what it needs
switch($query['filter2']) switch($query['filter2'] ?? '')
{ {
case 'disabled': case 'disabled':
case 'expired': case 'expired':
@ -356,12 +356,12 @@ class admin_ui
break; break;
} }
if ($query['searchletter']) if (!empty($query['searchletter']))
{ {
$params['query'] = $query['searchletter']; $params['query'] = $query['searchletter'];
$params['query_type'] = 'start'; $params['query_type'] = 'start';
} }
elseif($query['search']) elseif(!empty($query['search']))
{ {
$params['query'] = $query['search']; $params['query'] = $query['search'];
$params['query_type'] = 'all'; $params['query_type'] = 'all';
@ -377,7 +377,7 @@ class admin_ui
foreach($rows as $key => &$row) foreach($rows as $key => &$row)
{ {
// Filter by status // Filter by status
if ($need_status_filter && !static::filter_status($need_status_filter, $row)) if (!empty($need_status_filter) && !static::filter_status($need_status_filter, $row))
{ {
unset($rows[$key]); unset($rows[$key]);
$total--; $total--;
@ -391,8 +391,8 @@ class admin_ui
if (!self::$accounts->is_active($row)) $row['status_class'] = 'adminAccountInactive'; if (!self::$accounts->is_active($row)) $row['status_class'] = 'adminAccountInactive';
} }
// finally limit query, if status filter was used // finally, limit query, if status filter was used
if ($need_status_filter) if (!empty($need_status_filter))
{ {
$rows = array_values(array_slice($rows, (int)$query['start'], $query['num_rows'] ?: count($rows))); $rows = array_values(array_slice($rows, (int)$query['start'], $query['num_rows'] ?: count($rows)));
} }
@ -436,9 +436,9 @@ class admin_ui
{ {
$groups = $GLOBALS['egw']->accounts->search(array( $groups = $GLOBALS['egw']->accounts->search(array(
'type' => 'groups', 'type' => 'groups',
'query' => $query['search'], 'query' => $query['search'] ?? null,
'order' => $query['order'], 'order' => $query['order'] ?? null,
'sort' => $query['sort'], 'sort' => $query['sort'] ?? null,
'start' => (int)$query['start'], 'start' => (int)$query['start'],
'offset' => (int)$query['num_rows'] 'offset' => (int)$query['num_rows']
)); ));
@ -463,7 +463,7 @@ class admin_ui
$run_rights = $GLOBALS['egw']->acl->get_user_applications($group['account_id'], false, false); $run_rights = $GLOBALS['egw']->acl->get_user_applications($group['account_id'], false, false);
foreach($apps as $app) foreach($apps as $app)
{ {
if((boolean)$run_rights[$app]) if(!empty($run_rights[$app]))
{ {
$group['apps'][] = $app; $group['apps'][] = $app;
} }
@ -537,7 +537,7 @@ class admin_ui
if (!empty($data['icon'])) if (!empty($data['icon']))
{ {
$icon = Etemplate\Widget\Tree::imagePath($data['icon']); $icon = Etemplate\Widget\Tree::imagePath($data['icon']);
if ($data['child'] || $data[Tree::CHILDREN]) if (!empty($data['child']) || !empty($data[Tree::CHILDREN]))
{ {
$data[Tree::IMAGE_FOLDER_OPEN] = $data[Tree::IMAGE_FOLDER_CLOSED] = $icon; $data[Tree::IMAGE_FOLDER_OPEN] = $data[Tree::IMAGE_FOLDER_CLOSED] = $icon;
} }

View File

@ -241,7 +241,7 @@ export class et2_details extends et2_box
.click(function () { .click(function () {
self._toggle(); self._toggle();
}) })
.text(this.options.title); .text(this.egw().lang(this.options.title));
} }
// Align toggle button left/right // Align toggle button left/right

View File

@ -87,6 +87,12 @@ export class et2_placeholder_select extends et2_inputWidget
[], [],
function(_content) function(_content)
{ {
if(typeof _content === 'object' && _content.message)
{
// Something went wrong
this.egw().message(_content.message, 'error');
return;
}
this.egw().loading_prompt('placeholder_select', false); this.egw().loading_prompt('placeholder_select', false);
et2_placeholder_select.placeholders = _content; et2_placeholder_select.placeholders = _content;
callback.apply(self, arguments); callback.apply(self, arguments);
@ -132,7 +138,13 @@ export class et2_placeholder_select extends et2_inputWidget
let data = { let data = {
content: {app: '', group: '', entry: {}}, content: {app: '', group: '', entry: {}},
sel_options: {app: [], group: []}, sel_options: {app: [], group: []},
modifications: {outer_box: {entry: {}}} modifications: {
outer_box: {
entry: {
application_list: []
}
}
}
}; };
Object.keys(_data).map((key) => Object.keys(_data).map((key) =>
@ -145,9 +157,16 @@ export class et2_placeholder_select extends et2_inputWidget
}); });
data.sel_options.group = this._get_group_options(Object.keys(_data)[0]); data.sel_options.group = this._get_group_options(Object.keys(_data)[0]);
data.content.app = data.sel_options.app[0].value; data.content.app = data.sel_options.app[0].value;
data.content.group = data.sel_options.group[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.content.entry = {app: data.content.app};
data.modifications.outer_box.entry.application_list = Object.keys(_data); data.modifications.outer_box.entry.application_list = Object.keys(_data);
// Remove non-app placeholders (user & general)
let non_apps = ['user', 'general'];
for(let i = 0; i < non_apps.length; i++)
{
let index = data.modifications.outer_box.entry.application_list.indexOf(non_apps[i]);
data.modifications.outer_box.entry.application_list.splice(index, 1);
}
// callback for dialog // callback for dialog
this.submit_callback = function(submit_button_id, submit_value) this.submit_callback = function(submit_button_id, submit_value)
@ -162,7 +181,7 @@ export class et2_placeholder_select extends et2_inputWidget
this.dialog = <et2_dialog>et2_createWidget("dialog", this.dialog = <et2_dialog>et2_createWidget("dialog",
{ {
callback: this.submit_callback, callback: this.submit_callback,
title: this.options.dialog_title || this.egw().lang("Insert Placeholder"), title: this.egw().lang(this.options.dialog_title) || this.egw().lang("Insert Placeholder"),
buttons: buttons, buttons: buttons,
minWidth: 500, minWidth: 500,
minHeight: 400, minHeight: 400,
@ -207,14 +226,35 @@ export class et2_placeholder_select extends et2_inputWidget
// Bind some handlers // Bind some handlers
app.onchange = (node, widget) => app.onchange = (node, widget) =>
{ {
group.set_select_options(this._get_group_options(widget.get_value())); preview.set_value("");
entry.set_value({app: widget.get_value()}); if(['user'].indexOf(widget.get_value()) >= 0)
{
entry.set_disabled(true);
entry.app_select.val('user');
entry.set_value({app: 'user', id: '', query: ''});
}
else if(widget.get_value() == 'general')
{
// Don't change entry app, leave it
entry.set_disabled(false);
}
else
{
entry.set_disabled(false);
entry.app_select.val(widget.get_value());
entry.set_value({app: widget.get_value(), id: '', query: ''});
}
let groups = this._get_group_options(widget.get_value());
group.set_select_options(groups);
group.set_value(groups[0].value);
group.onchange();
} }
group.onchange = (select_node, select_widget) => group.onchange = (select_node, select_widget) =>
{ {
console.log(this, arguments); let options = this._get_placeholders(app.get_value(), group.get_value())
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value())); placeholder_list.set_select_options(options);
preview.set_value(""); preview.set_value("");
placeholder_list.set_value(options[0].value);
} }
placeholder_list.onchange = this._on_placeholder_select.bind(this); placeholder_list.onchange = this._on_placeholder_select.bind(this);
entry.onchange = this._on_placeholder_select.bind(this); entry.onchange = this._on_placeholder_select.bind(this);
@ -227,7 +267,7 @@ export class et2_placeholder_select extends et2_inputWidget
this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_content").getDOMNode().textContent); this.options.insert_callback(this.dialog.template.widgetContainer.getDOMWidgetById("preview_content").getDOMNode().textContent);
}; };
this._on_placeholder_select(); app.set_value(app.get_value());
} }
/** /**
@ -252,9 +292,13 @@ export class et2_placeholder_select extends et2_inputWidget
// Show the selected placeholder replaced with value from the selected entry // Show the selected placeholder replaced with value from the selected entry
this.egw().json( this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders', 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
[app.get_value(), placeholder_list.get_value(), entry.get_value()], [placeholder_list.get_value(), entry.get_value()],
function(_content) function(_content)
{ {
if(!_content)
{
_content = '';
}
preview_content.set_value(_content); preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden'; preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
}.bind(this) }.bind(this)
@ -277,11 +321,37 @@ export class et2_placeholder_select extends et2_inputWidget
let options = []; let options = [];
Object.keys(et2_placeholder_select.placeholders[appname]).map((key) => Object.keys(et2_placeholder_select.placeholders[appname]).map((key) =>
{ {
options.push( // @ts-ignore
if(Object.keys(et2_placeholder_select.placeholders[appname][key]).filter((key) => isNaN(key)).length > 0)
{ {
// Handle groups of groups
if(typeof et2_placeholder_select.placeholders[appname][key].label !== "undefined")
{
options[key] = et2_placeholder_select.placeholders[appname][key];
}
else
{
options[this.egw().lang(key)] = [];
for(let sub of Object.keys(et2_placeholder_select.placeholders[appname][key]))
{
if(!et2_placeholder_select.placeholders[appname][key][sub])
{
continue;
}
options[key].push({
value: key + '-' + sub,
label: this.egw().lang(sub)
});
}
}
}
else
{
options.push({
value: key, value: key,
label: this.egw().lang(key) label: this.egw().lang(key)
}); });
}
}); });
return options; return options;
} }
@ -295,16 +365,13 @@ export class et2_placeholder_select extends et2_inputWidget
*/ */
_get_placeholders(appname : string, group : string) _get_placeholders(appname : string, group : string)
{ {
let options = []; let _group = group.split('-', 2);
Object.keys(et2_placeholder_select.placeholders[appname][group]).map((key) => let ph = et2_placeholder_select.placeholders[appname];
for(let i = 0; typeof ph !== "undefined" && i < _group.length; i++)
{ {
options.push( ph = ph[_group[i]];
{ }
value: key, return ph || [];
label: et2_placeholder_select.placeholders[appname][group][key]
});
});
return options;
} }
/** /**
@ -342,8 +409,9 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select
static placeholders = { static placeholders = {
"addressbook": { "addressbook": {
"addresses": { "addresses": {
"{{n_fn}}\n{{adr_one_street}}{{NELF adr_one_street2}}\n{{adr_one_formatted}}": "Work address", "{{org_name}}\n{{n_fn}}\n{{adr_one_street}}{{NELF adr_one_street2}}\n{{adr_one_formatted}}": "Business address",
"{{n_fn}}\n{{adr_two_street}}{{NELF adr_two_street2}}\n{{adr_two_formatted}}": "Home address", "{{n_fn}}\n{{adr_two_street}}{{NELF adr_two_street2}}\n{{adr_two_formatted}}": "Home address",
"{{n_fn}}\n{{email}}\n{{tel_work}}": "Name, email, phone"
} }
} }
}; };
@ -385,6 +453,7 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select
placeholder_list.onchange = this._on_placeholder_select.bind(this); placeholder_list.onchange = this._on_placeholder_select.bind(this);
entry.onchange = this._on_placeholder_select.bind(this); entry.onchange = this._on_placeholder_select.bind(this);
app.set_value(app.get_value());
this._on_placeholder_select(); this._on_placeholder_select();
} }
@ -405,9 +474,13 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select
// Show the selected placeholder replaced with value from the selected entry // Show the selected placeholder replaced with value from the selected entry
this.egw().json( this.egw().json(
'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders', 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders',
[app.get_value(), placeholder_list.get_value(), entry.get_value()], [placeholder_list.get_value(), {app: "addressbook", id: entry.get_value()}],
function(_content) function(_content)
{ {
if(!_content)
{
_content = '';
}
this.set_value(_content); this.set_value(_content);
preview_content.set_value(_content); preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden'; preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
@ -459,7 +532,7 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select
options.push( options.push(
{ {
value: key, value: key,
label: et2_placeholder_snippet_select.placeholders[appname][group][key] label: this.egw().lang(et2_placeholder_snippet_select.placeholders[appname][group][key])
}); });
}); });
return options; return options;

View File

@ -991,7 +991,8 @@ export class et2_selectbox extends et2_inputWidget
if(sub == 'value') continue; if(sub == 'value') continue;
if (typeof _options[key][sub] === 'object' && _options[key][sub] !== null) if (typeof _options[key][sub] === 'object' && _options[key][sub] !== null)
{ {
this._appendOptionElement(sub, this._appendOptionElement(
typeof _options[key][sub]["value"] !== "undefined" ? _options[key][sub]["value"] : sub,
_options[key][sub]["label"] ? _options[key][sub]["label"] : "", _options[key][sub]["label"] ? _options[key][sub]["label"] : "",
_options[key][sub]["title"] ? _options[key][sub]["title"] : "", _options[key][sub]["title"] ? _options[key][sub]["title"] : "",
group group

View File

@ -853,6 +853,38 @@ export class etemplate2
} }
} }
/**
* Check if there is an invalid widget / all widgets are valid
*
* @param container
* @param values
* @return et2_widget|null first invalid widget or null, if all are valid
*/
isInvalid(container : et2_container|undefined, values : object|undefined) : et2_widget|null
{
if (typeof container === 'undefined')
{
container = this._widgetContainer;
}
if (typeof values === 'undefined')
{
values = this.getValues(container);
}
let invalid = null;
container.iterateOver(function (_widget)
{
if (_widget.submit(values) === false)
{
if(!invalid && !_widget.isValid([]))
{
invalid = _widget;
}
}
}, this, et2_ISubmitListener);
return invalid;
}
/** /**
* Submit form via ajax * Submit form via ajax
* *
@ -881,17 +913,7 @@ export class etemplate2
let invalid = null; let invalid = null;
if (!no_validation) if (!no_validation)
{ {
container.iterateOver(function (_widget) canSubmit = !(invalid = this.isInvalid(container, values));
{
if (_widget.submit(values) === false)
{
if (!invalid && !_widget.isValid())
{
invalid = _widget;
}
canSubmit = false;
}
}, this, et2_ISubmitListener);
} }
if (canSubmit) if (canSubmit)
@ -1098,7 +1120,6 @@ export class etemplate2
return result; return result;
} }
/** /**
* "Intelligently" refresh the template based on the given ID * "Intelligently" refresh the template based on the given ID
* *

View File

@ -733,6 +733,49 @@ export abstract class EgwApp
this.et2_view.close = destroy; this.et2_view.close = destroy;
} }
/**
* Merge selected entries into template document
*
* @param {egwAction} _action
* @param {egwActionObject[]} _selected
*/
merge(_action : egwAction, _selected : egwActionObject[])
{
// Find what we need
let nm = null;
let action = _action;
let as_pdf = false;
// Find Select all
while(nm == null && action != null)
{
if(action.data != null && action.data.nextmatch)
{
nm = action.data.nextmatch;
}
action = action.parent;
}
let all = nm?.getSelection().all || false;
as_pdf = action.getActionById('as_pdf')?.checked || false;
// Get list of entry IDs
let ids = [];
for(let i = 0; !all && i < _selected.length; i++)
{
let split = _selected[i].id.split("::");
ids.push(split[1]);
}
let vars = {
..._action.data.merge_data,
pdf: as_pdf,
select_all: all,
id: JSON.stringify(ids)
};
egw.open_link(egw.link('/index.php', vars), '_blank');
}
/** /**
* Initializes actions and handlers on sidebox (delete) * Initializes actions and handlers on sidebox (delete)
* *

View File

@ -724,6 +724,7 @@ insert new column behind this one common de Neue Spalte hinter dieser einfügen
insert new column in front of all common de Neue Spalte vor dieser einfügen insert new column in front of all common de Neue Spalte vor dieser einfügen
insert new row after this one common de Neue Zeile nach dieser einfügen insert new row after this one common de Neue Zeile nach dieser einfügen
insert new row in front of first line common de Neue Zeile vor dieser einfügen insert new row in front of first line common de Neue Zeile vor dieser einfügen
insert placeholder common de Platzhalter einfügen
insert row after common de Zeile danach einfügen insert row after common de Zeile danach einfügen
insert row before common de Zeile davor einfügen insert row before common de Zeile davor einfügen
insert timestamp into description field common de Zeitstempel in das Beschreibungs-Feld einfügen insert timestamp into description field common de Zeitstempel in das Beschreibungs-Feld einfügen
@ -1072,6 +1073,7 @@ preference common de Einstellung
preferences common de Einstellungen preferences common de Einstellungen
preferences for the %1 template set preferences de Einstellungen für das %1 Template preferences for the %1 template set preferences de Einstellungen für das %1 Template
prev common de Vorheriger prev common de Vorheriger
preview with entry common de Vorschau aus Eintrag
previous common de Vorherige previous common de Vorherige
previous page common de Vorherige Seite previous page common de Vorherige Seite
primary group common de Hauptgruppe primary group common de Hauptgruppe

View File

@ -724,6 +724,7 @@ insert new column behind this one common en Insert new column after
insert new column in front of all common en Insert new column before all insert new column in front of all common en Insert new column before all
insert new row after this one common en Insert new row after insert new row after this one common en Insert new row after
insert new row in front of first line common en Insert new row before first line insert new row in front of first line common en Insert new row before first line
insert placeholder common en Insert placeholder
insert row after common en Insert row after insert row after common en Insert row after
insert row before common en Insert row before insert row before common en Insert row before
insert timestamp into description field common en Insert timestamp into description field insert timestamp into description field common en Insert timestamp into description field
@ -1073,6 +1074,7 @@ preference common en Preference
preferences common en Preferences preferences common en Preferences
preferences for the %1 template set preferences en Preferences for the %1 template set preferences for the %1 template set preferences en Preferences for the %1 template set
prev common en Prev prev common en Prev
preview with entry common en Preview with entry
previous common en Previous previous common en Previous
previous page common en Previous page previous page common en Previous page
primary group common en Primary group primary group common en Primary group

View File

@ -223,7 +223,7 @@ class Accounts
if (!empty($param['offset']) && !isset($param['start'])) $param['start'] = 0; if (!empty($param['offset']) && !isset($param['start'])) $param['start'] = 0;
// Check for lang(Group) in search - if there, we search all groups // Check for lang(Group) in search - if there, we search all groups
$group_index = array_search(strtolower(lang('Group')), array_map('strtolower', $query = explode(' ',$param['query']))); $group_index = array_search(strtolower(lang('Group')), array_map('strtolower', $query = explode(' ',$param['query'] ?? '')));
if($group_index !== FALSE && !( if($group_index !== FALSE && !(
in_array($param['type'], array('accounts', 'groupmembers')) || is_int($param['type']) in_array($param['type'], array('accounts', 'groupmembers')) || is_int($param['type'])
)) ))
@ -595,12 +595,12 @@ class Accounts
/** /**
* Return formatted username for a given account_id * Return formatted username for a given account_id
* *
* @param string $account_id =null account id * @param int $account_id account id
* @return string full name of user or "#$accountid" if user not found * @return string full name of user or "#$account_id" if user not found
*/ */
static function username($account_id=null) static function username(int $account_id)
{ {
if ($account_id && !($account = self::cache_read((int)$account_id))) if (!($account = self::cache_read($account_id)))
{ {
return '#'.$account_id; return '#'.$account_id;
} }

View File

@ -993,6 +993,34 @@ class CalDAV extends HTTP_WebDAV_Server
parent::http_PROPFIND('REPORT'); parent::http_PROPFIND('REPORT');
} }
/**
* REST API PATCH handler
*
* Currently, only implemented for REST not CalDAV/CardDAV
*
* @param $options
* @param $files
* @return string|void
*/
function PATCH(array &$options)
{
if (!preg_match('#^application/([^; +]+\+)?json#', $_SERVER['HTTP_CONTENT_TYPE']))
{
return '501 Not implemented';
}
return $this->PUT($options, 'PATCH');
}
/**
* REST API PATCH handler
*
* Just calls http_PUT()
*/
function http_PATCH()
{
return parent::http_PUT('PATCH');
}
/** /**
* Check if client want or sends JSON * Check if client want or sends JSON
* *
@ -1003,7 +1031,7 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
if (!isset($type)) if (!isset($type))
{ {
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ? $type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PATCH', 'PROPPATCH']) ?
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT']; $_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
} }
return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ? return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ?
@ -1427,7 +1455,7 @@ class CalDAV extends HTTP_WebDAV_Server
substr($options['path'], -1) === '/' && self::isJSON()) substr($options['path'], -1) === '/' && self::isJSON())
{ {
$_GET['add-member'] = ''; // otherwise we give no Location header $_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options); return $this->PUT($options, 'POST');
} }
if ($this->debug) error_log(__METHOD__.'('.array2string($options).')'); if ($this->debug) error_log(__METHOD__.'('.array2string($options).')');
@ -1915,7 +1943,7 @@ class CalDAV extends HTTP_WebDAV_Server
* @param array parameter passing array * @param array parameter passing array
* @return bool true on success * @return bool true on success
*/ */
function PUT(&$options) function PUT(&$options, $method='PUT')
{ {
// read the content in a string, if a stream is given // read the content in a string, if a stream is given
if (isset($options['stream'])) if (isset($options['stream']))
@ -1934,9 +1962,14 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
return '404 Not Found'; return '404 Not Found';
} }
// REST API & PATCH only implemented for addressbook currently
if ($app !== 'addressbook' && $method === 'PATCH')
{
return '501 Not implemented';
}
if (($handler = self::app_handler($app))) if (($handler = self::app_handler($app)))
{ {
$status = $handler->put($options,$id,$user,$prefix); $status = $handler->put($options, $id, $user, $prefix, $method, $_SERVER['HTTP_CONTENT_TYPE']);
// set default stati: true --> 204 No Content, false --> should be already handled // set default stati: true --> 204 No Content, false --> should be already handled
if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong'; if (is_bool($status)) $status = $status ? '204 No Content' : '400 Something went wrong';

View File

@ -60,6 +60,7 @@ abstract class Handler
var $method2acl = array( var $method2acl = array(
'GET' => Api\Acl::READ, 'GET' => Api\Acl::READ,
'PUT' => Api\Acl::EDIT, 'PUT' => Api\Acl::EDIT,
'PATCH' => Api\Acl::EDIT,
'DELETE' => Api\Acl::DELETE, 'DELETE' => Api\Acl::DELETE,
); );
/** /**

View File

@ -483,7 +483,7 @@ class Contacts extends Contacts\Storage
'bday' => (int)$contact['bday'] ? DateTime::to($contact['bday'], true) : $contact['bday'], 'bday' => (int)$contact['bday'] ? DateTime::to($contact['bday'], true) : $contact['bday'],
))); )));
while ($fileas[0] == ':' || $fileas[0] == ',') while (!empty($fileas) && ($fileas[0] == ':' || $fileas[0] == ','))
{ {
$fileas = substr($fileas,2); $fileas = substr($fileas,2);
} }
@ -764,10 +764,10 @@ class Contacts extends Contacts\Storage
$data[$name] = DateTime::server2user($data[$name], $date_format); $data[$name] = DateTime::server2user($data[$name], $date_format);
} }
} }
$data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'] || ($data['files'] & self::FILES_BIT_PHOTO), '', $data['etag']); $data['photo'] = $this->photo_src($data['id'],!empty($data['jpegphoto']) || (($data['files']??0) & self::FILES_BIT_PHOTO), '', $data['etag'] ?? null);
// set freebusy_uri for accounts // set freebusy_uri for accounts
if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup'])) if (empty($data['freebusy_uri']) && empty($data['owner']) && !empty($data['account_id']) && empty($GLOBALS['egw_setup']))
{ {
if ($fb_url || @is_dir(EGW_SERVER_ROOT.'/calendar/inc')) if ($fb_url || @is_dir(EGW_SERVER_ROOT.'/calendar/inc'))
{ {
@ -1686,7 +1686,7 @@ class Contacts extends Contacts\Storage
{ {
$result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]); $result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]);
// make sure to return a correctly quoted rfc822 address, if requested // make sure to return a correctly quoted rfc822 address, if requested
if ($options['type'] === 'email') if (isset($options['type']) && $options['type'] === 'email')
{ {
$args = explode('@', $contact['email']); $args = explode('@', $contact['email']);
$args[] = $result[$contact['id']]; $args[] = $result[$contact['id']];

View File

@ -78,16 +78,34 @@ class JsContact
/** /**
* Parse JsCard * Parse JsCard
* *
* We use strict parsing for "application/jscontact+json" content-type, not for "application/json".
* Strict parsing checks objects for proper @type attributes and value attributes, non-strict allows scalar values.
*
* Non-strict parsing also automatic detects patch for POST requests.
*
* @param string $json * @param string $json
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param array $old=[] existing contact for patch
* @param ?string $content_type=null application/json no strict parsing and automatic patch detection, if method not 'PATCH' or 'PUT'
* @param string $method='PUT' 'PUT', 'POST' or 'PATCH'
* @return array * @return array
*/ */
public static function parseJsCard(string $json, bool $check_at_type=true) public static function parseJsCard(string $json, array $old=[], string $content_type=null, $method='PUT')
{ {
try try
{ {
$strict = !isset($content_type) || !preg_match('#^application/json#', $content_type);
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR); $data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
// check if we use patch: method is PATCH or method is POST AND keys contain slashes
if ($method === 'PATCH' || !$strict && $method === 'POST' && array_filter(array_keys($data), static function ($key)
{
return strpos($key, '/') !== false;
}))
{
// apply patch on JsCard of contact
$data = self::patch($data, $old ? self::getJsCard($old, false) : [], !$old);
}
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
$contact = []; $contact = [];
@ -96,53 +114,72 @@ class JsContact
switch ($name) switch ($name)
{ {
case 'uid': case 'uid':
$contact['uid'] = self::parseUid($value); $contact['uid'] = self::parseUid($value, $old['uid'], !$strict);
break; break;
case 'name': case 'name':
$contact += self::parseNameComponents($value, $check_at_type); $contact += self::parseNameComponents($value, $strict);
break; break;
case 'fullName': case 'fullName':
$contact['n_fn'] = self::parseString($value); $contact['n_fn'] = self::parseString($value);
// if no separate name-components given, simply split first word off as n_given and rest as n_family
if (!isset($data['name']) && !empty($contact['n_fn']))
{
if (preg_match('/^([^ ,]+)(,?) (.*)$/', $contact['n_fn'], $matches))
{
if (!empty($matches[2]))
{
list(, $contact['n_family'], , $contact['n_given']) = $matches;
}
else
{
list(, $contact['n_given'], , $contact['n_family']) = $matches;
}
}
else
{
$contact['n_family'] = $contact['n_fn'];
}
}
break; break;
case 'organizations': case 'organizations':
$contact += self::parseOrganizations($value, $check_at_type); $contact += self::parseOrganizations($value, $strict);
break; break;
case 'titles': case 'titles':
$contact += self::parseTitles($value, $check_at_type); $contact += self::parseTitles($value, $strict);
break; break;
case 'emails': case 'emails':
$contact += self::parseEmails($value, $check_at_type); $contact += self::parseEmails($value, $strict);
break; break;
case 'phones': case 'phones':
$contact += self::parsePhones($value, $check_at_type); $contact += self::parsePhones($value, $strict);
break; break;
case 'online': case 'online':
$contact += self::parseOnline($value, $check_at_type); $contact += self::parseOnline($value, $strict);
break; break;
case 'addresses': case 'addresses':
$contact += self::parseAddresses($value, $check_at_type); $contact += self::parseAddresses($value, $strict);
break; break;
case 'photos': case 'photos':
$contact += self::parsePhotos($value, $check_at_type); $contact += self::parsePhotos($value, $strict);
break; break;
case 'anniversaries': case 'anniversaries':
$contact += self::parseAnniversaries($value); $contact += self::parseAnniversaries($value, $strict);
break; break;
case 'notes': case 'notes':
$contact['note'] = implode("\n", array_map(static function ($note) { $contact['note'] = implode("\n", array_map(static function ($note) {
return self::parseString($note); return self::parseString($note);
}, $value)); }, (array)$value));
break; break;
case 'categories': case 'categories':
@ -150,7 +187,7 @@ class JsContact
break; break;
case 'egroupware.org:customfields': case 'egroupware.org:customfields':
$contact += self::parseCustomfields($value); $contact += self::parseCustomfields($value, $strict);
break; break;
case 'egroupware.org:assistant': case 'egroupware.org:assistant':
@ -197,11 +234,12 @@ class JsContact
* Parse and optionally generate UID * Parse and optionally generate UID
* *
* @param string|null $uid * @param string|null $uid
* @param string|null $old old value, if given it must NOT change
* @param bool $generate_when_empty true: generate UID if empty, false: throw error * @param bool $generate_when_empty true: generate UID if empty, false: throw error
* @return string without urn:uuid: prefix * @return string without urn:uuid: prefix
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
protected static function parseUid(string $uid=null, $generate_when_empty=false) protected static function parseUid(string $uid=null, string $old=null, bool $generate_when_empty=false)
{ {
if (empty($uid) || strlen($uid) < 12) if (empty($uid) || strlen($uid) < 12)
{ {
@ -211,7 +249,15 @@ class JsContact
} }
$uid = \HTTP_WebDAV_Server::_new_uuid(); $uid = \HTTP_WebDAV_Server::_new_uuid();
} }
return strpos($uid, self::URN_UUID_PREFIX) === 0 ? substr($uid, 9) : $uid; if (strpos($uid, self::URN_UUID_PREFIX) === 0)
{
$uid = substr($uid, strlen(self::URN_UUID_PREFIX));
}
if (isset($old) && $old !== $uid)
{
throw new \InvalidArgumentException("You must NOT change the UID ('$old'): ".json_encode($uid));
}
return $uid;
} }
/** /**
@ -248,15 +294,15 @@ class JsContact
* As we store only one organization, the rest get lost, multiple units get concatenated by space. * As we store only one organization, the rest get lost, multiple units get concatenated by space.
* *
* @param array $orgas * @param array $orgas
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseOrganizations(array $orgas, bool $check_at_type=true) protected static function parseOrganizations(array $orgas, bool $stict=true)
{ {
$contact = []; $contact = [];
foreach($orgas as $orga) foreach($orgas as $orga)
{ {
if ($check_at_type && $orga[self::AT_TYPE] !== self::TYPE_ORGANIZATION) if ($stict && $orga[self::AT_TYPE] !== self::TYPE_ORGANIZATION)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($orga, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($orga, self::JSON_OPTIONS_ERROR));
} }
@ -306,15 +352,19 @@ class JsContact
* Parse titles, thought we only have "title" and "role" available for storage. * Parse titles, thought we only have "title" and "role" available for storage.
* *
* @param array $titles * @param array $titles
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseTitles(array $titles, bool $check_at_type=true) protected static function parseTitles(array $titles, bool $stict=true)
{ {
$contact = []; $contact = [];
foreach($titles as $id => $title) foreach($titles as $id => $title)
{ {
if ($check_at_type && $title[self::AT_TYPE] !== self::TYPE_TITLE) if (!$stict && is_string($title))
{
$title = ['title' => $title];
}
if ($stict && $title[self::AT_TYPE] !== self::TYPE_TITLE)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: " . json_encode($title[self::AT_TYPE])); throw new \InvalidArgumentException("Missing or invalid @type: " . json_encode($title[self::AT_TYPE]));
} }
@ -397,8 +447,12 @@ class JsContact
foreach($definitions as $name => $definition) foreach($definitions as $name => $definition)
{ {
$data = $cfs[$name]; $data = $cfs[$name];
if (isset($data[$name])) if (isset($data))
{ {
if (is_scalar($data))
{
$data = ['value' => $data];
}
if (!is_array($data) || !array_key_exists('value', $data)) if (!is_array($data) || !array_key_exists('value', $data))
{ {
throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR));
@ -526,21 +580,21 @@ class JsContact
* Parse addresses object containing multiple addresses * Parse addresses object containing multiple addresses
* *
* @param array $addresses * @param array $addresses
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseAddresses(array $addresses, bool $check_at_type=true) protected static function parseAddresses(array $addresses, bool $stict=true)
{ {
$n = 0; $n = 0;
$last_type = null; $last_type = null;
$contact = []; $contact = [];
foreach($addresses as $id => $address) foreach($addresses as $id => $address)
{ {
if ($check_at_type && $address[self::AT_TYPE] !== self::TYPE_ADDRESS) if ($stict && $address[self::AT_TYPE] !== self::TYPE_ADDRESS)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($address)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($address));
} }
$contact += ($values=self::parseAddress($address, $id, $last_type)); $contact += ($values=self::parseAddress($address, $id, $last_type, $stict));
if (++$n > 2) if (++$n > 2)
{ {
@ -567,9 +621,10 @@ class JsContact
* @param array $address address-object * @param array $address address-object
* @param string $id index * @param string $id index
* @param ?string $last_type "work" or "home" * @param ?string $last_type "work" or "home"
* @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseAddress(array $address, string $id, string &$last_type=null) protected static function parseAddress(array $address, string $id, string &$last_type=null, bool $stict=true)
{ {
$type = !isset($last_type) && (empty($address['contexts']['private']) || $id === 'work') || $type = !isset($last_type) && (empty($address['contexts']['private']) || $id === 'work') ||
$last_type === 'home' ? 'work' : 'home'; $last_type === 'home' ? 'work' : 'home';
@ -577,7 +632,10 @@ class JsContact
$prefix = $type === 'work' ? 'adr_one_' : 'adr_two_'; $prefix = $type === 'work' ? 'adr_one_' : 'adr_two_';
$contact = [$prefix.'street' => null, $prefix.'street2' => null]; $contact = [$prefix.'street' => null, $prefix.'street2' => null];
list($contact[$prefix.'street'], $contact[$prefix.'street2']) = self::parseStreetComponents($address['street']); if (!empty($address['street']))
{
list($contact[$prefix.'street'], $contact[$prefix.'street2']) = self::parseStreetComponents($address['street'], $stict);
}
foreach(self::$jsAddress2attr+self::$jsAddress2workAttr as $js => $attr) foreach(self::$jsAddress2attr+self::$jsAddress2workAttr as $js => $attr)
{ {
if (isset($address[$js]) && !is_string($address[$js])) if (isset($address[$js]) && !is_string($address[$js]))
@ -586,6 +644,17 @@ class JsContact
} }
$contact[$prefix.$attr] = $address[$js]; $contact[$prefix.$attr] = $address[$js];
} }
// no country-code but a name translating to a code --> use it
if (empty($contact[$prefix.'countrycode']) && !empty($contact[$prefix.'countryname']) &&
strlen($code = Api\Country::country_code($contact[$prefix.'countryname'])) === 2)
{
$contact[$prefix.'countrycode'] = $code;
}
// if we have a valid code, the untranslated name as our UI does
if (!empty($contact[$prefix.'countrycode']) && !empty($name = Api\Country::get_full_name($contact[$prefix.'countrycode'], false)))
{
$contact[$prefix.'countryname'] = $name;
}
return $contact; return $contact;
} }
@ -637,12 +706,20 @@ class JsContact
* As we have only 2 address-lines, we combine all components, with one space as separator, if none given. * As we have only 2 address-lines, we combine all components, with one space as separator, if none given.
* Then we split it into 2 lines. * Then we split it into 2 lines.
* *
* @param array $components * @param array|string $components string only for relaxed parsing
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return string[] street and street2 values * @return string[] street and street2 values
*/ */
protected static function parseStreetComponents(array $components, bool $check_at_type=true) protected static function parseStreetComponents($components, bool $stict=true)
{ {
if (!$stict && is_string($components))
{
$components = [['type' => 'name', 'value' => $components]];
}
if (!is_array($components))
{
throw new \InvalidArgumentException("Invalid street-components: ".json_encode($components, self::JSON_OPTIONS_ERROR));
}
$street = []; $street = [];
$last_type = null; $last_type = null;
foreach($components as $component) foreach($components as $component)
@ -651,7 +728,7 @@ class JsContact
{ {
throw new \InvalidArgumentException("Invalid street-component: ".json_encode($component, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid street-component: ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
if ($check_at_type && $component[self::AT_TYPE] !== self::TYPE_STREET_COMPONENT) if ($stict && $component[self::AT_TYPE] !== self::TYPE_STREET_COMPONENT)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($component, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
@ -712,21 +789,25 @@ class JsContact
* Parse phone objects * Parse phone objects
* *
* @param array $phones $id => object with attribute "phone" and optional "features" and "context" * @param array $phones $id => object with attribute "phone" and optional "features" and "context"
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parsePhones(array $phones, bool $check_at_type=true) protected static function parsePhones(array $phones, bool $stict=true)
{ {
$contact = []; $contact = [];
// check for good matches // check for good matches
foreach($phones as $id => $phone) foreach($phones as $id => $phone)
{ {
if (!$stict && is_string($phone))
{
$phone = ['phone' => $phone];
}
if (!is_array($phone) || !is_string($phone['phone'])) if (!is_array($phone) || !is_string($phone['phone']))
{ {
throw new \InvalidArgumentException("Invalid phone: " . json_encode($phone, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid phone: " . json_encode($phone, self::JSON_OPTIONS_ERROR));
} }
if ($check_at_type && $phone[self::AT_TYPE] !== self::TYPE_PHONE) if ($stict && $phone[self::AT_TYPE] !== self::TYPE_PHONE)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($phone, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($phone, self::JSON_OPTIONS_ERROR));
} }
@ -818,19 +899,23 @@ class JsContact
* We currently only support 2 URLs, rest get's ignored! * We currently only support 2 URLs, rest get's ignored!
* *
* @param array $values * @param array $values
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseOnline(array $values, bool $check_at_type) protected static function parseOnline(array $values, bool $stict)
{ {
$contact = []; $contact = [];
foreach($values as $id => $value) foreach($values as $id => $value)
{ {
if (!$stict && is_string($value))
{
$value = ['resource' => $value];
}
if (!is_array($value) || !is_string($value['resource'])) if (!is_array($value) || !is_string($value['resource']))
{ {
throw new \InvalidArgumentException("Invalid online resource with id '$id': ".json_encode($value, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid online resource with id '$id': ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
if ($check_at_type && $value[self::AT_TYPE] !== self::TYPE_RESOURCE) if ($stict && $value[self::AT_TYPE] !== self::TYPE_RESOURCE)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($value, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
@ -889,15 +974,19 @@ class JsContact
* *
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.3.1 * @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.3.1
* @param array $emails id => object with attribute "email" and optional "context" * @param array $emails id => object with attribute "email" and optional "context"
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseEmails(array $emails, bool $check_at_type=true) protected static function parseEmails(array $emails, bool $stict=true)
{ {
$contact = []; $contact = [];
foreach($emails as $id => $value) foreach($emails as $id => $value)
{ {
if ($check_at_type && $value[self::AT_TYPE] !== self::TYPE_EMAIL) if (!$stict && is_string($value))
{
$value = ['email' => $value];
}
if ($stict && $value[self::AT_TYPE] !== self::TYPE_EMAIL)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($value, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
@ -905,7 +994,7 @@ class JsContact
{ {
throw new \InvalidArgumentException("Invalid email object (requires email attribute): ".json_encode($value, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid email object (requires email attribute): ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
if (!isset($contact['email']) && $id === 'work' && empty($value['context']['private'])) if (!isset($contact['email']) && ($id === 'work' || empty($value['contexts']['private']) || isset($contact['email_home'])))
{ {
$contact['email'] = $value['email']; $contact['email'] = $value['email'];
} }
@ -953,11 +1042,11 @@ class JsContact
* @return array * @return array
* @ToDo * @ToDo
*/ */
protected static function parsePhotos(array $photos, bool $check_at_type) protected static function parsePhotos(array $photos, bool $stict)
{ {
foreach($photos as $id => $photo) foreach($photos as $id => $photo)
{ {
if ($check_at_type && $photo[self::AT_TYPE] !== self::TYPE_FILE) if ($stict && $photo[self::AT_TYPE] !== self::TYPE_FILE)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($photo, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($photo, self::JSON_OPTIONS_ERROR));
} }
@ -1008,18 +1097,23 @@ class JsContact
* @param array $components * @param array $components
* @return array * @return array
*/ */
protected static function parseNameComponents(array $components, bool $check_at_type=true) protected static function parseNameComponents(array $components, bool $stict=true)
{ {
$contact = array_combine(array_values(self::$nameType2attribute), $contact = array_combine(array_values(self::$nameType2attribute),
array_fill(0, count(self::$nameType2attribute), null)); array_fill(0, count(self::$nameType2attribute), null));
foreach($components as $component) foreach($components as $type => $component)
{ {
// for relaxed checks, allow $type => $value pairs
if (!$stict && is_string($type) && is_scalar($component))
{
$component = ['type' => $type, 'value' => $component];
}
if (empty($component['type']) || isset($component) && !is_string($component['value'])) if (empty($component['type']) || isset($component) && !is_string($component['value']))
{ {
throw new \InvalidArgumentException("Invalid name-component (must have type and value attributes): ".json_encode($component, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid name-component (must have type and value attributes): ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
if ($check_at_type && $component[self::AT_TYPE] !== self::TYPE_NAME_COMPONENT) if ($stict && $component[self::AT_TYPE] !== self::TYPE_NAME_COMPONENT)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($component, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
@ -1052,14 +1146,28 @@ class JsContact
* *
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.5.1 * @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.5.1
* @param array $anniversaries id => object with attribute date and optional type * @param array $anniversaries id => object with attribute date and optional type
* @param bool $check_at_type true: check if objects have their proper @type attribute * @param bool $stict true: check if objects have their proper @type attribute
* @return array * @return array
*/ */
protected static function parseAnniversaries(array $anniversaries, bool $check_at_type=true) protected static function parseAnniversaries(array $anniversaries, bool $stict=true)
{ {
$contact = []; $contact = [];
foreach($anniversaries as $id => $anniversary) foreach($anniversaries as $id => $anniversary)
{ {
if (!$stict && is_string($anniversary))
{
// allow German date format "dd.mm.yyyy"
if (preg_match('/^(\d+)\.(\d+).(\d+)$/', $anniversary, $matches))
{
$matches = sprintf('%04d-%02d-%02d', (int)$matches[3], (int)$matches[2], (int)$matches[1]);
}
// allow US date format "mm/dd/yyyy"
elseif (preg_match('#^(\d+)/(\d+)/(\d+)$#', $anniversary, $matches))
{
$matches = sprintf('%04d-%02d-%02d', (int)$matches[3], (int)$matches[1], (int)$matches[2]);
}
$anniversary = ['type' => $id, 'date' => $anniversary];
}
if (!is_array($anniversary) || !is_string($anniversary['date']) || if (!is_array($anniversary) || !is_string($anniversary['date']) ||
!preg_match('/^\d{4}-\d{2}-\d{2}$/', $anniversary['date']) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $anniversary['date']) ||
(!list($year, $month, $day) = explode('-', $anniversary['date'])) || (!list($year, $month, $day) = explode('-', $anniversary['date'])) ||
@ -1067,7 +1175,7 @@ class JsContact
{ {
throw new \InvalidArgumentException("Invalid anniversary object with id '$id': ".json_encode($anniversary, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Invalid anniversary object with id '$id': ".json_encode($anniversary, self::JSON_OPTIONS_ERROR));
} }
if ($check_at_type && $anniversary[self::AT_TYPE] !== self::TYPE_ANNIVERSARY) if ($stict && $anniversary[self::AT_TYPE] !== self::TYPE_ANNIVERSARY)
{ {
throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($anniversary, self::JSON_OPTIONS_ERROR)); throw new \InvalidArgumentException("Missing or invalid @type: ".json_encode($anniversary, self::JSON_OPTIONS_ERROR));
} }
@ -1251,16 +1359,51 @@ class JsContact
return $members; return $members;
} }
/**
* Patch JsCard
*
* @param array $patches JSON path
* @param array $jscard to patch
* @param bool $create =false true: create missing components
* @return array patched $jscard
*/
public static function patch(array $patches, array $jscard, bool $create=false)
{
foreach($patches as $path => $value)
{
$parts = explode('/', $path);
$target = &$jscard;
foreach($parts as $n => $part)
{
if (!isset($target[$part]) && $n < count($parts)-1 && !$create)
{
throw new \InvalidArgumentException("Trying to patch not existing attribute with path $path!");
}
$parent = $target;
$target = &$target[$part];
}
if (isset($value))
{
$target = $value;
}
else
{
unset($parent[$part]);
}
}
return $jscard;
}
/** /**
* Map all kind of exceptions while parsing to a JsContactParseException * Map all kind of exceptions while parsing to a JsContactParseException
* *
* @param \Throwable $e * @param \Throwable $e
* @param string $type * @param string $type
* @param string $name * @param ?string $name
* @param mixed $value * @param mixed $value
* @throws JsContactParseException * @throws JsContactParseException
*/ */
protected static function handleExceptions(\Throwable $e, $type='JsContact', string $name, $value) protected static function handleExceptions(\Throwable $e, $type='JsContact', ?string $name, $value)
{ {
try { try {
throw $e; throw $e;

View File

@ -275,7 +275,30 @@ class Merge extends Api\Storage\Merge
*/ */
public function get_placeholder_list($prefix = '') public function get_placeholder_list($prefix = '')
{ {
$placeholders = []; // Specific order for these ones
$placeholders = [
'contact' => [],
'details' => [
[
'value' => $this->prefix($prefix, 'categories', '{'),
'label' => lang('Category path')
],
['value' => $this->prefix($prefix, 'note', '{'),
'label' => $this->contacts->contact_fields['note']],
['value' => $this->prefix($prefix, 'id', '{'),
'label' => $this->contacts->contact_fields['id']],
['value' => $this->prefix($prefix, 'owner', '{'),
'label' => $this->contacts->contact_fields['owner']],
['value' => $this->prefix($prefix, 'private', '{'),
'label' => $this->contacts->contact_fields['private']],
['value' => $this->prefix($prefix, 'cat_id', '{'),
'label' => $this->contacts->contact_fields['cat_id']],
],
];
// Iterate through the list & switch groups as we go
// Hopefully a little better than assigning each field to a group
$group = 'contact'; $group = 'contact';
foreach($this->contacts->contact_fields as $name => $label) foreach($this->contacts->contact_fields as $name => $label)
{ {
@ -299,25 +322,37 @@ class Merge extends Api\Storage\Merge
case 'email_home': case 'email_home':
$group = 'email'; $group = 'email';
break; break;
case 'url': case 'freebusy_uri':
$group = 'details'; $group = 'details';
} }
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $label; $marker = $this->prefix($prefix, $name, '{');
if($name == 'cat_id') if(!array_filter($placeholders, function ($a) use ($marker)
{ {
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = lang('Category path'); count(array_filter($a, function ($b) use ($marker)
{
return $b['value'] == $marker;
})
) > 0;
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
} }
} }
// Correctly formatted address by country / preference // Correctly formatted address by country / preference
$placeholders['business']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_one_formatted}}'] = "Formatted business address"; $placeholders['business'][] = [
$placeholders['private']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_two_formatted}}'] = "Formatted private address"; 'value' => $this->prefix($prefix, 'adr_one_formatted', '{'),
'label' => "Formatted business address"
];
$placeholders['private'][] = [
'value' => $this->prefix($prefix, 'adr_two_formatted', '{'),
'label' => "Formatted private address"
];
$group = 'customfields'; $this->add_customfield_placeholders($placeholders, $prefix);
foreach($this->contacts->customfields as $name => $field)
{
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $field['label'];
}
return $placeholders; return $placeholders;
} }

View File

@ -766,6 +766,19 @@ class Country
{ {
if (!$name) return ''; // nothing to do if (!$name) return ''; // nothing to do
// handle names like "Germany (Deutschland)"
if (preg_match('/^([^(]+) \(([^)]+)\)$/', $name, $matches))
{
if (($code = self::country_code($matches[1])) && strlen($code) === 2)
{
return $code;
}
if (($code = self::country_code($matches[2])) && strlen($code) === 2)
{
return $code;
}
}
if (strlen($name) == 2 && isset(self::$country_array[$name])) if (strlen($name) == 2 && isset(self::$country_array[$name]))
{ {
return $name; // $name is already a country-code return $name; // $name is already a country-code

View File

@ -114,7 +114,7 @@ class Widget
// Update content? // Update content?
if(self::$cont == null) if(self::$cont == null)
self::$cont = is_array(self::$request->content) ? self::$request->content : array(); self::$cont = is_array(self::$request->content) ? self::$request->content : array();
if($this->id && is_array(self::$cont[$this->id])) if($this->id && is_array(self::$cont[$this->id] ?? null))
{ {
$old_cont = self::$cont; $old_cont = self::$cont;
self::$cont = self::$cont[$this->id]; self::$cont = self::$cont[$this->id];
@ -147,7 +147,8 @@ class Widget
} }
// Reset content as we leave // Reset content as we leave
if($old_cont) { if (isset($old_cont))
{
self::$cont = $old_cont; self::$cont = $old_cont;
} }
} }
@ -206,7 +207,7 @@ class Widget
$template = $this; $template = $this;
while($reader->moveToNextAttribute()) while($reader->moveToNextAttribute())
{ {
if ($reader->name != 'id' && $template->attr[$reader->name] !== $reader->value) if ($reader->name != 'id' && (!isset($template->attr[$reader->name]) || $template->attr[$reader->name] !== $reader->value))
{ {
if (!$cloned) if (!$cloned)
{ {
@ -218,7 +219,7 @@ class Widget
$template->attrs[$reader->name] = $value = $reader->value; $template->attrs[$reader->name] = $value = $reader->value;
// expand attributes values, otherwise eg. validation can not use attrs referencing to content // expand attributes values, otherwise eg. validation can not use attrs referencing to content
if ($value[0] == '@' || strpos($value, '$cont') !== false) if (!empty($value) && ($value[0] === '@' || strpos($value, '$cont') !== false))
{ {
$value = self::expand_name($value, null, null, null, null, $value = self::expand_name($value, null, null, null, null,
isset(self::$cont) ? self::$cont : self::$request->content); isset(self::$cont) ? self::$cont : self::$request->content);
@ -237,7 +238,7 @@ class Widget
} }
// Add in anything in the modification array // Add in anything in the modification array
if(is_array(self::$request->modifications[$this->id])) if (is_array(self::$request->modifications[$this->id] ?? null))
{ {
$this->attrs = array_merge($this->attrs,self::$request->modifications[$this->id]); $this->attrs = array_merge($this->attrs,self::$request->modifications[$this->id]);
} }
@ -426,7 +427,7 @@ class Widget
class_exists($class_name = $basetype.'_etemplate_widget')))) class_exists($class_name = $basetype.'_etemplate_widget'))))
{ {
// Try for base type, it's probably better than the root // Try for base type, it's probably better than the root
if(self::$widget_registry[$basetype] && self::$widget_registry[$basetype] != $class_name) if(isset(self::$widget_registry[$basetype]) && self::$widget_registry[$basetype] !== $class_name)
{ {
$class_name = self::$widget_registry[$basetype]; $class_name = self::$widget_registry[$basetype];
} }
@ -535,12 +536,12 @@ class Widget
// maintain $expand array name-expansion // maintain $expand array name-expansion
$cname = $params[0]; $cname = $params[0];
$expand =& $params[1]; $expand =& $params[1];
if ($expand['cname'] && $expand['cname'] !== $cname) if (isset($expand['cname']) && $expand['cname'] !== $cname)
{ {
$expand['cont'] =& self::get_array(self::$request->content, $cname); $expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname; $expand['cname'] = $cname;
} }
if ($respect_disabled && ($disabled = $this->attrs['disabled'] && self::check_disabled($this->attrs['disabled'], $expand))) if ($respect_disabled && ($disabled = isset($this->attrs['disabled']) && self::check_disabled($this->attrs['disabled'], $expand)))
{ {
//error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running"); //error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running");
return; return;
@ -593,13 +594,13 @@ class Widget
foreach($attrs as $name => &$value) foreach($attrs as $name => &$value)
{ {
if(!is_string($value)) continue; if(!is_string($value)) continue;
$value = self::expand_name($value,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $value = self::expand_name($value, $expand['c'] ?? null, $expand['row'] ?? null, $expand['c_'] ?? null, $expand['row_'] ?? null, $expand['cont'] ?? []);
} }
if($attrs['attributes']) if (!empty($attrs['attributes']))
{ {
$attrs = array_merge($attrs, $attrs['attributes']); $attrs = array_merge($attrs, $attrs['attributes']);
} }
if(strpos($child->attrs['type'], '@') !== false || strpos($child->attrs['type'], '$') !== false) if (!empty($child->attrs['type']) && (strpos($child->attrs['type'], '@') !== false || strpos($child->attrs['type'], '$') !== false))
{ {
$type = self::expand_name($child->attrs['type'],$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $type = self::expand_name($child->attrs['type'],$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$id = self::expand_name($child->id,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $id = self::expand_name($child->id,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
@ -677,7 +678,7 @@ class Widget
*/ */
protected static function expand_name($name,$c,$row,$c_=0,$row_=0,$cont=array()) protected static function expand_name($name,$c,$row,$c_=0,$row_=0,$cont=array())
{ {
$is_index_in_content = $name[0] == '@'; $is_index_in_content = !empty($name) && $name[0] == '@';
if (($pos_var=strpos($name,'$')) !== false) if (($pos_var=strpos($name,'$')) !== false)
{ {
if (!$cont) if (!$cont)
@ -687,8 +688,8 @@ class Widget
if (!is_numeric($c)) $c = self::chrs2num($c); if (!is_numeric($c)) $c = self::chrs2num($c);
$col = self::num2chrs($c-1); // $c-1 to get: 0:'@', 1:'A', ... $col = self::num2chrs($c-1); // $c-1 to get: 0:'@', 1:'A', ...
if (is_numeric($c_)) $col_ = self::num2chrs($c_-1); if (is_numeric($c_)) $col_ = self::num2chrs($c_-1);
$row_cont = $cont[$row]; $row_cont = $cont[$row] ?? null;
$col_row_cont = $cont[$col.$row]; $col_row_cont = $cont[$col.$row] ?? null;
try { try {
eval('$name = "' . str_replace('"', '\\"', $name) . '";'); eval('$name = "' . str_replace('"', '\\"', $name) . '";');
@ -726,9 +727,9 @@ class Widget
*/ */
static function chrs2num($chrs) static function chrs2num($chrs)
{ {
if (empty($chrs)) return 0;
$min = ord('A'); $min = ord('A');
$max = ord('Z') - $min + 1; $max = ord('Z') - $min + 1;
$num = 1+ord($chrs[0])-$min; $num = 1+ord($chrs[0])-$min;
if (strlen($chrs) > 1) if (strlen($chrs) > 1)
{ {
@ -751,7 +752,7 @@ class Widget
if ($num >= $max) if ($num >= $max)
{ {
$chrs = chr(($num / $max) + $min - 1); $chrs = chr(($num / $max) + $min - 1);
} } else $chrs = '';
$chrs .= chr(($num % $max) + $min); $chrs .= chr(($num % $max) + $min);
return $chrs; return $chrs;
@ -829,7 +830,7 @@ class Widget
{ {
if ($expand && !empty($name)) if ($expand && !empty($name))
{ {
$name = self::expand_name($name, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $name = self::expand_name($name, $expand['c'] ?? null, $expand['row'] ?? null, $expand['c_'] ?? null, $expand['row_'] ?? null, $expand['cont'] ?? []);
} }
if (count($name_parts = explode('[', $name, 2)) > 1) if (count($name_parts = explode('[', $name, 2)) > 1)
{ {

View File

@ -50,12 +50,12 @@ class Box extends Etemplate\Widget
$old_expand = $params[1]; $old_expand = $params[1];
if ($this->id && $this->type != 'groupbox') $cname = self::form_name($cname, $this->id, $params[1]); if ($this->id && $this->type != 'groupbox') $cname = self::form_name($cname, $this->id, $params[1]);
if ($expand['cname'] !== $cname && trim($cname) != '') if (!empty($expand['cname']) && $expand['cname'] !== $cname && trim($cname))
{ {
$expand['cont'] =& self::get_array(self::$request->content, $cname); $expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname; $expand['cname'] = $cname;
} }
if ($respect_disabled && ($disabled = $this->attrs['disabled'] && self::check_disabled($this->attrs['disabled'], $expand))) if ($respect_disabled && isset($this->attrs['disabled']) && self::check_disabled($this->attrs['disabled'], $expand))
{ {
//error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running"); //error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running");
return; return;
@ -73,7 +73,7 @@ class Box extends Etemplate\Widget
// Expand children // Expand children
$columns_disabled = null; $columns_disabled = null;
if($this->id && $this->children[0] && strpos($this->children[0]->id, '$') !== false) if($this->id && isset($this->children[0]) && strpos($this->children[0]->id, '$') !== false)
{ {
// Need to set this so the first child can repeat // Need to set this so the first child can repeat
$expand['row'] = 0; $expand['row'] = 0;

View File

@ -120,7 +120,7 @@ class Date extends Transformer
{ {
$date = Api\DateTime::server2user($value); $date = Api\DateTime::server2user($value);
} }
elseif($this->attrs['data_format'] && $this->attrs['data_format'] !== 'object') elseif (!empty($this->attrs['data_format']) && $this->attrs['data_format'] !== 'object')
{ {
$date = Api\DateTime::createFromFormat($this->attrs['data_format'], $value, Api\DateTime::$user_timezone); $date = Api\DateTime::createFromFormat($this->attrs['data_format'], $value, Api\DateTime::$user_timezone);
} }

View File

@ -80,7 +80,7 @@ class Grid extends Box
$columns_disabled = array(); $columns_disabled = array();
} }
if ($respect_disabled && ($disabled = $this->attrs['disabled'] && self::check_disabled($this->attrs['disabled'], $expand))) if ($respect_disabled && isset($this->attrs['disabled']) && self::check_disabled($this->attrs['disabled'], $expand))
{ {
//error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running"); //error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running");
$params[0] = $old_cname; $params[0] = $old_cname;
@ -89,7 +89,7 @@ class Grid extends Box
} }
if ($this->id && $this->type !== 'row') $cname = self::form_name($cname, $this->id, $expand); if ($this->id && $this->type !== 'row') $cname = self::form_name($cname, $this->id, $expand);
if ($expand['cname'] !== $cname && $cname) if (!empty($expand['cname']) && $expand['cname'] !== $cname && $cname)
{ {
$expand['cont'] =& self::get_array(self::$request->content, $cname); $expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname; $expand['cname'] = $cname;

View File

@ -186,21 +186,21 @@ class Nextmatch extends Etemplate\Widget
if (true) $value =& self::get_array(self::$request->content, $form_name, true); if (true) $value =& self::get_array(self::$request->content, $form_name, true);
// Add favorite here so app doesn't save it in the session // Add favorite here so app doesn't save it in the session
if($_GET['favorite']) if (!empty($_GET['favorite']))
{ {
$send_value['favorite'] = $safe_name; $send_value['favorite'] = $safe_name;
} }
if (true) $value = $send_value; if (true) $value = $send_value;
$value['total'] = $total; $value['total'] = $total ?? null;
// Send categories // Send categories
if(!$value['no_cat'] && !$value['cat_is_select']) if(empty($value['no_cat']) && empty($value['cat_is_select']))
{ {
$cat_app = $value['cat_app'] ? $value['cat_app'] : $GLOBALS['egw_info']['flags']['current_app']; $cat_app = $value['cat_app'] ?? $GLOBALS['egw_info']['flags']['current_app'] ?? '';
$value['options-cat_id'] = self::$request->sel_options['cat_id'] ? self::$request->sel_options['cat_id'] : array(); $value['options-cat_id'] = self::$request->sel_options['cat_id'] ?? [];
// Add 'All', if not already there // Add 'All', if not already there
if(!$value['options-cat_id'][''] && !$value['options-cat_id'][0]) if(empty($value['options-cat_id']['']) && empty($value['options-cat_id'][0]))
{ {
$value['options-cat_id'][''] = lang('All categories'); $value['options-cat_id'][''] = lang('All categories');
} }
@ -220,7 +220,7 @@ class Nextmatch extends Etemplate\Widget
if(strpos($name, 'options-') !== false && $_value) if(strpos($name, 'options-') !== false && $_value)
{ {
$select = substr($name, 8); $select = substr($name, 8);
if(!self::$request->sel_options[$select]) if (empty(self::$request->sel_options[$select]))
{ {
self::$request->sel_options[$select] = array(); self::$request->sel_options[$select] = array();
} }
@ -231,21 +231,21 @@ class Nextmatch extends Etemplate\Widget
//unset($value[$name]); //unset($value[$name]);
} }
} }
if($value['rows']['sel_options']) if (!empty($value['rows']['sel_options']))
{ {
self::$request->sel_options = array_merge(self::$request->sel_options,$value['rows']['sel_options']); self::$request->sel_options = array_merge(self::$request->sel_options,$value['rows']['sel_options']);
unset($value['rows']['sel_options']); unset($value['rows']['sel_options']);
} }
// If column selection preference is forced, set a flag to turn off UI // If column selection preference is forced, set a flag to turn off UI
$pref_name = 'nextmatch-' . (isset($value['columnselection_pref']) ? $value['columnselection_pref'] : $this->attrs['template']); $pref_name = 'nextmatch-' . ($value['columnselection_pref'] ?? $this->attrs['template'] ?? '');
$value['no_columnselection'] = $value['no_columnselection'] || ( $value['no_columnselection'] = !empty($value['no_columnselection']) || (
$GLOBALS['egw']->preferences->forced[$app][$pref_name] && !empty($GLOBALS['egw']->preferences->forced[$app][$pref_name]) &&
// Need to check admin too, or it will be impossible to turn off // Need to check admin too, or it will be impossible to turn off
!$GLOBALS['egw_info']['user']['apps']['admin'] empty($GLOBALS['egw_info']['user']['apps']['admin'])
); );
// Use this flag to indicate to the admin that columns are forced (and that's why they can't change) // Use this flag to indicate to the admin that columns are forced (and that's why they can't change)
$value['columns_forced'] = (boolean)$GLOBALS['egw']->preferences->forced[$app][$pref_name]; $value['columns_forced'] = !empty($GLOBALS['egw']->preferences->forced[$app][$pref_name]);
// todo: no need to store rows in request, it's enought to send them to client // todo: no need to store rows in request, it's enought to send them to client
@ -256,7 +256,7 @@ class Nextmatch extends Etemplate\Widget
if (isset($value['actions']) && !isset($value['actions'][0])) if (isset($value['actions']) && !isset($value['actions'][0]))
{ {
$value['action_links'] = array(); $value['action_links'] = array();
$template_name = isset($value['template']) ? $value['template'] : ($this->attrs['template'] ?: $this->attrs['options']); $template_name = isset($value['template']) ? $value['template'] : ($this->attrs['template'] ?? $this->attrs['options'] ?? null);
if (!is_array($value['action_links'])) $value['action_links'] = array(); if (!is_array($value['action_links'])) $value['action_links'] = array();
$value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']); $value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']);
} }
@ -375,8 +375,8 @@ class Nextmatch extends Etemplate\Widget
$GLOBALS['egw']->session->commit_session(); $GLOBALS['egw']->session->commit_session();
$row_id = isset($value['row_id']) ? $value['row_id'] : 'id'; $row_id = $value['row_id'] ?? 'id';
$row_modified = $value['row_modified']; $row_modified = $value['row_modified'] ?? null;
foreach($rows as $n => $row) foreach($rows as $n => $row)
{ {
@ -384,12 +384,12 @@ class Nextmatch extends Etemplate\Widget
if (is_int($n) && $row) if (is_int($n) && $row)
{ {
if (!isset($row[$row_id])) unset($row_id); // unset default row_id of 'id', if not used if (!isset($row[$row_id])) unset($row_id); // unset default row_id of 'id', if not used
if (!isset($row[$row_modified])) unset($row_modified); if (empty($row[$row_modified])) unset($row_modified);
$id = $row_id ? $row[$row_id] : $n; $id = $row_id ? $row[$row_id] : $n;
$result['order'][] = $id; $result['order'][] = $id;
$modified = $row[$row_modified]; $modified = $row[$row_modified] ?? null;
if (isset($modified) && !(is_int($modified) || is_string($modified) && is_numeric($modified))) if (isset($modified) && !(is_int($modified) || is_string($modified) && is_numeric($modified)))
{ {
$modified = Api\DateTime::to(str_replace('Z', '', $modified), 'ts'); $modified = Api\DateTime::to(str_replace('Z', '', $modified), 'ts');
@ -497,7 +497,7 @@ class Nextmatch extends Etemplate\Widget
{ {
continue; continue;
} }
if($value_in[$key] == $value[$key]) continue; if (($value_in[$key]??null) == ($value[$key]??null)) continue;
// These keys we don't send row data back, as they cause a partial reload // These keys we don't send row data back, as they cause a partial reload
if (in_array($key, array('template'))) $no_rows = true; if (in_array($key, array('template'))) $no_rows = true;
@ -626,7 +626,7 @@ class Nextmatch extends Etemplate\Widget
), array(), true); // true = no permission check ), array(), true); // true = no permission check
// if we have a nextmatch widget, find the repeating row // if we have a nextmatch widget, find the repeating row
if ($widget && $widget->attrs['template']) if ($widget && !empty($widget->attrs['template']))
{ {
$row_template = $widget->getElementById($widget->attrs['template']); $row_template = $widget->getElementById($widget->attrs['template']);
if(!$row_template) if(!$row_template)
@ -642,12 +642,12 @@ class Nextmatch extends Etemplate\Widget
if($child->type == 'row') $repeating_row = $child; if($child->type == 'row') $repeating_row = $child;
} }
} }
// otherwise we might get stoped by max_excutiontime // otherwise, we might get stopped by max_excutiontime
if ($total > 200) @set_time_limit(0); if ($total > 200) @set_time_limit(0);
$is_parent = $value['is_parent']; $is_parent = $value['is_parent'] ?? null;
$is_parent_value = $value['is_parent_value']; $is_parent_value = $value['is_parent_value'] ?? null;
$parent_id = $value['parent_id']; $parent_id = $value['parent_id'] ?? null;
// remove empty rows required by old etemplate to compensate for header rows // remove empty rows required by old etemplate to compensate for header rows
$first = $total ? null : 0; $first = $total ? null : 0;
@ -658,14 +658,14 @@ class Nextmatch extends Etemplate\Widget
{ {
if (is_null($first)) $first = $n; if (is_null($first)) $first = $n;
if ($row[$is_parent]) // if app supports parent_id / hierarchy, set parent_id and is_parent if (!empty($row[$is_parent])) // if app supports parent_id / hierarchy, set parent_id and is_parent
{ {
$row['is_parent'] = isset($is_parent_value) ? $row['is_parent'] = isset($is_parent_value) ?
$row[$is_parent] == $is_parent_value : (boolean)$row[$is_parent]; $row[$is_parent] == $is_parent_value : (boolean)$row[$is_parent];
$row['parent_id'] = $row[$parent_id]; // seems NOT used on client! $row['parent_id'] = $row[$parent_id] ?? null; // seems NOT used on client!
} }
// run beforeSendToClient methods of widgets in row on row-data // run beforeSendToClient methods of widgets in row on row-data
if($repeating_row) if (!empty($repeating_row))
{ {
// Change anything by widget for each row ($row set to 1) // Change anything by widget for each row ($row set to 1)
$_row = array(1 => &$row); $_row = array(1 => &$row);
@ -894,7 +894,7 @@ class Nextmatch extends Etemplate\Widget
if ($default_attrs) $action += $default_attrs; if ($default_attrs) $action += $default_attrs;
// Add 'Select All' after first group // Add 'Select All' after first group
if ($first_level && $group !== false && $action['group'] != $group && !$egw_actions[$prefix.'select_all']) if ($first_level && $group !== false && $action['group'] != $group && empty($egw_actions[$prefix.'select_all']))
{ {
$egw_actions[$prefix.'select_all'] = array( $egw_actions[$prefix.'select_all'] = array(
@ -911,7 +911,7 @@ class Nextmatch extends Etemplate\Widget
); );
$action_links[] = $prefix.'select_all'; $action_links[] = $prefix.'select_all';
} }
$group = $action['group']; $group = $action['group'] ?? 0;
if (!$first_level && $n == $max_length && count($actions) > $max_length) if (!$first_level && $n == $max_length && count($actions) > $max_length)
{ {
@ -942,28 +942,28 @@ class Nextmatch extends Etemplate\Widget
// add all first level popup actions plus ones with enabled = 'javaScript:...' to action_links // add all first level popup actions plus ones with enabled = 'javaScript:...' to action_links
if ((!isset($action['type']) || in_array($action['type'], array('popup','drag','drop'))) && // popup is the default if ((!isset($action['type']) || in_array($action['type'], array('popup','drag','drop'))) && // popup is the default
($first_level || substr($action['enabled'],0,11) == 'javaScript:')) ($first_level || isset($action['enabled']) && substr($action['enabled'],0,11) === 'javaScript:'))
{ {
$action_links[] = $prefix.$id; $action_links[] = $prefix.$id;
} }
// add sub-menues // add sub-menus
if ($action['children']) if (!empty($action['children']))
{ {
static $inherit_attrs = array('url','popup','nm_action','onExecute','type','egw_open','allowOnMultiple','confirm','confirm_multiple'); static $inherit_attrs = array('url','popup','nm_action','onExecute','type','egw_open','allowOnMultiple','confirm','confirm_multiple');
$inherit_keys = array_flip($inherit_attrs); $inherit_keys = array_flip($inherit_attrs);
$action['children'] = self::egw_actions($action['children'], $template_name, $action['prefix'], $action_links, $max_length, $action['children'] = self::egw_actions($action['children'], $template_name, $action['prefix'] ?? '', $action_links, $max_length,
array_intersect_key($action, $inherit_keys)); array_intersect_key($action, $inherit_keys));
unset($action['prefix']); unset($action['prefix']);
// Allow default actions to keep their onExecute // Allow default actions to keep their onExecute
if($action['default']) unset($inherit_keys['onExecute']); if (!empty($action['default'])) unset($inherit_keys['onExecute']);
$action = array_diff_key($action, $inherit_keys); $action = array_diff_key($action, $inherit_keys);
} }
// link or popup action // link or popup action
if ($action['url']) if (!empty($action['url']))
{ {
$action['url'] = Api\Framework::link('/index.php',str_replace('$action',$id,$action['url'])); $action['url'] = Api\Framework::link('/index.php',str_replace('$action',$id,$action['url']));
if ($action['popup']) if ($action['popup'])
@ -984,7 +984,7 @@ class Nextmatch extends Etemplate\Widget
} }
} }
} }
if ($action['egw_open']) if (!empty($action['egw_open']))
{ {
$action['data']['nm_action'] = 'egw_open'; $action['data']['nm_action'] = 'egw_open';
} }
@ -998,9 +998,9 @@ class Nextmatch extends Etemplate\Widget
foreach($egw_actions as $id => &$_action) foreach($egw_actions as $id => &$_action)
{ {
if ($id == $prefix . 'select_all') continue; if ($id == $prefix . 'select_all') continue;
if($_action['group'] >= $egw_actions[$prefix.'select_all']['group'] ) if (($_action['group'] ?? 0) >= (($egw_actions[$prefix.'select_all'] ?? [])['group'] ?? 0))
{ {
$egw_actions[$id]['group']+=1; $egw_actions[$id]['group'] = ($egw_actions[$id]['group'] ?? 0) + 1;
} }
} }
//echo "egw_actions="; _debug_array($egw_actions); //echo "egw_actions="; _debug_array($egw_actions);
@ -1044,7 +1044,7 @@ class Nextmatch extends Etemplate\Widget
'no_lang' => true, 'no_lang' => true,
); );
// add category icon // add category icon
if (is_array($cat['data']) && $cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon']))) if (is_array($cat['data']) && !empty($cat['data']['icon']) && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon'])))
{ {
$cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon']; $cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon'];
} }
@ -1083,7 +1083,7 @@ class Nextmatch extends Etemplate\Widget
'prefix' => $prefix, 'prefix' => $prefix,
); );
// add category icon // add category icon
if ($cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon']))) if (!empty($cat['data']['icon']) && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon'])))
{ {
$cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon']; $cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon'];
} }
@ -1222,7 +1222,7 @@ class Nextmatch extends Etemplate\Widget
// Run on all the sub-templates // Run on all the sub-templates
foreach(array('template', 'header_left', 'header_right', 'header_row') as $sub_template) foreach(array('template', 'header_left', 'header_right', 'header_row') as $sub_template)
{ {
if($this->attrs[$sub_template]) if (!empty($this->attrs[$sub_template]))
{ {
$row_template = Template::instance($this->attrs[$sub_template]); $row_template = Template::instance($this->attrs[$sub_template]);
$row_template->run($method_name, $params, $respect_disabled); $row_template->run($method_name, $params, $respect_disabled);
@ -1230,14 +1230,6 @@ class Nextmatch extends Etemplate\Widget
} }
} }
$params[0] = $old_param0; $params[0] = $old_param0;
// Prevent troublesome keys from breaking the nextmatch
// TODO: Figure out where these come from
foreach(array('$row','${row}', '$', '0','1','2') as $key)
{
if(is_array(self::$request->content[$cname])) unset(self::$request->content[$cname][$key]);
if(is_array(self::$request->preserve[$cname])) unset(self::$request->preserve[$cname][$key]);
}
} }
/** /**

View File

@ -64,7 +64,9 @@ class Placeholder extends Etemplate\Widget
if(is_null($apps)) if(is_null($apps))
{ {
$apps = ['addressbook', 'user']; $apps = ['addressbook', 'user', 'general'] +
// We use linking for preview, so limit to apps that support links
array_keys(Api\Link::app_list('query'));
} }
foreach($apps as $appname) foreach($apps as $appname)
@ -75,31 +77,53 @@ class Placeholder extends Etemplate\Widget
case 'user': case 'user':
$list = $merge->get_user_placeholder_list(); $list = $merge->get_user_placeholder_list();
break; break;
case 'general':
$list = $merge->get_common_placeholder_list();
break;
default: default:
$list = $merge->get_placeholder_list(); if(get_class($merge) === 'EGroupware\Api\Contacts\Merge' && $appname !== 'addressbook' || $placeholders[$appname])
{
// Looks like app doesn't support merging
continue 2;
}
$list = method_exists($merge, 'get_placeholder_list') ? $merge->get_placeholder_list() : [];
break; break;
} }
if(!is_null($group)) if(!is_null($group) && is_array($list))
{ {
$list = array_intersect_key($list, $group); $list = array_intersect_key($list, $group);
} }
// Remove if empty
foreach($list as $p_group => $p_list)
{
if(count($p_list) == 0)
{
unset($list[$p_group]);
}
}
if($list)
{
$placeholders[$appname] = $list; $placeholders[$appname] = $list;
} }
}
$response = Api\Json\Response::get(); $response = Api\Json\Response::get();
$response->data($placeholders); $response->data($placeholders);
} }
public function ajax_fill_placeholders($app, $content, $entry) public function ajax_fill_placeholders($content, $entry)
{ {
$merge = Api\Storage\Merge::get_app_class($app); $merge = Api\Storage\Merge::get_app_class($entry['app']);
$err = ""; $err = "";
switch($app) switch($entry['app'])
{ {
case 'addressbook': case 'user':
$entry = ['id' => $GLOBALS['egw_info']['user']['person_id']];
// fall through
default: default:
$merged = $merge->merge_string($content, [$entry], $err, 'text/plain'); $merged = $merge->merge_string($content, [$entry['id']], $err, 'text/plain');
} }
$response = Api\Json\Response::get(); $response = Api\Json\Response::get();
$response->data($merged); $response->data($merged);

View File

@ -496,17 +496,10 @@ abstract class Framework extends Framework\Extra
{ {
$lang_code = $GLOBALS['egw_info']['user']['preferences']['common']['lang']; $lang_code = $GLOBALS['egw_info']['user']['preferences']['common']['lang'];
} }
// IE specific fixes
if (Header\UserAgent::type() == 'msie')
{
// tell IE to use it's own mode, not old compatibility modes (set eg. via group policy for all intranet sites)
// has to be before any other header tags, but meta and title!!!
$pngfix = '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'."\n";
}
$app = $GLOBALS['egw_info']['flags']['currentapp']; $app = $GLOBALS['egw_info']['flags']['currentapp'];
$app_title = isset($GLOBALS['egw_info']['apps'][$app]) ? $GLOBALS['egw_info']['apps'][$app]['title'] : lang($app); $app_title = isset($GLOBALS['egw_info']['apps'][$app]) ? $GLOBALS['egw_info']['apps'][$app]['title'] : lang($app);
$app_header = $GLOBALS['egw_info']['flags']['app_header'] ? $GLOBALS['egw_info']['flags']['app_header'] : $app_title; $app_header = $GLOBALS['egw_info']['flags']['app_header'] ?? $app_title;
$site_title = strip_tags($GLOBALS['egw_info']['server']['site_title'].' ['.($app_header ? $app_header : $app_title).']'); $site_title = strip_tags($GLOBALS['egw_info']['server']['site_title'].' ['.($app_header ? $app_header : $app_title).']');
// send appheader to clientside // send appheader to clientside
@ -516,7 +509,7 @@ abstract class Framework extends Framework\Extra
$var['favicon_file'] = self::get_login_logo_or_bg_url('favicon_file', 'favicon.ico'); $var['favicon_file'] = self::get_login_logo_or_bg_url('favicon_file', 'favicon.ico');
if ($GLOBALS['egw_info']['flags']['include_wz_tooltip'] && if (!empty($GLOBALS['egw_info']['flags']['include_wz_tooltip']) &&
file_exists(EGW_SERVER_ROOT.($wz_tooltip = '/phpgwapi/js/wz_tooltip/wz_tooltip.js'))) file_exists(EGW_SERVER_ROOT.($wz_tooltip = '/phpgwapi/js/wz_tooltip/wz_tooltip.js')))
{ {
$include_wz_tooltip = '<script src="'.$GLOBALS['egw_info']['server']['webserver_url']. $include_wz_tooltip = '<script src="'.$GLOBALS['egw_info']['server']['webserver_url'].
@ -525,7 +518,6 @@ abstract class Framework extends Framework\Extra
return $this->_get_css()+array( return $this->_get_css()+array(
'img_icon' => $var['favicon_file'], 'img_icon' => $var['favicon_file'],
'img_shortcut' => $var['favicon_file'], 'img_shortcut' => $var['favicon_file'],
'pngfix' => $pngfix,
'lang_code' => $lang_code, 'lang_code' => $lang_code,
'charset' => Translation::charset(), 'charset' => Translation::charset(),
'website_title' => $site_title, 'website_title' => $site_title,
@ -533,7 +525,7 @@ abstract class Framework extends Framework\Extra
'java_script' => self::_get_js($extra), 'java_script' => self::_get_js($extra),
'meta_robots' => $robots, 'meta_robots' => $robots,
'dir_code' => lang('language_direction_rtl') != 'rtl' ? '' : ' dir="rtl"', 'dir_code' => lang('language_direction_rtl') != 'rtl' ? '' : ' dir="rtl"',
'include_wz_tooltip'=> $include_wz_tooltip, 'include_wz_tooltip'=> $include_wz_tooltip ?? '',
'webserver_url' => $GLOBALS['egw_info']['server']['webserver_url'], 'webserver_url' => $GLOBALS['egw_info']['server']['webserver_url'],
'darkmode' => !empty(Cache::getSession('api','darkmode')) ?? $GLOBALS['egw_info']['user']['preferences']['common']['darkmode'] 'darkmode' => !empty(Cache::getSession('api','darkmode')) ?? $GLOBALS['egw_info']['user']['preferences']['common']['darkmode']
); );
@ -602,12 +594,12 @@ abstract class Framework extends Framework\Extra
*/ */
static function get_login_logo_or_bg_url ($type, $find_type) static function get_login_logo_or_bg_url ($type, $find_type)
{ {
$url = is_array($GLOBALS['egw_info']['server'][$type]) ? $url = !empty($GLOBALS['egw_info']['server'][$type]) && is_array($GLOBALS['egw_info']['server'][$type]) ?
$GLOBALS['egw_info']['server'][$type][0] : $GLOBALS['egw_info']['server'][$type][0] :
$GLOBALS['egw_info']['server'][$type]; $GLOBALS['egw_info']['server'][$type] ?? null;
if (substr($url, 0, 4) == 'http' || if (substr($url, 0, 4) === 'http' ||
$url[0] == '/') !empty($url) && $url[0] === '/')
{ {
return $url; return $url;
} }
@ -801,7 +793,7 @@ abstract class Framework extends Framework\Extra
$index = '/index.php?menuaction='.$data['index']; $index = '/index.php?menuaction='.$data['index'];
} }
} }
return self::link($index,$GLOBALS['egw_info']['flags']['params'][$app]); return self::link($index, $GLOBALS['egw_info']['flags']['params'][$app] ?? '');
} }
/** /**
@ -982,7 +974,7 @@ abstract class Framework extends Framework\Extra
{ {
if (file_exists(EGW_SERVER_ROOT.$theme_css)) break; if (file_exists(EGW_SERVER_ROOT.$theme_css)) break;
} }
$debug_minify = $GLOBALS['egw_info']['server']['debug_minify'] === 'True'; $debug_minify = !empty($GLOBALS['egw_info']['server']['debug_minify']) && $GLOBALS['egw_info']['server']['debug_minify'] === 'True';
if (!$debug_minify && file_exists(EGW_SERVER_ROOT.($theme_min_css = str_replace('.css', '.min.css', $theme_css)))) if (!$debug_minify && file_exists(EGW_SERVER_ROOT.($theme_min_css = str_replace('.css', '.min.css', $theme_css))))
{ {
//error_log(__METHOD__."() Framework\CssIncludes::get()=".array2string(Framework\CssIncludes::get())); //error_log(__METHOD__."() Framework\CssIncludes::get()=".array2string(Framework\CssIncludes::get()));
@ -1110,8 +1102,7 @@ abstract class Framework extends Framework\Extra
if(@isset($_GET['menuaction'])) if(@isset($_GET['menuaction']))
{ {
list(, $class) = explode('.',$_GET['menuaction']); list(, $class) = explode('.',$_GET['menuaction']);
if(is_array($GLOBALS[$class]->public_functions) && if (!empty($GLOBALS[$class]->public_functions['java_script']))
$GLOBALS[$class]->public_functions['java_script'])
{ {
$java_script .= $GLOBALS[$class]->java_script(); $java_script .= $GLOBALS[$class]->java_script();
} }
@ -1578,8 +1569,8 @@ abstract class Framework extends Framework\Extra
foreach(Framework\CssIncludes::get() as $path) foreach(Framework\CssIncludes::get() as $path)
{ {
unset($query); unset($query);
list($path,$query) = explode('?',$path,2); list($path,$query) = explode('?', $path,2)+[null,null];
$path .= '?'. ($query ? $query : filemtime(EGW_SERVER_ROOT.$path)); $path .= '?'. ($query ?? filemtime(EGW_SERVER_ROOT.$path));
$response->includeCSS($GLOBALS['egw_info']['server']['webserver_url'].$path); $response->includeCSS($GLOBALS['egw_info']['server']['webserver_url'].$path);
} }

View File

@ -125,7 +125,7 @@ class Bundle
} }
elseif (in_array($file, ['/api/js/jsapi.min.js', '/vendor/bower-asset/jquery/dist/jquery.min.js','/vendor/bower-asset/jquery/dist/jquery.js'])) elseif (in_array($file, ['/api/js/jsapi.min.js', '/vendor/bower-asset/jquery/dist/jquery.min.js','/vendor/bower-asset/jquery/dist/jquery.js']))
{ {
error_log(function_backtrace()); // no NOT include //error_log(function_backtrace()); // no NOT include
} }
else else
{ {

View File

@ -63,8 +63,8 @@ class Hooks
$location = is_array($args) ? (isset($args['hook_location']) ? $args['hook_location'] : $args['location']) : $args; $location = is_array($args) ? (isset($args['hook_location']) ? $args['hook_location'] : $args['location']) : $args;
if (!isset(self::$locations)) self::read(); if (!isset(self::$locations)) self::read();
if (empty(self::$locations[$location])) return []; // not a single app implements that hook
$hooks = self::$locations[$location]; $hooks = self::$locations[$location];
if (!isset($hooks) || empty($hooks)) return array(); // not a single app implements that hook
$apps = array_keys($hooks); $apps = array_keys($hooks);
if (!$no_permission_check) if (!$no_permission_check)
@ -115,7 +115,7 @@ class Hooks
} }
$ret = array(); $ret = array();
foreach((array)self::$locations[$location][$appname] as $hook) foreach(self::$locations[$location][$appname] ?? [] as $hook)
{ {
try { try {
// old style file hook // old style file hook
@ -130,7 +130,7 @@ class Hooks
return true; return true;
} }
list($class, $method) = explode('::', $hook); list($class, $method) = explode('::', $hook)+[null,null];
// static method of an autoloadable class // static method of an autoloadable class
if (isset($method) && class_exists($class)) if (isset($method) && class_exists($class))

View File

@ -391,7 +391,7 @@ function hl_email_tag_transform($element, $attribute_array=0)
// $GLOBALS['egw_info']['user']['preferences']['mail']['allowExternalIMGs'] ? '' : 'match' => '/^cid:.*/'), // $GLOBALS['egw_info']['user']['preferences']['mail']['allowExternalIMGs'] ? '' : 'match' => '/^cid:.*/'),
if (isset($attribute_array['src'])) if (isset($attribute_array['src']))
{ {
if (!(strlen($attribute_array['src'])>4 && strlen($attribute_array['src']<400))) if (!(strlen($attribute_array['src'])>4 && strlen($attribute_array['src'])<400))
{ {
$attribute_array['alt']= $attribute_array['alt'].' [blocked (reason: url length):'.$attribute_array['src'].']'; $attribute_array['alt']= $attribute_array['alt'].' [blocked (reason: url length):'.$attribute_array['src'].']';
if (!isset($attribute_array['title'])) $attribute_array['title']=$attribute_array['alt']; if (!isset($attribute_array['title'])) $attribute_array['title']=$attribute_array['alt'];

View File

@ -762,7 +762,7 @@ class Link extends Link\Storage
if ($must_support && !isset($reg[$must_support])) continue; if ($must_support && !isset($reg[$must_support])) continue;
list($app) = explode('-', $type); list($app) = explode('-', $type);
if ($GLOBALS['egw_info']['user']['apps'][$app]) if (!empty($GLOBALS['egw_info']['user']['apps'][$app]))
{ {
$apps[$type] = lang(self::get_registry($type, 'name')); $apps[$type] = lang(self::get_registry($type, 'name'));
} }
@ -1132,7 +1132,7 @@ class Link extends Link\Storage
*/ */
static function get_registry($app, $name, $url_id=false) static function get_registry($app, $name, $url_id=false)
{ {
$reg = self::$app_register[$app]; $reg = self::$app_register[$app] ?? null;
if (!isset($reg)) return false; if (!isset($reg)) return false;
@ -1181,7 +1181,7 @@ class Link extends Link\Storage
return $str; return $str;
} }
return isset($reg) ? $reg[$name] : false; return $reg[$name] ?? false;
} }
/** /**

View File

@ -2950,7 +2950,7 @@ class Mail
// Trigger examination of namespace to retrieve // Trigger examination of namespace to retrieve
// folders located in other and shared; needed only for some servers // folders located in other and shared; needed only for some servers
if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail'); if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail');
if (self::$mailConfig['examineNamespace']) if (!empty(self::$mailConfig['examineNamespace']))
{ {
$prefixes=array(); $prefixes=array();
if (is_array($nameSpace)) if (is_array($nameSpace))
@ -3014,7 +3014,7 @@ class Mail
$subFolders = $this->icServer->getMailboxes($node['MAILBOX'].$node['delimiter'], $_search, true); $subFolders = $this->icServer->getMailboxes($node['MAILBOX'].$node['delimiter'], $_search, true);
} }
if (is_array($mainFolder['INBOX'])) if (isset($mainFolder['INBOX']) && is_array($mainFolder['INBOX']))
{ {
// Array container of auto folders // Array container of auto folders
$aFolders = array(); $aFolders = array();
@ -3180,7 +3180,7 @@ class Mail
{ {
foreach(array('others','shared') as $type) foreach(array('others','shared') as $type)
{ {
if ($nameSpace[$type]['prefix_present']&&$nameSpace[$type]['prefix']) if (!empty($nameSpace[$type]['prefix_present']) && !empty($nameSpace[$type]['prefix']))
{ {
if (substr($k,0,strlen($nameSpace[$type]['prefix']))==$nameSpace[$type]['prefix']|| if (substr($k,0,strlen($nameSpace[$type]['prefix']))==$nameSpace[$type]['prefix']||
substr($k,0,strlen($nameSpace[$type]['prefix'])-strlen($nameSpace[$type]['delimiter']))==substr($nameSpace[$type]['prefix'],0,strlen($nameSpace[$type]['delimiter'])*-1)) { substr($k,0,strlen($nameSpace[$type]['prefix'])-strlen($nameSpace[$type]['delimiter']))==substr($nameSpace[$type]['prefix'],0,strlen($nameSpace[$type]['delimiter'])*-1)) {
@ -3211,7 +3211,7 @@ class Mail
} }
//error_log(__METHOD__.__LINE__.array2string($autoFolderObjects)); //error_log(__METHOD__.__LINE__.array2string($autoFolderObjects));
if (!$isGoogleMail) { if (!$isGoogleMail) {
$folders = array_merge($inboxFolderObject,$autoFolderObjects,(array)$inboxSubFolderObjects,(array)$folders,(array)$typeFolderObject['others'],(array)$typeFolderObject['shared']); $folders = array_merge($inboxFolderObject,$autoFolderObjects,(array)$inboxSubFolderObjects,(array)$folders,(array)$typeFolderObject['others'] ?? [],(array)$typeFolderObject['shared'] ?? []);
} else { } else {
// avoid calling sortByAutoFolder as it is not regarding subfolders // avoid calling sortByAutoFolder as it is not regarding subfolders
$gAutoFolderObjectsTmp = $googleAutoFolderObjects; $gAutoFolderObjectsTmp = $googleAutoFolderObjects;
@ -3910,7 +3910,7 @@ class Mail
{ {
//error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.array2string($_folder).', '.$_forceDeleteMethod); //error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.array2string($_folder).', '.$_forceDeleteMethod);
$oldMailbox = ''; $oldMailbox = '';
if (is_null($_folder) || empty($_folder)) $_folder = $this->sessionData['mailbox']; if (empty($_folder) && !empty($this->sessionData['mailbox'])) $_folder = $this->sessionData['mailbox'];
if (empty($_messageUID)) if (empty($_messageUID))
{ {
if (self::$debug) error_log(__METHOD__." no messages Message(s): ".implode(',',$_messageUID)); if (self::$debug) error_log(__METHOD__." no messages Message(s): ".implode(',',$_messageUID));
@ -4210,7 +4210,10 @@ class Mail
} }
if ($folder instanceof Horde_Imap_Client_Mailbox) $_folder = $folder->utf8; if ($folder instanceof Horde_Imap_Client_Mailbox) $_folder = $folder->utf8;
//error_log(__METHOD__.__LINE__.'#'.$this->icServer->ImapServerId.'#'.array2string($_folder).'#'); //error_log(__METHOD__.__LINE__.'#'.$this->icServer->ImapServerId.'#'.array2string($_folder).'#');
self::$folderStatusCache[$this->icServer->ImapServerId][(!empty($_folder)?$_folder: $this->sessionData['mailbox'])]['uidValidity'] = 0; if (isset(self::$folderStatusCache[$this->icServer->ImapServerId][($_folder??$this->sessionData['mailbox'])]['uidValidity']))
{
self::$folderStatusCache[$this->icServer->ImapServerId][($_folder??$this->sessionData['mailbox'])]['uidValidity'] = 0;
}
//error_log(__METHOD__.' ('.__LINE__.') '.'->' .$_flag." ".array2string($_messageUID).",".($_folder?$_folder:$this->sessionData['mailbox'])); //error_log(__METHOD__.' ('.__LINE__.') '.'->' .$_flag." ".array2string($_messageUID).",".($_folder?$_folder:$this->sessionData['mailbox']));
return true; // as we do not catch/examine setFlags returnValue return true; // as we do not catch/examine setFlags returnValue

View File

@ -313,7 +313,7 @@ class Account implements \ArrayAccess
try { try {
if ($this->acc_imap_type != __NAMESPACE__.'\\Imap' && if ($this->acc_imap_type != __NAMESPACE__.'\\Imap' &&
// do NOT query IMAP server, if we are in forward-only delivery-mode, imap will NOT answer, as switched off for that account! // do NOT query IMAP server, if we are in forward-only delivery-mode, imap will NOT answer, as switched off for that account!
$this->params['deliveryMode'] != Smtp::FORWARD_ONLY && $need_quota && ($this->params['deliveryMode'] ?? null) != Smtp::FORWARD_ONLY && $need_quota &&
$this->imapServer($this->user) && is_a($this->imapServer, __NAMESPACE__.'\\Imap') && $this->imapServer($this->user) && is_a($this->imapServer, __NAMESPACE__.'\\Imap') &&
($data = $this->imapServer->getUserData($GLOBALS['egw']->accounts->id2name($this->user)))) ($data = $this->imapServer->getUserData($GLOBALS['egw']->accounts->id2name($this->user))))
{ {
@ -335,7 +335,7 @@ class Account implements \ArrayAccess
} }
$this->params += array_fill_keys(self::$user_data, null); // make sure all keys exist now $this->params += array_fill_keys(self::$user_data, null); // make sure all keys exist now
return (array)$data + (array)$smtp_data; return ($data ?? []) + ($smtp_data ?? []);
} }
/** /**
@ -454,10 +454,10 @@ class Account implements \ArrayAccess
$this->smtpServer->host = 'tls://'.$this->smtpServer->host; $this->smtpServer->host = 'tls://'.$this->smtpServer->host;
} }
$this->smtpServer->smtpAuth = !empty($this->params['acc_smtp_username']); $this->smtpServer->smtpAuth = !empty($this->params['acc_smtp_username']);
$this->smtpServer->username = $this->params['acc_smtp_username']; $this->smtpServer->username = $this->params['acc_smtp_username'] ?? null;
$this->smtpServer->password = $this->params['acc_smtp_password']; $this->smtpServer->password = $this->params['acc_smtp_password'] ?? null;
$this->smtpServer->defaultDomain = $this->params['acc_domain']; $this->smtpServer->defaultDomain = $this->params['acc_domain'];
$this->smtpServer->loginType = $this->params['acc_imap_login_type']; $this->smtpServer->loginType = $this->params['acc_imap_login_type'] ?? null;
} }
return $this->smtpServer; return $this->smtpServer;
} }
@ -676,7 +676,7 @@ class Account implements \ArrayAccess
$to_replace = array(); $to_replace = array();
foreach($fields as $name) foreach($fields as $name)
{ {
if (strpos($identity[$name], '{{') !== false || strpos($identity[$name], '$$') !== false) if (!empty($identity[$name]) && (strpos($identity[$name], '{{') !== false || strpos($identity[$name], '$$') !== false))
{ {
$to_replace[$name] = $identity[$name]; $to_replace[$name] = $identity[$name];
} }
@ -781,7 +781,7 @@ class Account implements \ArrayAccess
'account_id' => self::is_multiple($identity) ? 0 : 'account_id' => self::is_multiple($identity) ? 0 :
(is_array($identity['account_id']) ? $identity['account_id'][0] : $identity['account_id']), (is_array($identity['account_id']) ? $identity['account_id'][0] : $identity['account_id']),
); );
if ($identity['ident_id'] > 0) if ($identity['ident_id'] !== 'new' && (int)$identity['ident_id'] > 0)
{ {
self::$db->update(self::IDENTITIES_TABLE, $data, array( self::$db->update(self::IDENTITIES_TABLE, $data, array(
'ident_id' => $identity['ident_id'], 'ident_id' => $identity['ident_id'],
@ -837,7 +837,7 @@ class Account implements \ArrayAccess
// let getUserData "know" if we are interested in quota (requiring IMAP login) or not // let getUserData "know" if we are interested in quota (requiring IMAP login) or not
$this->getUserData(substr($name, 0, 5) === 'quota'); $this->getUserData(substr($name, 0, 5) === 'quota');
} }
return $this->params[$name]; return $this->params[$name] ?? null;
} }
/** /**
@ -1196,7 +1196,7 @@ class Account implements \ArrayAccess
// store identity // store identity
$new_ident_id = self::save_identity($data); $new_ident_id = self::save_identity($data);
if (!($data['ident_id'] > 0)) if ($data['ident_id'] === 'new' || empty($data['ident_id']))
{ {
$data['ident_id'] = $new_ident_id; $data['ident_id'] = $new_ident_id;
self::$db->update(self::TABLE, array( self::$db->update(self::TABLE, array(
@ -1578,7 +1578,7 @@ class Account implements \ArrayAccess
'ident_realname' => $account['ident_realname'], 'ident_realname' => $account['ident_realname'],
'ident_org' => $account['ident_org'], 'ident_org' => $account['ident_org'],
'ident_email' => $account['ident_email'], 'ident_email' => $account['ident_email'],
'acc_name' => $account['acc_name'], 'acc_name' => $account['acc_name'] ?? null,
'acc_imap_username' => $account['acc_imap_username'], 'acc_imap_username' => $account['acc_imap_username'],
'acc_imap_logintype' => $account['acc_imap_logintype'], 'acc_imap_logintype' => $account['acc_imap_logintype'],
'acc_domain' => $account['acc_domain'], 'acc_domain' => $account['acc_domain'],
@ -1605,7 +1605,7 @@ class Account implements \ArrayAccess
} }
} }
// fill an empty ident_realname or ident_email of current user with data from user account // fill an empty ident_realname or ident_email of current user with data from user account
if ($replace_placeholders && (!isset($account_id) || $account_id == $GLOBALS['egw_info']['user']['acount_id'])) if ($replace_placeholders && (!isset($account_id) || $account_id == $GLOBALS['egw_info']['user']['account_id']))
{ {
if (empty($account['ident_realname'])) $account['ident_realname'] = $GLOBALS['egw_info']['user']['account_fullname']; if (empty($account['ident_realname'])) $account['ident_realname'] = $GLOBALS['egw_info']['user']['account_fullname'];
if (empty($account['ident_email'])) $account['ident_email'] = $GLOBALS['egw_info']['user']['account_email']; if (empty($account['ident_email'])) $account['ident_email'] = $GLOBALS['egw_info']['user']['account_email'];

View File

@ -87,7 +87,7 @@ class Notifications
$account_specific = $account_id; $account_specific = $account_id;
} }
} }
$folders = (array)self::$cache[$acc_id][$account_specific]; $folders = self::$cache[$acc_id][$account_specific] ?? [];
if (!$return_empty_marker && $folders == array(null)) $folders = array(); if (!$return_empty_marker && $folders == array(null)) $folders = array();
$result = array( $result = array(
'notify_folders' => $folders, 'notify_folders' => $folders,

View File

@ -233,10 +233,23 @@ class Smtp
* default use $this->loginType * default use $this->loginType
* @return string * @return string
*/ */
/*static*/ public function mailbox_addr($account,$domain=null,$mail_login_type=null) public function mailbox_addr($account, $domain=null, $mail_login_type=null)
{
return self::mailbox_address($account, $domain ?? $this->defaultDomain, $mail_login_type ?? $this->loginType);
}
/**
* Build mailbox address for given account and mail_addr_type
*
* If $account is an array (with values for keys account_(id|lid|email), it does NOT call accounts class
*
* @param int|array $account account_id or whole account array with values for keys
* @param string $domain domain
* @param string $mail_login_type=null standard(uid), vmailmgr(uid@domain), email or uidNumber
* @return string
*/
static public function mailbox_address($account, string $domain, string $mail_login_type=null)
{ {
if (is_null($domain)) $domain = $this->defaultDomain;
if (is_null($mail_login_type)) $mail_login_type = $this->loginType;
switch($mail_login_type) switch($mail_login_type)
{ {

View File

@ -340,7 +340,7 @@ class Sql extends Mail\Smtp
} }
} }
// let interesed parties know account was update // let interested parties know account was update
Api\Hooks::process(array( Api\Hooks::process(array(
'location' => 'mailaccount_userdata_updated', 'location' => 'mailaccount_userdata_updated',
'account_id' => $_uidnumber, 'account_id' => $_uidnumber,

View File

@ -196,7 +196,7 @@ class Preferences
foreach((array)$ids as $id) foreach((array)$ids as $id)
{ {
// if prefs are not returned, null or not an array, read them from db // if prefs are not returned, null or not an array, read them from db
if (!isset($prefs[$id]) && !is_array($prefs[$id])) $db_read[] = $id; if (!isset($prefs[$id]) || !is_array($prefs[$id])) $db_read[] = $id;
} }
if ($db_read) if ($db_read)
{ {
@ -237,7 +237,7 @@ class Preferences
$replace = $with = array(); $replace = $with = array();
foreach($vals as $key => $val) foreach($vals as $key => $val)
{ {
if ($this->debug) error_log(__METHOD__." replacing \$\$$key\$\$ with $val "); if (!empty($this->debug)) error_log(__METHOD__." replacing \$\$$key\$\$ with $val ");
$replace[] = '$$'.$key.'$$'; $replace[] = '$$'.$key.'$$';
$with[] = $val; $with[] = $val;
} }
@ -275,7 +275,7 @@ class Preferences
*/ */
function standard_substitutes() function standard_substitutes()
{ {
if ($this->debug) error_log(__METHOD__." is called "); if (!empty($this->debug)) error_log(__METHOD__." is called ");
if (!is_array(@$GLOBALS['egw_info']['user']['preferences'])) if (!is_array(@$GLOBALS['egw_info']['user']['preferences']))
{ {
$GLOBALS['egw_info']['user']['preferences'] = $this->data; // else no lang() $GLOBALS['egw_info']['user']['preferences'] = $this->data; // else no lang()
@ -301,7 +301,7 @@ class Preferences
'email' => lang('email-address of the user, eg. "%1"',$this->values['email']), 'email' => lang('email-address of the user, eg. "%1"',$this->values['email']),
'date' => lang('todays date, eg. "%1"',$this->values['date']), 'date' => lang('todays date, eg. "%1"',$this->values['date']),
); );
if ($this->debug) error_log(__METHOD__.print_r($this->vars,true)); if (!empty($this->debug)) error_log(__METHOD__.print_r($this->vars,true));
// do the substituetion in the effective prefs (data) // do the substituetion in the effective prefs (data)
// //
foreach($this->data as $app => $data) foreach($this->data as $app => $data)
@ -421,7 +421,7 @@ class Preferences
default: default:
foreach($values as $app => $vals) foreach($values as $app => $vals)
{ {
$this->group[$app] = (array)$vals + (array)$this->group[$app]; $this->group[$app] = (array)$vals + ($this->group[$app] ?? []);
} }
break; break;
} }
@ -474,7 +474,7 @@ class Preferences
} }
// setup the standard substitutes and substitutes the data in $this->data // setup the standard substitutes and substitutes the data in $this->data
// //
if ($GLOBALS['egw_info']['flags']['load_translations'] !== false) if (!empty($GLOBALS['egw_info']['flags']['load_translations']))
{ {
$this->standard_substitutes(); $this->standard_substitutes();
} }

View File

@ -1363,7 +1363,7 @@ class Session
return false; return false;
} }
if ($GLOBALS['egw_info']['server']['sessions_checkip']) if (!empty($GLOBALS['egw_info']['server']['sessions_checkip']))
{ {
if (strtoupper(substr(PHP_OS,0,3)) != 'WIN' && (!$GLOBALS['egw_info']['user']['session_ip'] || if (strtoupper(substr(PHP_OS,0,3)) != 'WIN' && (!$GLOBALS['egw_info']['user']['session_ip'] ||
$GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip())) $GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip()))
@ -1538,19 +1538,19 @@ class Session
} }
// check if the url already contains a query and ensure that vars is an array and all strings are in extravars // check if the url already contains a query and ensure that vars is an array and all strings are in extravars
list($ret_url,$othervars) = explode('?', $url, 2); if (strpos($ret_url=$url, '?') !== false) list($ret_url,$othervars) = explode('?', $url, 2)+[null,null];
if ($extravars && is_array($extravars)) if ($extravars && is_array($extravars))
{ {
$vars += $extravars; $vars += $extravars;
$extravars = $othervars; $extravars = $othervars;
} }
else elseif (!empty($othervars))
{ {
if ($othervars) $extravars .= ($extravars?'&':'').$othervars; $extravars .= ($extravars ? '&' : '') . $othervars;
} }
// parse extravars string into the vars array // parse extravars string into the vars array
if ($extravars) if (!empty($extravars))
{ {
foreach(explode('&', $extravars) as $expr) foreach(explode('&', $extravars) as $expr)
{ {
@ -1720,7 +1720,7 @@ class Session
{ {
if (PHP_SAPI === "cli") return; // gives warnings and has no benefit if (PHP_SAPI === "cli") return; // gives warnings and has no benefit
if ($GLOBALS['egw_info']['server']['cookiedomain']) if (!empty($GLOBALS['egw_info']['server']['cookiedomain']))
{ {
// Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com // Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com
self::$cookie_domain = $GLOBALS['egw_info']['server']['cookiedomain']; self::$cookie_domain = $GLOBALS['egw_info']['server']['cookiedomain'];
@ -1741,7 +1741,7 @@ class Session
// setcookie dont likes domains without dots, leaving it empty, gets setcookie to fill the domain in // setcookie dont likes domains without dots, leaving it empty, gets setcookie to fill the domain in
self::$cookie_domain = ''; self::$cookie_domain = '';
} }
if (!$GLOBALS['egw_info']['server']['cookiepath'] || if (empty($GLOBALS['egw_info']['server']['cookiepath']) ||
!(self::$cookie_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH))) !(self::$cookie_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH)))
{ {
self::$cookie_path = '/'; self::$cookie_path = '/';
@ -1851,7 +1851,7 @@ class Session
private function update_dla($update_access_log=false) private function update_dla($update_access_log=false)
{ {
// This way XML-RPC users aren't always listed as xmlrpc.php // This way XML-RPC users aren't always listed as xmlrpc.php
if (isset($_GET['menuaction'])) if (isset($_GET['menuaction']) && strpos($_GET['menuaction'], '.ajax_exec.template.') !== false)
{ {
list(, $action) = explode('.ajax_exec.template.', $_GET['menuaction']); list(, $action) = explode('.ajax_exec.template.', $_GET['menuaction']);

View File

@ -16,6 +16,7 @@ namespace EGroupware\Api\Storage;
use DOMDocument; use DOMDocument;
use EGroupware\Api; use EGroupware\Api;
use EGroupware\Api\Vfs; use EGroupware\Api\Vfs;
use EGroupware\Collabora\Conversion;
use EGroupware\Stylite; use EGroupware\Stylite;
use tidy; use tidy;
use uiaccountsel; use uiaccountsel;
@ -31,6 +32,26 @@ use ZipArchive;
*/ */
abstract class Merge abstract class Merge
{ {
/**
* Preference, path where we will put the generated document
*/
const PREF_STORE_LOCATION = "merge_store_path";
/**
* Preference, placeholders for creating the filename of the generated document
*/
const PREF_DOCUMENT_FILENAME = "document_download_name";
/**
* List of placeholders
*/
const DOCUMENT_FILENAME_OPTIONS = [
'$$document$$' => 'Template name',
'$$link_title$$' => 'Entry link-title',
'$$contact_title$$' => 'Contact link-title',
'$$current_date$$' => 'Current date',
];
/** /**
* Instance of the addressbook_bo class * Instance of the addressbook_bo class
* *
@ -48,7 +69,12 @@ abstract class Merge
/** /**
* Fields that are to be treated as datetimes, when merged into spreadsheets * Fields that are to be treated as datetimes, when merged into spreadsheets
*/ */
var $date_fields = array(); var $date_fields = [];
/**
* Fields that are numeric, for special numeric handling
*/
protected $numeric_fields = [];
/** /**
* Mimetype of document processed by merge * Mimetype of document processed by merge
@ -77,10 +103,10 @@ abstract class Merge
*/ */
public $export_limit; public $export_limit;
public $public_functions = array( public $public_functions = array(
"merge_entries" => true "merge_entries" => true
); );
/** /**
* Configuration for HTML Tidy to clean up any HTML content that is kept * Configuration for HTML Tidy to clean up any HTML content that is kept
*/ */
@ -237,24 +263,36 @@ abstract class Merge
$replacements = array(); $replacements = array();
foreach(array_keys($this->contacts->contact_fields) as $name) foreach(array_keys($this->contacts->contact_fields) as $name)
{ {
$value = $contact[$name]; $value = $contact[$name] ?? '';
if(!$value)
{
continue;
}
switch($name) switch($name)
{ {
case 'created': case 'modified': case 'created':
if($value) $value = Api\DateTime::to($value); case 'modified':
if($value)
{
$value = Api\DateTime::to($value);
}
break; break;
case 'bday': case 'bday':
if($value) if($value)
{ {
try { try
{
$value = Api\DateTime::to($value, true); $value = Api\DateTime::to($value, true);
} }
catch (\Exception $e) { catch (\Exception $e)
{
unset($e); // ignore exception caused by wrongly formatted date unset($e); // ignore exception caused by wrongly formatted date
} }
} }
break; break;
case 'owner': case 'creator': case 'modifier': case 'owner':
case 'creator':
case 'modifier':
$value = Api\Accounts::username($value); $value = Api\Accounts::username($value);
break; break;
case 'cat_id': case 'cat_id':
@ -354,7 +392,9 @@ abstract class Merge
$cats[$cat_id] = array(); $cats[$cat_id] = array();
} }
} }
foreach($cats as $main => $cat) { $replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] = '';
foreach($cats as $main => $cat)
{
$replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] .= $GLOBALS['egw']->categories->id2name($main,'name') $replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] .= $GLOBALS['egw']->categories->id2name($main,'name')
. (count($cat) > 0 ? ': ' : '') . implode(', ', $cats[$main]) . "\n"; . (count($cat) > 0 ? ': ' : '') . implode(', ', $cats[$main]) . "\n";
} }
@ -807,6 +847,7 @@ abstract class Merge
*/ */
public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null) public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null)
{ {
$ids = empty($ids) ? [] : (array)$ids;
$matches = null; $matches = null;
if ($mimetype == 'application/xml' && if ($mimetype == 'application/xml' &&
preg_match('/'.preg_quote('<?mso-application progid="', '/').'([^"]+)'.preg_quote('"?>', '/').'/',substr($_content,0,200),$matches)) preg_match('/'.preg_quote('<?mso-application progid="', '/').'([^"]+)'.preg_quote('"?>', '/').'/',substr($_content,0,200),$matches))
@ -838,7 +879,7 @@ abstract class Merge
$content = preg_replace(array_keys($fix),array_values($fix),$content); $content = preg_replace(array_keys($fix),array_values($fix),$content);
//die("<pre>".htmlspecialchars($content)."</pre>\n"); //die("<pre>".htmlspecialchars($content)."</pre>\n");
} }
list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY); //get differt parts of document, seperatet by Pagerepeat list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY)+[null,null,null]; //get differt parts of document, seperatet by Pagerepeat
if ($mimetype == 'text/plain' && $ids && count($ids) > 1) if ($mimetype == 'text/plain' && $ids && count($ids) > 1)
{ {
// textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag // textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag
@ -882,11 +923,11 @@ abstract class Merge
$contentstart .= '<w:body>'; $contentstart .= '<w:body>';
$contentend = '</w:body></w:document>'; $contentend = '</w:body></w:document>';
} }
list($Labelstart,$Labelrepeat,$Labeltend) = preg_split('/\$\$label\$\$/',$contentrepeat,-1, PREG_SPLIT_NO_EMPTY); //get the Lable content list($Labelstart,$Labelrepeat,$Labeltend) = preg_split('/\$\$label\$\$/',$contentrepeat,-1, PREG_SPLIT_NO_EMPTY)+[null,null,null]; //get the label content
preg_match_all('/\$\$labelplacement\$\$/',$contentrepeat,$countlables, PREG_SPLIT_NO_EMPTY); preg_match_all('/\$\$labelplacement\$\$/',$contentrepeat,$countlables, PREG_SPLIT_NO_EMPTY);
$countlables = count($countlables[0]); $countlables = count($countlables[0]);
preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1); preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1);
if ($countlables > 1) $lableprint = true; $lableprint = $countlables > 1;
if (count($ids) > 1 && !$contentrepeat) if (count($ids) > 1 && !$contentrepeat)
{ {
$err = lang('for more than one contact in a document use the tag pagerepeat!'); $err = lang('for more than one contact in a document use the tag pagerepeat!');
@ -937,7 +978,7 @@ abstract class Merge
if ($contentrepeat) $content = $contentrepeat; //content to repeat if ($contentrepeat) $content = $contentrepeat; //content to repeat
if ($lableprint) $content = $Labelrepeat; if ($lableprint) $content = $Labelrepeat;
// generate replacements; if exeption is thrown, catch it set error message and return false // generate replacements; if exception is thrown, catch it set error message and return false
try try
{ {
if(!($replacements = $this->get_replacements($id,$content))) if(!($replacements = $this->get_replacements($id,$content)))
@ -956,7 +997,7 @@ abstract class Merge
} }
if ($this->report_memory_usage) error_log(__METHOD__."() $n: $id ".Api\Vfs::hsize(memory_get_usage(true))); if ($this->report_memory_usage) error_log(__METHOD__."() $n: $id ".Api\Vfs::hsize(memory_get_usage(true)));
// some general replacements: current user, date and time // some general replacements: current user, date and time
if (strpos($content,'$$user/') !== null && ($user = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id'))) if(strpos($content, '$$user/') !== false && ($user = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'], 'person_id')))
{ {
$replacements += $this->contact_replacements($user, 'user', false, $content); $replacements += $this->contact_replacements($user, 'user', false, $content);
$replacements['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'], 'account_primary_group')); $replacements['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'], 'account_primary_group'));
@ -1131,7 +1172,7 @@ abstract class Merge
{ {
foreach($this->date_fields as $field) foreach($this->date_fields as $field)
{ {
if(($value = $replacements['$$'.$field.'$$'])) if(($value = $replacements['$$'.$field.'$$'] ?? null))
{ {
$time = Api\DateTime::createFromFormat('+'.Api\DateTime::$user_dateformat.' '.Api\DateTime::$user_timeformat.'*', $value); $time = Api\DateTime::createFromFormat('+'.Api\DateTime::$user_dateformat.' '.Api\DateTime::$user_timeformat.'*', $value);
$replacements['$$'.$field.'/date$$'] = $time ? $time->format(Api\DateTime::$user_dateformat) : ''; $replacements['$$'.$field.'/date$$'] = $time ? $time->format(Api\DateTime::$user_dateformat) : '';
@ -1254,7 +1295,8 @@ abstract class Merge
// Look for numbers, set their value if needed // Look for numbers, set their value if needed
if(property_exists($this,'numeric_fields') || count($names)) if(property_exists($this,'numeric_fields') || count($names))
{ {
foreach((array)$this->numeric_fields as $fieldname) { foreach($this->numeric_fields as $fieldname)
{
$names[] = preg_quote($fieldname,'/'); $names[] = preg_quote($fieldname,'/');
} }
$this->format_spreadsheet_numbers($content, $names, $mimetype.$mso_application_progid); $this->format_spreadsheet_numbers($content, $names, $mimetype.$mso_application_progid);
@ -1347,7 +1389,8 @@ abstract class Merge
*/ */
protected function format_spreadsheet_numbers(&$content, $names, $mimetype) protected function format_spreadsheet_numbers(&$content, $names, $mimetype)
{ {
foreach((array)$this->numeric_fields as $fieldname) { foreach($this->numeric_fields as $fieldname)
{
$names[] = preg_quote($fieldname,'/'); $names[] = preg_quote($fieldname,'/');
} }
switch($mimetype) switch($mimetype)
@ -1371,7 +1414,7 @@ abstract class Merge
break; break;
} }
if($format && $names) if (!empty($format) && $names)
{ {
// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs // Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
do { do {
@ -1599,10 +1642,10 @@ abstract class Merge
* @param $appname * @param $appname
*/ */
public static function get_app_class($appname) public static function get_app_class($appname)
{
if(class_exists($appname) && is_subclass_of($appname, 'EGroupware\\Api\\Storage\\Merge'))
{ {
$classname = "{$appname}_merge"; $classname = "{$appname}_merge";
if(class_exists($classname, false) && is_subclass_of($classname, 'EGroupware\\Api\\Storage\\Merge'))
{
$document_merge = new $classname(); $document_merge = new $classname();
} }
else else
@ -1630,12 +1673,7 @@ abstract class Merge
try try
{ {
$classname = "{$app}_merge"; $class = $this->get_app_class($app);
if(!class_exists($classname))
{
return $replacements;
}
$class = new $classname();
$method = $app . '_replacements'; $method = $app . '_replacements';
if(method_exists($class, $method)) if(method_exists($class, $method))
{ {
@ -1654,6 +1692,30 @@ abstract class Merge
return $replacements; return $replacements;
} }
/**
* Prefix a placeholder, taking care of $$ or {{}} markers
*
* @param string $prefix Placeholder prefix
* @param string $placeholder Placeholder, with or without {{...}} or $$...$$ markers
* @param null|string $wrap "{" or "$" to add markers, omit to exclude markers
* @return string
*/
protected function prefix($prefix, $placeholder, $wrap = null)
{
$marker = ['', ''];
if($placeholder[0] == '{' && is_null($wrap) || $wrap[0] == '{')
{
$marker = ['{{', '}}'];
}
elseif($placeholder[0] == '$' && is_null($wrap) || $wrap[0] == '$')
{
$marker = ['$$', '$$'];
}
$placeholder = str_replace(['{{', '}}', '$$'], '', $placeholder);
return $marker[0] . ($prefix ? $prefix . '/' : '') . $placeholder . $marker[1];
}
/** /**
* Process special flags, such as IF or NELF * Process special flags, such as IF or NELF
* *
@ -1780,13 +1842,12 @@ abstract class Merge
if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0) if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0)
{ //sets a Letterprefix { //sets a Letterprefix
$replaceprefixsort = array(); $replaceprefixsort = array();
// ToDo Stefan: $contentstart is NOT defined here!!!
$replaceprefix = explode(' ',substr($param[0],21,-2)); $replaceprefix = explode(' ',substr($param[0],21,-2));
foreach ($replaceprefix as $nameprefix) foreach ($replaceprefix as $nameprefix)
{ {
if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$']; if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$'];
} }
$replace = implode($replaceprefixsort,' '); $replace = implode(' ', $replaceprefixsort);
} }
return $replace; return $replace;
} }
@ -2092,7 +2153,6 @@ abstract class Merge
$export_limit=null) $export_limit=null)
{ {
$documents = array(); $documents = array();
$editable_mimes = array();
if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting
try { try {
@ -2120,9 +2180,9 @@ abstract class Merge
$documents['document'] = array( $documents['document'] = array(
'icon' => Api\Vfs::mime_icon($file['mime']), 'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\Vfs::decodePath(Api\Vfs::basename($default_doc)), 'caption' => Api\Vfs::decodePath(Api\Vfs::basename($default_doc)),
'group' => 1, 'group' => 1
'postSubmit' => true, // download needs post submit (not Ajax) to work
); );
self::document_editable_action($documents['document'], $file);
if ($file['mime'] == 'message/rfc822') if ($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($documents['document'], $file); self::document_mail_action($documents['document'], $file);
@ -2177,13 +2237,6 @@ abstract class Merge
} }
foreach($files as $file) foreach($files as $file)
{ {
$edit_attributes = array(
'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'].'.'.get_called_class().'.merge_entries',
'document' => $file['path'],
'merge' => get_called_class(),
'id' => '$id',
'select_all' => '$select_all'
);
if (count($dircount) > 1) if (count($dircount) > 1)
{ {
$name_arr = explode('/', $file['name']); $name_arr = explode('/', $file['name']);
@ -2201,14 +2254,8 @@ abstract class Merge
switch($count) switch($count)
{ {
case (count($name_arr) - 1): case (count($name_arr) - 1):
$current_level[$prefix.$file['name']] = array( $current_level[$prefix . $file['name']];
'icon' => Api\Vfs::mime_icon($file['mime']), self::document_editable_action($current_level[$prefix . $file['name']], $file);
'caption' => Api\Vfs::decodePath($name_arr[$count]),
'group' => 2,
'postSubmit' => true, // download needs post submit (not Ajax) to work,
'target' => '_blank',
'url' => urldecode(http_build_query($edit_attributes))
);
if($file['mime'] == 'message/rfc822') if($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($current_level[$prefix . $file['name']], $file); self::document_mail_action($current_level[$prefix . $file['name']], $file);
@ -2241,12 +2288,8 @@ abstract class Merge
'children' => array(), 'children' => array(),
); );
} }
$documents[$file['mime']]['children'][$prefix.$file['name']] = array( $documents[$file['mime']]['children'][$prefix . $file['name']] = array();
'caption' => Api\Vfs::decodePath($file['name']), self::document_editable_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file);
'target' => '_blank',
'postSubmit' => true, // download needs post submit (not Ajax) to work
);
$documents[$file['mime']]['children'][$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes));
if($file['mime'] == 'message/rfc822') if($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file); self::document_mail_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file);
@ -2254,13 +2297,8 @@ abstract class Merge
} }
else else
{ {
$documents[$prefix.$file['name']] = array( $documents[$prefix . $file['name']] = array();
'icon' => Api\Vfs::mime_icon($file['mime']), self::document_editable_action($documents[$prefix . $file['name']], $file);
'caption' => Api\Vfs::decodePath($file['name']),
'group' => 2,
'target' => '_blank'
);
$documents[$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes));
if($file['mime'] == 'message/rfc822') if($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($documents[$prefix . $file['name']], $file); self::document_mail_action($documents[$prefix . $file['name']], $file);
@ -2268,13 +2306,19 @@ abstract class Merge
} }
} }
// Add PDF checkbox
$documents['as_pdf'] = array(
'caption' => 'As PDF',
'checkbox' => true,
);
return array( return array(
'icon' => 'etemplate/merge', 'icon' => 'etemplate/merge',
'caption' => $caption, 'caption' => $caption,
'children' => $documents, 'children' => $documents,
// disable action if no document or export completly forbidden for non-admins // disable action if no document or export completly forbidden for non-admins
'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit, 'ISALLOWED') || self::is_export_limit_excepted()), 'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit, 'ISALLOWED') || self::is_export_limit_excepted()),
'hideOnDisabled' => true, // do not show 'Insert in document', if no documents defined or no export allowed 'hideOnDisabled' => true,
// do not show 'Insert in document', if no documents defined or no export allowed
'group' => $group, 'group' => $group,
); );
} }
@ -2293,6 +2337,7 @@ abstract class Merge
private static function document_mail_action(Array &$action, $file) private static function document_mail_action(Array &$action, $file)
{ {
unset($action['postSubmit']); unset($action['postSubmit']);
unset($action['onExecute']);
// Lots takes a while, confirm // Lots takes a while, confirm
$action['confirm_multiple'] = lang('Do you want to send the message to all selected entries, WITHOUT further editing?'); $action['confirm_multiple'] = lang('Do you want to send the message to all selected entries, WITHOUT further editing?');
@ -2325,15 +2370,30 @@ abstract class Merge
*/ */
private static function document_editable_action(Array &$action, $file) private static function document_editable_action(Array &$action, $file)
{ {
unset($action['postSubmit']); static $action_base = array(
// The same for every file
'group' => 2,
// Overwritten for every file
'icon' => '', //Api\Vfs::mime_icon($file['mime']),
'caption' => '', //Api\Vfs::decodePath($name_arr[$count]),
);
$edit_attributes = array( $edit_attributes = array(
'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit', 'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'] . '.' . get_called_class() . '.merge_entries',
'document' => $file['path'], 'document' => $file['path'],
'merge' => get_called_class(), 'merge' => get_called_class(),
'id' => '$id',
'select_all' => '$select_all'
); );
$action['url'] = urldecode(http_build_query($edit_attributes));
$action = array_merge(
$action_base,
array(
'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\Vfs::decodePath($file['name']),
'onExecute' => 'javaScript:app.' . $GLOBALS['egw_info']['flags']['currentapp'] . '.merge',
'merge_data' => $edit_attributes
),
// Merge in provided action last, so we can customize if needed (eg: default document)
$action
);
} }
/** /**
@ -2374,12 +2434,13 @@ abstract class Merge
* Merge the selected IDs into the given document, save it to the VFS, then * Merge the selected IDs into the given document, save it to the VFS, then
* either open it in the editor or have the browser download the file. * either open it in the editor or have the browser download the file.
* *
* @param String[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request. * @param string[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request.
* @param Merge|null $document_merge Already instantiated Merge object to do the merge. * @param Merge|null $document_merge Already instantiated Merge object to do the merge.
* @param boolean|null $pdf Convert result to PDF
* @throws Api\Exception * @throws Api\Exception
* @throws Api\Exception\AssertionFailed * @throws Api\Exception\AssertionFailed
*/ */
public static function merge_entries(array $ids = null, Merge &$document_merge = null) public static function merge_entries(array $ids = null, Merge &$document_merge = null, $pdf = null)
{ {
if(is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge')) if(is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
{ {
@ -2392,7 +2453,7 @@ abstract class Merge
if(($error = $document_merge->check_document($_REQUEST['document'],''))) if(($error = $document_merge->check_document($_REQUEST['document'],'')))
{ {
$response->error($error); error_log(__METHOD__ . "({$_REQUEST['document']}) $error");
return; return;
} }
@ -2405,34 +2466,32 @@ abstract class Merge
$ids = self::get_all_ids($document_merge); $ids = self::get_all_ids($document_merge);
} }
$filename = $document_merge->get_filename($_REQUEST['document']); if(is_null($pdf))
{
$pdf = (boolean)$_REQUEST['pdf'];
}
$filename = $document_merge->get_filename($_REQUEST['document'], $ids);
$result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header); $result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header);
if(!is_file($result) || !is_readable($result)) if(!is_file($result) || !is_readable($result))
{ {
throw new Api\Exception\AssertionFailed("Unable to generate merge file\n" . $result); throw new Api\Exception\AssertionFailed("Unable to generate merge file\n" . $result);
} }
// Put it into the vfs using user's configured home dir if writable, // Put it into the vfs using user's preferred directory if writable,
// or expected home dir (/home/username) if not // or expected home dir (/home/username) if not
$target = $_target = (Vfs::is_writable(Vfs::get_home_dir()) ? $target = $document_merge->get_save_path($filename);
Vfs::get_home_dir() :
"/home/{$GLOBALS['egw_info']['user']['account_lid']}" // Make sure we won't overwrite something already there
)."/$filename"; $target = Vfs::make_unique($target);
$dupe_count = 0;
while(is_file(Vfs::PREFIX.$target))
{
$dupe_count++;
$target = Vfs::dirname($_target) . '/' .
pathinfo($filename, PATHINFO_FILENAME) .
' ('.($dupe_count + 1).')' . '.' .
pathinfo($filename, PATHINFO_EXTENSION);
}
copy($result, Vfs::PREFIX . $target); copy($result, Vfs::PREFIX . $target);
unlink($result); unlink($result);
// Find out what to do with it // Find out what to do with it
$editable_mimes = array(); $editable_mimes = array();
try { try
{
if(class_exists('EGroupware\\collabora\\Bo') && if(class_exists('EGroupware\\collabora\\Bo') &&
$GLOBALS['egw_info']['user']['apps']['collabora'] && $GLOBALS['egw_info']['user']['apps']['collabora'] &&
($discovery = \EGroupware\collabora\Bo::discover()) && ($discovery = \EGroupware\collabora\Bo::discover()) &&
@ -2447,7 +2506,28 @@ abstract class Merge
// ignore failed discovery // ignore failed discovery
unset($e); unset($e);
} }
if($editable_mimes[Vfs::mime_content_type($target)])
// PDF conversion
if($editable_mimes[Vfs::mime_content_type($target)] && $pdf)
{
$error = '';
$converted_path = '';
$convert = new Conversion();
$convert->convert($target, $converted_path, 'pdf', $error);
if($error)
{
error_log(__METHOD__ . "({$_REQUEST['document']}) $target => $converted_path Error in PDF conversion: $error");
}
else
{
// Remove original
Vfs::unlink($target);
$target = $converted_path;
}
}
if($editable_mimes[Vfs::mime_content_type($target)] &&
!in_array(Vfs::mime_content_type($target), explode(',', $GLOBALS['egw_info']['user']['preferences']['filemanager']['collab_excluded_mimes'])))
{ {
\Egroupware\Api\Egw::redirect_link('/index.php', array( \Egroupware\Api\Egw::redirect_link('/index.php', array(
'menuaction' => 'collabora.EGroupware\\Collabora\\Ui.editor', 'menuaction' => 'collabora.EGroupware\\Collabora\\Ui.editor',
@ -2461,14 +2541,80 @@ abstract class Merge
} }
/** /**
* Generate a filename for the merged file * Generate a filename for the merged file, without extension
* *
* Default is just the name of the template * Default filename is just the name of the template.
* We use the placeholders from get_filename_placeholders() and the application's document filename preference
* to generate a custom filename.
*
* @param string $document Template filename
* @param string[] $ids List of IDs being merged
* @return string * @return string
*/ */
protected function get_filename($document) : string protected function get_filename($document, $ids = []) : string
{ {
return ''; $name = '';
if(isset($GLOBALS['egw_info']['user']['preferences'][$this->get_app()][static::PREF_DOCUMENT_FILENAME]))
{
$pref = $GLOBALS['egw_info']['user']['preferences'][$this->get_app()][static::PREF_DOCUMENT_FILENAME];
$placeholders = $this->get_filename_placeholders($document, $ids);
// Make values safe for VFS
foreach($placeholders as &$value)
{
$value = Api\Mail::clean_subject_for_filename($value);
}
// Do replacement
$name = str_replace(
array_keys($placeholders),
array_values($placeholders),
is_array($pref) ? implode(' ', $pref) : $pref
);
}
return $name;
}
protected function get_filename_placeholders($document, $ids)
{
$ext = '.' . pathinfo($document, PATHINFO_EXTENSION);
$link_title = count($ids) == 1 ? Api\Link::title($this->get_app(), $ids[0]) : lang("multiple");
$contact_title = count($ids) == 1 ? Api\Link::title($this->get_app(), $ids[0]) : lang("multiple");
$current_date = str_replace('/', '-', Api\DateTime::to('now', Api\DateTime::$user_dateformat));
$values = [
'$$document$$' => basename($document, $ext),
'$$link_title$$' => $link_title,
'$$contact_title$$' => $contact_title,
'$$current_date$$' => $current_date
];
return $values;
}
/**
* Return a path where we can save the generated file
* Takes into account user preference.
*
* @param string $filename The name of the generated file, including extension
* @return string
*/
protected function get_save_path($filename) : string
{
// Default is home directory
$target = (Vfs::is_writable(Vfs::get_home_dir()) ?
Vfs::get_home_dir() :
"/home/{$GLOBALS['egw_info']['user']['account_lid']}"
);
// Check for a configured preferred directory
if(($pref = $GLOBALS['egw_info']['user']['preferences']['filemanager'][Merge::PREF_STORE_LOCATION]) && Vfs::is_writable($pref))
{
$target = $pref;
}
return $target . "/$filename";
} }
/** /**
@ -2604,6 +2750,8 @@ abstract class Merge
// General information // General information
'date' => lang('Date'), 'date' => lang('Date'),
'datetime' => lang('Date + time'),
'time' => lang('Time'),
'user/n_fn' => lang('Name of current user, all other contact fields are valid too'), 'user/n_fn' => lang('Name of current user, all other contact fields are valid too'),
'user/account_lid' => lang('Username'), 'user/account_lid' => lang('Username'),
@ -2621,19 +2769,146 @@ abstract class Merge
); );
} }
/**
* Get a list of common placeholders
*
* @param string $prefix
*/
public function get_common_placeholder_list($prefix = '')
{
$placeholders = [
'URLs' => [],
'Egroupware links' => [],
'General' => [],
'Repeat' => [],
'Commands' => []
];
// Iterate through the list & switch groups as we go
// Hopefully a little better than assigning each field to a group
$group = 'URLs';
foreach($this->get_common_replacements() as $name => $label)
{
if(in_array($name, array('user/n_fn', 'user/account_lid')))
{
continue;
} // don't show them, they're in 'User'
switch($name)
{
case 'links':
$group = 'Egroupware links';
break;
case 'date':
$group = 'General';
break;
case 'pagerepeat':
$group = 'Repeat';
break;
case 'IF fieldname':
$group = 'Commands';
}
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
}
return $placeholders;
}
/** /**
* Get a list of placeholders for the current user * Get a list of placeholders for the current user
*/ */
public function get_user_placeholder_list($prefix = '') public function get_user_placeholder_list($prefix = '')
{ {
$contacts = new Api\Contacts\Merge(); $contacts = new Api\Contacts\Merge();
$replacements = $contacts->get_placeholder_list(($prefix ? $prefix . '/' : '') . 'user'); $replacements = $contacts->get_placeholder_list($this->prefix($prefix, 'user'));
unset($replacements['details']['{{' . ($prefix ? $prefix . '/' : '') . 'user/account_id}}']); unset($replacements['details'][$this->prefix($prefix, 'user/account_id', '{')]);
$replacements['account'] = [ $replacements['account'] = [
'{{' . ($prefix ? $prefix . '/' : '') . 'user/account_id}}' => 'Account ID', [
'{{' . ($prefix ? $prefix . '/' : '') . 'user/account_lid}}' => 'Login ID' 'value' => $this->prefix($prefix, 'user/account_id', '{'),
'label' => 'Account ID'
],
[
'value' => $this->prefix($prefix, 'user/account_lid', '{'),
'label' => 'Login ID'
]
]; ];
return $replacements; return $replacements;
} }
/**
* Get the list of placeholders for an application's customfields
* If the customfield is a link to another application, we expand and add those placeholders as well
*/
protected function add_customfield_placeholders(&$placeholders, $prefix = '')
{
foreach(Customfields::get($this->get_app()) as $name => $field)
{
if(array_key_exists($field['type'], Api\Link::app_list()))
{
$app = self::get_app_class($field['type']);
if($app)
{
$this->add_linked_placeholders($placeholders, $name, $app->get_placeholder_list('#' . $name));
}
}
else
{
$placeholders['customfields'][] = [
'value' => $this->prefix($prefix, '#' . $name, '{'),
'label' => $field['label'] . ($field['type'] == 'select-account' ? '*' : '')
];
}
}
}
/**
* Get a list of placeholders provided.
*
* Placeholders are grouped logically. Group key should have a user-friendly translation.
* Override this method and specify the placeholders, as well as groups or a specific order
*/
public function get_placeholder_list($prefix = '')
{
$placeholders = [
'placeholders' => []
];
$this->add_customfield_placeholders($placeholders, $prefix);
return $placeholders;
}
/**
* Add placeholders from another application into the given list of placeholders
*
* This is used for linked entries (like info_contact) and custom fields where the type is another application.
* Here we adjust the group name, and add the group to the end of the placeholder list
* @param array $placeholder_list Our placeholder list
* @param string $base_name Name of the entry (eg: Contact, custom field name)
* @param array $add_placeholder_groups Placeholder list from the other app. Placeholders should include any needed prefix
*/
protected function add_linked_placeholders(&$placeholder_list, $base_name, $add_placeholder_groups) : void
{
if(!$add_placeholder_groups)
{
// Skip empties
return;
}
/*
foreach($add_placeholder_groups as $group => $add_placeholders)
{
$placeholder_list[$base_name . ': ' . lang($group)] = $add_placeholders;
}
*/
$placeholder_list[$base_name] = $add_placeholder_groups;
}
} }

View File

@ -349,8 +349,8 @@ class Vfs extends Vfs\Base
{ {
//error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n"); //error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n");
$type = $options['type']; // 'd', 'f' or 'F' $type = $options['type'] ?? null; // 'd', 'f' or 'F'
$dirs_last = $options['depth']; // put content of dirs before the dir itself $dirs_last = !empty($options['depth']); // put content of dirs before the dir itself
// show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary) // show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary)
$dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0); $dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0);
if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work
@ -386,7 +386,7 @@ class Vfs extends Vfs\Base
$options['gid'] = 0; $options['gid'] = 0;
} }
} }
if ($options['order'] == 'mime') if (isset($options['order']) && $options['order'] === 'mime')
{ {
$options['need_mime'] = true; // we need to return the mime colum $options['need_mime'] = true; // we need to return the mime colum
} }
@ -403,7 +403,7 @@ class Vfs extends Vfs\Base
], ],
]); ]);
$url = $options['url']; $url = $options['url'] ?? null;
if (!is_array($base)) if (!is_array($base))
{ {
@ -422,7 +422,7 @@ class Vfs extends Vfs\Base
$options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0; $options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0;
} }
$is_dir = is_dir($path); $is_dir = is_dir($path);
if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir)) if (empty($options['mindepth']) && (!$dirs_last || !$is_dir))
{ {
self::_check_add($options,$path,$result); self::_check_add($options,$path,$result);
} }
@ -434,11 +434,11 @@ class Vfs extends Vfs\Base
{ {
if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir! if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir!
if (self::is_hidden($fname, $options['show-deleted']) && !$options['hidden']) continue; // ignore hidden files if (self::is_hidden($fname, $options['show-deleted'] ?? false) && !$options['hidden']) continue; // ignore hidden files
$file = self::concat($path, $fname); $file = self::concat($path, $fname);
if ((int)$options['mindepth'] <= 1) if (!isset($options['mindepth']) || (int)$options['mindepth'] <= 1)
{ {
self::_check_add($options,$file,$result); self::_check_add($options,$file,$result);
} }
@ -459,7 +459,7 @@ class Vfs extends Vfs\Base
} }
closedir($dir); closedir($dir);
} }
if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last) if ($is_dir && empty($options['mindepth']) && $dirs_last)
{ {
self::_check_add($options,$path,$result); self::_check_add($options,$path,$result);
} }
@ -569,9 +569,9 @@ class Vfs extends Vfs\Base
*/ */
private static function _check_add($options,$path,&$result) private static function _check_add($options,$path,&$result)
{ {
$type = $options['type']; // 'd' or 'f' $type = $options['type'] ?? null; // 'd' or 'f'
if ($options['url']) if (!empty($options['url']))
{ {
if (($stat = @lstat($path))) if (($stat = @lstat($path)))
{ {
@ -595,7 +595,7 @@ class Vfs extends Vfs\Base
$stat['path'] = self::parse_url($path,PHP_URL_PATH); $stat['path'] = self::parse_url($path,PHP_URL_PATH);
$stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path); $stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path);
if ($options['mime'] || $options['need_mime']) if (!empty($options['mime']) || !empty($options['need_mime']))
{ {
$stat['mime'] = self::mime_content_type($path); $stat['mime'] = self::mime_content_type($path);
} }
@ -642,7 +642,7 @@ class Vfs extends Vfs\Base
return; // not create/modified in the spezified time return; // not create/modified in the spezified time
} }
// do we return url or just vfs pathes // do we return url or just vfs pathes
if (!$options['url']) if (empty($options['url']))
{ {
$path = self::parse_url($path,PHP_URL_PATH); $path = self::parse_url($path,PHP_URL_PATH);
} }
@ -1227,7 +1227,7 @@ class Vfs extends Vfs\Base
} }
} }
} }
return $component >= 0 ? $result[$component2str[$component]] : $result; return $component >= 0 ? ($result[$component2str[$component]] ?? null) : $result;
} }
/** /**
@ -1240,7 +1240,7 @@ class Vfs extends Vfs\Base
*/ */
static function dirname($_url) static function dirname($_url)
{ {
list($url,$query) = explode('?',$_url,2); // strip the query first, as it can contain slashes if (strpos($url=$_url, '?') !== false) list($url, $query) = explode('?',$_url,2); // strip the query first, as it can contain slashes
if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/') if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/')
{ {
@ -1255,7 +1255,7 @@ class Vfs extends Vfs\Base
array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/ array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/
} }
//error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : '')); //error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : ''));
return implode('/',$parts).($query ? '?'.$query : ''); return implode('/',$parts).(!empty($query) ? '?'.$query : '');
} }
/** /**
@ -1283,7 +1283,7 @@ class Vfs extends Vfs\Base
*/ */
static function concat($_url,$relative) static function concat($_url,$relative)
{ {
list($url,$query) = explode('?',$_url,2); if (strpos($url=$_url, '?') !== false) list($url, $query) = explode('?',$_url,2);
if (substr($url,-1) == '/') $url = substr($url,0,-1); if (substr($url,-1) == '/') $url = substr($url,0,-1);
$ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative); $ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative);
@ -2264,17 +2264,17 @@ class Vfs extends Vfs\Base
} }
} }
} }
if (!$mime && is_dir($url)) if (empty($mime) && is_dir($url))
{ {
$mime = self::DIR_MIME_TYPE; $mime = self::DIR_MIME_TYPE;
} }
// if we operate on the regular filesystem and the mime_content_type function is available --> use it // if we operate on the regular filesystem and the mime_content_type function is available --> use it
if (!$mime && !$scheme && function_exists('mime_content_type')) if (empty($mime) && !$scheme && function_exists('mime_content_type'))
{ {
$mime = mime_content_type($path); $mime = mime_content_type($path);
} }
// using EGw's own mime magic (currently only checking the extension!) // using EGw's own mime magic (currently only checking the extension!)
if (!$mime) if (empty($mime))
{ {
$mime = MimeMagic::filename2mime(self::parse_url($url,PHP_URL_PATH)); $mime = MimeMagic::filename2mime(self::parse_url($url,PHP_URL_PATH));
} }
@ -2386,6 +2386,28 @@ class Vfs extends Vfs\Base
} }
return self::_call_on_backend('get_minimum_file_id', array($path)); return self::_call_on_backend('get_minimum_file_id', array($path));
} }
/**
* Make sure the path is unique, by appending (#) to the filename if it already exists
*
* @param string $path
*
* @return string The same path, but modified if it exists
*/
static function make_unique($path)
{
$filename = Vfs::basename($path);
$dupe_count = 0;
while(is_file(Vfs::PREFIX . $path))
{
$dupe_count++;
$path = Vfs::dirname($path) . '/' .
pathinfo($filename, PATHINFO_FILENAME) .
' (' . ($dupe_count + 1) . ')' . '.' .
pathinfo($filename, PATHINFO_EXTENSION);
}
return $path;
}
} }
Vfs::init_static(); Vfs::init_static();

View File

@ -295,7 +295,7 @@ class Base
'host' => $GLOBALS['egw_info']['user']['domain'], 'host' => $GLOBALS['egw_info']['user']['domain'],
'home' => str_replace(array('\\\\', '\\'), array('', '/'), $GLOBALS['egw_info']['user']['homedirectory']), 'home' => str_replace(array('\\\\', '\\'), array('', '/'), $GLOBALS['egw_info']['user']['homedirectory']),
); );
$parts = array_merge(Vfs::parse_url($_path), Vfs::parse_url($path), $defaults); $parts = array_merge(Vfs::parse_url($_path), Vfs::parse_url($path) ?: [], $defaults);
if(!$parts['host']) if(!$parts['host'])
{ {
// otherwise we get an invalid url (scheme:///path/to/something)! // otherwise we get an invalid url (scheme:///path/to/something)!

View File

@ -1701,10 +1701,10 @@ class HTTP_WebDAV_Server
/** /**
* PUT method handler * PUT method handler
* *
* @param void * @param string $method='PUT'
* @return void * @return void
*/ */
function http_PUT() function http_PUT(string $method='PUT')
{ {
if ($this->_check_lock_status($this->path)) { if ($this->_check_lock_status($this->path)) {
$options = Array(); $options = Array();
@ -1839,7 +1839,7 @@ class HTTP_WebDAV_Server
} }
} }
$stat = $this->PUT($options); $stat = $this->$method($options);
if ($stat === false) { if ($stat === false) {
$stat = "403 Forbidden"; $stat = "403 Forbidden";

View File

@ -21,7 +21,7 @@ use EGroupware\Api\Egw;
// E_STRICT in PHP 5.4 gives various strict warnings in working code, which can NOT be easy fixed in all use-cases :-( // E_STRICT in PHP 5.4 gives various strict warnings in working code, which can NOT be easy fixed in all use-cases :-(
// Only variables should be assigned by reference, eg. soetemplate::tree_walk() // Only variables should be assigned by reference, eg. soetemplate::tree_walk()
// Declaration of <extended method> should be compatible with <parent method>, varios places where method parameters change // Declaration of <extended method> should be compatible with <parent method>, various places where method parameters change
// --> switching it off for now, as it makes error-log unusable // --> switching it off for now, as it makes error-log unusable
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED); error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);

View File

@ -16,7 +16,7 @@ use EGroupware\Api;
/** /**
* Translate message only if translation object is already loaded * Translate message only if translation object is already loaded
* *
* This function is usefull for exception handlers or early stages of the initialisation of the egw object, * This function is useful for exception handlers or early stages of the initialisation of the egw object,
* as calling lang would try to load the translations, evtl. cause more errors, eg. because there's no db-connection. * as calling lang would try to load the translations, evtl. cause more errors, eg. because there's no db-connection.
* *
* @param string $key message in englich with %1, %2, ... placeholders * @param string $key message in englich with %1, %2, ... placeholders
@ -36,7 +36,7 @@ function try_lang($key,$vars=null)
} }
/** /**
* Clasify exception for a headline and log it to error_log, if not running as cli * Classify exception for a headline and log it to error_log, if not running as cli
* *
* @param Exception|Error $e * @param Exception|Error $e
* @param string &$headline * @param string &$headline
@ -82,7 +82,7 @@ function _egw_log_exception($e,&$headline=null)
} }
/** /**
* Fail a little bit more gracefully then an uncought exception * Fail a little more gracefully then an uncaught exception
* *
* Does NOT return * Does NOT return
* *
@ -159,7 +159,7 @@ if (!isset($GLOBALS['egw_info']['flags']['no_exception_handler']) || $GLOBALS['e
} }
/** /**
* Fail a little bit more gracefully then a catchable fatal error, by throwing an exception * Fail a little more gracefully then a catchable fatal error, by throwing an exception
* *
* @param int $errno level of the error raised: E_* constants * @param int $errno level of the error raised: E_* constants
* @param string $errstr error message * @param string $errstr error message
@ -178,7 +178,7 @@ function egw_error_handler ($errno, $errstr, $errfile, $errline)
case E_WARNING: case E_WARNING:
case E_USER_WARNING: case E_USER_WARNING:
// skip message for warnings supressed via @-error-control-operator (eg. @is_dir($path)) // skip message for warnings suppressed via @-error-control-operator (eg. @is_dir($path))
// can be commented out to get suppressed warnings too! // can be commented out to get suppressed warnings too!
if ((error_reporting() & $errno) && PHP_VERSION < 8.0) if ((error_reporting() & $errno) && PHP_VERSION < 8.0)
{ {

View File

@ -96,7 +96,7 @@ foreach(array('_COOKIE','_GET','_POST','_REQUEST','HTTP_GET_VARS','HTTP_POST_VAR
} }
} }
// do the check for script-tags only for _GET and _POST or if we found something in _GET and _POST // do the check for script-tags only for _GET and _POST or if we found something in _GET and _POST
// speeds up the execusion a bit // speeds up the execution a bit
if (isset($GLOBALS[$where]) && is_array($GLOBALS[$where]) && ($n < 3 || isset($GLOBALS['egw_unset_vars']))) if (isset($GLOBALS[$where]) && is_array($GLOBALS[$where]) && ($n < 3 || isset($GLOBALS['egw_unset_vars'])))
{ {
_check_script_tag($GLOBALS[$where],$where); _check_script_tag($GLOBALS[$where],$where);
@ -143,8 +143,8 @@ if (ini_get('register_globals'))
* *
* Should be used for all external content, to guard against exploidts. * Should be used for all external content, to guard against exploidts.
* *
* PHP 7.0+ can be told not to instanciate any classes (and calling eg. it's destructor). * PHP 7.0+ can be told not to instantiate any classes (and calling eg. it's destructor).
* In fact it instanciates it as __PHP_Incomplete_Class without any methods and therefore disarming threads. * In fact it instantiates it as __PHP_Incomplete_Class without any methods and therefore disarming threads.
* *
* @param string $str * @param string $str
* @return mixed * @return mixed

View File

@ -2322,6 +2322,8 @@ div.et2_toolbar_more h.ui-accordion-header.header_list-short span.ui-accordion-h
.et2_toolbar_more .ui-accordion-header-active.header_list-short.ui-state-active span.ui-accordion-header-icon { .et2_toolbar_more .ui-accordion-header-active.header_list-short.ui-state-active span.ui-accordion-header-icon {
background-position: bottom !important; background-position: bottom !important;
margin-top: 2px; margin-top: 2px;
left: 0;
top: 0;
} }
.et2_toolbar .et2_toolbar_more h .toolbar-admin-pref { .et2_toolbar .et2_toolbar_more h .toolbar-admin-pref {
background-image: url(../../../pixelegg/images/setup.svg); background-image: url(../../../pixelegg/images/setup.svg);

View File

@ -24,7 +24,7 @@
</hbox> </hbox>
</vbox> </vbox>
<styles> <styles>
/** Structural stuff **/
#api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects { #api\.insert_merge_placeholder_outer_box > #api\.insert_merge_placeholder_selects {
flex: 1 1 80%; flex: 1 1 80%;
} }
@ -76,6 +76,11 @@
border-radius: 0px; border-radius: 0px;
background-color: transparent; background-color: transparent;
} }
/** Cosmetics **/
#api\.insert_merge_placeholder_outer_box option:first-letter {
text-transform: capitalize;
}
</styles> </styles>
</template> </template>
</overlay> </overlay>

View File

@ -9,7 +9,7 @@
<select id="placeholder_list"/> <select id="placeholder_list"/>
</vbox> </vbox>
<hrule/> <hrule/>
<link-entry id="entry" label="Select entry"/> <link-entry id="entry" label="Select entry" only_app="addressbook"/>
<hbox class="preview"> <hbox class="preview">
<description id="preview_content"/> <description id="preview_content"/>
</hbox> </hbox>
@ -49,7 +49,7 @@
flex-grow: 0; flex-grow: 0;
} }
div.et2_link_entry input.ui-autocomplete-input { div.et2_link_entry input.ui-autocomplete-input {
width: 75% width: 70%
} }
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button { div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button, button#cancel, .et2_button {
border: none; border: none;

View File

@ -22,11 +22,13 @@ require_once realpath(__DIR__.'/../WidgetBaseTest.php');
use EGroupware\Api\Etemplate; use EGroupware\Api\Etemplate;
class EntryTest extends \EGroupware\Api\Etemplate\WidgetBaseTest { class ContactEntryTest extends \EGroupware\Api\Etemplate\WidgetBaseTest
{
const TEST_TEMPLATE = 'api.entry_test_contact'; const TEST_TEMPLATE = 'api.entry_test_contact';
public static function setUpBeforeClass() : void { public static function setUpBeforeClass() : void
{
parent::setUpBeforeClass(); parent::setUpBeforeClass();
} }

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage; namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
use EGroupware\Api\LoggedInTest as LoggedInTest; use EGroupware\Api\LoggedInTest as LoggedInTest;
class CustomfieldsTest extends LoggedInTest class CustomfieldsTest extends LoggedInTest

View File

@ -14,8 +14,10 @@
namespace EGroupware\Api\Storage; namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../../src/Storage/Tracking.php';
class TestTracking extends Tracking { class TestTracking extends Tracking
{
var $app = 'test'; var $app = 'test';

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage; namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
require_once __DIR__ . '/TestTracking.php'; require_once __DIR__ . '/TestTracking.php';
use EGroupware\Api; use EGroupware\Api;

View File

@ -300,11 +300,14 @@ class SharingBase extends LoggedInTest
{ {
$this->markTestSkipped("No versioning available"); $this->markTestSkipped("No versioning available");
} }
if(substr($path, -1) == '/') $path = substr($path, 0, -1); if(substr($path, -1) == '/')
{
$path = substr($path, 0, -1);
}
$backup = Vfs::$is_root; $backup = Vfs::$is_root;
Vfs::$is_root = true; Vfs::$is_root = true;
$url = Versioning\StreamWrapper::PREFIX . $path; $url = Versioning\StreamWrapper::PREFIX . $path;
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $path as versioned"); $this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $path as versioned");
Vfs::$is_root = $backup; Vfs::$is_root = $backup;
$this->mounts[] = $path; $this->mounts[] = $path;
@ -363,7 +366,7 @@ class SharingBase extends LoggedInTest
Vfs::chown($path, $GLOBALS['egw_info']['user']['account_id']); Vfs::chown($path, $GLOBALS['egw_info']['user']['account_id']);
$url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME . '://default' . $path . '?merge=' . realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount'); $url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME . '://default' . $path . '?merge=' . realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount');
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path"); $this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $url to $path");
Vfs::$is_root = $backup; Vfs::$is_root = $backup;
$this->mounts[] = $path; $this->mounts[] = $path;

View File

@ -901,7 +901,7 @@ END:VALARM';
{ {
Api\Translation::add_app('calendar'); Api\Translation::add_app('calendar');
// do not set actions for alarm type // do not set actions for alarm type
if ($params['data']['type'] == 6) if (isset($params['data']['type']) && $params['data']['type'] == 6)
{ {
if (!empty($params['data']['videoconference']) if (!empty($params['data']['videoconference'])
&& !self::isVideoconferenceDisabled()) && !self::isVideoconferenceDisabled())
@ -917,6 +917,8 @@ END:VALARM';
} }
return array(); return array();
} }
if (!isset($params['data']['event_id'])) $params['data']['event_id'] = '';
if (!isset($params['data']['user_id'])) $params['data']['user_id'] = '';
return array( return array(
array( array(
'id' => 'A', 'id' => 'A',

View File

@ -313,7 +313,7 @@ class calendar_uiforms extends calendar_ui
$msg = $this->export($content['id'],true); $msg = $this->export($content['id'],true);
} }
// delete a recur-exception // delete a recur-exception
if ($content['recur_exception']['delete_exception']) if (!empty($content['recur_exception']['delete_exception']))
{ {
$date = key($content['recur_exception']['delete_exception']); $date = key($content['recur_exception']['delete_exception']);
// eT2 converts time to // eT2 converts time to
@ -338,7 +338,7 @@ class calendar_uiforms extends calendar_ui
$update_type = 'edit'; $update_type = 'edit';
} }
// delete an alarm // delete an alarm
if ($content['alarm']['delete_alarm']) if (!empty($content['alarm']['delete_alarm']))
{ {
$id = key($content['alarm']['delete_alarm']); $id = key($content['alarm']['delete_alarm']);
//echo "delete alarm $id"; _debug_array($content['alarm']['delete_alarm']); //echo "delete alarm $id"; _debug_array($content['alarm']['delete_alarm']);
@ -1739,9 +1739,11 @@ class calendar_uiforms extends calendar_ui
$lock_path = Vfs::app_entry_lock_path('calendar',$event['id']); $lock_path = Vfs::app_entry_lock_path('calendar',$event['id']);
$lock_owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; $lock_owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email'];
$scope = 'shared';
$type = 'write';
if (($preserv['lock_token'] = $event['lock_token'])) // already locked --> refresh the lock if (($preserv['lock_token'] = $event['lock_token'])) // already locked --> refresh the lock
{ {
Vfs::lock($lock_path,$preserv['lock_token'],$locktime,$lock_owner,$scope='shared',$type='write',true,false); Vfs::lock($lock_path,$preserv['lock_token'],$locktime,$lock_owner,$scope,$type,true,false);
} }
if (($lock = Vfs::checkLock($lock_path)) && $lock['owner'] != $lock_owner) if (($lock = Vfs::checkLock($lock_path)) && $lock['owner'] != $lock_owner)
{ {
@ -1753,7 +1755,7 @@ class calendar_uiforms extends calendar_ui
{ {
$preserv['lock_token'] = $lock['token']; $preserv['lock_token'] = $lock['token'];
} }
elseif(Vfs::lock($lock_path,$preserv['lock_token'],$locktime,$lock_owner,$scope='shared',$type='write',false,false)) elseif(Vfs::lock($lock_path,$preserv['lock_token'],$locktime,$lock_owner,$scope,$type,false,false))
{ {
//We handle AJAX_REQUEST in client-side for unlocking the locked entry, in case of closing the entry by X button or close button //We handle AJAX_REQUEST in client-side for unlocking the locked entry, in case of closing the entry by X button or close button
} }
@ -2248,7 +2250,7 @@ class calendar_uiforms extends calendar_ui
$readonlys['button[reject]'] = $readonlys['button[cancel]'] = true; $readonlys['button[reject]'] = $readonlys['button[cancel]'] = true;
} }
} }
else elseif (!empty($event['button']))
{ {
//_debug_array($event); //_debug_array($event);
$button = key($event['button']); $button = key($event['button']);
@ -2906,7 +2908,7 @@ class calendar_uiforms extends calendar_ui
{ {
throw new Api\Exception\NoPermission\Admin(); throw new Api\Exception\NoPermission\Admin();
} }
if ($_content) if (!empty($_content['button']))
{ {
$button = key($_content['button']); $button = key($_content['button']);
unset($_content['button']); unset($_content['button']);

View File

@ -93,7 +93,7 @@ class calendar_uilist extends calendar_ui
// handle a single button like actions // handle a single button like actions
foreach(array('delete','timesheet','document') as $button) foreach(array('delete','timesheet','document') as $button)
{ {
if ($_content['nm']['rows'][$button]) if (!empty($_content['nm']['rows'][$button]))
{ {
$id = key($_content['nm']['rows'][$button]); $id = key($_content['nm']['rows'][$button]);
$_content['nm']['action'] = $button; $_content['nm']['action'] = $button;

880
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -253,7 +253,119 @@ Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/123
``` ```
</details> </details>
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources <details>
<summary>Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
{
"fullName": "First Tester",
"name/personal": "First",
"name/surname": "Tester",
"organizations/org/name": "Test Organization",
"emails/work": "test.user@test-user.org",
"addresses/work/locality": "Test-Town",
"addresses/work/postcode": "12345",
"addresses/work/street": "Teststr. 123",
"addresses/work/country": "Germany",
"addresses/work/countryCode": "DE",
"phones/tel_work": "+49 123 4567890",
"online/url": "https://www.example.org/",
"notes/note": "This is a note.",
"egroupware.org:customfields/Test": "Content for Test"
}
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 (requires to specify all attributes!)
<details>
<summary>Example: PUT request to update a resource</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -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 204 No Content
```
</details>
<details>
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -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
```
Update of an existing one:
```
HTTP/1.1 204 No Content
```
New contact:
```
HTTP/1.1 201 Created
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
```
</details>
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
<details>
<summary>Example: PATCH request to modify a contact with partial data</summary>
```
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
{
"name": [
{
"@type": "NameComponent",
"type": "personal",
"value": "Testfirst"
},
{
"@type": "NameComponent",
"type": "surname",
"value": "Username"
}
],
"fullName": "Testfirst Username",
"organizations/org/name": "Test-User.org",
"emails/work/email": "test.user@test-user.org"
}
EOF
HTTP/1.1 204 No content
```
</details>
* **DELETE** requests delete single resources * **DELETE** requests delete single resources
@ -266,3 +378,11 @@ use ```<domain-name>:<name>``` like in JsCalendar
* top-level objects need a ```@type``` attribute with one of the following values: * top-level objects need a ```@type``` attribute with one of the following values:
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```, ```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation``` ```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
### ToDos
- [x] Addressbook
- [ ] update of photos, keys, attachments
- [ ] InfoLog
- [ ] Calendar
- [ ] relatedTo / links
- [ ] storing not native supported attributes eg. localization

View File

@ -171,6 +171,13 @@ class filemanager_hooks
), ),
); );
$settings[Api\Storage\Merge::PREF_STORE_LOCATION] = array(
'type' => 'vfs_dir',
'size' => 60,
'label' => 'Directory for storing merged documents',
'name' => Api\Storage\Merge::PREF_STORE_LOCATION,
'help' => lang('When you merge entries into documents, they will be stored here. If no directory is provided, they will be stored in %1', Vfs::get_home_dir())
);
$settings['default_document'] = array( $settings['default_document'] = array(
'type' => 'vfs_file', 'type' => 'vfs_file',
'size' => 60, 'size' => 60,

View File

@ -106,11 +106,11 @@ class filemanager_select
} }
} }
$content['mime'] = key($sel_options['mime']); $content['mime'] = key($sel_options['mime'] ?? []);
error_log(array2string($content['options-mime'])); error_log(array2string($content['options-mime']));
} }
} }
elseif(isset($content['button'])) elseif(!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
@ -205,7 +205,7 @@ class filemanager_select
$sel_options['mime'] = $content['options-mime']; $sel_options['mime'] = $content['options-mime'];
} }
elseif(isset($content['apps'])) elseif(!empty($content['apps']))
{ {
$app = key($content['apps']); $app = key($content['apps']);
if ($app == 'home') $content['path'] = filemanager_ui::get_home_dir(); if ($app == 'home') $content['path'] = filemanager_ui::get_home_dir();

View File

@ -573,13 +573,11 @@ class filemanager_ui
{ {
$content['nm']['path'] = urldecode($content['nm']['path']); $content['nm']['path'] = urldecode($content['nm']['path']);
} }
if ($content['button']) if (!empty($content['button']))
{
if ($content['button'])
{ {
$button = key($content['button']); $button = key($content['button']);
unset($content['button']); unset($content['button']);
}
switch ($button) switch ($button)
{ {
case 'upload': case 'upload':
@ -1193,7 +1191,7 @@ class filemanager_ui
//_debug_array($content); //_debug_array($content);
$path =& $content['path']; $path =& $content['path'];
$button = @key($content['button']); $button = @key($content['button'] ?? []);
unset($content['button']); unset($content['button']);
if(!$button && $content['sudo'] && $content['sudouser']) if(!$button && $content['sudo'] && $content['sudouser'])
{ {
@ -1347,9 +1345,9 @@ class filemanager_ui
} }
} }
} }
elseif ($content['eacl'] && $content['is_owner']) elseif (!empty($content['eacl']) && !empty($content['is_owner']))
{ {
if ($content['eacl']['delete']) if (!empty($content['eacl']['delete']))
{ {
$ino_owner = key($content['eacl']['delete']); $ino_owner = key($content['eacl']['delete']);
list(, $owner) = explode('-',$ino_owner,2); // $owner is a group and starts with a minus! list(, $owner) = explode('-',$ino_owner,2); // $owner is a group and starts with a minus!

View File

@ -94,7 +94,7 @@ class infolog_customfields extends admin_customfields
$create = $fields['create']; $create = $fields['create'];
unset($fields['create']); unset($fields['create']);
if ($fields['delete']) if (!empty($fields['delete']))
{ {
$delete = key($fields['delete']); $delete = key($fields['delete']);
unset($fields['delete']); unset($fields['delete']);
@ -178,7 +178,7 @@ class infolog_customfields extends admin_customfields
$create = $status['create']; $create = $status['create'];
unset($status['create']); unset($status['create']);
if ($status['delete']) if (!empty($status['delete']))
{ {
$delete = key($status['delete']); $delete = key($status['delete']);
unset($status['delete']); unset($status['delete']);
@ -276,7 +276,7 @@ class infolog_customfields extends admin_customfields
unset($this->status[$content['type2']]); unset($this->status[$content['type2']]);
unset($this->status['defaults'][$content['type2']]); unset($this->status['defaults'][$content['type2']]);
unset($this->group_owners[$content['type2']]); unset($this->group_owners[$content['type2']]);
$content['type2'] = key($this->content_types); $content['type2'] = key($this->content_types ?? []);
// save changes to repository // save changes to repository
$this->save_repository(); $this->save_repository();

View File

@ -116,7 +116,7 @@ class infolog_favorite_portlet extends home_favorite_portlet
{ {
$popup =& $values; $popup =& $values;
} }
$values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action']); $values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action'] ?? []);
if($multi_action == 'link') if($multi_action == 'link')
{ {
$popup[$multi_action] = $popup['link']['app'] . ':'.$popup['link']['id']; $popup[$multi_action] = $popup['link']['app'] . ':'.$popup['link']['id'];

View File

@ -483,6 +483,16 @@ class infolog_hooks
'admin' => False, 'admin' => False,
'default' => '/templates/infolog', 'default' => '/templates/infolog',
); );
$settings[Api\Storage\Merge::PREF_DOCUMENT_FILENAME] = array(
'type' => 'taglist',
'label' => 'Document download filename',
'name' => 'document_download_name',
'values' => Api\Storage\Merge::DOCUMENT_FILENAME_OPTIONS,
'help' => 'Choose the default filename for downloaded documents.',
'xmlrpc' => True,
'admin' => False,
'default' => 'document',
);
} }
if ($GLOBALS['egw_info']['user']['apps']['calendar']) if ($GLOBALS['egw_info']['user']['apps']['calendar'])

View File

@ -73,6 +73,25 @@ class infolog_merge extends Api\Storage\Merge
return $replacements; return $replacements;
} }
/**
* Override contact filename placeholder to use info_contact
*
* @param $document
* @param $ids
* @return array|void
*/
public function get_filename_placeholders($document, $ids)
{
$placeholders = parent::get_filename_placeholders($document, $ids);
if(count($ids) == 1 && ($info = $this->bo->read($ids[0])))
{
$placeholders['$$contact_title$$'] = $info['info_contact']['title'] ??
(is_array($info['info_contact']) && Link::title($info['info_contact']['app'], $info['info_contact']['id'])) ??
'';
}
return $placeholders;
}
/** /**
* Get infolog replacements * Get infolog replacements
* *
@ -260,4 +279,46 @@ class infolog_merge extends Api\Storage\Merge
echo $GLOBALS['egw']->framework->footer(); echo $GLOBALS['egw']->framework->footer();
} }
public function get_placeholder_list($prefix = '')
{
$placeholders = parent::get_placeholder_list($prefix);
$tracking = new infolog_tracking($this->bo);
$fields = array('info_id' => lang('Infolog ID'), 'pm_id' => lang('Project ID'),
'project' => lang('Project name')) + $tracking->field2label + array('info_sum_timesheets' => lang('Used time'));
Api\Translation::add_app('projectmanager');
$group = 'placeholders';
foreach($fields as $name => $label)
{
if(in_array($name, array('custom')))
{
// dont show them
continue;
}
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
}
// Add contact placeholders
$insert_index = 1;
$placeholders = array_slice($placeholders, 0, $insert_index, true) +
[lang($tracking->field2label['info_from']) => []] +
array_slice($placeholders, $insert_index, count($placeholders) - $insert_index, true);
$contact_merge = new Api\Contacts\Merge();
$contact = $contact_merge->get_placeholder_list('info_contact');
$this->add_linked_placeholders($placeholders, lang($tracking->field2label['info_from']), $contact);
return $placeholders;
}
} }

View File

@ -608,7 +608,7 @@ class infolog_ui
} }
if(count($links)) if(count($links))
{ {
$query['col_filter']['info_id'] = count($links) > 1 ? call_user_func_array('array_intersect', $links) : $links[$key ?? 'info_id']; $query['col_filter']['info_id'] = count($links) > 1 ? call_user_func_array('array_intersect', array_values($links)) : $links[$key ?? 'info_id'];
} }
return $linked; return $linked;
} }
@ -831,7 +831,7 @@ class infolog_ui
{ {
$popup =& $values; $popup =& $values;
} }
$values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action']); $values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action'] ?? []);
if($multi_action == 'link') if($multi_action == 'link')
{ {
$popup[$multi_action] = $popup['link']['app'] . ':'.$popup['link']['id']; $popup[$multi_action] = $popup['link']['app'] . ':'.$popup['link']['id'];
@ -2498,7 +2498,7 @@ class infolog_ui
if($content) if($content)
{ {
// Save // Save
$button = key($content['button']); $button = key($content['button'] ?? []);
if($button == 'save' || $button == 'apply') if($button == 'save' || $button == 'apply')
{ {
$this->bo->responsible_edit = array('info_status','info_percent','info_datecompleted'); $this->bo->responsible_edit = array('info_status','info_percent','info_datecompleted');

View File

@ -551,7 +551,7 @@ class mail_compose
} }
if ($sendOK) if ($sendOK)
{ {
$workingFolder = $activeFolder; $workingFolder = $activeFolder['mailbox'];
$mode = 'compose'; $mode = 'compose';
$idsForRefresh = array(); $idsForRefresh = array();
if (isset($_content['mode']) && !empty($_content['mode'])) if (isset($_content['mode']) && !empty($_content['mode']))

View File

@ -145,7 +145,7 @@ class mail_tree
*/ */
private static function isAccountNode ($_node) private static function isAccountNode ($_node)
{ {
list(,$leaf) = explode(self::DELIMITER, $_node); list(,$leaf) = explode(self::DELIMITER, $_node)+[null,null];
if ($leaf || $_node == null) return false; if ($leaf || $_node == null) return false;
return true; return true;
} }
@ -404,7 +404,7 @@ class mail_tree
} }
$parents[] = $component; $parents[] = $component;
} }
if ($data['folderarray']['delimiter'] && $data['folderarray']['MAILBOX']) if (!empty($data['folderarray']['delimiter']) && !empty($data['folderarray']['MAILBOX']))
{ {
$path = explode($data['folderarray']['delimiter'], $data['folderarray']['MAILBOX']); $path = explode($data['folderarray']['delimiter'], $data['folderarray']['MAILBOX']);
$folderName = array_pop($path); $folderName = array_pop($path);
@ -428,7 +428,7 @@ class mail_tree
$data[Tree::IMAGE_FOLDER_OPEN] = $data[Tree::IMAGE_FOLDER_OPEN] =
$data [Tree::IMAGE_FOLDER_CLOSED] = basename(Api\Image::find('mail', 'dhtmlxtree/'."MailFolder".$key)); $data [Tree::IMAGE_FOLDER_CLOSED] = basename(Api\Image::find('mail', 'dhtmlxtree/'."MailFolder".$key));
} }
elseif(stripos(array2string($data['folderarray']['attributes']),'\noselect')!== false) elseif(!empty($data['folderarray']['attributes']) && stripos(array2string($data['folderarray']['attributes']),'\noselect') !== false)
{ {
$data[Tree::IMAGE_LEAF] = self::$leafImages['folderNoSelectClosed']; $data[Tree::IMAGE_LEAF] = self::$leafImages['folderNoSelectClosed'];
$data[Tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderNoSelectOpen']; $data[Tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderNoSelectOpen'];
@ -444,7 +444,7 @@ class mail_tree
} }
// Contains unseen mails for the folder // Contains unseen mails for the folder
$unseen = $data['folderarray']['counter']['UNSEEN']; $unseen = $data['folderarray']['counter']['UNSEEN'] ?? 0;
// if there's unseen mails then change the label and style // if there's unseen mails then change the label and style
// accordingly to indicate useen mails // accordingly to indicate useen mails
@ -477,7 +477,7 @@ class mail_tree
foreach(Mail\Account::search(true, false) as $acc_id => $accObj) foreach(Mail\Account::search(true, false) as $acc_id => $accObj)
{ {
if (!$accObj->is_imap()|| $_profileID && $acc_id != $_profileID) continue; if (!$accObj->is_imap()|| $_profileID && $acc_id != $_profileID) continue;
$identity = self::getIdentityName(Mail\Account::identity_name($accObj,true,$GLOBALS['egw_info']['user']['acount_id'], true)); $identity = self::getIdentityName(Mail\Account::identity_name($accObj,true, $GLOBALS['egw_info']['user']['account_id'], true));
// Open top level folders for active account // Open top level folders for active account
$openActiveAccount = $GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'] == $acc_id?1:0; $openActiveAccount = $GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'] == $acc_id?1:0;

View File

@ -2445,10 +2445,29 @@ app.classes.mail = AppJS.extend(
// we can NOT query global object manager for this.nm_index="nm", as we might not get the one from mail, // we can NOT query global object manager for this.nm_index="nm", as we might not get the one from mail,
// if other tabs are open, we have to query for obj_manager for "mail" and then it's child with id "nm" // if other tabs are open, we have to query for obj_manager for "mail" and then it's child with id "nm"
var obj_manager = egw_getObjectManager(this.appname).getObjectById(this.nm_index); var obj_manager = egw_getObjectManager(this.appname).getObjectById(this.nm_index);
let tree = this.et2.getWidgetById('nm[foldertree]');
var that = this; var that = this;
var rvMain = false; var rvMain = false;
if ((obj_manager && _elems.length>1 && obj_manager.getAllSelected() && !_action.paste) || _action.id=='readall') if ((obj_manager && _elems.length>1 && obj_manager.getAllSelected() && !_action.paste) || _action.id=='readall')
{ {
try {
let splitedID = [];
let mailbox = '';
// Avoid possibly doing select all action on not desired mailbox e.g. INBOX
for (let n=0;n<_elems.length;n++)
{
splitedID = _elems[n].id.split("::");
// find the mailbox from the constructed rowID, sometimes the rowID may not contain the app name
mailbox = splitedID.length == 4?atob(splitedID[2]):atob(splitedID[3]);
// drop the action if there's a mixedup mailbox found in the selected messages
if (mailbox != tree.getSelectedNode().id.split("::")[1]) return;
}
}catch(e)
{
// continue
}
if (_confirm) if (_confirm)
{ {
var buttons = [ var buttons = [

View File

@ -68,7 +68,7 @@ class preferences_settings
{ {
$is_admin = $content['is_admin'] || $content['type'] != 'user'; $is_admin = $content['is_admin'] || $content['type'] != 'user';
//error_log(__METHOD__."(".array2string($content).")"); //error_log(__METHOD__."(".array2string($content).")");
if ($content['button']) if (!empty($content['button']))
{ {
$button = key($content['button']); $button = key($content['button']);
$appname = $content['old_appname'] ? $content['old_appname'] : 'common'; $appname = $content['old_appname'] ? $content['old_appname'] : 'common';

View File

@ -176,7 +176,7 @@ class resources_acl_ui
$content = array('data' => array()); $content = array('data' => array());
} }
} }
elseif ($content['button']) elseif (!empty($content['button']))
{ {
$cats = new Categories($content['owner'] ? $content['owner'] : Categories::GLOBAL_ACCOUNT,'resources'); $cats = new Categories($content['owner'] ? $content['owner'] : Categories::GLOBAL_ACCOUNT,'resources');

View File

@ -32,7 +32,8 @@ class resources_reserve {
$display_days = $_GET['planner_days'] ? $_GET['planner_days'] : 3; $display_days = $_GET['planner_days'] ? $_GET['planner_days'] : 3;
$planner_date = $_GET['date'] ? $_GET['date'] : strtotime('yesterday',$content['date'] ? $content['date'] : time()); $planner_date = $_GET['date'] ? $_GET['date'] : strtotime('yesterday',$content['date'] ? $content['date'] : time());
if($_GET['confirm']) { if(!empty($_GET['confirm']))
{
$register_code = ($_GET['confirm'] && preg_match('/^[0-9a-f]{32}$/',$_GET['confirm'])) ? $_GET['confirm'] : false; $register_code = ($_GET['confirm'] && preg_match('/^[0-9a-f]{32}$/',$_GET['confirm'])) ? $_GET['confirm'] : false;
if($register_code && $registration = registration_bo::confirm($register_code)) { if($register_code && $registration = registration_bo::confirm($register_code)) {
// Get calendar through link // Get calendar through link
@ -48,14 +49,16 @@ class resources_reserve {
$planner_date = mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start'])); $planner_date = mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start']));
$readonlys['__ALL__'] = true; $readonlys['__ALL__'] = true;
$content = array( $content = array(
'resource' => key($data['participant_types']['r']), 'resource' => key($data['participant_types']['r'] ?? []),
'date' => $data['start'], 'date' => $data['start'],
'time' => $data['start'] - mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start'])), 'time' => $data['start'] - mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start'])),
'quantity' => 0 'quantity' => 0
); );
calendar_so::split_status($data['participant_types']['r'][$content['resource']], $content['quantity'],$role); calendar_so::split_status($data['participant_types']['r'][$content['resource']], $content['quantity'],$role);
$data['msg']= '<div class="confirm">'.lang('Registration confirmed %1', Api\DateTime::to($data['start'])) .'</div>'; $data['msg']= '<div class="confirm">'.lang('Registration confirmed %1', Api\DateTime::to($data['start'])) .'</div>';
} else { }
else
{
$data['msg']= '<div class="confirm">'.lang('Unable to process confirmation.').'</div>'; $data['msg']= '<div class="confirm">'.lang('Unable to process confirmation.').'</div>';
} }
} }

View File

@ -11,14 +11,21 @@
import path from 'path'; import path from 'path';
import babel from '@babel/core'; import babel from '@babel/core';
import { readFileSync, readdirSync, statSync } from "fs"; import { readFileSync, readdirSync, statSync, unlinkSync } from "fs";
import rimraf from 'rimraf'; //import rimraf from 'rimraf';
import { minify } from 'terser'; import { minify } from 'terser';
import resolve from '@rollup/plugin-node-resolve'; import resolve from '@rollup/plugin-node-resolve';
// Best practice: use this // Best practice: use this
//rimraf.sync('./dist/'); //rimraf.sync('./dist/');
rimraf.sync('./chunks/'); //rimraf.sync('./chunks/');
// remove only chunks older then 2 days, to allow UI to still load them and not require a reload / F5
const rm_older = Date.now() - 48*3600000;
readdirSync('./chunks').forEach(name => {
const stat = statSync('./chunks/'+name);
if (stat.atimeMs < rm_older) unlinkSync('./chunks/'+name);
});
// Turn on minification // Turn on minification
const do_minify = false; const do_minify = false;

View File

@ -38,7 +38,7 @@ $db_backup = new Api\Db\Backup();
$asyncservice = new Api\Asyncservice(); $asyncservice = new Api\Asyncservice();
// download a backup, has to be before any output !!! // download a backup, has to be before any output !!!
if ($_POST['download']) if (!empty($_POST['download']))
{ {
$file = key($_POST['download']); $file = key($_POST['download']);
$file = $db_backup->backup_dir.'/'.basename($file); // basename to now allow to change the dir $file = $db_backup->backup_dir.'/'.basename($file); // basename to now allow to change the dir
@ -166,7 +166,7 @@ if ($_POST['upload'] && is_array($_FILES['uploaded']) && !$_FILES['uploaded']['e
sprintf('%3.1f MB (%d)',$_FILES['uploaded']['size']/(1024*1024),$_FILES['uploaded']['size']).$md5)); sprintf('%3.1f MB (%d)',$_FILES['uploaded']['size']/(1024*1024),$_FILES['uploaded']['size']).$md5));
} }
// delete a backup // delete a backup
if ($_POST['delete']) if (!empty($_POST['delete']))
{ {
$file = key($_POST['delete']); $file = key($_POST['delete']);
$file = $db_backup->backup_dir.'/'.basename($file); // basename to not allow to change the dir $file = $db_backup->backup_dir.'/'.basename($file); // basename to not allow to change the dir
@ -174,7 +174,7 @@ if ($_POST['delete'])
if (unlink($file)) $setup_tpl->set_var('error_msg',lang("backup '%1' deleted",$file)); if (unlink($file)) $setup_tpl->set_var('error_msg',lang("backup '%1' deleted",$file));
} }
// rename a backup // rename a backup
if ($_POST['rename']) if (!empty($_POST['rename']))
{ {
$file = key($_POST['rename']); $file = key($_POST['rename']);
$new_name = $_POST['new_name'][$file]; $new_name = $_POST['new_name'][$file];
@ -190,7 +190,7 @@ if ($_POST['rename'])
} }
} }
// restore a backup // restore a backup
if ($_POST['restore']) if (!empty($_POST['restore']))
{ {
$file = key($_POST['restore']); $file = key($_POST['restore']);
$file = $db_backup->backup_dir.'/'.basename($file); // basename to not allow to change the dir $file = $db_backup->backup_dir.'/'.basename($file); // basename to not allow to change the dir
@ -217,12 +217,12 @@ if ($_POST['restore'])
} }
} }
// create a new scheduled backup // create a new scheduled backup
if ($_POST['schedule']) if (!empty($_POST['schedule']))
{ {
$asyncservice->set_timer($_POST['times'],'db_backup-'.implode(':',$_POST['times']),'admin.admin_db_backup.do_backup',''); $asyncservice->set_timer($_POST['times'],'db_backup-'.implode(':',$_POST['times']),'admin.admin_db_backup.do_backup','');
} }
// cancel a scheduled backup // cancel a scheduled backup
if (is_array($_POST['cancel'])) if (!empty($_POST['cancel']) && is_array($_POST['cancel']))
{ {
$id = key($_POST['cancel']); $id = key($_POST['cancel']);
$asyncservice->cancel_timer($id); $asyncservice->cancel_timer($id);

View File

@ -203,6 +203,16 @@ class timesheet_hooks
'admin' => False, 'admin' => False,
'default' => '/templates/timesheet', 'default' => '/templates/timesheet',
); );
$settings[Api\Storage\Merge::PREF_DOCUMENT_FILENAME] = array(
'type' => 'taglist',
'label' => 'Document download filename',
'name' => 'document_download_name',
'values' => Api\Storage\Merge::DOCUMENT_FILENAME_OPTIONS,
'help' => 'Choose the default filename for downloaded documents.',
'xmlrpc' => True,
'admin' => False,
'default' => 'document',
);
} }
return $settings; return $settings;

View File

@ -217,4 +217,49 @@ class timesheet_merge extends Api\Storage\Merge
echo $GLOBALS['egw']->framework->footer(); echo $GLOBALS['egw']->framework->footer();
} }
/**
* 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 = array(
'timesheet' => [],
lang('Project') => []
) + parent::get_placeholder_list($prefix);
$fields = array('ts_id' => lang('Timesheet ID')) + $this->bo->field2label + array(
'ts_total' => lang('total'),
'ts_created' => lang('Created'),
'ts_modified' => lang('Modified'),
);
$group = 'timesheet';
foreach($fields as $name => $label)
{
if(in_array($name, array('custom')))
{
// dont show them
continue;
}
$marker = $this->prefix($prefix, $name, '{');
if(!array_filter($placeholders, function ($a) use ($marker)
{
return array_key_exists($marker, $a);
}))
{
$placeholders[$group][] = [
'value' => $marker,
'label' => $label
];
}
}
// Add project placeholders
$pm_merge = new projectmanager_merge();
$this->add_linked_placeholders($placeholders, lang('Project'), $pm_merge->get_placeholder_list('ts_project'));
return $placeholders;
}
} }

View File

@ -80,26 +80,6 @@ class timesheet_tracking extends Api\Storage\Tracking
parent::__construct('timesheet'); parent::__construct('timesheet');
} }
/**
* Get a notification-config value
*
* @param string $name
* - 'copy' array of email addresses notifications should be copied too, can depend on $data
* - 'lang' string lang code for copy mail
* - 'sender' string send email address
* @param array $data current entry
* @param array $old =null old/last state of the entry or null for a new entry
* @return mixed
*/
function get_config($name,$data,$old=null)
{
$timesheet = $data['ts_id'];
//$config = $this->timesheet->notification[$timesheet][$name] ? $this->timesheet->notification[$timesheet][$name] : $this->$timesheet->notification[0][$name];
//no nitify configert (ToDo)
return $config;
}
/** /**
* Get the subject for a given entry, reimplementation for get_subject in Api\Storage\Tracking * Get the subject for a given entry, reimplementation for get_subject in Api\Storage\Tracking
* *
@ -107,9 +87,11 @@ class timesheet_tracking extends Api\Storage\Tracking
* *
* @param array $data * @param array $data
* @param array $old * @param array $old
* @param boolean $deleted =null can be set to true to let the tracking know the item got deleted or undeleted
* @param int|string $receiver numeric account_id or email address
* @return string * @return string
*/ */
function get_subject($data,$old) protected function get_subject($data,$old,$deleted=null,$receiver=null)
{ {
return '#'.$data['ts_id'].' - '.$data['ts_title']; return '#'.$data['ts_id'].' - '.$data['ts_title'];
} }
@ -119,9 +101,10 @@ class timesheet_tracking extends Api\Storage\Tracking
* *
* @param array $data * @param array $data
* @param array $old * @param array $old
* @param int|string $receiver nummeric account_id or email address
* @return string * @return string
*/ */
function get_message($data,$old) protected function get_message($data,$old,$receiver=null)
{ {
if (!$data['ts_modified'] || !$old) if (!$data['ts_modified'] || !$old)
{ {

View File

@ -879,8 +879,8 @@ class timesheet_ui extends timesheet_bo
{ {
$etpl = new Etemplate('timesheet.index'); $etpl = new Etemplate('timesheet.index');
if ($_GET['msg']) $msg = $_GET['msg']; if (!empty($_GET['msg'])) $msg = $_GET['msg'];
if ($content['nm']['rows']['delete']) if (!empty($content['nm']['rows']['delete']))
{ {
$ts_id = key($content['nm']['rows']['delete']); $ts_id = key($content['nm']['rows']['delete']);
if ($this->delete($ts_id)) if ($this->delete($ts_id))
@ -892,13 +892,13 @@ class timesheet_ui extends timesheet_bo
$msg = lang('Error deleting the entry!!!'); $msg = lang('Error deleting the entry!!!');
} }
} }
if (is_array($content) && isset($content['nm']['rows']['document'])) // handle insert in default document button like an action if (is_array($content) && !empty($content['nm']['rows']['document'])) // handle insert in default document button like an action
{ {
$id = @key($content['nm']['rows']['document']); $id = @key($content['nm']['rows']['document']);
$content['nm']['action'] = 'document'; $content['nm']['action'] = 'document';
$content['nm']['selected'] = array($id); $content['nm']['selected'] = array($id);
} }
if ($content['nm']['action']) if (!empty($content['nm']['action']))
{ {
// remove sum-* rows from checked rows // remove sum-* rows from checked rows
$content['nm']['selected'] = array_filter($content['nm']['selected'], function($id) $content['nm']['selected'] = array_filter($content['nm']['selected'], function($id)
@ -1309,7 +1309,7 @@ class timesheet_ui extends timesheet_bo
$GLOBALS['egw']->redirect_link('/admin/index.php', null, 'admin'); $GLOBALS['egw']->redirect_link('/admin/index.php', null, 'admin');
} }
} }
if (isset($content['statis']['delete'])) if (!empty($content['statis']['delete']))
{ {
$id = key($content['statis']['delete']); $id = key($content['statis']['delete']);
if (isset($this->status_labels_config[$id])) if (isset($this->status_labels_config[$id]))