Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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
279 code/ErrorPage.php
View
@@ -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);
+ }
+
+ }
+}
+
+
+?>
181 code/RedirectorPage.php
View
@@ -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>";
+ }
+}
289 code/SiteConfig.php
View
@@ -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;
+ }
+}
2,607 code/SiteTree.php
View
@@ -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();
+ foreach($allFields as $field => $fieldSpec) {
+ if(preg_match('/([^(]+)/', $fieldSpec, $matches)) {
+ $class = $matches[0];
+ if(class_exists($class)){
+ if($class == 'HTMLText' || is_subclass_of($class, 'HTMLText')) $htmlFields[] = $field;
+ }
+ }
+ }
+
+ $linkedPages = array();
+ $linkedFiles = array();
+ $this->HasBrokenLink = false;
+ $this->HasBrokenFile = false;
+
+ foreach($htmlFields as $field) {
+ $formField = new HTMLEditorField($field);
+ $formField->setValue($this->$field);
+ $formField->saveInto($this);
+ }
+ $this->extend('augmentSyncLinkTracking');
+ }
+
+ function onAfterWrite() {
+ // Need to flush cache to avoid outdated versionnumber references
+ $this->flushCache();
+
+ // Update any virtual pages that might need updating
+ $linkedPages = $this->VirtualPages();
+ if($linkedPages) foreach($linkedPages as $page) {
+ $page->copyFrom($page->CopyContentFrom());
+ $page->write();
+ }
+
+ parent::onAfterWrite();
+ }
+
+ function onBeforeDelete() {
+ parent::onBeforeDelete();
+
+ // If deleting this page, delete all its children.
+ if(SiteTree::get_enforce_strict_hierarchy() && $children = $this->Children()) {
+ foreach($children as $child) {
+ $child->delete();
+ }
+ }
+ }
+
+
+ function onAfterDelete() {
+ // Need to flush cache to avoid outdated versionnumber references
+ $this->flushCache();
+
+ // Need to mark pages depending to this one as broken
+ $dependentPages = $this->DependentPages();
+ if($dependentPages) foreach($dependentPages as $page) {
+ // $page->write() calls syncLinkTracking, which does all the hard work for us.
+ $page->write();
+ }
+
+ parent::onAfterDelete();
+ }
+
+ /**
+ * Returns TRUE if this object has a URLSegment value that does not conflict with any other objects. This methods
+ * checks for:
+ * - A page with the same URLSegment that has a conflict.
+ * - Conflicts with actions on the parent page.
+ * - A conflict caused by a root page having the same URLSegment as a class name.
+ *
+ * @return bool
+ */
+ public function validURLSegment() {
+ if(self::nested_urls() && $parent = $this->Parent()) {
+ if($controller = ModelAsController::controller_for($parent)) {
+ if($controller instanceof Controller && $controller->hasAction($this->URLSegment)) return false;
+ }
+ }
+
+ if(!self::nested_urls() || !$this->ParentID) {
+ if(class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'RequestHandler')) return false;
+ }
+
+ $IDFilter = ($this->ID) ? "AND \"SiteTree\".\"ID\" <> $this->ID" : null;
+ $parentFilter = null;
+
+ if(self::nested_urls()) {
+ if($this->ParentID) {
+ $parentFilter = " AND \"SiteTree\".\"ParentID\" = $this->ParentID";
+ } else {
+ $parentFilter = ' AND "SiteTree"."ParentID" = 0';
+ }
+ }
+
+ $existingPage = DataObject::get_one(
+ 'SiteTree',
+ "\"URLSegment\" = '$this->URLSegment' $IDFilter $parentFilter"
+ );
+ if ($existingPage) {
+ return false;
+ }
+
+ $values = $this->extend('augmentValidURLSegment');
+ if (count($values) && !min($values)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate a URL segment based on the title provided.
+ *
+ * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
+ * updateURLSegment(&$url, $title). $url will be passed by reference and should be modified.
+ * $title will contain the title that was originally used as the source of this generated URL.
+ * This lets decorators either start from scratch, or incrementally modify the generated URL.
+ *
+ * @param string $title Page title.
+ * @return string Generated url segment
+ */
+ function generateURLSegment($title){
+ $t = Convert::raw2url($title);
+
+ if(!$t || $t == '-' || $t == '-1') {
+ $t = "page-$this->ID";
+ }
+
+ // Hook for decorators
+ $this->extend('updateURLSegment', $t, $title);
+
+ return $t;
+ }
+
+ /**
+ * @return string
+ */
+ function getStageURLSegment() {
+ $stageRecord = Versioned::get_one_by_stage(
+ 'SiteTree',
+ 'Stage',
+ "\"SiteTree\".\"ID\" = $this->ID"
+ );
+ return ($stageRecord) ? $stageRecord->URLSegment : null;
+ }
+
+ /**
+ * @return string
+ */
+ function getLiveURLSegment() {
+ $liveRecord = Versioned::get_one_by_stage(
+ 'SiteTree',
+ 'Live',
+ "\"SiteTree\".\"ID\" = $this->ID"
+ );
+ return ($liveRecord) ? $liveRecord->URLSegment : null;
+ }
+