'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, true); 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]*