Solstice::Controller::Application::Main - Controls the lifcycle of Solstice requests.


Solstice documentation Contained in the Solstice distribution.

Index


Code Index:

NAME

Top

Solstice::Controller::Application::Main - Controls the lifcycle of Solstice requests.

SYNOPSIS

Top

DESCRIPTION

Top

Superclass

Solstice::Controller::Application

Export

No symbols exported.

Methods

runApp() =cut
_passiveCreateSession($screen)

Adds a header to the current request that will set a session cookie, but does not force a browser bounce - The session cookie will be send on subsequent requests if it's accepted.

_forceCreateSession($screen)

Write a cookie to the browser, and do a meta refresh back to the page. Can't be a 300 redirect, it needs to be a full round trip to the client for the cookie to be processed properly.

_checkHasJavascript()

Round trip to the client, to verify if javascript is enabled or not.

_isBrowserStable()

Check browser/OS combination

_getAccessorDefinition() =cut

Private Methods

Modules Used

Solstice::Controller::Application, Solstice::Session, Solstice::View::HelpPane (Solstice::View::HelpPane), Solstice::View::InvalidPreConditions, Solstice::View::Navigation (Solstice::View::Navigation), Solstice::CGI,

AUTHOR

Top

Catalyst Group, <catalyst@u.washington.edu>

VERSION

Top

$Revision: 3375 $

COPYRIGHT

Top


Solstice documentation Contained in the Solstice distribution.
package Solstice::Controller::Application::Main;

# $Id: Main.pm 3375 2006-05-12 23:03:35Z jdr99 $

use 5.006_000;
use strict;
use warnings;

use base qw(Solstice::Controller::Application Solstice::Model);

use Solstice::Session;
use Solstice::Server;
use Solstice::JavaScriptService;
use Solstice::NamespaceService;
use Solstice::ProcessService;
use Solstice::State::Tracker;
use Solstice::CGI qw(param header);
use Solstice::StringLibrary qw(unrender);

use Solstice::View::Application;
use Solstice::View::Boilerplate;
use Solstice::View::Developer::Toolbar;
use Solstice::View::InvalidPreConditions;
use Solstice::View::NoCookies;
use Solstice::View::DeniedBrowser;
use Solstice::View::DeniedUser;
#use Solstice::View::PopIn;

use Solstice::Controller::Application::Auth;

use HTTP::BrowserDetect;

use constant TRUE  => 1;
use constant FALSE => 0;

our ($VERSION) = ('$Revision: 3375 $' =~ /^\$Revision:\s*([\d.]*)/);

sub new {
    my $obj = shift;
    my $self = $obj->SUPER::new(@_);

    # Set some defaults...
    $self->setViewTopNav(TRUE);

    return $self;
}

sub runApp {
    my $self = shift;
    my $screen = shift;

    $self->_setScreen($screen);

    # session, user, js and so on
    # Could enforce url level auth
    return unless $self->_denyUnstableBrowser();
    return unless $self->_initializeSession();

    return if $self->requiresURLAuth();

    return unless $self->_isLoginValid();
    return unless $self->_checkJavascript();

    return unless $self->_initializeState();

    return unless $self->loadApplicationForNamespace();
    return unless $self->_handleButtonAction();
    $self->_setPreClickState(Solstice::Session->new()->getStateTracker()->getState());

    return unless $self->_handlePreClick();

    return unless $self->_paintPreClickView();
    $self->_getPreClickController()->finalize();

    return TRUE;
}

sub requiresURLAuth {
    my $self = shift;

    my $config   = $self->getConfigService();

    my $auth_controller = Solstice::Controller::Application::Auth->new();
    if ($self->_getRequiresAuth() ){
        if (!$auth_controller->hasAuthenticated()) {
            return $auth_controller->requiresUserLogin();
        }
        elsif (!$auth_controller->hasUserAuth()) {
            $self->_paintView(Solstice::View::DeniedUser->new());
            return TRUE;
        }
    }
    return FALSE;
}

