'EditForm',
];
private static $casting = [
'TreeIsFiltered' => 'Boolean',
'AddForm' => 'HTMLFragment',
'LinkPages' => 'Text',
'Link' => 'Text',
'ListViewForm' => 'HTMLFragment',
'ExtraTreeTools' => 'HTMLFragment',
'PageList' => 'HTMLFragment',
'PageListSidebar' => 'HTMLFragment',
'SiteTreeHints' => 'HTMLFragment',
'SecurityID' => 'Text',
'SiteTreeAsUL' => 'HTMLFragment',
];
private static $dependencies = [
'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_SiteTreeHints',
];
/**
* @var CacheInterface
*/
protected $hintsCache;
protected function init()
{
// set reading lang
if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages(SiteTree::class) ?? []));
}
parent::init();
Requirements::javascript('silverstripe/cms: client/dist/js/bundle.js');
Requirements::javascript('silverstripe/cms: client/dist/js/SilverStripeNavigator.js');
Requirements::css('silverstripe/cms: client/dist/styles/bundle.css');
Requirements::customCSS($this->generatePageIconsCss(), self::PAGE_ICONS_ID);
Requirements::add_i18n_javascript('silverstripe/cms: client/lang', false);
CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
}
public function index(HTTPRequest $request): HTTPResponse
{
// 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.
if (!$request->param('Action')) {
$this->setCurrentPageID(null);
}
return parent::index($request);
}
public function getResponseNegotiator(): PjaxResponseNegotiator
{
$negotiator = parent::getResponseNegotiator();
// ListViewForm
$negotiator->setCallback('ListViewForm', function () {
return $this->ListViewForm()->forTemplate();
});
return $negotiator;
}
/**
* Get pages listing area
*
* @return DBHTMLText
*/
public function PageList()
{
return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
}
/**
* Page list view for edit-form
*
* @return DBHTMLText
*/
public function PageListSidebar()
{
return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
}
/**
* If this is set to true, the "switchView" context in the
* template is shown, with links to the staging and publish site.
*
* @return boolean
*/
public function ShowSwitchView()
{
return true;
}
/**
* Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
* to switch view also for archived versions.
*
* @param SiteTree $page
* @return array
*/
public function SwitchView($page = null)
{
if (!$page) {
$page = $this->currentPage();
}
if ($page) {
$nav = SilverStripeNavigator::get_for_record($page);
return $nav['items'];
}
}
//------------------------------------------------------------------------------------------//
// Main controllers
//------------------------------------------------------------------------------------------//
// Main UI components
/**
* Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
*
* @param string|null $action Action to link to.
* @return string
*/
public function Link($action = null)
{
$link = Controller::join_links(
AdminRootController::admin_url(),
$this->config()->get('url_segment'), // in case we want to change the segment
'/', // trailing slash needed if $action is null!
"$action"
);
$this->extend('updateLink', $link);
return $link;
}
public function LinkPages()
{
return CMSPagesController::singleton()->Link();
}
public function LinkPagesWithSearch()
{
return $this->LinkWithSearch($this->LinkPages());
}
/**
* Get link to tree view
*
* @return string
*/
public function LinkTreeView()
{
// Tree view is just default link to main pages section (no /treeview suffix)
return CMSMain::singleton()->Link();
}
/**
* Get link to list view
*
* @return string
*/
public function LinkListView()
{
// Note : Force redirect to top level page controller (no parentid)
return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
}
/**
* Link to list view for children of a parent page
*
* @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for
* client side substitution
* @return string
*/
public function LinkListViewChildren($parentID)
{
return sprintf(
'%s?ParentID=%s',
CMSMain::singleton()->Link(),
$parentID
);
}
/**
* @return string
*/
public function LinkListViewRoot()
{
return $this->LinkListViewChildren(0);
}
/**
* Link to lazy-load deferred tree view
*
* @return string
*/
public function LinkTreeViewDeferred()
{
return $this->Link('treeview');
}
/**
* Link to lazy-load deferred list view
*
* @return string
*/
public function LinkListViewDeferred()
{
return $this->Link('listview');
}
/**
* Get the link for editing a page.
*
* @see CMSEditLinkExtension::getCMSEditLinkForManagedDataObject()
*/
public function getCMSEditLinkForManagedDataObject(SiteTree $obj): string
{
return Controller::join_links(CMSPageEditController::singleton()->Link('show'), $obj->ID);
}
public function LinkPageEdit($id = null)
{
if (!$id) {
$id = $this->currentPageID();
}
return $this->LinkWithSearch(
Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
);
}
public function LinkPageSettings()
{
if ($id = $this->currentPageID()) {
return $this->LinkWithSearch(
Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
);
} else {
return null;
}
}
public function LinkPageHistory()
{
$controller = Injector::inst()->get(CMSPageHistoryViewerController::class);
if (($id = $this->currentPageID()) && $controller) {
if ($controller) {
return $this->LinkWithSearch(
Controller::join_links($controller->Link('show'), $id)
);
}
} else {
return null;
}
}
/**
* Return the active tab identifier for the CMS. Used by templates to decide which tab to give the active state.
* The default value is "edit", as the primary content tab. Child controllers will override this.
*
* @return string
*/
public function getTabIdentifier()
{
return 'edit';
}
/**
* @param CacheInterface $cache
* @return $this
*/
public function setHintsCache(CacheInterface $cache)
{
$this->hintsCache = $cache;
return $this;
}
/**
* @return CacheInterface $cache
*/
public function getHintsCache()
{
return $this->hintsCache;
}
/**
* Clears all dependent cache backends
*/
public function clearCache()
{
$this->getHintsCache()->clear();
}
public function LinkWithSearch($link)
{
// Whitelist to avoid side effects
$params = [
'q' => (array)$this->getRequest()->getVar('q'),
'ParentID' => $this->getRequest()->getVar('ParentID')
];
$link = Controller::join_links(
$link,
array_filter(array_values($params ?? [])) ? '?' . http_build_query($params) : null
);
$this->extend('updateLinkWithSearch', $link);
return $link;
}
public function LinkPageAdd($extra = null, $placeholders = null)
{
$link = CMSPageAddController::singleton()->Link();
$this->extend('updateLinkPageAdd', $link);
if ($extra) {
$link = Controller::join_links($link, $extra);
}
if ($placeholders) {
$link .= (strpos($link ?? '', '?') === false ? "?$placeholders" : "&$placeholders");
}
return $link;
}
/**
* @return string
*/
public function LinkPreview()
{
$record = $this->getRecord($this->currentPageID());
$baseLink = Director::absoluteBaseURL();
if ($record && $record instanceof SiteTree) {
// if we are an external redirector don't show a link
if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
$baseLink = false;
} else {
$baseLink = $record->Link('?stage=Stage');
}
}
return $baseLink;
}
/**
* Return the entire site tree as a nested set of ULs
*/
public function SiteTreeAsUL()
{
$treeClass = $this->config()->get('tree_class');
$filter = $this->getSearchFilter();
DataObject::singleton($treeClass)->prepopulateTreeDataCache(null, [
'childrenMethod' => $filter ? $filter->getChildrenMethod() : 'AllChildrenIncludingDeleted',
'numChildrenMethod' => $filter ? $filter->getNumChildrenMethod() : 'numChildren',
]);
$html = $this->getSiteTreeFor($treeClass);
$this->extend('updateSiteTreeAsUL', $html);
return $html;
}
/**
* Get a site tree HTML listing which displays the nodes under the given criteria.
*
* @param string $className The class of the root object
* @param string $rootID The ID of the root object. If this is null then a complete tree will be
* shown
* @param string $childrenMethod The method to call to get the children of the tree. For example,
* Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
* @param string $numChildrenMethod
* @param callable $filterFunction
* @param int $nodeCountThreshold
* @return string Nested unordered list with links to each page
*/
public function getSiteTreeFor(
$className,
$rootID = null,
$childrenMethod = null,
$numChildrenMethod = null,
$filterFunction = null,
$nodeCountThreshold = null
) {
$nodeCountThreshold = is_null($nodeCountThreshold) ? Config::inst()->get($className, 'node_threshold_total') : $nodeCountThreshold;
// Provide better defaults from filter
$filter = $this->getSearchFilter();
if ($filter) {
if (!$childrenMethod) {
$childrenMethod = $filter->getChildrenMethod();
}
if (!$numChildrenMethod) {
$numChildrenMethod = $filter->getNumChildrenMethod();
}
if (!$filterFunction) {
$filterFunction = function ($node) use ($filter) {
return $filter->isPageIncluded($node);
};
}
}
// Build set from node and begin marking
$record = ($rootID) ? $this->getRecord($rootID) : null;
$rootNode = $record ? $record : DataObject::singleton($className);
$markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
// Set filter function
if ($filterFunction) {
$markingSet->setMarkingFilterFunction($filterFunction);
}
// Mark tree from this node
$markingSet->markPartialTree();
// Ensure current page is exposed
$currentPage = $this->currentPage();
if ($currentPage) {
$markingSet->markToExpose($currentPage);
}
// Pre-cache permissions
$checker = SiteTree::getPermissionChecker();
if ($checker instanceof InheritedPermissions) {
$checker->prePopulatePermissionCache(
InheritedPermissions::EDIT,
$markingSet->markedNodeIDs()
);
}
// Render using full-subtree template
return $markingSet->renderChildren(
[ self::class . '_SubTree', 'type' => 'Includes' ],
$this->getTreeNodeCustomisations()
);
}
/**
* Get callback to determine template customisations for nodes
*
* @return callable
*/
protected function getTreeNodeCustomisations()
{
$rootTitle = $this->getCMSTreeTitle();
return function (SiteTree $node) use ($rootTitle) {
return [
'listViewLink' => $this->LinkListViewChildren($node->ID),
'rootTitle' => $rootTitle,
'extraClass' => $this->getTreeNodeClasses($node),
'Title' => _t(
self::class . '.PAGETYPE_TITLE',
'(Page type: {type}) {title}',
[
'type' => $node->i18n_singular_name(),
'title' => $node->Title,
]
)
];
};
}
/**
* Get extra CSS classes for a page's tree node
*
* @param SiteTree $node
* @return string
*/
public function getTreeNodeClasses(SiteTree $node)
{
// Get classes from object
$classes = $node->CMSTreeClasses();
// Get status flag classes
$flags = $node->getStatusFlags();
if ($flags) {
$statuses = array_keys($flags ?? []);
foreach ($statuses as $s) {
$classes .= ' status-' . $s;
}
}
// Get additional filter classes
$filter = $this->getSearchFilter();
if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
if (is_array($filterClasses)) {
$filterClasses = implode(' ', $filterClasses);
}
$classes .= ' ' . $filterClasses;
}
return trim($classes ?? '');
}
/**
* Get a subtree underneath the request param 'ID'.
* If ID = 0, then get the whole tree.
*/
public function getsubtree(HTTPRequest $request): HTTPResponse
{
$html = $this->getSiteTreeFor(
$this->config()->get('tree_class'),
$request->getVar('ID'),
null,
null,
null,
$request->getVar('minNodeCount')
);
// Trim off the outer tag
$html = preg_replace('/^[\s\t\r\n]*
]*>/', '', $html ?? '');
$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html ?? '');
return $this->getResponse()->setBody($html);
}
/**
* Allows requesting a view update on specific tree nodes.
* Similar to {@link getsubtree()}, but doesn't enforce loading
* all children with the node. Useful to refresh views after
* state modifications, e.g. saving a form.
*/
public function updatetreenodes(HTTPRequest $request): HTTPResponse
{
$data = [];
$ids = explode(',', $request->getVar('ids') ?? '');
foreach ($ids as $id) {
if ($id === "") {
continue; // $id may be a blank string, which is invalid and should be skipped over
}
$record = $this->getRecord($id);
if (!$record) {
continue; // In case a page is no longer available
}
// Create marking set with sole marked root
$markingSet = MarkedSet::create($record);
$markingSet->setMarkingFilterFunction(function () {
return false;
});
$markingSet->markUnexpanded($record);
// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
$prev = null;
$className = $this->config()->get('tree_class');
$next = DataObject::get($className)
->filter('ParentID', $record->ParentID)
->filter('Sort:GreaterThan', $record->Sort)
->first();
if (!$next) {
$prev = DataObject::get($className)
->filter('ParentID', $record->ParentID)
->filter('Sort:LessThan', $record->Sort)
->reverse()
->first();
}
// Render using single node template
$html = $markingSet->renderChildren(
[ self::class . '_TreeNode', 'type' => 'Includes'],
$this->getTreeNodeCustomisations()
);
$data[$id] = [
'html' => $html,
'ParentID' => $record->ParentID,
'NextID' => $next ? $next->ID : null,
'PrevID' => $prev ? $prev->ID : null
];
}
return $this
->getResponse()
->addHeader('Content-Type', 'application/json')
->setBody(json_encode($data));
}
/**
* Update the position and parent of a tree node.
* Only saves the node if changes were made.
*
* Required data:
* - 'ID': The moved node
* - 'ParentID': New parent relation of the moved node (0 for root)
* - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
* In case of a 'ParentID' change, relates to the new siblings under the new parent.
*
* @throws HTTPResponse_Exception
*/
public function savetreenode(HTTPRequest $request): HTTPResponse
{
if (!SecurityToken::inst()->checkRequest($request)) {
$this->httpError(400);
}
if (!$this->CanOrganiseSitetree()) {
$this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
"You do not have permission to rearange the site tree. Your change was not saved."
)
);
}
$className = $this->config()->get('tree_class');
$id = $request->requestVar('ID');
$parentID = $request->requestVar('ParentID');
if (!is_numeric($id) || !is_numeric($parentID)) {
$this->httpError(400);
}
// Check record exists in the DB
/** @var SiteTree $node */
$node = DataObject::get_by_id($className, $id);
if (!$node) {
$this->httpError(
500,
_t(
__CLASS__.'.PLEASESAVE',
"Please Save Page: This page could not be updated because it hasn't been saved yet."
)
);
}
// Check top level permissions
$root = $node->getParentType();
if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
$this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
"You do not have permission to alter Top level pages. Your change was not saved."
)
);
}
$siblingIDs = $request->requestVar('SiblingIDs');
$statusUpdates = ['modified'=>[]];
if (!$node->canEdit()) {
return Security::permissionFailure($this);
}
// Update hierarchy (only if ParentID changed)
if ($node->ParentID != $parentID) {
$node->ParentID = (int)$parentID;
$node->write();
$statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle
];
// Update all dependent pages
$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
foreach ($virtualPages as $virtualPage) {
$statusUpdates['modified'][$virtualPage->ID] = [
'TreeTitle' => $virtualPage->TreeTitle
];
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '')
);
}
// Update sorting
if (is_array($siblingIDs)) {
$counter = 0;
foreach ($siblingIDs as $id) {
if ($id == $node->ID) {
$node->Sort = ++$counter;
$node->write();
$statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle
];
} elseif (is_numeric($id)) {
// Nodes that weren't "actually moved" shouldn't be registered as
// having been edited; do a direct SQL update instead
++$counter;
$table = DataObject::getSchema()->baseDataTable($className);
DB::prepared_query(
"UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
[$counter, $id]
);
}
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '')
);
}
return $this
->getResponse()
->addHeader('Content-Type', 'application/json')
->setBody(json_encode($statusUpdates));
}
/**
* Whether the current member has the permission to reorganise SiteTree objects.
* @return bool
*/
public function CanOrganiseSitetree()
{
return Permission::check('SITETREE_REORGANISE');
}
/**
* @return boolean
*/
public function TreeIsFiltered()
{
$query = $this->getRequest()->getVar('q');
return !empty($query);
}
public function ExtraTreeTools()
{
$html = '';
$this->extend('updateExtraTreeTools', $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
*
* @return string
*/
public function getSearchFieldSchema()
{
$schemaUrl = $this->Link('schema/SearchForm');
$context = $this->getSearchContext();
$params = $this->getRequest()->requestVar('q') ?: [];
$context->setSearchParams($params);
$placeholder = _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search') . ' "' .
SiteTree::singleton()->i18n_plural_name() . '"';
$searchParams = $context->getSearchParams();
$searchParams = array_combine(array_map(function ($key) {
return 'Search__' . $key;
}, array_keys($searchParams ?? [])), $searchParams ?? []);
$schema = [
'formSchemaUrl' => $schemaUrl,
'name' => 'Term',
'placeholder' => $placeholder,
'filters' => $searchParams ?: new \stdClass // stdClass maps to empty json object '{}'
];
return json_encode($schema);
}
/**
* Returns a Form for page searching for use in templates.
*
* Can be modified from a decorator by a 'updateSearchForm' method
*
* @return Form
*/
public function getSearchForm()
{
// Create the fields
$dateFrom = DateField::create(
'Search__LastEditedFrom',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
)->setLocale(Security::getCurrentUser()->Locale);
$dateTo = DateField::create(
'Search__LastEditedTo',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
)->setLocale(Security::getCurrentUser()->Locale);
$filters = CMSSiteTreeFilter::get_all_filters();
// Remove 'All pages' as we set that to empty/default value
unset($filters[CMSSiteTreeFilter_Search::class]);
$pageFilter = DropdownField::create(
'Search__FilterClass',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
$filters
);
$pageFilter->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGESALLOPT', 'All pages'));
$pageClasses = DropdownField::create(
'Search__ClassName',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
$this->getPageTypes()
);
$pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
// Group the Datefields
$dateGroup = FieldGroup::create(
_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'),
[$dateFrom, $dateTo]
)->setName('Search__LastEdited')
->addExtraClass('fieldgroup--fill-width');
// Create the Field list
$fields = new FieldList(
$pageFilter,
$pageClasses,
$dateGroup
);
// Create the form
/** @skipUpgrade */
$form = Form::create(
$this,
'SearchForm',
$fields,
new FieldList()
);
$form->addExtraClass('cms-search-form');
$form->setFormMethod('GET');
$form->setFormAction(CMSMain::singleton()->Link());
$form->disableSecurityToken();
$form->unsetValidator();
// Load the form with previously sent search data
$form->loadDataFrom($this->getRequest()->getVars());
// Allow decorators to modify the form
$this->extend('updateSearchForm', $form);
return $form;
}
/**
* Returns a sorted array suitable for a dropdown with pagetypes and their translated name
*
* @return array
*/
protected function getPageTypes()
{
$pageTypes = [];
foreach (SiteTree::page_type_classes() as $pageTypeClass) {
$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
}
asort($pageTypes);
return $pageTypes;
}
public function doSearch(array $data, Form $form): HTTPResponse
{
return $this->getsubtree($this->getRequest());
}
/**
* Get "back" url for breadcrumbs
*
* @return string
*/
public function getBreadcrumbsBackLink()
{
$breadcrumbs = $this->Breadcrumbs();
if ($breadcrumbs->count() < 2) {
return $this->LinkPages();
}
// Get second from end breadcrumb
return $breadcrumbs
->offsetGet($breadcrumbs->count() - 2)
->Link;
}
/**
* @param bool $unlinked
* @return ArrayList
*/
public function Breadcrumbs($unlinked = false)
{
$items = new ArrayList();
if ($this->TreeIsFiltered()) {
$items->push(new ArrayData([
'Title' => CMSPagesController::menu_title(),
'Link' => ($unlinked) ? false : $this->LinkPages()
]));
$items->push(new ArrayData([
'Title' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.SEARCHRESULTS', 'Search results'),
'Link' => ($unlinked) ? false : $this->LinkPages()
]));
$this->extend('updateBreadcrumbs', $items);
return $items;
}
// Check if we are editing a page
/** @var SiteTree $record */
$record = $this->currentPage();
if (!$record) {
$items->push(new ArrayData([
'Title' => CMSPagesController::menu_title(),
'Link' => ($unlinked) ? false : $this->LinkPages()
]));
$this->extend('updateBreadcrumbs', $items);
return $items;
}
// Add all ancestors
$ancestors = $record->getAncestors();
$ancestors = new ArrayList(array_reverse($ancestors->toArray() ?? []));
$ancestors->push($record);
/** @var SiteTree $ancestor */
foreach ($ancestors as $ancestor) {
$items->push(new ArrayData([
'Title' => $ancestor->getMenuTitle(),
'Link' => ($unlinked)
? false
: $ancestor->CMSEditLink()
]));
}
$this->extend('updateBreadcrumbs', $items);
return $items;
}
/**
* Create serialized JSON string with site tree hints data to be injected into
* 'data-hints' attribute of root node of jsTree.
*
* @return string Serialized JSON
*/
public function SiteTreeHints()
{
$classes = SiteTree::page_type_classes();
$memberID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
$cache = $this->getHintsCache();
$cacheKey = $this->generateHintsCacheKey($memberID);
$json = $cache->get($cacheKey);
if ($json) {
return $json;
}
$canCreate = [];
foreach ($classes as $class) {
$canCreate[$class] = singleton($class)->canCreate();
}
$def['Root'] = [];
$def['Root']['disallowedChildren'] = [];
// Contains all possible classes to support UI controls listing them all,
// such as the "add page here" context menu.
$def['All'] = [];
// Identify disallows and set globals
foreach ($classes as $class) {
$obj = singleton($class);
if ($obj instanceof HiddenClass) {
continue;
}
// Name item
$def['All'][$class] = [
'title' => $obj->i18n_singular_name()
];
// Check if can be created at the root
$needsPerm = $obj->config()->get('need_permission');
if (!$obj->config()->get('can_be_root')
|| (!array_key_exists($class, $canCreate ?? []) || !$canCreate[$class])
|| ($needsPerm && !$this->can($needsPerm))
) {
$def['Root']['disallowedChildren'][] = $class;
}
// Hint data specific to the class
$def[$class] = [];
$defaultChild = $obj->defaultChild();
if ($defaultChild !== 'Page' && $defaultChild !== null) {
$def[$class]['defaultChild'] = $defaultChild;
}
$defaultParent = $obj->defaultParent();
if ($defaultParent !== 1 && $defaultParent !== null) {
$def[$class]['defaultParent'] = $defaultParent;
}
}
$this->extend('updateSiteTreeHints', $def);
$json = json_encode($def);
$cache->set($cacheKey, $json);
return $json;
}
/**
* Populates an array of classes in the CMS
* which allows the user to change the page type.
*
* @return SS_List
*/
public function PageTypes()
{
$classes = SiteTree::page_type_classes();
$result = new ArrayList();
foreach ($classes as $class) {
$instance = SiteTree::singleton($class);
if ($instance instanceof HiddenClass) {
continue;
}
// skip this type if it is restricted
$needPermissions = $instance->config()->get('need_permission');
if ($needPermissions && !$this->can($needPermissions)) {
continue;
}
$result->push(new ArrayData([
'ClassName' => $class,
'AddAction' => $instance->i18n_singular_name(),
'Description' => $instance->i18n_classDescription(),
'IconURL' => $instance->getPageIconURL(),
'Title' => $instance->i18n_singular_name(),
]));
}
$result = $result->sort('AddAction');
return $result;
}
/**
* Get a database record to be managed by the CMS.
*
* @param int $id Record ID
* @param int $versionID optional Version id of the given record
* @return SiteTree
*/
public function getRecord($id, $versionID = null)
{
if (!$id) {
return null;
}
$treeClass = $this->config()->get('tree_class');
if ($id instanceof $treeClass) {
return $id;
}
if (substr($id ?? '', 0, 3) == 'new') {
return $this->getNewItem($id);
}
if (!is_numeric($id)) {
return null;
}
$currentStage = Versioned::get_reading_mode();
if ($this->getRequest()->getVar('Version')) {
$versionID = (int) $this->getRequest()->getVar('Version');
}
/** @var SiteTree $record */
if ($versionID) {
$record = Versioned::get_version($treeClass, $id, $versionID);
} else {
$record = DataObject::get_by_id($treeClass, $id);
}
// Then, try getting a record from the live site
if (!$record) {
// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
Versioned::set_stage(Versioned::LIVE);
singleton($treeClass)->flushCache();
$record = DataObject::get_by_id($treeClass, $id);
}
// Then, try getting a deleted record
if (!$record) {
$record = Versioned::get_latest_version($treeClass, $id);
}
// Set the reading mode back to what it was.
Versioned::set_reading_mode($currentStage);
return $record;
}
/**
* {@inheritdoc}
*
* @param HTTPRequest $request
* @return Form
*/
public function EditForm($request = null)
{
// set page ID from request
if ($request) {
// Validate id is present
$id = $request->param('ID');
if (!isset($id)) {
$this->httpError(400);
return null;
}
$this->setCurrentPageID($id);
}
return $this->getEditForm();
}
/**
* @param int $id
* @param FieldList $fields
* @return Form
*/
public function getEditForm($id = null, $fields = null)
{
// Get record
if (!$id) {
$id = $this->currentPageID();
}
/** @var SiteTree $record */
$record = $this->getRecord($id);
// Check parent form can be generated
$form = parent::getEditForm($record, $fields);
if (!$form || !$record) {
return $form;
}
if (!$fields) {
$fields = $form->Fields();
}
// Add extra fields
$deletedFromStage = !$record->isOnDraft();
$fields->push($idField = new HiddenField("ID", false, $id));
// Necessary for different subsites
$fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
$fields->push($liveLinkField = new HiddenField("LiveLink"));
$fields->push($stageLinkField = new HiddenField("StageLink"));
$fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
$fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
$archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
// Build preview / live links
$liveLink = $record->getAbsoluteLiveLink();
if ($liveLink) {
$liveLinkField->setValue($liveLink);
}
if (!$deletedFromStage) {
$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
if ($stageLink) {
$stageLinkField->setValue($stageLink);
}
}
// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
/** @skipUpgrade */
if (($record instanceof CMSPreviewable || $record->has_extension(CMSPreviewable::class))
&& !$fields->fieldByName('SilverStripeNavigator')
) {
$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
$navField->setAllowHTML(true);
$fields->push($navField);
}
// getAllCMSActions can be used to completely redefine the action list
if ($record->hasMethod('getAllCMSActions')) {
$actions = $record->getAllCMSActions();
} else {
$actions = $record->getCMSActions();
// Find and remove action menus that have no actions.
if ($actions && $actions->count()) {
/** @var TabSet $tabset */
$tabset = $actions->fieldByName('ActionMenus');
if ($tabset) {
/** @var Tab $tab */
foreach ($tabset->getChildren() as $tab) {
if (!$tab->getChildren()->count()) {
$tabset->removeByName($tab->getName());
}
}
}
}
}
// Use