From 64955e57d1239975183f47d3ac8c3e801ddbf122 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 16 Mar 2015 16:27:31 +1300 Subject: [PATCH 1/2] BUG Fix SiteTree / SiteConfig permissions --- code/controllers/CMSMain.php | 103 ++++--- code/controllers/CMSPageAddController.php | 15 +- code/model/SiteConfig.php | 86 ++++-- code/model/SiteTree.php | 63 ++-- javascript/CMSMain.AddForm.js | 109 +++++-- javascript/CMSMain.Tree.js | 30 +- templates/Includes/CMSMain_TreeView.ss | 2 +- tests/controller/CMSMainTest.php | 92 +++++- tests/model/SiteConfigTest.php | 49 +++- tests/model/SiteConfigTest.yml | 7 + tests/model/SiteTreeTest.php | 335 +++++++++++++--------- tests/model/SiteTreeTest.yml | 168 ++++++----- 12 files changed, 712 insertions(+), 347 deletions(-) create mode 100644 tests/model/SiteConfigTest.yml diff --git a/code/controllers/CMSMain.php b/code/controllers/CMSMain.php index 28d19548..9ad16b87 100644 --- a/code/controllers/CMSMain.php +++ b/code/controllers/CMSMain.php @@ -46,6 +46,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr 'treeview', 'listview', 'ListViewForm', + 'childfilter', ); public function init() { @@ -379,55 +380,49 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr $json = $cache->load($cacheKey); if(!$json) { $def['Root'] = array(); - $def['Root']['disallowedParents'] = array(); + $def['Root']['disallowedChildren'] = array(); + // Contains all possible classes to support UI controls listing them all, + // such as the "add page here" context menu. + $def['All'] = array(); + + // Identify disallows and set globals foreach($classes as $class) { $obj = singleton($class); if($obj instanceof HiddenClass) continue; - - $allowedChildren = $obj->allowedChildren(); - - // SiteTree::allowedChildren() returns null rather than an empty array if SiteTree::allowed_chldren == 'none' - if($allowedChildren == null) $allowedChildren = array(); - - // Exclude SiteTree from possible Children - $possibleChildren = array_diff($allowedChildren, array("SiteTree")); - // Find i18n - names and build allowed children array - foreach($possibleChildren as $child) { - $instance = singleton($child); - - if($instance instanceof HiddenClass) continue; + // Name item + $def['All'][$class] = array( + 'title' => $obj->i18n_singular_name() + ); - if(!array_key_exists($child, $cacheCanCreate) || !$cacheCanCreate[$child]) continue; - - // skip this type if it is restricted - if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue; - - $title = $instance->i18n_singular_name(); - - $def[$class]['allowedChildren'][] = array("ssclass" => $child, "ssname" => $title); + // 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; } - $allowedChildren = array_keys(array_diff($classes, $allowedChildren)); - if($allowedChildren) $def[$class]['disallowedChildren'] = $allowedChildren; + // Hint data specific to the class + $def[$class] = array(); + $defaultChild = $obj->defaultChild(); - 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) $def[$class]['defaultParent'] = $defaultParent; - if(isset($def[$class]['disallowedChildren'])) { - foreach($def[$class]['disallowedChildren'] as $disallowedChild) { - $def[$disallowedChild]['disallowedParents'][] = $class; - } + if($defaultChild !== 'Page' && $defaultChild !== null) { + $def[$class]['defaultChild'] = $defaultChild; + } + + $defaultParent = $obj->defaultParent(); + if ($defaultParent !== 1 && $defaultParent !== null) { + $def[$class]['defaultParent'] = $defaultParent; } - - // Are any classes allowed to be parents of root? - $def['Root']['disallowedParents'][] = $class; } - $json = Convert::raw2xml(Convert::raw2json($def)); + $this->extend('updateSiteTreeHints', $def); + + $json = Convert::raw2json($def); $cache->save($json, $cacheKey); } return $json; @@ -490,8 +485,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; @@ -674,6 +667,38 @@ 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); + $this->response->addHeader('Content-Type', 'application/json; charset=utf-8'); + $this->response->setBody(Convert::raw2json($disallowedChildren)); + return $this->response; + } /** * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page diff --git a/code/controllers/CMSPageAddController.php b/code/controllers/CMSPageAddController.php index 0f7743c9..e3cd30a7 100644 --- a/code/controllers/CMSPageAddController.php +++ b/code/controllers/CMSPageAddController.php @@ -21,7 +21,7 @@ class CMSPageAddController extends CMSPageEditController { $pageTypes = array(); foreach($this->PageTypes() as $type) { $html = sprintf('%s%s', - $type->getField('Title'), + $type->getField('ClassName'), $type->getField('AddAction'), $type->getField('Description') ); @@ -39,9 +39,6 @@ class CMSPageAddController extends CMSPageEditController { $childTitle = _t('CMSPageAddController.ParentMode_child', 'Under another page'); $fields = new FieldList( - // new HiddenField("ParentID", false, ($this->parentRecord) ? $this->parentRecord->ID : null), - // TODO Should be part of the form attribute, but not possible in current form API - $hintsField = new LiteralField('Hints', sprintf('', $this->SiteTreeHints())), new LiteralField('PageModeHeader', sprintf($numericLabelTmpl, 1, _t('CMSMain.ChoosePageParentMode', 'Choose where to create this page'))), $parentModeField = new SelectionGroup( @@ -87,6 +84,8 @@ class CMSPageAddController extends CMSPageEditController { $this->extend('updatePageOptions', $fields); $form = new Form($this, "AddForm", $fields, $actions); + $form->setAttribute('data-hints', $this->SiteTreeHints()); + $form->setAttribute('data-childfilter', $this->Link('childfilter')); $form->addExtraClass('cms-add-form stacked cms-content center cms-edit-form ' . $this->BaseCSSClasses()); $form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); @@ -113,12 +112,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 0fff4f9f..63c90dac 100644 --- a/code/model/SiteConfig.php +++ b/code/model/SiteConfig.php @@ -21,8 +21,21 @@ class SiteConfig extends DataObject implements PermissionProvider { "EditorGroups" => "Group", "CreateTopLevelGroups" => "Group" ); + + static $defaults = array( + "CanViewType" => "Anyone", + "CanEditType" => "LoggedInUsers", + "CanCreateTopLevelType" => "LoggedInUsers", + ); protected static $disabled_themes = array(); + + /** + * Default permission to check for 'LoggedInUsers' to create or edit pages + * + * @var array + */ + static $required_permission = array('CMS_ACCESS_CMSMain', 'CMS_ACCESS_LeftAndMain'); static public function disable_theme($theme) { self::$disabled_themes[$theme] = $theme; @@ -192,48 +205,70 @@ class SiteConfig extends DataObject implements PermissionProvider { * 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( @@ -249,25 +284,32 @@ class SiteConfig extends DataObject implements PermissionProvider { /** * 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 305cfe16..0c809a1b 100644 --- a/code/model/SiteTree.php +++ b/code/model/SiteTree.php @@ -815,7 +815,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 @@ -896,12 +896,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. * @@ -909,6 +909,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) { @@ -916,15 +919,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 @@ -966,7 +984,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid // Default for unsaved pages } else { - return $this->getSiteConfig()->canEdit($member); + return $this->getSiteConfig()->canEditPages($member); } } @@ -1207,7 +1225,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', 'CMS_ACCESS_CMSMain', $useCached); + return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', 'CMS_ACCESS_CMSMain', $useCached); } /** @@ -2574,9 +2592,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 0048bacc..d09b4875 100644 --- a/javascript/CMSMain.AddForm.js +++ b/javascript/CMSMain.AddForm.js @@ -15,37 +15,107 @@ }); $(".cms-add-form").entwine({ - onmatch: function() { + 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(); - this._super(); }, - onunmatch: function() { - this._super(); + 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(), + 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 ? newClassName : 'Root', - hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null; - - var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [], - defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : 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); + var className = $(this).find('input').val(), + isAllowed = ($.inArray(className, disallowedChildren) === -1); + $(this).setEnabled(isAllowed); + if(!isAllowed) $(this).setSelected(false); + if(allAllowed === null) allAllowed = isAllowed; + else allAllowed = allAllowed && isAllowed; }); // Set default child selection, or fall back to first available option @@ -69,10 +139,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 fb1b634f..87d085a8 100644 --- a/javascript/CMSMain.Tree.js +++ b/javascript/CMSMain.Tree.js @@ -9,23 +9,23 @@ 'items': function(node) { // Build a list for allowed children as submenu entries - var pagetype = node.data('pagetype'); var id = node.data('id'); + var allowedChildrenClasses = node.find('>a .item').data('allowedchildren'); var allowedChildren = new Object; - $(hints[pagetype].allowedChildren).each( - function(key, val){ - allowedChildren["allowedchildren-" + key ] = { - 'label': '' + val.ssname, - '_class': 'class-' + val.ssclass, - 'action': function(obj) { - $('.cms-container').entwine('.ss').loadPanel(ss.i18n.sprintf( - self.data('urlAddpage'), id, val.ssclass - )); - } - }; - } - ); + var hasAllowedChildren = false; + $.each(allowedChildrenClasses, function(klass, title) { + hasAllowedChildren = true; + allowedChildren["allowedchildren-" + klass ] = { + 'label': '' + title, + '_class': 'class-' + klass, + 'action': function(obj) { + $('.cms-container').entwine('.ss').loadPanel(ss.i18n.sprintf( + self.data('urlAddpage'), id, klass + )); + } + }; + }); var menuitems = { 'edit': { @@ -38,7 +38,7 @@ } }; // Test if there are any allowed Children and thus the possibility of adding some - if(allowedChildren.hasOwnProperty('allowedchildren-0')) { + if(hasAllowedChildren) { menuitems['addsubpage'] = { 'label': ss.i18n._t('Tree.AddSubPage', 'Add page under this page', 100, 'Used in the context menu when right-clicking on a page node in the CMS tree'), 'submenu': allowedChildren diff --git a/templates/Includes/CMSMain_TreeView.ss b/templates/Includes/CMSMain_TreeView.ss index 87fbf182..88898b1a 100644 --- a/templates/Includes/CMSMain_TreeView.ss +++ b/templates/Includes/CMSMain_TreeView.ss @@ -19,7 +19,7 @@ $ExtraTreeTools <% end_if %> -
+
$SiteTreeAsUL
diff --git a/tests/controller/CMSMainTest.php b/tests/controller/CMSMainTest.php index a561761e..cc8eaadc 100644 --- a/tests/controller/CMSMainTest.php +++ b/tests/controller/CMSMainTest.php @@ -8,7 +8,7 @@ class CMSMainTest extends FunctionalTest { static $fixture_file = 'CMSMainTest.yml'; static protected $orig = array(); - + public function setUpOnce() { self::$orig['CMSBatchActionHandler_batch_actions'] = CMSBatchActionHandler::$batch_actions; CMSBatchActionHandler::$batch_actions = array( @@ -19,12 +19,84 @@ class CMSMainTest extends FunctionalTest { parent::setUpOnce(); } - + public function tearDownOnce() { CMSBatchActionHandler::$batch_actions = self::$orig['CMSBatchActionHandler_batch_actions']; - + parent::tearDownOnce(); } + + 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(); + $this->assertNotNull($rawHints); + + $rawHints = preg_replace('/^"(.*)"$/', '$1', Convert::xml2raw($rawHints)); + $hints = Convert::json2array($rawHints); + + $this->assertArrayHasKey('Root', $hints); + $this->assertArrayHasKey('Page', $hints); + $this->assertArrayHasKey('All', $hints); + + $this->assertArrayHasKey( + 'CMSMainTest_ClassA', + $hints['All'], + 'Global list shows allowed classes' + ); + + $this->assertArrayNotHasKey( + 'CMSMainTest_HiddenClass', + $hints['All'], + 'Global list does not list hidden classes' + ); + + $this->assertNotContains( + 'CMSMainTest_ClassA', + $hints['Root']['disallowedChildren'], + 'Limits root classes' + ); + + $this->assertContains( + 'CMSMainTest_NotRoot', + $hints['Root']['disallowedChildren'], + 'Limits root classes' + ); + + } + + 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', + $children, + 'Limited parent lists disallowed classes' + ); + + // But it can create a ClassB + $this->assertNotContains( + 'CMSMainTest_ClassB', + $children, + 'Limited parent omits explicitly allowed classes in disallowedChildren' + ); + } /** * @todo Test the results of a publication better @@ -256,11 +328,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); @@ -314,3 +382,11 @@ class CMSMainTest_ClassA extends Page implements TestOnly { class CMSMainTest_ClassB extends Page implements TestOnly { } + +class CMSMainTest_NotRoot extends Page implements TestOnly { + static $can_be_root = false; +} + +class CMSMainTest_HiddenClass extends Page implements TestOnly, HiddenClass { + +} diff --git a/tests/model/SiteConfigTest.php b/tests/model/SiteConfigTest.php index a54f0762..cf13fe45 100644 --- a/tests/model/SiteConfigTest.php +++ b/tests/model/SiteConfigTest.php @@ -7,13 +7,15 @@ * SiteTreePermissionsTest */ class SiteConfigTest extends SapphireTest { + + 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 54662877..fe92589d 100644 --- a/tests/model/SiteTreeTest.php +++ b/tests/model/SiteTreeTest.php @@ -19,24 +19,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::set_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::set_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); } @@ -62,30 +69,30 @@ 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->MetaTitle = "asdfasdf"; $obj->write(); @@ -98,28 +105,27 @@ class SiteTreeTest extends SapphireTest { $this->assertTrue($obj->doPublish()); $this->assertNull(DB::query("SELECT \"MetaTitle\" 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 */ @@ -131,41 +137,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", "\"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(); @@ -173,13 +179,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(); @@ -187,15 +193,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(); @@ -205,7 +211,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(); @@ -215,7 +221,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(); @@ -226,7 +232,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 */ @@ -235,23 +241,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'); @@ -263,16 +269,16 @@ 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::disable_nested_urls(); $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID); @@ -290,12 +296,12 @@ class SiteTreeTest extends SapphireTest { $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'); @@ -320,52 +326,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() { SiteTree::set_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); SiteTree::set_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() { SiteTree::set_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)); @@ -374,21 +380,21 @@ class SiteTreeTest extends SapphireTest { Versioned::reading_stage('Stage'); SiteTree::set_enforce_strict_hierarchy(true); } - + public function testUnpublishDoesNotDeleteChildrenWithLooseHierachyOn() { SiteTree::set_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); @@ -396,28 +402,28 @@ class SiteTreeTest extends SapphireTest { Versioned::reading_stage('Stage'); SiteTree::set_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 @@ -429,27 +435,70 @@ class SiteTreeTest extends SapphireTest { Versioned::reading_archived_date(null); } - + 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)); } + + public function testCanEditWithAccessToAllSections() { + $page = new Page(); + $page->write(); + $allSectionMember = $this->objFromFixture('Member', 'allsections'); + $securityAdminMember = $this->objFromFixture('Member', 'securityadmin'); + + $this->assertTrue(SiteConfig::current_site_config()->canEditPages($allSectionMember)); + $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 @@ -458,17 +507,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'; @@ -476,25 +525,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); @@ -505,14 +554,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*\s*/','>',$processedContent); $this->assertEquals("This is a test", $processedContent); - + Diff::$html_cleaner_class = $oldCleanerClass; } @@ -530,51 +579,51 @@ 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'); $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)); @@ -582,50 +631,50 @@ class SiteTreeTest extends SapphireTest { $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 */ @@ -637,7 +686,7 @@ class SiteTreeTest extends SapphireTest { $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'); @@ -647,26 +696,26 @@ class SiteTreeTest extends SapphireTest { $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 */ @@ -675,16 +724,16 @@ class SiteTreeTest extends SapphireTest { $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'); } @@ -708,13 +757,13 @@ class SiteTreeTest extends SapphireTest { 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); @@ -735,45 +784,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' ); @@ -792,11 +841,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"); @@ -804,20 +853,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'); @@ -825,13 +874,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); } @@ -844,14 +893,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(); @@ -865,7 +914,7 @@ class SiteTreeTest extends SapphireTest { $page->Title = 'orig'; $page->MenuTitle = 'orig'; $page->write(); - + // change menu title $page->MenuTitle = 'changed'; $page->write(); @@ -879,7 +928,7 @@ class SiteTreeTest extends SapphireTest { $this->assertEquals(null, $page->getField('MenuTitle')); } -} + } /**#@+ * @ignore diff --git a/tests/model/SiteTreeTest.yml b/tests/model/SiteTreeTest.yml index f4fe7275..0f2f7483 100644 --- a/tests/model/SiteTreeTest.yml +++ b/tests/model/SiteTreeTest.yml @@ -1,82 +1,104 @@ +SiteConfig: + default: + Title: My test site + Tagline: Default site config + CanViewType: Anyone + CanEditType: LoggedInUsers + CanCreateTopLevelType: LoggedInUsers + Group: - editors: - Title: Editors - admins: - Title: Administrators - + editors: + Title: Editors + admins: + Title: Administrators + allsections: + Title: All Section Editors + securityadmins: + Title: Security Admins + Permission: - admins: - Code: ADMIN - Group: =>Group.admins - editors: - Code: CMS_ACCESS_CMSMain - Group: =>Group.editors - + admins: + Code: ADMIN + Group: =>Group.admins + editors: + Code: CMS_ACCESS_CMSMain + Group: =>Group.editors + allsections: + Code: CMS_ACCESS_CMSMain + Group: =>Group.allsections + securityadmins: + Code: CMS_ACCESS_SecurityAdmin + Group: =>Group.securityadmins + Member: - editor: - FirstName: Test - Surname: Editor - Groups: =>Group.editors - admin: - FirstName: Test - Surname: Administrator - Groups: =>Group.admins + editor: + FirstName: Test + Surname: Editor + Groups: =>Group.editors + admin: + FirstName: Test + Surname: Administrator + Groups: =>Group.admins + allsections: + Groups: =>Group.allsections + securityadmin: + Groups: =>Group.securityadmins Page: - home: - Title: Home - CanEditType: OnlyTheseUsers - EditorGroups: =>Group.admins - about: - Title: About Us - CanEditType: OnlyTheseUsers - EditorGroups: =>Group.admins - staff: - Title: Staff - URLSegment: my-staff - Parent: =>Page.about - ceo: - Title: CEO - Parent: =>Page.staff - staffduplicate: - Title: Staff - URLSegment: my-staff - Parent: =>Page.about - products: - Title: Products - CanEditType: OnlyTheseUsers - EditorGroups: =>Group.editors - product1: - Title: 1.1 Test Product - Parent: =>Page.products - CanEditType: Inherit - product2: - Title: Another Product - Parent: =>Page.products - CanEditType: Inherit - product3: - Title: Another Product - Parent: =>Page.products - CanEditType: Inherit - product4: - Title: Another Product - Parent: =>Page.products - CanEditType: OnlyTheseUsers - EditorGroups: =>Group.admins - contact: - Title: Contact Us - object: - Title: Object - controller: - Title: Controller - numericonly: - Title: 1930 + home: + Title: Home + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.admins + about: + Title: About Us + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.admins + staff: + Title: Staff + URLSegment: my-staff + Parent: =>Page.about + ceo: + Title: CEO + Parent: =>Page.staff + staffduplicate: + Title: Staff + URLSegment: my-staff + Parent: =>Page.about + products: + Title: Products + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.editors + product1: + Title: 1.1 Test Product + Parent: =>Page.products + CanEditType: Inherit + product2: + Title: Another Product + Parent: =>Page.products + CanEditType: Inherit + product3: + Title: Another Product + Parent: =>Page.products + CanEditType: Inherit + product4: + Title: Another Product + Parent: =>Page.products + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.admins + contact: + Title: Contact Us + object: + Title: Object + controller: + Title: Controller + numericonly: + Title: 1930 SiteTreeTest_Conflicted: - parent: - Title: Parent + parent: + Title: Parent ErrorPage: - 404: - Title: Page not Found - ErrorCode: 404 \ No newline at end of file + 404: + Title: Page not Found + ErrorCode: 404 \ No newline at end of file From 79ad7c38b7fe85eca58c9162cb6ea4077630e029 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 19 Mar 2015 16:16:54 +1300 Subject: [PATCH 2/2] Ensure SiteConfig::canView --- code/model/SiteConfig.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/code/model/SiteConfig.php b/code/model/SiteConfig.php index 63c90dac..1d48fea9 100644 --- a/code/model/SiteConfig.php +++ b/code/model/SiteConfig.php @@ -200,6 +200,23 @@ class SiteConfig extends DataObject implements PermissionProvider { return $config; } + /** + * Can a user view this SiteConfig instance? + * + * @param Member $member + * @return boolean + */ + public function canView($member = null) { + if(!$member) $member = Member::currentUserID(); + 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