Webylon 3.1 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • auth
  • Booking
  • cart
    • shipping
    • steppedcheckout
  • Catalog
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • BaseObjectCategory
  • BookingAdminPage
  • BookingPage
  • ErrorPage
  • ErrorPage_Controller
  • MediawebPage
  • Notifications
  • Page
  • Room
  • RoomCatalog
  • SiteConfig
  • SiteTree
  • SubsitesSelectorPage
  • SubsitesVirtualPage
  • SubsitesVirtualPage_Controller
  • VideoBankPage
  • VirtualPage
  • VirtualPage_Controller
  • VirtualProduct_Controller

Interfaces

  • HiddenClass
   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             $segment = ereg_replace('_+','_',$segment);
1355             $segment = ereg_replace('^[-_]+','',$segment);
1356             $segment = ereg_replace('[-_]+$','',$segment);
1357             
1358             // If after sanitising there is no URLSegment, give it a reasonable default
1359             if(!$segment) {
1360                 $segment = "page-$this->ID";
1361             }
1362             $this->URLSegment = $segment;
1363         }
1364         
1365         DataObject::set_context_obj($this);
1366         
1367         // Ensure that this object has a non-conflicting URLSegment value.
1368         $count = 2;
1369         while(!$this->validURLSegment()) {
1370             $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1371             $count++;
1372         }
1373         
1374         DataObject::set_context_obj(null);
1375 
1376         $this->syncLinkTracking();
1377 
1378         // Check to see if we've only altered fields that shouldn't affect versioning
1379         $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo');
1380         $changedFields = array_keys($this->getChangedFields(true, 2));
1381 
1382         // This more rigorous check is inline with the test that write()
1383         // does to dedcide whether or not to write to the DB.  We use that
1384         // to avoid cluttering the system with a migrateVersion() call
1385         // that doesn't get used
1386         $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1387 
1388         if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
1389             // This will have the affect of preserving the versioning
1390             $this->migrateVersion($this->Version);
1391         }
1392     }
1393     
1394     function syncLinkTracking() {
1395         // Build a list of HTMLText fields
1396         $allFields = $this->db();
1397         $htmlFields = array();
1398         foreach($allFields as $field => $fieldSpec) {
1399             if ($field == 'ExtraMeta') continue;
1400             if(preg_match('/([^(]+)/', $fieldSpec, $matches)) {
1401                 $class = $matches[0];
1402                 if(class_exists($class)){
1403                     if($class == 'HTMLText' || is_subclass_of($class, 'HTMLText')) $htmlFields[] = $field;
1404                 }
1405             }
1406         }
1407 
1408         $linkedPages = array();
1409         $linkedFiles = array();
1410         $this->HasBrokenLink = false;
1411         $this->HasBrokenFile = false;
1412         
1413         foreach($htmlFields as $field) {
1414             $formField = new HTMLEditorField($field);
1415             $formField->setValue($this->$field);
1416             $formField->saveInto($this);
1417         }
1418         
1419         // проверяем has_one связи с File
1420         if ($this->ID) {
1421             if (($has_one = $this->has_one()) && count($has_one)) {
1422                 foreach($has_one as $name => $type) {
1423                     if (singleton($type)->is_a('File') && !singleton($type)->is_a('Folder') && $this->{"{$name}ID"}) { // Folder не трекаем
1424                         if (!DataObject::get_by_id($type, $this->{"{$name}ID"})) {
1425                             $this->HasBrokenFile = true;
1426                         }
1427                     }
1428                 }
1429             }
1430         }
1431         $this->extend('augmentSyncLinkTracking');
1432     }
1433     
1434     function onAfterWrite() {
1435         // Need to flush cache to avoid outdated versionnumber references
1436         $this->flushCache();
1437         
1438         // Update any virtual pages that might need updating
1439         $linkedPages = $this->VirtualPages();
1440         if($linkedPages) foreach($linkedPages as $page) {
1441             $page->copyFrom($page->CopyContentFrom());
1442             $page->write();
1443         }
1444         
1445         parent::onAfterWrite();
1446     }
1447     
1448     function onBeforeDelete() {
1449         parent::onBeforeDelete();
1450         
1451         // If deleting this page, delete all its children.
1452         if(SiteTree::get_enforce_strict_hierarchy() && $children = $this->AllChildren(2)) {
1453             foreach($children as $child) {
1454                 $child->delete();
1455             }
1456         }
1457     }
1458     
1459     
1460     function onAfterDelete() {
1461         // Need to flush cache to avoid outdated versionnumber references
1462         $this->flushCache();
1463         
1464         // Need to mark pages depending to this one as broken
1465         $dependentPages = $this->DependentPages();
1466         if($dependentPages) foreach($dependentPages as $page) {
1467             // $page->write() calls syncLinkTracking, which does all the hard work for us.
1468             $page->write();
1469         }
1470         
1471         parent::onAfterDelete();
1472     }
1473     
1474     /**
1475      * Returns TRUE if this object has a URLSegment value that does not conflict with any other objects. This methods
1476      * checks for:
1477      *   - A page with the same URLSegment that has a conflict.
1478      *   - Conflicts with actions on the parent page.
1479      *   - A conflict caused by a root page having the same URLSegment as a class name.
1480      *
1481      * @return bool
1482      */
1483     public function validURLSegment() {
1484         if(self::nested_urls() && $parent = $this->Parent()) {
1485             if($controller = ModelAsController::controller_for($parent)) {
1486                 if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
1487             }
1488         }
1489         
1490         if(!self::nested_urls() || !$this->ParentID) {
1491             if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
1492         }
1493         
1494         $IDFilter     = ($this->ID) ? "AND \"SiteTree\".\"ID\" <> $this->ID" :  null;
1495         $parentFilter = null;
1496         
1497         if(self::nested_urls()) {
1498             if($this->ParentID) {
1499                 $parentFilter = " AND \"SiteTree\".\"ParentID\" = $this->ParentID";
1500             } else {
1501                 $parentFilter = ' AND "SiteTree"."ParentID" = 0';
1502             }
1503         }
1504         
1505         $existingPage = DataObject::get_one(
1506             'SiteTree', 
1507             "\"URLSegment\" = '$this->URLSegment' $IDFilter $parentFilter"
1508         );
1509         if ($existingPage) {
1510             return false;
1511         }
1512 
1513         $values = $this->extend('augmentValidURLSegment');
1514         if (count($values) && !min($values)) {
1515             return false;
1516         }
1517         
1518         return true;
1519     }
1520     
1521     /**
1522      * Generate a URL segment based on the title provided.
1523      * @param string $title Page title.
1524      * @return string Generated url segment
1525      */
1526     function generateURLSegment($title){
1527         $t = strtolower($title);
1528         $t = Convert::rus2lat($t);
1529         $t = str_replace('&amp;','-and-',$t);
1530         $t = str_replace('&','-and-',$t);
1531         $t = ereg_replace('[^A-Za-z0-9_-]+','-',$t);
1532         $t = ereg_replace('-+','-',$t);
1533         $t = ereg_replace('_+','_',$t);
1534         $t = ereg_replace('^[-_]+','',$t);
1535         $t = ereg_replace('[-_]+$','',$t);
1536 
1537         if(!$t || $t == '-' || $t == '-1') {
1538             $t = "page-$this->ID";
1539         }
1540         return trim($t, '-');
1541     }
1542     
1543     /**
1544      * Rewrite a file URL on this page, after its been renamed.
1545      * Triggers the onRenameLinkedAsset action on extensions.
1546      */
1547     function rewriteFileURL($old, $new) {
1548         $fields = $this->inheritedDatabaseFields();
1549         // Update the content without actually creating a new version
1550         foreach(array("SiteTree_Live", "SiteTree") as $table) {
1551             // Published site
1552             $published = DB::query("SELECT * FROM  \"$table\" WHERE \"ID\" = $this->ID")->record();
1553             $origPublished = $published;
1554 
1555             foreach($fields as $fieldName => $fieldType) {
1556                 if ($fieldType != 'HTMLText') continue;
1557 
1558                 // TODO: This doesn't work for HTMLText fields on other tables.
1559                 if(isset($published[$fieldName])) {
1560                     //$published[$fieldName] = str_replace($old, $new, $published[$fieldName], $numReplaced);
1561                     $oldFileMask = '!' . dirname($old) . '/(_resampled/resizedimage[0-9]+-)?' . basename($old) . '!';
1562                     $published[$fieldName] = preg_replace($oldFileMask, $new, $published[$fieldName], -1, $numReplaced);
1563                     if($numReplaced) {
1564                         DB::query("UPDATE \"$table\" SET \"$fieldName\" = '" 
1565                             . Convert::raw2sql($published[$fieldName]) . "' WHERE \"ID\" = $this->ID");
1566                             
1567                         // Tell static caching to update itself
1568                         if($table == 'SiteTree_Live') {
1569                             $publishedClass = $origPublished['ClassName'];
1570                             $origPublishedObj = new $publishedClass($origPublished);
1571                             $this->extend('onRenameLinkedAsset', $origPublishedObj);
1572                         }
1573                     }
1574                 }
1575             }
1576         }
1577     }
1578     
1579     /**
1580      * Returns the pages that depend on this page.
1581      * This includes virtual pages, pages that link to it, etc.
1582      * 
1583      * @param $includeVirtuals Set to false to exlcude virtual pages.
1584      */
1585     function DependentPages($includeVirtuals = true) {
1586         if(is_callable('Subsite::disable_subsite_filter')) Subsite::disable_subsite_filter(true);
1587         
1588         // Content links
1589         $items = $this->BackLinkTracking();
1590         if(!$items) $items = new DataObjectSet();
1591         else foreach($items as $item) $item->DependentLinkType = 'Content link';
1592         
1593         // Virtual pages
1594         if($includeVirtuals) {
1595             $virtuals = $this->VirtualPages();
1596             if($virtuals) {
1597                 foreach($virtuals as $item) $item->DependentLinkType = 'Virtual page';
1598                 $items->merge($virtuals);
1599             }
1600         }
1601 
1602         // Redirector pages
1603         $redirectors = DataObject::get("RedirectorPage", "\"RedirectionType\" = 'Internal' AND \"LinkToID\" = $this->ID");
1604         if($redirectors) {
1605             foreach($redirectors as $item) $item->DependentLinkType = 'Redirector page';
1606             $items->merge($redirectors);
1607         }
1608 
1609         if(is_callable('Subsite::disable_subsite_filter')) Subsite::disable_subsite_filter(false);
1610         return $items;
1611     }
1612     
1613     /**
1614      * Return the number of {@link DependentPages()}
1615      * 
1616      * @param $includeVirtuals Set to false to exlcude virtual pages.
1617      */
1618     function DependentPagesCount($includeVirtuals = true) {
1619         $links = DB::query("SELECT COUNT(*) FROM \"SiteTree_LinkTracking\" 
1620             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"SiteTree_LinkTracking\".\"SiteTreeID\"
1621             WHERE \"ChildID\" = $this->ID ")->value();
1622         if($includeVirtuals) {
1623             $virtuals = DB::query("SELECT COUNT(*) FROM \"VirtualPage\" 
1624             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"VirtualPage\".\"ID\"
1625             WHERE \"CopyContentFromID\" = $this->ID")->value();
1626         } else {
1627             $virtuals = 0;
1628         }
1629         $redirectors = DB::query("SELECT COUNT(*) FROM \"RedirectorPage\" 
1630             INNER JOIN \"SiteTree\" ON \"SiteTree\".\"ID\" = \"RedirectorPage\".\"ID\"
1631             WHERE \"RedirectionType\" = 'Internal' AND \"LinkToID\" = $this->ID")->value();
1632             
1633             
1634         return 0 + $links + $virtuals + $redirectors;
1635     }
1636     
1637     /**
1638      * Return all virtual pages that link to this page
1639      */
1640     function VirtualPages() {
1641         if(!$this->ID) return null;
1642         if(class_exists('Subsite')) {
1643             return Subsite::get_from_all_subsites('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
1644         } else {
1645             return DataObject::get('VirtualPage', "\"CopyContentFromID\" = " . (int)$this->ID);
1646         }
1647     }
1648 
1649     /**
1650      * Returns a FieldSet with which to create the CMS editing form.
1651      *
1652      * You can override this in your child classes to add extra fields - first
1653      * get the parent fields using parent::getCMSFields(), then use
1654      * addFieldToTab() on the FieldSet.
1655      *
1656      * @return FieldSet The fields to be displayed in the CMS.
1657      */
1658     function getCMSFields() {
1659         require_once("forms/Form.php");
1660         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/prototype/prototype.js");
1661         Requirements::javascript(SAPPHIRE_DIR . "/thirdparty/behaviour/behaviour.js");
1662         Requirements::javascript(CMS_DIR . "/javascript/SitetreeAccess.js");
1663         Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
1664         Requirements::javascript(SAPPHIRE_DIR . '/javascript/UpdateURL.js');
1665 
1666         // Status / message
1667         // Create a status message for multiple parents
1668         if($this->ID && is_numeric($this->ID)) {
1669             $linkedPages = $this->VirtualPages();
1670         }
1671         
1672         $parentPageLinks = array();
1673 
1674         if(isset($linkedPages)) {
1675             foreach($linkedPages as $linkedPage) {
1676                 $parentPage = $linkedPage->Parent;
1677                 if($parentPage) {
1678                     if($parentPage->ID) {
1679                         $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">{$parentPage->Title}</a>";
1680                     } else {
1681                         $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">" .
1682                             _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
1683                             "</a>";
1684                     }
1685                 }
1686             }
1687 
1688             $lastParent = array_pop($parentPageLinks);
1689             $parentList = "'$lastParent'";
1690 
1691             if(count($parentPageLinks) > 0) {
1692                 $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1693                     . $parentList;
1694             }
1695 
1696             $statusMessage[] = sprintf(
1697                 _t('SiteTree.APPEARSVIRTUALPAGES', "This content also appears on the virtual pages in the %s sections."),
1698                 $parentList
1699             );
1700         }
1701 
1702         if($this->HasBrokenLink || $this->HasBrokenFile) {
1703             $statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
1704         }
1705 
1706         $message = "STATUS: $this->Status<br />";
1707         if(isset($statusMessage)) {
1708             $message .= "NOTE: " . implode("<br />", $statusMessage);
1709         }
1710         
1711         $dependentNote = '';
1712         $dependentTable = new LiteralField('DependentNote', '<p>'._t('SiteTree.DEPENDENTNOTE','No dependent pages').'</p>');
1713         
1714         // Create a table for showing pages linked to this one
1715         $dependentPagesCount = $this->DependentPagesCount();
1716         if($dependentPagesCount) {
1717             $dependentColumns = array(
1718                 'Title' => $this->fieldLabel('Title'),
1719                 'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
1720                 'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
1721             );
1722             if(class_exists('Subsite')) $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1723             
1724             $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>');
1725             $dependentTable = new TableListField(
1726                 'DependentPages',
1727                 'SiteTree',
1728                 $dependentColumns
1729             );
1730             $dependentTable->setCustomSourceItems($this->DependentPages());
1731             $dependentTable->setFieldFormatting(array(
1732                 'Title' => '<a href=\"admin/show/$ID\">$Title</a>',
1733                 'AbsoluteLink' => '<a href=\"$value\">$value</a>',
1734             ));
1735             $dependentTable->setPermissions(array(
1736                 'show',
1737                 'export'
1738             ));
1739         }
1740         
1741         // Lay out the fields
1742         $fields = new FieldSet(
1743             $rootTab = new TabSet("Root",
1744                 $tabContent = new TabSet('Content',
1745                     $tabMain = new Tab('Main',
1746                         new TextField("Title", $this->fieldLabel('Title')),
1747                         new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1748                         new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", PR_MEDIUM, 'HTML editor title'))
1749                     ),
1750                     $tabMeta = new Tab('Metadata',
1751                         new FieldGroup(_t('SiteTree.URL', "URL"),
1752                             new LabelField('BaseUrlLabel',Controller::join_links (
1753                                 Director::absoluteBaseURL(),
1754                                 (self::nested_urls() && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
1755                             )),
1756                             new UniqueRestrictedTextField("URLSegment",
1757                                 "URLSegment",
1758                                 "SiteTree",
1759                                 _t('SiteTree.VALIDATIONURLSEGMENT1', "Another page is using that URL. URL must be unique for each page"),
1760                                 "[^A-Za-z0-9_-]+",
1761                                 "-",
1762                                 _t('SiteTree.VALIDATIONURLSEGMENT2', "URLs can only be made up of letters, digits and hyphens."),
1763                                 "",
1764                                 "",
1765                                 250
1766                             ),
1767                             new LabelField('TrailingSlashLabel',"/")
1768                         ),
1769                         new LiteralField('LinkChangeNote', self::nested_urls() && count($this->AllChildren()) ?
1770                             '<p>' . $this->fieldLabel('LinkChangeNote'). '</p>' : null
1771                         ),
1772                         new HeaderField('MetaTagsHeader',$this->fieldLabel('MetaTagsHeader')),
1773                         new TextField("MetaTitle", $this->fieldLabel('MetaTitle')),
1774                         new TextareaField("MetaKeywords", $this->fieldLabel('MetaKeywords'), 1),
1775                         new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1776                         new TextareaField("ExtraMeta",$this->fieldLabel('ExtraMeta'))
1777                     )
1778                 ),
1779                 $tabBehaviour = new Tab('Behaviour',
1780                     new DropdownField(
1781                         "ClassName", 
1782                         $this->fieldLabel('ClassName'), 
1783                         $this->getClassDropdown()
1784                     ),
1785                     
1786                     new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
1787                         "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1788                         "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page (choose below)"),
1789                     )),
1790                     $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree'),
1791                     
1792                     new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1793                     new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch')),
1794                     /*, new TreeMultiselectField("MultipleParents", "Page appears within", "SiteTree")*/
1795                     new CheckboxField("ProvideComments", $this->fieldLabel('ProvideComments')),
1796                     new LiteralField(
1797                         "HomepageForDomainInfo", 
1798                         "<p>" . 
1799                             _t('SiteTree.NOTEUSEASHOMEPAGE', 
1800                             "Use this page as the 'home page' for the following domains: 
1801                             (separate multiple domains with commas)") .
1802                         "</p>"
1803                     ),
1804                     new TextField(
1805                         "HomepageForDomain",
1806                         _t('SiteTree.HOMEPAGEFORDOMAIN', "Domain(s)", PR_MEDIUM, 'Listing domains that should be used as homepage')
1807                     ),
1808                     new NumericField(
1809                         "NumberCMSChildren", 
1810                         _t('SiteTree.NUMBERCMSCHILDREN',"Total displayed subpages in CMS (must be numeric, defaults to 0 == all, page refresh required after changing this)"),
1811                         $this->NumberCMSChildren
1812                     )
1813 
1814                 ),
1815                 $tabToDo = new Tab('Todo',
1816                     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>")),
1817                     new TextareaField("ToDo", "")
1818                 ),
1819                 $tabDependent = new Tab('Dependent',
1820                     $dependentNote,
1821                     $dependentTable
1822                 ),
1823                 $tabAccess = new Tab('Access',
1824                     new HeaderField('WhoCanViewHeader',_t('SiteTree.ACCESSHEADER', "Who can view this page?"), 2),
1825                     $viewersOptionsField = new OptionsetField(
1826                         "CanViewType", 
1827                         ""
1828                     ),
1829                     $viewerGroupsField = new TreeMultiselectField("ViewerGroups", $this->fieldLabel('ViewerGroups')),
1830                     new HeaderField('WhoCanEditHeader',_t('SiteTree.EDITHEADER', "Who can edit this page?"), 2),
1831                     $editorsOptionsField = new OptionsetField(
1832                         "CanEditType", 
1833                         ""
1834                     ),
1835                     $editorGroupsField = new TreeMultiselectField("EditorGroups", $this->fieldLabel('EditorGroups'))
1836                 )
1837             )
1838             //new NamedLabelField("Status", $message, "pageStatusMessage", true)
1839         );
1840 
1841         if ($this->stat('allowed_children') == 'none')
1842             $fields->removeByName("NumberCMSChildren");
1843 
1844 
1845 
1846         /*
1847          * This filter ensures that the ParentID dropdown selection does not show this node,
1848          * or its descendents, as this causes vanishing bugs.
1849          */
1850         $parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
1851         
1852         // Conditional dependent pages tab
1853         if($dependentPagesCount) $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1854         else $fields->removeFieldFromTab('Root', 'Dependent');
1855         
1856         // Make page location fields read-only if the user doesn't have the appropriate permission
1857         if(!Permission::check("SITETREE_REORGANISE")) {
1858             $fields->makeFieldReadonly('ParentType');
1859             if($this->ParentType == 'root') {
1860                 $fields->removeByName('ParentID');
1861             } else {
1862                 $fields->makeFieldReadonly('ParentID');
1863             }
1864         }
1865         
1866         $viewersOptionsSource = array();
1867         $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
1868         $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
1869         $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
1870         $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
1871         $viewersOptionsField->setSource($viewersOptionsSource);
1872         
1873         $editorsOptionsSource = array();
1874         $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
1875         $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
1876         $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
1877         $editorsOptionsField->setSource($editorsOptionsSource);
1878 
1879         if(!Permission::check('SITETREE_GRANT_ACCESS')) {
1880             $fields->makeFieldReadonly($viewersOptionsField);
1881             if($this->CanViewType == 'OnlyTheseUsers') {
1882                 $fields->makeFieldReadonly($viewerGroupsField);
1883             } else {
1884                 $fields->removeByName('ViewerGroups');
1885             }
1886             
1887             $fields->makeFieldReadonly($editorsOptionsField);
1888             if($this->CanEditType == 'OnlyTheseUsers') {
1889                 $fields->makeFieldReadonly($editorGroupsField);
1890             } else {
1891                 $fields->removeByName('EditorGroups');
1892             }
1893         }
1894         
1895         $tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content"));
1896         $tabMain->setTitle(_t('SiteTree.TABMAIN', "Main"));
1897         $tabMeta->setTitle(_t('SiteTree.TABMETA', "Metadata"));
1898         $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behaviour"));
1899         $tabAccess->setTitle(_t('SiteTree.TABACCESS', "Access"));
1900         $tabToDo->setTitle(_t('SiteTree.TODO', "To Do") . ($this->ToDo ? ' **' : ''));
1901         
1902         if(self::$runCMSFieldsExtensions) {
1903             $this->extend('updateCMSFields', $fields);
1904         }
1905 
1906         return $fields;
1907     }
1908     
1909     /**
1910      *
1911      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1912      * 
1913      */
1914     function fieldLabels($includerelations = true) {
1915         $labels = parent::fieldLabels($includerelations);
1916         
1917         $labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
1918         $labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
1919         $labels['MetaTagsHeader'] = _t('SiteTree.METAHEADER', "Search Engine Meta-tags");
1920         $labels['MetaTitle'] = _t('SiteTree.METATITLE', "Title");
1921         $labels['MetaDescription'] = _t('SiteTree.METADESC', "Description");
1922         $labels['MetaKeywords'] = _t('SiteTree.METAKEYWORDS', "Keywords");
1923         $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
1924         $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", PR_MEDIUM, 'Classname of a page object');
1925         $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location", PR_MEDIUM);
1926         $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page", PR_MEDIUM);
1927         $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
1928         $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
1929         $labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
1930         $labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
1931         $labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
1932         $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', PR_MEDIUM, 'URL for this page');
1933         $labels['Content'] = _t('SiteTree.Content', 'Content', PR_MEDIUM, 'Main HTML Content for a page');
1934         $labels['HomepageForDomain'] = _t('SiteTree.HomepageForDomain', 'Hompage for this domain');
1935         $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
1936         $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
1937         $labels['ToDo'] = _t('SiteTree.ToDo', 'Todo Notes');
1938         $labels['Comments'] = _t('SiteTree.Comments', 'Comments');
1939         $labels['LinkChangeNote'] = _t (
1940             'SiteTree.LINKCHANGENOTE', 'Changing this page\'s link will also affect the links of all child pages.'
1941         );
1942         
1943         if($includerelations){
1944             $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', PR_MEDIUM, 'The parent page in the site hierarchy');
1945             $labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
1946             $labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
1947         }
1948                 
1949         return $labels;
1950     }
1951 
1952     /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1953 
1954     /**
1955      * Get the actions available in the CMS for this page - eg Save, Publish.
1956      * @return FieldSet The available actions for this page.
1957      */
1958     function getCMSActions() {
1959         $actions = new FieldSet();
1960 
1961         if($this->isPublished() && $this->canPublish() && !$this->IsDeletedFromStage) {
1962             // "unpublish"
1963             $unpublish = FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete');
1964             $unpublish->describe(_t('SiteTree.BUTTONUNPUBLISHDESC', "Remove this page from the published site"));
1965             $unpublish->addExtraClass('delete');
1966             $actions->push($unpublish);
1967         }
1968 
1969         if($this->stagesDiffer('Stage', 'Live') && !$this->IsDeletedFromStage) {
1970             if($this->isPublished() && $this->canEdit())    {
1971                 // "rollback"
1972                 $rollback = FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete');
1973                 $rollback->describe(_t('SiteTree.BUTTONCANCELDRAFTDESC', "Delete your draft and revert to the currently published page"));
1974                 $rollback->addExtraClass('delete');
1975                 $actions->push($rollback);
1976             }
1977         }
1978 
1979         if($this->canEdit()) {
1980             if($this->IsDeletedFromStage) {
1981                 if($this->ExistsOnLive) {
1982                     // "restore"
1983                     $actions->push(new FormAction('revert',_t('CMSMain.RESTORE','Restore')));
1984                     if($this->canDelete() && $this->canDeleteFromLive()) {
1985                         // "delete from live"
1986                         $actions->push(new FormAction('deletefromlive',_t('CMSMain.DELETEFP','Unpublish')));
1987                     }
1988                 } else {
1989                     // "restore"
1990                     $actions->push(new FormAction('restore',_t('CMSMain.RESTORE','Restore')));
1991                 }
1992             } else {
1993                 if($this->canDelete()) {
1994                     // "delete"
1995                     $actions->push($deleteAction = new FormAction('delete',_t('CMSMain.DELETE_DRAFT','Delete Draft')));
1996                     $deleteAction->addExtraClass('delete');
1997                 }
1998             
1999                 // "save"
2000                 $actions->push(new FormAction('save',_t('CMSMain.SAVE_DRAFT','Save Draft')));
2001             }
2002         }
2003 
2004         if($this->canPublish() && !$this->IsDeletedFromStage) {
2005             // "publish"
2006             $actions->push(new FormAction('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save and Publish')));
2007         }
2008         
2009         // getCMSActions() can be extended with updateCMSActions() on a decorator
2010         $this->extend('updateCMSActions', $actions);
2011         
2012         return $actions;
2013     }
2014     
2015     /**
2016      * Publish this page.
2017      * 
2018      * @uses SiteTreeDecorator->onBeforePublish()
2019      * @uses SiteTreeDecorator->onAfterPublish()
2020      */
2021     function doPublish() {
2022         if (!$this->canPublish()) return false;
2023         
2024         $original = Versioned::get_one_by_stage("SiteTree", "Live", "\"SiteTree\".\"ID\" = $this->ID");
2025         if(!$original) $original = new SiteTree();
2026 
2027         // Handle activities undertaken by decorators
2028         $this->invokeWithExtensions('onBeforePublish', $original);
2029         $this->Status = "Published";
2030         //$this->PublishedByID = Member::currentUser()->ID;
2031         $this->write();
2032         $this->publish("Stage", "Live");
2033 
2034         DB::query("UPDATE \"SiteTree_Live\"
2035             SET \"Sort\" = (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\")
2036             WHERE EXISTS (SELECT \"SiteTree\".\"Sort\" FROM \"SiteTree\" WHERE \"SiteTree_Live\".\"ID\" = \"SiteTree\".\"ID\") AND \"ParentID\" = " . sprintf('%d', $this->ParentID) );
2037             
2038         // Publish any virtual pages that might need publishing
2039         $linkedPages = $this->VirtualPages();
2040         if($linkedPages) foreach($linkedPages as $page) {
2041             $page->copyFrom($page->CopyContentFrom());
2042             $page->write();
2043             if($page->ExistsOnLive) $page->doPublish();
2044         }
2045         
2046         // Need to update pages linking to this one as no longer broken, on the live site
2047         $origMode = Versioned::get_reading_mode();
2048         Versioned::reading_stage('Live');
2049         foreach($this->DependentPages(false) as $page) {
2050             // $page->write() calls syncLinkTracking, which does all the hard work for us.
2051             $page->write();
2052         }
2053         Versioned::set_reading_mode($origMode);
2054         
2055         // Check to write CMS homepage map.
2056         $usingStaticPublishing = false;
2057         foreach(ClassInfo::subclassesFor('StaticPublisher') as $class) if ($this->hasExtension($class)) $usingStaticPublishing = true;
2058 
2059         // NOTE: if you change the path here, you must also change it in sapphire/static-main.php
2060         if (self::$write_homepage_map) {
2061             if ($usingStaticPublishing && $map = SiteTree::generate_homepage_domain_map()) {
2062                 @file_put_contents(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php', "<?php\n\$homepageMap = ".var_export($map, true)."; ?>");
2063             } else { if (file_exists(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php')) unlink(BASE_PATH.'/'.ASSETS_DIR.'/_homepage-map.php'); }
2064         }
2065         
2066         // Handle activities undertaken by decorators
2067         $this->invokeWithExtensions('onAfterPublish', $original);
2068         
2069         return true;
2070     }
2071     
2072     static function generate_homepage_domain_map() {
2073         $domainSpecificHomepages = Versioned::get_by_stage('Page', 'Live', "\"HomepageForDomain\" != ''", "\"URLSegment\" ASC");
2074         if (!$domainSpecificHomepages) return false;
2075         
2076         $map = array();
2077         foreach($domainSpecificHomepages->map('URLSegment', 'HomepageForDomain') as $url => $domains) {
2078             foreach(explode(',', $domains) as $domain) $map[$domain] = $url;
2079         }
2080         return $map;
2081     }
2082     
2083     /**
2084      * Unpublish this page - remove it from the live site
2085      * 
2086      * @uses SiteTreeDecorator->onBeforeUnpublish()
2087      * @uses SiteTreeDecorator->onAfterUnpublish()
2088      */
2089     function doUnpublish() {
2090         if(!$this->canDeleteFromLive()) return false;
2091         if(!$this->ID) return false;
2092         
2093         $this->extend('onBeforeUnpublish');
2094         
2095         $origStage = Versioned::current_stage();
2096         Versioned::reading_stage('Live');
2097 
2098         // We should only unpublish virtualpages that exist on live
2099         $virtualPages = $this->VirtualPages();
2100 
2101         // This way our ID won't be unset
2102         $clone = clone $this;
2103         $clone->delete();
2104 
2105         // Rewrite backlinks
2106         $dependentPages = $this->DependentPages(false);
2107         if($dependentPages) foreach($dependentPages as $page) {
2108             // $page->write() calls syncLinkTracking, which does all the hard work for us.
2109             $page->write();
2110         }
2111         Versioned::reading_stage($origStage);
2112 
2113         // Unpublish any published virtual pages
2114         if ($virtualPages) foreach($virtualPages as $vp) $vp->doUnpublish();
2115 
2116         // If we're on the draft site, then we can update the status.
2117         // Otherwise, these lines will resurrect an inappropriate record
2118         if(DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()
2119             && Versioned::current_stage() != 'Live') {
2120             $this->Status = "Unpublished";
2121             $this->write();
2122         }
2123 
2124         $this->extend('onAfterUnpublish');
2125 
2126         return true;
2127     }
2128     
2129     /**
2130      * Roll the draft version of this page to match the published page.
2131      * Caution: Doesn't overwrite the object properties with the rolled back version.
2132      * 
2133      * @param $version Either the string 'Live' or a version number
2134      */
2135     function doRollbackTo($version) {
2136         $this->publish($version, "Stage", true);
2137         $this->Status = "Saved (update)";
2138         //$this->writeWithoutVersion(); // комментируем, т.к. из-за этого может портится поле Content (в функции onBeforeWrite при запуске syncLinkTracking (если текущий Content - пустой))
2139     }
2140     
2141     /**
2142      * Revert the draft changes: replace the draft content with the content on live
2143      */
2144     function doRevertToLive() {
2145         $this->publish("Live", "Stage", false);
2146 
2147         // Use a clone to get the updates made by $this->publish
2148         $clone = DataObject::get_by_id("SiteTree", $this->ID);
2149         $clone->Status = "Published";
2150         $clone->writeWithoutVersion();
2151 
2152         // Need to update pages linking to this one as no longer broken
2153         foreach($this->DependentPages(false) as $page) {
2154             // $page->write() calls syncLinkTracking, which does all the hard work for us.
2155             $page->write();
2156         }
2157         
2158         $this->extend('onAfterRevertToLive');
2159     }
2160     
2161     /**
2162      * Restore the content in the active copy of this SiteTree page to the stage site.
2163      * @return The SiteTree object.
2164      */
2165     function doRestoreToStage() {
2166         // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2167         // create an empty record
2168         if(!DB::query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = $this->ID")->value()) {
2169             $conn = DB::getConn();
2170             if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', true);
2171             DB::query("INSERT INTO \"SiteTree\" (\"ID\") VALUES ($this->ID)");
2172             if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('SiteTree', false);
2173         }
2174         
2175         $oldStage = Versioned::current_stage();
2176         Versioned::reading_stage('Stage');
2177         $this->forceChange();
2178         $this->writeWithoutVersion();
2179         
2180         $result = DataObject::get_by_id($this->class, $this->ID);
2181 
2182         // Need to update pages linking to this one as no longer broken
2183         foreach($result->DependentPages(false) as $page) {
2184             // $page->write() calls syncLinkTracking, which does all the hard work for us.
2185             $page->write();
2186         }
2187         
2188         Versioned::reading_stage($oldStage);
2189         
2190         return $result;
2191     }
2192 
2193     /**
2194      * Synonym of {@link doUnpublish}
2195      */
2196     function doDeleteFromLive() {
2197         return $this->doUnpublish();
2198     }
2199 
2200     /**
2201      * Check if this page is new - that is, if it has yet to have been written
2202      * to the database.
2203      *
2204      * @return boolean True if this page is new.
2205      */
2206     function isNew() {
2207         /**
2208          * This check was a problem for a self-hosted site, and may indicate a
2209          * bug in the interpreter on their server, or a bug here
2210          * Changing the condition from empty($this->ID) to
2211          * !$this->ID && !$this->record['ID'] fixed this.
2212          */
2213         if(empty($this->ID)) return true;
2214 
2215         if(is_numeric($this->ID)) return false;
2216 
2217         return stripos($this->ID, 'new') === 0;
2218     }
2219 
2220 
2221     /**
2222      * Check if this page has been published.
2223      *
2224      * @return boolean True if this page has been published.
2225      */
2226     function isPublished() {
2227         if($this->isNew())
2228             return false;
2229 
2230         return (DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $this->ID")->value())
2231             ? true
2232             : false;
2233     }
2234 
2235     /**
2236      * Get the class dropdown used in the CMS to change the class of a page.
2237      * This returns the list of options in the drop as a Map from class name
2238      * to text in dropdown.
2239      *
2240      * @return array
2241      */
2242     protected function getClassDropdown() {
2243         $classes = self::page_type_classes();
2244         $currentClass = null;
2245         $result = array();
2246         
2247         $result = array();
2248         foreach($classes as $class) {
2249             $instance = singleton($class);
2250             if((($instance instanceof HiddenClass) || !$instance->canCreate()) && ($class != $this->class)) continue;
2251 
2252             $pageTypeName = $instance->i18n_singular_name();
2253 
2254             if($class == $this->class) {
2255                 $currentClass = $class;
2256                 $result[$class] = $pageTypeName;
2257             } else {
2258                 $translation = _t(
2259                     'SiteTree.CHANGETO', 
2260                     'Change to "%s"', 
2261                     PR_MEDIUM,
2262                     "Pagetype selection dropdown with class names"
2263                 );
2264 
2265                 // @todo legacy fix to avoid empty classname dropdowns when translation doesn't include %s
2266                 if(strpos($translation, '%s') !== FALSE) {
2267                     $result[$class] = sprintf(
2268                         $translation, 
2269                         $pageTypeName
2270                     );
2271                 } else {
2272                     $result[$class] = "{$translation} \"{$pageTypeName}\"";
2273                 }
2274             }
2275 
2276             // if we're in translation mode, the link between the translated pagetype
2277             // title and the actual classname might not be obvious, so we add it in parantheses
2278             // Example: class "RedirectorPage" has the title "Weiterleitung" in German,
2279             // so it shows up as "Weiterleitung (RedirectorPage)"
2280             if(i18n::get_locale() != 'en_US') {
2281                 $result[$class] = $result[$class] .  " ({$class})";
2282             }
2283         }
2284         
2285         // sort alphabetically, and put current on top
2286         asort($result);
2287         if($currentClass) {
2288             $currentPageTypeName = $result[$currentClass];
2289             unset($result[$currentClass]);
2290             $result = array_reverse($result);
2291             $result[$currentClass] = $currentPageTypeName;
2292             $result = array_reverse($result);
2293         }
2294         
2295         return $result;
2296     }
2297 
2298 
2299     /**
2300      * Returns an array of the class names of classes that are allowed
2301      * to be children of this class.
2302      *
2303      * @return array
2304      */
2305     function allowedChildren() {
2306         $allowedChildren = array();
2307         $candidates = $this->stat('allowed_children');
2308         if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2309             foreach($candidates as $candidate) {
2310                 if(substr($candidate,0,1) == '*') {
2311                     $allowedChildren[] = substr($candidate,1);
2312                 } else {
2313                     $subclasses = ClassInfo::subclassesFor($candidate);
2314                     foreach($subclasses as $subclass) {
2315                         if (ClassInfo::exists($subclass) && $subclass != "SiteTree_root") {
2316                             $defParent = singleton($subclass)->defaultParent();
2317                             if (!$defParent || ($defParent == $this->class)) {
2318                                 $allowedChildren[] = $subclass;
2319                             }
2320                         }
2321                     }
2322                 }
2323             }
2324             return $allowedChildren;
2325         }
2326     }
2327 
2328 
2329     /**
2330      * Returns the class name of the default class for children of this page.
2331      *
2332      * @return string
2333      */
2334     function defaultChild() {
2335         $default = $this->stat('default_child');
2336         $allowed = $this->allowedChildren();
2337         if($allowed) {
2338             if(!$default || !in_array($default, $allowed))
2339                 $default = reset($allowed);
2340             return $default;
2341         }
2342     }
2343 
2344 
2345     /**
2346      * Returns the class name of the default class for the parent of this
2347      * page.
2348      *
2349      * @return string
2350      */
2351     function defaultParent() {
2352         return $this->stat('default_parent');
2353     }
2354 
2355 
2356     /**
2357      * Function to clean up the currently loaded page after a reorganise has
2358      * been called. It should return a piece of JavaScript to be executed on
2359      * the client side, to clean up the results of the reorganise.
2360      */
2361     function cmsCleanup_parentChanged() {
2362     }
2363 
2364 
2365     /**
2366      * Get the title for use in menus for this page. If the MenuTitle
2367      * field is set it returns that, else it returns the Title field.
2368      *
2369      * @return string
2370      */
2371     function getMenuTitle(){
2372         if($value = $this->getField("MenuTitle")) {
2373             return $value;
2374         } else {
2375             return $this->getField("Title");
2376         }
2377     }
2378 
2379 
2380     /**
2381      * Set the menu title for this page.
2382      *
2383      * @param string $value
2384      */
2385     function setMenuTitle($value) {
2386         if($value == $this->getField("Title")) {
2387             $this->setField("MenuTitle", null);
2388         } else {
2389             $this->setField("MenuTitle", $value);
2390         }
2391     }
2392 
2393     /**
2394      * TitleWithStatus will return the title in an <ins>, <del> or
2395      * <span class=\"modified\"> tag depending on its publication status.
2396      *
2397      * @return string
2398      */
2399     function TreeTitle() {
2400         if($this->IsDeletedFromStage) {
2401             if($this->ExistsOnLive) {
2402                 $tag ="del title=\"" . _t('SiteTree.REMOVEDFROMDRAFT', 'Removed from draft site') . "\"";
2403             } else {
2404                 $tag ="del class=\"deletedOnLive\" title=\"" . _t('SiteTree.DELETEDPAGE', 'Deleted page') . "\"";
2405             }
2406         } elseif($this->IsAddedToStage) {
2407             $tag = "ins title=\"" . _t('SiteTree.ADDEDTODRAFT', 'Added to draft site') . "\"";
2408         } elseif($this->IsModifiedOnStage) {
2409             $tag = "span title=\"" . _t('SiteTree.MODIFIEDONDRAFT', 'Modified on draft site') . "\" class=\"modified\"";
2410         } else {
2411             $tag = '';
2412         }
2413 
2414         $text = Convert::raw2xml(str_replace(array("\n","\r"),"",$this->MenuTitle));
2415         return ($tag) ? "<$tag>" . $text . "</" . strtok($tag,' ') . ">" : $text;
2416     }
2417 
2418     /**
2419      * Returns the page in the current page stack of the given level.
2420      * Level(1) will return the main menu item that we're currently inside, etc.
2421      */
2422     public function Level($level) {
2423         $parent = $this;
2424         $stack = array($parent);
2425         while($parent = $parent->Parent) {
2426             array_unshift($stack, $parent);
2427         }
2428 
2429         return isset($stack[$level-1]) ? $stack[$level-1] : null;
2430     }
2431     
2432     /**
2433      * Return the CSS classes to apply to this node in the CMS tree
2434      *
2435      * @param Controller $controller The controller object that the tree
2436      *                               appears on
2437      * @return string
2438      */
2439     function CMSTreeClasses($controller) {
2440         $classes = $this->class;
2441         if($this->HasBrokenFile || $this->HasBrokenLink)
2442             $classes .= " BrokenLink";
2443 
2444         if(!$this->canAddChildren())
2445             $classes .= " nochildren";
2446 
2447         if($controller->isCurrentPage($this))
2448             $classes .= " current";
2449 
2450         if(!$this->canEdit() && !$this->canAddChildren()) 
2451             $classes .= " disabled";
2452 
2453         if(!$this->ShowInMenus) 
2454             $classes .= " notinmenu";
2455             
2456         //TODO: Add integration
2457         /*
2458         if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2459             $classes .= " untranslated ";
2460         */
2461         $classes .= $this->markingClasses();
2462 
2463         return $classes;
2464     }
2465     
2466     /**
2467      * Compares current draft with live version,
2468      * and returns TRUE if no draft version of this page exists,
2469      * but the page is still published (after triggering "Delete from draft site" in the CMS).
2470      * 
2471      * @return boolean
2472      */
2473     function getIsDeletedFromStage() {
2474         if(!$this->ID) return true;
2475         if($this->isNew()) return false;
2476         
2477         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2478 
2479         // Return true for both completely deleted pages and for pages just deleted from stage.
2480         return !($stageVersion);
2481     }
2482     
2483     /**
2484      * Return true if this page exists on the live site
2485      */
2486     function getExistsOnLive() {
2487         return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2488     }
2489 
2490     /**
2491      * Compares current draft with live version,
2492      * and returns TRUE if these versions differ,
2493      * meaning there have been unpublished changes to the draft site.
2494      * 
2495      * @return boolean
2496      */
2497     public function getIsModifiedOnStage() {
2498         // new unsaved pages could be never be published
2499         if($this->isNew()) return false;
2500         
2501         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2502         $liveVersion =  Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2503 
2504         return ($stageVersion != $liveVersion);
2505     }
2506     
2507     /**
2508      * Compares current draft with live version,
2509      * and returns true if no live version exists,
2510      * meaning the page was never published.
2511      * 
2512      * @return boolean
2513      */
2514     public function getIsAddedToStage() {
2515         // new unsaved pages could be never be published
2516         if($this->isNew()) return false;
2517         
2518         $stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
2519         $liveVersion =  Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
2520 
2521         return ($stageVersion && !$liveVersion);
2522     }
2523     
2524     /**
2525      * Stops extendCMSFields() being called on getCMSFields().
2526      * This is useful when you need access to fields added by subclasses
2527      * of SiteTree in a decorator. Call before calling parent::getCMSFields(),
2528      * and reenable afterwards.
2529      */
2530     public static function disableCMSFieldsExtensions() {
2531         self::$runCMSFieldsExtensions = false;
2532     }
2533     
2534     /**
2535      * Reenables extendCMSFields() being called on getCMSFields() after
2536      * it has been disabled by disableCMSFieldsExtensions().
2537      */
2538     public static function enableCMSFieldsExtensions() {
2539         self::$runCMSFieldsExtensions = true;
2540     }
2541 
2542     function providePermissions() {
2543         return array(
2544             'SITETREE_GRANT_ACCESS' => array(
2545                 'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2546                 'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP',  'Allow setting of page-specific access restrictions in the "Pages" section.'),
2547                 'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2548                 'sort' => 100
2549             ),
2550             'SITETREE_VIEW_ALL' => array(
2551                 'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2552                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2553                 'sort' => -100,
2554                 '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')
2555             ),
2556             'SITETREE_EDIT_ALL' => array(
2557                 'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2558                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2559                 'sort' => -50,
2560                 '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')
2561             ),
2562             'SITETREE_REORGANISE' => array(
2563                 'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2564                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2565                 'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2566                 'sort' => 100
2567             ),
2568             'VIEW_DRAFT_CONTENT' => array(
2569                 'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
2570                 'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2571                 '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.'),
2572                 'sort' => 100
2573             )
2574         );
2575     }
2576     
2577     /**
2578      * Return the translated Singular name 
2579      * 
2580      * @return String
2581      */
2582     function i18n_singular_name() {
2583         return _t($this->class.'.SINGULARNAME', $this->singular_name());
2584     }
2585     
2586     /**
2587      * Overloaded to also provide entities for 'Page' class which is usually
2588      * located in custom code, hence textcollector picks it up for the wrong folder.
2589      */
2590     function provideI18nEntities() {
2591         $entities = parent::provideI18nEntities();
2592         
2593         if(isset($entities['Page.SINGULARNAME'])) $entities['Page.SINGULARNAME'][3] = 'sapphire';
2594         if(isset($entities['Page.PLURALNAME'])) $entities['Page.PLURALNAME'][3] = 'sapphire';       
2595 
2596         return $entities;
2597     }
2598     
2599     function getParentType() {
2600         return $this->ParentID == 0 ? 'root' : 'subpage';
2601     }
2602     
2603     static function reset() {
2604         self::$cache_permissions = array();
2605     }
2606 
2607 }
2608 
2609 ?>
2610 
[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. -
Webylon 3.1 API Docs API documentation generated by ApiGen 2.8.0