/** * EGroupware - Admin - Javascript UI * * @link: https://www.egroupware.org * @package filemanager * @author Ralf Becker * @copyright (c) 2013-20 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License */ /*egw:uses /api/js/jsapi/egw_app.js */ import {EgwApp, PushData} from '../../api/js/jsapi/egw_app'; import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {Et2Dialog} from "../../api/js/etemplate/Et2Dialog/Et2Dialog"; import {egw} from "../../api/js/jsapi/egw_global.js"; import {egwAction, egwActionObject} from '../../api/js/egw_action/egw_action'; import {et2_nextmatch} from "../../api/js/etemplate/et2_extension_nextmatch"; import {et2_DOMWidget} from "../../api/js/etemplate/et2_core_DOMWidget"; import {Et2SelectAccount} from "../../api/js/etemplate/Et2Select/Select/Et2SelectAccount"; import {EgwAction} from "../../api/js/egw_action/EgwAction"; import {EgwActionObject} from "../../api/js/egw_action/EgwActionObject"; import type {Et2Button} from "../../api/js/etemplate/Et2Button/Et2Button"; import {LitElement} from "lit"; /** * UI for Admin * * @augments AppJS */ class AdminApp extends EgwApp /** * @lends app.classes.admin */ { /** * reference to iframe * * {et2_iframe} */ iframe : any = null; /** * reference to nextmatch * * {et2_extension_nextmatch} */ nm : any = null; /** * Reference to div to hold AJAX loadable pages * * {et2_box} */ ajax_target : any = null; /** * Reference to ACL edit dialog (not the list) */ acl_dialog : any = null; tree : any = null; groups : any; /** * Constructor * * @memberOf app.classes.admin */ constructor() { // call parent super('admin'); } /** * Destructor */ destroy(_app) { this.iframe = null; this.nm = null; this.acl_dialog = null; this.tree = null; // call parent super.destroy(_app); } /** * This function is called when the etemplate2 object is loaded * and ready. If you must store a reference to the et2 object, * make sure to clean it up in destroy(). * * @param {etemplate2} _et2 * @param {string} _name name of template loaded */ et2_ready(_et2, _name) { // call parent super.et2_ready(_et2, _name); switch(_name) { case 'admin.index': var iframe = this.iframe = this.et2.getWidgetById('iframe'); this.nm = this.et2.getWidgetById('nm'); this.groups = this.et2.getWidgetById('groups'); this.groups.set_disabled(true); this.ajax_target = this.et2.getWidgetById('ajax_target'); this.tree = this.et2.getWidgetById('tree'); if (iframe) { var self = this; jQuery(iframe.getDOMNode()).off('load.admin') .bind('load.admin', function(){ if (this.contentDocument?.location.href.match(/(\/admin\/|\/admin\/index.php|menuaction=admin.admin_ui.index)/)) { this.contentDocument.location.href = 'about:blank'; // stops redirect from admin/index.php self.load(); // load own top-level index aka user-list } self._hide_navbar.call(self); } ); } if( this.ajax_target && this.et2.getArrayMgr('content').getEntry('ajax_target')) { this.load(this.et2.getArrayMgr('content').getEntry('ajax_target')); } break; case 'admin.customfield_edit': // Load settings appropriate to currently set type var widget = _et2.widgetContainer.getWidgetById('cf_type'); this.cf_type_change(null,widget); break; case 'admin.cmds': var selected = this.et2.getWidgetById('nm').getSelection(); if (selected && selected.ids.length == 1) { this.cmds_onselect(selected.ids); } else { this.et2.getWidgetById('splitter').dock(); } break; } } /** * Show given url in (visible) iframe or nextmatch with accounts (!_url) * * @param {string} [_url=] url to show in iframe or nothing for showing */ load(_url? : string) { if (this.iframe && this.iframe.getDOMNode().contentDocument?.location.href .match(/menuaction=admin.admin_statistics.submit.+required=true/) && ( !_url || !_url.match(/statistics=(postpone|canceled|submitted)/))) { this.egw.message(this.egw.lang('Please submit (or postpone) statistic first'), 'info'); return; // do not allow to leave statistics submit } // url outside EGroupware eg. eSyncPro linking to wikipedia if (_url && _url.indexOf(this.egw.webserverUrl) == -1) { window.open(_url, '_blank'); return; } var ajax : any = false; if (_url) { // Try to load it without the iframe ajax = _url.match(/ajax=true/) && _url.match(/menuaction=/); if(ajax) { if(this.ajax_target.getDOMNode().children.length) { // Node has children already? Check for loading over an // existing etemplate, and remove it first jQuery(this.ajax_target.getDOMNode().children).each(function() { var old = etemplate2.getById(this.id); if(old) old.clear(); }); jQuery(this.ajax_target.getDOMNode()).empty(); } this.egw.json( framework.activeApp.getMenuaction('ajax_exec', _url), // It's important that the context is null, or etemplate2 // won't load the template properly [_url], this._ajax_load_callback,null, true, this ).sendRequest(); } else { this.iframe.set_src(_url); } var m = _url.match(/menuaction=([^&]+)(?:.*appname=(\w+))?/); if(m && m.length >= 2) { var app = m[2] ? m[2] : m[1].split('.')[0]; this.tree.set_value('/apps/'+app+'/'+m[1]); } } else { this.egw.app_header(''); // blank iframe, to not keep something running there this.iframe.getDOMNode().contentDocument.location.href = 'about:blank'; } this.iframe.set_disabled(!_url || ajax); this.nm.set_disabled(!!_url || ajax); this.groups.set_disabled(true); this.ajax_target.set_disabled(!ajax); if(!this.nm.disabled) { // If nm was just re-enabled, resize it _after_ ajax_target gets hidden this.ajax_target.updateComplete.then(() => this.nm.resize()) } } /** * Observer method receives update notifications from all applications * * App is responsible for only reacting to "messages" it is interested in! * * @param {string} _msg message (already translated) to show, eg. 'Entry deleted' * @param {string} _app application name * @param {(string|number)} _id id of entry to refresh or null * @param {string} _type either 'update', 'edit', 'delete', 'add' or null * - update: request just modified data from given rows. Sorting is not considered, * so if the sort field is changed, the row will not be moved. * - edit: rows changed, but sorting may be affected. Requires full reload. * - delete: just delete the given rows clientside (no server interaction neccessary) * - add: requires full reload for proper sorting * @param {string} _msg_type 'error', 'warning' or 'success' (default) * @param {string} _targetapp which app's window should be refreshed, default current * @return {false|*} false to stop regular refresh, thought all observers are run */ observer(_msg, _app, _id, _type, _msg_type, _targetapp) { switch(_app) { case 'admin': // if iframe is used --> refresh it var iframe_node = this.iframe ? this.iframe.getDOMNode() : undefined; var iframe_url = iframe_node ? iframe_node.contentDocument.location.href : undefined; if (_id && iframe_url != 'about:blank') { var refresh_done = false; // Try for intelligent et2 refresh inside iframe if(iframe_node && iframe_node.contentWindow && iframe_node.contentWindow.etemplate2) { var templates = iframe_node.contentWindow.etemplate2.getByApplication('admin'); for(let i = 0; i < templates.length; i++) { templates[i].refresh(_msg, _app, _id, _type); refresh_done = true; } } if (!refresh_done) // --> reload iframe { this.load(iframe_url); } return false; // --> no regular refresh } else { // No iframe, but if there's a nm in the current view, refresh it let et2s = etemplate2.getByApplication('admin'); for(let i = 0; i < et2s.length; i++) { let nm = et2s[i].widgetContainer.getWidgetById('nm'); if(nm) { nm.refresh(undefined, undefined); } } // Get group list too, if visible, since it wasn't found in the loop above if(!this.groups.disabled) { this.groups.refresh(undefined, undefined); } return false; } // invalidate client-side account-cache this.egw.invalidate_account(_id, _type); // group deleted, added or updated if (_id < 0) { var tree = this.et2.getWidgetById('tree'); var nm = this.et2.getWidgetById('nm'); switch(_type) { case 'delete': tree.deleteItem('/groups/'+_id, false); if (nm) nm.getInstanceManager().submit(); break; default: // add, update, edit, null if (nm) { var activeFilters = nm.activeFilters; nm.getInstanceManager().submit(); var nm = this.et2.getWidgetById('nm'); nm.applyFilters(activeFilters); } } var refreshTree = this.et2.getWidgetById('tree'); if (refreshTree) refreshTree.refreshItem('/groups'); return false; // --> no regular refresh } // not a user or group, eg. categories else if (!_id) { return false; // --> no regular refresh needed } } } /** * Handle a push notification about entry changes from the websocket * * Get's called for data of all apps, but should only handle data of apps it displays, * which is by default only it's own, but can be for multiple apps eg. for calendar. * * @param pushData * @param {string} pushData.app application name * @param {(string|number)} pushData.id id of entry to refresh or null * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null * - update: request just modified data from given rows. Sorting is not considered, * so if the sort field is changed, the row will not be moved. * - edit: rows changed, but sorting may be affected. Requires full reload. * - delete: just delete the given rows clientside (no server interaction neccessary) * - add: requires full reload for proper sorting * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary * @param {number} pushData.account_id User that caused the notification */ push(pushData : PushData) { // Filter out what we're not interested in if([this.appname, "api-cats", "api-cf"].indexOf(pushData.app) == -1) { return; } const cat_template = "admin.categories.index"; const cf_template = "admin.customfields"; if(this.appname.indexOf(pushData.app) != -1 && pushData.id > 0) { this.nm.refresh(pushData.id, pushData.type); } else if(pushData.app == this.appname && pushData.id < 0) { this.groups.refresh(pushData.id, pushData.type); if(this.tree) { this.tree.refreshItem('/groups'); } } else if(pushData.app == "api-cats" && etemplate2.getByTemplate(cat_template).length == 1) { (etemplate2.getByTemplate(cat_template)[0].widgetContainer.getWidgetById("nm")).refresh(pushData.id, pushData.type); } else if(pushData.app == "api-cf" && etemplate2.getByTemplate(cf_template).length == 1) { (etemplate2.getByTemplate(cf_template)[0].widgetContainer.getWidgetById("nm")).refresh(pushData.id, pushData.type); } } /** * Hide navbar for idots template * * Just a hack for old idots, not neccesary for jdots */ _hide_navbar() { var document = this.iframe.getDOMNode().contentDocument; if (!document) return; // nothing we can do ... // set white background, as transparent one lets account-list show through document.getElementsByTagName('body')[0].style.backgroundColor = 'white'; // hide navbar elements var ids2hide = ['divLogo', 'topmenu', 'divAppIconBar', 'divStatusBar', 'tdSidebox', 'divAppboxHeader']; for(var i=0; i < ids2hide.length; ++i) { var elem = document.getElementById(ids2hide[i]); if (elem) elem.style.display = 'none'; } } /** * Set location of iframe for given _action and _sender (row) * * @param _action * @param _senders */ iframe_location(_action, _senders) { var id = _senders[0].id.split('::'); var url = _action.data.url.replace(/(%24|\$)id/, id[1]); this.load(url); } /** * Callback to load an etemplate * * @param {Object[]} _data */ _ajax_load_callback(_data) { if(!_data || _data.type != undefined) return; // Insert the content, etemplate will load into it if(typeof _data === "string" || typeof _data[0] !== "undefined") { jQuery(this.ajax_target.getDOMNode()).append(typeof _data === 'string' ? _data : _data[0]); } else if(typeof _data.DOMNodeID == "string") { this.ajax_target.setAttribute("id", _data.DOMNodeID); } } /** * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app * * @param _url * @return boolean true, if linkHandler took care of link, false otherwise */ linkHandler(_url) { var matches = _url.match(/menuaction=admin.admin_ui.index.*&load=([^&]+)/); if (_url !='about:blank' && (this.iframe != null && !_url.match('menuaction=admin.admin_ui.index') || matches)) { if (matches) { _url = _url.replace(/menuaction=admin.admin_ui.index/, 'menuaction='+matches[1]).replace(/&(load=[^&]+)/g, ''); } this.load(_url); return true; } // can not load our own index page, has to be done by framework return false; } /** * Run an admin module / onclick callback for tree * * @param {string} _id id of clicked node * @param {et2_tree} _widget reference to tree widget */ run(_id, _widget) { var link = _widget.getUserData(_id, 'link'); this.groups.set_disabled(true); if (_id == '/accounts' || _id.substr(0, 8) == '/groups/') { this.load(); var parts = _id.split('/'); this.nm.applyFilters({ filter: parts[2] ? parts[2] : '', search: ''}); } else if (_id === '/groups') { this.load(); this.group_list(); } else if (typeof link == 'undefined') { _widget.openItem(_id, 'toggle'); } else if (link[0] == '/' || link.substr(0,4) == 'http') { link += (link.indexOf('?') >= 0 ? '&' : '?')+'nonavbar=1'; this.load(link); } else if (link.substr(0,11) == 'javascript:') { const href_regexp = /^javascript:([^\(]+)\((.*)?\);?$/; const matches = link.match(href_regexp); let args = []; if (matches.length > 1 && matches[2] !== undefined) { try { args = JSON.parse('['+matches[2]+']'); } catch(e) { // deal with '-encloded strings (JSON allows only ") args = JSON.parse('['+matches[2].replace(/'/g, '"')+']'); } } egw.applyFunc(matches[1], args); } } /** * Show the group list in the main window */ group_list() { this.nm.set_disabled(true); this.groups.set_disabled(false); jQuery(this.et2.parentNode).trigger('show.et2_nextmatch'); } /** * View, edit or delete a group callback for tree * * @param {object} _action egwAction * @param {array} _senders egwActionObject _senders[0].id holds id */ group(_action, _senders) { // Tree IDs look like /groups/ID, nm uses admin::ID const from_nm = _senders[0].id.indexOf('::') > 0; const account_id = _senders[0].id.split(from_nm ? '::' : '/')[from_nm ? 1 : 2]; switch(_action.id) { case 'view': this.run(from_nm ? '/groups/'+account_id : _senders[0].id, this.et2.getWidgetById('tree')); break; case 'delete': this.egw.json('admin_account::ajax_delete_group', [account_id, _action.data, this.et2.getInstanceManager().etemplate_exec_id]).sendRequest(); break; default: if (!_action.data.url) { alert('Missing url in action '+_action.id+'!'); break; } let url = unescape(_action.data.url).replace('$id', account_id); if (url[0] != '/' && url.substr(0, 4) != 'http') { url = this.egw.link('/index.php', url); } if (_action.data.popup || _action.data.width && _action.data.height) { this.egw.open_link(url, '_blank', _action.data.popup ? _action.data.popup : _action.data.width + 'x' + _action.data.height); } else { this.load(url); } break; } } /** * Opens a dialog to add / remove application run rights for one or more groups * * @param _action * @param _senders */ group_run_rights(_action : EgwAction, _senders : EgwActionObject[]) { // Tree IDs look like /groups/ID, nm uses admin::ID const from_nm = _senders[0].id.indexOf('::') > 0; let ids = []; let row_ids = [] _senders.forEach((sender) => { const account_id = sender.id.split(from_nm ? '::' : '/')[from_nm ? 1 : 2]; row_ids.push('admin::'+account_id); ids.push(account_id); }) const dialog = new Et2Dialog(this.egw); let attrs = { template: this.egw.webserverUrl + "/admin/templates/default/group.run_rights.xet", title: "Applications", hideOnEscape: true, width: "400", height: "300px", value: { content: {groups: ids} }, callback: (button_id, value) => { if(button_id == "_cancel") return; let acl_id = []; (value.apps ?? []).forEach(app => { ids.forEach(account => { acl_id.push(app + ":" + account +":run"); }) }); if(value && value.apps && acl_id.length) { const button = dialog.querySelector("[id*='"+button_id+"']"); if(button) button.disabled=true; this.egw.request( 'admin_acl::ajax_change_acl', [acl_id, button_id == "_add" ? 1 : 0, [], this.et2.getInstanceManager().etemplate_exec_id] ).then((_data) => { this.et2.getInstanceManager().refresh(_data.msg, this.appname,row_ids,'update'); dialog.close(); }); return false; } } } dialog.transformAttributes(attrs); this.et2.getInstanceManager().DOMContainer.appendChild(dialog); dialog.updateComplete.then(() => { dialog.template.widgetContainer.getWidgetById("apps").focus(); }); } /** * Opens a dialog to add / remove access to one or more applications for one or more groups * * @param {EgwAction} _action * @param {EgwActionObject[]} _senders */ group_change_access(_action : EgwAction, _senders : EgwActionObject[]) { // Tree IDs look like /groups/ID, nm uses admin::ID const from_nm = _senders[0].id.indexOf('::') > 0; let ids = []; let row_ids = [] _senders.forEach((sender) => { const account_id = sender.id.split(from_nm ? '::' : '/')[from_nm ? 1 : 2]; row_ids.push('admin::' + account_id); ids.push(account_id); }) // Load application ACL settings const setChangeAccessCustomisation = async() => { this.acl_dialog.width = 700; const buttons = [ {label: egw.lang("Add"), id: "add", default: true, image: "add"}, {label: egw.lang("Remove"), id: "remove", image: "minus"}, {label: egw.lang("Cancel"), id: Et2Dialog.CANCEL_BUTTON, image: "cancel", align: "right"} ]; if(this.acl_dialog.buttons.length != buttons.length) { this.acl_dialog.buttons = buttons; // This should NOT be called, but Et2Dialog doesn't support changing buttons after this.acl_dialog.firstUpdated(); } await this.acl_dialog.updateComplete; const account = this.acl_dialog.querySelector("#_acl_account"); // Set account as multiple account.multiple = true; account.requestUpdate("multiple"); // Set account as hidden account.parentNode.parentNode.classList.add('hideme'); // Set location as multiple const location = this.acl_dialog.querySelector("#_acl_location"); location.multiple = true; location.requestUpdate("multiple"); }; // Dialog gets recreated several times, customise it each time document.body.addEventListener("open", setChangeAccessCustomisation); this._acl_dialog({acl_account: ids}, {}, this.et2).then(async() => { await this.acl_dialog.updateComplete document.body.addEventListener("close", (event) => { if(event.target instanceof Et2Dialog) { document.body.removeEventListener("open", setChangeAccessCustomisation); } }); }) } /** * Modify an ACL entry * * @param {object} _action egwAction * @param {array} _senders egwActionObject _senders[0].id holds the id "admin::app:account:location" */ acl(_action, _senders) { var ids = []; for(var i=0; i < _senders.length; ++i) { ids.push(_senders[i].id.split('::').pop()); // remove "admin::" prefix } // For edit, set some data from the list since it's already there var content = _senders[0].id ? jQuery.extend({}, egw.dataGetUIDdata(_senders[0].id).data) : {}; switch(_action.id) { case 'delete': this._acl_delete(ids); break; case 'add': // No content, even if they clicked on a row // Defaults set by _acl_content() based on nm values content = {}; // Fall through case 'edit': this._acl_dialog(content); break; } } _acl_delete(ids) { var app = egw.app_name(); // can be either admin or preferences! if(app != 'admin') { app = 'preferences'; } var className = app + '_acl'; var callback = function(_button_id, _value) { if(_button_id != Et2Dialog.OK_BUTTON) { return; } var request = egw.json(className + '::ajax_change_acl', [ids, null, _value, this.et2._inst.etemplate_exec_id], this._acl_callback, this, false, this) .sendRequest(); }.bind(this); var modifications : any = {}; var dialog_options = { callback: callback, title: this.egw.lang('Delete'), buttons: Et2Dialog.BUTTONS_OK_CANCEL, value: { content: {}, sel_options: {}, modifications: modifications, readonlys: {} }, template: egw.webserverUrl + '/admin/templates/default/acl.delete.xet' }; // Handle policy documentation tab here if(this.egw.user('apps').policy) { dialog_options['width'] = 550; modifications.tabs = { addTabs: true, extraTabs: [{ label: egw.lang('Documentation'), template: 'policy.admin_cmd', prepend: false }] }; } // Create the dialog this.acl_dialog = new Et2Dialog(app); this.acl_dialog.transformAttributes(dialog_options); document.body.appendChild(this.acl_dialog); } /** * Create the ACL edit dialog, including defaults & fetching what can be found * * @param content List of content for the dialog template * @param sel_options optional select options * @param {etemplate2} etemplate of etemplate that 'owns' the dialog * @param {string} app Name of app * @param {function} callback */ async _acl_dialog(content, sel_options?, etemplate?, app?, callback? : Function) { if(typeof content == 'undefined') { content = {}; } // Determine which application we're running as app = app ? app : egw.app_name(); // can be either admin or preferences! if(app != 'admin') { app = 'preferences'; } // Get by ID, since this.et2 isn't always the ACL list var et2 = etemplate ? etemplate : etemplate2.getById('admin-acl').widgetContainer; var className = app + '_acl'; var acl_rights : any = {}; var readonlys : any = {acl: {}}; var modifications : any = {}; // Select options are already here, just pull them and pass along sel_options = { ...{ acl_account: [], acl_location: [] }, ...(et2.getArrayMgr('sel_options').data || {}) }; // Some defaults if(et2 && et2.getWidgetById('nm')) { // This is which checkboxes are available for each app acl_rights = et2.getWidgetById('nm').getArrayMgr('content').getEntry('acl_rights') || await this.egw.request(className + '::ajax_get_rights', [content.acl_account]); if(!content.acl_appname) { // Pre-set appname to currently selected content.acl_appname = et2.getWidgetById('filter2').getValue() || ""; } if(!content.acl_account) { content.acl_account = et2.getWidgetById('nm').getArrayMgr('content').getEntry('account_id'); } if(!content.acl_location) { content.acl_location = et2.getWidgetById('filter').getValue() == 'run' ? 'run' : null; } // If no admin rights, change UI to not allow adding access to apps if(content.acl_location == 'run' && !egw.user('apps')['admin']) { content.acl_location = null; } if(content.acl_location == 'run') { // These are the apps the account has access to // Fetch current values from server this.egw.json(className+'::ajax_get_app_list', [content.acl_account], function(data) {content.apps = data;},this,false,this) .sendRequest(); } else { // Restrict application selectbox to only apps that support ACL sel_options.acl_appname = []; for(let app in acl_rights) { sel_options.acl_appname.push({value: app, label: app}); } // Sort list sel_options.acl_appname.sort(function(a, b) { if(a.label > b.label) return 1; if(a.label < b.label) return -1; return 0; }); } } if(content.acl_appname) { // Load checkboxes & their values content.acl_rights = content.acl_rights ? parseInt(content.acl_rights) : null; jQuery.extend(content, {acl: [], right: [], label: []}); // Use this to make sure we get correct app translations let app_egw = egw(content.acl_appname, window); for(var right in acl_rights[content.acl_appname]) { // only user himself is allowed to grant private (16) rights if(right == '16' && content['acl_account'] != egw.user('account_id')) { readonlys.acl[content.acl.length] = true; } content.acl.push(content.acl_rights & parseInt(right)); content.right.push(right); content.label.push(app_egw.lang(acl_rights[content.acl_appname][right])); } } if(content.acl_account && !egw.user('apps')['admin']) { readonlys.acl_account = true; } let wait = [] // Make sure new accounts are in the list, client side cache won't have them let accounts = Array.isArray(content.acl_account) ? content.acl_account : [content.acl_account]; accounts.forEach(account => { wait.push(this.egw.link_title('api-accounts', account, true).then(title => { sel_options.acl_account.push({value: account, label: title}); sel_options.acl_location.push({value: account, label: title}); })); }) var dialog_options = { callback: (_button_id, _value) => { this.acl_dialog = null; if(_button_id == Et2Dialog.CANCEL_BUTTON || !_button_id) { return; } // Restore account if it's readonly in dialog if(!_value.acl_account) { _value.acl_account = content.acl_account; } // Handle no applications selected if(typeof _value.apps == 'undefined' && content.acl_location == 'run') { _value.apps = []; } // Only send the request if they entered everything (or selected no apps) if(_value.acl_account && (_value.acl_appname && _value.acl_location || typeof _value.apps != 'undefined')) { let id : any = []; let account = Array.isArray(_value.acl_account) ? _value.acl_account : [_value.acl_account]; let location = Array.isArray(_value.acl_location) ? _value.acl_location : [_value.acl_location]; if(_value.acl_appname && account.length && location.length) { account.forEach(account => { location.forEach(location => { const acl_id = _value.acl_appname + ':' + account + ':' + location; if(content && content.id && acl_id != content.id) { // Changed the account or location, remove previous or we // get a new line instead of an edit this.egw.json(className + '::ajax_change_acl', [content.id, 0, [], this.et2._inst.etemplate_exec_id], null, this, false, this) .sendRequest(); } id.push(acl_id); }); }); } var rights = 0; for(var i in _value.acl) { rights += parseInt(_value.acl[i]) * (_button_id == "remove" ? -1 : 1); } if(typeof _value.apps != 'undefined' && !_value.acl_appname) { rights = 1; var removed = []; // Loop through all apps, remove the ones with no permission for(var idx in sel_options.filter2) { var app = sel_options.filter2[idx].value || false; if(!app) { continue; } var run_id = app + ":" + _value.acl_account + ":run"; if(_value.apps.indexOf(app) < 0 && (content.apps.indexOf(app) >= 0 || content.apps.length == 0)) { removed.push(run_id); } else if(_value.apps.indexOf(app) >= 0 && content.apps.indexOf(app) < 0) { id.push(run_id); } } // Remove any removed if(removed.length > 0) { this.egw.json(className + '::ajax_change_acl', [removed, 0, [], this.et2._inst.etemplate_exec_id], callback ? callback : this._acl_callback, this, false, this) .sendRequest(); } } this.egw.json(className + '::ajax_change_acl', [id, rights, _value, this.et2._inst.etemplate_exec_id], callback ? callback : this._acl_callback, this, false, this) .sendRequest(); } }, title: this.egw.lang('Access control'), buttons: Et2Dialog.BUTTONS_OK_CANCEL, value: { content: content, // @todo: we need to investigate more on et2_widget_selectbox type of apps // where the sel options are not ready while setting its content. Therefore, // the explicit apps should be removed after fixing it on the widget side. sel_options: sel_options,// {...sel_options, apps: sel_options.filter2}, modifications: modifications, readonlys: readonlys }, template: egw.webserverUrl + '/admin/templates/default/acl.edit.xet' }; // Handle policy documentation tab here if(this.egw.user('apps').policy) { dialog_options['width'] = 550; dialog_options['height'] = 450, modifications.tabs = { add_tabs: true, tabs: [{ label: egw.lang('Documentation'), template: 'policy.admin_cmd', prepend: false }] }; } // Create the dialog return Promise.all(wait).then(() => { this.acl_dialog = new Et2Dialog(app); this.acl_dialog.transformAttributes(dialog_options); this.acl_dialog.et2 = etemplate; document.body.appendChild(this.acl_dialog); }); } /** * Change handler for ACL edit dialog application selectbox. * Re-creates the dialog with the current values * * @param input * @param widget */ acl_reopen_dialog(input, widget) { let content = {}; let et2 = undefined; let callback = undefined; if(this.acl_dialog != null) { content = this.acl_dialog.get_value() || {}; // returning tabs as content seems to screw up recreating the dialog // @todo: maybe this should be investigated further in et2Tabs widget delete(content.tabs); et2 = this.acl_dialog.et2 ?? undefined; callback = this.acl_dialog.callback ?? undefined; // Destroy the dialog this.acl_dialog.destroy(); this.acl_dialog = null; } // Re-open the dialog this._acl_dialog(content, {}, et2, callback); } /** * Load the new application's lang files when the app filter is changed */ acl_app_change(event, nm) { let appname = nm.getWidgetById('filter2').getValue() || ''; if(appname) { let app_egw = egw(appname); app_egw.langRequireApp(window, appname); nm.getRoot().setApiInstance(app_egw); } } /** * Callback called on successfull call of serverside ACL handling * * @param {object} _data returned from server */ _acl_callback(_data) { // Avoid the window / framework / app and just refresh the etemplate // Framework will try to refresh the opener // Get by ID, since this.et2 isn't always the ACL list const et2 = etemplate2.getById('admin-acl')?.widgetContainer; if(et2) { et2.getInstanceManager().refresh(_data.msg, this.appname, _data.ids, _data.type); } else if(_data.msg) { this.egw.message(_data.msg); } } /** * Check to see if admin has taken away access to a category * * @@param {widget} button add/apply pressed button */ check_owner(button) { var select_owner = this.et2.getWidgetById('owner'); var diff = []; if (typeof select_owner != 'undefined') { var owner = select_owner.value; } if(typeof owner != 'object') { owner = [owner]; } // No owner probably means selectbox is read-only, so no need to check if(owner == null) return true; var all_users = owner.indexOf('0') >= 0; // If they checked all users, uncheck the others if(all_users) { select_owner.value = ['0']; return true; } // Find out what changed var cat_original_owner = this.et2.getArrayMgr('content').getEntry('owner'); if (cat_original_owner) { var selected_groups = select_owner.value.toString(); for(var i =0;i < cat_original_owner.length;i++) { if (selected_groups.search(cat_original_owner[i]) < 0) { diff.push(cat_original_owner[i]); } } if (diff.length > 0) { var removed_cat_label = jQuery.map(select_owner.options.select_options, function (val, i) { for (var j=0; j <= diff.length;j++) { if (diff[j] == val.value) { return val.label; } } }); // Somebody will lose permission, give warning. if(removed_cat_label) { var msg = this.egw.lang('Removing access for groups may cause problems for data in this category. Are you sure? Users in these groups may no longer have access:'); return Et2Dialog.confirm(button, msg + removed_cat_label.join(',')); } } } return true; } /** * Show icon based on icon-selectbox, hide placeholder (broken image), if no icon selected * * @param {widget} widget select box widget */ change_icon(widget) { var img = widget.getRoot().getWidgetById('icon_url'); if (img) { img.set_src(widget.getValue()); } } /** * Add / edit an account * * @param {object} _action egwAction * @param {array} _senders egwActionObject _senders[0].id holds account_id */ account(_action, _senders) { var params = jQuery.extend({}, this.egw.link_get_registry('addressbook', 'edit')); var popup = this.egw.link_get_registry('addressbook', 'edit_popup'); switch(_action.id) { case 'add': params.owner = '0'; break; case 'copy': params.owner = '0'; params.copy = true; // Fall through default: params.account_id = _senders[0].id.split('::').pop(); // get last :: separated part break; } this.egw.open_link(this.egw.link('/index.php', params), 'admin', popup, 'admin'); } /** * Submit statistic * * Webkit browsers (Chrome, Safari, ...) do NOT allow to call form.submit() from within onclick of a submit button. * Therefor we first store our own form action, replace it with egroupware.org submit url and set a timeout calling * submit_statistic again with just the form, to do the second submit to our own webserver * * @param {DOM} form * @param {string} submit_url * @return {boolean} */ submit_statistic(form, submit_url) { var that = this; var submit = function() { // submit to egroupware.org var method=form.method; form.method='POST'; var action = form.action; form.action=submit_url; var target = form.target; form.target='_blank'; form.submit(); // submit to own webserver form.method=method; form.action=action; form.target=target; that.et2.getInstanceManager().submit('submit'); }; // Safari does NOT allow to call form.submit() outside of onclick callback // so we have to use browsers ugly synchron confirm if (navigator.userAgent.match(/Safari/) && !navigator.userAgent.match(/Chrome/)) { if (confirm(this.egw.lang('Submit displayed information?'))) { submit(); } } else { Et2Dialog.show_dialog(function(_button) { if(_button == Et2Dialog.YES_BUTTON) { submit(); } }, this.egw.lang('Submit displayed information?'), '', {}, Et2Dialog.BUTTONS_YES_NO, Et2Dialog.QUESTION_MESSAGE, undefined, egw); } return false; } /** * Change handler for when you change the type of a custom field. * It toggles options / attributes as appropriate. * @param {event object} e * @param {widget object} widget */ cf_type_change(e,widget) { var root = widget.getRoot(); var attributes = widget.getArrayMgr('content').getEntry('attributes['+widget.getValue()+']')||{}; root.getWidgetById('cf_values').set_statustext(widget.egw().lang(widget.getArrayMgr('content').getEntry('options['+widget.getValue()+']')||'')); jQuery(root.getWidgetById('cf_len').getDOMNode()).toggle(attributes.cf_len && true); jQuery(root.getWidgetById('cf_rows').getDOMNode()).toggle(attributes.cf_rows && true); jQuery(root.getWidgetById('cf_values').getParentDOMNode()).toggle(attributes.cf_values && true); } /** * Change handler for when you delete a custom app type * If Policy app is available, it asks for documentation */ cf_type_delete(e, widget) { var callback = function(button, value) { if(button === Et2Dialog.YES_BUTTON) { var values = jQuery.extend( {}, this.getInstanceManager().getValues(this.getRoot()), value, {appname: this.getRoot().getArrayMgr('content').getEntry('content_types[appname]')} ); egw.json('admin.admin_customfields.ajax_delete_type', [values, this.getInstanceManager().etemplate_exec_id]).sendRequest(); // Immediately remove the type var types = this.getRoot().getWidgetById('types'); var options = types.options.select_options; var key; for(key in options) { if(options.hasOwnProperty(key) && key === types.getValue()) { delete options[key]; break; } } types.set_select_options(options); this.egw().message(''); // Trigger load of status for existing type types.set_value(Object.keys(options)[0]); } }.bind(widget); if(egw.app('policy')) { import(egw.link('/policy/js/app.min.js?' + ((new Date).valueOf() / 86400 | 0).toString())).then(() => { if(typeof app.policy === 'undefined' || typeof app.policy.confirm === 'undefined') { app.policy = new app.classes.policy(); } let dialog = new Et2Dialog(widget.egw()); dialog.transformAttributes({ callback: callback, template: egw.link('/policy/templates/default/admin_cmd_narrow.xet'), title: 'Delete', buttons: Et2Dialog.BUTTONS_YES_NO, value: {content: {}}, width: 'auto' }); widget.egw().window.document.body.appendChild(dialog); dialog.egw().message("Entries with a deleted type can cause problems.\nCheck for entries with this type before deleting.", 'warning'); }); } else { callback(Et2Dialog.YES_BUTTON); } return false; } /** * Activate none standard SMTP mail accounts for selected users * * @param {egw_action} _action * @param {array} _selected selected users */ emailadminActiveAccounts(_action, _selected) { var menuaction = 'admin.admin_mail.ajax_activeAccounts'; var accounts = []; var msg1 = egw.lang('%1 accounts being activated', ""+Object.keys(_selected).length); for (var i=0;i< Object.keys(_selected).length;i++) { accounts[i] = [{id:_selected[i]['id'].split('::')[1],quota:"", domain:"", status:_action.id == 'active'?_action.id:''}, this.et2._inst.etemplate_exec_id]; } var callbackDialog = function (btn){ if(btn === Et2Dialog.YES_BUTTON) { // long task dialog for de/activation accounts Et2Dialog.long_task(function(_val, _resp) { if(_val && _resp.type !== 'error') { console.log(_val, _resp); } else { } }, msg1, 'Mail Acounts Activation', menuaction, accounts, 'admin'); } }; // confirmation dialog Et2Dialog.show_dialog(callbackDialog, egw.lang('Are you sure you want to %1 mail for selected accounts?', egw.lang(_action.id)), 'Active Mail Accounts', {}, Et2Dialog.BUTTONS_YES_NO, Et2Dialog.WARNING_MESSAGE, undefined, egw); } /** * No SSL */ SSL_NONE = 0; /** * STARTTLS on regular tcp connection/port */ SSL_STARTTLS = 1; /** * SSL (inferior to TLS!) */ SSL_SSL = 3; /** * require TLS version 1+, no SSL version 2 or 3 */ SSL_TLS = 2; /** * if set, verify certifcate (currently not implemented in Horde_Imap_Client!) */ SSL_VERIFY = 8; /** * Resize window methode * * @returns {undefined} */ wizard_popup_resize() { var $main_div = jQuery('#popupMainDiv'); var $et2 = jQuery('.et2_container'); var w = { width: egw_getWindowInnerWidth(), height: egw_getWindowInnerHeight() }; // Use et2_container for width since #popupMainDiv is full width, but we still need // to take padding/margin into account var delta_width = w.width - ($et2.outerWidth(true) + ($main_div.outerWidth(true) - $main_div.width())); var delta_height = w.height - ($et2.outerHeight(true) + ($main_div.outerHeight(true) - $main_div.height())); if(delta_width != 0 || delta_height != 0) { window.resizeTo(egw_getWindowOuterWidth() - delta_width,egw_getWindowOuterHeight() - delta_height); } } /** * Switch account wizard to manual entry */ wizard_manual() { jQuery('.emailadmin_manual').fadeToggle();// not sure how to to this et2-isch this.wizard_popup_resize(); // popup needs to be resized after toggling } /** * onclick for continue button to show progress animation * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ wizard_detect(_event, _widget) { // we need to do a manual asynchronious submit to show progress animation // default synchronious submit stops animation! if (this.et2._inst.submit('button[continue]', true)) // true = async submit { var sieve_enabled = this.et2.getWidgetById('acc_sieve_enabled'); if (!sieve_enabled || sieve_enabled.get_value()) { jQuery('#admin-mailwizard_output').hide(); jQuery('td.emailadmin_progress').show(); } } return false; } /** * Set default port, if imap ssl-type changes * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ wizard_imap_ssl_onchange(_event, _widget) { var ssl_type = _widget.get_value(); this.et2.getWidgetById('acc_imap_port').set_value( ssl_type == this.SSL_SSL || ssl_type == this.SSL_TLS ? 993 : 143); } /** * Set default port, if imap ssl-type changes * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ wizard_smtp_ssl_onchange(_event, _widget) { var ssl_type = _widget.get_value(); this.et2.getWidgetById('acc_smtp_port').set_value( ssl_type == 'no' ? 25 : (ssl_type == this.SSL_SSL || ssl_type == this.SSL_TLS ? 465 : 587)); } /** * Set default port, if imap ssl-type changes * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ wizard_sieve_ssl_onchange(_event, _widget) { var ssl_type = _widget.get_value(); this.et2.getWidgetById('acc_sieve_port').set_value( ssl_type == this.SSL_SSL || ssl_type == this.SSL_TLS ? 5190 : 4190); this.wizard_sieve_onchange(_event, _widget); } /** * Enable sieve, if user changes some setting * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ wizard_sieve_onchange(_event, _widget) { this.et2.getWidgetById('acc_sieve_enabled').set_value(1); } /** * Callback if user changed account selction * * @param {object} _event event-object or information about event * @param {et2_baseWidget} _widget widget causing the event */ change_account(_event, _widget) { // todo check dirty and query user to a) save changes, b) discard changes, c) cancel selection _widget.getInstanceManager().submit(); } /** * Callback if user changes notification folders: unset use-default checkbox * * @param {object} _event * @param {et2_widget} _widget */ change_folders(_event, _widget) { var use_default = this.et2.getWidgetById('notify_use_default'); if (use_default) use_default.set_value(false); } /** * onchange callback for mail account account_id (valid for) * * @param {object} _event * @param {et2_widget} _widget */ warnMailAccountForAllChanged(_event : Event, _widget : Et2SelectAccount) { const account_id = _widget.value; const old_account_id = this.et2.getArrayMgr('content').getEntry('account_id'); // this is (no longer) an account for all if ((Array.isArray(account_id) ? account_id.length : account_id) && // but this was an account for all !(Array.isArray(old_account_id) ? old_account_id.length : old_account_id)) { _widget.blur(); Et2Dialog.alert(this.egw.lang('By selecting a user or group you effectively delete the mail account for all other users!\n\nAre you really sure you want to do that?'), this.egw.lang('This is a mail account for ALL users!'), Et2Dialog.WARNING_MESSAGE); } } /** * default onExecute for admin actions * * @param {object} _action * @param {object} _senders */ account_edit_action(_action, _senders) { if (_action.data.url) { this.egw.open_link(_action.data.url, _action.data.target || '_blank', _action.data.popup); } } /** * Clear instance cache * * If there is an error on server-side, resend request with an parameter allowing * cache to use different method not requiring eg. so much memory */ clear_cache() { let wait = this.egw.message(this.egw.lang('Clear cache and register hooks')+"\n"+this.egw.lang('Please wait...'),'info'); let success = function (){ wait.close(); egw.message('Done'); }; this.egw.json('admin.admin_hooks.ajax_clear_cache', null, success).sendRequest(true, undefined, jQuery.proxy(function(_xmlhttp, _err) { this.egw.json('admin.admin_hooks.ajax_clear_cache&errored=1', null, success).sendRequest(true); }, this)); } /** * Action handler for clear credentials action * * @param action * @param selected */ clear_credentials_handler(action : egwAction, selected: egwActionObject[]) { let ids = []; for(let row of selected) { ids.push(row.id.split("::").pop()); } this.egw.request("admin.admin_passwordreset.ajax_clear_credentials", [action.id, ids]); } /** * Export content of given field into relevant file */ smime_exportCert() { var $a = jQuery(document.createElement('a')).appendTo('body').hide(); var acc_id = this.et2.getArrayMgr("content").getEntry('acc_id'); var url = window.egw.webserverUrl+'/index.php?'; url += 'menuaction=mail.mail_ui.smimeExportCert'; url += '&acc_id='+acc_id; $a.prop('href',url); $a.prop('download',""); $a[0].click(); $a.remove(); } /** * Create certificate generator dialog */ smime_genCertificate() { var self = this; let dialog = new Et2Dialog("mail"); dialog.transformAttributes({ callback(_button_id, _value) { if(_button_id == 'create' && _value) { var isValid = true; var required = ['countryName', 'emailAddress']; var widget; // check the required fields for(var i = 0; i < required.length; i++) { if(_value[required[i]]) { continue; } widget = this.template.widgetContainer.getWidgetById(required[i]); widget.set_validation_error('This field is required!'); isValid = false; } // check mismatch passphrase if (_value.passphrase && _value.passphrase !== _value.passphraseConf) { var passphraseConf = this.template.widgetContainer.getWidgetById('passphrase'); passphraseConf.set_validation_error('Confirm passphrase is not match!'); isValid = false; } if (isValid) { egw.json('mail.mail_ui.ajax_smimeGenCertificate', _value, function(_cert){ if (_cert) { for (var key in _cert) { if (!_cert[key]) continue; switch (key) { case 'cert': self.et2.getWidgetById('smime_cert').set_value(_cert[key]); break; case 'privkey': self.et2.getWidgetById('acc_smime_password').set_value(_cert[key]); break; } } self.egw.message('New certificate information has been generated, please save your account if you want to store it.'); } }).sendRequest(true); } else { return false; } } }, title: egw.lang('Generate Certificate'), buttons: [ {text: this.egw.lang("Create"), id: "create", "class": "ui-priority-primary", "default": true}, {text: this.egw.lang("Cancel"), id: "cancel"} ], value: { content: { value: '' } }, template: egw.webserverUrl + '/mail/templates/default/smimeCertGen.xet?' + Date.now(), resizable: false, position: 'left top' }); document.body.appendChild(dialog); } /** * Triggers upload for background image and updates its taglist * * @param {type} node * @param {type} widget */ login_background_update(node, widget) { var taglist = widget._parent._children[0]; egw.json('admin.admin_config.ajax_upload_anon_images', [widget.get_value(), taglist.get_value()], function(_data){ taglist.set_value(_data); }).sendRequest(); } /** * Set content of selected row * * @param {array} node * @returns */ cmds_onselect(node) { var splitter = this.et2.getWidgetById('splitter'); var cmds_preview = this.et2.getWidgetById('cmds_preview'); if (node.length != 1) { splitter.dock(); return; } if (splitter.isDocked()) { splitter.undock(); } var data = egw.dataGetUIDdata(node[0]); var policy_preview = this.et2.getWidgetById('policy_preview'); var id = node[0].replace('admin::', ''); if (app.policy) { cmds_preview.set_disabled(true); policy_preview.set_src(egw.link('/index.php', { menuaction:'policy.EGroupware\\Policy\\History.view', 'cmd_id':id, 'cmd_template': "policy.admin_cmd_history" })); } else { policy_preview.set_disabled(true); cmds_preview.set_value({content:[data.data]}); } } /******************************************************************************************************************* * Groupadmin methods ******************************************************************************************************************/ /** * ACL button clicked * * @param {jQuery.Event} _ev * @param {et2_button} _widget */ aclGroup(_ev, _widget) { let app = _widget.id.substr(7, _widget.id.length-8); // button[appname] let apps = this.et2.getArrayMgr('content').getEntry('apps'); for (let i=0; i < apps.length; i++) { let data = apps[i]; if (data.appname == app && data.action) { if (data.action === true) { data.action = this.egw.link('/index.php', { menuaction: 'admin.admin_acl.index', account_id: this.et2.getArrayMgr('content').getEntry('account_id'), acl_filter: 'other', acl_app: app }); data.popup = '900x450'; } egw(opener).open_link(data.action, data.popup ? '_blank' : '_self', data.popup); break; } } } /** * Delete button clicked * * @param {jQuery.Event} _ev * @param {et2_button} _widget */ deleteGroup(_ev, _widget) { let account_id = this.et2.getArrayMgr('content').getEntry('account_id'); let egw = this.egw; Et2Dialog.show_dialog(function(button) { if(button == Et2Dialog.YES_BUTTON) { egw.json('admin_account::ajax_delete_group', [account_id, [], _widget.getInstanceManager().etemplate_exec_id]).sendRequest(false); // false = synchronious request window.close(); } }, this.egw.lang('Delete this group') + '?'); } /** * Field changed, call server validation * * @param {jQuery.Event} _ev * @param {et2_button} _widget */ changeGroup(_ev, _widget) { let account_id = this.et2.getArrayMgr('content').getEntry('account_id'); let data = {account_id: account_id}; data[_widget.id] = _widget.getValue(); this.egw.json('EGroupware\\Admin\\Groups::ajax_check', [data], function(_msg) { if (_msg) { egw(window).message(_msg, 'error'); // context gets lost :( _widget.getDOMNode().focus(); } }, this).sendRequest(); } /** * Clickhandler to copy given text or widget content to clipboard * @param _widget * @param _text default widget content */ copyClipboard(_widget : et2_DOMWidget, _text? : string, _event? : Event) { let value = _text || (typeof _widget.get_value === 'function' ? _widget.get_value() : _widget.options.value); let node = _widget.getDOMNode() !== _widget ? _widget.getDOMNode() : _widget; this.egw.copyTextToClipboard(value, node, _event).then((success) => { if(success !== false) { this.egw.message(this.egw.lang("Copied '%1' to clipboard", value), 'success'); } }); } /** * Batch reset multiple account passwords * * JS callback for admin_passwordreset so we can do longtask. Values come from the current admin etemplate. */ async bulkPasswordReset() { const data = []; const values = this.et2.getInstanceManager().getValues(this.et2); let users = values.users ?? []; delete values.users; if(users.includes("~all~")) { // Doesn't always give _all_ accounts const accounts = await egw.accounts("accounts"); debugger; } if(users.length == 0) { return; } for(let i = 0; i < users.length; i++) { data.push({values, user: users[i]}); } Et2Dialog.long_task( null, "", "Bulk Password Reset", "admin.admin_passwordreset.ajax_reset", data, this.egw ); } } app.classes.admin = AdminApp;