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

Packages

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