silverstripe-framework/core/model/SiteTree.php

1550 lines
44 KiB
PHP

<?php
/**
* Basic data-object representing all pages within the site tree.
* This data-object takes care of the heirachy. All page types that live within the heirachy
* should inherit from this.
*
* In addition, it contains a number of static methods for querying the site tree.
* @package cms
*/
class SiteTree extends DataObject {
/**
* Indicates what kind of children this page type can have.
* This can be an array of allowed child classes, or the string "none" -
* indicating that this page type can't have children.
* If a classname is prefixed by "*", such as "*Page", then only that
* class is allowed - no subclasses. Otherwise, the class and all its
* subclasses are allowed.
*
* @var array
*/
static $allowed_children = array("SiteTree");
/**
* The default child class for this page.
*
* @var string
*/
static $default_child = "Page";
/**
* The default parent class for this page.
*
* @var string
*/
static $default_parent = null;
/**
* Controls whether a page can be in the root of the site tree.
*
* @var bool
*/
static $can_be_root = true;
/**
* List of permission codes a user can have to allow a user to create a
* page of this type.
*
* @var array
*/
static $need_permission = null;
/**
* If you extend a class, and don't want to be able to select the old class
* in the cms, set this to the old class name. Eg, if you extended Product
* to make ImprovedProduct, then you would set $hide_ancestor to Product.
*
* @var string
*/
static $hide_ancestor = null;
static $db = array(
"URLSegment" => "Varchar(255)",
"Title" => "Varchar(255)",
"MenuTitle" => "Varchar(100)",
"Content" => "HTMLText",
"MetaTitle" => "Varchar(255)",
"MetaDescription" => "Varchar(255)",
"MetaKeywords" => "Varchar(255)",
"ExtraMeta" => "HTMLText",
"ShowInMenus" => "Boolean",
"ShowInSearch" => "Boolean",
"HomepageForDomain" => "Varchar(100)",
"ProvideComments" => "Boolean",
"Sort" => "Int",
"LegacyURL" => "Varchar(255)",
"HasBrokenFile" => "Boolean",
"HasBrokenLink" => "Boolean",
"Status" => "Varchar",
"ReportClass" => "Varchar",
"Priority" => "Float",
"Viewers" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')",
"Editors" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
"ViewersGroup" => "Int",
"EditorsGroup" => "Int",
// Simple task tracking
"ToDo" => "Text",
);
static $indexes = array(
"SearchFields" => "fulltext (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords)",
"TitleSearchFields" => "fulltext (Title)",
"URLSegment" => true,
);
static $has_many = array(
"Comments" => "PageComment"
);
static $many_many = array(
"LinkTracking" => "SiteTree",
"ImageTracking" => "File"
);
static $belongs_many_many = array(
"BackLinkTracking" => "SiteTree"
);
static $many_many_extraFields = array(
"LinkTracking" => array("FieldName" => "Varchar"),
"ImageTracking" => array("FieldName" => "Varchar")
);
static $casting = array(
"Breadcrumbs" => "HTMLText",
"LastEdited" => "Datetime",
"Created" => "Datetime",
);
static $defaults = array(
"ShowInMenus" => 1,
"ShowInSearch" => 1,
"Status" => "New page",
"CanCreateChildren" => array(10),
"Viewers" => "Anyone",
"Editors" => "LoggedInUsers"
);
static $has_one = array(
"Parent" => "SiteTree"
);
static $versioning = array(
"Stage", "Live"
);
static $default_sort = "Sort";
/**
* The text shown in the create page dropdown. If
* this is not set, default to "Create a ClassName".
* @var string
*/
static $add_action = null;
/**
* If this is false, the class cannot be created in the CMS.
* @var boolean
*/
static $can_create = true;
/**
* Icon to use in the CMS
*
* This should be the base filename. The suffixes -file.gif,
* -openfolder.gif and -closedfolder.gif will be appended to the base name
* that you provide there.
* If you prefer, you can pass an array:
* array("jsparty\tree\images\page", $option).
* $option can be either "file" or "folder" to force the icon to always
* be a file or folder, regardless of whether the page has children or not
*
* @var string|array
*/
static $icon = array("jsparty/tree/images/page", "file");
static $extensions = array(
"Hierarchy",
"Translatable('Title', 'MenuTitle', 'Content', 'URLSegment', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Status')",
"Versioned('Stage', 'Live')"
);
/**
* Delimit breadcrumb-links generated by BreadCrumbs()
*
* @var string
*/
public static $breadcrumbs_delimiter = " &raquo; ";
static $searchable_fields = array(
'Title',
'Content',
);
/**
* Get the URL for this page.
*
* @param string $action An action to include in the link
* @return string The URL for this page
*/
public function Link($action = null) {
if($action == "index") {
$action = "";
}
return Director::baseURL() . $this->URLSegment . "/$action";
}
/**
* Get the absolute URL for this page by stage
*/
public function AbsoluteLink() {
if($this->hasMethod('alternateAbsoluteLink')) return $this->alternateAbsoluteLink();
else return Director::absoluteURL($this->Link());
}
/**
* Returns link/current, depending on whether you're on the current page.
* This is useful for css styling of menus.
*
* @return string Either 'link' or 'current'.
*/
public function LinkOrCurrent() {
return ($this->isCurrent()) ? "current" : "link";
}
/**
* Returns link/section, depending on whether you're on the current section.
* This is useful for css styling of menus.
*
* @return string Either 'link' or 'section'.
*/
public function LinkOrSection() {
return ($this->isSection()) ? "section" : "link";
}
/**
* Returns link/current/section, depending if you're not in the current
* section, you're on the current page, or you're in the current section
* but not on the current page.
*
* @return string Either 'link', 'current' or 'section'.
*/
public function LinkingMode() {
$this->prepareCurrentAndSection();
if($this->ID == self::$currentPageID) {
return "current";
} else if(in_array($this->ID, self::$currentSectionIDs)) {
return "section";
} else {
return "link";
}
}
/**
* Get the URL segment for this page, eg 'home'
*
* @return string The URL segment
*/
public function ElementName() {
return $this->URLSegment;
}
/**
* Check if this page is in the given current section.
*
* @param string $sectionName Name of the section to check.
* @return boolean True if we are in the given section.
*/
public function InSection($sectionName) {
$page = Director::currentPage();
while($page) {
if($sectionName == $page->URLSegment)
return true;
$page = $page->Parent;
}
return false;
}
/**
* Returns comments on this page. This will only show comments that
* have been marked as spam if "?showspam=1" is appended to the URL.
*
* @return DataObjectSet Comments on this page.
*/
public function Comments() {
$spamfilter = isset($_GET['showspam']) ? '' : 'AND IsSpam=0';
$unmoderatedfilter = Permission::check('ADMIN') ? '' : 'AND NeedsModeration = 0';
$comments = DataObject::get("PageComment", "ParentID = '" . Convert::raw2sql($this->ID) . "' $spamfilter $unmoderatedfilter", "Created DESC");
return $comments ? $comments : new DataObjectSet();
}
/**
* Create a duplicate of this node. Doesn't affect joined data - create a
* custom overloading of this if you need such behaviour.
*
* @return SiteTree The duplicated object.
*/
public function duplicate($doWrite = true) {
$page = parent::duplicate($doWrite);
$page->CheckedPublicationDifferences = $page->AddedToStage = true;
return $page;
}
/**
* Duplicates each child of this node recursively and returns the
* duplicate node.
*
* @return SiteTree The duplicated object.
*/
public function duplicateWithChildren() {
$clone = $this->duplicate();
$children = $this->AllChildren();
if($children) {
foreach($children as $child) {
$childClone = method_exists($child, 'duplicateWithChildren')
? $child->duplicateWithChildren()
: $child->duplicate();
$childClone->ParentID = $clone->ID;
$childClone->write();
}
}
return $clone;
}
/**
* Duplicate this node and its children as a child of the node with the
* given ID
*
* @param int $id ID of the new node's new parent
*/
public function duplicateAsChild($id) {
$newSiteTree = $this->duplicate();
$newSiteTree->ParentID = $id;
$newSiteTree->write();
}
/**
* An array of this pages URL segment and it's parents.
* This is generated by prepareCurrentAndSection for use by
* isCurrent() and isSection()
*
* @var array
*/
protected static $currentSectionIDs;
/**
* The current page ID.
* This is generated by prepareCurrentAndSection for use by
* isCurrent() and isSection()
*
* @var int
*/
private static $currentPageID;
/**
* Records the URL segment that was used to set the current page ID
*/
private static $currentPageIDSetFromURLSegment;
/**
* This function is used for isCurrent() and isSection() to prepare
* the cached answers.
*/
protected function prepareCurrentAndSection() {
if(!self::$currentPageID || Director::urlParam('URLSegment') != self::$currentPageIDSetFromURLSegment) {
self::$currentPageID = Director::currentPage() ? Director::currentPage()->ID : null;
self::$currentPageIDSetFromURLSegment = Director::urlParam('URLSegment');
if(!isset(self::$currentPageID)) {
self::$currentPageID = -1;
$nextID = (Director::currentPage() && isset(Director::currentPage()->Parent->ID))
? Director::currentPage()->Parent->ID
: null;
} else {
$nextID = SiteTree::$currentPageID;
}
$table = (Versioned::current_stage() == "Live")
? "SiteTree_Live"
: "SiteTree";
SiteTree::$currentSectionIDs = array();
while($nextID) {
self::$currentSectionIDs[] = $nextID;
$nextID = DB::query("SELECT ParentID FROM SiteTree WHERE ID = $nextID")->value();
}
}
}
/**
* Check if this is the currently viewed page.
*
* @return boolean True if this is the current page.
*/
public function isCurrent() {
$this->prepareCurrentAndSection();
return $this->ID == SiteTree::$currentPageID;
}
/**
* Check if the currently viewed page is in this section.
*
* @return boolean True if the currently viewed page is in this section.
*/
public function isSection() {
$this->prepareCurrentAndSection();
return in_array($this->ID, self::$currentSectionIDs);
}
/**
* Return a breadcrumb trail to this page. Excludes "hidden" pages
* (with ShowInMenus=0).
*
* @param int $maxDepth The maximum depth to traverse.
* @param boolean $unlinked Do not make page names links
* @param string $stopAtPageType ClassName of a page to stop the upwards traversal.
* @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
* @return string The breadcrumb trail.
*/
public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
$page = $this;
$parts = array();
$i = 0;
while(
$page
&& (!$maxDepth || sizeof($parts) < $maxDepth)
&& ($stopAtPageType && $page->ClassName != $stopAtPageType)
) {
if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
if($page->URLSegment == 'home') $hasHome = true;
if(($page->ID == $this->ID) || $unlinked) {
$parts[] = Convert::raw2xml($page->Title);
} else {
$parts[] = ("<a href=\"" . $page->Link() . "\">" . Convert::raw2xml($page->Title) . "</a>");
}
}
$page = $page->Parent;
}
return implode(self::$breadcrumbs_delimiter, array_reverse($parts));
}
/**
* Make this page a child of another page.
*
* If the parent page does not exist, resolve it to a valid ID
* before updating this page's reference.
*
* @param SiteTree|int $item Either the parent object, or the parent ID
*/
public function setParent($item) {
if(is_object($item)) {
if (!$item->exists()) $item->write();
$this->setField("ParentID", $item->ID);
} else {
$this->setField("ParentID", $item);
}
}
/**
* Get the parent of this page.
*
* @return SiteTree Parent of this page.
*/
public function getParent() {
if ($this->getField("ParentID")) {
return DataObject::get_one("SiteTree", "`SiteTree`.ID = " . $this->getField("ParentID"));
}
}
/**
* Return a string of the form "parent - page" or
* "grandparent - parent - page".
*
* @param int $level The maximum amount of levels to traverse.
* @param string $seperator Seperating string
* @return string The resulting string
*/
function NestedTitle($level = 2, $separator = " - ") {
$item = $this;
while($item && $level > 0) {
$parts[] = $item->Title;
$item = $item->Parent;
$level--;
}
return implode($separator, array_reverse($parts));
}
/**
* This function should return true if the current user can add children
* to this page.
*
* It can be overloaded to customise the security model for an
* application.
*
* Returns true if the member is allowed to do the given action.
*
* @param string $perm The permission to be checked, such as 'View'.
* @param Member $member The member whose permissions need checking.
* Defaults to the currently logged in user.
*
* @return boolean True if the the member is allowed to do the given
* action.
*
* @todo Check we get a endless recursion if we use parent::can()
*/
function can($perm, $member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
if(method_exists($this, 'can' . ucfirst($perm))) {
$method = 'can' . ucfirst($perm);
return $this->$method($member);
}
$args = array($perm, $member, true);
$this->extend('alternateCan', $args);
if($args[2] == false) return false;
return true;
//return parent::can($perm, $member);
}
/**
* This function should return true if the current user can add children
* to this page.
*
* It can be overloaded to customise the security model for an
* application.
*
* @return boolean True if the current user can add children.
*/
public function canAddChildren($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanAddChildren', $args);
if($args[1] == false) return false;
return $this->canEdit() && $this->stat('allowed_children') != 'none';
}
/**
* This function should return true if the current user can view this
* page.
*
* It can be overloaded to customise the security model for an
* application.
*
* @return boolean True if the current user can view this page.
*/
public function canView($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanView', $args);
if($args[1] == false) return false;
if(((!$this->Viewers) || ($this->Viewers == 'Anyone') ||
($this->Viewers == 'LoggedInUsers' && $member) ||
($this->Viewers == 'OnlyTheseUsers' && $member &&
$member->inGroup($this->ViewersGroup))) == false)
return false;
return true;
}
/**
* This function should return true if the current user can delete this
* page.
*
* It can be overloaded to customise the security model for an
* application.
*
* @param Member $member
* @return boolean True if the current user can delete this page.
*/
public function canDelete($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanDelete', $args);
if($args[1] == false) return false;
return $this->stat('can_create') != false;
}
/**
* This function should return true if the current user can create new
* pages of this class.
*
* It can be overloaded to customise the security model for an
* application.
*
* @param Member $member
* @return boolean True if the current user can create pages on this
* class.
*/
public function canCreate($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanCreate', $args);
if($args[1] == false) return false;
return $this->stat('can_create') != false || Director::isDev();
}
/**
* This function should return true if the current user can edit this
* page.
*
* It can be overloaded to customise the security model for an
* application.
*
* @param Member $member
* @return boolean True if the current user can edit this page.
*/
public function canEdit($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanEdit', $args);
if($args[1] == false) return false;
if((Permission::check('CMS_ACCESS_CMSMain') &&
(($this->Editors == 'LoggedInUsers' && $member) ||
($this->Editors == 'OnlyTheseUsers' && $member &&
$member->inGroup($this->EditorsGroup)))) == false)
return false;
return true;
}
/**
* This function should return true if the current user can publish this
* page.
*
* It can be overloaded to customise the security model for an
* application.
*
* @param Member $member
* @return boolean True if the current user can publish this page.
*/
public function canPublish($member = null) {
if(!isset($member)) {
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true);
$this->extend('alternateCanPublish', $args);
if($args[1] == false) return false;
return $this->canEdit();
}
/**
* Collate selected descendants of this page.
*
* {@link $condition} will be evaluated on each descendant, and if it is
* succeeds, that item will be added to the $collator array.
*
* @param string $condition The PHP condition to be evaluated. The page
* will be called $item
* @param array $collator An array, passed by reference, to collect all
* of the matching descendants.
*/
public function collateDescendants($condition, &$collator) {
if($children = $this->Children()) {
foreach($children as $item) {
if(eval("return $condition;")) $collator[] = $item;
$item->collateDescendants($condition, $collator);
}
return true;
}
}
/**
* Return the title, description, keywords and language metatags.
*
* @todo Make generator tag dynamically determine version number (currently defaults to "2.0")
* @todo Move <title> tag in separate getter for easier customization and more obvious usage
*
* @param boolean|string $includeTitle Show default <title>-tag, set to false for custom templating
* @param boolean $includeTitle Show default <title>-tag, set to false for
* custom templating
* @return string The XHTML metatags
*/
public function MetaTags($includeTitle = true) {
$tags = "";
if($includeTitle === true || $includeTitle == 'true') {
$tags .= "<title>" . Convert::raw2xml(($this->MetaTitle)
? $this->MetaTitle
: $this->Title) . "</title>\n";
}
$tags .= "<meta name=\"generator\" http-equiv=\"generator\" content=\"SilverStripe - http://www.silverstripe.com\" />\n";
$charset = ContentNegotiator::get_encoding();
$tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
if($this->MetaKeywords) {
$tags .= "<meta name=\"keywords\" http-equiv=\"keywords\" content=\"" .
Convert::raw2att($this->MetaKeywords) . "\" />\n";
}
if($this->MetaDescription) {
$tags .= "<meta name=\"description\" http-equiv=\"description\" content=\"" .
Convert::raw2att($this->MetaDescription) . "\" />\n";
}
if($this->ExtraMeta) {
$tags .= $this->ExtraMeta . "\n";
}
$tags .= "<meta http-equiv=\"Content-Language\" content=\"". Translatable::current_lang() ."\"/>\n";
$this->extend('updateMetaTags', $tags);
return $tags;
}
/**
* Returns the object that contains the content that a user would
* associate with this page.
*
* Ordinarily, this is just the page itself, but for example on
* RedirectorPages or VirtualPages ContentSource() will return the page
* that is linked to.
*
* @return SiteTree The content source.
*/
public function ContentSource() {
return $this;
}
/**
* Add default records to database.
*
* This function is called whenever the database is built, after the
* database tables have all been created. Overload this to add default
* records when the database is built, but make sure you call
* parent::requireDefaultRecords().
*/
function requireDefaultRecords() {
parent::requireDefaultRecords();
if($this->class == 'SiteTree') {
if(!DataObject::get_one("SiteTree", "URLSegment = 'home'")) {
$homepage = new Page();
$homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
$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.com">developer documentation</a>, or begin <a href="http://doc.silverstripe.com/doku.php?id=tutorials">the tutorials.</a></p>');
$homepage->URLSegment = "home";
$homepage->Status = "Published";
$homepage->write();
$homepage->publish("Stage", "Live");
$homepage->flushCache();
Database::alteration_message("Home page created","created");
}
if(DB::query("SELECT COUNT(*) FROM SiteTree")->value() == 1) {
$aboutus = new Page();
$aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
$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>');
$aboutus->URLSegment = "about-us";
$aboutus->Status = "Published";
$aboutus->write();
$aboutus->publish("Stage", "Live");
Database::alteration_message("About Us created","created");
$contactus = new Page();
$contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
$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>');
$contactus->URLSegment = "contact-us";
$contactus->Status = "Published";
$contactus->write();
$contactus->publish("Stage", "Live");
$contactus->flushCache();
}
}
}
//------------------------------------------------------------------------------------//
protected function onBeforeWrite() {
if(!$this->Sort && $this->ParentID) {
$this->Sort = DB::query(
"SELECT MAX(Sort) + 1 FROM SiteTree WHERE ParentID = $this->ParentID")->value();
}
// Auto-set URLSegment
if((!$this->URLSegment || $this->URLSegment == 'new-page') &&
$this->Title) {
$this->URLSegment = $this->generateURLSegment($this->Title);
// Keep it clean
} else if(isset($this->changed['URLSegment']) &&
$this->changed['URLSegment']) {
$segment = ereg_replace('[^A-Za-z0-9]+','-',$this->URLSegment);
$segment = ereg_replace('-+','-',$segment);
if(!$segment) {
$segment = "page-$this->ID";
}
$this->URLSegment = $segment;
}
DataObject::set_context_obj($this);
// Ensure URLSegment is unique
$idFilter = ($this->ID)
? " AND `SiteTree`.ID <> '$this->ID'" :
'';
$count = 1;
while(class_exists($this->URLSegment) || DataObject::get_one("SiteTree", "URLSegment = '$this->URLSegment' $idFilter")) {
$count++;
$this->URLSegment = ereg_replace('-[0-9]+$','', $this->URLSegment) . "-$count";
}
DataObject::set_context_obj(null);
// If the URLSegment has been changed, rewrite links
if(isset($this->changed['URLSegment']) && $this->changed['URLSegment']) {
if($this->hasMethod('BackLinkTracking')) {
$links = $this->BackLinkTracking();
if($links) {
foreach($links as $link) {
$link->rewriteLink($this->original['URLSegment'] . '/',
$this->URLSegment . '/');
$link->write();
}
}
}
}
// If priority is empty or invalid, set it to the default value
if(!is_numeric($this->Priority) ||
(($this->Priority < 0) || ($this->Priority > 1)))
$this->Priority = self::$defaults['Priority'];
parent::onBeforeWrite();
}
/**
* Generate a URL segment based on the title provided.
* @param string $title Page title.
* @return string Generated url segment
*/
function generateURLSegment($title){
$t = strtolower($title);
$t = str_replace('&amp;','-and-',$t);
$t = str_replace('&','-and-',$t);
$t = ereg_replace('[^A-Za-z0-9]+','-',$t);
$t = ereg_replace('-+','-',$t);
if(!$t) {
$t = "page-$this->ID";
}
return $t;
}
function makelinksunique() {
$badURLs = "'" . implode("', '", DB::query("SELECT URLSegment, count(*) FROM SiteTree GROUP BY URLSegment HAVING count(*) > 1")->column()) . "'";
$pages = DataObject::get("SiteTree", "URLSegment IN ($badURLs)");
foreach($pages as $page) {
echo "<li>$page->Title: ";
$urlSegment = $page->URLSegment;
$page->write();
if($urlSegment != $page->URLSegment) {
echo sprintf(_t('SiteTree.LINKSCHANGEDTO', " changed %s -> %s"), $urlSegment, $page->URLSegment);
}
else {
echo sprintf(_t('SiteTree.LINKSALREADYUNIQUE', " %s is already unique"), $urlSegment);
}
die();
}
}
function makelinksuniquequick() {
$badURLs = "'" . implode("', '", DB::query("SELECT URLSegment, count(*) FROM SiteTree GROUP BY URLSegment HAVING count(*) > 1")->column()) . "'";
$pages = DB::query("SELECT *, SiteTree.ID FROM SiteTree LEFT JOIN Page ON Page.ID = SiteTree.ID WHERE URLSegment IN ($badURLs)");
foreach($pages as $page) {
echo "<li>$page[Title]: ";
$urlSegment = $page['URLSegment'];
$newURLSegment = $urlSegment . '-' . $page['ID'];
DB::query("UPDATE SiteTree SET URLSegment = '$newURLSegment' WHERE ID = $page[ID]");
if($urlSegment != $newURLSegment) {
echo sprintf(_t('SiteTree.LINKSCHANGEDTO'), $urlSegment, $newURLSegment);
}
else {
echo sprintf(_t('SiteTree.LINKSALREADYUNIQUE'), $urlSegment);
}
}
echo "<p>done";
}
/**
* Replace a URL in html content with a new URL.
* @param string $old The old URL
* @param string $new The new URL
*/
function rewriteLink($old, $new) {
$fields = $this->getCMSFields(null)->dataFields();
foreach($fields as $field) {
if(is_a($field, 'HtmlEditorField')) {
$fieldName = $field->Name();
$field->setValue($this->$fieldName);
$field->rewriteLink($old, $new);
$field->saveInto($this);
}
}
}
/**
* The default value of the priority field depends on the depth of the page in
* the site tree, so it must be calculated dynamically.
*/
function getPriority() {
if(!$this->getField('Priority')) {
$parentStack = $this->parentStack();
$numParents = is_array($parentStack) ? count($parentStack) - 1: 0;
return max(0.1, 1.0 - ($numParents / 10));
} else if($this->getField('Priority') == -1) {
return 0;
} else {
return $this->getField('Priority');
}
}
/**
* Returns a FieldSet with which to create the CMS editing form.
*
* You can override this in your child classes to add extra fields - first
* get the parent fields using parent::getCMSFields(), then use
* addFieldToTab() on the FieldSet.
*
* @return FieldSet The fields to be displayed in the CMS.
*/
function getCMSFields() {
require_once("forms/Form.php");
Requirements::javascript(CMS_DIR . "/javascript/SitetreeAccess.js");
// Backlink report
if($this->hasMethod('BackLinkTracking')) {
$links = $this->BackLinkTracking();
if($links->exists()) {
foreach($links as $link) {
$backlinks[] = "<li><a class=\"cmsEditlink\" href=\"admin/show/$link->ID\">" .
$link->Breadcrumbs(null,true) . "</a></li>";
}
$backlinks = "<div style=\"clear:left\">
" . _t('SiteTree.PAGESLINKING', 'The following pages link to this page:') .
"<ul>" . implode("",$backlinks) . "</ul></div>";
}
}
if(!isset($backlinks)) {
$backlinks = "<p>" . _t('SiteTree.NOBACKLINKS', 'This page hasn\'t been linked to from any pages.') . "</p>";
}
// Status / message
// Create a status message for multiple parents
if($this->ID && is_numeric($this->ID)) {
$linkedPages = DataObject::get("VirtualPage", "CopyContentFromID = $this->ID");
}
if(isset($linkedPages)) {
foreach($linkedPages as $linkedPage) {
$parentPage = $linkedPage->Parent;
$parentPageTitle = $parentPage->Title;
if($parentPage->ID) {
$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">{$parentPage->Title}</a>";
} else {
$parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"admin/show/$linkedPage->ID\">" .
_t('SiteTree.TOPLEVEL', 'Site Content (Top Level)') .
"</a>";
}
}
$lastParent = array_pop($parentPageLinks);
$parentList = "'$lastParent'";
if(count($parentPageLinks) > 0) {
$parentList = "'" . implode("', '", $parentPageLinks) . "' and "
. $parentList;
}
$statusMessage[] = sprintf(
_t('SiteTree.APPEARSVIRTUALPAGES', "This content also appears on the virtual pages in the %s sections."),
$parentList
);
}
if($this->HasBrokenLink || $this->HasBrokenFile) {
$statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
}
$message = "STATUS: $this->Status<br />";
if(isset($statusMessage)) {
$message .= "NOTE: " . implode("<br />", $statusMessage);
}
$pagePriorities = array(
'' => _t('SiteTree.PRIORITYAUTOSET','Auto-set based on page depth'),
'-1' => _t('SiteTree.PRIORITYNOTINDEXED', "Not indexed"), // We set this to -ve one because a blank value implies auto-generation of Priority
'1.0' => '1 - ' . _t('SiteTree.PRIORITYMOSTIMPORTANT', "Most important"),
'0.9' => '2',
'0.8' => '3',
'0.7' => '4',
'0.6' => '5',
'0.5' => '6',
'0.4' => '7',
'0.3' => '8',
'0.2' => '9',
'0.1' => '10 - ' . _t('SiteTree.PRIORITYLEASTIMPORTANT', "Least important")
);
// Lay out the fields
$fields = new FieldSet(
new TabSet("Root",
$tabContent = new TabSet('Content',
$tabMain = new Tab('Main',
new TextField("Title", _t('SiteTree.PAGETITLE', "Page name")),
/*new UniqueTextField("Title",
"Title",
"SiteTree",
"Another page is using that name. Page names should be unique.",
"Page Name"
),*/
new TextField("MenuTitle", _t('SiteTree.MENUTITLE', "Navigation label")),
new HtmlEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", PR_MEDIUM, 'HTML editor title'))
),
$tabMeta = new Tab('Meta-data',
new FieldGroup(_t('SiteTree.URL', "URL"),
new LabelField(Director::absoluteBaseURL()),
new UniqueRestrictedTextField("URLSegment",
"URLSegment",
"SiteTree",
_t('SiteTree.VALIDATIONURLSEGMENT1', "Another page is using that URL. URL must be unique for each page"),
"[^A-Za-z0-9-]+",
"-",
_t('SiteTree.VALIDATIONURLSEGMENT2', "URLs can only be made up of letters, digits and hyphens."),
"",
"",
"",
50
),
new LabelField("/")
),
new HeaderField(_t('SiteTree.METAHEADER', "Search Engine Meta-tags")),
new TextField("MetaTitle", _t('SiteTree.METATITLE', "Title")),
new TextareaField("MetaDescription", _t('SiteTree.METADESC', "Description")),
new TextareaField("MetaKeywords", _t('SiteTree.METAKEYWORDS', "Keywords")),
new ToggleCompositeField(
'AdvancedOptions',
_t('SiteTree.METAADVANCEDHEADER', "Advanced Options..."),
array(
new TextareaField("ExtraMeta",_t('SiteTree.METAEXTRA', "Custom Meta Tags")),
new LiteralField(
"",
"<p>" .
sprintf(
_t(
'SiteTree.METANOTEPRIORITY',
"Manually specify a Google Sitemaps priority for this page (%s)"
),
'<a href="https://www.google.com/webmasters/tools/docs/en/protocol.html#prioritydef">?</a>'
) .
"</p>"
),
new DropdownField("Priority", _t('SiteTree.METAPAGEPRIO', "Page Priority"), $pagePriorities)
),
true
)
)
),
$tabBehaviour = new Tab('Behaviour',
new DropdownField(
"ClassName",
_t('SiteTree.PAGETYPE', "Page type", PR_MEDIUM, 'Classname of a page object'),
$this->getClassDropdown()
),
new CheckboxField("ShowInMenus", _t('SiteTree.SHOWINMENUS', "Show in menus?")),
new CheckboxField("ShowInSearch", _t('SiteTree.SHOWINSEARCH', "Show in search?")),
/*, new TreeMultiselectField("MultipleParents", "Page appears within", "SiteTree")*/
new CheckboxField("ProvideComments", _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?")),
new LiteralField(
"",
"<p>" .
_t('SiteTree.NOTEUSEASHOMEPAGE',
"Use this page as the 'home page' for the following domains:
(separate multiple domains with commas)") .
"</p>"
),
new TextField(
"HomepageForDomain",
_t('SiteTree.HOMEPAGEFORDOMAIN', "Domain(s)", PR_MEDIUM, 'Listing domains that should be used as homepage')
)
),
$tabToDo = new Tab($this->ToDo ? 'To-do **' : 'To-do',
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>")),
new TextareaField("ToDo", "")
),
$tabReports = new TabSet('Reports',
$tabBacklinks =new Tab('Backlinks',
new LiteralField("Backlinks", $backlinks)
)
),
$tabAccess = new Tab('Access',
new HeaderField(_t('SiteTree.ACCESSHEADER', "Who can view this page on my site?"), 2),
new OptionsetField(
"Viewers",
"",
array(
"Anyone" => _t('SiteTree.ACCESSANYONE', "Anyone"),
"LoggedInUsers" => _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users"),
"OnlyTheseUsers" => _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)")
)
),
new DropdownField("ViewersGroup", _t('SiteTree.GROUP', "Group"), Group::map()),
new HeaderField(_t('SiteTree.EDITHEADER', "Who can edit this inside the CMS?"), 2),
new OptionsetField(
"Editors",
"",
array(
"LoggedInUsers" => _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS"),
"OnlyTheseUsers" => _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)")
)
),
new DropdownField("EditorsGroup", _t('SiteTree.GROUP'), Group::map())
)
)
//new NamedLabelField("Status", $message, "pageStatusMessage", true)
);
$tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content"));
$tabMain->setTitle(_t('SiteTree.TABMAIN', "Main"));
$tabMeta->setTitle(_t('SiteTree.TABMETA', "Meta-data"));
$tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behaviour"));
$tabReports->setTitle(_t('SiteTree.TABREPORTS', "Reports"));
$tabAccess->setTitle(_t('SiteTree.TABACCESS', "Access"));
$tabBacklinks->setTitle(_t('SiteTree.TABBACKLINKS', "BackLinks"));
$this->extend('updateCMSFields', $fields);
return $fields;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Get the actions available in the CMS for this page - eg Save, Publish.
*
* @return DataObjectSet The available actions for this page.
*/
function getCMSActions() {
$actions = array();
if($this->isPublished() && $this->canPublish()) {
$unpublish = FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete');
$unpublish->describe(_t('SiteTree.BUTTONUNPUBLISHDESC', "Remove this page from the published site"));
$unpublish->addExtraClass('delete');
$actions[] = $unpublish;
}
if($this->stagesDiffer('Stage', 'Live')) {
if($this->isPublished() && $this->canEdit()) {
$rollback = FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete');
$rollback->describe(_t('SiteTree.BUTTONCANCELDRAFTDESC', "Delete your draft and revert to the currently published page"));
$rollback->addExtraClass('delete');
$actions[] = $rollback;
}
}
if($this->canPublish()) {
$actions[] = new FormAction('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & Publish'));
}
// getCMSActions() can be extended with updateCmsActions() on a decorator
$this->extend('updateCMSActions', $actions);
return new DataObjectSet($actions);
}
/**
* Publish this page
*/
function doPublish() {
$original = Versioned::get_one_by_stage("SiteTree", "Live", "`SiteTree`.`ID` = $this->ID");
// Handle activities undertaken by decorators
$this->extend('onBeforePublish', $original);
$this->AssignedToID = 0;
$this->RequestedByID = 0;
$this->Status = "Published";
//$this->PublishedByID = Member::currentUser()->ID;
$this->write();
$this->publish("Stage", "Live");
GoogleSitemap::ping();
// Fix the sort order for this page's siblings
DB::query("UPDATE SiteTree_Live
INNER JOIN SiteTree ON SiteTree_Live.ID = SiteTree.ID
SET SiteTree_Live.Sort = SiteTree.Sort
WHERE SiteTree_Live.ParentID = " . sprintf('%d', $this->ParentID));
// Handle activities undertaken by decorators
$this->extend('onAfterPublish', $original);
}
/**
* Check if this page is new - that is, if it has yet to have been written
* to the database.
*
* @return boolean True if this page is new.
*/
function isNew() {
/**
* This check was a problem for a self-hosted site, and may indicate a
* bug in the interpreter on their server, or a bug here
* Changing the condition from empty($this->ID) to
* !$this->ID && !$this->record['ID'] fixed this.
*/
if(empty($this->ID))
return true;
if(is_numeric($this->ID))
return false;
return stripos($this->ID, 'new') === 0;
}
/**
* Check if this page has been published.
*
* @return boolean True if this page has been published.
*/
function isPublished() {
if($this->isNew())
return false;
return (DB::query("SELECT ID FROM `SiteTree_Live` WHERE ID = $this->ID")->value())
? true
: false;
}
/**
* Look for ghost parents
*/
function MultipleParents() {
$parents = new GhostPage_ComponentSet($this->Parent);
$parents->setOwner($this);
$ghostPages = DataObject::get("GhostPage", "LinkedPageID = '$this->ID'");
if($ghostPages) {
foreach($ghostPages as $ghostPage) {
// Ignore root ghost-pages
if($p = $ghostPage->getParent())
$parents->push($p);
}
}
return $parents;
}
/**
* Get the class dropdown used in the CMS to change the class of a page.
* This returns the list of options in the drop as a Map from class name
* to text in dropdown.
*
* @return array
*/
function getClassDropdown() {
$classes = ClassInfo::getValidSubClasses('SiteTree');
array_shift($classes);
foreach($classes as $class) {
$instance = singleton($class);
if((($instance instanceof HiddenClass) || !$instance->canCreate()) && ($class != $this->class)) continue;
/*
$addAction = $instance->uninherited('add_action', true);
if(!$addAction) {
$addAction = $instance->singular_name();
}
*/
$addAction = $instance->i18n_singular_name();
if($class == $this->class) {
$currentClass = $class;
$currentAddAction = $addAction;
} else {
$result[$class] = ($class == $this->class)
? _t('SiteTree.CURRENTLY', 'Currently').' '.$addAction
: _t('SiteTree.CHANGETO', 'Change to').' '.$addAction;
}
}
// sort alphabetically, and put current on top
asort($result);
$result = array_reverse($result);
$result[$currentClass] = $currentAddAction.' ('._t('SiteTree.CURRENT','current').')';
$result = array_reverse($result);
return $result;
}
/**
* Returns an array of the class names of classes that are allowed
* to be children of this class.
*
* @return array
*/
function allowedChildren() {
$candidates = $this->stat('allowed_children');
if($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
foreach($candidates as $candidate) {
if(substr($candidate,0,1) == '*') {
$allowedChildren[] = substr($candidate,1);
} else {
$subclasses = ClassInfo::subclassesFor($candidate);
foreach($subclasses as $subclass) {
if($subclass != "SiteTree_root") $allowedChildren[] = $subclass;
}
}
}
return $allowedChildren;
}
}
/**
* Returns the class name of the default class for children of this page.
*
* @return string
*/
function defaultChild() {
$default = $this->stat('default_child');
$allowed = $this->allowedChildren();
if($allowed) {
if(!$default || !in_array($default, $allowed))
$default = reset($allowed);
return $default;
}
}
/**
* Returns the class name of the default class for the parent of this
* page.
*
* @return string
*/
function defaultParent() {
return $this->stat('default_parent');
}
/**
* Function to clean up the currently loaded page after a reorganise has
* been called. It should return a piece of JavaScript to be executed on
* the client side, to clean up the results of the reorganise.
*/
function cmsCleanup_parentChanged() {
}
/**
* Get the title for use in menus for this page. If the MenuTitle
* field is set it returns that, else it returns the Title field.
*
* @return string
*/
function getMenuTitle(){
if($value = $this->getField("MenuTitle")) {
return $value;
} else {
return $this->getField("Title");
}
}
/**
* Set the menu title for this page.
*
* @param string $value
*/
function setMenuTitle($value) {
if($value == $this->getField("Title")) {
$this->setField("MenuTitle", null);
} else {
$this->setField("MenuTitle", $value);
}
}
/**
* TitleWithStatus will return the title in an <ins>, <del> or
* <span class=\"modified\"> tag depending on its publication status.
*
* @return string
*/
function TreeTitle() {
// If somthing
if(!$this->CheckedPublicationDifferences && $this->ID) {
$stageVersion =
DB::query("SELECT Version FROM SiteTree WHERE ID = $this->ID")->value();
$liveVersion =
DB::query("SELECT Version FROM SiteTree_Live WHERE ID = $this->ID")->value();
if($stageVersion && !$liveVersion)
$this->AddedToStage = true;
else if(!$stageVersion && $liveVersion)
$this->DeletedFromStage = true;
else if($stageVersion != $liveVersion)
$this->ModifiedOnStage = true;
}
$tag =
($this->DeletedFromStage ?
"del title=\"" . _t('SiteTree.REMOVEDFROMDRAFT', 'Removed from draft site') . "\"" :
($this->AddedToStage ?
"ins title=\"" . _t('SiteTree.ADDEDTODRAFT', 'Added to draft site') . "\"" :
($this->ModifiedOnStage ?
"span title=\"" . _t('SiteTree.MODIFIEDONDRAFT', 'Modified on draft site') . "\" class=\"modified\"" : "")));
if($tag) {
return "<$tag>" . $this->MenuTitle . "</" . strtok($tag,' ') . ">";
} else {
return $this->MenuTitle;
}
}
/**
* Returns the page in the current page stack of the given level.
* Level(1) will return the main menu item that we're currently inside, etc.
*/
public function Level($level) {
$parent = $this;
$stack = array($parent);
while($parent = $parent->Parent) {
array_unshift($stack, $parent);
}
return isset($stack[$level-1]) ? $stack[$level-1] : null;
}
/**
* Return the CSS classes to apply to this node in the CMS tree
*
* @param Controller $controller The controller object that the tree
* appears on
* @return string
*/
function CMSTreeClasses($controller) {
$classes = $this->class;
if($this->HasBrokenFile || $this->HasBrokenLink)
$classes .= " BrokenLink";
if(!$this->canAddChildren())
$classes .= " nochildren";
if(!$this->canDelete())
$classes .= " nodelete";
if($controller->isCurrentPage($this))
$classes .= " current";
if(!$this->canEdit() && !$this->canAddChildren())
$classes .= " disabled";
$classes .= $this->markingClasses();
return $classes;
}
}
?>