<?php
/**
 * The main "content" area of the CMS.
 *
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
 * admin menu.
 *
 * @package cms
 * @subpackage controller
 * @todo Create some base classes to contain the generic functionality that will be replicated.
 */
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider {

	private static $url_segment = 'pages';

	private static $url_rule = '/$Action/$ID/$OtherID';

	// Maintain a lower priority than other administration sections
	// so that Director does not think they are actions of CMSMain
	private static $url_priority = 39;

	private static $menu_title = 'Edit Page';

	private static $menu_priority = 10;

	private static $tree_class = "SiteTree";

	private static $subitem_class = "Member";

	/**
	 * Amount of results showing on a single page.
	 *
	 * @config
	 * @var int
	 */
	private static $page_length = 15;

	private static $allowed_actions = array(
		'archive',
		'buildbrokenlinks',
		'deleteitems',
		'DeleteItemsForm',
		'dialog',
		'duplicate',
		'duplicatewithchildren',
		'publishall',
		'publishitems',
		'PublishItemsForm',
		'submit',
		'EditForm',
		'SearchForm',
		'SiteTreeAsUL',
		'getshowdeletedsubtree',
		'batchactions',
		'treeview',
		'listview',
		'ListViewForm',
		'childfilter',
	);

	/**
	 * Enable legacy batch actions.
	 * @deprecated since version 4.0
	 * @var array
	 * @config
	 */
	private static $enabled_legacy_actions = array();

	public function init() {
		// set reading lang
		if(SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
			Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SiteTree')));
		}

		parent::init();

		Requirements::css(CMS_DIR . '/css/screen.css');
		Requirements::customCSS($this->generatePageIconsCss());

		Requirements::combine_files(
			'cmsmain.js',
			array_merge(
				array(
					CMS_DIR . '/javascript/CMSMain.js',
					CMS_DIR . '/javascript/CMSMain.EditForm.js',
					CMS_DIR . '/javascript/CMSMain.AddForm.js',
					CMS_DIR . '/javascript/CMSPageHistoryController.js',
					CMS_DIR . '/javascript/CMSMain.Tree.js',
					CMS_DIR . '/javascript/SilverStripeNavigator.js',
					CMS_DIR . '/javascript/SiteTreeURLSegmentField.js'
				),
				Requirements::add_i18n_javascript(CMS_DIR . '/javascript/lang', true, true)
			)
		);

		CMSBatchActionHandler::register('publish', 'CMSBatchAction_Publish');
		CMSBatchActionHandler::register('unpublish', 'CMSBatchAction_Unpublish');


		// Check legacy actions
		$legacy = $this->config()->enabled_legacy_actions;

		// Delete from live is unnecessary since we have unpublish which does the same thing
		if(in_array('CMSBatchAction_DeleteFromLive', $legacy)) {
			Deprecation::notice('4.0', 'Delete From Live is deprecated. Use Un-publish instead');
			CMSBatchActionHandler::register('deletefromlive', 'CMSBatchAction_DeleteFromLive');
		}

		// Delete action
		if(in_array('CMSBatchAction_Delete', $legacy)) {
			Deprecation::notice('4.0', 'Delete from Stage is deprecated. Use Archive instead.');
			CMSBatchActionHandler::register('delete', 'CMSBatchAction_Delete');
		} else {
			CMSBatchActionHandler::register('archive', 'CMSBatchAction_Archive');
			CMSBatchActionHandler::register('restore', 'CMSBatchAction_Restore');
		}
	}

	public function index($request) {
		// In case we're not showing a specific record, explicitly remove any session state,
		// to avoid it being highlighted in the tree, and causing an edit form to show.
		if(!$request->param('Action')) $this->setCurrentPageId(null);

		return parent::index($request);
	}

	public function getResponseNegotiator() {
		$negotiator = parent::getResponseNegotiator();
		$controller = $this;
		$negotiator->setCallback('ListViewForm', function() use(&$controller) {
			return $controller->ListViewForm()->forTemplate();
		});
		return $negotiator;
	}

	/**
	 * If this is set to true, the "switchView" context in the
	 * template is shown, with links to the staging and publish site.
	 *
	 * @return boolean
	 */
	public function ShowSwitchView() {
		return true;
	}

	/**
	 * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
	 * to switch view also for archived versions.
	 */
	public function SwitchView($page = null) {
		if(!$page) {
			$page = $this->currentPage();
		}

		if($page) {
			$nav = SilverStripeNavigator::get_for_record($page);
			return $nav['items'];
		}
	}

	//------------------------------------------------------------------------------------------//
	// Main controllers

	//------------------------------------------------------------------------------------------//
	// Main UI components

	/**
	 * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
	 *
	 * @param string|null $action Action to link to.
	 * @return string
	 */
	public function Link($action = null) {
		$link = Controller::join_links(
			$this->stat('url_base', true),
			$this->stat('url_segment', true), // in case we want to change the segment
			'/', // trailing slash needed if $action is null!
			"$action"
		);
		$this->extend('updateLink', $link);
		return $link;
	}

	public function LinkPages() {
		return singleton('CMSPagesController')->Link();
	}

	public function LinkPagesWithSearch() {
		return $this->LinkWithSearch($this->LinkPages());
	}

	public function LinkTreeView() {
		return $this->LinkWithSearch(singleton('CMSMain')->Link('treeview'));
	}

	public function LinkListView() {
		return $this->LinkWithSearch(singleton('CMSMain')->Link('listview'));
	}

	public function LinkGalleryView() {
		return $this->LinkWithSearch(singleton('CMSMain')->Link('galleryview'));
	}

	public function LinkPageEdit($id = null) {
		if(!$id) $id = $this->currentPageID();
		return $this->LinkWithSearch(
			Controller::join_links(singleton('CMSPageEditController')->Link('show'), $id)
		);
	}

	public function LinkPageSettings() {
		if($id = $this->currentPageID()) {
			return $this->LinkWithSearch(
				Controller::join_links(singleton('CMSPageSettingsController')->Link('show'), $id)
			);
		}
	}

	public function LinkPageHistory() {
		if($id = $this->currentPageID()) {
			return $this->LinkWithSearch(
				Controller::join_links(singleton('CMSPageHistoryController')->Link('show'), $id)
			);
		}
	}

	public function LinkWithSearch($link) {
		// Whitelist to avoid side effects
		$params = array(
			'q' => (array)$this->getRequest()->getVar('q'),
			'ParentID' => $this->getRequest()->getVar('ParentID')
		);
		$link = Controller::join_links(
			$link,
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
		);
		$this->extend('updateLinkWithSearch', $link);
		return $link;
	}

	public function LinkPageAdd($extra = null, $placeholders = null) {
		$link = singleton("CMSPageAddController")->Link();
		$this->extend('updateLinkPageAdd', $link);

		if($extra) {
			$link = Controller::join_links ($link, $extra);
		}

		if($placeholders) {
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&amp;$placeholders");
		}

		return $link;
	}

	/**
	 * @return string
	 */
	public function LinkPreview() {
		$record = $this->getRecord($this->currentPageID());
		$baseLink = Director::absoluteBaseURL();
		if ($record && $record instanceof Page) {
			// if we are an external redirector don't show a link
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
				$baseLink = false;
			}
			else {
				$baseLink = $record->Link('?stage=Stage');
			}
		}
		return $baseLink;
	}

	/**
	 * Return the entire site tree as a nested set of ULs
	 */
	public function SiteTreeAsUL() {
		// Pre-cache sitetree version numbers for querying efficiency
		Versioned::prepopulate_versionnumber_cache("SiteTree", "Stage");
		Versioned::prepopulate_versionnumber_cache("SiteTree", "Live");
		$html = $this->getSiteTreeFor($this->stat('tree_class'));

		$this->extend('updateSiteTreeAsUL', $html);

		return $html;
	}

	/**
	 * @return boolean
	 */
	public function TreeIsFiltered() {
		$query = $this->getRequest()->getVar('q');

		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'CMSSiteTreeFilter_Search')) {
			return false;
		}

		return true;
	}

	public function ExtraTreeTools() {
		$html = '';
		$this->extend('updateExtraTreeTools', $html);
		return $html;
	}

	/**
	 * Returns a Form for page searching for use in templates.
	 *
	 * Can be modified from a decorator by a 'updateSearchForm' method
	 *
	 * @return Form
	 */
	public function SearchForm() {
		// Create the fields
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
		$dateHeader = new HeaderField('q[Date]', _t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'), 4);
		$dateFrom = new DateField(
			'q[LastEditedFrom]',
			_t('CMSSearch.FILTERDATEFROM', 'From')
		);
		$dateFrom->setConfig('showcalendar', true);
		$dateTo = new DateField(
			'q[LastEditedTo]',
			_t('CMSSearch.FILTERDATETO', 'To')
		);
		$dateTo->setConfig('showcalendar', true);
		$pageFilter = new DropdownField(
			'q[FilterClass]',
			_t('CMSMain.PAGES', 'Page status'),
			CMSSiteTreeFilter::get_all_filters()
		);
		$pageClasses = new DropdownField(
			'q[ClassName]',
			_t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
			$this->getPageTypes()
		);
		$pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT','Any'));

		// Group the Datefields
		$dateGroup = new FieldGroup(
			$dateHeader,
			$dateFrom,
			$dateTo
		);
		$dateGroup->setFieldHolderTemplate('FieldGroup_DefaultFieldHolder')->addExtraClass('stacked');

		// Create the Field list
		$fields = new FieldList(
			$content,
			$dateGroup,
			$pageFilter,
			$pageClasses
		);

		// Create the Search and Reset action
		$actions = new FieldList(
			FormAction::create('doSearch',  _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
				->addExtraClass('ss-ui-action-constructive'),
			Object::create('ResetFormAction', 'clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
		);

		// Use <button> to allow full jQuery UI styling on the all of the Actions
		foreach($actions->dataFields() as $action) {
			$action->setUseButtonTag(true);
		}

		// Create the form
		$form = Form::create($this, 'SearchForm', $fields, $actions)
			->addExtraClass('cms-search-form')
			->setFormMethod('GET')
			->setFormAction($this->Link())
			->disableSecurityToken()
			->unsetValidator();

		// Load the form with previously sent search data
		$form->loadDataFrom($this->getRequest()->getVars());

		// Allow decorators to modify the form
		$this->extend('updateSearchForm', $form);

		return $form;
	}

	/**
	 * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
	 *
	 * @return array
	 */
	protected function getPageTypes() {
		$pageTypes = array();
		foreach(SiteTree::page_type_classes() as $pageTypeClass) {
			$pageTypes[$pageTypeClass] = _t($pageTypeClass.'.SINGULARNAME', $pageTypeClass);
		}
		asort($pageTypes);
		return $pageTypes;
	}

	public function doSearch($data, $form) {
		return $this->getsubtree($this->getRequest());
	}

	/**
	 * @param bool $unlinked
	 * @return ArrayList
	 */
	public function Breadcrumbs($unlinked = false) {
		$items = parent::Breadcrumbs($unlinked);

		if($items->count() > 1) {
			// Specific to the SiteTree admin section, we never show the cms section and current
			// page in the same breadcrumbs block.
			$items->shift();
		}

		return $items;
	}

	/**
	 * Create serialized JSON string with site tree hints data to be injected into
	 * 'data-hints' attribute of root node of jsTree.
	 *
	 * @return string Serialized JSON
	 */
	public function SiteTreeHints() {
		$json = '';
		$classes = SiteTree::page_type_classes();

	 	$cacheCanCreate = array();
	 	foreach($classes as $class) $cacheCanCreate[$class] = singleton($class)->canCreate();

	 	// Generate basic cache key. Too complex to encompass all variations
	 	$cache = SS_Cache::factory('CMSMain_SiteTreeHints');
	 	$cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
	 	if($this->getRequest()->getVar('flush')) $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
	 	$json = $cache->load($cacheKey);
	 	if(!$json) {
			$def['Root'] = array();
			$def['Root']['disallowedChildren'] = array();

			// Contains all possible classes to support UI controls listing them all,
			// such as the "add page here" context menu.
			$def['All'] = array();

			// Identify disallows and set globals
			foreach($classes as $class) {
				$obj = singleton($class);
				if($obj instanceof HiddenClass) continue;

				// Name item
				$def['All'][$class] = array(
					'title' => $obj->i18n_singular_name()
				);

				// Check if can be created at the root
				$needsPerm = $obj->stat('need_permission');
				if(
					!$obj->stat('can_be_root')
					|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
					|| ($needsPerm && !$this->can($needsPerm))
				) {
					$def['Root']['disallowedChildren'][] = $class;
				}

				// Hint data specific to the class
				$def[$class] = array();

				$defaultChild = $obj->defaultChild();
				if($defaultChild !== 'Page' && $defaultChild !== null) {
					$def[$class]['defaultChild'] = $defaultChild;
				}

				$defaultParent = $obj->defaultParent();
				if ($defaultParent !== 1 && $defaultParent !== null) {
					$def[$class]['defaultParent'] = $defaultParent;
				}
			}

			$this->extend('updateSiteTreeHints', $def);

			$json = Convert::raw2json($def);
			$cache->save($json, $cacheKey);
		}
		return $json;
	}

	/**
	 * Populates an array of classes in the CMS
	 * which allows the user to change the page type.
	 *
	 * @return SS_List
	 */
	public function PageTypes() {
		$classes = SiteTree::page_type_classes();

		$result = new ArrayList();

		foreach($classes as $class) {
			$instance = singleton($class);

			if($instance instanceof HiddenClass) continue;

			// skip this type if it is restricted
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue;

			$addAction = $instance->i18n_singular_name();

			// Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
			$description = _t((($class == 'Page') ? 'SiteTree' : $class) . '.DESCRIPTION');

			if(!$description) {
				$description = $instance->uninherited('description');
			}

			if($class == 'Page' && !$description) {
				$description = singleton('SiteTree')->uninherited('description');
			}

			$result->push(new ArrayData(array(
				'ClassName' => $class,
				'AddAction' => $addAction,
				'Description' => $description,
				// TODO Sprite support
				'IconURL' => $instance->stat('icon'),
				'Title' => singleton($class)->i18n_singular_name(),
			)));
		}

		$result = $result->sort('AddAction');

		return $result;
	}

	/**
	 * Get a database record to be managed by the CMS.
	 *
	 * @param int $id Record ID
	 * @param int $versionID optional Version id of the given record
	 * @return DataObject
	 */
 	public function getRecord($id, $versionID = null) {
		$treeClass = $this->stat('tree_class');

		if($id instanceof $treeClass) {
			return $id;
		}
		else if($id && is_numeric($id)) {
			$currentStage = Versioned::get_reading_mode();

			if($this->getRequest()->getVar('Version')) {
				$versionID = (int) $this->getRequest()->getVar('Version');
			}

			if($versionID) {
				$record = Versioned::get_version($treeClass, $id, $versionID);
			} else {
				$record = DataObject::get_by_id($treeClass, $id);
			}

			// Then, try getting a record from the live site
			if(!$record) {
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
				Versioned::reading_stage('Live');
				singleton($treeClass)->flushCache();

				$record = DataObject::get_by_id($treeClass, $id);
			}

			// Then, try getting a deleted record
			if(!$record) {
				$record = Versioned::get_latest_version($treeClass, $id);
			}

			// Don't open a page from a different locale
			/** The record's Locale is saved in database in 2.4, and not related with Session,
			 *  we should not check their locale matches the Translatable::get_current_locale,
			 * 	here as long as we all the HTTPRequest is init with right locale.
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
			 */
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
				$record = null;
			}*/

			// Set the reading mode back to what it was.
			Versioned::set_reading_mode($currentStage);

			return $record;

		} else if(substr($id,0,3) == 'new') {
			return $this->getNewItem($id);
		}
	}

	/**
	 * @param int $id
	 * @param FieldList $fields
	 * @return CMSForm
	 */
	public function getEditForm($id = null, $fields = null) {

		if(!$id) $id = $this->currentPageID();
		$form = parent::getEditForm($id);

		// TODO Duplicate record fetching (see parent implementation)
		$record = $this->getRecord($id);
		if($record && !$record->canView()) return Security::permissionFailure($this);

		if(!$fields) $fields = $form->Fields();
		$actions = $form->Actions();

		if($record) {
			$deletedFromStage = $record->getIsDeletedFromStage();
			$deleteFromLive = !$record->getExistsOnLive();

			$fields->push($idField = new HiddenField("ID", false, $id));
			// Necessary for different subsites
			$fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
			$fields->push($stageLinkField = new HiddenField("StageLink"));
			$fields->push(new HiddenField("TreeTitle", false, $record->TreeTitle));

			if($record->ID && is_numeric( $record->ID ) ) {
				$liveLink = $record->getAbsoluteLiveLink();
				if($liveLink) $liveLinkField->setValue($liveLink);
				if(!$deletedFromStage) {
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
					if($stageLink) $stageLinkField->setValue($stageLink);
				}
			}

			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
			if(in_array('CMSPreviewable', class_implements($record)) && !$fields->fieldByName('SilverStripeNavigator')) {
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
				$navField->setAllowHTML(true);
				$fields->push($navField);
			}

			// getAllCMSActions can be used to completely redefine the action list
			if($record->hasMethod('getAllCMSActions')) {
				$actions = $record->getAllCMSActions();
			} else {
				$actions = $record->getCMSActions();

				// Find and remove action menus that have no actions.
				if ($actions && $actions->Count()) {
					$tabset = $actions->fieldByName('ActionMenus');
					if ($tabset) {
						foreach ($tabset->getChildren() as $tab) {
							if (!$tab->getChildren()->count()) {
								$tabset->removeByName($tab->getName());
							}
						}
					}
				}
			}

			// Use <button> to allow full jQuery UI styling
			$actionsFlattened = $actions->dataFields();
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);

			if($record->hasMethod('getCMSValidator')) {
				$validator = $record->getCMSValidator();
			} else {
				$validator = new RequiredFields();
			}

			$form = CMSForm::create(
				$this, "EditForm", $fields, $actions, $validator
			)->setHTMLID('Form_EditForm');
			$form->setResponseNegotiator($this->getResponseNegotiator());
			$form->loadDataFrom($record);
			$form->disableDefaultAction();
			$form->addExtraClass('cms-edit-form');
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
			// TODO Can't merge $FormAttributes in template at the moment
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
			// if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
			// Set validation exemptions for specific actions
			$form->setValidationExemptActions(array(
				'restore',
				'revert',
				'deletefromlive',
				'delete',
				'unpublish',
				'rollback',
				'doRollback',
				'archive'
			));

			// Announce the capability so the frontend can decide whether to allow preview or not.
			if(in_array('CMSPreviewable', class_implements($record))) {
				$form->addExtraClass('cms-previewable');
			}

			if(!$record->canEdit() || $deletedFromStage) {
				$readonlyFields = $form->Fields()->makeReadonly();
				$form->setFields($readonlyFields);
			}

			$this->extend('updateEditForm', $form);
			return $form;
		} else if($id) {
			$form = CMSForm::create( $this, "EditForm", new FieldList(
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
			)->setHTMLID('Form_EditForm');
			$form->setResponseNegotiator($this->getResponseNegotiator());
			return $form;
		}
	}

	/**
	 * @param SS_HTTPRequest $request
	 * @return string HTML
	 */
	public function treeview($request) {
		return $this->renderWith($this->getTemplatesWithSuffix('_TreeView'));
	}

	/**
	 * @param SS_HTTPRequest $request
	 * @return string HTML
	 */
	public function listview($request) {
		return $this->renderWith($this->getTemplatesWithSuffix('_ListView'));
	}

	/**
	 * Callback to request the list of page types allowed under a given page instance.
	 * Provides a slower but more precise response over SiteTreeHints
	 *
	 * @param SS_HTTPRequest $request
	 * @return SS_HTTPResponse
	 */
	public function childfilter($request) {
		// Check valid parent specified
		$parentID = $request->requestVar('ParentID');
		$parent = SiteTree::get()->byID($parentID);
		if(!$parent || !$parent->exists()) return $this->httpError(404);

		// Build hints specific to this class
		// Identify disallows and set globals
		$classes = SiteTree::page_type_classes();
		$disallowedChildren = array();
		foreach($classes as $class) {
			$obj = singleton($class);
			if($obj instanceof HiddenClass) continue;

			if(!$obj->canCreate(null, array('Parent' => $parent))) {
				$disallowedChildren[] = $class;
			}
		}

		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
		return $this
			->getResponse()
			->addHeader('Content-Type', 'application/json; charset=utf-8')
			->setBody(Convert::raw2json($disallowedChildren));
	}

	/**
	 * Safely reconstruct a selected filter from a given set of query parameters
	 *
	 * @param array $params Query parameters to use
	 * @return CMSSiteTreeFilter The filter class, or null if none present
	 * @throws InvalidArgumentException if invalid filter class is passed.
	 */
	protected function getQueryFilter($params) {
		if(empty($params['FilterClass'])) return null;
		$filterClass = $params['FilterClass'];
		if(!is_subclass_of($filterClass, 'CMSSiteTreeFilter')) {
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
		}
		return $filterClass::create($params);
	}

	/**
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
	 * defaulting to no filter and show all pages in first level.
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
	 *
	 * @param array $params Search filter criteria
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
	 * @return SS_List
	 * @throws InvalidArgumentException if invalid filter class is passed.
	 */
	public function getList($params = array(), $parentID = 0) {
		if($filter = $this->getQueryFilter($params)) {
			return $filter->getFilteredPages();
		} else {
			$list = DataList::create($this->stat('tree_class'));
			$parentID = is_numeric($parentID) ? $parentID : 0;
			return $list->filter("ParentID", $parentID);
		}
	}

	public function ListViewForm() {
		$params = $this->getRequest()->requestVar('q');
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
			new GridFieldSortableHeader(),
			new GridFieldDataColumns(),
			new GridFieldPaginator(self::config()->page_length)
		);
		if($parentID){
			$gridFieldConfig->addComponent(
				GridFieldLevelup::create($parentID)
					->setLinkSpec('?ParentID=%d&view=list')
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
			);
		}
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
		$columns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');

		// Don't allow navigating into children nodes on filtered lists
		$fields = array(
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
			'singular_name' => _t('SiteTree.PAGETYPE'),
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
		);
		$gridField->getConfig()->getComponentByType('GridFieldSortableHeader')->setFieldSorting(array('getTreeTitle' => 'Title'));
		$gridField->getState()->ParentID = $parentID;

		if(!$params) {
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
		}

		$columns->setDisplayFields($fields);
		$columns->setFieldCasting(array(
			'Created' => 'Datetime->Ago',
			'LastEdited' => 'Datetime->FormatFromSettings',
			'getTreeTitle' => 'HTMLText'
		));

		$controller = $this;
		$columns->setFieldFormatting(array(
			'listChildrenLink' => function($value, &$item) use($controller) {
				$num = $item ? $item->numChildren() : null;
				if($num) {
					return sprintf(
						'<a class="cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s">%s</a>',
						Controller::join_links(
							$controller->Link(),
							sprintf("?ParentID=%d&view=list", (int)$item->ID)
						),
						$num
					);
				}
			},
			'getTreeTitle' => function($value, &$item) use($controller) {
				return sprintf(
					'<a class="action-detail" href="%s">%s</a>',
					Controller::join_links(
						singleton('CMSPageEditController')->Link('show'),
						(int)$item->ID
					),
					$item->TreeTitle // returns HTML, does its own escaping
				);
			}
		));

		$listview = CMSForm::create(
			$this,
			'ListViewForm',
			new FieldList($gridField),
			new FieldList()
		)->setHTMLID('Form_ListViewForm');
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
		$listview->setResponseNegotiator($this->getResponseNegotiator());

		$this->extend('updateListView', $listview);

		$listview->disableSecurityToken();
		return $listview;
	}

	public function currentPageID() {
		$id = parent::currentPageID();

		$this->extend('updateCurrentPageID', $id);

		return $id;
	}

	//------------------------------------------------------------------------------------------//
	// Data saving handlers

	/**
	 * Save and Publish page handler
	 */
	public function save($data, $form) {
		$className = $this->stat('tree_class');

		// Existing or new record?
		$id = $data['ID'];
		if(substr($id,0,3) != 'new') {
			$record = DataObject::get_by_id($className, $id);
			if($record && !$record->canEdit()) return Security::permissionFailure($this);
			if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
		} else {
			if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
			$record = $this->getNewItem($id, false);
		}

		// TODO Coupling to SiteTree
		$record->HasBrokenLink = 0;
		$record->HasBrokenFile = 0;

		if (!$record->ObsoleteClassName) $record->writeWithoutVersion();

		// Update the class instance if necessary
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
			$newClassName = $record->ClassName;
			// The records originally saved attribute was overwritten by $form->saveInto($record) before.
			// This is necessary for newClassInstance() to work as expected, and trigger change detection
			// on the ClassName attribute
			$record->setClassName($data['ClassName']);
			// Replace $record with a new instance
			$record = $record->newClassInstance($newClassName);
		}

		// save form data into record
		$form->saveInto($record);
		$record->write();

		// If the 'Save & Publish' button was clicked, also publish the page
		if (isset($data['publish']) && $data['publish'] == 1) {
			$record->doPublish();
		}

		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
	 */
	public function getNewItem($id, $setID = true) {
		$parentClass = $this->stat('tree_class');
		list($dummy, $className, $parentID, $suffix) = array_pad(explode('-',$id),4,null);

		if(!is_subclass_of($className, $parentClass) && strcasecmp($className, $parentClass) != 0) {
			$response = Security::permissionFailure($this);
			if (!$response) {
				$response = $this->getResponse();
			}
			throw new SS_HTTPResponse_Exception($response);
		}

		$newItem = new $className();

		if( !$suffix ) {
			$sessionTag = "NewItems." . $parentID . "." . $className;
			if(Session::get($sessionTag)) {
				$suffix = '-' . Session::get($sessionTag);
				Session::set($sessionTag, Session::get($sessionTag) + 1);
			}
			else
				Session::set($sessionTag, 1);

				$id = $id . $suffix;
		}

		$newItem->Title = _t(
			'CMSMain.NEWPAGE',
			"New {pagetype}",'followed by a page type title',
			array('pagetype' => singleton($className)->i18n_singular_name())
		);
		$newItem->ClassName = $className;
		$newItem->ParentID = $parentID;

		// DataObject::fieldExists only checks the current class, not the hierarchy
		// This allows the CMS to set the correct sort value
		if($newItem->castingHelper('Sort')) {
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
		}

		if($setID) $newItem->ID = $id;

		# Some modules like subsites add extra fields that need to be set when the new item is created
		$this->extend('augmentNewSiteTreeItem', $newItem);

		return $newItem;
	}

	/**
	 * Delete the page from live. This means a page in draft mode might still exist.
	 *
	 * @see delete()
	 */
	public function deletefromlive($data, $form) {
		Versioned::reading_stage('Live');
		$record = DataObject::get_by_id("SiteTree", $data['ID']);
		if($record && !($record->canDelete() && $record->canDeleteFromLive())) return Security::permissionFailure($this);

		$descRemoved = '';
		$descendantsRemoved = 0;
		$recordTitle = $record->Title;
		$recordID = $record->ID;

		// before deleting the records, get the descendants of this tree
		if($record) {
			$descendantIDs = $record->getDescendantIDList();

			// then delete them from the live site too
			$descendantsRemoved = 0;
			foreach( $descendantIDs as $descID )
				if( $descendant = DataObject::get_by_id('SiteTree', $descID) ) {
					$descendant->doDeleteFromLive();
					$descendantsRemoved++;
				}

			// delete the record
			$record->doDeleteFromLive();
		}

		Versioned::reading_stage('Stage');

		if(isset($descendantsRemoved)) {
			$descRemoved = ' ' . _t(
				'CMSMain.DESCREMOVED',
				'and {count} descendants',
				array('count' => $descendantsRemoved)
			);
		} else {
			$descRemoved = '';
		}

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(
				_t(
					'CMSMain.REMOVED',
					'Deleted \'{title}\'{description} from live site',
					array('title' => $recordTitle, 'description' => $descRemoved)
				)
			)
		);

		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * Actually perform the publication step
	 */
	public function performPublish($record) {
		if($record && !$record->canPublish()) return Security::permissionFailure($this);

		$record->doPublish();
	}

	/**
 	 * Reverts a page by publishing it to live.
 	 * Use {@link restorepage()} if you want to restore a page
 	 * which was deleted from draft without publishing.
 	 *
 	 * @uses SiteTree->doRevertToLive()
	 */
	public function revert($data, $form) {
		if(!isset($data['ID'])) return new SS_HTTPResponse("Please pass an ID in the form content", 400);

		$id = (int) $data['ID'];
		$restoredPage = Versioned::get_latest_version("SiteTree", $id);
		if(!$restoredPage) 	return new SS_HTTPResponse("SiteTree #$id not found", 400);

		$record = Versioned::get_one_by_stage('SiteTree', 'Live', array(
			'"SiteTree_Live"."ID"' => $id
		));

		// a user can restore a page without publication rights, as it just adds a new draft state
		// (this action should just be available when page has been "deleted from draft")
		if($record && !$record->canEdit()) return Security::permissionFailure($this);
		if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);

		$record->doRevertToLive();

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(_t(
				'CMSMain.RESTORED',
				"Restored '{title}' successfully",
				'Param %s is a title',
				array('title' => $record->Title)
			))
		);

		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * Delete the current page from draft stage.
	 * @see deletefromlive()
	 */
	public function delete($data, $form) {
		Deprecation::notice('4.0', 'Delete from stage is deprecated. Use archive instead');
		$id = $data['ID'];
		$record = DataObject::get_by_id("SiteTree", $id);
		if($record && !$record->canDelete()) return Security::permissionFailure();
		if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);

		// Delete record
		$record->delete();

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
		);

		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * Delete this page from both live and stage
	 *
	 * @param type $data
	 * @param type $form
	 */
	public function archive($data, $form) {
		$id = $data['ID'];
		$record = DataObject::get_by_id("SiteTree", $id);
		if(!$record || !$record->exists()) {
			throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
		}
		if(!$record->canArchive()) {
			return Security::permissionFailure();
		}

		// Archive record
		$record->doArchive();

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
		);

		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	public function publish($data, $form) {
		$data['publish'] = '1';

		return $this->save($data, $form);
	}

	public function unpublish($data, $form) {
		$className = $this->stat('tree_class');
		$record = DataObject::get_by_id($className, $data['ID']);

		if($record && !$record->canDeleteFromLive()) return Security::permissionFailure($this);
		if(!$record || !$record->ID) throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);

		$record->doUnpublish();

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(_t('CMSMain.REMOVEDPAGE',"Removed '{title}' from the published site", array('title' => $record->Title)))
		);

		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * @return array
	 */
	public function rollback() {
		return $this->doRollback(array(
			'ID' => $this->currentPageID(),
			'Version' => $this->getRequest()->param('VersionID')
		), null);
	}

	/**
	 * Rolls a site back to a given version ID
	 *
	 * @param array
	 * @param Form
	 *
	 * @return html
	 */
	public function doRollback($data, $form) {
		$this->extend('onBeforeRollback', $data['ID']);

		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;

		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
		if($record && !$record->canEdit()) return Security::permissionFailure($this);

		if($version) {
			$record->doRollbackTo($version);
			$message = _t(
				'CMSMain.ROLLEDBACKVERSIONv2',
				"Rolled back to version #%d.",
				array('version' => $data['Version'])
			);
		} else {
			$record->doRollbackTo('Live');
			$message = _t(
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
			);
		}

		$this->getResponse()->addHeader('X-Status', rawurlencode($message));

		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
		// The X-Pjax header forces a "full" content refresh on redirect.
		$url = Controller::join_links(singleton('CMSPageEditController')->Link('show'), $record->ID);
		$this->getResponse()->addHeader('X-ControllerURL', $url);
		$this->getRequest()->addHeader('X-Pjax', 'Content');
		$this->getResponse()->addHeader('X-Pjax', 'Content');

		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	/**
	 * Batch Actions Handler
	 */
	public function batchactions() {
		return new CMSBatchActionHandler($this, 'batchactions');
	}

	public function BatchActionParameters() {
		$batchActions = CMSBatchActionHandler::config()->batch_actions;

		$forms = array();
		foreach($batchActions as $urlSegment => $batchAction) {
			$SNG_action = singleton($batchAction);
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
				$formHtml = '';
				foreach($fieldset as $field) {
					$formHtml .= $field->Field();
				}
				$forms[$urlSegment] = $formHtml;
			}
		}
		$pageHtml = '';
		foreach($forms as $urlSegment => $html) {
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
		}
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
	}
	/**
	 * Returns a list of batch actions
	 */
	public function BatchActionList() {
		return $this->batchactions()->batchActionList();
	}

	public function buildbrokenlinks($request) {
		// Protect against CSRF on destructive action
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);

		increase_time_limit_to();
		increase_memory_limit_to();

		if($this->urlParams['ID']) {
			$newPageSet[] = DataObject::get_by_id("Page", $this->urlParams['ID']);
		} else {
			$pages = DataObject::get("Page");
			foreach($pages as $page) $newPageSet[] = $page;
			$pages = null;
		}

		$content = new HtmlEditorField('Content');
		$download = new HtmlEditorField('Download');

		foreach($newPageSet as $i => $page) {
			$page->HasBrokenLink = 0;
			$page->HasBrokenFile = 0;

			$content->setValue($page->Content);
			$content->saveInto($page);

			$download->setValue($page->Download);
			$download->saveInto($page);

			echo "<li>$page->Title (link:$page->HasBrokenLink, file:$page->HasBrokenFile)";

			$page->writeWithoutVersion();
			$page->destroy();
			$newPageSet[$i] = null;
		}
	}

	public function publishall($request) {
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);

		increase_time_limit_to();
		increase_memory_limit_to();

		$response = "";

		if(isset($this->requestParams['confirm'])) {
			// Protect against CSRF on destructive action
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);

			$start = 0;
			$pages = DataObject::get("SiteTree", "", "", "", "$start,30");
			$count = 0;
			while($pages) {
				foreach($pages as $page) {
					if($page && !$page->canPublish()) return Security::permissionFailure($this);

					$page->doPublish();
					$page->destroy();
					unset($page);
					$count++;
					$response .= "<li>$count</li>";
				}
				if($pages->Count() > 29) {
					$start += 30;
					$pages = DataObject::get("SiteTree", "", "", "", "$start,30");
				} else {
					break;
				}
			}
			$response .= _t('CMSMain.PUBPAGES',"Done: Published {count} pages", array('count' => $count));

		} else {
			$token = SecurityToken::inst();
			$fields = new FieldList();
			$token->updateFieldSet($fields);
			$tokenField = $fields->First();
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
			$publishAllDescription = _t(
				'CMSMain.PUBALLFUN2',
				'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
				. 'It\'s intended to be used after there have been massive edits of the content, such as when '
				. 'the site was first built.'
			);
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
				<p>' . $publishAllDescription . '</p>
				<form method="post" action="publishall">
					<input type="submit" name="confirm" value="'
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
					. $tokenHtml .
				'</form>';
		}

		return $response;
	}

	/**
	 * Restore a completely deleted page from the SiteTree_versions table.
	 */
	public function restore($data, $form) {
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
			return new SS_HTTPResponse("Please pass an ID in the form content", 400);
		}

		$id = (int)$data['ID'];
		$restoredPage = Versioned::get_latest_version("SiteTree", $id);
		if(!$restoredPage) 	return new SS_HTTPResponse("SiteTree #$id not found", 400);

		$restoredPage = $restoredPage->doRestoreToStage();

		$this->getResponse()->addHeader(
			'X-Status',
			rawurlencode(_t(
				'CMSMain.RESTORED',
				"Restored '{title}' successfully",
				array('title' => $restoredPage->Title)
			))
		);

		return $this->getResponseNegotiator()->respond($this->getRequest());
	}

	public function duplicate($request) {
		// Protect against CSRF on destructive action
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);

		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
			$page = DataObject::get_by_id("SiteTree", $id);
			if($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
				return Security::permissionFailure($this);
			}
			if(!$page || !$page->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);

			$newPage = $page->duplicate();

			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
				$newPage->ParentID = $_GET['parentID'];
				$newPage->write();
			}

			$this->getResponse()->addHeader(
				'X-Status',
				rawurlencode(_t(
					'CMSMain.DUPLICATED',
					"Duplicated '{title}' successfully",
					array('title' => $newPage->Title)
				))
			);
			$url = Controller::join_links(singleton('CMSPageEditController')->Link('show'), $newPage->ID);
			$this->getResponse()->addHeader('X-ControllerURL', $url);
			$this->getRequest()->addHeader('X-Pjax', 'Content');
			$this->getResponse()->addHeader('X-Pjax', 'Content');

			return $this->getResponseNegotiator()->respond($this->getRequest());
		} else {
			return new SS_HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
		}
	}

	public function duplicatewithchildren($request) {
		// Protect against CSRF on destructive action
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
		increase_time_limit_to();
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
			$page = DataObject::get_by_id("SiteTree", $id);
			if($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
				return Security::permissionFailure($this);
			}
			if(!$page || !$page->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);

			$newPage = $page->duplicateWithChildren();

			$this->getResponse()->addHeader(
				'X-Status',
				rawurlencode(_t(
					'CMSMain.DUPLICATEDWITHCHILDREN',
					"Duplicated '{title}' and children successfully",
					array('title' => $newPage->Title)
				))
			);
			$url = Controller::join_links(singleton('CMSPageEditController')->Link('show'), $newPage->ID);
			$this->getResponse()->addHeader('X-ControllerURL', $url);
			$this->getRequest()->addHeader('X-Pjax', 'Content');
			$this->getResponse()->addHeader('X-Pjax', 'Content');

			return $this->getResponseNegotiator()->respond($this->getRequest());
		} else {
			return new SS_HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
		}
	}

	public function providePermissions() {
		$title = _t("CMSPagesController.MENUTITLE", LeftAndMain::menu_title_for_class('CMSPagesController'));
		return array(
			"CMS_ACCESS_CMSMain" => array(
				'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
				'help' => _t(
					'CMSMain.ACCESS_HELP',
					'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
				),
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
			)
		);
	}

}