From 5702f37d2506fbadcd6a147b267f4747214170d3 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Tue, 10 Dec 2024 11:29:50 -0500 Subject: [PATCH] Trash manager updates Apply new permissions methods; includes adjustments to base grid class --- .../Processors/Resource/Trash/GetList.php | 67 +++------ .../assets/modext/widgets/core/modx.grid.js | 17 ++- .../widgets/resource/modx.grid.trash.js | 137 +++++++++++++----- 3 files changed, 134 insertions(+), 87 deletions(-) diff --git a/core/src/Revolution/Processors/Resource/Trash/GetList.php b/core/src/Revolution/Processors/Resource/Trash/GetList.php index 79234f46ac..52766c890e 100644 --- a/core/src/Revolution/Processors/Resource/Trash/GetList.php +++ b/core/src/Revolution/Processors/Resource/Trash/GetList.php @@ -40,11 +40,21 @@ class GetList extends GetListProcessor public $permission = 'view'; + public $canPurge = false; + public $canUndelete = false; + public $canUPublish = false; + private modManagerDateFormatter $formatter; public function initialize() { $this->formatter = $this->modx->services->get(modManagerDateFormatter::class); + + $canChange = $this->modx->hasPermission('save_document') && $this->modx->hasPermission('edit_document'); + $this->canPurge = $canChange && $this->modx->hasPermission('purge_deleted'); + $this->canUndelete = $canChange && $this->modx->hasPermission('undelete_document'); + $this->canUPublish = $canChange && $this->modx->hasPermission('publish_document'); + return parent::initialize(); } @@ -138,16 +148,22 @@ public function prepareRow(xPDOObject $object) return []; } + $permissions = [ + 'purge' => $this->canPurge && $object->checkPolicy('purge_deleted'), + 'undelete' => $this->canUndelete && $object->checkPolicy('undelete_document'), + 'publish' => $this->canUPublish && $object->checkPolicy('publish_document') + ]; + $charset = $this->modx->getOption('modx_charset', null, 'UTF-8'); - $objectArray = $object->toArray(); - $objectArray['pagetitle'] = htmlentities($objectArray['pagetitle'], ENT_COMPAT, $charset); - $objectArray['content'] = htmlentities($objectArray['content'], ENT_COMPAT, $charset); + $resourceData = $object->toArray(); + $resourceData['pagetitle'] = htmlentities($resourceData['pagetitle'], ENT_COMPAT, $charset); + $resourceData['content'] = htmlentities($resourceData['content'], ENT_COMPAT, $charset); // to enable a better detection of the resource's location, we also construct the // parent-child path to the resource $parents = []; - $parent = $objectArray['parent']; + $parent = $resourceData['parent']; while ($parent != 0) { $parentObject = $this->modx->getObject(modResource::class, $parent); @@ -163,46 +179,11 @@ public function prepareRow(xPDOObject $object) foreach ($parents as $parent) { $parentPath = $parent->get('pagetitle') . ' (' . $parent->get('id') . ') > ' . $parentPath; } - $objectArray['parentPath'] = '[' . $objectArray['context_key'] . '] ' . $parentPath; - - // TODO implement permission checks for every resource and return only resources user is allowed to see - - // show the permissions for the context - $canView = $this->modx->hasPermission('view_document'); - $canPurge = $this->modx->hasPermission('purge_deleted'); - $canUndelete = $this->modx->hasPermission('undelete_document'); - $canPublish = $this->modx->hasPermission('publish_document'); - $canSave = $this->modx->hasPermission('save_document'); - $canEdit = $this->modx->hasPermission('edit_document'); - $canList = $this->modx->hasPermission('list'); - $canLoad = $this->modx->hasPermission('load'); - - $objectArray['iconCls'] = $this->modx->getOption('mgr_source_icon', null, 'icon-folder-open-o'); - - $cls = []; - $cls[] = 'restore'; - $cls[] = 'purge'; - $cls[] = 'undelete_document'; - - $cls = []; - if ($object->checkPolicy('purge_deleted') && $canSave && $canEdit && $canPurge) { - $cls[] = 'trashpurge'; - } - if ($object->checkPolicy('undelete_document') && $canSave && $canEdit) { - $cls[] = 'trashundelete'; - } - if ($object->checkPolicy('save') && $canSave && $canEdit) { - $cls[] = 'trashsave'; - } - if ($object->checkPolicy('edit') && $canSave && $canEdit) { - $cls[] = 'trashedit'; - } - $cls[] = 'trashrow'; - - $objectArray['cls'] = implode(' ', $cls); + $resourceData['parentPath'] = '[' . $resourceData['context_key'] . '] ' . $parentPath; - $objectArray['deletedon'] = $this->formatter->formatDateTime($objectArray['deletedon']); + $resourceData['deletedon'] = $this->formatter->formatDateTime($resourceData['deletedon']); + $resourceData['permissions'] = $permissions; - return $objectArray; + return $resourceData; } } diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index 127d15370f..826d389778 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -386,13 +386,15 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { if (!this.userHasSavePermissions && isProtected) { return; } - // Checking record-level permissions; this block checking for 'cls' can be removed once all grids are updated - if (Object.hasOwn(record.data, 'cls')) { + const hasPermissionsProp = Object.hasOwn(record[this.permissionsProviderProp], 'permissions'); + // Checking record-level permissions + /** @todo This block checking for 'cls' can be removed once all grids are updated */ + if (!hasPermissionsProp && Object.hasOwn(record.data, 'cls')) { if (Ext.isEmpty(record.data.cls)) { return; } } - if (Object.hasOwn(record[this.permissionsProviderProp], 'permissions')) { + if (hasPermissionsProp) { if ( Ext.isEmpty(record[this.permissionsProviderProp].permissions) || Object.values(record[this.permissionsProviderProp].permissions).every(permission => !permission) @@ -448,6 +450,7 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { const { options } = item, { id } = this.menu.record, + // eslint-disable-next-line no-shadow doAction = (id, options) => { const action = Ext.urlEncode(options.params || { action: options.action }), @@ -703,9 +706,9 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { return records.some(record => this.userCanDeleteRecord(record)); }, - userCanDeleteRecord: function(record) { + userCanDeleteRecord: function(record, action = 'delete') { const objPermissions = record[this.permissionsProviderProp].permissions; - return !Ext.isEmpty(objPermissions) && !record[this.permissionsProviderProp].isProtected && objPermissions.delete === true; + return !Ext.isEmpty(objPermissions) && !record[this.permissionsProviderProp].isProtected && objPermissions[action] === true; }, userCanDuplicateRecord: function(record) { @@ -1048,13 +1051,13 @@ Ext.extend(MODx.grid.GridBase, Ext.grid.EditorGridPanel, { case true: case 'true': case 1: - metaData.css = 'green'; + metaData.css += ' green'; return _('yes'); case false: case 'false': case '': case 0: - metaData.css = 'red'; + metaData.css += ' red'; return _('no'); // no default } diff --git a/manager/assets/modext/widgets/resource/modx.grid.trash.js b/manager/assets/modext/widgets/resource/modx.grid.trash.js index 4dac169ce0..bbd1e2f814 100644 --- a/manager/assets/modext/widgets/resource/modx.grid.trash.js +++ b/manager/assets/modext/widgets/resource/modx.grid.trash.js @@ -16,8 +16,7 @@ MODx.grid.Trash = function(config = {}) { 'parentPath', 'deletedon', 'deletedby', - 'deletedby_name', - 'cls' + 'deletedby_name' ], paging: true, autosave: true, @@ -47,8 +46,21 @@ MODx.grid.Trash = function(config = {}) { width: 40, sortable: true, editor: { - xtype: 'combo-boolean', - renderer: 'boolean' + xtype: 'combo-boolean' + }, + renderer: { + fn: function(value, metaData, record) { + /* + This field depends on permission other than the typicaledit, + thus not using the base setEditableCellClasses() method here + */ + if (!record.json.permissions.publish) { + // eslint-disable-next-line no-param-reassign + metaData.css = 'editor-disabled'; + } + return this.rendYesNo(value, metaData); + }, + scope: this } }, { header: _('trash.deletedon_title'), @@ -64,39 +76,57 @@ MODx.grid.Trash = function(config = {}) { return record.data.deletedby_name; } }], - tbar: [ + /* + Not using base getBulkActionsButton() method here, as this menu utilizes + methods/actions specific to this class not supported by that method + */ { text: _('bulk_actions'), + id: 'modx-btn-bulk-actions', menu: [{ text: _('trash.selected_purge'), + itemId: 'modx-bulk-menu-opt-purge', handler: this.purgeSelected, scope: this }, { text: _('trash.selected_restore'), + itemId: 'modx-bulk-menu-opt-restore', handler: this.restoreSelected, scope: this - }] - }, { - text: _('trash.purge_all'), - id: 'modx-purge-all', - cls: 'x-btn-purge-all', + }], listeners: { click: { - fn: this.purgeAll, + fn: function(btn) { + const + menuOptPurge = btn.menu.getComponent('modx-bulk-menu-opt-purge'), + menuOptUndelete = btn.menu.getComponent('modx-bulk-menu-opt-restore') + ; + if (this.getSelectionModel().getCount() === 0) { + menuOptPurge.disable(); + menuOptUndelete.disable(); + } else { + if (this.userCanPurge) { + menuOptPurge.enable(); + } + if (this.userCanUndelete) { + menuOptUndelete.enable(); + } + } + }, scope: this } } + }, { + text: _('trash.purge_all'), + id: 'modx-btn-purge-all', + cls: 'x-btn-purge-all', + handler: this.purgeAll }, { text: _('trash.restore_all'), - id: 'modx-restore-all', + id: 'modx-btn-restore-all', cls: 'x-btn-restore-all', - listeners: { - click: { - fn: this.restoreAll, - scope: this - } - } + handler: this.restoreAll }, '->', { @@ -125,6 +155,38 @@ MODx.grid.Trash = function(config = {}) { }); MODx.grid.Trash.superclass.constructor.call(this, config); + + this.gridMenuActions = ['purge', 'undelete']; + this.setUserHasPermissions('purge', ['purge_deleted']); + this.setUserHasPermissions('undelete', ['undelete_document']); + this.setShowActionsMenu(); + + this.on({ + render: grid => { + const buttonsToHide = []; + if (!this.userCanPurge && !this.userCanUndelete) { + buttonsToHide.push('modx-btn-bulk-actions', 'modx-btn-purge-all', 'modx-btn-restore-all'); + } else { + const bulkMenu = Ext.getCmp('modx-btn-bulk-actions').menu; + if (!this.userCanPurge) { + buttonsToHide.push('modx-btn-purge-all'); + bulkMenu.getComponent('modx-bulk-menu-opt-purge').disable(); + } + if (!this.userCanUndelete) { + buttonsToHide.push('modx-btn-restore-all'); + bulkMenu.getComponent('modx-bulk-menu-opt-restore').disable(); + } + } + if (buttonsToHide.length > 0) { + buttonsToHide.forEach(btnId => Ext.getCmp(btnId)?.hide()); + } + }, + beforeedit: function(e) { + if (e.field === 'published' && !this.userCanEditRecord(e.record, 'publish')) { + return false; + } + } + }); }; Ext.extend(MODx.grid.Trash, MODx.grid.Grid, { @@ -133,37 +195,40 @@ Ext.extend(MODx.grid.Trash, MODx.grid.Grid, { const model = this.getSelectionModel(), record = model.getSelected(), - p = record.data.cls, + canPurge = this.userCanPurge && this.userCanDeleteRecord(record, 'purge'), + canUndelete = this.userCanUndelete && this.userCanEditRecord(record, 'undelete'), menu = [] ; if (model.getCount() > 1) { - menu.push({ - text: _('trash.selected_purge'), - handler: this.purgeSelected, - scope: this - }); - menu.push({ - text: _('trash.selected_restore'), - handler: this.restoreSelected, - scope: this - }); + if (canPurge) { + menu.push({ + text: _('trash.selected_purge'), + handler: this.purgeSelected, + scope: this + }); + } + if (canUndelete) { + menu.push({ + text: _('trash.selected_restore'), + handler: this.restoreSelected, + scope: this + }); + } } else { - if (p.indexOf('trashpurge') !== -1) { + if (canPurge) { menu.push({ text: _('trash.purge'), handler: this.purgeResource }); } - if (p.indexOf('trashundelete') !== -1) { + if (canUndelete) { menu.push({ text: _('trash.restore'), handler: this.restoreResource }); } } - if (menu.length > 0) { - this.addContextMenuItem(menu); - } + return menu; }, purgeResource: function() { @@ -428,9 +493,7 @@ Ext.extend(MODx.grid.Trash, MODx.grid.Grid, { Ext.getCmp('modx-trash-link')?.updateState(+total); }, - listResources: function(separator) { - separator = separator || ''; - + listResources: function(separator = '') { // creates a textual representation of the selected resources // we create a textlist of the resources here to show them again in the confirmation box const