Merge branch 'master' into web-components

This commit is contained in:
nathan
2021-08-12 10:35:46 -06:00
48 changed files with 2742 additions and 4242 deletions

2
.gitignore vendored
View File

@ -65,6 +65,8 @@ status/
smallpart/ smallpart/
swoolepush/ swoolepush/
webauthn/ webauthn/
*/js/*.map
*/js/app.min.js
addressbook/js/app.js addressbook/js/app.js
admin/js/app.js admin/js/app.js
api/js/etemplate/*.js api/js/etemplate/*.js

View File

@ -41,7 +41,7 @@ module.exports = function (grunt) {
files: { files: {
"pixelegg/css/pixelegg.min.css": [ "pixelegg/css/pixelegg.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -59,7 +59,7 @@ module.exports = function (grunt) {
], ],
"pixelegg/css/mobile.min.css": [ "pixelegg/css/mobile.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -77,7 +77,7 @@ module.exports = function (grunt) {
], ],
"pixelegg/mobile/fw_mobile.min.css": [ "pixelegg/mobile/fw_mobile.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -94,7 +94,7 @@ module.exports = function (grunt) {
], ],
"pixelegg/css/monochrome.min.css": [ "pixelegg/css/monochrome.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -112,7 +112,7 @@ module.exports = function (grunt) {
], ],
"pixelegg/css/modern.min.css": [ "pixelegg/css/modern.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -134,7 +134,7 @@ module.exports = function (grunt) {
files: { files: {
"jdots/css/high-contrast.min.css": [ "jdots/css/high-contrast.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -155,7 +155,7 @@ module.exports = function (grunt) {
], ],
"jdots/css/jdots.min.css": [ "jdots/css/jdots.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -175,7 +175,7 @@ module.exports = function (grunt) {
], ],
"jdots/css/orange-green.min.css": [ "jdots/css/orange-green.min.css": [
"api/js/jquery/chosen/chosen.css", "api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css", "node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css", "vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css", "api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css", "api/js/jquery/blueimp/css/blueimp-gallery.min.css",

View File

@ -1,24 +1,29 @@
# EGroupware # EGroupware
| Branch | Status | Tools | Usage |
| ------ | ------ | ----- | ----- |
| master | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=master)](https://travis-ci.org/EGroupware/egroupware) | <img src="https://travis-ci.com/images/logos/TravisCI-Full-Color.png" width="108" alt="Travis CI"/> | runs unit-tests after each commit |
| 20.1 | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=20.1)](https://travis-ci.org/EGroupware/egroupware) | [![Scrutinizer CI](https://scrutinizer-ci.com/images/logo.png) scrutinizer](https://scrutinizer-ci.com/g/EGroupware/egroupware/) | runs static analysis on our codebase |
| 19.1 | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=19.1)](https://travis-ci.org/EGroupware/egroupware) | <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ2scF5HUwLnJVnk2UhYwWpUXHmLQYNXM5yBw&usqp=CAU" width="110" alt="BrowserStack" /> | manual testing with unusual browser versions or platforms |
### Default and prefered installation method for EGroupware is via your package manager: | Tools | Usage |
| ----- | ----- |
| <img src="https://travis-ci.com/images/logos/TravisCI-Full-Color.png" width="108" alt="Travis CI"/> | runs unit-tests after each commit |
| [![Scrutinizer CI](https://scrutinizer-ci.com/images/logo.png) scrutinizer](https://scrutinizer-ci.com/g/EGroupware/egroupware/) | runs static analysis on our codebase |
| <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ2scF5HUwLnJVnk2UhYwWpUXHmLQYNXM5yBw&usqp=CAU" width="110" alt="BrowserStack" /> | manual testing with unusual browser versions or platforms |
https://software.opensuse.org/download.html?project=server%3AeGroupWare&package=egroupware-epl ### Default and prefered installation method for EGroupware is via your Linux package manager:
### Installing EGroupware 20.1 via Docker: * [Installation & Update instructions](https://github.com/EGroupware/egroupware/wiki/Installation-using-egroupware-docker-RPM-DEB-package)
EGroupware 20.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in [doc/docker](https://github.com/EGroupware/egroupware/tree/20.1/doc/docker) subdirectory. * [Distribution specific instructions](https://github.com/EGroupware/egroupware/wiki/Distribution-specific-instructions)
### Installing EGroupware 19.1 via Docker: > Every other method (including a developer installation by cloning the repo) is way more complicated AND does not include all features, as part's of EGroupware are running in different containers, eg. the push-server!
EGroupware 19.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in [doc/docker](https://github.com/EGroupware/egroupware/tree/19.1/doc/docker) subdirectory.
### Installing EGroupware 21.1 via Docker for non-Linux environments or not supported Linux distros:
EGroupware 21.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in our [Wiki](https://github.com/EGroupware/egroupware/wiki/Docker-compose-installation) and in [doc/docker](https://github.com/EGroupware/egroupware/tree/21.1/doc/docker) subdirectory.
### Installing EGroupware development version: ### Installing EGroupware development version via Docker:
* this is the prefered developer installation, as it contains eg. a push-server container
* https://github.com/EGroupware/egroupware/tree/master/doc/docker/development
### Deprecated EGroupware development installation:
* install composer.phar from https://getcomposer.org/download/ * install composer.phar from https://getcomposer.org/download/
* optional: for minified JavaScript and CSS install nodejs and grunt * for JavaScript dependencies and build install nodejs and npm
* optional: for minified CSS install grunt
``` ```
apt/yum/zypper install nodejs apt/yum/zypper install nodejs
npm install -g grunt-cli npm install -g grunt-cli

View File

@ -1528,6 +1528,9 @@ class AddressbookApp extends EgwApp
*/ */
private videoconference_isUserOnline(_action, _selected) private videoconference_isUserOnline(_action, _selected)
{ {
// ATM we're not supporting status in mobile theme
if (egwIsMobile()) return false;
let list = app.status ? app.status.getEntireList() : {}; let list = app.status ? app.status.getEntireList() : {};
for (let sel in _selected) for (let sel in _selected)
{ {

View File

@ -13,7 +13,6 @@
egw_action_common; egw_action_common;
egw_action_popup; egw_action_popup;
vendor.bower-asset.jquery.dist.jquery; vendor.bower-asset.jquery.dist.jquery;
/vendor/bower-asset/jquery-ui/jquery-ui.js;
*/ */
import {egwAction,egwActionImplementation} from "./egw_action.js"; import {egwAction,egwActionImplementation} from "./egw_action.js";

View File

@ -12,14 +12,12 @@
/*egw:uses /*egw:uses
vendor.bower-asset.jquery.dist.jquery; vendor.bower-asset.jquery.dist.jquery;
egw_menu; egw_menu;
/api/js/jquery/jquery-tap-and-hold/jquery.tapandhold.js;
*/ */
import {egwAction, egwActionImplementation, egwActionObject} from './egw_action.js'; import {egwAction, egwActionImplementation, egwActionObject} from './egw_action.js';
import {egwFnct} from './egw_action_common.js'; import {egwFnct} from './egw_action_common.js';
import {egwMenu, _egw_active_menu} from "./egw_menu.js"; import {egwMenu, _egw_active_menu} from "./egw_menu.js";
import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants.js"; import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants.js";
import "../jquery/jquery-tap-and-hold/jquery.tapandhold.js";
if (typeof window._egwActionClasses == "undefined") if (typeof window._egwActionClasses == "undefined")
window._egwActionClasses = {}; window._egwActionClasses = {};
@ -280,7 +278,39 @@ export function egwPopupActionImplementation()
return false; return false;
}; };
ai._handleTapHold = function (_node, _callback)
{
let holdTimer = 600;
let maxDistanceAllowed = 40;
let tapTimeout = null;
let startx = 0;
let starty = 0;
//TODO (todo-jquery): ATM we need to convert the possible given jquery dom node object into DOM Element, this
// should be no longer neccessary after removing jQuery nodes.
if (_node instanceof jQuery)
{
_node = _node[0];
}
_node.addEventListener('touchstart', function(e){
tapTimeout = setTimeout(function(event){
_callback(e);
}, holdTimer);
startx = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
starty = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
});
_node.addEventListener('touchend', function(){
clearTimeout(tapTimeout);
});
_node.addEventListener('touchmove', function(_event){
if (tapTimeout == null) return;
let e = _event.originalEvent;
let x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
let y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
if (Math.sqrt((x-startx)*(x-startx) + (y-starty)+(y-starty)) > maxDistanceAllowed) clearTimeout(tapTimeout);
});
}
/** /**
* Registers the handler for the context menu * Registers the handler for the context menu
* *
@ -292,14 +322,6 @@ export function egwPopupActionImplementation()
ai._registerContext = function(_node, _callback, _context) ai._registerContext = function(_node, _callback, _context)
{ {
var contextHandler = function(e) { var contextHandler = function(e) {
if(egwIsMobile())
{
if (e.originalEvent.which == 3)
{
// Enable onhold trigger till we define a better handler for tree contextmenu
// return;
}
}
//Obtain the event object //Obtain the event object
if (!e) if (!e)
@ -327,7 +349,7 @@ export function egwPopupActionImplementation()
}; };
// Safari still needs the taphold to trigger contextmenu // Safari still needs the taphold to trigger contextmenu
// Chrome has default event on touch and hold which acts like right click // Chrome has default event on touch and hold which acts like right click
jQuery(_node).bind('taphold', contextHandler); this._handleTapHold(_node, contextHandler);
jQuery(_node).on('contextmenu', contextHandler); jQuery(_node).on('contextmenu', contextHandler);
}; };

View File

@ -67,6 +67,7 @@ import {et2_template} from "./et2_widget_template";
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
import {egwIsMobile} from "../egw_action/egw_action_common.js"; import {egwIsMobile} from "../egw_action/egw_action_common.js";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
//import {et2_selectAccount} from "./et2_widget_SelectAccount"; //import {et2_selectAccount} from "./et2_widget_SelectAccount";
@ -2078,30 +2079,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
self.selectPopup = null; self.selectPopup = null;
}; };
const $select = jQuery(select.getDOMNode()); const $select = jQuery(select.getDOMNode());
$select.find('.ui-multiselect-checkboxes').sortable({
placeholder:'ui-fav-sortable-placeholder', let sortablejs = Sortable.create(select.getDOMNode().getElementsByClassName('ui-multiselect-checkboxes')[0], {
items:'li[class^="selcolumn_sortable_col"]', ghostClass: 'ui-fav-sortable-placeholder',
cancel: 'li[class^="selcolumn_sortable_#"]', draggable: 'li[class^="selcolumn_sortable_col"]',
cursor: "move", filter: 'li[class^="selcolumn_sortable_#"]',
tolerance: "pointer", direction: 'vertical',
axis: 'y', delay: 25,
containment: "parent",
delay: 250, //(millisecond) delay before the sorting should start
beforeStop: function(event, ui) {
jQuery('li[class^="selcolumn_sortable_#"]', this).css({
opacity: 1
});
},
start: function(event, ui){
jQuery('li[class^="selcolumn_sortable_#"]', this).css({
opacity: 0.5
});
},
sort: function (event, ui)
{
jQuery( this ).sortable("refreshPositions" );
}
}); });
$select.disableSelection(); $select.disableSelection();
$select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){
// @ts-ignore // @ts-ignore

View File

@ -12,7 +12,6 @@
/*egw:uses /*egw:uses
/vendor/bower-asset/jquery/dist/jquery.js; /vendor/bower-asset/jquery/dist/jquery.js;
/vendor/bower-asset/jquery-ui/jquery-ui.js;
et2_core_inputWidget; et2_core_inputWidget;
et2_core_valueWidget; et2_core_valueWidget;
*/ */

View File

@ -19,6 +19,7 @@ import {et2_INextmatchHeader} from "./et2_extension_nextmatch";
import {et2_dropdown_button} from "./et2_widget_dropdown_button"; import {et2_dropdown_button} from "./et2_widget_dropdown_button";
import {ClassWithAttributes} from "./et2_core_inheritance"; import {ClassWithAttributes} from "./et2_core_inheritance";
import {egw, egw_getFramework} from "../jsapi/egw_global"; import {egw, egw_getFramework} from "../jsapi/egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/** /**
* Favorites widget, designed for use with a nextmatch widget * Favorites widget, designed for use with a nextmatch widget
@ -177,18 +178,19 @@ export class et2_favorites extends et2_dropdown_button implements et2_INextmatch
} }
}; };
//Add Sortable handler to nm fav. menu /**
jQuery(this.menu).sortable({ * todo (@todo-jquery-ui): the sorting does not work at the moment becuase of jquery-ui menu being used in order to create dropdown
* buttons menu. Once we replace the et2_widget_dropdown_button with web component this should be adapted
items:'li:not([data-id$="add"])', * and working again.
placeholder:'ui-fav-sortable-placeholder', **/
delay: 250, //(millisecond) delay before the sorting should start let sortablejs = Sortable.create(this.menu[0], {
update: function () ghostClass: 'ui-fav-sortable-placeholder',
{ draggable: 'li:not([data-id$="add"])',
self.favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'}); delay: 25,
dataIdAttr:'data-id',
self.egw().set_preference(self.options.app,'fav_sort_pref',self.favSortedList); onSort: function(event){
self.favSortedList = sortablejs.toArray();
self.egw.set_preference(self.options.app,'fav_sort_pref', self.favSortedList );
sideBoxDOMNodeSort(self.favSortedList); sideBoxDOMNodeSort(self.favSortedList);
} }
}); });

View File

@ -23,6 +23,7 @@ import {et2_action_object_impl, et2_DOMWidget} from "./et2_core_DOMWidget";
import {egw_getAppObjectManager, egwActionObject} from '../egw_action/egw_action.js'; import {egw_getAppObjectManager, egwActionObject} from '../egw_action/egw_action.js';
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "./et2_core_xml"; import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "./et2_core_xml";
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/** /**
@ -117,6 +118,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
private wrapper = null; private wrapper = null;
private lastRowNode: null; private lastRowNode: null;
private sortablejs : Sortable = null;
/** /**
* Constructor * Constructor
* *
@ -943,55 +945,49 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
*/ */
set_sortable(sortable: boolean | Function) set_sortable(sortable: boolean | Function)
{ {
const $node = jQuery(this.getDOMNode()); const self = this;
if(!sortable) let tbody = this.getDOMNode().getElementsByTagName('tbody')[0];
if(!sortable && this.sortablejs)
{ {
$node.sortable("destroy"); this.sortablejs.destroy();
return; return;
} }
// Make sure rows have IDs, so sortable has something to return for (let i =0; i < tbody.children.length; i++)
jQuery('tr', this.tbody).each(function(index) { {
const $this = jQuery(this); if (!tbody.children[i].classList.contains('th') && !tbody.children[i].id)
{
tbody.children[i].setAttribute('id', i.toString());
}
}
// Header does not participate in sorting this.sortablejs = new Sortable(tbody,{
if($this.hasClass('th')) return; group: this.options.sortable_connectWith,
draggable: "tr:not(.th)",
// If row doesn't have an ID, assign the index as ID filter: this.options.sortable_cancel,
if(!$this.attr("id")) $this.attr("id", index); ghostClass: this.options.sortable_placeholder,
}); dataIdAttr: 'id',
onAdd:function (event) {
const self = this; if (typeof self.options.sortable_recieveCallback == 'function') {
self.options.sortable_recieveCallback.call(self, event, this, self.id);
// Set up sortable }
$node.sortable({ },
// Header does not participate in sorting onStart: function (event, ui) {
items: "> tbody > tr:not(.th)", if (typeof self.options.sortable_startCallback == 'function') {
distance: 15, self.options.sortable_startCallback.call(self, event, this, self.id);
cancel: this.options.sortable_cancel, }
placeholder: this.options.sortable_placeholder, },
containment: this.options.sortable_containment, onSort: function (event) {
connectWith: this.options.sortable_connectWith,
update: function(event, ui) {
self.egw().json(sortable,[ self.egw().json(sortable,[
self.getInstanceManager().etemplate_exec_id, self.getInstanceManager().etemplate_exec_id,
$node.sortable("toArray"), self.sortablejs.toArray(),
self.id], self.id],
null, null,
self, self,
true true
).sendRequest(); ).sendRequest();
}, },
receive: function (event, ui) {
if (typeof self.sortable_recieveCallback == 'function') {
self.sortable_recieveCallback.call(self, event, ui, self.id);
}
},
start: function (event, ui) {
if (typeof self.options.sortable_startCallback == 'function') {
self.options.sortable_startCallback.call(self, event, ui, self.id);
}
}
}); });
} }

View File

@ -638,7 +638,7 @@ export class et2_taglist extends et2_selectbox implements et2_IResizeable
}); });
// if value has already been set, re-set it by it's id(s) // if value has already been set, re-set it by it's id(s)
if (this.options.select_options.length && this.options.value.length) { if (this.options.select_options.length && this.options.value?.length) {
this.set_value(this.options.value.map((v) => v.id)); this.set_value(this.options.value.map((v) => v.id));
} }
} }

