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 $user =null account_id of owner, default null
* @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')
*/
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)");
$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 ($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 ?
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)
{
@ -757,10 +760,10 @@ class addressbook_groupdav extends Api\CalDAV\Handler
}
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!)
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;
}
@ -1105,6 +1108,11 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$keys['id'] = $id;
}
}
// json with uid
elseif (empty(self::$path_extension) && (string)$id !== (string)(int)$id)
{
$keys['uid'] = $id;
}
else
{
$keys[self::$path_attr] = $id;

View File

@ -304,17 +304,27 @@ class addressbook_hooks
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert contacts',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.',lang('addressbook')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','n_fn').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert contacts',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.', lang('addressbook')) . ' ' .
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'n_fn') . ' ' .
lang('The following document-types are supported:') . implode(',', Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/addressbook',
'xmlrpc' => True,
'admin' => False,
'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',
);
}

View File

@ -183,12 +183,12 @@ class addressbook_ui extends addressbook_bo
$msg = '';
}
}
if ($_content['nm']['rows']['infolog'])
if (!empty($_content['nm']['rows']['infolog']))
{
$org = key($_content['nm']['rows']['infolog']);
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']);
}
@ -1736,9 +1736,13 @@ class addressbook_ui extends addressbook_bo
if (isset($this->grouped_views[(string) $query['grouped_view']]))
{
// 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'];
}
@ -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)
$this->check_shared_with($content['shared']);
$button = @key($content['button']);
$button = @key($content['button'] ?? []);
unset($content['button']);
$content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p');
$content['owner'] = (string) (int) $content['owner'];
@ -2984,7 +2988,7 @@ class addressbook_ui extends addressbook_bo
if(is_array($content))
{
$button = is_array($content['button']) ? key($content['button']) : "";
$button = key($content['button'] ?? []);
switch ($button)
{
case 'vcard':
@ -3067,7 +3071,7 @@ class addressbook_ui extends addressbook_bo
$_GET['contact_id'] = array_shift($rows);
$_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)))
{
Egw::redirect_link('/index.php',array(

View File

@ -139,7 +139,7 @@ class admin_categories
$button = 'delete';
$delete_subs = $content['delete']['subs']?true:false;
}
else
elseif (!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -564,7 +564,7 @@ class admin_categories
{
$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]))
{
@ -680,7 +680,7 @@ class admin_categories
{
$cmd = new admin_cmd_delete_category(
$cat_id,
key($content['button']) == 'delete_sub',
key($content['button'] ?? []) == 'delete_sub',
$content['admin_cmd']
);
$cmd->run();

View File

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

View File

@ -212,7 +212,7 @@ class admin_mail
public function autoconfig(array $content)
{
// 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']);
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)
{
if (isset($content['button']))
if (!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -482,7 +482,7 @@ class admin_mail
);
$content['msg'] = $msg;
if (isset($content['button']))
if (!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -619,7 +619,7 @@ class admin_mail
);
$content['msg'] = $msg;
if (isset($content['button']))
if (!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -835,7 +835,7 @@ class admin_mail
{
$content['called_for'] = (int)$_GET['account_id'];
$content['accounts'] = iterator_to_array(Mail\Account::search($content['called_for']));
if ($content['accounts'])
if (!empty($content['accounts']))
{
$content['acc_id'] = key($content['accounts']);
//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_use_default', !$is_multiple);
if (isset($content['button']))
if (!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -1031,7 +1031,7 @@ class admin_mail
unset($content['smimeKeyUpload']);
}
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']);
self::fix_account_id_0($content['account_id']);
$msg = lang('Account saved.');

View File

@ -204,7 +204,7 @@ class admin_ui
$item['id'] = substr($item['extradata'], 11);
unset($item['extradata']);
$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];
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['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';
if (!isset($item['allowOnMultiple'])) $item['allowOnMultiple'] = false;
@ -297,7 +297,7 @@ class admin_ui
$item['id'] = substr($item['extradata'], 11);
unset($item['extradata']);
$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['onExecute'] = 'javaScript:nm_action';
@ -326,7 +326,7 @@ class admin_ui
public static function get_users(array $query, array &$rows=null)
{
$params = array(
'type' => (int)$query['filter'] ? (int)$query['filter'] : 'accounts',
'type' => (int)($query['filter'] ?? 0) ?: 'accounts',
'start' => $query['start'],
'offset' => $query['num_rows'],
'order' => $query['order'],
@ -334,7 +334,7 @@ class admin_ui
'active' => !empty($query['active']) ? $query['active'] : false,
);
// Make sure active filter give status what it needs
switch($query['filter2'])
switch($query['filter2'] ?? '')
{
case 'disabled':
case 'expired':
@ -356,12 +356,12 @@ class admin_ui
break;
}
if ($query['searchletter'])
if (!empty($query['searchletter']))
{
$params['query'] = $query['searchletter'];
$params['query_type'] = 'start';
}
elseif($query['search'])
elseif(!empty($query['search']))
{
$params['query'] = $query['search'];
$params['query_type'] = 'all';
@ -377,7 +377,7 @@ class admin_ui
foreach($rows as $key => &$row)
{
// 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]);
$total--;
@ -391,8 +391,8 @@ class admin_ui
if (!self::$accounts->is_active($row)) $row['status_class'] = 'adminAccountInactive';
}
// finally limit query, if status filter was used
if ($need_status_filter)
// finally, limit query, if status filter was used
if (!empty($need_status_filter))
{
$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(
'type' => 'groups',
'query' => $query['search'],
'order' => $query['order'],
'sort' => $query['sort'],
'query' => $query['search'] ?? null,
'order' => $query['order'] ?? null,
'sort' => $query['sort'] ?? null,
'start' => (int)$query['start'],
'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);
foreach($apps as $app)
{
if((boolean)$run_rights[$app])
if(!empty($run_rights[$app]))
{
$group['apps'][] = $app;
}
@ -537,7 +537,7 @@ class admin_ui
if (!empty($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;
}

View File

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

View File

@ -87,6 +87,12 @@ export class et2_placeholder_select extends et2_inputWidget
[],
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);
et2_placeholder_select.placeholders = _content;
callback.apply(self, arguments);
@ -132,7 +138,13 @@ export class et2_placeholder_select extends et2_inputWidget
let data = {
content: {app: '', group: '', entry: {}},
sel_options: {app: [], group: []},
modifications: {outer_box: {entry: {}}}
modifications: {
outer_box: {
entry: {
application_list: []
}
}
}
};
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.content.app = data.sel_options.app[0].value;
data.content.group = data.sel_options.group[0].value;
data.content.entry = data.modifications.outer_box.entry.only_app = data.content.app;
data.content.group = data.sel_options.group[0]?.value;
data.content.entry = {app: data.content.app};
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
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",
{
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,
minWidth: 500,
minHeight: 400,
@ -207,14 +226,35 @@ export class et2_placeholder_select extends et2_inputWidget
// Bind some handlers
app.onchange = (node, widget) =>
{
group.set_select_options(this._get_group_options(widget.get_value()));
entry.set_value({app: widget.get_value()});
preview.set_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) =>
{
console.log(this, arguments);
placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value()));
let options = this._get_placeholders(app.get_value(), group.get_value())
placeholder_list.set_select_options(options);
preview.set_value("");
placeholder_list.set_value(options[0].value);
}
placeholder_list.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._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
this.egw().json(
'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)
{
if(!_content)
{
_content = '';
}
preview_content.set_value(_content);
preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden';
}.bind(this)
@ -277,11 +321,37 @@ export class et2_placeholder_select extends et2_inputWidget
let options = [];
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,
label: this.egw().lang(key)
});
}
});
return options;
}
@ -295,16 +365,13 @@ export class et2_placeholder_select extends et2_inputWidget
*/
_get_placeholders(appname : string, group : string)
{
let options = [];
Object.keys(et2_placeholder_select.placeholders[appname][group]).map((key) =>
let _group = group.split('-', 2);
let ph = et2_placeholder_select.placeholders[appname];
for(let i = 0; typeof ph !== "undefined" && i < _group.length; i++)
{
options.push(
{
value: key,
label: et2_placeholder_select.placeholders[appname][group][key]
});
});
return options;
ph = ph[_group[i]];
}
return ph || [];
}
/**
@ -342,8 +409,9 @@ export class et2_placeholder_snippet_select extends et2_placeholder_select
static placeholders = {
"addressbook": {
"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{{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);
entry.onchange = this._on_placeholder_select.bind(this);
app.set_value(app.get_value());
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
this.egw().json(
'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)
{
if(!_content)
{
_content = '';
}
this.set_value(_content);
preview_content.set_value(_content);
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(
{
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;

View File

@ -991,7 +991,8 @@ export class et2_selectbox extends et2_inputWidget
if(sub == 'value') continue;
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]["title"] ? _options[key][sub]["title"] : "",
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
*
@ -881,17 +913,7 @@ export class etemplate2
let invalid = null;
if (!no_validation)
{
container.iterateOver(function (_widget)
{
if (_widget.submit(values) === false)
{
if (!invalid && !_widget.isValid())
{
invalid = _widget;
}
canSubmit = false;
}
}, this, et2_ISubmitListener);
canSubmit = !(invalid = this.isInvalid(container, values));
}
if (canSubmit)
@ -1098,7 +1120,6 @@ export class etemplate2
return result;
}
/**
* "Intelligently" refresh the template based on the given ID
*

View File

@ -725,7 +725,7 @@ export abstract class EgwApp
framework.pushState('view');
if(templateName)
{
this.et2_view.load(this.appname+'.'+templateName,templateURL, data, typeof et2_callback == 'function'?et2_callback:function(){}, app);
this.et2_view.load(this.appname + '.' + templateName, templateURL, data, typeof et2_callback == 'function' ? et2_callback : function() {}, app);
}
// define a global close function for view template
@ -733,6 +733,49 @@ export abstract class EgwApp
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)
*
@ -741,12 +784,12 @@ export abstract class EgwApp
_init_sidebox(sidebox)
{
// Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object
if (!this.egw.is_popup())
if(!this.egw.is_popup())
{
var egw_fw = egw_getFramework();
var tutorial = jQuery('#egw_tutorial_'+this.appname+'_sidebox', egw_fw ? egw_fw.sidemenuDiv : document);
var tutorial = jQuery('#egw_tutorial_' + this.appname + '_sidebox', egw_fw ? egw_fw.sidemenuDiv : document);
// _init_sidebox gets currently called multiple times, which needs to be fixed
if (tutorial.length && !this.tutorial_initialised)
if(tutorial.length && !this.tutorial_initialised)
{
this.egwTutorial_init(tutorial[0]);
this.tutorial_initialised = true;

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 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 placeholder common de Platzhalter einfügen
insert row after common de Zeile danach 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
@ -1072,6 +1073,7 @@ preference common de Einstellung
preferences common de Einstellungen
preferences for the %1 template set preferences de Einstellungen für das %1 Template
prev common de Vorheriger
preview with entry common de Vorschau aus Eintrag
previous common de Vorherige
previous page common de Vorherige Seite
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 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 placeholder common en Insert placeholder
insert row after common en Insert row after
insert row before common en Insert row before
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 for the %1 template set preferences en Preferences for the %1 template set
prev common en Prev
preview with entry common en Preview with entry
previous common en Previous
previous page common en Previous page
primary group common en Primary group

View File

@ -223,7 +223,7 @@ class Accounts
if (!empty($param['offset']) && !isset($param['start'])) $param['start'] = 0;
// 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 && !(
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
*
* @param string $account_id =null account id
* @return string full name of user or "#$accountid" if user not found
* @param int $account_id account id
* @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;
}

View File

@ -993,6 +993,34 @@ class CalDAV extends HTTP_WebDAV_Server
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
*
@ -1003,7 +1031,7 @@ class CalDAV extends HTTP_WebDAV_Server
{
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'];
}
return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ?
@ -1427,7 +1455,7 @@ class CalDAV extends HTTP_WebDAV_Server
substr($options['path'], -1) === '/' && self::isJSON())
{
$_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).')');
@ -1915,7 +1943,7 @@ class CalDAV extends HTTP_WebDAV_Server
* @param array parameter passing array
* @return bool true on success
*/
function PUT(&$options)
function PUT(&$options, $method='PUT')
{
// read the content in a string, if a stream is given
if (isset($options['stream']))
@ -1934,9 +1962,14 @@ class CalDAV extends HTTP_WebDAV_Server
{
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)))
{
$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
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(
'GET' => Api\Acl::READ,
'PUT' => Api\Acl::EDIT,
'PATCH' => Api\Acl::EDIT,
'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'],
)));
while ($fileas[0] == ':' || $fileas[0] == ',')
while (!empty($fileas) && ($fileas[0] == ':' || $fileas[0] == ','))
{
$fileas = substr($fileas,2);
}
@ -764,10 +764,10 @@ class Contacts extends Contacts\Storage
$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
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'))
{
@ -1686,7 +1686,7 @@ class Contacts extends Contacts\Storage
{
$result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]);
// 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[] = $result[$contact['id']];

View File

@ -78,16 +78,34 @@ class JsContact
/**
* 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 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
*/
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
{
$strict = !isset($content_type) || !preg_match('#^application/json#', $content_type);
$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
$contact = [];
@ -96,53 +114,72 @@ class JsContact
switch ($name)
{
case 'uid':
$contact['uid'] = self::parseUid($value);
$contact['uid'] = self::parseUid($value, $old['uid'], !$strict);
break;
case 'name':
$contact += self::parseNameComponents($value, $check_at_type);
$contact += self::parseNameComponents($value, $strict);
break;
case 'fullName':
$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;
case 'organizations':
$contact += self::parseOrganizations($value, $check_at_type);
$contact += self::parseOrganizations($value, $strict);
break;
case 'titles':
$contact += self::parseTitles($value, $check_at_type);
$contact += self::parseTitles($value, $strict);
break;
case 'emails':
$contact += self::parseEmails($value, $check_at_type);
$contact += self::parseEmails($value, $strict);
break;
case 'phones':
$contact += self::parsePhones($value, $check_at_type);
$contact += self::parsePhones($value, $strict);
break;
case 'online':
$contact += self::parseOnline($value, $check_at_type);
$contact += self::parseOnline($value, $strict);
break;
case 'addresses':
$contact += self::parseAddresses($value, $check_at_type);
$contact += self::parseAddresses($value, $strict);
break;
case 'photos':
$contact += self::parsePhotos($value, $check_at_type);
$contact += self::parsePhotos($value, $strict);
break;
case 'anniversaries':
$contact += self::parseAnniversaries($value);
$contact += self::parseAnniversaries($value, $strict);
break;
case 'notes':
$contact['note'] = implode("\n", array_map(static function ($note) {
return self::parseString($note);
}, $value));
}, (array)$value));
break;
case 'categories':
@ -150,7 +187,7 @@ class JsContact
break;
case 'egroupware.org:customfields':
$contact += self::parseCustomfields($value);
$contact += self::parseCustomfields($value, $strict);
break;
case 'egroupware.org:assistant':
@ -197,11 +234,12 @@ class JsContact
* Parse and optionally generate 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
* @return string without urn:uuid: prefix
* @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)
{
@ -211,7 +249,15 @@ class JsContact
}
$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.
*
* @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
*/
protected static function parseOrganizations(array $orgas, bool $check_at_type=true)
protected static function parseOrganizations(array $orgas, bool $stict=true)
{
$contact = [];
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));
}
@ -306,15 +352,19 @@ class JsContact
* Parse titles, thought we only have "title" and "role" available for storage.
*
* @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
*/
protected static function parseTitles(array $titles, bool $check_at_type=true)
protected static function parseTitles(array $titles, bool $stict=true)
{
$contact = [];
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]));
}
@ -397,8 +447,12 @@ class JsContact
foreach($definitions as $name => $definition)
{
$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))
{
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
*
* @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
*/
protected static function parseAddresses(array $addresses, bool $check_at_type=true)
protected static function parseAddresses(array $addresses, bool $stict=true)
{
$n = 0;
$last_type = null;
$contact = [];
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));
}
$contact += ($values=self::parseAddress($address, $id, $last_type));
$contact += ($values=self::parseAddress($address, $id, $last_type, $stict));
if (++$n > 2)
{
@ -567,9 +621,10 @@ class JsContact
* @param array $address address-object
* @param string $id index
* @param ?string $last_type "work" or "home"
* @param bool $stict true: check if objects have their proper @type attribute
* @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') ||
$last_type === 'home' ? 'work' : 'home';
@ -577,7 +632,10 @@ class JsContact
$prefix = $type === 'work' ? 'adr_one_' : 'adr_two_';
$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)
{
if (isset($address[$js]) && !is_string($address[$js]))
@ -586,6 +644,17 @@ class JsContact
}
$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;
}
@ -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.
* Then we split it into 2 lines.
*
* @param array $components
* @param bool $check_at_type true: check if objects have their proper @type attribute
* @param array|string $components string only for relaxed parsing
* @param bool $stict true: check if objects have their proper @type attribute
* @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 = [];
$last_type = null;
foreach($components as $component)
@ -651,7 +728,7 @@ class JsContact
{
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));
}
@ -712,21 +789,25 @@ class JsContact
* Parse phone objects
*
* @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
*/
protected static function parsePhones(array $phones, bool $check_at_type=true)
protected static function parsePhones(array $phones, bool $stict=true)
{
$contact = [];
// check for good matches
foreach($phones as $id => $phone)
{
if (!$stict && is_string($phone))
{
$phone = ['phone' => $phone];
}
if (!is_array($phone) || !is_string($phone['phone']))
{
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));
}
@ -818,19 +899,23 @@ class JsContact
* We currently only support 2 URLs, rest get's ignored!
*
* @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
*/
protected static function parseOnline(array $values, bool $check_at_type)
protected static function parseOnline(array $values, bool $stict)
{
$contact = [];
foreach($values as $id => $value)
{
if (!$stict && is_string($value))
{
$value = ['resource' => $value];
}
if (!is_array($value) || !is_string($value['resource']))
{
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));
}
@ -889,15 +974,19 @@ class JsContact
*
* @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 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
*/
protected static function parseEmails(array $emails, bool $check_at_type=true)
protected static function parseEmails(array $emails, bool $stict=true)
{
$contact = [];
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));
}
@ -905,7 +994,7 @@ class JsContact
{
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'];
}
@ -953,11 +1042,11 @@ class JsContact
* @return array
* @ToDo
*/
protected static function parsePhotos(array $photos, bool $check_at_type)
protected static function parsePhotos(array $photos, bool $stict)
{
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));
}
@ -1008,18 +1097,23 @@ class JsContact
* @param array $components
* @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),
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']))
{
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));
}
@ -1052,14 +1146,28 @@ class JsContact
*
* @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 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
*/
protected static function parseAnniversaries(array $anniversaries, bool $check_at_type=true)
protected static function parseAnniversaries(array $anniversaries, bool $stict=true)
{
$contact = [];
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']) ||
!preg_match('/^\d{4}-\d{2}-\d{2}$/', $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));
}
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));
}
@ -1251,16 +1359,51 @@ class JsContact
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
*
* @param \Throwable $e
* @param string $type
* @param string $name
* @param ?string $name
* @param mixed $value
* @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 {
throw $e;

View File

@ -275,7 +275,30 @@ class Merge extends Api\Storage\Merge
*/
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';
foreach($this->contacts->contact_fields as $name => $label)
{
@ -299,25 +322,37 @@ class Merge extends Api\Storage\Merge
case 'email_home':
$group = 'email';
break;
case 'url':
case 'freebusy_uri':
$group = 'details';
}
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $label;
if($name == 'cat_id')
$marker = $this->prefix($prefix, $name, '{');
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
$placeholders['business']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_one_formatted}}'] = "Formatted business address";
$placeholders['private']['{{' . ($prefix ? $prefix . '/' : '') . 'adr_two_formatted}}'] = "Formatted private address";
$placeholders['business'][] = [
'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';
foreach($this->contacts->customfields as $name => $field)
{
$placeholders[$group]["{{" . ($prefix ? $prefix . '/' : '') . $name . "}}"] = $field['label'];
}
$this->add_customfield_placeholders($placeholders, $prefix);
return $placeholders;
}