sub _paintPreClickView {
    my $self = shift;

    my $preclick_controller = $self->_getPreClickController();
    my $view = $preclick_controller->getView();
    $view->setError($preclick_controller->getController()->getError());

    return $self->_paintView($view);
}



sub _paintView {
    my $self = shift;
    my $view = shift;

    my $application = $self->_getApplication();
    my $server  = Solstice::Server->new();

    my $screen = $self->_getScreen();

    $view->sendHeaders();

    if($view->isDownloadView()){
        $server->printHeaders();
        $view->printData();

    }else{ ### The traditional view
        my $config = $self->getConfigService();

        ### Init the Application View
        my $app_view = Solstice::View::Application->new($application, 1);
        $app_view->setEscapeFrames($self->_getEscapeFrames());
        $app_view->setDocumentTitle($self->_getDocumentTitle());
        $app_view->setSessionAppKey('__'.Solstice::NamespaceService->new()->getNamespace().'__');
        #$app_view->addChildView('solstice_popin_container', Solstice::View::PopIn->new());
        
        ### Now create the boilerplate View
        my $boiler_class;
        if ($self->_getBoilerplateView()) {
            $boiler_class = $self->_getBoilerplateView();
            $self->loadModule($boiler_class);

        }elsif ($config->getBoilerplateView()) {
            $boiler_class = $config->getBoilerplateView();
            $self->loadModule($boiler_class);

        } else {
            $boiler_class = 'Solstice::View::Boilerplate';
        }

        my $boiler_view = $boiler_class->new($application);

        $boiler_view->setViewTopNav($self->_getViewTopNav()) if $boiler_view->can('setViewTopNav');

        $app_view->addChildView('boilerplate', $boiler_view);
        $boiler_view->addChildView('content', $view);
        
        $app_view->addChildView('developer_toolbar', Solstice::View::Developer::Toolbar->new($application));
        
        # Paint the App View - the content is returned by the screen 
        # scalar ref
        $app_view->paint($screen);
    }
    return  TRUE;
}

sub _setDevelopmentOptions {
    my $self = shift;

    my $application = $self->_getApplication();
    my $preference_service = $self->getPreferenceService($application->getNamespace());
    $Solstice::View::use_wireframes = $preference_service->getPreference('display_wire_frames');
    $Solstice::LangService::display_tags = $preference_service->getPreference('display_lang_tags');
    
    return TRUE;
}

sub _handlePreClick {
    my $self = shift;

    my $application = $self->_getApplication();
    my $state_tracker = Solstice::Session->new()->getStateTracker();
    my $selected_button = $self->getButtonService()->getSelectedButton();

    #most of the time a preclick will have been set, if not, pull from state tracker the
    #default for this url or pageflow
    my $preclick_controller = $self->_getPreClickController();
    unless( $preclick_controller ){
        $preclick_controller = $state_tracker->getController($application);
    }

    if (defined $selected_button) {
        $preclick_controller = $self->_preClickFallbacks($preclick_controller);
    }

    if ($preclick_controller->getRequiresAuth() &&
        $self->_requiresAuthentication()) {
            return FALSE;
    }

    unless ($preclick_controller->getController()){
        my $ref = ref $preclick_controller ;
        die("$ref did not set a controller in it's constructor.  Solstice cannot continue\n");
    }
    $preclick_controller->initialize();
    $self->_setPreClickController($preclick_controller);
    return TRUE;
}

sub _preClickFallbacks {
    my $self = shift;
    my $preclick_controller = shift;

    my $application = $self->_getApplication();
    my $state_tracker = Solstice::Session->new()->getStateTracker();
    
    #If we do not have a transition, eg for a bad back button or default state,
    #we cannot check for freshen
    my $transition = $self->_getTransition();

    if ( $transition && $transition->requiresFresh() ) {
        if (!$preclick_controller->freshen()) {
            return $self->_preClickFallbacks($state_tracker->failFreshen());
        }
    }
    if (!$preclick_controller->validPreConditions()) {
        return $self->_preClickFallbacks($state_tracker->failValidPreConditions($application));
    }

    return $preclick_controller;
}

