Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like CMSMain often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use CMSMain, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
80 | class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider |
||
81 | { |
||
82 | |||
83 | private static $url_segment = 'pages'; |
||
|
|||
84 | |||
85 | private static $url_rule = '/$Action/$ID/$OtherID'; |
||
86 | |||
87 | // Maintain a lower priority than other administration sections |
||
88 | // so that Director does not think they are actions of CMSMain |
||
89 | private static $url_priority = 39; |
||
90 | |||
91 | private static $menu_title = 'Edit Page'; |
||
92 | |||
93 | private static $menu_icon_class = 'font-icon-sitemap'; |
||
94 | |||
95 | private static $menu_priority = 10; |
||
96 | |||
97 | private static $tree_class = SiteTree::class; |
||
98 | |||
99 | private static $subitem_class = Member::class; |
||
100 | |||
101 | private static $session_namespace = self::class; |
||
102 | |||
103 | private static $required_permission_codes = 'CMS_ACCESS_CMSMain'; |
||
104 | |||
105 | /** |
||
106 | * Amount of results showing on a single page. |
||
107 | * |
||
108 | * @config |
||
109 | * @var int |
||
110 | */ |
||
111 | private static $page_length = 15; |
||
112 | |||
113 | private static $allowed_actions = array( |
||
114 | 'archive', |
||
115 | 'deleteitems', |
||
116 | 'DeleteItemsForm', |
||
117 | 'dialog', |
||
118 | 'duplicate', |
||
119 | 'duplicatewithchildren', |
||
120 | 'publishall', |
||
121 | 'publishitems', |
||
122 | 'PublishItemsForm', |
||
123 | 'submit', |
||
124 | 'EditForm', |
||
125 | 'SearchForm', |
||
126 | 'SiteTreeAsUL', |
||
127 | 'getshowdeletedsubtree', |
||
128 | 'savetreenode', |
||
129 | 'getsubtree', |
||
130 | 'updatetreenodes', |
||
131 | 'batchactions', |
||
132 | 'treeview', |
||
133 | 'listview', |
||
134 | 'ListViewForm', |
||
135 | 'childfilter', |
||
136 | ); |
||
137 | |||
138 | private static $url_handlers = [ |
||
139 | 'EditForm/$ID' => 'EditForm', |
||
140 | ]; |
||
141 | |||
142 | private static $casting = array( |
||
143 | 'TreeIsFiltered' => 'Boolean', |
||
144 | 'AddForm' => 'HTMLFragment', |
||
145 | 'LinkPages' => 'Text', |
||
146 | 'Link' => 'Text', |
||
147 | 'ListViewForm' => 'HTMLFragment', |
||
148 | 'ExtraTreeTools' => 'HTMLFragment', |
||
149 | 'PageList' => 'HTMLFragment', |
||
150 | 'PageListSidebar' => 'HTMLFragment', |
||
151 | 'SiteTreeHints' => 'HTMLFragment', |
||
152 | 'SecurityID' => 'Text', |
||
153 | 'SiteTreeAsUL' => 'HTMLFragment', |
||
154 | ); |
||
155 | |||
156 | protected function init() |
||
157 | { |
||
158 | // set reading lang |
||
159 | if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) { |
||
160 | Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree'))); |
||
161 | } |
||
162 | |||
163 | parent::init(); |
||
164 | |||
165 | Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js'); |
||
166 | Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js'); |
||
167 | Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css'); |
||
168 | Requirements::customCSS($this->generatePageIconsCss()); |
||
169 | Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true); |
||
170 | |||
171 | CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class); |
||
172 | CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class); |
||
173 | CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class); |
||
174 | CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class); |
||
175 | } |
||
176 | |||
177 | public function index($request) |
||
178 | { |
||
179 | // In case we're not showing a specific record, explicitly remove any session state, |
||
180 | // to avoid it being highlighted in the tree, and causing an edit form to show. |
||
181 | if (!$request->param('Action')) { |
||
182 | $this->setCurrentPageID(null); |
||
183 | } |
||
184 | |||
185 | return parent::index($request); |
||
186 | } |
||
187 | |||
188 | public function getResponseNegotiator() |
||
189 | { |
||
190 | $negotiator = parent::getResponseNegotiator(); |
||
191 | |||
192 | // ListViewForm |
||
193 | $negotiator->setCallback('ListViewForm', function () { |
||
194 | return $this->ListViewForm()->forTemplate(); |
||
195 | }); |
||
196 | |||
197 | // PageList view |
||
198 | $negotiator->setCallback('Content-PageList', function () { |
||
199 | return $this->PageList()->forTemplate(); |
||
200 | }); |
||
201 | |||
202 | // PageList view for edit controller |
||
203 | $negotiator->setCallback('Content-PageList-Sidebar', function () { |
||
204 | return $this->PageListSidebar()->forTemplate(); |
||
205 | }); |
||
206 | |||
207 | return $negotiator; |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Get pages listing area |
||
212 | * |
||
213 | * @return DBHTMLText |
||
214 | */ |
||
215 | public function PageList() |
||
216 | { |
||
217 | return $this->renderWith($this->getTemplatesWithSuffix('_PageList')); |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Page list view for edit-form |
||
222 | * |
||
223 | * @return DBHTMLText |
||
224 | */ |
||
225 | public function PageListSidebar() |
||
226 | { |
||
227 | return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar')); |
||
228 | } |
||
229 | |||
230 | /** |
||
231 | * If this is set to true, the "switchView" context in the |
||
232 | * template is shown, with links to the staging and publish site. |
||
233 | * |
||
234 | * @return boolean |
||
235 | */ |
||
236 | public function ShowSwitchView() |
||
237 | { |
||
238 | return true; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able |
||
243 | * to switch view also for archived versions. |
||
244 | * |
||
245 | * @param SiteTree $page |
||
246 | * @return array |
||
247 | */ |
||
248 | public function SwitchView($page = null) |
||
249 | { |
||
250 | if (!$page) { |
||
251 | $page = $this->currentPage(); |
||
252 | } |
||
253 | |||
254 | if ($page) { |
||
255 | $nav = SilverStripeNavigator::get_for_record($page); |
||
256 | return $nav['items']; |
||
257 | } |
||
258 | } |
||
259 | |||
260 | //------------------------------------------------------------------------------------------// |
||
261 | // Main controllers |
||
262 | |||
263 | //------------------------------------------------------------------------------------------// |
||
264 | // Main UI components |
||
265 | |||
266 | /** |
||
267 | * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain. |
||
268 | * |
||
269 | * @param string|null $action Action to link to. |
||
270 | * @return string |
||
271 | */ |
||
272 | public function Link($action = null) |
||
273 | { |
||
274 | $link = Controller::join_links( |
||
275 | AdminRootController::admin_url(), |
||
276 | $this->stat('url_segment'), // in case we want to change the segment |
||
277 | '/', // trailing slash needed if $action is null! |
||
278 | "$action" |
||
279 | ); |
||
280 | $this->extend('updateLink', $link); |
||
281 | return $link; |
||
282 | } |
||
283 | |||
284 | public function LinkPages() |
||
285 | { |
||
286 | return CMSPagesController::singleton()->Link(); |
||
287 | } |
||
288 | |||
289 | public function LinkPagesWithSearch() |
||
290 | { |
||
291 | return $this->LinkWithSearch($this->LinkPages()); |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * Get link to tree view |
||
296 | * |
||
297 | * @return string |
||
298 | */ |
||
299 | public function LinkTreeView() |
||
300 | { |
||
301 | // Tree view is just default link to main pages section (no /treeview suffix) |
||
302 | return $this->LinkWithSearch(CMSMain::singleton()->Link()); |
||
303 | } |
||
304 | |||
305 | /** |
||
306 | * Get link to list view |
||
307 | * |
||
308 | * @return string |
||
309 | */ |
||
310 | public function LinkListView() |
||
311 | { |
||
312 | // Note : Force redirect to top level page controller |
||
313 | return $this->LinkWithSearch(CMSMain::singleton()->Link('listview')); |
||
314 | } |
||
315 | |||
316 | View Code Duplication | public function LinkPageEdit($id = null) |
|
317 | { |
||
318 | if (!$id) { |
||
319 | $id = $this->currentPageID(); |
||
320 | } |
||
321 | return $this->LinkWithSearch( |
||
322 | Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id) |
||
323 | ); |
||
324 | } |
||
325 | |||
326 | View Code Duplication | public function LinkPageSettings() |
|
327 | { |
||
328 | if ($id = $this->currentPageID()) { |
||
329 | return $this->LinkWithSearch( |
||
330 | Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id) |
||
331 | ); |
||
332 | } else { |
||
333 | return null; |
||
334 | } |
||
335 | } |
||
336 | |||
337 | View Code Duplication | public function LinkPageHistory() |
|
338 | { |
||
339 | if ($id = $this->currentPageID()) { |
||
340 | return $this->LinkWithSearch( |
||
341 | Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id) |
||
342 | ); |
||
343 | } else { |
||
344 | return null; |
||
345 | } |
||
346 | } |
||
347 | |||
348 | public function LinkWithSearch($link) |
||
349 | { |
||
350 | // Whitelist to avoid side effects |
||
351 | $params = array( |
||
352 | 'q' => (array)$this->getRequest()->getVar('q'), |
||
353 | 'ParentID' => $this->getRequest()->getVar('ParentID') |
||
354 | ); |
||
355 | $link = Controller::join_links( |
||
356 | $link, |
||
357 | array_filter(array_values($params)) ? '?' . http_build_query($params) : null |
||
358 | ); |
||
359 | $this->extend('updateLinkWithSearch', $link); |
||
360 | return $link; |
||
361 | } |
||
362 | |||
363 | public function LinkPageAdd($extra = null, $placeholders = null) |
||
364 | { |
||
365 | $link = CMSPageAddController::singleton()->Link(); |
||
366 | $this->extend('updateLinkPageAdd', $link); |
||
367 | |||
368 | if ($extra) { |
||
369 | $link = Controller::join_links($link, $extra); |
||
370 | } |
||
371 | |||
372 | if ($placeholders) { |
||
373 | $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders"); |
||
374 | } |
||
375 | |||
376 | return $link; |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * @return string |
||
381 | */ |
||
382 | public function LinkPreview() |
||
383 | { |
||
384 | $record = $this->getRecord($this->currentPageID()); |
||
385 | $baseLink = Director::absoluteBaseURL(); |
||
386 | if ($record && $record instanceof SiteTree) { |
||
387 | // if we are an external redirector don't show a link |
||
388 | if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') { |
||
389 | $baseLink = false; |
||
390 | } else { |
||
391 | $baseLink = $record->Link('?stage=Stage'); |
||
392 | } |
||
393 | } |
||
394 | return $baseLink; |
||
395 | } |
||
396 | |||
397 | /** |
||
398 | * Return the entire site tree as a nested set of ULs |
||
399 | */ |
||
400 | public function SiteTreeAsUL() |
||
401 | { |
||
402 | // Pre-cache sitetree version numbers for querying efficiency |
||
403 | Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT); |
||
404 | Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE); |
||
405 | $html = $this->getSiteTreeFor($this->stat('tree_class')); |
||
406 | |||
407 | $this->extend('updateSiteTreeAsUL', $html); |
||
408 | |||
409 | return $html; |
||
410 | } |
||
411 | |||
412 | /** |
||
413 | * Get a site tree HTML listing which displays the nodes under the given criteria. |
||
414 | * |
||
415 | * @param string $className The class of the root object |
||
416 | * @param string $rootID The ID of the root object. If this is null then a complete tree will be |
||
417 | * shown |
||
418 | * @param string $childrenMethod The method to call to get the children of the tree. For example, |
||
419 | * Children, AllChildrenIncludingDeleted, or AllHistoricalChildren |
||
420 | * @param string $numChildrenMethod |
||
421 | * @param callable $filterFunction |
||
422 | * @param int $nodeCountThreshold |
||
423 | * @return string Nested unordered list with links to each page |
||
424 | */ |
||
425 | public function getSiteTreeFor( |
||
426 | $className, |
||
427 | $rootID = null, |
||
428 | $childrenMethod = null, |
||
429 | $numChildrenMethod = null, |
||
430 | $filterFunction = null, |
||
431 | $nodeCountThreshold = 30 |
||
432 | ) { |
||
433 | // Provide better defaults from filter |
||
434 | $filter = $this->getSearchFilter(); |
||
435 | if ($filter) { |
||
436 | if (!$childrenMethod) { |
||
437 | $childrenMethod = $filter->getChildrenMethod(); |
||
438 | } |
||
439 | if (!$numChildrenMethod) { |
||
440 | $numChildrenMethod = $filter->getNumChildrenMethod(); |
||
441 | } |
||
442 | if (!$filterFunction) { |
||
443 | $filterFunction = function ($node) use ($filter) { |
||
444 | return $filter->isPageIncluded($node); |
||
445 | }; |
||
446 | } |
||
447 | } |
||
448 | |||
449 | // Build set from node and begin marking |
||
450 | $record = ($rootID) ? $this->getRecord($rootID) : null; |
||
451 | $rootNode = $record ? $record : DataObject::singleton($className); |
||
452 | $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold); |
||
453 | |||
454 | // Set filter function |
||
455 | if ($filterFunction) { |
||
456 | $markingSet->setMarkingFilterFunction($filterFunction); |
||
457 | } |
||
458 | |||
459 | // Mark tree from this node |
||
460 | $markingSet->markPartialTree(); |
||
461 | |||
462 | // Ensure current page is exposed |
||
463 | $currentPage = $this->currentPage(); |
||
464 | if ($currentPage) { |
||
465 | $markingSet->markToExpose($currentPage); |
||
466 | } |
||
467 | |||
468 | // Pre-cache permissions |
||
469 | SiteTree::prepopulate_permission_cache( |
||
470 | 'CanEditType', |
||
471 | $markingSet->markedNodeIDs(), |
||
472 | [ SiteTree::class, 'can_edit_multiple'] |
||
473 | ); |
||
474 | |||
475 | // Render using full-subtree template |
||
476 | return $markingSet->renderChildren( |
||
477 | [ self::class . '_SubTree', 'type' => 'Includes' ], |
||
478 | $this->getTreeNodeCustomisations() |
||
479 | ); |
||
480 | } |
||
481 | |||
482 | |||
483 | /** |
||
484 | * Get callback to determine template customisations for nodes |
||
485 | * |
||
486 | * @return callable |
||
487 | */ |
||
488 | protected function getTreeNodeCustomisations() |
||
489 | { |
||
490 | $rootTitle = $this->getCMSTreeTitle(); |
||
491 | $linkWithSearch = $this->LinkWithSearch($this->Link()); |
||
492 | return function (SiteTree $node) use ($linkWithSearch, $rootTitle) { |
||
493 | return [ |
||
494 | 'listViewLink' => Controller::join_links( |
||
495 | $linkWithSearch, |
||
496 | '?view=listview&ParentID=' . $node->ID |
||
497 | ), |
||
498 | 'rootTitle' => $rootTitle, |
||
499 | 'extraClass' => $this->getTreeNodeClasses($node), |
||
500 | ]; |
||
501 | }; |
||
502 | } |
||
503 | |||
504 | /** |
||
505 | * Get extra CSS classes for a page's tree node |
||
506 | * |
||
507 | * @param SiteTree $node |
||
508 | * @return string |
||
509 | */ |
||
510 | public function getTreeNodeClasses(SiteTree $node) |
||
511 | { |
||
512 | // Get classes from object |
||
513 | $classes = $node->CMSTreeClasses(); |
||
514 | |||
515 | // Flag as current |
||
516 | if ($this->isCurrentPage($node)) { |
||
517 | $classes .= ' current'; |
||
518 | } |
||
519 | |||
520 | // Get status flag classes |
||
521 | $flags = $node->getStatusFlags(); |
||
522 | if ($flags) { |
||
523 | $statuses = array_keys($flags); |
||
524 | foreach ($statuses as $s) { |
||
525 | $classes .= ' status-' . $s; |
||
526 | } |
||
527 | } |
||
528 | |||
529 | // Get additional filter classes |
||
530 | $filter = $this->getSearchFilter(); |
||
531 | if ($filter && ($filterClasses = $filter->getPageClasses($node))) { |
||
532 | if (is_array($filterClasses)) { |
||
533 | $filterClasses = implode(' ', $filterClasses); |
||
534 | } |
||
535 | $classes .= ' ' . $filterClasses; |
||
536 | } |
||
537 | |||
538 | return trim($classes); |
||
539 | } |
||
540 | |||
541 | /** |
||
542 | * Get a subtree underneath the request param 'ID'. |
||
543 | * If ID = 0, then get the whole tree. |
||
544 | * |
||
545 | * @param HTTPRequest $request |
||
546 | * @return string |
||
547 | */ |
||
548 | public function getsubtree($request) |
||
549 | { |
||
550 | $html = $this->getSiteTreeFor( |
||
551 | $this->stat('tree_class'), |
||
552 | $request->getVar('ID'), |
||
553 | null, |
||
554 | null, |
||
555 | null, |
||
556 | $request->getVar('minNodeCount') |
||
557 | ); |
||
558 | |||
559 | // Trim off the outer tag |
||
560 | $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html); |
||
561 | $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html); |
||
562 | |||
563 | return $html; |
||
564 | } |
||
565 | |||
566 | /** |
||
567 | * Allows requesting a view update on specific tree nodes. |
||
568 | * Similar to {@link getsubtree()}, but doesn't enforce loading |
||
569 | * all children with the node. Useful to refresh views after |
||
570 | * state modifications, e.g. saving a form. |
||
571 | * |
||
572 | * @param HTTPRequest $request |
||
573 | * @return HTTPResponse |
||
574 | */ |
||
575 | public function updatetreenodes($request) |
||
576 | { |
||
577 | $data = array(); |
||
578 | $ids = explode(',', $request->getVar('ids')); |
||
579 | foreach ($ids as $id) { |
||
580 | if ($id === "") { |
||
581 | continue; // $id may be a blank string, which is invalid and should be skipped over |
||
582 | } |
||
583 | |||
584 | $record = $this->getRecord($id); |
||
585 | if (!$record) { |
||
586 | continue; // In case a page is no longer available |
||
587 | } |
||
588 | |||
589 | // Create marking set with sole marked root |
||
590 | $markingSet = MarkedSet::create($record); |
||
591 | $markingSet->setMarkingFilterFunction(function () { |
||
592 | return false; |
||
593 | }); |
||
594 | $markingSet->markUnexpanded($record); |
||
595 | |||
596 | // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset) |
||
597 | // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists |
||
598 | $prev = null; |
||
599 | |||
600 | $className = $this->stat('tree_class'); |
||
601 | $next = DataObject::get($className) |
||
602 | ->filter('ParentID', $record->ParentID) |
||
603 | ->filter('Sort:GreaterThan', $record->Sort) |
||
604 | ->first(); |
||
605 | |||
606 | if (!$next) { |
||
607 | $prev = DataObject::get($className) |
||
608 | ->filter('ParentID', $record->ParentID) |
||
609 | ->filter('Sort:LessThan', $record->Sort) |
||
610 | ->reverse() |
||
611 | ->first(); |
||
612 | } |
||
613 | |||
614 | // Render using single node template |
||
615 | $html = $markingSet->renderChildren( |
||
616 | [ self::class . '_TreeNode', 'type' => 'Includes'], |
||
617 | $this->getTreeNodeCustomisations() |
||
618 | ); |
||
619 | |||
620 | $data[$id] = array( |
||
621 | 'html' => $html, |
||
622 | 'ParentID' => $record->ParentID, |
||
623 | 'NextID' => $next ? $next->ID : null, |
||
624 | 'PrevID' => $prev ? $prev->ID : null |
||
625 | ); |
||
626 | } |
||
627 | return $this |
||
628 | ->getResponse() |
||
629 | ->addHeader('Content-Type', 'application/json') |
||
630 | ->setBody(Convert::raw2json($data)); |
||
631 | } |
||
632 | |||
633 | /** |
||
634 | * Update the position and parent of a tree node. |
||
635 | * Only saves the node if changes were made. |
||
636 | * |
||
637 | * Required data: |
||
638 | * - 'ID': The moved node |
||
639 | * - 'ParentID': New parent relation of the moved node (0 for root) |
||
640 | * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself). |
||
641 | * In case of a 'ParentID' change, relates to the new siblings under the new parent. |
||
642 | * |
||
643 | * @param HTTPRequest $request |
||
644 | * @return HTTPResponse JSON string with a |
||
645 | * @throws HTTPResponse_Exception |
||
646 | */ |
||
647 | public function savetreenode($request) |
||
648 | { |
||
649 | if (!SecurityToken::inst()->checkRequest($request)) { |
||
650 | return $this->httpError(400); |
||
651 | } |
||
652 | if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) { |
||
653 | return $this->httpError( |
||
654 | 403, |
||
655 | _t( |
||
656 | __CLASS__.'.CANT_REORGANISE', |
||
657 | "You do not have permission to rearange the site tree. Your change was not saved." |
||
658 | ) |
||
659 | ); |
||
660 | } |
||
661 | |||
662 | $className = $this->stat('tree_class'); |
||
663 | $id = $request->requestVar('ID'); |
||
664 | $parentID = $request->requestVar('ParentID'); |
||
665 | if (!is_numeric($id) || !is_numeric($parentID)) { |
||
666 | return $this->httpError(400); |
||
667 | } |
||
668 | |||
669 | // Check record exists in the DB |
||
670 | /** @var SiteTree $node */ |
||
671 | $node = DataObject::get_by_id($className, $id); |
||
672 | if (!$node) { |
||
673 | return $this->httpError( |
||
674 | 500, |
||
675 | _t( |
||
676 | __CLASS__.'.PLEASESAVE', |
||
677 | "Please Save Page: This page could not be updated because it hasn't been saved yet." |
||
678 | ) |
||
679 | ); |
||
680 | } |
||
681 | |||
682 | // Check top level permissions |
||
683 | $root = $node->getParentType(); |
||
684 | if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) { |
||
685 | return $this->httpError( |
||
686 | 403, |
||
687 | _t( |
||
688 | __CLASS__.'.CANT_REORGANISE', |
||
689 | "You do not have permission to alter Top level pages. Your change was not saved." |
||
690 | ) |
||
691 | ); |
||
692 | } |
||
693 | |||
694 | $siblingIDs = $request->requestVar('SiblingIDs'); |
||
695 | $statusUpdates = array('modified'=>array()); |
||
696 | |||
697 | if (!$node->canEdit()) { |
||
698 | return Security::permissionFailure($this); |
||
699 | } |
||
700 | |||
701 | // Update hierarchy (only if ParentID changed) |
||
702 | if ($node->ParentID != $parentID) { |
||
703 | $node->ParentID = (int)$parentID; |
||
704 | $node->write(); |
||
705 | |||
706 | $statusUpdates['modified'][$node->ID] = array( |
||
707 | 'TreeTitle' => $node->TreeTitle |
||
708 | ); |
||
709 | |||
710 | // Update all dependent pages |
||
711 | $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID); |
||
712 | foreach ($virtualPages as $virtualPage) { |
||
713 | $statusUpdates['modified'][$virtualPage->ID] = array( |
||
714 | 'TreeTitle' => $virtualPage->TreeTitle() |
||
715 | ); |
||
716 | } |
||
717 | |||
718 | $this->getResponse()->addHeader( |
||
719 | 'X-Status', |
||
720 | rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')) |
||
721 | ); |
||
722 | } |
||
723 | |||
724 | // Update sorting |
||
725 | if (is_array($siblingIDs)) { |
||
726 | $counter = 0; |
||
727 | foreach ($siblingIDs as $id) { |
||
728 | if ($id == $node->ID) { |
||
729 | $node->Sort = ++$counter; |
||
730 | $node->write(); |
||
731 | $statusUpdates['modified'][$node->ID] = array( |
||
732 | 'TreeTitle' => $node->TreeTitle |
||
733 | ); |
||
734 | } elseif (is_numeric($id)) { |
||
735 | // Nodes that weren't "actually moved" shouldn't be registered as |
||
736 | // having been edited; do a direct SQL update instead |
||
737 | ++$counter; |
||
738 | $table = DataObject::getSchema()->baseDataTable($className); |
||
739 | DB::prepared_query( |
||
740 | "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?", |
||
741 | array($counter, $id) |
||
742 | ); |
||
743 | } |
||
744 | } |
||
745 | |||
746 | $this->getResponse()->addHeader( |
||
747 | 'X-Status', |
||
748 | rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')) |
||
749 | ); |
||
750 | } |
||
751 | |||
752 | return $this |
||
753 | ->getResponse() |
||
754 | ->addHeader('Content-Type', 'application/json') |
||
755 | ->setBody(Convert::raw2json($statusUpdates)); |
||
756 | } |
||
757 | |||
758 | public function CanOrganiseSitetree() |
||
759 | { |
||
760 | return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true; |
||
761 | } |
||
762 | |||
763 | /** |
||
764 | * @return boolean |
||
765 | */ |
||
766 | public function TreeIsFiltered() |
||
767 | { |
||
768 | $query = $this->getRequest()->getVar('q'); |
||
769 | |||
770 | if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) { |
||
771 | return false; |
||
772 | } |
||
773 | |||
774 | return true; |
||
775 | } |
||
776 | |||
777 | public function ExtraTreeTools() |
||
778 | { |
||
779 | $html = ''; |
||
780 | $this->extend('updateExtraTreeTools', $html); |
||
781 | return $html; |
||
782 | } |
||
783 | |||
784 | /** |
||
785 | * Returns a Form for page searching for use in templates. |
||
786 | * |
||
787 | * Can be modified from a decorator by a 'updateSearchForm' method |
||
788 | * |
||
789 | * @return Form |
||
790 | */ |
||
791 | public function SearchForm() |
||
792 | { |
||
793 | // Create the fields |
||
794 | $content = new TextField('q[Term]', _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search')); |
||
795 | $dateFrom = new DateField( |
||
796 | 'q[LastEditedFrom]', |
||
797 | _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From') |
||
798 | ); |
||
799 | $dateTo = new DateField( |
||
800 | 'q[LastEditedTo]', |
||
801 | _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To') |
||
802 | ); |
||
803 | $pageFilter = new DropdownField( |
||
804 | 'q[FilterClass]', |
||
805 | _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'), |
||
806 | CMSSiteTreeFilter::get_all_filters() |
||
807 | ); |
||
808 | $pageClasses = new DropdownField( |
||
809 | 'q[ClassName]', |
||
810 | _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'), |
||
811 | $this->getPageTypes() |
||
812 | ); |
||
813 | $pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any')); |
||
814 | |||
815 | // Group the Datefields |
||
816 | $dateGroup = new FieldGroup( |
||
817 | $dateFrom, |
||
818 | $dateTo |
||
819 | ); |
||
820 | $dateGroup->setTitle(_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited')); |
||
821 | |||
822 | // view mode |
||
823 | $viewMode = HiddenField::create('view', false, $this->ViewState()); |
||
824 | |||
825 | // Create the Field list |
||
826 | $fields = new FieldList( |
||
827 | $content, |
||
828 | $pageFilter, |
||
829 | $pageClasses, |
||
830 | $dateGroup, |
||
831 | $viewMode |
||
832 | ); |
||
833 | |||
834 | // Create the Search and Reset action |
||
835 | $actions = new FieldList( |
||
836 | FormAction::create('doSearch', _t('SilverStripe\\CMS\\Controllers\\CMSMain.APPLY_FILTER', 'Search')) |
||
837 | ->addExtraClass('btn btn-primary'), |
||
838 | FormAction::create('clear', _t('SilverStripe\\CMS\\Controllers\\CMSMain.CLEAR_FILTER', 'Clear')) |
||
839 | ->setAttribute('type', 'reset') |
||
840 | ->addExtraClass('btn btn-secondary') |
||
841 | ); |
||
842 | |||
843 | // Use <button> to allow full jQuery UI styling on the all of the Actions |
||
844 | /** @var FormAction $action */ |
||
845 | foreach ($actions->dataFields() as $action) { |
||
846 | /** @var FormAction $action */ |
||
847 | $action->setUseButtonTag(true); |
||
848 | } |
||
849 | |||
850 | // Create the form |
||
851 | /** @skipUpgrade */ |
||
852 | $form = Form::create($this, 'SearchForm', $fields, $actions) |
||
853 | ->addExtraClass('cms-search-form') |
||
854 | ->setFormMethod('GET') |
||
855 | ->setFormAction($this->Link()) |
||
856 | ->disableSecurityToken() |
||
857 | ->unsetValidator(); |
||
858 | |||
859 | // Load the form with previously sent search data |
||
860 | $form->loadDataFrom($this->getRequest()->getVars()); |
||
861 | |||
862 | // Allow decorators to modify the form |
||
863 | $this->extend('updateSearchForm', $form); |
||
864 | |||
865 | return $form; |
||
866 | } |
||
867 | |||
868 | /** |
||
869 | * Returns a sorted array suitable for a dropdown with pagetypes and their translated name |
||
870 | * |
||
871 | * @return array |
||
872 | */ |
||
873 | protected function getPageTypes() |
||
882 | |||
883 | public function doSearch($data, $form) |
||
887 | |||
888 | /** |
||
889 | * @param bool $unlinked |
||
890 | * @return ArrayList |
||
891 | */ |
||
892 | public function Breadcrumbs($unlinked = false) |
||
904 | |||
905 | /** |
||
906 | * Create serialized JSON string with site tree hints data to be injected into |
||
907 | * 'data-hints' attribute of root node of jsTree. |
||
908 | * |
||
909 | * @return string Serialized JSON |
||
910 | */ |
||
911 | public function SiteTreeHints() |
||
977 | |||
978 | /** |
||
979 | * Populates an array of classes in the CMS |
||
980 | * which allows the user to change the page type. |
||
981 | * |
||
982 | * @return SS_List |
||
983 | */ |
||
984 | public function PageTypes() |
||
1018 | |||
1019 | /** |
||
1020 | * Get a database record to be managed by the CMS. |
||
1021 | * |
||
1022 | * @param int $id Record ID |
||
1023 | * @param int $versionID optional Version id of the given record |
||
1024 | * @return SiteTree |
||
1025 | */ |
||
1026 | public function getRecord($id, $versionID = null) |
||
1074 | |||
1075 | /** |
||
1076 | * {@inheritdoc} |
||
1077 | * |
||
1078 | * @param HTTPRequest $request |
||
1079 | * @return Form |
||
1080 | */ |
||
1081 | public function EditForm($request = null) |
||
1095 | |||
1096 | /** |
||
1097 | * @param int $id |
||
1098 | * @param FieldList $fields |
||
1099 | * @return Form |
||
1100 | */ |
||
1101 | public function getEditForm($id = null, $fields = null) |
||
1209 | |||
1210 | public function EmptyForm() |
||
1220 | |||
1221 | /** |
||
1222 | * Build an archive warning message based on the page's children |
||
1223 | * |
||
1224 | * @param SiteTree $record |
||
1225 | * @return string |
||
1226 | */ |
||
1227 | protected function getArchiveWarningMessage($record) |
||
1263 | |||
1264 | /** |
||
1265 | * @param HTTPRequest $request |
||
1266 | * @return string HTML |
||
1267 | */ |
||
1268 | public function treeview($request) |
||
1272 | |||
1273 | /** |
||
1274 | * @param HTTPRequest $request |
||
1275 | * @return string HTML |
||
1276 | */ |
||
1277 | public function listview($request) |
||
1281 | |||
1282 | /** |
||
1283 | * @return string |
||
1284 | */ |
||
1285 | public function ViewState() |
||
1297 | |||
1298 | /** |
||
1299 | * Callback to request the list of page types allowed under a given page instance. |
||
1300 | * Provides a slower but more precise response over SiteTreeHints |
||
1301 | * |
||
1302 | * @param HTTPRequest $request |
||
1303 | * @return HTTPResponse |
||
1304 | */ |
||
1305 | public function childfilter($request) |
||
1335 | |||
1336 | /** |
||
1337 | * Safely reconstruct a selected filter from a given set of query parameters |
||
1338 | * |
||
1339 | * @param array $params Query parameters to use |
||
1340 | * @return CMSSiteTreeFilter The filter class, or null if none present |
||
1341 | * @throws InvalidArgumentException if invalid filter class is passed. |
||
1342 | */ |
||
1343 | protected function getQueryFilter($params) |
||
1354 | |||
1355 | /** |
||
1356 | * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page |
||
1357 | * defaulting to no filter and show all pages in first level. |
||
1358 | * Doubles as search results, if any search parameters are set through {@link SearchForm()}. |
||
1359 | * |
||
1360 | * @param array $params Search filter criteria |
||
1361 | * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria) |
||
1362 | * @return SS_List |
||
1363 | * @throws InvalidArgumentException if invalid filter class is passed. |
||
1364 | */ |
||
1365 | public function getList($params = array(), $parentID = 0) |
||
1375 | |||
1376 | /** |
||
1377 | * @return Form |
||
1378 | */ |
||
1379 | public function ListViewForm() |
||
1476 | |||
1477 | public function currentPageID() |
||
1485 | |||
1486 | //------------------------------------------------------------------------------------------// |
||
1487 | // Data saving handlers |
||
1488 | |||
1489 | /** |
||
1490 | * Save and Publish page handler |
||
1491 | * |
||
1492 | * @param array $data |
||
1493 | * @param Form $form |
||
1494 | * @return HTTPResponse |
||
1495 | * @throws HTTPResponse_Exception |
||
1496 | */ |
||
1497 | public function save($data, $form) |
||
1564 | |||
1565 | /** |
||
1566 | * @uses LeftAndMainExtension->augmentNewSiteTreeItem() |
||
1567 | * |
||
1568 | * @param int|string $id |
||
1569 | * @param bool $setID |
||
1570 | * @return mixed|DataObject |
||
1571 | * @throws HTTPResponse_Exception |
||
1572 | */ |
||
1573 | public function getNewItem($id, $setID = true) |
||
1624 | |||
1625 | /** |
||
1626 | * Actually perform the publication step |
||
1627 | * |
||
1628 | * @param Versioned|DataObject $record |
||
1629 | * @return mixed |
||
1630 | */ |
||
1631 | public function performPublish($record) |
||
1639 | |||
1640 | /** |
||
1641 | * Reverts a page by publishing it to live. |
||
1642 | * Use {@link restorepage()} if you want to restore a page |
||
1643 | * which was deleted from draft without publishing. |
||
1644 | * |
||
1645 | * @uses SiteTree->doRevertToLive() |
||
1646 | * |
||
1647 | * @param array $data |
||
1648 | * @param Form $form |
||
1649 | * @return HTTPResponse |
||
1650 | * @throws HTTPResponse_Exception |
||
1651 | */ |
||
1652 | public function revert($data, $form) |
||
1692 | |||
1693 | /** |
||
1694 | * Delete the current page from draft stage. |
||
1695 | * |
||
1696 | * @see deletefromlive() |
||
1697 | * |
||
1698 | * @param array $data |
||
1699 | * @param Form $form |
||
1700 | * @return HTTPResponse |
||
1701 | * @throws HTTPResponse_Exception |
||
1702 | */ |
||
1703 | View Code Duplication | public function delete($data, $form) |
|
1725 | |||
1726 | /** |
||
1727 | * Delete this page from both live and stage |
||
1728 | * |
||
1729 | * @param array $data |
||
1730 | * @param Form $form |
||
1731 | * @return HTTPResponse |
||
1732 | * @throws HTTPResponse_Exception |
||
1733 | */ |
||
1734 | View Code Duplication | public function archive($data, $form) |
|
1757 | |||
1758 | public function publish($data, $form) |
||
1764 | |||
1765 | public function unpublish($data, $form) |
||
1787 | |||
1788 | /** |
||
1789 | * @return HTTPResponse |
||
1790 | */ |
||
1791 | public function rollback() |
||
1798 | |||
1799 | /** |
||
1800 | * Rolls a site back to a given version ID |
||
1801 | * |
||
1802 | * @param array $data |
||
1803 | * @param Form $form |
||
1804 | * @return HTTPResponse |
||
1805 | */ |
||
1806 | public function doRollback($data, $form) |
||
1846 | |||
1847 | /** |
||
1848 | * Batch Actions Handler |
||
1849 | */ |
||
1850 | public function batchactions() |
||
1854 | |||
1855 | public function BatchActionParameters() |
||
1877 | /** |
||
1878 | * Returns a list of batch actions |
||
1879 | */ |
||
1880 | public function BatchActionList() |
||
1884 | |||
1885 | public function publishall($request) |
||
1949 | |||
1950 | /** |
||
1951 | * Restore a completely deleted page from the SiteTree_versions table. |
||
1952 | * |
||
1953 | * @param array $data |
||
1954 | * @param Form $form |
||
1955 | * @return HTTPResponse |
||
1956 | */ |
||
1957 | public function restore($data, $form) |
||
1983 | |||
1984 | public function duplicate($request) |
||
2027 | |||
2028 | public function duplicatewithchildren($request) |
||
2065 | |||
2066 | public function providePermissions() |
||
2081 | |||
2082 | /** |
||
2083 | * Get title for root CMS node |
||
2084 | * |
||
2085 | * @return string |
||
2086 | */ |
||
2087 | protected function getCMSTreeTitle() |
||
2093 | } |
||
2094 |