View File

@ -766,6 +766,19 @@ class Country
{
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]))
{
return $name; // $name is already a country-code

View File

@ -114,7 +114,7 @@ class Widget
// Update content?
if(self::$cont == null)
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;
self::$cont = self::$cont[$this->id];
@ -147,7 +147,8 @@ class Widget
}
// Reset content as we leave
if($old_cont) {
if (isset($old_cont))
{
self::$cont = $old_cont;
}
}
@ -206,7 +207,7 @@ class Widget
$template = $this;
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)
{
@ -218,7 +219,7 @@ class Widget
$template->attrs[$reader->name] = $value = $reader->value;
// 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,
isset(self::$cont) ? self::$cont : self::$request->content);
@ -237,7 +238,7 @@ class Widget
}
// 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]);
}
@ -426,7 +427,7 @@ class Widget
class_exists($class_name = $basetype.'_etemplate_widget'))))
{
// 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];
}
@ -535,12 +536,12 @@ class Widget
// maintain $expand array name-expansion
$cname = $params[0];
$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['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");
return;
@ -593,13 +594,13 @@ class Widget
foreach($attrs as $name => &$value)
{
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']);
}
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']);
$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())
{
$is_index_in_content = $name[0] == '@';
$is_index_in_content = !empty($name) && $name[0] == '@';
if (($pos_var=strpos($name,'$')) !== false)
{
if (!$cont)
@ -687,8 +688,8 @@ class Widget
if (!is_numeric($c)) $c = self::chrs2num($c);
$col = self::num2chrs($c-1); // $c-1 to get: 0:'@', 1:'A', ...
if (is_numeric($c_)) $col_ = self::num2chrs($c_-1);
$row_cont = $cont[$row];
$col_row_cont = $cont[$col.$row];
$row_cont = $cont[$row] ?? null;
$col_row_cont = $cont[$col.$row] ?? null;
try {
eval('$name = "' . str_replace('"', '\\"', $name) . '";');
@ -726,9 +727,9 @@ class Widget
*/
static function chrs2num($chrs)
{
if (empty($chrs)) return 0;
$min = ord('A');
$max = ord('Z') - $min + 1;
$num = 1+ord($chrs[0])-$min;
if (strlen($chrs) > 1)
{
@ -751,7 +752,7 @@ class Widget
if ($num >= $max)
{
$chrs = chr(($num / $max) + $min - 1);
}
} else $chrs = '';
$chrs .= chr(($num % $max) + $min);
return $chrs;
@ -829,7 +830,7 @@ class Widget
{
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)
{

View File

@ -50,12 +50,12 @@ class Box extends Etemplate\Widget
$old_expand = $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['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");
return;
@ -73,7 +73,7 @@ class Box extends Etemplate\Widget
// Expand children
$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
$expand['row'] = 0;

View File

@ -29,7 +29,7 @@ use EGroupware\Api;
* &8 = dont show time for readonly and type date-time if time is 0:00,
* &16 = prefix r/o display with dow
* &32 = prefix r/o display with week-number
* &64 = prefix r/o display with weeknumber and dow
* &64 = prefix r/o display with weeknumber and dow
* &128 = no icon to trigger popup, click into input trigers it, also removing the separators to save space
*
* @todo validation of date-duration
@ -120,7 +120,7 @@ class Date extends Transformer
{
$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);
}

View File

@ -80,7 +80,7 @@ class Grid extends Box
$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");
$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 ($expand['cname'] !== $cname && $cname)
if (!empty($expand['cname']) && $expand['cname'] !== $cname && $cname)
{
$expand['cont'] =& self::get_array(self::$request->content, $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);
// Add favorite here so app doesn't save it in the session
if($_GET['favorite'])
if (!empty($_GET['favorite']))
{
$send_value['favorite'] = $safe_name;
}
if (true) $value = $send_value;
$value['total'] = $total;
$value['total'] = $total ?? null;
// 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'];
$value['options-cat_id'] = self::$request->sel_options['cat_id'] ? self::$request->sel_options['cat_id'] : array();
$cat_app = $value['cat_app'] ?? $GLOBALS['egw_info']['flags']['current_app'] ?? '';
$value['options-cat_id'] = self::$request->sel_options['cat_id'] ?? [];
// 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');
}
@ -220,7 +220,7 @@ class Nextmatch extends Etemplate\Widget
if(strpos($name, 'options-') !== false && $_value)
{
$select = substr($name, 8);
if(!self::$request->sel_options[$select])
if (empty(self::$request->sel_options[$select]))
{
self::$request->sel_options[$select] = array();
}
@ -231,21 +231,21 @@ class Nextmatch extends Etemplate\Widget
//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']);
unset($value['rows']['sel_options']);
}
// 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']);
$value['no_columnselection'] = $value['no_columnselection'] || (
$GLOBALS['egw']->preferences->forced[$app][$pref_name] &&
$pref_name = 'nextmatch-' . ($value['columnselection_pref'] ?? $this->attrs['template'] ?? '');
$value['no_columnselection'] = !empty($value['no_columnselection']) || (
!empty($GLOBALS['egw']->preferences->forced[$app][$pref_name]) &&
// 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)
$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
@ -256,7 +256,7 @@ class Nextmatch extends Etemplate\Widget
if (isset($value['actions']) && !isset($value['actions'][0]))
{
$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();
$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();
$row_id = isset($value['row_id']) ? $value['row_id'] : 'id';
$row_modified = $value['row_modified'];
$row_id = $value['row_id'] ?? 'id';
$row_modified = $value['row_modified'] ?? null;
foreach($rows as $n => $row)
{
@ -384,12 +384,12 @@ class Nextmatch extends Etemplate\Widget
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_modified])) unset($row_modified);
if (empty($row[$row_modified])) unset($row_modified);
$id = $row_id ? $row[$row_id] : $n;
$result['order'][] = $id;
$modified = $row[$row_modified];
$modified = $row[$row_modified] ?? null;
if (isset($modified) && !(is_int($modified) || is_string($modified) && is_numeric($modified)))
{
$modified = Api\DateTime::to(str_replace('Z', '', $modified), 'ts');
@ -497,10 +497,10 @@ class Nextmatch extends Etemplate\Widget
{
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
if(in_array($key, array('template'))) $no_rows = true;
if (in_array($key, array('template'))) $no_rows = true;
// Actions still need extra handling
if($key == 'actions' && !isset($value['actions'][0]))
@ -626,7 +626,7 @@ class Nextmatch extends Etemplate\Widget
), array(), true); // true = no permission check
// 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']);
if(!$row_template)
@ -642,12 +642,12 @@ class Nextmatch extends Etemplate\Widget
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);
$is_parent = $value['is_parent'];
$is_parent_value = $value['is_parent_value'];
$parent_id = $value['parent_id'];
$is_parent = $value['is_parent'] ?? null;
$is_parent_value = $value['is_parent_value'] ?? null;
$parent_id = $value['parent_id'] ?? null;
// remove empty rows required by old etemplate to compensate for header rows
$first = $total ? null : 0;
@ -658,14 +658,14 @@ class Nextmatch extends Etemplate\Widget
{
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] == $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
if($repeating_row)
if (!empty($repeating_row))
{
// Change anything by widget for each row ($row set to 1)
$_row = array(1 => &$row);
@ -894,7 +894,7 @@ class Nextmatch extends Etemplate\Widget
if ($default_attrs) $action += $default_attrs;
// 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(
@ -911,7 +911,7 @@ class Nextmatch extends Etemplate\Widget
);
$action_links[] = $prefix.'select_all';
}
$group = $action['group'];
$group = $action['group'] ?? 0;
if (!$first_level && $n == $max_length && count($actions) > $max_length)
{
@ -941,29 +941,29 @@ class Nextmatch extends Etemplate\Widget
}
// 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
($first_level || substr($action['enabled'],0,11) == 'javaScript:'))
if ((!isset($action['type']) || in_array($action['type'], array('popup','drag','drop'))) && // popup is the default
($first_level || isset($action['enabled']) && substr($action['enabled'],0,11) === 'javaScript:'))
{
$action_links[] = $prefix.$id;
}
// add sub-menues
if ($action['children'])
// add sub-menus
if (!empty($action['children']))
{
static $inherit_attrs = array('url','popup','nm_action','onExecute','type','egw_open','allowOnMultiple','confirm','confirm_multiple');
$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));
unset($action['prefix']);
// 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);
}
// link or popup action
if ($action['url'])
if (!empty($action['url']))
{
$action['url'] = Api\Framework::link('/index.php',str_replace('$action',$id,$action['url']));
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';
}
@ -997,10 +997,10 @@ class Nextmatch extends Etemplate\Widget
// Make sure select all is in a group by itself
foreach($egw_actions as $id => &$_action)
{
if($id == $prefix . 'select_all') continue;
if($_action['group'] >= $egw_actions[$prefix.'select_all']['group'] )
if ($id == $prefix . 'select_all') continue;
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);
@ -1044,7 +1044,7 @@ class Nextmatch extends Etemplate\Widget
'no_lang' => true,
);
// 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'];
}
@ -1083,7 +1083,7 @@ class Nextmatch extends Etemplate\Widget
'prefix' => $prefix,
);
// 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'];
}
@ -1222,7 +1222,7 @@ class Nextmatch extends Etemplate\Widget
// Run on all the sub-templates
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->run($method_name, $params, $respect_disabled);
@ -1230,14 +1230,6 @@ class Nextmatch extends Etemplate\Widget
}
}
$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))
{
$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)
@ -75,31 +77,53 @@ class Placeholder extends Etemplate\Widget
case 'user':
$list = $merge->get_user_placeholder_list();
break;
case 'general':
$list = $merge->get_common_placeholder_list();
break;
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;
}
if(!is_null($group))
if(!is_null($group) && is_array($list))
{
$list = array_intersect_key($list, $group);
}
$placeholders[$appname] = $list;
// Remove if empty
foreach($list as $p_group => $p_list)
{
if(count($p_list) == 0)
{
unset($list[$p_group]);
}
}
if($list)
{
$placeholders[$appname] = $list;
}
}
$response = Api\Json\Response::get();
$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 = "";
switch($app)
switch($entry['app'])
{
case 'addressbook':
case 'user':
$entry = ['id' => $GLOBALS['egw_info']['user']['person_id']];
// fall through
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->data($merged);

View File

@ -496,17 +496,10 @@ abstract class Framework extends Framework\Extra
{
$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_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).']');
// 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');
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')))
{
$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(
'img_icon' => $var['favicon_file'],
'img_shortcut' => $var['favicon_file'],
'pngfix' => $pngfix,
'lang_code' => $lang_code,
'charset' => Translation::charset(),
'website_title' => $site_title,
@ -533,7 +525,7 @@ abstract class Framework extends Framework\Extra
'java_script' => self::_get_js($extra),
'meta_robots' => $robots,
'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'],
'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)
{
$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];
$GLOBALS['egw_info']['server'][$type] ?? null;
if (substr($url, 0, 4) == 'http' ||
$url[0] == '/')
if (substr($url, 0, 4) === 'http' ||
!empty($url) && $url[0] === '/')
{
return $url;
}
@ -801,7 +793,7 @@ abstract class Framework extends Framework\Extra
$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;
}
$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))))
{
//error_log(__METHOD__."() Framework\CssIncludes::get()=".array2string(Framework\CssIncludes::get()));
@ -1110,8 +1102,7 @@ abstract class Framework extends Framework\Extra
if(@isset($_GET['menuaction']))
{
list(, $class) = explode('.',$_GET['menuaction']);
if(is_array($GLOBALS[$class]->public_functions) &&
$GLOBALS[$class]->public_functions['java_script'])
if (!empty($GLOBALS[$class]->public_functions['java_script']))
{
$java_script .= $GLOBALS[$class]->java_script();
}
@ -1578,8 +1569,8 @@ abstract class Framework extends Framework\Extra
foreach(Framework\CssIncludes::get() as $path)
{
unset($query);
list($path,$query) = explode('?',$path,2);
$path .= '?'. ($query ? $query : filemtime(EGW_SERVER_ROOT.$path));
list($path,$query) = explode('?', $path,2)+[null,null];
$path .= '?'. ($query ?? filemtime(EGW_SERVER_ROOT.$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']))
{
error_log(function_backtrace()); // no NOT include
//error_log(function_backtrace()); // no NOT include
}
else
{

View File

@ -63,8 +63,8 @@ class Hooks
$location = is_array($args) ? (isset($args['hook_location']) ? $args['hook_location'] : $args['location']) : $args;
if (!isset(self::$locations)) self::read();
if (empty(self::$locations[$location])) return []; // not a single app implements that hook
$hooks = self::$locations[$location];
if (!isset($hooks) || empty($hooks)) return array(); // not a single app implements that hook
$apps = array_keys($hooks);
if (!$no_permission_check)
@ -115,7 +115,7 @@ class Hooks
}
$ret = array();
foreach((array)self::$locations[$location][$appname] as $hook)
foreach(self::$locations[$location][$appname] ?? [] as $hook)
{
try {
// old style file hook
@ -130,7 +130,7 @@ class Hooks
return true;
}
list($class, $method) = explode('::', $hook);
list($class, $method) = explode('::', $hook)+[null,null];
// static method of an autoloadable 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:.*/'),
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'].']';
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;
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'));
}
@ -1132,7 +1132,7 @@ class Link extends Link\Storage
*/
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;
@ -1181,7 +1181,7 @@ class Link extends Link\Storage
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
// folders located in other and shared; needed only for some servers
if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail');
if (self::$mailConfig['examineNamespace'])
if (!empty(self::$mailConfig['examineNamespace']))
{
$prefixes=array();
if (is_array($nameSpace))
@ -3014,7 +3014,7 @@ class Mail
$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
$aFolders = array();
@ -3180,7 +3180,7 @@ class Mail
{
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']||
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));
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 {
// avoid calling sortByAutoFolder as it is not regarding subfolders
$gAutoFolderObjectsTmp = $googleAutoFolderObjects;
@ -3910,7 +3910,7 @@ class Mail
{
//error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.array2string($_folder).', '.$_forceDeleteMethod);
$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 (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;
//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']));
return true; // as we do not catch/examine setFlags returnValue

View File

@ -313,7 +313,7 @@ class Account implements \ArrayAccess
try {
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!
$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') &&
($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
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->smtpAuth = !empty($this->params['acc_smtp_username']);
$this->smtpServer->username = $this->params['acc_smtp_username'];
$this->smtpServer->password = $this->params['acc_smtp_password'];
$this->smtpServer->username = $this->params['acc_smtp_username'] ?? null;
$this->smtpServer->password = $this->params['acc_smtp_password'] ?? null;
$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;
}
@ -676,7 +676,7 @@ class Account implements \ArrayAccess
$to_replace = array();
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];
}
@ -781,7 +781,7 @@ class Account implements \ArrayAccess
'account_id' => self::is_multiple($identity) ? 0 :
(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(
'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
$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
$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;
self::$db->update(self::TABLE, array(
@ -1578,7 +1578,7 @@ class Account implements \ArrayAccess
'ident_realname' => $account['ident_realname'],
'ident_org' => $account['ident_org'],
'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_logintype' => $account['acc_imap_logintype'],
'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
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_email'])) $account['ident_email'] = $GLOBALS['egw_info']['user']['account_email'];

View File

@ -87,7 +87,7 @@ class Notifications
$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();
$result = array(
'notify_folders' => $folders,

View File

@ -233,10 +233,23 @@ class Smtp
* default use $this->loginType
* @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)
{

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(
'location' => 'mailaccount_userdata_updated',
'account_id' => $_uidnumber,

View File

@ -196,7 +196,7 @@ class Preferences
foreach((array)$ids as $id)
{
// 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)
{
@ -237,7 +237,7 @@ class Preferences
$replace = $with = array();
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.'$$';
$with[] = $val;
}
@ -275,7 +275,7 @@ class Preferences
*/
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']))
{
$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']),
'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)
//
foreach($this->data as $app => $data)
@ -421,7 +421,7 @@ class Preferences
default:
foreach($values as $app => $vals)
{
$this->group[$app] = (array)$vals + (array)$this->group[$app];
$this->group[$app] = (array)$vals + ($this->group[$app] ?? []);
}
break;
}
@ -474,7 +474,7 @@ class Preferences
}
// 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();
}

View File

@ -1363,7 +1363,7 @@ class Session
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'] ||
$GLOBALS['egw_info']['user']['session_ip'] != $this->getuser_ip()))
@ -1538,21 +1538,21 @@ class Session
}
// 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))
{
$vars += $extravars;
$extravars = $othervars;
}
else
elseif (!empty($othervars))
{
if ($othervars) $extravars .= ($extravars?'&':'').$othervars;
$extravars .= ($extravars ? '&' : '') . $othervars;
}
// parse extravars string into the vars array
if ($extravars)
if (!empty($extravars))
{
foreach(explode('&',$extravars) as $expr)
foreach(explode('&', $extravars) as $expr)
{
list($var,$val) = explode('=', $expr,2);
if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val); // make sure to not double encode &
@ -1720,7 +1720,7 @@ class Session
{
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
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
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 = '/';
@ -1851,7 +1851,7 @@ class Session
private function update_dla($update_access_log=false)
{
// 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']);

View File

@ -16,6 +16,7 @@ namespace EGroupware\Api\Storage;
use DOMDocument;
use EGroupware\Api;
use EGroupware\Api\Vfs;
use EGroupware\Collabora\Conversion;
use EGroupware\Stylite;
use tidy;
use uiaccountsel;
@ -31,6 +32,26 @@ use ZipArchive;
*/
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
*
@ -48,7 +69,12 @@ abstract class Merge
/**
* 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
@ -77,10 +103,10 @@ abstract class Merge
*/
public $export_limit;
public $public_functions = array(
"merge_entries" => true
);
/**
* Configuration for HTML Tidy to clean up any HTML content that is kept
*/
@ -237,47 +263,59 @@ abstract class Merge
$replacements = array();
foreach(array_keys($this->contacts->contact_fields) as $name)
{
$value = $contact[$name];
$value = $contact[$name] ?? '';
if(!$value)
{
continue;
}
switch($name)
{
case 'created': case 'modified':
if($value) $value = Api\DateTime::to($value);
case 'created':
case 'modified':
if($value)
{
$value = Api\DateTime::to($value);
}
break;
case 'bday':
if ($value)
if($value)
{
try {
try
{
$value = Api\DateTime::to($value, true);
}
catch (\Exception $e) {
unset($e); // ignore exception caused by wrongly formatted date
catch (\Exception $e)
{
unset($e); // ignore exception caused by wrongly formatted date
}
}
break;
case 'owner': case 'creator': case 'modifier':
case 'owner':
case 'creator':
case 'modifier':
$value = Api\Accounts::username($value);
break;
case 'cat_id':
if ($value)
if($value)
{
// if cat-tree is displayed, we return a full category path not just the name of the cat
$use = $GLOBALS['egw_info']['server']['cat_tab'] == 'Tree' ? 'path' : 'name';
$cats = array();
foreach(is_array($value) ? $value : explode(',',$value) as $cat_id)
foreach(is_array($value) ? $value : explode(',', $value) as $cat_id)
{
$cats[] = $GLOBALS['egw']->categories->id2name($cat_id,$use);
$cats[] = $GLOBALS['egw']->categories->id2name($cat_id, $use);
}
$value = implode(', ',$cats);
$value = implode(', ', $cats);
}
break;
case 'jpegphoto': // returning a link might make more sense then the binary photo
if ($contact['photo'])
case 'jpegphoto': // returning a link might make more sense then the binary photo
if($contact['photo'])
{
$value = Api\Framework::getUrl(Api\Framework::link('/index.php',$contact['photo']));
$value = Api\Framework::getUrl(Api\Framework::link('/index.php', $contact['photo']));
}
break;
case 'tel_prefer':
if ($value && $contact[$value])
if($value && $contact[$value])
{
$value = $contact[$value];
}
@ -354,7 +392,9 @@ abstract class Merge
$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')
. (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)
{
$ids = empty($ids) ? [] : (array)$ids;
$matches = null;
if ($mimetype == 'application/xml' &&
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);
//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)
{
// 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>';
$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);
$countlables = count($countlables[0]);
preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1);
if ($countlables > 1) $lableprint = true;
$lableprint = $countlables > 1;
if (count($ids) > 1 && !$contentrepeat)
{
$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 ($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
{
if(!($replacements = $this->get_replacements($id,$content)))
@ -956,10 +997,10 @@ abstract class Merge
}
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
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['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'account_primary_group'));
$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['$$date$$'] = Api\DateTime::to('now',true);
$replacements['$$datetime$$'] = Api\DateTime::to('now');
@ -1131,7 +1172,7 @@ abstract class Merge
{
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);
$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
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,'/');
}
$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)
{
foreach((array)$this->numeric_fields as $fieldname) {
foreach($this->numeric_fields as $fieldname)
{
$names[] = preg_quote($fieldname,'/');
}
switch($mimetype)
@ -1371,7 +1414,7 @@ abstract class Merge
break;
}
if($format && $names)
if (!empty($format) && $names)
{
// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
do {
@ -1600,9 +1643,9 @@ abstract class Merge
*/
public static function get_app_class($appname)
{
if(class_exists($appname) && is_subclass_of($appname, 'EGroupware\\Api\\Storage\\Merge'))
$classname = "{$appname}_merge";
if(class_exists($classname, false) && is_subclass_of($classname, 'EGroupware\\Api\\Storage\\Merge'))
{
$classname = "{$appname}_merge";
$document_merge = new $classname();
}
else
@ -1630,14 +1673,9 @@ abstract class Merge
try
{
$classname = "{$app}_merge";
if(!class_exists($classname))
{
return $replacements;
}
$class = new $classname();
$method = $app.'_replacements';
if(method_exists($class,$method))
$class = $this->get_app_class($app);
$method = $app . '_replacements';
if(method_exists($class, $method))
{
$replacements = $class->$method($id, $prefix, $content);
}
@ -1654,6 +1692,30 @@ abstract class Merge
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
*
@ -1780,13 +1842,12 @@ abstract class Merge
if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0)
{ //sets a Letterprefix
$replaceprefixsort = array();
// ToDo Stefan: $contentstart is NOT defined here!!!
$replaceprefix = explode(' ',substr($param[0],21,-2));
foreach ($replaceprefix as $nameprefix)
{
if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$'];
}
$replace = implode($replaceprefixsort,' ');
$replace = implode(' ', $replaceprefixsort);
}
return $replace;
}
@ -2092,7 +2153,6 @@ abstract class Merge
$export_limit=null)
{
$documents = array();
$editable_mimes = array();
if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting
try {
@ -2118,11 +2178,11 @@ abstract class Merge
$file['path'] = $default_doc;
}
$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)),
'group' => 1,
'postSubmit' => true, // download needs post submit (not Ajax) to work
'group' => 1
);
self::document_editable_action($documents['document'], $file);
if ($file['mime'] == 'message/rfc822')
{
self::document_mail_action($documents['document'], $file);
@ -2177,13 +2237,6 @@ abstract class Merge
}
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)
{
$name_arr = explode('/', $file['name']);
@ -2200,30 +2253,24 @@ abstract class Merge
}
switch($count)
{
case (count($name_arr)-1):
$current_level[$prefix.$file['name']] = array(
'icon' => Api\Vfs::mime_icon($file['mime']),
'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')
case (count($name_arr) - 1):
$current_level[$prefix . $file['name']];
self::document_editable_action($current_level[$prefix . $file['name']], $file);
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);
}
break;
default:
if(!is_array($current_level[$prefix.$name_arr[$count]]))
if(!is_array($current_level[$prefix . $name_arr[$count]]))
{
// create parent folder
$current_level[$prefix.$name_arr[$count]] = array(
'icon' => 'phpgwapi/foldertree_folder',
'caption' => Api\Vfs::decodePath($name_arr[$count]),
'group' => 2,
'children' => array(),
$current_level[$prefix . $name_arr[$count]] = array(
'icon' => 'phpgwapi/foldertree_folder',
'caption' => Api\Vfs::decodePath($name_arr[$count]),
'group' => 2,
'children' => array(),
);
}
break;
@ -2232,50 +2279,47 @@ abstract class Merge
}
else if (count($files) >= self::SHOW_DOCS_BY_MIME_LIMIT)
{
if (!isset($documents[$file['mime']]))
if(!isset($documents[$file['mime']]))
{
$documents[$file['mime']] = array(
'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\MimeMagic::mime2label($file['mime']),
'group' => 2,
'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\MimeMagic::mime2label($file['mime']),
'group' => 2,
'children' => array(),
);
}
$documents[$file['mime']]['children'][$prefix.$file['name']] = array(
'caption' => Api\Vfs::decodePath($file['name']),
'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')
$documents[$file['mime']]['children'][$prefix . $file['name']] = array();
self::document_editable_action($documents[$file['mime']]['children'][$prefix . $file['name']], $file);
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);
}
}
else
{
$documents[$prefix.$file['name']] = array(
'icon' => Api\Vfs::mime_icon($file['mime']),
'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')
$documents[$prefix . $file['name']] = array();
self::document_editable_action($documents[$prefix . $file['name']], $file);
if($file['mime'] == 'message/rfc822')
{
self::document_mail_action($documents[$prefix.$file['name']], $file);
self::document_mail_action($documents[$prefix . $file['name']], $file);
}
}
}
// Add PDF checkbox
$documents['as_pdf'] = array(
'caption' => 'As PDF',
'checkbox' => true,
);
return array(
'icon' => 'etemplate/merge',
'caption' => $caption,
'children' => $documents,
'icon' => 'etemplate/merge',
'caption' => $caption,
'children' => $documents,
// 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()),
'hideOnDisabled' => true, // do not show 'Insert in document', if no documents defined or no export allowed
'group' => $group,
'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
'group' => $group,
);
}
@ -2293,6 +2337,7 @@ abstract class Merge
private static function document_mail_action(Array &$action, $file)
{
unset($action['postSubmit']);
unset($action['onExecute']);
// Lots takes a while, confirm
$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)
{
unset($action['postSubmit']);
$edit_attributes = array(
'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit',
'document' => $file['path'],
'merge' => get_called_class(),
'id' => '$id',
'select_all' => '$select_all'
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(
'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'] . '.' . get_called_class() . '.merge_entries',
'document' => $file['path'],
'merge' => get_called_class(),
);
$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
);
$action['url'] = urldecode(http_build_query($edit_attributes));
}
/**
@ -2374,25 +2434,26 @@ abstract class Merge
* 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.
*
* @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 boolean|null $pdf Convert result to PDF
* @throws Api\Exception
* @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'))
{
$document_merge = new $_REQUEST['merge']();
}
elseif (is_null($document_merge))
elseif(is_null($document_merge))
{
$document_merge = new Api\Contacts\Merge();
}
if(($error = $document_merge->check_document($_REQUEST['document'],'')))
{
$response->error($error);
error_log(__METHOD__ . "({$_REQUEST['document']}) $error");
return;
}
@ -2405,35 +2466,33 @@ abstract class 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);
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
$target = $_target = (Vfs::is_writable(Vfs::get_home_dir()) ?
Vfs::get_home_dir() :
"/home/{$GLOBALS['egw_info']['user']['account_lid']}"
)."/$filename";
$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);
$target = $document_merge->get_save_path($filename);
// Make sure we won't overwrite something already there
$target = Vfs::make_unique($target);
copy($result, Vfs::PREFIX . $target);
unlink($result);
// Find out what to do with it
$editable_mimes = array();
try {
if (class_exists('EGroupware\\collabora\\Bo') &&
try
{
if(class_exists('EGroupware\\collabora\\Bo') &&
$GLOBALS['egw_info']['user']['apps']['collabora'] &&
($discovery = \EGroupware\collabora\Bo::discover()) &&
$GLOBALS['egw_info']['user']['preferences']['filemanager']['merge_open_handler'] != 'download'
@ -2447,11 +2506,32 @@ abstract class Merge
// ignore failed discovery
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(
'menuaction' => 'collabora.EGroupware\\Collabora\\Ui.editor',
'path'=> $target
'path' => $target
));
}
else
@ -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
*/
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
'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/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
*/
public function get_user_placeholder_list($prefix = '')
{
$contacts = new Api\Contacts\Merge();
$replacements = $contacts->get_placeholder_list(($prefix ? $prefix . '/' : '') . 'user');
unset($replacements['details']['{{' . ($prefix ? $prefix . '/' : '') . 'user/account_id}}']);
$replacements = $contacts->get_placeholder_list($this->prefix($prefix, 'user'));
unset($replacements['details'][$this->prefix($prefix, 'user/account_id', '{')]);
$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;
}
/**
* 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");
$type = $options['type']; // 'd', 'f' or 'F'
$dirs_last = $options['depth']; // put content of dirs before the dir itself
$type = $options['type'] ?? null; // 'd', 'f' or 'F'
$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)
$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
@ -386,7 +386,7 @@ class Vfs extends Vfs\Base
$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
}
@ -403,7 +403,7 @@ class Vfs extends Vfs\Base
],
]);
$url = $options['url'];
$url = $options['url'] ?? null;
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;
}
$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);
}
@ -434,11 +434,11 @@ class Vfs extends Vfs\Base
{
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);
if ((int)$options['mindepth'] <= 1)
if (!isset($options['mindepth']) || (int)$options['mindepth'] <= 1)
{
self::_check_add($options,$file,$result);
}
@ -459,7 +459,7 @@ class Vfs extends Vfs\Base
}
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);
}
@ -569,9 +569,9 @@ class Vfs extends Vfs\Base
*/
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)))
{
@ -595,7 +595,7 @@ class Vfs extends Vfs\Base
$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);
if ($options['mime'] || $options['need_mime'])
if (!empty($options['mime']) || !empty($options['need_mime']))
{
$stat['mime'] = self::mime_content_type($path);
}
@ -642,7 +642,7 @@ class Vfs extends Vfs\Base
return; // not create/modified in the spezified time
}
// do we return url or just vfs pathes
if (!$options['url'])
if (empty($options['url']))
{
$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)
{
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) == '/')
{
@ -1255,7 +1255,7 @@ class Vfs extends Vfs\Base
array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/
}
//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)
{
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);
$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;
}
// 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);
}
// 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));
}
@ -2386,6 +2386,28 @@ class Vfs extends Vfs\Base
}
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();

View File

@ -295,7 +295,7 @@ class Base
'host' => $GLOBALS['egw_info']['user']['domain'],
'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'])
{
// otherwise we get an invalid url (scheme:///path/to/something)!

View File

@ -1701,10 +1701,10 @@ class HTTP_WebDAV_Server
/**
* PUT method handler
*
* @param void
* @param string $method='PUT'
* @return void
*/
function http_PUT()
function http_PUT(string $method='PUT')
{
if ($this->_check_lock_status($this->path)) {
$options = Array();
@ -1839,7 +1839,7 @@ class HTTP_WebDAV_Server
}
}
$stat = $this->PUT($options);
$stat = $this->$method($options);
if ($stat === false) {
$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 :-(
// 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
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
*
* 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.
*
* @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 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
*
@ -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 string $errstr error message
@ -178,7 +178,7 @@ function egw_error_handler ($errno, $errstr, $errfile, $errline)
case E_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!
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
// 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'])))
{
_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.
*
* PHP 7.0+ can be told not to instanciate any classes (and calling eg. it's destructor).
* In fact it instanciates it as __PHP_Incomplete_Class without any methods and therefore disarming threads.
* PHP 7.0+ can be told not to instantiate any classes (and calling eg. it's destructor).
* In fact it instantiates it as __PHP_Incomplete_Class without any methods and therefore disarming threads.
*
* @param string $str
* @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 {
background-position: bottom !important;
margin-top: 2px;
left: 0;
top: 0;
}
.et2_toolbar .et2_toolbar_more h .toolbar-admin-pref {
background-image: url(../../../pixelegg/images/setup.svg);

View File

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

View File

@ -9,7 +9,7 @@
<select id="placeholder_list"/>
</vbox>
<hrule/>
<link-entry id="entry" label="Select entry"/>
<link-entry id="entry" label="Select entry" only_app="addressbook"/>
<hbox class="preview">
<description id="preview_content"/>
</hbox>
@ -49,7 +49,7 @@
flex-grow: 0;
}
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 {
border: none;

View File

@ -22,11 +22,13 @@ require_once realpath(__DIR__.'/../WidgetBaseTest.php');
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';
public static function setUpBeforeClass() : void {
public static function setUpBeforeClass() : void
{
parent::setUpBeforeClass();
}

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
use EGroupware\Api\LoggedInTest as LoggedInTest;
class CustomfieldsTest extends LoggedInTest
@ -20,19 +21,19 @@ class CustomfieldsTest extends LoggedInTest
protected $customfields = null;
protected $simple_field = array(
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
public function tearDown(): void
{
@ -209,35 +210,35 @@ class CustomfieldsTest extends LoggedInTest
// Expected options, file
return array(
array(array(
'' => 'Select',
'Α'=> 'α Alpha',
'Β'=> 'β Beta',
'Γ'=> 'γ Gamma',
'Δ'=> 'δ Delta',
'Ε'=> 'ε Epsilon',
'Ζ'=> 'ζ Zeta',
'Η'=> 'η Eta',
'Θ'=> 'θ Theta',
'Ι'=> 'ι Iota',
'Κ'=> 'κ Kappa',
'Λ'=> 'λ Lambda',
'Μ'=> 'μ Mu',
'Ν'=> 'ν Nu',
'Ξ'=> 'ξ Xi',
'Ο'=> 'ο Omicron',
'Π'=> 'π Pi',
'Ρ'=> 'ρ Rho',
'Σ'=> 'σ Sigma',
'Τ'=> 'τ Tau',
'Υ'=> 'υ Upsilon',
'Φ'=> 'φ Phi',
'Χ'=> 'χ Chi',
'Ψ'=> 'ψ Psi',
'Ω'=> 'ω Omega'
), 'greek_options.php'),
'' => 'Select',
'Α' => 'α Alpha',
'Β' => 'β Beta',
'Γ' => 'γ Gamma',
'Δ' => 'δ Delta',
'Ε' => 'ε Epsilon',
'Ζ' => 'ζ Zeta',
'Η' => 'η Eta',
'Θ' => 'θ Theta',
'Ι' => 'ι Iota',
'Κ' => 'κ Kappa',
'Λ' => 'λ Lambda',
'Μ' => 'μ Mu',
'Ν' => 'ν Nu',
'Ξ' => 'ξ Xi',
'Ο' => 'ο Omicron',
'Π' => 'π Pi',
'Ρ' => 'ρ Rho',
'Σ' => 'σ Sigma',
'Τ' => 'τ Tau',
'Υ' => 'υ Upsilon',
'Φ' => 'φ Phi',
'Χ' => 'χ Chi',
'Ψ' => 'ψ Psi',
'Ω' => 'ω Omega'
), 'greek_options.php'),
array(array(
'View Subs' => "egw_open('','infolog','list',{action:'sp',action_id:widget.getRoot().getArrayMgr('content').getEntry('info_id')},'infolog','infolog');"
), 'infolog_subs_option.php')
'View Subs' => "egw_open('','infolog','list',{action:'sp',action_id:widget.getRoot().getArrayMgr('content').getEntry('info_id')},'infolog','infolog');"
), 'infolog_subs_option.php')
);
}
@ -302,8 +303,8 @@ class CustomfieldsTest extends LoggedInTest
protected function get_another_user()
{
$accounts = $GLOBALS['egw']->accounts->search(array(
'type' => 'accounts'
));
'type' => 'accounts'
));
unset($accounts[$GLOBALS['egw_info']['user']['account_id']]);
if(count($accounts) == 0)
{

View File

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

View File

@ -12,6 +12,7 @@
namespace EGroupware\Api\Storage;
require_once __DIR__ . '/../LoggedInTest.php';
require_once __DIR__ . '/TestTracking.php';
use EGroupware\Api;
@ -22,18 +23,18 @@ class TrackingTest extends LoggedInTest
const APP = 'test';
protected $simple_field = array(
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
'app' => self::APP,
'name' => 'test_field',
'label' => 'Custom field',
'type' => 'text',
'type2' => array(),
'help' => 'Custom field created for automated testing by CustomfieldsTest',
'values' => null,
'len' => null,
'rows' => null,
'order' => null,
'needed' => null,
'private' => array()
);
/**
@ -57,8 +58,8 @@ class TrackingTest extends LoggedInTest
// Get another user
$accounts = $GLOBALS['egw']->accounts->search(array(
'type' => 'accounts'
));
'type' => 'accounts'
));
unset($accounts[$GLOBALS['egw_info']['user']['account_id']]);
if(count($accounts) == 0)
{

View File

@ -296,15 +296,18 @@ class SharingBase extends LoggedInTest
*/
protected function mountVersioned($path)
{
if (!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper'))
if(!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper'))
{
$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;
Vfs::$is_root = true;
$url = Versioning\StreamWrapper::PREFIX.$path;
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $path as versioned");
$url = Versioning\StreamWrapper::PREFIX . $path;
$this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $path as versioned");
Vfs::$is_root = $backup;
$this->mounts[] = $path;
@ -362,8 +365,8 @@ class SharingBase extends LoggedInTest
Vfs::chmod($path, 0750);
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');
$this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path");
$url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME . '://default' . $path . '?merge=' . realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount');
$this->assertTrue(Vfs::mount($url, $path, false), "Unable to mount $url to $path");
Vfs::$is_root = $backup;
$this->mounts[] = $path;

View File

@ -901,7 +901,7 @@ END:VALARM';
{
Api\Translation::add_app('calendar');
// 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'])
&& !self::isVideoconferenceDisabled())
@ -917,6 +917,8 @@ END:VALARM';
}
return array();
}
if (!isset($params['data']['event_id'])) $params['data']['event_id'] = '';
if (!isset($params['data']['user_id'])) $params['data']['user_id'] = '';
return array(
array(
'id' => 'A',

View File

@ -313,7 +313,7 @@ class calendar_uiforms extends calendar_ui
$msg = $this->export($content['id'],true);
}
// delete a recur-exception
if ($content['recur_exception']['delete_exception'])
if (!empty($content['recur_exception']['delete_exception']))
{
$date = key($content['recur_exception']['delete_exception']);
// eT2 converts time to
@ -338,7 +338,7 @@ class calendar_uiforms extends calendar_ui
$update_type = 'edit';
}
// delete an alarm
if ($content['alarm']['delete_alarm'])
if (!empty($content['alarm']['delete_alarm']))
{
$id = key($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_owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email'];
$scope = 'shared';
$type = 'write';
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)
{
@ -1753,7 +1755,7 @@ class calendar_uiforms extends calendar_ui
{
$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
}
@ -2248,7 +2250,7 @@ class calendar_uiforms extends calendar_ui
$readonlys['button[reject]'] = $readonlys['button[cancel]'] = true;
}
}
else
elseif (!empty($event['button']))
{
//_debug_array($event);
$button = key($event['button']);
@ -2906,7 +2908,7 @@ class calendar_uiforms extends calendar_ui
{
throw new Api\Exception\NoPermission\Admin();
}
if ($_content)
if (!empty($_content['button']))
{
$button = key($_content['button']);
unset($_content['button']);

View File

@ -93,7 +93,7 @@ class calendar_uilist extends calendar_ui
// handle a single button like actions
foreach(array('delete','timesheet','document') as $button)
{
if ($_content['nm']['rows'][$button])
if (!empty($_content['nm']['rows'][$button]))
{
$id = key($_content['nm']['rows'][$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>
* **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
@ -266,3 +378,11 @@ use ```<domain-name>:<name>``` like in JsCalendar
* top-level objects need a ```@type``` attribute with one of the following values:
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
### ToDos
- [x] Addressbook
- [ ] update of photos, keys, attachments
- [ ] InfoLog
- [ ] Calendar
- [ ] relatedTo / links
- [ ] storing not native supported attributes eg. localization

View File

@ -161,27 +161,34 @@ class filemanager_hooks
'forced' => 'yes',
),
'showusers' => array(
'type' => 'select',
'name' => 'showusers',
'values' => $yes_no,
'label' => lang('Show link "%1" in side box menu?',lang('Users and groups')),
'xmlrpc' => True,
'admin' => False,
'forced' => 'yes',
'type' => 'select',
'name' => 'showusers',
'values' => $yes_no,
'label' => lang('Show link "%1" in side box menu?', lang('Users and groups')),
'xmlrpc' => True,
'admin' => False,
'forced' => 'yes',
),
);
$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(
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.',lang('filemanager')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'name').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'type' => 'vfs_file',
'size' => 60,
'label' => 'Default document to insert entries',
'name' => 'default_document',
'help' => lang('If you specify a document (full vfs path) here, %1 displays an extra document icon for each entry. That icon allows to download the specified document with the data inserted.', lang('filemanager')) . ' ' .
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'name') . ' ' .
lang('The following document-types are supported:') . implode(',', Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'xmlrpc' => True,
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',

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']));
}
}
elseif(isset($content['button']))
elseif(!empty($content['button']))
{
$button = key($content['button']);
unset($content['button']);
@ -205,7 +205,7 @@ class filemanager_select
$sel_options['mime'] = $content['options-mime'];
}
elseif(isset($content['apps']))
elseif(!empty($content['apps']))
{
$app = key($content['apps']);
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']);
}
if ($content['button'])
if (!empty($content['button']))
{
if ($content['button'])
{
$button = key($content['button']);
unset($content['button']);
}
$button = key($content['button']);
unset($content['button']);
switch ($button)
{
case 'upload':
@ -1193,7 +1191,7 @@ class filemanager_ui
//_debug_array($content);
$path =& $content['path'];
$button = @key($content['button']);
$button = @key($content['button'] ?? []);
unset($content['button']);
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']);
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'];
unset($fields['create']);
if ($fields['delete'])
if (!empty($fields['delete']))
{
$delete = key($fields['delete']);
unset($fields['delete']);
@ -178,7 +178,7 @@ class infolog_customfields extends admin_customfields
$create = $status['create'];
unset($status['create']);
if ($status['delete'])
if (!empty($status['delete']))
{
$delete = key($status['delete']);
unset($status['delete']);
@ -276,7 +276,7 @@ class infolog_customfields extends admin_customfields
unset($this->status[$content['type2']]);
unset($this->status['defaults'][$content['type2']]);
unset($this->group_owners[$content['type2']]);
$content['type2'] = key($this->content_types);
$content['type2'] = key($this->content_types ?? []);
// save changes to repository
$this->save_repository();

View File

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

View File

@ -471,17 +471,27 @@ class infolog_hooks
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.',lang('infolog')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','info_subject').' '.
lang('The following document-types are supported:').'*.rtf, *.txt',
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the data inserted.', lang('infolog')) . ' ' .
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'info_subject') . ' ' .
lang('The following document-types are supported:') . '*.rtf, *.txt',
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/infolog',
'xmlrpc' => True,
'admin' => False,
'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',
);
}

View File

@ -64,23 +64,42 @@ class infolog_merge extends Api\Storage\Merge
* @param string &$content=null content to create some replacements only if they are use
* @return array|boolean
*/
protected function get_replacements($id,&$content=null)
protected function get_replacements($id, &$content = null)
{
if (!($replacements = $this->infolog_replacements($id, '', $content)))
if(!($replacements = $this->infolog_replacements($id, '', $content)))
{
return false;
}
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
*
* @param int $id id of entry
* @param string $prefix='' prefix like eg. 'erole'
* @param string $prefix ='' prefix like eg. 'erole'
* @return array|boolean
*/
public function infolog_replacements($id,$prefix='', &$content = '')
public function infolog_replacements($id, $prefix = '', &$content = '')
{
$record = new infolog_egw_record($id);
$info = array();
@ -250,14 +269,56 @@ class infolog_merge extends Api\Storage\Merge
echo '<tr><td>{{info_contact/#'.$name.'}}</td><td colspan="3">'.$field['label']."</td></tr>\n";
}
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
echo '<tr><td colspan="4"><h3>' . lang('General fields:') . "</h3></td></tr>";
foreach($this->get_common_replacements() as $name => $label)
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
echo '<tr><td>{{' . $name . '}}</td><td colspan="3">' . $label . "</td></tr>\n";
}
echo "</table>\n";
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))
{
$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;
}
@ -831,7 +831,7 @@ class infolog_ui
{
$popup =& $values;
}
$values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action']);
$values['nm']['multi_action'] .= '_' . key($popup[$multi_action . '_action'] ?? []);
if($multi_action == 'link')
{
$popup[$multi_action] = $popup['link']['app'] . ':'.$popup['link']['id'];
@ -2498,7 +2498,7 @@ class infolog_ui
if($content)
{
// Save
$button = key($content['button']);
$button = key($content['button'] ?? []);
if($button == 'save' || $button == 'apply')
{
$this->bo->responsible_edit = array('info_status','info_percent','info_datecompleted');

View File

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

View File

@ -145,7 +145,7 @@ class mail_tree
*/
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;
return true;
}
@ -404,7 +404,7 @@ class mail_tree
}
$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']);
$folderName = array_pop($path);
@ -428,7 +428,7 @@ class mail_tree
$data[Tree::IMAGE_FOLDER_OPEN] =
$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_FOLDER_OPEN] = self::$leafImages['folderNoSelectOpen'];
@ -444,7 +444,7 @@ class mail_tree
}
// 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
// accordingly to indicate useen mails
@ -477,7 +477,7 @@ class mail_tree
foreach(Mail\Account::search(true, false) as $acc_id => $accObj)
{
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
$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,
// 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);
let tree = this.et2.getWidgetById('nm[foldertree]');
var that = this;
var rvMain = false;
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)
{
var buttons = [

View File

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

View File

@ -176,7 +176,7 @@ class resources_acl_ui
$content = array('data' => array());
}
}
elseif ($content['button'])
elseif (!empty($content['button']))
{
$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;
$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;
if($register_code && $registration = registration_bo::confirm($register_code)) {
// Get calendar through link
@ -48,17 +49,19 @@ class resources_reserve {
$planner_date = mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start']));
$readonlys['__ALL__'] = true;
$content = array(
'resource' => key($data['participant_types']['r']),
'resource' => key($data['participant_types']['r'] ?? []),
'date' => $data['start'],
'time' => $data['start'] - mktime(0,0,0,date('m',$data['start']),date('d',$data['start']),date('Y',$data['start'])),
'quantity' => 0
);
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>';
} else {
}
else
{
$data['msg']= '<div class="confirm">'.lang('Unable to process confirmation.').'</div>';
}
}
}
$this->tmpl->read('resources.sitemgr_book');

View File

@ -11,14 +11,21 @@
import path from 'path';
import babel from '@babel/core';
import { readFileSync, readdirSync, statSync } from "fs";
import rimraf from 'rimraf';
import { readFileSync, readdirSync, statSync, unlinkSync } from "fs";
//import rimraf from 'rimraf';
import { minify } from 'terser';
import resolve from '@rollup/plugin-node-resolve';
// Best practice: use this
//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
const do_minify = false;

View File

@ -38,7 +38,7 @@ $db_backup = new Api\Db\Backup();
$asyncservice = new Api\Asyncservice();
// download a backup, has to be before any output !!!
if ($_POST['download'])
if (!empty($_POST['download']))
{
$file = key($_POST['download']);
$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));
}
// delete a backup
if ($_POST['delete'])
if (!empty($_POST['delete']))
{
$file = key($_POST['delete']);
$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));
}
// rename a backup
if ($_POST['rename'])
if (!empty($_POST['rename']))
{
$file = key($_POST['rename']);
$new_name = $_POST['new_name'][$file];
@ -190,7 +190,7 @@ if ($_POST['rename'])
}
}
// restore a backup
if ($_POST['restore'])
if (!empty($_POST['restore']))
{
$file = key($_POST['restore']);
$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
if ($_POST['schedule'])
if (!empty($_POST['schedule']))
{
$asyncservice->set_timer($_POST['times'],'db_backup-'.implode(':',$_POST['times']),'admin.admin_db_backup.do_backup','');
}
// cancel a scheduled backup
if (is_array($_POST['cancel']))
if (!empty($_POST['cancel']) && is_array($_POST['cancel']))
{
$id = key($_POST['cancel']);
$asyncservice->cancel_timer($id);

View File

@ -191,17 +191,27 @@ class timesheet_hooks
'admin' => False,
);
$settings['document_dir'] = array(
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the %1 data inserted.', lang('timesheet')).' '.
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.','ts_title').' '.
lang('The following document-types are supported:'). implode(',',Api\Storage\Merge::get_file_extensions()),
'type' => 'vfs_dirs',
'size' => 60,
'label' => 'Directory with documents to insert entries',
'name' => 'document_dir',
'help' => lang('If you specify a directory (full vfs path) here, %1 displays an action for each document. That action allows to download the specified document with the %1 data inserted.', lang('timesheet')) . ' ' .
lang('The document can contain placeholder like {{%1}}, to be replaced with the data.', 'ts_title') . ' ' .
lang('The following document-types are supported:') . implode(',', Api\Storage\Merge::get_file_extensions()),
'run_lang' => false,
'xmlrpc' => True,
'admin' => False,
'default' => '/templates/timesheet',
'xmlrpc' => True,
'admin' => False,
'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',
);
}

View File

@ -207,14 +207,59 @@ class timesheet_merge extends Api\Storage\Merge
$i++;
}
echo '<tr><td colspan="4"><h3>'.lang('General fields:')."</h3></td></tr>";
echo '<tr><td colspan="4"><h3>' . lang('General fields:') . "</h3></td></tr>";
foreach($this->get_common_replacements() as $name => $label)
{
echo '<tr><td>{{'.$name.'}}</td><td colspan="3">'.$label."</td></tr>\n";
echo '<tr><td>{{' . $name . '}}</td><td colspan="3">' . $label . "</td></tr>\n";
}
echo "</table>\n";
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');
}
/**
* 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
*
@ -107,9 +87,11 @@ class timesheet_tracking extends Api\Storage\Tracking
*
* @param array $data
* @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
*/
function get_subject($data,$old)
protected function get_subject($data,$old,$deleted=null,$receiver=null)
{
return '#'.$data['ts_id'].' - '.$data['ts_title'];
}
@ -119,9 +101,10 @@ class timesheet_tracking extends Api\Storage\Tracking
*
* @param array $data
* @param array $old
* @param int|string $receiver nummeric account_id or email address
* @return string
*/
function get_message($data,$old)
protected function get_message($data,$old,$receiver=null)
{
if (!$data['ts_modified'] || !$old)
{

View File

@ -879,8 +879,8 @@ class timesheet_ui extends timesheet_bo
{
$etpl = new Etemplate('timesheet.index');
if ($_GET['msg']) $msg = $_GET['msg'];
if ($content['nm']['rows']['delete'])
if (!empty($_GET['msg'])) $msg = $_GET['msg'];
if (!empty($content['nm']['rows']['delete']))
{
$ts_id = key($content['nm']['rows']['delete']);
if ($this->delete($ts_id))
@ -892,13 +892,13 @@ class timesheet_ui extends timesheet_bo
$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']);
$content['nm']['action'] = 'document';
$content['nm']['selected'] = array($id);
}
if ($content['nm']['action'])
if (!empty($content['nm']['action']))
{
// remove sum-* rows from checked rows
$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');
}
}
if (isset($content['statis']['delete']))
if (!empty($content['statis']['delete']))
{
$id = key($content['statis']['delete']);
if (isset($this->status_labels_config[$id]))