Skip to content

Commit

Permalink
Merge pull request silverstripe#115 from mateusz/admin-access
Browse files Browse the repository at this point in the history
Fix CMS Admin access issues
  • Loading branch information
chillu committed Oct 22, 2013
2 parents fc07486 + d85412a commit 7c100f9
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 140 deletions.
13 changes: 0 additions & 13 deletions code/SubsiteAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,4 @@ public function getEditForm($id = null, $fields = null) {
return $form;
}

public function getResponseNegotiator() {
$negotiator = parent::getResponseNegotiator();
$self = $this;
// Register a new callback
$negotiator->setCallback('SubsiteList', function() use(&$self) {
return $self->SubsiteList();
});
return $negotiator;
}

public function SubsiteList() {
return $this->renderWith('SubsiteList');
}
}
38 changes: 38 additions & 0 deletions code/SubsiteXHRController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* Section-agnostic PJAX controller.
*/
class SubsiteXHRController extends LeftAndMain {

/**
* Relax the access permissions, so anyone who has access to any CMS subsite can access this controller.
*/
public function canView($member = null) {
if (parent::canView()) return true;

if (Subsite::all_accessible_sites()->count()>0) return true;

return false;
}

public function getResponseNegotiator() {
$negotiator = parent::getResponseNegotiator();
$self = $this;

// Register a new callback
$negotiator->setCallback('SubsiteList', function() use(&$self) {
return $self->SubsiteList();
});

return $negotiator;
}

/**
* Provide the list of available subsites as a cms-section-agnostic PJAX handler.
*/
public function SubsiteList() {
return $this->renderWith('SubsiteList');
}

}
222 changes: 130 additions & 92 deletions code/extensions/LeftAndMainSubsites.php
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
<?php
/**
* Decorator designed to add subsites support to LeftAndMain
*
*
* @package subsites
*/
class LeftAndMainSubsites extends Extension {

private static $allowed_actions = array('CopyToSubsite');

function init() {

//Use the session variable for current subsite in the CMS only
Subsite::$use_session_subsiteid = true;

Requirements::css('subsites/css/LeftAndMain_Subsites.css');
Requirements::javascript('subsites/javascript/LeftAndMain_Subsites.js');
Requirements::javascript('subsites/javascript/VirtualPage_Subsites.js');

if(isset($_GET['SubsiteID'])) {
// Clear current page when subsite changes (or is set for the first time)
if(!Session::get('SubsiteID') || $_GET['SubsiteID'] != Session::get('SubsiteID')) {
Session::clear("{$this->owner->class}.currentPage");
}

// Update current subsite in session
Subsite::changeSubsite($_GET['SubsiteID']);

//Redirect to clear the current page
return $this->owner->redirect('admin/');
}

// Set subsite ID based on currently shown record
$req = $this->owner->getRequest();
Expand All @@ -50,24 +33,61 @@ function getCMSTreeTitle() {
function updatePageOptions(&$fields) {
$fields->push(new HiddenField('SubsiteID', 'SubsiteID', Subsite::currentSubsiteID()));
}

/*
* Returns a list of the subsites accessible to the current user

/**
* Find all subsites accessible for current user on this controller.
*
* @return ArrayList of {@link Subsite} instances.
*/
public function Subsites() {
// figure out what permission the controller needs
// Subsite::accessible_sites() expects something, so if there's no permission
// then fallback to using CMS_ACCESS_LeftAndMain.
$permission = 'CMS_ACCESS_' . $this->owner->class;
$available = Permission::get_codes(false);
if(!isset($available[$permission])) {
$permission = $this->owner->stat('required_permission_codes');
if(!$permission) {
$permission = 'CMS_ACCESS_LeftAndMain';
function sectionSites($includeMainSite = true, $mainSiteTitle = "Main site", $member = null) {
// Rationalise member arguments
if(!$member) $member = Member::currentUser();
if(!$member) return new ArrayList();
if(!is_object($member)) $member = DataObject::get_by_id('Member', $member);

// Collect permissions - honour the LeftAndMain::required_permission_codes, current model requires
// us to check if the user satisfies ALL permissions. Code partly copied from LeftAndMain::canView.
$codes = array();
$extraCodes = Config::inst()->get($this->owner->class, 'required_permission_codes');
if($extraCodes !== false) {
if($extraCodes) $codes = array_merge($codes, (array)$extraCodes);
else $codes[] = "CMS_ACCESS_{$this->owner->class}";
} else {
// Check overriden - all subsites accessible.
return Subsite::all_sites();
}

// Find subsites satisfying all permissions for the Member.
$codesPerSite = array();
$sitesArray = array();
foreach ($codes as $code) {
$sites = Subsite::accessible_sites($code, $includeMainSite, $mainSiteTitle, $member);
foreach ($sites as $site) {
// Build the structure for checking how many codes match.
$codesPerSite[$site->ID][$code] = true;

// Retain Subsite objects for later.
$sitesArray[$site->ID] = $site;
}
}

return Subsite::accessible_sites($permission);
// Find sites that satisfy all codes conjuncitvely.
$accessibleSites = new ArrayList();
foreach ($codesPerSite as $siteID => $siteCodes) {
if (count($siteCodes)==count($codes)) {
$accessibleSites->push($sitesArray[$siteID]);
}
}

return $accessibleSites;
}

/*
* Returns a list of the subsites accessible to the current user.
* It's enough for any section to be accessible for the section to be included.
*/
public function Subsites() {
return Subsite::all_accessible_sites();
}

/*
Expand Down Expand Up @@ -101,97 +121,115 @@ public function ListSubsites(){
return $output;
}

/*
* Returns a subset of the main menu, filtered by admins that have
* a subsiteCMSShowInMenu method returning true
*
* @return ArrayList
*/
public function SubsiteMainMenu(){
if(Subsite::currentSubsiteID() == 0){
return $this->owner->MainMenu();
public function alternateMenuDisplayCheck($controllerName) {
if(!class_exists($controllerName)){
return false;
}
// loop main menu items, add all items that have subsite support
$mainMenu = $this->owner->MainMenu();
$subsitesMenu = new ArrayList();

foreach($mainMenu as $menuItem){

$controllerName = $menuItem->MenuItem->controller;

if(class_exists($controllerName)){
$controller = singleton($controllerName);

if($controller->hasMethod('subsiteCMSShowInMenu') && $controller->subsiteCMSShowInMenu()){
$subsitesMenu->push($menuItem);
}
// Check subsite support.
if(Subsite::currentSubsiteID() == 0){
// Main site always supports everything.
return true;
} else {
$controller = singleton($controllerName);
if($controller->hasMethod('subsiteCMSShowInMenu') && $controller->subsiteCMSShowInMenu()){
return true;
}
}

if($menuItem->Code == 'Help'){
$subsitesMenu->push($menuItem);
}
// It's not necessary to check access permissions here. Framework calls canView on the controller,
// which in turn uses the Permission API which is augmented by our GroupSubsites.

}
return $subsitesMenu;
return false;
}

public function CanAddSubsites() {
return Permission::check("ADMIN", "any", null, "all");
}

/**
* Alternative security checker for LeftAndMain.
* If security isn't found, then it will switch to a subsite where we do have access.
* Do some pre-flight checks if a subsite switch is needed.
* We redirect the user to something accessible if the current section/subsite is forbidden.
*/
public function alternateAccessCheck() {
public function onBeforeInit() {
// We are accessing the CMS, so we need to let Subsites know we will be using the session.
Subsite::$use_session_subsiteid = true;

// Do not try to be smart for AJAX requests.
if ($this->owner->request->isAjax()) {
return;
}

// Catch forced subsite changes that need to cause CMS reloads.
if(isset($_GET['SubsiteID'])) {
// Clear current page when subsite changes (or is set for the first time)
if(!Session::get('SubsiteID') || $_GET['SubsiteID'] != Session::get('SubsiteID')) {
Session::clear("{$this->owner->class}.currentPage");
}

// Update current subsite in session
Subsite::changeSubsite($_GET['SubsiteID']);

//Redirect to clear the current page
return $this->owner->redirect('admin/');
}

$className = $this->owner->class;

// Switch to the subsite of the current page
// Transparently switch to the subsite of the current page.
if ($this->owner->class == 'CMSMain' && $currentPage = $this->owner->currentPage()) {
if (Subsite::currentSubsiteID() != $currentPage->SubsiteID) {
Subsite::changeSubsite($currentPage->SubsiteID);
}
}

// Switch to a subsite that this user can actually access.

// If we can view current URL there is nothing to do.
if ($this->owner->canView()) {
return;
}

// Admin can access everything, no point in checking.
$member = Member::currentUser();
if($member && Permission::checkMember($member, 'ADMIN')) return true; // admin can access all subsites

$sites = Subsite::accessible_sites("CMS_ACCESS_{$this->owner->class}", true)->map('ID', 'Title');
if(is_object($sites)) $sites = $sites->toArray();

if($sites && !isset($sites[Subsite::currentSubsiteID()])) {
$siteIDs = array_keys($sites);
Subsite::changeSubsite($siteIDs[0]);
return true;
if($member && Permission::checkMember($member, 'ADMIN')) return;

// Check if we have access to current section on the current subsite.
$accessibleSites = $this->owner->sectionSites($member);
if ($accessibleSites->count() && $accessibleSites->find('ID', Subsite::currentSubsiteID())) {
// Current section can be accessed on the current site, all good.
return;
}
// Switch to a different top-level menu item

// If the current section is not accessible, try at least to stick to the same subsite.
$menu = CMSMenu::get_menu_items();
foreach($menu as $candidate) {
if($candidate->controller != $this->owner->class) {
$sites = Subsite::accessible_sites("CMS_ACCESS_{$candidate->controller}", true)->map('ID', 'Title');
if(is_object($sites)) $sites = $sites->toArray();

if($sites && !isset($sites[Subsite::currentSubsiteID()])) {
$siteIDs = array_keys($sites);
Subsite::changeSubsite($siteIDs[0]);
$cClass = $candidate->controller;
$cObj = new $cClass();
$this->owner->redirect($cObj->Link());
return null;
if($candidate->controller && $candidate->controller!=$this->owner->class) {

$accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
if ($accessibleSites->count() && $accessibleSites->find('ID', Subsite::currentSubsiteID())) {
// Section is accessible, redirect there.
$this->owner->redirect(singleton($candidate->controller)->Link());
return;
}
}
}

// Finally, if no section is available, move to any other permitted subsite.
foreach($menu as $candidate) {
if($candidate->controller) {
$accessibleSites = singleton($candidate->controller)->sectionSites(true, 'Main site', $member);
if ($accessibleSites->count()) {
Subsite::changeSubsite($accessibleSites->First()->ID);
$this->owner->redirect(singleton($candidate->controller)->Link());
return;
}
}
}

// If all of those fail, you really don't have access to the CMS
return null;
}

function augmentNewSiteTreeItem(&$item) {
$item->SubsiteID = isset($_POST['SubsiteID']) ? $_POST['SubsiteID'] : Subsite::currentSubsiteID();
}

function onAfterSave($record) {
if($record->hasMethod('NormalRelated') && ($record->NormalRelated() || $record->ReverseRelated())) {
$this->owner->response->addHeader('X-Status', rawurlencode(_t('LeftAndMainSubsites.Saved', 'Saved, please update related pages.')));
Expand Down
Loading

0 comments on commit 7c100f9

Please sign in to comment.