Source of file CMSMain.php
Size: 79,938 Bytes - Last Modified: 2021-12-23T10:28:18+00:00
/var/www/docs.ssmods.com/process/src/code/Controllers/CMSMain.php
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404 | <?php namespace SilverStripe\CMS\Controllers; use InvalidArgumentException; use Psr\SimpleCache\CacheInterface; use SilverStripe\Admin\AdminRootController; use SilverStripe\Admin\CMSBatchActionHandler; use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMainFormRequestHandler; use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive; use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish; use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore; use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish; use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search; use SilverStripe\CMS\Model\CurrentPageIdentifier; use SilverStripe\CMS\Model\RedirectorPage; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\VirtualPage; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Core\Cache\MemberCacheFlusher; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\Core\Environment; use SilverStripe\Core\Flushable; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\DateField; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldGroup; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormField; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig; use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Forms\GridField\GridFieldLevelup; use SilverStripe\Forms\GridField\GridFieldPaginator; use SilverStripe\Forms\GridField\GridFieldSortableHeader; use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\LabelField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\Tab; use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TextField; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\HiddenClass; use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\MarkedSet; use SilverStripe\ORM\SS_List; use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\InheritedPermissions; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\PermissionProvider; use SilverStripe\Security\Security; use SilverStripe\Security\SecurityToken; use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\Versioned\ChangeSet; use SilverStripe\Versioned\ChangeSetItem; use SilverStripe\Versioned\Versioned; use SilverStripe\View\ArrayData; use SilverStripe\View\Requirements; use Translatable; /** * The main "content" area of the CMS. * * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main * admin menu. * * @todo Create some base classes to contain the generic functionality that will be replicated. * * @mixin LeftAndMainPageIconsExtension */ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider, Flushable, MemberCacheFlusher { /** * Unique ID for page icons CSS block */ const PAGE_ICONS_ID = 'PageIcons'; private static $url_segment = 'pages'; private static $url_rule = '/$Action/$ID/$OtherID'; // Maintain a lower priority than other administration sections // so that Director does not think they are actions of CMSMain private static $url_priority = 39; private static $menu_title = 'Edit Page'; private static $menu_icon_class = 'font-icon-sitemap'; private static $menu_priority = 10; private static $tree_class = SiteTree::class; private static $subitem_class = Member::class; private static $session_namespace = self::class; private static $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. * * @config * @var bool */ private static $enable_dynamic_archive_warning_message = true; /** * Amount of results showing on a single page. * * @config * @var int */ private static $page_length = 15; private static $allowed_actions = [ 'archive', 'deleteitems', 'DeleteItemsForm', 'dialog', 'duplicate', 'duplicatewithchildren', 'publishall', 'publishitems', 'PublishItemsForm', 'submit', 'EditForm', 'schema', 'SearchForm', 'SiteTreeAsUL', 'getshowdeletedsubtree', 'savetreenode', 'getsubtree', 'updatetreenodes', 'batchactions', 'treeview', 'listview', 'ListViewForm', 'childfilter', ]; private static $url_handlers = [ 'EditForm/$ID' => '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($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(); }); 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'); } 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; } } /** * 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. * * @param HTTPRequest $request * @return string */ public function getsubtree($request) { $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]*<ul[^>]*>/', '', $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 = []; $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. * * @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 (!$this->CanOrganiseSitetree()) { return $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)) { 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( __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()) { return $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($data, $form) { 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 && !$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 <button> to allow full jQuery UI styling $actionsFlattened = $actions->dataFields(); if ($actionsFlattened) { /** @var FormAction $action */ foreach ($actionsFlattened as $action) { $action->setUseButtonTag(true); } } // TODO Can't merge $FormAttributes in template at the moment $form->addExtraClass('center ' . $this->BaseCSSClasses()); // Set validation exemptions for specific actions $form->setValidationExemptActions([ 'restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback', 'archive', ]); // Announce the capability so the frontend can decide whether to allow preview or not. if ($record instanceof CMSPreviewable) { $form->addExtraClass('cms-previewable'); } $form->addExtraClass('fill-height flexbox-area-grow'); if (!$record->canEdit() || $deletedFromStage) { $readonlyFields = $form->Fields()->makeReadonly(); $form->setFields($readonlyFields); } $form->Fields()->setForm($form); $this->extend('updateEditForm', $form); // Use custom reqest handler for LeftAndMain requests; // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID) $form->setRequestHandler( LeftAndMainFormRequestHandler::create($form, [$id]) ); return $form; } public function EmptyForm() { $fields = new FieldList( new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist")) ); $form = parent::EmptyForm(); $form->setFields($fields); $fields->setForm($form); return $form; } /** * Build an archive warning message based on the page's children * * @param SiteTree $record * @return string */ /** * Build an archive warning message based on the page's children * * @param SiteTree $record * @return string */ protected function getArchiveWarningMessage($record) { $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?'); // Option to disable this feature as it is slow on large sites if (!$this->config()->enable_dynamic_archive_warning_message) { return $defaultMessage; } // Get all page's descendants $descendants = []; $this->collateDescendants([$record->ID], $descendants); if (!$descendants) { $descendants = []; } // Get the IDs of all changeset including at least one of the pages. $descendants[] = $record->ID; $inChangeSetIDs = ChangeSetItem::get()->filter([ 'ObjectID' => $descendants, 'ObjectClass' => SiteTree::class ])->column('ChangeSetID'); // Count number of affected change set $affectedChangeSetCount = 0; if (count($inChangeSetIDs) > 0) { $affectedChangeSetCount = ChangeSet::get() ->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]) ->count(); } $numCampaigns = ChangeSet::singleton()->i18n_pluralise($affectedChangeSetCount); $numCampaigns = mb_strtolower($numCampaigns); 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 ]); } elseif (count($descendants) > 0) { $archiveWarningMsg = $defaultMessage; } 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 ]); } 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?'); } return $archiveWarningMsg; } /** * Find IDs of all descendant pages for the provided ID lists. * @param int[] $recordIDs * @param array $collator * @return bool */ protected function collateDescendants($recordIDs, &$collator) { $children = SiteTree::get()->filter(['ParentID' => $recordIDs])->column(); if ($children) { foreach ($children as $item) { $collator[] = $item; } $this->collateDescendants($children, $collator); return true; } return false; } /** * This method exclusively handles deferred ajax requests to render the * pages tree deferred handler (no pjax-fragment) * * @return DBHTMLText HTML response with the rendered treeview */ public function treeview() { return $this->renderWith($this->getTemplatesWithSuffix('_TreeView')); } /** * Returns deferred listview for the current level * * @return DBHTMLText HTML response with the rendered listview */ public function listview() { return $this->renderWith($this->getTemplatesWithSuffix('_ListView')); } /** * Get view state based on the current action * * @param string $default * @return string */ public function ViewState($default = 'treeview') { $mode = $this->getRequest()->param('Action'); switch ($mode) { case 'listview': case 'treeview': return $mode; default: return $default; } } /** * Callback to request the list of page types allowed under a given page instance. * Provides a slower but more precise response over SiteTreeHints * * @param HTTPRequest $request * @return HTTPResponse */ public function childfilter($request) { // Check valid parent specified $parentID = $request->requestVar('ParentID'); $parent = SiteTree::get()->byID($parentID); if (!$parent || !$parent->exists()) { return $this->httpError(404); } // Build hints specific to this class // Identify disallows and set globals $classes = SiteTree::page_type_classes(); $disallowedChildren = []; foreach ($classes as $class) { $obj = singleton($class); if ($obj instanceof HiddenClass) { continue; } if (!$obj->canCreate(null, ['Parent' => $parent])) { $disallowedChildren[] = $class; } } $this->extend('updateChildFilter', $disallowedChildren, $parentID); return $this ->getResponse() ->addHeader('Content-Type', 'application/json; charset=utf-8') ->setBody(json_encode($disallowedChildren)); } /** * Safely reconstruct a selected filter from a given set of query parameters * * @param array $params Query parameters to use, or null if none present * @return CMSSiteTreeFilter The filter class * @throws InvalidArgumentException if invalid filter class is passed. */ protected function getQueryFilter($params) { if (empty($params['FilterClass'])) { return null; } $filterClass = $params['FilterClass']; if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) { throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}"); } return $filterClass::create($params); } /** * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page * defaulting to no filter and show all pages in first level. * Doubles as search results, if any search parameters are set through {@link SearchForm()}. * * @param array $params Search filter criteria * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria) * @return SS_List * @throws InvalidArgumentException if invalid filter class is passed. */ public function getList($params = [], $parentID = 0) { if ($filter = $this->getQueryFilter($params)) { return $filter->getFilteredPages(); } else { $list = DataList::create($this->config()->get('tree_class')); $parentID = is_numeric($parentID) ? $parentID : 0; return $list->filter("ParentID", $parentID); } } /** * @return Form */ public function ListViewForm() { $params = $this->getRequest()->requestVar('q'); $parentID = $this->getRequest()->requestVar('ParentID'); // Set default filter if other params are set if ($params && empty($params['FilterClass'])) { $params['FilterClass'] = CMSSiteTreeFilter_Search::class; } $list = $this->getList($params, $parentID); $gridFieldConfig = GridFieldConfig::create()->addComponents( new GridFieldSortableHeader(), new GridFieldDataColumns(), new GridFieldPaginator($this->config()->get('page_length')) ); if ($parentID) { $linkSpec = $this->LinkListViewChildren('%d'); $gridFieldConfig->addComponent( GridFieldLevelup::create($parentID) ->setLinkSpec($linkSpec) ->setAttributes(['data-pjax-target' => 'ListViewForm,Breadcrumbs']) ); $this->setCurrentPageID($parentID); } $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig); $gridField->setAttribute('cms-loading-ignore-url-params', true); /** @var GridFieldDataColumns $columns */ $columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class); // Don't allow navigating into children nodes on filtered lists $fields = [ 'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'), 'singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'), 'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'), ]; /** @var GridFieldSortableHeader $sortableHeader */ $sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class); $sortableHeader->setFieldSorting(['getTreeTitle' => 'Title']); $gridField->getState()->ParentID = $parentID; if (!$params) { $fields = array_merge(['listChildrenLink' => ''], $fields); } $columns->setDisplayFields($fields); $columns->setFieldCasting([ 'Created' => 'DBDatetime->Ago', 'LastEdited' => 'DBDatetime->FormatFromSettings', 'getTreeTitle' => 'HTMLFragment' ]); $columns->setFieldFormatting([ 'listChildrenLink' => function ($value, &$item) { /** @var SiteTree $item */ $num = $item ? $item->numChildren() : null; if ($num) { 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>', $this->LinkListViewChildren((int)$item->ID), $num ); } }, 'getTreeTitle' => function ($value, &$item) { /** @var SiteTree $item */ $title = sprintf( '<a class="action-detail" href="%s">%s</a>', $item->CMSEditLink(), $item->TreeTitle // returns HTML, does its own escaping ); $breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/'); // Remove item's tile $breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs)); // Trim spaces around delimiters $breadcrumbs = preg_replace('/\s?\/\s?/', '/', trim($breadcrumbs)); return $title . sprintf('<p class="small cms-list__item-breadcrumbs">%s</p>', $breadcrumbs); } ]); $negotiator = $this->getResponseNegotiator(); $listview = Form::create( $this, 'ListViewForm', new FieldList($gridField), new FieldList() )->setHTMLID('Form_ListViewForm'); $listview->setAttribute('data-pjax-fragment', 'ListViewForm'); $listview->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $listview) { $request = $this->getRequest(); if ($request->isAjax() && $negotiator) { $result = $listview->forTemplate(); return $negotiator->respond($request, [ 'CurrentForm' => function () use ($result) { return $result; } ]); } }); $this->extend('updateListView', $listview); $listview->disableSecurityToken(); return $listview; } public function currentPageID() { $id = parent::currentPageID(); $this->extend('updateCurrentPageID', $id); return $id; } //------------------------------------------------------------------------------------------// // Data saving handlers /** * Save and Publish page handler * * @param array $data * @param Form $form * @return HTTPResponse * @throws HTTPResponse_Exception */ public function save($data, $form) { $className = $this->config()->get('tree_class'); // Existing or new record? $id = $data['ID']; if (substr($id, 0, 3) != 'new') { /** @var SiteTree $record */ $record = DataObject::get_by_id($className, $id); // Check edit permissions if ($record && !$record->canEdit()) { return Security::permissionFailure($this); } if (!$record || !$record->ID) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } } else { if (!$className::singleton()->canCreate()) { return Security::permissionFailure($this); } $record = $this->getNewItem($id, false); } // Check publishing permissions $doPublish = !empty($data['publish']); if ($record && $doPublish && !$record->canPublish()) { return Security::permissionFailure($this); } // TODO Coupling to SiteTree $record->HasBrokenLink = 0; $record->HasBrokenFile = 0; if (!$record->ObsoleteClassName) { $record->writeWithoutVersion(); } // Update the class instance if necessary if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) { // Replace $record with a new instance of the new class $newClassName = $data['ClassName']; $record = $record->newClassInstance($newClassName); } // save form data into record $form->saveInto($record); $record->write(); // If the 'Publish' button was clicked, also publish the page if ($doPublish) { $record->publishRecursive(); $message = _t( __CLASS__ . '.PUBLISHED', "Published '{title}' successfully.", ['title' => $record->Title] ); } else { $message = _t( __CLASS__ . '.SAVED', "Saved '{title}' successfully.", ['title' => $record->Title] ); } $this->getResponse()->addHeader('X-Status', rawurlencode($message)); return $this->getResponseNegotiator()->respond($this->getRequest()); } /** * @uses LeftAndMainExtension->augmentNewSiteTreeItem() * * @param int|string $id * @param bool $setID * @return mixed|DataObject * @throws HTTPResponse_Exception */ public function getNewItem($id, $setID = true) { $parentClass = $this->config()->get('tree_class'); list(, $className, $parentID) = array_pad(explode('-', $id), 3, null); if (!is_a($className, $parentClass, true)) { $response = Security::permissionFailure($this); if (!$response) { $response = $this->getResponse(); } throw new HTTPResponse_Exception($response); } /** @var SiteTree $newItem */ $newItem = Injector::inst()->create($className); $newItem->Title = _t( __CLASS__ . '.NEWPAGE', "New {pagetype}", 'followed by a page type title', ['pagetype' => singleton($className)->i18n_singular_name()] ); $newItem->ClassName = $className; $newItem->ParentID = $parentID; // DataObject::fieldExists only checks the current class, not the hierarchy // This allows the CMS to set the correct sort value if ($newItem->castingHelper('Sort')) { $table = DataObject::singleton(SiteTree::class)->baseTable(); $maxSort = DB::prepared_query( "SELECT MAX(\"Sort\") FROM \"$table\" WHERE \"ParentID\" = ?", [$parentID] )->value(); $newItem->Sort = (int)$maxSort + 1; } if ($setID && $id) { $newItem->ID = $id; } # Some modules like subsites add extra fields that need to be set when the new item is created $this->extend('augmentNewSiteTreeItem', $newItem); return $newItem; } /** * Actually perform the publication step * * @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. * * @uses SiteTree->doRevertToLive() * * @param array $data * @param Form $form * @return HTTPResponse * @throws HTTPResponse_Exception */ public function revert($data, $form) { if (!isset($data['ID'])) { throw new HTTPResponse_Exception("Please pass an ID in the form content", 400); } $id = (int) $data['ID']; $restoredPage = Versioned::get_latest_version(SiteTree::class, $id); if (!$restoredPage) { throw new HTTPResponse_Exception("SiteTree #$id not found", 400); } /** @var SiteTree $record */ $table = DataObject::singleton(SiteTree::class)->baseTable(); $liveTable = DataObject::singleton(SiteTree::class)->stageTable($table, Versioned::LIVE); $record = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, [ "\"$liveTable\".\"ID\"" => $id ]); // a user can restore a page without publication rights, as it just adds a new draft state // (this action should just be available when page has been "deleted from draft") if ($record && !$record->canEdit()) { return Security::permissionFailure($this); } if (!$record || !$record->ID) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } $record->doRevertToLive(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.RESTORED', "Restored '{title}' successfully", 'Param {title} is a title', ['title' => $record->Title] )) ); return $this->getResponseNegotiator()->respond($this->getRequest()); } /** * Delete the current page from draft stage. * * @see deletefromlive() * * @param array $data * @param Form $form * @return HTTPResponse * @throws HTTPResponse_Exception */ public function delete($data, $form) { $id = $data['ID']; $record = SiteTree::get()->byID($id); if ($record && !$record->canDelete()) { return Security::permissionFailure(); } if (!$record || !$record->ID) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } // Delete record $record->delete(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __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" return $this->getResponseNegotiator()->respond($this->getRequest()); } /** * Delete this page from both live and stage * * @param array $data * @param Form $form * @return HTTPResponse * @throws HTTPResponse_Exception */ public function archive($data, $form) { $id = $data['ID']; /** @var SiteTree $record */ $record = SiteTree::get()->byID($id); if (!$record || !$record->exists()) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } if (!$record->canArchive()) { return Security::permissionFailure(); } // Archive record $record->doArchive(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.ARCHIVEDPAGE', "Archived page '{title}'", ['title' => $record->Title] )) ); // Even if the record has been deleted from stage and live, it can be viewed in "archive mode" return $this->getResponseNegotiator()->respond($this->getRequest()); } public function publish($data, $form) { $data['publish'] = '1'; return $this->save($data, $form); } public function unpublish($data, $form) { $className = $this->config()->get('tree_class'); /** @var SiteTree $record */ $record = DataObject::get_by_id($className, $data['ID']); if ($record && !$record->canUnpublish()) { return Security::permissionFailure($this); } if (!$record || !$record->ID) { throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404); } $record->doUnpublish(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.REMOVEDPAGE', "Removed '{title}' from the published site", ['title' => $record->Title] )) ); return $this->getResponseNegotiator()->respond($this->getRequest()); } /** * @return HTTPResponse */ public function rollback() { return $this->doRollback([ 'ID' => $this->currentPageID(), 'Version' => $this->getRequest()->param('VersionID') ], null); } /** * Rolls a site back to a given version ID * * @param array $data * @param Form $form * @return HTTPResponse */ public function doRollback($data, $form) { $this->extend('onBeforeRollback', $data['ID'], $data['Version']); $id = (isset($data['ID'])) ? (int) $data['ID'] : null; $version = (isset($data['Version'])) ? (int) $data['Version'] : null; /** @var SiteTree|Versioned $record */ $record = Versioned::get_latest_version($this->config()->get('tree_class'), $id); if ($record && !$record->canEdit()) { return Security::permissionFailure($this); } if ($version) { $record->doRollbackTo($version); $message = _t( __CLASS__ . '.ROLLEDBACKVERSIONv2', "Rolled back to version #{version}.", ['version' => $data['Version']] ); } else { $record->doRevertToLive(); $record->publishRecursive(); $message = _t( __CLASS__ . '.ROLLEDBACKPUBv2', "Rolled back to published version." ); } $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. // 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. $url = $record->CMSEditLink(); $this->getResponse()->addHeader('X-ControllerURL', $url); $this->getRequest()->addHeader('X-Pjax', 'Content'); $this->getResponse()->addHeader('X-Pjax', 'Content'); return $this->getResponseNegotiator()->respond($this->getRequest()); } /** * Batch Actions Handler */ public function batchactions() { return new CMSBatchActionHandler($this, 'batchactions'); } public function BatchActionParameters() { $batchActions = CMSBatchActionHandler::config()->batch_actions; $forms = []; foreach ($batchActions as $urlSegment => $batchAction) { $SNG_action = singleton($batchAction); if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) { $formHtml = ''; /** @var FormField $field */ foreach ($fieldset as $field) { $formHtml .= $field->Field(); } $forms[$urlSegment] = $formHtml; } } $pageHtml = ''; foreach ($forms as $urlSegment => $html) { $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n"; } return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>'); } /** * Returns a list of batch actions */ public function BatchActionList() { return $this->batchactions()->batchActionList(); } /** * @deprecated 5.0 Please use custom logic for this * @param $request * @return HTTPResponse|string|void */ public function publishall($request) { if (!Permission::check('ADMIN')) { return Security::permissionFailure($this); } Environment::increaseTimeLimitTo(); Environment::increaseMemoryLimitTo(); $response = ""; if (isset($this->requestParams['confirm'])) { // Protect against CSRF on destructive action if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } $start = 0; $pages = SiteTree::get()->limit("$start,30"); $count = 0; while ($pages) { /** @var SiteTree $page */ foreach ($pages as $page) { if ($page && !$page->canPublish()) { return Security::permissionFailure($this); } $page->publishRecursive(); $page->destroy(); unset($page); $count++; $response .= "<li>$count</li>"; } if ($pages->count() > 29) { $start += 30; $pages = SiteTree::get()->limit("$start,30"); } else { break; } } $response .= _t(__CLASS__ . '.PUBPAGES', "Done: Published {count} pages", ['count' => $count]); } else { $token = SecurityToken::inst(); $fields = new FieldList(); $token->updateFieldSet($fields); $tokenField = $fields->first(); $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : ''; $publishAllDescription = _t( __CLASS__ . '.PUBALLFUN2', 'Pressing this button will do the equivalent of going to every page and pressing "publish". ' . 'It\'s intended to be used after there have been massive edits of the content, such as when ' . 'the site was first built. ' . 'For large websites, this task might not be able to run through to completion. ' . 'In this case, we recommend talking to your developers to create a custom task' ); $response .= '<h1>' . _t(__CLASS__ . '.PUBALLFUN', '"Publish All" functionality') . '</h1> <p>' . $publishAllDescription . '</p> <form method="post" action="publishall"> <input type="submit" name="confirm" value="' . _t(__CLASS__ . '.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />' . $tokenHtml . '</form>'; } return $response; } /** * Restore a completely deleted page from the SiteTree_versions table. * * @param array $data * @param Form $form * @return HTTPResponse */ public function restore($data, $form) { if (!isset($data['ID']) || !is_numeric($data['ID'])) { return new HTTPResponse("Please pass an ID in the form content", 400); } $id = (int)$data['ID']; /** @var SiteTree $restoredPage */ $restoredPage = Versioned::get_latest_version(SiteTree::class, $id); if (!$restoredPage) { return new HTTPResponse("SiteTree #$id not found", 400); } $restoredPage = $restoredPage->doRestoreToStage(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.RESTORED', "Restored '{title}' successfully", ['title' => $restoredPage->Title] )) ); return $this->getResponseNegotiator()->respond($this->getRequest()); } public function duplicate($request) { // Protect against CSRF on destructive action if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } if (($id = $this->urlParams['ID']) && is_numeric($id)) { /** @var SiteTree $page */ $page = SiteTree::get()->byID($id); if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) { return Security::permissionFailure($this); } if (!$page || !$page->ID) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } /** @var SiteTree $newPage */ $newPage = $page->duplicate(); // ParentID can be hard-set in the URL. This is useful for pages with multiple parents if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) { $newPage->ParentID = $_GET['parentID']; $newPage->write(); } $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.DUPLICATED', "Duplicated '{title}' successfully", ['title' => $newPage->Title] )) ); $url = $newPage->CMSEditLink(); $this->getResponse()->addHeader('X-ControllerURL', $url); $this->getRequest()->addHeader('X-Pjax', 'Content'); $this->getResponse()->addHeader('X-Pjax', 'Content'); return $this->getResponseNegotiator()->respond($this->getRequest()); } return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400); } public function duplicatewithchildren($request) { // Protect against CSRF on destructive action if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } Environment::increaseTimeLimitTo(); if (($id = $this->urlParams['ID']) && is_numeric($id)) { /** @var SiteTree $page */ $page = SiteTree::get()->byID($id); if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) { return Security::permissionFailure($this); } if (!$page || !$page->ID) { throw new HTTPResponse_Exception("Bad record ID #$id", 404); } /** @var SiteTree $newPage */ $newPage = $page->duplicateWithChildren(); $this->getResponse()->addHeader( 'X-Status', rawurlencode(_t( __CLASS__ . '.DUPLICATEDWITHCHILDREN', "Duplicated '{title}' and children successfully", ['title' => $newPage->Title] )) ); $url = $newPage->CMSEditLink(); $this->getResponse()->addHeader('X-ControllerURL', $url); $this->getRequest()->addHeader('X-Pjax', 'Content'); $this->getResponse()->addHeader('X-Pjax', 'Content'); return $this->getResponseNegotiator()->respond($this->getRequest()); } return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400); } public function providePermissions() { $title = CMSPagesController::menu_title(); return [ "CMS_ACCESS_CMSMain" => [ 'name' => _t(__CLASS__ . '.ACCESS', "Access to '{title}' section", ['title' => $title]), 'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 'help' => _t( __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".' ), 'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else ] ]; } /** * Get title for root CMS node * * @return string */ protected function getCMSTreeTitle() { $rootTitle = SiteConfig::current_site_config()->Title; $this->extend('updateCMSTreeTitle', $rootTitle); return $rootTitle; } /** * Cache key for SiteTreeHints() method * * @param $memberID * @return string */ protected function generateHintsCacheKey($memberID) { $baseKey = $memberID . '_' . __CLASS__; $this->extend('updateHintsCacheKey', $baseKey); return md5($baseKey); } /** * Clear the cache on ?flush */ public static function flush() { CMSMain::singleton()->clearCache(); } /** * Flush the hints cache for a specific member * * @param array $memberIDs */ public function flushMemberCache($memberIDs = null) { $cache = $this->getHintsCache(); if (!$memberIDs) { $cache->clear(); return; } foreach ($memberIDs as $memberID) { $key = $this->generateHintsCacheKey($memberID); $cache->delete($key); } } } |