dataRecord. Any unrecognised method calls, for example, Title() * and Content(), will be passed along to the data record, * * Subclasses of ContentController are generally instantiated by ModelAsController; this will create * a controller based on the URLSegment action variable, by looking in the SiteTree table. * * @todo Can this be used for anything other than SiteTree controllers? */ class ContentController extends Controller { /** * @var SiteTree */ protected $dataRecord; private static $extensions = [ OldPageRedirector::class, ]; private static $allowed_actions = [ 'successfullyinstalled', 'deleteinstallfiles', // secured through custom code 'LoginForm', ]; private static $casting = [ 'SilverStripeNavigator' => 'HTMLFragment', ]; /** * The ContentController will take the URLSegment parameter from the URL and use that to look * up a SiteTree record. * * @param SiteTree $dataRecord */ public function __construct($dataRecord = null) { if (!$dataRecord) { $dataRecord = new SiteTree(); if ($this->hasMethod("Title")) { $dataRecord->Title = $this->Title(); } $dataRecord->URLSegment = static::class; $dataRecord->ID = -1; } $this->dataRecord = $dataRecord; parent::__construct(); $this->setFailover($this->dataRecord); } /** * Return the link to this controller, but force the expanded link to be returned so that form methods and * similar will function properly. * * @param string|null $action Action to link to. * @return string */ public function Link($action = null) { return $this->data()->Link(($action ? $action : true)); } //----------------------------------------------------------------------------------// // These flexible data methods remove the need for custom code to do simple stuff /** * Return the children of a given page. The parent reference can either be a page link or an ID. * * @param string|int $parentRef * @return SS_List */ public function ChildrenOf($parentRef) { $parent = SiteTree::get_by_link($parentRef); if (!$parent && is_numeric($parentRef)) { $parent = DataObject::get_by_id(SiteTree::class, $parentRef); } if ($parent) { return $parent->Children(); } return null; } /** * @param string $link * @return SiteTree */ public function Page($link) { return SiteTree::get_by_link($link); } protected function init() { parent::init(); // In the CMS Preview or draft contexts, we never want to cache page output. if ($this->getRequest()->getVar('CMSPreview') === '1' || $this->getRequest()->getVar('stage') === Versioned::DRAFT ) { HTTPCacheControlMiddleware::singleton()->disableCache(true); } // If we've accessed the homepage as /home/, then we should redirect to /. if ($this->dataRecord instanceof SiteTree && RootURLController::should_be_on_root($this->dataRecord) && (!isset($this->urlParams['Action']) || !$this->urlParams['Action']) && !$_POST && !$_FILES && !$this->redirectedTo() ) { $getVars = $_GET; unset($getVars['url']); if ($getVars) { $url = "?" . http_build_query($getVars ?? []); } else { $url = ""; } $this->redirect($url, 301); return; } if ($this->dataRecord) { $this->dataRecord->extend('contentcontrollerInit', $this); } else { SiteTree::singleton()->extend('contentcontrollerInit', $this); } if ($this->redirectedTo()) { return; } // Check page permissions /** @skipUpgrade */ if ($this->dataRecord && $this->URLSegment != 'Security' && !$this->dataRecord->canView()) { Security::permissionFailure($this); return; } } /** * This acts the same as {@link Controller::handleRequest()}, but if an action cannot be found this will attempt to * fall over to a child controller in order to provide functionality for nested URLs. * * @param HTTPRequest $request * @return HTTPResponse * @throws HTTPResponse_Exception */ public function handleRequest(HTTPRequest $request) { /** @var SiteTree $child */ $child = null; $action = $request->param('Action'); // If nested URLs are enabled, and there is no action handler for the current request then attempt to pass // control to a child controller. This allows for the creation of chains of controllers which correspond to a // nested URL. if ($action && SiteTree::config()->nested_urls && !$this->hasAction($action)) { // See ModelAdController->getNestedController() for similar logic if (class_exists('Translatable')) { Translatable::disable_locale_filter(); } $filter = URLSegmentFilter::create(); // look for a page with this URLSegment $child = SiteTree::get()->filter([ 'ParentID' => $this->ID, // url encode unless it's multibyte (already pre-encoded in the database) 'URLSegment' => $filter->getAllowMultibyte() ? $action : rawurlencode($action), ])->first(); if (class_exists('Translatable')) { Translatable::enable_locale_filter(); } } // we found a page with this URLSegment. if ($child) { $request->shiftAllParams(); $request->shift(); $response = ModelAsController::controller_for($child)->handleRequest($request); } else { // If a specific locale is requested, and it doesn't match the page found by URLSegment, // look for a translation and redirect (see #5001). Only happens on the last child in // a potentially nested URL chain. if (class_exists('Translatable')) { $locale = $request->getVar('locale'); if ($locale && i18n::getData()->validate($locale) && $this->dataRecord && $this->dataRecord->Locale != $locale ) { $translation = $this->dataRecord->getTranslation($locale); if ($translation) { $response = new HTTPResponse(); $response->redirect($translation->Link(), 301); throw new HTTPResponse_Exception($response); } } } Director::set_current_page($this->data()); try { $response = parent::handleRequest($request); Director::set_current_page(null); } catch (HTTPResponse_Exception $e) { $this->popCurrent(); Director::set_current_page(null); throw $e; } } return $response; } /** * Get the project name * * @return string */ public function project() { return ModuleManifest::config()->get('project'); } /** * Returns the associated database record */ public function data() { return $this->dataRecord; } /*--------------------------------------------------------------------------------*/ /** * Returns a fixed navigation menu of the given level. * @param int $level Menu level to return. * @return ArrayList */ public function getMenu($level = 1) { if ($level == 1) { $result = SiteTree::get()->filter([ "ShowInMenus" => 1, "ParentID" => 0, ]); } else { $parent = $this->data(); $stack = [$parent]; if ($parent) { while (($parent = $parent->Parent()) && $parent->exists()) { array_unshift($stack, $parent); } } if (isset($stack[$level - 2])) { $result = $stack[$level - 2]->Children(); } } $visible = []; // Remove all entries the can not be viewed by the current user // We might need to create a show in menu permission if (isset($result)) { foreach ($result as $page) { /** @var SiteTree $page */ if ($page->canView()) { $visible[] = $page; } } } return new ArrayList($visible); } public function Menu($level) { return $this->getMenu($level); } /** * Returns the default log-in form. * * @todo Check if here should be returned just the default log-in form or * all available log-in forms (also OpenID...) * @return \SilverStripe\Security\MemberAuthenticator\MemberLoginForm */ public function LoginForm() { return Injector::inst()->get(MemberAuthenticator::class)->getLoginHandler($this->Link())->loginForm(); } public function SilverStripeNavigator() { $member = Security::getCurrentUser(); $items = ''; $message = ''; if (Director::isDev() || Permission::check('CMS_ACCESS_CMSMain') || Permission::check('VIEW_DRAFT_CONTENT')) { if ($this->dataRecord) { Requirements::css('silverstripe/cms: client/dist/styles/SilverStripeNavigator.css'); Requirements::javascript('silverstripe/cms: client/dist/js/SilverStripeNavigator.js'); $return = $nav = SilverStripeNavigator::get_for_record($this->dataRecord); $items = $return['items']; $message = $return['message']; } if ($member) { $firstname = Convert::raw2xml($member->FirstName); $surname = Convert::raw2xml($member->Surname); $logInMessage = _t(__CLASS__ . '.LOGGEDINAS', 'Logged in as') . " {$firstname} {$surname} - " . _t(__CLASS__ . '.LOGOUT', 'Log out') . ""; } else { $logInMessage = sprintf( '%s - %s', _t(__CLASS__ . '.NOTLOGGEDIN', 'Not logged in'), Security::config()->login_url, _t(__CLASS__ . '.LOGIN', 'Login') . "" ); } $viewPageIn = _t(__CLASS__ . '.VIEWPAGEIN', 'View Page in:'); return <<
$logInMessage
$viewPageIn $items
$message HTML; // On live sites we should still see the archived message } else { if ($date = Versioned::current_archived_date()) { Requirements::css('silverstripe/cms: client/dist/styles/SilverStripeNavigator.css'); /** @var DBDatetime $dateObj */ $dateObj = DBField::create_field('Datetime', $date); // $dateObj->setVal($date); return "
" . _t(__CLASS__ . '.ARCHIVEDSITEFROM', 'Archived site from') . "
" . $dateObj->Nice() . "
"; } } return null; } public function SiteConfig() { if (method_exists($this->dataRecord, 'getSiteConfig')) { return $this->dataRecord->getSiteConfig(); } else { return SiteConfig::current_site_config(); } } /** * Returns an RFC1766 compliant locale string, e.g. 'fr-CA'. * Inspects the associated {@link dataRecord} for a {@link SiteTree->Locale} value if present, * and falls back to {@link Translatable::get_current_locale()} or {@link i18n::default_locale()}, * depending if Translatable is enabled. * * Suitable for insertion into lang= and xml:lang= * attributes in HTML or XHTML output. * * @return string */ public function ContentLocale() { if ($this->dataRecord && $this->dataRecord->hasExtension('Translatable')) { $locale = $this->dataRecord->Locale; } elseif (class_exists('Translatable') && SiteTree::has_extension('Translatable')) { $locale = Translatable::get_current_locale(); } else { $locale = i18n::get_locale(); } return i18n::convert_rfc1766($locale); } /** * Return an SSViewer object to render the template for the current page. * * @param $action string * * @return SSViewer */ public function getViewer($action) { // Manually set templates should be dealt with by Controller::getViewer() if (!empty($this->templates[$action]) || !empty($this->templates['index']) || $this->template ) { return parent::getViewer($action); } // Prepare action for template search $action = $action === 'index' ? '' : '_' . $action; $templatesFound = []; // Find templates for the record + action together - e.g. Page_action.ss if ($this->dataRecord instanceof SiteTree) { $templatesFound[] = $this->dataRecord->getViewerTemplates($action); } // Find templates for the controller + action together - e.g. PageController_action.ss $templatesFound[] = SSViewer::get_templates_by_class(static::class, $action, Controller::class); // Find templates for the record without an action - e.g. Page.ss if ($this->dataRecord instanceof SiteTree) { $templatesFound[] = $this->dataRecord->getViewerTemplates(); } // Find the templates for the controller without an action - e.g. PageController.ss $templatesFound[] = SSViewer::get_templates_by_class(static::class, "", Controller::class); $templates = array_merge(...$templatesFound); return SSViewer::create($templates); } /** * This action is called by the installation system */ public function successfullyinstalled() { // Return 410 Gone if this site is not actually a fresh installation if (!file_exists(PUBLIC_PATH . '/install.php')) { $this->httpError(410); } // TODO Allow this to work when allow_url_fopen=0 if (isset($_SESSION['StatsID']) && $_SESSION['StatsID']) { $url = 'http://ss2stat.silverstripe.com/Installation/installed?ID=' . $_SESSION['StatsID']; @file_get_contents($url ?? ''); } global $project; $data = new ArrayData([ 'Project' => Convert::raw2xml($project), 'Username' => Convert::raw2xml($this->getRequest()->getSession()->get('username')), 'Password' => Convert::raw2xml($this->getRequest()->getSession()->get('password')), ]); return [ "Title" => _t(__CLASS__ . ".INSTALL_SUCCESS", "Installation Successful!"), "Content" => $data->renderWith([ 'type' => 'Includes', 'Install_successfullyinstalled', ]), ]; } public function deleteinstallfiles() { if (!Permission::check("ADMIN")) { return Security::permissionFailure($this); } $title = new DBVarchar("Title"); $content = new DBHTMLText('Content'); // As of SS4, index.php is required and should never be deleted. $installfiles = [ 'install.php', 'install-frameworkmissing.html', 'index.html' ]; $unsuccessful = new ArrayList(); foreach ($installfiles as $installfile) { $installfilepath = PUBLIC_PATH . '/' . $installfile; if (file_exists($installfilepath ?? '')) { @unlink($installfilepath ?? ''); } if (file_exists($installfilepath ?? '')) { $unsuccessful->push(new ArrayData(['File' => $installfile])); } } $data = new ArrayData([ 'Username' => Convert::raw2xml($this->getRequest()->getSession()->get('username')), 'Password' => Convert::raw2xml($this->getRequest()->getSession()->get('password')), 'UnsuccessfulFiles' => $unsuccessful, ]); $content->setValue($data->renderWith([ 'type' => 'Includes', 'Install_deleteinstallfiles', ])); return [ "Title" => $title, "Content" => $content, ]; } }