Version: current

      1 <?php
      2 /**
      3  * The most common kind of controller; effectively a controller linked to a {@link DataObject}.
      4  *
      5  * ContentControllers are most useful in the content-focused areas of a site.  This is generally
      6  * the bulk of a site; however, they may be less appropriate in, for example, the user management
      7  * section of an application.
      8  *
      9  * On its own, content controller does very little.  Its constructor is passed a {@link DataObject}
     10  * which is stored in $this->dataRecord.  Any unrecognised method calls, for example, Title()
     11  * and Content(), will be passed along to the data record,
     12  *
     13  * Subclasses of ContentController are generally instantiated by ModelAsController; this will create
     14  * a controller based on the URLSegment action variable, by looking in the SiteTree table.
     15  *
     16  * @todo Can this be used for anything other than SiteTree controllers?
     17  *
     18  * @package sapphire
     19  * @subpackage control
     20  */
     21 class ContentController extends Controller {
     22
     23     protected $dataRecord;
     24
     25     static $url_handlers = array(
     26         'widget/$ID/$Action' => 'handleWidget'
     27     );
     28
     29     public static $allowed_actions = array (
     30         'PageComments',
     31         'successfullyinstalled',
     32         'deleteinstallfiles' // secured through custom code
     33     );
     34
     35     /**
     36      * The ContentController will take the URLSegment parameter from the URL and use that to look
     37      * up a SiteTree record.
     38      */
     39     public function __construct($dataRecord = null) {
     40         if(!$dataRecord) {
     41             $dataRecord = new Page();
     42             if($this->hasMethod("Title")) $dataRecord->Title = $this->Title();
     43             $dataRecord->URLSegment = get_class($this);
     44             $dataRecord->ID = -1;
     45         }
     46
     47         $this->dataRecord = $dataRecord;
     48         $this->failover = $this->dataRecord;
     49         parent::__construct();
     50     }
     51
     52     /**
     53      * Return the link to this controller, but force the expanded link to be returned so that form methods and
     54      * similar will function properly.
     55      *
     56      * @return string
     57      */
     58     public function Link($action = null) {
     59         return $this->data()->Link(($action ? $action : true));
     60     }
     61
     62     //----------------------------------------------------------------------------------//
     63     // These flexible data methods remove the need for custom code to do simple stuff
     64
     65     /**
     66      * Return the children of a given page. The parent reference can either be a page link or an ID.
     67      *
     68      * @param string|int $parentRef
     69      * @return DataObjectSet
     70      */
     71     public function ChildrenOf($parentRef) {
     72         $parent = SiteTree::get_by_link($parentRef);
     73
     74         if(!$parent && is_numeric($parentRef)) {
     75             $parent = DataObject::get_by_id('SiteTree', Convert::raw2sql($parentRef));
     76         }
     77
     78         if($parent) return $parent->Children();
     79     }
     80
     81     /**
     82      * @return DataObjectSet
     83      */
     84     public function Page($link) {
     85         return SiteTree::get_by_link($link);
     86     }
     87
     88     public function init() {
     89         parent::init();
     90
     91         // If we've accessed the homepage as /home/, then we should redirect to /.
     92         if($this->dataRecord && $this->dataRecord instanceof SiteTree
     93                 && RootURLController::should_be_on_root($this->dataRecord) && (!isset($this->urlParams['Action']) || !$this->urlParams['Action'] )
     94                 && !$_POST && !$_FILES && !Director::redirected_to() ) {
     95             $getVars = $_GET;
     96             unset($getVars['url']);
     97             if($getVars) $url = "?" . http_build_query($getVars);
     98             else $url = "";
     99             Director::redirect($url, 301);
    100             return;
    101         }
    102
    103         // Use theme from the site config
    104         if(($config = SiteConfig::current_site_config()) && $config->Theme) {
    105             SSViewer::set_theme($config->Theme);
    106         }
    107
    108         if($this->dataRecord) $this->dataRecord->extend('contentcontrollerInit', $this);
    109         else singleton('SiteTree')->extend('contentcontrollerInit', $this);
    110
    111         if(Director::redirected_to()) return;
    112
    113         // Check page permissions
    114         if($this->dataRecord && $this->URLSegment != 'Security' && !$this->dataRecord->canView()) {
    115             return Security::permissionFailure($this);
    116         }
    117
    118         // Draft/Archive security check - only CMS users should be able to look at stage/archived content
    119         if($this->URLSegment != 'Security' && !Session::get('unsecuredDraftSite') && (Versioned::current_archived_date() || (Versioned::current_stage() && Versioned::current_stage() != 'Live'))) {
    120             if(!Permission::check('CMS_ACCESS_CMSMain') && !Permission::check('VIEW_DRAFT_CONTENT')) {
    121                 $link = $this->Link();
    122                 $message = _t("ContentController.DRAFT_SITE_ACCESS_RESTRICTION", 'You must log in with your CMS password in order to view the draft or archived content.  <a href="%s">Click here to go back to the published site.</a>');
    123                 Session::clear('currentStage');
    124                 Session::clear('archiveDate');
    125                 Session::clear('readingMode');
    126
    127                 return Security::permissionFailure($this, sprintf($message, "$link?stage=Live"));
    128             }
    129         }
    130
    131
    132     }
    133
    134     /**
    135      * This acts the same as {@link Controller::handleRequest()}, but if an action cannot be found this will attempt to
    136      * fall over to a child controller in order to provide functionality for nested URLs.
    137      *
    138      * @return SS_HTTPResponse
    139      */
    140     public function handleRequest(SS_HTTPRequest $request) {
    141         $child  = null;
    142         $action = $request->param('Action');
    143
    144         // If nested URLs are enabled, and there is no action handler for the current request then attempt to pass
    145         // control to a child controller. This allows for the creation of chains of controllers which correspond to a
    146         // nested URL.
    147         if($action && SiteTree::nested_urls() && !$this->hasAction($action)) {
    148             // See ModelAdController->getNestedController() for similar logic
    149             Translatable::disable_locale_filter();
    150             // look for a page with this URLSegment
    151             $child = DataObject::get_one('SiteTree', sprintf (
    152                 "\"ParentID\" = %s AND \"URLSegment\" = '%s'", $this->ID, Convert::raw2sql($action)
    153             ));
    154             Translatable::enable_locale_filter();
    155
    156             // if we can't find a page with this URLSegment try to find one that used to have
    157             // that URLSegment but changed. See ModelAsController->getNestedController() for similiar logic.
    158             if(!$child){
    159                 $child = ModelAsController::find_old_page($action,$this->ID);
    160                 if($child){
    161                     $response = new SS_HTTPResponse();
    162                     $params = $request->getVars();
    163                     if(isset($params['url'])) unset($params['url']);
    164                     $response->redirect(
    165                         Controller::join_links(
    166                             $child->Link(
    167                                 Controller::join_links(
    168                                     $request->param('ID'), // 'ID' is the new 'URLSegment', everything shifts up one position
    169                                     $request->param('OtherID')
    170                                 )
    171                             ),
    172                             // Needs to be in separate join links to avoid urlencoding
    173                             ($params) ? '?' . http_build_query($params) : null
    174                         ),
    175                         301
    176                     );
    177                     return $response;
    178                 }
    179             }
    180         }
    181
    182         // we found a page with this URLSegment.
    183         if($child) {
    184             $request->shiftAllParams();
    185             $request->shift();
    186
    187             $response = ModelAsController::controller_for($child)->handleRequest($request);
    188         } else {
    189             // If a specific locale is requested, and it doesn't match the page found by URLSegment,
    190             // look for a translation and redirect (see #5001). Only happens on the last child in
    191             // a potentially nested URL chain.
    192             if($request->getVar('locale') && $this->dataRecord && $this->dataRecord->Locale != $request->getVar('locale')) {
    193                 $translation = $this->dataRecord->getTranslation($request->getVar('locale'));
    194                 if($translation) {
    195                     $response = new SS_HTTPResponse();
    196                     $response->redirect($translation->Link(), 301);
    197                     throw new SS_HTTPResponse_Exception($response);
    198                 }
    199             }
    200
    201             Director::set_current_page($this->data());
    202             $response = parent::handleRequest($request);
    203             Director::set_current_page(null);
    204         }
    205
    206         return $response;
    207     }
    208
    209     /**
    210      * @uses ErrorPage::response_for()
    211      */
    212     public function httpError($code, $message = null) {
    213         if($this->request->isMedia() || !$response = ErrorPage::response_for($code)) {
    214             parent::httpError($code, $message);
    215         } else {
    216             throw new SS_HTTPResponse_Exception($response);
    217         }
    218     }
    219
    220     /**
    221      * Handles widgets attached to a page through one or more {@link WidgetArea} elements.
    222      * Iterated through each $has_one relation with a {@link WidgetArea}
    223      * and looks for connected widgets by their database identifier.
    224      * Assumes URLs in the following format: <URLSegment>/widget/<Widget-ID>.
    225      *
    226      * @return RequestHandler
    227      */
    228     function handleWidget() {
    229         $SQL_id = $this->request->param('ID');
    230         if(!$SQL_id) return false;
    231
    232         // find WidgetArea relations
    233         $widgetAreaRelations = array();
    234         $hasOnes = $this->dataRecord->has_one();
    235         if(!$hasOnes) return false;
    236         foreach($hasOnes as $hasOneName => $hasOneClass) {
    237             if($hasOneClass == 'WidgetArea' || ClassInfo::is_subclass_of($hasOneClass, 'WidgetArea')) {
    238                 $widgetAreaRelations[] = $hasOneName;
    239             }
    240         }
    241
    242         // find widget
    243         $widget = null;
    244         foreach($widgetAreaRelations as $widgetAreaRelation) {
    245             if($widget) break;
    246             $widget = $this->dataRecord->$widgetAreaRelation()->Widgets(
    247                 sprintf('"Widget"."ID" = %d', $SQL_id)
    248             )->First();
    249         }
    250         if(!$widget) user_error('No widget found', E_USER_ERROR);
    251
    252         // find controller
    253         $controllerClass = '';
    254         foreach(array_reverse(ClassInfo::ancestry($widget->class)) as $widgetClass) {
    255             $controllerClass = "{$widgetClass}_Controller";
    256             if(class_exists($controllerClass)) break;
    257         }
    258         if(!$controllerClass) user_error(
    259             sprintf('No controller available for %s', $widget->class),
    260             E_USER_ERROR
    261         );
    262
    263         return new $controllerClass($widget);
    264     }
    265
    266     /**
    267      * Get the project name
    268      *
    269      * @return string
    270      */
    271     function project() {
    272         global $project;
    273         return $project;
    274     }
    275
    276     /**
    277      * Returns the associated database record
    278      */
    279     public function data() {
    280         return $this->dataRecord;
    281     }
    282
    283     /*--------------------------------------------------------------------------------*/
    284
    285     /**
    286      * Returns a fixed navigation menu of the given level.
    287      * @return DataObjectSet
    288      */
    289     public function getMenu($level = 1) {
    290         if($level == 1) {
    291             $result = DataObject::get("SiteTree", "\"ShowInMenus\" = 1 AND \"ParentID\" = 0");
    292
    293         } else {
    294             $parent = $this->data();
    295             $stack = array($parent);
    296
    297             if($parent) {
    298                 while($parent = $parent->Parent) {
    299                     array_unshift($stack, $parent);
    300                 }
    301             }
    302
    303             if(isset($stack[$level-2])) $result = $stack[$level-2]->Children();
    304         }
    305
    306         $visible = array();
    307
    308         // Remove all entries the can not be viewed by the current user
    309         // We might need to create a show in menu permission
    310         if(isset($result)) {
    311             foreach($result as $page) {
    312                 if($page->canView()) {
    313                     $visible[] = $page;
    314                 }
    315             }
    316         }
    317
    318         return new DataObjectSet($visible);
    319     }
    320
    321     public function Menu($level) {
    322         return $this->getMenu($level);
    323     }
    324
    325     /**
    326      * Returns the default log-in form.
    327      *
    328      * @todo Check if here should be returned just the default log-in form or
    329      *       all available log-in forms (also OpenID...)
    330      */
    331     public function LoginForm() {
    332         return MemberAuthenticator::get_login_form($this);
    333     }
    334
    335     public function SilverStripeNavigator() {
    336         $member = Member::currentUser();
    337         $items = '';
    338         $message = '';
    339
    340         if(Director::isDev() || Permission::check('CMS_ACCESS_CMSMain') || Permission::check('VIEW_DRAFT_CONTENT')) {
    341             if($this->dataRecord) {
    342                 Requirements::css(SAPPHIRE_DIR . '/css/SilverStripeNavigator.css');
    343                 // TODO Using jQuery for this is absolute overkill, and might cause conflicts
    344                 // with other libraries.
    345 //              Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js');
    346                 Requirements::javascript(SAPPHIRE_DIR . '/javascript/SilverStripeNavigator.js');
    347
    348                 $return = $nav = SilverStripeNavigator::get_for_record($this->dataRecord);
    349                 $items = $return['items'];
    350                 $message = $return['message'];
    351             }
    352
    353             if($member) {
    354                 $firstname = Convert::raw2xml($member->FirstName);
    355                 $surname = Convert::raw2xml($member->Surname);
    356                 $logInMessage = _t('ContentController.LOGGEDINAS', 'Logged in as') ." {$firstname} {$surname} - <a href=\"Security/logout\">". _t('ContentController.LOGOUT', 'Log out'). "</a>";
    357             } else {
    358                 $logInMessage = _t('ContentController.NOTLOGGEDIN', 'Not logged in') ." - <a href=\"Security/login\">". _t('ContentController.LOGIN', 'Login') ."</a>";
    359             }
    360             $viewPageIn = _t('ContentController.VIEWPAGEIN', 'View Page in:');
    361
    362             Requirements::customScript("window.name = windowName('site');");
    363
    364             return <<<HTML
    365                 <div id="SilverStripeNavigator">
    366                     <div class="holder">
    367                     <div id="logInStatus">
    368                         $logInMessage
    369                     </div>
    370
    371                     <div id="switchView" class="bottomTabs">
    372                         <div class="blank">$viewPageIn </div>
    373                         $items
    374                     </div>
    375                     </div>
    376                 </div>
    377                     $message
    378 HTML;
    379
    380         // On live sites we should still see the archived message
    381         } else {
    382             if($date = Versioned::current_archived_date()) {
    383                 Requirements::css(SAPPHIRE_DIR . '/css/SilverStripeNavigator.css');
    384                 $dateObj = Object::create('Datetime', $date, null);
    385                 // $dateObj->setVal($date);
    386                 return "<div id=\"SilverStripeNavigatorMessage\">". _t('ContentController.ARCHIVEDSITEFROM') ."<br>" . $dateObj->Nice() . "</div>";
    387             }
    388         }
    389     }
    390
    391     /**
    392      * Returns a page comment system
    393      */
    394     function PageComments() {
    395         $hasComments = DB::query("SELECT COUNT(*) FROM \"PageComment\" WHERE \"PageComment\".\"ParentID\" = '". Convert::raw2sql($this->ID) . "'")->value();
    396         if(($this->data() && $this->data()->ProvideComments) || ($hasComments > 0 && PageCommentInterface::$show_comments_when_disabled)) {
    397             return new PageCommentInterface($this, 'PageComments', $this->data());
    398         } else {
    399             if(isset($_REQUEST['executeForm']) && $_REQUEST['executeForm'] == 'PageComments.PostCommentForm') {
    400                 echo "Comments have been disabled for this page";
    401                 die();
    402             }
    403         }
    404     }
    405
    406     function SiteConfig() {
    407         return SiteConfig::current_site_config();
    408     }
    409
    410     /**
    411      * Returns the xml:lang and lang attributes.
    412      *
    413      * @deprecated 2.5 Use ContentLocale() instead and write attribute names suitable to XHTML/HTML
    414      * templates directly in the template.
    415      */
    416     function LangAttributes() {
    417         $locale = $this->ContentLocale();
    418         return "xml:lang=\"$locale\" lang=\"$locale\"";
    419     }
    420
    421     /**
    422      * Returns an RFC1766 compliant locale string, e.g. 'fr-CA'.
    423      * Inspects the associated {@link dataRecord} for a {@link SiteTree->Locale} value if present,
    424      * and falls back to {@link Translatable::get_current_locale()} or {@link i18n::default_locale()},
    425      * depending if Translatable is enabled.
    426      *
    427      * Suitable for insertion into lang= and xml:lang=
    428      * attributes in HTML or XHTML output.
    429      *
    430      * @return string
    431      */
    432     function ContentLocale() {
    433         if($this->dataRecord && $this->dataRecord->hasExtension('Translatable')) {
    434             $locale = $this->dataRecord->Locale;
    435         } elseif(Object::has_extension('SiteTree', 'Translatable')) {
    436             $locale = Translatable::get_current_locale();
    437         } else {
    438             $locale = i18n::get_locale();
    439         }
    440
    441         return i18n::convert_rfc1766($locale);
    442     }
    443
    444     /**
    445      * This action is called by the installation system
    446      */
    447     function successfullyinstalled() {
    448         // The manifest should be built by now, so it's safe to publish the 404 page
    449         $fourohfour = Versioned::get_one_by_stage('ErrorPage', 'Stage', '"ErrorCode" = 404');
    450         if($fourohfour) {
    451             $fourohfour->Status = "Published";
    452             $fourohfour->write();
    453             $fourohfour->publish("Stage", "Live");
    454         }
    455
    456         // TODO Allow this to work when allow_url_fopen=0
    457         if(isset($_SESSION['StatsID']) && $_SESSION['StatsID']) {
    458             $url = 'http://ss2stat.silverstripe.com/Installation/installed?ID=' . $_SESSION['StatsID'];
    459             @file_get_contents($url);
    460         }
    461
    462         $title = new Varchar("Title");
    463         $content = new HTMLText("Content");
    464         $username = Session::get('username');
    465         $password = Session::get('password');
    466         $title->setValue("Installation Successful");
    467         global $project;
    468         $tutorialOnly = ($project == 'tutorial') ? "<p>This website is a simplistic version of a SilverStripe 2 site. To extend this, please take a look at <a href=\"http://doc.silverstripe.org/doku.php?id=tutorials\">our new tutorials</a>.</p>" : '';
    469         $content->setValue(<<<HTML
    470             <p style="margin: 1em 0"><b>Congratulations, SilverStripe has been successfully installed.</b></p>
    471
    472             $tutorialOnly
    473             <p>You can start editing your site's content by opening <a href="admin/">the CMS</a>. <br />
    474                 &nbsp; &nbsp; Email: $username<br />
    475                 &nbsp; &nbsp; Password: $password<br />
    476             </p>
    477             <div style="background:#ddd; border:1px solid #ccc; padding:5px; margin:5px;"><img src="cms/images/dialogs/alert.gif" style="border: none; margin-right: 10px; float: left;" /><p style="color:red;">For security reasons you should now delete the install files, unless you are planning to reinstall later (<em>requires admin login, see above</em>). The web server also now only needs write access to the "assets" folder, you can remove write access from all other folders. <a href="home/deleteinstallfiles" style="text-align: center;">Click here to delete the install files.</a></p></div>
    478 HTML
    479 );
    480
    481         return array(
    482             "Title" => $title,
    483             "Content" => $content,
    484         );
    485     }
    486
    487     function deleteinstallfiles() {
    488         if(!Permission::check("ADMIN")) return Security::permissionFailure($this);
    489
    490         $title = new Varchar("Title");
    491         $content = new HTMLText("Content");
    492         $tempcontent = '';
    493         $username = Session::get('username');
    494         $password = Session::get('password');
    495
    496         // We can't delete index.php as it might be necessary for URL routing without mod_rewrite.
    497         // There's no safe way to detect usage of mod_rewrite across webservers,
    498         // so we have to assume the file is required.
    499         $installfiles = array(
    500             'install.php',
    501             'config-form.css',
    502             'config-form.html',
    503             'index.html'
    504         );
    505
    506         foreach($installfiles as $installfile) {
    507             if(file_exists(BASE_PATH . '/' . $installfile)) {
    508                 @unlink(BASE_PATH . '/' . $installfile);
    509             }
    510
    511             if(file_exists(BASE_PATH . '/' . $installfile)) {
    512                 $unsuccessful[] = $installfile;
    513             }
    514         }
    515
    516         if(isset($unsuccessful)) {
    517             $title->setValue("Unable to delete installation files");
    518             $tempcontent = "<p style=\"margin: 1em 0\">Unable to delete installation files. Please delete the files below manually:</p><ul>";
    519             foreach($unsuccessful as $unsuccessfulFile) {
    520                 $tempcontent .= "<li>$unsuccessfulFile</li>";
    521             }
    522             $tempcontent .= "</ul>";
    523         } else {
    524             $title->setValue("Deleted installation files");
    525             $tempcontent = <<<HTML
    526 <p style="margin: 1em 0">Installation files have been successfully deleted.</p>
    527 HTML
    528             ;
    529         }
    530
    531         $tempcontent .= <<<HTML
    532             <p style="margin: 1em 0">You can start editing your site's content by opening <a href="admin/">the CMS</a>. <br />
    533                 &nbsp; &nbsp; Email: $username<br />
    534                 &nbsp; &nbsp; Password: $password<br />
    535             </p>
    536 HTML
    537         ;
    538         $content->setValue($tempcontent);
    539
    540         return array(
    541             "Title" => $title,
    542             "Content" => $content,
    543         );
    544     }
    545 }
    546
    547 ?>
    548

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -