From b360ad9cd79c9dbc921b90bab57f4ed3baff5e74 Mon Sep 17 00:00:00 2001
From: Sean Harvey <sean@silverstripe.com>
Date: Thu, 14 May 2009 06:11:18 +0000
Subject: [PATCH] Merged from branches/2.3

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@76857 467b73ca-7a2a-4603-9d3b-597d59a354a9
---
 _config.php                        |  33 +++++
 code/CMSMain.php                   | 223 +++++------------------------
 code/LeftAndMain.php               |  15 +-
 css/cms_left.css                   |   7 +-
 css/layout.css                     |  23 +--
 javascript/CMSMain_left.js         | 173 ++++++++++++++++------
 javascript/LeftAndMain.js          |  26 +++-
 javascript/LeftAndMain_left.js     |  79 +++++-----
 javascript/WidgetAreaEditor.js     |   6 +-
 javascript/tinymce.template.js     |  63 --------
 lang/en_US.php                     |   3 +-
 templates/Includes/CMSMain_left.ss |  71 ++++++---
 tests/CMSMainTest.php              |   3 +-
 13 files changed, 351 insertions(+), 374 deletions(-)
 delete mode 100755 javascript/tinymce.template.js

diff --git a/_config.php b/_config.php
index 5d06b200..1d2d5bc9 100644
--- a/_config.php
+++ b/_config.php
@@ -17,4 +17,37 @@ Director::addRules(50, array(
 ));
 
 CMSMenu::populate_menu();
+
+HtmlEditorConfig::get('cms')->setOptions(array(
+	'mode' => 'none',
+	'language' => i18n::get_tinymce_lang(),
+	'content_css' => 'cms/css/editor.css, '.(SSViewer::current_theme() ? THEMES_DIR . "/" . SSViewer::current_theme() : project()) . "/css/editor.css",
+
+	'body_class' => 'typography',
+	'document_base_url' => Director::absoluteBaseURL(),
+
+	'urlconverter_callback' => "nullConverter",
+	'setupcontent_callback' => "sapphiremce_setupcontent",
+	'cleanup_callback' => "sapphiremce_cleanup",
+
+	'template_templates' => array(
+    	array( 'title' => "Three column", 'src' => "assets/snippet.html", 'description' => "A simple 3 column layout" )
+	),
+
+	'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|usemap],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling],object[width|height|data|type],param[name|value],map[class|name|id],area[shape|coords|href|target|alt]"
+));
+			
+
+HtmlEditorConfig::get('cms')->disablePlugins('blockquote');
+HtmlEditorConfig::get('cms')->enablePlugins('media', '../../tinymce_ssbuttons', 'fullscreen');
+			
+HtmlEditorConfig::get('cms')->insertButtonsBefore('formatselect', 'styleselect');
+HtmlEditorConfig::get('cms')->insertButtonsBefore('advcode', 'ssimage', 'ssflash', 'sslink', 'unlink', 'anchor', 'separator' );
+HtmlEditorConfig::get('cms')->insertButtonsAfter ('advcode', 'fullscreen', 'separator');
+			
+HtmlEditorConfig::get('cms')->removeButtons('tablecontrols');
+HtmlEditorConfig::get('cms')->addButtonsToLine(3, 'tablecontrols');
+
 ?>
diff --git a/code/CMSMain.php b/code/CMSMain.php
index bd00be28..ff36da37 100644
--- a/code/CMSMain.php
+++ b/code/CMSMain.php
@@ -40,7 +40,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 		'dialog',
 		'duplicate',
 		'duplicatewithchildren',
-		'filtersitetree',
 		'getpagecount',
 		'getversion',
 		'publishall',
@@ -56,7 +55,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 		'EditForm',
 		'AddPageOptionsForm',
 		'SiteTreeAsUL',
-		'getshowdeletedsubtree'
+		'getshowdeletedsubtree',
+		'getfilteredsubtree',
+		'batchactions'
 	);
 	
 	/**
@@ -75,13 +76,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 		return array(
 			'Title' => _t('CMSMain.TITLEOPT', 'Title', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
 			'MenuTitle' => _t('CMSMain.MENUTITLEOPT', 'Navigation Label', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
-			'ClassName' => _t('CMSMain.PAGETYPEOPT', 'Page Type', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions"), 
 			'Status' => _t('CMSMain.STATUSOPT', 'Status',  0, "The dropdown title in CMSMain left SiteTreeFilterOptions"), 
 			'MetaDescription' => _t('CMSMain.METADESCOPT', 'Description', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions"), 
 			'MetaKeywords' => _t('CMSMain.METAKEYWORDSOPT', 'Keywords', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions")
 		);
 	}
-
+	
 	public function init() {
 		parent::init();
 		
@@ -110,18 +110,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 		$spellcheckSpec = array();
 		foreach($spellcheckLangs as $lang => $title) $spellcheckSpec[] = "{$title}={$lang}";
 
+		HtmlEditorConfig::get('cms')->setOption('spellchecker_languages', '+' . implode(',', $spellcheckSpec));
+		
+		// @todo Do we need this - I'm pretty sure not, since HtmlEditorField#Field() will include it on being called.
+		//       The only time you might need it is if you are creating an textarea.htmlfield yourself, in which case bad things are going to happen now we've moved configuration
 		// We don't want this showing up in every ajax-response, it should always be present in a CMS-environment
 		if(!Director::is_ajax()) {
 			Requirements::javascript(MCE_ROOT . "tiny_mce_src.js");
-			Requirements::javascriptTemplate(CMS_DIR . "/javascript/tinymce.template.js", array(
-				"ContentCSS" => (SSViewer::current_theme() ? THEMES_DIR . "/" . SSViewer::current_theme() : project()) . "/css/editor.css",
-				"BaseURL" => Director::absoluteBaseURL(),
-				"Lang" => i18n::get_tinymce_lang(),
-				'SpellcheckLangs' => '+' . implode(',', $spellcheckSpec)
-			));
 		}
-		// Always block the HtmlEditorField.js otherwise it will be sent with an ajax request
-		Requirements::block(SAPPHIRE_DIR . '/javascript/HtmlEditorField.js');
 		
 		Requirements::javascript(CMS_DIR . '/javascript/CMSMain.js');
 		Requirements::javascript(CMS_DIR . '/javascript/CMSMain_left.js');
@@ -186,7 +182,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 
 		return $tree;
 	}
+	
+	public function getfilteredsubtree() {
+		// Get the tree
+		$tree = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID'], null, 'cmsMainMarkingFilterFunction');
 
+		// Trim off the outer tag
+		$tree = ereg_replace('^[ \t\r\n]*<ul[^>]*>','', $tree);
+		$tree = ereg_replace('</ul[^>]*>[ \t\r\n]*$','', $tree);
+		
+		return $tree;
+	}
+	
 	/**
 	 * Returns the SiteTree columns that can be filtered using the the Site Tree Search button as a DataObjectSet
 	 */
