diff --git a/addressbook/inc/class.addressbook_hooks.inc.php b/addressbook/inc/class.addressbook_hooks.inc.php index 5b29fe86dd..e7daf4b20e 100644 --- a/addressbook/inc/class.addressbook_hooks.inc.php +++ b/addressbook/inc/class.addressbook_hooks.inc.php @@ -408,7 +408,8 @@ class addressbook_hooks 'owner' => array( 'key' => 'egw_addressbook.contact_id', 'column' => 'egw_addressbook.contact_owner' - ) + ), + 'push_data' => ['owner','tid','cat_id'] ); return $links; } diff --git a/addressbook/js/app.js b/addressbook/js/app.js index b268c66600..38a975742f 100644 --- a/addressbook/js/app.js +++ b/addressbook/js/app.js @@ -44,8 +44,13 @@ var AddressbookApp = /** @class */ (function (_super) { * @memberOf app.addressbook */ function AddressbookApp() { + var _this = // call parent - return _super.call(this, 'addressbook') || this; + _super.call(this, 'addressbook') || this; + // These fields help with push + _this.push_grant_fields = ["owner"]; + _this.push_filter_fields = ["tid", "owner", "cat_id"]; + return _this; } /** * Destructor @@ -1231,9 +1236,8 @@ var AddressbookApp = /** @class */ (function (_super) { * Remove the entry again, if user is not allowed */ AddressbookApp.prototype.shared_changed = function () { - var _a; var shared = this.et2.getInputWidgetById('shared_values'); - var value = (_a = shared) === null || _a === void 0 ? void 0 : _a.get_value(); + var value = shared === null || shared === void 0 ? void 0 : shared.get_value(); if (value) { this.egw.json('addressbook.addressbook_ui.ajax_check_shared', [{ contact: this.et2.getInstanceManager().getValues(this.et2), diff --git a/addressbook/js/app.ts b/addressbook/js/app.ts index 0629f57a13..abe4154813 100644 --- a/addressbook/js/app.ts +++ b/addressbook/js/app.ts @@ -27,6 +27,10 @@ import {etemplate2} from "../../api/js/etemplate/etemplate2"; */ class AddressbookApp extends EgwApp { + // These fields help with push + protected push_grant_fields = ["owner"]; + protected push_filter_fields = ["tid","owner","cat_id"] + /** * Constructor * diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js index abb3ffb8e7..f6251d35ac 100644 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ b/api/js/etemplate/et2_extension_nextmatch.js @@ -55,6 +55,7 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); +exports.et2_nextmatch_accountfilterheader = exports.et2_nextmatch_filterheader = exports.et2_nextmatch_sortheader = exports.et2_nextmatch_customfields = exports.et2_nextmatch_header = exports.et2_nextmatch = void 0; require("./et2_core_common"); require("./et2_core_interfaces"); var et2_core_inheritance_1 = require("./et2_core_inheritance"); @@ -1921,11 +1922,10 @@ var et2_nextmatch = /** @class */ (function (_super) { * @param {object} target */ et2_nextmatch.prototype.handle_drop = function (event, target) { - var _a; // Check to see if we can handle the link // First, find the UID var row = this.controller.getRowByNode(target); - var uid = ((_a = row) === null || _a === void 0 ? void 0 : _a.uid) || null; + var uid = (row === null || row === void 0 ? void 0 : row.uid) || null; // Get the file information var files = []; if (event.originalEvent && event.originalEvent.dataTransfer && @@ -2014,7 +2014,8 @@ var et2_nextmatch = /** @class */ (function (_super) { idsArr[i] = idsArr[i].split("::").pop(); } var value = { - "selected": idsArr + "selected": idsArr, + col_filter: {} }; jQuery.extend(value, this.activeFilters, this.value); return value; diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index 507601f5d3..1a17ca2343 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -2707,7 +2707,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 idsArr[i] = idsArr[i].split("::").pop(); } const value = { - "selected": idsArr + "selected": idsArr, + col_filter: {} }; jQuery.extend(value, this.activeFilters, this.value); return value; diff --git a/api/js/jsapi/egw_app.js b/api/js/jsapi/egw_app.js index 126755cdf2..7933d78da4 100644 --- a/api/js/jsapi/egw_app.js +++ b/api/js/jsapi/egw_app.js @@ -163,10 +163,107 @@ var EgwApp = /** @class */ (function () { // don't care about other apps data, reimplement if your app does care eg. calendar if (pushData.app !== this.appname) return; - // only handle delete by default, for simple case of uid === "$app::$id" + // 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))) { + 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. + var nm = 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 + */ + EgwApp.prototype._push_grant_check = function (pushData, grant_fields) { + var grants = egw.grants(this.appname); + // check user has a grant from owner or something + for (var i = 0; i < grant_fields.length; i++) { + if (grants && typeof grants[pushData.acl[grant_fields[i]]] !== 'undefined') { + // ACL access + return true; + } + } + 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 + */ + EgwApp.prototype._push_field_filter = function (pushData, nm, filter_fields) { + var filters = {}; + for (var i = 0; i < filter_fields.length; i++) { + filters[filter_fields[i]] = { + col: filter_fields[i], + filter_values: [] + }; + } + // Get current filter values + if (this.et2) { + var value = nm.getValue(); + if (!value || !value.col_filter) + return false; + for (var _i = 0, _a = Object.values(filters); _i < _a.length; _i++) { + var field_filter = _a[_i]; + if (value.col_filter[field_filter.col]) { + field_filter.filter_values.push(value.col_filter[field_filter.col]); + } + } + } + var _loop_1 = function (field_filter) { + // no filter set + if (field_filter.filter_values.length == 0) + return "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 { value: false }; + } + return "continue"; + } + // acl value is an array (eg. tr_assigned) --> check intersection with filter + if (!field_filter.filter_values.filter(function (account) { return pushData.acl[field_filter.col].indexOf(account) >= 0; }).length) { + return { value: false }; + } + }; + // check filters against pushData.acl data + for (var _b = 0, _c = Object.values(filters); _b < _c.length; _b++) { + var field_filter = _c[_b]; + var state_1 = _loop_1(field_filter); + if (typeof state_1 === "object") + return state_1.value; + } + return true; }; /** * Get (possible) app-specific uid diff --git a/api/js/jsapi/egw_app.ts b/api/js/jsapi/egw_app.ts index 65ec5c0304..bc22a3c8f5 100644 --- a/api/js/jsapi/egw_app.ts +++ b/api/js/jsapi/egw_app.ts @@ -131,6 +131,22 @@ export abstract class EgwApp */ 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. @@ -259,11 +275,129 @@ export abstract class EgwApp // don't care about other apps data, reimplement if your app does care eg. calendar if (pushData.app !== this.appname) return; - // only handle delete by default, for simple case of uid === "$app::$id" + // 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))) + { + 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 = 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 + */ + _push_grant_check(pushData : PushData, grant_fields : string[]) : boolean + { + let grants = egw.grants(this.appname); + + // check user has a grant from owner or something + for(let i = 0; i < grant_fields.length; i++) + { + if(grants && typeof grants[pushData.acl[grant_fields[i]]] !== 'undefined') + { + // ACL access + return true; + } + } + 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 + if(this.et2) + { + let value = nm.getValue(); + if(!value || !value.col_filter) return false; + + for(let field_filter of Object.values(filters)) + { + if(value.col_filter[field_filter.col]) + { + field_filter.filter_values.push(value.col_filter[field_filter.col]); + } + } + } + + // 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; } /**