diff --git a/code/controllers/CMSMain.php b/code/controllers/CMSMain.php
index 701e3c93..f5225cbd 100644
--- a/code/controllers/CMSMain.php
+++ b/code/controllers/CMSMain.php
@@ -54,6 +54,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'treeview',
'listview',
'ListViewForm',
+ 'childfilter',
);
public function init() {
@@ -409,60 +410,38 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Contains all possible classes to support UI controls listing them all,
// such as the "add page here" context menu.
- $def['All'] = array();
+ $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) {
$obj = singleton($class);
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();
- $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();
- if($defaultChild != 'Page' && $defaultChild != null) {
+ if($defaultChild !== 'Page' && $defaultChild !== null) {
$def[$class]['defaultChild'] = $defaultChild;
}
$defaultParent = $obj->defaultParent();
- $parent = SiteTree::get_by_link($defaultParent);
- $id = $parent ? $parent->id : null;
- if ($defaultParent != 1 && $defaultParent != null) {
+ if ($defaultParent !== 1 && $defaultParent !== null) {
$def[$class]['defaultParent'] = $defaultParent;
}
}
@@ -491,8 +470,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if($instance instanceof HiddenClass) continue;
- if(!$instance->canCreate()) continue;
-
// skip this type if it is restricted
if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue;
@@ -705,6 +682,39 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function listview($request) {
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
diff --git a/code/controllers/CMSPageAddController.php b/code/controllers/CMSPageAddController.php
index bace5550..52761591 100644
--- a/code/controllers/CMSPageAddController.php
+++ b/code/controllers/CMSPageAddController.php
@@ -16,7 +16,7 @@ class CMSPageAddController extends CMSPageEditController {
/**
* @return Form
*/
- function AddForm() {
+ public function AddForm() {
$pageTypes = array();
foreach($this->PageTypes() as $type) {
$html = sprintf('%s%s',
@@ -38,11 +38,6 @@ class CMSPageAddController extends CMSPageEditController {
$childTitle = _t('CMSPageAddController.ParentMode_child', 'Under another page');
$fields = new FieldList(
- // TODO Should be part of the form attribute, but not possible in current form API
- $hintsField = new LiteralField(
- 'Hints',
- sprintf('', Convert::raw2xml($this->SiteTreeHints()))
- ),
new LiteralField('PageModeHeader', sprintf($numericLabelTmpl, 1, _t('CMSMain.ChoosePageParentMode', 'Choose where to create this page'))),
$parentModeField = new SelectionGroup(
"ParentModeField",
@@ -68,7 +63,7 @@ class CMSPageAddController extends CMSPageEditController {
$typeField = new OptionsetField(
"PageType",
sprintf($numericLabelTmpl, 2, _t('CMSMain.ChoosePageType', 'Choose page type')),
- $pageTypes,
+ $pageTypes,
'Page'
),
new LiteralField(
@@ -78,7 +73,7 @@ class CMSPageAddController extends CMSPageEditController {
_t(
'CMSMain.AddPageRestriction',
'Note: Some page types are not allowed for this selection'
- )
+ )
)
)
);
@@ -122,6 +117,9 @@ class CMSPageAddController extends CMSPageEditController {
$form = CMSForm::create(
$this, "AddForm", $fields, $actions
)->setHTMLID('Form_AddForm');
+ $form->setAttribute('data-hints', $this->SiteTreeHints());
+ $form->setAttribute('data-childfilter', $this->Link('childfilter'));
+
$form->setResponseNegotiator($this->getResponseNegotiator());
$form->addExtraClass('cms-add-form stacked cms-content center cms-edit-form ' . $this->BaseCSSClasses());
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
@@ -145,12 +143,8 @@ class CMSPageAddController extends CMSPageEditController {
if(!$parentObj || !$parentObj->ID) $parentID = 0;
- if($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);
+ if(!singleton($className)->canCreate(Member::currentUser(), array('Parent' => $parentObj))) {
+ return Security::permissionFailure($this);
}
$record = $this->getNewItem("new-$className-$parentID".$suffix, false);
diff --git a/code/model/SiteConfig.php b/code/model/SiteConfig.php
index 01cfc00a..eadf4245 100644
--- a/code/model/SiteConfig.php
+++ b/code/model/SiteConfig.php
@@ -32,12 +32,26 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
"EditorGroups" => "Group",
"CreateTopLevelGroups" => "Group"
);
+
+ private static $defaults = array(
+ "CanViewType" => "Anyone",
+ "CanEditType" => "LoggedInUsers",
+ "CanCreateTopLevelType" => "LoggedInUsers",
+ );
/**
* @config
* @var 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
@@ -230,48 +244,70 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
* called if a page is set to Inherit, but there is nothing
* to inherit from.
*
- * @param mixed $member
+ * @param Member $member
* @return boolean
*/
- public function canView($member = null) {
+ 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;
+ $extended = $this->extendedCan('canViewPages', $member);
+ if($extended !== null) return $extended;
+
if (!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
// check for any logged-in users
- if($this->CanViewType == 'LoggedInUsers' && $member) return true;
+ if($this->CanViewType === 'LoggedInUsers' && $member) return true;
// 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;
}
-
+
/**
* Can a user edit pages on this site? This method is only
* 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
*/
- public function canEdit($member = null) {
+ public function canEditPages($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;
- // check for any logged-in users
- if(!$this->CanEditType || $this->CanEditType == 'LoggedInUsers' && $member) return true;
+ $extended = $this->extendedCan('canEditPages', $member);
+ 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
- if($this->CanEditType == 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) return true;
+ if($this->CanEditType === 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) {
+ return true;
+ }
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() {
return array(
@@ -287,25 +323,32 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba
/**
* Can a user create pages in the root of this site?
*
- * @param mixed $member
+ * @param Member $member
* @return boolean
*/
public function canCreateTopLevel($member = null) {
- if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
- $member = Member::currentUserID();
- }
-
- if (Permission::check('ADMIN')) return true;
+ 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;
+
+ $extended = $this->extendedCan('canCreateTopLevel', $member);
+ if($extended !== null) return $extended;
- // check for any logged-in users
- if($this->CanCreateTopLevelType == 'LoggedInUsers' && $member) return true;
+ // 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
- if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
- if($this->CanCreateTopLevelType == 'OnlyTheseUsers' && $member && $member->inGroups($this->CreateTopLevelGroups())) return true;
-
+ if( $this->CanCreateTopLevelType === 'OnlyTheseUsers'
+ && $member
+ && $member->inGroups($this->CreateTopLevelGroups())
+ ) {
+ return true;
+ }
return false;
}
diff --git a/code/model/SiteTree.php b/code/model/SiteTree.php
index 553ce046..b4ddd548 100644
--- a/code/model/SiteTree.php
+++ b/code/model/SiteTree.php
@@ -915,7 +915,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// check for inherit
if($this->CanViewType == 'Inherit') {
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
@@ -994,12 +994,12 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
/**
* 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
- * application.
+ * pages of this class, regardless of context. It can be overloaded
+ * to customise the security model for an application.
*
- * Denies permission if any of the following conditions is TRUE:
- * - canCreate() returns FALSE on any extension
- * - $can_create is set to FALSE and the site is not in "dev mode"
+ * By default, permission to create at the root level is based on the SiteConfig
+ * configuration, and permission to create beneath a parent is based on the
+ * ability to edit that parent 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()
*
* @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.
*/
public function canCreate($member = null) {
@@ -1014,15 +1017,30 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$member = Member::currentUserID();
}
- if($member && Permission::checkMember($member, "ADMIN")) return true;
-
- // Standard mechanism for accepting permission changes from extensions
- $extended = $this->extendedCan('canCreate', $member);
- if($extended !== null) return $extended;
-
- return $this->stat('can_create') != false || Director::isDev();
- }
+ // 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;
+
+ // Standard mechanism for accepting permission changes from extensions
+ $results = $this->extend('canCreate', $member, $parent);
+ if(is_array($results) && ($results = array_filter($results, function($v) {return $v !== null;}))) {
+ return min($results);
+ }
+
+ // 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
@@ -1064,7 +1082,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// Default for unsaved pages
} else {
- return $this->getSiteConfig()->canEdit($member);
+ return $this->getSiteConfig()->canEditPages($member);
}
}
@@ -1260,7 +1278,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if(empty(self::$cache_permissions[$cacheKey])) self::$cache_permissions[$cacheKey] = array();
self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
- return $combinedStageResult;
+ return $combinedStageResult;
} else {
return array();
}
@@ -1276,7 +1294,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
* page can be edited.
*/
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
*/
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();
$treeTitle = sprintf(
- "%s",
+ "%s",
+ Convert::raw2att(Convert::raw2json($children)),
Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
);
foreach($flags as $class => $data) {
diff --git a/javascript/CMSMain.AddForm.js b/javascript/CMSMain.AddForm.js
index ce8fc24f..82ddba79 100644
--- a/javascript/CMSMain.AddForm.js
+++ b/javascript/CMSMain.AddForm.js
@@ -15,38 +15,107 @@
});
$(".cms-add-form").entwine({
+ ParentID: 0, // Last selected parentID
+ ParentCache: {}, // Cache allowed children for each selected page
onadd: function() {
var self = this;
this.find('#ParentID .TreeDropdownField').bind('change', function() {
self.updateTypeList();
});
+ this.find(".SelectionGroup.parent-mode").bind('change', function() {
+ self.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.
*/
updateTypeList: function() {
- var hints = this.find('.hints').data('hints'),
- metadata = this.find('#ParentID .TreeDropdownField').data('metadata'),
- id = this.find('#ParentID .TreeDropdownField').getValue(),
- newClassName = (id && metadata) ? metadata.ClassName : null,
- hintKey = (newClassName) ? newClassName : 'Root',
- hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null,
- allAllowed = true;
-
- var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [],
- defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : null;
-
+ var hints = this.data('hints'),
+ parentTree = this.find('#ParentID .TreeDropdownField'),
+ parentMode = this.find("input[name=ParentModeField]:checked").val(),
+ metadata = parentTree.data('metadata'),
+ id = (metadata && parentMode === 'child')
+ ? (parentTree.getValue() || this.getParentID())
+ : 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 = [];
+
+ if(id) {
+ // 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
+ var allAllowed = null; // troolian
this.find('#PageType li').each(function() {
var className = $(this).find('input').val(),
- isAllowed = ($.inArray(className, disallowedChildren) == -1);
+ isAllowed = ($.inArray(className, disallowedChildren) === -1);
$(this).setEnabled(isAllowed);
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
@@ -72,10 +141,13 @@
},
setSelected: function(bool) {
var input = this.find('input');
- this.toggleClass('selected', bool);
if(bool && !input.is(':disabled')) {
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) {
diff --git a/javascript/CMSMain.Tree.js b/javascript/CMSMain.Tree.js
index a2f2ac6a..4416d537 100644
--- a/javascript/CMSMain.Tree.js
+++ b/javascript/CMSMain.Tree.js
@@ -68,27 +68,15 @@
// Build a list for allowed children as submenu entries
var pagetype = node.data('pagetype'),
id = node.data('id'),
- disallowedChildren = (typeof hints[pagetype] != 'undefined') ? hints[pagetype].disallowedChildren : null,
- allowedChildren = $.extend(true, {}, hints['All']), // clone
- disallowedClass,
+ allowedChildren = node.find('>a .item').data('allowedchildren'),
menuAllowedChildren = {},
hasAllowedChildren = false;
- // Filter allowed
- if(disallowedChildren) {
- for(var i=0; i' + klassData.title,
+ 'label': '' + title,
'_class': 'class-' + klass,
'action': function(obj) {
$('.cms-container').entwine('.ss').loadPanel(
diff --git a/templates/Includes/CMSMain_TreeView.ss b/templates/Includes/CMSMain_TreeView.ss
index a755196f..06bc30b1 100644
--- a/templates/Includes/CMSMain_TreeView.ss
+++ b/templates/Includes/CMSMain_TreeView.ss
@@ -19,7 +19,7 @@ $ExtraTreeTools
<% end_if %>
-
diff --git a/tests/controller/CMSMainTest.php b/tests/controller/CMSMainTest.php
index 5bcf119a..26d043ed 100644
--- a/tests/controller/CMSMainTest.php
+++ b/tests/controller/CMSMainTest.php
@@ -11,6 +11,9 @@ class CMSMainTest extends FunctionalTest {
function testSiteTreeHints() {
$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);
$rawHints = singleton('CMSMain')->SiteTreeHints();
@@ -46,23 +49,36 @@ class CMSMainTest extends FunctionalTest {
$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'
- );
+
+ }
+
+ public function testChildFilter() {
+ $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(
'Page',
- $hints['CMSMainTest_ClassA']['disallowedChildren'],
+ $children,
'Limited parent lists disallowed classes'
);
+
+ // But it can create a ClassB
$this->assertNotContains(
'CMSMainTest_ClassB',
- $hints['CMSMainTest_ClassA']['disallowedChildren'],
+ $children,
'Limited parent omits explicitly allowed classes in disallowedChildren'
);
-
}
/**
@@ -300,11 +316,7 @@ class CMSMainTest extends FunctionalTest {
'admin/pages/add/AddForm',
array('ParentID' => $newPageId, 'PageType' => 'Page', 'Locale' => 'en_US', 'action_doAdd' => 1)
);
- $this->assertFalse($response->isError());
- $this->assertContains(
- htmlentities(_t('SiteTree.PageTypeNotAllowed', array('type' => 'Page'))),
- $response->getBody()
- );
+ $this->assertEquals(403, $response->getStatusCode(), 'Add disallowed child should fail');
$this->session()->inst_set('loggedInAs', NULL);
diff --git a/tests/model/SiteConfigTest.php b/tests/model/SiteConfigTest.php
index f4626ad2..2602989e 100644
--- a/tests/model/SiteConfigTest.php
+++ b/tests/model/SiteConfigTest.php
@@ -7,13 +7,15 @@
* SiteTreePermissionsTest
*/
class SiteConfigTest extends SapphireTest {
+
+ protected static $fixture_file = 'SiteConfigTest.yml';
protected $illegalExtensions = array(
'SiteTree' => array('SiteTreeSubsites')
);
public function testAvailableThemes() {
- $config = SiteConfig::current_site_config();
+ $config = $this->objFromFixture('SiteConfig', 'default');
$ds = DIRECTORY_SEPARATOR;
$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
@@ -34,5 +36,50 @@ class SiteConfigTest extends SapphireTest {
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());
+ }
+
+
}
diff --git a/tests/model/SiteConfigTest.yml b/tests/model/SiteConfigTest.yml
new file mode 100644
index 00000000..a7c54988
--- /dev/null
+++ b/tests/model/SiteConfigTest.yml
@@ -0,0 +1,7 @@
+SiteConfig:
+ default:
+ Title: My test site
+ Tagline: Default site config
+ CanViewType: Anyone
+ CanEditType: LoggedInUsers
+ CanCreateTopLevelType: LoggedInUsers
diff --git a/tests/model/SiteTreeTest.php b/tests/model/SiteTreeTest.php
index 7f0dda50..b7bdd479 100644
--- a/tests/model/SiteTreeTest.php
+++ b/tests/model/SiteTreeTest.php
@@ -20,24 +20,31 @@ class SiteTreeTest extends SapphireTest {
'SiteTreeTest_NotRoot',
'SiteTreeTest_StageStatusInherit',
);
+
+ /**
+ * Ensure any current member is logged out
+ */
+ public function logOut() {
+ if($member = Member::currentUser()) $member->logOut();
+ }
public function testCreateDefaultpages() {
$remove = DataObject::get('SiteTree');
if($remove) foreach($remove as $page) $page->delete();
// Make sure the table is empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
-
+
// Disable the creation
SiteTree::config()->create_default_pages = false;
singleton('SiteTree')->requireDefaultRecords();
-
+
// The table should still be empty
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
-
+
// Enable the creation
SiteTree::config()->create_default_pages = true;
singleton('SiteTree')->requireDefaultRecords();
-
+
// The table should now have three rows (home, about-us, contact-us)
$this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
}
@@ -63,64 +70,64 @@ class SiteTreeTest extends SapphireTest {
'controller' => 'controller-2',
'numericonly' => '1930',
);
-
+
foreach($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture('Page', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
-
+
/**
* Test that publication copies data to SiteTree_Live
*/
public function testPublishCopiesToLiveTable() {
$obj = $this->objFromFixture('Page','about');
$obj->publish('Stage', 'Live');
-
+
$createdID = DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'")->value();
$this->assertEquals($obj->ID, $createdID);
}
-
+
/**
* Test that field which are set and then cleared are also transferred to the published site.
*/
public function testPublishDeletedFields() {
$this->logInWithPermission('ADMIN');
-
+
$obj = $this->objFromFixture('Page', 'about');
$obj->Title = "asdfasdf";
$obj->write();
$this->assertTrue($obj->doPublish());
-
+
$this->assertEquals('asdfasdf', DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
-
+
$obj->Title = null;
$obj->write();
$this->assertTrue($obj->doPublish());
-
+
$this->assertNull(DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
-
+
}
-
+
public function testParentNodeCachedInMemory() {
$parent = new SiteTree();
$parent->Title = 'Section Title';
$child = new SiteTree();
$child->Title = 'Page Title';
$child->setParent($parent);
-
+
$this->assertInstanceOf("SiteTree", $child->Parent);
$this->assertEquals("Section Title", $child->Parent->Title);
}
-
+
public function testParentModelReturnType() {
$parent = new SiteTreeTest_PageNode();
$child = new SiteTreeTest_PageNode();
-
+
$child->setParent($parent);
$this->assertInstanceOf('SiteTreeTest_PageNode', $child->Parent);
}
-
+
/**
* Confirm that DataObject::get_one() gets records from SiteTree_Live
*/
@@ -132,41 +139,41 @@ class SiteTreeTest extends SapphireTest {
$s->publish("Stage", "Live");
$s->Title = "V2";
$s->write();
-
+
$oldMode = Versioned::get_reading_mode();
Versioned::reading_stage('Live');
-
+
$checkSiteTree = DataObject::get_one("SiteTree", "\"SiteTree\".\"URLSegment\" = 'get-one-test-page'");
$this->assertEquals("V1", $checkSiteTree->Title);
-
+
Versioned::set_reading_mode($oldMode);
}
-
+
public function testChidrenOfRootAreTopLevelPages() {
$pages = DataObject::get("SiteTree");
foreach($pages as $page) $page->publish('Stage', 'Live');
unset($pages);
-
+
/* If we create a new SiteTree object with ID = 0 */
$obj = new SiteTree();
/* Then its children should be the top-level pages */
$stageChildren = $obj->stageChildren()->map('ID','Title');
$liveChildren = $obj->liveChildren()->map('ID','Title');
$allChildren = $obj->AllChildrenIncludingDeleted()->map('ID','Title');
-
+
$this->assertContains('Home', $stageChildren);
$this->assertContains('Products', $stageChildren);
$this->assertNotContains('Staff', $stageChildren);
-
+
$this->assertContains('Home', $liveChildren);
$this->assertContains('Products', $liveChildren);
$this->assertNotContains('Staff', $liveChildren);
-
+
$this->assertContains('Home', $allChildren);
$this->assertContains('Products', $allChildren);
$this->assertNotContains('Staff', $allChildren);
}
-
+
public function testCanSaveBlankToHasOneRelations() {
/* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
$page = new SiteTree();
@@ -174,13 +181,13 @@ class SiteTreeTest extends SapphireTest {
$page->ParentID = $parentID;
$page->write();
$this->assertEquals($parentID, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
-
+
/* You should then be able to save a null/0/'' value to the relation */
$page->ParentID = null;
$page->write();
$this->assertEquals(0, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
}
-
+
public function testStageStates() {
// newly created page
$createdPage = new SiteTree();
@@ -188,15 +195,15 @@ class SiteTreeTest extends SapphireTest {
$this->assertFalse($createdPage->IsDeletedFromStage);
$this->assertTrue($createdPage->IsAddedToStage);
$this->assertTrue($createdPage->IsModifiedOnStage);
-
- // published page
+
+ // published page
$publishedPage = new SiteTree();
$publishedPage->write();
$publishedPage->publish('Stage','Live');
$this->assertFalse($publishedPage->IsDeletedFromStage);
$this->assertFalse($publishedPage->IsAddedToStage);
- $this->assertFalse($publishedPage->IsModifiedOnStage);
-
+ $this->assertFalse($publishedPage->IsModifiedOnStage);
+
// published page, deleted from stage
$deletedFromDraftPage = new SiteTree();
$deletedFromDraftPage->write();
@@ -206,7 +213,7 @@ class SiteTreeTest extends SapphireTest {
$this->assertTrue($deletedFromDraftPage->IsDeletedFromStage);
$this->assertFalse($deletedFromDraftPage->IsAddedToStage);
$this->assertFalse($deletedFromDraftPage->IsModifiedOnStage);
-
+
// published page, deleted from live
$deletedFromLivePage = new SiteTree();
$deletedFromLivePage->write();
@@ -216,7 +223,7 @@ class SiteTreeTest extends SapphireTest {
$this->assertTrue($deletedFromLivePage->IsDeletedFromStage);
$this->assertFalse($deletedFromLivePage->IsAddedToStage);
$this->assertFalse($deletedFromLivePage->IsModifiedOnStage);
-
+
// published page, modified
$modifiedOnDraftPage = new SiteTree();
$modifiedOnDraftPage->write();
@@ -227,7 +234,7 @@ class SiteTreeTest extends SapphireTest {
$this->assertFalse($modifiedOnDraftPage->IsAddedToStage);
$this->assertTrue($modifiedOnDraftPage->IsModifiedOnStage);
}
-
+
/**
* Test that a page can be completely deleted and restored to the stage site
*/
@@ -236,23 +243,23 @@ class SiteTreeTest extends SapphireTest {
$pageID = $page->ID;
$page->delete();
$this->assertTrue(!DataObject::get_by_id("Page", $pageID));
-
+
$deletedPage = Versioned::get_latest_version('SiteTree', $pageID);
$resultPage = $deletedPage->doRestoreToStage();
-
+
$requeriedPage = DataObject::get_by_id("Page", $pageID);
-
+
$this->assertEquals($pageID, $resultPage->ID);
$this->assertEquals($pageID, $requeriedPage->ID);
$this->assertEquals('About Us', $requeriedPage->Title);
$this->assertEquals('Page', $requeriedPage->class);
-
-
+
+
$page2 = $this->objFromFixture('Page', 'products');
$page2ID = $page2->ID;
$page2->doUnpublish();
$page2->delete();
-
+
// Check that if we restore while on the live site that the content still gets pushed to
// stage
Versioned::reading_stage('Live');
@@ -264,39 +271,39 @@ class SiteTreeTest extends SapphireTest {
$requeriedPage = DataObject::get_by_id("Page", $page2ID);
$this->assertEquals('Products', $requeriedPage->Title);
$this->assertEquals('Page', $requeriedPage->class);
-
+
}
-
+
public function testGetByLink() {
$home = $this->objFromFixture('Page', 'home');
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$product = $this->objFromFixture('Page', 'product1');
$notFound = $this->objFromFixture('ErrorPage', '404');
-
+
SiteTree::config()->nested_urls = false;
-
+
$this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
$this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
$this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
$this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
$this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
$this->assertEquals($notFound->ID, SiteTree::get_by_link($notFound->Link(), false)->ID);
-
+
Config::inst()->update('SiteTree', 'nested_urls', true);
-
+
$this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
$this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
$this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
$this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
$this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
$this->assertEquals($notFound->ID, SiteTree::get_by_link($notFound->Link(), false)->ID);
-
+
$this->assertEquals (
$staff->ID, SiteTree::get_by_link('/my-staff/', false)->ID, 'Assert a unique URLSegment can be used for b/c.'
);
}
-
+
public function testRelativeLink() {
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
@@ -313,7 +320,7 @@ class SiteTreeTest extends SapphireTest {
$parent = $this->objFromFixture('Page', 'about');
$child = $this->objFromFixture('Page', 'staff');
- Config::inst()->update('SiteTree', 'nested_urls', true);
+ Config::inst()->update('SiteTree', 'nested_urls', true);
$child->publish('Stage', 'Live');
$parent->URLSegment = 'changed-on-live';
@@ -321,52 +328,52 @@ class SiteTreeTest extends SapphireTest {
$parent->publish('Stage', 'Live');
$parent->URLSegment = 'changed-on-draft';
$parent->write();
-
+
$this->assertStringEndsWith('changed-on-live/my-staff/', $child->getAbsoluteLiveLink(false));
$this->assertStringEndsWith('changed-on-live/my-staff/?stage=Live', $child->getAbsoluteLiveLink());
}
-
+
public function testDeleteFromStageOperatesRecursively() {
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', false);
$pageAbout = $this->objFromFixture('Page', 'about');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
-
+
$pageAbout->delete();
-
+
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
$this->assertTrue(DataObject::get_by_id('Page', $pageStaffDuplicate->ID) instanceof Page);
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
}
-
+
public function testDeleteFromStageOperatesRecursivelyStrict() {
$pageAbout = $this->objFromFixture('Page', 'about');
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
-
+
$pageAbout->delete();
-
+
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaff->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaffDuplicate->ID));
}
-
+
public function testDeleteFromLiveOperatesRecursively() {
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', false);
$this->logInWithPermission('ADMIN');
-
+
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->doPublish();
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->doPublish();
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->doPublish();
-
+
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doDeleteFromLive();
-
+
Versioned::reading_stage('Live');
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
@@ -375,21 +382,21 @@ class SiteTreeTest extends SapphireTest {
Versioned::reading_stage('Stage');
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
}
-
+
public function testUnpublishDoesNotDeleteChildrenWithLooseHierachyOn() {
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', false);
$this->logInWithPermission('ADMIN');
-
+
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->doPublish();
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->doPublish();
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->doPublish();
-
+
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doUnpublish();
-
+
Versioned::reading_stage('Live');
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertTrue(DataObject::get_by_id('Page', $pageStaff->ID) instanceof Page);
@@ -397,28 +404,28 @@ class SiteTreeTest extends SapphireTest {
Versioned::reading_stage('Stage');
Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
}
-
-
+
+
public function testDeleteFromLiveOperatesRecursivelyStrict() {
$this->logInWithPermission('ADMIN');
-
+
$pageAbout = $this->objFromFixture('Page', 'about');
$pageAbout->doPublish();
$pageStaff = $this->objFromFixture('Page', 'staff');
$pageStaff->doPublish();
$pageStaffDuplicate = $this->objFromFixture('Page', 'staffduplicate');
$pageStaffDuplicate->doPublish();
-
+
$parentPage = $this->objFromFixture('Page', 'about');
$parentPage->doDeleteFromLive();
-
+
Versioned::reading_stage('Live');
$this->assertFalse(DataObject::get_by_id('Page', $pageAbout->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaff->ID));
$this->assertFalse(DataObject::get_by_id('Page', $pageStaffDuplicate->ID));
Versioned::reading_stage('Stage');
}
-
+
/**
* Simple test to confirm that querying from a particular archive date doesn't throw
* an error
@@ -433,24 +440,29 @@ class SiteTreeTest extends SapphireTest {
'Archive.'
);
}
-
+
public function testEditPermissions() {
$editor = $this->objFromFixture("Member", "editor");
-
+
$home = $this->objFromFixture("Page", "home");
+ $staff = $this->objFromFixture("Page", "staff");
$products = $this->objFromFixture("Page", "products");
$product1 = $this->objFromFixture("Page", "product1");
$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
$this->assertFalse($home->canEdit($editor));
-
+
// Can edit a page that is locked to editors
$this->assertTrue($products->canEdit($editor));
-
+
// Can edit a child of that page that inherits
$this->assertTrue($product1->canEdit($editor));
-
+
// Can't edit a child of that page that has its permissions overridden
$this->assertFalse($product4->canEdit($editor));
}
@@ -464,6 +476,33 @@ class SiteTreeTest extends SapphireTest {
$this->assertTrue($page->canEdit($allSectionMember));
$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() {
// Create an inherit-permission page
@@ -472,17 +511,17 @@ class SiteTreeTest extends SapphireTest {
$page->CanEditType = "Inherit";
$page->doPublish();
$pageID = $page->ID;
-
+
// Lock down the site config
$sc = $page->SiteConfig;
$sc->CanEditType = 'OnlyTheseUsers';
$sc->EditorGroups()->add($this->idFromFixture('Group', 'admins'));
$sc->write();
-
+
// Confirm that Member.editor can't edit the page
$this->objFromFixture('Member','editor')->logIn();
$this->assertFalse($page->canEdit());
-
+
// Change the page to be editable by Group.editors, but do not publish
$this->objFromFixture('Member','admin')->logIn();
$page->CanEditType = 'OnlyTheseUsers';
@@ -490,25 +529,25 @@ class SiteTreeTest extends SapphireTest {
$page->write();
// Clear permission cache
SiteTree::on_db_reset();
-
+
// Confirm that Member.editor can now edit the page
$this->objFromFixture('Member','editor')->logIn();
$this->assertTrue($page->canEdit());
-
+
// Publish the changes to the page
$this->objFromFixture('Member','admin')->logIn();
$page->doPublish();
-
+
// Confirm that Member.editor can still edit the page
$this->objFromFixture('Member','editor')->logIn();
$this->assertTrue($page->canEdit());
- }
-
+}
+
public function testCompareVersions() {
// Necessary to avoid
$oldCleanerClass = Diff::$html_cleaner_class;
Diff::$html_cleaner_class = 'SiteTreeTest_NullHtmlCleaner';
-
+
$page = new Page();
$page->write();
$this->assertEquals(1, $page->Version);
@@ -519,14 +558,14 @@ class SiteTreeTest extends SapphireTest {
$page->Content = "This is a test";
$page->write();
$this->assertEquals(2, $page->Version);
-
+
$diff = $page->compareVersions(1, 2);
-
+
$processedContent = trim($diff->Content);
$processedContent = preg_replace('/\s*','<',$processedContent);
$processedContent = preg_replace('/>\s*/','>',$processedContent);
$this->assertEquals("This is a test", $processedContent);
-
+
Diff::$html_cleaner_class = $oldCleanerClass;
}
@@ -544,52 +583,52 @@ class SiteTreeTest extends SapphireTest {
$about = $this->objFromFixture('Page','about');
$about->Title = "Another title";
$about->write();
-
+
// Check the version created
- $savedVersion = DB::query("SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_versions\"
+ $savedVersion = DB::query("SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $about->ID ORDER BY \"Version\" DESC")->first();
$this->assertEquals($memberID, $savedVersion['AuthorID']);
$this->assertEquals(0, $savedVersion['PublisherID']);
-
+
// Publish the page
$about->doPublish();
- $publishedVersion = DB::query("SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_versions\"
+ $publishedVersion = DB::query("SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_versions\"
WHERE \"RecordID\" = $about->ID ORDER BY \"Version\" DESC")->first();
-
+
// Check the version created
$this->assertEquals($memberID, $publishedVersion['AuthorID']);
$this->assertEquals($memberID, $publishedVersion['PublisherID']);
-
+
}
-
+
public function testLinkShortcodeHandler() {
$aboutPage = $this->objFromFixture('Page', 'about');
$errorPage = $this->objFromFixture('ErrorPage', '404');
$redirectPage = $this->objFromFixture('RedirectorPage', 'external');
-
+
$parser = new ShortcodeParser();
$parser->register('sitetree_link', array('SiteTree', 'link_shortcode_handler'));
-
+
$aboutShortcode = sprintf('[sitetree_link,id=%d]', $aboutPage->ID);
$aboutEnclosed = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $aboutPage->ID);
-
+
$aboutShortcodeExpected = $aboutPage->Link();
$aboutEnclosedExpected = sprintf('Example Content', $aboutPage->Link());
-
+
$this->assertEquals($aboutShortcodeExpected, $parser->parse($aboutShortcode), 'Test that simple linking works.');
$this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed), 'Test enclosed content is linked.');
-
+
$aboutPage->delete();
-
+
$this->assertEquals($aboutShortcodeExpected, $parser->parse($aboutShortcode), 'Test that deleted pages still link.');
$this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
-
+
$aboutShortcode = '[sitetree_link,id="-1"]';
$aboutEnclosed = '[sitetree_link,id="-1"]Example Content[/sitetree_link]';
-
+
$aboutShortcodeExpected = $errorPage->Link();
$aboutEnclosedExpected = sprintf('Example Content', $errorPage->Link());
-
+
$this->assertEquals($aboutShortcodeExpected, $parser->parse($aboutShortcode), 'Test link to 404 page if no suitable matches.');
$this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
@@ -599,114 +638,114 @@ class SiteTreeTest extends SapphireTest {
$this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
$this->assertEquals(sprintf('Example Content', $redirectExpected), $parser->parse($redirectEnclosed));
-
+
$this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
$this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
$this->assertEquals('', $parser->parse('[sitetree_link]Example Content[/sitetree_link]'));
}
-
+
public function testIsCurrent() {
$aboutPage = $this->objFromFixture('Page', 'about');
$errorPage = $this->objFromFixture('ErrorPage', '404');
-
+
Director::set_current_page($aboutPage);
$this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isSection checks works.');
$this->assertFalse($errorPage->isCurrent());
-
+
Director::set_current_page($errorPage);
$this->assertTrue($errorPage->isCurrent(), 'Assert isSection works on error pages.');
$this->assertFalse($aboutPage->isCurrent());
-
+
Director::set_current_page($aboutPage);
$this->assertTrue (
DataObject::get_one('SiteTree', '"Title" = \'About Us\'')->isCurrent(),
'Assert that isCurrent works on another instance with the same ID.'
);
-
+
Director::set_current_page($newPage = new SiteTree());
$this->assertTrue($newPage->isCurrent(), 'Assert that isCurrent works on unsaved pages.');
}
-
+
public function testIsSection() {
$about = $this->objFromFixture('Page', 'about');
$staff = $this->objFromFixture('Page', 'staff');
$ceo = $this->objFromFixture('Page', 'ceo');
-
+
Director::set_current_page($about);
$this->assertTrue($about->isSection());
$this->assertFalse($staff->isSection());
$this->assertFalse($ceo->isSection());
-
+
Director::set_current_page($staff);
$this->assertTrue($about->isSection());
$this->assertTrue($staff->isSection());
$this->assertFalse($ceo->isSection());
-
+
Director::set_current_page($ceo);
$this->assertTrue($about->isSection());
$this->assertTrue($staff->isSection());
$this->assertTrue($ceo->isSection());
}
-
+
/**
* @covers SiteTree::validURLSegment
*/
public function testValidURLSegmentURLSegmentConflicts() {
$sitetree = new SiteTree();
SiteTree::config()->nested_urls = false;
-
+
$sitetree->URLSegment = 'home';
$this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
$sitetree->URLSegment = 'home-noconflict';
$this->assertTrue($sitetree->validURLSegment());
-
+
$sitetree->ParentID = $this->idFromFixture('Page', 'about');
$sitetree->URLSegment = 'home';
$this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
-
+
Config::inst()->update('SiteTree', 'nested_urls', true);
-
+
$sitetree->ParentID = 0;
$sitetree->URLSegment = 'home';
$this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
-
+
$sitetree->ParentID = $this->idFromFixture('Page', 'about');
$this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
-
+
$sitetree->URLSegment = 'my-staff';
$this->assertFalse($sitetree->validURLSegment(), 'Nested URLSegment conflicts are recognised');
$sitetree->URLSegment = 'my-staff-noconflict';
$this->assertTrue($sitetree->validURLSegment());
}
-
+
/**
* @covers SiteTree::validURLSegment
*/
public function testValidURLSegmentClassNameConflicts() {
$sitetree = new SiteTree();
$sitetree->URLSegment = 'Controller';
-
+
$this->assertFalse($sitetree->validURLSegment(), 'Class name conflicts are recognised');
}
-
+
/**
* @covers SiteTree::validURLSegment
*/
public function testValidURLSegmentControllerConflicts() {
Config::inst()->update('SiteTree', 'nested_urls', true);
-
+
$sitetree = new SiteTree();
$sitetree->ParentID = $this->idFromFixture('SiteTreeTest_Conflicted', 'parent');
-
+
$sitetree->URLSegment = 'index';
$this->assertFalse($sitetree->validURLSegment(), 'index is not a valid URLSegment');
-
+
$sitetree->URLSegment = 'conflicted-action';
$this->assertFalse($sitetree->validURLSegment(), 'allowed_actions conflicts are recognised');
-
+
$sitetree->URLSegment = 'conflicted-template';
$this->assertFalse($sitetree->validURLSegment(), 'Action-specific template conflicts are recognised');
-
+
$sitetree->URLSegment = 'valid';
$this->assertTrue($sitetree->validURLSegment(), 'Valid URLSegment values are allowed');
}
@@ -742,13 +781,13 @@ class SiteTreeTest extends SapphireTest {
Config::inst()->update('URLSegmentFilter', 'default_allow_multibyte', $origAllow);
}
-
+
public function testVersionsAreCreated() {
$p = new Page();
$p->Content = "one";
$p->write();
$this->assertEquals(1, $p->Version);
-
+
// No changes don't bump version
$p->write();
$this->assertEquals(1, $p->Version);
@@ -769,45 +808,45 @@ class SiteTreeTest extends SapphireTest {
$this->assertEquals(3, $p->Version);
}
-
+
public function testPageTypeClasses() {
$classes = SiteTree::page_type_classes();
$this->assertNotContains('SiteTree', $classes, 'Page types do not include base class');
$this->assertContains('Page', $classes, 'Page types do contain subclasses');
}
-
+
public function testAllowedChildren() {
$page = new SiteTree();
$this->assertContains(
- 'VirtualPage',
+ 'VirtualPage',
$page->allowedChildren(),
'Includes core subclasses by default'
);
-
+
$classA = new SiteTreeTest_ClassA();
$this->assertEquals(
- array('SiteTreeTest_ClassB'),
+ array('SiteTreeTest_ClassB'),
$classA->allowedChildren(),
'Direct setting of allowed children'
);
-
+
$classB = new SiteTreeTest_ClassB();
$this->assertEquals(
- array('SiteTreeTest_ClassC', 'SiteTreeTest_ClassCext'),
+ array('SiteTreeTest_ClassC', 'SiteTreeTest_ClassCext'),
$classB->allowedChildren(),
'Includes subclasses'
);
-
+
$classD = new SiteTreeTest_ClassD();
$this->assertEquals(
- array('SiteTreeTest_ClassC'),
+ array('SiteTreeTest_ClassC'),
$classD->allowedChildren(),
'Excludes subclasses if class is prefixed by an asterisk'
);
-
+
$classC = new SiteTreeTest_ClassC();
$this->assertEquals(
- array(),
+ array(),
$classC->allowedChildren(),
'Null setting'
);
@@ -826,11 +865,11 @@ class SiteTreeTest extends SapphireTest {
$classD->write();
$classCext = new SiteTreeTest_ClassCext();
$classCext->write();
-
+
$classB->ParentID = $page->ID;
$valid = $classB->validate();
$this->assertTrue($valid->valid(), "Does allow children on unrestricted parent");
-
+
$classB->ParentID = $classA->ID;
$valid = $classB->validate();
$this->assertTrue($valid->valid(), "Does allow child specifically allowed by parent");
@@ -838,20 +877,20 @@ class SiteTreeTest extends SapphireTest {
$classC->ParentID = $classA->ID;
$valid = $classC->validate();
$this->assertFalse($valid->valid(), "Doesnt allow child on parents specifically restricting children");
-
+
$classB->ParentID = $classC->ID;
$valid = $classB->validate();
$this->assertFalse($valid->valid(), "Doesnt allow child on parents disallowing all children");
-
+
$classB->ParentID = $classC->ID;
$valid = $classB->validate();
$this->assertFalse($valid->valid(), "Doesnt allow child on parents disallowing all children");
-
+
$classCext->ParentID = $classD->ID;
$valid = $classCext->validate();
$this->assertFalse($valid->valid(), "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used");
}
-
+
public function testClassDropdown() {
$sitetree = new SiteTree();
$method = new ReflectionMethod($sitetree, 'getClassDropdown');
@@ -859,13 +898,13 @@ class SiteTreeTest extends SapphireTest {
Session::set("loggedInAs", null);
$this->assertArrayNotHasKey('SiteTreeTest_ClassA', $method->invoke($sitetree));
-
+
$this->loginWithPermission('ADMIN');
$this->assertArrayHasKey('SiteTreeTest_ClassA', $method->invoke($sitetree));
-
+
$this->loginWithPermission('CMS_ACCESS_CMSMain');
$this->assertArrayHasKey('SiteTreeTest_ClassA', $method->invoke($sitetree));
-
+
Session::set("loggedInAs", null);
}
@@ -878,14 +917,14 @@ class SiteTreeTest extends SapphireTest {
$notRootPage->ParentID = 0;
$isDetected = false;
try {
- $notRootPage->write();
+ $notRootPage->write();
} catch(ValidationException $e) {
$this->assertContains('is not allowed on the root level', $e->getMessage());
$isDetected = true;
- }
+ }
if(!$isDetected) $this->fail('Fails validation with $can_be_root=false');
- }
+ }
public function testModifyStatusFlagByInheritance(){
$node = new SiteTreeTest_StageStatusInherit();
@@ -899,7 +938,7 @@ class SiteTreeTest extends SapphireTest {
$page->Title = 'orig';
$page->MenuTitle = 'orig';
$page->write();
-
+
// change menu title
$page->MenuTitle = 'changed';
$page->write();
@@ -943,7 +982,7 @@ class SiteTreeTest extends SapphireTest {
// reset original value
Config::inst()->update('SiteTree', 'meta_generator', $generator);
}
-
+
/**
* Tests SiteTree::MetaTags
* Note that this test makes no assumption on the closing of tags (other than )
@@ -951,7 +990,7 @@ class SiteTreeTest extends SapphireTest {
public function testMetaTags() {
$this->logInWithPermission('ADMIN');
$page = $this->objFromFixture('Page', 'metapage');
-
+
// Test with title
$meta = $page->MetaTags();
$charset = Config::inst()->get('ContentNegotiator', 'encoding');
@@ -961,18 +1000,18 @@ class SiteTreeTest extends SapphireTest {
$this->assertContains('assertContains('', $meta);
$this->assertContains('HTML & XML', $meta);
-
+
// Test without title
$meta = $page->MetaTags(false);
$this->assertNotContains('', $meta);
}
-
+
/**
* Test that orphaned pages are handled correctly
*/
public function testOrphanedPages() {
$origStage = Versioned::get_reading_mode();
-
+
// Setup user who can view draft content, but lacks cms permission.
// To users such as this, orphaned pages should be inaccessible. canView for these pages is only
// necessary for admin / cms users, who require this permission to edit / rearrange these pages.
@@ -985,7 +1024,7 @@ class SiteTreeTest extends SapphireTest {
$member->Email = 'someguy@example.com';
$member->write();
$member->Groups()->add($group);
-
+
// both pages are viewable in stage
Versioned::reading_stage('Stage');
$about = $this->objFromFixture('Page', 'about');
@@ -994,7 +1033,7 @@ class SiteTreeTest extends SapphireTest {
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($about->canView($member));
$this->assertTrue($staff->canView($member));
-
+
// Publishing only the child page to live should orphan the live record, but not the staging one
$staff->publish('Stage', 'Live');
$this->assertFalse($staff->isOrphaned());
@@ -1003,7 +1042,7 @@ class SiteTreeTest extends SapphireTest {
$staff = $this->objFromFixture('Page', 'staff'); // Live copy of page
$this->assertTrue($staff->isOrphaned()); // because parent isn't published
$this->assertFalse($staff->canView($member));
-
+
// Publishing the parent page should restore visibility
Versioned::reading_stage('Stage');
$about = $this->objFromFixture('Page', 'about');
@@ -1012,19 +1051,19 @@ class SiteTreeTest extends SapphireTest {
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
-
+
// Removing staging page should not prevent live page being visible
$about->deleteFromStage('Stage');
$staff->deleteFromStage('Stage');
$staff = $this->objFromFixture('Page', 'staff');
$this->assertFalse($staff->isOrphaned());
$this->assertTrue($staff->canView($member));
-
+
// Cleanup
Versioned::set_reading_mode($origStage);
-
+
}
-
+
}
/**#@+
diff --git a/tests/model/SiteTreeTest.yml b/tests/model/SiteTreeTest.yml
index e032d3ec..e55daa10 100644
--- a/tests/model/SiteTreeTest.yml
+++ b/tests/model/SiteTreeTest.yml
@@ -1,3 +1,11 @@
+SiteConfig:
+ default:
+ Title: My test site
+ Tagline: Default site config
+ CanViewType: Anyone
+ CanEditType: LoggedInUsers
+ CanCreateTopLevelType: LoggedInUsers
+
Group:
editors:
Title: Editors