Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Minor Moved ErrorPage, RedirectorPage, SiteConfig, SiteTree, SiteTree…

…Decorator, VirtualPage from sapphire/core/model/ to cms module
  • Loading branch information...
commit d3c5a309bfa967a061ced78406a86e6625ccdee3 1 parent 9b4f3aa
Paul Meyrick authored chillu committed
View
279 code/ErrorPage.php
@@ -0,0 +1,279 @@
+<?php
+/**
+ * ErrorPage holds the content for the page of an error response.
+ * Renders the page on each publish action into a static HTML file
+ * within the assets directory, after the naming convention
+ * /assets/error-<statuscode>.html.
+ * This enables us to show errors even if PHP experiences a recoverable error.
+ * ErrorPages
+ *
+ * @see Debug::friendlyError()
+ *
+ * @package cms
+ */
+class ErrorPage extends Page {
+
+ static $db = array(
+ "ErrorCode" => "Int",
+ );
+
+ static $defaults = array(
+ "ShowInMenus" => 0,
+ "ShowInSearch" => 0
+ );
+
+ static $icon = array("sapphire/javascript/tree/images/page", "file");
+
+ protected static $static_filepath = ASSETS_PATH;
+
+ /**
+ * Get a {@link SS_HTTPResponse} to response to a HTTP error code if an {@link ErrorPage} for that code is present.
+ *
+ * @param int $statusCode
+ * @return SS_HTTPResponse
+ */
+ public static function response_for($statusCode) {
+ // first attempt to dynamically generate the error page
+ if($errorPage = DataObject::get_one('ErrorPage', "\"ErrorCode\" = $statusCode")) {
+ return ModelAsController::controller_for($errorPage)->handleRequest(new SS_HTTPRequest('GET', ''));
+ }
+
+ // then fall back on a cached version
+ $cachedPath = self::get_filepath_for_errorcode($statusCode, Translatable::get_current_locale());
+
+ if(file_exists($cachedPath)) {
+ $response = new SS_HTTPResponse();
+
+ $response->setStatusCode($statusCode);
+ $response->setBody(file_get_contents($cachedPath));
+
+ return $response;
+ }
+ }
+
+ /**
+ * Ensures that there is always a 404 page
+ * by checking if there's an instance of
+ * ErrorPage with a 404 and 500 error code. If there
+ * is not, one is created when the DB is built.
+ */
+ function requireDefaultRecords() {
+ parent::requireDefaultRecords();
+
+ // Ensure that an assets path exists before we do any error page creation
+ if(!file_exists(ASSETS_PATH)) {
+ mkdir(ASSETS_PATH);
+ }
+
+ $pageNotFoundErrorPage = DataObject::get_one('ErrorPage', "\"ErrorCode\" = '404'");
+ $pageNotFoundErrorPageExists = ($pageNotFoundErrorPage && $pageNotFoundErrorPage->exists()) ? true : false;
+ $pageNotFoundErrorPagePath = self::get_filepath_for_errorcode(404);
+ if(!($pageNotFoundErrorPageExists && file_exists($pageNotFoundErrorPagePath))) {
+ if(!$pageNotFoundErrorPageExists) {
+ $pageNotFoundErrorPage = new ErrorPage();
+ $pageNotFoundErrorPage->ErrorCode = 404;
+ $pageNotFoundErrorPage->Title = _t('ErrorPage.DEFAULTERRORPAGETITLE', 'Page not found');
+ $pageNotFoundErrorPage->Content = _t('ErrorPage.DEFAULTERRORPAGECONTENT', '<p>Sorry, it seems you were trying to access a page that doesn\'t exist.</p><p>Please check the spelling of the URL you were trying to access and try again.</p>');
+ $pageNotFoundErrorPage->Status = 'New page';
+ $pageNotFoundErrorPage->write();
+ $pageNotFoundErrorPage->publish('Stage', 'Live');
+ }
+
+ // Ensure a static error page is created from latest error page content
+ $response = Director::test(Director::makeRelative($pageNotFoundErrorPage->Link()));
+ if($fh = fopen($pageNotFoundErrorPagePath, 'w')) {
+ $written = fwrite($fh, $response->getBody());
+ fclose($fh);
+ }
+
+ if($written) {
+ DB::alteration_message('404 error page created', 'created');
+ } else {
+ DB::alteration_message(sprintf('404 error page could not be created at %s. Please check permissions', $pageNotFoundErrorPagePath), 'error');
+ }
+ }
+
+ $serverErrorPage = DataObject::get_one('ErrorPage', "\"ErrorCode\" = '500'");
+ $serverErrorPageExists = ($serverErrorPage && $serverErrorPage->exists()) ? true : false;
+ $serverErrorPagePath = self::get_filepath_for_errorcode(500);
+ if(!($serverErrorPageExists && file_exists($serverErrorPagePath))) {
+ if(!$serverErrorPageExists) {
+ $serverErrorPage = new ErrorPage();
+ $serverErrorPage->ErrorCode = 500;
+ $serverErrorPage->Title = _t('ErrorPage.DEFAULTSERVERERRORPAGETITLE', 'Server error');
+ $serverErrorPage->Content = _t('ErrorPage.DEFAULTSERVERERRORPAGECONTENT', '<p>Sorry, there was a problem with handling your request.</p>');
+ $serverErrorPage->Status = 'New page';
+ $serverErrorPage->write();
+ $serverErrorPage->publish('Stage', 'Live');
+ }
+
+ // Ensure a static error page is created from latest error page content
+ $response = Director::test(Director::makeRelative($serverErrorPage->Link()));
+ if($fh = fopen($serverErrorPagePath, 'w')) {
+ $written = fwrite($fh, $response->getBody());
+ fclose($fh);
+ }
+
+ if($written) {
+ DB::alteration_message('500 error page created', 'created');
+ } else {
+ DB::alteration_message(sprintf('500 error page could not be created at %s. Please check permissions', $serverErrorPagePath), 'error');
+ }
+ }
+ }
+
+ function getCMSFields() {
+ $fields = parent::getCMSFields();
+
+ $fields->addFieldToTab(
+ "Root.Content.Main",
+ new DropdownField(
+ "ErrorCode",
+ $this->fieldLabel('ErrorCode'),
+ array(
+ 400 => _t('ErrorPage.400', '400 - Bad Request'),
+ 401 => _t('ErrorPage.401', '401 - Unauthorized'),
+ 403 => _t('ErrorPage.403', '403 - Forbidden'),
+ 404 => _t('ErrorPage.404', '404 - Not Found'),
+ 405 => _t('ErrorPage.405', '405 - Method Not Allowed'),
+ 406 => _t('ErrorPage.406', '406 - Not Acceptable'),
+ 407 => _t('ErrorPage.407', '407 - Proxy Authentication Required'),
+ 408 => _t('ErrorPage.408', '408 - Request Timeout'),
+ 409 => _t('ErrorPage.409', '409 - Conflict'),
+ 410 => _t('ErrorPage.410', '410 - Gone'),
+ 411 => _t('ErrorPage.411', '411 - Length Required'),
+ 412 => _t('ErrorPage.412', '412 - Precondition Failed'),
+ 413 => _t('ErrorPage.413', '413 - Request Entity Too Large'),
+ 414 => _t('ErrorPage.414', '414 - Request-URI Too Long'),
+ 415 => _t('ErrorPage.415', '415 - Unsupported Media Type'),
+ 416 => _t('ErrorPage.416', '416 - Request Range Not Satisfiable'),
+ 417 => _t('ErrorPage.417', '417 - Expectation Failed'),
+ 500 => _t('ErrorPage.500', '500 - Internal Server Error'),
+ 501 => _t('ErrorPage.501', '501 - Not Implemented'),
+ 502 => _t('ErrorPage.502', '502 - Bad Gateway'),
+ 503 => _t('ErrorPage.503', '503 - Service Unavailable'),
+ 504 => _t('ErrorPage.504', '504 - Gateway Timeout'),
+ 505 => _t('ErrorPage.505', '505 - HTTP Version Not Supported'),
+ )
+ ),
+ "Content"
+ );
+
+ return $fields;
+ }
+
+ /**
+ * When an error page is published, create a static HTML page with its
+ * content, so the page can be shown even when SilverStripe is not
+ * functioning correctly before publishing this page normally.
+ * @param string|int $fromStage Place to copy from. Can be either a stage name or a version number.
+ * @param string $toStage Place to copy to. Must be a stage name.
+ * @param boolean $createNewVersion Set this to true to create a new version number. By default, the existing version number will be copied over.
+ */
+ function doPublish() {
+ parent::doPublish();
+
+ // Run the page (reset the theme, it might've been disabled by LeftAndMain::init())
+ $oldTheme = SSViewer::current_theme();
+ SSViewer::set_theme(SSViewer::current_custom_theme());
+ $response = Director::test(Director::makeRelative($this->Link()));
+ SSViewer::set_theme($oldTheme);
+
+ $errorContent = $response->getBody();
+
+ // Make the base tag dynamic.
+ // $errorContent = preg_replace('/<base[^>]+href="' . str_replace('/','\\/', Director::absoluteBaseURL()) . '"[^>]*>/i', '<base href="$BaseURL" />', $errorContent);
+
+ // Check we have an assets base directory, creating if it we don't
+ if(!file_exists(ASSETS_PATH)) {
+ mkdir(ASSETS_PATH, 02775);
+ }
+
+
+ // if the page is published in a language other than default language,
+ // write a specific language version of the HTML page
+ $filePath = self::get_filepath_for_errorcode($this->ErrorCode, $this->Locale);
+ if($fh = fopen($filePath, "w")) {
+ fwrite($fh, $errorContent);
+ fclose($fh);
+ } else {
+ $fileErrorText = sprintf(
+ _t(
+ "ErrorPage.ERRORFILEPROBLEM",
+ "Error opening file \"%s\" for writing. Please check file permissions."
+ ),
+ $errorFile
+ );
+ FormResponse::status_message($fileErrorText, 'bad');
+ FormResponse::respond();
+ return;
+ }
+ }
+
+ /**
+ *
+ * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
+ *
+ */
+ function fieldLabels($includerelations = true) {
+ $labels = parent::fieldLabels($includerelations);
+ $labels['ErrorCode'] = _t('ErrorPage.CODE', "Error code");
+
+ return $labels;
+ }
+
+ /**
+ * Returns an absolute filesystem path to a static error file
+ * which is generated through {@link publish()}.
+ *
+ * @param int $statusCode A HTTP Statuscode, mostly 404 or 500
+ * @param String $locale A locale, e.g. 'de_DE' (Optional)
+ * @return String
+ */
+ static function get_filepath_for_errorcode($statusCode, $locale = null) {
+ if (singleton('ErrorPage')->hasMethod('alternateFilepathForErrorcode')) {
+ return singleton('ErrorPage')-> alternateFilepathForErrorcode($statusCode, $locale);
+ }
+ if(singleton('SiteTree')->hasExtension('Translatable') && $locale && $locale != Translatable::default_locale()) {
+ return self::$static_filepath . "/error-{$statusCode}-{$locale}.html";
+ } else {
+ return self::$static_filepath . "/error-{$statusCode}.html";
+ }
+ }
+
+ /**
+ * Set the path where static error files are saved through {@link publish()}.
+ * Defaults to /assets.
+ *
+ * @param string $path
+ */
+ static function set_static_filepath($path) {
+ self::$static_filepath = $path;
+ }
+
+ /**
+ * @return string
+ */
+ static function get_static_filepath() {
+ return self::$static_filepath;
+ }
+}
+
+/**
+ * Controller for ErrorPages.
+ * @package cms
+ */
+class ErrorPage_Controller extends Page_Controller {
+ function init() {
+ parent::init();
+
+ $action = $this->request->param('Action');
+ if(!$action || $action == 'index') {
+ Director::set_status_code($this->failover->ErrorCode ? $this->failover->ErrorCode : 404);
+ }
+
+ }
+}
+
+
+?>
View
181 code/RedirectorPage.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * A redirector page redirects when the page is visited.
+ *
+ * @package cms
+ * @subpackage content
+ */
+class RedirectorPage extends Page {
+
+ static $icon = array("cms/images/treeicons/page-shortcut","file");
+
+ static $db = array(
+ "RedirectionType" => "Enum('Internal,External','Internal')",
+ "ExternalURL" => "Varchar(2083)" // 2083 is the maximum length of a URL in Internet Explorer.
+ );
+
+ static $defaults = array(
+ "RedirectionType" => "Internal"
+ );
+
+ static $has_one = array(
+ "LinkTo" => "SiteTree",
+ );
+
+ static $many_many = array(
+ );
+
+ /**
+ * Returns this page if the redirect is external, otherwise
+ * returns the target page.
+ * @return SiteTree
+ */
+ function ContentSource() {
+ if($this->RedirectionType == 'Internal') {
+ return $this->LinkTo();
+ } else {
+ return $this;
+ }
+ }
+
+ /**
+ * Return the the link that should be used for this redirector page, in navigation, etc.
+ * If the redirectorpage has been appropriately configured, then it will return the redirection
+ * destination, to prevent unnecessary 30x redirections. However, if it's misconfigured, then
+ * it will return a link to itself, which will then display an error message.
+ */
+ function Link() {
+ if($link = $this->redirectionLink()) return $link;
+ else return $this->regularLink();
+ }
+
+ /**
+ * Return the normal link directly to this page. Once you visit this link, a 30x redirection
+ * will take you to your final destination.
+ */
+ function regularLink($action = null) {
+ return parent::Link($action);
+ }
+
+ /**
+ * Return the link that we should redirect to.
+ * Only return a value if there is a legal redirection destination.
+ */
+ function redirectionLink() {
+ if($this->RedirectionType == 'External') {
+ if($this->ExternalURL) {
+ return $this->ExternalURL;
+ }
+
+ } else {
+ $linkTo = $this->LinkToID ? DataObject::get_by_id("SiteTree", $this->LinkToID) : null;
+
+ if($linkTo) {
+ // We shouldn't point to ourselves - that would create an infinite loop! Return null since we have a
+ // bad configuration
+ if($this->ID == $linkTo->ID) {
+ return null;
+
+ // If we're linking to another redirectorpage then just return the URLSegment, to prevent a cycle of redirector
+ // pages from causing an infinite loop. Instead, they will cause a 30x redirection loop in the browser, but
+ // this can be handled sufficiently gracefully by the browser.
+ } elseif($linkTo instanceof RedirectorPage) {
+ return $linkTo->regularLink();
+
+ // For all other pages, just return the link of the page.
+ } else {
+ return $linkTo->Link();
+ }
+ }
+ }
+ }
+
+ function syncLinkTracking() {
+ if ($this->RedirectionType == 'Internal') {
+ if($this->LinkToID) {
+ $this->HasBrokenLink = DataObject::get_by_id('SiteTree', $this->LinkToID) ? false : true;
+ } else {
+ // An incomplete redirector page definitely has a broken link
+ $this->HasBrokenLink = true;
+ }
+ } else {
+ // TODO implement checking of a remote site
+ $this->HasBrokenLink = false;
+ }
+ }
+
+ function onBeforeWrite() {
+ parent::onBeforeWrite();
+
+ // Prefix the URL with "http://" if no prefix is found
+ if($this->ExternalURL && (strpos($this->ExternalURL, '://') === false)) {
+ $this->ExternalURL = 'http://' . $this->ExternalURL;
+ }
+ }
+
+ function getCMSFields() {
+ Requirements::javascript(SAPPHIRE_DIR . "/javascript/RedirectorPage.js");
+
+ $fields = parent::getCMSFields();
+ $fields->removeByName('Content', true);
+
+ // Remove all metadata fields, does not apply for redirector pages
+ $fields->removeByName('MetaTagsHeader');
+ $fields->removeByName('MetaTitle');
+ $fields->removeByName('MetaKeywords');
+ $fields->removeByName('MetaDescription');
+ $fields->removeByName('ExtraMeta');
+
+ $fields->addFieldsToTab('Root.Content.Main',
+ array(
+ new HeaderField('RedirectorDescHeader',_t('RedirectorPage.HEADER', "This page will redirect users to another page")),
+ new OptionsetField(
+ "RedirectionType",
+ _t('RedirectorPage.REDIRECTTO', "Redirect to"),
+ array(
+ "Internal" => _t('RedirectorPage.REDIRECTTOPAGE', "A page on your website"),
+ "External" => _t('RedirectorPage.REDIRECTTOEXTERNAL', "Another website"),
+ ),
+ "Internal"
+ ),
+ new TreeDropdownField(
+ "LinkToID",
+ _t('RedirectorPage.YOURPAGE', "Page on your website"),
+ "SiteTree"
+ ),
+ new TextField("ExternalURL", _t('RedirectorPage.OTHERURL', "Other website URL"))
+ )
+ );
+
+ return $fields;
+ }
+
+ // Don't cache RedirectorPages
+ function subPagesToCache() {
+ return array();
+ }
+}
+
+/**
+ * Controller for the {@link RedirectorPage}.
+ * @package cms
+ * @subpackage content
+ */
+class RedirectorPage_Controller extends Page_Controller {
+ function init() {
+ if($link = $this->redirectionLink()) {
+ Director::redirect($link, 301);
+ }
+
+ parent::init();
+ }
+
+ /**
+ * If we ever get this far, it means that the redirection failed.
+ */
+ function Content() {
+ return "<p class=\"message-setupWithoutRedirect\">" .
+ _t('RedirectorPage.HASBEENSETUP', 'A redirector page has been set up without anywhere to redirect to.') .
+ "</p>";
+ }
+}
View
289 code/SiteConfig.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * Sitewide configuration.
+ *
+ * h2. Translation
+ *
+ * To enable translation of configurations alongside the {@link Translatable} extension.
+ * This also allows assigning language-specific toplevel permissions for viewing and editing
+ * pages, in addition to the normal `TRANSLATE_*`/`TRANSLATE_ALL` permissions.
+ *
+ * Object::add_extension('SiteConfig', 'Translatable');
+ *
+ * @author Tom Rix
+ * @package cms
+ */
+class SiteConfig extends DataObject implements PermissionProvider {
+ static $db = array(
+ "Title" => "Varchar(255)",
+ "Tagline" => "Varchar(255)",
+ "Theme" => "Varchar(255)",
+ "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')",
+ "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
+ "CanCreateTopLevelType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
+ );
+
+ static $many_many = array(
+ "ViewerGroups" => "Group",
+ "EditorGroups" => "Group",
+ "CreateTopLevelGroups" => "Group"
+ );
+
+ protected static $disabled_themes = array();
+
+ public static function disable_theme($theme) {
+ self::$disabled_themes[$theme] = $theme;
+ }
+
+ /**
+ * Get the fields that are sent to the CMS. In
+ * your decorators: updateCMSFields(&$fields)
+ *
+ * @return Fieldset
+ */
+ function getCMSFields() {
+ Requirements::javascript(CMS_DIR . "/javascript/SitetreeAccess.js");
+
+ $fields = new FieldSet(
+ new TabSet("Root",
+ $tabMain = new Tab('Main',
+ $titleField = new TextField("Title", _t('SiteConfig.SITETITLE', "Site title")),
+ $taglineField = new TextField("Tagline", _t('SiteConfig.SITETAGLINE', "Site Tagline/Slogan")),
+ new DropdownField("Theme", _t('SiteConfig.THEME', 'Theme'), $this->getAvailableThemes(), '', null, _t('SiteConfig.DEFAULTTHEME', '(Use default theme)'))
+ ),
+ $tabAccess = new Tab('Access',
+ new HeaderField('WhoCanViewHeader', _t('SiteConfig.VIEWHEADER', "Who can view pages on this site?"), 2),
+ $viewersOptionsField = new OptionsetField("CanViewType"),
+ $viewerGroupsField = new TreeMultiselectField("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups")),
+ new HeaderField('WhoCanEditHeader', _t('SiteConfig.EDITHEADER', "Who can edit pages on this site?"), 2),
+ $editorsOptionsField = new OptionsetField("CanEditType"),
+ $editorGroupsField = new TreeMultiselectField("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups")),
+ new HeaderField('WhoCanCreateTopLevelHeader', _t('SiteConfig.TOPLEVELCREATE', "Who can create pages in the root of the site?"), 2),
+ $topLevelCreatorsOptionsField = new OptionsetField("CanCreateTopLevelType"),
+ $topLevelCreatorsGroupsField = new TreeMultiselectField("CreateTopLevelGroups", _t('SiteTree.TOPLEVELCREATORGROUPS', "Top level creators"))
+ )
+ )
+ );
+
+ $viewersOptionsSource = array();
+ $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
+ $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
+ $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
+ $viewersOptionsField->setSource($viewersOptionsSource);
+
+ $editorsOptionsSource = array();
+ $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
+ $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
+ $editorsOptionsField->setSource($editorsOptionsSource);
+
+ $topLevelCreatorsOptionsField->setSource($editorsOptionsSource);
+
+ // Translatable doesn't handle updateCMSFields on DataObjects,
+ // so add it here to save the current Locale,
+ // because onBeforeWrite does not work.
+ if(Object::has_extension('SiteConfig',"Translatable")){
+ $fields->push(new HiddenField("Locale"));
+ }
+
+ if (!Permission::check('EDIT_SITECONFIG')) {
+ $fields->makeFieldReadonly($viewersOptionsField);
+ $fields->makeFieldReadonly($viewerGroupsField);
+ $fields->makeFieldReadonly($editorsOptionsField);
+ $fields->makeFieldReadonly($editorGroupsField);
+ $fields->makeFieldReadonly($topLevelCreatorsOptionsField);
+ $fields->makeFieldReadonly($topLevelCreatorsGroupsField);
+ $fields->makeFieldReadonly($taglineField);
+ $fields->makeFieldReadonly($titleField);
+ }
+
+ if(file_exists(BASE_PATH . '/install.php')) {
+ $fields->addFieldToTab("Root.Main", new LiteralField("InstallWarningHeader",
+ "<p class=\"message warning\">" . _t("SiteTree.REMOVE_INSTALL_WARNING",
+ "Warning: You should remove install.php from this SilverStripe install for security reasons.")
+ . "</p>"), "Title");
+ }
+
+ $tabMain->setTitle(_t('SiteConfig.TABMAIN', "Main"));
+ $tabAccess->setTitle(_t('SiteConfig.TABACCESS', "Access"));
+ $this->extend('updateCMSFields', $fields);
+
+ return $fields;
+ }
+
+ /**
+ * Get all available themes that haven't been marked as disabled.
+ * @param string $baseDir Optional alternative theme base directory for testing
+ * @return array of theme directory names
+ */
+ public function getAvailableThemes($baseDir = null) {
+ $themes = ManifestBuilder::get_themes($baseDir);
+ foreach(self::$disabled_themes as $theme) {
+ if(isset($themes[$theme])) unset($themes[$theme]);
+ }
+ return $themes;
+ }
+
+ /**
+ * Get the actions that are sent to the CMS. In
+ * your decorators: updateEditFormActions(&$actions)
+ *
+ * @return Fieldset
+ */
+ function getCMSActions() {
+ if (Permission::check('ADMIN') || Permission::check('EDIT_SITECONFIG')) {
+ $actions = new FieldSet(
+ new FormAction('save_siteconfig', _t('CMSMain.SAVE','Save'))
+ );
+ } else {
+ $actions = new FieldSet();
+ }
+
+ $this->extend('updateCMSActions', $actions);
+
+ return $actions;
+ }
+
+ /**
+ * Get the current sites SiteConfig, and creates a new one
+ * through {@link make_site_config()} if none is found.
+ *
+ * @param string $locale
+ * @return SiteConfig
+ */
+ static function current_site_config($locale = null) {
+ if(Object::has_extension('SiteConfig',"Translatable")){
+ $locale = isset($locale) ? $locale : Translatable::get_current_locale();
+ $siteConfig = Translatable::get_one_by_locale('SiteConfig', $locale);
+ } else {
+ $siteConfig = DataObject::get_one('SiteConfig');
+ }
+
+ if (!$siteConfig) $siteConfig = self::make_site_config($locale);
+
+ return $siteConfig;
+ }
+
+ /**
+ * Setup a default SiteConfig record if none exists
+ */
+ function requireDefaultRecords() {
+ parent::requireDefaultRecords();
+ $siteConfig = DataObject::get_one('SiteConfig');
+ if(!$siteConfig) {
+ self::make_site_config();
+ DB::alteration_message("Added default site config","created");
+ }
+ }
+
+ /**
+ * Create SiteConfig with defaults from language file.
+ * if Translatable is enabled on SiteConfig, see if one already exist
+ * and use those values for the translated defaults.
+ *
+ * @param string $locale
+ * @return SiteConfig
+ */
+ static function make_site_config($locale = null) {
+ if(!$locale) $locale = Translatable::get_current_locale();
+
+ $siteConfig = new SiteConfig();
+ $siteConfig->Title = _t('SiteConfig.SITENAMEDEFAULT',"Your Site Name");
+ $siteConfig->Tagline = _t('SiteConfig.TAGLINEDEFAULT',"your tagline here");
+
+ if($siteConfig->hasExtension('Translatable')){
+ $defaultConfig = DataObject::get_one('SiteConfig');
+ if($defaultConfig){
+ $siteConfig->Title = $defaultConfig->Title;
+ $siteConfig->Tagline = $defaultConfig->Tagline;
+ }
+
+ // TODO Copy view/edit group settings
+
+ // set the correct Locale
+ $siteConfig->Locale = $locale;
+ }
+
+ $siteConfig->write();
+
+ return $siteConfig;
+ }
+
+ /**
+ * Can a user view pages on this site? This method is only
+ * called if a page is set to Inherit, but there is nothing
+ * to inherit from.
+ *
+ * @param mixed $member
+ * @return boolean
+ */
+ public function canView($member = null) {
+ if(!$member) $member = Member::currentUserID();
+ if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
+
+ if (!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
+
+ // check for any logged-in users
+ if($this->CanViewType == 'LoggedInUsers' && $member) return true;
+
+ // check for specific groups
+ if($this->CanViewType == 'OnlyTheseUsers' && $member && $member->inGroups($this->ViewerGroups())) return true;
+
+ return false;
+ }
+
+ /**
+ * Can a user edit pages on this site? This method is only
+ * called if a page is set to Inherit, but there is nothing
+ * to inherit from.
+ *
+ * @param mixed $member
+ * @return boolean
+ */
+ public function canEdit($member = null) {
+ if(!$member) $member = Member::currentUserID();
+ if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
+
+ // check for any logged-in users
+ if(!$this->CanEditType || $this->CanEditType == 'LoggedInUsers' && $member) return true;
+
+ // check for specific groups
+ if($this->CanEditType == 'OnlyTheseUsers' && $member && $member->inGroups($this->EditorGroups())) return true;
+
+ return false;
+ }
+
+ function providePermissions() {
+ return array(
+ 'EDIT_SITECONFIG' => array(
+ 'name' => _t('SiteConfig.EDIT_PERMISSION', 'Manage site configuration'),
+ 'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
+ 'help' => _t('SiteConfig.EDIT_PERMISSION_HELP', 'Ability to edit global access settings/top-level page permissions.'),
+ 'sort' => 400
+ )
+ );
+ }
+
+ /**
+ * Can a user create pages in the root of this site?
+ *
+ * @param mixed $member
+ * @return boolean
+ */
+ public function canCreateTopLevel($member = null) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
+ $member = Member::currentUserID();
+ }
+
+ if (Permission::check('ADMIN')) return true;
+
+ // check for any logged-in users
+ if($this->CanCreateTopLevelType == 'LoggedInUsers' && $member) return true;
+
+ // check for specific groups
+ if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
+ if($this->CanCreateTopLevelType == 'OnlyTheseUsers' && $member && $member->inGroups($this->CreateTopLevelGroups())) return true;
+
+
+ return false;
+ }
+}
View
2,607 code/SiteTree.php
@@ -0,0 +1,2607 @@
+<?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 implements PermissionProvider,i18nEntityProvider {
+
+ /**
+ * 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" => "Text",
+ "MetaKeywords" => "Varchar(255)",
+ "ExtraMeta" => "HTMLText",
+ "ShowInMenus" => "Boolean",
+ "ShowInSearch" => "Boolean",
+ "HomepageForDomain" => "Varchar(100)",
+ "Sort" => "Int",
+ "HasBrokenFile" => "Boolean",
+ "HasBrokenLink" => "Boolean",
+ "ReportClass" => "Varchar",
+ "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
+ "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
+
+ // Simple task tracking
+ "ToDo" => "Text",
+ );
+
+ static $indexes = array(
+ "URLSegment" => true,
+ );
+
+ static $many_many = array(
+ "LinkTracking" => "SiteTree",
+ "ImageTracking" => "File",
+ "ViewerGroups" => "Group",
+ "EditorGroups" => "Group",
+ );
+
+ 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" => "SS_Datetime",
+ "Created" => "SS_Datetime",
+ 'Link' => 'Text',
+ 'RelativeLink' => 'Text',
+ 'AbsoluteLink' => 'Text',
+ );
+
+ static $defaults = array(
+ "ShowInMenus" => 1,
+ "ShowInSearch" => 1,
+ "CanViewType" => "Inherit",
+ "CanEditType" => "Inherit"
+ );
+
+ static $versioning = array(
+ "Stage", "Live"
+ );
+
+ static $default_sort = "\"Sort\"";
+
+ /**
+ * 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("sapphire\javascript\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("sapphire/javascript/tree/images/page", "file");
+
+
+ static $extensions = array(
+ "Hierarchy",
+ "Versioned('Stage', 'Live')",
+ );
+
+ /**
+ * Delimit breadcrumb-links generated by BreadCrumbs()
+ *
+ * @var string
+ */
+ public static $breadcrumbs_delimiter = " &raquo; ";
+
+ /**
+ * Whether or not to write the homepage map for static publisher
+ */
+ public static $write_homepage_map = true;
+
+ static $searchable_fields = array(
+ 'Title',
+ 'Content',
+ );
+
+ /**
+ * @see SiteTree::nested_urls()
+ */
+ private static $nested_urls = false;
+
+ /**
+ * @see SiteTree::set_create_defaultpages()
+ */
+ private static $create_default_pages = true;
+
+ /**
+ * This controls whether of not extendCMSFields() is called by getCMSFields.
+ */
+ private static $runCMSFieldsExtensions = true;
+
+ /**
+ * Cache for canView/Edit/Publish/Delete permissions
+ */
+ public static $cache_permissions = array();
+
+ /**
+ * @see SiteTree::enforce_strict_hierarchy()
+ */
+ private static $enforce_strict_hierarchy = true;
+
+ public static function set_enforce_strict_hierarchy($to) {
+ self::$enforce_strict_hierarchy = $to;
+ }
+
+ public static function get_enforce_strict_hierarchy() {
+ return self::$enforce_strict_hierarchy;
+ }
+
+ /**
+ * Returns TRUE if nested URLs (e.g. page/sub-page/) are currently enabled on this site.
+ *
+ * @return bool
+ */
+ public static function nested_urls() {
+ return self::$nested_urls;
+ }
+
+ public static function enable_nested_urls() {
+ self::$nested_urls = true;
+ }
+
+ public static function disable_nested_urls() {
+ self::$nested_urls = false;
+ }
+
+ /**
+ * Set the (re)creation of default pages on /dev/build
+ *
+ * @param bool $option
+ */
+ public static function set_create_default_pages($option = true) {
+ self::$create_default_pages = $option;
+ }
+
+ /**
+ * Fetches the {@link SiteTree} object that maps to a link.
+ *
+ * If you have enabled {@link SiteTree::nested_urls()} on this site, then you can use a nested link such as
+ * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
+ *
+ * Note that if no model can be found, this method will fall over to a decorated alternateGetByLink method provided
+ * by a decorator attached to {@link SiteTree}
+ *
+ * @param string $link
+ * @param bool $cache
+ * @return SiteTree
+ */
+ public static function get_by_link($link, $cache = true) {
+ if(trim($link, '/')) {
+ $link = trim(Director::makeRelative($link), '/');
+ } else {
+ $link = RootURLController::get_homepage_link();
+ }
+
+ $parts = Convert::raw2sql(preg_split('|/+|', $link));
+
+ // Grab the initial root level page to traverse down from.
+ $URLSegment = array_shift($parts);
+ $sitetree = DataObject::get_one (
+ 'SiteTree', "\"URLSegment\" = '$URLSegment'" . (self::nested_urls() ? ' AND "ParentID" = 0' : ''), $cache
+ );
+
+ /// Fall back on a unique URLSegment for b/c.
+ if(!$sitetree && self::nested_urls() && $pages = DataObject::get('SiteTree', "\"URLSegment\" = '$URLSegment'")) {
+ return ($pages->Count() == 1) ? $pages->First() : null;
+ }
+
+ // Attempt to grab an alternative page from decorators.
+ if(!$sitetree) {
+ $parentID = self::nested_urls() ? 0 : null;
+
+ if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $URLSegment, $parentID)) {
+ foreach($alternatives as $alternative) if($alternative) $sitetree = $alternative;
+ }
+
+ if(!$sitetree) return false;
+ }
+
+ // Check if we have any more URL parts to parse.
+ if(!self::nested_urls() || !count($parts)) return $sitetree;
+
+ // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
+ foreach($parts as $segment) {
+ $next = DataObject::get_one (
+ 'SiteTree', "\"URLSegment\" = '$segment' AND \"ParentID\" = $sitetree->ID", $cache
+ );
+
+ if(!$next) {
+ $parentID = (int) $sitetree->ID;
+
+ if($alternatives = singleton('SiteTree')->extend('alternateGetByLink', $segment, $parentID)) {
+ foreach($alternatives as $alternative) if($alternative) $next = $alternative;
+ }
+
+ if(!$next) return false;
+ }
+
+ $sitetree->destroy();
+ $sitetree = $next;
+ }
+
+ return $sitetree;
+ }
+
+ /**
+ * Return a subclass map of SiteTree
+ * that shouldn't be hidden through
+ * {@link SiteTree::$hide_ancestor}
+ *
+ * @return array
+ */
+ public static function page_type_classes() {
+ $classes = ClassInfo::getValidSubClasses();
+
+ $baseClassIndex = array_search('SiteTree', $classes);
+ if($baseClassIndex !== FALSE) unset($classes[$baseClassIndex]);
+
+ $kill_ancestors = array();
+
+ // figure out if there are any classes we don't want to appear
+ foreach($classes as $class) {
+ $instance = singleton($class);
+
+ // do any of the progeny want to hide an ancestor?
+ if($ancestor_to_hide = $instance->stat('hide_ancestor')) {
+ // note for killing later
+ $kill_ancestors[] = $ancestor_to_hide;
+ }
+ }
+
+ // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to requirements.
+ if($kill_ancestors) {
+ $kill_ancestors = array_unique($kill_ancestors);
+ foreach($kill_ancestors as $mark) {
+ // unset from $classes
+ $idx = array_search($mark, $classes);
+ unset($classes[$idx]);
+ }
+ }
+
+ return $classes;
+ }
+
+ /**
+ * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
+ *
+ * @return string
+ */
+ public static function link_shortcode_handler($arguments, $content = null, $parser = null) {
+ if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
+
+ if (
+ !($page = DataObject::get_by_id('SiteTree', $arguments['id'])) // Get the current page by ID.
+ && !($page = Versioned::get_latest_version('SiteTree', $arguments['id'])) // Attempt link to old version.
+ && !($page = DataObject::get_one('ErrorPage', '"ErrorCode" = \'404\'')) // Link to 404 page directly.
+ ) {
+ return; // There were no suitable matches at all.
+ }
+
+ if($content) {
+ return sprintf('<a href="%s">%s</a>', $page->Link(), $parser->parse($content));
+ } else {
+ return $page->Link();
+ }
+ }
+
+ /**
+ * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
+ *
+ * @param string $action Optional controller action (method).
+ * Note: URI encoding of this parameter is applied automatically through template casting,
+ * don't encode the passed parameter.
+ * Please use {@link Controller::join_links()} instead to append GET parameters.
+ * @return string
+ */
+ public function Link($action = null) {
+ return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
+ }
+
+ /**
+ * Get the absolute URL for this page, including protocol and host.
+ *
+ * @param string $action See {@link Link()}
+ * @return string
+ */
+ public function AbsoluteLink($action = null) {
+ if($this->hasMethod('alternateAbsoluteLink')) {
+ return $this->alternateAbsoluteLink($action);
+ } else {
+ return Director::absoluteURL($this->Link($action));
+ }
+ }
+
+ /**
+ * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
+ *
+ * By default, it this page is the current home page, and there is no action specified then this will return a link
+ * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
+ * and returned in its full form.
+ *
+ * @uses RootURLController::get_homepage_link()
+ *
+ * @param string $action See {@link Link()}
+ * @return string
+ */
+ public function RelativeLink($action = null) {
+ if($this->ParentID && self::nested_urls()) {
+ $base = $this->Parent()->RelativeLink($this->URLSegment);
+ } else {
+ $base = $this->URLSegment;
+ }
+
+ // Unset base for homepage URLSegments in their default language.
+ // Homepages with action parameters or in different languages
+ // need to retain their URLSegment. We can only do this if the homepage
+ // is on the root level.
+ if(!$action && $base == RootURLController::get_homepage_link() && !$this->ParentID) {
+ $base = null;
+ if($this->hasExtension('Translatable') && $this->Locale != Translatable::default_locale()){
+ $base = $this->URLSegment;
+ }
+ }
+
+ // Legacy support
+ if($action === true) $action = null;
+
+ return Controller::join_links($base, '/', $action);
+ }
+
+ /**
+ * Get the absolute URL for this page on the Live site.
+ */
+ public function getAbsoluteLiveLink($includeStageEqualsLive = true) {
+ $live = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $this->ID);
+
+ if($live) {
+ $link = $live->AbsoluteLink();
+
+ if($includeStageEqualsLive) {
+ $link .= '?stage=Live';
+ }
+
+ return $link;
+
+ }
+ }
+
+
+ /**
+ * Return a CSS identifier generated from this page's link.
+ *
+ * @return string The URL segment
+ */
+ public function ElementName() {
+ return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
+ }
+
+ /**
+ * Returns TRUE if this is the currently active page that is being used to handle a request.
+ *
+ * @return bool
+ */
+ public function isCurrent() {
+ return $this->ID ? $this->ID == Director::get_current_page()->ID : $this === Director::get_current_page();
+ }
+
+ /**
+ * Check if this page is in the currently active section (e.g. it is either current or one of it's children is
+ * currently being viewed.
+ *
+ * @return bool
+ */
+ public function isSection() {
+ return $this->isCurrent() || (
+ Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
+ );
+ }
+
+ /**
+ * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
+ *
+ * @return string
+ */
+ public function LinkOrCurrent() {
+ return $this->isCurrent() ? 'current' : 'link';
+ }
+
+ /**
+ * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
+ *
+ * @return string
+ */
+ public function LinkOrSection() {
+ return $this->isSection() ? 'section' : 'link';
+ }
+
+ /**
+ * Return "link", "current" or section depending on if this page is the current page, or not on the current page but
+ * in the current section.
+ *
+ * @return string
+ */
+ public function LinkingMode() {
+ if($this->isCurrent()) {
+ return 'current';
+ } elseif($this->isSection()) {
+ return 'section';
+ } else {
+ return 'link';
+ }
+ }
+
+ /**
+ * 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::get_current_page();
+ while($page) {
+ if($sectionName == $page->URLSegment)
+ return true;
+ $page = $page->Parent;
+ }
+ return false;
+ }
+
+ /**
+ * 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(false);
+ $page->Sort = 0;
+ $this->extend('onBeforeDuplicate', $page);
+
+ if($doWrite) {
+ $page->write();
+
+ $page = $this->duplicateManyManyRelations($this, $page);
+ }
+ $this->extend('onAfterDuplicate', $page);
+
+ 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->Sort = 0;
+ $newSiteTree->write();
+ }
+
+ /**
+ * 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.
+ *
+ * @uses DataObjectDecorator->can()
+ *
+ * If a page is set to inherit, but has no parent, it inherits from
+ * {@link SiteConfig}
+ *
+ * @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(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
+ $member = Member::currentUserID();
+ }
+
+ if($member && Permission::checkMember($member, "ADMIN")) return true;
+
+ if(method_exists($this, 'can' . ucfirst($perm))) {
+ $method = 'can' . ucfirst($perm);
+ return $this->$method($member);
+ }
+
+ $results = $this->extend('can', $member);
+ if($results && is_array($results)) if(!min($results)) return false;
+
+ return true;
+ }
+
+
+ /**
+ * 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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - alternateCanAddChildren() on a decorator returns FALSE
+ * - canEdit() is not granted
+ * - There are no classes defined in {@link $allowed_children}
+ *
+ * @uses SiteTreeDecorator->canAddChildren()
+ * @uses canEdit()
+ * @uses $allowed_children
+ *
+ * @return boolean True if the current user can add children.
+ */
+ public function canAddChildren($member = null) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
+ $member = Member::currentUserID();
+ }
+
+ if($member && Permission::checkMember($member, "ADMIN")) return true;
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canAddChildren', $member);
+ if($extended !== null) return $extended;
+
+ return $this->canEdit($member) && $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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - canView() on any decorator returns FALSE
+ * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
+ * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
+ * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
+ *
+ * @uses DataObjectDecorator->canView()
+ * @uses ViewerGroups()
+ *
+ * @return boolean True if the current user can view this page.
+ */
+ public function canView($member = null) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
+ $member = Member::currentUserID();
+ }
+
+ // admin override
+ if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canView', $member);
+ if($extended !== null) return $extended;
+
+ // check for empty spec
+ if(!$this->CanViewType || $this->CanViewType == 'Anyone') return true;
+
+ // check for inherit
+ if($this->CanViewType == 'Inherit') {
+ if($this->ParentID) return $this->Parent()->canView($member);
+ else return $this->getSiteConfig()->canView($member);
+ }
+
+ // check for any logged-in users
+ if($this->CanViewType == 'LoggedInUsers' && $member) {
+ return true;
+ }
+
+ // check for specific groups
+ if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
+ if(
+ $this->CanViewType == 'OnlyTheseUsers'
+ && $member
+ && $member->inGroups($this->ViewerGroups())
+ ) return true;
+
+ return false;
+ }
+
+ /**
+ * Determines permissions for a specific stage (see {@link Versioned}).
+ * Usually the stage is read from {@link Versioned::current_stage()}.
+ * Falls back to {@link canView}.
+ *
+ * @todo Implement in CMS UI.
+ *
+ * @param String $stage
+ * @param Member $member
+ * @return boolean
+ */
+ function canViewStage($stage, $member = null) {
+ if(!$member) $member = Member::currentUser();
+
+ if(
+ strtolower($stage) == 'stage' &&
+ !(Permission::checkMember($member, 'CMS_ACCESS_CMSMain') || Permission::checkMember($member, 'VIEW_DRAFT_CONTENT'))
+ ) return false;
+
+ return $this->canView($member);
+ }
+
+ /**
+ * 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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - canDelete() returns FALSE on any decorator
+ * - canEdit() returns FALSE
+ * - any descendant page returns FALSE for canDelete()
+ *
+ * @uses canDelete()
+ * @uses DataObjectDecorator->canDelete()
+ * @uses canEdit()
+ *
+ * @param Member $member
+ * @return boolean True if the current user can delete this page.
+ */
+ public function canDelete($member = null) {
+ if($member instanceof Member) $memberID = $member->ID;
+ else if(is_numeric($member)) $memberID = $member;
+ else $memberID = Member::currentUserID();
+
+ if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
+ return true;
+ }
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canDelete', $memberID);
+ if($extended !== null) return $extended;
+
+ // Check cache (the can_edit_multiple call below will also do this, but this is quicker)
+ if(isset(self::$cache_permissions['delete'][$this->ID])) {
+ return self::$cache_permissions['delete'][$this->ID];
+ }
+
+ // Regular canEdit logic is handled by can_edit_multiple
+ $results = self::can_delete_multiple(array($this->ID), $memberID);
+
+ // If this page no longer exists in stage/live results won't contain the page.
+ // Fail-over to false
+ return isset($results[$this->ID]) ? $results[$this->ID] : 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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - canCreate() returns FALSE on any decorator
+ * - $can_create is set to FALSE and the site is not in "dev mode"
+ *
+ * Use {@link canAddChildren()} to control behaviour of creating children under this page.
+ *
+ * @uses $can_create
+ * @uses DataObjectDecorator->canCreate()
+ *
+ * @param Member $member
+ * @return boolean True if the current user can create pages on this class.
+ */
+ public function canCreate($member = null) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
+ $member = Member::currentUserID();
+ }
+
+ if($member && Permission::checkMember($member, "ADMIN")) return true;
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canCreate', $member);
+ if($extended !== null) return $extended;
+
+ 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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - canEdit() on any decorator returns FALSE
+ * - canView() return false
+ * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
+ * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the CMS_Access_CMSMAIN permission code
+ * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
+ *
+ * @uses canView()
+ * @uses EditorGroups()
+ * @uses DataObjectDecorator->canEdit()
+ *
+ * @param Member $member Set to FALSE if you want to explicitly test permissions without a valid user (useful for unit tests)
+ * @return boolean True if the current user can edit this page.
+ */
+ public function canEdit($member = null) {
+ if($member instanceof Member) $memberID = $member->ID;
+ else if(is_numeric($member)) $memberID = $member;
+ else $memberID = Member::currentUserID();
+
+ if($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) return true;
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canEdit', $memberID);
+ if($extended !== null) return $extended;
+
+ if($this->ID) {
+ // Check cache (the can_edit_multiple call below will also do this, but this is quicker)
+ if(isset(self::$cache_permissions['CanEditType'][$this->ID])) {
+ return self::$cache_permissions['CanEditType'][$this->ID];
+ }
+
+ // Regular canEdit logic is handled by can_edit_multiple
+ $results = self::can_edit_multiple(array($this->ID), $memberID);
+
+ // If this page no longer exists in stage/live results won't contain the page.
+ // Fail-over to false
+ return isset($results[$this->ID]) ? $results[$this->ID] : false;
+
+ // Default for unsaved pages
+ } else {
+ return $this->getSiteConfig()->canEdit($member);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * Denies permission if any of the following conditions is TRUE:
+ * - canPublish() on any decorator returns FALSE
+ * - canEdit() returns FALSE
+ *
+ * @uses SiteTreeDecorator->canPublish()
+ *
+ * @param Member $member
+ * @return boolean True if the current user can publish this page.
+ */
+ public function canPublish($member = null) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
+
+ if($member && Permission::checkMember($member, "ADMIN")) return true;
+
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canPublish', $member);
+ if($extended !== null) return $extended;
+
+ // Normal case - fail over to canEdit()
+ return $this->canEdit($member);
+ }
+
+ public function canDeleteFromLive($member = null) {
+ // Standard mechanism for accepting permission changes from decorators
+ $extended = $this->extendedCan('canDeleteFromLive', $member);
+ if($extended !==null) return $extended;
+
+ return $this->canPublish($member);
+ }
+
+ /**
+ * Stub method to get the site config, provided so it's easy to override
+ */
+ function getSiteConfig() {
+ $altConfig = false;
+ if($this->hasMethod('alternateSiteConfig')) {
+ $altConfig = $this->alternateSiteConfig();
+ }
+ if($altConfig) {
+ return $altConfig;
+ } elseif($this->hasExtension('Translatable')) {
+ return SiteConfig::current_site_config($this->Locale);
+ } else {
+ return SiteConfig::current_site_config();
+ }
+ }
+
+
+ /**
+ * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions.
+ * This method will use the static can_(perm)_multiple method for efficiency.
+ *
+ * @param $permission String The permission: edit, view, publish, approve, etc.
+ * @param $ids array An array of page IDs
+ * @param $batchCallBack The function/static method to call to calculate permissions. Defaults
+ * to 'SiteTree::can_(permission)_multiple'
+ */
+ static function prepopuplate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null) {
+ if(!$batchCallback) $batchCallback = "SiteTree::can_{$permission}_multiple";
+
+ //PHP 5.1 requires an array rather than a string for the call_user_func function
+ $batchCallback=explode('::', $batchCallback);
+
+ if(is_callable($batchCallback)) {
+ $permissionValues = call_user_func($batchCallback, $ids,
+ Member::currentUserID(), false);
+
+ if(!isset(self::$cache_permissions[$permission])) {
+ self::$cache_permissions[$permission] = array();
+ }
+
+ self::$cache_permissions[$permission] = $permissionValues
+ + self::$cache_permissions[$permission];
+
+ } else {
+ user_error("SiteTree::prepopuplate_permission_cache can't calculate '$permission' "
+ . "with callback '$batchCallback'", E_USER_WARNING);
+ }
+ }
+
+ static function batch_permission_check($ids, $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission = 'CMS_ACCESS_CMSMain', $useCached = true) {
+ // Sanitise the IDs
+ $ids = array_filter($ids, 'is_numeric');
+
+ // This is the name used on the permission cache
+ // converts something like 'CanEditType' to 'edit'.
+ $cacheKey = strtolower(substr($typeField, 3, -4));
+
+ // Default result: nothing editable
+ $result = array_fill_keys($ids, false);
+ if($ids) {
+
+ // Look in the cache for values
+ if($useCached && isset(self::$cache_permissions[$cacheKey])) {
+ $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
+
+ // If we can't find everything in the cache, then look up the remainder separately
+ $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
+ if($uncachedValues) {
+ $cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
+ }
+ return $cachedValues;
+ }
+
+ // If a member doesn't have CMS_ACCESS_CMSMain permission then they can't edit anything
+ if(!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
+ return $result;
+ }
+
+ $SQL_idList = implode($ids, ", ");
+
+ // if page can't be viewed, don't grant edit permissions
+ // to do - implement can_view_multiple(), so this can be enabled
+ //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
+
+ // Get the groups that the given member belongs to
+ $groupIDs = DataObject::get_by_id('Member', $memberID)->Groups()->column("ID");
+ $SQL_groupList = implode(", ", $groupIDs);
+ if (!$SQL_groupList) $SQL_groupList = '0';
+
+ $combinedStageResult = array();
+
+ foreach(array('Stage', 'Live') as $stage) {
+ // Start by filling the array with the pages that actually exist
+ $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
+
+ $result = array_fill_keys(DB::query("SELECT \"ID\" FROM \"$table\"
+ WHERE \"ID\" IN (".implode(", ", $ids).")")->column(), false);
+
+ // Get the uninherited permissions
+ $uninheritedPermissions = Versioned::get_by_stage("SiteTree", $stage, "(\"$typeField\" = 'LoggedInUsers' OR
+ (\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
+ AND \"SiteTree\".\"ID\" IN ($SQL_idList)",
+ "",
+ "LEFT JOIN \"$groupJoinTable\"
+ ON \"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\"
+ AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
+
+ if($uninheritedPermissions) {
+ // Set all the relevant items in $result to true
+ $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
+ }
+
+ // Get permissions that are inherited
+ $potentiallyInherited = Versioned::get_by_stage("SiteTree", $stage, "\"$typeField\" = 'Inherit'
+ AND \"SiteTree\".\"ID\" IN ($SQL_idList)");
+
+ if($potentiallyInherited) {
+ // Group $potentiallyInherited by ParentID; we'll look at the permission of all those
+ // parents and then see which ones the user has permission on
+ $siteConfigPermission = SiteConfig::current_site_config()->{$siteConfigMethod}($memberID);
+ $groupedByParent = array();
+ foreach($potentiallyInherited as $item) {
+ if($item->ParentID) {
+ if(!isset($groupedByParent[$item->ParentID])) $groupedByParent[$item->ParentID] = array();
+ $groupedByParent[$item->ParentID][] = $item->ID;
+ } else {
+ $result[$item->ID] = $siteConfigPermission;
+ }
+ }
+
+ if($groupedByParent) {
+ $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
+ if($actuallyInherited) {
+ $parentIDs = array_keys(array_filter($actuallyInherited));
+ foreach($parentIDs as $parentID) {
+ // Set all the relevant items in $result to true
+ $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
+ }
+ }
+ }
+ }
+
+ $combinedStageResult = $combinedStageResult + $result;
+
+ }
+ }
+
+ if(isset($combinedStageResult)) {
+ // Cache results
+ // TODO - Caching permissions is breaking unit tests. One possible issue
+ // is the cache needs to be flushed when permission on a page is changed,
+ // but this only solved some of the failing unit tests. Disabled for now.
+ /*foreach($combinedStageResult as $id => $val) {
+ self::$cache_permissions[$typeField][$id] = $val;
+ }*/
+ return $combinedStageResult;
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Get the 'can edit' information for a number of SiteTree pages.
+ *
+ * @param An array of IDs of the SiteTree pages to look up.
+ * @param useCached Return values from the permission cache if they exist.
+ * @return A map where the IDs are keys and the values are booleans stating whether the given
+ * page can be edited.
+ */
+ static function can_edit_multiple($ids, $memberID, $useCached = true) {
+ return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEdit', 'CMS_ACCESS_CMSMain', $useCached);
+ }
+
+ /**
+ * Get the 'can edit' information for a number of SiteTree pages.
+ * @param An array of IDs of the SiteTree pages to look up.
+ * @param useCached Return values from the permission cache if they exist.
+ */
+ static function can_delete_multiple($ids, $memberID, $useCached = true) {
+ $deletable = array();
+
+ $result = array_fill_keys($ids, false);
+
+ // Look in the cache for values
+ if($useCached && isset(self::$cache_permissions['delete'])) {
+ $cachedValues = array_intersect_key(self::$cache_permissions['delete'], $result);
+
+ // If we can't find everything in the cache, then look up the remainder separately
+ $uncachedValues = array_diff_key($result, self::$cache_permissions['delete']);
+ if($uncachedValues) {
+ $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
+ + $cachedValues;
+ }
+ return $cachedValues;
+ }
+
+ // You can only delete pages that you can edit
+ $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
+ if($editableIDs) {
+ $idList = implode(",", $editableIDs);
+
+ // You can only delete pages whose children you can delete
+ $childRecords = DataObject::get("SiteTree", "\"ParentID\" IN ($idList)");
+ if($childRecords) {
+ $children = $childRecords->map("ID", "ParentID");
+
+ // Find out the children that can be deleted
+ $deletableChildren = self::can_delete_multiple(array_keys($children), $memberID);
+
+ // Get a list of all the parents that have no undeletable children
+ $deletableParents = array_fill_keys($editableIDs, true);
+ foreach($deletableChildren as $id => $canDelete) {
+ if(!$canDelete) unset($deletableParents[$children[$id]]);
+ }
+
+ // Use that to filter the list of deletable parents that have children
+ $deletableParents = array_keys($deletableParents);
+
+ // Also get the $ids that don't have children
+ $parents = array_unique($children);
+ $deletableLeafNodes = array_diff($editableIDs, $parents);
+
+ // Combine the two
+ $deletable = array_merge($deletableParents, $deletableLeafNodes);
+
+ } else {
+ $deletable = $editableIDs;
+ }
+ } else {
+ $deletable = array();
+ }
+
+ // Convert the array of deletable IDs into a map of the original IDs with true/false as the
+ // value
+ return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
+ }
+
+ /**
+ * 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 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\" content=\"SilverStripe - http://silverstripe.org\" />\n";
+
+ $charset = ContentNegotiator::get_encoding();
+ $tags .= "<meta http-equiv=\"Content-type\" content=\"text/html; charset=$charset\" />\n";
+ if($this->MetaKeywords) {
+ $tags .= "<meta name=\"keywords\" content=\"" . Convert::raw2att($this->MetaKeywords) . "\" />\n";
+ }
+ if($this->MetaDescription) {
+ $tags .= "<meta name=\"description\" content=\"" . Convert::raw2att($this->MetaDescription) . "\" />\n";
+ }
+ if($this->ExtraMeta) {
+ $tags .= $this->ExtraMeta . "\n";
+ }
+
+ $this->extend('MetaTags', $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();
+
+ // default pages
+ if($this->class == 'SiteTree' && self::$create_default_pages) {
+ if(!SiteTree::get_by_link('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.org">developer documentation</a>, or begin <a href="http://doc.silverstripe.org/doku.php?id=tutorials">the tutorials.</a></p>');
+ $homepage->URLSegment = 'home';
+ $homepage->Status = 'Published';
+ $homepage->Sort = 1;
+ $homepage->write();
+ $homepage->publish('Stage', 'Live');
+ $homepage->flushCache();
+ DB::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->Status = 'Published';
+ $aboutus->Sort = 2;
+ $aboutus->write();
+ $aboutus->publish('Stage', 'Live');
+ $aboutus->flushCache();
+ DB::alteration_message('About Us page 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->Status = 'Published';
+ $contactus->Sort = 3;
+ $contactus->write();
+ $contactus->publish('Stage', 'Live');
+ $contactus->flushCache();
+ DB::alteration_message('Contact Us page created', 'created');
+ }
+ }
+
+ // schema migration
+ // @todo Move to migration task once infrastructure is implemented
+ if($this->class == 'SiteTree') {
+ $conn = DB::getConn();
+ // only execute command if fields haven't been renamed to _obsolete_<fieldname> already by the task
+ if(array_key_exists('Viewers', $conn->fieldList('SiteTree'))) {
+ $task = new UpgradeSiteTreePermissionSchemaTask();
+ $task->run(new SS_HTTPRequest('GET','/'));
+ }
+ }
+ }
+
+
+ //------------------------------------------------------------------------------------//
+
+ protected function onBeforeWrite() {
+ parent::onBeforeWrite();
+
+ // If Sort hasn't been set, make this page come after it's siblings
+ if(!$this->Sort) {
+ $parentID = ($this->ParentID) ? $this->ParentID : 0;
+ $this->Sort = DB::query("SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = $parentID")->value();
+ }
+
+ // If there is no URLSegment set, generate one from Title
+ if((!$this->URLSegment || $this->URLSegment == 'new-page') && $this->Title) {
+ $this->URLSegment = $this->generateURLSegment($this->Title);
+ } else if($this->isChanged('URLSegment')) {
+ // Make sure the URLSegment is valid for use in a URL
+ $segment = ereg_replace('[^A-Za-z0-9]+','-',$this->URLSegment);
+ $segment = ereg_replace('-+','-',$segment);
+
+ // If after sanitising there is no URLSegment, give it a reasonable default
+ if(!$segment) {
+ $segment = "page-$this->ID";
+ }
+ $this->URLSegment = $segment;
+ }
+
+ DataObject::set_context_obj($this);
+
+ // Ensure that this object has a non-conflicting URLSegment value.
+ $count = 2;
+ while(!$this->validURLSegment()) {
+ $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
+ $count++;
+ }
+
+ DataObject::set_context_obj(null);
+
+ $this->syncLinkTracking();
+
+ // Check to see if we've only altered fields that shouldn't affect versioning
+ $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo');
+ $changedFields = array_keys($this->getChangedFields(true, 2));
+
+ // This more rigorous check is inline with the test that write()
+ // does to dedcide whether or not to write to the DB. We use that
+ // to avoid cluttering the system with a migrateVersion() call
+ // that doesn't get used
+ $oneChangedFields = array_keys($this->getChangedFields(true, 1));
+
+ if($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
+ // This will have the affect of preserving the versioning
+ $this->migrateVersion($this->Version);
+ }
+ }
+
+ function syncLinkTracking() {
+ // Build a list of HTMLText fields
+ $allFields = $this->db();
+ $htmlFields = array();