Version: current

       1 <?php
       2 /**
       3  * The main "content" area of the CMS.
       4  * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
       5  * admin menu.
       6  *
       7  * @package cms
       8  * @subpackage content
       9  * @todo Create some base classes to contain the generic functionality that will be replicated.
      10  */
      11 class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider {
      12
      13     static $url_segment = '';
      14
      15     static $url_rule = '/$Action/$ID/$OtherID';
      16
      17     // Maintain a lower priority than other administration sections
      18     // so that Director does not think they are actions of CMSMain
      19     static $url_priority = 40;
      20
      21     static $menu_title = 'Pages';
      22
      23     static $menu_priority = 10;
      24
      25     static $tree_class = "SiteTree";
      26
      27     static $subitem_class = "Member";
      28
      29     static $allowed_actions = array(
      30         'addmember',
      31         'addpage',
      32         'buildbrokenlinks',
      33         'canceldraftchangesdialog',
      34         'compareversions',
      35         'createtranslation',
      36         'delete',
      37         'deletefromlive',
      38         'deleteitems',
      39         'DeleteItemsForm',
      40         'dialog',
      41         'duplicate',
      42         'duplicatewithchildren',
      43         'getpagecount',
      44         'getversion',
      45         'publishall',
      46         'publishitems',
      47         'PublishItemsForm',
      48         'restore',
      49         'revert',
      50         'rollback',
      51         'sidereport',
      52         'submit',
      53         'unpublish',
      54         'versions',
      55         'EditForm',
      56         'AddPageOptionsForm',
      57         'SiteTreeAsUL',
      58         'getshowdeletedsubtree',
      59         'getfilteredsubtree',
      60         'batchactions'
      61     );
      62
      63     static function T_SiteTreeFilterOptions(){
      64         return array(
      65             'Title' => _t('CMSMain.TITLEOPT', 'Title', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
      66             'MenuTitle' => _t('CMSMain.MENUTITLEOPT', 'Navigation Label', 0, 'The dropdown title in CMSMain left SiteTreeFilterOptions'),
      67             'ClassName' => _t('CMSMain.PAGETYPEOPT', 'Page Type'),
      68             'Status' => _t('CMSMain.STATUSOPT', 'Status',  0, "The dropdown title in CMSMain left SiteTreeFilterOptions"),
      69             'MetaDescription' => _t('CMSMain.METADESCOPT', 'Description', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions"),
      70             'MetaKeywords' => _t('CMSMain.METAKEYWORDSOPT', 'Keywords', 0, "The dropdown title in CMSMain left SiteTreeFilterOptions")
      71         );
      72     }
      73
      74     public function init() {
      75         parent::init();
      76
      77         // Locale" attribute is either explicitly added by LeftAndMain Javascript logic,
      78         // or implied on a translated record (see {@link Translatable->updateCMSFields()}).
      79         // $Lang serves as a "context" which can be inspected by Translatable - hence it
      80         // has the same name as the database property on Translatable.
      81         if($this->getRequest()->requestVar("Locale")) {
      82             $this->Locale = $this->getRequest()->requestVar("Locale");
      83         } elseif($this->getRequest()->requestVar("locale")) {
      84             $this->Locale = $this->getRequest()->requestVar("locale");
      85         } else {
      86             $this->Locale = Translatable::default_locale();
      87         }
      88         Translatable::set_current_locale($this->Locale);
      89
      90         // collect languages for TinyMCE spellchecker plugin.
      91         // see http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/spellchecker
      92         $langName = i18n::get_locale_name($this->Locale);
      93         HtmlEditorConfig::get('cms')->setOption('spellchecker_languages', "+{$langName}={$this->Locale}");
      94
      95         Requirements::javascript(CMS_DIR . '/javascript/CMSMain.js');
      96         Requirements::javascript(CMS_DIR . '/javascript/CMSMain_left.js');
      97         Requirements::javascript(CMS_DIR . '/javascript/CMSMain_right.js');
      98     }
      99
     100     /**
     101      * If this is set to true, the "switchView" context in the
     102      * template is shown, with links to the staging and publish site.
     103      *
     104      * @return boolean
     105      */
     106     function ShowSwitchView() {
     107         return true;
     108     }
     109
     110     /**
     111      * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
     112      * to switch view also for archived versions.
     113      */
     114     function SwitchView($page = null) {
     115         if(!$page) {
     116             $page = $this->currentPage();
     117         }
     118
     119         if($page) {
     120             $nav = SilverStripeNavigator::get_for_record($page);
     121             Requirements::customScript("window.name = windowName('cms');");
     122             return $nav['items'];
     123         }
     124     }
     125
     126     //------------------------------------------------------------------------------------------//
     127     // Main controllers
     128
     129     //------------------------------------------------------------------------------------------//
     130     // Main UI components
     131
     132     /**
     133      * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
     134      *
     135      * @return string
     136      */
     137     public function Link($action = null) {
     138         return Controller::join_links(
     139             $this->stat('url_base', true),
     140             $this->stat('url_segment', true), // in case we want to change the segment
     141             '/', // trailing slash needed if $action is null!
     142             "$action"
     143         );
     144     }
     145
     146     /**
     147      * Return the entire site tree as a nested set of ULs
     148      */
     149     public function SiteTreeAsUL() {
     150         $this->generateDataTreeHints();
     151         $this->generateTreeStylingJS();
     152
     153         // Pre-cache sitetree version numbers for querying efficiency
     154         Versioned::prepopulate_versionnumber_cache("SiteTree", "Stage");
     155         Versioned::prepopulate_versionnumber_cache("SiteTree", "Live");
     156
     157         return $this->getSiteTreeFor("SiteTree");
     158     }
     159
     160     public function getCMSTreeTitle() {
     161         return SiteConfig::current_site_config()->i18n_singular_name();
     162     }
     163
     164     /**
     165      * Use a CMSSiteTreeFilter to only get certain nodes
     166      *
     167      * @return string
     168      */
     169     public function getfilteredsubtree() {
     170         // Sanity and security checks
     171         if (!isset($_REQUEST['filter'])) die('No filter passed');
     172         if (!ClassInfo::exists($_REQUEST['filter'])) die ('That filter class does not exist');
     173         if (!is_subclass_of($_REQUEST['filter'], 'CMSSiteTreeFilter')) die ('That is not a valid filter');
     174
     175         // Do eeet!
     176         $filter = new $_REQUEST['filter']();
     177         return $filter->getTree();
     178     }
     179
     180     /**
     181      * Returns a list of batch actions
     182      */
     183     function SiteTreeFilters() {
     184         $filters = ClassInfo::subclassesFor('CMSSiteTreeFilter');
     185         array_shift($filters);
     186         $doSet = new DataObjectSet();
     187         $doSet->push(new ArrayData(array(
     188             'ClassName' => 'all',
     189             'Title' => _t('CMSSiteTreeFilter.ALL', 'All items')
     190         )));
     191         foreach($filters as $filter) {
     192             if (call_user_func(array($filter, 'showInList'))) {
     193                 $doSet->push(new ArrayData(array(
     194                     'ClassName' => $filter,
     195                     'Title' => call_user_func(array($filter, 'title'))
     196                 )));
     197             }
     198         }
     199         return $doSet;
     200     }
     201
     202     /**
     203      * Returns the SiteTree columns that can be filtered using the the Site Tree Search button as a DataObjectSet
     204      */
     205     public function SiteTreeFilterOptions() {
     206         $filter_options = new DataObjectSet();
     207         foreach(self::T_SiteTreeFilterOptions() as $key => $value) {
     208             $record = array(
     209                 'Column' => $key,
     210                 'Title' => $value,
     211             );
     212             $filter_options->push(new ArrayData($record));
     213         }
     214         return $filter_options;
     215     }
     216         public function SiteTreeFilterDateField() {
     217             $dateField = new DateField('SiteTreeFilterDate');
     218             $dateField->setConfig('showcalendar', true);
     219             return $dateField->Field();
     220         }
     221         public function SiteTreeFilterPageTypeField() {
     222             $types = SiteTree::page_type_classes();
     223             $source = array_combine($types, $types); // FIXME use i18n names
     224             $source['All']= _t('CMSMain.AllClasses', 'All');
     225             ksort($source);
     226             $optionsetField = new DropdownField('ClassName', 'ClassName', $source, 'Any');
     227             return $optionsetField->Field();
     228         }
     229
     230     public function generateDataTreeHints() {
     231         $classes = ClassInfo::subclassesFor( $this->stat('tree_class') );
     232
     233         $def['Root'] = array();
     234
     235         foreach($classes as $class) {
     236             $obj = singleton($class);
     237             if($obj instanceof HiddenClass) continue;
     238
     239             $allowedChildren = $obj->allowedChildren();
     240             if($allowedChildren != "none")  $def[$class]['allowedChildren'] = $allowedChildren;
     241             $def[$class]['defaultChild'] = $obj->defaultChild();
     242             $def[$class]['defaultParent'] = ($obj->defaultParent() && isset(SiteTree::get_by_link($obj->defaultParent())->ID)) ? SiteTree::get_by_link($obj->defaultParent())->ID : null;
     243
     244             if(is_array($allowedChildren)) foreach($allowedChildren as $allowedChild) {
     245                 $def[$allowedChild]['allowedParents'][] = $class;
     246             }
     247
     248             if($obj->stat('can_be_root')) {
     249                 $def['Root']['allowedChildren'][] = $class;
     250             }
     251         }
     252
     253         // Put data hints into a script tag at the top
     254         Requirements::customScript("siteTreeHints = " . $this->jsDeclaration($def) . ";");
     255     }
     256
     257     public function generateTreeStylingJS() {
     258         $classes = ClassInfo::subclassesFor('SiteTree');
     259         foreach($classes as $class) {
     260             $obj = singleton($class);
     261             if($obj instanceof HiddenClass) continue;
     262             if($icon = $obj->stat('icon')) $iconInfo[$class] = $icon;
     263         }
     264         $iconInfo['BrokenLink'] = 'cms/images/treeicons/brokenlink';
     265
     266
     267         $js = "var _TREE_ICONS = [];\n";
     268
     269
     270         foreach($iconInfo as $class => $icon) {
     271             // SiteTree::$icon can be set to array($icon, $option)
     272             // $option can be "file" or "folder" to force the icon to always be the file or the folder form
     273             $option = null;
     274             if(is_array($icon)) list($icon, $option) = $icon;
     275
     276             $fileImage = ($option == "folder") ? $icon . '-closedfolder.gif' : $icon . '-file.gif';
     277             $openFolderImage = $icon . '-openfolder.gif';
     278             if(!Director::fileExists($openFolderImage) || $option == "file") $openFolderImage = $fileImage;
     279             $closedFolderImage = $icon . '-closedfolder.gif';
     280             if(!Director::fileExists($closedFolderImage) || $option == "file") $closedFolderImage = $fileImage;
     281
     282             $js .= <<<JS
     283                 _TREE_ICONS['$class'] = {
     284                     fileIcon: '$fileImage',
     285                     openFolderIcon: '$openFolderImage',
     286                     closedFolderIcon: '$closedFolderImage'
     287                 };
     288
     289 JS;
     290         }
     291
     292         Requirements::customScript($js);
     293     }
     294
     295     /**
     296      * Return a javascript instanciation of this array
     297      */
     298     protected function jsDeclaration($array) {
     299         $parts = array();
     300         if(is_array($array)) {
     301             $object = false;
     302             foreach(array_keys($array) as $key) {
     303                 if(!is_numeric($key)) {
     304                     $object = true;
     305                     break;
     306                 }
     307             }
     308
     309             if($object) {
     310                 foreach($array as $k => $v) {
     311                     $parts[] = "$k : " . $this->jsDeclaration($v);
     312                 }
     313                 return " {\n " . implode(", \n", $parts) . " }\n";
     314             } else {
     315                 foreach($array as $part) $parts[] = $this->jsDeclaration($part);
     316                 return " [ " . implode(", ", $parts) . " ]\n";
     317             }
     318         } else {
     319             return "'" . addslashes($array) . "'";
     320         }
     321     }
     322
     323     /**
     324      * Populates an array of classes in the CMS
     325      * which allows the user to change the page type.
     326      *
     327      * @return DataObjectSet
     328      */
     329     public function PageTypes() {
     330         $classes = SiteTree::page_type_classes();
     331
     332         $result = new DataObjectSet();
     333
     334         foreach($classes as $class) {
     335             $instance = singleton($class);
     336
     337             if($instance instanceof HiddenClass) continue;
     338
     339             if(!$instance->canCreate()) continue;
     340
     341             // skip this type if it is restricted
     342             if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) continue;
     343
     344             $addAction = $instance->i18n_singular_name();
     345
     346             // if we're in translation mode, the link between the translated pagetype
     347             // title and the actual classname might not be obvious, so we add it in parantheses
     348             // Example: class "RedirectorPage" has the title "Weiterleitung" in German,
     349             // so it shows up as "Weiterleitung (RedirectorPage)"
     350             if (Director::isDev()) {
     351                 $addAction .= " ({$class})";
     352             }
     353
     354             $result->push(new ArrayData(array(
     355                 'ClassName' => $class,
     356                 'AddAction' => $addAction,
     357                 'Sort' => ($class == 'DocPage') ? '0' : $addAction, // DocPage first
     358             )));
     359         }
     360
     361         $result->sort('Sort');
     362
     363         return $result;
     364     }
     365
     366     /**
     367      * Save the current sites {@link SiteConfig} into the database
     368      *
     369      * @param array $data
     370      * @param Form $form
     371      * @return FormResponse
     372      */
     373     function save_siteconfig($data, $form) {
     374         $siteConfig = SiteConfig::current_site_config();
     375         $form->saveInto($siteConfig);
     376         $siteConfig->write();
     377         FormResponse::status_message('Saved site configuration', "good");
     378         FormResponse::add("$('Form_EditForm').resetElements();");
     379
     380         return FormResponse::respond();
     381     }
     382     /**
     383      * Get a database record to be managed by the CMS
     384      */
     385     public function getRecord($id, $className = NULL) {
     386
     387         $treeClass = $this->stat('tree_class');
     388
     389         if($id && is_numeric($id)) {
     390             $record = DataObject::get_one( $treeClass, "\"$treeClass\".\"ID\" = $id");
     391
     392             // Then, try getting a record from the live site
     393             if(!$record) {
     394                 // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
     395                 Versioned::reading_stage('Live');
     396                 singleton($treeClass)->flushCache();
     397
     398                 $record = DataObject::get_one( $treeClass, "\"$treeClass\".\"ID\" = $id");
     399                 if($record) Versioned::set_reading_mode('');
     400             }
     401
     402             // Then, try getting a deleted record
     403             if(!$record) {
     404                 $record = Versioned::get_latest_version($treeClass, $id);
     405             }
     406
     407             // Don't open a page from a different locale
     408             /** The record's Locale is saved in database in 2.4, and not related with Session,
     409              *  we should not check their locale matches the Translatable::get_current_locale,
     410              *  here as long as we all the HTTPRequest is init with right locale.
     411              *  This bit breaks the all FileIFrameField functions if the field is used in CMS
     412              *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
     413              */
     414             /* if($record && SS_Object::has_extension('SiteTree', 'Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
     415                 $record = null;
     416             }*/
     417
     418             return $record;
     419
     420         } else if(substr($id,0,3) == 'new') {
     421             return $this->getNewItem($id);
     422         }
     423     }
     424
     425     public function getEditForm($id) {
     426         $record = $this->getRecord($id);
     427
     428         if($record) {
     429             if($record->IsDeletedFromStage) $record->Status = _t('CMSMain.REMOVEDFD',"Removed from the draft site");
     430
     431             $fields = $record->getCMSFields();
     432             if ($fields == null) {
     433                 user_error("getCMSFields returned null on a '".get_class($record)."' object - it should return a FieldSet object. Perhaps you forgot to put a return statement at the end of your method?", E_USER_ERROR);
     434             }
     435             $fields->push($idField = new HiddenField("ID"));
     436             $fields->push($liveURLField = new HiddenField("LiveURLSegment"));
     437             $fields->push($stageURLField = new HiddenField("StageURLSegment"));
     438
     439             /*if( substr($record->ID, 0, 3 ) == 'new' )*/
     440             $fields->push(new HiddenField('Sort','', $record->Sort ));
     441
     442             $idField->setValue($id);
     443
     444             if($record->ID && is_numeric( $record->ID ) ) {
     445                 $liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', "\"SiteTree\".\"ID\" = $record->ID");
     446                 if($liveRecord) $liveURLField->setValue($liveRecord->AbsoluteLink());
     447             }
     448
     449             if(!$record->IsDeletedFromStage) {
     450                 $stageURLField->setValue($record->AbsoluteLink());
     451             }
     452
     453             // getAllCMSActions can be used to completely redefine the action list
     454             if($record->hasMethod('getAllCMSActions')) {
     455                 $actions = $record->getAllCMSActions();
     456             } else {
     457                 $actions = $record->getCMSActions();
     458             }
     459
     460             // Add a default or custom validator.
     461             // @todo Currently the default Validator.js implementation
     462             //  adds javascript to the document body, meaning it won't
     463             //  be included properly if the associated fields are loaded
     464             //  through ajax. This means only serverside validation
     465             //  will kick in for pages+validation loaded through ajax.
     466             //  This will be solved by using less obtrusive javascript validation
     467             //  in the future, see http://open.silverstripe.com/ticket/2915 and http://open.silverstripe.com/ticket/3386
     468             if($record->hasMethod('getCMSValidator')) {
     469                 $validator = $record->getCMSValidator();
     470             } else {
     471                 $validator = new RequiredFields();
     472             }
     473
     474             // The clientside (mainly LeftAndMain*.js) rely on ajax responses
     475             // which can be evaluated as javascript, hence we need
     476             // to override any global changes to the validation handler.
     477             $validator->setJavascriptValidationHandler('prototype');
     478
     479             $form = new Form($this, "EditForm", $fields, $actions, $validator);
     480             $form->loadDataFrom($record);
     481             $form->disableDefaultAction();
     482
     483             if(!$record->canEdit() || $record->IsDeletedFromStage) {
     484                 $readonlyFields = $form->Fields()->makeReadonly();
     485                 $form->setFields($readonlyFields);
     486             }
     487
     488             $this->extend('updateEditForm', $form);
     489
     490             return $form;
     491         } if ($id == 0 || $id == 'root') {
     492             return $this->RootForm();
     493         } else if($id) {
     494             return new Form($this, "EditForm", new FieldSet(
     495                 new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldSet());
     496
     497         }
     498     }
     499
     500     /**
     501      * @return Form
     502      */
     503     function RootForm() {
     504         $siteConfig = SiteConfig::current_site_config();
     505         $fields = $siteConfig->getCMSFields();
     506         if(SS_Object::has_extension('SiteConfig',"Translatable")){
     507             $fields->push(new HiddenField('Locale','', $siteConfig->Locale ));
     508         }
     509         $form = new Form($this, "EditForm", $fields, $siteConfig->getCMSActions());
     510         $form->loadDataFrom($siteConfig);
     511
     512         $this->extend('updateEditForm', $form);
     513
     514         return $form;
     515     }
     516
     517     //------------------------------------------------------------------------------------------//
     518     // Data saving handlers
     519
     520
     521     public function addpage() {
     522                 // FIX singleton error. Inxo
     523                 $className = 'DocPage';
     524                 if(isset($_REQUEST['PageType']) && $_REQUEST['PageType']!='' && class_exists($_REQUEST['PageType'])){
     525                     $className = $_REQUEST['PageType'];
     526                 }
     527         //$className = isset($_REQUEST['PageType']) ? $_REQUEST['PageType'] : "Page";
     528                 // end FIX
     529         $parent = isset($_REQUEST['ParentID']) ? $_REQUEST['ParentID'] : 0;
     530         $suffix = isset($_REQUEST['Suffix']) ? "-" . $_REQUEST['Suffix'] : null;
     531
     532         if(!$parent && isset($_REQUEST['Parent'])) {
     533             $page = SiteTree::get_by_link($_REQUEST['Parent']);
     534             if($page) $parent = $page->ID;
     535         }
     536
     537         if(is_numeric($parent)) $parentObj = DataObject::get_by_id("SiteTree", $parent);
     538         if(!$parentObj || !$parentObj->ID) $parent = 0;
     539
     540         if($parentObj){
     541             if(!$parentObj->canAddChildren()) return Security::permissionFailure($this);
     542             if(!singleton($className)->canCreate()) return Security::permissionFailure($this);
     543         }else{
     544             if(!SiteConfig::current_site_config()->canCreateTopLevel())
     545                 return Security::permissionFailure($this);
     546         }
     547
     548         $p = $this->getNewItem("new-$className-$parent".$suffix, false);
     549         $p->Locale = $_REQUEST['Locale'];
     550         $p->write();
     551
     552         return $this->returnItemToUser($p);
     553     }
     554
     555     /**
     556      * @uses LeftAndMainDecorator->augmentNewSiteTreeItem()
     557      */
     558     public function getNewItem($id, $setID = true) {
     559         list($dummy, $className, $parentID, $suffix) = array_pad(explode('-',$id),4,null);
     560
     561         $newItem = new $className();
     562
     563         if( !$suffix ) {
     564             $sessionTag = "NewItems." . $parentID . "." . $className;
     565             if(Session::get($sessionTag)) {
     566                 $suffix = '-' . Session::get($sessionTag);
     567                 Session::set($sessionTag, Session::get($sessionTag) + 1);
     568             }
     569             else
     570                 Session::set($sessionTag, 1);
     571
     572                 $id = $id . $suffix;
     573         }
     574
     575 //      if (!$newItem->Title) $newItem->Title = _t('CMSMain.NEW',"New ",PR_MEDIUM,'"New " followed by a className').singleton($className)->i18n_singular_name();
     576         if (!$newItem->Title) $newItem->Title = singleton($className)->i18n_singular_name();
     577         if (!$newItem->URLSegment) $newItem->URLSegment = "new-" . strtolower($className);
     578         $newItem->ClassName = $className;
     579         $newItem->ParentID = $parentID;
     580
     581         // DataObject::fieldExists only checks the current class, not the hierarchy
     582         // This allows the CMS to set the correct sort value
     583         if($newItem->castingHelper('Sort')) {
     584             $newItem->Sort = DB::query("SELECT MAX(\"Sort\") FROM \"SiteTree\" WHERE \"ParentID\" = '" . Convert::raw2sql($parentID) . "'")->value() + 1;
     585         }
     586
     587         if($setID) $newItem->ID = $id;
     588
     589         # Some modules like subsites add extra fields that need to be set when the new item is created
     590         $this->extend('augmentNewSiteTreeItem', $newItem);
     591
     592         return $newItem;
     593     }
     594
     595     /**
     596      * Delete the page from live. This means a page in draft mode might still exist.
     597      *
     598      * @see delete()
     599      */
     600     public function deletefromlive($urlParams, $form) {
     601         $id = $_REQUEST['ID'];
     602         Versioned::reading_stage('Live');
     603         $record = DataObject::get_by_id("SiteTree", $id);
     604         if($record && !($record->canDelete() && $record->canDeleteFromLive())) return Security::permissionFailure($this);
     605
     606         $descRemoved = '';
     607         $descendantsRemoved = 0;
     608
     609         // before deleting the records, get the descendants of this tree
     610         if($record) {
     611             $descendantIDs = $record->getDescendantIDList();
     612
     613             // then delete them from the live site too
     614             $descendantsRemoved = 0;
     615             foreach( $descendantIDs as $descID )
     616                 if( $descendant = DataObject::get_by_id('SiteTree', $descID) ) {
     617                     $descendant->doDeleteFromLive();
     618                     $descendantsRemoved++;
     619                 }
     620
     621             // delete the record
     622             $record->doDeleteFromLive();
     623         }
     624
     625         Versioned::reading_stage('Stage');
     626
     627         if(isset($descendantsRemoved)) {
     628             $descRemoved = " and $descendantsRemoved descendants";
     629             $descRemoved = sprintf(' '._t('CMSMain.DESCREMOVED', 'and %s descendants'), $descendantsRemoved);
     630         } else {
     631             $descRemoved = '';
     632         }
     633
     634         FormResponse::add($this->deleteTreeNodeJS($record));
     635         FormResponse::status_message(sprintf(_t('CMSMain.REMOVED', 'Deleted \'%s\'%s from live site'), $record->Title, $descRemoved), 'good');
     636
     637         return FormResponse::respond();
     638     }
     639
     640     /**
     641      * Actually perform the publication step
     642      */
     643     public function performPublish($record) {
     644         if($record && !$record->canPublish()) return Security::permissionFailure($this);
     645
     646         $record->doPublish();
     647     }
     648
     649     /**
     650      * Reverts a page by publishing it to live.
     651      * Use {@link restorepage()} if you want to restore a page
     652      * which was deleted from draft without publishing.
     653      *
     654      * @uses SiteTree->doRevertToLive()
     655      */
     656     public function revert($urlParams, $form) {
     657         $id = (int)$_REQUEST['ID'];
     658         $record = Versioned::get_one_by_stage('SiteTree', 'Live', "\"SiteTree_Live\".\"ID\" = '{$id}'");
     659
     660         // a user can restore a page without publication rights, as it just adds a new draft state
     661         // (this action should just be available when page has been "deleted from draft")
     662         if(isset($record) && $record && !$record->canEdit()) return Security::permissionFailure($this);
     663
     664         $record->doRevertToLive();
     665
     666         $title = Convert::raw2js($record->Title);
     667         FormResponse::get_page($id);
     668         FormResponse::add("$('sitetree').setNodeTitle($id, '$title');");
     669         FormResponse::status_message(sprintf(_t('CMSMain.RESTORED',"Restored '%s' successfully",PR_MEDIUM,'Param %s is a title'),$title),'good');
     670
     671         return FormResponse::respond();
     672     }
     673
     674     /**
     675      * Delete the current page from draft stage.
     676      * @see deletefromlive()
     677      */
     678     public function delete($data, $form) {
     679         $record = DataObject::get_one(
     680             "SiteTree",
     681             sprintf("\"SiteTree\".\"ID\" = %d", Convert::raw2sql($data['ID']))
     682         );
     683         if($record && !$record->canDelete()) return Security::permissionFailure();
     684
     685         // save ID and delete record
     686         $recordID = $record->ID;
     687         $record->delete();
     688
     689         if(Director::is_ajax()) {
     690             // need a valid ID value even if the record doesn't have one in the database
     691             // (its still present in the live tables)
     692             $liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', "\"SiteTree_Live\".\"ID\" = $recordID");
     693             // if the page has never been published to live, we need to act the same way as in deletefromlive()
     694             if($liveRecord) {
     695                 // the form is readonly now, so we need to refresh the representation
     696                 FormResponse::get_page($recordID);
     697                 return $this->tellBrowserAboutPublicationChange($liveRecord, sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"),$record->Title));
     698             } else {
     699                 FormResponse::add($this->deleteTreeNodeJS($record));
     700                 FormResponse::status_message(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"),$record->Title), 'good');
     701                 return FormResponse::respond();
     702             }
     703         } else {
     704             Director::redirectBack();
     705         }
     706     }
     707
     708     function SideReports() {
     709         return SS_Report::get_reports('SideReport');
     710     }
     711
     712     /*
     713      * Return a dropdown for selecting reports
     714      */
     715     function ReportSelector() {
     716         // $options[""] = _t('CMSMain.CHOOSEREPORT',"(Choose a report)");
     717         foreach($this->SideReports() as $report) {
     718             if($report->canView()) {
     719                 $options[$report->group()][$report->sort()][$report->ID()] = $report->title();
     720             }
     721         }
     722
     723         $finalOptions = array();
     724         foreach($options as $group => $weights) {
     725             ksort($weights);
     726             foreach($weights as $weight => $reports) {
     727                 foreach($reports as $class => $report) {
     728                     $finalOptions[$group][$class] = $report;
     729                 }
     730             }
     731         }
     732
     733         return new GroupedDropdownField("ReportSelector", _t('CMSMain.REPORT', 'Report'),$finalOptions);
     734     }
     735
     736     /**
     737      * Generate the parameter HTML for SideReports that have params
     738      *
     739      * @return LiteralField
     740      */
     741     function ReportFormParameters() {
     742         $forms = array();
     743         foreach($this->SideReports() as $report) {
     744             if ($report->canView()) {
     745                 if ($fieldset = $report->parameterFields()) {
     746                     $formHtml = '';
     747                     foreach($fieldset as $field) {
     748                         $formHtml .= $field->FieldHolder();
     749                     }
     750                     $forms[$report->ID()] = $formHtml;
     751                 }
     752             }
     753         }
     754         $pageHtml = '';
     755         foreach($forms as $class => $html) {
     756             $pageHtml .= "<div id=\"SideReportForm_$class\" style=\"display:none\">$html</div>\n\n";
     757         }
     758         return new LiteralField("ReportFormParameters", '<div id="SideReportForms" style="display:none">'.$pageHtml.'</div>');
     759     }
     760
     761     /**
     762      * Get the content for a side report
     763      */
     764     function sidereport() {
     765         $reportClass = $this->urlParams['ID'];
     766         $reports = $this->SideReports();
     767         if(isset($reports[$reportClass])) {
     768             $report = $reports[$reportClass];
     769             if($report) {
     770                 $view = new SideReportView($this, $report);
     771                 $view->setParameters($this->request->requestVars());
     772                 return $view->forTemplate();
     773             } else {
     774                 return false;
     775             }
     776         }
     777     }
     778
     779
     780     /**
     781      * Get the versions of the current page
     782      */
     783     function versions() {
     784         $pageID = $this->urlParams['ID'];
     785         $page = $this->getRecord($pageID);
     786         if($page) {
     787             $versions = $page->allVersions($_REQUEST['unpublished'] ? "" : "\"SiteTree\".\"WasPublished\" = 1");
     788             return array(
     789                 'Versions' => $versions,
     790             );
     791         } else {
     792             return sprintf(_t('CMSMain.VERSIONSNOPAGE',"Can't find page #%d",PR_LOW),$pageID);
     793         }
     794     }
     795
     796     /**
     797      * Roll a page back to a previous version
     798      */
     799     function rollback() {
     800         if(isset($_REQUEST['Version']) && (bool)$_REQUEST['Version']) {
     801             $this->extend('onBeforeRollback', $_REQUEST['ID']);
     802             $record = $this->performRollback($_REQUEST['ID'], $_REQUEST['Version']);
     803             echo sprintf(_t('CMSMain.ROLLEDBACKVERSION',"Rolled back to version #%d.  New version number is #%d"),$_REQUEST['Version'],$record->Version);
     804         } else {
     805             $record = $this->performRollback($_REQUEST['ID'], "Live");
     806             echo sprintf(_t('CMSMain.ROLLEDBACKPUB',"Rolled back to published version. New version number is #%d"),$record->Version);
     807         }
     808     }
     809
     810     function unpublish() {
     811         $SQL_id = Convert::raw2sql($_REQUEST['ID']);
     812
     813         $page = DataObject::get_by_id("SiteTree", $SQL_id);
     814
     815         if($page && !$page->canDeleteFromLive()) return Security::permissionFailure($this);
     816
     817         $page->doUnpublish();
     818
     819         return $this->tellBrowserAboutPublicationChange($page, sprintf(_t('CMSMain.REMOVEDPAGE',"Removed '%s' from the published site"),$page->Title));
     820     }
     821
     822     /**
     823      * Return a few pieces of information about a change to a page
     824      *  - Send the new status message
     825      *  - Update the action buttons
     826      *  - Update the treenote
     827      *  - Send a status message
     828      */
     829     function tellBrowserAboutPublicationChange($page, $statusMessage) {
     830         $JS_title = Convert::raw2js($page->TreeTitle());
     831
     832         $JS_stageURL = $page->IsDeletedFromStage ? '' : Convert::raw2js($page->AbsoluteLink());
     833         $liveRecord = Versioned::get_one_by_stage('SiteTree', 'Live', "\"SiteTree\".\"ID\" = $page->ID");
     834
     835         $JS_liveURL = $liveRecord ? Convert::raw2js($liveRecord->AbsoluteLink()) : '';
     836
     837         FormResponse::add($this->getActionUpdateJS($page));
     838         FormResponse::update_status($page->Status);
     839
     840         if($JS_stageURL || $JS_liveURL) {
     841             FormResponse::add("\$('sitetree').setNodeTitle($page->ID, '$JS_title');");
     842         } else {
     843             FormResponse::add("var node = $('sitetree').getTreeNodeByIdx('$page->ID');");
     844             FormResponse::add("if(node && node.parentTreeNode) node.parentTreeNode.removeTreeNode(node);");
     845             FormResponse::add("$('Form_EditForm').reloadIfSetTo($page->ID);");
     846         }
     847
     848         if($statusMessage) FormResponse::status_message($statusMessage, 'good');
     849         FormResponse::add("$('Form_EditForm').elements.StageURLSegment.value = '$JS_stageURL';");
     850         FormResponse::add("$('Form_EditForm').elements.LiveURLSegment.value = '$JS_liveURL';");
     851         FormResponse::add("$('Form_EditForm').notify('PagePublished', $('Form_EditForm').elements.ID.value);");
     852
     853         return FormResponse::respond();
     854     }
     855
     856     function performRollback($id, $version) {
     857         $record = DataObject::get_by_id($this->stat('tree_class'), $id);
     858         if($record && !$record->canEdit()) return Security::permissionFailure($this);
     859
     860         $record->doRollbackTo($version);
     861         return $record;
     862     }
     863
     864     function getversion() {
     865         $id = $this->urlParams['ID'];
     866         $version = str_replace('&ajax=1','',$this->urlParams['OtherID']);
     867         $record = Versioned::get_version("SiteTree", $id, $version);
     868
     869         if($record) {
     870             if($record && !$record->canView()) return Security::permissionFailure($this);
     871             $fields = $record->getCMSFields();
     872             $fields->removeByName("Status");
     873
     874             $fields->push(new HiddenField("ID"));
     875             $fields->push(new HiddenField("Version"));
     876
     877             $versionAuthor = DataObject::get_by_id('Member', $record->AuthorID);
     878             if(!$versionAuthor) $versionAuthor = new ArrayData(array('Title' => 'Unknown author'));
     879             $fields->insertBefore(
     880                 new LiteralField(
     881                     'YouAreViewingHeader',
     882                     '<p class="message notice">' .
     883                     sprintf(
     884                         _t(
     885                             'CMSMain.VIEWING',
     886                             "You are viewing version #%s, created %s by %s",
     887                             PR_MEDIUM,
     888                             'Version number is a linked string, created is a relative time (e.g. 2 days ago), by a specific author'
     889                         ),
     890                         "<a href=\"admin/getversion/$record->ID/$version\" title=\"" . ($versionAuthor ? $versionAuthor->Title : '') . "\">$version</a>",
     891                         $record->obj('LastEdited')->Ago(),
     892                         ($versionAuthor ? $versionAuthor->Title : '')
     893                     ) .
     894                     '</p>'
     895                 ),
     896                 'Root'
     897             );
     898
     899             $actions = new FieldSet(
     900                 new FormAction("email", _t('CMSMain.EMAIL',"Email")),
     901                 new FormAction("rollback", _t('CMSMain.ROLLBACK',"Roll back to this version"))
     902             );
     903
     904             // encode the message to appear in the body of the email
     905             $archiveURL = Director::absoluteBaseURL() . $record->URLSegment . '?archiveDate=' . $record->obj('LastEdited')->URLDatetime();
     906
     907             // Ensure that source file comments are disabled
     908             SSViewer::set_source_file_comments(false);
     909
     910             $archiveEmailMessage = urlencode( $this->customise( array( 'ArchiveDate' => $record->obj('LastEdited'), 'ArchiveURL' => $archiveURL ) )->renderWith( 'ViewArchivedEmail' ) );
     911             $archiveEmailMessage = preg_replace( '/\+/', '%20', $archiveEmailMessage );
     912
     913             $fields->push( new HiddenField( 'ArchiveEmailMessage', '', $archiveEmailMessage ) );
     914             $fields->push( new HiddenField( 'ArchiveEmailSubject', '', preg_replace( '/\+/', '%20', urlencode( 'Archived version of ' . $record->Title ) ) ) );
     915             $fields->push( new HiddenField( 'ArchiveURL', '', $archiveURL ) );
     916
     917             $form = new Form($this, "EditForm", $fields, $actions);
     918             $form->loadDataFrom($record);
     919             $form->loadDataFrom(array(
     920                 "ID" => $id,
     921                 "Version" => $version,
     922             ));
     923
     924             // historical version shouldn't be editable
     925             $readonlyFields = $form->Fields()->makeReadonly();
     926             $form->setFields($readonlyFields);
     927
     928             $templateData = $this->customise(array(
     929                 "EditForm" => $form
     930             ));
     931
     932             SSViewer::setOption('rewriteHashlinks', false);
     933
     934             if(Director::is_ajax()) {
     935                 $result = $templateData->renderWith($this->class . '_right');
     936                 $parts = preg_split('!</?form[^>]*>!', $result);
     937                 $content = $parts[sizeof($parts)-2];
     938                 if($this->ShowSwitchView()) {
     939                     $content .= '<div id="AjaxSwitchView">' . $this->SwitchView($record) . '</div>';
     940                 }
     941                 return $content;
     942             } else {
     943                 return $templateData->renderWith('LeftAndMain');
     944             }
     945         }
     946     }
     947
     948     function compareversions() {
     949         $id = (int)$this->urlParams['ID'];
     950         $version1 = (int)$_REQUEST['From'];
     951         $version2 = (int)$_REQUEST['To'];
     952
     953         if( $version1 > $version2 ) {
     954             $toVersion = $version1;
     955             $fromVersion = $version2;
     956         } else {
     957             $toVersion = $version2;
     958             $fromVersion = $version1;
     959         }
     960
     961         $page = DataObject::get_by_id("SiteTree", $id);
     962         if($page && !$page->canView()) return Security::permissionFailure($this);
     963
     964         $record = $page->compareVersions($fromVersion, $toVersion);
     965
     966         $fromVersionRecord = Versioned::get_version('SiteTree', $id, $fromVersion);
     967         $toVersionRecord = Versioned::get_version('SiteTree', $id, $toVersion);
     968         if(!$fromVersionRecord) user_error("Can't find version $fromVersion of page $id", E_USER_ERROR);
     969         if(!$toVersionRecord) user_error("Can't find version $toVersion of page $id", E_USER_ERROR);
     970
     971         if($record) {
     972             $fromDateNice = $fromVersionRecord->obj('LastEdited')->Ago();
     973             $toDateNice = $toVersionRecord->obj('LastEdited')->Ago();
     974             $fromAuthor = DataObject::get_by_id('Member', $fromVersionRecord->AuthorID);
     975             if(!$fromAuthor) $fromAuthor = new ArrayData(array('Title' => 'Unknown author'));
     976             $toAuthor = DataObject::get_by_id('Member', $toVersionRecord->AuthorID);
     977             if(!$toAuthor) $toAuthor = new ArrayData(array('Title' => 'Unknown author'));
     978
     979             $fields = $record->getCMSFields();
     980             $fields->push(new HiddenField("ID"));
     981             $fields->push(new HiddenField("Version"));
     982             $fields->insertBefore(
     983                 new LiteralField(
     984                     'YouAreComparingHeader',
     985                     '<p class="message notice">' .
     986                     sprintf(
     987                         _t('CMSMain.COMPARINGV',"Comparing versions %s and %s"),
     988                         "<a href=\"admin/getversion/$id/$fromVersionRecord->Version\" title=\"$fromAuthor->Title\">$fromVersionRecord->Version</a> <small>($fromDateNice)</small>",
     989                         "<a href=\"admin/getversion/$id/$toVersionRecord->Version\" title=\"$toAuthor->Title\">$toVersionRecord->Version</a> <small>($toDateNice)</small>"
     990                     ) .
     991                     '</p>'
     992                 ),
     993                 "Root"
     994             );
     995
     996             $actions = new FieldSet();
     997
     998             $form = new Form($this, "EditForm", $fields, $actions);
     999             $form->loadDataFrom($record);
    1000             $form->loadDataFrom(array(
    1001                 "ID" => $id,
    1002                 "Version" => $fromVersion,
    1003             ));
    1004
    1005             // comparison views shouldn't be editable
    1006             $readonlyFields = $form->Fields()->makeReadonly();
    1007             $form->setFields($readonlyFields);
    1008
    1009             foreach($form->Fields()->dataFields() as $field) {
    1010                 $field->dontEscape = true;
    1011             }
    1012
    1013             return $this->sendFormToBrowser(array(
    1014                 "EditForm" => $form
    1015             ));
    1016         }
    1017     }
    1018
    1019     function sendFormToBrowser($templateData) {
    1020         if(Director::is_ajax()) {
    1021             SSViewer::setOption('rewriteHashlinks', false);
    1022             $result = $this->customise($templateData)->renderWith($this->class . '_right');
    1023             $parts = preg_split('!</?form[^>]*>!', $result);
    1024             return $parts[sizeof($parts)-2];
    1025         } else {
    1026             return array(
    1027                 "Right" => $this->customise($templateData)->renderWith($this->class . '_right'),
    1028             );
    1029         }
    1030     }
    1031
    1032     function dialog() {
    1033         Requirements::clear();
    1034
    1035         $buttons = new DataObjectSet;
    1036         if($_REQUEST['Buttons']) foreach($_REQUEST['Buttons'] as $button) {
    1037             list($name, $title) = explode(',',$button,2);
    1038             $buttons->push(new ArrayData(array(
    1039                 "Name" => $name,
    1040                 "Title" => $title,
    1041             )));
    1042         }
    1043
    1044         return array(
    1045             "Message" => htmlentities($_REQUEST['Message']),
    1046             "Buttons" => $buttons,
    1047             "Modal" => $_REQUEST['Modal'] ? true : false,
    1048         );
    1049     }
    1050
    1051     function canceldraftchangesdialog() {
    1052         Requirements::clear();
    1053         Requirements::css(CMS_DIR . 'css/dialog.css');
    1054         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
    1055         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
    1056         Requirements::javascript(SAPPHIRE_DIR . '/javascript/prototype_improvements.js');
    1057         Requirements::javascript(CMS_DIR . '/javascript/dialog.js');
    1058
    1059         $message = _t('CMSMain.COPYPUBTOSTAGE',"Do you really want to copy the published content to the stage site?");
    1060         $buttons = "<button name=\"OK\">" . _t('CMSMain.OK','OK') ."</button><button name=\"Cancel\">" . _t('CMSMain.CANCEL',"Cancel") . "</button>";
    1061
    1062         return $this->customise( array(
    1063             'Message' => $message,
    1064             'Buttons' => $buttons,
    1065             'DialogType' => 'alert'
    1066         ))->renderWith('Dialog');
    1067     }
    1068
    1069     /**
    1070      * Batch Actions Handler
    1071      */
    1072     function batchactions() {
    1073         return new CMSBatchActionHandler($this, 'batchactions');
    1074     }
    1075
    1076     /**
    1077      * @return Form
    1078      */
    1079     public function PublishItemsForm() {
    1080         $form = new Form(
    1081             $this,
    1082             'PublishItemsForm',
    1083             new FieldSet(
    1084                 new HiddenField('csvIDs'),
    1085                 new CheckboxField('ShowDrafts', _t('CMSMain_left.ss.SHOWONLYCHANGED','Show only changed pages'))
    1086             ),
    1087             new FieldSet(
    1088                 new FormAction('publishitems', _t('CMSMain_left.ss.PUBLISHCONFIRM','Publish the selected pages'))
    1089             )
    1090         );
    1091         $form->addExtraClass('actionparams');
    1092         return $form;
    1093     }
    1094
    1095     function BatchActionParameters() {
    1096         $batchActions = CMSBatchActionHandler::$batch_actions;
    1097
    1098         $forms = array();
    1099         foreach($batchActions as $urlSegment => $batchAction) {
    1100             $SNG_action = singleton($batchAction);
    1101             if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
    1102                 $formHtml = '';
    1103                 foreach($fieldset as $field) {
    1104                     $formHtml .= $field->Field();
    1105                 }
    1106                 $forms[$urlSegment] = $formHtml;
    1107             }
    1108         }
    1109         $pageHtml = '';
    1110         foreach($forms as $urlSegment => $html) {
    1111             $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
    1112         }
    1113         return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
    1114     }
    1115     /**
    1116      * Returns a list of batch actions
    1117      */
    1118     function BatchActionList() {
    1119         return $this->batchactions()->batchActionList();
    1120     }
    1121
    1122     /**
    1123      * @return Form
    1124      */
    1125     function DeleteItemsForm() {
    1126         $form = new Form(
    1127             $this,
    1128             'DeleteItemsForm',
    1129             new FieldSet(
    1130                 new LiteralField('SelectedPagesNote',
    1131                     sprintf('<p>%s</p>', _t('CMSMain_left.ss.SELECTPAGESACTIONS','Select the pages that you want to change &amp; then click an action:'))
    1132                 ),
    1133                 new HiddenField('csvIDs')
    1134             ),
    1135             new FieldSet(
    1136                 new FormAction('deleteitems', _t('CMSMain_left.ss.DELETECONFIRM','Delete the selected pages'))
    1137             )
    1138         );
    1139         $form->addExtraClass('actionparams');
    1140         return $form;
    1141     }
    1142
    1143     function buildbrokenlinks() {
    1144         if($this->urlParams['ID']) {
    1145             $newPageSet[] = DataObject::get_by_id("Page", $this->urlParams['ID']);
    1146         } else {
    1147             $pages = DataObject::get("Page");
    1148             foreach($pages as $page) $newPageSet[] = $page;
    1149             $pages = null;
    1150         }
    1151
    1152         $content = new HtmlEditorField('Content');
    1153         $download = new HtmlEditorField('Download');
    1154
    1155         foreach($newPageSet as $i => $page) {
    1156             $page->HasBrokenLink = 0;
    1157             $page->HasBrokenFile = 0;
    1158
    1159             $lastUsage = (memory_get_usage() - $lastPoint);
    1160             $lastPoint = memory_get_usage();
    1161             $content->setValue($page->Content);
    1162             $content->saveInto($page);
    1163
    1164             $download->setValue($page->Download);
    1165             $download->saveInto($page);
    1166
    1167             echo "<li>$page->Title (link:$page->HasBrokenLink, file:$page->HasBrokenFile)";
    1168
    1169             $page->writeWithoutVersion();
    1170             $page->destroy();
    1171             $newPageSet[$i] = null;
    1172         }
    1173     }
    1174
    1175     function AddPageOptionsForm() {
    1176         $pageTypes = array();
    1177
    1178         foreach( $this->PageTypes() as $arrayData ) {
    1179             $pageTypes[$arrayData->getField('ClassName')] = $arrayData->getField('AddAction');
    1180         }
    1181
    1182         $fields = new FieldSet(
    1183             new HiddenField("ParentID"),
    1184             new HiddenField("Locale", 'Locale', Translatable::get_current_locale()),
    1185             new DropdownField("PageType", "", $pageTypes, 'Page')
    1186         );
    1187
    1188         $this->extend('updatePageOptions', $fields);
    1189
    1190         $actions = new FieldSet(
    1191             new FormAction("addpage", _t('CMSMain.GO',"Go"))
    1192         );
    1193
    1194         return new Form($this, "AddPageOptionsForm", $fields, $actions);
    1195     }
    1196
    1197     /**
    1198      * Helper function to get page count
    1199      */
    1200     function getpagecount() {
    1201         ini_set('max_execution_time', 0);
    1202         $excludePages = preg_split("! *, *!", $_GET['exclude']);
    1203
    1204         $pages = DataObject::get("SiteTree", "\"ParentID\" = 0");
    1205         foreach($pages as $page) $pageArr[] = $page;
    1206
    1207         while(list($i,$page) = each($pageArr)) {
    1208             if(!in_array($page->URLSegment, $excludePages)) {
    1209                 if($children = $page->AllChildren()) {
    1210                     foreach($children as $child) $pageArr[] = $child;
    1211                 }
    1212
    1213
    1214                 if(!$_GET['onlywithcontent'] || strlen(Convert::xml2raw($page->Content)) > 100) {
    1215                     echo "<li>" . $page->Breadcrumbs(null, true) . "</li>";
    1216                     $count++;
    1217                 } else {
    1218                     echo "<li style=\"color: #777\">" . $page->Breadcrumbs(null, true) . " - " . _t('CMSMain.NOCONTENT',"no content") . "</li>";
    1219                 }
    1220
    1221             }
    1222         }
    1223
    1224         echo '<p>' . _t('CMSMain.TOTALPAGES',"Total pages: ") . "$count</p>";
    1225     }
    1226
    1227     function publishall() {
    1228         ini_set("memory_limit", -1);
    1229         ini_set('max_execution_time', 0);
    1230
    1231         $response = "";
    1232
    1233         if(isset($this->requestParams['confirm'])) {
    1234             $start = 0;
    1235             $pages = DataObject::get("SiteTree", "", "", "", "$start,30");
    1236             $count = 0;
    1237             if($pages){
    1238                 while(true) {
    1239                     foreach($pages as $page) {
    1240                         if($page && !$page->canPublish()) return Security::permissionFailure($this);
    1241
    1242                         $page->doPublish();
    1243                         $page->destroy();
    1244                         unset($page);
    1245                         $count++;
    1246                         $response .= "<li>$count</li>";
    1247                     }
    1248                     if($pages->Count() > 29) {
    1249                         $start += 30;
    1250                         $pages = DataObject::get("SiteTree", "", "", "", "$start,30");
    1251                     } else {
    1252                         break;
    1253                     }
    1254                 }
    1255             }
    1256             $response .= sprintf(_t('CMSMain.PUBPAGES',"Done: Published %d pages"), $count);
    1257
    1258         } else {
    1259             $response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
    1260                 <p>' . _t('CMSMain.PUBALLFUN2', 'Pressing this button will do the equivalent of going to every page and pressing "publish".  It\'s
    1261                 intended to be used after there have been massive edits of the content, such as when the site was
    1262                 first built.') . '</p>
    1263                 <form method="post" action="publishall">
    1264                     <input type="submit" name="confirm" value="'
    1265                     . _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",PR_LOW,'Confirmation button') .'" />
    1266                 </form>';
    1267         }
    1268
    1269         return $response;
    1270     }
    1271
    1272     /**
    1273      * Restore a completely deleted page from the SiteTree_versions table.
    1274      */
    1275     function restore() {
    1276         if(($id = $_REQUEST['ID']) && is_numeric($id)) {
    1277             $restoredPage = Versioned::get_latest_version("SiteTree", $id);
    1278             if($restoredPage) {
    1279                 $restoredPage = $restoredPage->doRestoreToStage();
    1280
    1281                 FormResponse::get_page($id);
    1282                 $title = Convert::raw2js($restoredPage->TreeTitle());
    1283                 FormResponse::add("$('sitetree').setNodeTitle($id, '$title');");
    1284                 FormResponse::status_message(sprintf(_t('CMSMain.RESTORED',"Restored '%s' successfully",PR_MEDIUM,'Param %s is a title'),$title),'good');
    1285                 return FormResponse::respond();
    1286
    1287             } else {
    1288                 return new SS_HTTPResponse("SiteTree #$id not found", 400);
    1289             }
    1290         } else {
    1291             return new SS_HTTPResponse("Please pass an ID in the form content", 400);
    1292         }
    1293     }
    1294
    1295     function duplicate() {
    1296         if(($id = $this->urlParams['ID']) && is_numeric($id)) {
    1297             $page = DataObject::get_by_id("SiteTree", $id);
    1298             if($page && (!$page->canEdit() || !$page->canCreate())) {
    1299                 return Security::permissionFailure($this);
    1300             }
    1301
    1302             $newPage = $page->duplicate();
    1303
    1304             // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
    1305             if($_GET['parentID'] && is_numeric($_GET['parentID'])) {
    1306                 $newPage->ParentID = $_GET['parentID'];
    1307                 $newPage->write();
    1308             }
    1309
    1310             return $this->returnItemToUser($newPage);
    1311         } else {
    1312             user_error("CMSMain::duplicate() Bad ID: '$id'", E_USER_WARNING);
    1313         }
    1314     }
    1315
    1316     function duplicatewithchildren() {
    1317         if(($id = $this->urlParams['ID']) && is_numeric($id)) {
    1318             $page = DataObject::get_by_id("SiteTree", $id);
    1319             if($page && (!$page->canEdit() || !$page->canCreate())) {
    1320                 return Security::permissionFailure($this);
    1321             }
    1322
    1323             $newPage = $page->duplicateWithChildren();
    1324
    1325             return $this->returnItemToUser($newPage);
    1326         } else {
    1327             user_error("CMSMain::duplicate() Bad ID: '$id'", E_USER_WARNING);
    1328         }
    1329     }
    1330
    1331
    1332
    1333     /**
    1334      * Create a new translation from an existing item, switch to this language and reload the tree.
    1335      */
    1336     function createtranslation () {
    1337         $langCode = Convert::raw2sql($_REQUEST['newlang']);
    1338         $originalLangID = (int)$_REQUEST['ID'];
    1339
    1340         $record = $this->getRecord($originalLangID);
    1341
    1342         $this->Locale = $langCode;
    1343         Translatable::set_current_locale($langCode);
    1344
    1345         // Create a new record in the database - this is different
    1346         // to the usual "create page" pattern of storing the record
    1347         // in-memory until a "save" is performed by the user, mainly
    1348         // to simplify things a bit.
    1349         // @todo Allow in-memory creation of translations that don't persist in the database before the user requests it
    1350         $translatedRecord = $record->createTranslation($langCode);
    1351
    1352         $url = sprintf(
    1353             "%s/%d/?locale=%s",
    1354             $this->Link('show'),
    1355             $translatedRecord->ID,
    1356             $langCode
    1357         );
    1358         FormResponse::add(sprintf('window.location.href = "%s";', $url));
    1359         return FormResponse::respond();
    1360     }
    1361
    1362     /**
    1363      * Provide the permission codes used by LeftAndMain.
    1364      * Can't put it on LeftAndMain since that's an abstract base class.
    1365      */
    1366     function providePermissions() {
    1367         $classes = ClassInfo::subclassesFor('LeftAndMain');
    1368
    1369         foreach($classes as $i => $class) {
    1370             $title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class));
    1371             $perms["CMS_ACCESS_" . $class] = array(
    1372                 'name' => sprintf(_t(
    1373                     'CMSMain.ACCESS',
    1374                     "Access to '%s' section",
    1375                     PR_MEDIUM,
    1376                     "Item in permission selection identifying the admin section. Example: Access to 'Files & Images'"
    1377                 ), $title, null),
    1378                 'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
    1379             );
    1380         }
    1381         $perms["CMS_ACCESS_LeftAndMain"] = array(
    1382             'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
    1383             'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
    1384             'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
    1385             'sort' => -100
    1386         );
    1387         $perms['CMS_ACCESS_CMSMain']['help'] = _t(
    1388             'CMSMain.ACCESS_HELP',
    1389             '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".'
    1390         );
    1391         $perms['CMS_ACCESS_SecurityAdmin']['help'] = _t(
    1392             'SecurityAdmin.ACCESS_HELP',
    1393             'Allow viewing, adding and editing users, as well as assigning permissions and roles to them.'
    1394         );
    1395
    1396         if (isset($perms['CMS_ACCESS_ModelAdmin'])) unset($perms['CMS_ACCESS_ModelAdmin']);
    1397
    1398         return $perms;
    1399     }
    1400
    1401     /**
    1402      * Returns all languages with languages already used appearing first.
    1403      * Called by the SSViewer when rendering the template.
    1404      */
    1405     function LangSelector() {
    1406         $member = Member::currentUser();
    1407         $dropdown = new LanguageDropdownField(
    1408             'LangSelector',
    1409             'Language',
    1410             array(),
    1411             'SiteTree',
    1412             'Locale-English',
    1413             singleton('SiteTree')
    1414         );
    1415         $dropdown->setValue(Translatable::get_current_locale());
    1416         return $dropdown;
    1417     }
    1418
    1419     /**
    1420      * Determine if there are more than one languages in our site tree.
    1421      *
    1422      * @return boolean
    1423      */
    1424     function MultipleLanguages() {
    1425         $langs = Translatable::get_existing_content_languages('SiteTree');
    1426
    1427         return (count($langs) > 1);
    1428     }
    1429
    1430     /**
    1431      * @return boolean
    1432      */
    1433     function IsTranslatableEnabled() {
    1434         return SS_Object::has_extension('SiteTree', 'Translatable');
    1435     }
    1436 }
    1437
    1438 /**
    1439  * @package cms
    1440  * @subpackage content
    1441  */
    1442 class CMSMainMarkingFilter {
    1443
    1444     function __construct() {
    1445         $this->ids = array();
    1446         $this->expanded = array();
    1447
    1448         $where = array();
    1449
    1450         // Match against URLSegment, Title, MenuTitle & Content
    1451         if (isset($_REQUEST['SiteTreeSearchTerm'])) {
    1452             $term = Convert::raw2sql($_REQUEST['SiteTreeSearchTerm']);
    1453             $where[] = "\"URLSegment\" LIKE '%$term%' OR \"Title\" LIKE '%$term%' OR \"MenuTitle\" LIKE '%$term%' OR \"Content\" LIKE '%$term%'";
    1454         }
    1455
    1456         // Match against date
    1457         if (isset($_REQUEST['SiteTreeFilterDate'])) {
    1458             $date = $_REQUEST['SiteTreeFilterDate'];
    1459             $date = ((int)substr($date,6,4)) . '-' . ((int)substr($date,3,2)) . '-' . ((int)substr($date,0,2));
    1460             $where[] = "\"LastEdited\" > '$date'";
    1461         }
    1462
    1463         // Match against exact ClassName
    1464         if (isset($_REQUEST['ClassName']) && $_REQUEST['ClassName'] != 'All') {
    1465             $klass = Convert::raw2sql($_REQUEST['ClassName']);
    1466             $where[] = "\"ClassName\" = '$klass'";
    1467         }
    1468
    1469         // Partial string match against a variety of fields
    1470         foreach (CMSMain::T_SiteTreeFilterOptions() as $key => $value) {
    1471             if (!empty($_REQUEST[$key])) {
    1472                 $match = Convert::raw2sql($_REQUEST[$key]);
    1473                 $where[] = "\"$key\" LIKE '%$match%'";
    1474             }
    1475         }
    1476
    1477         $where = empty($where) ? '' : 'WHERE (' . implode(') AND (',$where) . ')';
    1478
    1479         $parents = array();
    1480
    1481         /* Do the actual search */
    1482         $res = DB::query('SELECT "ParentID", "ID" FROM "SiteTree" '.$where);
    1483         if (!$res) return;
    1484
    1485         /* And keep a record of parents we don't need to get parents of themselves, as well as IDs to mark */
    1486         foreach($res as $row) {
    1487             if ($row['ParentID']) $parents[$row['ParentID']] = true;
    1488             $this->ids[$row['ID']] = true;
    1489         }
    1490
    1491         /* We need to recurse up the tree, finding ParentIDs for each ID until we run out of parents */
    1492         while (!empty($parents)) {
    1493             $res = DB::query('SELECT "ParentID", "ID" FROM "SiteTree" WHERE "ID" in ('.implode(',',array_keys($parents)).')');
    1494             $parents = array();
    1495
    1496             foreach($res as $row) {
    1497                 if ($row['ParentID']) $parents[$row['ParentID']] = true;
    1498                 $this->ids[$row['ID']] = true;
    1499                 $this->expanded[$row['ID']] = true;
    1500             }
    1501         }
    1502     }
    1503
    1504     function mark($node) {
    1505         $id = $node->ID;
    1506         if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
    1507         return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
    1508     }
    1509 }
    1510
    1511 ?>
    1512

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -