Merge remote-tracking branch 'origin/3.1.11' into 3.1

This commit is contained in:
Damian Mooyman 2015-03-19 17:51:25 +13:00
commit 7f1f615634
12 changed files with 589 additions and 323 deletions

View File

@ -54,6 +54,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'treeview', 'treeview',
'listview', 'listview',
'ListViewForm', 'ListViewForm',
'childfilter',
); );
public function init() { public function init() {
@ -412,57 +413,35 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$def['All'] = array(); $def['All'] = array();
// Identify disallows and set globals // 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;
// Name item
$def['All'][$class] = array(
'title' => $obj->i18n_singular_name()
);
// Check if can be created at the root
$needsPerm = $obj->stat('need_permission');
if(
!$obj->stat('can_be_root')
|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
|| ($needsPerm && !$this->can($needsPerm))
) {
$def['Root']['disallowedChildren'][] = $class;
}
// Hint data specific to the class
$def[$class] = array(); $def[$class] = array();
$allowed = $obj->allowedChildren();
if($pos = array_search('SiteTree', $allowed)) unset($allowed[$pos]);
// Start by disallowing all classes which aren't specifically allowed,
// then add the ones which are globally disallowed.
$disallowed = array_diff($classes, (array)$allowed);
$disallowed = array_unique(array_merge($disallowed, $globalDisallowed));
// Re-index the array for JSON non sequential key issue
if($disallowed) $def[$class]['disallowedChildren'] = array_values($disallowed);
$defaultChild = $obj->defaultChild(); $defaultChild = $obj->defaultChild();
if($defaultChild != 'Page' && $defaultChild != null) { if($defaultChild !== 'Page' && $defaultChild !== null) {
$def[$class]['defaultChild'] = $defaultChild; $def[$class]['defaultChild'] = $defaultChild;
} }
$defaultParent = $obj->defaultParent(); $defaultParent = $obj->defaultParent();
$parent = SiteTree::get_by_link($defaultParent); if ($defaultParent !== 1 && $defaultParent !== null) {
$id = $parent ? $parent->id : null;
if ($defaultParent != 1 && $defaultParent != null) {
$def[$class]['defaultParent'] = $defaultParent; $def[$class]['defaultParent'] = $defaultParent;
} }
} }
@ -491,8 +470,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if($instance instanceof HiddenClass) continue; if($instance instanceof HiddenClass) continue;
if(!$instance->canCreate()) continue;
// skip this type if it is restricted // skip this type if it is restricted
if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue; if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue;
@ -706,6 +683,39 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $this->renderWith($this->getTemplatesWithSuffix('_ListView')); return $this->renderWith($this->getTemplatesWithSuffix('_ListView'));
} }
/**
* Callback to request the list of page types allowed under a given page instance.
* Provides a slower but more precise response over SiteTreeHints
*
* @param SS_HTTPRequest $request
* @return SS_HTTPResponse
*/
public function childfilter($request) {
// Check valid parent specified
$parentID = $request->requestVar('ParentID');
$parent = SiteTree::get()->byID($parentID);
if(!$parent || !$parent->exists()) return $this->httpError(404);
// Build hints specific to this class
// Identify disallows and set globals
$classes = SiteTree::page_type_classes();
$disallowedChildren = array();
foreach($classes as $class) {
$obj = singleton($class);
if($obj instanceof HiddenClass) continue;
if(!$obj->canCreate(null, array('Parent' => $parent))) {
$disallowedChildren[] = $class;
}
}
$this->extend('updateChildFilter', $disallowedChildren, $parentID);
return $this
->response
->addHeader('Content-Type', 'application/json; charset=utf-8')
->setBody(Convert::raw2json($disallowedChildren));
}
/** /**
* Safely reconstruct a selected filter from a given set of query parameters * Safely reconstruct a selected filter from a given set of query parameters
* *

View File

@ -16,7 +16,7 @@ class CMSPageAddController extends CMSPageEditController {
/** /**
* @return Form * @return Form
*/ */
function AddForm() { public function AddForm() {
$pageTypes = array(); $pageTypes = array();
foreach($this->PageTypes() as $type) { foreach($this->PageTypes() as $type) {
$html = sprintf('<span class="page-icon class-%s"></span><strong class="title">%s</strong><span class="description">%s</span>', $html = sprintf('<span class="page-icon class-%s"></span><strong class="title">%s</strong><span class="description">%s</span>',
@ -38,11 +38,6 @@ class CMSPageAddController extends CMSPageEditController {
$childTitle = _t('CMSPageAddController.ParentMode_child', 'Under another page'); $childTitle = _t('CMSPageAddController.ParentMode_child', 'Under another page');
$fields = new FieldList( $fields = new FieldList(
// 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>', 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",
@ -122,6 +117,9 @@ class CMSPageAddController extends CMSPageEditController {
$form = CMSForm::create( $form = CMSForm::create(
$this, "AddForm", $fields, $actions $this, "AddForm", $fields, $actions
)->setHTMLID('Form_AddForm'); )->setHTMLID('Form_AddForm');
$form->setAttribute('data-hints', $this->SiteTreeHints());
$form->setAttribute('data-childfilter', $this->Link('childfilter'));
$form->setResponseNegotiator($this->getResponseNegotiator()); $form->setResponseNegotiator($this->getResponseNegotiator());
$form->addExtraClass('cms-add-form stacked cms-content center cms-edit-form ' . $this->BaseCSSClasses()); $form->addExtraClass('cms-add-form stacked cms-content center cms-edit-form ' . $this->BaseCSSClasses());
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
@ -145,11 +143,7 @@ class CMSPageAddController extends CMSPageEditController {
if(!$parentObj || !$parentObj->ID) $parentID = 0; if(!$parentObj || !$parentObj->ID) $parentID = 0;
if($parentObj) { if(!singleton($className)->canCreate(Member::currentUser(), array('Parent' => $parentObj))) {
if(!$parentObj->canAddChildren()) return Security::permissionFailure($this);
if(!singleton($className)->canCreate()) return Security::permissionFailure($this);
} else {
if(!SiteConfig::current_site_config()->canCreateTopLevel())
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }

View File

@ -33,12 +33,26 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
"CreateTopLevelGroups" => "Group" "CreateTopLevelGroups" => "Group"
); );
private static $defaults = array(
"CanViewType" => "Anyone",
"CanEditType" => "LoggedInUsers",
"CanCreateTopLevelType" => "LoggedInUsers",
);
/** /**
* @config * @config
* @var array * @var array
*/ */
private static $disabled_themes = array(); private static $disabled_themes = array();
/**
* Default permission to check for 'LoggedInUsers' to create or edit pages
*
* @var array
* @config
*/
private static $required_permission = array('CMS_ACCESS_CMSMain', 'CMS_ACCESS_LeftAndMain');
/** /**
* @deprecated 3.2 Use the "SiteConfig.disabled_themes" config setting instead * @deprecated 3.2 Use the "SiteConfig.disabled_themes" config setting instead
*/ */
@ -226,26 +240,46 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
} }
/** /**
* Can a user view pages on this site? This method is only * Can a user view this SiteConfig instance?
* called if a page is set to Inherit, but there is nothing
* to inherit from.
* *
* @param mixed $member * @param Member $member
* @return boolean * @return boolean
*/ */
public function canView($member = null) { public function canView($member = null) {
if(!$member) $member = Member::currentUserID(); if(!$member) $member = Member::currentUserID();
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member); if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
$extended = $this->extendedCan('canView', $member);
if($extended !== null) return $extended;
// Assuming all that can edit this object can also view it
return $this->canEdit($member);
}
/**
* Can a user view pages on this site? This method is only
* called if a page is set to Inherit, but there is nothing
* to inherit from.
*
* @param Member $member
* @return boolean
*/
public function canViewPages($member = null) {
if(!$member) $member = Member::currentUserID();
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
if ($member && Permission::checkMember($member, "ADMIN")) return true; if ($member && Permission::checkMember($member, "ADMIN")) return true;
$extended = $this->extendedCan('canViewPages', $member);
if($extended !== null) return $extended;
if (!$this->CanViewType || $this->CanViewType == 'Anyone') return true; if (!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
// check for any logged-in users // check for any logged-in users
if($this->CanViewType == 'LoggedInUsers' && $member) return true; if($this->CanViewType === 'LoggedInUsers' && $member) return true;
// check for specific groups // check for specific groups
if($this->CanViewType == 'OnlyTheseUsers' && $member && $member->inGroups($this->ViewerGroups())) return true; if($this->CanViewType === 'OnlyTheseUsers' && $member && $member->inGroups($this->ViewerGroups())) return true;
return false; return false;
} }
@ -253,26 +287,45 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
/** /**
* Can a user edit pages on this site? This method is only * Can a user edit pages on this site? This method is only
* called if a page is set to Inherit, but there is nothing * called if a page is set to Inherit, but there is nothing
* to inherit from. * to inherit from, or on new records without a parent.
* *
* @param mixed $member * @param Member $member
* @return boolean * @return boolean
*/ */
public function canEdit($member = null) { public function canEditPages($member = null) {
if(!$member) $member = Member::currentUserID(); if(!$member) $member = Member::currentUserID();
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member); if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
if ($member && Permission::checkMember($member, "ADMIN")) return true; if ($member && Permission::checkMember($member, "ADMIN")) return true;
// check for any logged-in users $extended = $this->extendedCan('canEditPages', $member);
if(!$this->CanEditType || $this->CanEditType == 'LoggedInUsers' && $member) return true; if($extended !== null) return $extended;
// check for any logged-in users with CMS access
if( $this->CanEditType === 'LoggedInUsers'
&& Permission::checkMember($member, $this->config()->required_permission)
) {
return true;
}
// check for specific groups // check for specific groups
if($this->CanEditType == 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) return true; if($this->CanEditType === 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) {
return true;
}
return false; return false;
} }
public function canEdit($member = null) {
if(!$member) $member = Member::currentUserID();
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
$extended = $this->extendedCan('canEdit', $member);
if($extended !== null) return $extended;
return Permission::checkMember($member, "EDIT_SITECONFIG");
}
public function providePermissions() { public function providePermissions() {
return array( return array(
'EDIT_SITECONFIG' => array( 'EDIT_SITECONFIG' => array(
@ -287,25 +340,32 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
/** /**
* Can a user create pages in the root of this site? * Can a user create pages in the root of this site?
* *
* @param mixed $member * @param Member $member
* @return boolean * @return boolean
*/ */
public function canCreateTopLevel($member = null) { public function canCreateTopLevel($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) { if(!$member) $member = Member::currentUserID();
$member = Member::currentUserID(); if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
}
if (Permission::check('ADMIN')) return true;
if ($member && Permission::checkMember($member, "ADMIN")) return true; if ($member && Permission::checkMember($member, "ADMIN")) return true;
// check for any logged-in users $extended = $this->extendedCan('canCreateTopLevel', $member);
if($this->CanCreateTopLevelType == 'LoggedInUsers' && $member) return true; if($extended !== null) return $extended;
// check for any logged-in users with CMS permission
if( $this->CanCreateTopLevelType === 'LoggedInUsers'
&& Permission::checkMember($member, $this->config()->required_permission)
) {
return true;
}
// check for specific groups // check for specific groups
if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member); if( $this->CanCreateTopLevelType === 'OnlyTheseUsers'
if($this->CanCreateTopLevelType == 'OnlyTheseUsers' && $member && $member->inGroups($this->CreateTopLevelGroups())) return true; && $member
&& $member->inGroups($this->CreateTopLevelGroups())
) {
return true;
}
return false; return false;
} }

View File

@ -915,7 +915,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// check for inherit // check for inherit
if($this->CanViewType == 'Inherit') { if($this->CanViewType == 'Inherit') {
if($this->ParentID) return $this->Parent()->canView($member); if($this->ParentID) return $this->Parent()->canView($member);
else return $this->getSiteConfig()->canView($member); else return $this->getSiteConfig()->canViewPages($member);
} }
// check for any logged-in users // check for any logged-in users
@ -994,12 +994,12 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
/** /**
* This function should return true if the current user can create new * This function should return true if the current user can create new
* pages of this class. It can be overloaded to customise the security model for an * pages of this class, regardless of context. It can be overloaded
* application. * to customise the security model for an application.
* *
* Denies permission if any of the following conditions is TRUE: * By default, permission to create at the root level is based on the SiteConfig
* - canCreate() returns FALSE on any extension * configuration, and permission to create beneath a parent is based on the
* - $can_create is set to FALSE and the site is not in "dev mode" * ability to edit that parent page.
* *
* Use {@link canAddChildren()} to control behaviour of creating children under this page. * Use {@link canAddChildren()} to control behaviour of creating children under this page.
* *
@ -1007,6 +1007,9 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @uses DataExtension->canCreate() * @uses DataExtension->canCreate()
* *
* @param Member $member * @param Member $member
* @param array $context Optional array which may contain array('Parent' => $parentObj)
* If a parent page is known, it will be checked for validity.
* If omitted, it will be assumed this is to be created as a top level page.
* @return boolean True if the current user can create pages on this class. * @return boolean True if the current user can create pages on this class.
*/ */
public function canCreate($member = null) { public function canCreate($member = null) {
@ -1014,15 +1017,30 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$member = Member::currentUserID(); $member = Member::currentUserID();
} }
// Check parent (custom canCreate option for SiteTree)
// Block children not allowed for this parent type
$context = func_num_args() > 1 ? func_get_arg(1) : array();
$parent = isset($context['Parent']) ? $context['Parent'] : null;
if($parent && !in_array(get_class($this), $parent->allowedChildren())) return false;
// Check permission
if($member && Permission::checkMember($member, "ADMIN")) return true; if($member && Permission::checkMember($member, "ADMIN")) return true;
// Standard mechanism for accepting permission changes from extensions // Standard mechanism for accepting permission changes from extensions
$extended = $this->extendedCan('canCreate', $member); $results = $this->extend('canCreate', $member, $parent);
if($extended !== null) return $extended; if(is_array($results) && ($results = array_filter($results, function($v) {return $v !== null;}))) {
return min($results);
return $this->stat('can_create') != false || Director::isDev();
} }
// Fall over to inherited permissions
if($parent) {
return $parent->canAddChildren($member);
} else {
// This doesn't necessarily mean we are creating a root page, but that
// we don't know if there is a parent, so default to this permission
return SiteConfig::current_site_config()->canCreateTopLevel($member);
}
}
/** /**
* This function should return true if the current user can edit this * This function should return true if the current user can edit this
@ -1064,7 +1082,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// Default for unsaved pages // Default for unsaved pages
} else { } else {
return $this->getSiteConfig()->canEdit($member); return $this->getSiteConfig()->canEditPages($member);
} }
} }
@ -1276,7 +1294,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* page can be edited. * page can be edited.
*/ */
static public function can_edit_multiple($ids, $memberID, $useCached = true) { static public function can_edit_multiple($ids, $memberID, $useCached = true) {
return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEdit', null, $useCached); return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
} }
/** /**
@ -2701,9 +2719,20 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* @return string a html string ready to be directly used in a template * @return string a html string ready to be directly used in a template
*/ */
public function getTreeTitle() { public function getTreeTitle() {
// Build the list of candidate children
$children = array();
$candidates = static::page_type_classes();
foreach($this->allowedChildren() as $childClass) {
if(!in_array($childClass, $candidates)) continue;
$child = singleton($childClass);
if($child->canCreate(null, array('Parent' => $this))) {
$children[$childClass] = $child->i18n_singular_name();
}
}
$flags = $this->getStatusFlags(); $flags = $this->getStatusFlags();
$treeTitle = sprintf( $treeTitle = sprintf(
"<span class=\"jstree-pageicon\"></span><span class=\"item\">%s</span>", "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
Convert::raw2att(Convert::raw2json($children)),
Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle)) Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
); );
foreach($flags as $class => $data) { foreach($flags as $class => $data) {

View File

@ -15,38 +15,107 @@
}); });
$(".cms-add-form").entwine({ $(".cms-add-form").entwine({
ParentID: 0, // Last selected parentID
ParentCache: {}, // Cache allowed children for each selected page
onadd: function() { onadd: function() {
var self = this; var self = this;
this.find('#ParentID .TreeDropdownField').bind('change', function() { this.find('#ParentID .TreeDropdownField').bind('change', function() {
self.updateTypeList(); self.updateTypeList();
}); });
this.find(".SelectionGroup.parent-mode").bind('change', function() {
self.updateTypeList();
});
this.updateTypeList(); this.updateTypeList();
}, },
loadCachedChildren: function(parentID) {
var cache = this.getParentCache();
if(typeof cache[parentID] !== 'undefined') return cache[parentID];
else return null;
},
saveCachedChildren: function(parentID, children) {
var cache = this.getParentCache();
cache[parentID] = children;
this.setParentCache(cache);
},
/** /**
* Limit page type selection based on parent class. * Limit page type selection based on parent selection.
* Select of root classes is pre-computed, but selections with a given parent
* are updated on-demand.
* Similar implementation to LeftAndMain.Tree.js. * Similar implementation to LeftAndMain.Tree.js.
*/ */
updateTypeList: function() { updateTypeList: function() {
var hints = this.find('.hints').data('hints'), var hints = this.data('hints'),
metadata = this.find('#ParentID .TreeDropdownField').data('metadata'), parentTree = this.find('#ParentID .TreeDropdownField'),
id = this.find('#ParentID .TreeDropdownField').getValue(), parentMode = this.find("input[name=ParentModeField]:checked").val(),
newClassName = (id && metadata) ? metadata.ClassName : null, metadata = parentTree.data('metadata'),
hintKey = (newClassName) ? newClassName : 'Root', id = (metadata && parentMode === 'child')
hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null, ? (parentTree.getValue() || this.getParentID())
allAllowed = true; : null,
newClassName = metadata ? metadata.ClassName : null,
hintKey = (newClassName && parentMode === 'child')
? newClassName
: 'Root',
hint = (typeof hints[hintKey] !== 'undefined') ? hints[hintKey] : null,
self = this,
defaultChildClass = (hint && typeof hint.defaultChild !== 'undefined')
? hint.defaultChild
: null,
disallowedChildren = [];
var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [], if(id) {
defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : null; // Prevent interface operations
if(this.hasClass('loading')) return;
this.addClass('loading');
// Enable last parent ID to be re-selected from memory
this.setParentID(id);
if(!parentTree.getValue()) parentTree.setValue(id);
// Use cached data if available
disallowedChildren = this.loadCachedChildren(id);
if(disallowedChildren !== null) {
this.updateSelectionFilter(disallowedChildren, defaultChildClass);
this.removeClass('loading');
return;
}
$.ajax({
url: self.data('childfilter'),
data: {'ParentID': id},
success: function(data) {
// reload current form and tree
self.saveCachedChildren(id, data);
self.updateSelectionFilter(data, defaultChildClass);
},
complete: function() {
self.removeClass('loading');
}
});
return false;
} else {
disallowedChildren = (hint && typeof hint.disallowedChildren !== 'undefined')
? hint.disallowedChildren
: [],
this.updateSelectionFilter(disallowedChildren, defaultChildClass);
}
},
/**
* Update the selection filter with the given blacklist and default selection
*
* @param array disallowedChildren
* @param string defaultChildClass
*/
updateSelectionFilter: function(disallowedChildren, defaultChildClass) {
// Limit selection // Limit selection
var allAllowed = null; // troolian
this.find('#PageType li').each(function() { this.find('#PageType li').each(function() {
var className = $(this).find('input').val(), var className = $(this).find('input').val(),
isAllowed = ($.inArray(className, disallowedChildren) == -1); isAllowed = ($.inArray(className, disallowedChildren) === -1);
$(this).setEnabled(isAllowed); $(this).setEnabled(isAllowed);
if(!isAllowed) $(this).setSelected(false); if(!isAllowed) $(this).setSelected(false);
allAllowed = allAllowed && isAllowed; if(allAllowed === null) allAllowed = isAllowed;
else 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
@ -72,10 +141,13 @@
}, },
setSelected: function(bool) { setSelected: function(bool) {
var input = this.find('input'); var input = this.find('input');
this.toggleClass('selected', bool);
if(bool && !input.is(':disabled')) { if(bool && !input.is(':disabled')) {
this.siblings().setSelected(false); this.siblings().setSelected(false);
input.attr('checked', 'checked'); this.toggleClass('selected', true);
input.prop('checked', true);
} else {
this.toggleClass('selected', false);
input.prop('checked', false);
} }
}, },
setEnabled: function(bool) { setEnabled: function(bool) {

View File

@ -68,27 +68,15 @@
// Build a list for allowed children as submenu entries // Build a list for allowed children as submenu entries
var pagetype = node.data('pagetype'), var pagetype = node.data('pagetype'),
id = node.data('id'), id = node.data('id'),
disallowedChildren = (typeof hints[pagetype] != 'undefined') ? hints[pagetype].disallowedChildren : null, allowedChildren = node.find('>a .item').data('allowedchildren'),
allowedChildren = $.extend(true, {}, hints['All']), // clone
disallowedClass,
menuAllowedChildren = {}, menuAllowedChildren = {},
hasAllowedChildren = false; 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 // Convert to menu entries
$.each(allowedChildren, function(klass, klassData){ $.each(allowedChildren, function(klass, title){
hasAllowedChildren = true; hasAllowedChildren = true;
menuAllowedChildren["allowedchildren-" + klass ] = { menuAllowedChildren["allowedchildren-" + klass ] = {
'label': '<span class="jstree-pageicon"></span>' + klassData.title, 'label': '<span class="jstree-pageicon"></span>' + title,
'_class': 'class-' + klass, '_class': 'class-' + klass,
'action': function(obj) { 'action': function(obj) {
$('.cms-container').entwine('.ss').loadPanel( $('.cms-container').entwine('.ss').loadPanel(

View File

@ -19,7 +19,7 @@ $ExtraTreeTools
</div> </div>
<% end_if %> <% end_if %>
<div class="cms-tree" data-url-tree="$LinkWithSearch($Link(getsubtree))" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&amp;PageType=%s')}" data-url-editpage="$LinkPageEdit('%s')" data-url-duplicate="{$Link('duplicate/%s')}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s')}" data-url-listview="{$Link('?view=list')}" data-hints="$SiteTreeHints.XML" data-extra-params="SecurityID=$SecurityID"> <div class="cms-tree" data-url-tree="$LinkWithSearch($Link(getsubtree))" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&amp;PageType=%s')}" data-url-editpage="$LinkPageEdit('%s')" data-url-duplicate="{$Link('duplicate/%s')}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s')}" data-url-listview="{$Link('?view=list')}" data-hints="$SiteTreeHints.XML" data-childfilter="$Link('childfilter')" data-extra-params="SecurityID=$SecurityID">
$SiteTreeAsUL $SiteTreeAsUL
</div> </div>
</div> </div>

View File

@ -11,6 +11,9 @@ class CMSMainTest extends FunctionalTest {
function testSiteTreeHints() { function testSiteTreeHints() {
$cache = SS_Cache::factory('CMSMain_SiteTreeHints'); $cache = SS_Cache::factory('CMSMain_SiteTreeHints');
// Login as user with root creation privileges
$user = $this->objFromFixture('Member', 'rootedituser');
$user->logIn();
$cache->clean(Zend_Cache::CLEANING_MODE_ALL); $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
$rawHints = singleton('CMSMain')->SiteTreeHints(); $rawHints = singleton('CMSMain')->SiteTreeHints();
@ -46,23 +49,36 @@ class CMSMainTest extends FunctionalTest {
$hints['Root']['disallowedChildren'], $hints['Root']['disallowedChildren'],
'Limits root classes' 'Limits root classes'
); );
$this->assertNotContains(
'CMSMainTest_ClassA', }
// Lenient checks because other modules might influence state
(array)@$hints['Page']['disallowedChildren'], public function testChildFilter() {
'Does not limit types on unlimited parent' $this->logInWithPermission('ADMIN');
);
// Check page A
$pageA = new CMSMainTest_ClassA();
$pageA->write();
$pageB = new CMSMainTest_ClassB();
$pageB->write();
// Check query
$response = $this->get('CMSMain/childfilter?ParentID='.$pageA->ID);
$children = json_decode($response->getBody());
$this->assertFalse($response->isError());
// Page A can't have unrelated children
$this->assertContains( $this->assertContains(
'Page', 'Page',
$hints['CMSMainTest_ClassA']['disallowedChildren'], $children,
'Limited parent lists disallowed classes' 'Limited parent lists disallowed classes'
); );
// But it can create a ClassB
$this->assertNotContains( $this->assertNotContains(
'CMSMainTest_ClassB', 'CMSMainTest_ClassB',
$hints['CMSMainTest_ClassA']['disallowedChildren'], $children,
'Limited parent omits explicitly allowed classes in disallowedChildren' 'Limited parent omits explicitly allowed classes in disallowedChildren'
); );
} }
/** /**
@ -300,11 +316,7 @@ class CMSMainTest extends FunctionalTest {
'admin/pages/add/AddForm', 'admin/pages/add/AddForm',
array('ParentID' => $newPageId, 'PageType' => 'Page', 'Locale' => 'en_US', 'action_doAdd' => 1) array('ParentID' => $newPageId, 'PageType' => 'Page', 'Locale' => 'en_US', 'action_doAdd' => 1)
); );
$this->assertFalse($response->isError()); $this->assertEquals(403, $response->getStatusCode(), 'Add disallowed child should fail');
$this->assertContains(
htmlentities(_t('SiteTree.PageTypeNotAllowed', array('type' => 'Page'))),
$response->getBody()
);
$this->session()->inst_set('loggedInAs', NULL); $this->session()->inst_set('loggedInAs', NULL);

View File

@ -8,12 +8,14 @@
*/ */
class SiteConfigTest extends SapphireTest { class SiteConfigTest extends SapphireTest {
protected static $fixture_file = 'SiteConfigTest.yml';
protected $illegalExtensions = array( protected $illegalExtensions = array(
'SiteTree' => array('SiteTreeSubsites') 'SiteTree' => array('SiteTreeSubsites')
); );
public function testAvailableThemes() { public function testAvailableThemes() {
$config = SiteConfig::current_site_config(); $config = $this->objFromFixture('SiteConfig', 'default');
$ds = DIRECTORY_SEPARATOR; $ds = DIRECTORY_SEPARATOR;
$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes'; $testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
@ -35,4 +37,49 @@ class SiteConfigTest extends SapphireTest {
Filesystem::removeFolder($testThemeBaseDir); Filesystem::removeFolder($testThemeBaseDir);
} }
public function testCanCreateRootPages() {
$config = $this->objFromFixture('SiteConfig', 'default');
// Log in without pages admin access
$this->logInWithPermission('CMS_ACCESS_AssetAdmin');
$this->assertFalse($config->canCreateTopLevel());
// Login with necessary edit permission
$perms = SiteConfig::config()->required_permission;
$this->logInWithPermission(reset($perms));
$this->assertTrue($config->canCreateTopLevel());
}
public function testCanViewPages() {
$config = $this->objFromFixture('SiteConfig', 'default');
$this->assertTrue($config->canViewPages());
}
public function testCanEdit() {
$config = $this->objFromFixture('SiteConfig', 'default');
// Unrelated permissions don't allow siteconfig
$this->logInWithPermission('CMS_ACCESS_AssetAdmin');
$this->assertFalse($config->canEdit());
// Only those with edit permission can do this
$this->logInWithPermission('EDIT_SITECONFIG');
$this->assertTrue($config->canEdit());
}
public function testCanEditPages() {
$config = $this->objFromFixture('SiteConfig', 'default');
// Log in without pages admin access
$this->logInWithPermission('CMS_ACCESS_AssetAdmin');
$this->assertFalse($config->canEditPages());
// Login with necessary edit permission
$perms = SiteConfig::config()->required_permission;
$this->logInWithPermission(reset($perms));
$this->assertTrue($config->canEditPages());
}
} }

View File

@ -0,0 +1,7 @@
SiteConfig:
default:
Title: My test site
Tagline: Default site config
CanViewType: Anyone
CanEditType: LoggedInUsers
CanCreateTopLevelType: LoggedInUsers

View File

@ -21,6 +21,13 @@ class SiteTreeTest extends SapphireTest {
'SiteTreeTest_StageStatusInherit', 'SiteTreeTest_StageStatusInherit',
); );
/**
* Ensure any current member is logged out
*/
public function logOut() {
if($member = Member::currentUser()) $member->logOut();
}
public function testCreateDefaultpages() { public function testCreateDefaultpages() {
$remove = DataObject::get('SiteTree'); $remove = DataObject::get('SiteTree');
if($remove) foreach($remove as $page) $page->delete(); if($remove) foreach($remove as $page) $page->delete();
@ -438,10 +445,15 @@ class SiteTreeTest extends SapphireTest {
$editor = $this->objFromFixture("Member", "editor"); $editor = $this->objFromFixture("Member", "editor");
$home = $this->objFromFixture("Page", "home"); $home = $this->objFromFixture("Page", "home");
$staff = $this->objFromFixture("Page", "staff");
$products = $this->objFromFixture("Page", "products"); $products = $this->objFromFixture("Page", "products");
$product1 = $this->objFromFixture("Page", "product1"); $product1 = $this->objFromFixture("Page", "product1");
$product4 = $this->objFromFixture("Page", "product4"); $product4 = $this->objFromFixture("Page", "product4");
// Test logged out users cannot edit
$this->logOut();
$this->assertFalse($staff->canEdit());
// Can't edit a page that is locked to admins // Can't edit a page that is locked to admins
$this->assertFalse($home->canEdit($editor)); $this->assertFalse($home->canEdit($editor));
@ -465,6 +477,33 @@ class SiteTreeTest extends SapphireTest {
$this->assertFalse($page->canEdit($securityAdminMember)); $this->assertFalse($page->canEdit($securityAdminMember));
} }
public function testCreatePermissions() {
// Test logged out users cannot create
$this->logOut();
$this->assertFalse(singleton('SiteTree')->canCreate());
// Login with another permission
$this->logInWithPermission('DUMMY');
$this->assertFalse(singleton('SiteTree')->canCreate());
// Login with basic CMS permission
$perms = SiteConfig::config()->required_permission;
$this->logInWithPermission(reset($perms));
$this->assertTrue(singleton('SiteTree')->canCreate());
// Test creation underneath a parent which this user doesn't have access to
$parent = $this->objFromFixture('Page', 'about');
$this->assertFalse(singleton('SiteTree')->canCreate(null, array('Parent' => $parent)));
// Test creation underneath a parent which doesn't allow a certain child
$parentB = new SiteTreeTest_ClassB();
$parentB->Title = 'Only Allows SiteTreeTest_ClassC';
$parentB->write();
$this->assertTrue(singleton('SiteTreeTest_ClassA')->canCreate(null));
$this->assertFalse(singleton('SiteTreeTest_ClassA')->canCreate(null, array('Parent' => $parentB)));
$this->assertTrue(singleton('SiteTreeTest_ClassC')->canCreate(null, array('Parent' => $parentB)));
}
public function testEditPermissionsOnDraftVsLive() { public function testEditPermissionsOnDraftVsLive() {
// Create an inherit-permission page // Create an inherit-permission page
$page = new Page(); $page = new Page();
@ -502,7 +541,7 @@ class SiteTreeTest extends SapphireTest {
// Confirm that Member.editor can still edit the page // Confirm that Member.editor can still edit the page
$this->objFromFixture('Member','editor')->logIn(); $this->objFromFixture('Member','editor')->logIn();
$this->assertTrue($page->canEdit()); $this->assertTrue($page->canEdit());
} }
public function testCompareVersions() { public function testCompareVersions() {
// Necessary to avoid // Necessary to avoid

View File

@ -1,3 +1,11 @@
SiteConfig:
default:
Title: My test site
Tagline: Default site config
CanViewType: Anyone
CanEditType: LoggedInUsers
CanCreateTopLevelType: LoggedInUsers
Group: Group:
editors: editors:
Title: Editors Title: Editors