Version: current

       1 <?php
       2 /**
       3  * Basic data-object representing all pages within the site tree.
       4  * This data-object takes care of the heirachy.  All page types that live within the heirachy
       5  * should inherit from this.
       6  *
       7  * In addition, it contains a number of static methods for querying the site tree.
       8  * @package cms
       9  */
      10 class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvider {
      11
      12     /**
      13      * Indicates what kind of children this page type can have.
      14      * This can be an array of allowed child classes, or the string "none" -
      15      * indicating that this page type can't have children.
      16      * If a classname is prefixed by "*", such as "*Page", then only that
      17      * class is allowed - no subclasses. Otherwise, the class and all its
      18      * subclasses are allowed.
      19      *
      20      * @var array
      21      */
      22     static $allowed_children = array("SiteTree");
      23
      24     /**
      25      * The default child class for this page.
      26      *
      27      * @var string
      28      */
      29     static $default_child = "Page";
      30
      31     /**
      32      * The default parent class for this page.
      33      *
      34      * @var string
      35      */
      36     static $default_parent = null;
      37
      38     /**
      39      * Controls whether a page can be in the root of the site tree.
      40      *
      41      * @var bool
      42      */
      43     static $can_be_root = true;
      44
      45     /**
      46      * List of permission codes a user can have to allow a user to create a
      47      * page of this type.
      48      *
      49      * @var array
      50      */
      51     static $need_permission = null;
      52
      53     /**
      54      * If you extend a class, and don't want to be able to select the old class
      55      * in the cms, set this to the old class name. Eg, if you extended Product
      56      * to make ImprovedProduct, then you would set $hide_ancestor to Product.
      57      *
      58      * @var string
      59      */
      60     static $hide_ancestor = null;
      61
      62     static $db = array(
      63         "URLSegment" => "Varchar(255)",
      64         "Title" => "Varchar(255)",
      65         "MenuTitle" => "Varchar(100)",
      66         "Content" => "HTMLText",
      67         "MetaTitle" => "Varchar(255)",
      68         "MetaDescription" => "Text",
      69         "MetaKeywords" => "Varchar(255)",
      70         "ExtraMeta" => "HTMLText",
      71         "ShowInMenus" => "Boolean",
      72         "ShowInSearch" => "Boolean",
      73         "HomepageForDomain" => "Varchar(100)",
      74         "ProvideComments" => "Boolean",
      75         "Sort" => "Int",
      76         "HasBrokenFile" => "Boolean",
      77         "HasBrokenLink" => "Boolean",
      78         "Status" => "Varchar",
      79         "ReportClass" => "Varchar",
      80         "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
      81         "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
      82
      83         // Simple task tracking
      84         "ToDo" => "Text",
      85         'NumberCMSChildren' => 'Int', // by dvp
      86     );
      87
      88     static $indexes = array(
      89         "LastEdited" => true,
      90         "URLSegment" => true,
      91     );
      92
      93     static $has_many = array(
      94         "Comments" => "PageComment"
      95     );
      96
      97     static $many_many = array(
      98         "LinkTracking" => "SiteTree",
      99         "ViewerGroups" => "Group",
     100         "EditorGroups" => "Group",
     101     );
     102
     103     static $belongs_many_many = array(
     104         "BackLinkTracking" => "SiteTree"
     105     );
     106
     107     static $many_many_extraFields = array(
     108         "LinkTracking" => array("FieldName" => "Varchar"),
     109     );
     110
     111     static $casting = array(
     112         "Breadcrumbs" => "HTMLText",
     113         "LastEdited" => "SS_Datetime",
     114         "Created" => "SS_Datetime",
     115     );
     116
     117     static $defaults = array(
     118         "ShowInMenus" => 1,
     119         "ShowInSearch" => 1,
     120         "Status" => "New page",
     121         "CanViewType" => "Inherit",
     122         "CanEditType" => "Inherit"
     123     );
     124
     125     static $has_one = array(
     126         "Parent" => "SiteTree"
     127     );
     128
     129     static $versioning = array(
     130         "Stage",  "Live"
     131     );
     132
     133     static $default_sort = "\"Sort\"";
     134
     135     /**
     136      * If this is false, the class cannot be created in the CMS.
     137      * @var boolean
     138     */
     139     static $can_create = true;
     140
     141
     142     /**
     143      * Icon to use in the CMS
     144      *
     145      * This should be the base filename.  The suffixes -file.gif,
     146      * -openfolder.gif and -closedfolder.gif will be appended to the base name
     147      * that you provide there.
     148      * If you prefer, you can pass an array:
     149      * array("sapphire\thirdparty\tree\images\page", $option).
     150      * $option can be either "file" or "folder" to force the icon to always
     151      * be a file or folder, regardless of whether the page has children or not
     152      *
     153      * @var string|array
     154      */
     155     static $icon = array("sapphire/javascript/tree/images/page", "file");
     156
     157
     158     static $extensions = array(
     159         "Hierarchy",
     160         "Versioned('Stage', 'Live')",
     161     );
     162
     163     /**
     164      * Delimit breadcrumb-links generated by BreadCrumbs()
     165      *
     166      * @var string
     167      */
     168     public static $breadcrumbs_delimiter = " &raquo; ";
     169
     170     /**
     171      * Whether or not to write the homepage map for static publisher
     172      */
     173     public static $write_homepage_map = true;
     174
     175     static $searchable_fields = array(
     176         'Title',
     177         'Content',
     178     );
     179
     180     /**
     181      * @see SiteTree::nested_urls()
     182      */
     183     private static $nested_urls = false;
     184
     185     /**
     186      * This controls whether of not extendCMSFields() is called by getCMSFields.
     187      */
     188     private static $runCMSFieldsExtensions = true;
     189
     190     /**
     191      * Cache for canView/Edit/Publish/Delete permissions
     192      */
     193     public static $cache_permissions = array();
     194
     195     /**
     196      * @see SiteTree::enforce_strict_hierarchy()
     197      */
     198     private static $enforce_strict_hierarchy = true;
     199
     200     public static function set_enforce_strict_hierarchy($to) {
     201         self::$enforce_strict_hierarchy = $to;
     202     }
     203
     204     public static function get_enforce_strict_hierarchy() {
     205         return self::$enforce_strict_hierarchy;
     206     }
     207
     208     /**
     209      * Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
     210      *
     211      * @return bool
     212      */
     213     public static function nested_urls() {
     214         return self::$nested_urls;
     215     }
     216
     217     public static function enable_nested_urls() {
     218         self::$nested_urls = true;
     219     }
     220
     221     public static function disable_nested_urls() {
     222         self::$nested_urls = false;
     223     }
     224
     225     /**
     226      * Fetches the {@link SiteTree} object that maps to a link.
     227      *
     228      * If you have enabled {@link SiteTree::nested_urls()} on this site, then you can use a nested link such as
     229      * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
     230      *
     231      * Note that if no model can be found, this method will fall over to a decorated alternateGetByLink method provided
     232      * by a decorator attached to {@link SiteTree}
     233      *
     234      * @param string $link
     235      * @param bool $cache
     236      * @return SiteTree
     237      */
     238     public static function get_by_link($link, $cache = true) {
     239         if(trim($link, '/')) {
     240             $link = trim(Director::makeRelative($link), '/');
     241         } else {
     242             $link = RootURLController::get_homepage_link();
     243         }
     244
     245         $parts = Convert::raw2sql(preg_split('|/+|', $link));
     246
     247         // Grab the initial root level page to traverse down from.
     248         $URLSegment = array_shift($parts);
     249         $sitetree   = DataObject::get_one (
     250             'SiteTree', "\"URLSegment\" = '$URLSegment'" . (self::nested_urls() ? ' AND "ParentID" = 0' : ''), $cache
     251         );
     252
     253         /// Fall back on a unique URLSegment for b/c.
     254         if(!$sitetree && self::nested_urls() && $pages = DataObject::get('SiteTree', "\"URLSegment\" = '$URLSegment'")) {
     255             return ($pages->Count() == 1) ? $pages->First() : null;
     256         }
     257
     258         // Attempt to grab an alternative page from decorators.
     259         if(!$sitetree) {
     260             $parentID = self::nested_urls() ? 0 : null;
     261
     262             if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $URLSegment, $parentID)) {
     263                 foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
     264             }
     265
     266             if(!$sitetree) return false;
     267         }
     268
     269         // Check if we have any more URL parts to parse.
     270         if(!self::nested_urls() || !count($parts)) return $sitetree;
     271
     272         // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
     273         foreach($parts as $segment) {
     274             $next = DataObject::get_one (
     275                 'SiteTree', "\"URLSegment\" = '$segment' AND \"ParentID\" = $sitetree->ID", $cache
     276             );
     277
     278             if(!$next) {
     279                 $parentID = (int) $sitetree->ID;
     280
     281                 if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $segment, $parentID)) {
     282                     foreach($alternatives as $alternative) if($alternative) $next = $alternative;
     283                 }
     284
     285                 if(!$next) return false;
     286             }
     287
     288             $sitetree->destroy();
     289             $sitetree = $next;
     290         }
     291
     292         return $sitetree;
     293     }
     294
     295     /**
     296      * Return a subclass map of SiteTree
     297      * that shouldn't be hidden through
     298      * {@link SiteTree::$hide_ancestor}
     299      *
     300      * @return array
     301      */
     302     public static function page_type_classes() {
     303         $classes = ClassInfo::getValidSubClasses();
     304
     305         $baseClassIndex = array_search('SiteTree', $classes);
     306         if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
     307
     308         $kill_ancestors = array();
     309
     310         // figure out if there are any classes we don't want to appear
     311         foreach($classes as $class) {
     312             $instance = singleton($class);
     313
     314             // do any of the progeny want to hide an ancestor?
     315             if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
     316                 // note for killing later
     317                 $kill_ancestors[] = $ancestor_to_hide;
     318             }
     319         }
     320
     321         // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to requirements.
     322         if($kill_ancestors) {
     323             $kill_ancestors = array_unique($kill_ancestors);
     324             foreach($kill_ancestors as $mark) {
     325                 // unset from $classes
     326                 $idx = array_search($mark, $classes);
     327                 unset($classes[$idx]);
     328             }
     329         }
     330
     331         return $classes;
     332     }
     333
     334     /**
     335      * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
     336      *
     337      * @return string
     338      */
     339     public static function link_shortcode_handler($arguments, $content = null, $parser = null) {
     340         if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
     341
     342         if (
     343                !($page = DataObject::get_by_id('SiteTree', $arguments['id']))         // Get the current page by ID.
     344             && !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
     345             && !($page = DataObject::get_one('ErrorPage', '"ErrorCode" = \'404\''))   // Link to 404 page directly.
     346         ) {
     347              return; // There were no suitable matches at all.
     348         }
     349
     350         if($content) {
     351             return sprintf('<a href="%s">%s</a>', $page->Link(), $parser->parse($content));
     352         } else {
     353             return $page->Link();
     354         }
     355     }
     356
     357     /**
     358      * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
     359      *
     360      * @param string $action
     361      * @return string
     362      */
     363     public function Link($action = null) {
     364         return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
     365     }
     366
     367     /**
     368      * Get the absolute URL for this page, including protocol and host.
     369      *
     370      * @param string $action
     371      * @return string
     372      */
     373     public function AbsoluteLink($action = null) {
     374         if($this->hasMethod('alternateAbsoluteLink')) {
     375             return $this->alternateAbsoluteLink($action);
     376         } else {
     377             return Director::absoluteURL($this->Link($action));
     378         }
     379     }
     380
     381     /**
     382      * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
     383      *
     384      * By default, it this page is the current home page, and there is no action specified then this will return a link
     385      * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
     386      * and returned in its full form.
     387      *
     388      * @uses RootURLController::get_homepage_link()
     389      * @param string $action
     390      * @return string
     391      */
     392     public function RelativeLink($action = null) {
     393         if($this->ParentID && self::nested_urls()) {
     394             $base = $this->Parent()->RelativeLink($this->URLSegment);
     395         } else {
     396             $base = $this->URLSegment;
     397         }
     398
     399         // Unset base for homepage URLSegments in their default language.
     400         // Homepages with action parameters or in different languages
     401         // need to retain their URLSegment. We can only do this if the homepage
     402         // is on the root level.
     403         if(!$action && $base == RootURLController::get_homepage_link() && !$this->ParentID) {
     404             $base = null;
     405             if($this->hasExtension('Translatable') && $this->Locale != Translatable::default_locale()){
     406                 $base = $this->URLSegment;
     407             }
     408         }
     409
     410         if(is_string($action)) {
     411             $action = str_replace('&', '&amp;', $action);
     412         } elseif($action === true) {
     413             $action = null;
     414         }
     415
     416         return Controller::join_links($base, '/', $action);
     417     }
     418
     419     /**
     420      * Get the absolute URL for this page on the Live site.
     421      */
     422     public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
     423         $live = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $this->ID);
     424
     425         if($live) {
     426             $link = $live->AbsoluteLink();
     427
     428             if($includeStageEqualsLive) {
     429                 $link .= '?stage=Live';
     430             }
     431
     432             return $link;
     433
     434         }
     435     }
     436
     437
     438     /**
     439      * Return a CSS identifier generated from this page's link.
     440      *
     441      * @return string The URL segment
     442      */
     443     public function ElementName() {
     444         return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
     445     }
     446
     447     /**
     448      * Returns TRUE if this is the currently active page that is being used to handle a request.
     449      *
     450      * @return bool
     451      */
     452     public function isCurrent() {
     453         return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
     454     }
     455
     456     /**
     457      * Check if this page is in the currently active section (e.g. it is either current or one of it's children is
     458      * currently being viewed.
     459      *
     460      * @return bool
     461      */
     462     public function isSection() {
     463         return $this->isCurrent() || (
     464             Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
     465         );
     466     }
     467
     468     /**
     469      * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
     470      *
     471      * @return string
     472      */
     473     public function LinkOrCurrent() {
     474         return $this->isCurrent() ? 'current' : 'link';
     475     }
     476
     477     /**
     478      * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
     479      *
     480      * @return string
     481      */
     482     public function LinkOrSection() {
     483         return $this->isSection() ? 'section' : 'link';
     484     }
     485
     486     /**
     487      * Return "link", "current" or section depending on if this page is the current page, or not on the current page but
     488      * in the current section.
     489      *
     490      * @return string
     491      */
     492     public function LinkingMode() {
     493         if($this->isCurrent()) {
     494             return 'current';
     495         } elseif($this->isSection()) {
     496             return 'section';
     497         } else {
     498             return 'link';
     499         }
     500     }
     501
     502     /**
     503      * Check if this page is in the given current section.
     504      *
     505      * @param string $sectionName Name of the section to check.
     506      * @return boolean True if we are in the given section.
     507      */
     508     public function InSection($sectionName) {
     509         $page = Director::get_current_page();
     510         while($page) {
     511             if($sectionName == $page->URLSegment)
     512                 return true;
     513             $page = $page->Parent;
     514         }
     515         return false;
     516     }
     517
     518
     519     /**
     520      * Returns comments on this page. This will only show comments that
     521      * have been marked as spam if "?showspam=1" is appended to the URL.
     522      *
     523      * @return DataObjectSet Comments on this page.
     524      */
     525     public function Comments() {
     526         $spamfilter = isset($_GET['showspam']) ? '' : "AND \"IsSpam\"=0";
     527         $unmoderatedfilter = Permission::check('ADMIN') ? '' : "AND \"NeedsModeration\"=0";
     528         $comments =  DataObject::get("PageComment", "\"ParentID\" = '" . Convert::raw2sql($this->ID) . "' $spamfilter $unmoderatedfilter", "\"Created\" DESC");
     529
     530         return $comments ? $comments : new DataObjectSet();
     531     }
     532
     533         /**
     534          * Allow Comments
     535          * @return bool Permission for comments
     536          */
     537         function allowComments(){
     538             return true;
     539         }
     540     /**
     541      * Create a duplicate of this node. Doesn't affect joined data - create a
     542      * custom overloading of this if you need such behaviour.
     543      *
     544      * @return SiteTree The duplicated object.
     545      */
     546      public function duplicate($doWrite = true) {
     547
     548         $page = parent::duplicate(false);
     549         $page->Sort = 0;
     550         $this->extend('onBeforeDuplicate', $page);
     551
     552         if($doWrite) {
     553             $page->write();
     554         }
     555         $this->extend('onAfterDuplicate', $page);
     556
     557         return $page;
     558     }
     559
     560
     561     /**
     562      * Duplicates each child of this node recursively and returns the
     563      * duplicate node.
     564      *
     565      * @return SiteTree The duplicated object.
     566      */
     567     public function duplicateWithChildren() {
     568         $clone = $this->duplicate();
     569         $children = $this->AllChildren(2);
     570
     571         if($children) {
     572             foreach($children as $child) {
     573                 $childClone = method_exists($child, 'duplicateWithChildren')
     574                     ? $child->duplicateWithChildren()
     575                     : $child->duplicate();
     576                 $childClone->ParentID = $clone->ID;
     577                 $childClone->write();
     578             }
     579         }
     580
     581         return $clone;
     582     }
     583
     584
     585     /**
     586      * Duplicate this node and its children as a child of the node with the
     587      * given ID
     588      *
     589      * @param int $id ID of the new node's new parent
     590      */
     591     public function duplicateAsChild($id) {
     592         $newSiteTree = $this->duplicate();
     593         $newSiteTree->ParentID = $id;
     594         $newSiteTree->Sort = 0;
     595         $newSiteTree->write();
     596     }
     597
     598     /**
     599      * Return a breadcrumb trail to this page. Excludes "hidden" pages
     600      * (with ShowInMenus=0).
     601      *
     602      * @param int $maxDepth The maximum depth to traverse.
     603      * @param boolean $unlinked Do not make page names links
     604      * @param string $stopAtPageType ClassName of a page to stop the upwards traversal.
     605      * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
     606      * @return string The breadcrumb trail.
     607      */
     608     public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
     609         $page = $this;
     610         $parts = array();
     611         $i = 0;
     612         while(
     613             $page
     614             && (!$maxDepth || sizeof($parts) < $maxDepth)
     615             && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
     616         ) {
     617             if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
     618                 if($page->URLSegment == 'home') $hasHome = true;
     619                 if(($page->ID == $this->ID) || $unlinked) {
     620                     $parts[] = Convert::raw2xml($page->Title);
     621                 } else {
     622                     $parts[] = ("<a href=\"" . $page->Link() . "\">" . Convert::raw2xml($page->Title) . "</a>");
     623                 }
     624             }
     625             $page = $page->Parent;
     626         }
     627
     628         return implode(self::$breadcrumbs_delimiter, array_reverse($parts));
     629     }
     630
     631     /**
     632      * Make this page a child of another page.
     633      *
     634      * If the parent page does not exist, resolve it to a valid ID
     635      * before updating this page's reference.
     636      *
     637      * @param SiteTree|int $item Either the parent object, or the parent ID
     638      */
     639     public function setParent($item) {
     640         if(is_object($item)) {
     641             if (!$item->exists()) $item->write();
     642             $this->setField("ParentID", $item->ID);
     643         } else {
     644             $this->setField("ParentID", $item);
     645         }
     646     }
     647
     648     /**
     649      * Get the parent of this page.
     650      *
     651      * @return SiteTree Parent of this page.
     652      */
     653     public function getParent() {
     654         if ($this->getField("ParentID")) {
     655             return DataObject::get_one("SiteTree", "\"SiteTree\".\"ID\" = " . $this->getField("ParentID"));
     656         }
     657     }
     658
     659     /**
     660      * Return a string of the form "parent - page" or
     661      * "grandparent - parent - page".
     662      *
     663      * @param int $level The maximum amount of levels to traverse.
     664      * @param string $seperator Seperating string
     665      * @return string The resulting string
     666      */
     667     function NestedTitle($level = 2, $separator = " - ") {
     668         $item = $this;
     669         while($item && $level > 0) {
     670             $parts[] = $item->Title;
     671             $item = $item->Parent;
     672             $level--;
     673         }
     674         return implode($separator, array_reverse($parts));
     675     }
     676
     677     /**
     678      * This function should return true if the current user can add children
     679      * to this page. It can be overloaded to customise the security model for an
     680      * application.
     681      *
     682      * Returns true if the member is allowed to do the given action.
     683      *
     684      * @uses DataObjectDecorator->can()
     685      *
     686      * If a page is set to inherit, but has no parent, it inherits from
     687      * {@link SiteConfig}
     688      *
     689      * @param string $perm The permission to be checked, such as 'View'.
     690      * @param Member $member The member whose permissions need checking.
     691      *                       Defaults to the currently logged in user.
     692      *
     693      * @return boolean True if the the member is allowed to do the given
     694      *                 action.
     695      *
     696      * @todo Check we get a endless recursion if we use parent::can()
     697      */
     698     function can($perm, $member = null) {
     699         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
     700             $member = Member::currentUserID();
     701         }
     702
     703         if($member && Permission::checkMember($member, "ADMIN")) return true;
     704
     705         if(method_exists($this, 'can' . ucfirst($perm))) {
     706             $method = 'can' . ucfirst($perm);
     707             return $this->$method($member);
     708         }
     709
     710         $results = $this->extend('can', $member);
     711         if($results && is_array($results)) if(!min($results)) return false;
     712
     713         return true;
     714     }
     715
     716
     717     /**
     718      * This function should return true if the current user can add children
     719      * to this page. It can be overloaded to customise the security model for an
     720      * application.
     721      *
     722      * Denies permission if any of the following conditions is TRUE:
     723      * - alternateCanAddChildren() on a decorator returns FALSE
     724      * - canEdit() is not granted
     725      * - There are no classes defined in {@link $allowed_children}
     726      *
     727      * @uses SiteTreeDecorator->canAddChildren()
     728      * @uses canEdit()
     729      * @uses $allowed_children
     730      *
     731      * @return boolean True if the current user can add children.
     732      */
     733     public function canAddChildren($member = null) {
     734         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
     735             $member = Member::currentUserID();
     736         }
     737
     738         if($member && Permission::checkMember($member, "ADMIN")) return $this->stat('allowed_children') != 'none';
     739
     740         $results = $this->extend('canAddChildren', $member);
     741         if($results && is_array($results)) if(!min($results)) return false;
     742
     743         return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
     744     }
     745
     746
     747     /**
     748      * This function should return true if the current user can view this
     749      * page. It can be overloaded to customise the security model for an
     750      * application.
     751      *
     752      * Denies permission if any of the following conditions is TRUE:
     753      * - canView() on any decorator returns FALSE
     754      * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
     755      * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
     756      * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
     757      *
     758      * @uses DataObjectDecorator->canView()
     759      * @uses ViewerGroups()
     760      *
     761      * @return boolean True if the current user can view this page.
     762      */
     763     public function canView($member = null) {
     764         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
     765             $member = Member::currentUserID();
     766         }
     767
     768         // admin override
     769         if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
     770
     771         // decorated access checks
     772         $results = $this->extend('canView', $member);
     773         if($results && is_array($results)) if(!min($results)) return false;
     774
     775         // check for empty spec
     776         if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
     777
     778         // check for inherit
     779         if($this->CanViewType == 'Inherit') {
     780             if($this->ParentID) return $this->Parent()->canView($member);
     781             else return $this->getSiteConfig()->canView($member);
     782         }
     783
     784         // check for any logged-in users
     785         if($this->CanViewType == 'LoggedInUsers' && $member) {
     786             return true;
     787         }
     788
     789         // check for specific groups
     790         if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
     791         if(
     792             $this->CanViewType == 'OnlyTheseUsers'
     793             && $member
     794             && $member->inGroups($this->ViewerGroups())
     795         ) return true;
     796
     797         return false;
     798     }
     799
     800     /**
     801      * This function should return true if the current user can delete this
     802      * page. It can be overloaded to customise the security model for an
     803      * application.
     804      *
     805      * Denies permission if any of the following conditions is TRUE:
     806      * - canDelete() returns FALSE on any decorator
     807      * - canEdit() returns FALSE
     808      * - any descendant page returns FALSE for canDelete()
     809      *
     810      * @uses canDelete()
     811      * @uses DataObjectDecorator->canDelete()
     812      * @uses canEdit()
     813      *
     814      * @param Member $member
     815      * @return boolean True if the current user can delete this page.
     816      */
     817     public function canDelete($member = null) {
     818         if($member instanceof Member) $memberID = $member->ID;
     819         else if(is_numeric($member)) $memberID = $member;
     820         else $memberID = Member::currentUserID();
     821
     822         if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
     823             return true;
     824         }
     825
     826         // decorated access checks
     827         $results = $this->extend('canDelete', $memberID);
     828         if($results && is_array($results)) if(!min($results)) return false;
     829
     830         // Check cache (the can_edit_multiple call below will also do this, but this is quicker)
     831         if(isset(self::$cache_permissions['delete'][$this->ID])) {
     832             return self::$cache_permissions['delete'][$this->ID];
     833         }
     834
     835         // Regular canEdit logic is handled by can_edit_multiple
     836         $results = self::can_delete_multiple(array($this->ID), $memberID);
     837
     838         // If this page no longer exists in stage/live results won't contain the page.
     839         // Fail-over to false
     840         return isset($results[$this->ID]) ? $results[$this->ID] : false;
     841     }
     842
     843     /**
     844      * This function should return true if the current user can create new
     845      * pages of this class. It can be overloaded to customise the security model for an
     846      * application.
     847      *
     848      * Denies permission if any of the following conditions is TRUE:
     849      * - canCreate() returns FALSE on any decorator
     850      * - $can_create is set to FALSE and the site is not in "dev mode"
     851      *
     852      * Use {@link canAddChildren()} to control behaviour of creating children under this page.
     853      *
     854      * @uses $can_create
     855      * @uses DataObjectDecorator->canCreate()
     856      *
     857      * @param Member $member
     858      * @return boolean True if the current user can create pages on this class.
     859      */
     860     public function canCreate($member = null) {
     861         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
     862             $member = Member::currentUserID();
     863         }
     864 // даже админ иногда не может создавать страницы
     865 //      if($member && Permission::checkMember($member, "ADMIN")) return true;
     866
     867         // decorated permission checks
     868         $results = $this->extend('canCreate', $member);
     869         if($results && is_array($results)) if(!min($results)) return false;
     870
     871         return $this->stat('can_create') != false || Director::isDev();
     872     }
     873
     874
     875     /**
     876      * This function should return true if the current user can edit this
     877      * page. It can be overloaded to customise the security model for an
     878      * application.
     879      *
     880      * Denies permission if any of the following conditions is TRUE:
     881      * - canEdit() on any decorator returns FALSE
     882      * - canView() return false
     883      * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
     884      * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the CMS_Access_CMSMAIN permission code
     885      * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
     886      *
     887      * @uses canView()
     888      * @uses EditorGroups()
     889      * @uses DataObjectDecorator->canEdit()
     890      *
     891      * @param Member $member Set to FALSE if you want to explicitly test permissions without a valid user (useful for unit tests)
     892      * @return boolean True if the current user can edit this page.
     893      */
     894     public function canEdit($member = null) {
     895         if($member instanceof Member) $memberID = $member->ID;
     896         else if(is_numeric($member)) $memberID = $member;
     897         else $memberID = Member::currentUserID();
     898
     899         if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
     900
     901         // decorated access checks
     902         $results = $this->extend('canEdit', $memberID);
     903         if($results && is_array($results)) if(!min($results)) return false;
     904
     905         if($this->ID) {
     906             // Check cache (the can_edit_multiple call below will also do this, but this is quicker)
     907             if(isset(self::$cache_permissions['CanEditType'][$this->ID])) {
     908                 return self::$cache_permissions['CanEditType'][$this->ID];
     909             }
     910
     911             // Regular canEdit logic is handled by can_edit_multiple
     912             $results = self::can_edit_multiple(array($this->ID), $memberID);
     913
     914             // If this page no longer exists in stage/live results won't contain the page.
     915             // Fail-over to false
     916             return isset($results[$this->ID]) ? $results[$this->ID] : false;
     917
     918         // Default for unsaved pages
     919         } else {
     920             return $this->getSiteConfig()->canEdit($member);
     921         }
     922     }
     923
     924     /**
     925      * This function should return true if the current user can publish this
     926      * page. It can be overloaded to customise the security model for an
     927      * application.
     928      *
     929      * Denies permission if any of the following conditions is TRUE:
     930      * - canPublish() on any decorator returns FALSE
     931      * - canEdit() returns FALSE
     932      *
     933      * @uses SiteTreeDecorator->canPublish()
     934      *
     935      * @param Member $member
     936      * @return boolean True if the current user can publish this page.
     937      */
     938     public function canPublish($member = null) {
     939         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
     940
     941         if($member && Permission::checkMember($member, "ADMIN")) return true;
     942
     943         // If we have a result, then that means at least one decorator specified alternateCanPublish
     944         // Allow the permission check only if *all* voting decorators allow it.
     945         $results = $this->extend('canPublish', $member);
     946         if($results && is_array($results)) if(!min($results)) return false;
     947
     948         // Normal case
     949         return $this->canEdit($member);
     950     }
     951
     952     public function canDeleteFromLive($member = null) {
     953         // If we have a result, then that means at least one decorator specified canDeleteFromLive
     954         // Allow the permission check only if *all* voting decorators allow it.
     955         $results = $this->extend('canDeleteFromLive', $member);
     956         if($results && is_array($results)) if(!min($results)) return false;
     957
     958         return $this->canPublish($member);
     959     }
     960
     961     /**
     962      * Stub method to get the site config, provided so it's easy to override
     963      */
     964     function getSiteConfig() {
     965         $altConfig = false;
     966         if($this->hasMethod('alternateSiteConfig')) {
     967             $altConfig = $this->alternateSiteConfig();
     968         }
     969         if($altConfig) {
     970             return $altConfig;
     971         } elseif($this->hasExtension('Translatable')) {
     972              return SiteConfig::current_site_config($this->Locale);
     973         } else {
     974             return SiteConfig::current_site_config();
     975         }
     976     }
     977
     978
     979     /**
     980      * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions.
     981      * This method will use the static can_(perm)_multiple method for efficiency.
     982      *
     983      * @param $permission String The permission: edit, view, publish, approve, etc.
     984      * @param $ids array An array of page IDs
     985      * @param $batchCallBack The function/static method to call to calculate permissions.  Defaults
     986      * to 'SiteTree::can_(permission)_multiple'
     987      */
     988     static function prepopuplate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
     989         if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
     990
     991         //PHP 5.1 requires an array rather than a string for the call_user_func function
     992         $batchCallback=explode('::', $batchCallback);
     993
     994         if(is_callable($batchCallback)) {
     995             $permissionValues = call_user_func($batchCallback, $ids,
     996                 Member::currentUserID(), false);
     997
     998             if(!isset(self::$cache_permissions[$permission])) {
     999                 self::$cache_permissions[$permission] = array();
    1000             }
    1001
    1002             self::$cache_permissions[$permission] = $permissionValues
    1003                 + self::$cache_permissions[$permission];
    1004
    1005         } else {
    1006             user_error("SiteTree::prepopuplate_permission_cache can't calculate '$permission' "
    1007                 . "with callback '$batchCallback'", E_USER_WARNING);
    1008         }
    1009     }
    1010
    1011     static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission = 'CMS_ACCESS_CMSMain', $useCached = true) {
    1012         // Sanitise the IDs
    1013         $ids = array_filter($ids, 'is_numeric');
    1014
    1015         // This is the name used on the permission cache
    1016         // converts something like 'CanEditType' to 'edit'.
    1017         $cacheKey = strtolower(substr($typeField, 3, -4));
    1018
    1019         // Default result: nothing editable
    1020         $result = array_fill_keys($ids, false);
    1021         if($ids) {
    1022
    1023             // Look in the cache for values
    1024             if($useCached && isset(self::$cache_permissions[$cacheKey])) {
    1025                 $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
    1026
    1027                 // If we can't find everything in the cache, then look up the remainder separately
    1028                 $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
    1029                 if($uncachedValues) {
    1030                     $cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
    1031                 }
    1032                 return $cachedValues;
    1033             }
    1034
    1035             // If a member doesn't have CMS_ACCESS_CMSMain permission then they can't edit anything
    1036             if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
    1037                 return $result;
    1038             }
    1039
    1040             $SQL_idList = implode($ids, ", ");
    1041
    1042             // if page can't be viewed, don't grant edit permissions
    1043             // to do - implement can_view_multiple(), so this can be enabled
    1044             //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
    1045
    1046             // Get the groups that the given member belongs to
    1047             $groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
    1048             $SQL_groupList = implode(", ", $groupIDs);
    1049             if (!$SQL_groupList) $SQL_groupList = '0';
    1050
    1051             $combinedStageResult = array();
    1052
    1053             foreach(array('Stage', 'Live') as $stage) {
    1054                 // Start by filling the array with the pages that actually exist
    1055                 $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
    1056
    1057                 $result = array_fill_keys(DB::query("SELECT \"ID\" FROM \"$table\"
    1058                         WHERE \"ID\" IN (".implode(", ", $ids).")")->column(), false);
    1059
    1060                 // Get the uninherited permissions
    1061                 $uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage, "(\"$typeField\" = 'LoggedInUsers' OR
    1062                     (\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
    1063                     AND \"SiteTree\".\"ID\" IN ($SQL_idList)",
    1064                     "",
    1065                     "LEFT JOIN \"$groupJoinTable\"
    1066                     ON \"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\"
    1067                     AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
    1068
    1069                 if($uninheritedPermissions) {
    1070                     // Set all the relevant items in $result to true
    1071                     $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
    1072                 }
    1073
    1074                 // Get permissions that are inherited
    1075                 $potentiallyInherited = Versioned::get_by_stage("SiteTree", $stage, "\"$typeField\" = 'Inherit'
    1076                     AND \"SiteTree\".\"ID\" IN ($SQL_idList)");
    1077
    1078                 if($potentiallyInherited) {
    1079                     // Group $potentiallyInherited by ParentID; we'll look at the permission of all those
    1080                     // parents and then see which ones the user has permission on
    1081                     $siteConfigPermission = SiteConfig::current_site_config()->{$siteConfigMethod}($memberID);
    1082                     $groupedByParent = array();
    1083                     foreach($potentiallyInherited as $item) {
    1084                         if($item->ParentID) {
    1085                             if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
    1086                             $groupedByParent[$item->ParentID][] = $item->ID;
    1087                         } else {
    1088                             $result[$item->ID] = $siteConfigPermission;
    1089                         }
    1090                     }
    1091
    1092                     if($groupedByParent) {
    1093                         $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
    1094                         if($actuallyInherited) {
    1095                             $parentIDs = array_keys(array_filter($actuallyInherited));
    1096                             foreach($parentIDs as $parentID) {
    1097                                 // Set all the relevant items in $result to true
    1098                                 $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
    1099                             }
    1100                         }
    1101                     }
    1102                 }
    1103
    1104                 $combinedStageResult = $combinedStageResult + $result;
    1105
    1106             }
    1107         }
    1108
    1109         if(isset($combinedStageResult)) {
    1110             // Cache results
    1111             // TODO - Caching permissions is breaking unit tests. One possible issue
    1112             // is the cache needs to be flushed when permission on a page is changed,
    1113             // but this only solved some of the failing unit tests. Disabled for now.
    1114             /*foreach($combinedStageResult as $id => $val) {
    1115                 self::$cache_permissions[$typeField][$id] = $val;
    1116             }*/
    1117             return $combinedStageResult;
    1118         } else {
    1119             return array();
    1120         }
    1121     }
    1122
    1123     /**
    1124      * Get the 'can edit' information for a number of SiteTree pages.
    1125      *
    1126      * @param An array of IDs of the SiteTree pages to look up.
    1127      * @param useCached Return values from the permission cache if they exist.
    1128      * @return A map where the IDs are keys and the values are booleans stating whether the given
    1129      * page can be edited.
    1130      */
    1131     static function can_edit_multiple($ids, $memberID, $useCached = true) {
    1132         return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEdit', 'CMS_ACCESS_CMSMain', $useCached);
    1133     }
    1134
    1135     /**
    1136      * Get the 'can edit' information for a number of SiteTree pages.
    1137      * @param An array of IDs of the SiteTree pages to look up.
    1138      * @param useCached Return values from the permission cache if they exist.
    1139      */
    1140     static function can_delete_multiple($ids, $memberID, $useCached = true) {
    1141         $deletable = array();
    1142
    1143         $result = array_fill_keys($ids, false);
    1144
    1145         // Look in the cache for values
    1146         if($useCached && isset(self::$cache_permissions['delete'])) {
    1147             $cachedValues = array_intersect_key(self::$cache_permissions['delete'], $result);
    1148
    1149             // If we can't find everything in the cache, then look up the remainder separately
    1150             $uncachedValues = array_diff_key($result, self::$cache_permissions['delete']);
    1151             if($uncachedValues) {
    1152                 $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
    1153                     + $cachedValues;
    1154             }
    1155             return $cachedValues;
    1156         }
    1157
    1158         // You can only delete pages that you can edit
    1159         $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
    1160         if($editableIDs) {
    1161             $idList = implode(",", $editableIDs);
    1162
    1163             // You can only delete pages whose children you can delete
    1164             $childRecords = DataObject::get("SiteTree", "\"ParentID\" IN ($idList)");
    1165             if($childRecords) {
    1166                 $children = $childRecords->map("ID", "ParentID");
    1167
    1168                 // Find out the children that can be deleted
    1169                 $deletableChildren = self::can_delete_multiple(array_keys($children), $memberID);
    1170
    1171                 // Get a list of all the parents that have no undeletable children
    1172                 $deletableParents = array_fill_keys($editableIDs, true);
    1173                 foreach($deletableChildren as $id => $canDelete) {
    1174                     if(!$canDelete) unset($deletableParents[$children[$id]]);
    1175                 }
    1176
    1177                 // Use that to filter the list of deletable parents that have children
    1178                 $deletableParents = array_keys($deletableParents);
    1179
    1180                 // Also get the $ids that don't have children
    1181                 $parents = array_unique($children);
    1182                 $deletableLeafNodes = array_diff($editableIDs, $parents);
    1183
    1184                 // Combine the two
    1185                 $deletable = array_merge($deletableParents, $deletableLeafNodes);
    1186
    1187             } else {
    1188                 $deletable = $editableIDs;
    1189             }
    1190         } else {
    1191             $deletable = array();
    1192         }
    1193
    1194         // Convert the array of deletable IDs into a map of the original IDs with true/false as the
    1195         // value
    1196         return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
    1197     }
    1198
    1199     /**
    1200      * Collate selected descendants of this page.
    1201      *
    1202      * {@link $condition} will be evaluated on each descendant, and if it is
    1203      * succeeds, that item will be added to the $collator array.
    1204      *
    1205      * @param string $condition The PHP condition to be evaluated. The page
    1206      *                          will be called $item
    1207      * @param array $collator An array, passed by reference, to collect all
    1208      *                        of the matching descendants.
    1209      */
    1210     public function collateDescendants($condition, &$collator) {
    1211         if($children = $this->Children()) {
    1212             foreach($children as $item) {
    1213                 if(eval("return $condition;")) $collator[] = $item;
    1214                 $item->collateDescendants($condition, $collator);
    1215             }
    1216             return true;
    1217         }
    1218     }
    1219
    1220
    1221     /**
    1222      * Return the title, description, keywords and language metatags.
    1223      *
    1224      * @todo Move <title> tag in separate getter for easier customization and more obvious usage
    1225      *
    1226      * @param boolean|string $includeTitle Show default <title>-tag, set to false for custom templating
    1227      * @param boolean $includeTitle Show default <title>-tag, set to false for
    1228      *                              custom templating
    1229      * @return string The XHTML metatags
    1230      */
    1231     public function MetaTags($includeTitle = true) {
    1232         $tags = "";
    1233         if($includeTitle === true || $includeTitle == 'true') {
    1234             $tags .= "<title>" . Convert::raw2xml(($this->MetaTitle)
    1235                 ? $this->MetaTitle
    1236                 : $this->Title) . "</title>\n";
    1237         }
    1238
    1239         $charset = ContentNegotiator::get_encoding();
    1240         $tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
    1241         if($this->MetaKeywords) {
    1242             $tags .= "<meta name=\"keywords\" content=\"" . Convert::raw2att($this->MetaKeywords) . "\" />\n";
    1243         }
    1244         if($this->MetaDescription) {
    1245             $tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
    1246         }
    1247         if($this->ExtraMeta) {
    1248             $tags .= $this->ExtraMeta . "\n";
    1249         }
    1250
    1251         $this->extend('MetaTags', $tags);
    1252
    1253         return $tags;
    1254     }
    1255
    1256
    1257     /**
    1258      * Returns the object that contains the content that a user would
    1259      * associate with this page.
    1260      *
    1261      * Ordinarily, this is just the page itself, but for example on
    1262      * RedirectorPages or VirtualPages ContentSource() will return the page
    1263      * that is linked to.
    1264      *
    1265      * @return SiteTree The content source.
    1266      */
    1267     public function ContentSource() {
    1268         return $this;
    1269     }
    1270
    1271
    1272     /**
    1273      * Add default records to database.
    1274      *
    1275      * This function is called whenever the database is built, after the
    1276      * database tables have all been created. Overload this to add default
    1277      * records when the database is built, but make sure you call
    1278      * parent::requireDefaultRecords().
    1279      */
    1280     function requireDefaultRecords() {
    1281         parent::requireDefaultRecords();
    1282
    1283         // default pages
    1284         if($this->class == 'SiteTree') {
    1285             if(!SiteTree::get_by_link('home')) {
    1286                 $homepage = new Page();
    1287                 $homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
    1288                 $homepage->Content = _t('SiteTree.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>. You can now access the <a href="http://doc.silverstripe.org">developer documentation</a>, or begin <a href="http://doc.silverstripe.org/doku.php?id=tutorials">the tutorials.</a></p>');
    1289                 $homepage->URLSegment = 'home';
    1290                 $homepage->Status = 'Published';
    1291                 $homepage->Sort = 1;
    1292                 $homepage->write();
    1293                 $homepage->publish('Stage', 'Live');
    1294                 $homepage->flushCache();
    1295                 DB::alteration_message('Home page created', 'created');
    1296             }
    1297
    1298             if(DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
    1299                 $aboutus = new Page();
    1300                 $aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
    1301                 $aboutus->Content = _t('SiteTree.DEFAULTABOUTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
    1302                 $aboutus->Status = 'Published';
    1303                 $aboutus->URLSegment = 'about';
    1304                 $aboutus->Sort = 2;
    1305                 $aboutus->write();
    1306                 $aboutus->publish('Stage', 'Live');
    1307                 $aboutus->flushCache();
    1308                 DB::alteration_message('About Us page created', 'created');
    1309
    1310                 $contactus = new Page();
    1311                 $contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
    1312                 $contactus->Content = _t('SiteTree.DEFAULTCONTACTCONTENT', '<p>You can fill this page out with your own content, or delete it and create your own pages.<br /></p>');
    1313                 $contactus->Status = 'Published';
    1314                 $contactus->URLSegment = 'contacts';
    1315                 $contactus->Sort = 3;
    1316                 $contactus->write();
    1317                 $contactus->publish('Stage', 'Live');
    1318                 $contactus->flushCache();
    1319                 DB::alteration_message('Contact Us page created', 'created');
    1320             }
    1321         }
    1322
    1323         // schema migration
    1324         // @todo Move to migration task once infrastructure is implemented
    1325         if($this->class == 'SiteTree') {
    1326             $conn = DB::getConn();
    1327             // only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
    1328             if(array_key_exists('Viewers', $conn->fieldList('SiteTree'))) {
    1329                 $task = new UpgradeSiteTreePermissionSchemaTask();
    1330                 $task->run(new SS_HTTPRequest('GET','/'));
    1331             }
    1332         }
    1333     }
    1334
    1335
    1336     //------------------------------------------------------------------------------------//
    1337
    1338     protected function onBeforeWrite() {
    1339         parent::onBeforeWrite();
    1340
    1341         // If Sort hasn't been set, make this page come after it's siblings
    1342         if(!$this->Sort) {
    1343             $parentID = ($this->ParentID) ? $this->ParentID : 0;
    1344             $this->Sort = DB::query("SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = $parentID")->value();
    1345         }
    1346
    1347         // If there is no URLSegment set, generate one from Title
    1348         if((!$this->URLSegment || $this->URLSegment == 'new-page') && $this->Title) {
    1349             $this->URLSegment = $this->generateURLSegment($this->Title);
    1350         } else if($this->isChanged('URLSegment')) {
    1351             // Make sure the URLSegment is valid for use in a URL
    1352             $segment = ereg_replace('[^A-Za-z0-9_]+','-',Convert::rus2lat($this->URLSegment));
    1353             $segment = ereg_replace('-+','-',$segment);
    1354
    1355             // If after sanitising there is no URLSegment, give it a reasonable default
    1356             if(!$segment) {
    1357                 $segment = "page-$this->ID";
    1358             }
    1359             $this->URLSegment = $segment;
    1360         }
    1361
    1362         DataObject::set_context_obj($this);
    1363
    1364         // Ensure that this object has a non-conflicting URLSegment value.
    1365         $count = 2;
    1366         while(!$this->validURLSegment()) {
    1367             $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
    1368             $count++;
    1369         }
    1370
    1371         DataObject::set_context_obj(null);
    1372
    1373         $this->syncLinkTracking();
    1374
    1375         // Check to see if we've only altered fields that shouldn't affect versioning
    1376         $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo');
    1377         $changedFields = array_keys($this->getChangedFields(true, 2));
    1378
    1379         // This more rigorous check is inline with the test that write()
    1380         // does to dedcide whether or not to write to the DB.  We use that
    1381         // to avoid cluttering the system with a migrateVersion() call
    1382         // that doesn't get used
    1383         $oneChangedFields = array_keys($this->getChangedFields(true, 1));
    1384
    1385         if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
    1386             // This will have the affect of preserving the versioning
    1387             $this->migrateVersion($this->Version);
    1388         }
    1389     }
    1390
    1391     function syncLinkTracking() {
    1392         // Build a list of HTMLText fields
    1393         $allFields = $this->db();
    1394         $htmlFields = array();
    1395         foreach($allFields as $field => $fieldSpec) {
    1396             if ($field == 'ExtraMeta') continue;
    1397             if(preg_match('/([^(]+)/', $fieldSpec, $matches)) {
    1398                 $class = $matches[0];
    1399                 if(class_exists($class)){
    1400                     if($class == 'HTMLText' || is_subclass_of($class, 'HTMLText')) $htmlFields[] = $field;
    1401                 }
    1402             }
    1403         }
    1404
    1405         $linkedPages = array();
    1406         $linkedFiles = array();
    1407         $this->HasBrokenLink = false;
    1408         $this->HasBrokenFile = false;
    1409
    1410         foreach($htmlFields as $field) {
    1411             $formField = new HTMLEditorField($field);
    1412             $formField->setValue($this->$field);
    1413             $formField->saveInto($this);
    1414         }
    1415
    1416         // проверяем has_one связи с File
    1417         if ($this->ID) {
    1418             if (($has_one = $this->has_one()) && count($has_one)) {
    1419                 foreach($has_one as $name => $type) {
    1420                     if (singleton($type)->is_a('File') && !singleton($type)->is_a('Folder') && $this->{"{$name}ID"}) { // Folder не трекаем
    1421                         if (!DataObject::get_by_id($type, $this->{"{$name}ID"})) {
    1422                             $this->HasBrokenFile = true;
    1423                         }
    1424                     }
    1425                 }
    1426             }
    1427         }
    1428         $this->extend('augmentSyncLinkTracking');
    1429     }
    1430
    1431     function onAfterWrite() {
    1432         // Need to flush cache to avoid outdated versionnumber references
    1433         $this->flushCache();
    1434
    1435         // Update any virtual pages that might need updating
    1436         $linkedPages = $this->VirtualPages();
    1437         if($linkedPages) foreach($linkedPages as $page) {
    1438             $page->copyFrom($page->CopyContentFrom());
    1439             $page->write();
    1440         }
    1441
    1442         parent::onAfterWrite();
    1443     }
    1444
    1445     function onBeforeDelete() {
    1446         parent::onBeforeDelete();
    1447
    1448         // If deleting this page, delete all its children.
    1449         if(SiteTree::get_enforce_strict_hierarchy() && $children = $this->AllChildren(2)) {
    1450             foreach($children as $child) {
    1451                 $child->delete();
    1452             }
    1453         }
    1454     }
    1455
    1456
    1457     function onAfterDelete() {
    1458         // Need to flush cache to avoid outdated versionnumber references
    1459         $this->flushCache();
    1460
    1461         // Need to mark pages depending to this one as broken
    1462         $dependentPages = $this->DependentPages();
    1463         if($dependentPages) foreach($dependentPages as $page) {
    1464             // $page->write() calls syncLinkTracking, which does all the hard work for us.
    1465             $page->write();
    1466         }
    1467
    1468         parent::onAfterDelete();
    1469     }
    1470
    1471     /**
    1472      * Returns TRUE if this object has a URLSegment value that does not conflict with any other objects. This methods
    1473      * checks for:
    1474      *   - A page with the same URLSegment that has a conflict.
    1475      *   - Conflicts with actions on the parent page.
    1476      *   - A conflict caused by a root page having the same URLSegment as a class name.
    1477      *
    1478      * @return bool
    1479      */
    1480     public function validURLSegment() {
    1481         if(self::nested_urls() && $parent = $this->Parent()) {
    1482             if($controller = ModelAsController::controller_for($parent)) {
    1483                 if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
    1484             }
    1485         }
    1486
    1487         if(!self::nested_urls() || !$this->ParentID) {
    1488             if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
    1489         }
    1490
    1491         $IDFilter     = ($this->ID) ? "AND \"SiteTree\".\"ID\" <> $this->ID" :  null;
    1492         $parentFilter = null;
    1493
    1494         if(self::nested_urls()) {
    1495             if($this->ParentID) {
    1496                 $parentFilter = " AND \"SiteTree\".\"ParentID\" = $this->ParentID";
    1497             } else {
    1498                 $parentFilter = ' AND "SiteTree"."ParentID" = 0';
    1499             }
    1500         }
    1501
    1502         $existingPage = DataObject::get_one(
    1503             'SiteTree',
    1504             "\"URLSegment\" = '$this->URLSegment' $IDFilter $parentFilter"
    1505         );
    1506         if ($existingPage) {
    1507             return false;
    1508         }
    1509
    1510         $values = $this->extend('augmentValidURLSegment');
    1511         if (count($values) && !min($values)) {
    1512             return false;
    1513         }
    1514
    1515         return true;
    1516     }
    1517
    1518     /**
    1519      * Generate a URL segment based on the title provided.
    1520      * @param string $title Page title.
    1521      * @return string Generated url segment
    1522      */
    1523     function generateURLSegment($title){
    1524         $t = strtolower($title);
    1525         $t = Convert::rus2lat($t);
    1526         $t = str_replace('&amp;','-and-',$t);
    1527         $t = str_replace('&','-and-',$t);
    1528         $t = ereg_replace('[^A-Za-z0-9]+','-',$t);
    1529         $t = ereg_replace('-+','-',$t);
    1530         if(!$t || $t == '-' || $t == '-1') {
    1531             $t = "page-$this->ID";
    1532         }
    1533         return trim($t, '-');
    1534     }
    1535
    1536     /**
    1537      * Rewrite a file URL on this page, after its been renamed.
    1538      * Triggers the onRenameLinkedAsset action on extensions.
    1539      */
    1540     function rewriteFileURL($old, $new) {
    1541         $fields = $this->inheritedDatabaseFields();
    1542         // Update the content without actually creating a new version
    1543         foreach(array("SiteTree_Live", "SiteTree") as $table) {
    1544             // Published site
    1545             $published = DB::query("SELECT * FROM  \"$table\" WHERE \"ID\" = $this->ID")->record();
    1546             $origPublished = $published;
    1547
    1548             foreach($fields as $fieldName => $fieldType) {
    1549                 if ($fieldType != 'HTMLText') continue;
    1550
    1551                 // TODO: This doesn't work for HTMLText fields on other tables.
    1552                 if(isset($published[$fieldName])) {
    1553                     //$published[$fieldName] = str_replace($old, $new, $published[$fieldName], $numReplaced);
    1554                     $oldFileMask = '!' . dirname($old) . '/(_resampled/resizedimage[0-9]+-)?' . basename($old) . '!';
    1555                     $published[$fieldName] = preg_replace($oldFileMask, $new, $published[$fieldName], -1, $numReplaced);
    1556                     if($numReplaced) {
    1557                         DB::query("UPDATE \"$table\" SET \"$fieldName\" = '"
    1558                             . Convert::raw2sql($published[$fieldName]) . "' WHERE \"ID\" = $this->ID");
    1559
    1560                         // Tell static caching to update itself
    1561                         if($table == 'SiteTree_Live') {
    1562                             $publishedClass = $origPublished['ClassName'];
    1563                             $origPublishedObj = new $publishedClass($origPublished);
    1564                             $this->extend('onRenameLinkedAsset', $origPublishedObj);
    1565                         }
    1566                     }
    1567                 }
    1568             }
    1569         }
    1570     }
    1571
    1572     /**
    1573      * Returns the pages that depend on this page.
    1574      * This includes virtual pages, pages that link to it, etc.
    1575      *
    1576      * @param $includeVirtuals Set to false to exlcude virtual pages.
    1577      */
    1578     function DependentPages($includeVirtuals = true) {
    1579         if(is_callable('Subsite::disable_subsite_filter')) Subsite::disable_subsite_filter(true);
    1580
    1581         // Content links
    1582         $items = $this->BackLinkTracking();
    1583         if(!$items) $items = new DataObjectSet();
    1584         else foreach($items as $item) $item->DependentLinkType = 'Content link';
    1585
    1586         // Virtual pages
    1587         if($includeVirtuals) {
    1588             $virtuals = $this->VirtualPages();
    1589             if($virtuals) {
    1590                 foreach($virtuals as $item) $item->DependentLinkType = 'Virtual page';
    1591                 $items->merge($virtuals);
    1592             }
    1593         }
    1594
    1595         // Redirector pages
    1596         $redirectors = DataObject::get("RedirectorPage", "\"RedirectionType\" = 'Internal' AND \"LinkToID\" = $this->ID");
    1597         if($redirectors) {
    1598             foreach($redirectors as $item) $item->DependentLinkType = 'Redirector page';
    1599             $items->merge($redirectors);
    1600         }
    1601
    1602         if(is_callable('Subsite::disable_subsite_filter')) Subsite::disable_subsite_filter(false);
    1603         return $items;
    1604     }
    1605
    1606     /**
    1607      * Return the number of {@link DependentPages()}
    1608      *
    1609      * @param $includeVirtuals Set to false to exlcude virtual pages.
    1610      */
    1611     function DependentPagesCount($includeVirtuals = true) {
    1612         $links = DB::query("SELECT COUNT(*) FROM \"SiteTree_LinkTracking\"
    1613             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"SiteTree_LinkTracking\".\"SiteTreeID\"
    1614             WHERE \"ChildID\" = $this->ID ")->value();
    1615         if($includeVirtuals) {
    1616             $virtuals = DB::query("SELECT COUNT(*) FROM \"VirtualPage\"
    1617             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"VirtualPage\".\"ID\"
    1618             WHERE \"CopyContentFromID\" = $this->ID")->value();
    1619         } else {
    1620             $virtuals = 0;
    1621         }
    1622         $redirectors = DB::query("SELECT COUNT(*) FROM \"RedirectorPage\"
    1623             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"RedirectorPage\".\"ID\"
    1624             WHERE \"RedirectionType\" = 'Internal' AND \"LinkToID\" = $this->ID")->value();
    1625
    1626
    1627         return 0 + $links + $virtuals + $redirectors;
    1628     }
    1629
    1630     /**
    1631      * Return all virtual pages that link to this page
    1632      */
    1633     function VirtualPages() {
    1634         if(!$this->ID) return null;
    1635         if(class_exists('Subsite')) {
    1636             return Subsite::get_from_all_subsites('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
    1637         } else {
    1638             return DataObject::get('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
    1639         }
    1640     }
    1641
    1642     /**
    1643      * Returns a FieldSet with which to create the CMS editing form.
    1644      *
    1645      * You can override this in your child classes to add extra fields - first
    1646      * get the parent fields using parent::getCMSFields(), then use
    1647      * addFieldToTab() on the FieldSet.
    1648      *
    1649      * @return FieldSet The fields to be displayed in the CMS.
    1650      */
    1651     function getCMSFields() {
    1652         require_once("forms/Form.php");
    1653         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/prototype/prototype.js");
    1654         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
    1655         Requirements::javascript(CMS_DIR . "/javascript/SitetreeAccess.js");
    1656         Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
    1657         Requirements::javascript(SAPPHIRE_DIR . '/javascript/UpdateURL.js');
    1658
    1659         // Status / message
    1660         // Create a status message for multiple parents
    1661         if($this->ID && is_numeric($this->ID)) {
    1662             $linkedPages = $this->VirtualPages();
    1663         }
    1664
    1665         $parentPageLinks = array();
    1666
    1667         if(isset($linkedPages)) {
    1668             foreach($linkedPages as $linkedPage) {
    1669                 $parentPage = $linkedPage->Parent;
    1670                 if($parentPage) {
    1671                     if($parentPage->ID) {
    1672                         $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">{$parentPage->Title}</a>";
    1673                     } else {
    1674                         $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">" .
    1675                             _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
    1676                             "</a>";
    1677                     }
    1678                 }
    1679             }
    1680
    1681             $lastParent = array_pop($parentPageLinks);
    1682             $parentList = "'$lastParent'";
    1683
    1684             if(count($parentPageLinks) > 0) {
    1685                 $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
    1686                     . $parentList;
    1687             }
    1688
    1689             $statusMessage[] = sprintf(
    1690                 _t('SiteTree.APPEARSVIRTUALPAGES', "This content also appears on the virtual pages in the %s sections."),
    1691                 $parentList
    1692             );
    1693         }
    1694
    1695         if($this->HasBrokenLink || $this->HasBrokenFile) {
    1696             $statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
    1697         }
    1698
    1699         $message = "STATUS: $this->Status<br />";
    1700         if(isset($statusMessage)) {
    1701             $message .= "NOTE: " . implode("<br />", $statusMessage);
    1702         }
    1703
    1704         $dependentNote = '';
    1705         $dependentTable = new LiteralField('DependentNote', '<p>'._t('SiteTree.DEPENDENTNOTE','No dependent pages').'</p>');
    1706
    1707         // Create a table for showing pages linked to this one
    1708         $dependentPagesCount = $this->DependentPagesCount();
    1709         if($dependentPagesCount) {
    1710             $dependentColumns = array(
    1711                 'Title' => $this->fieldLabel('Title'),
    1712                 'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
    1713                 'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
    1714             );
    1715             if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
    1716
    1717             $dependentNote = new LiteralField('DependentNote', '<p>' . _t('SiteTree.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
    1718             $dependentTable = new TableListField(
    1719                 'DependentPages',
    1720                 'SiteTree',
    1721                 $dependentColumns
    1722             );
    1723             $dependentTable->setCustomSourceItems($this->DependentPages());
    1724             $dependentTable->setFieldFormatting(array(
    1725                 'Title' => '<a href=\"admin/show/$ID\">$Title</a>',
    1726                 'AbsoluteLink' => '<a href=\"$value\">$value</a>',
    1727             ));
    1728             $dependentTable->setPermissions(array(
    1729                 'show',
    1730                 'export'
    1731             ));
    1732         }
    1733
    1734         // Lay out the fields
    1735         $fields = new FieldSet(
    1736             $rootTab = new TabSet("Root",
    1737                 $tabContent = new TabSet('Content',
    1738                     $tabMain = new Tab('Main',
    1739                         new TextField("Title", $this->fieldLabel('Title')),
    1740                         new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
    1741                         new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", PR_MEDIUM, 'HTML editor title'))
    1742                     ),
    1743                     $tabMeta = new Tab('Metadata',
    1744                         new FieldGroup(_t('SiteTree.URL', "URL"),
    1745                             new LabelField('BaseUrlLabel',Controller::join_links (
    1746                                 Director::absoluteBaseURL(),
    1747                                 (self::nested_urls() && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
    1748                             )),
    1749                             new UniqueRestrictedTextField("URLSegment",
    1750                                 "URLSegment",
    1751                                 "SiteTree",
    1752                                 _t('SiteTree.VALIDATIONURLSEGMENT1', "Another page is using that URL. URL must be unique for each page"),
    1753                                 "[^A-Za-z0-9_-]+",
    1754                                 "-",
    1755                                 _t('SiteTree.VALIDATIONURLSEGMENT2', "URLs can only be made up of letters, digits and hyphens."),
    1756                                 "",
    1757                                 "",
    1758                                 250
    1759                             ),
    1760                             new LabelField('TrailingSlashLabel',"/")
    1761                         ),
    1762                         new LiteralField('LinkChangeNote', self::nested_urls() && count($this->AllChildren()) ?
    1763                             '<p>' . $this->fieldLabel('LinkChangeNote'). '</p>' : null
    1764                         ),
    1765                         new HeaderField('MetaTagsHeader',$this->fieldLabel('MetaTagsHeader')),
    1766                         new TextField("MetaTitle", $this->fieldLabel('MetaTitle')),
    1767                         new TextareaField("MetaKeywords", $this->fieldLabel('MetaKeywords'), 1),
    1768                         new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
    1769                         new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
    1770                     )
    1771                 ),
    1772                 $tabBehaviour = new Tab('Behaviour',
    1773                     new DropdownField(
    1774                         "ClassName",
    1775                         $this->fieldLabel('ClassName'),
    1776                         $this->getClassDropdown()
    1777                     ),
    1778
    1779                     new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
    1780                         "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
    1781                         "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page (choose below)"),
    1782                     )),
    1783                     $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree'),
    1784
    1785                     new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
    1786                     new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch')),
    1787                     /*, new TreeMultiselectField("MultipleParents", "Page appears within", "SiteTree")*/
    1788                     new CheckboxField("ProvideComments", $this->fieldLabel('ProvideComments')),
    1789                     new LiteralField(
    1790                         "HomepageForDomainInfo",
    1791                         "<p>" .
    1792                             _t('SiteTree.NOTEUSEASHOMEPAGE',
    1793                             "Use this page as the 'home page' for the following domains:
    1794                             (separate multiple domains with commas)") .
    1795                         "</p>"
    1796                     ),
    1797                     new TextField(
    1798                         "HomepageForDomain",
    1799                         _t('SiteTree.HOMEPAGEFORDOMAIN', "Domain(s)", PR_MEDIUM, 'Listing domains that should be used as homepage')
    1800                     ),
    1801                     new NumericField(
    1802                         "NumberCMSChildren",
    1803                         _t('SiteTree.NUMBERCMSCHILDREN',"Total displayed subpages in CMS (must be numeric, defaults to 0 == all, page refresh required after changing this)"),
    1804                         $this->NumberCMSChildren
    1805                     )
    1806
    1807                 ),
    1808                 $tabToDo = new Tab('Todo',
    1809                     new LiteralField("ToDoHelp", _t('SiteTree.TODOHELP', "<p>You can use this to keep track of work that needs to be done to the content of your site.  To see all your pages with to do information, open the 'Site Reports' window on the left and select 'To Do'</p>")),
    1810                     new TextareaField("ToDo", "")
    1811                 ),
    1812                 $tabDependent = new Tab('Dependent',
    1813                     $dependentNote,
    1814                     $dependentTable
    1815                 ),
    1816                 $tabAccess = new Tab('Access',
    1817                     new HeaderField('WhoCanViewHeader',_t('SiteTree.ACCESSHEADER', "Who can view this page?"), 2),
    1818                     $viewersOptionsField = new OptionsetField(
    1819                         "CanViewType",
    1820                         ""
    1821                     ),
    1822                     $viewerGroupsField = new TreeMultiselectField("ViewerGroups", $this->fieldLabel('ViewerGroups')),
    1823                     new HeaderField('WhoCanEditHeader',_t('SiteTree.EDITHEADER', "Who can edit this page?"), 2),
    1824                     $editorsOptionsField = new OptionsetField(
    1825                         "CanEditType",
    1826                         ""
    1827                     ),
    1828                     $editorGroupsField = new TreeMultiselectField("EditorGroups", $this->fieldLabel('EditorGroups'))
    1829                 )
    1830             )
    1831             //new NamedLabelField("Status", $message, "pageStatusMessage", true)
    1832         );
    1833
    1834         if ($this->stat('allowed_children') == 'none')
    1835             $fields->removeByName("NumberCMSChildren");
    1836
    1837
    1838
    1839         /*
    1840          * This filter ensures that the ParentID dropdown selection does not show this node,
    1841          * or its descendents, as this causes vanishing bugs.
    1842          */
    1843         $parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
    1844
    1845         // Conditional dependent pages tab
    1846         if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
    1847         else $fields->removeFieldFromTab('Root', 'Dependent');
    1848
    1849         // Make page location fields read-only if the user doesn't have the appropriate permission
    1850         if(!Permission::check("SITETREE_REORGANISE")) {
    1851             $fields->makeFieldReadonly('ParentType');
    1852             if($this->ParentType == 'root') {
    1853                 $fields->removeByName('ParentID');
    1854             } else {
    1855                 $fields->makeFieldReadonly('ParentID');
    1856             }
    1857         }
    1858
    1859         $viewersOptionsSource = array();
    1860         $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
    1861         $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
    1862         $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
    1863         $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
    1864         $viewersOptionsField->setSource($viewersOptionsSource);
    1865
    1866         $editorsOptionsSource = array();
    1867         $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
    1868         $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
    1869         $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
    1870         $editorsOptionsField->setSource($editorsOptionsSource);
    1871
    1872         if(!Permission::check('SITETREE_GRANT_ACCESS')) {
    1873             $fields->makeFieldReadonly($viewersOptionsField);
    1874             if($this->CanViewType == 'OnlyTheseUsers') {
    1875                 $fields->makeFieldReadonly($viewerGroupsField);
    1876             } else {
    1877                 $fields->removeByName('ViewerGroups');
    1878             }
    1879
    1880             $fields->makeFieldReadonly($editorsOptionsField);
    1881             if($this->CanEditType == 'OnlyTheseUsers') {
    1882                 $fields->makeFieldReadonly($editorGroupsField);
    1883             } else {
    1884                 $fields->removeByName('EditorGroups');
    1885             }
    1886         }
    1887
    1888         $tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content"));
    1889         $tabMain->setTitle(_t('SiteTree.TABMAIN', "Main"));
    1890         $tabMeta->setTitle(_t('SiteTree.TABMETA', "Metadata"));
    1891         $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behaviour"));
    1892         $tabAccess->setTitle(_t('SiteTree.TABACCESS', "Access"));
    1893         $tabToDo->setTitle(_t('SiteTree.TODO', "To Do") . ($this->ToDo ? ' **' : ''));
    1894
    1895         if(self::$runCMSFieldsExtensions) {
    1896             $this->extend('updateCMSFields', $fields);
    1897         }
    1898
    1899         return $fields;
    1900     }
    1901
    1902     /**
    1903      *
    1904      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
    1905      *
    1906      */
    1907     function fieldLabels($includerelations = true) {
    1908         $labels = parent::fieldLabels($includerelations);
    1909
    1910         $labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
    1911         $labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
    1912         $labels['MetaTagsHeader'] = _t('SiteTree.METAHEADER', "Search Engine Meta-tags");
    1913         $labels['MetaTitle'] = _t('SiteTree.METATITLE', "Title");
    1914         $labels['MetaDescription'] = _t('SiteTree.METADESC', "Description");
    1915         $labels['MetaKeywords'] = _t('SiteTree.METAKEYWORDS', "Keywords");
    1916         $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
    1917         $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", PR_MEDIUM, 'Classname of a page object');
    1918         $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location", PR_MEDIUM);
    1919         $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page", PR_MEDIUM);
    1920         $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
    1921         $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
    1922         $labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
    1923         $labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
    1924         $labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
    1925         $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', PR_MEDIUM, 'URL for this page');
    1926         $labels['Content'] = _t('SiteTree.Content', 'Content', PR_MEDIUM, 'Main HTML Content for a page');
    1927         $labels['HomepageForDomain'] = _t('SiteTree.HomepageForDomain', 'Hompage for this domain');
    1928         $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
    1929         $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
    1930         $labels['ToDo'] = _t('SiteTree.ToDo', 'Todo Notes');
    1931         $labels['Comments'] = _t('SiteTree.Comments', 'Comments');
    1932         $labels['LinkChangeNote'] = _t (
    1933             'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
    1934         );
    1935
    1936         if($includerelations){
    1937             $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', PR_MEDIUM, 'The parent page in the site hierarchy');
    1938             $labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
    1939             $labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
    1940         }
    1941
    1942         return $labels;
    1943     }
    1944
    1945     /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    1946
    1947     /**
    1948      * Get the actions available in the CMS for this page - eg Save, Publish.
    1949      * @return FieldSet The available actions for this page.
    1950      */
    1951     function getCMSActions() {
    1952         $actions = new FieldSet();
    1953
    1954         if($this->isPublished() && $this->canPublish() && !$this->IsDeletedFromStage) {
    1955             // "unpublish"
    1956             $unpublish = FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete');
    1957             $unpublish->describe(_t('SiteTree.BUTTONUNPUBLISHDESC', "Remove this page from the published site"));
    1958             $unpublish->addExtraClass('delete');
    1959             $actions->push($unpublish);
    1960         }
    1961
    1962         if($this->stagesDiffer('Stage', 'Live') && !$this->IsDeletedFromStage) {
    1963             if($this->isPublished() && $this->canEdit())    {
    1964                 // "rollback"
    1965                 $rollback = FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete');
    1966                 $rollback->describe(_t('SiteTree.BUTTONCANCELDRAFTDESC', "Delete your draft and revert to the currently published page"));
    1967                 $rollback->addExtraClass('delete');
    1968                 $actions->push($rollback);
    1969             }
    1970         }
    1971
    1972         if($this->canEdit()) {
    1973             if($this->IsDeletedFromStage) {
    1974                 if($this->ExistsOnLive) {
    1975                     // "restore"
    1976                     $actions->push(new FormAction('revert',_t('CMSMain.RESTORE','Restore')));
    1977                     if($this->canDelete() && $this->canDeleteFromLive()) {
    1978                         // "delete from live"
    1979                         $actions->push(new FormAction('deletefromlive',_t('CMSMain.DELETEFP','Unpublish')));
    1980                     }
    1981                 } else {
    1982                     // "restore"
    1983                     $actions->push(new FormAction('restore',_t('CMSMain.RESTORE','Restore')));
    1984                 }
    1985             } else {
    1986                 if($this->canDelete()) {
    1987                     // "delete"
    1988                     $actions->push($deleteAction = new FormAction('delete',_t('CMSMain.DELETE_DRAFT','Delete Draft')));
    1989                     $deleteAction->addExtraClass('delete');
    1990                 }
    1991
    1992                 // "save"
    1993                 $actions->push(new FormAction('save',_t('CMSMain.SAVE_DRAFT','Save Draft')));
    1994             }
    1995         }
    1996
    1997         if($this->canPublish() && !$this->IsDeletedFromStage) {
    1998             // "publish"
    1999             $actions->push(new FormAction('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save and Publish')));
    2000         }
    2001
    2002         // getCMSActions() can be extended with updateCMSActions() on a decorator
    2003         $this->extend('updateCMSActions', $actions);
    2004
    2005         return $actions;
    2006     }
    2007
    2008     /**
    2009      * Publish this page.
    2010      *
    2011      * @uses SiteTreeDecorator->onBeforePublish()
    2012      * @uses SiteTreeDecorator->onAfterPublish()
    2013      */
    2014     function doPublish() {
    2015         if (!$this->canPublish()) return false;
    2016
    2017         $original = Versioned::get_one_by_stage("SiteTree", "Live", "\"SiteTree\".\"ID\" = $this->ID");
    2018         if(!$original) $original = new SiteTree();
    2019
    2020         // Handle activities undertaken by decorators
    2021         $this->invokeWithExtensions('onBeforePublish', $original);
    2022         $this->Status = "Published";
    2023         //$this->PublishedByID = Member::currentUser()->ID;
    2024         $this->write();
    2025         $this->publish("Stage", "Live");
    2026
    2027         DB::query("UPDATE \"SiteTree_Live\"
    2028             SET \"Sort\" = (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\")
    2029             WHERE EXISTS (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\") AND \"ParentID\" = " . sprintf('%d', $this->ParentID) );
    2030
    2031         // Publish any virtual pages that might need publishing
    2032         $linkedPages = $this->VirtualPages();
    2033         if($linkedPages) foreach($linkedPages as $page) {
    2034             $page->copyFrom($page->CopyContentFrom());
    2035             $page->write();
    2036             if($page->ExistsOnLive) $page->doPublish();
    2037         }
    2038
    2039         // Need to update pages linking to this one as no longer broken, on the live site
    2040         $origMode = Versioned::get_reading_mode();
    2041         Versioned::reading_stage('Live');
    2042         foreach($this->DependentPages(false) as $page) {
    2043             // $page->write() calls syncLinkTracking, which does all the hard work for us.
    2044             $page->write();
    2045         }
    2046         Versioned::set_reading_mode($origMode);
    2047
    2048         // Check to write CMS homepage map.
    2049         $usingStaticPublishing = false;
    2050         foreach(ClassInfo::subclassesFor('StaticPublisher') as $class) if ($this->hasExtension($class)) $usingStaticPublishing = true;
    2051
    2052         // NOTE: if you change the path here, you must also change it in sapphire/static-main.php
    2053         if (self::$write_homepage_map) {
    2054             if ($usingStaticPublishing && $map = SiteTree::generate_homepage_domain_map()) {
    2055                 @file_put_contents(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php', "<?php\n\$homepageMap = ".var_export($map, true)."; ?>");
    2056             } else { if (file_exists(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php')) unlink(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php'); }
    2057         }
    2058
    2059         // Handle activities undertaken by decorators
    2060         $this->invokeWithExtensions('onAfterPublish', $original);
    2061
    2062         return true;
    2063     }
    2064
    2065     static function generate_homepage_domain_map() {
    2066         $domainSpecificHomepages = Versioned::get_by_stage('Page', 'Live', "\"HomepageForDomain\" != ''", "\"URLSegment\" ASC");
    2067         if (!$domainSpecificHomepages) return false;
    2068
    2069         $map = array();
    2070         foreach($domainSpecificHomepages->map('URLSegment', 'HomepageForDomain') as $url => $domains) {
    2071             foreach(explode(',', $domains) as $domain) $map[$domain] = $url;
    2072         }
    2073         return $map;
    2074     }
    2075
    2076     /**
    2077      * Unpublish this page - remove it from the live site
    2078      *
    2079      * @uses SiteTreeDecorator->onBeforeUnpublish()
    2080      * @uses SiteTreeDecorator->onAfterUnpublish()
    2081      */
    2082     function doUnpublish() {
    2083         if(!$this->canDeleteFromLive()) return false;
    2084         if(!$this->ID) return false;
    2085
    2086         $this->extend('onBeforeUnpublish');
    2087
    2088         $origStage = Versioned::current_stage();
    2089         Versioned::reading_stage('Live');
    2090
    2091         // We should only unpublish virtualpages that exist on live
    2092         $virtualPages = $this->VirtualPages();
    2093
    2094         // This way our ID won't be unset
    2095         $clone = clone $this;
    2096         $clone->delete();
    2097
    2098         // Rewrite backlinks
    2099         $dependentPages = $this->DependentPages(false);
    2100         if($dependentPages) foreach($dependentPages as $page) {
    2101             // $page->write() calls syncLinkTracking, which does all the hard work for us.
    2102             $page->write();
    2103         }
    2104         Versioned::reading_stage($origStage);
    2105
    2106         // Unpublish any published virtual pages
    2107         if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();
    2108
    2109         // If we're on the draft site, then we can update the status.
    2110         // Otherwise, these lines will resurrect an inappropriate record
    2111         if(DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()
    2112             && Versioned::current_stage() != 'Live') {
    2113             $this->Status = "Unpublished";
    2114             $this->write();
    2115         }
    2116
    2117         $this->extend('onAfterUnpublish');
    2118
    2119         return true;
    2120     }
    2121
    2122     /**
    2123      * Roll the draft version of this page to match the published page.
    2124      * Caution: Doesn't overwrite the object properties with the rolled back version.
    2125      *
    2126      * @param $version Either the string 'Live' or a version number
    2127      */
    2128     function doRollbackTo($version) {
    2129         $this->publish($version, "Stage", true);
    2130         $this->Status = "Saved (update)";
    2131         //$this->writeWithoutVersion(); // комментируем, т.к. из-за этого может портится поле Content (в функции onBeforeWrite при запуске syncLinkTracking (если текущий Content - пустой))
    2132     }
    2133
    2134     /**
    2135      * Revert the draft changes: replace the draft content with the content on live
    2136      */
    2137     function doRevertToLive() {
    2138         $this->publish("Live", "Stage", false);
    2139
    2140         // Use a clone to get the updates made by $this->publish
    2141         $clone = DataObject::get_by_id("SiteTree", $this->ID);
    2142         $clone->Status = "Published";
    2143         $clone->writeWithoutVersion();
    2144
    2145         // Need to update pages linking to this one as no longer broken
    2146         foreach($this->DependentPages(false) as $page) {
    2147             // $page->write() calls syncLinkTracking, which does all the hard work for us.
    2148             $page->write();
    2149         }
    2150
    2151         $this->extend('onAfterRevertToLive');
    2152     }
    2153
    2154     /**
    2155      * Restore the content in the active copy of this SiteTree page to the stage site.
    2156      * @return The SiteTree object.
    2157      */
    2158     function doRestoreToStage() {
    2159         // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
    2160         // create an empty record
    2161         if(!DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()) {
    2162             $conn = DB::getConn();
    2163             if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
    2164             DB::query("INSERT INTO \"SiteTree\" (\"ID\") VALUES ($this->ID)");
    2165             if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
    2166         }
    2167
    2168         $oldStage = Versioned::current_stage();
    2169         Versioned::reading_stage('Stage');
    2170         $this->forceChange();
    2171         $this->writeWithoutVersion();
    2172
    2173         $result = DataObject::get_by_id($this->class, $this->ID);
    2174
    2175         // Need to update pages linking to this one as no longer broken
    2176         foreach($result->DependentPages(false) as $page) {
    2177             // $page->write() calls syncLinkTracking, which does all the hard work for us.
    2178             $page->write();
    2179         }
    2180
    2181         Versioned::reading_stage($oldStage);
    2182
    2183         return $result;
    2184     }
    2185
    2186     /**
    2187      * Synonym of {@link doUnpublish}
    2188      */
    2189     function doDeleteFromLive() {
    2190         return $this->doUnpublish();
    2191     }
    2192
    2193     /**
    2194      * Check if this page is new - that is, if it has yet to have been written
    2195      * to the database.
    2196      *
    2197      * @return boolean True if this page is new.
    2198      */
    2199     function isNew() {
    2200         /**
    2201          * This check was a problem for a self-hosted site, and may indicate a
    2202          * bug in the interpreter on their server, or a bug here
    2203          * Changing the condition from empty($this->ID) to
    2204          * !$this->ID && !$this->record['ID'] fixed this.
    2205          */
    2206         if(empty($this->ID)) return true;
    2207
    2208         if(is_numeric($this->ID)) return false;
    2209
    2210         return stripos($this->ID, 'new') === 0;
    2211     }
    2212
    2213
    2214     /**
    2215      * Check if this page has been published.
    2216      *
    2217      * @return boolean True if this page has been published.
    2218      */
    2219     function isPublished() {
    2220         if($this->isNew())
    2221             return false;
    2222
    2223         return (DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $this->ID")->value())
    2224             ? true
    2225             : false;
    2226     }
    2227
    2228     /**
    2229      * Get the class dropdown used in the CMS to change the class of a page.
    2230      * This returns the list of options in the drop as a Map from class name
    2231      * to text in dropdown.
    2232      *
    2233      * @return array
    2234      */
    2235     protected function getClassDropdown() {
    2236         $classes = self::page_type_classes();
    2237         $currentClass = null;
    2238         $result = array();
    2239
    2240         $result = array();
    2241         foreach($classes as $class) {
    2242             $instance = singleton($class);
    2243             if((($instance instanceof HiddenClass) || !$instance->canCreate()) && ($class != $this->class)) continue;
    2244
    2245             $pageTypeName = $instance->i18n_singular_name();
    2246
    2247             if($class == $this->class) {
    2248                 $currentClass = $class;
    2249                 $result[$class] = $pageTypeName;
    2250             } else {
    2251                 $translation = _t(
    2252                     'SiteTree.CHANGETO',
    2253                     'Change to "%s"',
    2254                     PR_MEDIUM,
    2255                     "Pagetype selection dropdown with class names"
    2256                 );
    2257
    2258                 // @todo legacy fix to avoid empty classname dropdowns when translation doesn't include %s
    2259                 if(strpos($translation, '%s') !== FALSE) {
    2260                     $result[$class] = sprintf(
    2261                         $translation,
    2262                         $pageTypeName
    2263                     );
    2264                 } else {
    2265                     $result[$class] = "{$translation} \"{$pageTypeName}\"";
    2266                 }
    2267             }
    2268
    2269             // if we're in translation mode, the link between the translated pagetype
    2270             // title and the actual classname might not be obvious, so we add it in parantheses
    2271             // Example: class "RedirectorPage" has the title "Weiterleitung" in German,
    2272             // so it shows up as "Weiterleitung (RedirectorPage)"
    2273             if(i18n::get_locale() != 'en_US') {
    2274                 $result[$class] = $result[$class] .  " ({$class})";
    2275             }
    2276         }
    2277
    2278         // sort alphabetically, and put current on top
    2279         asort($result);
    2280         if($currentClass) {
    2281             $currentPageTypeName = $result[$currentClass];
    2282             unset($result[$currentClass]);
    2283             $result = array_reverse($result);
    2284             $result[$currentClass] = $currentPageTypeName;
    2285             $result = array_reverse($result);
    2286         }
    2287
    2288         return $result;
    2289     }
    2290
    2291
    2292     /**
    2293      * Returns an array of the class names of classes that are allowed
    2294      * to be children of this class.
    2295      *
    2296      * @return array
    2297      */
    2298     function allowedChildren() {
    2299         $allowedChildren = array();
    2300         $candidates = $this->stat('allowed_children');
    2301         if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
    2302             foreach($candidates as $candidate) {
    2303                 if(substr($candidate,0,1) == '*') {
    2304                     $allowedChildren[] = substr($candidate,1);
    2305                 } else {
    2306                     $subclasses = ClassInfo::subclassesFor($candidate);
    2307                     foreach($subclasses as $subclass) {
    2308                         if (ClassInfo::exists($subclass) && $subclass != "SiteTree_root") {
    2309                             $defParent = singleton($subclass)->defaultParent();
    2310                             if (!$defParent || ($defParent == $this->class)) {
    2311                                 $allowedChildren[] = $subclass;
    2312                             }
    2313                         }
    2314                     }
    2315                 }
    2316             }
    2317             return $allowedChildren;
    2318         }
    2319     }
    2320
    2321
    2322     /**
    2323      * Returns the class name of the default class for children of this page.
    2324      *
    2325      * @return string
    2326      */
    2327     function defaultChild() {
    2328         $default = $this->stat('default_child');
    2329         $allowed = $this->allowedChildren();
    2330         if($allowed) {
    2331             if(!$default || !in_array($default, $allowed))
    2332                 $default = reset($allowed);
    2333             return $default;
    2334         }
    2335     }
    2336
    2337
    2338     /**
    2339      * Returns the class name of the default class for the parent of this
    2340      * page.
    2341      *
    2342      * @return string
    2343      */
    2344     function defaultParent() {
    2345         return $this->stat('default_parent');
    2346     }
    2347
    2348
    2349     /**
    2350      * Function to clean up the currently loaded page after a reorganise has
    2351      * been called. It should return a piece of JavaScript to be executed on
    2352      * the client side, to clean up the results of the reorganise.
    2353      */
    2354     function cmsCleanup_parentChanged() {
    2355     }
    2356
    2357
    2358     /**
    2359      * Get the title for use in menus for this page. If the MenuTitle
    2360      * field is set it returns that, else it returns the Title field.
    2361      *
    2362      * @return string
    2363      */
    2364     function getMenuTitle(){
    2365         if($value = $this->getField("MenuTitle")) {
    2366             return $value;
    2367         } else {
    2368             return $this->getField("Title");
    2369         }
    2370     }
    2371
    2372
    2373     /**
    2374      * Set the menu title for this page.
    2375      *
    2376      * @param string $value
    2377      */
    2378     function setMenuTitle($value) {
    2379         if($value == $this->getField("Title")) {
    2380             $this->setField("MenuTitle", null);
    2381         } else {
    2382             $this->setField("MenuTitle", $value);
    2383         }
    2384     }
    2385
    2386     /**
    2387      * TitleWithStatus will return the title in an <ins>, <del> or
    2388      * <span class=\"modified\"> tag depending on its publication status.
    2389      *
    2390      * @return string
    2391      */
    2392     function TreeTitle() {
    2393         if($this->IsDeletedFromStage) {
    2394             if($this->ExistsOnLive) {
    2395                 $tag ="del title=\"" . _t('SiteTree.REMOVEDFROMDRAFT', 'Removed from draft site') . "\"";
    2396             } else {
    2397                 $tag ="del class=\"deletedOnLive\" title=\"" . _t('SiteTree.DELETEDPAGE', 'Deleted page') . "\"";
    2398             }
    2399         } elseif($this->IsAddedToStage) {
    2400             $tag = "ins title=\"" . _t('SiteTree.ADDEDTODRAFT', 'Added to draft site') . "\"";
    2401         } elseif($this->IsModifiedOnStage) {
    2402             $tag = "span title=\"" . _t('SiteTree.MODIFIEDONDRAFT', 'Modified on draft site') . "\" class=\"modified\"";
    2403         } else {
    2404             $tag = '';
    2405         }
    2406
    2407         $text = Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle));
    2408         return ($tag) ? "<$tag>" . $text . "</" . strtok($tag,' ') . ">" : $text;
    2409     }
    2410
    2411     /**
    2412      * Returns the page in the current page stack of the given level.
    2413      * Level(1) will return the main menu item that we're currently inside, etc.
    2414      */
    2415     public function Level($level) {
    2416         $parent = $this;
    2417         $stack = array($parent);
    2418         while($parent = $parent->Parent) {
    2419             array_unshift($stack, $parent);
    2420         }
    2421
    2422         return isset($stack[$level-1]) ? $stack[$level-1] : null;
    2423     }
    2424
    2425     /**
    2426      * Return the CSS classes to apply to this node in the CMS tree
    2427      *
    2428      * @param Controller $controller The controller object that the tree
    2429      *                               appears on
    2430      * @return string
    2431      */
    2432     function CMSTreeClasses($controller) {
    2433         $classes = $this->class;
    2434         if($this->HasBrokenFile || $this->HasBrokenLink)
    2435             $classes .= " BrokenLink";
    2436
    2437         if(!$this->canAddChildren())
    2438             $classes .= " nochildren";
    2439
    2440         if($controller->isCurrentPage($this))
    2441             $classes .= " current";
    2442
    2443         if(!$this->canEdit() && !$this->canAddChildren())
    2444             $classes .= " disabled";
    2445
    2446         if(!$this->ShowInMenus)
    2447             $classes .= " notinmenu";
    2448
    2449         //TODO: Add integration
    2450         /*
    2451         if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
    2452             $classes .= " untranslated ";
    2453         */
    2454         $classes .= $this->markingClasses();
    2455
    2456         return $classes;
    2457     }
    2458
    2459     /**
    2460      * Compares current draft with live version,
    2461      * and returns TRUE if no draft version of this page exists,
    2462      * but the page is still published (after triggering "Delete from draft site" in the CMS).
    2463      *
    2464      * @return boolean
    2465      */
    2466     function getIsDeletedFromStage() {
    2467         if(!$this->ID) return true;
    2468         if($this->isNew()) return false;
    2469
    2470         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
    2471
    2472         // Return true for both completely deleted pages and for pages just deleted from stage.
    2473         return !($stageVersion);
    2474     }
    2475
    2476     /**
    2477      * Return true if this page exists on the live site
    2478      */
    2479     function getExistsOnLive() {
    2480         return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
    2481     }
    2482
    2483     /**
    2484      * Compares current draft with live version,
    2485      * and returns TRUE if these versions differ,
    2486      * meaning there have been unpublished changes to the draft site.
    2487      *
    2488      * @return boolean
    2489      */
    2490     public function getIsModifiedOnStage() {
    2491         // new unsaved pages could be never be published
    2492         if($this->isNew()) return false;
    2493
    2494         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
    2495         $liveVersion =  Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
    2496
    2497         return ($stageVersion != $liveVersion);
    2498     }
    2499
    2500     /**
    2501      * Compares current draft with live version,
    2502      * and returns true if no live version exists,
    2503      * meaning the page was never published.
    2504      *
    2505      * @return boolean
    2506      */
    2507     public function getIsAddedToStage() {
    2508         // new unsaved pages could be never be published
    2509         if($this->isNew()) return false;
    2510
    2511         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
    2512         $liveVersion =  Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
    2513
    2514         return ($stageVersion && !$liveVersion);
    2515     }
    2516
    2517     /**
    2518      * Stops extendCMSFields() being called on getCMSFields().
    2519      * This is useful when you need access to fields added by subclasses
    2520      * of SiteTree in a decorator. Call before calling parent::getCMSFields(),
    2521      * and reenable afterwards.
    2522      */
    2523     public static function disableCMSFieldsExtensions() {
    2524         self::$runCMSFieldsExtensions = false;
    2525     }
    2526
    2527     /**
    2528      * Reenables extendCMSFields() being called on getCMSFields() after
    2529      * it has been disabled by disableCMSFieldsExtensions().
    2530      */
    2531     public static function enableCMSFieldsExtensions() {
    2532         self::$runCMSFieldsExtensions = true;
    2533     }
    2534
    2535     function providePermissions() {
    2536         return array(
    2537             'SITETREE_GRANT_ACCESS' => array(
    2538                 'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
    2539                 'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
    2540                 'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
    2541                 'sort' => 100
    2542             ),
    2543             'SITETREE_VIEW_ALL' => array(
    2544                 'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
    2545                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
    2546                 'sort' => -100,
    2547                 'help' => _t('SiteTree.VIEW_ALL_HELP', 'Ability to view any page on the site, regardless of the settings on the Access tab.  Requires the "Access to Site Content" permission')
    2548             ),
    2549             'SITETREE_EDIT_ALL' => array(
    2550                 'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
    2551                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
    2552                 'sort' => -50,
    2553                 'help' => _t('SiteTree.EDIT_ALL_HELP', 'Ability to edit any page on the site, regardless of the settings on the Access tab.  Requires the "Access to Site Content" permission')
    2554             ),
    2555             'SITETREE_REORGANISE' => array(
    2556                 'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
    2557                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
    2558                 'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
    2559                 'sort' => 100
    2560             ),
    2561             'VIEW_DRAFT_CONTENT' => array(
    2562                 'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
    2563                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
    2564                 'help' => _t('SiteTree.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
    2565                 'sort' => 100
    2566             )
    2567         );
    2568     }
    2569
    2570     /**
    2571      * Return the translated Singular name
    2572      *
    2573      * @return String
    2574      */
    2575     function i18n_singular_name() {
    2576         return _t($this->class.'.SINGULARNAME', $this->singular_name());
    2577     }
    2578
    2579     /**
    2580      * Overloaded to also provide entities for 'Page' class which is usually
    2581      * located in custom code, hence textcollector picks it up for the wrong folder.
    2582      */
    2583     function provideI18nEntities() {
    2584         $entities = parent::provideI18nEntities();
    2585
    2586         if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = 'sapphire';
    2587         if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = 'sapphire';
    2588
    2589         return $entities;
    2590     }
    2591
    2592     function getParentType() {
    2593         return $this->ParentID == 0 ? 'root' : 'subpage';
    2594     }
    2595
    2596     static function reset() {
    2597         self::$cache_permissions = array();
    2598     }
    2599
    2600 }
    2601
    2602 ?>
    2603

[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. -