@@ -205,44 +212,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
 			$dateField = new CalendarDateField('SiteTreeFilterDate');
 			return $dateField->Field();
 		}
-
-	/**
-	 * Returns a filtered Site Tree
-	 */
-	public function filtersitetree() {
-		// Pre-cache sitetree version numbers for querying efficiency
-		Versioned::prepopulate_versionnumber_cache("SiteTree", "Stage");
-		Versioned::prepopulate_versionnumber_cache("SiteTree", "Live");
-		
-		$className = 'SiteTree';
-		$rootID = null;
-		$obj = $rootID ? $this->getRecord($rootID) : singleton($className);
-		$obj->setMarkingFilterFunction('cmsMainMarkingFilterFunction');
-		$obj->markPartialTree();
-
-		if($p = $this->currentPage()) $obj->markToExpose($p);
-
-		// getChildrenAsUL is a flexible and complex way of traversing the tree
-		$siteTree = $obj->getChildrenAsUL("", '
-					"<li id=\"record-$child->ID\" class=\"" . $child->CMSTreeClasses($extraArg) . "\">" .
-					"<a href=\"" . Director::link(substr($extraArg->Link(),0,-1), "show", $child->ID) . "\" " . (($child->canEdit() || $child->canAddChildren()) ? "" : "class=\"disabled\"") . " title=\"' . _t('LeftAndMain.PAGETYPE') . '".$child->class."\" >" .
-					($child->TreeTitle()) .
-					"</a>"
-'
-					,$this, true);
-
-		// Wrap the root if needs be.
-
-		if(!$rootID) {
-			$rootLink = $this->Link() . '0';
-			$siteTree = "<ul id=\"sitetree\" class=\"tree unformatted\"><li id=\"record-0\" class=\"Root nodelete\"><a href=\"$rootLink\">" .
-				 _t('LeftAndMain.TREESITECONTENT',"Site Content",PR_HIGH,'Root node on left') . "</a>"
-				. $siteTree . "</li></ul>";
-		}
-
-		return $siteTree;
-
-	}
+		public function SiteTreeFilterPageTypeField() {
+			$types = SiteTree::page_type_classes(); array_unshift($types, 'All');
+			$optionsetField = new DropdownField('ClassName', 'ClassName', array_combine($types, $types), 'Any');
+			return $optionsetField->Field();
+		}	
 
 	public function generateDataTreeHints() {
 		$classes = ClassInfo::subclassesFor( $this->stat('tree_class') );
@@ -975,54 +949,12 @@ JS;
 			'DialogType' => 'alert'
 		))->renderWith('Dialog');
 	}
-
+	
 	/**
-	 * Publishes a number of items.
-	 * Called by AJAX
+	 * Batch Actions Handler
 	 */
