Compare commits

...

2 Commits

Author SHA1 Message Date
Guy Sartorelli
cfc479acc0
Merge 47dbda8381 into bd48b04731 2024-10-17 03:01:50 +00:00
Guy Sartorelli
47dbda8381
API Make CMSMain more generic
Remove hardcoded references to pages and SiteTree
Remove assumption that records are versioned
Remove or validate assumptions about methods on the model class
Improve general architecture of CMSMain
2024-10-17 16:01:33 +13:00
27 changed files with 648 additions and 628 deletions

View File

@ -2,7 +2,6 @@
use SilverStripe\Admin\CMSMenu; use SilverStripe\Admin\CMSMenu;
use SilverStripe\CMS\Controllers\CMSMain; use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageAddController;
use SilverStripe\CMS\Controllers\CMSPageEditController; use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\CMSPageSettingsController; use SilverStripe\CMS\Controllers\CMSPageSettingsController;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
@ -35,4 +34,3 @@ ShortcodeParser::get('default')->register(
CMSMenu::remove_menu_class(CMSMain::class); CMSMenu::remove_menu_class(CMSMain::class);
CMSMenu::remove_menu_class(CMSPageEditController::class); CMSMenu::remove_menu_class(CMSPageEditController::class);
CMSMenu::remove_menu_class(CMSPageSettingsController::class); CMSMenu::remove_menu_class(CMSPageSettingsController::class);
CMSMenu::remove_menu_class(CMSPageAddController::class);

View File

@ -4,10 +4,10 @@ After:
- '#corecache' - '#corecache'
--- ---
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.CMSMain_SiteTreeHints: Psr\SimpleCache\CacheInterface.CMSMain_TreeHints:
factory: SilverStripe\Core\Cache\CacheFactory factory: SilverStripe\Core\Cache\CacheFactory
constructor: constructor:
namespace: "CMSMain_SiteTreeHints" namespace: "CMSMain_TreeHints"
Psr\SimpleCache\CacheInterface.SiteTree_CreatableChildren: Psr\SimpleCache\CacheInterface.SiteTree_CreatableChildren:
factory: SilverStripe\Core\Cache\CacheFactory factory: SilverStripe\Core\Cache\CacheFactory
constructor: constructor:

View File

@ -13,10 +13,12 @@ use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish; use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish; use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search; use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search;
use SilverStripe\CMS\Model\CurrentPageIdentifier; use SilverStripe\CMS\Forms\CMSMainAddForm;
use SilverStripe\CMS\Model\CurrentRecordIdentifier;
use SilverStripe\CMS\Model\RedirectorPage; use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\VirtualPage; use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Search\SearchForm;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
@ -25,7 +27,6 @@ use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\PjaxResponseNegotiator; use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Cache\MemberCacheFlusher; use SilverStripe\Core\Cache\MemberCacheFlusher;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Environment; use SilverStripe\Core\Environment;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
@ -47,10 +48,8 @@ use SilverStripe\Forms\LabelField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\Tab; use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField;
use SilverStripe\Model\List\ArrayList; use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
@ -60,7 +59,6 @@ use SilverStripe\ORM\Hierarchy\MarkedSet;
use SilverStripe\Model\List\SS_List; use SilverStripe\Model\List\SS_List;
use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Security\InheritedPermissions; use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionProvider; use SilverStripe\Security\PermissionProvider;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
@ -71,6 +69,7 @@ use SilverStripe\Versioned\ChangeSetItem;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController; use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
use SilverStripe\Versioned\RecursivePublishable;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
/** /**
@ -78,53 +77,47 @@ use SilverStripe\View\Requirements;
* *
* This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
* admin menu. * admin menu.
*
* @mixin LeftAndMainPageIconsExtension
*/ */
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider, Flushable, MemberCacheFlusher class CMSMain extends LeftAndMain implements CurrentRecordIdentifier, PermissionProvider, Flushable, MemberCacheFlusher
{ {
/** /**
* Unique ID for page icons CSS block * Unique ID for page icons CSS block
*/ */
const PAGE_ICONS_ID = 'PageIcons'; public const PAGE_ICONS_ID = 'PageIcons'; // @TODO AHHHH!!!
private static $url_segment = 'pages'; private static string $url_segment = 'pages';
private static $url_rule = '/$Action/$ID/$OtherID'; private static string $url_rule = '/$Action/$ID/$OtherID';
// Maintain a lower priority than other administration sections // Maintain a lower priority than other administration sections
// so that Director does not think they are actions of CMSMain // so that Director does not think they are actions of CMSMain
private static $url_priority = 39; private static int $url_priority = 39;
private static $menu_title = 'Edit Page'; private static $menu_title = 'Pages';
private static $menu_icon_class = 'font-icon-sitemap'; private static string $menu_icon_class = 'font-icon-sitemap';
private static $menu_priority = 10; private static int $menu_priority = 10;
private static $tree_class = SiteTree::class; private static string $tree_class = SiteTree::class;
private static $session_namespace = CMSMain::class; private static string $session_namespace = CMSMain::class;
private static $required_permission_codes = 'CMS_ACCESS_CMSMain'; private static string|array $required_permission_codes = 'CMS_ACCESS_CMSMain';
/** /**
* Should the archive warning message be dynamic based on the specific content? This is slow on larger sites and can be disabled. * Should the archive warning message be dynamic based on the specific content? This is slow on larger sites and can be disabled.
*
* @config
* @var bool
*/ */
private static $enable_dynamic_archive_warning_message = true; private static bool $enable_dynamic_archive_warning_message = true;
/** /**
* Amount of results showing on a single page. * Amount of results showing on a single page.
*
* @config
* @var int
*/ */
private static $page_length = 15; private static int $page_length = 15;
private static $allowed_actions = [ private static array $allowed_actions = [
'add',
'AddForm',
'archive', 'archive',
'deleteitems', 'deleteitems',
'DeleteItemsForm', 'DeleteItemsForm',
@ -138,7 +131,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'EditForm', 'EditForm',
'schema', 'schema',
'SearchForm', 'SearchForm',
'SiteTreeAsUL', 'TreeAsUL',
'getshowdeletedsubtree', 'getshowdeletedsubtree',
'savetreenode', 'savetreenode',
'getsubtree', 'getsubtree',
@ -150,26 +143,26 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'childfilter', 'childfilter',
]; ];
private static $url_handlers = [ private static array $url_handlers = [
'EditForm/$ID' => 'EditForm', 'EditForm/$ID' => 'EditForm',
]; ];
private static $casting = [ private static array $casting = [
'TreeIsFiltered' => 'Boolean', 'TreeIsFiltered' => 'Boolean',
'AddForm' => 'HTMLFragment', 'AddForm' => 'HTMLFragment',
'LinkPages' => 'Text', 'LinkRecords' => 'Text',
'Link' => 'Text', 'Link' => 'Text',
'ListViewForm' => 'HTMLFragment', 'ListViewForm' => 'HTMLFragment',
'ExtraTreeTools' => 'HTMLFragment', 'ExtraTreeTools' => 'HTMLFragment',
'PageList' => 'HTMLFragment', 'RecordList' => 'HTMLFragment',
'PageListSidebar' => 'HTMLFragment', 'RecordListSidebar' => 'HTMLFragment',
'SiteTreeHints' => 'HTMLFragment', 'TreeHints' => 'HTMLFragment',
'SecurityID' => 'Text', 'SecurityID' => 'Text',
'SiteTreeAsUL' => 'HTMLFragment', 'TreeAsUL' => 'HTMLFragment',
]; ];
private static $dependencies = [ private static array $dependencies = [
'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_SiteTreeHints', 'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_TreeHints',
]; ];
/** /**
@ -197,7 +190,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// In case we're not showing a specific record, explicitly remove any session state, // In case we're not showing a specific record, explicitly remove any session state,
// to avoid it being highlighted in the tree, and causing an edit form to show. // to avoid it being highlighted in the tree, and causing an edit form to show.
if (!$request->param('Action')) { if (!$request->param('Action')) {
$this->setCurrentPageID(null); $this->setCurrentRecordID(null);
} }
return parent::index($request); return parent::index($request);
@ -216,51 +209,42 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Get pages listing area * Get record listing area
*
* @return DBHTMLText
*/ */
public function PageList() public function RecordList(): DBHTMLText
{ {
return $this->renderWith($this->getTemplatesWithSuffix('_PageList')); return $this->renderWith($this->getTemplatesWithSuffix('_RecordList'));
} }
/** /**
* Page list view for edit-form * Record list view for edit-form
*
* @return DBHTMLText
*/ */
public function PageListSidebar() public function RecordListSidebar(): DBHTMLText
{ {
return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar')); return $this->renderWith($this->getTemplatesWithSuffix('_RecordList_Sidebar'));
} }
/** /**
* If this is set to true, the "switchView" context in the * If this is set to true, the "switchView" context in the
* template is shown, with links to the staging and publish site. * template is shown, with links to the staging and publish site.
*
* @return boolean
*/ */
public function ShowSwitchView() public function ShowSwitchView(): bool
{ {
return true; return true;
} }
/** /**
* Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able * Overloads the LeftAndMain::ShowView. Allows to pass a record as a parameter, so we are able
* to switch view also for archived versions. * to switch view also for archived versions.
*
* @param SiteTree $page
* @return array
*/ */
public function SwitchView($page = null) public function SwitchView(?DataObject $record = null): array
{ {
if (!$page) { if (!$record) {
$page = $this->currentPage(); $record = $this->currentRecord();
} }
if ($page) { if ($record) {
$nav = SilverStripeNavigator::get_for_record($page); $nav = SilverStripeNavigator::get_for_record($record);
return $nav['items']; return $nav['items'];
} }
} }
@ -289,14 +273,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link; return $link;
} }
public function LinkPages() public function LinkRecords()
{ {
return CMSPagesController::singleton()->Link(); return CMSPagesController::singleton()->Link();
} }
public function LinkPagesWithSearch() public function LinkRecordsWithSearch()
{ {
return $this->LinkWithSearch($this->LinkPages()); return $this->LinkWithSearch($this->LinkRecords());
} }
/** /**
@ -306,7 +290,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/ */
public function LinkTreeView() public function LinkTreeView()
{ {
// Tree view is just default link to main pages section (no /treeview suffix) // Tree view is just default link to main section (no /treeview suffix)
return CMSMain::singleton()->Link(); return CMSMain::singleton()->Link();
} }
@ -317,12 +301,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/ */
public function LinkListView() public function LinkListView()
{ {
// Note : Force redirect to top level page controller (no parentid) // Note : Force redirect to top level record controller (no parentid)
return $this->LinkWithSearch(CMSMain::singleton()->Link('listview')); return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
} }
/** /**
* Link to list view for children of a parent page * Link to list view for children of a parent record
* *
* @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for * @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for
* client side substitution * client side substitution
@ -366,28 +350,31 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Get the link for editing a page. * Get the link for editing a record.
* *
* @see CMSEditLinkExtension::getCMSEditLinkForManagedDataObject() * @see CMSEditLinkExtension::getCMSEditLinkForManagedDataObject()
*/ */
public function getCMSEditLinkForManagedDataObject(SiteTree $obj): string public function getCMSEditLinkForManagedDataObject(DataObject $obj): string
{ {
return Controller::join_links(CMSPageEditController::singleton()->Link('show'), $obj->ID); return Controller::join_links(CMSPageEditController::singleton()->Link('show'), $obj->ID);
} }
public function LinkPageEdit($id = null) public function LinkRecordEdit($id = null)
{ {
if (!$id) { if (!$id) {
$id = $this->currentPageID(); $id = $this->currentRecordID();
} }
return $this->LinkWithSearch( return $this->LinkWithSearch(
Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id) Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
); );
} }
public function LinkPageSettings() public function LinkRecordSettings()
{ {
if ($id = $this->currentPageID()) { if (!DataObject::singleton($this->getModelClass())->hasMethod('getSettingsFields')) { // @TODO This is awful, I'd much rather it just be part of the main form.
return null;
}
if ($id = $this->currentRecordID()) {
return $this->LinkWithSearch( return $this->LinkWithSearch(
Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id) Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
); );
@ -396,10 +383,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
} }
public function LinkPageHistory() public function LinkRecordHistory()
{ {
$controller = Injector::inst()->get(CMSPageHistoryViewerController::class); $controller = Injector::inst()->get(CMSPageHistoryViewerController::class);
if (($id = $this->currentPageID()) && $controller) { if (($id = $this->currentRecordID()) && $controller) {
if ($controller) { if ($controller) {
return $this->LinkWithSearch( return $this->LinkWithSearch(
Controller::join_links($controller->Link('show'), $id) Controller::join_links($controller->Link('show'), $id)
@ -463,10 +450,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link; return $link;
} }
public function LinkPageAdd($extra = null, $placeholders = null) public function LinkRecordAdd($extra = null, $placeholders = null)
{ {
$link = CMSPageAddController::singleton()->Link(); $link = $this->Link('add');
$this->extend('updateLinkPageAdd', $link); $this->extend('updateLinkRecordAdd', $link);
if ($extra) { if ($extra) {
$link = Controller::join_links($link, $extra); $link = Controller::join_links($link, $extra);
@ -484,10 +471,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/ */
public function LinkPreview() public function LinkPreview()
{ {
$record = $this->getRecord($this->currentPageID()); $record = $this->getRecord($this->currentRecordID());
$baseLink = Director::absoluteBaseURL(); $baseLink = Director::absoluteBaseURL();
if ($record && $record instanceof SiteTree) { if ($record && $record->hasMethod('Link')) {
// if we are an external redirector don't show a link // if we are an external redirector don't show a link //@TODO generalise this!!!!!
if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') { if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
$baseLink = false; $baseLink = false;
} else { } else {
@ -497,12 +484,27 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $baseLink; return $baseLink;
} }
public function add()
{
if ($this->getRequest()->isAjax()) {
return $this->AddForm()->forTemplate();
}
return $this->render([
'Content' => DBHTMLText::create()->setValue($this->AddForm()->forTemplate()),
]);
}
public function AddForm(): Form
{
return CMSMainAddForm::create($this);
}
/** /**
* Return the entire site tree as a nested set of ULs * Return the entire site tree as a nested set of ULs
*/ */
public function SiteTreeAsUL() public function TreeAsUL()
{ {
$treeClass = $this->config()->get('tree_class'); $treeClass = $this->getModelClass();
$filter = $this->getSearchFilter(); $filter = $this->getSearchFilter();
DataObject::singleton($treeClass)->prepopulateTreeDataCache(null, [ DataObject::singleton($treeClass)->prepopulateTreeDataCache(null, [
@ -510,9 +512,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'numChildrenMethod' => $filter ? $filter->getNumChildrenMethod() : 'numChildren', 'numChildrenMethod' => $filter ? $filter->getNumChildrenMethod() : 'numChildren',
]); ]);
$html = $this->getSiteTreeFor($treeClass); $html = $this->getTreeFor($treeClass);
$this->extend('updateSiteTreeAsUL', $html); $this->extend('updateTreeAsUL', $html);
return $html; return $html;
} }
@ -528,9 +530,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* @param string $numChildrenMethod * @param string $numChildrenMethod
* @param callable $filterFunction * @param callable $filterFunction
* @param int $nodeCountThreshold * @param int $nodeCountThreshold
* @return string Nested unordered list with links to each page * @return string Nested unordered list with links to each record
*/ */
public function getSiteTreeFor( public function getTreeFor(
$className, $className,
$rootID = null, $rootID = null,
$childrenMethod = null, $childrenMethod = null,
@ -550,7 +552,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
if (!$filterFunction) { if (!$filterFunction) {
$filterFunction = function ($node) use ($filter) { $filterFunction = function ($node) use ($filter) {
return $filter->isPageIncluded($node); return $filter->isRecordIncluded($node);
}; };
} }
} }
@ -568,20 +570,23 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Mark tree from this node // Mark tree from this node
$markingSet->markPartialTree(); $markingSet->markPartialTree();
// Ensure current page is exposed // Ensure current record is exposed
$currentPage = $this->currentPage(); $currentRecord = $this->currentRecord();
if ($currentPage) { if ($currentRecord) {
$markingSet->markToExpose($currentPage); $markingSet->markToExpose($currentRecord);
} }
// Pre-cache permissions // Pre-cache permissions
$checker = SiteTree::getPermissionChecker(); $modelClass = $this->getModelClass();
$singleton = DataObject::singleton($modelClass);
$checker = $singleton->hasMethod('getPermissionChecker') ? $modelClass::getPermissionChecker() : null; // @TODO eww why is it static?
if ($checker instanceof InheritedPermissions) { if ($checker instanceof InheritedPermissions) {
$checker->prePopulatePermissionCache( $checker->prePopulatePermissionCache(
InheritedPermissions::EDIT, InheritedPermissions::EDIT,
$markingSet->markedNodeIDs() $markingSet->markedNodeIDs()
); );
} }
// @TODO if we don't have inherited permissions, make sure we still DO do permission checks where needed!!
// Render using full-subtree template // Render using full-subtree template
return $markingSet->renderChildren( return $markingSet->renderChildren(
@ -590,7 +595,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
); );
} }
/** /**
* Get callback to determine template customisations for nodes * Get callback to determine template customisations for nodes
* *
@ -599,14 +603,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function getTreeNodeCustomisations() protected function getTreeNodeCustomisations()
{ {
$rootTitle = $this->getCMSTreeTitle(); $rootTitle = $this->getCMSTreeTitle();
return function (SiteTree $node) use ($rootTitle) { return function (DataObject $node) use ($rootTitle) {
return [ return [
'listViewLink' => $this->LinkListViewChildren($node->ID), 'listViewLink' => $this->LinkListViewChildren($node->ID),
'rootTitle' => $rootTitle, 'rootTitle' => $rootTitle,
'extraClass' => $this->getTreeNodeClasses($node), 'extraClass' => $this->getTreeNodeClasses($node),
'Title' => _t( 'Title' => _t(
CMSMain::class . '.PAGETYPE_TITLE', CMSMain::class . '.RECORD_TYPE_TITLE',
'(Page type: {type}) {title}', '(Record type: {type}) {title}',
[ [
'type' => $node->i18n_singular_name(), 'type' => $node->i18n_singular_name(),
'title' => $node->Title, 'title' => $node->Title,
@ -617,15 +621,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Get extra CSS classes for a page's tree node * Get extra CSS classes for a record's tree node
*
* @param SiteTree $node
* @return string
*/ */
public function getTreeNodeClasses(SiteTree $node) public function getTreeNodeClasses(DataObject $node): string
{ {
// Get classes from object // Get classes from object
$classes = $node->CMSTreeClasses(); $classes = $node->CMSTreeClasses(); // @TODO that's obviously not a thing.
// Get status flag classes // Get status flag classes
$flags = $node->getStatusFlags(); $flags = $node->getStatusFlags();
@ -638,7 +639,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Get additional filter classes // Get additional filter classes
$filter = $this->getSearchFilter(); $filter = $this->getSearchFilter();
if ($filter && ($filterClasses = $filter->getPageClasses($node))) { if ($filter && ($filterClasses = $filter->getRecordClasses($node))) { // @TODO rename getRecordClasses or similar (though this is probably part of https://github.com/silverstripe/silverstripe-cms/issues/2949)
if (is_array($filterClasses)) { if (is_array($filterClasses)) {
$filterClasses = implode(' ', $filterClasses); $filterClasses = implode(' ', $filterClasses);
} }
@ -654,8 +655,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/ */
public function getsubtree(HTTPRequest $request): HTTPResponse public function getsubtree(HTTPRequest $request): HTTPResponse
{ {
$html = $this->getSiteTreeFor( $html = $this->getTreeFor(
$this->config()->get('tree_class'), $this->getModelClass(),
$request->getVar('ID'), $request->getVar('ID'),
null, null,
null, null,
@ -687,7 +688,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$record = $this->getRecord($id); $record = $this->getRecord($id);
if (!$record) { if (!$record) {
continue; // In case a page is no longer available continue; // In case a record is no longer available
} }
// Create marking set with sole marked root // Create marking set with sole marked root
@ -700,7 +701,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset) // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
$prev = null; $prev = null;
$className = $this->config()->get('tree_class'); $className = $this->getModelClass();
$next = DataObject::get($className) $next = DataObject::get($className)
->filter('ParentID', $record->ParentID) ->filter('ParentID', $record->ParentID)
->filter('Sort:GreaterThan', $record->Sort) ->filter('Sort:GreaterThan', $record->Sort)
@ -750,17 +751,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if (!SecurityToken::inst()->checkRequest($request)) { if (!SecurityToken::inst()->checkRequest($request)) {
$this->httpError(400); $this->httpError(400);
} }
if (!$this->CanOrganiseSitetree()) { if (!$this->CanOrganiseTree()) {
$this->httpError( $this->httpError(
403, 403,
_t( _t(
__CLASS__.'.CANT_REORGANISE', __CLASS__.'.CANT_REORGANISE2',
"You do not have permission to rearange the site tree. Your change was not saved." "You do not have permission to rearange the tree. Your change was not saved.",
) )
); );
} }
$className = $this->config()->get('tree_class'); $className = $this->getModelClass();
$id = $request->requestVar('ID'); $id = $request->requestVar('ID');
$parentID = $request->requestVar('ParentID'); $parentID = $request->requestVar('ParentID');
if (!is_numeric($id) || !is_numeric($parentID)) { if (!is_numeric($id) || !is_numeric($parentID)) {
@ -768,26 +769,25 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
// Check record exists in the DB // Check record exists in the DB
/** @var SiteTree $node */
$node = DataObject::get_by_id($className, $id); $node = DataObject::get_by_id($className, $id);
if (!$node) { if (!$node) {
$this->httpError( $this->httpError(
500, 500,
_t( _t(
__CLASS__.'.PLEASESAVE', __CLASS__.'.PLEASESAVE2',
"Please Save Page: This page could not be updated because it hasn't been saved yet." "Please Save Record: This record could not be updated because it hasn't been saved yet."
) )
); );
} }
// Check top level permissions // Check top level permissions
$root = $node->getParentType(); $root = $node->getParentType(); // @TODO generalise. POC has `$node->ParentID == 0 ? 'root' : 'subpage';`
if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) { if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
$this->httpError( $this->httpError(
403, 403,
_t( _t(
__CLASS__.'.CANT_REORGANISE', __CLASS__.'.CANT_REORGANISE_TOPLEVEL',
"You do not have permission to alter Top level pages. Your change was not saved." 'You do not have permission to alter Top level records. Your change was not saved.'
) )
); );
} }
@ -805,20 +805,20 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->write(); $node->write();
$statusUpdates['modified'][$node->ID] = [ $statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle 'TreeTitle' => $node->TreeTitle // @TODO generalise.
]; ];
// Update all dependent pages // Update all dependent pages // @TODO generalise!! We shouldn't reference explicitly page types here.
$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID); $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
foreach ($virtualPages as $virtualPage) { foreach ($virtualPages as $virtualPage) {
$statusUpdates['modified'][$virtualPage->ID] = [ $statusUpdates['modified'][$virtualPage->ID] = [
'TreeTitle' => $virtualPage->TreeTitle 'TreeTitle' => $virtualPage->TreeTitle // @TODO generalise.
]; ];
} }
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '') rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL2', 'Reorganised the tree successfully.') ?? '')
); );
} }
@ -830,7 +830,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->Sort = ++$counter; $node->Sort = ++$counter;
$node->write(); $node->write();
$statusUpdates['modified'][$node->ID] = [ $statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle 'TreeTitle' => $node->TreeTitle // @TODO generalise.
]; ];
} elseif (is_numeric($id)) { } elseif (is_numeric($id)) {
// Nodes that weren't "actually moved" shouldn't be registered as // Nodes that weren't "actually moved" shouldn't be registered as
@ -846,7 +846,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '') rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL2', 'Reorganised the tree successfully.') ?? '')
); );
} }
@ -857,64 +857,43 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Whether the current member has the permission to reorganise SiteTree objects. * Whether the current member has the permission to reorganise records.
* @return bool
*/ */
public function CanOrganiseSitetree() public function CanOrganiseTree(): bool
{ {
return Permission::check('SITETREE_REORGANISE'); return (bool) Permission::check('SITETREE_REORGANISE'); // @TODO model needs a method or config to say "this is the permission for that!!"
} }
/** /**
* @return boolean * Whether the tree has been filtered in this request or not.
*/ */
public function TreeIsFiltered() public function TreeIsFiltered(): bool
{ {
$query = $this->getRequest()->getVar('q'); $query = $this->getRequest()->getVar('q');
return !empty($query); return !empty($query);
} }
public function ExtraTreeTools() public function ExtraTreeTools(): string
{ {
$html = ''; $html = '';
$this->extend('updateExtraTreeTools', $html); $this->extend('updateExtraTreeTools', $html);
return $html; return $html;
} }
/**
* This provides information required to generate the search form
* and can be modified on extensions through updateSearchContext
*
* @return \SilverStripe\ORM\Search\SearchContext
*/
public function getSearchContext()
{
$context = SiteTree::singleton()->getDefaultSearchContext();
$this->extend('updateSearchContext', $context);
return $context;
}
/** /**
* Returns the search form schema for the current model * Returns the search form schema for the current model
*
* @return string
*/ */
public function getSearchFieldSchema() public function getSearchFieldSchema(): string
{ {
$schemaUrl = $this->Link('schema/SearchForm'); $schemaUrl = $this->Link('schema/SearchForm');
$context = $this->getSearchContext(); $singleton = DataObject::singleton($this->getModelClass());
$context = $singleton->getDefaultSearchContext();
$params = $this->getRequest()->requestVar('q') ?: []; $params = $this->getRequest()->requestVar('q') ?: [];
$context->setSearchParams($params); $context->setSearchParams($params);
$placeholder = _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search') . ' "' . $placeholder = _t(SearchForm::class . '.FILTERLABELTEXT2', 'Search "{model}"', ['model' => $singleton->i18n_plural_name()]);
SiteTree::singleton()->i18n_plural_name() . '"';
$searchParams = $context->getSearchParams(); $searchParams = $context->getSearchParams();
$searchParams = array_combine(array_map(function ($key) { $searchParams = array_combine(array_map(function ($key) {
return 'Search__' . $key; return 'Search__' . $key;
}, array_keys($searchParams ?? [])), $searchParams ?? []); }, array_keys($searchParams ?? [])), $searchParams ?? []);
@ -930,50 +909,59 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Returns a Form for page searching for use in templates. * Returns a Form for record searching for use in templates.
* *
* Can be modified from a decorator by a 'updateSearchForm' method * Can be modified from a decorator by a 'updateSearchForm' method
*
* @return Form
*/ */
public function getSearchForm() public function getSearchForm(): Form
{ {
$modelClass = $this->getModelClass();
$singleton = DataObject::singleton($modelClass);
// Create the fields // Create the fields
$dateFrom = DateField::create( $dateFrom = DateField::create(
'Search__LastEditedFrom', 'Search__LastEditedFrom',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From') _t(SearchForm::class . '.FILTERDATEFROM', 'From')
)->setLocale(Security::getCurrentUser()->Locale); )->setLocale(Security::getCurrentUser()->Locale);
$dateTo = DateField::create( $dateTo = DateField::create(
'Search__LastEditedTo', 'Search__LastEditedTo',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To') _t(SearchForm::class . '.FILTERDATETO', 'To')
)->setLocale(Security::getCurrentUser()->Locale); )->setLocale(Security::getCurrentUser()->Locale);
$filters = CMSSiteTreeFilter::get_all_filters(); $filters = CMSSiteTreeFilter::get_all_filters();
// Remove 'All pages' as we set that to empty/default value // Remove 'All records' as we set that to empty/default value
unset($filters[CMSSiteTreeFilter_Search::class]); unset($filters[CMSSiteTreeFilter_Search::class]);
$pageFilter = DropdownField::create( $recordFilter = DropdownField::create(
'Search__FilterClass', 'Search__FilterClass',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'), _t(SearchForm::class . '.RECORD_STATUS', '{model} status', ['model' => $singleton->i18n_singular_name()]),
$filters $filters
); );
$pageFilter->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGESALLOPT', 'All pages')); $recordFilter->setEmptyString(_t(
$pageClasses = DropdownField::create( SearchForm::class . '.RECORDS_ALLOPT',
'All {model}',
['model' => mb_strtolower($singleton->i18n_plural_name())]
));
$classes = DropdownField::create(
'Search__ClassName', 'Search__ClassName',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'), _t(
$this->getPageTypes() SearchForm::class . '.RECORD_TYPEOPT',
'{model} type',
'Dropdown for limiting search to a record type',
['model' => $singleton->i18n_singular_name()]
),
$this->getRecordTypes()
); );
$pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any')); $classes->setEmptyString(_t(SearchForm::class . '.RECORD_TYPEANYOPT', 'Any'));
// Group the Datefields // Group the Datefields
$dateGroup = FieldGroup::create( $dateGroup = FieldGroup::create(
_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'), _t(SearchForm::class . '.RECORD_FILTERDATEHEADING', 'Last edited'),
[$dateFrom, $dateTo] [$dateFrom, $dateTo]
)->setName('Search__LastEdited') )->setName('Search__LastEdited')
->addExtraClass('fieldgroup--fill-width'); ->addExtraClass('fieldgroup--fill-width');
// Create the Field list // Create the Field list
$fields = new FieldList( $fields = new FieldList(
$pageFilter, $recordFilter,
$pageClasses, $classes,
$dateGroup $dateGroup
); );
@ -1000,18 +988,16 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Returns a sorted array suitable for a dropdown with pagetypes and their translated name * Returns a sorted array suitable for a dropdown with classes and their localised name
*
* @return array
*/ */
protected function getPageTypes() protected function getRecordTypes(): array
{ {
$pageTypes = []; $types = [];
foreach (SiteTree::page_type_classes() as $pageTypeClass) { foreach (SiteTree::page_type_classes() as $class) { // @TODO generalise!! Not fully into the POC way.
$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name(); $types[$class] = DataObject::singleton($class)->i18n_singular_name();
} }
asort($pageTypes); asort($types);
return $pageTypes; return $types;
} }
public function doSearch(array $data, Form $form): HTTPResponse public function doSearch(array $data, Form $form): HTTPResponse
@ -1028,7 +1014,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
{ {
$breadcrumbs = $this->Breadcrumbs(); $breadcrumbs = $this->Breadcrumbs();
if ($breadcrumbs->count() < 2) { if ($breadcrumbs->count() < 2) {
return $this->LinkPages(); return $this->LinkRecords();
} }
// Get second from end breadcrumb // Get second from end breadcrumb
return $breadcrumbs return $breadcrumbs
@ -1040,15 +1026,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
{ {
$items = ArrayList::create(); $items = ArrayList::create();
if (($this->getAction() !== 'index') && ($record = $this->currentPage())) { if (($this->getAction() !== 'index') && ($record = $this->currentRecord())) {
// The page is being edited // The record is being edited
$this->buildEditFormBreadcrumb($items, $record, $unlinked); $this->buildEditFormBreadcrumb($items, $record, $unlinked);
} else { } else {
// Ensure we always have the "Pages" crumb first // Ensure we always have the admin section crumb first
$this->pushCrumb( $this->pushCrumb(
$items, $items,
CMSPagesController::menu_title(), CMSPagesController::menu_title(),
$unlinked ? false : $this->LinkPages() $unlinked ? false : $this->LinkRecords()
); );
if ($this->TreeIsFiltered()) { if ($this->TreeIsFiltered()) {
@ -1056,13 +1042,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->pushCrumb( $this->pushCrumb(
$items, $items,
_t(CMSMain::class . '.SEARCHRESULTS', 'Search results'), _t(CMSMain::class . '.SEARCHRESULTS', 'Search results'),
($unlinked) ? false : $this->LinkPages() ($unlinked) ? false : $this->LinkRecords()
); );
} elseif ($parentID = $this->getRequest()->getVar('ParentID')) { } elseif ($parentID = $this->getRequest()->getVar('ParentID')) {
// We're navigating the listview. ParentID is the page whose // We're navigating the listview. ParentID is the record whose
// children are currently displayed. // children are currently displayed.
if ($page = SiteTree::get()->byID($parentID)) { if ($record = DataObject::get($this->getModelClass())->byID($parentID)) {
$this->buildListViewBreadcrumb($items, $page); $this->buildListViewBreadcrumb($items, $record);
} }
} }
} }
@ -1084,30 +1070,32 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Build Breadcrumb for the Edit page form. Each crumb links back to its own edit form. * Build Breadcrumb for the Edit form. Each crumb links back to its own edit form.
*/ */
private function buildEditFormBreadcrumb(ArrayList $items, SiteTree $page, bool $unlinked): void private function buildEditFormBreadcrumb(ArrayList $items, DataObject $record, bool $unlinked): void
{ {
// Find all ancestors of the provided page // Find all ancestors of the provided record
$ancestors = $page->getAncestors(true); /** @var DataObject&Hierarchy $record */
$ancestors = $record->getAncestors(true);
$ancestors = array_reverse($ancestors->toArray() ?? []); $ancestors = array_reverse($ancestors->toArray() ?? []);
foreach ($ancestors as $ancestor) { foreach ($ancestors as $ancestor) {
// Link to the ancestor's edit form // Link to the ancestor's edit form
$this->pushCrumb( $this->pushCrumb(
$items, $items,
$ancestor->getMenuTitle(), $ancestor->getMenuTitle(), // @TODO generalise!!
$unlinked ? false : $ancestor->getCMSEditLink() $unlinked ? false : $ancestor->getCMSEditLink()
); );
} }
} }
/** /**
* Build Breadcrumb for the List view. Each crumb links to the list view for that page. * Build Breadcrumb for the List view. Each crumb links to the list view for that record.
*/ */
private function buildListViewBreadcrumb(ArrayList $items, SiteTree $page): void private function buildListViewBreadcrumb(ArrayList $items, DataObject $record): void
{ {
// Find all ancestors of the provided page // Find all ancestors of the provided record
$ancestors = $page->getAncestors(true); /** @var DataObject&Hierarchy $record */
$ancestors = $record->getAncestors(true);
$ancestors = array_reverse($ancestors->toArray() ?? []); $ancestors = array_reverse($ancestors->toArray() ?? []);
//turns the title and link of the breadcrumbs into template-friendly variables //turns the title and link of the breadcrumbs into template-friendly variables
@ -1121,7 +1109,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$params['ParentID'] = $ancestor->ID; $params['ParentID'] = $ancestor->ID;
$this->pushCrumb( $this->pushCrumb(
$items, $items,
$ancestor->getMenuTitle(), $ancestor->getMenuTitle(), // @TODO generalise!!
Controller::join_links($this->Link(), '?' . http_build_query($params ?? [])) Controller::join_links($this->Link(), '?' . http_build_query($params ?? []))
); );
} }
@ -1130,13 +1118,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/** /**
* Create serialized JSON string with site tree hints data to be injected into * Create serialized JSON string with site tree hints data to be injected into
* 'data-hints' attribute of root node of jsTree. * 'data-hints' attribute of root node of jsTree.
*
* @return string Serialized JSON
*/ */
public function SiteTreeHints() public function TreeHints(): string // @TODO rename
{ {
$classes = SiteTree::page_type_classes(); $classes = SiteTree::page_type_classes(); // @TODO generalise!!
$memberID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0; $memberID = Security::getCurrentUser()?->ID ?? 0;
$cache = $this->getHintsCache(); $cache = $this->getHintsCache();
$cacheKey = $this->generateHintsCacheKey($memberID); $cacheKey = $this->generateHintsCacheKey($memberID);
$json = $cache->get($cacheKey); $json = $cache->get($cacheKey);
@ -1154,12 +1140,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$def['Root']['disallowedChildren'] = []; $def['Root']['disallowedChildren'] = [];
// Contains all possible classes to support UI controls listing them all, // Contains all possible classes to support UI controls listing them all,
// such as the "add page here" context menu. // such as the "add record here" context menu.
$def['All'] = []; $def['All'] = [];
// Identify disallows and set globals // Identify disallows and set globals
foreach ($classes as $class) { foreach ($classes as $class) {
$obj = singleton($class); $obj = DataObject::singleton($class);
if ($obj instanceof HiddenClass) { if ($obj instanceof HiddenClass) {
continue; continue;
} }
@ -1170,8 +1156,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
]; ];
// Check if can be created at the root // Check if can be created at the root
$needsPerm = $obj->config()->get('need_permission'); $needsPerm = $obj::config()->get('need_permission');
if (!$obj->config()->get('can_be_root') if ($obj::config()->get('can_be_root') === false
|| (!array_key_exists($class, $canCreate ?? []) || !$canCreate[$class]) || (!array_key_exists($class, $canCreate ?? []) || !$canCreate[$class])
|| ($needsPerm && !$this->can($needsPerm)) || ($needsPerm && !$this->can($needsPerm))
) { ) {
@ -1181,18 +1167,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Hint data specific to the class // Hint data specific to the class
$def[$class] = []; $def[$class] = [];
$defaultChild = $obj->defaultChild(); $defaultChild = $obj->defaultChild(); // @TODO generalise!!
if ($defaultChild !== 'Page' && $defaultChild !== null) { if ($defaultChild !== 'Page' && $defaultChild !== null) { // @TODO Find out where that 'Page' string comes from and fix it
$def[$class]['defaultChild'] = $defaultChild; $def[$class]['defaultChild'] = $defaultChild;
} }
$defaultParent = $obj->defaultParent(); $defaultParent = $obj->defaultParent(); // @TODO generalise!!
if ($defaultParent !== 1 && $defaultParent !== null) { if ($defaultParent !== 1 && $defaultParent !== null) {
$def[$class]['defaultParent'] = $defaultParent; $def[$class]['defaultParent'] = $defaultParent;
} }
} }
$this->extend('updateSiteTreeHints', $def); $this->extend('updateTreeHints', $def);
$json = json_encode($def); $json = json_encode($def);
$cache->set($cacheKey, $json); $cache->set($cacheKey, $json);
@ -1202,24 +1188,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/** /**
* Populates an array of classes in the CMS * Populates an array of classes in the CMS
* which allows the user to change the page type. * which allows the user to change the record's ClassName field.
*
* @return SS_List
*/ */
public function PageTypes() public function RecordTypes(): SS_List
{ {
$classes = SiteTree::page_type_classes(); $classes = SiteTree::page_type_classes(); // @TODO generalise!!
$result = new ArrayList(); $result = new ArrayList();
foreach ($classes as $class) { foreach ($classes as $class) {
$instance = SiteTree::singleton($class); $instance = DataObject::singleton($class);
if ($instance instanceof HiddenClass) { if ($instance instanceof HiddenClass) {
continue; continue;
} }
// skip this type if it is restricted // skip this type if it is restricted
$needPermissions = $instance->config()->get('need_permission'); $needPermissions = $instance::config()->get('need_permission'); // @TODO consider renaming that since it's so vague
if ($needPermissions && !$this->can($needPermissions)) { if ($needPermissions && !$this->can($needPermissions)) {
continue; continue;
} }
@ -1227,8 +1211,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$result->push(new ArrayData([ $result->push(new ArrayData([
'ClassName' => $class, 'ClassName' => $class,
'AddAction' => $instance->i18n_singular_name(), 'AddAction' => $instance->i18n_singular_name(),
'Description' => $instance->i18n_classDescription(), 'Description' => $instance->i18n_classDescription(), // @TODO generalise!!
'IconURL' => $instance->getPageIconURL(), 'IconURL' => $instance->getPageIconURL(), // @TODO generalise!!
'Title' => $instance->i18n_singular_name(), 'Title' => $instance->i18n_singular_name(),
])); ]));
} }
@ -1243,20 +1227,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* *
* @param int $id Record ID * @param int $id Record ID
* @param int $versionID optional Version id of the given record * @param int $versionID optional Version id of the given record
* @return SiteTree
*/ */
public function getRecord($id, $versionID = null) public function getRecord($id, ?int $versionID = null): ?DataObject
{ {
if (!$id) { if (!$id) {
return null; return null;
} }
$treeClass = $this->config()->get('tree_class'); $modelClass = $this->getModelClass();
if ($id instanceof $treeClass) { if ($id instanceof $modelClass) {
return $id; return $id;
} }
if (substr($id ?? '', 0, 3) == 'new') { if (substr($id ?? '', 0, 3) == 'new') {
return $this->getNewItem($id); return $this->getNewItem($id);
} }
if ($id === 'singleton') {
return DataObject::singleton($modelClass);
}
if (!is_numeric($id)) { if (!is_numeric($id)) {
return null; return null;
} }
@ -1267,25 +1253,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$versionID = (int) $this->getRequest()->getVar('Version'); $versionID = (int) $this->getRequest()->getVar('Version');
} }
/** @var SiteTree $record */ $isVersioned = $modelClass::has_extension(Versioned::class);
if ($versionID) { if ($versionID) {
$record = Versioned::get_version($treeClass, $id, $versionID); if (!$isVersioned) {
throw new HTTPResponse_Exception("Cannot get a version of non-versioned $modelClass record", 400);
}
$record = Versioned::get_version($modelClass, $id, $versionID);
} else { } else {
$record = DataObject::get_by_id($treeClass, $id); $record = DataObject::get_by_id($modelClass, $id);
} }
// Then, try getting a record from the live site // Then, try getting a record from the live site
if (!$record) { if (!$record && $isVersioned) {
// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id"); // $record = Versioned::get_one_by_stage($modelClass, "Live", "\"$modelClass\".\"ID\" = $id");
Versioned::set_stage(Versioned::LIVE); Versioned::set_stage(Versioned::LIVE);
singleton($treeClass)->flushCache(); DataObject::singleton($modelClass)->flushCache();
$record = DataObject::get_by_id($treeClass, $id); $record = DataObject::get_by_id($modelClass, $id);
} }
// Then, try getting a deleted record // Then, try getting a deleted record
if (!$record) { if (!$record && $isVersioned) {
$record = Versioned::get_latest_version($treeClass, $id); $record = Versioned::get_latest_version($modelClass, $id);
} }
// Set the reading mode back to what it was. // Set the reading mode back to what it was.
@ -1298,11 +1287,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* {@inheritdoc} * {@inheritdoc}
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @return Form
*/ */
public function EditForm($request = null) public function EditForm($request = null): Form
{ {
// set page ID from request // set record ID from request
if ($request) { if ($request) {
// Validate id is present // Validate id is present
$id = $request->param('ID'); $id = $request->param('ID');
@ -1310,7 +1298,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->httpError(400); $this->httpError(400);
return null; return null;
} }
$this->setCurrentPageID($id); $this->setCurrentRecordID($id);
} }
return $this->getEditForm(); return $this->getEditForm();
} }
@ -1318,13 +1306,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/** /**
* @param int $id * @param int $id
* @param FieldList $fields * @param FieldList $fields
* @return Form
*/ */
public function getEditForm($id = null, $fields = null) public function getEditForm($id = null, $fields = null): Form
{ {
// Get record // Get record
if (!$id) { if (!$id) {
$id = $this->currentPageID(); $id = $this->currentRecordID();
} }
$record = $this->getRecord($id); $record = $this->getRecord($id);
@ -1340,18 +1327,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Add extra fields // Add extra fields
$deletedFromStage = !$record->isOnDraft(); $deletedFromStage = !$record->isOnDraft();
$fields->push($idField = new HiddenField("ID", false, $id)); $fields->push(new HiddenField("ID", false, $id));
// Necessary for different subsites // Necessary for different subsites
$fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink())); $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink())); // @TODO generalise!!
$fields->push($liveLinkField = new HiddenField("LiveLink")); $fields->push($liveLinkField = new HiddenField("LiveLink"));
$fields->push($stageLinkField = new HiddenField("StageLink")); $fields->push($stageLinkField = new HiddenField("StageLink"));
$fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage")); $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
$fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle())); $fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle())); // @TODO generalise!!
$archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record)); $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
// Build preview / live links // Build preview / live links
$liveLink = $record->getAbsoluteLiveLink(); $liveLink = $record->getAbsoluteLiveLink(); // @TODO generalise!!
if ($liveLink) { if ($liveLink) {
$liveLinkField->setValue($liveLink); $liveLinkField->setValue($liveLink);
} }
@ -1437,10 +1424,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $form; return $form;
} }
public function EmptyForm() public function EmptyForm(): Form
{ {
$fields = new FieldList( $fields = new FieldList(
new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist")) new LabelField('RecordDoesntExistLabel', _t(__CLASS__ . '.RECORDNOTEXISTS', "This record doesn't exist"))
); );
$form = parent::EmptyForm(); $form = parent::EmptyForm();
$form->setFields($fields); $form->setFields($fields);
@ -1449,39 +1436,32 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Build an archive warning message based on the page's children * Build an archive warning message based on the record's children
*
* @param SiteTree $record
* @return string
*/ */
/** protected function getArchiveWarningMessage(DataObject $record): string
* Build an archive warning message based on the page's children
*
* @param SiteTree $record
* @return string
*/
protected function getArchiveWarningMessage($record)
{ {
$defaultMessage = _t(
$defaultMessage = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'); LeftAndMain::class . '.ArchiveWarningWithChildren',
'Warning: This record and all of its child records will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'
);
// Option to disable this feature as it is slow on large sites // Option to disable this feature as it is slow on large sites
if (!$this->config()->enable_dynamic_archive_warning_message) { if (!static::config()->get('enable_dynamic_archive_warning_message')) {
return $defaultMessage; return $defaultMessage;
} }
// Get all page's descendants // Get all record's descendants
$descendants = []; $descendants = [];
$this->collateDescendants([$record->ID], $descendants); $this->collateDescendants([$record->ID], $descendants);
if (!$descendants) { if (!$descendants) {
$descendants = []; $descendants = [];
} }
// Get the IDs of all changeset including at least one of the pages. // Get the IDs of all changeset including at least one of the records.
$descendants[] = $record->ID; $descendants[] = $record->ID;
$inChangeSetIDs = ChangeSetItem::get()->filter([ $inChangeSetIDs = ChangeSetItem::get()->filter([
'ObjectID' => $descendants, 'ObjectID' => $descendants,
'ObjectClass' => SiteTree::class 'ObjectClass' => $this->getModelClass(),
])->column('ChangeSetID'); ])->column('ChangeSetID');
// Count number of affected change set // Count number of affected change set
@ -1496,20 +1476,31 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$numCampaigns = mb_strtolower($numCampaigns ?? ''); $numCampaigns = mb_strtolower($numCampaigns ?? '');
if (count($descendants ?? []) > 0 && $affectedChangeSetCount > 0) { if (count($descendants ?? []) > 0 && $affectedChangeSetCount > 0) {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]); $archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarningWithChildrenAndCampaigns',
'Warning: This record and all of its child records will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?',
[ 'NumCampaigns' => $numCampaigns ]
);
} elseif (count($descendants ?? []) > 0) { } elseif (count($descendants ?? []) > 0) {
$archiveWarningMsg = $defaultMessage; $archiveWarningMsg = $defaultMessage;
} elseif ($affectedChangeSetCount > 0) { } elseif ($affectedChangeSetCount > 0) {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]); $archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarningWithCampaigns',
'Warning: This record will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?',
[ 'NumCampaigns' => $numCampaigns ]
);
} else { } else {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'); $archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarning',
'Warning: This record will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'
);
} }
return $archiveWarningMsg; return $archiveWarningMsg;
} }
/** /**
* Find IDs of all descendant pages for the provided ID lists. * Find IDs of all descendant records for the provided ID lists.
* @param int[] $recordIDs * @param int[] $recordIDs
* @param array $collator * @param array $collator
* @return bool * @return bool
@ -1517,7 +1508,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function collateDescendants($recordIDs, &$collator) protected function collateDescendants($recordIDs, &$collator)
{ {
$children = SiteTree::get()->filter(['ParentID' => $recordIDs])->column(); $children = DataObject::get($this->getModelClass())->filter(['ParentID' => $recordIDs])->column();
if ($children) { if ($children) {
foreach ($children as $item) { foreach ($children as $item) {
$collator[] = $item; $collator[] = $item;
@ -1528,10 +1519,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return false; return false;
} }
/** /**
* This method exclusively handles deferred ajax requests to render the * This method exclusively handles deferred ajax requests to render the
* pages tree deferred handler (no pjax-fragment) * records tree deferred handler (no pjax-fragment)
* *
* @return DBHTMLText HTML response with the rendered treeview * @return DBHTMLText HTML response with the rendered treeview
*/ */
@ -1569,21 +1559,21 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Callback to request the list of page types allowed under a given page instance. * Callback to request the list of record types allowed under a given record instance.
* Provides a slower but more precise response over SiteTreeHints * Provides a slower but more precise response over TreeHints
*/ */
public function childfilter(HTTPRequest $request): HTTPResponse public function childfilter(HTTPRequest $request): HTTPResponse
{ {
// Check valid parent specified // Check valid parent specified
$parentID = $request->requestVar('ParentID'); $parentID = $request->requestVar('ParentID');
$parent = SiteTree::get()->byID($parentID); $parent = DataObject::get($this->getModelClass())->byID($parentID);
if (!$parent || !$parent->exists()) { if (!$parent || !$parent->exists()) {
$this->httpError(404); $this->httpError(404);
} }
// Build hints specific to this class // Build hints specific to this class
// Identify disallows and set globals // Identify disallows and set globals
$classes = SiteTree::page_type_classes(); $classes = SiteTree::page_type_classes(); // @TODO generalise!!
$disallowedChildren = []; $disallowedChildren = [];
foreach ($classes as $class) { foreach ($classes as $class) {
$obj = singleton($class); $obj = singleton($class);
@ -1623,8 +1613,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page * Returns the records meet a certain criteria as {@see CMSSiteTreeFilter} or the subrecords of a parent record
* defaulting to no filter and show all pages in first level. * defaulting to no filter and show all records in first level.
* Doubles as search results, if any search parameters are set through {@link SearchForm()}. * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
* *
* @param array $params Search filter criteria * @param array $params Search filter criteria
@ -1637,7 +1627,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if ($filter = $this->getQueryFilter($params)) { if ($filter = $this->getQueryFilter($params)) {
return $filter->getFilteredPages(); return $filter->getFilteredPages();
} else { } else {
$list = DataList::create($this->config()->get('tree_class')); $list = DataObject::get($this->getModelClass());
$parentID = is_numeric($parentID) ? $parentID : 0; $parentID = is_numeric($parentID) ? $parentID : 0;
return $list->filter("ParentID", $parentID); return $list->filter("ParentID", $parentID);
} }
@ -1658,7 +1648,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$gridFieldConfig = GridFieldConfig::create()->addComponents( $gridFieldConfig = GridFieldConfig::create()->addComponents(
Injector::inst()->create(GridFieldSortableHeader::class), Injector::inst()->create(GridFieldSortableHeader::class),
Injector::inst()->create(GridFieldDataColumns::class), Injector::inst()->create(GridFieldDataColumns::class),
Injector::inst()->createWithArgs(GridFieldPaginator::class, [$this->config()->get('page_length')]) Injector::inst()->createWithArgs(GridFieldPaginator::class, [static::config()->get('page_length')])
); );
if ($parentID) { if ($parentID) {
$linkSpec = $this->LinkListViewChildren('%d'); $linkSpec = $this->LinkListViewChildren('%d');
@ -1667,17 +1657,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
->setLinkSpec($linkSpec) ->setLinkSpec($linkSpec)
->setAttributes(['data-pjax-target' => 'ListViewForm,Breadcrumbs']) ->setAttributes(['data-pjax-target' => 'ListViewForm,Breadcrumbs'])
); );
$this->setCurrentPageID($parentID); $this->setCurrentRecordID($parentID);
} }
$gridField = GridField::create('Page', 'Pages', $list, $gridFieldConfig); $gridField = GridField::create('Record', 'Records', $list, $gridFieldConfig); // @TODO make title string i18n
$gridField->setAttribute('cms-loading-ignore-url-params', true); $gridField->setAttribute('cms-loading-ignore-url-params', true);
$columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class); $columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
// Don't allow navigating into children nodes on filtered lists // Don't allow navigating into children nodes on filtered lists
$modelClass = $this->getModelClass();
$fields = [ $fields = [
'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'), 'getTreeTitle' => _t($modelClass . '.TREETITLE', 'Title'),
'i18n_singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'), 'i18n_singular_name' => _t($modelClass . '.TREETYPE', 'Record Type'),
'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'), 'LastEdited' => _t($modelClass . '.LASTUPDATED', 'Last Updated'),
]; ];
$sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class); $sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
$sortableHeader->setFieldSorting(['getTreeTitle' => 'Title']); $sortableHeader->setFieldSorting(['getTreeTitle' => 'Title']);
@ -1696,24 +1687,24 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$columns->setFieldFormatting([ $columns->setFieldFormatting([
'listChildrenLink' => function ($value, &$item) { 'listChildrenLink' => function ($value, &$item) {
/** @var SiteTree $item */ /** @var DataObject&Hierarchy $item */
$num = $item ? $item->numChildren() : null; $num = $item?->numChildren();
if ($num) { if ($num) {
return sprintf( return sprintf(
'<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>', '<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>', // @TODO generalise and i18n-ify
$this->LinkListViewChildren((int)$item->ID), $this->LinkListViewChildren((int)$item->ID),
$num $num
); );
} }
}, },
'getTreeTitle' => function ($value, &$item) { 'getTreeTitle' => function ($value, &$item) {
/** @var SiteTree $item */ /** @var DataObject $item */
$title = sprintf( $title = sprintf(
'<a class="action-detail" href="%s">%s</a>', '<a class="action-detail" href="%s">%s</a>',
$item->getCMSEditLink(), $item->getCMSEditLink(),
$item->TreeTitle // returns HTML, does its own escaping $item->TreeTitle // returns HTML, does its own escaping // @TODO generalise!!
); );
$breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/'); $breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/'); // @TODO generalise!!
// Remove item's tile // Remove item's tile
$breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs ?? '')); $breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs ?? ''));
// Trim spaces around delimiters // Trim spaces around delimiters
@ -1748,11 +1739,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $listview; return $listview;
} }
public function currentPageID() public function currentRecordID()
{ {
$id = parent::currentPageID(); $id = parent::currentRecordID();
$this->extend('updateCurrentPageID', $id); $this->extend('updateCurrentRecordID', $id);
return $id; return $id;
} }
@ -1761,18 +1752,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Data saving handlers // Data saving handlers
/** /**
* Save and Publish page handler * Save and Publish record handler
* *
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function save(array $data, Form $form): HTTPResponse public function save(array $data, Form $form): HTTPResponse
{ {
$className = $this->config()->get('tree_class'); $className = $this->getModelClass();
// Existing or new record? // Existing or new record?
$id = $data['ID']; $id = $data['ID'];
if (substr($id ?? '', 0, 3) != 'new') { if (substr($id ?? '', 0, 3) != 'new') {
/** @var SiteTree $record */
$record = DataObject::get_by_id($className, $id); $record = DataObject::get_by_id($className, $id);
// Check edit permissions // Check edit permissions
if ($record && !$record->canEdit()) { if ($record && !$record->canEdit()) {
@ -1790,14 +1780,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Check publishing permissions // Check publishing permissions
$doPublish = !empty($data['publish']); $doPublish = !empty($data['publish']);
if ($record && $doPublish && !$record->canPublish()) { $isVersioned = $record->hasExtension(Versioned::class);
if ($isVersioned && $doPublish && !$record->canPublish()) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
$record->HasBrokenLink = 0; $record->HasBrokenLink = 0; // @TODO generalise!!
$record->HasBrokenFile = 0; $record->HasBrokenFile = 0; // @TODO generalise!!
if (!$record->ObsoleteClassName) { if ($isVersioned && !$record->ObsoleteClassName) { // @TODO I think that's specific to SiteTree??
$record->writeWithoutVersion(); $record->writeWithoutVersion();
} }
@ -1812,19 +1803,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$form->saveInto($record); $form->saveInto($record);
$record->write(); $record->write();
// If the 'Publish' button was clicked, also publish the page // If the 'Publish' button was clicked, also publish the record
if ($doPublish) { if ($doPublish) {
if (!$record->hasExtension(RecursivePublishable::class)) {
throw new HTTPResponse_Exception(get_class($record) . ' record is not publishable.', 400);
}
$record->publishRecursive(); $record->publishRecursive();
$message = _t( $message = _t(
__CLASS__ . '.PUBLISHED', LeftAndMain::class . '.PUBLISHED_RECORD',
"Published '{title}' successfully.", 'Published {name} "{title}"',
['title' => $record->Title] [
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
); );
} else { } else {
$message = _t( $message = _t(
__CLASS__ . '.SAVED', LeftAndMain::class . '.SAVED_RECORD',
"Saved '{title}' successfully.", 'Saved {name} "{title}"',
['title' => $record->Title] [
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
); );
} }
@ -1835,12 +1835,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/** /**
* @param int|string $id * @param int|string $id
* @param bool $setID * @param bool $setID
* @return mixed|DataObject
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function getNewItem($id, $setID = true) public function getNewItem($id, $setID = true): DataObject
{ {
$parentClass = $this->config()->get('tree_class'); $parentClass = $this->getModelClass();
list(, $className, $parentID) = array_pad(explode('-', $id ?? ''), 3, null); list(, $className, $parentID) = array_pad(explode('-', $id ?? ''), 3, null);
if (!is_a($className, $parentClass ?? '', true)) { if (!is_a($className, $parentClass ?? '', true)) {
@ -1851,23 +1850,20 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
throw new HTTPResponse_Exception($response); throw new HTTPResponse_Exception($response);
} }
/** @var SiteTree $newItem */ /** @var DataObject $newItem */
$newItem = Injector::inst()->create($className); $newItem = Injector::inst()->create($className);
$newItem->Title = _t( $newItem->Title = _t(
__CLASS__ . '.NEWPAGE', LeftAndMain::class . '.NEW_RECORD',
"New {pagetype}", 'New {recordtype}',
'followed by a page type title', ['recordtype' => DataObject::singleton($className)->i18n_singular_name()]
['pagetype' => singleton($className)->i18n_singular_name()]
); );
$newItem->ClassName = $className; $newItem->ClassName = $className;
$newItem->ParentID = $parentID; $newItem->ParentID = $parentID;
// DataObject::fieldExists only checks the current class, not the hierarchy if ($newItem->hasDatabaseField('Sort')) { // @TODO Let the field name be configurable
// This allows the CMS to set the correct sort value $table = DataObject::singleton($parentClass)->baseTable();
if ($newItem->castingHelper('Sort')) {
$table = DataObject::singleton(SiteTree::class)->baseTable();
$maxSort = DB::prepared_query( $maxSort = DB::prepared_query(
"SELECT MAX(\"Sort\") FROM \"$table\" WHERE \"ParentID\" = ?", "SELECT MAX(\"Sort\") FROM \"$table\" WHERE \"ParentID\" = ?", // @TODO this won't work if sort is on a subclass! Use ORM instead
[$parentID] [$parentID]
)->value(); )->value();
$newItem->Sort = (int)$maxSort + 1; $newItem->Sort = (int)$maxSort + 1;
@ -1878,55 +1874,43 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
# Some modules like subsites add extra fields that need to be set when the new item is created # Some modules like subsites add extra fields that need to be set when the new item is created
$this->extend('augmentNewSiteTreeItem', $newItem); $this->extend('updateNewItem', $newItem); // @TODO rename
return $newItem; return $newItem;
} }
/** /**
* Actually perform the publication step * Reverts a record by publishing it to live.
* * Use {@link restoreRecord()} if you want to restore a record
* @param Versioned|DataObject $record
* @return mixed
*/
public function performPublish($record)
{
if ($record && !$record->canPublish()) {
return Security::permissionFailure($this);
}
$record->publishRecursive();
}
/**
* Reverts a page by publishing it to live.
* Use {@link restorepage()} if you want to restore a page
* which was deleted from draft without publishing. * which was deleted from draft without publishing.
* *
* @uses SiteTree->doRevertToLive()
*
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function revert(array $data, Form $form): HTTPResponse public function revert(array $data, Form $form): HTTPResponse
{ {
$modelClass = $this->getModelClass();
if (!isset($data['ID'])) { if (!isset($data['ID'])) {
throw new HTTPResponse_Exception("Please pass an ID in the form content", 400); throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
} }
$id = (int) $data['ID']; if (!$modelClass::has_extension(Versioned::class)) {
$restoredPage = Versioned::get_latest_version(SiteTree::class, $id); throw new HTTPResponse_Exception("$modelClass record cannot be reverted", 400);
if (!$restoredPage) {
throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
} }
$table = DataObject::singleton(SiteTree::class)->baseTable(); $id = (int) $data['ID'];
$liveTable = DataObject::singleton(SiteTree::class)->stageTable($table, Versioned::LIVE); $restoredRecord = Versioned::get_latest_version($modelClass, $id);
$record = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, [ if (!$restoredRecord) {
throw new HTTPResponse_Exception("Record #$id not found", 400);
}
$table = DataObject::singleton($modelClass)->baseTable();
$liveTable = DataObject::singleton($modelClass)->stageTable($table, Versioned::LIVE);
$record = Versioned::get_one_by_stage($modelClass, Versioned::LIVE, [
"\"$liveTable\".\"ID\"" => $id "\"$liveTable\".\"ID\"" => $id
]); ]);
// a user can restore a page without publication rights, as it just adds a new draft state // a user can restore a record without publication rights, as it just adds a new draft state
// (this action should just be available when page has been "deleted from draft") // (this action should just be available when the record has been "deleted from draft")
if ($record && !$record->canEdit()) { if ($record && !$record->canEdit()) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
@ -1939,27 +1923,27 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.RESTORED', LeftAndMain::class . '.RESTORED_RECORD',
"Restored '{title}' successfully", 'Restored {name} "{title}"',
'Param {title} is a title', [
['title' => $record->Title] 'name' => $record->i18n_singular_name(),
) ?? '') 'title' => $record->Title,
]
))
); );
return $this->getResponseNegotiator()->respond($this->getRequest()); return $this->getResponseNegotiator()->respond($this->getRequest());
} }
/** /**
* Delete the current page from draft stage. * Delete the current record from draft stage.
*
* @see deletefromlive()
* *
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function delete(array $data, Form $form): HTTPResponse public function delete(array $data, Form $form): HTTPResponse
{ {
$id = $data['ID']; $id = $data['ID'];
$record = SiteTree::get()->byID($id); $record = DataObject::get($this->getModelClass())->byID($id);
if ($record && !$record->canDelete()) { if ($record && !$record->canDelete()) {
return Security::permissionFailure(); return Security::permissionFailure();
} }
@ -1970,13 +1954,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Delete record // Delete record
$record->delete(); $record->delete();
if ($record->hasExtension(Versioned::class) && $record->hasStages()) {
$message = _t(
LeftAndMain::class . '.ARCHIVED_RECORD',
'Archived {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
} else {
$message = _t(
LeftAndMain::class . '.DELETED_RECORD',
'Deleted {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
}
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode($message)
__CLASS__ . '.REMOVEDPAGEFROMDRAFT',
"Removed '{title}' from the draft site",
['title' => $record->Title]
) ?? '')
); );
// Even if the record has been deleted from stage and live, it can be viewed in "archive mode" // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
@ -1984,14 +1983,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Delete this page from both live and stage * Delete this record from both live and stage
* *
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function archive(array $data, Form $form): HTTPResponse public function archive(array $data, Form $form): HTTPResponse
{ {
$id = $data['ID']; $id = $data['ID'];
$record = SiteTree::get()->byID($id); $record = DataObject::get($this->getModelClass())->byID($id);
if (!$record || !$record->exists()) { if (!$record || !$record->exists()) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404); throw new HTTPResponse_Exception("Bad record ID #$id", 404);
} }
@ -2005,10 +2004,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.ARCHIVEDPAGE', LeftAndMain::class . '.ARCHIVED_RECORD',
"Archived page '{title}'", 'Archived {name} "{title}"',
['title' => $record->Title] [
) ?? '') 'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
))
); );
// Even if the record has been deleted from stage and live, it can be viewed in "archive mode" // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
@ -2024,10 +2026,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function unpublish(array $data, Form $form): HTTPResponse public function unpublish(array $data, Form $form): HTTPResponse
{ {
$className = $this->config()->get('tree_class'); $className = $this->getModelClass();
/** @var SiteTree $record */
$record = DataObject::get_by_id($className, $data['ID']); $record = DataObject::get_by_id($className, $data['ID']);
if (!$record->hasExtension(Versioned::class)) {
throw new HTTPResponse_Exception(get_class($record) . ' record cannot be unpublished.', 400);
}
if ($record && !$record->canUnpublish()) { if ($record && !$record->canUnpublish()) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
@ -2040,10 +2045,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.REMOVEDPAGE', LeftAndMain::class . '.UNPUBLISHED_RECORD',
"Removed '{title}' from the published site", 'Unpublished {name} "{title}"',
['title' => $record->Title] [
) ?? '') 'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
))
); );
return $this->getResponseNegotiator()->respond($this->getRequest()); return $this->getResponseNegotiator()->respond($this->getRequest());
@ -2055,7 +2063,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function rollback() public function rollback()
{ {
return $this->doRollback([ return $this->doRollback([
'ID' => $this->currentPageID(), 'ID' => $this->currentRecordID(),
'Version' => $this->getRequest()->param('VersionID') 'Version' => $this->getRequest()->param('VersionID')
], null); ], null);
} }
@ -2074,8 +2082,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$id = (isset($data['ID'])) ? (int) $data['ID'] : null; $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
$version = (isset($data['Version'])) ? (int) $data['Version'] : null; $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
/** @var SiteTree|Versioned $record */ $modelClass = $this->getModelClass();
$record = Versioned::get_latest_version($this->config()->get('tree_class'), $id); if (!$modelClass::has_extension(Versioned::class)) {
throw new HTTPResponse_Exception("$modelClass record cannot be rolled back", 400);
}
/** @var DataObject&Versioned $record */
$record = Versioned::get_latest_version($modelClass, $id);
if ($record && !$record->canEdit()) { if ($record && !$record->canEdit()) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
@ -2083,22 +2096,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if ($version) { if ($version) {
$record->rollbackRecursive($version); $record->rollbackRecursive($version);
$message = _t( $message = _t(
__CLASS__ . '.ROLLEDBACKVERSIONv2', LeftAndMain::class . '.ROLLEDBACK_VERSION',
"Rolled back to version #{version}.", 'Rolled back to version #{version}.',
['version' => $data['Version']] ['version' => $data['Version']]
); );
} else { } else {
$record->doRevertToLive(); $record->doRevertToLive();
$record->publishRecursive(); $record->publishRecursive();
$message = _t( $message = _t(
__CLASS__ . '.ROLLEDBACKPUBv2', LeftAndMain::class . '.ROLLEDBACK_PUBLISHED',
"Rolled back to published version." 'Rolled back to published version.'
); );
} }
$this->getResponse()->addHeader('X-Status', rawurlencode($message ?? '')); $this->getResponse()->addHeader('X-Status', rawurlencode($message ?? ''));
// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect. // Can be used in different contexts: In normal record edit view, in which case the redirect won't have any effect.
// Or in history view, in which case a revert causes the CMS to re-load the edit view. // Or in history view, in which case a revert causes the CMS to re-load the edit view.
// The X-Pjax header forces a "full" content refresh on redirect. // The X-Pjax header forces a "full" content refresh on redirect.
$url = $record->getCMSEditLink(); $url = $record->getCMSEditLink();
@ -2142,11 +2155,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$forms[$urlSegment] = $formHtml; $forms[$urlSegment] = $formHtml;
} }
} }
$pageHtml = ''; $recordHtml = '';
foreach ($forms as $urlSegment => $html) { foreach ($forms as $urlSegment => $html) {
$pageHtml .= '<div class="params" id="BatchActionParameters_' . $urlSegment . '" style="display:none">' . $html . '</div>'; $recordHtml .= '<div class="params" id="BatchActionParameters_' . $urlSegment . '" style="display:none">' . $html . '</div>';
} }
return new LiteralField('BatchActionParameters', '<div id="BatchActionParameters" class="action-parameters" style="display:none">' . $pageHtml . '</div>'); return new LiteralField('BatchActionParameters', '<div id="BatchActionParameters" class="action-parameters" style="display:none">' . $recordHtml . '</div>');
} }
/** /**
@ -2158,7 +2171,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Restore a completely deleted page from the SiteTree_versions table. * Restore a completely deleted record from the *_versions table.
*/ */
public function restore(array $data, Form $form): HTTPResponse public function restore(array $data, Form $form): HTTPResponse
{ {
@ -2166,21 +2179,29 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return new HTTPResponse("Please pass an ID in the form content", 400); return new HTTPResponse("Please pass an ID in the form content", 400);
} }
$id = (int)$data['ID']; $modelClass = $this->getModelClass();
$restoredPage = Versioned::get_latest_version(SiteTree::class, $id); if (!$modelClass::has_extension(Versioned::class)) {
if (!$restoredPage) { throw new HTTPResponse_Exception("$modelClass record cannot be restored", 400);
return new HTTPResponse("SiteTree #$id not found", 400);
} }
$restoredPage = $restoredPage->doRestoreToStage(); $id = (int)$data['ID'];
$restoredRecord = Versioned::get_latest_version($modelClass, $id);
if (!$restoredRecord) {
return new HTTPResponse("Record #$id not found", 400);
}
$restoredRecord = $restoredRecord->doRestoreToStage();
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.RESTORED', LeftAndMain::class . '.RESTORED_RECORD',
"Restored '{title}' successfully", 'Restored {name} "{title}"',
['title' => $restoredPage->Title] [
) ?? '') 'name' => $restoredRecord->i18n_singular_name(),
'title' => $restoredRecord->Title,
]
))
); );
return $this->getResponseNegotiator()->respond($this->getRequest()); return $this->getResponseNegotiator()->respond($this->getRequest());
@ -2194,31 +2215,34 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
if (($id = $this->urlParams['ID']) && is_numeric($id)) { if (($id = $this->urlParams['ID']) && is_numeric($id)) {
$page = SiteTree::get()->byID($id); $record = DataObject::get($this->getModelClass())->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) { if ($record && !$record->canCreate(null, ['Parent' => $record->Parent()])) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
if (!$page || !$page->ID) { if (!$record || !$record->ID) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404); throw new HTTPResponse_Exception("Bad record ID #$id", 404);
} }
$newPage = $page->duplicate(); $newRecord = $record->duplicate();
// ParentID can be hard-set in the URL. This is useful for pages with multiple parents // ParentID can be hard-set in the URL. This is useful for pages with multiple parents
if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) { if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
$newPage->ParentID = $_GET['parentID']; $newRecord->ParentID = $_GET['parentID'];
$newPage->write(); $newRecord->write();
} }
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.DUPLICATED', LeftAndMain::class . '.DUPLICATED_RECORD',
"Duplicated '{title}' successfully", 'Duplicated {name} "{title}"',
['title' => $newPage->Title] [
) ?? '') 'name' => $newRecord->i18n_singular_name(),
'title' => $newRecord->Title,
]
))
); );
$url = $newPage->getCMSEditLink(); $url = $newRecord->getCMSEditLink();
$this->getResponse()->addHeader('X-ControllerURL', $url); $this->getResponse()->addHeader('X-ControllerURL', $url);
$this->getRequest()->addHeader('X-Pjax', 'Content'); $this->getRequest()->addHeader('X-Pjax', 'Content');
$this->getResponse()->addHeader('X-Pjax', 'Content'); $this->getResponse()->addHeader('X-Pjax', 'Content');
@ -2236,25 +2260,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
Environment::increaseTimeLimitTo(); Environment::increaseTimeLimitTo();
if (($id = $this->urlParams['ID']) && is_numeric($id)) { if (($id = $this->urlParams['ID']) && is_numeric($id)) {
$page = SiteTree::get()->byID($id); $record = DataObject::get($this->getModelClass())->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) { if ($record && !$record->canCreate(null, ['Parent' => $record->Parent()])) {
return Security::permissionFailure($this); return Security::permissionFailure($this);
} }
if (!$page || !$page->ID) { if (!$record || !$record->ID) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404); throw new HTTPResponse_Exception("Bad record ID #$id", 404);
} }
$newPage = $page->duplicateWithChildren(); $newRecord = $record->duplicateWithChildren();
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
__CLASS__ . '.DUPLICATEDWITHCHILDREN', LeftAndMain::class . '.DUPLICATED_RECORD_WITH_CHILDREN',
"Duplicated '{title}' and children successfully", 'Duplicated {name} "{title}" and children',
['title' => $newPage->Title] [
'name' => $newRecord->i18n_singular_name(),
'title' => $newRecord->Title,
]
) ?? '') ) ?? '')
); );
$url = $newPage->getCMSEditLink(); $url = $newRecord->getCMSEditLink();
$this->getResponse()->addHeader('X-ControllerURL', $url); $this->getResponse()->addHeader('X-ControllerURL', $url);
$this->getRequest()->addHeader('X-Pjax', 'Content'); $this->getRequest()->addHeader('X-Pjax', 'Content');
$this->getResponse()->addHeader('X-Pjax', 'Content'); $this->getResponse()->addHeader('X-Pjax', 'Content');
@ -2269,8 +2296,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$title = CMSPagesController::menu_title(); $title = CMSPagesController::menu_title();
return [ return [
"CMS_ACCESS_CMSMain" => [ "CMS_ACCESS_CMSMain" => [
'name' => _t(__CLASS__ . '.ACCESS', "Access to '{title}' section", ['title' => $title]), 'name' => _t(LeftAndMain::class . '.ACCESS', "Access to '{title}' section", ['title' => $title]),
'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 'category' => _t(LeftAndMain::class . '.CMS_ACCESS_CATEGORY', 'CMS Access'),
'help' => _t( 'help' => _t(
__CLASS__ . '.ACCESS_HELP', __CLASS__ . '.ACCESS_HELP',
'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".' 'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
@ -2293,7 +2320,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
/** /**
* Cache key for SiteTreeHints() method * Cache key for TreeHints() method
* *
* @param $memberID * @param $memberID
* @return string * @return string

View File

@ -2,17 +2,15 @@
namespace SilverStripe\CMS\Controllers; namespace SilverStripe\CMS\Controllers;
use Page;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CampaignAdmin\AddToCampaignHandler; use SilverStripe\CampaignAdmin\AddToCampaignHandler;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Core\ArrayLib; use SilverStripe\Core\ArrayLib;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\ORM\DataObject;
/** /**
* @package cms * @package cms
@ -57,7 +55,7 @@ class CMSPageEditController extends CMSMain
public function addtocampaign(array $data, Form $form): HTTPResponse public function addtocampaign(array $data, Form $form): HTTPResponse
{ {
$id = $data['ID']; $id = $data['ID'];
$record = \Page::get()->byID($id); $record = DataObject::get($this->getModelClass())->byID($id);
$handler = AddToCampaignHandler::create($this, $record); $handler = AddToCampaignHandler::create($this, $record);
$response = $handler->addToCampaign($record, $data); $response = $handler->addToCampaign($record, $data);
@ -95,15 +93,16 @@ class CMSPageEditController extends CMSMain
*/ */
public function getAddToCampaignForm($id) public function getAddToCampaignForm($id)
{ {
$modelClass = $this->getModelClass();
// Get record-specific fields // Get record-specific fields
$record = SiteTree::get()->byID($id); $record = DataObject::get($modelClass)->byID($id);
if (!$record) { if (!$record) {
$this->httpError(404, _t( $this->httpError(404, _t(
__CLASS__ . '.ErrorNotFound', __CLASS__ . '.ErrorNotFound',
'That {Type} couldn\'t be found', 'That {Type} couldn\'t be found',
'', '',
['Type' => Page::singleton()->i18n_singular_name()] ['Type' => DataObject::singleton($modelClass)->i18n_singular_name()]
)); ));
return null; return null;
} }
@ -112,7 +111,7 @@ class CMSPageEditController extends CMSMain
__CLASS__.'.ErrorItemPermissionDenied', __CLASS__.'.ErrorItemPermissionDenied',
'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign', 'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign',
'', '',
['ObjectTitle' => Page::singleton()->i18n_singular_name()] ['ObjectTitle' => DataObject::singleton($modelClass)->i18n_singular_name()]
)); ));
return null; return null;
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Controllers; namespace SilverStripe\CMS\Controllers;
use SilverStripe\Forms\Form;
use SilverStripe\Model\ArrayData; use SilverStripe\Model\ArrayData;
class CMSPageSettingsController extends CMSMain class CMSPageSettingsController extends CMSMain
@ -15,11 +16,19 @@ class CMSPageSettingsController extends CMSMain
private static $required_permission_codes = 'CMS_ACCESS_CMSMain'; private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
public function getEditForm($id = null, $fields = null) public function getEditForm($id = null, $fields = null): Form
{ {
$record = $this->getRecord($id ?: $this->currentPageID()); $record = $this->getRecord($id ?: $this->currentRecordID());
return parent::getEditForm($id, ($record) ? $record->getSettingsFields() : null); // @TODO ideally settings isn't its own special thing...
// can we refactor this so it's just another tab in the main form? And just have it lazyload or something?
// At the very least this tab must NOT appear if there are no fields for it.
if ($record && $record->hasMethod('getSettingsFields')) {
$fields = $record->getSettingsFields();
} else {
$fields = null;
}
return parent::getEditForm($id, $fields);
} }
public function getTabIdentifier() public function getTabIdentifier()

View File

@ -2,16 +2,11 @@
namespace SilverStripe\CMS\Controllers; namespace SilverStripe\CMS\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Model\ArrayData;
use stdClass;
// @TODO What a pointless class!!!!!!
class CMSPagesController extends CMSMain class CMSPagesController extends CMSMain
{ {
private static $url_segment = 'pages'; private static $url_segment = 'pages';
private static $url_rule = '/$Action/$ID/$OtherID'; private static $url_rule = '/$Action/$ID/$OtherID';
@ -27,7 +22,7 @@ class CMSPagesController extends CMSMain
return false; return false;
} }
public function isCurrentPage(DataObject $record) public function isCurrentRecord(DataObject $record)
{ {
return false; return false;
} }

View File

@ -113,7 +113,7 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
return $this->numChildrenMethod; return $this->numChildrenMethod;
} }
public function getPageClasses($page) public function getRecordClasses($page)
{ {
if ($this->_cache_ids === null) { if ($this->_cache_ids === null) {
$this->populateIDs(); $this->populateIDs();
@ -178,7 +178,7 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
} }
} }
public function isPageIncluded($page) public function isRecordIncluded($page)
{ {
if ($this->_cache_ids === null) { if ($this->_cache_ids === null) {
$this->populateIDs(); $this->populateIDs();

View File

@ -18,6 +18,8 @@ use SilverStripe\View\Requirements;
/** /**
* Extension to include custom page icons * Extension to include custom page icons
* *
* @TODO AAAHHHHHHHHHHH
*
* @extends Extension<LeftAndMain> * @extends Extension<LeftAndMain>
*/ */
class LeftAndMainPageIconsExtension extends Extension implements Flushable class LeftAndMainPageIconsExtension extends Extension implements Flushable

View File

@ -1,10 +1,10 @@
<?php <?php
namespace SilverStripe\CMS\Controllers; namespace SilverStripe\CMS\Forms;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
@ -18,39 +18,22 @@ use SilverStripe\Forms\SelectionGroup_Item;
use SilverStripe\Forms\TreeDropdownField; use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\SiteConfig\SiteConfig;
class CMSPageAddController extends CMSPageEditController class CMSMainAddForm extends Form
{ {
public function __construct(CMSMain $controller)
private static $url_segment = 'pages/add';
private static $url_rule = '/$Action/$ID/$OtherID';
private static $url_priority = 42;
private static $menu_title = 'Add page';
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
private static $allowed_actions = [
'AddForm',
'doAdd',
'doCancel'
];
/**
* @return Form
*/
public function AddForm()
{ {
$modelClass = $controller->getModelClass();
$pageTypes = []; $pageTypes = [];
$defaultIcon = Config::inst()->get(SiteTree::class, 'icon_class'); $defaultIcon = Config::inst()->get($modelClass, 'icon_class'); // @TODO need a better place for default - maybe try default on class, and fallback to default on cmsmain?
foreach ($this->PageTypes() as $type) { foreach ($controller->RecordTypes() as $type) {
$class = $type->getField('ClassName'); $class = $type->getField('ClassName');
$icon = Config::inst()->get($class, 'icon_class') ?: $defaultIcon; $icon = Config::inst()->get($class, 'icon_class') ?: $defaultIcon;
// If the icon is the SiteTree default and there's some specific icon being provided by `getPageIconURL` // If the icon is the default and there's some specific icon being provided by `getPageIconURL`
// then we don't need to add the icon class. Otherwise the class take precedence. // then we don't need to add the icon class. Otherwise the class take precedence.
if ($icon === $defaultIcon && !empty(singleton($class)->getPageIconURL())) { if ($icon === $defaultIcon && !empty(singleton($class)->getPageIconURL())) {
$icon = ''; $icon = '';
@ -73,24 +56,28 @@ class CMSPageAddController extends CMSPageEditController
$numericLabelTmpl = '<span class="step-label"><span class="flyout">Step %d. </span><span class="title">%s</span></span>'; $numericLabelTmpl = '<span class="step-label"><span class="flyout">Step %d. </span><span class="title">%s</span></span>';
$topTitle = _t('SilverStripe\\CMS\\Controllers\\CMSPageAddController.ParentMode_top', 'Top level'); $topTitle = _t(__CLASS__ . '.ParentMode_top', 'Top level');
$childTitle = _t('SilverStripe\\CMS\\Controllers\\CMSPageAddController.ParentMode_child', 'Under another page'); $childTitle = _t(
__CLASS__ . '.ParentMode_child',
'Under another {type}',
['type' => mb_strtolower(DataObject::singleton($modelClass)->i18n_singular_name())]
);
$fields = new FieldList( $fields = FieldList::create(
$parentModeField = new SelectionGroup( $parentModeField = SelectionGroup::create(
"ParentModeField", 'ParentModeField',
[ [
$topField = new SelectionGroup_Item( $topField = SelectionGroup_Item::create(
"top", 'top',
null, null,
$topTitle $topTitle
), ),
new SelectionGroup_Item( SelectionGroup_Item::create(
'child', 'child',
$parentField = new TreeDropdownField( $parentField = TreeDropdownField::create(
"ParentID", 'ParentID',
"", '',
SiteTree::class, $modelClass,
'ID', 'ID',
'TreeTitle' 'TreeTitle'
), ),
@ -98,7 +85,7 @@ class CMSPageAddController extends CMSPageEditController
) )
] ]
), ),
new LiteralField( LiteralField::create(
'RestrictedNote', 'RestrictedNote',
sprintf( sprintf(
'<p class="alert alert-info message-restricted">%s</p>', '<p class="alert alert-info message-restricted">%s</p>',
@ -108,8 +95,8 @@ class CMSPageAddController extends CMSPageEditController
) )
) )
), ),
$typeField = new OptionsetField( OptionsetField::create(
"PageType", 'PageType',
DBField::create_field( DBField::create_field(
'HTMLFragment', 'HTMLFragment',
sprintf($numericLabelTmpl ?? '', 2, _t('SilverStripe\\CMS\\Controllers\\CMSMain.ChoosePageType', 'Choose page type')) sprintf($numericLabelTmpl ?? '', 2, _t('SilverStripe\\CMS\\Controllers\\CMSMain.ChoosePageType', 'Choose page type'))
@ -134,9 +121,9 @@ class CMSPageAddController extends CMSPageEditController
$parentModeField->addExtraClass('parent-mode'); $parentModeField->addExtraClass('parent-mode');
// CMSMain->currentPageID() automatically sets the homepage, // CMSMain->currentRecordID() automatically sets the homepage,
// which we need to counteract in the default selection (which should default to root, ID=0) // which we need to counteract in the default selection (which should default to root, ID=0)
if ($parentID = $this->getRequest()->getVar('ParentID')) { if ($parentID = $controller->getRequest()->getVar('ParentID')) {
$parentModeField->setValue('child'); $parentModeField->setValue('child');
$parentField->setValue((int)$parentID); $parentField->setValue((int)$parentID);
} else { } else {
@ -145,35 +132,32 @@ class CMSPageAddController extends CMSPageEditController
// Check if the current user has enough permissions to create top level pages // Check if the current user has enough permissions to create top level pages
// If not, then disable the option to do that // If not, then disable the option to do that
if (!SiteConfig::current_site_config()->canCreateTopLevel()) { if (is_a($modelClass, SiteTree::class, true) && !SiteConfig::current_site_config()->canCreateTopLevel()) { // @TODO probably need to make this generic
$topField->setDisabled(true); $topField->setDisabled(true);
$parentModeField->setValue('child'); $parentModeField->setValue('child');
} }
$actions = new FieldList( $actions = FieldList::create(
FormAction::create("doAdd", _t('SilverStripe\\CMS\\Controllers\\CMSMain.Create', "Create")) FormAction::create('doAdd', _t('SilverStripe\\CMS\\Controllers\\CMSMain.Create', 'Create'))
->addExtraClass('btn-primary font-icon-plus-circled') ->addExtraClass('btn-primary font-icon-plus-circled')
->setUseButtonTag(true), ->setUseButtonTag(true),
FormAction::create("doCancel", _t('SilverStripe\\CMS\\Controllers\\CMSMain.Cancel', "Cancel")) FormAction::create('doCancel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.Cancel', 'Cancel'))
->addExtraClass('btn-secondary') ->addExtraClass('btn-secondary')
->setUseButtonTag(true) ->setUseButtonTag(true)
); );
$this->extend('updatePageOptions', $fields); $controller->extend('updatePageOptions', $fields);
$negotiator = $this->getResponseNegotiator(); parent::__construct($controller, 'AddForm', $fields, $actions);
$form = Form::create(
$this, $negotiator = $controller->getResponseNegotiator();
"AddForm", $this->setHTMLID('Form_AddForm')->setStrictFormMethodCheck(false);
$fields, $this->setAttribute('data-hints', $controller->TreeHints());
$actions $this->setAttribute('data-childfilter', $controller->Link('childfilter'));
)->setHTMLID('Form_AddForm')->setStrictFormMethodCheck(false); $this->setValidationResponseCallback(function () use ($negotiator, $controller) {
$form->setAttribute('data-hints', $this->SiteTreeHints()); $request = $controller->getRequest();
$form->setAttribute('data-childfilter', $this->Link('childfilter'));
$form->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $form) {
$request = $this->getRequest();
if ($request->isAjax() && $negotiator) { if ($request->isAjax() && $negotiator) {
$result = $form->forTemplate(); $result = $this->forTemplate();
return $negotiator->respond($request, [ return $negotiator->respond($request, [
'CurrentForm' => function () use ($result) { 'CurrentForm' => function () use ($result) {
return $result; return $result;
@ -182,26 +166,26 @@ class CMSPageAddController extends CMSPageEditController
} }
return null; return null;
}); });
$form->addExtraClass('flexbox-area-grow fill-height cms-add-form cms-content cms-edit-form ' . $this->BaseCSSClasses()); $this->addExtraClass('flexbox-area-grow fill-height cms-add-form cms-content cms-edit-form ' . $controller->BaseCSSClasses());
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); $this->setTemplate($controller->getTemplatesWithSuffix('_AddForm'));
return $form;
} }
public function doAdd(array $data, Form $form): HTTPResponse public function doAdd(array $data, Form $form): HTTPResponse
{ {
$controller = $this->getController();
$modelClass = $controller->getModelClass();
$className = isset($data['PageType']) ? $data['PageType'] : "Page"; $className = isset($data['PageType']) ? $data['PageType'] : "Page";
$parentID = isset($data['ParentID']) ? (int)$data['ParentID'] : 0; $parentID = isset($data['ParentID']) ? (int)$data['ParentID'] : 0;
if (!$parentID && isset($data['Parent'])) { if (!$parentID && isset($data['Parent'])) {
$page = SiteTree::get_by_link($data['Parent']); $page = $modelClass::get_by_link($data['Parent']); // @TODO Obviously no good
if ($page) { if ($page) {
$parentID = $page->ID; $parentID = $page->ID;
} }
} }
if (is_numeric($parentID) && $parentID > 0) { if (is_numeric($parentID) && $parentID > 0) {
$parentObj = SiteTree::get()->byID($parentID); $parentObj = DataObject::get($modelClass)->byID($parentID);
} else { } else {
$parentObj = null; $parentObj = null;
} }
@ -211,16 +195,16 @@ class CMSPageAddController extends CMSPageEditController
} }
if (!singleton($className)->canCreate(Security::getCurrentUser(), ['Parent' => $parentObj])) { if (!singleton($className)->canCreate(Security::getCurrentUser(), ['Parent' => $parentObj])) {
return Security::permissionFailure($this); return Security::permissionFailure($controller);
} }
$record = $this->getNewItem("new-$className-$parentID", false); $record = $controller->getNewItem("new-$className-$parentID", false);
$this->extend('updateDoAdd', $record, $form); $controller->extend('updateDoAdd', $record, $form);
$record->write(); $record->write();
$editController = CMSPageEditController::singleton(); $editController = CMSPageEditController::singleton();
$editController->setRequest($this->getRequest()); $editController->setRequest($controller->getRequest());
$editController->setCurrentPageID($record->ID); $editController->setCurrentRecordID($record->ID);
$session = $this->getRequest()->getSession(); $session = $this->getRequest()->getSession();
$session->set( $session->set(
@ -229,11 +213,11 @@ class CMSPageAddController extends CMSPageEditController
); );
$session->set("FormInfo.Form_EditForm.formError.type", 'good'); $session->set("FormInfo.Form_EditForm.formError.type", 'good');
return $this->redirect(Controller::join_links($editController->Link('show'), $record->ID)); return $controller->redirect($editController->Link('show/' . $record->ID));
} }
public function doCancel(array $data, Form $form): HTTPResponse public function doCancel(): HTTPResponse
{ {
return $this->redirect(CMSMain::singleton()->Link()); return $this->getController()->redirect(CMSMain::singleton()->Link()); // @TODO when there's no CMSPageEditController anymore, change this to $this->getController()->Link()
} }
} }

View File

@ -7,19 +7,19 @@ use SilverStripe\ORM\DataObject;
/** /**
* This interface lets us set up objects that will tell us what the current page is. * This interface lets us set up objects that will tell us what the current page is.
*/ */
interface CurrentPageIdentifier interface CurrentRecordIdentifier
{ {
/** /**
* Get the current page ID. * Get the current page ID.
* @return int * @return int
*/ */
public function currentPageID(); public function currentRecordID();
/** /**
* Check if the given DataObject is the current page. * Check if the given DataObject is the current page.
* @param DataObject $page The page to check. * @param DataObject $page The page to check.
* @return boolean * @return boolean
*/ */
public function isCurrentPage(DataObject $page); public function isCurrentRecord(DataObject $page);
} }

View File

@ -783,12 +783,12 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/ */
public function isCurrent() public function isCurrent()
{ {
$currentPage = Director::get_current_page(); $currentRecord = Director::get_current_page();
if ($currentPage instanceof ContentController) { if ($currentRecord instanceof ContentController) {
$currentPage = $currentPage->data(); $currentRecord = $currentRecord->data();
} }
if ($currentPage instanceof SiteTree) { if ($currentRecord instanceof SiteTree) {
return $currentPage === $this || $currentPage->ID === $this->ID; return $currentRecord === $this || $currentRecord->ID === $this->ID;
} }
return false; return false;
} }
@ -2778,9 +2778,9 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Sort alphabetically, and put current on top // Sort alphabetically, and put current on top
asort($result); asort($result);
if (isset($result[$this->ClassName])) { if (isset($result[$this->ClassName])) {
$currentPageTypeName = $result[$this->ClassName]; $currentRecordTypeName = $result[$this->ClassName];
unset($result[$this->ClassName]); unset($result[$this->ClassName]);
$result = [$this->ClassName => $currentPageTypeName] + $result; $result = [$this->ClassName => $currentRecordTypeName] + $result;
} }
return $result; return $result;

View File

@ -256,6 +256,8 @@ en:
TABCONTENT: 'Main content' TABCONTENT: 'Main content'
TABDEPENDENT: 'Dependent pages' TABDEPENDENT: 'Dependent pages'
TOPLEVEL: 'Site Content (Top Level)' TOPLEVEL: 'Site Content (Top Level)'
TREETITLE: 'Page name'
TREETYPE: 'Page type'
UNTITLED: 'Untitled {pagetype}' UNTITLED: 'Untitled {pagetype}'
URLSegment: 'URL segment' URLSegment: 'URL segment'
UntitledDependentObject: 'Untitled {instanceType}' UntitledDependentObject: 'Untitled {instanceType}'

View File

@ -0,0 +1,47 @@
<div class="flexbox-area-grow cms-content $Controller.BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
<form $FormAttributes data-layout-type="border">
<div class="toolbar toolbar--north">
<div class="toolbar__navigation">
<ol class="breadcrumb">
<li class="breadcrumb__item">
<% if $Controller.SectionTitle %>
$Controller.SectionTitle
<% else %>
<%t SilverStripe\CMS\Controllers\CMSMain.Title 'Data Models'%>
<% end_if %>
</li>
<li class="breadcrumb__item breadcrumb__item--last breadcrumb__item--no-crumb">
<h2 class="breadcrumb__item-title breadcrumb__item-title--last">
<%t SilverStripe\Admin\LeftAndMain.NewRecord 'New {name}' name=$Controller.getRecord('singleton').i18n_singular_name() %>
</h2>
</li>
</ol>
</div>
</div>
<div class="panel panel--padded panel--scrollable flexbox-area-grow">
<% if $Message %>
<p id="{$FormName}_error" class="alert $AlertType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="alert $AlertType" style="display: none"></p>
<% end_if %>
<fieldset>
<% if $Legend %><legend>$Legend</legend><% end_if %>
<% loop $Fields %>
$FieldHolder
<% end_loop %>
</fieldset>
</div>
<div class="toolbar--south">
<% if $Actions %>
<div class="btn-toolbar">
<% loop $Actions %>
$Field
<% end_loop %>
</div>
<% end_if %>
</div>
</form>
</div>

View File

@ -11,17 +11,17 @@
<div class="cms-content-header-tabs cms-tabset"> <div class="cms-content-header-tabs cms-tabset">
<ul class="cms-tabset-nav-primary nav nav-tabs"> <ul class="cms-tabset-nav-primary nav nav-tabs">
<li class="nav-item content-treeview<% if $TabIdentifier == 'edit' %> ui-tabs-active<% end_if %>"> <li class="nav-item content-treeview<% if $TabIdentifier == 'edit' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageEdit" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageEdit"> <a href="$LinkRecordEdit" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordEdit">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabContent 'Content' %> <%t SilverStripe\\CMS\\Controllers\\CMSMain.TabContent 'Content' %>
</a> </a>
</li> </li>
<li class="nav-item content-listview<% if $TabIdentifier == 'settings' %> ui-tabs-active<% end_if %>"> <li class="nav-item content-listview<% if $TabIdentifier == 'settings' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageSettings" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageSettings"> <a href="$LinkRecordSettings" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordSettings">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabSettings 'Settings' %> <%t SilverStripe\\CMS\\Controllers\\CMSMain.TabSettings 'Settings' %>
</a> </a>
</li> </li>
<li class="nav-item content-listview<% if $TabIdentifier == 'history' %> ui-tabs-active<% end_if %>"> <li class="nav-item content-listview<% if $TabIdentifier == 'history' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageHistory" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageHistory"> <a href="$LinkRecordHistory" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordHistory">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabHistory 'History' %> <%t SilverStripe\\CMS\\Controllers\\CMSMain.TabHistory 'History' %>
</a> </a>
</li> </li>

View File

@ -1,6 +1,6 @@
<% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions %> <% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions %>
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\\CMS\\Controllers\\CMSMain.AddNew 'Add new page' %>"> <div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\Admin\\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>">
$AddForm $AddForm
</div> </div>

View File

@ -1 +0,0 @@
<% include SilverStripe\\CMS\\Controllers\\CMSMain_PageList %>

View File

@ -0,0 +1 @@
<% include SilverStripe\\CMS\\Controllers\\CMSMain_RecordList %>

View File

@ -4,7 +4,7 @@
<% if $limited %> <% if $limited %>
<ul><li class="readonly"> <ul><li class="readonly">
<span class="item"> <span class="item">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TOO_MANY_PAGES 'Too many pages' %> <%t SilverStripe\\CMS\\Controllers\\CMSMain.TOO_MANY_RECORDS 'Too many records' %>
(<a href="{$listViewLink.ATT}" class="subtree-list-link" data-id="$node.ID" data-pjax-target="Content"><%t SilverStripe\\CMS\\Controllers\\CMSMain.SHOW_AS_LIST 'show as list' %></a>) (<a href="{$listViewLink.ATT}" class="subtree-list-link" data-id="$node.ID" data-pjax-target="Content"><%t SilverStripe\\CMS\\Controllers\\CMSMain.SHOW_AS_LIST 'show as list' %></a>)
</span> </span>
</li></ul> </li></ul>

View File

@ -2,7 +2,7 @@
<div class="cms-content-header north vertical-align-items"> <div class="cms-content-header north vertical-align-items">
<div class="cms-content-header-info vertical-align-items fill-width"> <div class="cms-content-header-info vertical-align-items fill-width">
<div class="section-heading flexbox-area-grow"> <div class="section-heading flexbox-area-grow">
<span class="section-label"><a href="$LinkPages">{$MenuCurrentItem.Title}</a></span> <span class="section-label"><a href="$LinkRecords">{$MenuCurrentItem.Title}</a></span>
</div> </div>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %> <% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %>
</div> </div>
@ -14,7 +14,7 @@
data-schema="$SearchFieldSchema" data-schema="$SearchFieldSchema"
></div> ></div>
</div> </div>
$PageListSidebar $RecordListSidebar
</div> </div>
<div class="cms-panel-content-collapsed"> <div class="cms-panel-content-collapsed">
<h3 class="cms-panel-header">$SiteConfig.Title</h3> <h3 class="cms-panel-header">$SiteConfig.Title</h3>

View File

@ -1,6 +1,6 @@
<% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions View='Tree' %> <% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions View='Tree' %>
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\CMS\Controllers\CMSMain.AddNew 'Add new page' %>"> <div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\Admin\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>">
$AddForm $AddForm
</div> </div>
@ -17,15 +17,15 @@ $ExtraTreeTools
data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT" data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT"
data-url-savetreenode="$Link('savetreenode').ATT" data-url-savetreenode="$Link('savetreenode').ATT"
data-url-updatetreenodes="$Link('updatetreenodes').ATT" data-url-updatetreenodes="$Link('updatetreenodes').ATT"
data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&PageType=%s').ATT}" data-url-addpage="{$LinkRecordAdd('AddForm/?action_doAdd=1', 'ParentID=%s&RecordType=%s').ATT}"
data-url-editpage="$LinkPageEdit('%s').ATT" data-url-editpage="$LinkRecordEdit('%s').ATT"
data-url-duplicate="{$Link('duplicate/%s').ATT}" data-url-duplicate="{$Link('duplicate/%s').ATT}"
data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}"
data-url-listview="{$Link('?view=list').ATT}" data-url-listview="{$Link('?view=list').ATT}"
data-hints="$SiteTreeHints.ATT" data-hints="$TreeHints.ATT"
data-childfilter="$Link('childfilter').ATT" data-childfilter="$Link('childfilter').ATT"
data-extra-params="SecurityID=$SecurityID.ATT"> data-extra-params="SecurityID=$SecurityID.ATT">
$SiteTreeAsUL $TreeAsUL
</div> </div>
</div> </div>
<% else %> <% else %>
@ -33,14 +33,14 @@ $ExtraTreeTools
data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT" data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT"
data-url-savetreenode="$Link('savetreenode').ATT" data-url-savetreenode="$Link('savetreenode').ATT"
data-url-updatetreenodes="$Link('updatetreenodes').ATT" data-url-updatetreenodes="$Link('updatetreenodes').ATT"
data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&PageType=%s').ATT}" data-url-addpage="{$LinkRecordAdd('AddForm/?action_doAdd=1', 'ParentID=%s&RecordType=%s').ATT}"
data-url-editpage="$LinkPageEdit('%s').ATT" data-url-editpage="$LinkRecordEdit('%s').ATT"
data-url-duplicate="{$Link('duplicate/%s').ATT}" data-url-duplicate="{$Link('duplicate/%s').ATT}"
data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}" data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}"
data-url-listview="{$Link('?view=list').ATT}" data-url-listview="{$Link('?view=list').ATT}"
data-hints="$SiteTreeHints.ATT" data-hints="$TreeHints.ATT"
data-childfilter="$Link('childfilter').ATT" data-childfilter="$Link('childfilter').ATT"
data-extra-params="SecurityID=$SecurityID.ATT"> data-extra-params="SecurityID=$SecurityID.ATT">
$SiteTreeAsUL $TreeAsUL
</div> </div>
<% end_if %> <% end_if %>

View File

@ -1,6 +1,6 @@
<div class="view-controls view-controls--{$ViewState}"> <div class="view-controls view-controls--{$ViewState}">
<% if not $TreeIsFiltered %> <% if not $TreeIsFiltered %>
<%-- Change to data-pjax-target="Content-PageList" to enable in-edit listview --%> <%-- Change to data-pjax-target="Content-RecordList" to enable in-edit listview --%>
<a class="page-view-link btn btn-secondary btn--icon-sm btn--no-text font-icon-tree" <a class="page-view-link btn btn-secondary btn--icon-sm btn--no-text font-icon-tree"
href="$LinkTreeView.ATT" href="$LinkTreeView.ATT"
data-view="treeview" data-view="treeview"

View File

@ -1,45 +0,0 @@
<div class="flexbox-area-grow cms-content $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
<% with $AddForm %>
<form $FormAttributes data-layout-type="border">
<div class="toolbar toolbar--north">
<div class="toolbar__navigation">
<ol class="breadcrumb">
<li class="breadcrumb__item">
<%t SilverStripe\CMS\Controllers\CMSPagesController.MENUTITLE 'Pages'%>
</li>
<li class="breadcrumb__item breadcrumb__item--last breadcrumb__item--no-crumb">
<h2 class="breadcrumb__item-title breadcrumb__item-title--last">
<%t SilverStripe\CMS\Controllers\CMSPageAddController.Title 'Add page' %>
</h2>
</li>
</ol>
</div>
</div>
<div class="panel panel--padded panel--scrollable flexbox-area-grow">
<% if $Message %>
<p id="{$FormName}_error" class="alert $AlertType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="alert $AlertType" style="display: none"></p>
<% end_if %>
<fieldset>
<% if $Legend %><legend>$Legend</legend><% end_if %>
<% loop $Fields %>
$FieldHolder
<% end_loop %>
</fieldset>
</div>
<div class="toolbar--south">
<% if $Actions %>
<div class="btn-toolbar">
<% loop $Actions %>
$Field
<% end_loop %>
</div>
<% end_if %>
</div>
</form>
<% end_with %>
</div>

View File

@ -13,6 +13,6 @@
<div class="flexbox-area-grow fill-height cms-content-fields ui-widget-content cms-panel-padded"> <div class="flexbox-area-grow fill-height cms-content-fields ui-widget-content cms-panel-padded">
$Tools $Tools
$PageList $RecordList
</div> </div>
</div> </div>

View File

@ -1,7 +1,9 @@
<div class="toolbar toolbar--content cms-content-toolbar"> <div class="toolbar toolbar--content cms-content-toolbar">
<div class="btn-toolbar cms-actions-buttons-row"> <div class="btn-toolbar cms-actions-buttons-row">
<% if not $TreeIsFiltered %> <% if not $TreeIsFiltered %>
<a class="btn btn-primary cms-content-addpage-button tool-button font-icon-plus" href="$LinkPageAdd" data-url-addpage="{$LinkPageAdd('', 'ParentID=%s')}"><%t SilverStripe\CMS\Controllers\CMSMain.AddNewButton 'Add new' %></a> <a class="btn btn-primary cms-content-addpage-button tool-button font-icon-plus" href="$LinkRecordAdd" data-url-addpage="{$LinkRecordAdd('', 'ParentID=%s')}">
<%t SilverStripe\Admin\\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>
</a>
<% if $View == 'Tree' %> <% if $View == 'Tree' %>
<button type="button" class="cms-content-batchactions-button btn btn-secondary tool-button font-icon-check-mark-2 btn--last" data-toolid="batch-actions"> <button type="button" class="cms-content-batchactions-button btn btn-secondary tool-button font-icon-check-mark-2 btn--last" data-toolid="batch-actions">
@ -10,7 +12,7 @@
<% end_if %> <% end_if %>
<% end_if %> <% end_if %>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_ViewControls PJAXTarget='Content-PageList' %> <% include SilverStripe\\CMS\\Controllers\\CMSMain_ViewControls PJAXTarget='Content-RecordList' %>
</div> </div>

View File

@ -44,15 +44,15 @@ class CMSMainTest extends FunctionalTest
} }
} }
public function testSiteTreeHints() public function testTreeHints()
{ {
$cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints'); $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_TreeHints');
// Login as user with root creation privileges // Login as user with root creation privileges
$user = $this->objFromFixture(Member::class, 'rootedituser'); $user = $this->objFromFixture(Member::class, 'rootedituser');
Security::setCurrentUser($user); Security::setCurrentUser($user);
$cache->clear(); $cache->clear();
$rawHints = singleton(CMSMain::class)->SiteTreeHints(); $rawHints = singleton(CMSMain::class)->TreeHints();
$this->assertNotNull($rawHints); $this->assertNotNull($rawHints);
$rawHints = preg_replace('/^"(.*)"$/', '$1', Convert::xml2raw($rawHints) ?? ''); $rawHints = preg_replace('/^"(.*)"$/', '$1', Convert::xml2raw($rawHints) ?? '');
@ -611,7 +611,7 @@ class CMSMainTest extends FunctionalTest
$this->assertEquals('Class A', $newPage->Title); $this->assertEquals('Class A', $newPage->Title);
} }
public function testSiteTreeHintsCache() public function testTreeHintsCache()
{ {
$cms = CMSMain::create(); $cms = CMSMain::create();
/** @var Member $user */ /** @var Member $user */
@ -635,31 +635,31 @@ class CMSMainTest extends FunctionalTest
// Initially, cache misses (1) // Initially, cache misses (1)
Injector::inst()->registerService($mockPageMissesCache, $pageClass); Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints(); $hints = $cms->TreeHints();
$this->assertNotNull($hints); $this->assertNotNull($hints);
// Now it hits // Now it hits
Injector::inst()->registerService($mockPageHitsCache, $pageClass); Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints(); $hints = $cms->TreeHints();
$this->assertNotNull($hints); $this->assertNotNull($hints);
// Mutating member record invalidates cache. Misses (2) // Mutating member record invalidates cache. Misses (2)
$user->FirstName = 'changed'; $user->FirstName = 'changed';
$user->write(); $user->write();
Injector::inst()->registerService($mockPageMissesCache, $pageClass); Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints(); $hints = $cms->TreeHints();
$this->assertNotNull($hints); $this->assertNotNull($hints);
// Now it hits again // Now it hits again
Injector::inst()->registerService($mockPageHitsCache, $pageClass); Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints(); $hints = $cms->TreeHints();
$this->assertNotNull($hints); $this->assertNotNull($hints);
// Different user. Misses. (3) // Different user. Misses. (3)
$user = $this->objFromFixture(Member::class, 'allcmssectionsuser'); $user = $this->objFromFixture(Member::class, 'allcmssectionsuser');
Security::setCurrentUser($user); Security::setCurrentUser($user);
Injector::inst()->registerService($mockPageMissesCache, $pageClass); Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints(); $hints = $cms->TreeHints();
$this->assertNotNull($hints); $this->assertNotNull($hints);
} }
@ -703,21 +703,21 @@ class CMSMainTest extends FunctionalTest
); );
} }
public function testCanOrganiseSitetree() public function testCanOrganiseTree()
{ {
$cms = CMSMain::create(); $cms = CMSMain::create();
$this->assertFalse($cms->CanOrganiseSitetree()); $this->assertFalse($cms->CanOrganiseTree());
$this->logInWithPermission('CMS_ACCESS_CMSMain'); $this->logInWithPermission('CMS_ACCESS_CMSMain');
$this->assertFalse($cms->CanOrganiseSitetree()); $this->assertFalse($cms->CanOrganiseTree());
$this->logOut(); $this->logOut();
$this->logInWithPermission('SITETREE_REORGANISE'); $this->logInWithPermission('SITETREE_REORGANISE');
$this->assertTrue($cms->CanOrganiseSitetree()); $this->assertTrue($cms->CanOrganiseTree());
$this->logOut(); $this->logOut();
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$this->assertTrue($cms->CanOrganiseSitetree()); $this->assertTrue($cms->CanOrganiseTree());
} }
} }

View File

@ -24,8 +24,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search(); $f = new CMSSiteTreeFilter_Search();
$results = $f->pagesIncluded(); $results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1)); $this->assertTrue($f->isRecordIncluded($page1));
$this->assertTrue($f->isPageIncluded($page2)); $this->assertTrue($f->isRecordIncluded($page2));
} }
public function testSearchFilterByTitle() public function testSearchFilterByTitle()
@ -36,8 +36,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 1']); $f = new CMSSiteTreeFilter_Search(['Title' => 'Page 1']);
$results = $f->pagesIncluded(); $results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1)); $this->assertTrue($f->isRecordIncluded($page1));
$this->assertFalse($f->isPageIncluded($page2)); $this->assertFalse($f->isRecordIncluded($page2));
$this->assertEquals(1, count($results ?? [])); $this->assertEquals(1, count($results ?? []));
$this->assertEquals( $this->assertEquals(
['ID' => $page1->ID, 'ParentID' => 0], ['ID' => $page1->ID, 'ParentID' => 0],
@ -50,10 +50,10 @@ class CMSSiteTreeFilterTest extends SapphireTest
$page = $this->objFromFixture(SiteTree::class, 'page8'); $page = $this->objFromFixture(SiteTree::class, 'page8');
$filter = CMSSiteTreeFilter_Search::create(['Term' => 'lake-wanaka+adventure']); $filter = CMSSiteTreeFilter_Search::create(['Term' => 'lake-wanaka+adventure']);
$this->assertTrue($filter->isPageIncluded($page)); $this->assertTrue($filter->isRecordIncluded($page));
$filter = CMSSiteTreeFilter_Search::create(['URLSegment' => 'lake-wanaka+adventure']); $filter = CMSSiteTreeFilter_Search::create(['URLSegment' => 'lake-wanaka+adventure']);
$this->assertTrue($filter->isPageIncluded($page)); $this->assertTrue($filter->isRecordIncluded($page));
} }
public function testIncludesParentsForNestedMatches() public function testIncludesParentsForNestedMatches()
@ -64,8 +64,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 3b']); $f = new CMSSiteTreeFilter_Search(['Title' => 'Page 3b']);
$results = $f->pagesIncluded(); $results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($parent)); $this->assertTrue($f->isRecordIncluded($parent));
$this->assertTrue($f->isPageIncluded($child)); $this->assertTrue($f->isRecordIncluded($child));
$this->assertEquals(1, count($results ?? [])); $this->assertEquals(1, count($results ?? []));
$this->assertEquals( $this->assertEquals(
['ID' => $child->ID, 'ParentID' => $parent->ID], ['ID' => $child->ID, 'ParentID' => $parent->ID],
@ -91,8 +91,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'Changed']); $f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'Changed']);
$results = $f->pagesIncluded(); $results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($changedPage)); $this->assertTrue($f->isRecordIncluded($changedPage));
$this->assertFalse($f->isPageIncluded($unchangedPage)); $this->assertFalse($f->isRecordIncluded($unchangedPage));
$this->assertEquals(1, count($results ?? [])); $this->assertEquals(1, count($results ?? []));
$this->assertEquals( $this->assertEquals(
['ID' => $changedPage->ID, 'ParentID' => 0], ['ID' => $changedPage->ID, 'ParentID' => 0],
@ -130,11 +130,11 @@ class CMSSiteTreeFilterTest extends SapphireTest
); );
$f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'Page']); $f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'Page']);
$this->assertTrue($f->isPageIncluded($deletedPage)); $this->assertTrue($f->isRecordIncluded($deletedPage));
// Check that only changed pages are returned // Check that only changed pages are returned
$f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'No Matches']); $f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'No Matches']);
$this->assertFalse($f->isPageIncluded($deletedPage)); $this->assertFalse($f->isRecordIncluded($deletedPage));
} }
public function testStatusDraftPagesFilter() public function testStatusDraftPagesFilter()
@ -148,16 +148,16 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check draft page is shown // Check draft page is shown
$f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'Page']); $f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'Page']);
$this->assertTrue($f->isPageIncluded($draftPage)); $this->assertTrue($f->isRecordIncluded($draftPage));
// Check filter respects parameters // Check filter respects parameters
$f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'No Match']); $f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'No Match']);
$this->assertEmpty($f->isPageIncluded($draftPage)); $this->assertEmpty($f->isRecordIncluded($draftPage));
// Ensures empty array returned if no data to show // Ensures empty array returned if no data to show
$f = new CMSSiteTreeFilter_StatusDraftPages(); $f = new CMSSiteTreeFilter_StatusDraftPages();
$draftPage->delete(); $draftPage->delete();
$this->assertEmpty($f->isPageIncluded($draftPage)); $this->assertEmpty($f->isRecordIncluded($draftPage));
} }
public function testDateFromToLastSameDate() public function testDateFromToLastSameDate()
@ -171,7 +171,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
'LastEditedTo' => $date, 'LastEditedTo' => $date,
]); ]);
$this->assertTrue( $this->assertTrue(
$filter->isPageIncluded($draftPage), $filter->isRecordIncluded($draftPage),
'Using the same date for from and to should show find that page' 'Using the same date for from and to should show find that page'
); );
} }
@ -189,16 +189,16 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check live-only page is included // Check live-only page is included
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedFrom' => '2000-01-01 00:00']); $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedFrom' => '2000-01-01 00:00']);
$this->assertTrue($f->isPageIncluded($removedDraftPage)); $this->assertTrue($f->isRecordIncluded($removedDraftPage));
// Check filter is respected // Check filter is respected
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedTo' => '1999-01-01 00:00']); $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedTo' => '1999-01-01 00:00']);
$this->assertEmpty($f->isPageIncluded($removedDraftPage)); $this->assertEmpty($f->isRecordIncluded($removedDraftPage));
// Ensures empty array returned if no data to show // Ensures empty array returned if no data to show
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(); $f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages();
$removedDraftPage->delete(); $removedDraftPage->delete();
$this->assertEmpty($f->isPageIncluded($removedDraftPage)); $this->assertEmpty($f->isRecordIncluded($removedDraftPage));
} }
public function testStatusDeletedFilter() public function testStatusDeletedFilter()
@ -214,10 +214,10 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check deleted page is included // Check deleted page is included
$f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Page']); $f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Page']);
$this->assertTrue($f->isPageIncluded($checkParentExists)); $this->assertTrue($f->isRecordIncluded($checkParentExists));
// Check filter is respected // Check filter is respected
$f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Bobby']); $f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Bobby']);
$this->assertFalse($f->isPageIncluded($checkParentExists)); $this->assertFalse($f->isRecordIncluded($checkParentExists));
} }
} }