diff --git a/_build/test/Tests/Controllers/ResourceAccessPolicyTest.php b/_build/test/Tests/Controllers/ResourceAccessPolicyTest.php new file mode 100644 index 00000000000..d24501ab67e --- /dev/null +++ b/_build/test/Tests/Controllers/ResourceAccessPolicyTest.php @@ -0,0 +1,106 @@ +assertStringContainsString( + "\$_lang['perm.publish_document_desc'] = 'To publish any Resource. Unpublishing uses unpublish_document.';", + $contents + ); + $this->assertStringNotContainsString( + 'To publish or unpublish any Resource.', + $contents + ); + } + + public function testEditLockedLexiconDescribesElementsNotResources() + { + $lexiconFile = MODX_CORE_PATH . 'lexicon/en/permissions.inc.php'; + $contents = file_get_contents($lexiconFile); + $this->assertStringContainsString('edit Elements that have the Locked checkbox', $contents); + $this->assertStringContainsString('Does not apply to Resource session locks', $contents); + } + + public function testDeleteDocumentLexiconDescribesSoftDelete() + { + $lexiconFile = MODX_CORE_PATH . 'lexicon/en/permissions.inc.php'; + $contents = file_get_contents($lexiconFile); + $this->assertStringContainsString('soft-delete any Resource', $contents); + $this->assertStringContainsString('requires purge_deleted', $contents); + } + + /** + * @dataProvider providerClassMapGatedFiles + */ + public function testClassKeyFieldIsGatedOnClassMap(string $relativePath) + { + $file = MODX_MANAGER_PATH . $relativePath; + $this->assertFileExists($file); + $contents = file_get_contents($file); + $this->assertMatchesRegularExpression( + "/xtype:\\s*MODx\\.perm\\.class_map\\s*\\?\\s*'modx-combo-class-derivatives'\\s*:\\s*'hidden'/", + $contents, + $relativePath . ' must toggle Resource Type combo vs hidden via MODx.perm.class_map' + ); + } + + public function providerClassMapGatedFiles(): array + { + return [ + ['assets/modext/widgets/resource/modx.panel.resource.js'], + ['assets/modext/widgets/resource/modx.window.resource.js'], + ['assets/modext/widgets/resource/modx.tree.resource.js'], + ]; + } + + public function testViewDocumentLexiconDistinguishesFromGetList() + { + $lexiconFile = MODX_CORE_PATH . 'lexicon/en/permissions.inc.php'; + $contents = file_get_contents($lexiconFile); + $this->assertStringContainsString('Resource/Get uses the generic view permission', $contents); + $this->assertStringContainsString('per-row object list', $contents); + } + + public function testClassMapLexiconIsNotCreateTypeGate() + { + $lexiconFile = MODX_CORE_PATH . 'lexicon/en/permissions.inc.php'; + $contents = file_get_contents($lexiconFile); + $this->assertStringContainsString('Does not grant creating or changing Resource types', $contents); + } + + public function testQuickCreateHasSingleClassKeyField() + { + $file = MODX_MANAGER_PATH . 'assets/modext/widgets/resource/modx.tree.resource.js'; + $contents = file_get_contents($file); + $fnStart = strpos($contents, 'MODx.getQuickCreateResourceSettingsFields'); + $this->assertNotFalse($fnStart); + $fnEnd = strpos($contents, 'MODx.getQRSettings', $fnStart); + $this->assertNotFalse($fnEnd); + $chunk = substr($contents, $fnStart, $fnEnd - $fnStart); + $this->assertStringContainsString('MODx.perm.class_map', $chunk); + $this->assertStringNotContainsString( + 'id: `modx-${id}-class_key`', + $chunk, + 'Pre-existing duplicate hidden class_key field must stay removed' + ); + $this->assertStringContainsString('id: `modx-${id}-class-key`', $chunk); + $this->assertSame( + 1, + substr_count($chunk, "name: 'class_key'"), + 'Quick create settings must define class_key once' + ); + } +} diff --git a/core/lexicon/en/permissions.inc.php b/core/lexicon/en/permissions.inc.php index 43d2ae252d5..bad04e3c5c5 100644 --- a/core/lexicon/en/permissions.inc.php +++ b/core/lexicon/en/permissions.inc.php @@ -13,7 +13,7 @@ $_lang['perm.change_password_desc'] = 'User can change their user password.'; $_lang['perm.change_profile_desc'] = 'User can change their profile.'; $_lang['perm.charsets_desc'] = 'To view a list of charsets.'; -$_lang['perm.class_map_desc'] = 'To view a list of classes in the Class Map.'; +$_lang['perm.class_map_desc'] = 'To view class lists used by Class Map selectors, including the Resource Type combo. Does not grant creating or changing Resource types; those still need new_document and the matching type key (new_weblink, new_symlink, new_static_resource).'; $_lang['perm.components_desc'] = 'To view the Extras menu.'; $_lang['perm.content_types_desc'] = 'The Content Types page.'; $_lang['perm.copy_desc'] = 'The ability to copy an object.'; @@ -28,10 +28,10 @@ $_lang['perm.delete_category_desc'] = 'To delete or remove any Categories.'; $_lang['perm.delete_chunk_desc'] = 'To delete or remove any Chunks.'; $_lang['perm.delete_context_desc'] = 'To delete or remove any Contexts.'; -$_lang['perm.delete_document_desc'] = 'To delete or remove any Resource.'; -$_lang['perm.delete_weblink_desc'] = 'To delete or remove any weblink, also requires delete_document permission.'; -$_lang['perm.delete_symlink_desc'] = 'To delete or remove any symlink, also requires delete_document permission.'; -$_lang['perm.delete_static_resource_desc'] = 'To delete or remove any static resource, also requires delete_document permission.'; +$_lang['perm.delete_document_desc'] = 'To soft-delete any Resource (mark as deleted). Permanent removal from the database requires purge_deleted.'; +$_lang['perm.delete_weblink_desc'] = 'To soft-delete any weblink; also requires delete_document.'; +$_lang['perm.delete_symlink_desc'] = 'To soft-delete any symlink; also requires delete_document.'; +$_lang['perm.delete_static_resource_desc'] = 'To soft-delete any static resource; also requires delete_document.'; $_lang['perm.delete_eventlog_desc'] = 'To empty the Event Log.'; $_lang['perm.delete_plugin_desc'] = 'To delete or remove any Plugins.'; $_lang['perm.delete_propertyset_desc'] = 'To delete or remove any Property Sets.'; @@ -52,7 +52,7 @@ $_lang['perm.edit_weblink_desc'] = 'To edit weblinks, also requires edit_document permission.'; $_lang['perm.edit_symlink_desc'] = 'To edit symlinks, also requires edit_document permission.'; $_lang['perm.edit_static_resource_desc'] = 'To edit static resources, also requires edit_document permission.'; -$_lang['perm.edit_locked_desc'] = 'Allows a user to override a lock and edit a locked Resource.'; +$_lang['perm.edit_locked_desc'] = 'To edit Elements that have the Locked checkbox set. Does not apply to Resource session locks; those use steal_locks and the Resource object policy steal_lock.'; $_lang['perm.edit_plugin_desc'] = 'To edit any Plugins. WARNING: combined with save_plugin permission, grants ability to execute arbitrary server-side code.'; $_lang['perm.edit_propertyset_desc'] = 'To edit any Property Sets.'; $_lang['perm.edit_role_desc'] = 'To edit any Roles.'; @@ -101,18 +101,18 @@ $_lang['perm.new_category_desc'] = 'To create a new Category.'; $_lang['perm.new_chunk_desc'] = 'To create a new Chunk.'; $_lang['perm.new_context_desc'] = 'To create a new Context.'; -$_lang['perm.new_document_desc'] = 'To create a new Resource.'; +$_lang['perm.new_document_desc'] = 'To create a new Resource (Document). Opening Create Resource requires this key; derivative types also need their type key.'; $_lang['perm.new_document_in_root_desc'] = 'To be able to create a Resource at the root level.'; $_lang['perm.new_plugin_desc'] = 'To create a new Plugin.'; $_lang['perm.new_propertyset_desc'] = 'To create a new Property Set.'; $_lang['perm.new_role_desc'] = 'To create a new Role.'; $_lang['perm.new_snippet_desc'] = 'To create a new Snippet.'; -$_lang['perm.new_static_resource_desc'] = 'To create a new Static Resource.'; -$_lang['perm.new_symlink_desc'] = 'To create a new SymLink.'; +$_lang['perm.new_static_resource_desc'] = 'To create a new Static Resource. Also requires new_document. Enforced on create and when changing Resource Type.'; +$_lang['perm.new_symlink_desc'] = 'To create a new SymLink. Also requires new_document. Enforced on create and when changing Resource Type.'; $_lang['perm.new_template_desc'] = 'To create a new Template.'; $_lang['perm.new_tv_desc'] = 'To create a new TV.'; $_lang['perm.new_user_desc'] = 'To create a new User.'; -$_lang['perm.new_weblink_desc'] = 'To create a new WebLink.'; +$_lang['perm.new_weblink_desc'] = 'To create a new WebLink. Also requires new_document. Enforced on create and when changing Resource Type.'; $_lang['perm.packages_desc'] = 'To use any Transport Packages in the Package Management system.'; $_lang['perm.policy_delete_desc'] = 'To delete an Access Policy.'; $_lang['perm.policy_edit_desc'] = 'To edit an Access Policy. WARNING: combined with policy_save permission, grants ability to escalate privileges, including security-related permissions.'; @@ -127,8 +127,8 @@ $_lang['perm.property_sets_desc'] = 'To view and edit Properties and Property Sets.'; $_lang['perm.providers_desc'] = 'To view and edit Providers across the site.'; $_lang['perm.publish_desc'] = 'Basic "publish" access on any object.'; -$_lang['perm.publish_document_desc'] = 'To publish or unpublish any Resource.'; -$_lang['perm.purge_deleted_desc'] = 'To empty the Recycle Bin.'; +$_lang['perm.publish_document_desc'] = 'To publish any Resource. Unpublishing uses unpublish_document.'; +$_lang['perm.purge_deleted_desc'] = 'To permanently remove soft-deleted Resources from the Recycle Bin.'; $_lang['perm.remove_desc'] = 'Basic permission to delete any object.'; $_lang['perm.remove_locks_desc'] = 'To delete all existing Locks throughout the site.'; $_lang['perm.resourcegroup_delete_desc'] = 'To delete a Resource Group.'; @@ -163,8 +163,8 @@ $_lang['perm.source_save_desc'] = 'To create or save a Media Source.'; $_lang['perm.source_view_desc'] = 'To view and list Media Sources.'; $_lang['perm.sources_desc'] = 'To manage Media Sources and Media Source Types.'; -$_lang['perm.steal_lock_desc'] = 'To "steal", or delete, a lock from a Resource.'; -$_lang['perm.steal_locks_desc'] = 'To "steal" locks, overriding a current lock on a Resource.'; +$_lang['perm.steal_lock_desc'] = 'Resource object policy: allow stealing the session lock on that Resource. Also requires the manager permission steal_locks.'; +$_lang['perm.steal_locks_desc'] = 'Manager permission to steal Resource session locks (another user has the Resource open). Also requires the Resource object policy steal_lock on that Resource.'; $_lang['perm.tree_show_resource_ids_desc'] = 'Show the IDs in the Resource tree.'; $_lang['perm.tree_show_element_ids_desc'] = 'Show the IDs in the Elements tree.'; $_lang['perm.undelete_desc'] = 'Basic permission to "undelete" any object.'; @@ -183,7 +183,7 @@ $_lang['perm.view_category_desc'] = 'To view any Categories.'; $_lang['perm.view_chunk_desc'] = 'To view any Chunks.'; $_lang['perm.view_context_desc'] = 'To view any Contexts.'; -$_lang['perm.view_document_desc'] = 'To view any Resources.'; +$_lang['perm.view_document_desc'] = 'To open Resource Data and related Manager views (resource/data, site_schedule, tree preview). Resource/Get uses the generic view permission (and object view). Resource/GetList uses view plus per-row object list. Neither uses this key.'; $_lang['perm.view_element_desc'] = 'To get a list of Elements or Element classes.'; $_lang['perm.view_eventlog_desc'] = 'To view the Event Log.'; $_lang['perm.view_offline_desc'] = 'To be able to view the site when it is in offline status.'; diff --git a/manager/assets/modext/widgets/resource/modx.panel.resource.js b/manager/assets/modext/widgets/resource/modx.panel.resource.js index 5db4e4d82e0..707d38498fe 100644 --- a/manager/assets/modext/widgets/resource/modx.panel.resource.js +++ b/manager/assets/modext/widgets/resource/modx.panel.resource.js @@ -929,15 +929,17 @@ Ext.extend(MODx.panel.Resource, MODx.FormPanel, { }, getSettingLeftFields: function(config) { + const classKey = config.record.class_key || 'MODX\\Revolution\\modDocument'; + return [{ - xtype: 'modx-combo-class-derivatives', + xtype: MODx.perm.class_map ? 'modx-combo-class-derivatives' : 'hidden', fieldLabel: _('resource_type'), description: '[[*class_key]]
', name: 'class_key', hiddenName: 'class_key', id: 'modx-resource-class-key', allowBlank: false, - value: config.record.class_key || 'MODX\\Revolution\\modDocument' + value: classKey }, { xtype: 'modx-combo-content-type', fieldLabel: _('resource_content_type'), diff --git a/manager/assets/modext/widgets/resource/modx.tree.resource.js b/manager/assets/modext/widgets/resource/modx.tree.resource.js index a1f2265859d..f1f4eebce6b 100644 --- a/manager/assets/modext/widgets/resource/modx.tree.resource.js +++ b/manager/assets/modext/widgets/resource/modx.tree.resource.js @@ -1290,11 +1290,6 @@ MODx.getQuickCreateResourceSettingsFields = function(id, parentData) { name: 'context_key', id: `modx-${id}-context_key`, value: parentData.context_key - }, { - xtype: 'hidden', - name: 'class_key', - id: `modx-${id}-class_key`, - value: parentData.class_key }, { xtype: 'hidden', name: 'publishedon', @@ -1312,7 +1307,7 @@ MODx.getQuickCreateResourceSettingsFields = function(id, parentData) { contextcmp: `modx-${id}-context_key`, currentid: parentData.id || 0 }, { - xtype: 'modx-combo-class-derivatives', + xtype: MODx.perm.class_map ? 'modx-combo-class-derivatives' : 'hidden', fieldLabel: _('resource_type'), description: '[[*class_key]]
', name: 'class_key', diff --git a/manager/assets/modext/widgets/resource/modx.window.resource.js b/manager/assets/modext/widgets/resource/modx.window.resource.js index bbafe856cfc..8a484b47f03 100644 --- a/manager/assets/modext/widgets/resource/modx.window.resource.js +++ b/manager/assets/modext/widgets/resource/modx.window.resource.js @@ -3,19 +3,20 @@ MODx.window.CreateResource = function(config = {}) { const id = this.ident, requireAlias = parseInt(MODx.config.friendly_urls, 10) && !parseInt(MODx.config.automatic_alias, 10), aliasLength = parseInt(MODx.config.friendly_alias_max_length, 10) || 0, + classKey = config.record.class_key || 'MODX\\Revolution\\modDocument', resourceDetail = [ { columnWidth: requireAlias ? 0.33 : 0.5, items: [ { - xtype: 'modx-combo-class-derivatives', + xtype: MODx.perm.class_map ? 'modx-combo-class-derivatives' : 'hidden', fieldLabel: _('resource_type'), description: MODx.expandHelp ? '' : _('resource_type_help'), name: 'class_key', hiddenName: 'class_key', anchor: '100%', allowBlank: false, - value: config.record.class_key || 'MODX\\Revolution\\modDocument' + value: classKey } ] }, { @@ -35,8 +36,7 @@ MODx.window.CreateResource = function(config = {}) { } ] } - ] - ; + ]; if (requireAlias) { resourceDetail.push({ columnWidth: 0.34, @@ -220,8 +220,7 @@ Ext.extend(MODx.combo.TemplatePicker, Ext.Panel, { loadItems: function(store, data) { const { value } = this, - items = [] - ; + items = []; let category = ''; Ext.each(data, function(record) { @@ -260,8 +259,7 @@ Ext.extend(MODx.combo.TemplatePicker, Ext.Panel, { fn: function(cmp) { const pickerValue = cmp.getValue(), - record = pickerValue?.record - ; + record = pickerValue?.record; if (record) { this.fireEvent('select', record); }