# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Web.pm 3233 2008-08-21 12:47:26Z josh $

package BigMed::App::Web;
use strict;
use warnings;
use utf8;
use Carp;
$Carp::Verbose = 1;

#multiple inheritance, ugh... go carefully.
#use `our @ISA` instead of use base to avoid trouble in 5.6.0
use BigMed::App;
use CGI::Application;
our @ISA = qw{CGI::Application BigMed::App};

use BigMed::App::Web::Prompt;
use BigMed::App::Web::Parse;
use BigMed::DiskUtil qw(bm_file_path bm_confirm_dir);
use BigMed::Data;
use JSON;

my $PERL_5_8 = ( $] >= 5.008 ); #determines utf-8 handling
if ($PERL_5_8) {
    binmode( STDOUT, ':utf8' );
    binmode( STDIN, ':bytes' ); #convert to utf8 via utf8_param
}

#hold js validation info across multiple forms (generated by
#the html_build_form method).
my @js_validation;
my %js_behavior;
my %js_add_script;
my %js_init_object;
my @js_scripts;
my $js_onload          = '';
my $js_focus_id        = '';
my $js_add_code        = '';
my $js_init_radio_tabs = 0;

###########################################################
# INITIALIZATION ROUTINES
###########################################################

#constructor is handled via CGI::Application (and called
#from the .cgi scripts). These methods run when the
#application object is first constructed by CGI::Application.

sub cgiapp_init {
    my $app = shift;

    #load BM system object and plug in
    $app->register_app();

    #clear some values for benefit of persistent environments like mod_perl
    $app->js_clear_all();

    #set template paths if we can
    my $moxiedir = $app->env('MOXIEDATA');
    if ( $moxiedir && -e $moxiedir ) {
        my $tmpl = bm_file_path( $moxiedir, 'templates', 'cp_templates' );
        my $custom =
          bm_file_path( $moxiedir, 'templates_custom', 'cp_templates' );
        my $cache = bm_file_path( $moxiedir, 'worktemp', 'tmpl_cache' );
        bm_confirm_dir( $cache, { build_path => 1, data => 1 } )
          or $app->error_stop;
        $app->param( 'APPWEB_TMPLCACHE', $cache );
        $app->tmpl_path( [$custom, $tmpl] );
    }

    #set header preferences
    $app->header_type('header');
    $app->header_props(
        -type          => 'text/html',
        -charset       => 'utf-8',
        -pragma        => 'no-cache',
        -cache_control => 'no-cache',
        -expires       => '-1d',
        -p3p           => 'ALL CUR ADM TAI OUR NOR ONL UNI INT STA PRE',
    );

    #limit post size to upload limit plus 1meg
    $app->_limit_uploads;

    #capture our errors nicely
    $app->run_modes( 'crash' => 'rm_crash' );
    $app->error_mode('rm_crash');

    #we'll get run modes from path or, failing that, from rm
    $app->mode_param( path_info => 1, param => 'rm' );
}

sub _limit_uploads {    #set and check the CGI upload limit
    my $app = shift;
    my $doclimit = $app->env('ADMIN_DOCLIMIT') || 5120;    #5MB default
    $CGI::POST_MAX = $doclimit * 1024;
    if ( !$app->param() && $app->query->cgi_error() ) {
        $app->basic_message(
            head => 'BM_Upload too large',
            text => ['BM_TEXT_Upload too large', $doclimit],
            exit => 1,
        );
    }
}

###########################################################
# REGISTRATION OF PARSERS AND PROMPTERS
###########################################################

#These methods override the BigMed::App methods and use
#the BigMed::App::Web::Prompt and Parse modules to
#define routines for each BigMed::Element type

sub register_all_app_parsers {    #overrides base BigMed::App method
    BigMed::App::Web::Parse::register_all( ref $_[0] );
}

sub register_all_app_prompters {    #overrides base BigMed::App method
    BigMed::App::Web::Prompt::register_all( ref $_[0] );
}

###########################################################
# URL INFO
###########################################################

sub build_url {
    my $app     = shift;
    my %options = @_;
    return $options{url} if $options{url};

    #find the base script url; if no script/plugin specified, use current url
    $options{dir} ||= $app->env('MOXIEBIN');
    if ( !$options{dir} ) {    #base on current request
        $options{dir} = $app->query->url()
          or croak 'Could not determine current URL to build new URL';
        $options{dir} = $1 if $options{dir} =~ m{^(.*)[\\/]};
    }
    my $plugin = $options{plugin} ? "plugins/$options{plugin}" : undef;
    $options{script} ||= $plugin;
    my $url;
    if ($options{script}) {
        $url = "$options{dir}/$options{script}";
    }
    else {
        $url = $app->query->url()
          or croak 'Could not determine a script filename to use in new URL';
        $url =~ s{[/\\]+$}{}g; #apache2 can append / if path_info
    }

    #build path info
    $options{rm} ||= q{};
    $options{site} = $options{site}->id if ref $options{site};
    $options{site} ||= $app->path_site || 'x';
    my @path = @options{qw(rm site)};
    push @path, $options{path} if $options{path};
    my @args =
        !defined $options{args} ? ()
      : ref $options{args} eq 'ARRAY' ? @{ $options{args} }
      : ( $options{args} );
    push @path, map { defined $_ ? CGI::escape($_) : q{} } @args;
    my $path = join( '/', @path );
    return ( $app->env('USE_BMQUERY') && $path ) ? "$url?$path"
      : $path ? "$url/$path"
      : $url;
}

sub path_site {
    my $app  = shift;
    my $path = $app->query->path_info() or return undef;
    my $site = ( split( m{/}, $path ) )[2] || '';
    $site =~ s/\D+//;
    return $site;
}

sub path_args {
    my $app  = shift;
    my $path = $app->query->path_info() or return undef;
    my @args = split( m{/}, $path );
    my $last = @args - 1;
    return @args > 3 ? @args[3 .. $last] : ();
}

###########################################################
# RUN MODES
###########################################################

sub rm_crash {
    my $app    = shift;
    my $error  = shift || '';
    my %option = @_;            #from the Global Moxie crash catcher itself

    my $machine_version = $app->bigmed->VERSION || '';
    my $version         = "Big Medium Version " . $app->bigmed->version;
    my $server          = $ENV{SERVER_SOFTWARE} || '';
    my $perl_version    = "Perl " . ( sprintf "%vd", $^V );
    $error =~ s/\n/\n\n/g;
    my $error_report =
      $app->escape(
        $machine_version . ": $version\n$perl_version, $server\n$error" );
    my $error_html = $error_report;
    $error_html =~ s{\n}{<br />}g;
    my $runmode = $app->get_current_runmode() || '';

    if ( $runmode =~ /^ajax/ ) {
        return $app->ajax_error($error_html);
    }

    #set the behavior for revealing the error report
    $app->js_clear_all();
    $app->js_make_toggle( 'bm_ErrorShow', 'bm_ErrorBox' );

    #build the form fields
    my $comments = $app->prompt_field_ref(
        prompt_as   => 'raw_text',
        value       => '',
        label       => 'WEB_CRASH_What Happened?',
        description =>
          'WEB_CRASH_What were you doing when this problem occurred?',
        id          => 'comments',
        value       => '',
        no_validate => 1,
        focus       => 1,
    );
    my $email = $app->prompt_field_ref(
        prompt_as   => 'email',
        value       => '',
        label       => 'WEB_CRASH_E-mail Address',
        description => 'WEB_CRASH_Optional.',
        id          => 'email',
        value       => '',
    );
    my $submit = $app->prompt_field_ref(
        id        => 'bm_submit',
        prompt_as => 'submit',
        value     => $app->language('BM_SUBMIT_LABEL_Send'),
    );
    my $error_detail = $app->prompt_field_ref(
        prompt_as => 'hidden',
        id        => 'error_detail',
        value     => $error_report,
    );
    my $fieldset = $app->prompt_fieldset_ref(
        fields    => [$comments, $email, $error_detail, $submit],
        field_msg => $option{field_msg},
        query     => $option{query}
    );

    $app->html_template_screen(
        'screen_web_crash.tmpl',
        bmcp_title => $app->language('WEB_CRASH_Software Trouble'),
        error_html => $error_html,
        fieldsets  => [$fieldset],
    );

}

###########################################################
# TEXT MANIPULATION/ESCAPING ROUTINES
###########################################################

#Manage and massage javascript html to be inserted in the
#page's head tag

sub escape {    #overrides BigMed::App's stub method
    my $string = $_[1] || return $_[1];
    $string =~ s/&/&amp;/g;
    $string =~ s/>/&gt;/g;
    $string =~ s/</&lt;/g;
    $string =~ s/"/&quot;/g;
    $string;
}

