Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-11-01 09:37:26 -06:00
commit 76d7447dab
63 changed files with 542 additions and 270 deletions

10
SECURITY.md Normal file
View File

@ -0,0 +1,10 @@
# Security Policy
## Supported Versions
* next release / master
* latest released version: **21.1**
* old released version: 20.1 (major security fixes only!)
Please report security issues to <security@egroupware.org>
If you need to send information in a secure way, please contakt us first, so we can send you our PGP keys to do so.

View File

@ -971,11 +971,12 @@ class addressbook_vcal extends addressbook_bo
{ {
if (!empty($fieldName)) if (!empty($fieldName))
{ {
$value = trim($vcardValues[$vcardKey]['values'][$fieldKey]); $value = $vcardValues[$vcardKey]['values'][$fieldKey];
if (is_string($value)) $value = trim($value);
if ($pref_tel && (($vcardKey == $pref_tel) || if ($pref_tel && (($vcardKey == $pref_tel) ||
($vcardValues[$vcardKey]['name'] == 'TEL') && ($vcardValues[$vcardKey]['name'] == 'TEL') &&
($vcardValues[$vcardKey]['value'] == $vcardValues[$pref_tel]['value']))) ($vcardValues[$vcardKey]['value'] == $vcardValues[$pref_tel]['value'])))
{ {
$contact['tel_prefer'] = $fieldName; $contact['tel_prefer'] = $fieldName;
} }

View File

@ -163,9 +163,9 @@
</vbox> </vbox>
<description id="${row}[id]" class="contactid"/> <description id="${row}[id]" class="contactid"/>
<vbox> <vbox>
<link id="${row}[last_link]"/> <link id="${row}[last_link]" readonly="true"/>
<link id="${row}[next_link]"/> <link id="${row}[next_link]" readonly="true"/>
</vbox> </vbox>
<vbox> <vbox>
<date-time id="${row}[created]" readonly="true" class="noWrap"/> <date-time id="${row}[created]" readonly="true" class="noWrap"/>
<select-account id="${row}[creator]" readonly="true"/> <select-account id="${row}[creator]" readonly="true"/>

View File

@ -869,7 +869,14 @@ class admin_mail
// clear current account-data, as account has changed and we going to read selected one // clear current account-data, as account has changed and we going to read selected one
$content = array_intersect_key($content, array_flip(array('called_for', 'accounts', 'acc_id', 'tabs'))); $content = array_intersect_key($content, array_flip(array('called_for', 'accounts', 'acc_id', 'tabs')));
if ($content['acc_id'] > 0) if ($content['acc_id'] === 'new')
{
$content['account_id'] = $content['called_for'];
$content['old_acc_id'] = $content['acc_id']; // to not call add/wizard, if we return from to
unset($content['tabs']);
return $this->add($content);
}
elseif ($content['acc_id'] > 0)
{ {
try { try {
$account = Mail\Account::read($content['acc_id'], $this->is_admin && $content['called_for'] ? $account = Mail\Account::read($content['acc_id'], $this->is_admin && $content['called_for'] ?
@ -899,13 +906,6 @@ class admin_mail
Framework::window_close($e->getMessage().' ('.get_class($e).': '.$e->getCode().')'); Framework::window_close($e->getMessage().' ('.get_class($e).': '.$e->getCode().')');
} }
} }
elseif ($content['acc_id'] === 'new')
{
$content['account_id'] = $content['called_for'];
$content['old_acc_id'] = $content['acc_id']; // to not call add/wizard, if we return from to
unset($content['tabs']);
return $this->add($content);
}
} }
// some defaults for new accounts // some defaults for new accounts
if (!isset($content['account_id']) || empty($content['acc_id']) || $content['acc_id'] === 'new') if (!isset($content['account_id']) || empty($content['acc_id']) || $content['acc_id'] === 'new')
@ -1005,7 +1005,7 @@ class admin_mail
{ {
$account->imapServer()->retrieveRules(); $account->imapServer()->retrieveRules();
} }
$new_account = !($content['acc_id'] > 0); $new_account = !((int)$content['acc_id'] > 0);
// check for deliveryMode="forwardOnly", if a forwarding-address is given // check for deliveryMode="forwardOnly", if a forwarding-address is given
if ($content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp' && if ($content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp' &&
$content['deliveryMode'] == Mail\Smtp::FORWARD_ONLY && $content['deliveryMode'] == Mail\Smtp::FORWARD_ONLY &&
@ -1340,7 +1340,7 @@ class admin_mail
$readonlys['button[multiple]'] = true; $readonlys['button[multiple]'] = true;
} }
// when called by admin for existing accounts, display further administrative actions // when called by admin for existing accounts, display further administrative actions
if ($content['called_for'] && $content['acc_id'] > 0) if ($content['called_for'] && (int)$content['acc_id'] > 0)
{ {
$admin_actions = array(); $admin_actions = array();
foreach(Api\Hooks::process(array( foreach(Api\Hooks::process(array(
@ -1471,8 +1471,8 @@ class admin_mail
$url = 'https://autoconfig.thunderbird.net/v1.1/'.$domain; $url = 'https://autoconfig.thunderbird.net/v1.1/'.$domain;
try { try {
$xml = @simplexml_load_file($url); $xml = simplexml_load_string(file_get_contents($url) ?: '');
if (!$xml->emailProvider) throw new Api\Exception\NotFound(); if (!$xml || !$xml->emailProvider) throw new Api\Exception\NotFound();
$provider = array( $provider = array(
'displayName' => (string)$xml->emailProvider->displayName, 'displayName' => (string)$xml->emailProvider->displayName,
); );
@ -1604,11 +1604,11 @@ class admin_mail
if (strpos($_data['domain'], '.') !== false) if (strpos($_data['domain'], '.') !== false)
{ {
$userData['mailLocalAddress'] = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $userData['mailLocalAddress']); $userData['mailLocalAddress'] = preg_replace('/@'.preg_quote($ea_account->acc_domain, '/').'$/', '@'.$_data['domain'], $userData['mailLocalAddress']);
foreach($userData['mailAlternateAddress'] as &$alias) foreach($userData['mailAlternateAddress'] as &$alias)
{ {
$alias = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $alias); $alias = preg_replace('/@'.preg_quote($ea_account->acc_domain, '/').'$/', '@'.$_data['domain'], $alias);
} }
} }
// fullfill the saveUserData requirements // fullfill the saveUserData requirements
@ -1618,7 +1618,7 @@ class admin_mail
} }
else else
{ {
$msg .= lang('No profile defined for user %1', '#'.$_data['id'].' '.$account['account_fullname']."\n"); $msg = lang('No profile defined for user %1', '#'.$_data['id'].' '.$account['account_fullname']."\n");
} }
} }

View File

@ -397,9 +397,15 @@ String: A string in the user\'s date format, or a relative date. Relative dates
*/ */
set_min(_value) set_min(_value)
{ {
// minDate option checks the type, so make sure we're passing numbers as numbers (0, not "0")
// @ts-ignore
if(typeof _value === "string" && !isNaN(_value) && "" + parseInt(_value) == _value)
{
_value = parseInt(_value);
}
if(this.input_date) if(this.input_date)
{ {
if (this.is_mobile) if(this.is_mobile)
{ {
this.input_date.attr('min', this._relativeDate(_value)); this.input_date.attr('min', this._relativeDate(_value));
} }
@ -452,9 +458,15 @@ String: A string in the user\'s date format, or a relative date. Relative dates
*/ */
set_max(_value) set_max(_value)
{ {
// maxDate option checks the type, so make sure we're passing numbers as numbers (0, not "0")
// @ts-ignore
if(typeof _value === "string" && !isNaN(_value) && "" + parseInt(_value) == _value)
{
_value = parseInt(_value);
}
if(this.input_date) if(this.input_date)
{ {
if (this.is_mobile) if(this.is_mobile)
{ {
this.input_date.attr('max', this._relativeDate(_value)); this.input_date.attr('max', this._relativeDate(_value));
} }
@ -1516,12 +1528,18 @@ export class et2_date_range extends et2_inputWidget
this.from = <et2_date>et2_createWidget('date',{ this.from = <et2_date>et2_createWidget('date',{
id: this.id+'[from]', id: this.id+'[from]',
blur: egw.lang('From'), blur: egw.lang('From'),
onchange() { widget.to.set_min(widget.from.getValue()); } onchange(_node,_widget) {
widget.to.set_min(widget.from.getValue());
if (_node instanceof jQuery) widget.onchange.call(widget, _widget, widget);
}
},this); },this);
this.to = <et2_date>et2_createWidget('date',{ this.to = <et2_date>et2_createWidget('date',{
id: this.id+'[to]', id: this.id+'[to]',
blur: egw.lang('To'), blur: egw.lang('To'),
onchange() {widget.from.set_max(widget.to.getValue()); } onchange(_node,_widget) {
widget.from.set_max(widget.to.getValue());
if (_node instanceof jQuery) widget.onchange.call(widget, _widget,widget);
}
},this); },this);
this.select = <et2_selectbox><unknown>et2_createWidget('select',{ this.select = <et2_selectbox><unknown>et2_createWidget('select',{
id: this.id+'[relative]', id: this.id+'[relative]',

View File

@ -986,7 +986,7 @@ class Accounts
} }
if ($account_id && ($data = self::cache_read($account_id))) if ($account_id && ($data = self::cache_read($account_id)))
{ {
$ret = $just_id && $data['memberships'] ? array_keys($data['memberships']) : $data['memberships']; $ret = $just_id && $data['memberships'] ? array_keys($data['memberships']) : ($data['memberships'] ?? []);
} }
//error_log(__METHOD__."($account_id, $just_id) data=".array2string($data)." returning ".array2string($ret)); //error_log(__METHOD__."($account_id, $just_id) data=".array2string($data)." returning ".array2string($ret));
return $ret ?? []; return $ret ?? [];

View File

@ -166,8 +166,8 @@ class Sql
$data['account_id'] = -$data['account_id']; $data['account_id'] = -$data['account_id'];
$data['mailAllowed'] = true; $data['mailAllowed'] = true;
} }
if (!$data['account_firstname']) $data['account_firstname'] = $data['account_lid']; if (empty($data['account_firstname'])) $data['account_firstname'] = $data['account_lid'];
if (!$data['account_lastname']) if (empty($data['account_lastname']))
{ {
$data['account_lastname'] = $data['account_type'] == 'g' ? 'Group' : 'User'; $data['account_lastname'] = $data['account_type'] == 'g' ? 'Group' : 'User';
// if we call lang() before the translation-class is correctly setup, // if we call lang() before the translation-class is correctly setup,
@ -177,7 +177,7 @@ class Sql
$data['account_lastname'] = lang($data['account_lastname']); $data['account_lastname'] = lang($data['account_lastname']);
} }
} }
if (!$data['account_fullname']) $data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname']; if (empty($data['account_fullname'])) $data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname'];
return $data; return $data;
} }

View File

@ -74,7 +74,7 @@ class Acl
*/ */
function __construct($account_id = null) function __construct($account_id = null)
{ {
if (is_object($GLOBALS['egw_setup']->db)) if (isset($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']->db))
{ {
$this->db = $GLOBALS['egw_setup']->db; $this->db = $GLOBALS['egw_setup']->db;
} }
@ -481,6 +481,10 @@ class Acl
'acl_appname' => $appname, 'acl_appname' => $appname,
),__LINE__,__FILE__) as $row) ),__LINE__,__FILE__) as $row)
{ {
if (!isset($rights[$row['acl_location']]))
{
$rights[$row['acl_location']] = 0;
}
$rights[$row['acl_location']] |= $row['acl_rights']; $rights[$row['acl_location']] |= $row['acl_rights'];
} }
return $rights; return $rights;

View File

@ -232,7 +232,7 @@ class Asyncservice
{ {
if ((string)$t == '*') $t = '*/1'; if ((string)$t == '*') $t = '*/1';
list($one,$inc) = $arr = explode('/',$t); list($one,$inc) = ($arr = explode('/', $t))+[null,null];
if (!(is_numeric($one) && count($arr) == 1 || if (!(is_numeric($one) && count($arr) == 1 ||
count($arr) == 2 && is_numeric($inc))) count($arr) == 2 && is_numeric($inc)))
@ -247,7 +247,7 @@ class Asyncservice
} }
else else
{ {
list($min,$max) = $arr = explode('-',$one); list($min,$max) = ($arr = explode('-', $one))+[null,null];
if (empty($one) || $one == '*') if (empty($one) || $one == '*')
{ {
$min = $min_unit[$u]; $min = $min_unit[$u];

View File

@ -207,8 +207,8 @@ class Contacts extends Contacts\Storage
$this->default_private = substr($GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] ?? '',-1) == 'p'; $this->default_private = substr($GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] ?? '',-1) == 'p';
if ($this->default_addressbook > 0 && $this->default_addressbook != $this->user && if ($this->default_addressbook > 0 && $this->default_addressbook != $this->user &&
($this->default_private || ($this->default_private ||
$this->default_addressbook == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default'] || $this->default_addressbook == (int)($GLOBALS['egw']->preferences->forced['addressbook']['add_default'] ?? 0) ||
$this->default_addressbook == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default'])) $this->default_addressbook == (int)($GLOBALS['egw']->preferences->default['addressbook']['add_default'] ?? 0)))
{ {
$this->default_addressbook = $this->user; // admin set a default or forced pref for personal addressbook $this->default_addressbook = $this->user; // admin set a default or forced pref for personal addressbook
} }

View File

@ -1601,7 +1601,7 @@ class Db
} }
if (empty($column_definitions)) if (empty($column_definitions))
{ {
$column_definitions = $this->column_definitions; $column_definitions = $this->column_definitions ?? null;
} }
if ($this->Debug) echo "<p>db::column_data_implode('$glue',".print_r($array,True).",'$use_key',".print_r($only,True).",<pre>".print_r($column_definitions,True)."</pre>\n"; if ($this->Debug) echo "<p>db::column_data_implode('$glue',".print_r($array,True).",'$use_key',".print_r($only,True).",<pre>".print_r($column_definitions,True)."</pre>\n";
@ -2164,7 +2164,7 @@ class Db
} }
if (is_array($where)) if (is_array($where))
{ {
$where = $this->column_data_implode(' AND ',$where,True,False,$table_def['fd']); $where = $this->column_data_implode(' AND ',$where,True,False, $table_def ? $table_def['fd'] : null);
} }
if (self::$tablealiases && isset(self::$tablealiases[$table])) if (self::$tablealiases && isset(self::$tablealiases[$table]))
{ {

View File

@ -39,7 +39,7 @@ class Applications
*/ */
function __construct($account_id = '') function __construct($account_id = '')
{ {
if (is_object($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']->db)) if (isset($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']->db))
{ {
$this->db = $GLOBALS['egw_setup']->db; $this->db = $GLOBALS['egw_setup']->db;
} }

View File

@ -634,17 +634,22 @@ class Widget
*/ */
protected static function check_disabled($disabled, array $expand) protected static function check_disabled($disabled, array $expand)
{ {
if (($not = $disabled[0] == '!')) if(($not = $disabled[0] == '!'))
{ {
$disabled = substr($disabled,1); $disabled = substr($disabled, 1);
} }
list($value,$check) = $vals = explode('=',$disabled); list($value, $check) = $vals = explode('=', $disabled);
// use expand_name to be able to use @ or $ // use expand_name to be able to use @ or $
$val = self::expand_name($value, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $val = self::expand_name($value, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$check_val = self::expand_name($check, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $check_val = self::expand_name($check, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$result = count($vals) == 1 ? $val != '' : ($check_val[0] == '/' ? preg_match($check_val,$val) : $val == $check_val); $result = count($vals) == 1 ?
if ($not) $result = !$result; boolval($val) :
($check_val[0] == '/' ? preg_match($check_val, $val) : $val == $check_val);
if($not)
{
$result = !$result;
}
//error_log(__METHOD__."('".($not?'!':'')."$disabled' = '$val' ".(count($vals) == 1 ? '' : ($not?'!':'=')."= '$check_val'")." = ".($result?'True':'False')); //error_log(__METHOD__."('".($not?'!':'')."$disabled' = '$val' ".(count($vals) == 1 ? '' : ($not?'!':'=')."= '$check_val'")." = ".($result?'True':'False'));
return $result; return $result;

View File

@ -92,7 +92,7 @@ class Grid extends Box
{ {
$cname = self::form_name($cname, $this->id, $expand); $cname = self::form_name($cname, $this->id, $expand);
} }
if($cname && (!empty($expand['cname']) && $expand['cname'] !== $cname || !$expand['cname'])) if ($cname && (empty($expand['cname']) || $expand['cname'] !== $cname))
{ {
$expand['cont'] =& self::get_array(self::$request->content, $cname); $expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname; $expand['cname'] = $cname;

View File

@ -87,7 +87,7 @@ class Link extends Etemplate\Widget
// ToDo: implement on client-side // ToDo: implement on client-side
if (!$attrs['help']) self::setElementAttribute($form_name, 'help', 'view this linked entry in its application'); if (!$attrs['help']) self::setElementAttribute($form_name, 'help', 'view this linked entry in its application');
if($attrs['type'] == 'link-list') if (!empty($attrs['type']) && $attrs['type'] === 'link-list')
{ {
$app = $value['to_app']; $app = $value['to_app'];
$id = $value['to_id']; $id = $value['to_id'];

View File

@ -317,10 +317,10 @@ class Select extends Etemplate\Widget
{ {
// Check selection preference, we may be able to skip reading some data // Check selection preference, we may be able to skip reading some data
$select_pref = $GLOBALS['egw_info']['user']['preferences']['common']['account_selection']; $select_pref = $GLOBALS['egw_info']['user']['preferences']['common']['account_selection'];
if($this->attrs['type'] == 'select-account' && !$GLOBALS['egw_info']['user']['apps']['admin'] && $select_pref == 'none') if($this->attrs['type'] == 'select-account' && empty($GLOBALS['egw_info']['user']['apps']['admin']) && $select_pref == 'none')
{ {
// Preserve but do not send the value if preference is 'none' // Preserve but do not send the value if preference is 'none'
self::$request->preserv[$this->id] = self::$request->content[$this->id]; self::$request->preserv[$this->id] = self::$request->content[$this->id] ?? null;
unset(self::$request->content[$this->id]); unset(self::$request->content[$this->id]);
$this->attrs['readonly'] = true; $this->attrs['readonly'] = true;
} }
@ -335,7 +335,7 @@ class Select extends Etemplate\Widget
if (!isset($form_names_done[$form_name]) && if (!isset($form_names_done[$form_name]) &&
($type_options = self::typeOptions($this, ($type_options = self::typeOptions($this,
// typeOptions thinks # of rows is the first thing in options // typeOptions thinks # of rows is the first thing in options
(!empty($this->attrs['rows']) && !empty($this->attrs['options']) && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options']), (!empty($this->attrs['rows']) && !empty($this->attrs['options']) && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : ($this->attrs['options']??null)),
$no_lang, $this->attrs['readonly'] ?? false, self::get_array(self::$request->content, $form_name), $form_name))) $no_lang, $this->attrs['readonly'] ?? false, self::get_array(self::$request->content, $form_name), $form_name)))
{ {
self::fix_encoded_options($type_options); self::fix_encoded_options($type_options);
@ -966,6 +966,9 @@ class Select extends Etemplate\Widget
*/ */
public static function ajax_get_options($type, $attributes, $value = null) public static function ajax_get_options($type, $attributes, $value = null)
{ {
// close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session();
$no_lang = false; $no_lang = false;
if(is_array($attributes)) if(is_array($attributes))
{ {

View File

@ -85,7 +85,7 @@ class Tabbox extends Etemplate\Widget
{ {
foreach($this->children[1]->children as $tab) foreach($this->children[1]->children as $tab)
{ {
if($readonlys[$tab->id]) if (!empty($readonlys[$tab->id]))
{ {
$tab->attrs['disabled'] = $readonlys[$tab->id]; $tab->attrs['disabled'] = $readonlys[$tab->id];
} }

View File

@ -42,7 +42,7 @@ class Vfs extends File
$form_name = self::form_name($cname, $this->id, $expand ? $expand : array('cont'=>self::$request->content)); $form_name = self::form_name($cname, $this->id, $expand ? $expand : array('cont'=>self::$request->content));
if (!empty($this->attrs['path'])) if (!empty($this->attrs['path']))
{ {
$path = self::expand_name($this->attrs['path'],$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']); $path = self::expand_name($this->attrs['path'], $expand['c']??null, $expand['row'], $expand['c_']??null, $expand['row_']??null, $expand['cont']);
} }
else else
{ {

View File

@ -1662,6 +1662,9 @@ abstract class Framework extends Framework\Extra
*/ */
public static function ajax_user_list() public static function ajax_user_list()
{ {
// close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session();
$list = array('accounts' => array(),'groups' => array(), 'owngroups' => array()); $list = array('accounts' => array(),'groups' => array(), 'owngroups' => array());
if($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'primary_group') if($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'primary_group')
{ {
@ -1688,6 +1691,9 @@ abstract class Framework extends Framework\Extra
*/ */
public static function ajax_account_data($_account_ids, $_field, $_resolve_groups=false) public static function ajax_account_data($_account_ids, $_field, $_resolve_groups=false)
{ {
// close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session();
$list = array(); $list = array();
foreach((array)$_account_ids as $account_id) foreach((array)$_account_ids as $account_id)
{ {

View File

@ -70,17 +70,17 @@ class Html
{ {
$additionalQuote="";//at the end, ... $additionalQuote="";//at the end, ...
// only one &quot at the end is found. chance is, it is not belonging to the URL // only one &quot at the end is found. chance is, it is not belonging to the URL
if ($match[5]==';' && (strlen($match[4])-6) >=0 && strpos($match[4],'&quot',strlen($match[4])-6)!==false && strpos(substr($match[4],0,strlen($match[4])-6),'&quot')===false) if (!empty($match[5]) && $match[5]===';' && (strlen($match[4])-6) >=0 && strpos($match[4],'&quot',strlen($match[4])-6)!==false && strpos(substr($match[4],0,strlen($match[4])-6),'&quot')===false)
{ {
$match[4] = substr($match[4],0,strpos($match[4],'&quot',strlen($match[4])-6)); $match[4] = substr($match[4],0,strpos($match[4],'&quot',strlen($match[4])-6));
$additionalQuote = "&quot;"; $additionalQuote = "&quot;";
} }
// if there is quoted stuff within the URL then we have at least one more &quot; in match[4], so chance is the last &quot is matched by the one within // if there is quoted stuff within the URL then we have at least one more &quot; in match[4], so chance is the last &quot is matched by the one within
if ($match[5]==';' && (strlen($match[4])-6) >=0 && strpos($match[4],'&quot',strlen($match[4])-6)!==false && strpos(substr($match[4],0,strlen($match[4])-6),'&quot')!==false) if (!empty($match[5]) && $match[5]===';' && (strlen($match[4])-6) >=0 && strpos($match[4],'&quot',strlen($match[4])-6)!==false && strpos(substr($match[4],0,strlen($match[4])-6),'&quot')!==false)
{ {
$match[4] .= $match[5]; $match[4] .= $match[5];
} }
if ($match[5]==';'&&$match[4]=="&quot") if (!empty($match[5]) && $match[5]===';' && $match[4]==="&quot")
{ {
$match[4] =''; $match[4] ='';
$additionalQuote = "&quot;"; $additionalQuote = "&quot;";
@ -101,18 +101,18 @@ class Html
$result4 = preg_replace_callback( $Expr, function ($match) { $result4 = preg_replace_callback( $Expr, function ($match) {
//error_log(__METHOD__.__LINE__.array2string($match)); //error_log(__METHOD__.__LINE__.array2string($match));
$match += [null,null,null,null]; $match += [null,null,null,null];
if ($match[4]==';' && (strlen($match[3])-4) >=0 && strpos($match[3],'&gt',strlen($match[3])-4)!==false) if (!empty($match[4]) && $match[4]===';' && (strlen($match[3])-4) >=0 && strpos($match[3],'&gt',strlen($match[3])-4)!==false)
{ {
$match[3] = substr($match[3],0,strpos($match[3],'&gt',strlen($match[3])-4)); $match[3] = substr($match[3],0,strpos($match[3],'&gt',strlen($match[3])-4));
$match[4] = "&gt;"; $match[4] = "&gt;";
} }
if ($match[4]==';'&&$match[3]=="&gt") if (!empty($match[4]) && $match[4]===';' && $match[3]=="&gt")
{ {
$match[3] =''; $match[3] ='';
$match[4] = "&gt;"; $match[4] = "&gt;";
} }
//error_log(__METHOD__.__LINE__.array2string($match)); //error_log(__METHOD__.__LINE__.array2string($match));
return $match[1]."<a href=\"https://www".$match[2].$match[3]."\" target=\"_blank\">"."www".$match[2].$match[3]."</a>".$match[4]; return $match[1]."<a href=\"https://www".$match[2].$match[3]."\" target=\"_blank\">"."www".$match[2].$match[3]."</a>".($match[4]??'');
}, $result3 ); }, $result3 );
} }
return $result4; return $result4;

View File

@ -182,7 +182,7 @@ class Image
} }
$app_map =& $map['vfs']; $app_map =& $map['vfs'];
if (true) $app_map = array(); if (true) $app_map = array();
if (($dir = $GLOBALS['egw_info']['server']['vfs_image_dir']) && Vfs::file_exists($dir) && Vfs::is_readable($dir)) if (!empty($dir = $GLOBALS['egw_info']['server']['vfs_image_dir']) && Vfs::file_exists($dir) && Vfs::is_readable($dir))
{ {
foreach(Vfs::find($dir) as $img) foreach(Vfs::find($dir) as $img)
{ {

View File

@ -1527,7 +1527,7 @@ class Link extends Link\Storage
{ {
self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data); self::notify('update',$link['app'],$link['id'],$app,$id,$link_id,$data);
} }
if($data[Link::OLD_LINK_TITLE] && Json\Response::isJSONResponse()) if (!empty($data[Link::OLD_LINK_TITLE]) && Json\Response::isJSONResponse())
{ {
// Update client side with new title // Update client side with new title
Json\Response::get()->apply('egw.link_title_callback',array(array($app => array($id => self::title($app, $id))))); Json\Response::get()->apply('egw.link_title_callback',array(array($app => array($id => self::title($app, $id)))));

View File

@ -213,7 +213,7 @@ class Mail
} }
if ($_oldImapServerObject instanceof Mail\Imap) if ($_oldImapServerObject instanceof Mail\Imap)
{ {
if (!is_object(self::$instances[$_profileID])) if (!isset(self::$instances[$_profileID]))
{ {
self::$instances[$_profileID] = new Mail('utf-8',false,$_profileID,false,$_reuseCache); self::$instances[$_profileID] = new Mail('utf-8',false,$_profileID,false,$_reuseCache);
} }
@ -1113,7 +1113,7 @@ class Mail
//error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_sent)); //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_sent));
//error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_draft)); //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_draft));
//error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_template)); //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_template));
self::$specialUseFolders = $_specialUseFolders[$this->icServer->ImapServerId]; self::$specialUseFolders = $_specialUseFolders[$this->icServer->ImapServerId] ?? [];
if (isset($_specialUseFolders[$this->icServer->ImapServerId]) && !empty($_specialUseFolders[$this->icServer->ImapServerId])) if (isset($_specialUseFolders[$this->icServer->ImapServerId]) && !empty($_specialUseFolders[$this->icServer->ImapServerId]))
return $_specialUseFolders[$this->icServer->ImapServerId]; return $_specialUseFolders[$this->icServer->ImapServerId];
$_specialUseFolders[$this->icServer->ImapServerId]=array(); $_specialUseFolders[$this->icServer->ImapServerId]=array();

View File

@ -154,10 +154,10 @@ class Imap extends Horde_Imap_Client_Socket implements Imap\PushIface
$this->params = $params; $this->params = $params;
$this->isAdminConnection = $_adminConnection; $this->isAdminConnection = $_adminConnection;
$this->enableSieve = (boolean)$this->params['acc_sieve_enabled']; $this->enableSieve = (boolean)$this->params['acc_sieve_enabled'];
$this->loginType = $this->params['acc_imap_logintype']; $this->loginType = $this->params['acc_imap_logintype'] ?? null;
$this->domainName = $this->params['acc_domain']; $this->domainName = $this->params['acc_domain'] ?? null;
if (is_null($_timeout)) $_timeout = $this->params['acc_imap_timeout']?$this->params['acc_imap_timeout']:self::getTimeOut (); if (is_null($_timeout)) $_timeout = $this->params['acc_imap_timeout']??self::getTimeOut ();
// Horde use locale for translation of error messages // Horde use locale for translation of error messages
// need to set LC_CTYPE for charachter classification (eg. Umlauts) // need to set LC_CTYPE for charachter classification (eg. Umlauts)

View File

@ -1554,7 +1554,7 @@ class Session
{ {
foreach(explode('&', $extravars) as $expr) foreach(explode('&', $extravars) as $expr)
{ {
list($var,$val) = explode('=', $expr,2); list($var,$val) = explode('=', $expr,2)+[null,null];
if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val); // make sure to not double encode & if (strpos($val,'%26') != false) $val = str_replace('%26','&',$val); // make sure to not double encode &
if (substr($var,-2) == '[]') if (substr($var,-2) == '[]')
{ {

View File

@ -965,7 +965,7 @@ class Base
} }
} }
$num_rows = 0; // as spec. in max_matches in the user-prefs $num_rows = 0; // as spec. in max_matches in the user-prefs
if (is_array($start)) list($start,$num_rows) = $start; if (is_array($start)) list($start,$num_rows) = $start+[null,null];
// fix GROUP BY clause to contain all non-aggregate selected columns // fix GROUP BY clause to contain all non-aggregate selected columns
if ($order_by && stripos($order_by,'GROUP BY') !== false) if ($order_by && stripos($order_by,'GROUP BY') !== false)
@ -1106,8 +1106,8 @@ class Base
$query[$db_col] = ''; $query[$db_col] = '';
} }
} }
elseif ($wildcard || $criteria[$col][0] == '!' || elseif ($wildcard || is_string($criteria[$col]) && ($criteria[$col][0] == '!' ||
is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false)) (strpos($criteria[$col],'*') !== false || strpos($criteria[$col],'?') !== false)))
{ {
// if search pattern alread contains a wildcard, do NOT add further ones automatic // if search pattern alread contains a wildcard, do NOT add further ones automatic
if (is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false)) if (is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false))

View File

@ -62,7 +62,7 @@ class History
$this->appname = $appname ?: $GLOBALS['egw_info']['flags']['currentapp']; $this->appname = $appname ?: $GLOBALS['egw_info']['flags']['currentapp'];
$this->user = $user ?: $GLOBALS['egw_info']['user']['account_id']; $this->user = $user ?: $GLOBALS['egw_info']['user']['account_id'];
if(is_object($GLOBALS['egw_setup']->db)) if (isset($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']->db))
{ {
$this->db = $GLOBALS['egw_setup']->db; $this->db = $GLOBALS['egw_setup']->db;
} }

View File

@ -1723,7 +1723,7 @@ abstract class Merge
{ {
// If we send the real content it can result in infinite loop of lookups // If we send the real content it can result in infinite loop of lookups
// so we send only the used fields // so we send only the used fields
$content = $expand_sub_cfs[$field] ? $expand_sub_cfs[$field] : ''; $content = $expand_sub_cfs[$field] ?? $matches[0][$index];
$app_replacements[$field] = $this->get_app_replacements($field_app, $values['#' . $field], $content); $app_replacements[$field] = $this->get_app_replacements($field_app, $values['#' . $field], $content);
} }
$replacements[$placeholders[$index]] = $app_replacements[$field]['$$' . $sub[$index] . '$$']; $replacements[$placeholders[$index]] = $app_replacements[$field]['$$' . $sub[$index] . '$$'];
@ -1793,7 +1793,7 @@ abstract class Merge
public function get_app_replacements($app, $id, $content, $prefix = '') public function get_app_replacements($app, $id, $content, $prefix = '')
{ {
$replacements = array(); $replacements = array();
if(!$app || $id || !$content) if(!$app || !$id || !$content)
{ {
return $replacements; return $replacements;
} }
@ -1820,7 +1820,7 @@ abstract class Merge
// Don't break merge, just log it // Don't break merge, just log it
error_log($e->getMessage()); error_log($e->getMessage());
} }
return $replacements; return $replacements ?: [];
} }
/** /**
@ -2607,7 +2607,7 @@ abstract class Merge
$action_base, $action_base,
array( array(
'icon' => Api\Vfs::mime_icon($file['mime']), 'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\Vfs::decodePath($file['name']), 'caption' => Api\Vfs::decodePath(Api\Vfs::basename($file['name'])),
'onExecute' => 'javaScript:app.' . $GLOBALS['egw_info']['flags']['currentapp'] . '.merge', 'onExecute' => 'javaScript:app.' . $GLOBALS['egw_info']['flags']['currentapp'] . '.merge',
'merge_data' => $edit_attributes 'merge_data' => $edit_attributes
), ),
@ -2832,7 +2832,7 @@ abstract class Merge
); );
// Check for a configured preferred directory // Check for a configured preferred directory
if(($pref = $GLOBALS['egw_info']['user']['preferences'][$this->get_app()][Merge::PREF_STORE_LOCATION]) && Vfs::is_writable($pref)) if(!empty($pref = $GLOBALS['egw_info']['user']['preferences'][$this->get_app()][Merge::PREF_STORE_LOCATION]) && Vfs::is_writable($pref))
{ {
$target = $pref; $target = $pref;
} }

View File

@ -435,7 +435,7 @@ abstract class Tracking
$this->historylog = new History($this->app, $this->user); $this->historylog = new History($this->app, $this->user);
} }
// log user-agent and session-action // log user-agent and session-action
if ($GLOBALS['egw_info']['server']['log_user_agent_action'] && ($changed_fields || !$old)) if (!empty($GLOBALS['egw_info']['server']['log_user_agent_action']) && ($changed_fields || !$old))
{ {
$this->historylog->add('user_agent_action', $data[$this->id_field], $this->historylog->add('user_agent_action', $data[$this->id_field],
$_SERVER['HTTP_USER_AGENT'], $_SESSION[Api\Session::EGW_SESSION_VAR]['session_action']); $_SERVER['HTTP_USER_AGENT'], $_SESSION[Api\Session::EGW_SESSION_VAR]['session_action']);
@ -509,7 +509,7 @@ abstract class Tracking
$changed_fields = array(); $changed_fields = array();
foreach($this->field2history as $name => $status) foreach($this->field2history as $name => $status)
{ {
if (!$old[$name] && !$data[$name]) continue; // treat all sorts of empty equally if (empty($old[$name]) && empty($data[$name])) continue; // treat all sorts of empty equally
if ($name[0] == '#' && !isset($data[$name])) continue; // no set customfields are not stored, therefore not changed if ($name[0] == '#' && !isset($data[$name])) continue; // no set customfields are not stored, therefore not changed
@ -544,7 +544,7 @@ abstract class Tracking
} }
foreach($data as $name => $value) foreach($data as $name => $value)
{ {
if ($name[0] == '#' && $name[1] == '#' && $value !== $old[$name]) if (isset($name[0]) && $name[0] == '#' && $name[1] == '#' && $value !== $old[$name])
{ {
$changed_fields[] = $name; $changed_fields[] = $name;
} }
@ -843,7 +843,7 @@ abstract class Tracking
$notification->set_reply_to($reply_to); $notification->set_reply_to($reply_to);
$notification->set_subject($subject); $notification->set_subject($subject);
$notification->set_links(array($link)); $notification->set_links(array($link));
$notification->set_popupdata($link['app'], $link); $notification->set_popupdata($link?$link['app']:null, $link);
if ($attachments && is_array($attachments)) if ($attachments && is_array($attachments))
{ {
$notification->set_attachments($attachments); $notification->set_attachments($attachments);
@ -938,9 +938,9 @@ abstract class Tracking
$sender = $name ? $name.' <'.$email.'>' : $email; $sender = $name ? $name.' <'.$email.'>' : $email;
} }
} }
elseif(!$sender) elseif (!$sender)
{ {
$sender = 'EGroupware '.lang($this->app).' <noreply@'.$GLOBALS['egw_info']['server']['mail_suffix'].'>'; $sender = 'EGroupware '.lang($this->app).' <noreply@'.($GLOBALS['egw_info']['server']['mail_suffix']??'nodomain.org').'>';
} }
//echo "<p>".__METHOD__."()='".htmlspecialchars($sender)."'</p>\n"; //echo "<p>".__METHOD__."()='".htmlspecialchars($sender)."'</p>\n";
return $sender; return $sender;

View File

@ -283,7 +283,7 @@ class Translation
self::add_app($apps); self::add_app($apps);
} }
$phrase = static::translate($message, $vars); $phrase = static::translate($message, $vars);
if($old_lang) if (!empty($old_lang))
{ {
$GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $old_lang; $GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $old_lang;
self::init(true); self::init(true);

View File

@ -1,4 +1,5 @@
<?php <?php /** @noinspection ALL */
/** /**
* EGroupware API: VFS - static methods to use the new eGW virtual file system * EGroupware API: VFS - static methods to use the new eGW virtual file system
* *
@ -427,7 +428,7 @@ class Vfs extends Vfs\Base
self::_check_add($options,$path,$result); self::_check_add($options,$path,$result);
} }
if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 && if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 &&
$options['depth'] < $options['maxdepth'])) && ($options['depth'] ?? 0) < $options['maxdepth'])) &&
($dir = @opendir($path, $context))) ($dir = @opendir($path, $context)))
{ {
while(($fname = readdir($dir)) !== false) while(($fname = readdir($dir)) !== false)
@ -1296,7 +1297,7 @@ class Vfs extends Vfs\Base
$b = explode('/',$b_str); $b = explode('/',$b_str);
$ret = implode('/',array_merge($a,$b)); $ret = implode('/',array_merge($a,$b));
} }
return $ret.($query ? (strpos($url,'?')===false ? '?' : '&').$query : ''); return $ret.(isset($query) ? (strpos($url,'?')===false ? '?' : '&').$query : '');
} }
/** /**
@ -1662,39 +1663,57 @@ class Vfs extends Vfs\Base
* checkLock() helper * checkLock() helper
* *
* @param string $url url or path, lock is granted for the path only, but url is used for access checks * @param string $url url or path, lock is granted for the path only, but url is used for access checks
* @return array|boolean false if there's no lock, else array with lock info * @param int $depth=0 currently only 0 or >0 = infinit/whole tree is evaluated
* @return array[]|array|boolean $depth > 0: array of path => lock info arrays for $depth > 0
* $depth=0: false if there's no lock, else array with lock info
*/ */
static function checkLock($url) static function checkLock($url, int $depth=0)
{ {
$path = self::parse_url($url, PHP_URL_PATH); $path = self::parse_url($url, PHP_URL_PATH);
if (isset(self::$lock_cache[$path])) if (!$depth && isset(self::$lock_cache[$path]))
{ {
if (self::LOCK_DEBUG) error_log(__METHOD__."($url) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$url],true))); if (self::LOCK_DEBUG) error_log(__METHOD__."($url) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$url],true)));
return self::$lock_cache[$path]; return self::$lock_cache[$path];
} }
$where = 'lock_path='.self::$db->quote($path); if ($depth > 0)
{
$where = ['lock_path LIKE '.self::$db->quote($path.'%')];
}
else
{
$where = 'lock_path='.self::$db->quote($path);
}
// ToDo: additional check parent dirs for locks and children of the requested directory // ToDo: additional check parent dirs for locks and children of the requested directory
//$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%'); //$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%');
// ToDo: shared locks can return multiple rows // ToDo: shared locks can return multiple rows
if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch())) $results = [];
foreach(self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__) as $result)
{ {
$result = Db::strip_array_keys($result,'lock_'); $result = Db::strip_array_keys($result, 'lock_');
$result['type'] = Db::from_bool($result['write']) ? 'write' : 'read'; $result['type'] = Db::from_bool($result['write']) ? 'write' : 'read';
$result['scope'] = Db::from_bool($result['exclusive']) ? 'exclusive' : 'shared'; $result['scope'] = Db::from_bool($result['exclusive']) ? 'exclusive' : 'shared';
$result['depth'] = Db::from_bool($result['recursive']) ? 'infinite' : 0; $result['depth'] = Db::from_bool($result['recursive']) ? 'infinite' : 0;
} if ($result['expires'] < time()) // lock is expired --> remove it
if ($result && $result['expires'] < time()) // lock is expired --> remove it {
{ self::$db->delete(self::LOCK_TABLE, array(
self::$db->delete(self::LOCK_TABLE,array( 'lock_path' => $result['path'],
'lock_path' => $result['path'], 'lock_token' => $result['token'],
'lock_token' => $result['token'], ), __LINE__, __FILE__);
),__LINE__,__FILE__);
if (self::LOCK_DEBUG) error_log(__METHOD__."($url) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed"); if (self::LOCK_DEBUG) error_log(__METHOD__ . "($url) lock is expired at " . date('Y-m-d H:i:s', $result['expires']) . " --> removed");
$result = false; $result = false;
}
else
{
if ($result['path'] === $path || str_starts_with($result['path'], $path))
{
$results[$result['path']] = $result;
}
self::$lock_cache[$result['path']] = $result;
}
} }
if (self::LOCK_DEBUG) error_log(__METHOD__."($url) returns ".($result?array2string($result):'false')); if (self::LOCK_DEBUG) error_log(__METHOD__."($url, $depth) returns ".array2string($depth ? $result : ($result ?? false)));
return self::$lock_cache[$path] = $result; return $depth ? $results : ($result ?? false);
} }
/** /**
@ -2043,7 +2062,7 @@ class Vfs extends Vfs\Base
} }
else else
{ {
$ret = ($context ? copy($tmp_name, self::PREFIX.$target, $context) : $ret = (isset($context) ? copy($tmp_name, self::PREFIX.$target, $context) :
copy($tmp_name, self::PREFIX.$target)) ? copy($tmp_name, self::PREFIX.$target)) ?
self::stat($target) : false; self::stat($target) : false;
} }

View File

@ -573,7 +573,7 @@ class Base
{ {
return __CLASS__; return __CLASS__;
} }
list($app, $app_scheme) = explode('.', $scheme); list($app, $app_scheme) = explode('.', $scheme)+[null,null];
foreach(array( foreach(array(
empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\' . ucfirst($scheme) . '\\StreamWrapper' : // streamwrapper in Api\Vfs empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\' . ucfirst($scheme) . '\\StreamWrapper' : // streamwrapper in Api\Vfs
'EGroupware\\' . ucfirst($app) . '\\Vfs\\' . ucfirst($app_scheme) . '\\StreamWrapper', 'EGroupware\\' . ucfirst($app) . '\\Vfs\\' . ucfirst($app_scheme) . '\\StreamWrapper',

View File

@ -262,7 +262,7 @@ class StreamWrapper extends LinksParent
if($path[0] != '/') if($path[0] != '/')
{ {
if (strpos($path,'?') !== false) $query = Vfs::parse_url($path,PHP_URL_QUERY); if (strpos($path,'?') !== false) $query = Vfs::parse_url($path,PHP_URL_QUERY);
$path = Vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); $path = Vfs::parse_url($path,PHP_URL_PATH).(!empty($query) ? '?'.$query : '');
} }
list(,$apps,$app,$id) = explode('/',$path); list(,$apps,$app,$id) = explode('/',$path);

View File

@ -310,7 +310,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
{ {
if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)");
// if creating a new file as root eg. via (docker exec) filemanager/cli.php do NOT create files unreadable by webserver // if creating a new file as root eg. via (docker exec) filemanager/cli.php do NOT create files unreadable by webserver
if ($new_file && function_exists('posix_getuid') && !posix_getuid()) if (!empty($new_file) && function_exists('posix_getuid') && !posix_getuid())
{ {
umask(0666); umask(0666);
} }
@ -1628,7 +1628,7 @@ GROUP BY A.fs_id';
// first check our stat-cache for the ids // first check our stat-cache for the ids
foreach(self::$stat_cache as $path => $stat) foreach(self::$stat_cache as $path => $stat)
{ {
if (($key = array_search($stat['fs_id'],$ids)) !== false) if ($stat && ($key = array_search($stat['fs_id'],$ids)) !== false)
{ {
$pathes[$stat['fs_id']] = $path; $pathes[$stat['fs_id']] = $path;
unset($ids[$key]); unset($ids[$key]);
@ -1948,7 +1948,7 @@ GROUP BY A.fs_id';
} }
if (!is_array($path_ids)) if (!is_array($path_ids))
{ {
$props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array(); // return empty array for no props $props = isset($row) && !empty($props[$row['fs_id']]) ? $props[$row['fs_id']] : []; // return empty array for no props
} }
elseif ($props && isset($stat) && is_array($id2path = self::id2path(array_keys($props)))) // need to map fs_id's to pathes elseif ($props && isset($stat) && is_array($id2path = self::id2path(array_keys($props)))) // need to map fs_id's to pathes
{ {

View File

@ -209,7 +209,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
// are we requested to treat the opened file as new file (only for files opened NOT for reading) // are we requested to treat the opened file as new file (only for files opened NOT for reading)
if ($mode[0] != 'r' && !$this->opened_stream_is_new && $this->context && if ($mode[0] != 'r' && !$this->opened_stream_is_new && $this->context &&
($opts = stream_context_get_options($this->context)) && ($opts = stream_context_get_options($this->context)) &&
$opts['options'][self::SCHEME]['treat_as_new']) !empty($opts['options'][self::SCHEME]['treat_as_new']))
{ {
$this->opened_stream_is_new = true; $this->opened_stream_is_new = true;
//error_log(__METHOD__."($path,$mode,...) stat=$stat, context=".array2string($opts)." --> ".array2string($this->opened_stream_is_new)); //error_log(__METHOD__."($path,$mode,...) stat=$stat, context=".array2string($opts)." --> ".array2string($this->opened_stream_is_new));
@ -994,7 +994,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
if (Vfs::$user != $GLOBALS['egw_info']['user']['account_id']) if (Vfs::$user != $GLOBALS['egw_info']['user']['account_id'])
{ {
$prefs = new Api\Preferences(Vfs::$user); $prefs = new Api\Preferences(Vfs::$user);
$vfs_fstab = $prefs->data['common']['vfs_fstab']; $vfs_fstab = $prefs->data['common']['vfs_fstab'] ?? [];
} }
else else
{ {

View File

@ -117,7 +117,7 @@ trait UserContextTrait
} }
// if we check writable and have a readonly mount --> return false, as backends dont know about r/o url parameter // if we check writable and have a readonly mount --> return false, as backends dont know about r/o url parameter
if ($check == Vfs::WRITABLE && Vfs\StreamWrapper::url_is_readonly($stat['url'])) if ($check == Vfs::WRITABLE && Vfs\StreamWrapper::url_is_readonly($stat['url'] ?? null))
{ {
//error_log(__METHOD__."(path=$path, check=writable, ...) failed because mount is readonly"); //error_log(__METHOD__."(path=$path, check=writable, ...) failed because mount is readonly");
return false; return false;
@ -125,7 +125,7 @@ trait UserContextTrait
// check if we use an EGroupwre stream wrapper, or a stock php one // check if we use an EGroupwre stream wrapper, or a stock php one
// if it's not an EGroupware one, we can NOT use uid, gid and mode! // if it's not an EGroupware one, we can NOT use uid, gid and mode!
if (($scheme = Vfs::parse_url($stat['url'], PHP_URL_SCHEME)) && !(class_exists(Vfs::scheme2class($scheme)))) if (($scheme = Vfs::parse_url($stat['url'] ?? null, PHP_URL_SCHEME)) && !(class_exists(Vfs::scheme2class($scheme))))
{ {
switch($check) switch($check)
{ {

View File

@ -26,22 +26,18 @@
</box> </box>
</box> </box>
<template template="@extra_template"/> <template template="@extra_template"/>
<details title="Common">
<description value="Common" class="group title"/> <description value="Common" class="group title"/>
<box id="common"> <box id="common">
<box id="${row}"> <box id="${row}">
<template template="api.show_replacements.placeholder_list"/> <template template="api.show_replacements.placeholder_list"/>
</box> </box>
</box> </box>
</details>
<details title="Current user">
<description value="Current user" class="group title"/> <description value="Current user" class="group title"/>
<box id="user"> <box id="user">
<box id="${row}"> <box id="${row}">
<template template="api.show_replacements.placeholder_list"/> <template template="api.show_replacements.placeholder_list"/>
</box> </box>
</box> </box>
</details>
</vbox> </vbox>
<styles> <styles>
.et2_details_title, .title { .et2_details_title, .title {
@ -60,6 +56,17 @@
#api-show_replacements_title:first-letter, .title { #api-show_replacements_title:first-letter, .title {
text-transform: capitalize; text-transform: capitalize;
} }
div#api-show_replacements_placeholders, #api-show_replacements_common, #api-show_replacements_user {
display: flex;
flex-wrap: wrap;
gap: 10px 20px;
}
div[id^="api-show_replacements"] {
min-width: 350px;
}
table#api-show_replacements_placeholders > tbody > tr > td:first-child {
padding-right: 5em;
}
</styles> </styles>
</template> </template>
</overlay> </overlay>

View File

@ -1356,7 +1356,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
} }
// from now on we deal with exceptions // from now on we deal with exceptions
$org_recurrence = $org_recurrences[$recurrence['recurrence']]; $org_recurrence = isset($recurrence['recurrence']) ? $org_recurrences[$recurrence['recurrence']] : null;
if (isset($org_recurrence)) // already existing recurrence if (isset($org_recurrence)) // already existing recurrence
{ {
//error_log(__METHOD__.'() setting id #'.$org_recurrence['id']).' for '.$recurrence['recurrence'].' = '.date('Y-m-d H:i:s',$recurrence['recurrence']); //error_log(__METHOD__.'() setting id #'.$org_recurrence['id']).' for '.$recurrence['recurrence'].' = '.date('Y-m-d H:i:s',$recurrence['recurrence']);

View File

@ -33,7 +33,6 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist
public function beforeSendToClient($cname, array $expand=null) public function beforeSendToClient($cname, array $expand=null)
{ {
Framework::includeJS('.','et2_widget_owner','calendar');
Framework::includeCSS('calendar','calendar'); Framework::includeCSS('calendar','calendar');
$bo = new calendar_bo(); $bo = new calendar_bo();
@ -139,6 +138,9 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist
*/ */
public static function ajax_owner($id = null) public static function ajax_owner($id = null)
{ {
// close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session();
// Handle a request for a single ID // Handle a request for a single ID
if($id && !is_array($id)) if($id && !is_array($id))
{ {

View File

@ -785,7 +785,7 @@ class calendar_so
} }
if (!empty($params['sql_filter'])) if (!empty($params['sql_filter']))
{ {
if (is_string($params['sql_filter'])) if(is_string($params['sql_filter']))
{ {
$where[] = $params['sql_filter']; $where[] = $params['sql_filter'];
} }
@ -794,15 +794,45 @@ class calendar_so
$where = array_merge($where, $params['sql_filter']); $where = array_merge($where, $params['sql_filter']);
} }
} }
if(array_filter($where, fn($key) => str_contains($key, '#'), ARRAY_FILTER_USE_KEY))
{
$custom_fields = Api\Storage\Customfields::get('calendar');
foreach($where as $col => $data)
{
if($col[0] == '#' && $data)
{
unset($where[$col]);
$filtermethod = " $this->cal_table.cal_id IN (SELECT DISTINCT cal_id FROM $this->extra_table WHERE ";
if($custom_fields[substr($col, 1)]['type'] == 'select' && $custom_fields[substr($col, 1)]['rows'] > 1)
{
// Multi-select - any entry with the filter value selected matches
$filtermethod .= $this->db->expression($this->extra_table, array(
'cal_extra_name' => substr($col, 1),
$this->db->concat("','", 'cal_extra_value', "','") . ' ' . $this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE] . ' ' . $this->db->quote('%,' . $data . ',%'),
)) . ')';
}
else
{
$filtermethod .= $this->db->expression($this->extra_table, array(
'cal_extra_name' => substr($col, 1),
'cal_extra_value' => $data,
)) . ')';
}
$where[] = $filtermethod;
}
}
}
$useUnionQuery = $this->db->capabilities['distinct_on_text'] && $this->db->capabilities['union']; $useUnionQuery = $this->db->capabilities['distinct_on_text'] && $this->db->capabilities['union'];
if ($users)
if($users)
{ {
$users_by_type = array(); $users_by_type = array();
foreach((array)$users as $user) foreach((array)$users as $user)
{ {
if (is_numeric($user)) if(is_numeric($user))
{ {
$users_by_type['u'][] = (int) $user; $users_by_type['u'][] = (int)$user;
} }
else else
{ {

View File

@ -408,14 +408,18 @@ class calendar_uilist extends calendar_ui
$col_filter = array(); $col_filter = array();
foreach($params['col_filter'] as $name => $val) foreach($params['col_filter'] as $name => $val)
{ {
if (!in_array($name, array('participant','row_id')) && (string)$val !== '') if(!in_array($name, array('participant', 'row_id')) && (string)$val !== '')
{ {
$col_filter[$name] = $val; $col_filter[$name] = $val;
} }
else if ( $name == 'row_id' && (string)$val !== '') elseif($name == 'row_id' && (string)$val !== '')
{ {
$col_filter['cal_id'] = $val; $col_filter['cal_id'] = $val;
} }
if($name[0] == '#')
{
$search_params['cfs'][] = $name;
}
} }
// Videocalls // Videocalls
if(array_key_exists('include_videocalls',$params['col_filter'])) if(array_key_exists('include_videocalls',$params['col_filter']))

View File

@ -31,7 +31,6 @@ class calendar_wizard_import_ical
*/ */
function __construct() function __construct()
{ {
Api\Framework::includeJS('.','et2_widget_owner','calendar');
Api\Framework::includeCSS('calendar','calendar'); Api\Framework::includeCSS('calendar','calendar');
$this->steps = array( $this->steps = array(
'wizard_step55' => lang('Edit conditions'), 'wizard_step55' => lang('Edit conditions'),
@ -73,7 +72,7 @@ class calendar_wizard_import_ical
$content['step'] = 'wizard_step55'; $content['step'] = 'wizard_step55';
foreach(array('skip_conflicts','empty_before_import','remove_past','remove_future','override_values') as $field) foreach(array('skip_conflicts','empty_before_import','remove_past','remove_future','override_values') as $field)
{ {
if(!$content[$field] && array_key_exists($field, $content['plugin_options'])) if(!$content[$field] && is_array($content['plugin_options']) && array_key_exists($field, $content['plugin_options']))
{ {
$content[$field] = $content['plugin_options'][$field]; $content[$field] = $content['plugin_options'][$field];
} }

View File

@ -22,7 +22,7 @@ export abstract class View
* @param {Object} state * @param {Object} state
* @returns {string} * @returns {string}
*/ */
static header(state) public static header(state)
{ {
let formatDate = new Date(state.date); let formatDate = new Date(state.date);
formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000);
@ -34,7 +34,7 @@ export abstract class View
* *
* @param {object} state * @param {object} state
*/ */
static _owner(state) public static _owner(state)
{ {
let owner = ''; let owner = '';
if(state.owner.length && state.owner.length == 1 && app.calendar.sidebox_et2) if(state.owner.length && state.owner.length == 1 && app.calendar.sidebox_et2)
@ -53,7 +53,7 @@ export abstract class View
* @param {Object} state * @param {Object} state
* @returns {Date} * @returns {Date}
*/ */
static start_date(state) public static start_date(state)
{ {
const d = state.date ? new Date(state.date) : new Date(); const d = state.date ? new Date(state.date) : new Date();
d.setUTCHours(0); d.setUTCHours(0);
@ -68,7 +68,7 @@ export abstract class View
* @param {Object} state * @param {Object} state
* @returns {Date} * @returns {Date}
*/ */
static end_date(state) public static end_date(state)
{ {
const d = state.date ? new Date(state.date) : new Date(); const d = state.date ? new Date(state.date) : new Date();
d.setUTCHours(23); d.setUTCHours(23);
@ -87,7 +87,7 @@ export abstract class View
* @param {number[]|String} state state.owner List of owner IDs, or a comma seperated list * @param {number[]|String} state state.owner List of owner IDs, or a comma seperated list
* @returns {number[]|String} * @returns {number[]|String}
*/ */
static owner(state) public static owner(state)
{ {
return state.owner || 0; return state.owner || 0;
} }
@ -98,7 +98,7 @@ export abstract class View
* @param {object} state * @param {object} state
* @returns {boolean} Current preference to show 5 or 7 days in weekview * @returns {boolean} Current preference to show 5 or 7 days in weekview
*/ */
static show_weekend(state) public static show_weekend(state)
{ {
return state.weekend; return state.weekend;
} }
@ -108,27 +108,30 @@ export abstract class View
* *
* @param {object} state * @param {object} state
*/ */
static granularity(state) public static granularity(state)
{ {
var list = egw.preference('use_time_grid', 'calendar'); var list = egw.preference('use_time_grid', 'calendar');
if(list == '0' || typeof list === 'undefined') if(list == '0' || typeof list === 'undefined')
{ {
return parseInt('' + egw.preference('interval', 'calendar')) || 30; return parseInt('' + egw.preference('interval', 'calendar')) || 30;
} }
if(typeof list == 'string') list = list.split(','); if(typeof list == 'string')
{
list = list.split(',');
}
if(!(<string><unknown>list).indexOf && jQuery.isPlainObject(list)) if(!(<string><unknown>list).indexOf && jQuery.isPlainObject(list))
{ {
list = jQuery.map(list, function (el) list = jQuery.map(list, function(el)
{ {
return el; return el;
}); });
} }
return (<string[]> list).indexOf(state.view) >= 0 ? return (<string[]>list).indexOf(state.view) >= 0 ?
0 : 0 :
parseInt(<string> egw.preference('interval', 'calendar')) || 30; parseInt(<string>egw.preference('interval', 'calendar')) || 30;
} }
static extend(sub) public static extend(sub)
{ {
return jQuery.extend({}, this, {_super: this}, sub); return jQuery.extend({}, this, {_super: this}, sub);
} }
@ -140,7 +143,7 @@ export abstract class View
* forward, negative for backward * forward, negative for backward
* @returns {Date} * @returns {Date}
*/ */
static scroll(delta) public static scroll(delta)
{ {
var d = new Date(app.calendar.state.date); var d = new Date(app.calendar.state.date);
d.setUTCDate(d.getUTCDate() + (7 * delta)); d.setUTCDate(d.getUTCDate() + (7 * delta));
@ -158,27 +161,27 @@ export class day extends View
{ {
public static etemplates : (string | etemplate2)[] = ['calendar.view', 'calendar.todo']; public static etemplates : (string | etemplate2)[] = ['calendar.view', 'calendar.todo'];
static header(state) public static header(state)
{ {
var formatDate = new Date(state.date); var formatDate = new Date(state.date);
formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000);
return date('l, ', formatDate) + super.header(state); return date('l, ', formatDate) + super.header(state);
} }
static start_date(state) public static start_date(state)
{ {
var d = super.start_date(state); var d = super.start_date(state);
state.date = app.calendar.date.toString(d); state.date = app.calendar.date.toString(d);
return d; return d;
} }
static show_weekend(state) public static show_weekend(state)
{ {
state.days = '1'; state.days = '1';
return true; return true;
} }
static scroll(delta) public static scroll(delta)
{ {
var d = new Date(app.calendar.state.date); var d = new Date(app.calendar.state.date);
d.setUTCDate(d.getUTCDate() + (delta)); d.setUTCDate(d.getUTCDate() + (delta));
@ -188,7 +191,7 @@ export class day extends View
export class day4 extends View export class day4 extends View
{ {
static end_date(state) public static end_date(state)
{ {
var d = super.end_date(state); var d = super.end_date(state);
state.days = '4'; state.days = '4';
@ -199,13 +202,13 @@ export class day4 extends View
return d; return d;
} }
static show_weekend(state) public static show_weekend(state)
{ {
state.weekend = 'true'; state.weekend = 'true';
return true; return true;
} }
static scroll(delta) public static scroll(delta)
{ {
var d = new Date(app.calendar.state.date); var d = new Date(app.calendar.state.date);
d.setUTCDate(d.getUTCDate() + (4 * delta)); d.setUTCDate(d.getUTCDate() + (4 * delta));
@ -215,7 +218,7 @@ export class day4 extends View
export class week extends View export class week extends View
{ {
static header(state) public static header(state)
{ {
var end_date = state.last; var end_date = state.last;
if(!week.show_weekend(state)) if(!week.show_weekend(state))
@ -228,12 +231,12 @@ export class week extends View
app.calendar.date.long_date(state.first, end_date); app.calendar.date.long_date(state.first, end_date);
} }
static start_date(state) public static start_date(state)
{ {
return app.calendar.date.start_of_week(super.start_date(state)); return app.calendar.date.start_of_week(super.start_date(state));
} }
static end_date(state) public static end_date(state)
{ {
var d = app.calendar.date.start_of_week(state.date || new Date()); var d = app.calendar.date.start_of_week(state.date || new Date());
// Always 7 days, we just turn weekends on or off // Always 7 days, we just turn weekends on or off
@ -247,7 +250,7 @@ export class week extends View
export class weekN extends View export class weekN extends View
{ {
static header(state) public static header(state)
{ {
return super._owner(state) + app.calendar.egw.lang('Week') + ' ' + return super._owner(state) + app.calendar.egw.lang('Week') + ' ' +
app.calendar.date.week_number(state.first) + ' - ' + app.calendar.date.week_number(state.first) + ' - ' +
@ -255,12 +258,12 @@ export class weekN extends View
app.calendar.date.long_date(state.first, state.last); app.calendar.date.long_date(state.first, state.last);
} }
static start_date(state) public static start_date(state)
{ {
return app.calendar.date.start_of_week(super.start_date(state)); return app.calendar.date.start_of_week(super.start_date(state));
} }
static end_date(state) public static end_date(state)
{ {
state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview', 'calendar') || 7); state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview', 'calendar') || 7);
@ -273,21 +276,21 @@ export class weekN extends View
export class month extends View export class month extends View
{ {
static header(state) public static header(state)
{ {
var formatDate = new Date(state.date); var formatDate = new Date(state.date);
formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000);
return super._owner(state) + app.calendar.egw.lang(date('F', formatDate)) + ' ' + date('Y', formatDate); return super._owner(state) + app.calendar.egw.lang(date('F', formatDate)) + ' ' + date('Y', formatDate);
} }
static start_date(state) public static start_date(state)
{ {
var d = super.start_date(state); var d = super.start_date(state);
d.setUTCDate(1); d.setUTCDate(1);
return app.calendar.date.start_of_week(d); return app.calendar.date.start_of_week(d);
} }
static end_date(state) public static end_date(state)
{ {
var d = super.end_date(state); var d = super.end_date(state);
d = new Date(d.getFullYear(), d.getUTCMonth() + 1, 1, 0, -d.getTimezoneOffset(), 0); d = new Date(d.getFullYear(), d.getUTCMonth() + 1, 1, 0, -d.getTimezoneOffset(), 0);
@ -295,7 +298,7 @@ export class month extends View
return app.calendar.date.end_of_week(d); return app.calendar.date.end_of_week(d);
} }
static scroll(delta) public static scroll(delta)
{ {
var d = new Date(app.calendar.state.date); var d = new Date(app.calendar.state.date);
// Set day to 15 so we don't get overflow on short months // Set day to 15 so we don't get overflow on short months
@ -310,7 +313,7 @@ export class planner extends View
{ {
public static etemplates : (string | etemplate2)[] = ['calendar.planner']; public static etemplates : (string | etemplate2)[] = ['calendar.planner'];
static header(state) public static header(state)
{ {
var startDate = new Date(state.first); var startDate = new Date(state.first);
startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000); startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000);
@ -321,17 +324,17 @@ export class planner extends View
(startDate == endDate ? '' : ' - ' + date(<string>egw.preference('dateformat'), endDate)); (startDate == endDate ? '' : ' - ' + date(<string>egw.preference('dateformat'), endDate));
} }
static group_by(state) public static group_by(state)
{ {
return state.sortby ? state.sortby : 0; return state.sortby ? state.sortby : 0;
} }
// Note: Planner uses the additional value of planner_view to determine // Note: Planner uses the additional value of planner_view to determine
// the start & end dates using other view's functions // the start & end dates using other view's functions
static start_date(state) public static start_date(state)
{ {
// Start here, in case we can't find anything better // Start here, in case we can't find anything better
var d = super.start_date( state); var d = super.start_date(state);
if(state.sortby && state.sortby === 'month') if(state.sortby && state.sortby === 'month')
{ {
@ -353,10 +356,10 @@ export class planner extends View
return d; return d;
} }
static end_date(state) public static end_date(state)
{ {
var d = super.end_date( state); var d = super.end_date(state);
if(state.sortby && state.sortby === 'month') if(state.sortby && state.sortby === 'month')
{ {
d.setUTCDate(0); d.setUTCDate(0);
@ -379,13 +382,13 @@ export class planner extends View
return d; return d;
} }
static hide_empty(state) public static hide_empty(state)
{ {
var check = state.sortby == 'user' ? ['user', 'both'] : ['cat', 'both']; var check = state.sortby == 'user' ? ['user', 'both'] : ['cat', 'both'];
return (check.indexOf(egw.preference('planner_show_empty_rows', 'calendar') + '') === -1); return (check.indexOf(egw.preference('planner_show_empty_rows', 'calendar') + '') === -1);
} }
static scroll(delta) public static scroll(delta)
{ {
if(app.calendar.state.planner_view && !isNaN(delta) && app.calendar.state.sortby !== "month") if(app.calendar.state.planner_view && !isNaN(delta) && app.calendar.state.sortby !== "month")
{ {
@ -425,7 +428,7 @@ export class listview extends View
{ {
public static etemplates : (string | etemplate2)[] = ['calendar.list']; public static etemplates : (string | etemplate2)[] = ['calendar.list'];
static header(state) public static header(state)
{ {
var startDate = new Date(state.first || state.date); var startDate = new Date(state.first || state.date);
startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000); startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000);

View File

@ -29,6 +29,7 @@ import {day, day4, listview, month, planner, week, weekN} from "./View";
import {et2_calendar_view} from "./et2_widget_view"; import {et2_calendar_view} from "./et2_widget_view";
import {et2_calendar_timegrid} from "./et2_widget_timegrid"; import {et2_calendar_timegrid} from "./et2_widget_timegrid";
import {et2_calendar_daycol} from "./et2_widget_daycol"; import {et2_calendar_daycol} from "./et2_widget_daycol";
import {et2_calendar_planner} from "./et2_widget_planner";
import {et2_calendar_planner_row} from "./et2_widget_planner_row"; import {et2_calendar_planner_row} from "./et2_widget_planner_row";
import {et2_calendar_event} from "./et2_widget_event"; import {et2_calendar_event} from "./et2_widget_event";
import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog"; import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog";
@ -256,10 +257,12 @@ export class CalendarApp extends EgwApp
if(sidebox.length == 0 && egw_getFramework() != null) if(sidebox.length == 0 && egw_getFramework() != null)
{ {
// Force rollup to load owner widget, it leaves it out otherwise // Force rollup to load owner widget, it leaves it out otherwise
new et2_calendar_owner(_et2.widgetContainer,{}); new et2_calendar_owner(_et2.widgetContainer, {});
// Force rollup to load planner widget, it leaves it out otherwise
new et2_calendar_planner(_et2.widgetContainer, {});
var egw_fw = egw_getFramework(); var egw_fw = egw_getFramework();
sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv); sidebox = jQuery('#favorite_sidebox_' + this.appname, egw_fw.sidemenuDiv);
} }
var content = this.et2.getArrayMgr('content'); var content = this.et2.getArrayMgr('content');
@ -3051,21 +3054,28 @@ export class CalendarApp extends EgwApp
{ {
// Simple, easy case - just one widget for the selected time span. (planner) // Simple, easy case - just one widget for the selected time span. (planner)
// Update existing view's special attribute filters, defined in the view list // Update existing view's special attribute filters, defined in the view list
for(var updater in view) for(let updater of Object.getOwnPropertyNames(view))
{ {
if(typeof view[updater] === 'function') if(typeof view[updater] === 'function')
{ {
let value = view[updater].call(this,state.state); let value = view[updater].call(this, state.state);
if(updater === 'start_date') state.state.first = this.date.toString(value); if(updater === 'start_date')
if(updater === 'end_date') state.state.last = this.date.toString(value); {
state.state.first = this.date.toString(value);
}
if(updater === 'end_date')
{
state.state.last = this.date.toString(value);
}
// Set value // Set value
for(var i = 0; i < view.etemplates.length; i++) for(var i = 0; i < view.etemplates.length; i++)
{ {
view.etemplates[i].widgetContainer.iterateOver(function(widget) { view.etemplates[i].widgetContainer.iterateOver(function(widget)
if(typeof widget['set_'+updater] === 'function') {
if(typeof widget['set_' + updater] === 'function')
{ {
widget['set_'+updater](value); widget['set_' + updater](value);
} }
}, this, et2_calendar_view); }, this, et2_calendar_view);
} }

View File

@ -927,7 +927,7 @@ export class et2_calendar_daycol extends et2_valueWidget implements et2_IDetache
(new Date(b.options.value.end) - new Date(b.options.value.start)) - (new Date(b.options.value.end) - new Date(b.options.value.start)) -
(new Date(a.options.value.end) - new Date(a.options.value.start)); (new Date(a.options.value.end) - new Date(a.options.value.start));
return duration ? duration : (a.options.value.app_id - b.options.value.app_id); return (Math.abs(duration) > 360000) ? duration : (a.options.value.title.localeCompare(b.options.value.title));
} }
else if (a.options.value.whole_day || b.options.value.whole_day) else if (a.options.value.whole_day || b.options.value.whole_day)
{ {

View File

@ -8,8 +8,7 @@
* @author Nathan Gray * @author Nathan Gray
*/ */
import {et2_register_widget} from "../../api/js/etemplate/et2_core_widget"; import {et2_register_widget} from "../../api/js/etemplate/et2_core_widget.ts";
import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox";
import {et2_taglist_email} from "../../api/js/etemplate/et2_widget_taglist"; import {et2_taglist_email} from "../../api/js/etemplate/et2_widget_taglist";
/** /**
@ -20,7 +19,7 @@ import {et2_taglist_email} from "../../api/js/etemplate/et2_widget_taglist";
* *
* Uses MagicSuggest library * Uses MagicSuggest library
* @see http://nicolasbize.github.io/magicsuggest/ * @see http://nicolasbize.github.io/magicsuggest/
* @augments et2_selectbox * @augments et2_taglist_email
*/ */
export class et2_calendar_owner extends et2_taglist_email export class et2_calendar_owner extends et2_taglist_email
{ {

View File

@ -29,6 +29,8 @@ import {et2_compileLegacyJS} from "../../api/js/etemplate/et2_core_legacyJSFunct
import {et2_no_init} from "../../api/js/etemplate/et2_core_common"; import {et2_no_init} from "../../api/js/etemplate/et2_core_common";
import {CalendarApp} from "./app"; import {CalendarApp} from "./app";
import {sprintf} from "../../api/js/egw_action/egw_action_common.js"; import {sprintf} from "../../api/js/egw_action/egw_action_common.js";
import {et2_dataview_grid} from "../../api/js/etemplate/et2_dataview_view_grid";
import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox";
/** /**
* Class which implements the "calendar-planner" XET-Tag for displaying a longer * Class which implements the "calendar-planner" XET-Tag for displaying a longer
@ -1906,7 +1908,6 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta
} }
else else
{ {
fetch = true;
// Assume it's empty, if there is data it will be filled later // Assume it's empty, if there is data it will be filled later
egw.dataStoreUID(cache_id, []); egw.dataStoreUID(cache_id, []);
} }
@ -2375,22 +2376,26 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta
*/ */
_get_time_from_position( x,y) _get_time_from_position( x,y)
{ {
if(!this.options.start_date || !this.options.end_date)
{
return false;
}
x = Math.round(x); x = Math.round(x);
y = Math.round(y); y = Math.round(y);
// Round to user's preferred event interval // Round to user's preferred event interval
var interval = egw.preference('interval','calendar') || 30; var interval = egw.preference('interval', 'calendar') || 30;
// Relative horizontal position, as a percentage // Relative horizontal position, as a percentage
var width = 0; var width = 0;
jQuery('.calendar_eventRows',this.div).each(function() {width = Math.max(width,jQuery(this).width());}); jQuery('.calendar_eventRows', this.div).each(function() {width = Math.max(width, jQuery(this).width());});
var rel_x = Math.min(x / width,1); var rel_x = Math.min(x / width, 1);
// Relative time, in minutes from start // Relative time, in minutes from start
var rel_time = 0; var rel_time = 0;
var day_header = jQuery('.calendar_plannerScaleDay',this.headers); var day_header = jQuery('.calendar_plannerScaleDay', this.headers);
// Simple math, the x is offset from start date // Simple math, the x is offset from start date
if(this.options.group_by !== 'month' && ( if(this.options.group_by !== 'month' && (

View File

@ -125,7 +125,7 @@
"robrichards/xmlseclibs": "^3.1.1", "robrichards/xmlseclibs": "^3.1.1",
"simplesamlphp/simplesamlphp": "^1.19.0", "simplesamlphp/simplesamlphp": "^1.19.0",
"simplesamlphp/twig-configurable-i18n": "~2.3.3", "simplesamlphp/twig-configurable-i18n": "~2.3.3",
"tinymce/tinymce": "^5" "tinymce/tinymce": "5.9.*"
}, },
"require-dev": { "require-dev": {
"guzzlehttp/guzzle": "^6.5", "guzzlehttp/guzzle": "^6.5",

20
composer.lock generated
View File

@ -8493,16 +8493,16 @@
}, },
{ {
"name": "tinymce/tinymce", "name": "tinymce/tinymce",
"version": "5.5.1", "version": "5.9.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/tinymce/tinymce-dist.git", "url": "https://github.com/tinymce/tinymce-dist.git",
"reference": "a436d254bc8d62e50be1fdb3d3e98981ab0e4c40" "reference": "48c665ad12ba0e4d8068ba0784026c7488aa4746"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/tinymce/tinymce-dist/zipball/a436d254bc8d62e50be1fdb3d3e98981ab0e4c40", "url": "https://api.github.com/repos/tinymce/tinymce-dist/zipball/48c665ad12ba0e4d8068ba0784026c7488aa4746",
"reference": "a436d254bc8d62e50be1fdb3d3e98981ab0e4c40", "reference": "48c665ad12ba0e4d8068ba0784026c7488aa4746",
"shasum": "" "shasum": ""
}, },
"type": "component", "type": "component",
@ -8528,16 +8528,22 @@
"LGPL-2.1-only" "LGPL-2.1-only"
], ],
"description": "Web based JavaScript HTML WYSIWYG editor control.", "description": "Web based JavaScript HTML WYSIWYG editor control.",
"homepage": "http://www.tinymce.com", "homepage": "https://www.tiny.cloud/",
"keywords": [ "keywords": [
"editor", "contenteditable",
"editing",
"html", "html",
"javascript", "javascript",
"rich editor",
"rich text",
"rich text editor",
"richtext", "richtext",
"rte",
"text",
"tinymce", "tinymce",
"wysiwyg" "wysiwyg"
], ],
"time": "2020-10-01T05:31:22+00:00" "time": "2021-09-08T03:45:09+00:00"
}, },
{ {
"name": "twig/extensions", "name": "twig/extensions",

View File

@ -193,7 +193,7 @@ class filemanager_merge extends Api\Storage\Merge
if(is_array($link)) if(is_array($link))
{ {
// Directories have their internal protocol in path here // Directories have their internal protocol in path here
if($link['path'] && strpos($link['path'], '://') !== false) $link['path'] = $file['path']; if (!empty($link['path']) && strpos($link['path'], '://') !== false) $link['path'] = $file['path'];
$link = Api\Session::link('/index.php', $link); $link = Api\Session::link('/index.php', $link);
} }
else else
@ -215,7 +215,7 @@ class filemanager_merge extends Api\Storage\Merge
if(!$value) $value = ''; if(!$value) $value = '';
$info['$$' . ($prefix ? $prefix . '/' : '') . $key . '$$'] = $value; $info['$$' . ($prefix ? $prefix . '/' : '') . $key . '$$'] = $value;
} }
if($app_placeholders) if (!empty($app_placeholders) && is_array($app_placeholders))
{ {
$info = array_merge($app_placeholders, $info); $info = array_merge($app_placeholders, $info);
} }

View File

@ -142,58 +142,68 @@ class filemanager_ui
public static function get_actions() public static function get_actions()
{ {
$actions = array( $actions = array(
'open' => array( 'open' => array(
'caption' => lang('Open'), 'caption' => lang('Open'),
'icon' => '', 'icon' => '',
'group' => $group=1, 'group' => $group = 1,
'allowOnMultiple' => false, 'allowOnMultiple' => false,
'onExecute' => 'javaScript:app.filemanager.open', 'onExecute' => 'javaScript:app.filemanager.open',
'default' => true 'default' => true
), ),
'new' => array( 'new' => array(
'caption' => 'New', 'caption' => 'New',
'group' => $group, 'group' => $group,
'disableClass' => 'noEdit', 'disableClass' => 'noEdit',
'children' => array ( 'children' => array(
'document' => array ( 'document' => array(
'caption' => 'Document', 'caption' => 'Document',
'icon' => 'new', 'icon' => 'new',
'onExecute' => 'javaScript:app.filemanager.create_new', 'onExecute' => 'javaScript:app.filemanager.create_new',
) )
) )
), ),
'mkdir' => array( 'mkdir' => array(
'caption' => lang('Create directory'), 'caption' => lang('Create directory'),
'icon' => 'filemanager/button_createdir', 'icon' => 'filemanager/button_createdir',
'group' => $group, 'group' => $group,
'allowOnMultiple' => false, 'allowOnMultiple' => false,
'disableClass' => 'noEdit', 'disableClass' => 'noEdit',
'onExecute' => 'javaScript:app.filemanager.createdir' 'onExecute' => 'javaScript:app.filemanager.createdir'
), ),
'edit' => array( 'edit' => array(
'caption' => lang('Edit settings'), 'caption' => lang('Edit settings'),
'group' => $group, 'group' => $group,
'allowOnMultiple' => false, 'allowOnMultiple' => false,
'onExecute' => Api\Header\UserAgent::mobile()?'javaScript:app.filemanager.viewEntry':'javaScript:app.filemanager.editprefs', 'onExecute' => Api\Header\UserAgent::mobile() ? 'javaScript:app.filemanager.viewEntry' : 'javaScript:app.filemanager.editprefs',
'mobileViewTemplate' => 'file?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/filemanager/templates/mobile/file.xet')) 'mobileViewTemplate' => 'file?' . filemtime(Api\Etemplate\Widget\Template::rel2path('/filemanager/templates/mobile/file.xet'))
), ),
'saveas' => array( 'unlock' => array(
'caption' => lang('Save as'), 'caption' => lang('Unlock'),
'group' => $group, 'icon' => 'unlock',
'enableClass' => 'locked',
'group' => $group,
'allowOnMultiple' => true, 'allowOnMultiple' => true,
'icon' => 'filesave', 'hideOnDisabled' => true
'onExecute' => 'javaScript:app.filemanager.force_download', ),
'disableClass' => 'isDir', 'saveas' => array(
'enabled' => 'javaScript:app.filemanager.is_multiple_allowed', 'caption' => lang('Save as'),
'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 83, 'caption' => 'Ctrl + Shift + S'), 'group' => $group,
'allowOnMultiple' => true,
'icon' => 'filesave',
'onExecute' => 'javaScript:app.filemanager.force_download',
'disableClass' => 'isDir',
'enabled' => 'javaScript:app.filemanager.is_multiple_allowed',
'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 83,
'caption' => 'Ctrl + Shift + S'),
), ),
'saveaszip' => array( 'saveaszip' => array(
'caption' => lang('Save as ZIP'), 'caption' => lang('Save as ZIP'),
'group' => $group, 'group' => $group,
'allowOnMultiple' => true, 'allowOnMultiple' => true,
'icon' => 'save_zip', 'icon' => 'save_zip',
'postSubmit' => true, 'postSubmit' => true,
'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 90, 'caption' => 'Ctrl + Shift + Z'), 'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 90,
'caption' => 'Ctrl + Shift + Z'),
), ),
'egw_paste' => array( 'egw_paste' => array(
'enabled' => false, 'enabled' => false,
@ -815,7 +825,7 @@ class filemanager_ui
case 'createdir': case 'createdir':
$dst = Vfs::concat($dir, is_array($selected) ? $selected[0] : $selected); $dst = Vfs::concat($dir, is_array($selected) ? $selected[0] : $selected);
if (Vfs::mkdir($dst, null, STREAM_MKDIR_RECURSIVE)) if(Vfs::mkdir($dst, null, STREAM_MKDIR_RECURSIVE))
{ {
return lang("Directory successfully created."); return lang("Directory successfully created.");
} }
@ -824,16 +834,36 @@ class filemanager_ui
case 'saveaszip': case 'saveaszip':
Vfs::download_zip($selected); Vfs::download_zip($selected);
exit; exit;
case 'unlock':
foreach((array)$selected as $target)
{
$link = Vfs::concat($dir, Vfs::basename($target));
$lock = Vfs::checkLock($link);
if($lock && Vfs::unlock($link, $lock['token']))
{
$files++;
}
else
{
$errs++;
}
}
return lang('%1 files unlocked.', $files);
default: default:
list($action, $settings) = explode('_', $action, 2); list($action, $settings) = explode('_', $action, 2);
switch($action) switch($action)
{ {
case 'document': case 'document':
if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document']; if(!$settings)
{
$settings = $GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document'];
}
$document_merge = new filemanager_merge(Vfs::decodePath($dir)); $document_merge = new filemanager_merge(Vfs::decodePath($dir));
$msg = $document_merge->download($settings, $selected, '', $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir']); $msg = $document_merge->download($settings, $selected, '', $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir']);
if($msg) return $msg; if($msg)
{
return $msg;
}
$errs = count($selected); $errs = count($selected);
return false; return false;
} }
@ -982,6 +1012,13 @@ class filemanager_ui
$GLOBALS['egw']->session->commit_session(); $GLOBALS['egw']->session->commit_session();
$rows = $dir_is_writable = array(); $rows = $dir_is_writable = array();
$vfs_options = $this->get_vfs_options($query); $vfs_options = $this->get_vfs_options($query);
// query and cache locks for whole directory
$locks = [];
foreach(!empty($query['col_filter']['dir']) ? (array)$query['col_filter']['dir'] : (array)$query['path'] as $path)
{
$locks += Vfs::checkLock($path, 999);
}
$n = 0;
foreach(Vfs::find(!empty($query['col_filter']['dir']) ? $query['col_filter']['dir'] : $query['path'],$vfs_options,true) as $path => $row) foreach(Vfs::find(!empty($query['col_filter']['dir']) ? $query['col_filter']['dir'] : $query['path'],$vfs_options,true) as $path => $row)
{ {
//echo $path; _debug_array($row); //echo $path; _debug_array($row);
@ -1009,16 +1046,33 @@ class filemanager_ui
$row['class'] .= 'noExecute'; $row['class'] .= 'noExecute';
} }
} }
elseif (!$dir_is_writable[Vfs::dirname($path)]) elseif(!$dir_is_writable[Vfs::dirname($path)])
{ {
$row['class'] .= 'noEdit '; $row['class'] .= 'noEdit ';
} }
if (!empty($lock = $locks[$path]))
{
$row['locked'] = 'lock';
$row['locked_status'] = lang(
"LOCK from %1, created %2",
// Not sure why sometimes the lock is owned by a user ID, sometimes mailto:user@email
is_numeric($lock['owner']) ? $GLOBALS['egw']->accounts->username($lock['owner']) : str_replace('mailto:', '', $lock['owner']),
Api\DateTime::to(APi\DateTime::server2user($lock['created']), '')
);
if($GLOBALS['egw_info']['user']['apps']['admin'] || Vfs::$is_admin || Vfs::$is_root ||
$lock['owner'] == $GLOBALS['egw_info']['user']['account_id'] ||
$lock['owner'] == 'mailto:' . $GLOBALS['egw_info']['user']['account_email']
)
{
$row['class'] .= ' locked ';
}
}
$row['class'] .= !$dir_is_writable[$dir] ? 'noDelete' : ''; $row['class'] .= !$dir_is_writable[$dir] ? 'noDelete' : '';
$row['download_url'] = Vfs::download_url($path); $row['download_url'] = Vfs::download_url($path);
$row['gid'] = -abs($row['gid']); // gid are positive, but we use negagive account_id for groups internal $row['gid'] = -abs($row['gid']); // gid are positive, but we use negagive account_id for groups internal
foreach(['mtime','ctime'] as $date_field) foreach(['mtime', 'ctime'] as $date_field)
{ {
$row[$date_field] = Api\DateTime::server2user($row[$date_field]); $row[$date_field] = Api\DateTime::server2user($row[$date_field]);
} }

View File

@ -192,6 +192,7 @@ link target filemanager de Ziel der Verknüpfung
link target %1 not found! filemanager de Verknüpfungsziel %1 nicht gefunden! link target %1 not found! filemanager de Verknüpfungsziel %1 nicht gefunden!
list view filemanager de Listenansicht list view filemanager de Listenansicht
location filemanager de Ort location filemanager de Ort
lock filemanager de Gesperrt
log out as superuser filemanager de Superuser abmelden log out as superuser filemanager de Superuser abmelden
mail files filemanager de Dateien per E-Mail versenden mail files filemanager de Dateien per E-Mail versenden
mail paste filemanager de per E-Mail versenden mail paste filemanager de per E-Mail versenden
@ -302,6 +303,7 @@ to overwrite the existing file store again. filemanager de Zum Überschreiben de
total files filemanager de Gesamtanzahl Dateien total files filemanager de Gesamtanzahl Dateien
ui mode filemanager de Benutzeroberfläche ui mode filemanager de Benutzeroberfläche
under directory filemanager de unter dem Verzeichnis under directory filemanager de unter dem Verzeichnis
unlock filemanager de entsperren
unmount filemanager de Unmount unmount filemanager de Unmount
unused space filemanager de Nicht benutzter Platz unused space filemanager de Nicht benutzter Platz
up filemanager de Nach oben up filemanager de Nach oben

View File

@ -14,6 +14,7 @@
%1 files deleted. filemanager en %1 files deleted. %1 files deleted. filemanager en %1 files deleted.
%1 files moved. filemanager en %1 files moved. %1 files moved. filemanager en %1 files moved.
%1 files or directories deleted in %2 seconds. filemanager en %1 files or directories deleted in %2 seconds. %1 files or directories deleted in %2 seconds. filemanager en %1 files or directories deleted in %2 seconds.
%1 files unlocked. filemanager en %1 files unlocked.
%1 shares deleted. filemanager en %1 shares deleted. %1 shares deleted. filemanager en %1 shares deleted.
%1 starts with '%2' filemanager en %1 starts with '%2' %1 starts with '%2' filemanager en %1 starts with '%2'
%1 successful unmounted. filemanager en %1 successful unmounted. %1 successful unmounted. filemanager en %1 successful unmounted.
@ -192,6 +193,7 @@ link target filemanager en Link target
link target %1 not found! filemanager en Link target %1 not found! link target %1 not found! filemanager en Link target %1 not found!
list view filemanager en List view list view filemanager en List view
location filemanager en Location location filemanager en Location
lock filemanager en Lock
log out as superuser filemanager en Log out as super user log out as superuser filemanager en Log out as super user
mail files filemanager en Mail files mail files filemanager en Mail files
mail paste filemanager en Mail paste mail paste filemanager en Mail paste
@ -303,6 +305,7 @@ to overwrite the existing file store again. filemanager en To overwrite the exis
total files filemanager en Total files total files filemanager en Total files
ui mode filemanager en Standard Toolbar ui mode filemanager en Standard Toolbar
under directory filemanager en under directory under directory filemanager en under directory
unlock filemanager en Unlock
unmount filemanager en Unmount unmount filemanager en Unmount
unused space filemanager en Unused space unused space filemanager en Unused space
up filemanager en Up up filemanager en Up

View File

@ -6,6 +6,7 @@
<grid width="100%"> <grid width="100%">
<columns> <columns>
<column width="150"/> <column width="150"/>
<column width="80"/>
<column width="50%"/> <column width="50%"/>
<column width="80"/> <column width="80"/>
<column width="120"/> <column width="120"/>
@ -19,6 +20,7 @@
<rows> <rows>
<row class="th"> <row class="th">
<nextmatch-sortheader align="center" label="Type" id="mime"/> <nextmatch-sortheader align="center" label="Type" id="mime"/>
<nextmatch-header align="center" label="Lock" id="lock"/>
<nextmatch-sortheader label="Name" id="name"/> <nextmatch-sortheader label="Name" id="name"/>
<nextmatch-sortheader label="Size" id="size"/> <nextmatch-sortheader label="Size" id="size"/>
<nextmatch-sortheader label="Modified" id="mtime"/> <nextmatch-sortheader label="Modified" id="mtime"/>
@ -31,6 +33,7 @@
</row> </row>
<row class="row $row_cont[class]"> <row class="row $row_cont[class]">
<vfs-mime align="center" id="$row"/> <vfs-mime align="center" id="$row"/>
<image src="${row}[locked]" statustext="$row_cont[locked_status]"/>
<vfs id="$row" no_lang="1"/> <vfs id="$row" no_lang="1"/>
<vfs-size align="right" id="${row}[size]"/> <vfs-size align="right" id="${row}[size]"/>
<date-time id="${row}[mtime]" readonly="true"/> <date-time id="${row}[mtime]" readonly="true"/>

View File

@ -320,7 +320,7 @@ class importexport_export_csv implements importexport_iface_export_record
list($c_fields, $c_selects, $links, $methods) = self::$cf_parse_cache[$appname]; list($c_fields, $c_selects, $links, $methods) = self::$cf_parse_cache[$appname];
// Add in any fields that are keys to another app // Add in any fields that are keys to another app
foreach((array)$fields['links'] as $link_field => $app) foreach($fields['links'] ?? [] as $link_field => $app)
{ {
if(is_numeric($link_field)) continue; if(is_numeric($link_field)) continue;
$links[$link_field] = $app; $links[$link_field] = $app;
@ -332,7 +332,7 @@ class importexport_export_csv implements importexport_iface_export_record
// Not quite a recursive merge, since only one level // Not quite a recursive merge, since only one level
foreach($fields as $type => &$list) foreach($fields as $type => &$list)
{ {
if($c_fields[$type]) if (!empty($c_fields[$type]))
{ {
$list = array_merge($c_fields[$type], $list); $list = array_merge($c_fields[$type], $list);
unset($c_fields[$type]); unset($c_fields[$type]);
@ -341,7 +341,7 @@ class importexport_export_csv implements importexport_iface_export_record
$fields += $c_fields; $fields += $c_fields;
$selects += $c_selects; $selects += $c_selects;
} }
foreach((array)$fields['select'] as $name) foreach($fields['select'] ?? [] as $name)
{ {
if($record->$name != null && is_array($selects) && $selects[$name]) if($record->$name != null && is_array($selects) && $selects[$name])
{ {
@ -366,7 +366,7 @@ class importexport_export_csv implements importexport_iface_export_record
$record->$name = ''; $record->$name = '';
} }
} }
foreach((array)$fields['links'] as $name) { foreach($fields['links'] ?? [] as $name) {
if($record->$name) { if($record->$name) {
if(is_numeric($record->$name) && !$links[$name]) { if(is_numeric($record->$name) && !$links[$name]) {
$link = Link::get_link($record->$name); $link = Link::get_link($record->$name);
@ -387,7 +387,7 @@ class importexport_export_csv implements importexport_iface_export_record
$record->$name = ''; $record->$name = '';
} }
} }
foreach((array)$fields['select-account'] as $name) { foreach($fields['select-account'] ?? [] as $name) {
// Compare against null to deal with empty arrays // Compare against null to deal with empty arrays
if ($record->$name !== null) { if ($record->$name !== null) {
if(is_array($record->$name)) { if(is_array($record->$name)) {
@ -405,25 +405,25 @@ class importexport_export_csv implements importexport_iface_export_record
$record->$name = ''; $record->$name = '';
} }
} }
foreach((array)$fields['select-bool'] as $name) { foreach($fields['select-bool'] ?? [] as $name) {
if($record->$name !== null) { if($record->$name !== null) {
$record->$name = $record->$name ? lang('Yes') : lang('No'); $record->$name = $record->$name ? lang('Yes') : lang('No');
} }
} }
foreach((array)$fields['date-time'] as $name) { foreach($fields['date-time'] ?? [] as $name) {
//if ($record->$name) $record->$name = date('Y-m-d H:i:s',$record->$name); // Standard date format //if ($record->$name) $record->$name = date('Y-m-d H:i:s',$record->$name); // Standard date format
if ($record->$name && !is_numeric($record->$name)) $record->$name = strtotime($record->$name); // Custom fields stored as string if ($record->$name && !is_numeric($record->$name)) $record->$name = strtotime($record->$name); // Custom fields stored as string
if ($record->$name && is_numeric($record->$name)) $record->$name = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] . ' '. if ($record->$name && is_numeric($record->$name)) $record->$name = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] . ' '.
($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] == '24' ? 'H:i:s' : 'h:i:s a'),$record->$name); // User date format ($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] == '24' ? 'H:i:s' : 'h:i:s a'),$record->$name); // User date format
if (!$record->$name) $record->$name = ''; if (!$record->$name) $record->$name = '';
} }
foreach((array)$fields['date'] as $name) { foreach($fields['date'] ?? [] as $name) {
//if ($record->$name) $record->$name = date('Y-m-d',$record->$name); // Standard date format //if ($record->$name) $record->$name = date('Y-m-d',$record->$name); // Standard date format
if ($record->$name && !is_numeric($record->$name)) $record->$name = strtotime($record->$name); // Custom fields stored as string if ($record->$name && !is_numeric($record->$name)) $record->$name = strtotime($record->$name); // Custom fields stored as string
if ($record->$name && is_numeric($record->$name)) $record->$name = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'], $record->$name); // User date format if ($record->$name && is_numeric($record->$name)) $record->$name = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'], $record->$name); // User date format
if (!$record->$name) $record->$name = ''; if (!$record->$name) $record->$name = '';
} }
foreach((array)$fields['float'] as $name) foreach($fields['float'] ?? [] as $name)
{ {
static $dec_separator,$thousands_separator; static $dec_separator,$thousands_separator;
if (is_null($dec_separator)) if (is_null($dec_separator))
@ -445,7 +445,7 @@ class importexport_export_csv implements importexport_iface_export_record
} }
// Some custom methods for conversion // Some custom methods for conversion
foreach((array)$methods as $name => $method) { foreach($methods ?? [] as $name => $method) {
if ($record->$name) if ($record->$name)
{ {
if(is_string($method)) if(is_string($method))

View File

@ -358,7 +358,7 @@ class importexport_import_csv implements importexport_iface_import_record { //,
if ($record[$name]) { if ($record[$name]) {
// Automatically handle text owner without explicit translation // Automatically handle text owner without explicit translation
$new_owner = importexport_helper_functions::account_name2id($record[$name]); $new_owner = importexport_helper_functions::account_name2id($record[$name]);
if(count($new_owner) != count(explode(',',$record[$name]))) if(!is_array($new_owner) || count($new_owner) != count(explode(',', $record[$name])))
{ {
// Unable to parse value into account // Unable to parse value into account
$warnings[] = $name . ': ' .lang('%1 is not a known user or group', $record[$name]); $warnings[] = $name . ': ' .lang('%1 is not a known user or group', $record[$name]);

View File

@ -784,9 +784,9 @@ class infolog_so
'event' => 'calendar' 'event' => 'calendar'
); );
// query children independent of action // query children independent of action
if ((string)$query['col_filter']['info_id_parent'] === '') if (empty($query['col_filter']['info_id_parent']))
{ {
$action = isset($action2app[$query['action']]) ? $action2app[$query['action']] : $query['action']; $action = isset($action2app[$query['action']]) ? $action2app[$query['action']] : ($query['action'] ?? null);
if ($action) if ($action)
{ {
$links = Link\Storage::get_links($action=='sp'?'infolog':$action, $links = Link\Storage::get_links($action=='sp'?'infolog':$action,
@ -833,10 +833,10 @@ class infolog_so
$ordermethod = 'ORDER BY info_datemodified DESC'; // newest first $ordermethod = 'ORDER BY info_datemodified DESC'; // newest first
} }
$filtermethod = $no_acl ? '1=1' : $this->aclFilter($query['filter']); $filtermethod = $no_acl ? '1=1' : $this->aclFilter($query['filter']);
if (!$query['col_filter']['info_status']) $filtermethod .= $this->statusFilter($query['filter']); if (empty($query['col_filter']['info_status'])) $filtermethod .= $this->statusFilter($query['filter']);
$filtermethod .= $this->dateFilter($query['filter']); $filtermethod .= $this->dateFilter($query['filter']);
$cfcolfilter=0; $cfcolfilter=0;
if (is_array($query['col_filter'])) if (isset($query['col_filter']) && is_array($query['col_filter']))
{ {
foreach($query['col_filter'] as $col => $data) foreach($query['col_filter'] as $col => $data)
{ {
@ -894,15 +894,15 @@ class infolog_so
} }
//echo "<p>filtermethod='$filtermethod'</p>"; //echo "<p>filtermethod='$filtermethod'</p>";
if ((int)$query['cat_id']) if (!empty($query['cat_id']) && (int)$query['cat_id'])
{ {
$categories = new Api\Categories('','infolog'); $categories = new Api\Categories('','infolog');
$cats = $categories->return_all_children((int)$query['cat_id']); $cats = $categories->return_all_children((int)$query['cat_id']);
$filtermethod .= ' AND info_cat'.(count($cats)>1? ' IN ('.implode(',',$cats).') ' : '='.(int)$query['cat_id']); $filtermethod .= ' AND info_cat'.(count($cats)>1? ' IN ('.implode(',',$cats).') ' : '='.(int)$query['cat_id']);
} }
$join = $distinct = ''; $join = $distinct = '';
if ($query['query']) $query['search'] = $query['query']; // allow both names if (!empty($query['query'])) $query['search'] = $query['query']; // allow both names
if ($query['search']) // we search in _from, _subject, _des and _extra_value for $query if (!empty($query['search'])) // we search in _from, _subject, _des and _extra_value for $query
{ {
$columns = array('info_from','info_location','info_subject'); $columns = array('info_from','info_location','info_subject');
// at the moment MaxDB 7.5 cant cast nor search text columns, it's suppost to change in 7.6 // at the moment MaxDB 7.5 cant cast nor search text columns, it's suppost to change in 7.6
@ -924,7 +924,7 @@ class infolog_so
$join .= " LEFT JOIN $this->users_table attendees ON main.info_id=attendees.info_id AND attendees.info_res_deleted IS NULL"; $join .= " LEFT JOIN $this->users_table attendees ON main.info_id=attendees.info_id AND attendees.info_res_deleted IS NULL";
$group_by = ' GROUP BY main.info_id '; $group_by = ' GROUP BY main.info_id ';
// check if $query['append'] already contains a GROUP BY clause // check if $query['append'] already contains a GROUP BY clause
if (stripos($query['append'], 'group by') !== false) if (!empty($query['append']) && stripos($query['append'], 'group by') !== false)
{ {
$query['append'] .= ',main.info_id '; $query['append'] .= ',main.info_id ';
} }
@ -943,7 +943,7 @@ class infolog_so
$ids = array( ); $ids = array( );
if ($action == '' || $action == 'sp' || count($links)) if ($action == '' || $action == 'sp' || count($links))
{ {
$sql_query = "FROM $this->info_table main $join WHERE ($filtermethod $pid $sql_query) $link_extra"; $sql_query = "FROM $this->info_table main $join WHERE ($filtermethod $pid ".($sql_query ?? '').') '.($link_extra??'');
#error_log("infolog.so.search:\n" . print_r($sql_query, true)); #error_log("infolog.so.search:\n" . print_r($sql_query, true));
if ($this->db->Type == 'mysql' && (float)$this->db->ServerInfo['version'] >= 4.0) if ($this->db->Type == 'mysql' && (float)$this->db->ServerInfo['version'] >= 4.0)
@ -984,7 +984,7 @@ class infolog_so
$cols .= ','.$this->db->group_concat('attendees.info_res_attendee').' AS info_cc'; $cols .= ','.$this->db->group_concat('attendees.info_res_attendee').' AS info_cc';
$rs = $this->db->query($sql='SELECT '.$mysql_calc_rows.' '.$distinct.' '.$cols.' '.$info_customfield.' '.$sql_query. $rs = $this->db->query($sql='SELECT '.$mysql_calc_rows.' '.$distinct.' '.$cols.' '.$info_customfield.' '.$sql_query.
$query['append'].$ordermethod,__LINE__,__FILE__, $query['append'].$ordermethod,__LINE__,__FILE__,
(int) $query['start'],isset($query['start']) ? (int) $query['num_rows'] : -1,false,Api\Db::FETCH_ASSOC); (int)($query['start']??0),isset($query['start']) ? (int) $query['num_rows'] : -1,false,Api\Db::FETCH_ASSOC);
//echo "<p>db::query('$sql',,,".(int)$query['start'].','.(isset($query['start']) ? (int) $query['num_rows'] : -1).")</p>\n"; //echo "<p>db::query('$sql',,,".(int)$query['start'].','.(isset($query['start']) ? (int) $query['num_rows'] : -1).")</p>\n";
if ($mysql_calc_rows) if ($mysql_calc_rows)
@ -1011,7 +1011,7 @@ class infolog_so
$ids[$info['info_id']] = $info; $ids[$info['info_id']] = $info;
} }
static $index_load_cfs = null; static $index_load_cfs = null;
if (is_null($index_load_cfs) && $query['col_filter']['info_type']) if (is_null($index_load_cfs) && !empty($query['col_filter']['info_type']))
{ {
$config_data = Api\Config::read('infolog'); $config_data = Api\Config::read('infolog');
$index_load_cfs = $config_data['index_load_cfs']; $index_load_cfs = $config_data['index_load_cfs'];

View File

@ -608,7 +608,10 @@ class infolog_ui
} }
if(count($links)) if(count($links))
{ {
$query['col_filter']['info_id'] = count($links) > 1 ? call_user_func_array('array_intersect', array_values($links)) : $links[$key ?? 'info_id']; $query['col_filter']['info_id'] = count($links) > 1 ? array_intersect(...array_map(static function($ids)
{
return (array)$ids;
}, array_values($links))) : $links[$key ?? 'info_id'];
} }
return $linked; return $linked;
} }

View File

@ -4998,6 +4998,10 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
{ {
if(Mail::$debug) error_log(__METHOD__."->".array2string($_messageList)); if(Mail::$debug) error_log(__METHOD__."->".array2string($_messageList));
$uidA = self::splitRowID($_messageList['msg'][0]); $uidA = self::splitRowID($_messageList['msg'][0]);
if ($uidA['profileID'] && $uidA['profileID'] != $this->mail_bo->profileID)
{
$this->changeProfile($uidA['profileID']);
}
$folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder $folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder
$this->mail_bo->sendMDN($uidA['msgUID'],$folder); $this->mail_bo->sendMDN($uidA['msgUID'],$folder);
} }
@ -5024,6 +5028,10 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
{ {
// we have both messageIds AND allFlag folder information // we have both messageIds AND allFlag folder information
$uidA = self::splitRowID($_messageList['msg'][0]); $uidA = self::splitRowID($_messageList['msg'][0]);
if ($uidA['profileID'] && $uidA['profileID'] != $this->mail_bo->profileID)
{
$this->changeProfile($uidA['profileID']);
}
$folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder $folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder
if(!$folder && !$uidA['msg'] && $uidA['accountID']) if(!$folder && !$uidA['msg'] && $uidA['accountID'])
{ {
@ -5161,6 +5169,10 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
else else
{ {
$uidA = self::splitRowID($_messageList['msg'][0]); $uidA = self::splitRowID($_messageList['msg'][0]);
if ($uidA['profileID'] && $uidA['profileID'] != $this->mail_bo->profileID)
{
$this->changeProfile($uidA['profileID']);
}
$folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder $folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder
} }
if (!$alreadyFlagged) if (!$alreadyFlagged)
@ -5222,6 +5234,10 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
{ {
// we have both messageIds AND allFlag folder information // we have both messageIds AND allFlag folder information
$uidA = self::splitRowID($_messageList['msg'][0]); $uidA = self::splitRowID($_messageList['msg'][0]);
if ($uidA['profileID'] && $uidA['profileID'] != $this->mail_bo->profileID)
{
$this->changeProfile($uidA['profileID']);
}
$folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder $folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder
if (isset($_messageList['activeFilters']) && $_messageList['activeFilters']) if (isset($_messageList['activeFilters']) && $_messageList['activeFilters'])
{ {
@ -5292,6 +5308,10 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
else else
{ {
$uidA = self::splitRowID($_messageList['msg'][0]); $uidA = self::splitRowID($_messageList['msg'][0]);
if ($uidA['profileID'] && $uidA['profileID'] != $this->mail_bo->profileID)
{
$this->changeProfile($uidA['profileID']);
}
$folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder $folder = $uidA['folder']; // all messages in one set are supposed to be within the same folder
foreach($_messageList['msg'] as $rowID) foreach($_messageList['msg'] as $rowID)
{ {

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<?xml-stylesheet type="text/css" href="../less/svg.css" ?><svg
version="1.1"
id="pixelegg_lock"
x="0px"
y="0px"
width="32px"
height="32px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
sodipodi:docname="unlock.svg"
inkscape:version="1.1.1 (eb90963e84, 2021-10-02)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs11">
</defs><sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="22.46875"
inkscape:cx="11.527121"
inkscape:cy="14.50904"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="pixelegg_lock" />
<path
id="path3096"
style="fill:#636363;fill-opacity:1;fill-rule:nonzero"
d="m 8.1533873,0.59754662 c -4.246,0 -7.68945304,2.99159378 -7.68945304,6.68359378 V 14.912 l 3.84006004,0 c 0.00369,-0.945313 0.00369,-0.612867 0.00369,-0.945313 V 7.2811404 c 0,-1.583 1.722703,-2.8652344 3.845703,-2.8652344 2.1219997,0 3.8437497,1.2822344 3.8437497,2.8652344 v 6.6855466 c 0,0.332446 -0.09295,0.648089 -0.232422,0.945313 h 4.076172 V 7.2811404 c 0,-3.692 -3.4415,-6.68359377 -7.6874997,-6.68359378 z"
sodipodi:nodetypes="ssccsssssccss" /><path
id="path3098"
style="fill:#636363;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-opacity:1"
d="m 11.568704,14.902344 v 0.0078 h -0.960937 c -1.5920001,0 -2.8847657,1.283235 -2.8847657,2.865235 V 28.28125 c 0,1.581 1.2937656,2.865234 2.8847657,2.865234 h 17.300781 c 1.591,0 2.882812,-1.284234 2.882812,-2.865234 V 17.775391 c -0.001,-1.582 -1.292812,-2.863282 -2.882812,-2.863282 h -0.962891 v -0.0098 z m 7.6875,2.875 c 2.122,0 3.845703,1.711312 3.845703,3.820312 0,1.41 -0.778828,2.630016 -1.923828,3.291016 v 1.482422 c 0,1.055 -0.859875,1.910156 -1.921875,1.910156 -1.062,0 -1.921875,-0.855156 -1.921875,-1.910156 v -1.484375 c -1.144,-0.661 -1.921875,-1.879063 -1.921875,-3.289063 0,-2.109 1.72075,-3.820312 3.84375,-3.820312 z" /><path
id="path3102"
d="M 20.27049 14.902473 C 20.268999 14.905651 20.267502 14.908827 20.266 14.912 L 13.048 14.912 C 13.046487 14.908827 13.04498 14.905651 13.043479 14.902473 "
style="fill:#000000;fill-rule:evenodd;fill-opacity:0.49119297" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -89,7 +89,7 @@ class preferences_password
break; break;
case 'two_factor_auth': case 'two_factor_auth':
switch(key($content['2fa']['action'])) switch(key($content['2fa']['action'] ?? []))
{ {
case 'show': case 'show':
$content['2fa'] = $this->generateQRCode($google2fa, false); $content['2fa'] = $this->generateQRCode($google2fa, false);

View File

@ -412,7 +412,7 @@ class preferences_settings
continue 2; continue 2;
case 'notify': case 'notify':
$vars = $GLOBALS['egw']->preferences->vars; $vars = $GLOBALS['egw']->preferences->vars ?? [];
if (is_array($setting['values'])) $vars += $setting['values']; if (is_array($setting['values'])) $vars += $setting['values'];
$GLOBALS['egw']->preferences->{$attribute}[$appname][$setting['name']] = $GLOBALS['egw']->preferences->{$attribute}[$appname][$setting['name']] =
$GLOBALS['egw']->preferences->lang_notify($GLOBALS['egw']->preferences->{$attribute}[$appname][$setting['name']], $vars); $GLOBALS['egw']->preferences->lang_notify($GLOBALS['egw']->preferences->{$attribute}[$appname][$setting['name']], $vars);

View File

@ -5,14 +5,15 @@
<template id="resources.show.rows" template="" lang="" group="0" version="1.9.003"> <template id="resources.show.rows" template="" lang="" group="0" version="1.9.003">
<grid width="100%"> <grid width="100%">
<columns> <columns>
<column width="70"/> <column width="70"/>
<column width="50%"/> <column width="50%"/>
<column width="50"/> <column width="50"/>
<column width="15%"/> <column width="15%"/>
<column width="15%"/> <column width="15%"/>
<column width="15%"/> <column width="15%"/>
<column width="15%" disabled="@no_customfields"/> <column width="15%"/>
</columns> <column width="15%" disabled="@no_customfields"/>
</columns>
<rows> <rows>
<row class="th"> <row class="th">
<nextmatch-header label="Image" id="image"/> <nextmatch-header label="Image" id="image"/>
@ -32,6 +33,7 @@
<nextmatch-sortheader label="Location" id="location"/> <nextmatch-sortheader label="Location" id="location"/>
<description value="Storage information"/> <description value="Storage information"/>
</vbox> </vbox>
<nextmatch-sortheader label="Inventory Number" id="inventory_number"/>
<nextmatch-header label="Resource / Accessories"/> <nextmatch-header label="Resource / Accessories"/>
<nextmatch-customfields id="customfields"/> <nextmatch-customfields id="customfields"/>
</row> </row>
@ -57,6 +59,7 @@
<description id="${row}[location]" no_lang="1"/> <description id="${row}[location]" no_lang="1"/>
<description id="${row}[storage_info]" no_lang="1"/> <description id="${row}[storage_info]" no_lang="1"/>
</vbox> </vbox>
<description id="${row}[inventory_number]" no_lang="1"/>
<vbox no_lang="1"> <vbox no_lang="1">
<description extra_link_popup="850x600" href="resources.resources_ui.edit&amp;res_id=$row_cont[accessory_of]" id="${row}[accessory_of_label]" no_lang="1"/> <description extra_link_popup="850x600" href="resources.resources_ui.edit&amp;res_id=$row_cont[accessory_of]" id="${row}[accessory_of_label]" no_lang="1"/>
<grid width="100%" id="${row}[accessories]"> <grid width="100%" id="${row}[accessories]">