mirror of
https://github.com/silverstripe/silverstripe-reports
synced 2024-10-22 11:05:53 +02:00
BUG Enforce "add page" restrictions, improve UI (fixes #7879)
- Fix support for $allowed_children - Added note when type selection is restricted - Removed overly complex specs for "allowed children", the data should be inferred from "disallowed children" - Added support for SiteTree::$can_be_root - Return raw JSON (not entity-encoded) from CMSMain->SiteTreeHints() - Added tests for CMSMain->SiteTreeHints()
This commit is contained in:
parent
98cd09a852
commit
f9a5601fa3
@ -364,7 +364,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
|
|||||||
*/
|
*/
|
||||||
public function SiteTreeHints() {
|
public function SiteTreeHints() {
|
||||||
$json = '';
|
$json = '';
|
||||||
|
|
||||||
$classes = SiteTree::page_type_classes();
|
$classes = SiteTree::page_type_classes();
|
||||||
|
|
||||||
$cacheCanCreate = array();
|
$cacheCanCreate = array();
|
||||||
@ -377,55 +376,68 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
|
|||||||
$json = $cache->load($cacheKey);
|
$json = $cache->load($cacheKey);
|
||||||
if(!$json) {
|
if(!$json) {
|
||||||
$def['Root'] = array();
|
$def['Root'] = array();
|
||||||
$def['Root']['disallowedParents'] = array();
|
$def['Root']['disallowedChildren'] = array();
|
||||||
|
|
||||||
|
// Contains all possible classes to support UI controls listing them all,
|
||||||
|
// such as the "add page here" context menu.
|
||||||
|
$def['All'] = array();
|
||||||
|
|
||||||
|
// Identify disallows and set globals
|
||||||
|
$globalDisallowed = array();
|
||||||
|
foreach($classes as $class) {
|
||||||
|
$obj = singleton($class);
|
||||||
|
$needsPerm = $obj->stat('need_permission');
|
||||||
|
|
||||||
|
if(!($obj instanceof HiddenClass)) {
|
||||||
|
$def['All'][$class] = array(
|
||||||
|
'title' => $obj->i18n_singular_name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$obj->stat('can_be_root')) {
|
||||||
|
$def['Root']['disallowedChildren'][] = $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
($obj instanceof HiddenClass)
|
||||||
|
|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
|
||||||
|
|| ($needsPerm && !$this->can($needsPerm))
|
||||||
|
) {
|
||||||
|
$globalDisallowed[] = $class;
|
||||||
|
$def['Root']['disallowedChildren'][] = $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set disallows by class
|
||||||
foreach($classes as $class) {
|
foreach($classes as $class) {
|
||||||
$obj = singleton($class);
|
$obj = singleton($class);
|
||||||
if($obj instanceof HiddenClass) continue;
|
if($obj instanceof HiddenClass) continue;
|
||||||
|
|
||||||
$allowedChildren = $obj->allowedChildren();
|
$def[$class] = array();
|
||||||
|
|
||||||
// SiteTree::allowedChildren() returns null rather than an empty array if SiteTree::allowed_chldren == 'none'
|
$allowed = $obj->allowedChildren();
|
||||||
if($allowedChildren == null) $allowedChildren = array();
|
if($pos = array_search('SiteTree', $allowed)) unset($allowed[$pos]);
|
||||||
|
|
||||||
// Exclude SiteTree from possible Children
|
// Start by disallowing all classes which aren't specifically allowed,
|
||||||
$possibleChildren = array_diff($allowedChildren, array("SiteTree"));
|
// then add the ones which are globally disallowed.
|
||||||
|
$disallowed = array_diff($classes, (array)$allowed);
|
||||||
|
$disallowed = array_unique(array_merge($disallowed, $globalDisallowed));
|
||||||
|
if($disallowed) $def[$class]['disallowedChildren'] = $disallowed;
|
||||||
|
|
||||||
// Find i18n - names and build allowed children array
|
$defaultChild = $obj->defaultChild();
|
||||||
foreach($possibleChildren as $child) {
|
if($defaultChild != 'Page' && $defaultChild != null) {
|
||||||
$instance = singleton($child);
|
$def[$class]['defaultChild'] = $defaultChild;
|
||||||
|
|
||||||
if($instance instanceof HiddenClass) continue;
|
|
||||||
|
|
||||||
if(!array_key_exists($child, $cacheCanCreate) || !$cacheCanCreate[$child]) continue;
|
|
||||||
|
|
||||||
// skip this type if it is restricted
|
|
||||||
if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue;
|
|
||||||
|
|
||||||
$title = $instance->i18n_singular_name();
|
|
||||||
|
|
||||||
$def[$class]['allowedChildren'][] = array("ssclass" => $child, "ssname" => $title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowedChildren = array_keys(array_diff($classes, $allowedChildren));
|
|
||||||
if($allowedChildren) $def[$class]['disallowedChildren'] = $allowedChildren;
|
|
||||||
$defaultChild = $obj->defaultChild();
|
|
||||||
if($defaultChild != 'Page' && $defaultChild != null) $def[$class]['defaultChild'] = $defaultChild;
|
|
||||||
$defaultParent = $obj->defaultParent();
|
$defaultParent = $obj->defaultParent();
|
||||||
$parent = SiteTree::get_by_link($defaultParent);
|
$parent = SiteTree::get_by_link($defaultParent);
|
||||||
$id = $parent ? $parent->id : null;
|
$id = $parent ? $parent->id : null;
|
||||||
if ($defaultParent != 1 && $defaultParent != null) $def[$class]['defaultParent'] = $defaultParent;
|
if ($defaultParent != 1 && $defaultParent != null) {
|
||||||
if(isset($def[$class]['disallowedChildren'])) {
|
$def[$class]['defaultParent'] = $defaultParent;
|
||||||
foreach($def[$class]['disallowedChildren'] as $disallowedChild) {
|
|
||||||
$def[$disallowedChild]['disallowedParents'][] = $class;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are any classes allowed to be parents of root?
|
$json = Convert::raw2json($def);
|
||||||
$def['Root']['disallowedParents'][] = $class;
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Convert::raw2xml(Convert::raw2json($def));
|
|
||||||
$cache->save($json, $cacheKey);
|
$cache->save($json, $cacheKey);
|
||||||
}
|
}
|
||||||
return $json;
|
return $json;
|
||||||
|
@ -41,9 +41,11 @@ class CMSPageAddController extends CMSPageEditController {
|
|||||||
$fields = new FieldList(
|
$fields = new FieldList(
|
||||||
// new HiddenField("ParentID", false, ($this->parentRecord) ? $this->parentRecord->ID : null),
|
// new HiddenField("ParentID", false, ($this->parentRecord) ? $this->parentRecord->ID : null),
|
||||||
// TODO Should be part of the form attribute, but not possible in current form API
|
// TODO Should be part of the form attribute, but not possible in current form API
|
||||||
$hintsField = new LiteralField('Hints', sprintf('<span class="hints" data-hints="%s"></span>', $this->SiteTreeHints())),
|
$hintsField = new LiteralField(
|
||||||
|
'Hints',
|
||||||
|
sprintf('<span class="hints" data-hints="%s"></span>', Convert::raw2xml($this->SiteTreeHints()))
|
||||||
|
),
|
||||||
new LiteralField('PageModeHeader', sprintf($numericLabelTmpl, 1, _t('CMSMain.ChoosePageParentMode', 'Choose where to create this page'))),
|
new LiteralField('PageModeHeader', sprintf($numericLabelTmpl, 1, _t('CMSMain.ChoosePageParentMode', 'Choose where to create this page'))),
|
||||||
|
|
||||||
$parentModeField = new SelectionGroup(
|
$parentModeField = new SelectionGroup(
|
||||||
"ParentModeField",
|
"ParentModeField",
|
||||||
array(
|
array(
|
||||||
@ -62,6 +64,16 @@ class CMSPageAddController extends CMSPageEditController {
|
|||||||
sprintf($numericLabelTmpl, 2, _t('CMSMain.ChoosePageType', 'Choose page type')),
|
sprintf($numericLabelTmpl, 2, _t('CMSMain.ChoosePageType', 'Choose page type')),
|
||||||
$pageTypes,
|
$pageTypes,
|
||||||
'Page'
|
'Page'
|
||||||
|
),
|
||||||
|
new LiteralField(
|
||||||
|
'RestrictedNote',
|
||||||
|
sprintf(
|
||||||
|
'<p class="message notice message-restricted">%s</p>',
|
||||||
|
_t(
|
||||||
|
'CMSMain.AddPageRestriction',
|
||||||
|
'Note: Some page types are not allowed for this selection'
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// TODO Re-enable search once it allows for HTML title display,
|
// TODO Re-enable search once it allows for HTML title display,
|
||||||
|
@ -34,17 +34,22 @@
|
|||||||
var hints = this.find('.hints').data('hints'),
|
var hints = this.find('.hints').data('hints'),
|
||||||
metadata = this.find('#ParentID .TreeDropdownField').data('metadata'),
|
metadata = this.find('#ParentID .TreeDropdownField').data('metadata'),
|
||||||
id = this.find('#ParentID .TreeDropdownField').getValue(),
|
id = this.find('#ParentID .TreeDropdownField').getValue(),
|
||||||
newClassName = metadata ? metadata.ClassName : null,
|
newClassName = (id && metadata) ? metadata.ClassName : null,
|
||||||
hintKey = newClassName ? newClassName : 'Root',
|
hintKey = (newClassName) ? newClassName : 'Root',
|
||||||
hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null;
|
hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null,
|
||||||
|
allAllowed = true;
|
||||||
|
|
||||||
var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [],
|
var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [],
|
||||||
defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : null;
|
defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : null;
|
||||||
|
|
||||||
// Limit selection
|
// Limit selection
|
||||||
this.find('#PageType li').each(function() {
|
this.find('#PageType li').each(function() {
|
||||||
var className = $(this).find('input').val(), isAllowed = ($.inArray(className, disallowedChildren) == -1);
|
var className = $(this).find('input').val(),
|
||||||
|
isAllowed = ($.inArray(className, disallowedChildren) == -1);
|
||||||
|
|
||||||
$(this).setEnabled(isAllowed);
|
$(this).setEnabled(isAllowed);
|
||||||
|
if(!isAllowed) $(this).setSelected(false);
|
||||||
|
allAllowed = allAllowed && isAllowed;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default child selection, or fall back to first available option
|
// Set default child selection, or fall back to first available option
|
||||||
@ -59,6 +64,8 @@
|
|||||||
// Disable the "Create" button if none of the pagetypes are available
|
// Disable the "Create" button if none of the pagetypes are available
|
||||||
var buttonState = (this.find('#PageType li:not(.disabled)').length) ? 'enable' : 'disable';
|
var buttonState = (this.find('#PageType li:not(.disabled)').length) ? 'enable' : 'disable';
|
||||||
this.find('button[name=action_doAdd]').button(buttonState);
|
this.find('button[name=action_doAdd]').button(buttonState);
|
||||||
|
|
||||||
|
this.find('.message-restricted')[allAllowed ? 'hide' : 'show']();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,26 +8,7 @@
|
|||||||
config.contextmenu = {
|
config.contextmenu = {
|
||||||
'items': function(node) {
|
'items': function(node) {
|
||||||
|
|
||||||
// Build a list for allowed children as submenu entries
|
var menuitems = {
|
||||||
var pagetype = node.data('pagetype');
|
|
||||||
var id = node.data('id');
|
|
||||||
|
|
||||||
var allowedChildren = new Object;
|
|
||||||
$(hints[pagetype].allowedChildren).each(
|
|
||||||
function(key, val){
|
|
||||||
allowedChildren["allowedchildren-" + key ] = {
|
|
||||||
'label': '<span class="jstree-pageicon"></span>' + val.ssname,
|
|
||||||
'_class': 'class-' + val.ssclass,
|
|
||||||
'action': function(obj) {
|
|
||||||
$('.cms-container').entwine('.ss').loadPanel(ss.i18n.sprintf(
|
|
||||||
self.data('urlAddpage'), id, val.ssclass
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
var menuitems =
|
|
||||||
{
|
|
||||||
'edit': {
|
'edit': {
|
||||||
'label': ss.i18n._t('Tree.EditPage'),
|
'label': ss.i18n._t('Tree.EditPage'),
|
||||||
'action': function(obj) {
|
'action': function(obj) {
|
||||||
@ -37,13 +18,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Test if there are any allowed Children and thus the possibility of adding some
|
|
||||||
if(allowedChildren.hasOwnProperty('allowedchildren-0')) {
|
// Build a list for allowed children as submenu entries
|
||||||
|
var pagetype = node.data('pagetype'),
|
||||||
|
id = node.data('id'),
|
||||||
|
disallowedChildren = hints[pagetype].disallowedChildren,
|
||||||
|
allowedChildren = $.extend(true, {}, hints['All']), // clone
|
||||||
|
disallowedClass,
|
||||||
|
menuAllowedChildren = {},
|
||||||
|
hasAllowedChildren = false;
|
||||||
|
|
||||||
|
// Filter allowed
|
||||||
|
if(disallowedChildren) {
|
||||||
|
for(var i=0; i<disallowedChildren.length; i++) {
|
||||||
|
disallowedClass = disallowedChildren[i];
|
||||||
|
if(allowedChildren[disallowedClass]) {
|
||||||
|
delete allowedChildren[disallowedClass];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to menu entries
|
||||||
|
$.each(allowedChildren, function(klass, klassData){
|
||||||
|
hasAllowedChildren = true;
|
||||||
|
menuAllowedChildren["allowedchildren-" + klass ] = {
|
||||||
|
'label': '<span class="jstree-pageicon"></span>' + klassData.title,
|
||||||
|
'_class': 'class-' + klass,
|
||||||
|
'action': function(obj) {
|
||||||
|
$('.cms-container').entwine('.ss').loadPanel(ss.i18n.sprintf(
|
||||||
|
self.data('urlAddpage'), id, klass
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if(hasAllowedChildren) {
|
||||||
menuitems['addsubpage'] = {
|
menuitems['addsubpage'] = {
|
||||||
'label': ss.i18n._t('Tree.AddSubPage'),
|
'label': ss.i18n._t('Tree.AddSubPage'),
|
||||||
'submenu': allowedChildren
|
'submenu': menuAllowedChildren
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuitems;
|
return menuitems;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ $ExtraTreeTools
|
|||||||
</div>
|
</div>
|
||||||
<% end_if %>
|
<% end_if %>
|
||||||
|
|
||||||
<div class="cms-tree" data-url-tree="$Link(getsubtree)" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1')}&ParentID=%s&PageType=%s&SecurityID=$SecurityID" data-url-editpage="$LinkPageEdit('%s')" data-hints="$SiteTreeHints">
|
<div class="cms-tree" data-url-tree="$Link(getsubtree)" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1')}&ParentID=%s&PageType=%s&SecurityID=$SecurityID" data-url-editpage="$LinkPageEdit('%s')" data-hints="$SiteTreeHints.XML">
|
||||||
$SiteTreeAsUL
|
$SiteTreeAsUL
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,6 +26,62 @@ class CMSMainTest extends FunctionalTest {
|
|||||||
parent::tearDownOnce();
|
parent::tearDownOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testSiteTreeHints() {
|
||||||
|
$cache = SS_Cache::factory('CMSMain_SiteTreeHints');
|
||||||
|
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
|
||||||
|
|
||||||
|
$rawHints = singleton('CMSMain')->SiteTreeHints();
|
||||||
|
$this->assertNotNull($rawHints);
|
||||||
|
|
||||||
|
$rawHints = preg_replace('/^"(.*)"$/', '$1', Convert::xml2raw($rawHints));
|
||||||
|
$hints = Convert::json2array($rawHints);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('Root', $hints);
|
||||||
|
$this->assertArrayHasKey('Page', $hints);
|
||||||
|
$this->assertArrayHasKey('All', $hints);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey(
|
||||||
|
'CMSMainTest_ClassA',
|
||||||
|
$hints['All'],
|
||||||
|
'Global list shows allowed classes'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey(
|
||||||
|
'CMSMainTest_HiddenClass',
|
||||||
|
$hints['All'],
|
||||||
|
'Global list does not list hidden classes'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertNotContains(
|
||||||
|
'CMSMainTest_ClassA',
|
||||||
|
$hints['Root']['disallowedChildren'],
|
||||||
|
'Limits root classes'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
'CMSMainTest_NotRoot',
|
||||||
|
$hints['Root']['disallowedChildren'],
|
||||||
|
'Limits root classes'
|
||||||
|
);
|
||||||
|
$this->assertNotContains(
|
||||||
|
'CMSMainTest_ClassA',
|
||||||
|
// Lenient checks because other modules might influence state
|
||||||
|
(array)@$hints['Page']['disallowedChildren'],
|
||||||
|
'Does not limit types on unlimited parent'
|
||||||
|
);
|
||||||
|
$this->assertContains(
|
||||||
|
'Page',
|
||||||
|
$hints['CMSMainTest_ClassA']['disallowedChildren'],
|
||||||
|
'Limited parent lists disallowed classes'
|
||||||
|
);
|
||||||
|
$this->assertNotContains(
|
||||||
|
'CMSMainTest_ClassB',
|
||||||
|
$hints['CMSMainTest_ClassA']['disallowedChildren'],
|
||||||
|
'Limited parent omits explicitly allowed classes in disallowedChildren'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Test the results of a publication better
|
* @todo Test the results of a publication better
|
||||||
*/
|
*/
|
||||||
@ -294,3 +350,11 @@ class CMSMainTest_ClassA extends Page implements TestOnly {
|
|||||||
class CMSMainTest_ClassB extends Page implements TestOnly {
|
class CMSMainTest_ClassB extends Page implements TestOnly {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CMSMainTest_NotRoot extends Page implements TestOnly {
|
||||||
|
static $can_be_root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CMSMainTest_HiddenClass extends Page implements TestOnly, HiddenClass {
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user