View File

@ -25,7 +25,7 @@ import './fw_browser.js';
import './fw_ui.js'; import './fw_ui.js';
import './fw_classes.js'; import './fw_classes.js';
import '../jsapi/egw_inheritance.js'; import '../jsapi/egw_inheritance.js';
import "sortablejs/Sortable.min.js";
/** /**
* *
* @param {DOMWindow} window * @param {DOMWindow} window
@ -48,31 +48,16 @@ import '../jsapi/egw_inheritance.js';
init: function() init: function()
{ {
this._super.apply(this,arguments); this._super.apply(this,arguments);
let self = this;
this.setBottomLine(this.parent.entries); this.setBottomLine(this.parent.entries);
//Make the base Div sortable. Set all elements with the style "egw_fw_ui_sidemenu_entry_header"
//as handle this.elemDiv.classList.add('ui-sortable')
if(jQuery(this.elemDiv).data('uiSortable')) Sortable.create(this.elemDiv,{
{ onSort: function (evt) {
jQuery(this.elemDiv).sortable("destroy"); self.parent.isDraged = true;
} self.parent.refreshSort();
jQuery(this.elemDiv).sortable({
handle: ".egw_fw_ui_sidemenu_entry_header",
distance: 15,
start: function(event, ui)
{
var parent = ui.item.context._parent;
parent.isDraged = true;
parent.parent.startDrag.call(parent.parent);
}, },
stop: function(event, ui) direction: 'vertical'
{
var parent = ui.item.context._parent;
parent.parent.stopDrag.call(parent.parent);
parent.parent.refreshSort.call(parent.parent);
},
opacity: 0.7,
axis: 'y'
}); });
}, },
@ -107,31 +92,6 @@ import '../jsapi/egw_inheritance.js';
this._super.apply(this,arguments); this._super.apply(this,arguments);
this.sortCallback = _sortCallback; this.sortCallback = _sortCallback;
}, },
/**
*
* @returns {undefined}
*/
startDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).show();
jQuery(this.elemDiv).sortable("refresh");
}
},
/**
*
* @returns {undefined}
*/
stopDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).hide();
jQuery(this.elemDiv).sortable("refresh");
}
},
/** /**
* Called by the sidemenu elements whenever they were sorted. An array containing * Called by the sidemenu elements whenever they were sorted. An array containing

View File

@ -1,19 +0,0 @@
jQuery - Tap and Hold
=====================
This jQuery plugin lets you detect a tap and hold event on touch interfaces.
How to use it?
1) Add the jQuery Tap and Hold plugin into your HTML
<script src="jquery.tapandhold.js" type="text/javascript"></script>
2) Bind a tap and hold handler function to the tap and hold event of an element.
$("#myDiv").bind("taphold", function(event){
alert("This is a tap and hold!");
});
You can check a working example in examples/example1.html

View File

@ -1,17 +0,0 @@
<html>
<head>
<title>jQuery - Tap and Hold</title>
<script src="http://code.jquery.com/jquery-1.7.min.js" type="text/javascript"></script>
<script src="https://raw.github.com/zaubersoftware/jquery-tap-and-hold/master/jquery.tapandhold.js" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$(".tap").bind("taphold", function(){
alert("Hello Tap and Hold World!");
});
});
</script>
</head>
<body>
<div class="tap" style="width:200px; height:200px; background-color: blue; margin: 100px 300px;"></div>
</body>
</html>

View File

@ -1,136 +0,0 @@
/**
* Copyright (c) 2011 Zauber S.A. <http://www.zaubersoftware.com/>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Guido Marucci Blas - guido@zaubersoftware.com
* @description Adds a handler for a custom event 'taphold' that handles a
* tap and hold on touch interfaces.
*/
(function($) {
var TAP_AND_HOLD_TRIGGER_TIMER = 600;
var MAX_DISTANCE_ALLOWED_IN_TAP_AND_HOLD_EVENT = 40;
var TOUCHSTART = "touchstart";
var TOUCHEND = "touchend";
var TOUCHMOVE = "touchmove";
// For debugging only
// var TOUCHSTART = "mousedown";
// var TOUCHEND = "mouseup";
// var TOUCHMOVE = "mousemove";
var tapAndHoldTimer = null;
function calculateEuclideanDistance(x1, y1, x2, y2) {
var diffX = (x2 - x1);
var diffY = (y2 - y1);
return Math.sqrt((diffX * diffX) + (diffY * diffY));
};
function onTouchStart(event) {
var e = event.originalEvent;
// Only start detector if and only if one finger is over the widget
if (!e.touches || (e.targetTouches.length === 1 && e.touches.length === 1)) {
startTapAndHoldDetector.call(this, event)
var element = $(this);
element.bind(TOUCHMOVE, onTouchMove);
element.bind(TOUCHEND, onTouchEnd);
} else {
stopTapAndHoldDetector.call(this);
}
};
function onTouchMove(event) {
if (tapAndHoldTimer == null) {
return;
}
var e = event.originalEvent;
var x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
var y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
var tapAndHoldPoint = $(this).data("taphold.point");
var euclideanDistance = calculateEuclideanDistance(tapAndHoldPoint.x, tapAndHoldPoint.y, x, y);
if (euclideanDistance > MAX_DISTANCE_ALLOWED_IN_TAP_AND_HOLD_EVENT) {
stopTapAndHoldDetector.call(this);
}
};
function onTouchEnd(event) {
stopTapAndHoldDetector.call(this);
};
function onTapAndHold(event) {
clear.call(this);
$(this).data("taphold.handler").call(this, event);
};
function clear() {
tapAndHoldTimer = null;
$(this).unbind(TOUCHMOVE, onTouchMove);
$(this).unbind(TOUCHEND, onTouchEnd);
};
function startTapAndHoldDetector(event) {
if (tapAndHoldTimer != null) {
return;
}
var self = this;
tapAndHoldTimer = setTimeout(function(){
onTapAndHold.call(self, event)
}, TAP_AND_HOLD_TRIGGER_TIMER);
// Stores tap x & y
var e = event.originalEvent;
var tapAndHoldPoint = {};
tapAndHoldPoint.x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
tapAndHoldPoint.y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
$(this).data("taphold.point", tapAndHoldPoint);
};
function stopTapAndHoldDetector() {
clearTimeout(tapAndHoldTimer);
clear.call(this);
};
$.event.special["taphold"] = {
setup: function() {
},
add: function(handleObj) {
$(this).data("taphold.handler", handleObj.handler);
if (handleObj.data) {
$(this).bind(TOUCHSTART, handleObj.data, onTouchStart);
} else {
$(this).bind(TOUCHSTART, onTouchStart);
}
},
remove: function(handleObj) {
stopTapAndHoldDetector.call(this);
if (handleObj.data) {
$(this).unbind(TOUCHSTART, handleObj.data, onTouchStart);
} else {
$(this).unbind(TOUCHSTART, onTouchStart);
}
},
teardown: function() {
}
};
})(jQuery);

View File

@ -78,7 +78,7 @@ window.app = {classes: {}};
window.egw_appName = egw_script.getAttribute('data-app'); window.egw_appName = egw_script.getAttribute('data-app');
// split includes in legacy js and modules // split includes in legacy js and modules
const legacy_js_regexp = /\/dhtmlx|jquery-ui/; const legacy_js_regexp = /\/dhtmlx|jquery-ui-dist/;
// check if egw object was injected by window open // check if egw object was injected by window open
if (typeof window.egw == 'undefined') if (typeof window.egw == 'undefined')

View File

@ -17,6 +17,7 @@ import {et2_dialog} from "../etemplate/et2_widget_dialog";
import {et2_createWidget} from "../etemplate/et2_core_widget"; import {et2_createWidget} from "../etemplate/et2_core_widget";
import {et2_favorites} from "../etemplate/et2_widget_favorites"; import {et2_favorites} from "../etemplate/et2_widget_favorites";
import type {IegwAppLocal} from "./egw_global"; import type {IegwAppLocal} from "./egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/** /**
* Type for push-message * Type for push-message
@ -781,29 +782,15 @@ export abstract class EgwApp
}) })
.addClass("ui-helper-clearfix"); .addClass("ui-helper-clearfix");
//Add Sortable handler to sideBox fav. menu let el = document.getElementById('favorite_sidebox_'+this.appname).getElementsByTagName('ul')[0];
jQuery('ul','#favorite_sidebox_'+this.appname).sortable({ let sortablejs = Sortable.create(el, {
items:'li:not([data-id$="add"])', ghostClass: 'ui-fav-sortable-placeholder',
placeholder:'ui-fav-sortable-placeholder', draggable: 'li:not([data-id$="add"])',
delay:250, //(millisecond) delay before the sorting should start delay: 25,
helper: function(event, item : any) { dataIdAttr:'data-id',
// We'll need to know which app this is for onSort: function(event){
item.attr('data-appname',self.appname); let favSortedList = sortablejs.toArray();
// Create custom helper so it can be dragged to Home
var h_parent = item.parent().parent().clone();
h_parent.find('li').not('[data-id="'+item.attr('data-id')+'"]').remove();
h_parent.appendTo('body');
return h_parent;
},
// @ts-ignore
refreshPositions: true,
update: function (event, ui)
{
// @ts-ignore
var favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'});
self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList); self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList);
self._refresh_fav_nm(); self._refresh_fav_nm();
} }
}); });