sub _handleButtonAction {
    my $self = shift;

    my $state_tracker = Solstice::Session->new()->getStateTracker();
    # Get the user action and destination application
    my $selected_button = $self->getButtonService()->getSelectedButton();

    if ($self->getConfigService()->getDevelopmentMode()) {
        $self->_setDevelopmentOptions();
    }

    if(!defined $selected_button){
        return TRUE;
    }

    $self->_setPostClickState($state_tracker->getState()); # for logging only

    my $action = $selected_button->getAction();

    #if the button clicked was a navigation-only link (a "StaticButton")
    #we do not want to act as if any action was selected
    if($action eq '__nav_only__'){
        return TRUE;
    }

    $self->_setAction($action);
    my $session = Solstice::Session->new();

    # __bad_back_button__ is the action on the ajax submit that blocks the back button, 
    # getIllegalSession() traps the person who manages to click a button on a bad screen
    if ($action eq '__bad_back_button__' || $session->getIllegalSession()) {
        return $self->_handleBadBackButton();
    }
    if ($action eq "__set_preference__") {
        return $self->_handlePreferenceButton();
    }
    if ($action eq "__requires_auth__") {
        return $self->_handleLoginButton();
    }

    # Clear the session history if the user should not be able to use
    # the back button after this transition.
    if ( !$state_tracker->canUseBackButton($action) || $self->_getDisableBackButton() ) {
        #Clear the session history so this can't be backed over.
        $session->deleteSubsessionChain();
    }

    $self->_findTransition($action);

    return FALSE if ($self->_requiresPostClickAuthentication());

    # Returns false if validation fails - stay on the current state.
    if ($self->_runPostClickLifeCycle()) {

        if ($self->_getIsGlobalTransition()) {
            $self->_runGlobalTransition();
        }else{
            #run standard transition
            $state_tracker->transition($action);
        }

        #reset application incase we changed pageflows
        $state_tracker->getState() =~ /^(.*?):/;
        my $namespace = $1;
        $self->_setNamespace($namespace);
        $self->loadApplicationForNamespace();
    }

    return TRUE;
}

sub _requiresPostClickAuthentication {
    my $self = shift;
    my $transition      = $self->_getTransition();
    my $state_tracker   = Solstice::Session->new()->getStateTracker();
    my $application     = $self->_getApplication();
    my $postclick_controller = $state_tracker->getController($application);
    $self->setPostClickController($postclick_controller);

    if ($postclick_controller->getRequiresAuth() &&
        $self->_requiresAuthentication()) {
            return TRUE;
    }
    return FALSE;
}

sub _requiresAuthentication {
    my $self = shift;

    my $auth_controller = Solstice::Controller::Application::Auth->new();

    if (!$auth_controller->hasUserAuth() ) {
        $auth_controller->getAuthentication();
        return TRUE;
    }
    return FALSE;
}

sub _runPostClickLifeCycle {
    my $self = shift;

    my $transition      = $self->_getTransition();
    my $state_tracker   = Solstice::Session->new()->getStateTracker();
    my $application     = $self->_getApplication();
    my $postclick_controller = $self->_getPostClickController();

    if ($transition->requiresRevert()) {
        if (!$postclick_controller->revert()) {
            $self->_setPreClickController($state_tracker->failRevert($application));
            return FALSE;
        }
    }

    if ($transition->requiresUpdate()) {
        if (!$postclick_controller->update()) {
            $self->_setPreClickController($state_tracker->failUpdate($application));
            return FALSE;
        }
    }
    if ($transition->requiresValidation()) {
        if (!$postclick_controller->validate()) {

            if (my $controller = $state_tracker->failValidation($application)) {
                $self->_setPreClickController($controller);
            } else {
                $self->_setPreClickController($postclick_controller);
            }
            return FALSE;
        }
    }

    if ($transition->requiresCommit()) {
        if (!$postclick_controller->commit()) {
            $self->_setPreClickController($state_tracker->failCommit($application));
            return FALSE;
        }
    }
    return TRUE;
}