sub unescape {
    my $string = $_[1] or return $_[1];
    $string =~ s{&(\#?[xX]?(?:[0-9a-fA-F]+|\w+));}{
	    local $_ = $1;
        /^amp$/i      ? '&'
          : /^quot$/i ? '"'
          : /^gt$/i   ? '>'
          : /^lt$/i   ? '<'
          : /^#(\d+)$/ ? chr($1)
          : /^#x([0-9a-f]+)$/i ? chr( hex($1) )
          : $_
	}gex;
    return $string;
}


sub _utf8_perl58 { #for Perl 5.8 and greater
    local $SIG{'__DIE__'};
    require Encode;
    return Encode::decode('utf8', $_[0]);
}

my $utf8;
sub _utf8_perl56 { #for Perl 5.6
    if (!$utf8) {
        require String::Multibyte;
        $utf8 = String::Multibyte->new("UTF8");
    }
    if ( $utf8->islegal($_[0]) ) {
        return pack "U0C*", unpack "C*", $_[0];
    }
    else {
        return $_[0]; #not a utf-8 string, let's not make things worse
    }
}

sub utf8_param { #get value from query param, encode, cache return
    my $app = shift;
    my $name = shift;
    return if !defined $name;
    my $rparam = $app->param('_WEB_UTF8');
    $app->param('_WEB_UTF8', ( $rparam = {} )) if !$rparam;
    
    if (!defined $rparam->{$name}) {
        my $q = $app->query;
        return if ref $q->{$name} ne 'ARRAY';
        my $rtrans =  $PERL_5_8 ? \&_utf8_perl58 : \&_utf8_perl56;
        $rparam->{$name} = [ map { $rtrans->($_) } @{$q->{$name}} ];
    }
    return wantarray ? @{$rparam->{$name}} : $rparam->{$name}->[0];
}

sub clear_utf8_param {
    my $app = shift;
    my $rparam = $app->param('_WEB_UTF8');
    $app->param('_WEB_UTF8', ( $rparam = {} )) if !$rparam;
    $app->query->delete(@_);
    delete @{$rparam}{@_};
    return;
}

###########################################################
# HTML GENERATORS
###########################################################

sub html_template_screen {
    my $app      = shift;
    my $filename = shift;
    my $html     =
      $app->html_template( $filename, $app->html_general_params, @_ );

    #localize by parsing the <TMPL_LANG> and translating text attributes
    $html =~ s{<BM_LANG ([^>]+)>}{
        #chunk it up into html-like attributes
        my $arg_string = $1;
        my %attr;
        while ($arg_string =~ /(\w+)\s*=\s*(["'])(.*?)\2/g) {  #"
            $attr{$1} = $3;
        }
        return '' if !$attr{text};
        $attr{params} ||= '';
        my @params = split /\s*\|\|\s*/, $attr{params};
        $app->language([$attr{text}, @params]);
    }ge;
    $html;
}

sub html_template {
    my ( $app, $filename, %value ) = @_;
    my $tmpl;
    if ( $app->tmpl_path ) {    #load the template file
        $tmpl = $app->load_tmpl(
            $filename,
            die_on_bad_params => 0,
            loop_context_vars => 1,

            cache => 1,

            # can't handle this in all cases (progress bars, for example)...
            #            file_cache        => 1,
            #            file_cache_dir    => $app->param('APPWEB_TMPLCACHE'),
        );
    }
    else {    #don't know where the template file is located
        require HTML::Template;
        require BigMed::DefaultTmpl;
        my $tmpl_string = BigMed::DefaultTmpl::template($filename);
        $tmpl = HTML::Template->new(
            scalarref         => \$tmpl_string,
            die_on_bad_params => 0,
            loop_context_vars => 1
        );
    }
    $tmpl->param(%value);
    $tmpl->output;
}

sub prompt_fieldset_ref {
    my $app       = shift;
    my %set_param = @_;

    my $rfield_msg = $set_param{field_msg} || {};
    my $query = $set_param{query};
    my $fieldset_html;

    my @fields = $set_param{fields} ? @{ $set_param{fields} } : ();
    my @fieldset_fields;
    my $not_optional;
    foreach my $rfield_args (@fields) {
        my ( $fieldname, $roptions ) = @$rfield_args[1, 2];
        if ($rfield_msg) {
            $roptions->{field_msg} = $rfield_msg;
            $not_optional = 1 if $rfield_msg->{$fieldname};
        }
        $roptions->{query} = $query if $query;
        push @fieldset_fields, $app->prompt(@$rfield_args);
    }
    my $title = $set_param{title} || '';
    $title = $app->language($title) if $title;
    my $help =
        $set_param{help}
      ? $app->language( $set_param{help} )
      : $set_param{help_html};
    if ($help) {
        croak 'prompt_fieldset_ref requires fieldset id to include help text'
          if !$set_param{id};
        $app->js_make_toggle( $set_param{id} . 'HelpLink',
            $set_param{id} . 'HelpBox', 'slide', );
    }

    my $optional = $set_param{id} && $set_param{optional} && !$not_optional;
    $app->js_make_toggle( "$set_param{id}Trigger", "$set_param{id}Toggle",
        'slide' )
      if $optional;

    {   fieldset_id        => $set_param{id},
        fieldset_class     => $set_param{css_class},
        fieldset_title     => $title,
        fieldset_akey      => $set_param{access_key},
        fieldset_fields    => \@fieldset_fields,
        fieldset_help      => $help,
        fieldset_pre_html  => $set_param{pre_html},
        fieldset_post_html => $set_param{post_html},
        fieldset_optional  => $optional,
    };
}

sub title_and_message {
    my $app     = shift;
    my %options = @_;
    $options{html_id} ||= 'bm_MainMessage';
    my ( $title, $message );
    if ( $app->error ) {
        $app->js_make_error_box( $options{html_id} );
        my %error = $app->error_html_hash;
        $message = $app->language( $error{text} );
        $title   = $app->language( $error{head} );
    }
    elsif ( $options{field_msg} && $options{field_msg}->{_ERR_LIST} ) {
        $app->js_make_error_box( $options{html_id} );
        $message = $app->language('BM_Trouble_processing_form') . "\n<ul>";
        foreach my $msg ( @{ $options{field_msg}->{_ERR_LIST} } ) {
            $message .= "<li>$msg</li>\n";
        }
        $message .= '</ul>';
        $title = $app->language('BM_Please_Review_Your_Entry');
    }
    else {
        $message = $options{message} || '';
        $message = $app->language( $options{message} ) if $message;
        $title = $options{title} || '';
        $title = $app->language( $options{title} ) if $title;
    }
    return ( $title, $message );
}

sub html_general_params {
    my $app = shift;

    #organize behaviors. sort in reverse so that our . and # show up after
    my @behaviors;
    foreach my $element ( sort { $b cmp $a } keys %js_behavior ) {
        my %function = %{ $js_behavior{$element} };
        foreach my $method ( sort keys %function ) {
            push @behaviors,
              { element  => $element,
                method   => $method,
                function => $function{$method},
              };
        }
    }

    my $focus =
      $js_focus_id ? "           BM.Field.focus('$js_focus_id');\n" : '';
    my $init_radio =
      $js_init_radio_tabs ? "           BM.RadioTab.init();\n" : '';
    my $js_code = '';
    $js_code .= qq{BM.pathDiv = "?";\n} if $app->env('USE_BMQUERY');
    $js_code .= "        $_ = new Object;\n" for ( keys %js_init_object );
    $js_code .= $js_add_code;

    my $language = lc( $app->env('LANGUAGE') ) || 'en-us';
    if ( $language ne 'en-us' && $app->env('BMADMINDIR') ) {
        my $path = bm_file_path( $app->env('BMADMINDIR'), 'js', 'bm-lang-' );
        my ($base) = split( /-/, $language );
        $language =
            -e "$path$language.js" ? $language
          : -e "$path$base.js"     ? $base
          :                          'en-us';
    }
    unshift @js_scripts, $app->env('BMADMINURL') . "/js/bm-lang-$language.js";

    my $include_script = '';
    foreach my $url (@js_scripts) {
        my ( $src, $limit ) = ref $url eq 'ARRAY' ? @{$url} : ($url);
        my $script =
            '    <script src="' . $src
          . '" type="text/javascript" language="javascript" charset="utf-8">'
          . "</script>\n";
        if ($limit) {
            $include_script .= "    <!--[$limit]>\n$script    <![endif]-->\n";
        }
        else {
            $include_script .= $script;
        }
    }

    return (
        bmcp_bmadmin       => $app->env('BMADMINURL'),
        bmcp_env_dot       => $app->env('DOT'),
        bmcp_moxiebin      => $app->env('MOXIEBIN'),
        bmcp_js_validation => \@js_validation,
        bmcp_js_behaviors  => \@behaviors,
        bmcp_js_custom     => $js_code,
        bmcp_js_onload     => $focus . $init_radio . $js_onload,
        bmcp_js_scripts    => $include_script,
        bmcp_year          => $BigMed::BMYEAR,
        bmcp_bmversion     => $BigMed::BMVERSION,
        bmcp_pathdiv       => ( $app->env('USE_BMQUERY') ? '?' : '/' ),
    );
}

sub basic_message {
    my $app     = shift;
    my %message = @_;

    my $html = $app->html_template_screen(
        'shell_message.tmpl',
        bmcp_title => $message{head} || $app->language('Message'),
        message => $message{html} || $message{text} || '',
    );

    if ( $message{exit} ) {
        $app->header_type('header');
        $app->header_props(
            -type    => 'text/html',
            -charset => 'utf-8',
            -pragma  => 'no-cache',
            -expires => '-1d',
        );
        print $app->_send_headers();
        print $html;
        $app->teardown();
        exit;
    }
    $html;
}

###########################################################
# JAVASCRIPT ROUTINES
###########################################################

sub js_clear_all {
    $js_onload          = '';
    $js_focus_id        = '';
    $js_add_code        = '';
    $js_init_radio_tabs = 0;
    undef @js_validation;
    undef %js_behavior;
    undef @js_scripts;
    undef %js_add_script;
    undef %js_init_object;
}

my %js_esc = (
    "\n" => '\n',
    "\r" => '\r',
    "\t" => '\t',
    "\f" => '\f',
    "\b" => '\b',
    "\"" => '\"',
    "\\" => '\\\\',
    "'"  => "\\'",
);

sub js_escape {
    my $class   = shift;
    my @strings = @_;
    foreach my $string (@strings) {
        $string = '' unless $string;
        $string =~ s/([\\'"\n\r\t\f\b])/$js_esc{$1}/eg;
        $string =~ s{</}{<\\/}g;    #causes trouble inside html page
        $string =~ s/([\x00-\x07\x0b\x0e-\x1f])/'\\u00' . unpack('H2',$1)/eg;
    }
    return wantarray ? @strings : $strings[0];
}

sub js_add_behavior {
    my ( $app, $element, $js_method, $js_function ) = @_;
    return undef if !$element || !$js_method || !$js_function;
    my $function = $js_behavior{$element}->{$js_method} || '';
    $function .= "\n" . $js_function;
    $js_behavior{$element}->{$js_method} = $function;
    return 1;
}

sub js_make_trigger {
    my ( $app, $trigger_id, $target_id ) = @_;
    return if !$trigger_id || !$target_id;
    $trigger_id =~ s{^\#}{};    #trim any leading #id indicator
    $target_id  =~ s{^\#}{};
    my $selector = '#' . $trigger_id;
    $app->js_add_behavior( $selector, 'click',
        "BM.FX.trigger(e,'$target_id');Event.stop(evt);return false;",
    );
}

sub js_make_toggle {
    my ( $app, $trigger_id, $target_id, $effect ) = @_;
    return if !$trigger_id || !$target_id;
    $trigger_id =~ s{^\#}{};    #trim any leading #id indicator
    $target_id  =~ s{^\#}{};
    my $selector = '#' . $trigger_id;
    $effect = $effect ? ", {effect:'" . $app->js_escape($effect) . "'}" : '';
    $app->js_add_behavior( $selector, 'click',
        "BM.FX.toggle(e,'$target_id'$effect);Event.stop(evt);return false;",
    );
}

sub js_focus_field {
    my ( $app, $field_id ) = @_;
    return undef unless $field_id;
    $field_id =~ s{^\#}{};
    $js_focus_id = $app->js_escape($field_id);
    1;
}

sub js_init_radio_tabs { $js_init_radio_tabs = 1 }

sub js_add_onload {
    my ( $app, $js_method ) = @_;
    return undef unless $js_method;
    $js_onload &&= $js_onload . "\n";
    $js_onload .= $js_method;
    1;
}

sub js_add_validation {
    my ( $app, %field_info ) = @_;

    #can't validate if we don't have a field id.
    #no validation needed if no type and not 'required' field.
    return undef if !$field_info{id};
    return 1 if !$field_info{type} && !$field_info{required};
    my $required = $field_info{required} ? 1 : '';
    my ( $id, $validate_type, $error, $label ) =
      $app->js_escape( @field_info{qw(id type error label)} );
    $label ||= $id;

    push @js_validation,
      { FIELD_ID       => $id,
        FIELD_REQUIRED => $required,
        VALIDATE_TYPE  => $validate_type,
        FIELD_LABEL    => $label,
      };
    1;
}

sub js_make_error_box {
    my $id = $_[1] or croak 'js_make_error_box requires JS object id';

    $_[0]->js_add_onload("BM.FX.tintError('$id');");
}

sub js_show_gmt_help {
    my $app = $_[0];
    return unless $_[1];
    my $quoted_id = q{'} . $app->js_escape( $_[1] ) . q{'};
    $app->js_add_onload("BM.TimeOffset.init($quoted_id);");
    $app->js_add_behavior( '#' . $_[1] . '-sign',
        'change', "BM.TimeOffset.recalc($quoted_id);" );
    $app->js_add_behavior( '#' . $_[1] . '-hour',
        'change', "BM.TimeOffset.recalc($quoted_id);" );
    $app->js_add_behavior( '#' . $_[1] . '-minute',
        'change', "BM.TimeOffset.recalc($quoted_id);" );
}

sub js_make_lists_sortable {
    my $app = shift;
    return unless $_[0];
    my $quoted_id = $app->js_escape( $_[0] );
    $app->js_add_onload("BM.Sort.init('$quoted_id');\n");
    $app->js_add_behavior( "\#$quoted_id span.bmcp_handle",
        'mousedown', 'BM.Sort.closeDetails(e);' );
}

sub js_add_script {
    my $app   = shift;
    my $entry = shift or return;
    my $url   = ref $entry eq 'ARRAY' ? $entry->[0] : $entry;
    if ( !$js_add_script{$url} ) {
        $js_add_script{$url} = 1;
        push @js_scripts, $entry;
    }
}

sub js_init_object {
    my $app = shift;
    my $object_name = shift or return;
    $js_init_object{$object_name} = 1;
}

sub js_add_code {
    my $app = shift;
    my $code = shift or return;
    $js_add_code .= "        $code\n";
}

###########################################################
# AJAX HANDLERS
###########################################################

sub require_post {
    return ( $_[0]->query->request_method() eq 'POST' )
      || $_[0]->set_error(
        head => 'BM_Incorrect method',
        text => 'BM_Request requires POST',
      );
}

#set all non-iframe json responses to application/json to help prevent some
#scripting attacks:
#http://jibbering.com/blog/?p=514
my $JSON_HEADER        = 'application/json';
my $JSON_IFRAME_HEADER = 'text/html';

sub _ajax_escape_and_headers {
    my ( $app, $json_obj ) = @_;

    #iframe requests have to be returned as escaped html, or more complex
    #json objects won't get eval'd correctly.
    if ( $app->utf8_param('BM_IFRAMEAJAX') ) {
        $json_obj = $app->escape($json_obj);
        $app->header_add( -type => $JSON_IFRAME_HEADER );
    }
    else {
        $app->header_add( -type => $JSON_HEADER );
    }
    return $json_obj;
}

sub ajax_json_response {    #arg is regular perl data structure to convert
    my $app      = shift;
    my $json_obj = JSON::objToJson( shift @_ );
    $json_obj = $app->_ajax_escape_and_headers($json_obj);
    return "ajaxJSON = $json_obj";
}

sub ajax_data_prefab_json {    #arg is hashref of prefab json strings
    my $app   = shift;
    my $rjson = shift;
    if ( ref $rjson ne 'HASH' ) {
        croak 'ajax_data_prefab_json requires hash reference as argument';
    }
    my @json_val;
    while ( my ( $k, $v ) = each(%$rjson) ) {
        push @json_val, '"' . $app->js_escape($k) . '":' . $v;
    }
    my $json_obj = 'ajaxJSON = {"data":{' . join( ',', @json_val ) . '}}';

    return $app->_ajax_escape_and_headers($json_obj);
}

sub ajax_data {
    return $_[0]->ajax_json_response( { 'data' => $_[1] } );
}

sub ajax_error {
    return _ajax_string( $_[0], 'error', $_[1] );
}

sub ajax_html {
    return _ajax_string( $_[0], 'html', $_[1] );
}

sub _ajax_string {
    my $app    = shift;
    my $field  = shift;
    my $string = shift || '';
    if ( ref $string ) {
        croak 'argument for ajax_' . $field . ' must be a string';
    }
    return $app->ajax_json_response( { $field => $string } );
}

sub ajax_system_error {
    my $app   = shift;
    my %error = $app->error_html_hash();
    return $app->ajax_error( $app->language( $error{text} ) );
}

sub ajax_parse_error {
    my $app    = shift;
    my $rerror = shift;
    my %error  = ref $rerror eq 'HASH' ? %$rerror : ();
    my @msg    = map {
        substr( $_, 0, 1 ) ne '_'
          ? $_ . ': ' . $app->language( $error{$_} )
          : ()
    } sort keys %error;
    return $app->ajax_error( join( '<br />', @msg ) );
}

sub ajax_check_uniqueness {
    my $app   = shift;
    my %param = @_;
    my ( $class, $column, $site, $name, $type ) =
      @param{qw(class column site name type)};
    my %value = $app->parse_submission(
        {   id         => 'v',
            required   => 1,
            data_class => $class,
            column     => $column,
        },
        {   id       => 'id',
            parse_as => 'id',
        }
    );
    if ( $value{_ERROR} ) {    #not a valid value for this column
        return $app->ajax_error( $app->ajax_parse_error( $value{_ERROR} ) );
    }

    #load test object
    my $data_obj = $class->new();
    $data_obj->set_id( $value{id} ) if $value{id};
    $data_obj->set_site($site)      if $site;
    my $setter = 'set_' . $column;
    $data_obj->$setter( $value{v} );

    my $unique;
    $name ||= 'AJAX_value';
    $type ||= 'AJAX_record';
    if ( $unique = $data_obj->is_unique($column) ) {
        my $html = $app->html_template( 'wi_ajax_status_ok.tmpl',
            STATUS =>
              $app->language( ['AJAX_Is available', $app->language($name)] )
        );
        return $app->ajax_html($html);
    }
    elsif ( !defined $unique ) {    #caught an error
        my %err = $data_obj->error_html_hash;
        return $app->ajax_error( $app->language( $err{text} ) );
    }

    return $app->ajax_error(
        $app->language(
            ['BM_Not unique', $app->language($name), $app->language($type)]
        )
    );

}

1;
__END__


=head1 BigMed::App::Web

Superclass for the Big Medium web application

=head1 SYNOPSIS

    # In "MyApp.pm"...
    package MyApp;
    use base 'BigMed::App::Web';
    
    sub setup {
        my $app = shift;
        $app->start_mode('login');   #use this run mode if none specified
        $app->run_modes(             #map run modes to names of routines
            'login'        => 'rm_login',
            'verify_login' => 'rm_verify_login',
            'info'         => 'rm_display_info',
        );
    }
    
    sub rm_login {
        my $app = shift;
        my %options = @_;
   
        #construct a field reference for username
        my $username = $app->prompt_field_ref(
            data_class      => 'BigMed::User',
            column          => 'name',
            id              => 'username',
            required        => 1,
            client_validate => 1,
            value           => '',
        );
        
        #construct a field reference for password
        my $password = $app->prompt_field_ref(
            data_class => 'BigMed::User',
            column     => 'password',
            required   => 1,
            value      => '',
        );
        
        #construct a field reference for the submit button
        my $submit_text = $app->language('Sign In');
        my $submit = $app->prompt_field_ref(
            prompt_as => 'submit',
            value     => $app->escape( $submit ),
        );
        
        #create the forum
        my $form = $app->html_build_form(
            form_url    => "my_app.cgi/screen2",
            fieldsets   => { fields => [$username, $password, $submit] },
            query => $options{query},
            field_msg => $options{field_msg},
        );
    
        my $title = $options{head} || 'Please sign in';
        my $message = $app->title_and_message(
            field_msg => $options{field_msg},
            custom    => $options{message},
            default   => '',
        );
        $app->basic_message(
            head => $app->language($title),
            text => $message,
            html => $form
        );
    }
    
    sub rm_verify_login {
        my $app    = shift;
        my %fields = $app->parse_submission( #load and validate entry
            {   data_class => 'BigMed::User',
                column     => 'name',
                id         => 'username',
                required   => 1,
            },
            {   data_class => 'BigMed::User',
                column     => 'password',
                required   => 1,
            },
        );

        if ( $field{_ERROR} ) {    #validation error, return to login screen
            return $app->rm_about_you(
                {   head      => 'Please review your entry',
                    field_msg => $field{_ERROR},
                    query     => $app->query,

                }
            );
        }

        # ... verify login and proceed to next screen
        if ( $app->some_login_verification_routine ) {
            return $app->rm_display_info;
        }
        else {
            return $app->rm_about_you(
                head    => 'Login failed',
                message => 'Username and password combination not found',
                query   => $app->query,
            );
        }
    }
    
    sub rm_display_info {
        my $app = shift;
        # ... info screen after successful login ...
    }

     # In "my_app.cgi"...
     #!/usr/bin/perl -w
     use MyApp;
     use strict;
     my $app = MyApp->new();
     $app->run();
     exit;
     

=head1 DESCRIPTION

BigMed::App::Web is the base superclass for all Big Medium web application
modules and provides a framework for interacting with the underlying
Big Medium system and with the user via a web interface.

BigMed::App::Web is an abstract class and is intended to be subclassed
by other modules rather than being used directly by web scripts. It is
itself a subclass of both CGI::Application and BigMed::App and inherits
the methods of those superclasses. Relevant inherited methods are described
below, but the thorough perfectionist might get a thrill out of reading
the documentation for those methods, too.

Web pages are generated via HTML::Template, and template files should be
placed in the C<moxiedata/cp_templates> directory.

=head1 USAGE

=head2 Sub-classing BigMed::App::Web;

To create a subclass module, use BigMed::App::Web as the base module:

    package MyApp;
    use base 'BigMed::App::Web';

=head2 C<setup()> and the "run mode" map

The c<setup()> method is called automatically by the inherited C<new()>
method. A BigMed::App::Web subclass should use the C<setup()> method
to establish its "run modes." You can also use C<setup()> to take care of
any business that needs to be done before C<run()> is called.

A run mode is roughly analogous to a single screen of your web application,
and each run mode has its own routine in your application module.

When your application script calls the run() method to trigger your application's
functionality, the run mode tells your application module which routine to run.
The run mode should be part of the URL of the application script or, if not
found there, in the value of the 'rm' CGI parameter ('rm' stands for "Run
Mode"). To include the run mode in the URL of your application script,
append a slash and the run mode name to the URL. For example, the
following URL specifies the run mode "screen1" for the my_app.cgi script:

    http://www.example.com/cgi-bin/my_app.cgi/screen1

The map of run mode names to routines is defined in your module's
C<setup> method, which is called automatically when the application
object is first created. Your module's C<setup> method should include
calls to two methods:

=over 4

=item * $app->start_mode('run mode')

Defines the default run mode to use if no run mode is specified in either
the URL path or the 'rm' parameter.

        $app->start_mode('login');   #use 'login' run mode as default

=item * $app->run_modes(%map_of_run_modes)

Registers the application's run mode routines. The keys of the hash are
the run mode names, and the values are either names of the routines
or code references. Specifying the run modes by name instead of reference
means that you can more easily create derivative applications using
inheritance. Subclasses of your application module can override the parent
run mode by providing its own routine of the same name.

=back

A simple C<setup> method would look like this:

    sub setup {
        my $app = shift;
        $app->start_mode('login');   #use this run mode if none specified
        $app->run_modes(             #map run modes to names of routines
            'login'        => 'rm_login',
            'verify_login' => 'rm_verify_login',
            'info'         => 'rm_display_info',
        );
    }
    
    sub rm_login {
        my $app = shift;
        # ... login screen ...
    }

    sub rm_verify_login {
        my $app = shift;
        # ... verify login screen ...
    }

    sub rm_display_info {
        my $app = shift;
        # ... info screen ...
    }

The values in the C<run_modes> hash may be either names of the routines
or code references. Specifying the run modes by name instead of reference
means that you can more easily create derivative applications using
inheritance. Subclasses of your application module can override the parent
run mode by providing its own routine of the same name.

The C<setup> method may also be used to override some default BigMed::App::Web
settings via the following methods (see the CGI::Application documentation for
details):

    mode_param() - sets the name of the run mode CGI param.
    error_mode() - the run mode to use if an exception is thrown
    tmpl_path() - path to the template directory to use

=head2 Running your application

Your application exists entirely within your application module, which
you create as a subclass of BigMed::App::Web. The application is then
run via a simple cgi script. If your application is called, for example,
MyApp, then your cgi script would simply look like this:

     #!/usr/bin/perl -w
     use MyApp;
     use strict;
     my $app = MyApp->new();
     $app->run();
     exit;

You can also specify starting parameters for the application object
by adding the C<PARAMS> argument to the C<new> method:

    my $app = MyApp->new(
        params => { 'param1' => 'value1', 'param2' => 'value2' }
    );

See the C<param> method for more information about parameters.

=head2 Run mode routines

=head3 How It Works

When your application script calls the run() method, the appropriate
run mode routine is called with the application object as the only
argument.

Run mode routines are expected to return a string that will be displayed
in a web browser. This string should not include a http header (this will
be included automatically).

B<Important>. Your application should *NEVER* print() to STDOUT.
Using print() to send output to STDOUT (including HTTP headers) is
exclusively the domain of the inherited run() method. Sending content
to the web server before sending the HTTP header will result in an
error.

=head3 Trapping Errors with C<error_mode()>

If a fatal exception is unexpectedly encountered  in one of your run modes
(and really, who *expects* a fatal exception?), the default behavior is
to display a form to users, prompting them to report the crash to
Global Moxie. You can override this behavior by setting a run mode
to use on error, with the C<error_mode()> method in the module's
C<setup()> method:

    sub setup {
        my $app = shift;
        $app->error_mode('crash');
        $app->run_modes(
            'crash' => 'rm_crash',
            # other run modes ...
        );
        
        $app->start_mode('default');
    }

=head2 Displaying Pages

Whatever processing and nifty tricks your run-mode routines might do along
the way, the end result is that they should return a string of HTML to
display in the user's browser.

=head3 Using Templates

BigMed::App::Web encourages the separation of HTML markup into external
templates. There should ideally be no HTML in your code. To enable this,
BigMed::App::Web internally uses HTML::Template for template processing.
For details on template markup syntax, refer to the HTML::Template
documentation.

By default, BigMed::App::Web expects to find templates in the
moxiedata/cp_templates directory. (Note that these are almost never the
templates used by BigMed::Format subclasses to generate the end-result
websites, RSS feeds, etc. The templates described here are more typically to
generate Big Medium control panel pages.) You can override the default
location of the templates by using the C<tmpl_path> method in your
application's C<setup> method:

    $app->tmpl_path('/path/to/template/directory');

Many of the JavaScript-related routines in BigMed::App::Web, however,
do rely on the presence of certain markup in some templates. Changing
the templates or the template path could interfere with the proper
display of some DHTML behaviors in your control panel pages.
(C<shell_header.tmpl> is particularly important, as is
C<wi_form_fieldset.tmpl>.)

If Big Medium has not yet been configured (and thus does not yet
know where the moxiedata/cp_templates directory is located), the
html_template methods listed below will try to find the template
in BigMed::DefaultTmpl.

=head3 $app->html_template_screen('screen_html.tmpl', %template_params)

Returns the html for a complete html page/screen, using the
template named in the first argument and the template parameters
in the parameter hash. In addition to filling the template, the
method also:

=over 4

=item 1. Generates the template parameters for completing the
header and footer templates for the control panel, including all
JavaScript behaviors (see the C<html_general_params> method for details).

=item 2. Translates any C<< <BM_LANG> >> language tags in the template
into the current language.

=back

=head3 $app->html_template('html_chunk.tmpl', %template_params)

Returns templatized html, using the first argument using the parameters
in the parameter hash, and returns a string containing the (x)html.

    my $html = $app->html_template(
        'example.tmpl',
        SALUTATION => 'Hello, Starshine!',
        MESSAGE    => 'The Earth says, "Hello"!',
        SIGNED_BY  => 'Willy'
    );

This method is appropriate for processing snippets of html that may be
included in a larger page; to generate a complete control-panel
page (including all JavaScript head tags etc), you should use
C<html_template_screen> instead.

=head3 $app->basic_message(%message)

Generates a standardized HTML wrapper for displaying simple messages, alerts,
etc. Generally used internally for unexpected error messages etc.
Returns a string containing the complete HTML, unless the C<exit>
parameter is set, which causes the application to quit after displaying the
message.

The method accepts an argument hash with these key/value options:

=over 4

=item * head => 'headline text'

A string to display as a headline in the site. (This text should already be
localized/escaped).

=item * html => $html_to_display

The html to display as the body text of the message. (Text within the message
should already be localized/escaped).

=item * text => 'text to display'

If C<html> is not specified, this string will be used as the text to display.
(This text should already be localized/escaped).

=item * exit => 1

If true, the application will quit immediately after displaying the message.

=back

=head3 C<< $app->html_general_params() >>

Generates a standard set of template parameters that are common to most
Big Medium pages, including template info for JavaScript behaviors.

It's unlikely that you'll need to call this method directly (it's called
automatically by C<html_template_screen>, which includes the generated
parameters into the set of parameters that you supply). But it can be
useful to override the method in subclasses to add additional template
parameters to the page. For example, BigMed::App::Web::Login does this
to add parameters with content specific to the current user and site.

The routine returns a hash of parameters whose keys all start with C<bmcp_>
to help avoid collisions with your own parameters. Any of these parameters
may be used in any template called via BigMed::App::Web and its subclasses:

=over 4

=item * bmcp_bmadmin

URL to the bmadmin directory, the value of $app->env('BMADMINURL')

=item * bmcp_moxiebin

URL to the moxiebin directory, the value of $app->env('MOXIEBIN')

=item * bmcp_version

The Big Medium version number.

=item * bmcp_year

The year when this version of Big Medium was released.

=item * bmcp_js_validation

An array reference of validation descriptions, used by the shell_header.tmpl
template to generate form-validation information in the <head> tag (see the
C<js_add_validation> method for info about client-side validation).

=item * bmcp_js_behaviors

An array reference of behavior additions, used by the shell_header.tmpl
template to generate new element behavior definitions information in the
<head> tag (see the C<js_add_behavior> method for info about behaviors).

=item * bmcp_js_onload

A string containing a block of JavaScript to run when the page has completed
loading.

=item * bmcp_js_scripts

A string containing script tags, if any, for additional scripts to include
via the page's C<< <head> >> tag.

=back

=head2 Building URLs and Retrieving URL Values

=head3 C<< $app->build_url( %params ) >>

Returns a URL built from the parameters specified in the argument hash.
The URL follows this format:

    http://www.example.com/cgi-bin/moxiebin/SCRIPT/RM/SITE/ARG1/ARG2/etc

This hash may include the following key/value pairs:

=over 4

=item * url => 'http://www.example.com/any/url'

Any arbitrary string to include as the URL for this item. If this value
is supplied, all other parameters are ignored.

=item * script => 'script_name.cgi'

The name of the target script for this URL. The script ("SCRIPT" in the
example URL shown above). If you omit both the script and plugin parameters,
the URL of the currently called script will be used (unless you have supply
a plugin value).

The url parameter takes precedence over the script parameter, so the url
value will always be used if present.

=item * plugin =>  'plugin_name.cgi'

The name of the target script for this URL. The script should be
located in the moxiebin/plugins directory. The url and script
parameters take precedence over plugin, so those values will be used
instead of plugin if present.

    $app->build_url( plugin => 'myplugin.cgi', rm => 'foo', site => 3 );
    
    #results in:
    http://www.example.com/cgi-bin/moxiebin/plugins/myplugin.cgi/foo/3

=item * rm => 'runmode'

The run mode to pass to the target script. ("RUNMODE" in the
example URL shown above).

=item * site=> $site_id

ID of the site on which to perform the operation. ("SITE_ID" in the
example URL shown above). If no site is specified, the current site
id will be used. This site id can later be retrieved via the
C<path_site> method.

=item * dir => 'http://www.example.com/cgi-bin/moxiebin'

The URL directory in which the script is located. If you omit this
parameter, the moxiebin URL will be used if you supply a plugin
or script parameter; otherwise, the URL of the current script will be used
instead.

=item * args => ['arg1', 'arg2', 'arg3']

An optional array of arguments to include in the URL path,
after the runmode and site id. (This is "ARG1" and "ARG2" in
the example shown above). These arguments can later be
retrieved via the C<path_args> method.

=back

=head3 C<< $app->path_site() >>

Returns the site id, if any, included in the path info of
the current URL.

=head3 C<< $app->path_args() >>

Retrieves the array of arguments, if any, embedded in
the current URL's path info. Note that this array does
not include the runmode or site id, which are also part
of the path info, but only the path elements that appear
after the runmode and site id.

=head2 HTTP Headers

=head3 Regular Headers

By default, the header contains the following information:

=over 4

=item * Content-type: text/html

=item * Charset: utf-8

=item * Pragma: no-cache

=item * Expires: -1d

(i.e. yesterday; prevents browser caching)

=item * P3P: policyref="/w3c/p3p.xml" cp="ALL CUR ADM TAI OUR NOR ONL UNI INT STA PRE" 

The compact privacy policy for the Big Medium application.

=back

Individual properties of the header can be added or replaced using the
C<header_add> method:

    # add or replace the 'type' header
    $app->header_add( -type => 'image/png' );

    # add an additional cookie
    $app->header_add( -cookie => [$extra_cookie] );

If an array reference is passed as a value to header_add(), values in that
array ref will be appended to any existing values values for that key.
This is useful for setting an additional cookie after one has
already been set.

To wipe out all header properties and build your own header properties from
scratch, use the C<header_props> method:

    $app->header_props(
        -type    => 'text/plain',
        -expires => '+1d',
    );

Both C<header_add> and C<header_props> use the key/value arguments
defined by CGI.pm. Recognized parameters are -type, -status, -expires,
and -cookie. Any other named parameters will be stripped of their initial
hyphens and turned into header fields, allowing you to specify any HTTP
header you desire. Internal underscores will be turned into hyphens:

    $app->header_add( -Content_length => 3002 );

Worth repeating: You don't have to (and should not) do anything to send
the header. BigMed::App::Web will do this for you automatically when
the time comes.

=head3 Redirecting to a new page

Use the C<header_type> method to set the header type to 'redirect' and
use C<header_props> to set the redirect URL:

    sub some_redirect_mode {
        my $app = shift;
        my $new_url = "http://site/path/doc.html";;
        $app->header_type('redirect');
        $app->header_props(-url=>$new_url);
        return "Redirecting to $new_url";
     }

C<header_type> accepts one of three values: 'header', 'redirect' or 'none'.
The default is 'header' -- a regular http header -- and if C<header_type>
is never called, that's what will get sent. You only need to use the
C<header_type> method when you're redirecting to a new page.

=head3 Cookies

To add cookies, you can use L<the built-in CGI.pm query
object|"CGI Query Object and Methods"> to generate a new cookie and add it
to the header via C<header_add>.

    my $q          = $app->query;
    my $new_cookie = $q->cookie(
        -name    => 'my_cookie',
        -value   => 'value string',
        -expires => '+1h',
        -path    => '/cgi-bin/myapp',
        -domain  => '.mydomain.org',
    );
    $app->header_add( -cookie => [$new_cookie] );

To read the cookie, you can likewise use the CGI query object:

    my $q = $app->query;
    my $value = $q->cookie('my_cookie');

=head2 Building Forms and Fields

BigMed::App::Web gives you a built-in library for generating html
for input fields tailored to the specific input types in Big Medium
data objects. In combination with the Big Medium CSS and JavaScript
files (moxiedata/css/bigmedium_cp.css and moxiedata/js/bigmedium_cp.js),
this library makes it a snap to create attractive, dynamic forms for
requesting user input.

The two workhorse methods are C<prompt_field_ref> and
C<prompt_fieldset_ref>. The recommended method
of building forms is to generate field references via C<prompt_field_ref>, collect them into fieldsets via C<prompt_fieldset_ref>, and then
displaying the fieldsets with C<screen_> templates which include the
C<wi_form_fieldset.tmpl> template. This process automatically handles the
inclusion of any necessary JavaScript effects and validation.

=head3 C<< $app->prompt_field_ref(%options) >>

Generates a field reference that can be used as part of a form fieldset.
See the documentation for this method in BigMed::App for details, as well
as the prompt-specific details in the BigMed::App::Web::Prompt docs.

In addition to the standard parameters available via the BigMed::App method,
BigMed::App::Web's implementation additionally accepts these parameters
(as well as any custom paramters that individual prompt methods my accept
or require):

=over 4

=item * focus => 1

If true, this field will get focus when the page loads.

=item * ajax_status => 1

If true, an ajax status field will be added for the field. This creates
a region where any ajax validation for the field will be alerted. The
field must have an ID in order to add the ajax_status field.

=item * container_id => $container_id

The id to use, if any, for the <div> containing the field entry.

=item * hidden => 1

Boolean indicating whether the <div> containing the entry should be
hidden via display:none. (Default is undef, so that the field will
be displayed unless you specify that it should not be).

=item * css_class => 'cssClassName'

The class name to use for the field. This is typically unnecessary
since the field prompt methods typically select the correct class name.
Use this if you're doing something fancy and need your own custom class.

=item * hide_label => 1

A boolean value indicating whether the label and description should be
suppressed in the form xhtml.

=item * hide_status => 1

A boolean value indicating whether the status div (used to display the
status/error message from the field_msg value) should be suppressed in the
form xhtml.

=item * other key/value pairs

Some field types accept additional options. See BigMed::App::Web::Prompt
for details.

=back

=head3 C<< $app->prompt_fieldset_ref(%options) >>

Returns a reference to a hash of template parameters to be processed by
the C<wi_form_fieldset.tmpl> template common to most form templates
in the Big Medium web application.

A fieldset is a collection of one or more form fields, a useful device for
organizing forms into categories or panels. All forms should have
at least one fieldset, and more complex forms will typically have several.
A fieldset is represented by a hash reference with the following parameters:

The method accepts an argument hash with these key/value pairs:

=over 4

=item * title => 'fieldset title'

The title of the fieldset, which appears as a legend above the fields in
the set. This should be the "raw", unlocalized, unescaped string.

=item * access_key => 'A'

Optional parameter that adds a browser "access key" to the fieldset, giving
users a keyboard shortcut to go to the fieldset.

The fieldset must have a title in order for the access key to be included.

About access keys: The browser and operating
system determine if the user must press a modifier key (e.g., ctrl, alt,
command, etc) with the access key to activate the action. In Windows versions
of Internet Explorer and Mozilla-based browsers, the alt key is required,
and the key is not case-sensitive. Macintosh versions of these browsers
require the ctrl key.

=item * fields => [$field_ref1, $fieldref2, ...]

A reference to an array of field references. Field references should be
generated using C<prompt_field_ref>.

=item * id

The id of the fieldset's enclosing C<< <div> >> tag.

=item * css_class

The CSS class, if any, of the fieldset's enclosing C<< <div> >> tag.

=item * pre_html

HTML to be displayed above the fields in the field set. Because it will
be displayed as HTML, any non-HTML text should be localized/escaped
beforehand.

=item * post_html

HTML to be displayed after the fields in the field set. Because it will
be displayed as HTML, any non-HTML text should be localized/escaped
beforehand.

=item * help => $unlocalized_text

Help text to be displayed with the field set. This text will be
localized/escaped. If you want to include html, use the C<help_html>
parameter instead. (An id is required to include help text.)

=item * help_html => $html

Help html to be displayed with the field set. This text will be displayed
as HTML, so any non-HTML text should be localized/escaped beforehand.
(An id is required to include help html.)

=item * query => $boolean

If true, the application's current query values will be used to populate
the form's fields. If a value is present, the query parameter values for
each field will be used instead of any values set in C<make_field_ref>.

This is an easy way to populate a form with values just submitted by the
user in the case, for example, of a server-side validation problem where
you need to prompt the user to fix an entry in the form.

=item * field_msg = \%field_messages

Reference to a hash of status/error messages to display with the fields
in this fieldset. Keys are the field names and the values are the error
messages. (C<prompt_fieldset_ref> localizes/escapes these messages
for you.)

C<prompt_fieldset_ref> passes the hash reference through to the C<prompt>
routine for each field, and the C<prompt> routine attaches the appropriate
message to each field in the fieldset. When processed by the
C<wi_form_fieldset.tmpl> template, the status message appears with the field.

See the C<prompt_field_ref> method above for more details about field_msg
handling.

=item * optional => 1

If true, will hide the fieldset and display a trigger link to toggle the
fieldset open and closed.

=back

=head3 C<< $app->prompt($element_type, $field_name, \%options) >>

This method is used by C<prompt_fieldset_ref> to generate the template
parameters for each field's HTML (these parameters are used by the
C<wi_form_fieldset.tmpl> template). You're certainly welcome to use
it directly, but it's easier, lazier and generally a happier
experience to use C<prompt_field_ref> to create field
references and pass them to C<prompt_fieldset_ref> rather than use
C<prompt> directly.

The method returns html for an individual input field (or group of fields)
based on the element type that you indicate. See the BigMed::Web::App::Prompt
documentation for details on the available element types, the html
that each type's prompt generates, and the available options.

=head3 Custom prompt routines

To add your own custom prompt routine, use the C<register_prompter>
method in your application module's C<setup> method.  See BigMed::App
for details.

=head3 Messaging the User

=head4 C<< ($title, $message) = $app->title_and_message(%message_info) >>

Returns localized/escaped title and main text message to display in a
run mode screen and may be used to populate the C<bmcp_title> and
C<message> HTML::Template parameters in a page's template.
If there's no text to display for either element, the method returns the
empty string '' for that element.

If messages exist in the error queue, those error messages will be selected as
the message to display If there are system errors in the error queue, that
error text is returned; if not, and there are form validation errors,
that error text is returned; if not, the default text is returned.

=over 4

=item * field_msg => \%field_messages

This is a reference to a hash of status/error messages to display
with the various fields on the page (typically, the hash reference
from the _ERRORS value of the %results hash generated by the
BigMed::App C<parse_submissions> method, which BigMed::App::Web
inherits).

If this hash contains an array reference in the _ERR_LIST value
(generated automatically by the C<prompt_fieldset_ref> method),
then the message will be a list of all of the (localized) status
messages included in that hash array. In addition, the message
div will be given  the error style via C<js_make_error_box>
(giving it a red tint).

=item * message => 'default message (unlocalized)'

If there's no field_msg value or no _ERR_LIST value, the localized value
of this parameter will be returned as the message text.

=item * title => 'default title'

If there's no field_msg value or no _ERR_LIST value, the localized value
of this parameter will be returned as the title text.

=item * id => 'html_div_id'

The id of the html div you're using to display the message to
the user. Default is C<bm_MainMessage>. (This information is used to
tint the div in case of an error).

=back

=head2 JavaScript-Generating Methods

BigMed::App::Web offers several methods to help activate the control panel
with a variety of effects and Ajax actions. In particular, these methods
do a lot to help you separate your JavaScript from the body of your markup.

=head3 C<< $app->js_add_behavior($css_selector, $action, $js_function) >>

Adds a JavaScript behavior to a class, element or id. This behavior
is automagically incorporated into the JavaScript template parameters
generated by the C<html_template_screen> method, so any html page generated
by that method and using the default C<shell_header.tmpl> template
will have the behavior enabled.

This adds an event listener via Big Medium's BM.Behavior.register
method. The JavaScript function receives two arguments:

=over

=item * C<e>

The element that generated the event.

=item * C<evt>

The JavaScript event object.

=back

For example:

    $app->js_add_behavior('a', 'click', 'alert(e.href);Event.stop(evt););

...would cause all anchor links to display an alert showing their href
location whenever you click them; C<the Event.stop(evt)> portion of the
JavaScript function prevents the action from completing and taking to another
page.

Multiple behaviors can be added to the same event method of individual
elements. If you do this, though, be sure to include semi-colons at the
end of the JavaScript function string.

If you don't supply values for all three arguments, no behavior is added
(with no warning or exception generated). A true value is returned if
the behavior is added successfully.

The method accepts even relatively complex CSS selectors as the first
argument. For example:

    #add focus behavior to all input
    $app->js_add_behavior('input', 'focus', "alert('any input');");
    
    #add focus behavior to specific class
    $app->js_add_behavior('input.alert', 'focus', "alert('alert class');");

    #add focus to specific input
    $app->js_add_behavior('#alert', 'focus', "alert('alert id');");
    
    #add focus to a specific class inside a specific form
    $app->js_add_behavior('#myForm input.alert'), 'focus',
      "alert('alert class in myForm');" );

=head3 C<< $app->js_add_script($url) >>

    $app->js_add_script('http://www.example.com/script.js');
    $app->js_add_script(['http://www.example.com/script.js', 'if lt IE 7']);

Adds a C<< <script> >> tag to the page's C<< <head> >> element to include
a JavaScript file. If the argument is a string, that string is used in the
C<< <script> >> tag's C<src> attribute. If the argument is an array
reference, the first element is used in the C<src> attribute, and  the
second element is used as an Internet Explorer conditional statement.

These script tags appear in the order in which they are added.

=head3 C<< $app->js_add_code($url) >>

Adds the argument string as JavaScript in the head tag's C<< script >>
tag. Unlike C<js_add_onload> below, which runs the code only after the
page has loaded, the code for C<js_add_code> is run as soon as it's
encountered in the head tag.

The code appears in the html in the order in which it is added.

=head3 C<< $app->js_add_onload($javascript) >>

Adds the argument string as JavaScript to be called as part of the html
C<< <body> >> onload event. Multiple blocks of JavaScript may be
added via this method, and it's good practice to make sure that all
submitted JavaScript ends with a semi-colon.


=head3 C<< $app->js_init_object($object_name) >>

Causes the JavaScript object named in the first argument to be initialized
in the page's C<< <head> >> tag. For example, this:

    $app->js_init_object('foo');

...would add this JavaScript statement to the C<< <script > >> tag
in the page header:

    var foo = new Object;

=head3 C<< $app->js_make_trigger($trigger_id, $target_id) >>

Adds a "trigger" behavioral relationship between two elements of the
html page so that clicking the trigger element causes the trigger to
disappear the target element to appear. The method takes two
arguments: The first is the html ID of the trigger element,
and the second is the html ID of the target element.

The behavior is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

=head3 C<< $app->js_make_toggle($trigger_id, $target_id, $effect) >>

Adds a "toggle" behavioral relationship between two elements of the
html page so that clicking the trigger element causes the target element
to appear if it's hidden and disappear if it's revealed. Every click
of the trigger element causes the target element to change state.

The behavior is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

The optional third argument, $effect, specifies a type of effect to use
for the toggle. If 'slide' is specified, the toggle will slide up and
down. Otherwise, the toggle will fade in and out.

=head3 C<< $app->js_focus_field($field_id) >>

Selects the form input field that should receive focus when the page
loads. (This method is called automatically by the C<prompt_field_ref> and
C<prompt> methods when the C<focus> parameter is true.) Only one field
can receive focus, of course; if C<js_focus_field> is called multiple
times, the field whose ID was most recently "focused" will receive focus.

The behavior is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

=head3 C<< $app->js_make_error_box($id) >>

Gives the element with the specified ID an error display class (a red tint).

The behavior is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

=head3 C<< $app->js_add_validation(%field_info) >>

Adds client-side validation to a html field when its parent form is submitted.
The C<prompt> method calls this method for you automatically (see the
C<no_validation> and C<required> parameters of the C<prompt_field_ref>
method).

Validation is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

The method accepts a hash argument with the following parameters:

=over

=item * id => $field_id

The html id of the field to validate.

=item * type => $validation_type

The type of field, which determines the validation method to be used.

=item * label => The user-friendly name of the field. Should be localized
beforehand.

=item * required => 1

If true, validation will not allow the user to leave the field empty.

=back

=head3 C<< $app->js_init_radio_tabs() >>

Enables the addition of an onload function that initializes any
radio_toggle radio tabs on the page (specifically, it makes sure that
the correct tab is highlighted, the correct fields displayed).

The behavior is enabled for any page whose template is generated by the
C<html_template_screen> method and using the default C<shell_header.tmpl>
template.

=head3 C<< $app->js_show_gmt_help($field_id) >>

Enables the addition of an onload function that activates a time_offset
prompt field with some helpful hints based on the user's current time
zone. Used internally by BigMed::App::Web::Prompt to generate the
time_offset prompt.

Requires a single parameter, the id of the time_offset prompt field
to activate.

=head3 C<< $app->js_make_lists_sortable($container_id) >>

Enables all lists within the container element specified by the
container_id element to be sortable/draggable.

=head3 C<< $app->js_clear_all >>

Clears any previously added javascript behaviors, onload script, and
form validation routines. Useful if you need to switch run modes midstream
and need to clear javascript already queued for the old run mode
(the crash report error mode does this, for example).

=head2 AJAX formatting

Big Medium uses JSON objects to communicate for most AJAX requests. Several
methods are supplied to handle the formatting of these JSON objects.

=head3 C<< $app->ajax_json_response($data_ref) >>

Returns a JavaScript string setting a variable named ajaxJSON to a JSON
object corresponding to the data reference in the argument.
The data reference may be a reference to an unblessed array or hash;
most of Big Medium's client-side JavaScript routines expect a hash.

    my $json = $app->ajax_json_response( { html => '<p>Response</p>' } );
    
    # returns string:
    # ajaxJSON = {"html":"<p>Response</p>"}

The following methods help with the formatting of common responses, calling
C<ajax_json_response> behind the scenes.

=head3 C<< $app->ajax_html($html_string) >>

Returns a JavaScript string appropriate to send back for an AJAX request
expecting a html string. Accepts a string as the argument.

    my $json = $app->ajax_html( '<p>Response</p>' );
    
    # returns string:
    # ajaxJSON = {"html":"<p>Response</p>"}

=head3 C<< $app->ajax_data($data_obj) >>

Returns a JavaScript string appropriate to send back as a data response.
Accepts any data structure, scalar or string as an argument.

    my %data = (
        first_name => 'Joe',
        last_name => 'Smith',
    );
    my $json = $app->ajax_data( \%data );
    
    # returns string:
    # ajaxJSON = {"data":{"first_name":"Joe","last_name":"Smith"}}

=head3 C<< $app->ajax_data_prefab_json(\%json_string) >>

Returns a JavaScript string appropriate to send back as a data response.
Expects a hash reference where the values are already converted into
strings. These strings are passed along as-is.

    my %prefab_data = (
        name => '{"first":"Joe","last":"Smith"}',
        degrees => '["high school","bachelors"]',
    );
    my $json = $app->ajax_data_prefab_json( \%prefab_data );
    
    # returns string:
    # ajaxJSON = {"data":{"name":{"first":"Joe","last":"Smith"},
    # "degrees":["high school","bachelors"]}}

=head3 C<< $app->ajax_error($html_string) >>

Returns a JavaScript string appropriate to send back as an error message

    my $json = $app->ajax_error( '<p>Error message</p>' );
    
    # returns string:
    # ajaxJSON = {"error":"<p>Error message</p>"}

=head3 C<< $app->ajax_system_error() >>

Formats any error messages in the error queue and returns a JSON string
via C<ajax_error>.

=head3 C<< $app->ajax_parse_error(\%error_hash) >>

Formats error messages generated by the C<parse_submission> method and
returns a JSON string via C<ajax_error>:

     my %field = $app->parse_submission(
        {   data_class => 'BigMed::User',
            column     => 'name',
        },
        {   data_class => 'BigMed::User',
            column     => 'password',
        },
    );
    return $app->ajax_parse_error( $field{_ERROR} ) if $field{_ERROR};

=head2 Parsing User Submissions

=head3 C<$app->parse($element_type, $field_name, \%options)>

Parses, cleans and validates the indicated field according to the rules
of the indicated field name (you should be sure that your parse request
matches the element type used by C<prompt> to generate the field the first
time around).

=head3 Custom parse routines

To add your own custom parse routine, use the C<register_parser>
method in your application module's C<setup> method.  See BigMed::App
for details.

=head3 C<$app->require_post()>

Returns a true value if the current request used the POST method; otherwise
returns false value and sets a "requires POST" error message in the error
queue.

=head2 CGI Query Object and Methods

BigMed::App::Web objects contain a CGI.pm object which can be used to obtain
user-submitted parameters or to access all of the various HTML-building
methods normally available to CGI.pm objects

=head3 C<< $app->query >>

Returns the CGI.pm query object which includes the user-submitted values
(via CGI's C<param> method) as well as all of the methods normally available
via CGI.pm. IMPORTANT: In most cases, except where retrieving binary data
like document uploads, you should retrieve query parameters via C<utf8_param>,
which converts and normalizes data into utf8 strings.

    my $q        = $app->query;
    my $value = $q->param('field_name');
    $q->param('field_name', 'new_value');
    my $headline = $q->h1('this is a h1 headline');
    my $select   = $q->popup_menu(
        -name   => 'menu_name',
        -values => [qw/eenie meenie minie/],
        -labels => {
            'eenie'  => 'one',
            'meenie' => 'two',
            'minie'  => 'three'
        },
        -default => 'meenie'
    );

=head3 C<< $app->utf8_param($param_name) >>

    my @values = $app->utf8_param($param_name); #for checkboxes, e.g.
    my $value = $app->utf8_param($param_name);

Returns the utf8-encoded value of the parameter named in the argument.

=head3 C<< $app->clear_utf8_param(@param_names) >>

Deletes the parameter(s) named in the arguments from the utf8_param
cache and the underlying CGI query object.

=head2 Utility Methods

=head3 AJAX Request Catchers

Many of the client-side prompters use AJAX requests to validate and
update information on the fly. BigMed::App::Web provides the following
general-purpose methods to help support fielding those requests.

=head4 C<< $app->ajax_check_uniqueness(%params) >>

Returns an HTML snippet with either a confirmation or error message
for whether a form field contains a valid and unique value. This is
appropriate for data types with unique columns (see BigMed::Data for
details).

The method looks for two parameters in the http query, which should be
submitted by the AJAX request:

=over 4

=item * C<v>

The value to validate

=item * C<id>

The id of the record that contains the data value (the value should
be 0 or an empty string if it is a data value for a new record or
a record that otherwise does not exist).

=back

The routine accepts a hash of parameter values with the following
key/value pairs:

=over 4

=item * class => 'data class name'

The name of the data class (e.g. BigMed::User, BigMed::Site, etc) whose
data you're going to validate.

=item * column => 'column name'

The name of the column in the data class whose data you're going to
validate.

=item * site => $site_id

The site id for the site for which you're requiring a unique value,
only for data types that are not system-wide data types.

=item * field_name => 'display name for the field'

The (unescaped, unlocalized) user-friendly label to describe the field
you're validating (e.g. user name, site name, slug name, etc).

=item * type => 'display name for the record type'

The (unescaped, unlocalized) user-friendly label to describe the
type of record to which this data type belongs (e.g. account, site,
article, webpage, etc).

=back

So, for example, a run mode that uses this method to check that a
new user name is unique would be as simple as this:

    sub rm_check_username {
        $_[0]->ajax_check_uniqueness(
            class      => 'BigMed::User',
            column     => 'name',
            field_name => 'user name',
            type       => 'account',
        );
    }

=head3 Language Localization

=over 4

=item * $app->language('string to localize');

Returns a localized/escaped version of the string.
This is a straight wrapper to the BigMed C<language> method. It's
the same as calling C<< $app->bigmed->language($string) >>.
See the BigMed documentation for details.

=item * $app->language_list(@strings_to_localize);

Returns string containing a html-formatted unordered list of
the localized/escaped strings in the array argument.

This is a straight wrapper to the BigMed C<language> method. It's
the same as calling C<< $app->bigmed->language($string) >>.
See the BigMed documentation for details.

=back

=head3 C<< $app->param() >>

Allows you to get/set instance properties for the application object.

    $app->param('scalar_param', '123'); # set 'scalar_param' to '123'
    my $value = $app->param('some_param'); #get value of 'some_param'

Also, when called in the context of an array, with no parameter name
specified, param() returns an array containing all the parameters which
currently exist:

    my @all_params = $app->param();

The param() method also allows you to set a bunch of parameters at once
by passing in a hash (or hashref):

    $app->param(
        'key1' => 'val1',
        'key2' => 'val2',
        'key3' => 'val3',
    );

Your application can define whatever parameters you like. BigMed system
objects do look for values of two specific parameters, however:

=over 4

=item * C<< $app->param('NO_CONFIG_OK') >>

If true, BigMed will not generate an error message if no configuration file
can be loaded. This is useful for setup wizards and other contexts when
the configuration file may not yet exist.

=item * C<< $app->param('CONFIG') >>

An alternate location for the configuration file. If this returns no
value, then BigMed will try to use a file named C<bm-setup.pl> in the
current working directory.

=back

=head3 HTML-Safe and JS-Safe Strings

=over 4

=item * C<< $app->escape('string to make html-safe') >>

Escapes a string to make it safe for display in html (and xml for that
matter). Makes these changes to a string:

=over 4

=item * & --> &amp;

=item * < --> &lt;

=item * > --> &gt;

=item * " --> &quot;

=back

=item * C<< $app->unescape('remove html entities') >>

Returns an "unescaped" string, reversing the effects of a C<escape>
call by converting a few html entities into their plain-text alternatives:

=over 4

=item * &amp; --> &

=item * &lt; --> <

=item * &gt; --> >

=item * &quot; --> "

=back

=item * C<< $app->js_escape('string to make JavaScript-safe') >>

Escapes a string to make it safe for use in JavaScript strings.
Specifically, it escapes quotes, apostrophes, backslashes, and white space.

It's often the case that you should additionally call the C<escape>
method on JavaScript strings, too; that's not done
in C<js_escape> in case you want to use html in the string.

=back

=head3 Error Methods

BigMed::App::Web is a subclass of BigMed::Error and inherits all of that class's
error methods, allowing the application class and objects to set and
retrieve error values shared by other BigMed modules. The methods:

=over 4

=item * $app->set_error(head=>$headline_key, text=>$text_key)

=item * $app->set_io_error($fh, $action, $filepath, $error)

=item * $app->error()

=item * $app->display_error()

=item * $app->error_html_hash()

=item * $app->clear_error()

=back

For more, see the BigMed::Error documentation.

=head1 SEE ALSO

=over 4

=item * BigMed::App

=item * CGI::Application

=item * CGI.pm

=back

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