View File

@ -10,7 +10,6 @@
*/ */
/*egw:uses /*egw:uses
/vendor/bower-asset/jquery-ui/jquery-ui.js;
jquery.jquery-ui-timepicker-addon; jquery.jquery-ui-timepicker-addon;
egw_core; egw_core;
@ -19,8 +18,6 @@
egw_css; egw_css;
*/ */
import "../../../vendor/bower-asset/jquery/dist/jquery.min.js";
//import "../../../vendor/bower-asset/jquery-ui/jquery-ui.js";
import "../jquery/jquery.noconflict.js"; import "../jquery/jquery.noconflict.js";
//import "../jquery/jquery-ui-timepicker-addon.js"; //import "../jquery/jquery-ui-timepicker-addon.js";
import './egw_core.js'; import './egw_core.js';

View File

@ -20,6 +20,13 @@ egw.extend('jsonq', egw.MODULE_GLOBAL, function()
{ {
"use strict"; "use strict";
/**
* Explicit registered push callbacks
*
* @type {Function[]}
*/
let push_callbacks = [];
/** /**
* Queued json requests (objects with attributes menuaction, parameters, context, callback, sender and callbeforesend) * Queued json requests (objects with attributes menuaction, parameters, context, callback, sender and callbeforesend)
* *
@ -149,6 +156,34 @@ egw.extend('jsonq', egw.MODULE_GLOBAL, function()
}, 100); }, 100);
} }
return uid; return uid;
},
/**
* Register a callback to receive push broadcasts eg. in a popup or iframe
*
* It's also used internally by egw_message's push method to dispatch to the registered callbacks.
*
* @param {Function|PushData} data callback (with bound context) or PushData to dispatch to callbacks
*/
registerPush: function(data)
{
if (typeof data === "function")
{
push_callbacks.push(data);
}
else
{
for (let n in push_callbacks)
{
try {
push_callbacks[n].call(this, data);
}
// if we get an exception, we assume the callback is no longer available and remove it
catch (ex) {
push_callbacks.splice(n, 1);
}
}
}
} }
}; };

View File

@ -459,6 +459,9 @@ egw.extend('message', egw.MODULE_WND_LOCAL, function(_app, _wnd)
app_obj.push(pushData); app_obj.push(pushData);
} }
} }
// call the global registered push callbacks
this.registerPush(pushData);
} }
}; };

View File

@ -14,7 +14,7 @@ $setup_info['api']['title'] = 'EGroupware API';
$setup_info['api']['version'] = '21.1'; $setup_info['api']['version'] = '21.1';
$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.20210629'; $setup_info['api']['versions']['maintenance_release'] = '21.1.20210723';
$setup_info['api']['enable'] = 3; $setup_info['api']['enable'] = 3;
$setup_info['api']['app_order'] = 1; $setup_info['api']['app_order'] = 1;
$setup_info['api']['license'] = 'GPL'; $setup_info['api']['license'] = 'GPL';

View File

@ -425,7 +425,7 @@ class Asyncservice
{ {
// checking / setting up egw_info/user // checking / setting up egw_info/user
// //
if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id']) //if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id'])
{ {
// run notifications, before changing account_id of enviroment // run notifications, before changing account_id of enviroment
Link::run_notifies(); Link::run_notifies();

View File

@ -71,15 +71,18 @@ class Applications
} }
/** /**
* populate array with a list of installed apps * Populate array with a list of installed apps
* *
* egw_applications.app_enabled = -1 is NOT installed, but an uninstalled autoinstall app!
*
* @return array[]
*/ */
function read_installed_apps() function read_installed_apps()
{ {
$GLOBALS['egw_info']['apps'] = Api\Cache::getInstance(__CLASS__, 'apps', function() $GLOBALS['egw_info']['apps'] = Api\Cache::getInstance(__CLASS__, 'apps', function()
{ {
$apps = array(); $apps = array();
foreach($this->db->select($this->table_name,'*',false,__LINE__,__FILE__,false,'ORDER BY app_order ASC') as $row) foreach($this->db->select($this->table_name,'*', ['app_enabled != -1'],__LINE__,__FILE__,false,'ORDER BY app_order ASC') as $row)
{ {
$apps[$row['app_name']] = Array( $apps[$row['app_name']] = Array(
'title' => $row['app_name'], 'title' => $row['app_name'],

View File

@ -1003,7 +1003,7 @@ abstract class Framework extends Framework\Extra
self::includeCSS('/api/js/jquery/chosen/chosen.css'); self::includeCSS('/api/js/jquery/chosen/chosen.css');
// eTemplate2 uses jQueryUI, so load it first so et2 can override if needed // eTemplate2 uses jQueryUI, so load it first so et2 can override if needed
self::includeCSS("/vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css"); self::includeCSS("/node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css");
// eTemplate2 - load in top so sidebox has styles too // eTemplate2 - load in top so sidebox has styles too
self::includeCSS('/api/templates/default/etemplate2.css'); self::includeCSS('/api/templates/default/etemplate2.css');
@ -1074,8 +1074,8 @@ abstract class Framework extends Framework\Extra
)); ));
} }
// manually load old legacy javascript dhtmlx & jQuery-UI via script tag // manually load old legacy javascript dhtmlx & jQuery-UI via script tag
self::includeJS('/vendor/bower-asset/jquery-ui/jquery-ui.js'); self::includeJS('/node_modules/jquery-ui-dist/jquery-ui.min.js');
self::includeJS('/api/js/jquery/jquery-ui-timepicker-addon.js'); self::includeJS('/node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js');
self::includeJS('/api/js/dhtmlxtree/codebase/dhtmlxcommon.js'); self::includeJS('/api/js/dhtmlxtree/codebase/dhtmlxcommon.js');
self::includeJS('/api/js/dhtmlxMenu/sources/dhtmlxmenu.js'); self::includeJS('/api/js/dhtmlxMenu/sources/dhtmlxmenu.js');
self::includeJS('/api/js/dhtmlxMenu/sources/ext/dhtmlxmenu_ext.js'); self::includeJS('/api/js/dhtmlxMenu/sources/ext/dhtmlxmenu_ext.js');

View File

@ -241,7 +241,7 @@ class Bundle
// generate api bundle // generate api bundle
$inc_mgr->include_js_file('/vendor/bower-asset/jquery/dist/jquery.js'); $inc_mgr->include_js_file('/vendor/bower-asset/jquery/dist/jquery.js');
$inc_mgr->include_js_file('/api/js/jquery/jquery.noconflict.js'); $inc_mgr->include_js_file('/api/js/jquery/jquery.noconflict.js');
$inc_mgr->include_js_file('/vendor/bower-asset/jquery-ui/jquery-ui.js'); $inc_mgr->include_js_file('/node_modules/jquery-ui-dist/jquery-ui.min.js');
$inc_mgr->include_js_file('/api/js/jsapi/jsapi.js'); $inc_mgr->include_js_file('/api/js/jsapi/jsapi.js');
$inc_mgr->include_js_file('/api/js/egw_json.js'); $inc_mgr->include_js_file('/api/js/egw_json.js');
$inc_mgr->include_js_file('/api/js/jsapi/egw.js'); $inc_mgr->include_js_file('/api/js/jsapi/egw.js');

View File

@ -2333,14 +2333,19 @@ abstract class Merge
/** /**
* Merge the selected IDs into the given document, save it to the VFS, then * Merge the selected IDs into the given document, save it to the VFS, then
* either open it in the editor or have the browser download the file. * either open it in the editor or have the browser download the file.
*
* @param String[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request.
* @param Merge|null $document_merge Already instantiated Merge object to do the merge.
* @throws Api\Exception
* @throws Api\Exception\AssertionFailed
*/ */
public static function merge_entries() public static function merge_entries(array $ids = null, Merge &$document_merge = null)
{ {
if (class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge')) if (is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
{ {
$document_merge = new $_REQUEST['merge'](); $document_merge = new $_REQUEST['merge']();
} }
else elseif (is_null($document_merge))
{ {
$document_merge = new Api\Contacts\Merge(); $document_merge = new Api\Contacts\Merge();
} }
@ -2351,13 +2356,16 @@ abstract class Merge
return; return;
} }
if(is_null(($ids)))
{
$ids = is_string($_REQUEST['id']) && strpos($_REQUEST['id'], '[') === FALSE ? explode(',', $_REQUEST['id']) : json_decode($_REQUEST['id'], true); $ids = is_string($_REQUEST['id']) && strpos($_REQUEST['id'], '[') === FALSE ? explode(',', $_REQUEST['id']) : json_decode($_REQUEST['id'], true);
}
if($_REQUEST['select_all'] === 'true') if($_REQUEST['select_all'] === 'true')
{ {
$ids = self::get_all_ids($document_merge); $ids = self::get_all_ids($document_merge);
} }
$filename = ''; $filename = $document_merge->get_filename($_REQUEST['document']);
$result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header); $result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header);
if(!is_file($result) || !is_readable($result)) if(!is_file($result) || !is_readable($result))
@ -2412,6 +2420,17 @@ abstract class Merge
} }
} }
/**
* Generate a filename for the merged file
*
* Default is just the name of the template
* @return string
*/
protected function get_filename($document) : string
{
return '';
}
/** /**
* Get all ids for when they try to do 'Select All', then merge into document * Get all ids for when they try to do 'Select All', then merge into document
* *

View File

@ -82,7 +82,10 @@ class Vfs extends Vfs\Base
*/ */
static $is_root = false; static $is_root = false;
/** /**
* Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id'] * Current Vfs user id, set from $GLOBALS['egw_info']['user']['account_id'] by self::init_static()
*
* Should be protected and moved to Vfs\Base plus a getter and setter method added for public access,
* as after setting it in 21.1+, Api\Vfs\StreamWrapper::init_static() need to be called to set the default user context!
* *
* @var int * @var int
*/ */
@ -749,22 +752,22 @@ class Vfs extends Vfs\Base
* @return boolean * @return boolean
* @todo deprecated or even remove $user parameter and code * @todo deprecated or even remove $user parameter and code
*/ */
static function check_access($path, $check, $stat=null, $user=null) static function check_access($path, $check, $stat=null, int $user=null)
{ {
static $vfs = null; static $vfs = null;
if (is_null($stat) && $user && $user != self::$user) if (is_null($stat) && $user && $user !== self::$user)
{ {
static $path_user_stat = array(); static $path_user_stat = array();
$backup_user = self::$user; $backup_user = self::$user;
self::$user = $user; self::$user = $user;
Vfs\StreamWrapper::init_static();
self::clearstatcache($path);
if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user])) if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user]))
{ {
self::clearstatcache($path); $vfs = new Vfs\StreamWrapper();
if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
$path_user_stat[$path][$user] = $vfs->url_stat($path, 0); $path_user_stat[$path][$user] = $vfs->url_stat($path, 0);
self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again! self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again!
@ -786,6 +789,8 @@ class Vfs extends Vfs\Base
$ret = false; // no access, if we can not stat the file $ret = false; // no access, if we can not stat the file
} }
self::$user = $backup_user; self::$user = $backup_user;
Vfs\StreamWrapper::init_static();
$vfs = null;
// we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session // we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session
self::clearstatcache($path); self::clearstatcache($path);

