Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-08-12 10:35:46 -06:00
commit fac1dfb8d4
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,32 +782,18 @@ 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 self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList);
var h_parent = item.parent().parent().clone(); self._refresh_fav_nm();
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._refresh_fav_nm();
}
});
// Bind favorite de-select // Bind favorite de-select
var egw_fw = egw_getFramework(); var egw_fw = egw_getFramework();

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;
} }
$ids = is_string($_REQUEST['id']) && strpos($_REQUEST['id'],'[') === FALSE ? explode(',',$_REQUEST['id']) : json_decode($_REQUEST['id'],true); if(is_null(($ids)))
{
$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);
} }
} }

File diff suppressed because it is too large Load Diff

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,9 +38,13 @@
"issuses": "https://my.egroupware.org" "issuses": "https://my.egroupware.org"
}, },
"repositories": [ "repositories": [
{ {
"type": "pear", "type": "composer",
"url": "https://pear.horde.org" "url": "https://asset-packagist.org"
},
{
"type": "pear",
"url": "https://pear.horde.org"
}, },
{ {
"type": "vcs", "type": "vcs",
@ -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,16 +516,17 @@ 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 'app_version' => $setup_info[$appname]['version'],
'app_version' => $setup_info[$appname]['version'], '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);