'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() { 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::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) $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 $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; } 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(); } $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 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