View File

@ -254,10 +254,11 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
{ {
$umaskbefore = umask(); $umaskbefore = umask();
if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace());
self::mkdir_recursive($fs_dir,0700,true); // if running as root eg. via (docker exec) filemanager/cli.php do NOT create dirs not readable by webserver
self::mkdir_recursive($fs_dir,function_exists('posix_getuid') && !posix_getuid() ? 0777 : 0700,true);
} }
} }
// check if opend file is a directory // check if opened file is a directory
elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR)
{ {
if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!");
@ -308,6 +309,11 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
if ($this->operation == self::STORE2FS) if ($this->operation == self::STORE2FS)
{ {
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 ($new_file && function_exists('posix_getuid') && !posix_getuid())
{
umask(0666);
}
if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file)
{ {
// delete db entry again, if we are not able to open a new(!) file // delete db entry again, if we are not able to open a new(!) file

View File

@ -972,6 +972,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
/** /**
* Init our static properties and register this wrapper * Init our static properties and register this wrapper
* *
* Must be called when Vfs::$user is changed!
*/ */
static function init_static() static function init_static()
{ {
@ -984,16 +985,31 @@ class StreamWrapper extends Base implements StreamWrapperIface
{ {
self::$fstab = $fstab; self::$fstab = $fstab;
} }
if (!empty($GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab']) &&
is_array($GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'])) // get the user Vfs is currently using, might be different from $GLOBALS['egw_info']['user']['account_id']
if (!isset(Vfs::$user))
{ {
self::$fstab += $GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab']; Vfs::init_static();
}
if (Vfs::$user != $GLOBALS['egw_info']['user']['account_id'])
{
$prefs = new Api\Preferences(Vfs::$user);
$vfs_fstab = $prefs->data['common']['vfs_fstab'];
}
else
{
$vfs_fstab = $GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'];
}
if (!empty($vfs_fstab) && is_array($vfs_fstab))
{
self::$fstab += $vfs_fstab;
} }
// set default context for our schema ('vfs') with current user // set default context for our schema ('vfs') with current user
if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user'])) if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user']) ||
$context[self::SCHEME]['user'] !== (int)Vfs::$user)
{ {
$context[self::SCHEME]['user'] = (int)$GLOBALS['egw_info']['user']['account_id']; $context[self::SCHEME]['user'] = (int)Vfs::$user;
stream_context_set_default($context); stream_context_set_default($context);
} }
} }

View File

@ -20,7 +20,7 @@ import {et2_action_object_impl, et2_DOMWidget} from "../../api/js/etemplate/et2_
import {et2_calendar_daycol} from "./et2_widget_daycol"; import {et2_calendar_daycol} from "./et2_widget_daycol";
import {et2_calendar_planner_row} from "./et2_widget_planner_row"; import {et2_calendar_planner_row} from "./et2_widget_planner_row";
import {et2_IDetachedDOM} from "../../api/js/etemplate/et2_core_interfaces"; import {et2_IDetachedDOM} from "../../api/js/etemplate/et2_core_interfaces";
import {et2_no_init} from "../../api/js/etemplate/et2_core_common"; import {et2_activateLinks, et2_insertLinkText, et2_no_init} from "../../api/js/etemplate/et2_core_common";
import {egw_getAppObjectManager, egwActionObject} from '../../api/js/egw_action/egw_action.js'; import {egw_getAppObjectManager, egwActionObject} from '../../api/js/egw_action/egw_action.js';
import {egw} from "../../api/js/jsapi/egw_global"; import {egw} from "../../api/js/jsapi/egw_global";
import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox"; import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox";
@ -88,7 +88,8 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
.addClass("calendar_calEvent") .addClass("calendar_calEvent")
.addClass(this.options.class) .addClass(this.options.class)
.css('width', this.options.width) .css('width', this.options.width)
.on('mouseenter', function() { .on('mouseenter', function ()
{
// Bind actions on first mouseover for faster creation // Bind actions on first mouseover for faster creation
if (event._need_actions_linked) if (event._need_actions_linked)
{ {
@ -105,16 +106,20 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
} }
} }
// Hacky to remove egw's tooltip border and let the mouse in // Hacky to remove egw's tooltip border and let the mouse in
window.setTimeout(function() { window.setTimeout(function ()
{
jQuery('body .egw_tooltip') jQuery('body .egw_tooltip')
.css('border', 'none') .css('border', 'none')
.on('mouseenter', function() { .on('mouseenter', function ()
{
event.div.off('mouseleave.tooltip'); event.div.off('mouseleave.tooltip');
jQuery('body.egw_tooltip').remove(); jQuery('body.egw_tooltip').remove();
jQuery('body').append(this); jQuery('body').append(this);
jQuery(this).stop(true).fadeTo(400, 1) jQuery(this).stop(true).fadeTo(400, 1)
.on('mouseleave', function() { .on('mouseleave', function ()
jQuery(this).fadeOut('400', function() { {
jQuery(this).fadeOut('400', function ()
{
jQuery(this).remove(); jQuery(this).remove();
// Set up to work again // Set up to work again
event.set_statustext(event._tooltip()); event.set_statustext(event._tooltip());
@ -289,7 +294,10 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
const im = this.getInstanceManager(); const im = this.getInstanceManager();
et2_selectbox.cat_options({ et2_selectbox.cat_options({
_type: 'select-cat', _type: 'select-cat',
getInstanceManager: function() {return im;} getInstanceManager: function ()
{
return im;
}
}, },
{application: event.app || 'calendar'} {application: event.app || 'calendar'}
); );
@ -328,11 +336,13 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
.attr("tabindex", 0) .attr("tabindex", 0)
.attr("aria-label", tooltip) .attr("aria-label", tooltip)
// Remove any category classes // Remove any category classes
.removeClass(function(index, css) { .removeClass(function (index, css)
{
return (css.match(/(^|\s)cat_\S+/g) || []).join(' '); return (css.match(/(^|\s)cat_\S+/g) || []).join(' ');
}) })
// Remove any status classes // Remove any status classes
.removeClass(function(index, css) { .removeClass(function (index, css)
{
return (css.match(/calendar_calEvent\S+/g) || []).join(' '); return (css.match(/calendar_calEvent\S+/g) || []).join(' ');
}) })
.removeClass('calendar_calEventSmall') .removeClass('calendar_calEventSmall')
@ -530,7 +540,8 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : []; cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : [];
if (typeof cat_label != 'string') if (typeof cat_label != 'string')
{ {
cat.span.children().each(function() { cat.span.children().each(function ()
{
(<string[]>cat_label).push(jQuery(this).text()); (<string[]>cat_label).push(jQuery(this).text());
}); });
cat_label = cat_label.join(', '); cat_label = cat_label.join(', ');
@ -538,12 +549,25 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
cat.destroy(); cat.destroy();
} }
// Activate links in description
let description_node = document.createElement("p");
description_node.className = "calendar_calEvent_description";
et2_insertLinkText(
et2_activateLinks(egw.htmlspecialchars(this.options.value.description)), description_node, '_blank'
);
// Location + Videoconference // Location + Videoconference
let location = ''; let location = '';
if (this.options.value.location || this.options.value['##videoconference']) if (this.options.value.location || this.options.value['##videoconference'])
{ {
location += '<p><span class="calendar_calEventLabel">' + this.egw().lang('Location') + '</span>:' + location = '<p>';
egw.htmlspecialchars(this.options.value.location); let location_node = document.createElement("span");
location_node.className = "calendar_calEventLabel";
et2_insertLinkText(et2_activateLinks(
this.egw().lang('Location') + ':' +
egw.htmlspecialchars(this.options.value.location)), location_node, '_blank');
location += location_node.outerHTML;
if (this.options.value['##videoconference']) if (this.options.value['##videoconference'])
{ {
// Click handler is set in _bind_videoconference() // Click handler is set in _bind_videoconference()
@ -580,8 +604,8 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
this.icons[0].outerHTML + this.icons[0].outerHTML +
'</div>' + '</div>' +
'<div class="calendar_calEventBody">' + '<div class="calendar_calEventBody">' +
'<h1 class="calendar_calEventTitle">'+egw.htmlspecialchars(this.options.value.title)+'</h1><br><p>'+ '<h1 class="calendar_calEventTitle">' + egw.htmlspecialchars(this.options.value.title) + '</h1><br>' +
egw.htmlspecialchars(this.options.value.description)+'</p>'+ description_node.outerHTML +
'<p style="margin: 2px 0px;">' + times + '</p>' + '<p style="margin: 2px 0px;">' + times + '</p>' +
location + location +
(cat_label ? '<p><h2 class="calendar_calEventLabel">' + this.egw().lang('Category') + ':</h2>' + cat_label + '</p>' : '') + (cat_label ? '<p><h2 class="calendar_calEventLabel">' + this.egw().lang('Category') + ':</h2>' + cat_label + '</p>' : '') +
@ -718,11 +742,13 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
{ {
let vc_event = 'click.calendar_videoconference'; let vc_event = 'click.calendar_videoconference';
jQuery('body').off(vc_event) jQuery('body').off(vc_event)
.on(vc_event, '[data-videoconference]',function(event) { .on(vc_event, '[data-videoconference]', function (event)
{
let data = egw.dataGetUIDdata("calendar::" + this.dataset.id); let data = egw.dataGetUIDdata("calendar::" + this.dataset.id);
app.calendar.joinVideoConference(this.dataset.videoconference, data.data || this.dataset); app.calendar.joinVideoConference(this.dataset.videoconference, data.data || this.dataset);
}); });
} }
/** /**
* Get a text representation of the timespan of the event. Either start * Get a text representation of the timespan of the event. Either start
* - end, or 'all day' * - end, or 'all day'
@ -1113,10 +1139,13 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
{ {
action_parent = action_parent.getParent(); action_parent = action_parent.getParent();
} }
try { try
{
this._link_actions(action_parent.options.actions || {}); this._link_actions(action_parent.options.actions || {});
this._need_actions_linked = false; this._need_actions_linked = false;
} catch (e) { }
catch (e)
{
// something went wrong, but keep quiet about it // something went wrong, but keep quiet about it
} }
} }
@ -1137,7 +1166,8 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
this._actionObject = objectManager.getObjectById('calendar::' + this.options.value.row_id); this._actionObject = objectManager.getObjectById('calendar::' + this.options.value.row_id);
} }
if (this._actionObject == null) { if (this._actionObject == null)
{
// Add a new container to the object manager which will hold the widget // Add a new container to the object manager which will hold the widget
// objects // objects
this._actionObject = objectManager.insertObject(false, new egwActionObject( this._actionObject = objectManager.insertObject(false, new egwActionObject(
@ -1234,7 +1264,10 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
// we need the list - pull it from sidebox owner // we need the list - pull it from sidebox owner
if ((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && typeof options.find == "function") if ((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && typeof options.find == "function")
{ {
var resource = options.find(function(element) {return element.id == parent_owner[i];}) || {}; var resource = options.find(function (element)
{
return element.id == parent_owner[i];
}) || {};
if (resource && resource.resources) if (resource && resource.resources)
{ {
parent_owner.splice(i, 1); parent_owner.splice(i, 1);
@ -1253,14 +1286,18 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
{ {
// Add in groups, if we can get them from options, great // Add in groups, if we can get them from options, great
var resource; var resource;
if(options && options.find && (resource = options.find(function(element) {return element.id === id;})) && resource.resources) if (options && options.find && (resource = options.find(function (element)
{
return element.id === id;
})) && resource.resources)
{ {
participants = participants.concat(resource.resources); participants = participants.concat(resource.resources);
} }
else else
{ {
// Add in groups, if we can get them (this is asynchronous) // Add in groups, if we can get them (this is asynchronous)
egw.accountData(id,'account_id',true,function(members) { egw.accountData(id, 'account_id', true, function (members)
{
participants = participants.concat(Object.keys(members)); participants = participants.concat(Object.keys(members));
}, this); }, this);
} }
@ -1325,7 +1362,8 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
{ {
egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar');
} }
catch(e){ catch (e)
{
egw = window.egw('calendar'); egw = window.egw('calendar');
} }
@ -1361,7 +1399,10 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
{text: egw.lang("Cancel"), id: "cancel"} {text: egw.lang("Cancel"), id: "cancel"}
]; ];
et2_dialog.show_dialog( et2_dialog.show_dialog(
function(button_id) {callback.call(that, button_id, event_data);}, function (button_id)
{
callback.call(that, button_id, event_data);
},
(!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" +
egw.lang("Do you want to edit this event as an exception or the whole series?"), egw.lang("Do you want to edit this event as an exception or the whole series?"),
egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE
@ -1395,10 +1436,12 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
let egw; let egw;
// seems window.opener somehow in certian conditions could be from different origin // seems window.opener somehow in certian conditions could be from different origin
// we try to catch the exception and in this case retrieve the egw object from current window. // we try to catch the exception and in this case retrieve the egw object from current window.
try { try
{
egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar');
} }
catch(e){ catch (e)
{
egw = window.egw('calendar'); egw = window.egw('calendar');
} }
@ -1417,7 +1460,10 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
if (parseInt(event_data.recur_type)) if (parseInt(event_data.recur_type))
{ {
et2_dialog.show_dialog( et2_dialog.show_dialog(
function(button_id) {callback.call(that, button_id, event_data);}, function (button_id)
{
callback.call(that, button_id, event_data);
},
(!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" +
egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date), egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date),
egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.WARNING_MESSAGE egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.WARNING_MESSAGE
@ -1478,10 +1524,12 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached
// _outerCall may be used to determine, whether the state change has been // _outerCall may be used to determine, whether the state change has been
// evoked from the outside and the stateChangeCallback has to be called // evoked from the outside and the stateChangeCallback has to be called
// or not. // or not.
aoi.doSetState = function(_state, _outerCall) { aoi.doSetState = function (_state, _outerCall)
{
}; };
return aoi; return aoi;
} }
} }
et2_register_widget(et2_calendar_event, ["calendar-event"]); et2_register_widget(et2_calendar_event, ["calendar-event"]);

View File

@ -85,7 +85,7 @@
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png); background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none; border: none;
box-shadow: none; box-shadow: none;
} }

View File

@ -107,7 +107,7 @@
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png); background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none; border: none;
box-shadow: none; box-shadow: none;
} }

View File

@ -95,7 +95,7 @@
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png); background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none; border: none;
box-shadow: none; box-shadow: none;
} }

View File

@ -38,6 +38,10 @@
"issuses": "https://my.egroupware.org" "issuses": "https://my.egroupware.org"
}, },
"repositories": [ "repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{ {
"type": "pear", "type": "pear",
"url": "https://pear.horde.org" "url": "https://pear.horde.org"
@ -51,6 +55,9 @@
"platform": { "platform": {
"php": "7.3" "php": "7.3"
}, },
"fxp-asset": {
"enabled": false
},
"sort-packages": true "sort-packages": true
}, },
"require": { "require": {
@ -70,14 +77,12 @@
"bower-asset/fastclick": "1.0.*", "bower-asset/fastclick": "1.0.*",
"bower-asset/jquery": "^1.12.4", "bower-asset/jquery": "^1.12.4",
"bower-asset/jquery-touchswipe": "1.6.*", "bower-asset/jquery-touchswipe": "1.6.*",
"bower-asset/jquery-ui": "=1.11.2",
"egroupware/activesync": "self.version", "egroupware/activesync": "self.version",
"egroupware/adodb-php": "self.version", "egroupware/adodb-php": "self.version",
"egroupware/bookmarks": "self.version", "egroupware/bookmarks": "self.version",
"egroupware/collabora": "self.version", "egroupware/collabora": "self.version",
"egroupware/guzzlestream": "dev-master", "egroupware/guzzlestream": "dev-master",
"egroupware/icalendar": "^2.1.9", "egroupware/icalendar": "^2.1.9",
"egroupware/imap-client": "^2.30.2",
"egroupware/magicsuggest": "^2.1", "egroupware/magicsuggest": "^2.1",
"egroupware/news_admin": "self.version", "egroupware/news_admin": "self.version",
"egroupware/openid": "self.version", "egroupware/openid": "self.version",
@ -90,13 +95,13 @@
"egroupware/tracker": "self.version", "egroupware/tracker": "self.version",
"egroupware/webdav": "dev-master", "egroupware/webdav": "dev-master",
"egroupware/z-push-dev": "^2.5", "egroupware/z-push-dev": "^2.5",
"fxp/composer-asset-plugin": "^1.2.2",
"giggsey/libphonenumber-for-php": "^8.12", "giggsey/libphonenumber-for-php": "^8.12",
"npm-asset/as-jqplot": "1.0.*", "npm-asset/as-jqplot": "1.0.*",
"npm-asset/gridster": "0.5.*", "npm-asset/gridster": "0.5.*",
"oomphinc/composer-installers-extender": "^1.1", "oomphinc/composer-installers-extender": "^1.1",
"pear-pear.horde.org/horde_compress": "^2.0.8", "pear-pear.horde.org/horde_compress": "^2.0.8",
"pear-pear.horde.org/horde_crypt": "^2.7.9", "pear-pear.horde.org/horde_crypt": "^2.7.9",
"pear-pear.horde.org/horde_imap_client": "^2.30.3",
"pear-pear.horde.org/horde_mail": "^2.1.2", "pear-pear.horde.org/horde_mail": "^2.1.2",
"pear-pear.horde.org/horde_managesieve": "^1.0.2", "pear-pear.horde.org/horde_managesieve": "^1.0.2",
"pear-pear.horde.org/horde_mapi": "^1.0.9", "pear-pear.horde.org/horde_mapi": "^1.0.9",

910
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -64,7 +64,7 @@ else \
RESULT=$?; \ RESULT=$?; \
fi; \ fi; \
rm composer-setup.php; \ rm composer-setup.php; \
composer.phar self-update 1.8.6; \ composer.phar self-update 1.10.22; \
exit $RESULT' \ exit $RESULT' \
# disable certificate checks for LDAP as most LDAP and AD servers have no "valid" cert # disable certificate checks for LDAP as most LDAP and AD servers have no "valid" cert
&& echo "TLS_REQCERT never" >> /etc/ldap/ldap.conf && echo "TLS_REQCERT never" >> /etc/ldap/ldap.conf

View File

@ -69,7 +69,7 @@ fi; \
rm composer-setup.php; \ rm composer-setup.php; \
exit $RESULT' \ exit $RESULT' \
# build EGroupware # build EGroupware
&& composer.phar self-update 1.8.6 \ && composer.phar self-update 1.10.22 \
&& cd /usr/share \ && cd /usr/share \
&& [ $PHP_VERSION = "8.0" ] && COMPOSER_EXTRA=--ignore-platform-reqs || true \ && [ $PHP_VERSION = "8.0" ] && COMPOSER_EXTRA=--ignore-platform-reqs || true \
&& composer.phar create-project $COMPOSER_EXTRA --prefer-dist --no-scripts --no-dev egroupware/egroupware:$VERSION \ && composer.phar create-project $COMPOSER_EXTRA --prefer-dist --no-scripts --no-dev egroupware/egroupware:$VERSION \

36
doc/ldif2sql.php Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env php
<?php
if (!$_SERVER['argc'])
{
echo "cat test.ldif | ".basename($_SERVER['argv'][0])."attr1[, attr2[, ...]]\n";
exit(1);
}
$attrs = array_slice($_SERVER['argv'], 1);
$values = $rows = [];
while(!feof(STDIN))
{
$line = trim(fgets(STDIN));
if (empty($line) || $line[0] === '#' ||
!preg_match('/^([^:]+): (.*)$/', $line, $matches))
{
$values = [];
continue;
}
if ($matches[1] === 'dn') $values = [];
$values[$matches[1]] = $matches[2];
if (count(array_intersect(array_keys($values), $attrs)) === count($attrs))
{
$cols = [];
foreach($attrs as $attr)
{
$cols[$attr] = "'".addslashes($values[$attr])."'";
}
$cols = '('.implode(', ', $cols).')';
if (!in_array($cols, $rows)) $rows[] = $cols;
}
}
echo implode(",\n", $rows)."\n";

View File

@ -1,3 +1,14 @@
egroupware-docker (21.1.20210723) hardy; urgency=low
* Security Update: all 21.1 users should upgrade ASAP, 20.1 and below is not affected
* Filemanager/VFS: when creating a new file as root eg. via (docker exec) filemanager/cli.php do NOT create files unreadable by webserver
* Collabora: Fix editing files in mounted share
* Kanban/PostgreSQL: fix installation of example board under PostgreSQL
* smallPART/PostgreSQL: fix multiple SQL errors
* smallPART/PostgreSQL: fix installation under PostgreSQL
-- Ralf Becker <rb@egroupware.org> Fri, 23 Jul 2021 08:09:49 +0200
egroupware-docker (21.1.20210629) hardy; urgency=low egroupware-docker (21.1.20210629) hardy; urgency=low
* Mail/Admin: fix not working mail wizard * Mail/Admin: fix not working mail wizard

View File

@ -75,8 +75,8 @@
</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="${row}[info_type]" default_src="infolog/navbar"/> <image label="$row_cont[info_type]" src="infolog/${row}[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="$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');"/>
</hbox> </hbox>

View File

@ -176,6 +176,24 @@ class mail_hooks
'always_display' => lang('always show html emails'), 'always_display' => lang('always show html emails'),
); );
$contactLabelOptions = array (
'n_prefix' => array (
'id' => 'n_prefix',
'label' => lang('Prefix'),
),
'n_given' => array (
'id' => 'n_given',
'label' => lang('First name')
),
'n_family' => array(
'id' => 'n_family',
'label' => lang('Last name')
),
'org_name' => array(
'id' => 'org_name',
'label' => lang('Organisation')
)
);
// otherwise we get warnings during setup // otherwise we get warnings during setup
if (!is_array($folderList)) $folderList = array(); if (!is_array($folderList)) $folderList = array();
@ -457,29 +475,12 @@ class mail_hooks
'label' => 'Contact label', 'label' => 'Contact label',
'help' => 'Defines what to show as contact label for added contact into To/Cc/Bcc when composing an email. Default is firstname lastname and empty means include eveything available.', 'help' => 'Defines what to show as contact label for added contact into To/Cc/Bcc when composing an email. Default is firstname lastname and empty means include eveything available.',
'name' => 'contactLabel', 'name' => 'contactLabel',
'values' => '', 'values' => $contactLabelOptions,
'attributes' => array( 'attributes' => array(
'allowFreeEntries' => false, 'allowFreeEntries' => false,
'editModeEnabled' => false, 'editModeEnabled' => false,
'autocomplete_url' => ' ', 'autocomplete_url' => ' ',
'select_options' => array ( 'select_options' => $contactLabelOptions
'n_prefix' => array (
'id' => 'n_prefix',
'label' => lang('Prefix'),
),
'n_given' => array (
'id' => 'n_given',
'label' => lang('First name')
),
'n_family' => array(
'id' => 'n_family',
'label' => lang('Last name')
),
'org_name' => array(
'id' => 'org_name',
'label' => lang('Organisation')
)
)
), ),
'default' => ['n_given','n_family'] 'default' => ['n_given','n_family']
) )

2335
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,13 +45,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@lion/button": "^0.14.1", "@andxor/jquery-ui-touch-punch-fix": "^1.0.2",
"@lion/core": "^0.18.1", "jquery-ui-dist": "^1.12.1",
"@lion/input": "^0.15.3", "jquery-ui-themes": "^1.12.0",
"carbon-components": "^10.37.0", "jquery-ui-timepicker-addon": "^1.6.3",
"carbon-web-components": "^1.14.1",
"lit-element": "^2.5.1", "lit-element": "^2.5.1",
"lit-html": "^1.4.1" "lit-html": "^1.4.1",
"sortablejs": "^1.14.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"

View File

@ -23,7 +23,7 @@ import '../../api/js/framework/fw_browser.js';
import '../../api/js/framework/fw_ui.js'; import '../../api/js/framework/fw_ui.js';
import '../../api/js/framework/fw_classes.js'; import '../../api/js/framework/fw_classes.js';
import '../../api/js/jsapi/egw_inheritance.js'; import '../../api/js/jsapi/egw_inheritance.js';
import '@andxor/jquery-ui-touch-punch-fix/jquery.ui.touch-punch.js';
/** /**
* *
* @param {DOMWindow} window * @param {DOMWindow} window

View File

@ -226,6 +226,9 @@ if (!empty($detail = $_GET['detail']))
{ {
switch($key) switch($key)
{ {
case 'autoinstall':
$val = json_encode($val);
break;
case 'title': case 'title':
continue 2; continue 2;
case 'tables': case 'tables':

View File

@ -516,8 +516,7 @@ class setup
),False,__LINE__,__FILE__); ),False,__LINE__,__FILE__);
} }
try { try {
$this->db->insert($this->applications_table,array( $this->db->insert($this->applications_table, [
'app_name' => $appname,
'app_enabled' => $enable, 'app_enabled' => $enable,
'app_order' => $setup_info[$appname]['app_order'], 'app_order' => $setup_info[$appname]['app_order'],
'app_tables' => (string)$tables, // app_tables is NOT NULL 'app_tables' => (string)$tables, // app_tables is NOT NULL
@ -525,7 +524,9 @@ class setup
'app_index' => $setup_info[$appname]['index'], 'app_index' => $setup_info[$appname]['index'],
'app_icon' => $setup_info[$appname]['icon'], 'app_icon' => $setup_info[$appname]['icon'],
'app_icon_app' => $setup_info[$appname]['icon_app'], 'app_icon_app' => $setup_info[$appname]['icon_app'],
),False,__LINE__,__FILE__); ], [
'app_name' => $appname,
], __LINE__, __FILE__);
} }
catch (Api\Db\Exception\InvalidSql $e) catch (Api\Db\Exception\InvalidSql $e)
{ {
@ -548,7 +549,7 @@ class setup
* Check if an application has info in the db * Check if an application has info in the db
* *
* @param $appname Application 'name' with a matching $setup_info[$appname] array slice * @param $appname Application 'name' with a matching $setup_info[$appname] array slice
* @param $enabled optional, set to False to not enable this app * @return boolean|null null: autoinstalled app which got uninstalled
*/ */
function app_registered($appname) function app_registered($appname)
{ {
@ -563,13 +564,13 @@ class setup
// _debug_array($setup_info[$appname]); // _debug_array($setup_info[$appname]);
} }
if ($this->db->select($this->applications_table,'COUNT(*)',array('app_name' => $appname),__LINE__,__FILE__)->fetchColumn()) if (($enabled = $this->db->select($this->applications_table, 'app_enabled', ['app_name' => $appname], __LINE__,__FILE__)->fetchColumn()) !== false)
{ {
if(@$GLOBALS['DEBUG']) if(@$GLOBALS['DEBUG'])
{ {
echo '... app previously registered.'; echo '... app previously registered.';
} }
return True; return $enabled <= -1 ? null : true;
} }
if(@$GLOBALS['DEBUG']) if(@$GLOBALS['DEBUG'])
{ {
@ -676,10 +677,35 @@ class setup
$this->db->delete(Api\Config::TABLE, array('config_app'=>$appname),__LINE__,__FILE__); $this->db->delete(Api\Config::TABLE, array('config_app'=>$appname),__LINE__,__FILE__);
} }
//echo 'DELETING application: ' . $appname; //echo 'DELETING application: ' . $appname;
$this->db->delete($this->applications_table,array('app_name'=>$appname),__LINE__,__FILE__);
// when uninstalling an autoinstall app, we must mark it deleted in the DB, otherwise it will install again the next update
if (file_exists($file = EGW_SERVER_ROOT.'/'.$appname.'/setup/setup.inc.php'))
{
$setup_info = [];
include($file);
}
if (!empty($setup_info[$appname]['autoinstall']) && $setup_info[$appname]['autoinstall'] === true)
{
$this->db->update($this->applications_table, [
'app_enabled' => -1,
'app_tables' => '',
'app_version' => 'uninstalled',
'app_index' => null,
], [
'app_name' => $appname,
], __LINE__, __FILE__);
}
else
{
$this->db->delete($this->applications_table, ['app_name' => $appname], __LINE__, __FILE__);
}
Api\Egw\Applications::invalidate(); Api\Egw\Applications::invalidate();
// unregister hooks, before removing links
unset($GLOBALS['egw_info']['apps'][$appname]);
Api\Hooks::read(true);
// Remove links to the app // Remove links to the app
Link::unlink(0, $appname); Link::unlink(0, $appname);
} }
@ -1219,6 +1245,8 @@ class setup
{ {
static $table_names = False; static $table_names = False;
if(!is_object($this->db)) $this->loaddb();
if (!$table_names || $force_refresh) $table_names = $this->db->table_names(); if (!$table_names || $force_refresh) $table_names = $this->db->table_names();
if (!$table_names) return false; if (!$table_names) return false;

View File

@ -62,7 +62,7 @@ class setup_detection
/* one of these tables exists. checking for post/pre beta version */ /* one of these tables exists. checking for post/pre beta version */
if($GLOBALS['egw_setup']->applications_table != 'applications') if($GLOBALS['egw_setup']->applications_table != 'applications')
{ {
foreach($GLOBALS['egw_setup']->db->select($GLOBALS['egw_setup']->applications_table,'*',false,__LINE__,__FILE__) as $row) foreach($GLOBALS['egw_setup']->db->select($GLOBALS['egw_setup']->applications_table, '*', 'app_enabled != -1', __LINE__, __FILE__) as $row)
{ {
$app = $row['app_name']; $app = $row['app_name'];
if (!isset($setup_info[$app])) // app source no longer there if (!isset($setup_info[$app])) // app source no longer there

View File

@ -618,10 +618,10 @@ class setup_process
foreach($setup_info as $appname => &$appdata) foreach($setup_info as $appname => &$appdata)
{ {
// check if app is NOT installed // check if app is NOT installed
if(!$GLOBALS['egw_setup']->app_registered($appname)) if (!($registered = $GLOBALS['egw_setup']->app_registered($appname)))
{ {
// check if app wants to be automatically installed on update to version x or allways // check if app wants to be automatically installed on update to version x or always (unless uninstalled prior)
if (isset($appdata['autoinstall']) && ($appdata['autoinstall'] === true || if (isset($appdata['autoinstall']) && ($appdata['autoinstall'] === true && $registered !== null ||
$appdata['autoinstall'] === $this->api_version_target)) $appdata['autoinstall'] === $this->api_version_target))
{ {
$info_c = $this->current(array($appname => $appdata), $DEBUG); $info_c = $this->current(array($appname => $appdata), $DEBUG);