diff --git a/code/CMSMain.php b/code/CMSMain.php index a0c93632..0cd0e77f 100644 --- a/code/CMSMain.php +++ b/code/CMSMain.php @@ -46,7 +46,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr 'publishall', 'publishitems', 'PublishItemsForm', - 'restorepage', + 'restore', 'revert', 'rollback', 'sidereport', @@ -56,6 +56,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr 'EditForm', 'AddPageOptionsForm', 'SiteTreeAsUL', + 'getshowdeletedsubtree' ); /** @@ -171,6 +172,21 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr return $this->getSiteTreeFor("SiteTree"); } + /** + * Get a subtree underneath the request param 'ID', of the tree that includes deleted pages. + * If ID = 0, then get the whole tree. + */ + public function getshowdeletedsubtree() { + // Get the tree + $tree = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID'], "AllHistoricalChildren"); + + // Trim off the outer tag + $tree = ereg_replace('^[ \t\r\n]*]*>','', $tree); + $tree = ereg_replace(']*>[ \t\r\n]*$','', $tree); + + return $tree; + } + /** * Returns the SiteTree columns that can be filtered using the the Site Tree Search button as a DataObjectSet */ @@ -372,6 +388,7 @@ JS; if($id && is_numeric($id)) { $record = DataObject::get_one( $treeClass, "\"$treeClass\".\"ID\" = $id"); + // Then, try getting a record from the live site if(!$record) { // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id"); Versioned::reading_stage('Live'); @@ -380,6 +397,17 @@ JS; $record = DataObject::get_one( $treeClass, "\"$treeClass\".\"ID\" = $id"); if($record) Versioned::reading_stage(null); } + + // Then, try getting a deleted record + if(!$record) { + $record = Versioned::get_latest_version($treeClass, $id); + } + + // Don't open a page from a different locale + if($record && Translatable::is_enabled() && $record->Locale && $record->Locale != Translatable::current_locale()) { + $record = null; + } + return $record; } else if(substr($id,0,3) == 'new') { @@ -1255,25 +1283,25 @@ JS; } /** - * Restore a previously deleted page. - * Internal action which shouldn't be executed through URL-handlers. + * Restore a completely deleted page from the SiteTree_versions table. */ - function restorepage() { - if($id = $this->urlParams['ID']) { + function restore() { + if(($id = $_REQUEST['ID']) && is_numeric($id)) { $restoredPage = Versioned::get_latest_version("SiteTree", $id); - $restoredPage->ID = $restoredPage->RecordID; + if($restoredPage) { + $restoredPage = $restoredPage->doRestoreToStage(); - // if no record can be found on draft stage (meaning it has been "deleted from draft" before), - // create an empty record - if(!DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $restoredPage->ID")->value()) { - DB::query("INSERT INTO \"SiteTree\" (\"ID\") VALUES ($restoredPage->ID)"); + FormResponse::get_page($id); + $title = Convert::raw2js($restoredPage->TreeTitle()); + FormResponse::add("$('sitetree').setNodeTitle($id, '$title');"); + FormResponse::status_message(sprintf(_t('CMSMain.RESTORED',"Restored '%s' successfully",PR_MEDIUM,'Param %s is a title'),$title),'good'); + return FormResponse::respond(); + + } else { + return new HTTPResponse("SiteTree #$id not found", 400); } - - $restoredPage->forceChange(); - $restoredPage->writeWithoutVersion(); - - } else { - echo _t('CMSMain.VISITRESTORE',"visit restorepage/(ID)",PR_LOW,'restorepage/(ID) should not be translated (is an URL)'); + } else { + return new HTTPResponse("Please pass an ID in the form content", 400); } } diff --git a/code/LeftAndMain.php b/code/LeftAndMain.php index 76bd3796..572b1e98 100644 --- a/code/LeftAndMain.php +++ b/code/LeftAndMain.php @@ -451,13 +451,24 @@ class LeftAndMain extends Controller { } public function getRecord($id, $className = null) { - if(!$className) $className = $this->stat('tree_class'); - return DataObject::get_by_id($className, $rootID); + if($id && is_numeric($id)) { + if(!$className) $className = $this->stat('tree_class'); + return DataObject::get_by_id($className, $id); + } } - function getSiteTreeFor($className, $rootID = null) { + /** + * Get a site tree displaying the nodes under the given objects + * @param $className The class of the root object + * @param $rootID The ID of the root object. If this is null then a complete tree will be + * shown + * @param $childrenMethod The method to call to get the children of the tree. For example, + * Children, AllChildrenIncludingDeleted, or AllHistoricalChildren + */ + function getSiteTreeFor($className, $rootID = null, + $childrenMethod = "AllChildrenIncludingDeleted") { $obj = $rootID ? $this->getRecord($rootID) : singleton($className); - $obj->markPartialTree(30, $this); + $obj->markPartialTree(30, $this, $childrenMethod); if($p = $this->currentPage()) $obj->markToExpose($p); // getChildrenAsUL is a flexible and complex way of traversing the tree @@ -467,7 +478,7 @@ class LeftAndMain extends Controller { ($child->TreeTitle()) . "" ' - ,$this, true); + ,$this, true, $childrenMethod); // Wrap the root if needs be. @@ -485,9 +496,19 @@ class LeftAndMain extends Controller { return $siteTree; } + /** + * Get a subtree underneath the request param 'ID'. + * If ID = 0, then get the whole tree. + */ public function getsubtree() { - $results = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID']); - return substr(trim($results), 4,-5); + // Get the tree + $tree = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID']); + + // Trim off the outer tag + $tree = ereg_replace('^[ \t\r\n]*]*>','', $tree); + $tree = ereg_replace(']*>[ \t\r\n]*$','', $tree); + + return $tree; } /** @@ -845,12 +866,7 @@ JS; } public function EditForm() { - if(isset($_REQUEST['ID']) && is_numeric($_REQUEST['ID'])) { - $record = DataObject::get_by_id($this->stat('tree_class'), $_REQUEST['ID']); - } else { - $record = $this->CurrentPage(); - } - + $record = $this->currentPage(); if(!$record) return false; if($record && !$record->canView()) return Security::permissionFailure($this); @@ -900,15 +916,7 @@ JS; } public function currentPage() { - $id = $this->currentPageID(); - if($id && is_numeric($id)) { - $page = DataObject::get_by_id($this->stat('tree_class'), $id); - if($page && Translatable::is_enabled() && $page->Locale && $page->Locale != Translatable::current_locale()) { - return false; - } else { - return $page; - } - } + return $this->getRecord($this->currentPageID()); } public function isCurrentPage(DataObject $page) { diff --git a/css/cms_left.css b/css/cms_left.css index 0242cfcc..7a6706d2 100644 --- a/css/cms_left.css +++ b/css/cms_left.css @@ -180,6 +180,12 @@ ul.tree span.a a del, ul.tree span.a a.notinmenu del, #publication_key del { color: red; } +/* Deleted on stage & live (when show deleted pages is active) */ +ul.tree span.a a del, ul.tree span.a a.notinmenu del.deletedOnLive, +#publication_key del.deletedOnLive { + color: #700; +} + ul.tree span.a span.modified, #publication_key span.modified { color: green; @@ -406,14 +412,14 @@ ul.tree span.untranslated a:visited { font-style: italic; } -#SortItems { +.checkboxAboveTree { border-top: 1px solid #cccccc; padding: 5px 0 0 0; overflow: hidden; clear: left; width: 100%; } - #SortItems input, #ShowChanged input { + .checkboxAboveTree input, #ShowChanged input { float: left; margin: 0 3px 0 0; } diff --git a/css/layout.css b/css/layout.css index 4df0e96b..dd176571 100644 --- a/css/layout.css +++ b/css/layout.css @@ -400,13 +400,13 @@ body.stillLoading select { width: auto; } #left #TreeActions, - #left #SortItems { + #left .checkboxAboveTree { background: #EEE; padding: 5px; float: left; width: 95%; } - #left #SortItems { + #left .checkboxAboveTree { border-bottom: 1px solid #CCC; } #TreeTools label { diff --git a/javascript/CMSMain_left.js b/javascript/CMSMain_left.js index 13d95998..d5465eff 100755 --- a/javascript/CMSMain_left.js +++ b/javascript/CMSMain_left.js @@ -118,6 +118,43 @@ searchclass.prototype = { } } +/** + * Show deleted pages checkbox + */ +ShowDeletedPagesAction = Class.create(); +ShowDeletedPagesAction.applyTo('#showdeletedpages'); +ShowDeletedPagesAction.prototype = { + initialize: function () { + }, + + onclick : function() { + if(this.checked) { + SiteTreeHandlers.loadTree_url = SiteTreeHandlers.controller_url + '/getshowdeletedsubtree'; + } else { + SiteTreeHandlers.loadTree_url = SiteTreeHandlers.controller_url + '/getsubtree'; + } + + // We can't update the tree while it's draggable; it gets b0rked. + var __makeDraggableAfterUpdate = false; + if($('sitetree').isDraggable) { + $('sitetree').stopBeingDraggable(); + __makeDraggableAfterUpdate = true; + } + + var request = new Ajax.Request(SiteTreeHandlers.loadTree_url + '?ID=0&ajax=1', { + onSuccess: function(response) { + $('sitetree').innerHTML = response.responseText; + SiteTree.applyTo($('sitetree')); + if(__makeDraggableAfterUpdate) $('sitetree').makeDraggable(); + }, + onFailure: function(response) { + errorMessage('Could not update tree', response); + } + }); + + } +} + /** * Add Criteria Drop-down onchange action which allows more criteria to be shown */ diff --git a/javascript/LeftAndMain.js b/javascript/LeftAndMain.js index d9995b7e..d2e54c6d 100644 --- a/javascript/LeftAndMain.js +++ b/javascript/LeftAndMain.js @@ -182,7 +182,7 @@ window.onresize = function(init) { if(navigator.appName == "Microsoft Internet Explorer") { fitToParent('Image'); } else { - fitToParent('Image', 210); + fitToParent('Image', 250); } } if($('Form_EditorToolbarFlashForm') && $('Form_EditorToolbarFlashForm').style.display == "block") { diff --git a/javascript/tinymce.template.js b/javascript/tinymce.template.js index 16077682..9645478e 100755 --- a/javascript/tinymce.template.js +++ b/javascript/tinymce.template.js @@ -43,7 +43,7 @@ if((typeof tinyMCE != 'undefined')) { verify_html : true, use_native_selects : true, // fancy selects are bug as of SS 2.3.0 valid_elements : "+a[id|rel|rev|dir|tabindex|accesskey|type|name|href|target|title|class],-strong/-b[class],-em/-i[class],-strike[class],-u[class],#p[id|dir|class|align],-ol[class],-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align],-sub[class],-sup[class],-blockquote[dir|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|dir|id|style],-tr[id|dir|class|rowspan|width|height|align|valign|bgcolor|background|bordercolor|style],tbody[id|class|style],thead[id|class|style],tfoot[id|class|style],-td[id|dir|class|colspan|rowspan|width|height|align|valign|scope|style],-th[id|dir|class|colspan|rowspan|width|height|align|valign|scope|style],caption[id|dir|class],-div[id|dir|class|align|style],-span[class|align],-pre[class|align],address[class|align],-h1[id|dir|class|align],-h2[id|dir|class|align],-h3[id|dir|class|align],-h4[id|dir|class|align],-h5[id|dir|class|align],-h6[id|dir|class|align],hr[class],dd[id|class|title|dir],dl[id|class|title|dir],dt[id|class|title|dir]", - extended_valid_elements : "img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling]" + extended_valid_elements : "img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling],object[width|height|data|type],param[name|value]" }); } diff --git a/templates/Includes/AssetAdmin_left.ss b/templates/Includes/AssetAdmin_left.ss index 4222579b..edbf8106 100755 --- a/templates/Includes/AssetAdmin_left.ss +++ b/templates/Includes/AssetAdmin_left.ss @@ -17,7 +17,7 @@ -
+
diff --git a/templates/Includes/CMSMain_left.ss b/templates/Includes/CMSMain_left.ss index d7f591a0..34bdd71d 100755 --- a/templates/Includes/CMSMain_left.ss +++ b/templates/Includes/CMSMain_left.ss @@ -61,9 +61,15 @@ $PublishItemsForm
-
- +
+
+ +
+
+ +
+ <% if IsTranslatableEnabled %>
Language: $LangSelector diff --git a/templates/Includes/SecurityAdmin_left.ss b/templates/Includes/SecurityAdmin_left.ss index 7bad2d6d..d8e38d3e 100644 --- a/templates/Includes/SecurityAdmin_left.ss +++ b/templates/Includes/SecurityAdmin_left.ss @@ -22,7 +22,7 @@

<% _t('TOREORG','To reorganise your site, drag the pages around as desired.') %>

-
+
diff --git a/tests/CMSMainTest.php b/tests/CMSMainTest.php index 98950311..918e09fe 100644 --- a/tests/CMSMainTest.php +++ b/tests/CMSMainTest.php @@ -4,7 +4,6 @@ * @subpackage tests */ class CMSMainTest extends FunctionalTest { - static $fixture_file = 'cms/tests/CMSMainTest.yml'; protected $autoFollowRedirection = false; @@ -88,4 +87,48 @@ class CMSMainTest extends FunctionalTest { $this->assertTrue($page->getCMSFields(null) instanceof FieldSet); } } -} \ No newline at end of file + + /** + * Test that a draft-deleted page can still be opened in the CMS + */ + function testDraftDeletedPageCanBeOpenedInCMS() { + // Set up a page that is delete from live + $page = $this->objFromFixture('Page','page1'); + $pageID = $page->ID; + $page->doPublish(); + $page->delete(); + + $this->session()->inst_set('loggedInAs', $this->idFromFixture('Member', 'admin')); + $response = $this->get('admin/getitem?ID=' . $pageID . '&ajax=1'); + + // Check that the 'delete from live' button exists as a simple way of checking that the correct page is returned. + $this->assertRegExp('/]+type="submit"[^>]+name="action_deletefromlive"/i', $response->getBody()); + } + + /** + * Test CMSMain::getRecord() + */ + function testGetRecord() { + // Set up a page that is delete from live + $page1 = $this->objFromFixture('Page','page1'); + $page1ID = $page1->ID; + $page1->doPublish(); + $page1->delete(); + + $cmsMain = new CMSMain(); + + // Bad calls + $this->assertNull($cmsMain->getRecord('0')); + $this->assertNull($cmsMain->getRecord('asdf')); + + // Pages that are on draft and aren't on draft should both work + $this->assertType('Page', $cmsMain->getRecord($page1ID)); + $this->assertType('Page', $cmsMain->getRecord($this->idFromFixture('Page','page2'))); + + // This functionality isn't actually used any more. + $newPage = $cmsMain->getRecord('new-Page-5'); + $this->assertType('Page', $newPage); + $this->assertEquals('5', $newPage->ParentID); + + } +}