diff --git a/code/controllers/ContentController.php b/code/controllers/ContentController.php index 2cd3f418..8f8e8afc 100644 --- a/code/controllers/ContentController.php +++ b/code/controllers/ContentController.php @@ -102,19 +102,24 @@ class ContentController extends Controller { // Check page permissions if($this->dataRecord && $this->URLSegment != 'Security' && !$this->dataRecord->canView()) { - return Security::permissionFailure($this); - } + $permissionMessage = null; - // Draft/Archive security check - only CMS users should be able to look at stage/archived content - if($this->URLSegment != 'Security' && !Session::get('unsecuredDraftSite') && (Versioned::current_archived_date() || (Versioned::current_stage() && Versioned::current_stage() != 'Live'))) { - if(!$this->dataRecord->canViewStage(Versioned::current_stage())) { - $link = $this->Link(); - $message = _t("ContentController.DRAFT_SITE_ACCESS_RESTRICTION", 'You must log in with your CMS password in order to view the draft or archived content. Click here to go back to the published site.'); + // Check if we could view the live version, offer redirect if so + if($this->canViewStage('Live')) { Session::clear('currentStage'); Session::clear('archiveDate'); - - return Security::permissionFailure($this, sprintf($message, Controller::join_links($link, "?stage=Live"))); + + $permissionMessage = sprintf( + _t( + "ContentController.DRAFT_SITE_ACCESS_RESTRICTION", + 'You must log in with your CMS password in order to view the draft or archived content. '. + 'Click here to go back to the published site.' + ), + Controller::join_links($this->Link(), "?stage=Live") + ); } + + return Security::permissionFailure($this, $permissionMessage); } // Use theme from the site config diff --git a/code/model/SiteTree.php b/code/model/SiteTree.php index dc6029e6..54eb6f4b 100644 --- a/code/model/SiteTree.php +++ b/code/model/SiteTree.php @@ -830,6 +830,23 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid // admin override if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true; + // make sure we were loaded off an allowed stage + + // Were we definitely loaded directly off Live during our query? + $fromLive = true; + + foreach (array('mode' => 'stage', 'stage' => 'live') as $param => $match) { + $fromLive = $fromLive && strtolower((string)$this->getSourceQueryParam("Versioned.$param")) == $match; + } + + if(!$fromLive + && !Session::get('unsecuredDraftSite') + && !Permission::checkMember($member, array('CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT'))) { + // If we weren't definitely loaded from live, and we can't view non-live content, we need to + // check to make sure this version is the live version and so can be viewed + if (Versioned::get_versionnumber_by_stage($this->class, 'Live', $this->ID) != $this->Version) return false; + } + // Standard mechanism for accepting permission changes from extensions $extended = $this->extendedCan('canView', $member); if($extended !== null) return $extended; @@ -858,27 +875,25 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid return false; } - + /** - * Determines permissions for a specific stage (see {@link Versioned}). + * Determines canView permissions for the latest version of this Page on a specific stage (see {@link Versioned}). * Usually the stage is read from {@link Versioned::current_stage()}. - * Falls back to {@link canView}. - * + * * @todo Implement in CMS UI. * * @param String $stage * @param Member $member * @return boolean */ - public function canViewStage($stage, $member = null) { - if(!$member) $member = Member::currentUser(); + public function canViewStage($stage = 'Live', $member = null) { + $oldMode = Versioned::get_reading_mode(); + Versioned::reading_stage($stage); - if( - strtolower($stage) == 'stage' && - !(Permission::checkMember($member, 'CMS_ACCESS_CMSMain') || Permission::checkMember($member, 'VIEW_DRAFT_CONTENT')) - ) return false; - - return $this->canView($member); + $versionFromStage = DataObject::get($this->class)->byID($this->ID); + + Versioned::set_reading_mode($oldMode); + return $versionFromStage ? $versionFromStage->canView($member) : false; } /** diff --git a/tests/controller/ContentControllerPermissionsTest.php b/tests/controller/ContentControllerPermissionsTest.php index e8ce8eb0..189da4af 100644 --- a/tests/controller/ContentControllerPermissionsTest.php +++ b/tests/controller/ContentControllerPermissionsTest.php @@ -10,11 +10,16 @@ class ContentControllerPermissionsTest extends FunctionalTest { protected $autoFollowRedirection = false; public function testCanViewStage() { + // Create a new page $page = new Page(); $page->URLSegment = 'testpage'; $page->write(); $page->publish('Stage', 'Live'); - + + // Add a stage-only version + $page->Content = "Version2"; + $page->write(); + $response = $this->get('/testpage'); $this->assertEquals($response->getStatusCode(), 200, 'Doesnt require login for implicit live stage'); diff --git a/tests/model/SiteTreePermissionsTest.php b/tests/model/SiteTreePermissionsTest.php index 93731aff..c890ade3 100644 --- a/tests/model/SiteTreePermissionsTest.php +++ b/tests/model/SiteTreePermissionsTest.php @@ -126,7 +126,16 @@ class SiteTreePermissionsTest extends FunctionalTest { } public function testCanViewStage() { + $this->useDraftSite(false); // useDraftSite deliberately disables checking the stage as part of canView + + // Get page & make sure it exists on Live $page = $this->objFromFixture('Page', 'standardpage'); + $page->publish('Stage', 'Live'); + + // Then make sure there's a new version on Stage + $page->Title = 1; + $page->write(); + $editor = $this->objFromFixture('Member', 'editor'); $websiteuser = $this->objFromFixture('Member', 'websiteuser'); @@ -135,6 +144,8 @@ class SiteTreePermissionsTest extends FunctionalTest { $this->assertTrue($page->canViewStage('Live', $editor)); $this->assertTrue($page->canViewStage('Stage', $editor)); + + $this->useDraftSite(); } public function testAccessTabOnlyDisplaysWithGrantAccessPermissions() { diff --git a/tests/search/SearchFormTest.php b/tests/search/SearchFormTest.php index bb59b214..f060842b 100644 --- a/tests/search/SearchFormTest.php +++ b/tests/search/SearchFormTest.php @@ -11,7 +11,7 @@ class ZZZSearchFormTest extends FunctionalTest { protected static $fixture_file = 'SearchFormTest.yml'; - + protected $mockController; public function waitUntilIndexingFinished() { @@ -88,7 +88,6 @@ class ZZZSearchFormTest extends FunctionalTest { ); } - /* public function testUnpublishedPagesNotIncluded() { if(!$this->checkFulltextSupport()) return; @@ -102,14 +101,15 @@ class ZZZSearchFormTest extends FunctionalTest { 'Unpublished pages are not found by searchform' ); } - */ - + public function testPagesRestrictedToLoggedinUsersNotIncluded() { if(!$this->checkFulltextSupport()) return; $sf = new SearchForm($this->mockController, 'SearchForm'); $page = $this->objFromFixture('SiteTree', 'restrictedViewLoggedInUsers'); + $page->publish('Stage', 'Live'); + $results = $sf->getResults(null, array('Search'=>'restrictedViewLoggedInUsers')); $this->assertNotContains( $page->ID, @@ -134,6 +134,8 @@ class ZZZSearchFormTest extends FunctionalTest { $sf = new SearchForm($this->mockController, 'SearchForm'); $page = $this->objFromFixture('SiteTree', 'restrictedViewOnlyWebsiteUsers'); + $page->publish('Stage', 'Live'); + $results = $sf->getResults(null, array('Search'=>'restrictedViewOnlyWebsiteUsers')); $this->assertNotContains( $page->ID, @@ -162,13 +164,17 @@ class ZZZSearchFormTest extends FunctionalTest { $member->logOut(); } - public function testInheritedRestrictedPagesNotInlucded() { + public function testInheritedRestrictedPagesNotIncluded() { if(!$this->checkFulltextSupport()) return; $sf = new SearchForm($this->mockController, 'SearchForm'); - + + $parent = $this->objFromFixture('SiteTree', 'restrictedViewLoggedInUsers'); + $parent->publish('Stage', 'Live'); + $page = $this->objFromFixture('SiteTree', 'inheritRestrictedView'); - + $page->publish('Stage', 'Live'); + $results = $sf->getResults(null, array('Search'=>'inheritRestrictedView')); $this->assertNotContains( $page->ID,