-	public function publishitems() {
-		// This method can't be called without ajax.
-		if(!Director::is_ajax()) {
-			Director::redirectBack();
-			return;
-		}
-
-		$ids = split(' *, *', $this->requestParams['csvIDs']);
-
-		$notifications = array();
-
-		$idList = array();
-
-		// make sure all the ids are numeric.
-		// Add all the children to the list of IDs if they are missing
-		foreach($ids as $id) {
-			$brokenPageList = '';
-			if(is_numeric($id)) {
-				$record = DataObject::get_by_id($this->stat('tree_class'), $id);
-				
-				if($record) {
-					if($record && !$record->canPublish()) return Security::permissionFailure($this);
-					
-					// Publish this page
-					$record->doPublish();
-
-					// Now make sure the 'changed' icon is removed
-					$publishedRecord = DataObject::get_by_id($this->stat('tree_class'), $id);
-					$JS_title = Convert::raw2js($publishedRecord->TreeTitle());
-					FormResponse::add("\$('sitetree').setNodeTitle($id, '$JS_title');");
-					FormResponse::add("$('Form_EditForm').reloadIfSetTo($record->ID);");
-					$record->destroy();
-					unset($record);
-				}
-			}
-		}
-
-		if (sizeof($ids) > 1) $message = sprintf(_t('CMSMain.PAGESPUB', "%d pages published "), sizeof($ids));
-		else $message = sprintf(_t('CMSMain.PAGEPUB', "%d page published "), sizeof($ids));
-
-		FormResponse::add('statusMessage("'.$message.'","good");');
-
-		return FormResponse::respond();
+	function batchactions() {
+		return new CMSBatchActionHandler($this, 'batchactions');
 	}
 	
 	/**
@@ -1045,97 +977,10 @@ JS;
 	}
 
 	/**
-	 * Delete a number of items.
-	 * This code supports notification
+	 * Returns a list of batch actions
 	 */
-	public function deleteitems() {
-		// This method can't be called without ajax.
-		if(!Director::is_ajax()) {
-			Director::redirectBack();
-			return;
-		}
-
-		$ids = split(' *, *', $_REQUEST['csvIDs']);
-
-		$notifications = array();
-
-		$idList = array();
-
-		// make sure all the ids are numeric.
-		// Add all the children to the list of IDs if they are missing
-		foreach($ids as $id) {
-			$brokenPageList = '';
-			if(is_numeric($id)) {
-				$record = DataObject::get_by_id($this->stat('tree_class'), $id);
-
-				if($record) {
-					if($record && !$record->canDelete()) return Security::permissionFailure($this);	
-					
-					// add all the children for this record if they are not already in the list
-					// this check is a little slower but will prevent circular dependencies
-					// (should they exist, which they probably shouldn't) from causing
-					// the function to not terminate
-					$children = $record->AllChildren();
-
-					if( $children )
-						foreach( $children as $child )
-							if( array_search( $child->ID, $ids ) !== FALSE )
-								$ids[] = $child->ID;
-
-					if($record->hasMethod('BackLinkTracking')) {
-						$brokenPages = $record->BackLinkTracking();
-						foreach($brokenPages as $brokenPage) {
-							$brokenPageList .= "<li style=\"font-size: 65%\">" . $brokenPage->Breadcrumbs(3, true) . "</li>";
-							$brokenPage->HasBrokenLink = true;
-							$notifications[$brokenPage->OwnerID][] = $brokenPage;
-							$brokenPage->writeWithoutVersion();
-						}
-					}
-
-					$oldID = $record->ID;
-					$record->delete();
-					$record->destroy();
-
-					// DataObject::delete_by_id($this->stat('tree_class'), $id);
-
-					// check to see if the record exists on the live site, if it doesn't remove the tree node
-					$liveRecord = Versioned::get_one_by_stage( $this->stat('tree_class'), 'Live', "\"{$this->stat('tree_class')}\".\"ID\"={$id}");
-
-					if($liveRecord) {
-						$liveRecord->IsDeletedFromStage = true;
-						$title = Convert::raw2js($liveRecord->TreeTitle());
-						FormResponse::add("$('sitetree').setNodeTitle($oldID, '$title');");
-						FormResponse::add("$('Form_EditForm').reloadIfSetTo($oldID);");
-					} else {
-						FormResponse::add("var node = $('sitetree').getTreeNodeByIdx('$id');");
-						FormResponse::add("if(node && node.parentTreeNode)	node.parentTreeNode.removeTreeNode(node);");
-						FormResponse::add("$('Form_EditForm').reloadIfSetTo($oldID);");
-					}
-				}
-			}
-		}
-
-		if($notifications) foreach($notifications as $memberID => $pages) {
-			if(class_exists('Page_BrokenLinkEmail')) {
-				$email = new Page_BrokenLinkEmail();
-				$email->populateTemplate(new ArrayData(array(
-					"Recipient" => DataObject::get_by_id("Member", $memberID),
-					"BrokenPages" => new DataObjectSet($pages),
-				)));
-				$email->debug();
-				$email->send();
-			}
-		}
-
-		if (sizeof($ids) > 1) $message = sprintf(_t('CMSMain.PAGESDEL', "%d pages deleted "), sizeof($ids));
-		else $message = sprintf(_t('CMSMain.PAGEDEL', "%d page deleted "), sizeof($ids));
-		if(isset($brokenPageList) && $brokenPageList != '') {
-			$message .= _t('CMSMain.NOWBROKEN',"  The following pages now have broken links:")."<ul>" . addslashes($brokenPageList) . "</ul>" . _t('CMSMain.NOWBROKEN2',"Their owners have been emailed and they will fix up those pages.");
-		}
-
-		FormResponse::add('statusMessage("'.$message.'","good");');
-
-		return FormResponse::respond();
+	function BatchActionList() {
+		return $this->batchactions()->batchActionList();
 	}
 	
 	/**
@@ -1435,6 +1280,7 @@ $filterCache = array();
 
 // TODO: Find way to put this in a class
 function cmsMainMarkingFilterFunction($node) {
+	global $filterCache;
 	// Expand all nodes
 	// $node->markingFinished();
 
@@ -1458,6 +1304,11 @@ function cmsMainMarkingFilterFunction($node) {
 			$failed_filter = true;
 		}
 	}
+	// Check the ClassName
+	if (!empty($_REQUEST['ClassName']) && $_REQUEST['ClassName'] != 'Any') {
+		if ($node->ClassName != $_REQUEST['ClassName']) $failed_filter = true;
+	}
+	
 	// Now check if a specified Criteria attribute matches
 	foreach (CMSMain::T_SiteTreeFilterOptions() as $key => $value)
 	{
diff --git a/code/LeftAndMain.php b/code/LeftAndMain.php
index 07c8ca8e..c2acea8b 100644
--- a/code/LeftAndMain.php
+++ b/code/LeftAndMain.php
@@ -153,6 +153,9 @@ class LeftAndMain extends Controller {
 
 		// Audit logging hook
 		if(empty($_REQUEST['executeForm']) && !Director::is_ajax()) $this->extend('accessedCMS');
+
+		// Override HtmlEditorField's config with our own
+		HtmlEditorconfig::set_active('cms');
 		
 		Requirements::css(CMS_DIR . '/css/typography.css');
 		Requirements::css(CMS_DIR . '/css/layout.css');
@@ -465,10 +468,18 @@ class LeftAndMain extends Controller {
 	 * @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") {
+	function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $filterFunction = null) {
+		// Default childrenMethod
+		if (!$childrenMethod) $childrenMethod = 'AllChildrenIncludingDeleted';
+		
+		// Get the tree root
 		$obj = $rootID ? $this->getRecord($rootID) : singleton($className);
+		
+		// Mark the nodes of the tree to return
+		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
 		$obj->markPartialTree(30, $this, $childrenMethod);
+		
+		// Ensure current page is exposed
 		if($p = $this->currentPage()) $obj->markToExpose($p);
 
 		// getChildrenAsUL is a flexible and complex way of traversing the tree
diff --git a/css/cms_left.css b/css/cms_left.css
index 6d90ea38..73155920 100644
--- a/css/cms_left.css
+++ b/css/cms_left.css
@@ -374,14 +374,15 @@ ul.tree span.untranslated a:visited {
 }
 
 #left form.actionparams div.SearchCriteria {
-	width: 40%;
+	width: 28%;
 	overflow: hidden;
 	float: left;
 }
 
-#left form.actionparams input.SearchCriteria {
-	width: 43%;
+#left form.actionparams input.SearchCriteria, #left form.actionparams #InputSiteTreeFilterClassName select {
+	width: 60%;
 	float: left;
+	margin: 0;
 }
 #left form.actionparams #InputSiteTreeFilterDate .calendar {
 	margin-left: -96px;
diff --git a/css/layout.css b/css/layout.css
index 0b2bd9b4..f677ce23 100644
--- a/css/layout.css
+++ b/css/layout.css
@@ -357,17 +357,25 @@ body.stillLoading select {
 		padding-left: 5px;
 		position: relative;
 	}
-		#SearchBox #SiteTreeSearchTerm {
-			padding: 1px 0 2px 0;
+	#SearchControls {
+		float: left;
+		position: relative;
+		margin-top:2px;
+	}
+		#SearchControls label {
+			display: none;
+		}
+		#SearchControls select#SiteTreeFilterAddCriteria {
+			width: 8.8em;
+			padding:1px 0; margin:0;
 		}
-	
 	#searchIndicator {
 		display: none;
 		width: 16px;
 		height: 16px;
 		background: #EFEFEF url(../images/network-save.gif) no-repeat;
 		position: absolute;
-		left: 145px;
+		left: 95px;
 		top: 2px;
 	}
 		#searchIndicator.loading {
@@ -379,13 +387,6 @@ body.stillLoading select {
 		padding-left: 5px;
 		margin-bottom: 4px;
 	}
-	#addCriteria {
-		float: left;
-		width: 100%;
-	}
-		#addCriteria label {
-			display: none;
-		}
 	#sitetree_ul, ul#sitetree {
 		padding: 3px 0 0 3px;
 		clear: left;
diff --git a/javascript/CMSMain_left.js b/javascript/CMSMain_left.js
index c47b2d4f..cc588e80 100755
--- a/javascript/CMSMain_left.js
+++ b/javascript/CMSMain_left.js
@@ -129,9 +129,9 @@ ShowDeletedPagesAction.prototype = {
 	
 	onclick : function() {
 		if(this.checked) { 
-			SiteTreeHandlers.loadTree_url = SiteTreeHandlers.controller_url + '/getshowdeletedsubtree';
+			$('sitetree').setCustomURL(SiteTreeHandlers.controller_url+'/getshowdeletedsubtree');
 		} else {
-			SiteTreeHandlers.loadTree_url = SiteTreeHandlers.controller_url + '/getsubtree';
+			$('sitetree').clearCustomURL();
 		}
 
 		// We can't update the tree while it's draggable; it gets b0rked.
@@ -144,13 +144,8 @@ ShowDeletedPagesAction.prototype = {
 		var indicator = $('checkboxActionIndicator');
 		indicator.style.display = 'block';
 		
-		var url = SiteTreeHandlers.loadTree_url + '?ID=0&ajax=1';
-		if($('LangSelector')) url += "&locale=" + $('LangSelector').value;
-		
-		var request = new Ajax.Request(url, {
-			onSuccess: function(response) {
-				$('sitetree').innerHTML = response.responseText;
-				SiteTree.applyTo($('sitetree'));
+		$('sitetree').reload({
+			onSuccess: function() {
 				if(__makeDraggableAfterUpdate) $('sitetree').makeDraggable();
 				indicator.style.display = 'none';
 			},
@@ -161,6 +156,107 @@ ShowDeletedPagesAction.prototype = {
 	}
 }
 
+/**
+ * Show only drafts checkbox click action
+ */
+showonlydrafts = Class.create();
+showonlydrafts.applyTo('#publishpage_show_drafts');
+showonlydrafts.prototype = {
+	onclick : function() {
+		if(this.checked) { 
+			$('sitetree').setCustomURL(SiteTreeHandlers.controller_url+'/getfilteredsubtree', {Status:'Saved'});
+		} else {
+			$('sitetree').clearCustomURL();
+		}
+		
+		$('sitetree').reload({
+			onSuccess: function() {
+				statusMessage(ss.i18n._t('CMSMAIN.FILTEREDTREE'),'good');
+			},
+			onFailure: function(response) {
+				errorMessage(ss.i18n.sprintf(
+					ss.i18n._t('CMSMAIN.ERRORFILTERPAGES'),
+					response.responseText
+				));
+			}
+		});
+	}
+}
+
+/**
+ * Control the site tree filter
+ */
+SiteTreeFilterForm = Class.create();
+SiteTreeFilterForm.applyTo('form#search_options');
+SiteTreeFilterForm.prototype = {
+	initialize: function() {
+		var self = this;
+		Form.getElements(this).each(function(el){
+			if (el.type == 'submit') el.onclick = function(){self.clicked = $F(this); console.log(self.clicked)};
+		});
+	},
+	onsubmit: function() {
+		var filters = $H();
+		
+		if (this.clicked == 'Search') {
+			Form.getElements(this).each(function(el){
+				if (el.type == 'text') {
+					if ($F(el)) filters[el.name] = $F(el);
+				}
+				else if (el.type == 'select-one') {
+					if ($F(el) && $F(el) != 'All') filters[el.name] = $F(el);
+				}
+			});
+		}
+		else {
+			Form.getElements(this).each(function(el){
+				if (el.type == 'text') $(el).clear();
+				else if (el.type == 'select-one') el.value = 'All';
+			});
+			document.getElementsBySelector('.SearchCriteriaContainer', this).each(function(el){
+				Element.hide(el);
+			})
+		}
+		
+		if (filters.keys().length) {
+			// Set new URL
+			$('sitetree').setCustomURL(SiteTreeHandlers.controller_url + '/getfilteredsubtree', filters);
+			
+			// Disable checkbox tree controls that currently don't work with search.
+			// @todo: Make them work together
+			if ($('sitetree').isDraggable) $('sitetree').stopBeingDraggable();
+			document.getElementsBySelector('.checkboxAboveTree input[type=checkbox]').each(function(el){
+				el.value = false; el.disabled = true;	
+			})
+		}
+		else {
+			// Reset URL to default
+			$('sitetree').clearCustomURL();
+			
+			// Enable checkbox tree controls
+			document.getElementsBySelector('.checkboxAboveTree input[type=checkbox]').each(function(el){
+				el.disabled = false;	
+			})
+		}
+		
+		$('SiteTreeSearchButton').className = $('SiteTreeSearchClearButton').className = 'hidden';
+		$('searchIndicator').className = 'loading';
+		
+		$('sitetree').reload({
+			onSuccess :  function(response) {
+				$('SiteTreeSearchButton').className = $('SiteTreeSearchClearButton').className = 'action';
+				$('searchIndicator').className = '';
+				statusMessage('Filtered tree','good');
+			},
+			onFailure : function(response) {
+				errorMessage('Could not filter site tree<br />' + response.responseText);
+			}
+		});
+		
+		return false;
+	}
+}
+
 /**
  * Add Criteria Drop-down onchange action which allows more criteria to be shown
  */
@@ -210,37 +306,6 @@ batchactionsclass.prototype = {
 	}
 }
 
-/**
- * Show only drafts checkbox click action
- */
-showonlydrafts = Class.create();
-showonlydrafts.applyTo('#publishpage_show_drafts');
-showonlydrafts.prototype = {
-	onclick : function() {
-			if (0 == $('SiteTreeIsFiltered').value) {
-			// Show all items in Site Tree again
-			new Ajax.Request( 'admin/filterSiteTree?Status=Saved&ajax=1', {
-				onSuccess: function( response ) {
-					$('sitetree_ul').innerHTML = response.responseText;
-					Behaviour.apply($('sitetree_ul'));
-					$('SiteTreeIsFiltered').value = 1;
-					$('batchactions').multiselectTransform();
-					statusMessage(ss.i18n._t('CMSMAIN.FILTEREDTREE'),'good');
-				},
-				onFailure : function(response) {
-					errorMessage(ss.i18n.sprintf(
-						ss.i18n._t('CMSMAIN.ERRORFILTERPAGES'),
-						response.responseText
-					));
-				}
-			});
-			} else {
-				batchActionGlobals.unfilterSiteTree();
-			}
-	}
-}
-
-
 // batchActionGlobals is needed because calls to observeMethod doesn't seem to preserve instance variables so a Prototype can't be used
 batchActionGlobals = {
 	selectedNodes: { },
@@ -337,25 +402,39 @@ batchActionGlobals = {
  * Publish selected pages action
  */
 publishpage = Class.create();
-publishpage.applyTo('#Form_PublishItemsForm');
+publishpage.applyTo('#batchactions_options');
 publishpage.prototype = {
 	onsubmit : function() {
 		csvIDs = batchActionGlobals.getCsvIds();
 		if(csvIDs) {		
+			var optionEl = $('choose_batch_action').options[$('choose_batch_action').selectedIndex];
+			var actionText = optionEl.text;
+			var optionParams = eval(optionEl.className);
+			var ingText = optionParams.doingText;
+
+			// Confirmation
+			if(!confirm("You have " + batchActionGlobals.count + " pages selected.\n\nDo your really want to " + actionText.toLowerCase() + "?")) {
+				return false;
+			}
+
 			this.elements.csvIDs.value = csvIDs;
+
+			// Select form submission URL
+			this.action = $('choose_batch_action').value;
 			
-			statusMessage(ss.i18n._t('CMSMAIN.PUBLISHINGPAGES'));
+			// Loading indicator
+			statusMessage(ingText);
+			$('batchactions_go').className = 'loading';
 			
-			// Put an AJAXY loading icon on the button
-			$('Form_PublishItemsForm_action_publishitems').className = 'loading';
+			// Submit form
 			Ajax.SubmitForm(this, null, {
 				onSuccess :  function(response) {
 					Ajax.Evaluator(response);
-					$('Form_PublishItemsForm_action_publishitems').className = '';
+					$('batchactions_go').className = '';
 					treeactions.closeSelection($('batchactions'));
 				},
 				onFailure : function(response) {
-					errorMessage(ss.i18n._t('CMSMAIN.ERRORPUBLISHING'), response);
+					errorMessage('Error ' + ingText, response);
 				}
 			});
 		} else {
@@ -450,4 +529,4 @@ TreeContextMenu = {
 		
 		treeNode.onOrderChanged(sortedChildren,treeNode);
 	}
-};
\ No newline at end of file
+};
diff --git a/javascript/LeftAndMain.js b/javascript/LeftAndMain.js
index 407ffb10..57d90a09 100644
--- a/javascript/LeftAndMain.js
+++ b/javascript/LeftAndMain.js
@@ -886,4 +886,28 @@ function hideIndicator(id) {
 
 setInterval(function() {
     new Ajax.Request("Security/ping");
-}, 5*60*1000);
\ No newline at end of file
+}, 5*60*1000);
+
+/**
+ * Find and enable TinyMCE on all htmleditor fields
+ * Pulled in from old tinymce.template.js
+ */
+
+function nullConverter(url) {
+	return url;
+}
+
+Behaviour.register({
+    'textarea.htmleditor' : {
+        initialize : function() {
+            tinyMCE.execCommand("mceAddControl", true, this.id);
+            this.isChanged = function() {
+                return tinyMCE.getInstanceById(this.id).isDirty();
+            }
+            this.resetChanged = function() {
+                inst = tinyMCE.getInstanceById(this.id);
+                inst.startContent = tinymce.trim(inst.getContent({format : 'raw', no_events : 1}));
+            }
+        }
+    }
+})
diff --git a/javascript/LeftAndMain_left.js b/javascript/LeftAndMain_left.js
index e999f5ad..ff2c9ca8 100755
--- a/javascript/LeftAndMain_left.js
+++ b/javascript/LeftAndMain_left.js
@@ -27,6 +27,48 @@ var TreeContextMenu = null;
  */
 TreeAPI = Class.create();
 TreeAPI.prototype = {
+
+	setCustomURL: function(url, arguments) {
+		this.customURL = url;
+		this.customArguments = $H(arguments);
+	},
+	
+	clearCustomURL: function() {
+		this.customURL = this.customArguments = null;
+	},
+	
+	url: function(args) {
+		var args = $H(args).merge(this.customArguments);
+
+		var url = this.customURL ? this.customURL : SiteTreeHandlers.loadTree_url; 
+		url = url + (url.match(/\?/) ? '&' : '?') + args.toQueryString();
+
+		console.log('Loading tree from ' + url);
+		return url;
+	},
+	
+	reload: function(options) {
+		this.innerHTML = 'Loading...';
+
+		var args = {ajax:1, ID:0};
+		if ($('Form_EditForm_Locale')) args.local = $('Form_EditForm_Locale').value;
+		
+		url = this.url(args); 
+		
+		var self = this;
+		new Ajax.Request(url, {
+			onSuccess: function(response){
+				self.innerHTML = response.responseText;
+				self.castAsTreeNode(self.firstChild);
+				if (options.onSuccess) options.onSuccess(response);
+			},
+			onFailure: function(response){
+				errorMessage('error loading tree', response);
+				if (options.onError) options.onError(response);
+			}
+		});
+	},
+	
 	/**
 	 * Perform the given code on the each tree node with the given index.
 	 * There could be more than one :-)
@@ -177,11 +219,11 @@ TreeNodeAPI.prototype = {
 			this.cuedNewNodes[this.cuedNewNodes.length] = node;
 		}
 		
-
-		var url = SiteTreeHandlers.loadTree_url;
-		url += (url.match(/\?/)) ? '&' : '?';
-		url += 'ajax=1&ID=' + this.getIdx();
-		if($('Form_EditForm_Locale')) url += "&locale=" + $('Form_EditForm_Locale').value;
+		var args = {ajax:1, ID:this.getIdx()};
+		if ($('Form_EditForm_Locale')) args.local = $('Form_EditForm_Locale').value;
+		
+		url = this.tree.url(args); 
+		
 		new Ajax.Request(url, {
 			onSuccess : this.installSubtree.bind(this),
 			onFailure : this.showSubtreeLoadingError
@@ -481,30 +523,3 @@ ReorganiseAction.prototype = {
 		}
 	}
 }
-
-/**
- * Control the site tree filter
- */
-SiteTreeFilterForm = Class.create();
-SiteTreeFilterForm.applyTo('form#search_options');
-SiteTreeFilterForm.prototype = {
-	onsubmit: function() {
-			$('SiteTreeSearchButton').className = 'hidden';
-			$('searchIndicator').className = 'loading';
-			Ajax.SubmitForm(this, null, {
-				onSuccess :  function(response) {
-					$('SiteTreeIsFiltered').value = 1;
-					$('SiteTreeSearchButton').className = '';
-					$('searchIndicator').className = '';
-					$('sitetree_ul').innerHTML = response.responseText;
-					Behaviour.apply($('sitetree_ul'));
-					statusMessage('Filtered tree','good');
-				},
-				onFailure : function(response) {
-					errorMessage('Could not filter site tree<br />' + response.responseText);
-				}
-			});
-		
-		return false;
-	}
-}
diff --git a/javascript/WidgetAreaEditor.js b/javascript/WidgetAreaEditor.js
index 9d6a2fa7..31fdfcf7 100644
--- a/javascript/WidgetAreaEditor.js
+++ b/javascript/WidgetAreaEditor.js
@@ -1,7 +1,7 @@
-WidgetAreaEditor = Class.create();
-WidgetAreaEditor.applyTo('div.WidgetAreaEditor');
+WidgetAreaEditorClass = Class.create();
+WidgetAreaEditorClass.applyTo('div.WidgetAreaEditor');
 
-WidgetAreaEditor.prototype = {
+WidgetAreaEditorClass.prototype = {
 	initialize: function() {
 		UsedWidget.applyToChildren($('WidgetAreaEditor_usedWidgets'), 'div.Widget');
 	
diff --git a/javascript/tinymce.template.js b/javascript/tinymce.template.js
deleted file mode 100755
index ffef4da9..00000000
--- a/javascript/tinymce.template.js
+++ /dev/null
@@ -1,63 +0,0 @@
-function nullConverter(url) {
-	return url;
-}
-
-/**
- * TinyMCE initialisation template.
- * $ variables are replaced by string search & replace.  It's pretty crude.
- */
-// Prevents "Error: 'tinyMCE' is undefined" error in IE7 on Newsletter Recipient import.
-if((typeof tinyMCE != 'undefined')) {
-	tinyMCE.init({
-		mode : "none",
-		language: "$Lang",
-		width: "100%",
-		auto_resize : false,
-		theme : "advanced",
-		content_css : "cms/css/editor.css, $ContentCSS",
-		body_class : 'typography',
-		document_base_url: "$BaseURL",
-		urlconverter_callback : "nullConverter",
-		
-		setupcontent_callback : "sapphiremce_setupcontent",
-		cleanup_callback : "sapphiremce_cleanup",
-		
-		theme_advanced_layout_manager: "SimpleLayout",
-		theme_advanced_toolbar_location : "top",
-		theme_advanced_toolbar_align : "left",
-		theme_advanced_toolbar_parent : "right",
-		plugins : "media,contextmenu,table,emotions,paste,../../tinymce_ssbuttons,../../tinymce_advcode,spellchecker,fullscreen",	
-		blockquote_clear_tag : "p",
-		table_inline_editing : true,
-		theme_advanced_buttons1 : "bold,italic,underline,strikethrough,separator,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,separator,bullist,numlist,outdent,indent,blockquote,hr,charmap",
-		theme_advanced_buttons2 : "undo,redo,separator,cut,copy,paste,pastetext,pasteword,spellchecker,separator,ssimage,ssflash,sslink,unlink,anchor,separator,advcode,fullscreen,separator,search,replace,selectall,visualaid",
-		theme_advanced_buttons3 : "tablecontrols",
-		spellchecker_languages : "$SpellcheckLangs",
-		
-		template_templates : [
-		    { title : "Three column", src : "assets/snippet.html", description : "A simple 3 column layout"},
-		],
-
-		safari_warning : false,
-		relative_urls : true,
-		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|usemap],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling],object[width|height|data|type],param[name|value],map[class|name|id],area[shape|coords|href|target|alt]"
-	});
-}
-
-Behaviour.register({
-    'textarea.htmleditor' : {
-        initialize : function() {
-            tinyMCE.execCommand("mceAddControl", true, this.id);
-            this.isChanged = function() {
-                return tinyMCE.getInstanceById(this.id).isDirty();
-            }
-            this.resetChanged = function() {
-                inst = tinyMCE.getInstanceById(this.id);
-                inst.startContent = tinymce.trim(inst.getContent({format : 'raw', no_events : 1}));
-            }
-        }
-    }
-})
diff --git a/lang/en_US.php b/lang/en_US.php
index 93499386..9bfb1ece 100755
--- a/lang/en_US.php
+++ b/lang/en_US.php
@@ -150,7 +150,7 @@ $lang['en_US']['CMSMain']['WORKTODO'] = 'You have work to do on these <b>%d</b>
 $lang['en_US']['CMSMain_dialog.ss']['BUTTONNOTFOUND'] = 'Couldn\'t find the button name';
 $lang['en_US']['CMSMain_dialog.ss']['NOLINKED'] = 'Can\'t find window.linkedObject to send the button click back to the main window';
 $lang['en_US']['CMSMain_left.ss']['ADDEDNOTPUB'] = 'Added to the draft site and not published yet';
-$lang['en_US']['CMSMain_left.ss']['ADDSEARCHCRITERIA'] = 'Add Criteria...';
+$lang['en_US']['CMSMain_left.ss']['ADDSEARCHCRITERIA'] = 'Add Criteria';
 $lang['en_US']['CMSMain_left.ss']['BATCHACTIONS'] = array(
 	'Batch Actions',
 	PR_HIGH
@@ -177,6 +177,7 @@ $lang['en_US']['CMSMain_left.ss']['NEW'] = 'new';
 $lang['en_US']['CMSMain_left.ss']['OPENBOX'] = 'click to open this box';
 $lang['en_US']['CMSMain_left.ss']['PAGEVERSIONH'] = 'Page Version History';
 $lang['en_US']['CMSMain_left.ss']['PUBLISHCONFIRM'] = 'Publish the selected pages';
+$lang['en_US']['CMSMain_left.ss']['CLEAR'] = 'Clear';
 $lang['en_US']['CMSMain_left.ss']['SEARCH'] = array(
 	'Search',
 	PR_HIGH
diff --git a/templates/Includes/CMSMain_left.ss b/templates/Includes/CMSMain_left.ss
index d78659c4..7a4d7c0a 100755
--- a/templates/Includes/CMSMain_left.ss
+++ b/templates/Includes/CMSMain_left.ss
@@ -29,15 +29,19 @@
 					<input type="hidden" id="SiteTreeIsFiltered" value="0" />
 					
 					<div id="SearchBox">
-						<input type="text" id="SiteTreeSearchTerm" name="SiteTreeSearchTerm" />
-						<div id="searchIndicator">&nbsp;</div>
-						<input type="submit" id="SiteTreeSearchButton" class="action" value="<% _t('SEARCH') %>" title="<% _t('SEARCHTITLE','Search through URL, Title, Menu Title, &amp; Content') %>" />
+						<div class="SearchCriteria">Text:</div>
+						<input type="text" id="SiteTreeSearchTerm" class='SearchCriteria' name="SiteTreeSearchTerm" />
 					</div>
 					
 					<div id="ContainerSiteTreeFilterDate" class="SearchCriteriaContainer" style="display:none">
 						<div id="TextSiteTreeFilterDate" class="SearchCriteria"><% _t('EDITEDSINCE','Edited Since') %>:</div>
 						<div id="InputSiteTreeFilterDate">$SiteTreeFilterDateField</div>
 					</div>
+					<div id='ContainerSiteTreeFilterClassName' class='SearchCriteriaContainer' style="display:none">
+						<div id="TextSiteTreeFilterClassName" class="SearchCriteria">Page type: </div>
+						<div id="InputSiteTreeFilterClassName">$SiteTreeFilterPageTypeField</div>
+					</div>
+
 					<% control SiteTreeFilterOptions %>
 						<div id="Container$Column" class="SearchCriteriaContainer" style="display:none">
 							<div id="Text$Column" class="SearchCriteria">$Title:</div>
@@ -45,39 +49,60 @@
 						</div>
 					<% end_control %>
 					
-					<div id="addCriteria">
+					<div id='SearchControls'>
 						<select id="SiteTreeFilterAddCriteria">
-							<option value=""><% _t('ADDSEARCHCRITERIA','Add Criteria...') %></option>
+							<option value=""><% _t('ADDSEARCHCRITERIA','Add Criteria') %></option>
 							<option value="SiteTreeFilterDate"><% _t('EDITEDSINCE','Edited Since') %></option>
+							<option value="SiteTreeFilterClassName">Page type</option>
 							<% control SiteTreeFilterOptions %>
 		        				<option value="$Column">$Title</option>
 							<% end_control %>
 						</select>
+						<div id="searchIndicator">&nbsp;</div>
+						<input type="submit" id="SiteTreeSearchClearButton" class="action" value="<% _t('CLEAR') %>" title="<% _t('CLEARTITLE','Clear the search and view all items') %>" />
+						<input type="submit" id="SiteTreeSearchButton" class="action" value="<% _t('SEARCH') %>" title="<% _t('SEARCHTITLE','Search through URL, Title, Menu Title, &amp; Content') %>" />
 					</div>
 				</div>
 				</form>
-				<div id="batchactionsforms" style="display: none">
-					$DeleteItemsForm
-					$PublishItemsForm
-					
-				</div>
-				<div class="checkboxAboveTree">
-					<img id="checkboxActionIndicator" src="cms/images/network-save.gif">
+
+			<div id="batchactionsforms" style="display: none">
+				<form class="actionparams" style="border:0" id="batchactions_options" action="">
+					<p><% _t('SELECTPAGESACTIONS','Select the pages that you want to change &amp; then click an action:') %></p>
+
+					<input type="hidden" name="csvIDs" />
+
 					<div>
-						<input type="checkbox" id="sortitems" />
-						<label for="sortitems"><% _t('ENABLEDRAGGING','Allow drag &amp; drop reordering', PR_HIGH) %></label>
-					</div>
-					<div>
-						<input type="checkbox" id="showdeletedpages" />
-						<label for="showdeletedpages"><% _t('SHOW_DELETED_PAGES','Show deleted pages', PR_HIGH) %></label>
+						<select id="choose_batch_action">
+							<% control BatchActionList %>
+							<option value="$Link" class="{doingText:'$DoingText'}">$Title</option>
+							<% end_control %>
+						</select>
+						<input id="batchactions_go" type="submit" class="action" value="Go" />
 					</div>
+				</form>
+			</div>
+			<div class="checkboxAboveTree">
+				<img id="checkboxActionIndicator" src="cms/images/network-save.gif">
+				<div>
+					<input type="checkbox" id="sortitems" />
+					<label for="sortitems"><% _t('ENABLEDRAGGING','Allow drag &amp; drop reordering', PR_HIGH) %></label>
 				</div>
-					
-				<% if IsTranslatableEnabled %>
-				<div id="LangSelector_holder">
-					Language: $LangSelector
+				<div>
+					<input type="checkbox" id="publishpage_show_drafts" />
+					<label for="publishpage_show_drafts"><% _t('SHOWONLYCHANGED','Show only changed pages') %></label>
 				</div>
-				<% end_if %>
+				<div>
+					<input type="checkbox" id="showdeletedpages" />
+					<label for="showdeletedpages"><% _t('SHOW_DELETED_PAGES','Show deleted pages', PR_HIGH) %></label>
+				</div>
+			</div>
+
+			<% if IsTranslatableEnabled %>
+			<div id="LangSelector_holder">
+				Language: $LangSelector
+			</div>
+			<% end_if %>
+
 			</div>
 			<div id="sitetree_ul">
 				$SiteTreeAsUL
diff --git a/tests/CMSMainTest.php b/tests/CMSMainTest.php
index 918e09fe..e8ed96ae 100644
--- a/tests/CMSMainTest.php
+++ b/tests/CMSMainTest.php
@@ -21,8 +21,7 @@ class CMSMainTest extends FunctionalTest {
 			$response->getBody()
 		);
 
-		$response = $this->post('admin/cms/publishitems', array('csvIDs' => '1,2', 'ajax' => 1));
-		
+		$response = Director::test("admin/cms/batchactions/publish", array('csvIDs' => '1,2', 'ajax' => 1), $session);
 		$this->assertContains('setNodeTitle(1, \'Page 1\');', $response->getBody());
 		$this->assertContains('setNodeTitle(2, \'Page 2\');', $response->getBody());