Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-08-23 15:41:27 -06:00
commit 538e483499
20 changed files with 1401 additions and 948 deletions

View File

@ -17,6 +17,7 @@
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
import {et2_IDOMNode} from "./et2_core_interfaces"; import {et2_IDOMNode} from "./et2_core_interfaces";
import {et2_form_name} from "./et2_core_common";
export function et2_compileLegacyJS(_code, _widget, _context) export function et2_compileLegacyJS(_code, _widget, _context)
{ {
@ -144,7 +145,8 @@ function js_pseudo_funcs(_val,widget)
{ {
// et2_form_name doesn't care about ][, just [ // et2_form_name doesn't care about ][, just [
var _cname = widget.getPath() ? widget.getPath().join("[") : false; var _cname = widget.getPath() ? widget.getPath().join("[") : false;
_val = _val.replace(/form::name\(/g, "'"+widget.getRoot()._inst.uniqueId+"_'+"+(_cname ? "et2_form_name('"+_cname+"'," : '(')); document.et2_form_name = et2_form_name;
_val = _val.replace(/form::name\(/g, "'"+widget.getRoot()._inst.uniqueId+"_'+"+(_cname ? "document.et2_form_name('"+_cname+"'," : '('));
} }
if (_val.indexOf('egw::lang(') != -1) if (_val.indexOf('egw::lang(') != -1)

File diff suppressed because it is too large Load Diff

View File

@ -521,7 +521,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
// Apply widget's class to td, for backward compatability // Apply widget's class to td, for backward compatability
if (node.getAttribute("class")) if (node.getAttribute("class"))
{ {
cell.class += (cell.class ? " " : "") + node.getAttribute("class"); cell.class += (cell.class ? " " : "") + this.getArrayMgr("content").expandName(node.getAttribute("class"));
} }
// Create the element // Create the element

View File

@ -641,7 +641,7 @@ export class et2_avatar extends et2_image
} }
).sendRequest(true); ).sendRequest(true);
} }
if (this.options.crop) if (this.options.crop && !this.options.readonly)
{ {
jQuery(this.image).cropper({ jQuery(this.image).cropper({
aspectRatio: 1/1, aspectRatio: 1/1,

View File

@ -639,7 +639,6 @@ export class et2_link_entry extends et2_inputWidget
protected search: JQuery; protected search: JQuery;
protected clear: JQuery; protected clear: JQuery;
protected link_button: JQuery; protected link_button: JQuery;
private response: any;
private request: any; private request: any;
private last_search: string; private last_search: string;
processing: boolean = false; processing: boolean = false;
@ -1041,6 +1040,13 @@ export class et2_link_entry extends et2_inputWidget
}; };
} }
} }
// display a search query, not a selected entry
else if (_value !== null && typeof _value === 'object' && typeof _value.query === 'string')
{
this.options.value = { app: _value.app || this.options.only_app, id: null };
this.search.val(_value.query);
return;
}
this._oldValue = this.options.value; this._oldValue = this.options.value;
if (!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) if (!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value))
{ {
@ -1183,18 +1189,29 @@ export class et2_link_entry extends et2_inputWidget
return response(this.cache[request.term]); return response(this.cache[request.term]);
} }
// Remember callback
this.response = response;
this.search.addClass("loading"); this.search.addClass("loading");
// Remove specific display and revert to CSS file // Remove specific display and revert to CSS file
// show() would use inline, should be inline-block // show() would use inline, should be inline-block
this.clear.css('display', ''); this.clear.css('display', '');
this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search",
[this.app_select.val(), '', request.term, request.options], this.request = egw.request("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search",
this._results, [this.app_select.val(), '', request.term, request.options]);
this, true, this
).sendRequest(); this.request.then((data) =>
{
if (this.request)
{
this.request = null;
}
this.search.removeClass("loading");
let result = [];
for (var id in data)
{
result.push({"value": id, "label": data[id]});
}
this.cache[this.search.val()] = result;
response(result);
});
} }
/** /**
@ -1239,27 +1256,6 @@ export class et2_link_entry extends et2_inputWidget
}, event.data)); }, event.data));
} }
/**
* Server found some results
*
* @param {Array} data
*/
_results(data)
{
if (this.request)
{
this.request = null;
}
this.search.removeClass("loading");
var result = [];
for (var id in data)
{
result.push({"value": id, "label": data[id]});
}
this.cache[this.search.val()] = result;
this.response(result);
}
/** /**
* Create a link using the current internal values * Create a link using the current internal values
* *

View File

@ -19,7 +19,7 @@ import {ClassWithAttributes} from "./et2_core_inheritance";
import {et2_no_init} from "./et2_core_common"; import {et2_no_init} from "./et2_core_common";
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
import {et2_IInput} from "./et2_core_interfaces"; import {et2_IInput} from "./et2_core_interfaces";
import {date} from "./lib/date.js";
/** /**
* Class which implements the "button-timestamper" XET-Tag * Class which implements the "button-timestamper" XET-Tag
* *

View File

@ -96,6 +96,18 @@ export class et2_video extends et2_baseWidget implements et2_IDOMNode
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"description": "Defines if the video should be played repeatedly" "description": "Defines if the video should be played repeatedly"
},
"volume": {
"name": "Video volume",
"type": "float",
"default": 0,
"description": "Set video's volume"
},
"playbackrate": {
"name": "Video playBackRate",
"type": "float",
"default": 1,
"description": "Set video's playBackRate"
} }
}; };
@ -264,6 +276,105 @@ export class et2_video extends et2_baseWidget implements et2_IDOMNode
} }
} }
/**
* Method to set volume
* @param _value
*/
set_volume(_value: number)
{
let value = _value>100?100:_value;
if (value>= 0)
{
if (this._isYoutube() && this.youtube)
{
this.youtube.setVolume(value);
}
else if(!this._isYoutube())
{
this.video[0].volume = value/100;
}
}
}
/**
* get volume
*/
get_volume()
{
if (this._isYoutube() && this.youtube)
{
return this.youtube.getVolume();
}
else
{
return this.video[0].volume * 100;
}
}
/**
* method to set playBackRate
* @param _value
*/
set_playBackRate(_value: number)
{
let value = _value>16?16:_value;
if (value>= 0)
{
if (this._isYoutube() && this.youtube)
{
this.youtube.setPlaybackRate(value);
}
else
{
this.video[0].playbackRate = value;
}
}
}
/**
* get playBackRate
*/
get_playBackRate()
{
if (this._isYoutube() && this.youtube)
{
return this.youtube.getPlaybackRate();
}
else
{
return this.video[0].playbackRate;
}
}
set_mute(_value)
{
if (this._isYoutube() && this.youtube) {
if (_value)
{
this.youtube.mute();
}
else
{
this.youtube.unMute();
}
}
else
{
this.video[0].muted = _value;
}
}
get_mute()
{
if (this._isYoutube() && this.youtube)
{
return this.youtube.isMuted();
}
else
{
return this.video[0].muted;
}
}
/** /**
* Set poster attribute in order to specify * Set poster attribute in order to specify
* an image to be shown while video is loading or before user play it * an image to be shown while video is loading or before user play it

View File

@ -782,7 +782,9 @@ export abstract class EgwApp
}) })
.addClass("ui-helper-clearfix"); .addClass("ui-helper-clearfix");
let el = document.getElementById('favorite_sidebox_'+this.appname).getElementsByTagName('ul')[0]; let el = document.getElementById('favorite_sidebox_'+this.appname)?.getElementsByTagName('ul')[0];
if (el && el instanceof HTMLElement)
{
let sortablejs = Sortable.create(el, { let sortablejs = Sortable.create(el, {
ghostClass: 'ui-fav-sortable-placeholder', ghostClass: 'ui-fav-sortable-placeholder',
draggable: 'li:not([data-id$="add"])', draggable: 'li:not([data-id$="add"])',
@ -794,6 +796,7 @@ export abstract class EgwApp
self._refresh_fav_nm(); self._refresh_fav_nm();
} }
}); });
}
// Bind favorite de-select // Bind favorite de-select
var egw_fw = egw_getFramework(); var egw_fw = egw_getFramework();

View File

@ -127,16 +127,16 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
}.bind(this); }.bind(this);
this.websocket = new WebSocket(url); this.websocket = new WebSocket(url);
this.websocket.onopen = jQuery.proxy(function(e) this.websocket.onopen = (e) =>
{ {
check_timer = window.setTimeout(check, check_interval); check_timer = window.setTimeout(check, check_interval);
this.websocket.send(JSON.stringify({ this.websocket.send(JSON.stringify({
subscribe: tokens, subscribe: tokens,
account_id: parseInt(account_id) account_id: parseInt(account_id)
})); }));
}, this); };
this.websocket.onmessage = jQuery.proxy(function(event) this.websocket.onmessage = (event) =>
{ {
reconnect_time = min_reconnect_time; reconnect_time = min_reconnect_time;
console.log(event); console.log(event);
@ -148,18 +148,18 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
{ {
this.handleResponse({ response: [data]}); this.handleResponse({ response: [data]});
} }
}, this); };
this.websocket.onerror = jQuery.proxy(function(error) this.websocket.onerror = (error) =>
{ {
reconnect_time *= 2; reconnect_time *= 2;
if (reconnect_time > max_reconnect_time) reconnect_time = max_reconnect_time; if (reconnect_time > max_reconnect_time) reconnect_time = max_reconnect_time;
console.log(error); console.log(error);
(error||this.handleError({}, error)); (error||this.handleError({}, error));
}, this); };
this.websocket.onclose = jQuery.proxy(function(event) this.websocket.onclose = (event) =>
{ {
if (event.wasClean) if (event.wasClean)
{ {
@ -176,9 +176,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
console.log('[close] Connection died --> reconnect in '+reconnect_time+'ms'); console.log('[close] Connection died --> reconnect in '+reconnect_time+'ms');
if (check_timer) window.clearTimeout(check_timer); if (check_timer) window.clearTimeout(check_timer);
check_timer = null; check_timer = null;
window.setTimeout(jQuery.proxy(this.openWebSocket, this, url, tokens, account_id, error, reconnect_time), reconnect_time); window.setTimeout(() => this.openWebSocket(url, tokens, account_id, error, reconnect_time), reconnect_time);
} }
}, this); };
}, },
/** /**
@ -189,6 +189,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
* @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error * @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error
* *
* @return {Promise|boolean} Promise or for async==="keepalive" boolean is returned * @return {Promise|boolean} Promise or for async==="keepalive" boolean is returned
* Promise.abort() allows to abort the pending request
*/ */
json_request.prototype.sendRequest = function(async, method, error) json_request.prototype.sendRequest = function(async, method, error)
{ {
@ -231,7 +232,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
let promise; let promise;
if (this.async) if (this.async)
{ {
promise = (this.egw.window?this.egw.window:window).fetch(url, init) const controller = new AbortController();
const signal = controller.signal;
promise = (this.egw.window?this.egw.window:window).fetch(url, {...init, ...signal})
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw response; throw response;
@ -242,6 +245,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
.catch((_err) => { .catch((_err) => {
(error || this.handleError).call(this, _err) (error || this.handleError).call(this, _err)
}); });
// offering a simple abort mechanism and compatibility with jQuery.ajax
promise.abort = () => controller.abort();
} }
else else
{ {
@ -496,19 +502,15 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
* @param {string} _menuaction * @param {string} _menuaction
* @param {any[]} _parameters * @param {any[]} _parameters
* *
* @return Promise * @return Promise resolving to data part (not full response, which can contain other parts)
* Promise.abort() allows to abort the pending request
*/ */
request: function(_menuaction, _parameters) request: function(_menuaction, _parameters)
{ {
let request = new json_request(_menuaction, _parameters, null, this, true, this, this); const request = new json_request(_menuaction, _parameters, null, this, true, this, this);
let ajax_promise = request.sendRequest(); const response = request.sendRequest();
let promise = response.then(function(response)
// This happens first, immediately {
let resolvePromise = function(resolve, reject) {
// Bind to ajax response - this is called _after_ any other handling
ajax_promise.always(function(response, status, p) {
if(status !== "success") reject();
// The ajax request has completed, get just the data & pass it on // The ajax request has completed, get just the data & pass it on
if(response && response.response) if(response && response.response)
{ {
@ -517,24 +519,23 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
if(value.type && value.type === "data" && typeof value.data !== "undefined") if(value.type && value.type === "data" && typeof value.data !== "undefined")
{ {
// Data was packed in response // Data was packed in response
resolve(value.data); return value.data;
} }
else if (value && typeof value.type === "undefined" && typeof value.data === "undefined") else if (value && typeof value.type === "undefined" && typeof value.data === "undefined")
{ {
// Just raw data // Just raw data
resolve(value); return value;
} }
} }
} }
return undefined;
// No data? Resolve the promise with nothing
resolve();
}); });
}; // pass abort method to returned response
if (typeof response.abort === 'function')
const myPromise = new Promise(resolvePromise); {
promise.abort = response.abort;
return myPromise; }
return promise;
}, },
/** /**

View File

@ -11,7 +11,7 @@
/* Basic information about this app */ /* Basic information about this app */
$setup_info['api']['name'] = 'api'; $setup_info['api']['name'] = 'api';
$setup_info['api']['title'] = 'EGroupware API'; $setup_info['api']['title'] = 'EGroupware API';
$setup_info['api']['version'] = '21.1'; $setup_info['api']['version'] = '21.1.001';
$setup_info['api']['versions']['current_header'] = '1.29'; $setup_info['api']['versions']['current_header'] = '1.29';
// maintenance release in sync with changelog in doc/rpm-build/debian.changes // maintenance release in sync with changelog in doc/rpm-build/debian.changes
$setup_info['api']['versions']['maintenance_release'] = '21.1.20210723'; $setup_info['api']['versions']['maintenance_release'] = '21.1.20210723';

View File

@ -789,5 +789,18 @@ function api_upgrade20_1_002()
*/ */
function api_upgrade20_1_003() function api_upgrade20_1_003()
{ {
return $GLOBALS['setup_info']['activesync']['currentver'] = '21.1'; return $GLOBALS['setup_info']['api']['currentver'] = '21.1';
}
/**
* Remove non-email addresses from egw_history_log.share_email
*
* @return string
*/
function api_upgrade21_1()
{
$GLOBALS['egw_setup']->db->query("UPDATE egw_history_log SET share_email=NULL, history_timestamp=history_timestamp".
" WHERE share_email is NOT NULL AND share_email NOT LIKE '%@%'", __LINE__, __FILE__);
return $GLOBALS['setup_info']['api']['currentver'] = '21.1.001';
} }

View File

@ -923,9 +923,10 @@ class Widget
/** /**
* Checks if a widget is readonly: * Checks if a widget is readonly:
* - readonly attribute set * 1. $readonlys set to true for $form_name:
* - $readonlys[__ALL__] set and $readonlys[$form_name] !== false * a) $readonlys[$form_name] is set to true (flat array)
* - $readonlys[$form_name] evaluates to true * b) self::get_array($readonlys, $form_name) is set to true (hierarchical)
* 2. ($readonlys[__ALL__] or widget readonly attribute) is true AND NOT $readonlys set to false for $form_name
* *
* @param string $cname ='' * @param string $cname =''
* @param string $form_name =null form_name, to not calculate him again * @param string $form_name =null form_name, to not calculate him again
@ -940,13 +941,12 @@ class Widget
); );
$form_name = self::form_name($cname, $this->id, $expand); $form_name = self::form_name($cname, $this->id, $expand);
} }
$readonly = $this->attrs['readonly'] || self::$request->readonlys[$form_name] || // readonlys can either be set / used as flat array with complete form-name, hierarchical
self::get_array(self::$request->readonlys,$form_name) === true || $readonlys = self::$request->readonlys[$form_name] ?? self::get_array(self::$request->readonlys,$form_name);
isset(self::$request->readonlys['__ALL__']) && (
// Exceptions to all $readonly = $readonlys === true ||
self::$request->readonlys[$form_name] !== false && // exception to __ALL__ or readonly="true" attribute by setting $readonlys[$from_name] === false
self::get_array(self::$request->readonlys,$form_name) !== false ($this->attrs['readonly'] || isset(self::$request->readonlys['__ALL__'])) && $readonlys !== false;
);
//error_log(__METHOD__."('$cname') this->id='$this->id' --> form_name='$form_name': attrs[readonly]=".array2string($this->attrs['readonly']).", readonlys['$form_name']=".array2string(self::$request->readonlys[$form_name]).", readonlys[$form_name]=".array2string(self::get_array(self::$request->readonlys,$form_name)).", readonlys['__ALL__']=".array2string(self::$request->readonlys['__ALL__'])." returning ".array2string($readonly)); //error_log(__METHOD__."('$cname') this->id='$this->id' --> form_name='$form_name': attrs[readonly]=".array2string($this->attrs['readonly']).", readonlys['$form_name']=".array2string(self::$request->readonlys[$form_name]).", readonlys[$form_name]=".array2string(self::get_array(self::$request->readonlys,$form_name)).", readonlys['__ALL__']=".array2string(self::$request->readonlys['__ALL__'])." returning ".array2string($readonly));
return $readonly; return $readonly;

View File

@ -513,6 +513,9 @@ tinymce.init({
language: language_code["'. $GLOBALS['egw_info']['user']['preferences']['common']['lang'].'"], language: language_code["'. $GLOBALS['egw_info']['user']['preferences']['common']['lang'].'"],
language_url: egw.webserverUrl+"/api/js/tinymce/langs/"+language_code[egw.preference("lang", "common")]+".js", language_url: egw.webserverUrl+"/api/js/tinymce/langs/"+language_code[egw.preference("lang", "common")]+".js",
browser_spellcheck: true, browser_spellcheck: true,
images_upload_url: imageUpload,
paste_data_images: true,
paste_filter_drop: true,
contextmenu: false, contextmenu: false,
file_picker_callback: function(_callback, _value, _meta){ file_picker_callback: function(_callback, _value, _meta){
var callback = _callback; var callback = _callback;

View File

@ -867,7 +867,7 @@ class Link extends Link\Storage
if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it
{ {
static $unlinking = array(); static $unlinking = array();
// check if we are already trying to unlink the entry, to avoid an infinit recursion // check if we are already trying to unlink the entry, to avoid an infinite recursion
if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id])) if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id]))
{ {
$unlinking[$app][$id] = true; $unlinking[$app][$id] = true;
@ -1344,11 +1344,17 @@ class Link extends Link\Storage
Storage\History::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', Vfs::basename($url)); Storage\History::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', Vfs::basename($url));
} }
} }
try {
if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname)) if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname))
{ {
// try removing the dir, in case it's empty // try removing the dir, in case it's empty
if (($dir = Vfs::dirname($url))) @Vfs::rmdir($dir); if (($dir = Vfs::dirname($url))) @Vfs::rmdir($dir);
} }
}
catch (\Exception $e) {
// ignore SQL error caused by only virtual directories with non-integer (hash) fs_id
$Ok = false;
}
if (!is_null($current_is_root)) if (!is_null($current_is_root))
{ {
Vfs::$is_root = $current_is_root; Vfs::$is_root = $current_is_root;

View File

@ -21,7 +21,7 @@ use EGroupware\Api;
/** /**
* Record history logging service * Record history logging service
* *
* This class need to be instanciated for EACH app, which wishes to use it! * This class need to be instantiated for EACH app, which wishes to use it!
*/ */
class History class History
{ {
@ -59,8 +59,8 @@ class History
*/ */
function __construct($appname = '', $user = null) function __construct($appname = '', $user = null)
{ {
$this->appname = $appname ? $appname : $GLOBALS['egw_info']['flags']['currentapp']; $this->appname = $appname ?: $GLOBALS['egw_info']['flags']['currentapp'];
$this->user = !is_null($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(is_object($GLOBALS['egw_setup']->db))
{ {
@ -124,11 +124,8 @@ class History
{ {
if($new_value != $old_value) if($new_value != $old_value)
{ {
$share_with = ''; $share_with = static::get_share_with($this->appname, $record_id);
foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj)
{
$share_with .= $share_obj->get_share_with();
}
$this->db->insert(self::TABLE, array( $this->db->insert(self::TABLE, array(
'history_record_id' => $record_id, 'history_record_id' => $record_id,
'history_appname' => $this->appname, 'history_appname' => $this->appname,
@ -150,11 +147,8 @@ class History
{ {
if($new_value != $old_value) if($new_value != $old_value)
{ {
$share_with = ''; $share_with = static::get_share_with($appname, $id);
foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj)
{
$share_with .= $share_obj->get_share_with();
}
$GLOBALS['egw']->db->insert(self::TABLE, array( $GLOBALS['egw']->db->insert(self::TABLE, array(
'history_record_id' => $id, 'history_record_id' => $id,
'history_appname' => $appname, 'history_appname' => $appname,
@ -169,6 +163,32 @@ class History
} }
} }
/**
* If a record was accessed via a share, we want to record who the entry was shared with, rather than the current
* user. Since multiple shares can be active at once, and they might not be for the current entry, we check to
* see if the given entry was accessed via a share, and which share was used.
* The share's share_with is recorded into the history for some hope of tracking who made the change.
* share_with is a list of email addresses, and may be empty.
*
* @param $appname
* @param $id
*
* @return ?string
*/
static function get_share_with($appname, $id)
{
$share_with = null;
foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj)
{
// Make sure share is of the correct type to access an entry, and it is the correct entry
if($share_obj instanceof Api\Link\Sharing && "$appname::$id" === $share_obj['share_path'])
{
$share_with .= $share_obj->get_share_with();
}
}
return $share_with;
}
/** /**
* Search history-log * Search history-log
* *
@ -181,7 +201,10 @@ class History
*/ */
function search($filter, $order = 'history_id', $sort = 'DESC', $limit = null) function search($filter, $order = 'history_id', $sort = 'DESC', $limit = null)
{ {
if (!is_array($filter)) $filter = is_numeric($filter) ? array('history_record_id' => $filter) : array(); if(!is_array($filter))
{
$filter = is_numeric($filter) ? array('history_record_id' => $filter) : array();
}
if(!$order || !preg_match('/^[a-z0-9_]+$/i', $order) || !preg_match('/^(asc|desc)?$/i', $sort)) if(!$order || !preg_match('/^[a-z0-9_]+$/i', $order) || !preg_match('/^(asc|desc)?$/i', $sort))
{ {
@ -199,14 +222,21 @@ class History
unset($filter[$col]); unset($filter[$col]);
} }
} }
if (!isset($filter['history_appname'])) $filter['history_appname'] = $this->appname; if(!isset($filter['history_appname']))
{
$filter['history_appname'] = $this->appname;
}
// do not try to read all history entries of an app // do not try to read all history entries of an app
if (!$filter['history_record_id']) return array(); if(!$filter['history_record_id'])
{
return array();
}
$rows = array(); $rows = array();
foreach($this->db->select(self::TABLE, '*', $filter, __LINE__, __FILE__, foreach($this->db->select(self::TABLE, '*', $filter, __LINE__, __FILE__,
isset($limit) ? 0 : false, $orderby, 'phpgwapi', $limit) as $row) isset($limit) ? 0 : false, $orderby, 'phpgwapi', $limit
) as $row)
{ {
$row['user_ts'] = $this->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; $row['user_ts'] = $this->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset'];
$rows[] = Api\Db::strip_array_keys($row, 'history_'); $rows[] = Api\Db::strip_array_keys($row, 'history_');
@ -227,8 +257,10 @@ class History
$rows = array(); $rows = array();
$filter['history_appname'] = $query['appname']; $filter['history_appname'] = $query['appname'];
$filter['history_record_id'] = $query['record_id']; $filter['history_record_id'] = $query['record_id'];
if(is_array($query['colfilter'])) { if(is_array($query['colfilter']))
foreach($query['colfilter'] as $column => $value) { {
foreach($query['colfilter'] as $column => $value)
{
$filter[$column] = $value; $filter[$column] = $value;
} }
} }
@ -247,7 +279,8 @@ class History
$to_or[] = 'history_status IN (' . implode(',', array_map(function ($str) $to_or[] = 'history_status IN (' . implode(',', array_map(function ($str)
{ {
return $GLOBALS['egw']->db->quote('#' . $str); return $GLOBALS['egw']->db->quote('#' . $str);
}, array_keys($cfs))).')'; }, array_keys($cfs))
) . ')';
} }
$filter[] = '(' . implode(' OR ', $to_or) . ')'; $filter[] = '(' . implode(' OR ', $to_or) . ')';
} }
@ -274,7 +307,8 @@ class History
{ {
$_query[] = array( $_query[] = array(
'table' => Api\Vfs\Sqlfs\StreamWrapper::TABLE, 'table' => Api\Vfs\Sqlfs\StreamWrapper::TABLE,
'cols' =>array('fs_id', 'fs_dir', "'filemanager'",'COALESCE(fs_modifier,fs_creator)',"'~file~'",'fs_name','fs_modified', 'fs_mime', '"" AS share_email'), 'cols' => array('fs_id', 'fs_dir', "'filemanager'", 'COALESCE(fs_modifier,fs_creator)', "'~file~'",
'fs_name', 'fs_modified', 'fs_mime', '"" AS share_email'),
'where' => array('fs_dir' => $file['ino']) 'where' => array('fs_dir' => $file['ino'])
); );
} }
@ -303,7 +337,8 @@ class History
)) ))
{ {
// Larger text stored with full old / new value - calculate diff and just send that // Larger text stored with full old / new value - calculate diff and just send that
$diff = new \Horde_Text_Diff('auto', array(explode("\n",$row['history_old_value']), explode("\n",$row['history_new_value']))); $diff = new \Horde_Text_Diff('auto', array(explode("\n", $row['history_old_value']),
explode("\n", $row['history_new_value'])));
$renderer = new \Horde_Text_Diff_Renderer_Unified(); $renderer = new \Horde_Text_Diff_Renderer_Unified();
$row['history_new_value'] = $renderer->render($diff); $row['history_new_value'] = $renderer->render($diff);
$row['history_old_value'] = Tracking::DIFF_MARKER; $row['history_old_value'] = Tracking::DIFF_MARKER;
@ -330,6 +365,14 @@ class History
$rows[$new_version]['old_value'] = $row['history_new_value']; $rows[$new_version]['old_value'] = $row['history_new_value'];
} }
} }
// TODO: This is just here to hide bad values before we clean them with an update. If you're here, remove this IF block
// Clear invalid share_email values
if($row['share_email'] && stripos($row['share_email'], '@') === false)
{
$row['share_email'] = '';
}
$rows[] = Api\Db::strip_array_keys($row, 'history_'); $rows[] = Api\Db::strip_array_keys($row, 'history_');
} }
$total = $GLOBALS['egw']->db->union($_query, __LINE__, __FILE__)->NumRows(); $total = $GLOBALS['egw']->db->union($_query, __LINE__, __FILE__)->NumRows();

View File

@ -83,7 +83,10 @@ class Base
*/ */
static function mount($url = null, $path = null, $check_url = null, $persistent_mount = true, $clear_fstab = false) static function mount($url = null, $path = null, $check_url = null, $persistent_mount = true, $clear_fstab = false)
{ {
if (is_null($check_url)) $check_url = strpos($url,'$') === false; if(is_null($check_url))
{
$check_url = strpos($url, '$') === false;
}
if(!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup if(!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup
{ {
@ -103,12 +106,18 @@ class Base
} }
if(is_null($url) || is_null($path)) if(is_null($url) || is_null($path))
{ {
if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') returns ' . array2string(self::$fstab));
}
return self::$fstab; return self::$fstab;
} }
if(!Vfs::$is_root) if(!Vfs::$is_root)
{ {
if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') permission denied, you are NOT root!');
}
return false; // only root can mount return false; // only root can mount
} }
if($clear_fstab) if($clear_fstab)
@ -117,14 +126,20 @@ class Base
} }
if(isset(self::$fstab[$path]) && self::$fstab[$path] === $url) if(isset(self::$fstab[$path]) && self::$fstab[$path] === $url)
{ {
if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') already mounted.');
}
return true; // already mounted return true; // already mounted
} }
self::load_wrapper(Vfs::parse_url($url, PHP_URL_SCHEME)); self::load_wrapper(Vfs::parse_url($url, PHP_URL_SCHEME));
if($check_url && (!file_exists($url) || opendir($url) === false)) if($check_url && (!file_exists($url) || opendir($url) === false))
{ {
if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') url does NOT exist!');
}
return false; // url does not exist return false; // url does not exist
} }
self::$fstab[$path] = $url; self::$fstab[$path] = $url;
@ -157,7 +172,10 @@ class Base
$_SESSION[Api\Session::EGW_INFO_CACHE]['user']['preferences']['common']['vfs_fstab'][$path] = $url; $_SESSION[Api\Session::EGW_INFO_CACHE]['user']['preferences']['common']['vfs_fstab'][$path] = $url;
} }
} }
if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') returns true (successful new mount).');
}
return true; return true;
} }
@ -170,12 +188,18 @@ class Base
{ {
if(!Vfs::$is_root) if(!Vfs::$is_root)
{ {
if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . '(' . array2string($path) . ',' . array2string($path) . ') permission denied, you are NOT root!');
}
return false; // only root can mount return false; // only root can mount
} }
if(!isset(self::$fstab[$path]) && ($path = array_search($path, self::$fstab)) === false) if(!isset(self::$fstab[$path]) && ($path = array_search($path, self::$fstab)) === false)
{ {
if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . '(' . array2string($path) . ') NOT mounted!');
}
return false; // $path not mounted return false; // $path not mounted
} }
unset(self::$fstab[$path], $GLOBALS['egw_info']['server']['vfs_fstab'][$path]); unset(self::$fstab[$path], $GLOBALS['egw_info']['server']['vfs_fstab'][$path]);
@ -194,7 +218,10 @@ class Base
{ {
$GLOBALS['egw']->invalidate_session_cache(); $GLOBALS['egw']->invalidate_session_cache();
} }
if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . '(' . array2string($path) . ') returns true (successful unmount).');
}
return true; return true;
} }
@ -244,7 +271,10 @@ class Base
// we do some caching here // we do some caching here
if(isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host) if(isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host)
{ {
if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '".self::$resolve_url_cache[$path]."' (from cache)"); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . "('$path') = '" . self::$resolve_url_cache[$path] . "' (from cache)");
}
$mounted = self::$resolve_url_cache[$path]['mounted']; $mounted = self::$resolve_url_cache[$path]['mounted'];
return self::$resolve_url_cache[$path]['url']; return self::$resolve_url_cache[$path]['url'];
} }
@ -261,14 +291,24 @@ class Base
'home' => str_replace(array('\\\\', '\\'), array('', '/'), $GLOBALS['egw_info']['user']['homedirectory']), 'home' => str_replace(array('\\\\', '\\'), array('', '/'), $GLOBALS['egw_info']['user']['homedirectory']),
); );
$parts = array_merge(Vfs::parse_url($_path), Vfs::parse_url($path), $defaults); $parts = array_merge(Vfs::parse_url($_path), Vfs::parse_url($path), $defaults);
if (!$parts['host']) $parts['host'] = 'default'; // otherwise we get an invalid url (scheme:///path/to/something)! if(!$parts['host'])
{
// otherwise we get an invalid url (scheme:///path/to/something)!
$parts['host'] = 'default';
}
if(!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME) if(!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME)
{ {
if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$path' (path is already an url)"); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . "('$path') = '$path' (path is already an url)");
}
return $path; // path is already a non-vfs url --> nothing to do return $path; // path is already a non-vfs url --> nothing to do
} }
if (empty($parts['path'])) $parts['path'] = '/'; if(empty($parts['path']))
{
$parts['path'] = '/';
}
foreach(array_reverse(self::$fstab) as $mounted => $url) foreach(array_reverse(self::$fstab) as $mounted => $url)
{ {
@ -292,12 +332,27 @@ class Base
if($replace_user_pass_host) if($replace_user_pass_host)
{ {
$url = str_replace(array('$user','$pass','$host','$home'),array($parts['user'],$parts['pass'],$parts['host'],$parts['home']),$url); $url = str_replace(array('$user',
'$pass',
'$host',
'$home'), array($parts['user'],
$parts['pass'],
$parts['host'],
$parts['home']), $url);
}
if($parts['query'])
{
$url .= '?' . $parts['query'];
}
if($parts['fragment'])
{
$url .= '#' . $parts['fragment'];
} }
if ($parts['query']) $url .= '?'.$parts['query'];
if ($parts['fragment']) $url .= '#'.$parts['fragment'];
if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$url'"); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . "('$path') = '$url'");
}
if(($class = self::scheme2class($scheme)) && is_callable([$class, 'replace'])) if(($class = self::scheme2class($scheme)) && is_callable([$class, 'replace']))
{ {
@ -316,7 +371,8 @@ class Base
$old_url = $url; $old_url = $url;
$_url = self::symlinkCache_resolve($url); $_url = self::symlinkCache_resolve($url);
$url = @readlink($url) ?: (Vfs::parse_url($_url, PHP_URL_PATH) != $parts['path'] ? $url = @readlink($url) ?: (Vfs::parse_url($_url, PHP_URL_PATH) != $parts['path'] ?
str_replace([$parts['path'],Vfs::parse_url($old_url,PHP_URL_SCHEME)],[$_url,Vfs::parse_url(Vfs::resolve_url($_url),PHP_URL_SCHEME)],$url) : null) ?:$url; str_replace([$parts['path'], Vfs::parse_url($old_url, PHP_URL_SCHEME)], [$_url,
Vfs::parse_url(Vfs::resolve_url($_url), PHP_URL_SCHEME)], $url) : null) ?: $url;
$is_link = $old_url == $url; $is_link = $old_url == $url;
} }
if($replace_user_pass_host && !$is_link) if($replace_user_pass_host && !$is_link)
@ -327,7 +383,10 @@ class Base
return $url; return $url;
} }
} }
if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path') can't resolve path!\n"); if(self::LOG_LEVEL > 0)
{
error_log(__METHOD__ . "('$path') can't resolve path!\n");
}
trigger_error(__METHOD__ . "($path) can't resolve path!\n", E_USER_WARNING); trigger_error(__METHOD__ . "($path) can't resolve path!\n", E_USER_WARNING);
return false; return false;
} }
@ -349,9 +408,21 @@ class Base
{ {
$path = self::get_path($_path); $path = self::get_path($_path);
if (isset(self::$symlink_cache[$path])) return; // nothing to do if(isset(self::$symlink_cache[$path]))
{
if ($target[0] != '/') $target = Vfs::parse_url($target,PHP_URL_PATH); // nothing to do
return;
}
if($target[0] != '/')
{
$query = Vfs::parse_url($target, PHP_URL_QUERY);
$target = Vfs::parse_url($target, PHP_URL_PATH);
if($query)
{
// Don't cache without query, some StreamWrappers need those parameters
$target = "?$query";
}
}
self::$symlink_cache[$path] = $target; self::$symlink_cache[$path] = $target;
@ -360,7 +431,10 @@ class Base
{ {
return strlen($a) - strlen($b); return strlen($a) - strlen($b);
}); });
if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$target) cache now ".array2string(self::$symlink_cache)); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . "($path,$target) cache now " . array2string(self::$symlink_cache));
}
} }
/** /**
@ -373,7 +447,10 @@ class Base
$path = self::get_path($_path); $path = self::get_path($_path);
unset(self::$symlink_cache[$path]); unset(self::$symlink_cache[$path]);
if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path) cache now ".array2string(self::$symlink_cache)); if(self::LOG_LEVEL > 1)
{
error_log(__METHOD__ . "($path) cache now " . array2string(self::$symlink_cache));
}
} }
/** /**
@ -398,7 +475,10 @@ class Base
if($path == $p) if($path == $p)
{ {
if ($do_symlink) $target = $t; if($do_symlink)
{
$target = $t;
}
break; break;
} }
elseif(substr($path, 0, $strlen_p + 1) == $p . '/') elseif(substr($path, 0, $strlen_p + 1) == $p . '/')
@ -407,7 +487,10 @@ class Base
break; break;
} }
} }
if (self::LOG_LEVEL > 1 && isset($target)) error_log(__METHOD__."($path) = $target"); if(self::LOG_LEVEL > 1 && isset($target))
{
error_log(__METHOD__ . "($path) = $target");
}
return isset($target) ? $target : $path; return isset($target) ? $target : $path;
} }
@ -489,12 +572,16 @@ class Base
list($app, $app_scheme) = explode('.', $scheme); list($app, $app_scheme) = explode('.', $scheme);
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', // streamwrapper in $app\Vfs 'EGroupware\\' . ucfirst($app) . '\\Vfs\\' . ucfirst($app_scheme) . '\\StreamWrapper',
// streamwrapper in $app\Vfs
str_replace('.', '_', $scheme) . '_stream_wrapper', // old (flat) name str_replace('.', '_', $scheme) . '_stream_wrapper', // old (flat) name
) as $class) ) as $class)
{ {
//error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class))); //error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class)));
if (class_exists($class)) return $class; if(class_exists($class))
{
return $class;
}
} }
} }
@ -565,7 +652,10 @@ class Base
return false; return false;
} }
$k = (string)Vfs::parse_url($url, PHP_URL_SCHEME); $k = (string)Vfs::parse_url($url, PHP_URL_SCHEME);
if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array(); if(!(is_array($scheme2urls[$k])))
{
$scheme2urls[$k] = array();
}
$scheme2urls[$k][$path] = $url; $scheme2urls[$k][$path] = $url;
} }
$ret = array(); $ret = array();
@ -575,7 +665,10 @@ class Base
{ {
if(!class_exists($class = Vfs\StreamWrapper::scheme2class($scheme)) || !method_exists($class, $name)) if(!class_exists($class = Vfs\StreamWrapper::scheme2class($scheme)) || !method_exists($class, $name))
{ {
if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING); if(!$fail_silent)
{
trigger_error("Can't $name for scheme $scheme!\n", E_USER_WARNING);
}
return $fail_silent === 'null' ? null : false; return $fail_silent === 'null' ? null : false;
} }
$callback = [$instanciate ? new $class($url) : $class, $name]; $callback = [$instanciate ? new $class($url) : $class, $name];

View File

@ -2264,7 +2264,6 @@ div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button {
float: left; float: left;
margin-right: 1em; margin-right: 1em;
background-position: 3px; background-position: 3px;
padding-left: 15px;
outline: none; outline: none;
} }
div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button.right { div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button.right {

View File

@ -645,8 +645,7 @@ class InfologApp extends EgwApp
*/ */
getWindowTitle() getWindowTitle()
{ {
var widget = this.et2.getWidgetById('info_subject'); return this.et2.getValueById("info_subject");
if(widget) return widget.options.value;
} }
/** /**

View File

@ -75,7 +75,7 @@
</row> </row>
<row class="$row_cont[info_cat] $row_cont[class]" valign="top"> <row class="$row_cont[info_cat] $row_cont[class]" valign="top">
<hbox align="center" class="infolog_CompletedClmn"> <hbox align="center" class="infolog_CompletedClmn">
<image label="$row_cont[info_type]" src="infolog/${row}[info_type]" default_src="infolog/navbar"/> <image label="$row_cont[info_type]" src="infolog/$row_cont[info_type]" default_src="infolog/navbar"/>
<image label="$row_cont[info_status_label]" id="edit_status[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="infolog/$row_cont[info_status_label]" default_src="status"/> <image label="$row_cont[info_status_label]" id="edit_status[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="infolog/$row_cont[info_status_label]" default_src="status"/>
<image label="$row_cont[info_percent]" id="edit_percent[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="$row_cont[info_percent]"/> <image label="$row_cont[info_percent]" id="edit_percent[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="$row_cont[info_percent]"/>
<progress label="$row_cont[info_percent]" id="{$row}[info_percent2]" href="javascript:egw.open($row_cont[info_id],'infolog');"/> <progress label="$row_cont[info_percent]" id="{$row}[info_percent2]" href="javascript:egw.open($row_cont[info_id],'infolog');"/>

View File

@ -326,6 +326,11 @@ class mail_integration {
} }
} }
//consider all addresses in the header
foreach (['TO','CC','BCC', 'FROM'] as $h)
{
$mailcontent['mailaddress'] .= ','.$headers[$h];
}
// Convert addresses to email and personal // Convert addresses to email and personal
$addresses = imap_rfc822_parse_adrlist($mailcontent['mailaddress'],''); $addresses = imap_rfc822_parse_adrlist($mailcontent['mailaddress'],'');
foreach ($addresses as $address) foreach ($addresses as $address)