'EditForm', ]; private static $casting = array( 'TreeIsFiltered' => 'Boolean', 'AddForm' => 'HTMLFragment', 'LinkPages' => 'Text', 'Link' => 'Text', 'ListViewForm' => 'HTMLFragment', 'ExtraTreeTools' => 'HTMLFragment', 'PageList' => 'HTMLFragment', 'PageListSidebar' => 'HTMLFragment', 'SiteTreeHints' => 'HTMLFragment', 'SecurityID' => 'Text', 'SiteTreeAsUL' => 'HTMLFragment', ); 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('SilverStripe\\CMS\\Model\\SiteTree'))); } parent::init(); Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js'); Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js'); Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css'); Requirements::customCSS($this->generatePageIconsCss()); Requirements::add_i18n_javascript(CMS_DIR . '/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($request) { // 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() { $negotiator = parent::getResponseNegotiator(); // ListViewForm $negotiator->setCallback('ListViewForm', function () { return $this->ListViewForm()->forTemplate(); }); // PageList view $negotiator->setCallback('Content-PageList', function () { return $this->PageList()->forTemplate(); }); // PageList view for edit controller $negotiator->setCallback('Content-PageList-Sidebar', function () { return $this->PageListSidebar()->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->stat('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 $this->LinkWithSearch(CMSMain::singleton()->Link()); } /** * Get link to list view * * @return string */ public function LinkListView() { // Note : Force redirect to top level page controller return $this->LinkWithSearch(CMSMain::singleton()->Link('listview')); } 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() { if ($id = $this->currentPageID()) { return $this->LinkWithSearch( Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id) ); } else { return null; } } public function LinkWithSearch($link) { // Whitelist to avoid side effects $params = array( '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() { // Pre-cache sitetree version numbers for querying efficiency Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT); Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE); $html = $this->getSiteTreeFor($this->stat('tree_class')); $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 = 30 ) { // Filter criteria $filter = $this->getSearchFilter(); // Default childrenMethod and numChildrenMethod if (!$childrenMethod) { $childrenMethod = ($filter && $filter->getChildrenMethod()) ? $filter->getChildrenMethod() : 'AllChildrenIncludingDeleted'; } if (!$numChildrenMethod) { $numChildrenMethod = 'numChildren'; if ($filter && $filter->getNumChildrenMethod()) { $numChildrenMethod = $filter->getNumChildrenMethod(); } } if (!$filterFunction && $filter) { $filterFunction = function ($node) use ($filter) { return $filter->isPageIncluded($node); }; } // Get the tree root $record = ($rootID) ? $this->getRecord($rootID) : null; $obj = $record ? $record : singleton($className); // Get the current page // NOTE: This *must* be fetched before markPartialTree() is called, as this // causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord) // which means that deleted pages stored in the marked tree would be removed $currentPage = $this->currentPage(); // Mark the nodes of the tree to return if ($filterFunction) { $obj->setMarkingFilterFunction($filterFunction); } $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod); // Ensure current page is exposed if ($currentPage) { $obj->markToExpose($currentPage); } SiteTree::prepopulate_permission_cache( 'CanEditType', $obj->markedNodeIDs(), [ SiteTree::class, 'can_edit_multiple'] ); // getChildrenAsUL is a flexible and complex way of traversing the tree $controller = $this; $recordController = CMSPageEditController::singleton(); $titleFn = function (&$child, $numChildrenMethod) use (&$controller, &$recordController, $filter) { $link = Controller::join_links($recordController->Link("show"), $child->ID); $node = CMSTreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter); return $node->forTemplate(); }; // Limit the amount of nodes shown for performance reasons. // Skip the check if we're filtering the tree, since its not clear how many children will // match the filter criteria until they're queried (and matched up with previously marked nodes). $nodeThresholdLeaf = SiteTree::config()->get('node_threshold_leaf'); if ($nodeThresholdLeaf && !$filterFunction) { $nodeCountCallback = function ($parent, $numChildren) use (&$controller, $nodeThresholdLeaf) { if (!$parent->ID || $numChildren <= $nodeThresholdLeaf) { return null; } return sprintf( '', _t('LeftAndMain.TooManyPages', 'Too many pages'), Controller::join_links( $controller->LinkWithSearch($controller->Link()), '?view=listview&ParentID=' . $parent->ID ), _t( 'LeftAndMain.ShowAsList', 'show as list', 'Show large amount of pages in list instead of tree view' ) ); }; } else { $nodeCountCallback = null; } // If the amount of pages exceeds the node thresholds set, use the callback $html = null; if ($obj->ParentID && $nodeCountCallback) { $html = $nodeCountCallback($obj, $obj->$numChildrenMethod()); } // Otherwise return the actual tree (which might still filter leaf thresholds on children) if (!$html) { $html = $obj->getChildrenAsUL( "", $titleFn, CMSPagesController::singleton(), true, $childrenMethod, $numChildrenMethod, $nodeCountThreshold, $nodeCountCallback ); } // Wrap the root if needs be. if (!$rootID) { // This lets us override the tree title with an extension if ($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) { $treeTitle = $customTreeTitle; } elseif (class_exists(SiteConfig::class)) { $siteConfig = SiteConfig::current_site_config(); $treeTitle = Convert::raw2xml($siteConfig->Title); } else { $treeTitle = '...'; } $html = ""; } return $html; } /** * Get a subtree underneath the request param 'ID'. * If ID = 0, then get the whole tree. * * @param HTTPRequest $request * @return string */ public function getsubtree($request) { $html = $this->getSiteTreeFor( $this->stat('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 $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. * * @param HTTPRequest $request * @return HTTPResponse */ public function updatetreenodes($request) { $data = array(); $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 } $recordController = CMSPageEditController::singleton(); // 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->stat('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(); } $link = Controller::join_links($recordController->Link("show"), $record->ID); $html = CMSTreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate(). ''; $data[$id] = array( 'html' => $html, 'ParentID' => $record->ParentID, 'NextID' => $next ? $next->ID : null, 'PrevID' => $prev ? $prev->ID : null ); } return $this ->getResponse() ->addHeader('Content-Type', 'application/json') ->setBody(Convert::raw2json($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. * * @param HTTPRequest $request * @return HTTPResponse JSON string with a * @throws HTTPResponse_Exception */ public function savetreenode($request) { if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) { return $this->httpError( 403, _t( 'LeftAndMain.CANT_REORGANISE', "You do not have permission to rearange the site tree. Your change was not saved." ) ); } $className = $this->stat('tree_class'); $id = $request->requestVar('ID'); $parentID = $request->requestVar('ParentID'); if (!is_numeric($id) || !is_numeric($parentID)) { return $this->httpError(400); } // Check record exists in the DB /** @var SiteTree $node */ $node = DataObject::get_by_id($className, $id); if (!$node) { return $this->httpError( 500, _t( 'LeftAndMain.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()) { return $this->httpError( 403, _t( 'LeftAndMain.CANT_REORGANISE', "You do not have permission to alter Top level pages. Your change was not saved." ) ); } $siblingIDs = $request->requestVar('SiblingIDs'); $statusUpdates = array('modified'=>array()); 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] = array( 'TreeTitle' => $node->TreeTitle ); // Update all dependent pages $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID); foreach ($virtualPages as $virtualPage) { $statusUpdates['modified'][$virtualPage->ID] = array( 'TreeTitle' => $virtualPage->TreeTitle() ); } $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t('LeftAndMain.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] = array( '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\" = ?", array($counter, $id) ); } } $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')) ); } return $this ->getResponse() ->addHeader('Content-Type', 'application/json') ->setBody(Convert::raw2json($statusUpdates)); } public function CanOrganiseSitetree() { return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true; } /** * @return boolean */ public function TreeIsFiltered() { $query = $this->getRequest()->getVar('q'); if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) { return false; } return true; } public function ExtraTreeTools() { $html = ''; $this->extend('updateExtraTreeTools', $html); return $html; } /** * Returns a Form for page searching for use in templates. * * Can be modified from a decorator by a 'updateSearchForm' method * * @return Form */ public function SearchForm() { // Create the fields $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search')); $dateFrom = new DateField( 'q[LastEditedFrom]', _t('CMSSearch.FILTERDATEFROM', 'From') ); $dateTo = new DateField( 'q[LastEditedTo]', _t('CMSSearch.FILTERDATETO', 'To') ); $pageFilter = new DropdownField( 'q[FilterClass]', _t('CMSMain.PAGES', 'Page status'), CMSSiteTreeFilter::get_all_filters() ); $pageClasses = new DropdownField( 'q[ClassName]', _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'), $this->getPageTypes() ); $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any')); // Group the Datefields $dateGroup = new FieldGroup( $dateFrom, $dateTo ); $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited')); // view mode $viewMode = HiddenField::create('view', false, $this->ViewState()); // Create the Field list $fields = new FieldList( $content, $pageFilter, $pageClasses, $dateGroup, $viewMode ); // Create the Search and Reset action $actions = new FieldList( FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search')) ->addExtraClass('btn btn-primary'), ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear')) ->addExtraClass('btn btn-secondary') ); // Use