sub _runGlobalTransition {
    my $self = shift;

    my $transition = $self->_getTransition();
    my $target_pageflow = $transition->getTargetPageFlow();
    my $machine = Solstice::State::Machine->new();
    my $application = $self->_getApplication();
    my $state_tracker = Solstice::Session->new()->getStateTracker();

    #assume main page flow if none is specified
    my $pageflow = $application->getNamespace().'::'.((defined $target_pageflow) ? $target_pageflow : 'Main');

    my $state;
    if ("Solstice::State::FlowTransition" eq ref($transition)) {
       $pageflow = $transition->getPageFlowName();
       my $current_pageflow = $machine->getPageFlow($pageflow);
       $state = $current_pageflow->getEntrance();
    }else{
        $state_tracker->clearPageFlowStack();
    }
    $state_tracker->startApplication($pageflow, $transition->getName());
    $state_tracker->_setState($transition->getTargetState()||$state);

    return;
}

sub _findTransition {
    my $self = shift;
    my $action = shift;

    my $state_tracker = Solstice::Session->new()->getStateTracker();

    if( my $transition = $state_tracker->getTransition($action)){
        $self->_setTransition($transition);
        $self->_setIsGlobalTransition(FALSE);

    }elsif( $transition = $state_tracker->getPageFlowGlobalTransition($action, $self->_getNamespace())){
        $self->_setTransition($transition);
        $self->_setIsGlobalTransition(TRUE)

    }elsif( $transition = $state_tracker->getGlobalTransition($action, $self->_getNamespace())){
        $self->_setTransition($transition);
        $self->_setIsGlobalTransition(TRUE);

   }else{
        my $state = $state_tracker->getState();
        die "Could not transition from state $state via action $action\n"
   }
}

sub _handleBadBackButton {
    my $self = shift;

    my $state_tracker   = Solstice::Session->new()->getStateTracker();
    my $application     = $self->_getApplication();

    if( $state_tracker->getBackErrorMessage() ){
        my $ns = Solstice::NamespaceService->new()->getAppNamespace();
        $self->getMessageService()->addInfoMessage(
            $self->getLangService($ns)->getMessage($state_tracker->getBackErrorMessage())
        );
    }else{
        $self->getMessageService()->addInfoMessage(
            $self->getLangService()->getMessage('generic_back_error')
        );
    }

    $self->_setPreClickController($state_tracker->getController($application));

    return TRUE;
}

sub _handlePreferenceButton {
    my $self = shift;

    my $selected_button = $self->getButtonService()->getSelectedButton();
    my $application     = $self->_getApplication();
    my $state_tracker   = Solstice::Session->new()->getStateTracker();

    my $key = $selected_button->getPreferenceKey();
    my $value = $selected_button->getPreferenceValue();

    if (defined $key) {
        my $preference_service = $self->getPreferenceService($application->getNamespace());
        $preference_service->setPreference($key, $value);
    }

    my $controller = $state_tracker->getController($application);
    $controller->update();
    $self->_setPreClickController($controller);

    # This ensures that dev toolbar prefs get picked up immediately
    if ($self->getConfigService()->getDevelopmentMode()) {
        $self->_setDevelopmentOptions();
    }

    return TRUE;
}

sub _handleLoginButton {
    my $self = shift;

    my $selected_button = $self->getButtonService()->getSelectedButton();
    my $application     = $self->_getApplication();
    my $state_tracker   = Solstice::Session->new()->getStateTracker();
    
    my $controller = $state_tracker->getController($application);
    $controller->setRequiresAuth(TRUE);
    $self->_setPreClickController($controller);
    
    return TRUE;
}

sub _denyUnstableBrowser {
    my $self = shift;

    # Do we support this browser?
    if( $self->_isBrowserStable() ){
        return TRUE;
    }else{
        $self->_paintView(Solstice::View::DeniedBrowser->new());
        return FALSE;
    }
}

sub _initializeSession {
    my $self = shift;

    # Initialize session
    my $session = Solstice::Session->new;

    # Before we do anything else, make sure we have a proper session
    # to work with
    unless ($session->hasSession()) {
        if($self->_getRequireSession()){
            $self->_forceCreateSession();
            return FALSE;
        }else{
            $self->_passiveCreateSession();
        }
    }
    # Okay, we have a useful session - since this is a user-pageview
    # controller, we'll need a subsession too
    $session->loadSubsession();
    return TRUE;
}

