mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-21 23:43:17 +01:00
2420 lines
71 KiB
TypeScript
2420 lines
71 KiB
TypeScript
/**
|
|
* EGroupware clientside Application javascript base object
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link https://www.egroupware.org
|
|
* @author Ralf Becker <rb@egroupware.org>
|
|
* @author Hadi Nategh <hn@groupware.org>
|
|
* @author Nathan Gray <ng@groupware.org>
|
|
*/
|
|
|
|
import {etemplate2} from "../etemplate/etemplate2";
|
|
import type {et2_container} from "../etemplate/et2_core_baseWidget";
|
|
import {et2_nextmatch} from "../etemplate/et2_extension_nextmatch";
|
|
import {et2_createWidget} from "../etemplate/et2_core_widget";
|
|
import type {IegwAppLocal} from "./egw_global";
|
|
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
|
|
import {et2_valueWidget} from "../etemplate/et2_core_valueWidget";
|
|
import {fetchAll, nm_action} from "../etemplate/et2_extension_nextmatch_actions";
|
|
import {Et2Dialog} from "../etemplate/Et2Dialog/Et2Dialog";
|
|
import {Et2Favorites} from "../etemplate/Et2Favorites/Et2Favorites";
|
|
import {loadWebComponent} from "../etemplate/Et2Widget/Et2Widget";
|
|
import type {EgwAction} from "../egw_action/EgwAction";
|
|
import {Et2MergeDialog} from "../etemplate/Et2Dialog/Et2MergeDialog";
|
|
import {EgwActionObject} from "../egw_action/EgwActionObject";
|
|
import type {Et2Details} from "../etemplate/Layout/Et2Details/Et2Details";
|
|
import {Et2Checkbox} from "../etemplate/Et2Checkbox/Et2Checkbox";
|
|
import {egw_globalObjectManager} from "../egw_action/egw_action";
|
|
|
|
/**
|
|
* Type for push-message
|
|
*/
|
|
export interface PushData
|
|
{
|
|
type : "add" | "edit" | "update" | "delete" | "unknown";
|
|
app : string; // app-name, can include a subtype eg. "projectmanager-element"
|
|
id : string | number;
|
|
acl? : any; // app-specific acl data, eg. the owner, or array of participants
|
|
account_id : number; // user that caused the change
|
|
[propName : string] : any; // arbitrary more parameters
|
|
}
|
|
|
|
/**
|
|
* Common base class for application javascript
|
|
* Each app should extend as needed.
|
|
*
|
|
* All application javascript should be inside. Intitialization goes in init(),
|
|
* clean-up code goes in destroy(). Initialization is done once all js is loaded.
|
|
*
|
|
* var app.appname = AppJS.extend({
|
|
* // Actually set this one, the rest is example
|
|
* appname: appname,
|
|
*
|
|
* internal_var: 1000,
|
|
*
|
|
* init: function()
|
|
* {
|
|
* // Call the super
|
|
* this._super.apply(this, arguments);
|
|
*
|
|
* // Init the stuff
|
|
* if ( egw.preference('dateformat', 'common') )
|
|
* {
|
|
* // etc
|
|
* }
|
|
* },
|
|
* _private: function()
|
|
* {
|
|
* // Underscore private by convention
|
|
* }
|
|
* });
|
|
*/
|
|
export abstract class EgwApp
|
|
{
|
|
/**
|
|
* Internal application name - pass this in constructor
|
|
*/
|
|
readonly appname : string;
|
|
|
|
/**
|
|
* Internal reference to the most recently loaded etemplate2 widget tree
|
|
*
|
|
* NOTE: This variable can change which etemplate it points to as the user
|
|
* works. For example, loading the home or admin apps can cause
|
|
* et2_ready() to be called again with a different template. this.et2 will
|
|
* then point to a different template. If the user then closes that tab,
|
|
* this.et2 will point to a destroyed object, and trying to use it will fail.
|
|
*
|
|
* If you need a reference to a certain template you can either store a local
|
|
* reference or access it through etemplate2.
|
|
*
|
|
* @example <caption>Store a local reference</caption>
|
|
* // in et2_ready()
|
|
* if(name == 'index') this.index_et2 = et2.widgetContainer;
|
|
*
|
|
* // Remember to clean up in destroy()
|
|
* delete this.index_et2;
|
|
*
|
|
* // Instead of this.et2, using a local reference
|
|
* this.index_et2 ...
|
|
*
|
|
*
|
|
* @example <caption>Access via etemplate2 object</caption>
|
|
* // Instead of this.et2, using its unique ID
|
|
* var et2 = etemplate2.getById("myapp-index")
|
|
* if(et2)
|
|
* {
|
|
* et2.widgetContainer. ...
|
|
* }
|
|
*
|
|
* @var {et2_container}
|
|
*/
|
|
et2 : et2_container;
|
|
|
|
/**
|
|
* Internal reference to egw client-side api object for current app and window
|
|
*
|
|
* @var {egw}
|
|
*/
|
|
egw : IegwAppLocal;
|
|
|
|
sidebox : JQuery;
|
|
|
|
viewContainer : JQuery;
|
|
viewTemplate : JQuery;
|
|
et2_view : any;
|
|
favorite_popup : JQuery | any;
|
|
|
|
dom_id : string;
|
|
|
|
mailvelopeSyncHandlerObj : any;
|
|
|
|
/**
|
|
* In some cases (CRM) a private, disconnected app instance is created instead of
|
|
* using the global. We want to be able to access them for observer() & push(), so
|
|
* we track all instances.
|
|
*/
|
|
static _instances : EgwApp[] = [];
|
|
|
|
/**
|
|
* If pushData.acl has fields that can help filter based on ACL grants, list them
|
|
* here and we can check them and ignore push messages if there is no ACL for that entry
|
|
*
|
|
* @protected
|
|
*/
|
|
protected push_grant_fields : string[];
|
|
|
|
/**
|
|
* If pushData.acl has fields that can help filter based on current nextmatch filters,
|
|
* list them here and we can check and ignore push messages if the nextmatch filters do not exclude them
|
|
*
|
|
* @protected
|
|
*/
|
|
protected push_filter_fields : string[];
|
|
|
|
/**
|
|
* Initialization and setup goes here, but the etemplate2 object
|
|
* is not yet ready.
|
|
*/
|
|
constructor(appname : string, _wnd? : Window)
|
|
{
|
|
this.appname = appname;
|
|
this.egw = egw(this.appname, _wnd || window);
|
|
|
|
// Initialize sidebox for non-popups.
|
|
// ID set server side
|
|
if(!this.egw.is_popup())
|
|
{
|
|
var sidebox = jQuery('#favorite_sidebox_' + this.appname);
|
|
if(sidebox.length == 0 && egw_getFramework() != null)
|
|
{
|
|
var egw_fw = egw_getFramework();
|
|
sidebox = jQuery('#favorite_sidebox_' + this.appname, egw_fw.sidemenuDiv);
|
|
}
|
|
// Make sure we're running in the top window when we init sidebox
|
|
//@ts-ignore
|
|
if(window.app[this.appname] === this && egw.top.app[this.appname] !== this && egw.top.app[this.appname])
|
|
{
|
|
//@ts-ignore
|
|
egw.top.app[this.appname]._init_sidebox(sidebox);
|
|
}
|
|
else
|
|
{
|
|
this._init_sidebox(sidebox);
|
|
}
|
|
}
|
|
this.mailvelopeSyncHandlerObj = this.mailvelopeSyncHandler();
|
|
|
|
// Keep track of this instance
|
|
EgwApp._register_instance(this);
|
|
}
|
|
|
|
/**
|
|
* Clean up any created objects & references
|
|
* @param {object} _app local app object
|
|
*/
|
|
destroy(_app)
|
|
{
|
|
delete this.et2;
|
|
if(this.sidebox)
|
|
this.sidebox.off();
|
|
delete this.sidebox;
|
|
if (!_app) delete app[this.appname];
|
|
let index = -1;
|
|
if((index = EgwApp._instances.indexOf(this)) >= 0)
|
|
{
|
|
EgwApp._instances.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(). Note that this can be called
|
|
* several times, with different et2 objects, as templates are loaded.
|
|
*
|
|
* @param {etemplate2} et2
|
|
* @param {string} name template name
|
|
*/
|
|
et2_ready(et2 : etemplate2, name : string)
|
|
{
|
|
if(this.et2 !== null)
|
|
{
|
|
egw.debug('log', "Changed et2 object");
|
|
}
|
|
this.et2 = et2.widgetContainer;
|
|
this._fix_iFrameScrolling();
|
|
if(this.egw && this.egw.is_popup())
|
|
{
|
|
this._set_Window_title();
|
|
}
|
|
|
|
// Highlights the favorite based on initial list state
|
|
this.highlight_favorite();
|
|
}
|
|
|
|
/**
|
|
* 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 {object|null} _links app => array of ids of linked entries
|
|
* or null, if not triggered on server-side, which adds that info
|
|
* @return {false|*} false to stop regular refresh, thought all observers are run
|
|
*/
|
|
observer(_msg, _app, _id, _type, _msg_type, _links)
|
|
{
|
|
|
|
}
|
|
|
|
/**
|
|
* Handle a push notification about entry changes from the websocket
|
|
*
|
|
* Gets called for data of all apps, but should only handle data of apps it displays,
|
|
* which is by default only its 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)
|
|
{
|
|
// don't care about other apps data, reimplement if your app does care eg. calendar
|
|
if(pushData.app !== this.appname)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// handle delete, for simple case of uid === "$app::$id"
|
|
if(pushData.type === 'delete' && egw.dataHasUID(this.uid(pushData)))
|
|
{
|
|
egw.refresh('', pushData.app, pushData.id, 'delete');
|
|
return;
|
|
}
|
|
|
|
// If we know about it and it's an update, just update.
|
|
// This must be before all ACL checks, as responsible might have changed and entry need to be removed
|
|
// (server responds then with null / no entry causing the entry to disappear)
|
|
if(pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData)) && this.et2)
|
|
{
|
|
return this.et2.getInstanceManager().refresh("", pushData.app, pushData.id, pushData.type);
|
|
}
|
|
|
|
// Check grants to see if we know we aren't supposed to show it
|
|
if(typeof this.push_grant_fields !== "undefined" && this.push_grant_fields.length > 0
|
|
&& !this._push_grant_check(pushData, this.push_grant_fields)
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Nextmatch does the hard part of updating. Try to find one.
|
|
let nm = <et2_nextmatch>this.et2?.getDOMWidgetById('nm');
|
|
if(!nm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Filter what's allowed down to those we can see / care about based on nm filters
|
|
if(typeof this.push_filter_fields !== "undefined" && this.push_filter_fields.length > 0 &&
|
|
!this._push_field_filter(pushData, nm, this.push_filter_fields)
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Pass actual refresh on to just nextmatch
|
|
nm.refresh(pushData.id, pushData.type);
|
|
}
|
|
|
|
/**
|
|
* Check grants to see if we can quickly tell if this entry is not for us
|
|
*
|
|
* Override this method if the app has non-standard access control.
|
|
*
|
|
* @param pushData
|
|
* @param grant_fields List of fields in pushData.acl with account IDs that might grant access eg: info_responsible
|
|
* @param appname Optional, to check against the grants for a different application. Defaults to this.appname.
|
|
*
|
|
* @return boolean Entry has ACL access
|
|
*/
|
|
_push_grant_check(pushData : PushData, grant_fields : string[], appname? : string) : boolean
|
|
{
|
|
let grants = egw.grants(appname || this.appname);
|
|
|
|
// No grants known
|
|
if(!grants)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// check user has a grant from owner or something
|
|
for(let i = 0; i < grant_fields.length; i++)
|
|
{
|
|
let grant_field = pushData.acl[grant_fields[i]];
|
|
if(["number", "string"].indexOf(typeof grant_field) >= 0 && grants[grant_field] !== 'undefined')
|
|
{
|
|
// ACL access
|
|
return true;
|
|
}
|
|
else if(!Object.keys(grants).filter(function(grant_account)
|
|
{
|
|
return grant_field.indexOf(grant_account) >= 0 ||
|
|
grant_field.indexOf(parseInt(grant_account)).length
|
|
}))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check pushData.acl values against a list of fields to see if we care about this entry based on current nextmatch
|
|
* filter values. This is not a definitive yes or no (the server will tell us when we ask), we just want to cheaply
|
|
* avoid a server call if we know it won't be in the list.
|
|
*
|
|
* @param pushData
|
|
* @param filter_fields List of filter field names eg: [owner, cat_id]
|
|
* @return boolean True if the nextmatch filters might include the entry, false if not
|
|
*/
|
|
_push_field_filter(pushData : PushData, nm : et2_nextmatch, filter_fields : string[]) : boolean
|
|
{
|
|
let filters = {};
|
|
for(let i = 0; i < filter_fields.length; i++)
|
|
{
|
|
filters[filter_fields[i]] = {
|
|
col: filter_fields[i],
|
|
filter_values: []
|
|
};
|
|
}
|
|
|
|
// Get current filter values
|
|
let value = nm.getValue();
|
|
if(!value || !value.col_filter) return false;
|
|
|
|
for(let field_filter of Object.values(filters))
|
|
{
|
|
let val = value.col_filter[field_filter.col];
|
|
if(val && (
|
|
typeof val == "string" && val.trim().length > 0
|
|
))
|
|
{
|
|
field_filter.filter_values.push(val);
|
|
}
|
|
else if(val && typeof val == "object" && !jQuery.isEmptyObject(val))
|
|
{
|
|
field_filter.filter_values = field_filter.filter_values.concat(Object.values(val))
|
|
}
|
|
}
|
|
|
|
// check filters against pushData.acl data
|
|
for(let field_filter of Object.values(filters))
|
|
{
|
|
// no filter set
|
|
if (field_filter.filter_values.length == 0) continue;
|
|
|
|
// acl value is a scalar (not array) --> check contained in filter
|
|
if(pushData.acl && typeof pushData.acl[field_filter.col] !== 'object')
|
|
{
|
|
if(field_filter.filter_values.indexOf(pushData.acl[field_filter.col]) < 0)
|
|
{
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
// acl value is an array (eg. tr_assigned) --> check intersection with filter
|
|
if(!field_filter.filter_values.filter(account => pushData.acl[field_filter.col].indexOf(account) >= 0).length)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get (possible) app-specific uid
|
|
*
|
|
* @param {object} pushData see push method for individual attributes
|
|
*/
|
|
uid(pushData)
|
|
{
|
|
return pushData.app + '::' + pushData.id;
|
|
}
|
|
|
|
/**
|
|
* Open an entry.
|
|
*
|
|
* Designed to be used with the action system as a callback
|
|
* eg: onExecute => app.<appname>.open
|
|
*
|
|
* @param _action
|
|
* @param _senders
|
|
*/
|
|
open(_action, _senders)
|
|
{
|
|
var id_app = _senders[0].id.split('::');
|
|
egw.open(id_app[1], this.appname);
|
|
}
|
|
|
|
_do_action(action_id : string, selected : [])
|
|
{
|
|
}
|
|
|
|
/**
|
|
* A generic method to action to server asynchronously
|
|
*
|
|
* Designed to be used with the action system as a callback.
|
|
* In the PHP side, set the action
|
|
* 'onExecute' => 'javaScript:app.<appname>.action', and
|
|
* implement _do_action(action_id, selected)
|
|
*
|
|
* @param {egwAction} _action
|
|
* @param {egwActionObject[]} _elems
|
|
*/
|
|
action(_action, _elems)
|
|
{
|
|
// let user confirm select-all
|
|
var select_all = _action.getManager().getActionById("select_all");
|
|
var confirm_msg = (_elems.length > 1 || select_all && select_all.checked) &&
|
|
typeof _action.data.confirm_multiple != 'undefined' ?
|
|
_action.data.confirm_multiple : _action.data.confirm;
|
|
|
|
if(typeof confirm_msg != 'undefined')
|
|
{
|
|
var that = this;
|
|
var action_id = _action.id;
|
|
Et2Dialog.show_dialog(function(button_id, value)
|
|
{
|
|
if(button_id != Et2Dialog.NO_BUTTON)
|
|
{
|
|
that._do_action(action_id, _elems);
|
|
}
|
|
}, confirm_msg, 'Confirmation required', null, Et2Dialog.BUTTONS_YES_NO, Et2Dialog.QUESTION_MESSAGE);
|
|
}
|
|
else if(typeof this._do_action == 'function')
|
|
{
|
|
this._do_action(_action.id, _elems);
|
|
}
|
|
else
|
|
{
|
|
// If this is a nextmatch action, do an ajax submit setting the action
|
|
var nm = null;
|
|
var action = _action;
|
|
while(nm == null && action.parent != null)
|
|
{
|
|
if(action.data.nextmatch) nm = action.data.nextmatch;
|
|
action = action.parent;
|
|
}
|
|
if(nm != null)
|
|
{
|
|
var value = {};
|
|
value[nm.options.settings.action_var] = _action.id;
|
|
nm.set_value(value);
|
|
nm.getInstanceManager().submit();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the application's state to the given state.
|
|
*
|
|
* While not pretending to implement the history API, it is patterned similarly
|
|
* @link http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html
|
|
*
|
|
* The default implementation works with the favorites to apply filters to a nextmatch.
|
|
*
|
|
*
|
|
* @param {{name: string, state: object}|string} state Object (or JSON string) for a state.
|
|
* Only state is required, and its contents are application specific.
|
|
* @param {string} template template name to check, instead of trying all templates of current app
|
|
* @return {boolean} false - Returns false to stop event propagation
|
|
*/
|
|
setState(state, template? : string) : string | false | void
|
|
{
|
|
// State should be an object, not a string, but we'll parse
|
|
if(typeof state == "string")
|
|
{
|
|
if(state.indexOf('{') != -1 || state == 'null')
|
|
{
|
|
state = JSON.parse(state);
|
|
}
|
|
}
|
|
if(typeof state != "object")
|
|
{
|
|
egw.debug('error', 'Unable to set state to %o, needs to be an object', state);
|
|
return;
|
|
}
|
|
if(state == null)
|
|
{
|
|
state = {};
|
|
}
|
|
|
|
// Check for egw.open() parameters
|
|
if(state.state && state.state.id && state.state.app)
|
|
{
|
|
return egw.open(state.state, undefined, undefined, {}, '_self');
|
|
}
|
|
|
|
// Try and find a nextmatch widget, and set its filters
|
|
var nextmatched = false;
|
|
var et2 = template ? etemplate2.getByTemplate(template) : etemplate2.getByApplication(this.appname);
|
|
for(var i = 0; i < et2.length; i++)
|
|
{
|
|
et2[i].widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
// Firefox has trouble with spaces in search
|
|
if(state.state && state.state.search) state.state.search = unescape(state.state.search);
|
|
|
|
// Apply
|
|
if(state.state && state.state.sort && state.state.sort.id)
|
|
{
|
|
_widget.sortBy(state.state.sort.id, state.state.sort.asc, false);
|
|
}
|
|
else
|
|
{
|
|
// Not using resetSort() to avoid the extra applyFilters() call
|
|
_widget.sortBy(undefined, undefined, false);
|
|
}
|
|
_widget.applyFilters(state.state || state.filter || {});
|
|
if(state.state && state.state.selectcols)
|
|
{
|
|
// Make sure it's a real array, not an object, then set cols
|
|
_widget.set_columns(jQuery.extend([], state.state.selectcols));
|
|
}
|
|
nextmatched = true;
|
|
}, this, et2_nextmatch);
|
|
if(nextmatched) return false;
|
|
}
|
|
|
|
// 'blank' is the special name for no filters, send that instead of the nice translated name
|
|
var safe_name = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state || state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_');
|
|
var url = '/' + this.appname + '/index.php';
|
|
|
|
// Try a redirect to list, if app defines a "list" value in registry
|
|
if(egw.link_get_registry(this.appname, 'list'))
|
|
{
|
|
url = egw.link('/index.php', jQuery.extend({'favorite': safe_name}, egw.link_get_registry(this.appname, 'list')));
|
|
}
|
|
// if no list try index value from application
|
|
else if(egw.app(this.appname)?.index)
|
|
{
|
|
url = egw.link('/index.php', 'menuaction=' + egw.app(this.appname).index + '&favorite=' + safe_name);
|
|
}
|
|
egw.open_link(url, undefined, undefined, this.appname);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the current state of the application for future restoration
|
|
*
|
|
* The state can be anything, as long as it's an object. The contents are
|
|
* application specific. The default implementation finds a nextmatch and
|
|
* returns its value.
|
|
* The return value of this function cannot be passed directly to setState(),
|
|
* since setState is expecting an additional wrapper, eg:
|
|
* {name: 'something', state: getState()}
|
|
*
|
|
* @return {object} Application specific map representing the current state
|
|
*/
|
|
getState() : { [propName : string] : any }
|
|
{
|
|
var state = {};
|
|
|
|
// Try and find a nextmatch widget, and set its filters
|
|
var et2 = etemplate2.getByApplication(this.appname);
|
|
for(var i = 0; i < et2.length; i++)
|
|
{
|
|
et2[i].widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
state = _widget.getValue();
|
|
}, this, et2_nextmatch);
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Function to load selected row from nm into a template view
|
|
*
|
|
* @param {object} _action
|
|
* @param {object} _senders
|
|
* @param {boolean} _noEdit defines whether to set edit button or not default is false
|
|
* @param {function} et2_callback function to run after et2 is loaded
|
|
*/
|
|
viewEntry(_action, _senders, _noEdit?, et2_callback?)
|
|
{
|
|
//full id in nm
|
|
var id = _senders[0].id;
|
|
// flag for edit button
|
|
var noEdit = _noEdit || false;
|
|
// nm row id
|
|
var rowID = '';
|
|
// content to feed to etemplate2
|
|
var content : any = {};
|
|
|
|
var self = this;
|
|
|
|
if(id)
|
|
{
|
|
var parts = id.split('::');
|
|
rowID = parts[1];
|
|
content = egw.dataGetUIDdata(id);
|
|
if (content.data) content = content.data;
|
|
}
|
|
|
|
// create a new app object with just constructors for our new etemplate2 object
|
|
var app = {classes: window.app.classes};
|
|
|
|
/* destroy generated etemplate for view mode in DOM*/
|
|
var destroy = function()
|
|
{
|
|
self.viewContainer.remove();
|
|
delete self.viewTemplate;
|
|
delete self.viewContainer;
|
|
delete self.et2_view;
|
|
// we need to reference back into parent context this
|
|
for(var v in self)
|
|
{
|
|
this[v] = self[v];
|
|
}
|
|
app = null;
|
|
};
|
|
|
|
// view container
|
|
this.viewContainer = jQuery(document.createElement('div'))
|
|
.addClass('et2_mobile_view')
|
|
.css({
|
|
"z-index": 102,
|
|
width: "100%",
|
|
height: "100%",
|
|
background: "white",
|
|
display: 'block',
|
|
position: 'absolute',
|
|
left: 0,
|
|
bottom: 0,
|
|
right: 0,
|
|
overflow: 'auto'
|
|
})
|
|
.attr('id', 'popupMainDiv')
|
|
.appendTo('body');
|
|
|
|
// close button
|
|
var close = jQuery(document.createElement('span'))
|
|
.addClass('egw_fw_mobile_popup_close loaded')
|
|
.click(function()
|
|
{
|
|
destroy.call(app[self.appname]);
|
|
//disable selected actions after close
|
|
egw_globalObjectManager.setAllSelected(false);
|
|
})
|
|
.appendTo(this.viewContainer);
|
|
if(!noEdit)
|
|
{
|
|
// edit button
|
|
var edit = jQuery(document.createElement('span'))
|
|
.addClass('mobile-view-editBtn')
|
|
.click(function()
|
|
{
|
|
egw.open(rowID, self.appname);
|
|
})
|
|
.appendTo(this.viewContainer);
|
|
}
|
|
// view template main container (content)
|
|
this.viewTemplate = jQuery(document.createElement('div'))
|
|
.attr('id', this.appname + '-view')
|
|
.addClass('et2_mobile-view-container popupMainDiv')
|
|
.appendTo(this.viewContainer);
|
|
|
|
var mobileViewTemplate = (_action.data.mobileViewTemplate || 'edit').split('?');
|
|
var templateName = mobileViewTemplate[0];
|
|
var templateTimestamp = mobileViewTemplate[1];
|
|
var templateURL = egw.webserverUrl + '/' + this.appname + '/templates/mobile/' + templateName + '.xet' + '?' + templateTimestamp;
|
|
|
|
var data = {
|
|
'content': content,
|
|
'readonlys': {'__ALL__': true, 'link_to': false},
|
|
'currentapp': this.appname,
|
|
'langRequire': this.et2.getArrayMgr('langRequire').data,
|
|
'sel_options': this.et2.getArrayMgr('sel_options').data,
|
|
'modifications': this.et2.getArrayMgr('modifications').data,
|
|
'validation_errors': this.et2.getArrayMgr('validation_errors').data
|
|
};
|
|
|
|
// etemplate2 object for view
|
|
this.et2_view = new etemplate2(this.viewTemplate[0], '');
|
|
framework.pushState('view');
|
|
if(templateName)
|
|
{
|
|
this.et2_view.load(this.appname + '.' + templateName, templateURL, data, typeof et2_callback == 'function' ? et2_callback : function() {}, app);
|
|
}
|
|
|
|
// define a global close function for view template
|
|
// in order to be able to destroy view on action
|
|
this.et2_view.close = destroy;
|
|
}
|
|
|
|
/**
|
|
* Opens _menuaction in an Et2Dialog
|
|
*
|
|
* Equivalent to egw.openDialog, though this one works in popups too.
|
|
*
|
|
* @param _menuaction
|
|
* @return Promise<Et2Dialog>
|
|
*/
|
|
openDialog(_menuaction : string) : Promise<Et2Dialog>
|
|
{
|
|
let resolver;
|
|
let rejector;
|
|
const dialog_promise = new Promise((resolve, reject) =>
|
|
{
|
|
resolver = value => resolve(value);
|
|
rejector = reason => reject(reason);
|
|
});
|
|
let request = this.egw.json(_menuaction.match(/^([^.:]+)/)[0] + '.jdots_framework.ajax_exec.template.' + _menuaction,
|
|
['index.php?menuaction=' + _menuaction, true], _response =>
|
|
{
|
|
if(Array.isArray(_response) && typeof _response[0] === 'string')
|
|
{
|
|
let dialog = jQuery(_response[0]).appendTo(document.body);
|
|
if(dialog.length > 0 && dialog.get(0))
|
|
{
|
|
resolver(dialog.get(0));
|
|
}
|
|
else
|
|
{
|
|
console.log("Unable to add dialog with dialogExec('" + _menuaction + "')", _response);
|
|
rejector(new Error("Unable to add dialog"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
console.log("Invalid response to dialogExec('" + _menuaction + "')", _response);
|
|
rejector(new Error("Invalid response to dialogExec('" + _menuaction + "')"));
|
|
}
|
|
}).sendRequest();
|
|
return dialog_promise;
|
|
}
|
|
|
|
/**
|
|
* Merge selected entries into template document
|
|
*
|
|
* @param {egwAction} _action
|
|
* @param {egwActionObject[]} _selected
|
|
*/
|
|
async mergeAction(_action : egwAction, _selected : egwActionObject[])
|
|
{
|
|
// Find what we need
|
|
let nm = null;
|
|
let action = _action;
|
|
let as_pdf = null;
|
|
|
|
// Find Select all
|
|
while(nm == null && action.parent != null)
|
|
{
|
|
if(action.data != null && action.data.nextmatch)
|
|
{
|
|
nm = action.data.nextmatch;
|
|
}
|
|
action = action.parent;
|
|
}
|
|
let all = nm?.getSelection().all || false;
|
|
|
|
// Get list of entry IDs
|
|
let ids = [];
|
|
for(let i = 0; !all && i < _selected.length; i++)
|
|
{
|
|
let split = _selected[i].id.split("::");
|
|
ids.push(split[1]);
|
|
}
|
|
let document = await this._getMergeDocument(nm?.getInstanceManager(), _action, _selected);
|
|
if(!document.documents || document.documents.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let vars = {
|
|
..._action.data.merge_data,
|
|
options: document.options,
|
|
select_all: all,
|
|
id: ids
|
|
};
|
|
if(document.options.link)
|
|
{
|
|
vars.options.app = this.appname;
|
|
}
|
|
// Just one file, an email - merge & edit or merge & send
|
|
if(document.documents.length == 1 && document.documents[0].mime == "message/rfc822")
|
|
{
|
|
vars.document = document.documents[0].path;
|
|
// Remove not applicable options
|
|
['pdf', 'download'].forEach(k => delete vars.options[k]);
|
|
return this._mergeEmail(_action.clone(), vars);
|
|
}
|
|
else
|
|
{
|
|
vars.document = document.documents.map(f => f.path);
|
|
}
|
|
if(document.documents.length == 1 && !document.options.individual && !document.options.download)
|
|
{
|
|
// Only 1 document, we can open it
|
|
vars.id = JSON.stringify(ids);
|
|
this.egw.open_link(this.egw.link('/index.php', vars), '_blank');
|
|
}
|
|
else
|
|
{
|
|
// Multiple files, or merging individually - will result in multiple documents that we can't just open
|
|
vars.menuaction = vars.menuaction.replace("merge_entries", "ajax_merge_multiple");
|
|
vars.menuaction += "&merge=" + vars.merge;
|
|
let mergedFiles = [];
|
|
|
|
// Check for an email template - all other files will be merged & attached
|
|
let email = document.documents.find(f => f.mime == "message/rfc822");
|
|
|
|
// Can we do this in one, or do we have to split it up for feedback?
|
|
if(!vars.options.individual && (!email || email && !all && ids.length == 1))
|
|
{
|
|
vars.options.open_email = !vars.options.download && typeof email != "undefined";
|
|
|
|
// Handle it all on the server in one request
|
|
this.egw.loading_prompt(vars.menuaction, true);
|
|
mergedFiles = await this.egw.request(vars.menuaction, [vars.id, vars.document, vars.options]);
|
|
this.egw.loading_prompt(vars.menuaction, false);
|
|
|
|
// One entry, email template selected - we can open that in the compose window
|
|
if(email)
|
|
{
|
|
debugger;
|
|
}
|
|
else
|
|
{
|
|
this.egw.message(mergedFiles, "success");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Merging documents, merge email, attach to email, send.
|
|
// Handled like this here so we can give feedback, server could do it all in one request
|
|
let idGroup = await new Promise<string[]>((resolve) =>
|
|
{
|
|
if(all)
|
|
{
|
|
fetchAll(ids, nm, idsArr => resolve(vars.options.individual ? idsArr : [idsArr]));
|
|
}
|
|
else
|
|
{
|
|
resolve(vars.options.individual ? ids : [ids])
|
|
}
|
|
});
|
|
Et2Dialog.long_task(null /*done*/, this.egw.lang("Merging"),
|
|
email ? this.egw.lang("Merging into %1 and sending", email.path) : this.egw.lang("Merging into %1", vars.document.join(", ")),
|
|
vars.menuaction,
|
|
idGroup.map((ids) => {return [Array.isArray(ids) ? ids : [ids], vars.document, vars.options];}), this.egw
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ask the user for a target document to merge into
|
|
*
|
|
* @returns {Promise<{document : string, pdf : boolean, mime : string}>}
|
|
* @protected
|
|
*/
|
|
|
|
protected _getMergeDocument(et2?, action? : EgwAction, selected? : EgwActionObject[]) : Promise<{
|
|
documents : { path : string; mime : string }[];
|
|
options : { [p : string] : string | boolean }
|
|
}>
|
|
{
|
|
let path = action?.data?.merge_data?.directory ?? "";
|
|
let dirPref = <string>this.egw.preference('document_dir', this.appname) ?? "";
|
|
let dirs = dirPref.split('/[,\s]+\//');
|
|
dirs.forEach((d, index) =>
|
|
{
|
|
if(index)
|
|
{
|
|
d = "/" + d;
|
|
}
|
|
});
|
|
let fileSelect = <Et2MergeDialog><unknown>loadWebComponent('et2-merge-dialog', {
|
|
application: this.appname,
|
|
path: dirs.pop() || ""
|
|
}, et2?.widgetContainer ?? null);
|
|
if(!et2)
|
|
{
|
|
document.body.append(fileSelect);
|
|
}
|
|
// Customize dialog
|
|
fileSelect.updateComplete.then(() =>
|
|
{
|
|
// Start details open when you have multiple selected
|
|
// @ts-ignore
|
|
(<Et2Details>fileSelect.shadowRoot.querySelector('et2-details')).open = selected.length > 1;
|
|
|
|
// Disable individual when only one entry is selected
|
|
// @ts-ignore
|
|
(<Et2Checkbox>fileSelect.shadowRoot.querySelector("et2-details > [id='individual']")).disabled = selected.length == 1;
|
|
});
|
|
// Remove when done
|
|
fileSelect.getComplete().then(() => {fileSelect.remove();});
|
|
|
|
return fileSelect.getComplete();
|
|
}
|
|
|
|
/**
|
|
* Merge into an email, then open it in compose for a single, send directly for multiple
|
|
*
|
|
* @param {object} data
|
|
* @protected
|
|
*/
|
|
protected _mergeEmail(action, data : object)
|
|
{
|
|
const ids = data['id'];
|
|
// egw.open() used if only 1 row selected
|
|
data['egw_open'] = 'edit-mail--';
|
|
data['target'] = 'compose_' + data.document;
|
|
|
|
// long_task runs menuaction once for each selected row
|
|
data['nm_action'] = 'long_task';
|
|
data['popup'] = this.egw.link_get_registry('mail', 'edit_popup');
|
|
data['message'] = this.egw.lang('insert in %1', data.document);
|
|
|
|
data['menuaction'] = 'mail.mail_compose.ajax_merge';
|
|
action.data = data;
|
|
|
|
if(data['select_all'] || ids.length > 1)
|
|
{
|
|
data['menuaction'] += "&document=" + data.document + "&merge=" + data.merge;
|
|
nm_action(action, null, data['target'], {all: data['select_all'], ids: ids});
|
|
}
|
|
else
|
|
{
|
|
this.egw.open(ids.pop(), 'mail', 'edit', {
|
|
from: 'merge',
|
|
document: data.document,
|
|
merge: data.merge
|
|
}, data['target']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes actions and handlers on sidebox (delete)
|
|
*
|
|
* @param {jQuery} sidebox jQuery of DOM node
|
|
*/
|
|
_init_sidebox(sidebox)
|
|
{
|
|
|
|
if(sidebox.length)
|
|
{
|
|
var self = this;
|
|
if(this.sidebox) this.sidebox.off();
|
|
this.sidebox = sidebox;
|
|
sidebox
|
|
.off()
|
|
// removed .on("mouse(enter|leave)" (wrapping trash icon), as it stalls delete in IE11
|
|
.on("click.sidebox", "div.ui-icon-trash", this, this.delete_favorite)
|
|
// need to install a favorite handler, as we switch original one off with .off()
|
|
.on('click.sidebox', 'li[data-id]', this, function(event)
|
|
{
|
|
var li = jQuery(this);
|
|
li.siblings().removeClass('ui-state-highlight');
|
|
|
|
var state = {};
|
|
var pref = egw.preference('favorite_' + this.dataset.id, self.appname);
|
|
if(pref)
|
|
{
|
|
// Extend, to prevent changing the preference by reference
|
|
jQuery.extend(true, state, pref);
|
|
}
|
|
if(this.dataset.id != 'add')
|
|
{
|
|
// check for mobile framework and close the sidebox/-bar
|
|
if (typeof framework.toggleMenu === 'function')
|
|
{
|
|
framework.toggleMenu('on');
|
|
}
|
|
|
|
event.stopImmediatePropagation();
|
|
self.setState.call(self, state);
|
|
return false;
|
|
}
|
|
})
|
|
.addClass("ui-helper-clearfix");
|
|
|
|
let el = document.getElementById('favorite_sidebox_' + this.appname)?.getElementsByTagName('ul')[0];
|
|
if(el && el instanceof HTMLElement)
|
|
{
|
|
let sortablejs = Sortable.create(el, {
|
|
ghostClass: 'ui-fav-sortable-placeholder',
|
|
draggable: 'li:not([data-id$="add"])',
|
|
delay: 25,
|
|
dataIdAttr: 'data-id',
|
|
onSort: function(event)
|
|
{
|
|
let favSortedList = sortablejs.toArray();
|
|
self.egw.set_preference(self.appname, 'fav_sort_pref', favSortedList);
|
|
self._refresh_fav_nm();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bind favorite de-select
|
|
var egw_fw = egw_getFramework();
|
|
if(egw_fw && egw_fw.applications[this.appname] && egw_fw.applications[this.appname].browser
|
|
&& egw_fw.applications[this.appname].browser.baseDiv)
|
|
{
|
|
jQuery(egw_fw.applications[this.appname].browser.baseDiv)
|
|
.off('.sidebox')
|
|
.on('change.sidebox', function()
|
|
{
|
|
self.highlight_favorite();
|
|
});
|
|
egw_fw.applications[this.appname].browser.baseDiv.addEventListener("change", (e) =>
|
|
{
|
|
if(e.target.localName == "et2-favorites")
|
|
{
|
|
sidebox[0].querySelectorAll("li:not([data-id='add']) > a > div:first-child").forEach(f =>
|
|
{
|
|
f.classList.add("sideboxstar");
|
|
f.classList.remove('ui-icon', 'ui-heart');
|
|
});
|
|
const new_pref = sidebox[0].querySelector("li[data-id='" + e.target.preferred + "'] > a > div:first-child");
|
|
if(new_pref)
|
|
{
|
|
new_pref.classList.add('ui-icon', 'ui-icon-heart');
|
|
new_pref.classList.remove("sideboxstar");
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add a new favorite
|
|
*
|
|
* Fetches the current state from the application, then opens a dialog to get the
|
|
* name and other settings. If user proceeds, the favorite is saved, and if possible
|
|
* the sidebox is directly updated to include the new favorite
|
|
*
|
|
* @param {object} [state] State settings to be merged into the application state
|
|
*/
|
|
add_favorite(state)
|
|
{
|
|
// Get current state
|
|
// Make sure it's an object - deep copy to prevent references in sub-objects (col_filters)
|
|
state = {...this.getState(), ...(state || {})};
|
|
|
|
this._create_favorite_popup(state);
|
|
|
|
// Stop the normal bubbling if this is called on click
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update favorite items in nm fav. menu
|
|
*
|
|
*/
|
|
_refresh_fav_nm()
|
|
{
|
|
var self = this;
|
|
|
|
if(etemplate2 && etemplate2.getByApplication)
|
|
{
|
|
var et2 = etemplate2.getByApplication(self.appname);
|
|
for(var i = 0; i < et2.length; i++)
|
|
{
|
|
et2[i].widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
_widget.stored_filters = _widget.load_favorites(self.appname);
|
|
}, self, Et2Favorites);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new Error("_refresh_fav_nm():Either et2 is not ready/ not there yet. Make sure that etemplate2 is ready before call this method.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the "Add new" popup dialog
|
|
*/
|
|
_create_favorite_popup(state)
|
|
{
|
|
const favorite_prefix = 'favorite_';
|
|
|
|
// Clear old, if existing
|
|
if(this.favorite_popup && this.favorite_popup.group)
|
|
{
|
|
this.favorite_popup.group.destroy();
|
|
delete this.favorite_popup;
|
|
}
|
|
|
|
// Add some controls if user is an admin
|
|
const apps = this.egw.user('apps');
|
|
const is_admin = (typeof apps['admin'] != "undefined");
|
|
|
|
// Setup data
|
|
let data = {
|
|
content: {
|
|
state: state || [],
|
|
current_filters: []
|
|
},
|
|
readonlys: {
|
|
group: !is_admin
|
|
}
|
|
};
|
|
|
|
|
|
// Show current set filters (more for debug than user)
|
|
let filter_list = [];
|
|
let add_to_popup = function(arr, inset = "")
|
|
{
|
|
Object.keys(arr).forEach((index) =>
|
|
{
|
|
let filter = arr[index];
|
|
filter_list.push({
|
|
label: inset + index.toString(),
|
|
value: (typeof filter != "object" ? "" + filter : "")
|
|
});
|
|
if(typeof filter == "object" && filter != null)
|
|
{
|
|
add_to_popup(filter, inset + " ");
|
|
}
|
|
});
|
|
};
|
|
add_to_popup(data.content.state);
|
|
data.content.current_filters = filter_list;
|
|
|
|
let save_callback = async(button, value) =>
|
|
{
|
|
if(button !== Et2Dialog.OK_BUTTON)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(value.name)
|
|
{
|
|
// Add to the list
|
|
value.name = (<string>value.name).replace(/(<([^>]+)>)/ig, "");
|
|
let safe_name = (<string>value.name).replace(/[^A-Za-z0-9-_]/g, "_");
|
|
if(safe_name != value.name)
|
|
{
|
|
// Check if the label matches an existing preference, consider it an update
|
|
let existing = this.egw.preference(favorite_prefix + safe_name, this.appname);
|
|
if(existing && existing.name !== value.name)
|
|
{
|
|
// Name mis-match, this is a new favorite with the same safe name
|
|
safe_name += "_" + await this.egw.hashString(value.name);
|
|
}
|
|
}
|
|
let favorite = {
|
|
name: value.name,
|
|
group: value.group || false,
|
|
state: data.content.state
|
|
};
|
|
|
|
let favorite_pref = favorite_prefix + safe_name;
|
|
|
|
// Save to preferences
|
|
if(typeof value.group != "undefined" && value.group != '')
|
|
{
|
|
// Admin stuff - save preference server side
|
|
this.egw.jsonq('EGroupware\\Api\\Framework::ajax_set_favorite',
|
|
[
|
|
this.appname,
|
|
value.name,
|
|
"add",
|
|
value.group,
|
|
data.content.state
|
|
]
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// Normal user - just save to preferences client side
|
|
this.egw.set_preference(this.appname, favorite_pref, favorite);
|
|
}
|
|
|
|
// Trigger event so widgets can update
|
|
document.dispatchEvent(new CustomEvent("preferenceChange", {
|
|
bubbles: true,
|
|
detail: {
|
|
application: this.appname,
|
|
preference: favorite_pref
|
|
}
|
|
}));
|
|
|
|
// Add to list immediately
|
|
if(this.sidebox)
|
|
{
|
|
// Remove any existing with that name
|
|
this.sidebox.get(0).querySelectorAll('[data-id="' + safe_name + '"]').forEach(e => e.remove());
|
|
|
|
// Create new item
|
|
var html = "<li data-id='" + safe_name + "' data-group='" + favorite.group + "' class='ui-menu-item' role='menuitem'>\n";
|
|
var href = 'javascript:app.' + this.appname + '.setState(' + JSON.stringify(favorite) + ');';
|
|
html += "<a href='" + href + "' class='ui-corner-all' tabindex='-1'>";
|
|
html += "<div class='" + 'sideboxstar' + "'></div>" +
|
|
favorite.name;
|
|
html += "<div class='ui-icon ui-icon-trash' title='" + this.egw.lang('Delete') + "'/>";
|
|
html += "</a></li>\n";
|
|
jQuery(html).insertBefore(jQuery('li', this.sidebox).last());
|
|
this._init_sidebox(this.sidebox);
|
|
}
|
|
|
|
// Try to update nextmatch favorites too
|
|
this._refresh_fav_nm();
|
|
}
|
|
};
|
|
|
|
|
|
// Create popup
|
|
this.favorite_popup = new Et2Dialog(this.egw);
|
|
this.favorite_popup.transformAttributes({
|
|
callback: save_callback,
|
|
title: this.egw.lang("New favorite"),
|
|
buttons: Et2Dialog.BUTTONS_OK_CANCEL,
|
|
width: 400,
|
|
value: data,
|
|
template: this.egw.webserverUrl + '/api/templates/default/add_favorite.xet'
|
|
});
|
|
document.body.appendChild(this.favorite_popup);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Delete a favorite from the list and update preferences
|
|
* Registered as a handler on the delete icons
|
|
*
|
|
* @param {jQuery.event} event event object
|
|
*/
|
|
delete_favorite(event)
|
|
{
|
|
// Don't do the menu
|
|
event.stopImmediatePropagation();
|
|
|
|
var app = event.data;
|
|
var id = jQuery(this).parentsUntil('li').parent().attr("data-id");
|
|
var group = jQuery(this).parentsUntil('li').parent().attr("data-group") || '';
|
|
var line = jQuery('li[data-id="' + id + '"]', app.sidebox);
|
|
var name = line.first().text();
|
|
var trash = this;
|
|
line.addClass('loading');
|
|
|
|
// Make sure first
|
|
var do_delete = function(button_id)
|
|
{
|
|
if(button_id != Et2Dialog.YES_BUTTON)
|
|
{
|
|
line.removeClass('loading');
|
|
return;
|
|
}
|
|
|
|
// Hide the trash
|
|
jQuery(trash).hide();
|
|
|
|
// Delete preference server side
|
|
var request = egw.json("EGroupware\\Api\\Framework::ajax_set_favorite",
|
|
[app.appname, id, "delete", group, ''],
|
|
function(result)
|
|
{
|
|
// Got the full response from callback, which we don't want
|
|
if(result.type) return;
|
|
|
|
if(result && typeof result == 'boolean')
|
|
{
|
|
// Remove line from list
|
|
line.slideUp("slow", function() { });
|
|
|
|
app._refresh_fav_nm();
|
|
}
|
|
else
|
|
{
|
|
// Something went wrong server side
|
|
line.removeClass('loading').addClass('ui-state-error');
|
|
}
|
|
},
|
|
jQuery(trash).parentsUntil("li").parent(),
|
|
true,
|
|
jQuery(trash).parentsUntil("li").parent()
|
|
);
|
|
request.sendRequest(true);
|
|
};
|
|
Et2Dialog.show_dialog(do_delete, (egw.lang("Delete") + " " + name + "?"),
|
|
"Delete", null, Et2Dialog.BUTTONS_YES_NO, Et2Dialog.QUESTION_MESSAGE);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Mark the favorite closest matching the current state
|
|
*
|
|
* Closest matching takes into account not set values, so we pick the favorite
|
|
* with the most matching values without a value that differs.
|
|
*/
|
|
highlight_favorite()
|
|
{
|
|
if(!this.sidebox) return;
|
|
|
|
var state = this.getState();
|
|
var best_match : any = false;
|
|
var best_count = 0;
|
|
var self = this;
|
|
|
|
jQuery('li[data-id]', this.sidebox).removeClass('ui-state-highlight');
|
|
|
|
jQuery('li[data-id]', this.sidebox).each(function(i, href)
|
|
{
|
|
var favorite : any = {};
|
|
if(this.dataset.id && egw.preference('favorite_' + this.dataset.id, self.appname))
|
|
{
|
|
favorite = egw.preference('favorite_' + this.dataset.id, self.appname);
|
|
}
|
|
if(!favorite || jQuery.isEmptyObject(favorite)) return;
|
|
|
|
// Handle old style by making it like new style
|
|
if(favorite.filter && !favorite.state)
|
|
{
|
|
favorite.state = favorite.filter;
|
|
}
|
|
|
|
var match_count = 0;
|
|
var extra_keys = Object.keys(favorite.state);
|
|
for(var state_key in state)
|
|
{
|
|
extra_keys.splice(extra_keys.indexOf(state_key), 1);
|
|
if(typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined' && typeof favorite.state[state_key] != 'undefined' && (state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key]))
|
|
{
|
|
match_count++;
|
|
}
|
|
else if(state_key == 'selectcols' && typeof favorite.state.selectcols == "undefined")
|
|
{
|
|
// Skip, not set in favorite
|
|
}
|
|
else if(typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object'
|
|
&& typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object')
|
|
{
|
|
if((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined')
|
|
&& (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length))
|
|
{
|
|
// State or favorite has a length, but the other does not
|
|
if((state[state_key].length === 0 || Object.keys(state[state_key]).length === 0) &&
|
|
(favorite.state[state_key].length == 0 || Object.keys(favorite.state[state_key]).length === 0))
|
|
{
|
|
// Just missing, or one is an array and the other is an object
|
|
continue;
|
|
}
|
|
// One has a value and the other doesn't, no match
|
|
return;
|
|
}
|
|
else if(state[state_key].length !== 'undefined' && typeof favorite.state[state_key].length !== 'undefined' &&
|
|
state[state_key].length === 0 && favorite.state[state_key].length === 0)
|
|
{
|
|
// Both set, but both empty
|
|
match_count++;
|
|
continue;
|
|
}
|
|
// Consider sub-objects (column filters) individually
|
|
for(var sub_key in state[state_key])
|
|
{
|
|
if(state[state_key][sub_key] == favorite.state[state_key][sub_key] || !state[state_key][sub_key] && !favorite.state[state_key][sub_key])
|
|
{
|
|
match_count++;
|
|
}
|
|
else if(state[state_key][sub_key] && favorite.state[state_key][sub_key] &&
|
|
typeof state[state_key][sub_key] === 'object' && typeof favorite.state[state_key][sub_key] === 'object')
|
|
{
|
|
// Too deep to keep going, just string compare for perfect match
|
|
if(JSON.stringify(state[state_key][sub_key]) === JSON.stringify(favorite.state[state_key][sub_key]))
|
|
{
|
|
match_count++;
|
|
}
|
|
}
|
|
else if(typeof state[state_key][sub_key] !== 'undefined' && state[state_key][sub_key] != favorite.state[state_key][sub_key])
|
|
{
|
|
// Different values, do not match
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if(typeof state[state_key] !== 'undefined'
|
|
&& typeof favorite.state != 'undefined' && typeof favorite.state[state_key] !== 'undefined'
|
|
&& state[state_key] != favorite.state[state_key])
|
|
{
|
|
// Different values, do not match
|
|
return;
|
|
}
|
|
}
|
|
// Check for anything set that the current one does not have
|
|
for(var i = 0; i < extra_keys.length; i++)
|
|
{
|
|
if(favorite.state[extra_keys[i]]) return;
|
|
}
|
|
if(match_count > best_count)
|
|
{
|
|
best_match = this.dataset.id;
|
|
best_count = match_count;
|
|
}
|
|
});
|
|
if(best_match)
|
|
{
|
|
jQuery('li[data-id="' + best_match + '"]', this.sidebox).addClass('ui-state-highlight');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fix scrolling iframe browsed by iPhone/iPod/iPad touch devices
|
|
*/
|
|
_fix_iFrameScrolling()
|
|
{
|
|
if(/iPhone|iPod|iPad/.test(navigator.userAgent))
|
|
{
|
|
jQuery("iframe").on({
|
|
load: function()
|
|
{
|
|
var body = this.contentWindow.document.body;
|
|
|
|
var div = jQuery(document.createElement("div"))
|
|
.css({
|
|
'height': jQuery(this.parentNode).height(),
|
|
'width': jQuery(this.parentNode).width(),
|
|
'overflow': 'scroll'
|
|
});
|
|
while(body.firstChild)
|
|
{
|
|
div.append(body.firstChild);
|
|
}
|
|
jQuery(body).append(div);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set document title, uses getWindowTitle to get the correct title,
|
|
* otherwise set it with uniqueID as default title
|
|
*/
|
|
_set_Window_title()
|
|
{
|
|
var title = this.getWindowTitle();
|
|
if(title)
|
|
{
|
|
document.title = this.et2._inst.uniqueId + ": " + title;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Window title getter function in order to set the window title
|
|
* this can be overridden on each application app.js file to customize the title value
|
|
*
|
|
* @returns {string} window title
|
|
*/
|
|
getWindowTitle()
|
|
{
|
|
var titleWidget = <et2_valueWidget>this.et2.getWidgetById('title');
|
|
if(titleWidget)
|
|
{
|
|
return titleWidget.get_value ? titleWidget.get_value() : (titleWidget.value || "");
|
|
}
|
|
else
|
|
{
|
|
return this.et2._inst.uniqueId;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for drag and drop when dragging nextmatch rows from mail app
|
|
* and dropped on a row in the current application. We copy the mail into
|
|
* the filemanager to link it since we can't link directly.
|
|
*
|
|
* This doesn't happen automatically. Each application must indicate that
|
|
* it will accept dropped mail by its nextmatch actions:
|
|
*
|
|
* $actions['info_drop_mail'] = array(
|
|
* 'type' => 'drop',
|
|
* 'acceptedTypes' => 'mail',
|
|
* 'onExecute' => 'javaScript:app.infolog.handle_dropped_mail',
|
|
* 'hideOnDisabled' => true
|
|
* );
|
|
*
|
|
* This action, when defined, will not affect the automatic linking between
|
|
* normal applications.
|
|
*
|
|
* @param {egwAction} _action
|
|
* @param {egwActionObject[]} _selected Dragged mail rows
|
|
* @param {egwActionObject} _target Current application's nextmatch row the mail was dropped on
|
|
*/
|
|
handle_dropped_mail(_action, _selected, _target)
|
|
{
|
|
/**
|
|
* Mail doesn't support link system, so we copy it to VFS
|
|
*/
|
|
var ids = _target.id.split("::");
|
|
if(ids.length != 2 || ids[0] == 'mail') return;
|
|
|
|
var vfs_path = "/apps/" + ids[0] + "/" + ids[1];
|
|
var mail_ids = [];
|
|
|
|
for(var i = 0; i < _selected.length; i++)
|
|
{
|
|
mail_ids.push(_selected[i].id);
|
|
}
|
|
if(mail_ids.length)
|
|
{
|
|
egw.message(egw.lang("Please wait..."));
|
|
this.egw.json('filemanager.filemanager_ui.ajax_action', ['mail', mail_ids, vfs_path], function(data)
|
|
{
|
|
// Trigger an update (minimal, no sorting changes) to display the new link
|
|
egw.refresh(data.msg || '', ids[0], ids[1], 'update');
|
|
}).sendRequest(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it
|
|
*
|
|
* @param {function} _callback called if and only if mailvelope is available (context is this!)
|
|
*/
|
|
mailvelopeAvailable(_callback)
|
|
{
|
|
var self = this;
|
|
var callback = jQuery.proxy(_callback, this);
|
|
|
|
if(typeof mailvelope !== 'undefined')
|
|
{
|
|
this.mailvelopeOpenKeyring().then(callback);
|
|
}
|
|
else
|
|
{
|
|
jQuery(window).on('mailvelope', function()
|
|
{
|
|
self.mailvelopeOpenKeyring().then(callback);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* mailvelope object contains SyncHandlers
|
|
*
|
|
* @property {function} descriptionuploadSync function called by Mailvelope to upload encrypted private key backup
|
|
* @property {function} downloadSync function called by Mailvelope to download encrypted private key backup
|
|
* @property {function} backup function called by Mailvelope to upload a public keyring backup
|
|
* @property {function} restore function called by Mailvelope to restore a public keyring backup
|
|
*/
|
|
private mailvelopeSyncHandler()
|
|
{
|
|
return {
|
|
/**
|
|
* function called by Mailvelope to upload a public keyring
|
|
* @param {UploadSyncHandler} _uploadObj
|
|
* @property {string} etag entity tag for the uploaded encrypted keyring, or null if initial upload
|
|
* @property {AsciiArmored} keyringMsg encrypted keyring as PGP armored message
|
|
* @returns {Promise.<UploadSyncReply, Error>}
|
|
*/
|
|
uploadSync: function(_uploadObj)
|
|
{
|
|
return new Promise(function(_resolve, _reject) {});
|
|
},
|
|
|
|
/**
|
|
* function called by Mailvelope to download a public keyring
|
|
*
|
|
* @param {object} _downloadObj
|
|
* @property {string} etag entity tag for the current local keyring, or null if no local eTag
|
|
* @returns {Promise.<DownloadSyncReply, Error>}
|
|
*/
|
|
downloadSync: function(_downloadObj)
|
|
{
|
|
return new Promise(function(_resolve, _reject) {});
|
|
},
|
|
|
|
/**
|
|
* function called by Mailvelope to upload an encrypted private key backup
|
|
*
|
|
* @param {BackupSyncPacket} _backup
|
|
* @property {AsciiArmored} backup an encrypted private key as PGP armored message
|
|
* @returns {Promise.<undefined, Error>}
|
|
*/
|
|
backup: function(_backup)
|
|
{
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
// Store backup sync packet into .PGP-Key-Backup file in user directory
|
|
jQuery.ajax({
|
|
method: 'PUT',
|
|
url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify(_backup),
|
|
success: function()
|
|
{
|
|
_resolve(_backup);
|
|
},
|
|
error: function(_err)
|
|
{
|
|
_reject(_err);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* function called by Mailvelope to restore an encrypted private key backup
|
|
*
|
|
* @returns {Promise.<BackupSyncPacket, Error>}
|
|
* @todo
|
|
*/
|
|
restore: function()
|
|
{
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
var resolve = _resolve;
|
|
var reject = _reject;
|
|
jQuery.ajax({
|
|
url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup',
|
|
method: 'GET',
|
|
success: function(_backup)
|
|
{
|
|
resolve(JSON.parse(_backup));
|
|
egw.message('Your key has been restored successfully.');
|
|
},
|
|
error: function(_err)
|
|
{
|
|
//Try with old back file name
|
|
if(_err.status == 404)
|
|
{
|
|
jQuery.ajax({
|
|
method: 'GET',
|
|
url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PK_PGP',
|
|
success: function(_backup)
|
|
{
|
|
resolve(JSON.parse(_backup));
|
|
egw.message('Your key has been restored successfully.');
|
|
},
|
|
error: function(_err)
|
|
{
|
|
_reject(_err);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
_reject(_err);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Function for backup file operations
|
|
*
|
|
* @param {type} _url Url of the backup file
|
|
* @param {type} _cmd command to operate
|
|
* - PUT: to store backup file
|
|
* - GET: to read backup file
|
|
* - DELETE: to delete backup file
|
|
*
|
|
* @param {type} _successCallback function called when the operation is successful
|
|
* @param {type} _errorCallback function called when the operation fails
|
|
* @param {type} _data data which needs to be stored in file via PUT command
|
|
*/
|
|
_mailvelopeBackupFileOperator(_url, _cmd, _successCallback, _errorCallback, _data?)
|
|
{
|
|
var ajaxObj = {
|
|
url: _url || egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup',
|
|
method: _cmd,
|
|
success: _successCallback,
|
|
error: _errorCallback
|
|
};
|
|
switch(_cmd)
|
|
{
|
|
case 'PUT':
|
|
jQuery.extend({}, ajaxObj, {
|
|
data: JSON.stringify(_data),
|
|
contentType: 'application/json'
|
|
});
|
|
break;
|
|
case 'GET':
|
|
jQuery.extend({}, ajaxObj, {
|
|
dataType: 'json'
|
|
});
|
|
break;
|
|
case 'DELETE':
|
|
break;
|
|
}
|
|
jQuery.ajax(ajaxObj);
|
|
}
|
|
|
|
/**
|
|
* Create backup dialog
|
|
* @param {string} _selector DOM selector to attach backupDialog
|
|
* @param {boolean} _initSetup determine whether it's an initialization backup or restore backup
|
|
*
|
|
* @returns {Promise.<backupPopupId, Error>}
|
|
*/
|
|
mailvelopeCreateBackupDialog(_selector?, _initSetup?)
|
|
{
|
|
var self = this;
|
|
var selector = _selector || 'body';
|
|
var initSetup = _initSetup;
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove();
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
var resolve = _resolve;
|
|
var reject = _reject;
|
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring : any)
|
|
{
|
|
_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj);
|
|
|
|
var options = {
|
|
initialSetup: initSetup
|
|
};
|
|
_keyring.createKeyBackupContainer(selector, options).then(function(_popupId)
|
|
{
|
|
var $backup_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]');
|
|
$backup_selector.css({position: 'absolute', "z-index": 1});
|
|
_popupId.isReady().then(function(result)
|
|
{
|
|
egw.message('Your key has been backedup into .PGP-Key-Backup successfully.');
|
|
jQuery(selector).empty();
|
|
});
|
|
resolve(_popupId);
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete backup key from filesystem
|
|
*/
|
|
mailvelopeDeleteBackup()
|
|
{
|
|
var self = this;
|
|
Et2Dialog.show_dialog(function(_button_id)
|
|
{
|
|
if(_button_id == Et2Dialog.YES_BUTTON)
|
|
{
|
|
self._mailvelopeBackupFileOperator(undefined, 'DELETE', function()
|
|
{
|
|
self.egw.message(self.egw.lang('The backup key has been deleted.'));
|
|
}, function(_err)
|
|
{
|
|
self.egw.message(self.egw.lang('Was not able to delete the backup key because %1', _err));
|
|
});
|
|
}
|
|
},
|
|
'Are you sure, you would like to delete the backup key?',
|
|
'Delete backup key',
|
|
{}, Et2Dialog.BUTTONS_YES_NO_CANCEL, Et2Dialog.QUESTION_MESSAGE, undefined, self.egw);
|
|
}
|
|
|
|
/**
|
|
* Create mailvelope restore dialog
|
|
* @param {string} _selector DOM selector to attach restorDialog
|
|
* @param {boolean} _restorePassword if true, will restore key password too
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
mailvelopeCreateRestoreDialog(_selector, _restorePassword)
|
|
{
|
|
var self = this;
|
|
var restorePassword = _restorePassword;
|
|
var selector = _selector || 'body';
|
|
//Clear the
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove();
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
var resolve = _resolve;
|
|
var reject = _reject;
|
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring)
|
|
{
|
|
_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj);
|
|
|
|
var options = {
|
|
restorePassword: restorePassword
|
|
};
|
|
_keyring.restoreBackupContainer(selector, options).then(function(_restoreId)
|
|
{
|
|
var $restore_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]');
|
|
$restore_selector.css({position: 'absolute', "z-index": 1});
|
|
resolve(_restoreId);
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a dialog to show all backup/restore options
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
mailvelopeCreateBackupRestoreDialog()
|
|
{
|
|
var self = this;
|
|
var appname = egw.app_name();
|
|
var menu = [
|
|
// Header row should be empty item 0
|
|
{},
|
|
// Restore Keyring item 1
|
|
{
|
|
label: "Restore key",
|
|
image: "lock",
|
|
onclick: "app." + appname + ".mailvelopeCreateRestoreDialog('#_mvelo')"
|
|
},
|
|
// Restore pass phrase item 2
|
|
{
|
|
label: "Restore password",
|
|
image: "password",
|
|
onclick: "app." + appname + ".mailvelopeCreateRestoreDialog('#_mvelo', true)"
|
|
},
|
|
// Delete backup Key item 3
|
|
{label: "Delete backup", image: "delete", onclick: "app." + appname + ".mailvelopeDeleteBackup"},
|
|
// Backup Key item 4
|
|
{
|
|
label: "Backup Key",
|
|
image: "save",
|
|
onclick: "app." + appname + ".mailvelopeCreateBackupDialog('#_mvelo', false)"
|
|
}
|
|
];
|
|
|
|
var dialog = function(_content, _callback?)
|
|
{
|
|
let dialog = new Et2Dialog(this.egw);
|
|
dialog.transformAttributes({
|
|
callback: function(_button_id, _value)
|
|
{
|
|
if(typeof _callback == "function")
|
|
{
|
|
_callback.call(this, _button_id, _value.value);
|
|
}
|
|
},
|
|
title: egw.lang('Backup/Restore'),
|
|
buttons: [{
|
|
"button_id": 'close',
|
|
"label": egw.lang('Close'),
|
|
id: 'dialog[close]',
|
|
image: 'cancelled',
|
|
"default": true
|
|
}],
|
|
value: {
|
|
content: {
|
|
menu: _content
|
|
}
|
|
},
|
|
template: egw.webserverUrl + '/api/templates/default/pgp_backup_restore.xet',
|
|
class: "pgp_backup_restore",
|
|
isModal: true
|
|
});
|
|
return dialog;
|
|
};
|
|
if(typeof mailvelope != 'undefined')
|
|
{
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring)
|
|
{
|
|
self._mailvelopeBackupFileOperator(undefined, 'GET', function(_data)
|
|
{
|
|
dialog(menu);
|
|
},
|
|
function()
|
|
{
|
|
// Remove delete item
|
|
menu.splice(3, 1);
|
|
menu[3]['onclick'] = "app." + appname + ".mailvelopeCreateBackupDialog('#_mvelo', true)";
|
|
dialog(menu);
|
|
});
|
|
},
|
|
function()
|
|
{
|
|
mailvelope.createKeyring('egroupware').then(function() {dialog(menu);});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
this.mailvelopeInstallationOffer();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a dialog and offers installation option for installing mailvelope plugin
|
|
* plus it offers a video tutorials to get the user morte familiar with mailvelope
|
|
*/
|
|
mailvelopeInstallationOffer()
|
|
{
|
|
var buttons = [
|
|
{"text": egw.lang('Install'), id: 'install', image: 'check', "default": true},
|
|
{"text": egw.lang('Close'), id: 'close', image: 'cancelled'}
|
|
];
|
|
var dialog = function(_content, _callback)
|
|
{
|
|
return et2_createWidget("dialog", {
|
|
callback: function(_button_id, _value)
|
|
{
|
|
if(typeof _callback == "function")
|
|
{
|
|
_callback.call(this, _button_id, _value.value);
|
|
}
|
|
},
|
|
title: egw.lang('PGP Encryption Installation'),
|
|
buttons: buttons,
|
|
dialog_type: 'info',
|
|
value: {
|
|
content: _content
|
|
},
|
|
template: egw.webserverUrl + '/api/templates/default/pgp_installation.xet',
|
|
class: "pgp_installation",
|
|
isModal: true
|
|
//resizable:false,
|
|
});
|
|
};
|
|
var content = [
|
|
// Header row should be empty item 0
|
|
{},
|
|
{
|
|
domain: this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.',
|
|
'*.' + this._mailvelopeDomain()), video: "test", control: "true"
|
|
}
|
|
];
|
|
|
|
dialog(content, function(_button)
|
|
{
|
|
if(_button == 'install')
|
|
{
|
|
if(typeof chrome != 'undefined')
|
|
{
|
|
// ATM we are not able to trigger mailvelope installation directly
|
|
// since the installation should be triggered from the extension
|
|
// owner validate website (mailvelope.com), therefore, we just redirect
|
|
// user to chrome webstore to install mailvelope from there.
|
|
window.open('https://chrome.google.com/webstore/detail/mailvelope/kajibbejlbohfaggdiogboambcijhkke');
|
|
}
|
|
else if(typeof InstallTrigger != 'undefined' && InstallTrigger.enabled())
|
|
{
|
|
InstallTrigger.install({mailvelope: "https://download.mailvelope.com/releases/latest/mailvelope.firefox.xpi"},
|
|
function(_url, _status)
|
|
{
|
|
if(_status == 0)
|
|
{
|
|
Et2Dialog.alert(egw.lang('Mailvelope addon installation succeded. Now you may configure the options.'));
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Et2Dialog.alert(egw.lang('Mailvelope addon installation failed! Please try again.'));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PGP begin and end tags
|
|
*/
|
|
readonly begin_pgp_message : string = '-----BEGIN PGP MESSAGE-----';
|
|
readonly end_pgp_message : string = '-----END PGP MESSAGE-----';
|
|
|
|
/**
|
|
* Mailvelope "egroupware" Keyring
|
|
*/
|
|
mailvelope_keyring : any = undefined;
|
|
|
|
/**
|
|
* jQuery selector for Mailvelope iframes in all browsers
|
|
*/
|
|
readonly mailvelope_iframe_selector : string = 'iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]';
|
|
|
|
/**
|
|
* Open (or create) "egroupware" keyring and call callback with it
|
|
*
|
|
* @returns {Promise.<Keyring, Error>} Keyring or Error with message
|
|
*/
|
|
mailvelopeOpenKeyring()
|
|
{
|
|
let self = this;
|
|
let mailvelope = this.egw.window.mailvelope; // use Mailvelope of correct window
|
|
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
if (self.mailvelope_keyring) _resolve(self.mailvelope_keyring);
|
|
|
|
let resolve = _resolve;
|
|
let reject = _reject;
|
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring)
|
|
{
|
|
self.mailvelope_keyring = _keyring;
|
|
|
|
resolve(_keyring);
|
|
},
|
|
function(_err)
|
|
{
|
|
mailvelope.createKeyring('egroupware').then(function(_keyring)
|
|
{
|
|
self.mailvelope_keyring = _keyring;
|
|
var mvelo_settings_selector = self.mailvelope_iframe_selector
|
|
.split(',').map(function(_val) {return 'body>' + _val;}).join(',');
|
|
|
|
mailvelope.createSettingsContainer('body', _keyring, {
|
|
email: self.egw.user('account_email'),
|
|
fullName: self.egw.user('account_fullname')
|
|
}).then(function()
|
|
{
|
|
// make only Mailvelope settings dialog visible
|
|
jQuery(mvelo_settings_selector).css({position: 'absolute', top: 0});
|
|
// add a close button, so we know when to offer storing public key to AB
|
|
jQuery('<button class="et2_button et2_button_text" id="mailvelope_close_settings">' + self.egw.lang('Close') + '</button>')
|
|
.css({position: 'absolute', top: 8, right: 8, "z-index": 2})
|
|
.click(function()
|
|
{
|
|
// try fetching public key, to check user created onw
|
|
self.mailvelope_keyring.exportOwnPublicKey(self.egw.user('account_email')).then(function(_pubKey)
|
|
{
|
|
// CreateBackupDialog
|
|
self.mailvelopeCreateBackupDialog().then(function(_popupId)
|
|
{
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').css({
|
|
position: 'absolute',
|
|
"z-index": 1
|
|
});
|
|
},
|
|
function(_err)
|
|
{
|
|
egw.message(_err);
|
|
});
|
|
|
|
// if yes, hide settings dialog
|
|
jQuery(mvelo_settings_selector).each(function(index, item : any)
|
|
{
|
|
if (!item.src.match(/keyBackupDialog.html/,'ig')) item.remove();
|
|
});
|
|
jQuery('button#mailvelope_close_settings').remove();
|
|
|
|
// offer user to store his public key to AB for other users to find
|
|
var buttons = [
|
|
{
|
|
button_id: 2,
|
|
label: 'Yes',
|
|
id: 'dialog[yes]',
|
|
image: 'check',
|
|
default: true
|
|
},
|
|
{button_id: 3, label: 'No', id: 'dialog[no]', image: 'cancelled'}
|
|
];
|
|
if(egw.user('apps').admin)
|
|
{
|
|
buttons.unshift({
|
|
button_id: 5,
|
|
label: 'Yes and allow non-admin users to do that too (recommended)',
|
|
id: 'dialog[yes_allow]',
|
|
image: 'check',
|
|
default: true
|
|
});
|
|
delete buttons[1].default;
|
|
}
|
|
Et2Dialog.show_dialog(function(_button_id)
|
|
{
|
|
if(_button_id != Et2Dialog.NO_BUTTON)
|
|
{
|
|
var keys = {};
|
|
keys[self.egw.user('account_id')] = _pubKey;
|
|
self.egw.json('addressbook.addressbook_bo.ajax_set_pgp_keys',
|
|
[keys, _button_id != Et2Dialog.YES_BUTTON ? true : undefined]).sendRequest()
|
|
.then(function(_data)
|
|
{
|
|
self.egw.message(_data.response['0'].data);
|
|
});
|
|
}
|
|
},
|
|
'It is recommended to store your public key in addressbook, so other users can write you encrypted mails.',
|
|
'Store your public key in Addressbook?',
|
|
{}, buttons, Et2Dialog.QUESTION_MESSAGE, undefined, self.egw);
|
|
},
|
|
function(_err)
|
|
{
|
|
self.egw.message(_err.message + "\n\n" +
|
|
self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'error');
|
|
});
|
|
})
|
|
.appendTo('body');
|
|
});
|
|
resolve(_keyring);
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de"
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
_mailvelopeDomain()
|
|
{
|
|
var parts = document.location.hostname.split('.');
|
|
if (parts.length > 1) parts.shift();
|
|
return parts.join('.');
|
|
}
|
|
|
|
/**
|
|
* Check if we have a key for all recipients
|
|
*
|
|
* @param {Array} _recipients
|
|
* @returns {Promise.<Array, Error>} Array of recipients or Error with recipients without key
|
|
*/
|
|
mailvelopeGetCheckRecipients(_recipients)
|
|
{
|
|
// replace rfc822 addresses with raw email, as Mailvelop does not like them and lowercase all email
|
|
var rfc822_preg = /<([^'" <>]+)>$/;
|
|
var recipients = _recipients.map(function(_recipient)
|
|
{
|
|
var matches = _recipient.match(rfc822_preg);
|
|
return matches ? matches[1].toLowerCase() : _recipient.toLowerCase();
|
|
});
|
|
|
|
// check if we have keys for all recipients
|
|
var self = this;
|
|
return new Promise(function(_resolve, _reject)
|
|
{
|
|
var resolve = _resolve;
|
|
var reject = _reject;
|
|
self.mailvelopeOpenKeyring().then(function(_keyring : any)
|
|
{
|
|
var keyring = _keyring;
|
|
_keyring.validKeyForAddress(recipients).then(function(_status)
|
|
{
|
|
var no_key = [];
|
|
for(var email in _status)
|
|
{
|
|
if (!_status[email]) no_key.push(email);
|
|
}
|
|
if(no_key.length)
|
|
{
|
|
// server addressbook on server for missing public keys
|
|
self.egw.json('addressbook.addressbook_bo.ajax_get_pgp_keys', [no_key]).sendRequest().then(function(_data)
|
|
{
|
|
var data = _data.response['0'].data;
|
|
var promises = [];
|
|
for(var email in data)
|
|
{
|
|
promises.push(keyring.importPublicKey(data[email]).then(function(_result)
|
|
{
|
|
if(_result == 'IMPORTED' || _result == 'UPDATED')
|
|
{
|
|
no_key.splice(no_key.indexOf(email), 1);
|
|
}
|
|
}));
|
|
}
|
|
Promise.all(promises).then(function()
|
|
{
|
|
if(no_key.length)
|
|
{
|
|
reject(new Error(self.egw.lang('No key for recipient:') + ' ' + no_key.join(', ')));
|
|
}
|
|
else
|
|
{
|
|
resolve(recipients);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
resolve(recipients);
|
|
}
|
|
});
|
|
},
|
|
function(_err)
|
|
{
|
|
reject(_err);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the share action is enabled for this entry
|
|
*
|
|
* @param {egwAction} _action
|
|
* @param {egwActionObject[]} _entries
|
|
* @param {egwActionObject} _target
|
|
* @returns {boolean} if action is enabled
|
|
*/
|
|
is_share_enabled(_action, _entries, _target)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* create a share-link for the given entry
|
|
*
|
|
* @param {egwAction} _action egw actions
|
|
* @param {egwActionObject[]} _senders selected nm row
|
|
* @param {egwActionObject} _target Drag source. Not used here.
|
|
* @param {Boolean} _writable Allow edit access from the share.
|
|
* @param {Boolean} _files Allow access to files from the share.
|
|
* @param {Function} _callback Callback with results
|
|
* @param {Object} _extra Additional (app-specific or special) parameters
|
|
* @returns {Boolean} returns false if not successful
|
|
*/
|
|
share_link(_action, _senders, _target, _writable?, _files?, _callback?, _extra?)
|
|
{
|
|
var path = _senders[0].id;
|
|
if(!path)
|
|
{
|
|
return this.egw.message(this.egw.lang('Missing share path. Unable to create share.'), 'error');
|
|
}
|
|
switch(_action.id)
|
|
{
|
|
case 'shareFilemanager':
|
|
// Sharing a link to just files in filemanager
|
|
var id = path.split('::');
|
|
path = '/apps/' + id[0] + '/' + id[1];
|
|
}
|
|
if(typeof _writable === 'undefined' && _action.parent && _action.parent.getActionById('shareWritable'))
|
|
{
|
|
_writable = _action.parent.getActionById('shareWritable').checked || false;
|
|
}
|
|
if(typeof _files === 'undefined' && _action.parent && _action.parent.getActionById('shareFiles'))
|
|
{
|
|
_files = _action.parent.getActionById('shareFiles').checked || false;
|
|
}
|
|
if(typeof _extra === 'undefined')
|
|
{
|
|
_extra = {};
|
|
}
|
|
|
|
return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files, _extra],
|
|
_callback ? _callback : this._share_link_callback, this, true, this).sendRequest();
|
|
}
|
|
|
|
share_merge(_action, _senders, _target)
|
|
{
|
|
var parent = _action.parent.parent;
|
|
var _writable = false;
|
|
var _files = false;
|
|
if(parent && parent.getActionById('shareWritable'))
|
|
{
|
|
_writable = parent.getActionById('shareWritable').checked || false;
|
|
}
|
|
if(parent && parent.getActionById('shareFiles'))
|
|
{
|
|
_files = parent.getActionById('shareFiles').checked || false;
|
|
}
|
|
|
|
// Share only works on one at a time
|
|
var promises = [];
|
|
for(var i = 0; i < _senders.length; i++)
|
|
{
|
|
promises.push(new Promise(function(resolve, reject)
|
|
{
|
|
this.share_link(_action, [_senders[i]], _target, _writable, _files, resolve);
|
|
}.bind(this)));
|
|
}
|
|
|
|
// But merge into email can handle several
|
|
Promise.all(promises.map(function(p) {p.catch(function(e) {console.log(e)})}))
|
|
.then(function(values)
|
|
{
|
|
// Process document after all shares created
|
|
return nm_action(_action, _senders, _target);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Share-link callback
|
|
* @param {object} _data
|
|
*/
|
|
_share_link_callback(_data)
|
|
{
|
|
if (_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname);
|
|
|
|
var copy_link_to_clipboard = function(evt)
|
|
{
|
|
var $target = jQuery(evt.target);
|
|
$target.select();
|
|
try
|
|
{
|
|
var successful = document.execCommand('copy');
|
|
if(successful)
|
|
{
|
|
egw.message('Share link copied into clipboard');
|
|
return true;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
}
|
|
egw.message('Failed to copy the link!');
|
|
};
|
|
jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard);
|
|
et2_createWidget("dialog", {
|
|
callback: function(button_id, value)
|
|
{
|
|
jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard);
|
|
return true;
|
|
},
|
|
title: _data.title ? _data.title : egw.lang("%1 Share Link", _data.writable ? egw.lang("Writable") : egw.lang("Readonly")),
|
|
template: _data.template,
|
|
width: 450,
|
|
value: {content: {"share_link": _data.share_link}}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Keep a list of all EgwApp instances
|
|
*
|
|
* This is not just the globals available in window.app, it also includes private instances as well
|
|
*
|
|
* @private
|
|
* @param app_obj
|
|
*/
|
|
private static _register_instance(app_obj : EgwApp)
|
|
{
|
|
// Reject improper objects
|
|
if(!app_obj.appname) return;
|
|
|
|
EgwApp._instances.push(app_obj);
|
|
}
|
|
|
|
/**
|
|
* Iterator over all app instances
|
|
*
|
|
* Use for(const app of EgwApp) {...} to iterate over all app objects.
|
|
*/
|
|
static [Symbol.iterator]()
|
|
{
|
|
return EgwApp._instances[Symbol.iterator]();
|
|
}
|
|
}
|
|
|
|
// EgwApp need to be global on window, as it's used to iterate through all EgwApp instances
|
|
window.EgwApp = EgwApp; |