From 3df41e1176385215f15fffb04fcba033a5151fb4 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 11 Mar 2015 18:54:08 +1300 Subject: [PATCH 1/2] BUG Fix SiteTree / SiteConfig permissions --- code/controllers/CMSMain.php | 96 +++--- code/controllers/CMSPageAddController.php | 22 +- code/model/SiteConfig.php | 87 +++-- code/model/SiteTree.php | 65 ++-- javascript/CMSMain.AddForm.js | 106 +++++- javascript/CMSMain.Tree.js | 18 +- templates/Includes/CMSMain_TreeView.ss | 2 +- tests/controller/CMSMainTest.php | 40 ++- tests/model/SiteConfigTest.php | 49 ++- tests/model/SiteConfigTest.yml | 7 + tests/model/SiteTreeTest.php | 375 ++++++++++++---------- tests/model/SiteTreeTest.yml | 8 + 12 files changed, 562 insertions(+), 313 deletions(-) create mode 100644 tests/model/SiteConfigTest.yml 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 %> -
+
$SiteTreeAsUL
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*\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 From a84361e4e8eb1a15ca5896c62a796b832b7b24e8 Mon Sep 17 00:00:00 2001 From: Damian Mooyman <damian@silverstripe.com> 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 eadf4245..5198e74e 100644 --- a/code/model/SiteConfig.php +++ b/code/model/SiteConfig.php @@ -239,6 +239,23 @@ class SiteConfig extends DataObject implements PermissionProvider, TemplateGloba 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