sub _isLoginValid {
    my $self = shift;

    # Is the login name valid?
    if (my $user = $self->getUserService()->getOriginalUser()) {
        # Is this really the first place where we need to load the login realm module?
        $self->loadModule($user->getLoginRealm());
        unless ($user->getLoginRealm()->isValidLogin($user->getLoginName())) {
            $self->_paintView(Solstice::View::DeniedUser->new());
            return FALSE;
        }
    }
    return TRUE;
}

sub _checkJavascript {
    my $self = shift;

    my $session = Solstice::Session->new();
    # Check that javascript flag has been set
    if($self->_getRequireSession()){
        if (!defined $session->hasJavascript()) {
            my $has_js = param('has_js');
            if (!defined $has_js) {
                $self->_checkHasJavascript();
                return FALSE;
            }
            $session->setHasJavascript($has_js);
            Solstice::ButtonService->new()->setHasJavascript($has_js);
        }
        Solstice::JavaScriptService->new()->setHasJavascript($session->hasJavascript());
    }else{
        #if we aren't doing a session bounce we just have to assume
        #TODO: this exposes that our JS detection method is kinda weak. Can we do better?
        Solstice::JavaScriptService->new()->setHasJavascript(TRUE);
    }

    return TRUE;
}

sub _initializeState {
    my $self = shift;

    my $session = Solstice::Session->new();
    my $namespace = Solstice::NamespaceService->new()->getNamespace();

    my $pageflow = $self->_getPageFlow() || 'Main';
    my $initial_state = $namespace.'::'.$self->_getInitialState();
    $pageflow = $namespace.'::'.$pageflow;

    ### Init the App's State Machine
    my $state_tracker = $session->getStateTracker();

    #if we are just navigating via a button, we want to keep our subsesion
    #but we need to refresh our state to whatever this url thinks is default
    my $clear_state = FALSE;
    my $selected_button = $self->getButtonService()->getSelectedButton();
    if($selected_button && $selected_button->getAction() eq '__nav_only__'){
        $clear_state = TRUE;
    }

    if (!$state_tracker || $clear_state) {
        $state_tracker = new Solstice::State::Tracker();
        $state_tracker->startApplication($pageflow, $initial_state);
        $session->setStateTracker($state_tracker);
    }

    #if there was a state tracker and we were ina pageflow, we may need
    #to capture the name of that pageflow - if we are in the url's default
    #pageflow, this will be essentially a no-op
    $state_tracker->getState() =~ /(.*?):/;
    $namespace = $1;
    $self->_setNamespace($namespace);

    return TRUE;
}

sub _passiveCreateSession {
    my $self = shift;
    my $screen = $self->_getScreen();

    my $session = Solstice::Session->new();
    my $server  = Solstice::Server->new();

    my $session_cookie = $session->getCookie()->bake();

    return;
}


sub _forceCreateSession {
    my $self = shift;

    my $screen = $self->_getScreen();

    my $session = Solstice::Session->new();
    my $server  = Solstice::Server->new();

    if (defined param('has_js')) {
        #cookies are probably not enabled, we're just bouncing around
        $self->_paintView(Solstice::View::NoCookies->new(), undef, $screen);

        return TRUE;
    }

    my $session_cookie = $session->getCookie()->bake();

    $self->_checkHasJavascript();

    return TRUE;
}

sub _checkHasJavascript {
    my $self = shift;

    my $server = Solstice::Server->new();
    $server->setContentType('text/html; charset=UTF-8');

    my $query_string = $ENV{'QUERY_STRING'} ? ('?'.$ENV{'QUERY_STRING'}) : '';
    my $url = $server->getURI();

    my $existing_params = '';

    my $params = param();
    foreach my $key (keys %$params) {
        my @values = param($key);
        my $name = unrender($key);
        foreach my $value (@values) {
            $value = unrender($value);
            $existing_params.= qq|<input type="hidden" name="$name" value="$value"/>\n|;
        }
    }

    # Non-javascript clickthrough message
    my $content = $self->getLangService()->getString('clickthrough_no_javascript');

    my $screen = $self->_getScreen();
    $$screen = qq|<html><head></head><body>
        <form method="post" id="init_form" action="$url$query_string">
                $existing_params
                <input type="hidden" id="js_tracker" name="has_js" value="0" />
                <noscript>
                <input id="submit_button" type="submit" value="$content" />
                </noscript>
        </form>
        <script type="text/javascript">
                var has_js = document.getElementById('js_tracker');
                has_js.value = '1';
                document.getElementById('init_form').submit()
        </script>
        </body></html>|;

    return TRUE;
}


