Merge remote-tracking branch 'origin/3'

This commit is contained in:
Ingo Schommer 2015-04-30 08:40:03 +12:00
commit 3b7abb09ed
22 changed files with 597 additions and 331 deletions

View File

@ -54,6 +54,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'treeview',
'listview',
'ListViewForm',
'childfilter',
);
public function init() {
@ -412,57 +413,35 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$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;
@ -706,6 +683,39 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
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
*

View File

@ -16,7 +16,7 @@ class CMSPageAddController extends CMSPageEditController {
/**
* @return Form
*/
function AddForm() {
public function AddForm() {
$pageTypes = array();
foreach($this->PageTypes() as $type) {
$html = sprintf('<span class="page-icon class-%s"></span><strong class="title">%s</strong><span class="description">%s</span>',
@ -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('<span class="hints" data-hints="%s"></span>', Convert::raw2xml($this->SiteTreeHints()))
),
new LiteralField('PageModeHeader', sprintf($numericLabelTmpl, 1, _t('CMSMain.ChoosePageParentMode', 'Choose where to create this page'))),
$parentModeField = new SelectionGroup(
"ParentModeField",
@ -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,11 +143,7 @@ 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())
if(!singleton($className)->canCreate(Member::currentUser(), array('Parent' => $parentObj))) {
return Security::permissionFailure($this);
}

View File

@ -227,34 +227,34 @@ class ErrorPage extends Page {
public function doPublish() {
parent::doPublish();
return $this->writeStaticPage();
}
/**
* Write out the published version of the page to the filesystem
*
* @return mixed Either true, or an error
*/
public function writeStaticPage() {
// Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
$oldEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
Config::inst()->update('SSViewer', 'theme_enabled', true);
$response = Director::test(Director::makeRelative($this->Link()));
Config::inst()->update('SSViewer', 'theme_enabled', $oldEnabled);
$errorContent = $response->getBody();
// Make the base tag dynamic.
// $errorContent = preg_replace('/<base[^>]+href="' . str_replace('/','\\/', Director::absoluteBaseURL()) . '"[^>]*>/i', '<base href="$BaseURL" />', $errorContent);
// Check we have an assets base directory, creating if it we don't
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH, 02775);
}
// if the page is published in a language other than default language,
// write a specific language version of the HTML page
$filePath = self::get_filepath_for_errorcode($this->ErrorCode, $this->Locale);
if($fh = fopen($filePath, "w")) {
fwrite($fh, $errorContent);
fclose($fh);
} else {
if (!file_put_contents($filePath, $errorContent)) {
$fileErrorText = _t(
"ErrorPage.ERRORFILEPROBLEM",
"Error opening file \"{filename}\" for writing. Please check file permissions.",
'ErrorPage.ERRORFILEPROBLEM',
'Error opening file "{filename}" for writing. Please check file permissions.',
array('filename' => $errorFile)
);
$this->response->addHeader('X-Status', rawurlencode($fileErrorText));

View File

@ -934,7 +934,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
@ -1013,12 +1013,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.
*
@ -1026,6 +1026,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) {
@ -1033,15 +1036,30 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$member = Member::currentUserID();
}
// 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
$extended = $this->extendedCan('canCreate', $member);
if($extended !== null) return $extended;
return $this->stat('can_create') != false || Director::isDev();
$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
@ -1083,7 +1101,7 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// Default for unsaved pages
} else {
return $this->getSiteConfig()->canEdit($member);
return $this->getSiteConfig()->canEditPages($member);
}
}
@ -1305,7 +1323,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);
}
/**
@ -2740,9 +2758,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(
"<span class=\"jstree-pageicon\"></span><span class=\"item\">%s</span>",
"<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
Convert::raw2att(Convert::raw2json($children)),
Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle))
);
foreach($flags as $class => $data) {

View File

@ -10,13 +10,41 @@ class SiteTreeFileExtension extends DataExtension {
);
public function updateCMSFields(FieldList $fields) {
$fields->insertAfter(new ReadonlyField('BackLinkCount',
$fields->insertAfter(
ReadonlyField::create(
'BackLinkCount',
_t('AssetTableField.BACKLINKCOUNT', 'Used on:'),
$this->BackLinkTracking()->Count() . ' ' . _t('AssetTableField.PAGES', 'page(s)')),
$this->BackLinkTracking()->Count() . ' ' . _t('AssetTableField.PAGES', 'page(s)'))
->addExtraClass('cms-description-toggle')
->setDescription($this->BackLinkHTMLList()),
'LastEdited'
);
}
/**
* Generate an HTML list which provides links to where a file is used.
*
* @return String
*/
public function BackLinkHTMLList() {
$html = '<em>' . _t('SiteTreeFileExtension.BACKLINK_LIST_DESCRIPTION', 'This list shows all pages where the file has been added through a WYSIWYG editor.') . '</em>';
$html .= '<ul>';
foreach ($this->BackLinkTracking() as $backLink) {
$listItem = '<li>';
// Add the page link
$listItem .= '<a href="' . $backLink->Link() . '" target="_blank">' . Convert::raw2xml($backLink->MenuTitle) . '</a> &ndash; ';
// Add the CMS link
$listItem .= '<a href="' . $backLink->CMSEditLink() . '">' . _t('SiteTreeFileExtension.EDIT', 'Edit') . '</a>';
$html .= $listItem . '</li>';
}
return $html .= '</ul>';
}
/**
* Extend through {@link updateBackLinkTracking()} in your own {@link Extension}.
*
@ -26,13 +54,31 @@ class SiteTreeFileExtension extends DataExtension {
* @param string $limit
* @return ManyManyList
*/
public function BackLinkTracking($filter = "", $sort = "", $join = "", $limit = "") {
public function BackLinkTracking($filter = null, $sort = null, $join = null, $limit = null) {
if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
Deprecation::notice('3.2', 'The $filter, $sort, $join and $limit parameters for
SiteTreeFileExtension::BackLinkTracking() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
if(class_exists("Subsite")){
$rememberSubsiteFilter = Subsite::$disable_subsite_filter;
Subsite::disable_subsite_filter(true);
}
$links = $this->owner->getManyManyComponents('BackLinkTracking', $filter, $sort, $join, $limit);
if($filter || $sort || $join || $limit) {
Deprecation::notice('3.2', 'The $filter, $sort, $join and $limit parameters for
SiteTreeFileExtension::BackLinkTracking() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
$links = $this->owner->getManyManyComponents('BackLinkTracking');
if($this->owner->ID) {
$links = $links
->where($filter)
->sort($sort)
->limit($limit);
}
$this->owner->extend('updateBackLinkTracking', $links);
if(class_exists("Subsite")){

View File

@ -27,7 +27,7 @@ class SiteTreeFolderExtension extends DataExtension {
$ids = $query->execute()->column();
if(!count($ids)) continue;
foreach(singleton($className)->has_one() as $relName => $joinClass) {
foreach(singleton($className)->hasOne() as $relName => $joinClass) {
if($joinClass == 'Image' || $joinClass == 'File') {
$fieldName = $relName .'ID';
$query = DataList::create($className)->where("$fieldName > 0");

View File

@ -109,7 +109,7 @@ class SiteTreeLinkTracking extends DataExtension {
}
// Update the "LinkTracking" many_many
if($record->ID && $record->many_many('LinkTracking') && $tracker = $record->LinkTracking()) {
if($record->ID && $record->manyManyComponent('LinkTracking') && $tracker = $record->LinkTracking()) {
$tracker->removeByFilter(sprintf(
'"FieldName" = \'%s\' AND "%s" = %d',
$fieldName,
@ -123,7 +123,7 @@ class SiteTreeLinkTracking extends DataExtension {
}
// Update the "ImageTracking" many_many
if($record->ID && $record->many_many('ImageTracking') && $tracker = $record->ImageTracking()) {
if($record->ID && $record->manyManyComponent('ImageTracking') && $tracker = $record->ImageTracking()) {
$tracker->removeByFilter(sprintf(
'"FieldName" = \'%s\' AND "%s" = %d',
$fieldName,

View File

@ -54,7 +54,7 @@ class VirtualPage extends Page {
$record = $this->CopyContentFrom();
$allFields = $record->db();
if($hasOne = $record->has_one()) foreach($hasOne as $link) $allFields[$link . 'ID'] = "Int";
if($hasOne = $record->hasOne()) foreach($hasOne as $link) $allFields[$link . 'ID'] = "Int";
$virtualFields = array();
foreach($allFields as $field => $type) {
if(!in_array($field, $nonVirtualFields)) $virtualFields[] = $field;
@ -458,6 +458,22 @@ class VirtualPage extends Page {
if(parent::hasMethod($method)) return true;
return $this->copyContentFrom()->hasMethod($method);
}
/**
* Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
* on this object.
*
* @param string $field
* @return string
*/
public function castingHelper($field) {
if($this->copyContentFrom()) {
return $this->copyContentFrom()->castingHelper($field);
} else {
return parent::castingHelper($field);
}
}
}
/**

View File

@ -20,7 +20,7 @@
"composer/installers": "*",
"silverstripe/framework": "4.0.x-dev",
"silverstripe/reports": "*",
"silverstripe/siteconfig": "*"
"silverstripe/siteconfig": "3.2.x-dev"
},
"extra": {
"branch-alias": {

View File

@ -15,43 +15,114 @@
});
$(".cms-add-form").entwine({
ParentID: 0, // Last selected parentID
ParentCache: {}, // Cache allowed children for each selected page
onadd: function() {
var self = this;
this.find('#Form_AddForm_ParentID_Holder .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('#Form_AddForm_ParentID_Holder .TreeDropdownField').data('metadata'),
id = this.find('#Form_AddForm_ParentID_Holder .TreeDropdownField').getValue(),
newClassName = (id && metadata) ? metadata.ClassName : null,
hintKey = (newClassName) ? newClassName : 'Root',
hint = (typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null,
allAllowed = true;
var hints = this.data('hints'),
parentTree = this.find('#Form_AddForm_ParentID_Holder .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 = [];
var disallowedChildren = (hint && typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [],
defaultChildClass = (hint && typeof hint.defaultChild != 'undefined') ? hint.defaultChild : null;
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('#Form_AddForm_PageType_Holder 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
if(defaultChildClass) {
var selectedEl = this.find('#Form_AddForm_PageType_Holder li input[value=' + defaultChildClass + ']').parents('li:first');
var selectedEl = this
.find('#Form_AddForm_PageType_Holder li input[value=' + defaultChildClass + ']')
.parents('li:first');
} else {
var selectedEl = this.find('#Form_AddForm_PageType_Holder li:not(.disabled):first');
}
@ -59,7 +130,9 @@
selectedEl.siblings().setSelected(false);
// Disable the "Create" button if none of the pagetypes are available
var buttonState = (this.find('#Form_AddForm_PageType_Holder li:not(.disabled)').length) ? 'enable' : 'disable';
var buttonState = this.find('#Form_AddForm_PageType_Holder li:not(.disabled)').length
? 'enable'
: 'disable';
this.find('button[name=action_doAdd]').button(buttonState);
this.find('.message-restricted')[allAllowed ? 'hide' : 'show']();
@ -72,10 +145,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) {

View File

@ -118,7 +118,7 @@
// update button
updateURLFromTitle = $('<button />', {
'class': 'update ss-ui-button-small',
'text': 'Update URL',
'text': ss.i18n._t('URLSEGMENT.UpdateURL'),
'click': function(e) {
e.preventDefault();
self.updateURLSegment(self.val());

View File

@ -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<disallowedChildren.length; i++) {
disallowedClass = disallowedChildren[i];
if(allowedChildren[disallowedClass]) {
delete allowedChildren[disallowedClass];
}
}
}
// Convert to menu entries
$.each(allowedChildren, function(klass, klassData){
$.each(allowedChildren, function(klass, title){
hasAllowedChildren = true;
menuAllowedChildren["allowedchildren-" + klass ] = {
'label': '<span class="jstree-pageicon"></span>' + klassData.title,
'label': '<span class="jstree-pageicon"></span>' + title,
'_class': 'class-' + klass,
'action': function(obj) {
$('.cms-container').entwine('.ss').loadPanel(

View File

@ -40,7 +40,8 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
"CMSMain.RollbackToVersion": "Do you really want to roll back to version #%s of this page?",
"URLSEGMENT.Edit": "Edit",
"URLSEGMENT.OK": "OK",
"URLSEGMENT.Cancel": "Cancel"
"URLSEGMENT.Cancel": "Cancel",
"URLSEGMENT.UpdateURL": "Update URL"
}
);
}

View File

@ -35,5 +35,6 @@
"CMSMain.RollbackToVersion": "Do you really want to roll back to version #%s of this page?",
"URLSEGMENT.Edit": "Edit",
"URLSEGMENT.OK": "OK",
"URLSEGMENT.Cancel": "Cancel"
"URLSEGMENT.Cancel": "Cancel",
"URLSEGMENT.UpdateURL": "Update URL"
}

View File

@ -26,14 +26,14 @@
"AssetAdmin.ConfirmDelete": "Vill du verkligen radera denna mapp och alla filer i den?",
"Folder.Name": "Mappnamn",
"Tree.AddSubPage": "Lägg till ny sida här",
"Tree.Duplicate": "Duplicate",
"Tree.EditPage": "Editera",
"Tree.ThisPageOnly": "This page only",
"Tree.ThisPageAndSubpages": "This page and subpages",
"Tree.ShowAsList": "Show children as list",
"Tree.Duplicate": "Duplicera",
"Tree.EditPage": "Redigera",
"Tree.ThisPageOnly": "Endast denna sida",
"Tree.ThisPageAndSubpages": "Denna sida och undersidor",
"Tree.ShowAsList": "Visa undersidor som lista",
"CMSMain.ConfirmRestoreFromLive": "Vill du verkligen kopiera det publicerade innehållet till utkastsajten?",
"CMSMain.RollbackToVersion": "Vill du verkligen gå tillbaka till version %s av denna sida?",
"URLSEGMENT.Edit": "Edit",
"URLSEGMENT.Edit": "Redigera",
"URLSEGMENT.OK": "OK",
"URLSEGMENT.Cancel": "Cancel"
"URLSEGMENT.Cancel": "Avbryt"
}

View File

@ -31,15 +31,15 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
"AssetAdmin.ConfirmDelete": "Vill du verkligen radera denna mapp och alla filer i den?",
"Folder.Name": "Mappnamn",
"Tree.AddSubPage": "Lägg till ny sida här",
"Tree.Duplicate": "Duplicate",
"Tree.EditPage": "Editera",
"Tree.ThisPageOnly": "This page only",
"Tree.ThisPageAndSubpages": "This page and subpages",
"Tree.ShowAsList": "Show children as list",
"Tree.Duplicate": "Duplicera",
"Tree.EditPage": "Redigera",
"Tree.ThisPageOnly": "Endast denna sida",
"Tree.ThisPageAndSubpages": "Denna sida och undersidor",
"Tree.ShowAsList": "Visa undersidor som lista",
"CMSMain.ConfirmRestoreFromLive": "Vill du verkligen kopiera det publicerade innehållet till utkastsajten?",
"CMSMain.RollbackToVersion": "Vill du verkligen gå tillbaka till version %s av denna sida?",
"URLSEGMENT.Edit": "Edit",
"URLSEGMENT.Edit": "Redigera",
"URLSEGMENT.OK": "OK",
"URLSEGMENT.Cancel": "Cancel"
"URLSEGMENT.Cancel": "Avbryt"
});
}

View File

@ -5,7 +5,9 @@ sv:
AppCategoryArchive: Arkivera
AppCategoryAudio: Ljud
AppCategoryDocument: Dokument
AppCategoryFlash: Flash
AppCategoryImage: Bild
AppCategoryVideo: Video
BackToFolder: 'Tillbaka till mappen'
CREATED: Datum
CurrentFolderOnly: 'Begränsa till aktuell mapp?'
@ -43,6 +45,7 @@ sv:
ColumnDateLastModified: 'Datum vid senaste modifiering'
ColumnDateLastPublished: 'Datum vid senaste publicering'
ColumnProblemType: 'Problemtyp'
ColumnURL: URL
HasBrokenFile: 'har trasig fil'
HasBrokenLink: 'har trasig länk'
HasBrokenLinkAndFile: 'har trasig länk och fil'
@ -72,6 +75,7 @@ sv:
AddNew: 'Skapa ny sida'
AddNewButton: 'Skapa ny'
AddPageRestriction: 'OBS: Vissa sidtyper är inte tillåtna här'
Cancel: Avbryt
ChoosePageParentMode: 'Välj var du vill skapa denna sida'
ChoosePageType: 'Välj sidtyp'
Create: Skapa
@ -124,6 +128,7 @@ sv:
SHOWUNPUBLISHED: 'Visa opublicerade versioner'
SHOWVERSION: 'Visa version'
VIEW: Titta på
VIEWINGLATEST: 'Nu visas den senaste versionen.'
VIEWINGVERSION: 'Nu visas version {version}.'
MENUTITLE: Historia
CMSPageHistoryController_versions_ss:
@ -139,6 +144,8 @@ sv:
TreeView: 'Trädvy'
CMSPagesController_ContentToolbar_ss:
MULTISELECT: Flerval
CMSPagesController_Tools_ss:
FILTER: Filter
CMSSearch:
FILTERDATEFROM: Från
FILTERDATEHEADING: Datum
@ -155,6 +162,7 @@ sv:
ContentController:
ARCHIVEDSITE: 'Utkast version'
ARCHIVEDSITEFROM: 'Arkiverad sajt från'
CMS: CMS
DRAFT: Utkast
DRAFTSITE: 'Utkast'
DRAFT_SITE_ACCESS_RESTRICTION: 'Du måste logga med ditt CMS-lösenord för att kunna se utkast och arkiverat material. <a href="%s">Klicka här för att gå tillbaks till den publicerade sajten.</a>'
@ -192,6 +200,8 @@ sv:
415: '415 - Mediatypen stöds inte'
416: '416 - Det efterfrågade intervallet går inte att leverera'
417: '417 - Förväntningen gick inte att infria'
422: '422 - Obehandlingsbar entitet'
429: '429 - För många anrop'
500: '500 - Internt serverfel'
501: '501 - Inte implementerad'
502: '502 - Felaktig gateway'
@ -262,6 +272,7 @@ sv:
SilverStripeNavigator:
ARCHIVED: Arkiverad
SilverStripeNavigatorLink:
ShareInstructions: 'Kopiera och klistra in länken nedan för att dela den här sidan.'
ShareLink: 'Dela länk'
SilverStripeNavigatorLinkl:
CloseLink: Stäng
@ -310,6 +321,7 @@ sv:
DEPENDENT_NOTE: 'Följande sidor berörs av den här sidan. Inklusive virtuella sidor, omdirigeringssidor, och sidor med innehållslänkar.'
DESCRIPTION: 'Generisk innehållssida'
DependtPageColumnLinkType: 'Länktyp'
DependtPageColumnURL: URL
EDITANYONE: 'Alla som kan logga in'
EDITHEADER: 'Vem kan redigera den här sidan?'
EDITONLYTHESE: 'Bara de här (välj från listan)'
@ -332,6 +344,7 @@ sv:
METAEXTRAHELP: 'HTML taggar för övrig meta information. Till exempel &lt;meta name="Namn" content="innehållet kommer här" /&gt;'
MODIFIEDONDRAFTHELP: 'Sidan har ej publicerade ändringar'
MODIFIEDONDRAFTSHORT: Ändrad
MetadataToggle: Metadata
MoreOptions: 'Fler alternativ'
NOTPUBLISHED: 'Ej publicerad'
OBSOLETECLASS: 'Denna sida är den föråldrade typen {type}. Att spara kommer att återställa dess typ och du kan förlora data'
@ -375,12 +388,19 @@ sv:
SiteTreeURLSegmentField:
EMPTY: 'Ange ett URL-segment eller klicka på avbryt'
HelpChars: 'Specialtecken konverteras eller tas bort'
URLSegmentField:
Cancel: Avbryt
Edit: Redigera
OK: OK
ViewArchivedEmail_ss:
CANACCESS: 'Du kan komma åt den arkiverade sajten med denna länk:'
HAVEASKED: 'Du har efterfrågat att se innehållet på vår sajt på'
VirtualPage:
CHOOSE: 'Länkad sida'
DESCRIPTION: 'Visar innehåll från en annan sida'
EditLink: redigera
HEADER: 'Det här är en virutell sida'
HEADERWITHLINK: 'Det här är en virtuell sida som kopierar innehållet från "{title}" ({link})'
PLURALNAME: 'Virtuella sidor'
PageTypNotAllowedOnRoot: 'Ursprungliga sidan av typ "{type}" tillåts inte på rotnivå för denna virtuella sida'
SINGULARNAME: 'Virtuell sida'
@ -392,3 +412,9 @@ sv:
MENUTITLE: 'Redigera sida'
CMSSettingsController:
MENUTITLE: Inställningar
CMSSiteTreeFilter_StatusDeletedPages:
Title: 'Raderade sidor'
CMSSiteTreeFilter_StatusDraftPages:
Title: 'Ej publicerade utkast'
CMSSiteTreeFilter_StatusRemovedFromDraftPages:
Title: 'Live men borttagen från utkast'

View File

@ -19,7 +19,7 @@ $ExtraTreeTools
</div>
<% end_if %>
<div class="cms-tree" data-url-tree="$LinkWithSearch($Link(getsubtree))" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&amp;PageType=%s')}" data-url-editpage="$LinkPageEdit('%s')" data-url-duplicate="{$Link('duplicate/%s')}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s')}" data-url-listview="{$Link('?view=list')}" data-hints="$SiteTreeHints.XML" data-extra-params="SecurityID=$SecurityID">
<div class="cms-tree" data-url-tree="$LinkWithSearch($Link(getsubtree))" data-url-savetreenode="$Link(savetreenode)" data-url-updatetreenodes="$Link(updatetreenodes)" data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&amp;PageType=%s')}" data-url-editpage="$LinkPageEdit('%s')" data-url-duplicate="{$Link('duplicate/%s')}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s')}" data-url-listview="{$Link('?view=list')}" data-hints="$SiteTreeHints.XML" data-childfilter="$Link('childfilter')" data-extra-params="SecurityID=$SecurityID">
$SiteTreeAsUL
</div>
</div>

View File

@ -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'
);
}
/**
@ -302,11 +318,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);

View File

@ -21,6 +21,13 @@ class SiteTreeTest extends SapphireTest {
'SiteTreeTest_StageStatusInherit',
);
/**
* Ensure any current member is logged out
*/
public function logOut() {
if($member = Member::currentUser()) $member->logOut();
}
public function testCreateDefaultpages() {
$remove = SiteTree::get();
if($remove) foreach($remove as $page) $page->delete();
@ -442,10 +449,15 @@ class SiteTreeTest extends SapphireTest {
$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));
@ -469,6 +481,33 @@ class SiteTreeTest extends SapphireTest {
$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
$page = new Page();
@ -506,7 +545,7 @@ class SiteTreeTest extends SapphireTest {
// Confirm that Member.editor can still edit the page
$this->objFromFixture('Member','editor')->logIn();
$this->assertTrue($page->canEdit());
}
}
public function testCompareVersions() {
// Necessary to avoid

View File

@ -1,3 +1,11 @@
SiteConfig:
default:
Title: My test site
Tagline: Default site config
CanViewType: Anyone
CanEditType: LoggedInUsers
CanCreateTopLevelType: LoggedInUsers
Group:
editors:
Title: Editors

View File

@ -591,6 +591,19 @@ class VirtualPageTest extends SapphireTest {
'No field copying from previous original after page type changed'
);
}
public function testVirtualPageFindsCorrectCasting() {
$page = new VirtualPageTest_ClassA();
$page->CastingTest = "Some content";
$page->write();
$virtual = new VirtualPage();
$virtual->CopyContentFromID = $page->ID;
$virtual->write();
$this->assertEquals('VirtualPageTest_TestDBField', $virtual->castingHelper('CastingTest'));
$this->assertEquals('SOME CONTENT', $virtual->obj('CastingTest')->forTemplate());
}
}
class VirtualPageTest_ClassA extends Page implements TestOnly {
@ -599,6 +612,7 @@ class VirtualPageTest_ClassA extends Page implements TestOnly {
'MyInitiallyCopiedField' => 'Text',
'MyVirtualField' => 'Text',
'MyNonVirtualField' => 'Text',
'CastingTest' => 'VirtualPageTest_TestDBField'
);
private static $allowed_children = array('VirtualPageTest_ClassB');
@ -616,6 +630,12 @@ class VirtualPageTest_NotRoot extends Page implements TestOnly {
private static $can_be_root = false;
}
class VirtualPageTest_TestDBField extends Varchar implements TestOnly {
public function forTemplate() {
return strtoupper($this->XML());
}
}
class VirtualPageTest_VirtualPageSub extends VirtualPage implements TestOnly {
private static $db = array(
'MyProperty' => 'Varchar',