sub loadApplicationForNamespace {
    my $self = shift;

    my $namespace = $self->_getNamespace();
    my $session = Solstice::Session->new();

    # Get our app config params
    my $session_app_key = '__'.$namespace.'__';

    # Get our app classes, and require them in.
    my $app_class = $namespace.'::Application';

    $self->loadModule($app_class);

    my $application = $session->get($session_app_key);

    unless (defined $application) {
        $application = $app_class->new();
        unless( $application ){
            die "Failed to load or create an application object $app_class";
        }
        $session->set($session_app_key, $application);
    }

    $self->_setApplication($application);

    return TRUE;
}


sub _isBrowserStable {
    my $self = shift;

    my $b = HTTP::BrowserDetect->new();

    # special netscape 8 case - we only bock the IE rendering engine
    return FALSE if ($b->user_agent =~ /Netscape\/8/ && $b->user_agent =~ /MSIE/);

    # white list
    # developer tested and APPROVED
    return TRUE if ($b->firefox || $b->mozilla || $b->safari);
    return TRUE if ($b->ie && $b->version > 5.5);
    return TRUE if ($b->netscape && $b->version > 6.1);
    
    # black list
    # developer tested and DENIED
    return FALSE if ($b->netscape || $b->ie);

    # unknown
    return TRUE;
}


sub _getAccessorDefinition {
    return [
        {
            name        => 'RequiresFreshen',
            key         => '_require_freshen',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'ViewTopNav',
            key         => '_view_top_nav',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'BoilerplateView',
            key         => '_boilerplate_view',
            type        => 'String',
            private_get => TRUE,
        },
        {
            name        => 'RequiresAuth',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'RequireSession',
            key         => '_require_session',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'InitialState',
            key         => '_initial_state',
            type        => 'String',
            private_get => TRUE,
        },
        {
            name        => 'PageFlow',
            key         => '_page_flow',
            type        => 'String',
            private_get => TRUE,
        },
        {
            name        => 'EscapeFrames',
            key         => '_escape_frames',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'DisableBackButton',
            key         => '_disable_back_button',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'DocumentTitle',
            key         => '_document_title',
            type        => 'String',
            private_get => TRUE,
        },
        {
            name        => 'PostClickState',
            key         => '_post_click_state',
            type        => 'String',
        },
        {
            name        => 'PreClickState',
            key         => '_pre_click_state',
            type        => 'String',
        },
        {
            name        => 'PostClickController',
            key         => '_post_click_controller',
            type        => 'Solstice::Controller',
            private_get => TRUE,
        },
        {
            name        => 'PreClickController',
            key         => '_pre_click_controller',
            type        => 'Solstice::Controller',
            private_get => TRUE,
        },
        {
            name        => 'Action',
            key         => '_action',
            type        => 'String',
        },
        {
            name        => 'Screen',
            key         => '_screen',
            type        => 'SCALAR',
            private_get => TRUE,
        },
        {
            name        => 'Namespace',
            key         => '_namespace',
            type        => 'String',
            private_get => TRUE,
        },
        {
            name        => 'Application',
            key         => '_application',
            type        => 'Solstice::Application',
            private_get => TRUE,
        },
        {
            name        => 'IsGlobalTransition',
            key         => '_is_global_trans',
            type        => 'Boolean',
            private_get => TRUE,
        },
        {
            name        => 'Transition',
            key         => '_transition',
            type        => 'Solstice::State::Transition',
            private_get => TRUE,
        },

    ];
}



1;

__END__