# 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: Config.pm 3233 2008-08-21 12:47:26Z josh $

package BigMed::App::Web::Config;
use strict;
use warnings;
use utf8;
use base qw(BigMed::App::Web::CP);
use Carp;
$Carp::Verbose = 1;
use English qw( -no_match_vars );
use BigMed::DiskUtil (
    'bm_file_path',        'bm_file_chmod',
    'bm_untaint_filepath', 'bm_confirm_dir',
    'bm_write_file',       'bm_copy_dir',
    'bm_copy_file',        'bm_delete_file',
    'bm_dir_permissions',
);

use BigMed::Site;

sub setup {
    my $app = shift;
    $app->set_cp_selected_nav('Settings');
    $app->start_mode('edit-site');
    $app->run_modes(
        'AUTOLOAD' => sub { $_[0]->rm_edit_site() },

        #about info
        'about-you'    => 'rm_about_you',
        'about-verify' => 'rm_about_verify',

        #server settings
        'server'        => 'rm_server',
        'server-verify' => 'rm_server_verify',

        #saving first account from start.pm
        'save-acct' => 'rm_first_site',

        #site settings
        'first-site' => 'rm_first_site',
        'edit-site'  => 'rm_edit_site',
        'new-site'   => 'rm_new_site',
        'clone-site' => 'rm_clone_site',

        #save site settings
        'save-first-site' => 'rm_save_first_site',
        'save-new-site'   => 'rm_save_new_site',
        'save-site'       => 'rm_save_site',

        #site confirmed, cleanup confirm graphics, dispatch to next screen
        'first-site-cleanup' => 'rm_first_site_cleanup',
        'new-site-cleanup'   => 'rm_new_site_cleanup',
        'site-cleanup'       => 'rm_site_cleanup',

        #complete clone process
        'clone-menu'       => 'rm_clone_menu',
        'ajax-apply-clone' => 'rm_ajax_apply_clone',

        #file types
        'filetypes'      => 'rm_filetypes',
        'save-filetypes' => 'rm_save_filetypes',

        #registration
        'register' => 'rm_register',
        'activate' => 'rm_activate',

        #ajax calls along the way...
        'ajax-verify-dir'       => 'rm_ajax_verify_dir',
        'ajax-sitename-avail'   => 'rm_ajax_sitename_available',
        'ajax-make-htmltrace'   => 'rm_ajax_make_htmltrace',
        'ajax-remove-htmltrace' => 'rm_ajax_remove_htmltrace',
        'ajax-confirm-homepage' => 'rm_ajax_confirm_homepage',
        'ajax-confirm-html'     => 'rm_ajax_confirm_html',
    );
    return;
}

sub cgiapp_prerun {    # administrators only
    my $app = shift;
    $app->SUPER::cgiapp_prerun;

    #don't use the CP require_privilege_level, because that looks for a site.
    #all we need to check here is whether user is admin
    #if no user, no problem, they're getting bumped to login page anyway
    my $user = $app->current_user;
    if ( $user && $user->level < 6 ) {
        $app->prerun_mode('not-permitted');
    }
    return;
}

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

sub rm_about_you {
    my $app     = shift;
    my %options = @_;

    # fields for the "about you" fieldset
    my $organization = $app->prompt_field_ref(
        id          => 'organization',
        prompt_as   => 'simple_text',
        label       => 'CONFIG_LABEL_Organization_Name',
        description => 'CONFIG_DESC_Organization_Name',
        required    => 1,
        value => $app->env('ORGNAME'),                #stored value is encoded
    );
    my $admin_email = $app->prompt_field_ref(
        id          => 'admin_email',
        prompt_as   => 'email',
        label       => 'CONFIG_LABEL_Admin_Email',
        required    => 1,
        description => 'CONFIG_DESC_Admin_Email',
        value       => $app->escape( $app->env('ADMINEMAIL') ),
    );
    my $post_html =
        '<div class="bmcpSupportText" style="clear:both";margin-top:1em">'
      . $app->language('CONFIG_About_You_Privacy_Notice')
      . '</div>';

    #the "about you" fieldset itself
    my $fs_about_you = $app->prompt_fieldset_ref(
        fields    => [$organization, $admin_email],
        title     => 'CONFIG_FIELDSET_About_You',
        query     => $options{query},
        field_msg => $options{field_msg},
        post_html => $post_html,
    );

    #run mode and submit fields, and fieldset
    my $submit_text = $options{submit_text} || 'BM_SUBMIT_LABEL_Save';
    my $submit = $app->prompt_field_ref(
        id          => 'about_submit',
        prompt_as   => 'submit',
        value       => $app->language($submit_text),
        no_validate => 1,
    );
    my $fs_submit = $app->prompt_fieldset_ref( fields => [$submit], );

    my $title_unlocal = $options{head} || 'CONFIG_Registration_Info_Title';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );
    _set_breadcrumbs( $app, $title_unlocal, $options{breadcrumbs} );
    my $form_url = $options{form_url}
      || $app->build_url( script => 'bm-config.cgi', rm => 'about-verify' );

    my $template = $options{template} || 'screen_cp_generic.tmpl';
    return $app->html_template_screen(
        $template,
        bmcp_title => $title,
        form_url   => $form_url,
        step1      => 1,                             #for benefit of start.cgi
        show_steps => $options{show_steps},
        message    => $message,
        fieldsets  => [$fs_about_you, $fs_submit],
    );

}

sub rm_about_verify {
    my $app     = shift;
    my %options = @_;                                #received via Start.pm
    my %field   = $app->parse_submission(
        {   id       => 'organization',
            required => 1,
            parse_as => 'simple_text',
        },
        {   id       => 'admin_email',
            required => 1,
            parse_as => 'email',
        },
    );
    if ( $field{_ERROR} ) {
        my %param = (
            head      => 'BM_Please_Review_Your_Entry',
            field_msg => $field{_ERROR},
            query     => $app->query,
        );

        return $options{prev_screen_ref}
          ? $options{prev_screen_ref}->( $app, %param )
          : $app->rm_about_you(%param);
    }

    #store the values
    my $bigmed = $app->bigmed;
    $bigmed->set_env(
        'ORGNAME'    => $field{organization},
        'ADMINEMAIL' => $field{admin_email},
    );

    my $prev_screen = $options{prev_screen_ref} || \&rm_about_you;
    my $next_screen = $options{next_screen_ref}
      || sub {
        $_[0]->_redirect_to_main_menu('BM_Your changes have been saved.');
      };
    return _store_env_values( $app, $prev_screen, $next_screen );
}

sub rm_server {
    my $app     = shift;
    my %options = @_;

    #WEB ADDRESSES FIELDSET -----------------------
    #moxiebin field
    my $domain_url     = _domain_url();
    my $moxiebin_url   = $app->env('MOXIEBIN') || _moxiebin_url($domain_url);
    my $moxiebin_field = $app->prompt_field_ref(
        id          => 'bigmedurl',
        label       => 'CONFIG_LABEL_Moxiebin URL',
        prompt_as   => 'dir_url',
        validate_as => 'moxiebin_url',
        required    => 1,
        focus       => 1,
        ajax_status => 1,
        description => q{CONFIG_DESC_Moxiebin URL},
        value       => $app->escape($moxiebin_url),
    );

    #bmadmin field
    my $bmadmin_url = $app->env('BMADMINURL') || $domain_url . '/bmadmin';
    my $bmadmin_field = $app->prompt_field_ref(
        id          => 'bigmedadmin',
        label       => 'CONFIG_LABEL_BMadmin URL',
        prompt_as   => 'dir_url',
        validate_as => 'bmadmin_url',
        required    => 1,
        ajax_status => 1,
        description => q{CONFIG_DESC_BMadmin URL},
        value       => $app->escape($bmadmin_url),
    );

    #assemble fieldset
    my $web_addr_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_web_addresses',
        fields    => [$moxiebin_field, $bmadmin_field],
        title     => 'CONFIG_LABEL_Web Addresses',
        query     => $options{query},
        field_msg => $options{field_msg},
        help      => 'CONFIG_HELP_Web Addresses',
    );

    #DIRECTORY PATHS FIELDSET ----------------------------
    my ( $moxiedata_rec, $bmadmindir_rec, $moxiedata_text ) =
      _moxiedata_recommendation($app);
    my $moxiedata_dir = $app->env('MOXIEDATA') || $moxiedata_rec;
    my $moxiedata_field = $app->prompt_field_ref(
        id          => 'moxiedata',
        label       => 'CONFIG_LABEL_Moxiedata Directory Path',
        prompt_as   => 'dir_path',
        validate_as => 'moxiedata_path',
        required    => 1,
        ajax_status => 1,
        description => q{CONFIG_DESC_Moxiedata Directory Path},
        value       => $app->escape($moxiedata_dir),
    );
    my $bmadmindir = $app->env('BMADMINDIR') || $bmadmindir_rec;
    my $bmadmindir_field = $app->prompt_field_ref(
        id          => 'bmadmin',
        label       => 'CONFIG_LABEL_bmadmin Directory Path',
        prompt_as   => 'dir_path',
        validate_as => 'bmadmin_path',
        required    => 1,
        ajax_status => 1,
        description => q{CONFIG_DESC_bmadmin Directory Path},
        value       => $app->escape($bmadmindir),
    );

    my $dir_path_help_html =
        $app->language('CONFIG_HELP_Directory Paths')
      . $app->language($moxiedata_text);
    my $dir_path_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_moxiedata',
        fields    => [$moxiedata_field, $bmadmindir_field],
        title     => 'CONFIG_LABEL_Directory Paths',
        query     => $options{query},
        field_msg => $options{field_msg},
        help_html => $dir_path_help_html,
    );

    #EMAIL FIELDSET ----------------------------

    #smtp field
    my $smtp_server = $app->env('SMTPSERVER') || _smtp_server_name();
    my $smtp_field = $app->prompt_field_ref(
        id        => 'smtp_server',
        label     => 'CONFIG_LABEL_smtp server',
        prompt_as => 'simple_text',
        value     => $app->escape($smtp_server),
        ajax_status => 1,    #don't need it, but gives same height as sendmail
        description => 'CONFIG_DESC_smtp server',
    );

    my @email_fields;
    my $email_help;
    if ( index( $OSNAME, 'MSWin' ) < 0 )
    {                        #sendmail option never applies to win

        #sendmail field
        my $sendmail = $app->env('SENDMAILPATH') || _sendmail_location();
        my $sendmail_field = $app->prompt_field_ref(
            id          => 'sendmail',
            label       => 'CONFIG_LABEL_sendmail path',
            prompt_as   => 'simple_text',
            ajax_status => 1,
            value       => $app->escape($sendmail),
            description => 'CONFIG_DESC_sendmail path',
        );

        #toggle choice field
        my $email_choice = $app->prompt_field_ref(
            id        => 'email_method',
            prompt_as => 'radio_toggle',
            label     => 'CONFIG_LABEL_email_method',
            value     => [
                {   id      => 'em_sendmail',
                    label   => 'CONFIG_LABEL_email_method_sendmail',
                    value   => 'sendmail',
                    checked => !$app->env('SMTPSERVER'),
                    field   => $sendmail_field,
                },
                {   id      => 'em_smtp',
                    label   => 'CONFIG_LABEL_email_method_smtp',
                    value   => 'smtp',
                    checked => $app->env('SMTPSERVER'),
                    field   => $smtp_field,
                },
            ],
        );
        @email_fields = ($email_choice);
        $email_help   =
            $app->language('CONFIG_HELP_email_method_unix')
          . $app->language('CONFIG_HELP_email_method_smtp');
    }
    else {
        my $smtp_choice = $app->prompt_field_ref(
            id        => 'email_method',
            prompt_as => 'hidden',
            value     => 'smtp',
        );
        @email_fields = ( $smtp_choice, $smtp_field );
        $email_help = $app->language('CONFIG_HELP_email_method_smtp');
    }

    #assemble fieldset
    my $email_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_email_method',
        fields    => \@email_fields,
        title     => 'CONFIG_LABEL_Email Server',
        query     => $options{query},
        field_msg => $options{field_msg},
        help_html => $email_help,
    );

    #field sets so far...
    my @fieldsets = ( $web_addr_fs, $dir_path_fs, $email_fs );

    #SECURITY FIELDSET ----------------------------
    if ( index( $OSNAME, 'MSWin' ) < 0 ) {    #not relevant for windows
        my $file_chmod     = bm_file_chmod();
        my $security_level =
            $file_chmod == oct(644) ? 'high'
          : $file_chmod == oct(664) ? 'medium'
          : 'low';
        my $security_field = $app->prompt_field_ref(
            id        => 'file_security',
            label     => 'CONFIG_LABEL_file_security',
            prompt_as => 'value_list',
            required  => 1,
            value     => $security_level,
            options   => ['high', 'medium', 'low'],
            labels    => {
                'high' => $app->language('CONFIG_LABEL_file_security: high'),
                'medium' =>
                  $app->language('CONFIG_LABEL_file_security: medium'),
                'low' => $app->language('CONFIG_LABEL_file_security: low'),
            },
            description => 'CONFIG_DESC_file_security',
        );
        my $security_fs = $app->prompt_fieldset_ref(
            id        => 'bmfs_security',
            fields    => [$security_field],
            title     => 'CONFIG_LABEL_File Permissions',
            query     => $options{query},
            field_msg => $options{field_msg},
            help      => 'CONFIG_HELP_File Permissions',
        );
        push @fieldsets, $security_fs;
    }

    #PROXY SERVER FIELDSET ----------------------------
    my $proxy = $app->prompt_field_ref(
        id          => 'proxy_server',
        label       => 'CONFIG_LABEL_proxy_server',
        description => 'CONFIG_DESC_proxy_server',
        prompt_as   => 'url',
        value       => $app->env('PROXY'),
    );
    my $noproxy = $app->prompt_field_ref(
        id          => 'noproxy',
        label       => 'CONFIG_LABEL_noproxy',
        description => 'CONFIG_DESC_noproxy',
        prompt_as   => 'value_freeform',
        value       => $app->env('NO_PROXY') || [],
    );
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id        => 'bmfs_firewall',
        fields    => [$proxy, $noproxy],
        title     => 'CONFIG_LABEL_Firewall',
        query     => $options{query},
        field_msg => $options{field_msg},
        help      => 'CONFIG_HELP_Proxy server',
      );

    #AKISMET ----------------------------
    my $akismet = $app->prompt_field_ref(
        id          => 'akismet_key',
        label       => 'CONFIG_LABEL_akismet_key',
        description => 'CONFIG_DESC_akismet_key',
        prompt_as   => 'simple_text',
        value       => $app->env('AKISMET_KEY'),
    );
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id        => 'bmfs_akismet',
        fields    => [$akismet],
        title     => 'CONFIG_LABEL_Akismet anti-spam service',
        query     => $options{query},
        field_msg => $options{field_msg},
        help      => 'CONFIG_HELP_Akismet',
      );

    #SUBMIT FIELDSET ----------------------------
    my $submit_text = $options{submit_text} || 'BM_SUBMIT_LABEL_Save';
    my $submit = $app->prompt_field_ref(
        id          => 'server_submit',
        prompt_as   => 'submit',
        value       => $app->language($submit_text),
        no_validate => 1,
    );
    my $submit_fs = $app->prompt_fieldset_ref( fields => [$submit], );
    push @fieldsets, $submit_fs;

    #PAGE INFO AND DISPLAY ----------------------------
    my $title_unlocal = $options{head} || 'CONFIG_Server_Info_Title';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );
    _set_breadcrumbs( $app, $title_unlocal, $options{breadcrumbs} );
    my $form_url = $options{form_url}
      || $app->build_url( script => 'bm-config.cgi', rm => 'server-verify' );
    if ( $app->env('BMADMINURL') ) {
        $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-config.js' );
    }

    my $template = $options{template} || 'screen_cp_generic.tmpl';
    return $app->html_template_screen(
        $template,
        bmcp_title => $title,
        form_url   => $form_url,
        step2      => 1,                      #for benefit of start.cgi
        show_steps => $options{show_steps},
        message    => $message,
        fieldsets  => \@fieldsets,
    );
}

sub rm_server_verify {
    my $app     = shift;
    my %options = @_;                         #received via Start.pm
    my $prev_screen = $options{prev_screen_ref} || \&rm_server;
    my $next_screen = $options{next_screen_ref}
      || sub {
        $_[0]->_redirect_to_main_menu('BM_Your changes have been saved.');
      };

    my @parse_fields = (
        {   id       => 'bigmedurl',
            required => 1,
            parse_as => 'dir_url',
        },
        {   id       => 'bigmedadmin',
            required => 1,
            parse_as => 'dir_url',
        },
        {   id       => 'moxiedata',
            required => 1,
            parse_as => 'dir_path',
        },
        {   id       => 'bmadmin',
            required => 1,
            parse_as => 'dir_path',
        },
        {   id       => 'email_method',
            required => 1,
            parse_as => 'raw_text',
        },
        {   id       => 'smtp_server',
            parse_as => 'raw_text',
        },
        {   id       => 'proxy_server',
            parse_as => 'url',
        },
        {   id       => 'noproxy',
            parse_as => 'value_freeform',
            multiple => 1,
        },
        {   id       => 'akismet_key',
            parse_as => 'simple_text',
        },
    );
    if ( index( $OSNAME, 'MSWin' ) < 0 )
    {    #sendmail option never applies to win
        push @parse_fields,
          { id       => 'sendmail',
            parse_as => 'dir_path',
          };
    }

    my %field = $app->parse_submission(@parse_fields);

    #check the sendmail/smtp paths
    my ( $sendmail, $smtp );
    if ( $field{email_method} eq 'sendmail' ) {
        $sendmail = bm_untaint_filepath( $field{sendmail} );
        $smtp     = q{};
        if ( !$sendmail || !-e $sendmail ) {
            $field{_ERROR}->{sendmail} =
              'CONFIG_ERR_TEXTsendmail path does not exist';
        }
    }
    else {
        $smtp                         = $field{smtp_server};
        $sendmail                     = q{};
        $field{_ERROR}->{smtp_server} = 'CONFIG_ERR_HEADsmtp required'
          if !$smtp;
    }

    #check the directories
    if ( $field{moxiedata} ) {
        if ( !-e $field{moxiedata} ) {
            $field{_ERROR}->{moxiedata} = 'CONFIG_Directory does not exist';
        }
        elsif (
            !-e bm_file_path( $field{moxiedata}, 'templates', 'cp_templates' )
          )
        {
            $field{_ERROR}->{moxiedata} = 'CONFIG_Directory not moxiedata';
        }
    }

    if ( $field{bmadmin} ) {
        if ( !-e $field{bmadmin} ) {
            $field{_ERROR}->{bmadmin} = 'CONFIG_Directory does not exist';
        }
        elsif ( !-e bm_file_path( $field{bmadmin}, 'themes' ) ) {
            $field{_ERROR}->{bmadmin} = 'CONFIG_Directory not bmadmin';
        }
    }

    #check for errors and kick back if we've got 'em
    my $q = $app->query;
    if ( $field{_ERROR} ) {
        my %param = (
            head      => 'BM_Please_Review_Your_Entry',
            field_msg => $field{_ERROR},
            query     => $q,
        );
        return $prev_screen->( $app, %param );
    }

    #parse out the file security entries (can't load via
    #parse_submission, because it may not be included in the
    #param string in windows servers)
    my $file_security = $app->utf8_param('file_security') || 'high';
    my ( $file_chmod, $datafile_chmod, $dir_chmod, $datadir_chmod );
    if ( $file_security eq 'low' ) {
        $file_chmod     = '0666';
        $datafile_chmod = '0666';
        $dir_chmod      = '0777';
        $datadir_chmod  = '0777';
    }
    elsif ( $file_security eq 'medium' ) {
        $file_chmod     = '0664';
        $datafile_chmod = '0660';
        $dir_chmod      = '0775';
        $datadir_chmod  = '0770';
    }
    else {
        $file_chmod     = '0644';
        $datafile_chmod = '0600';
        $dir_chmod      = '0755';
        $datadir_chmod  = '0700';
    }

    #store the values
    my $bigmed = $app->bigmed;
    $bigmed->set_env(
        'MOXIEBIN'       => $field{bigmedurl},
        'BMADMINURL'     => $field{bigmedadmin},
        'MOXIEDATA'      => $field{moxiedata},
        'BMADMINDIR'     => $field{bmadmin},
        'FILE_CHMOD'     => $file_chmod,
        'DATAFILE_CHMOD' => $datafile_chmod,
        'DIR_CHMOD'      => $dir_chmod,
        'DATADIR_CHMOD'  => $datadir_chmod,
        'SENDMAILPATH'   => $sendmail,
        'SMTPSERVER'     => $smtp,
        'PROXY'          => $field{proxy_server},
        'NO_PROXY'       => $field{noproxy},
        'AKISMET_KEY'    => $field{akismet_key},
    );

    #reset the template paths; do cache first to avoid template error
    my $cache = bm_file_path( $field{moxiedata}, 'worktemp', 'tmpl_cache' );
    bm_confirm_dir( $cache, { build_path => 1, data => 1 } )
      or return $prev_screen->( $app, query => $app->query );
    $app->param( 'APPWEB_TMPLCACHE', $cache );

    my @tmpl =
      ( bm_file_path( $field{moxiedata}, 'templates', 'cp_templates' ) );
    my $custom = $tmpl[0] . '_custom';
    unshift @tmpl, $custom if -e $custom;
    $app->tmpl_path( \@tmpl );

    return _store_env_values( $app, $prev_screen, $next_screen );
}

sub rm_edit_site {
    my $app     = shift;
    my %options = @_;
    my $site    = $options{site} || $app->current_site || BigMed::Site->new();

    my @fieldsets;

    #SITE NAME FIELDSET ----------------------------
    if ( $options{include_name} ) {    #do this for new sites only
        my $name_field = $app->prompt_field_ref(
            data_class  => 'BigMed::Site',
            column      => 'name',
            required    => 1,
            ajax_status => 1,
            value       => $site->name
              || $app->language('CONFIG_Default site name'),
            focus       => 1,
            description => 'CONFIG_DESC_Site Name',
            validate_as => 'sitename',
        );

        #assemble the fieldset
        push @fieldsets,
          $app->prompt_fieldset_ref(
            id        => 'bmfs_site_name',
            fields    => [$name_field],
            title     => 'CONFIG_LABEL_fs_Site Name',
            query     => $options{query},
            field_msg => $options{field_msg},
            help      => 'CONFIG_HELP_Site name',
          );
    }

    #URL FIELDSET ----------------------------
    my $homepage_url = $site->homepage_url || _domain_url();
    my $home_url_field = $app->prompt_field_ref(
        data_class  => 'BigMed::Site',
        column      => 'homepage_url',
        required    => 1,
        ajax_status => 1,
        value       => $app->escape($homepage_url),
        description => 'CONFIG_DESC_Homepage URL',
    );

    my $page_url = $site->html_url || ( $homepage_url .= '/bm' );
    my $page_url_field = $app->prompt_field_ref(
        data_class  => 'BigMed::Site',
        column      => 'html_url',
        required    => 1,
        ajax_status => 1,
        value       => $app->escape($page_url),
        description => 'CONFIG_DESC_Page URL',
    );

    #assemble the fieldset
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id        => 'bmfs_site_urls',
        fields    => [$home_url_field, $page_url_field],
        title     => 'CONFIG_LABEL_Web Addresses',
        query     => $options{query},
        field_msg => $options{field_msg},
        help      => 'CONFIG_HELP_Web Addresses',
      );

    #DIRECTORY PATH FIELDSET ----------------------------
    my $homepage_path = $site->homepage_dir || _document_root();
    my $home_path_field = $app->prompt_field_ref(
        data_class  => 'BigMed::Site',
        column      => 'homepage_dir',
        required    => 1,
        ajax_status => 1,
        validate_as => 'html_path',
        value       => $app->escape($homepage_path),
        description => 'CONFIG_DESC_Homepage Path',
    );

    my $page_path = $site->html_dir || ( $homepage_path .= '/bm' );
    my $page_path_field = $app->prompt_field_ref(
        data_class  => 'BigMed::Site',
        column      => 'html_dir',
        required    => 1,
        ajax_status => 1,
        validate_as => 'html_path',
        value       => $app->escape($page_path),
        description => 'CONFIG_DESC_Page Path',
    );

    #assemble the fieldset
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id        => 'bmfs_site_paths',
        fields    => [$home_path_field, $page_path_field],
        title     => 'CONFIG_LABEL_Directory Paths',
        query     => $options{query},
        field_msg => $options{field_msg},
        help      => 'CONFIG_HELP_Directory Paths',
      );

    #SUBMIT FIELDSET ----------------------------
    my $submit_text = $options{submit_text} || 'BM_SUBMIT_LABEL_Save';
    my $submit = $app->prompt_field_ref(
        id        => 'site_submit',
        prompt_as => 'submit',
        value     => $app->language($submit_text),
    );
    my $id_field = $app->prompt_field_ref(
        data_class => 'BigMed::Site',
        column     => 'id',
        value      => $site->id || q{},
    );
    my $clone_field = $app->prompt_field_ref(
        id        => 'clone_site_id',
        prompt_as => 'id',
        value     => $options{clone_site_id},
    );
    push @fieldsets,
      $app->prompt_fieldset_ref( fields => [$id_field, $clone_field, $submit],
      );

    #PAGE INFO AND DISPLAY ----------------------------
    my $title_unlocal = $options{head} || 'CONFIG_Edit Site';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );
    _set_breadcrumbs( $app, $title_unlocal, $options{breadcrumbs} );
    my $form_url = $options{form_url}
      || $app->build_url(
        script => 'bm-config.cgi',
        rm     => 'save-site',
        site   => $site->id
      );
    my $template = $options{template} || 'screen_cp_generic.tmpl';
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-config.js' );

    return $app->html_template_screen(
        $template,
        bmcp_title => $title,
        form_url   => $form_url,
        step4      => 1,             #for benefit of startup wizard
        message    => $message,
        fieldsets  => \@fieldsets,
    );

}

sub rm_first_site {
    my $app     = shift;
    my %options = @_;

    my $all_sites = BigMed::Site->select();
    return $app->_redirect_to_first_sections()
      if $all_sites && $all_sites->count;

    my $user = $app->current_user;
    $options{head} ||= 'CONFIG_First Site Info';
    $options{message} ||= ['CONFIG_First Site Message', $user->name];
    $options{submit_text}  = 'START_Submit_Button_Text';
    $options{include_name} = 1;
    $options{form_url}     =
      $app->build_url( script => 'bm-config.cgi', rm => 'save-first-site' );
    $options{template} = 'screen_start_generic.tmpl';
    $options{site}     = BigMed::Site->new();
    return $app->rm_edit_site(%options);
}

sub rm_new_site {
    my $app     = shift;
    my %options = @_;
    $options{head}    ||= 'CONFIG_New Site';
    $options{message} ||= 'CONFIG_New Site Message';
    $options{include_name} = 1;
    $options{form_url}     =
      $app->build_url( script => 'bm-config.cgi', rm => 'save-new-site' );
    $options{site} = BigMed::Site->new();
    return $app->rm_edit_site(%options);
}

sub rm_clone_site {
    my $app     = shift;
    my %options = @_;
    my $site    = $app->current_site;
    $options{head} ||= 'CONFIG_Clone Site';
    $options{message} ||= ['CONFIG_Clone Site Message', $site->name];
    $options{clone_site_id} = $site->id;
    return $app->rm_new_site(%options);
}

sub rm_save_site {
    my $app     = shift;
    my %options = @_;
    $options{prev_screen_ref} ||= \&rm_edit_site;

    my @fields;
    if ( $options{include_name} ) {    #for new sites
        push @fields,
          { data_class => 'BigMed::Site',
            column     => 'name',
            required   => 1,
          };
    }

    push @fields,
      ( {   data_class => 'BigMed::Site',
            column     => 'id',
        },
        {   parse_as => 'id',
            id       => 'clone_site_id',
        },
        {   data_class => 'BigMed::Site',
            column     => 'homepage_url',
            required   => 1,
        },
        {   data_class => 'BigMed::Site',
            column     => 'html_url',
            required   => 1,
        },
        {   data_class => 'BigMed::Site',
            column     => 'homepage_dir',
            required   => 1,
        },
        {   data_class => 'BigMed::Site',
            column     => 'html_dir',
            required   => 1,
        },
      );
    my %field = $app->parse_submission(@fields);
    if ( $field{_ERROR} ) {    #caught validation errors
        return $app->_return_to_rm(
            $options{prev_screen_ref},
            field_msg => $field{_ERROR},
            query     => $app->query
        );
    }

    #construct/populate the site object
    my $site;
    if ( $field{site_id} ) {
        if ( !defined( $site = BigMed::Site->fetch( $field{site_id} ) ) ) {
            return $app->_return_to_rm( $options{prev_screen_ref},
                query => $app->query );
        }
    }
    if ( !$site ) {    #either requested site doesn't exist, or new site
        $site = BigMed::Site->new();
    }
    
    #grab original home/html directories to move them later
    my $o_homedir = $site->homepage_dir;
    my $o_htmldir = $site->html_dir;
    $options{is_new} = $site->id ? 0 : 1;    #determines run mode later

    #get site for cloning if appropriate
    my $osite;
    if ( $field{clone_site_id} && $options{is_new} ) {
        $osite = BigMed::Site->fetch( $field{clone_site_id} );
        if ( !$osite ) {
            $app->set_error(
                head => 'BM_No such site',
                text => ['BM_TEXT_No such site', $field{clone_site_id}],
            );
            return $app->_return_to_rm( $options{prev_screen_ref},
                query => $app->query );
        }
        $options{clone_site_id} = $field{clone_site_id};
    }

    my @update_cols = qw(homepage_url html_url homepage_dir html_dir);
    push @update_cols, 'name' if $options{include_name};
    foreach my $value (@update_cols) {
        my $setter = 'set_' . $value;
        $site->$setter( $field{$value} );
    }

    #have to have a name; if there's data trouble and requested site
    #doesn't exist, for example, can run into trouble since name
    #isn't provided unless include_name flag is on.
    $site->set_name('My Website') if !defined $site->name;

    #check unique values
    my $unique;
    foreach my $value (qw(name homepage_dir html_dir)) {
        if ( !defined( $unique = $site->is_unique($value) ) ) {    #error
            return $app->_return_to_rm( $options{prev_screen_ref},
                query => $app->query );
        }
        if ( !$unique ) {
            $field{_ERROR}->{$value} = [
                'BM_Not unique',
                $app->language("site:$value"),
                $app->language('CONFIG_site')
            ];
            $field{_ERROR}->{_FIRST_ERR} ||= $value;
        }
    }
    if ( $field{_ERROR} ) {    #one or more non-unique values
        return $app->_return_to_rm(
            $options{prev_screen_ref},
            field_msg => $field{_ERROR},
            query     => $app->query
        );
    }

    #apply original site's settings if cloning
    if ($osite) {
        $site->set_time_offset( $osite->time_offset );
        $site->set_flags( { $osite->flags } );
        $site->set_site_doclimit( $osite->site_doclimit );
        $site->set_date_format( $osite->date_format );
        $site->set_time_format( $osite->time_format );
    }

    #calculate server's timezone offset if necessary
    if ( !defined $site->time_offset ) {
        my ( $hour, $day, $mon, $year ) = ( localtime() )[2 .. 5];
        my ( $gmt_hour, $gmt_day, $gmt_mon, $gmt_year ) =
          ( gmtime(time) )[2 .. 5];
        if (   $gmt_year < $year
            || ( $gmt_year == $year && $gmt_mon < $mon )
            || ( $gmt_year == $year && $gmt_mon == $mon && $gmt_day < $day ) )
        {

            #it's the day before in gmt time
            $hour += 24;
        }
        elsif ($gmt_year > $year
            || ( $gmt_year == $year && $gmt_mon > $mon )
            || ( $gmt_year == $year && $gmt_mon == $mon && $gmt_day > $day ) )
        {

            #it's the day after in gmt time
            $gmt_hour += 24;
        }

        my $hour_offset = ( $gmt_hour - $hour ) * -1;
        my $sign        = q{+};
        if ( $hour_offset < 0 ) {
            $sign = q{-};
            $hour_offset *= -1;
        }
        $hour = int($hour_offset);
        my $minute = ( $hour_offset - $hour ) * 60;
        $site->set_time_offset(
            $sign . $hour . q{:} . sprintf( '%02d', $minute ) );
    }

    #make sure the directories are good and remove trace files if still around
    #(trace files should normally get removed via ajax, but they could linger
    #in the case of fast field tabbing).
    foreach my $dir qw(homepage_dir html_dir) {
        my $dirpath = $site->$dir;
        bm_confirm_dir($dirpath);
        my $trace =
          bm_untaint_filepath(
            bm_file_path( $dirpath, 'bm_' . $dir . '.txt' ) );
        unlink $trace if ( -e $trace );
    }
    if ( $app->error ) {
        return $app->_return_to_rm( $options{prev_screen_ref},
            query => $app->query );
    }

    #confirm that we have our test images
    my %test_image = $app->_locate_test_images
      or return $app->_return_to_rm( $options{prev_screen_ref},
        query => $app->query );

    #copy the test images, and kick back if there's a problem.
    my $random = int( rand(999_999) ) + 1;
    $test_image{'t_html_dir'} =
      bm_file_path( $site->html_dir, "html_dir$random.jpg" );
    $test_image{'t_homepage_dir'} =
      bm_file_path( $site->homepage_dir, "homepage_dir$random.jpg" );
    foreach my $dirtype (qw(homepage_dir html_dir)) {
        if (!bm_copy_file(
                $test_image{ 's_' . $dirtype },
                $test_image{ 't_' . $dirtype }
            )
          )
        {
            $field{_ERROR}->{$dirtype} = ( $app->error )[0]->{text};
            $app->clear_error;
        }
    }
    if ( $field{_ERROR} ) {
        unlink $test_image{'t_html_dir'} if -e $test_image{'t_html_dir'};
        unlink $test_image{'t_homepage_dir'}
          if -e $test_image{'t_homepage_dir'};
        return $app->_return_to_rm(
            $options{prev_screen_ref},
            field_msg => $field{_ERROR},
            query     => $app->query
        );
    }

    #data looks good; save and copy assets over to html directory
    $site->save
      or return $app->_return_to_rm( $options{prev_screen_ref},
        query => $app->query );
    $app->set_current_site($site);

    #copy original directories to new location if necessary
    if ( $o_homedir || $o_htmldir ) {
        $site->move_public_files(
            { orig_homepage_dir => $o_homedir, orig_html_dir => $o_htmldir } )
          or return $app->_return_to_rm( $options{prev_screen_ref},
            query => $app->query );
    }

    #copy assets over to the html directory, and ensure that
    #the support assets have public permissions for reading
    my $orig_assets =
      bm_file_path( $app->env('MOXIEDATA'), 'support', 'assets' );
    my $site_assets = bm_file_path( $site->html_dir, 'bm.assets' );
    bm_copy_dir( $orig_assets, $site_assets )
      or return $app->_return_to_rm( $options{prev_screen_ref},
        query => $app->query );
    bm_dir_permissions($site_assets)
      or return $app->_return_to_rm( $options{prev_screen_ref},
        query => $app->query );

    #build the CSS file
    require BigMed::CSS;
    BigMed::CSS->build_sheet($site)
      or return $app->_return_to_rm( $options{prev_screen_ref},
        query => $app->query );

    #flip to confirmation screen
    return $app->_screen_confirm_site_dirs( %options, random => $random );
}

sub rm_save_first_site {
    my $app     = shift;
    my %options = @_;
    $options{include_name} = 1;
    $options{submit_text}  = 'START_Submit_Button_Text';
    $options{next_url}     =
      { script => 'bm-config.cgi', rm => 'first-site-cleanup' };
    $options{template}        = 'screen_start_confirm-site-dirs.tmpl';
    $options{prev_screen_ref} = \&rm_first_site;
    return $app->rm_save_site(%options);
}

sub rm_save_new_site {
    my $app     = shift;
    my %options = @_;
    $options{include_name}    = 1;
    $options{prev_screen_ref} = \&rm_new_site;
    return $app->rm_save_site(%options);
}

sub rm_new_site_cleanup {
    my $app = shift;
    return $app->rm_site_cleanup(
        next_screen_ref => \&_redirect_to_new_sections );
}

sub rm_first_site_cleanup {
    my $app = shift;
    return $app->rm_site_cleanup(
        next_screen_ref => \&_redirect_to_first_sections );
}

sub rm_site_cleanup {
    my $app     = shift;
    my %options = @_;
    my $site    = $app->current_site or return $app->rm_login_site();

    #remove any test graphics in the html and page directories
    _cleanup_test_files($site);

    return $options{next_screen_ref}->($app) if $options{next_screen_ref};

    my $build_all = $app->build_url(
        script => 'bm-build.cgi',
        rm     => 'menu',
        site   => $site->id,
    );
    return $app->_redirect_to_main_menu(
        'SECTIONS_Changes saved, rebuild pages', $build_all );
}

sub _cleanup_test_files {
    my $site = shift;
    foreach my $dir ( $site->html_dir, $site->homepage_dir ) {
        $dir = bm_untaint_filepath($dir);
        next unless $dir;
        my $DIR;
        opendir $DIR, $dir or next;
        my @files =
          map { bm_file_path( $dir, $_ ) }
          grep { m{^(homepage|html)_dir\d+[.]jpg$}ms } readdir $DIR;
        foreach my $file (@files) {
            bm_delete_file($file) or next;
        }
    }
    return;
}

sub rm_clone_menu {
    my $app     = shift;
    my %options = @_;

    my $site = $app->current_site or return $app->rm_login_site();
    _cleanup_test_files($site);
    my $osite = $app->_get_orig_id_for_clone($site) or $app->error_stop;

    #build form to ask about content/privilege prefs
    my $content_field = $app->prompt_field_ref(
        prompt_as    => 'boolean',
        id           => 'clone_content',
        label        => 'CONFIG_Clone Content',
        option_label => 'CONFIG_Copy all content and libraries',
    );
    my $comment_field = $app->prompt_field_ref(
        prompt_as    => 'boolean',
        id           => 'clone_comments',
        label        => 'CONFIG_Clone Comments',
        option_label => 'CONFIG_Include comments with copied pages',
    );
    my $priv_field = $app->prompt_field_ref(
        prompt_as    => 'boolean',
        id           => 'clone_privileges',
        label        => 'CONFIG_Clone Privileges',
        option_label => 'CONFIG_Give users same privileges',
    );
    my $submit = $app->prompt_field_ref(
        id        => 'site_submit',
        prompt_as => 'submit',
        value     => $app->language('CONFIG_Clone it!'),
    );
    my $clone_field = $app->prompt_field_ref(
        id        => 'clone_site_id',
        prompt_as => 'id',
        value     => $osite->id,
    );

    #assemble the fieldset
    my @fieldsets = (
        $app->prompt_fieldset_ref(
            id     => 'bmfs_clone_prefs',
            fields => [
                $content_field, $comment_field, $priv_field,
                $submit,        $clone_field
            ],
            query     => $options{query},
            field_msg => $options{field_msg},
        )
    );

    #PAGE INFO AND DISPLAY ----------------------------
    my $title_unlocal = $options{head} || 'CONFIG_Clone Preferences';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message} || 'CONFIG_Clone preference text',
        title     => $title_unlocal,
    );
    _set_breadcrumbs( $app, $title_unlocal, $options{breadcrumbs} );
    my $form_url = $options{form_url}
      || $app->build_url(
        script => 'bm-config.cgi',
        rm     => 'ajax-apply-clone',
        site   => $site->id,
        args   => [$osite->id],
      );
    my $template = $options{template} || 'screen_cp_generic.tmpl';
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-copyxsite.js' );

    return $app->html_template_screen(
        'screen_editor_copyxsite.tmpl',
        bmcp_title => $title,
        form_url   => $form_url,
        message    => $message,
        fieldsets  => \@fieldsets,
    );
}

sub rm_ajax_apply_clone {
    my $app   = shift;
    my $site  = $app->current_site;
    my ($oid) = $app->path_args;
    $oid =~ s/\D//g;

    require BigMed::Status;
    my $statusbar = BigMed::Status->new();

    my %param = $app->parse_submission(
        {   id       => 'clone_content',
            parse_as => 'boolean',
        },
        {   id       => 'clone_comments',
            parse_as => 'boolean',
        },
        {   id       => 'clone_privileges',
            parse_as => 'boolean',
        },
        {   id       => 'clone_site_id',
            parse_as => 'id',
            required => 1,
        },
    );
    return $app->ajax_parse_error( $param{_ERROR} ) if $param{_ERROR};

    #collect original site to clone
    if ( !$oid || $oid != $param{clone_site_id} ) {
        $app->set_error(
            head => 'ACCOUNT_ERR_Missing data',
            text => 'ACCOUNT_ERR_TEXT_Missing data',
        );
        $statusbar->send_error();
    }
    my $osite = $app->_get_orig_id_for_clone($site) or $statusbar->send_error;

    my ( @libraries, @ctypes );
    if ( $param{clone_content} ) {
        @libraries = BigMed::Plugin->load_library_types();
        @ctypes    = BigMed::Plugin->load_content_types();
    }
    my $progress = 0;
    my $steps    = 3;    #structure, index, rebuild pages
    $steps++ if $param{clone_privileges};
    if ( @libraries + @ctypes ) {
        $steps += @libraries + @ctypes + 1;    #+1 for urls
    }

    $statusbar->update_status(
        message  => 'CONFIG_Cloning site structure and theme',
        progress => $progress,
        steps    => $steps,
    );
    my $rmap = $osite->clone_to_site($site) or $statusbar->send_error();

    if ( $param{clone_privileges} ) {
        $statusbar->update_status(
            message  => 'CONFIG_Cloning user privileges',
            progress => ++$progress,
        );
        require BigMed::Priv;
        my $all_priv = BigMed::Priv->select( { site => $osite->id } )
          or $statusbar->send_error();
        my $priv;
        while ( $priv = $all_priv->next ) {
            my $clone =
              $priv->copy( { target_site => $site, source_site => $osite } )
              or $statusbar->send_error;
            next if !$clone->user;
            my $user = BigMed::User->fetch( $clone->user ) or next;
            my @sections = grep { $_ } map { $rmap->{$_} } $clone->sections;
            $clone->set_sections( \@sections );
            $clone->save or $statusbar->send_error;
            $app->log( 'info' => 'Config: Saved clone '
                  . $app->log_data_tag($priv)
                  . ' for user '
                  . $app->log_data_tag($user) );
        }
        $statusbar->send_error if !defined $priv;
    }

    #copy all content, if applicable
    my %page_map;
    my $omit_rel = $param{clone_comments} ? undef: ['comment'];
    foreach my $class ( @libraries, @ctypes ) {
        my $is_page = $class->isa('BigMed::Content::Page');

        #process everything in order of oldest mod first
        my $all = $class->select( { site => $osite->id },
            { 'sort' => 'mod_time', 'order' => 'ascend' } )
          or return $app->ajax_system_error;
        my $total = $all->count;
        my $type  = $app->language( 'TITLE_PL_' . $class->data_label );
        my $i     = 0;
        $statusbar->update_status(
            message  => ['CONFIG_Cloning object', $type, "$total"],
            progress => ++$progress,
        );
        my $obj;
        while ( $obj = $all->next ) {
            $i++;
            $statusbar->update_status(
                message => ['CONFIG_Cloning object', $type, "$i/$total"], );

            my $clone = $obj->copy(
                {   target_site => $site,
                    source_site => $osite,
                    omit_rel    => $omit_rel,
                }
              )
              or $statusbar->send_error();
            $clone->save or $statusbar->send_error();
            my $clonetag = $app->log_data_tag($clone);
            $app->log( 'info' => "Config: Saved clone $clonetag" );
            $page_map{ $obj->id } = $clone->id; #for related url updates later

            #replace original, empty section page with copy
            if ( $is_page && $obj->subtype eq 'section' ) {
                my $sec = $site->section_obj_by_id( $clone->sections )
                  or next;
                my $old = $sec->section_page_obj;
                $old->set_pub_status('draft');    #avoid rebuild on trash
                $sec->set_page( $clone->id );
                if (   !defined $old
                    || !$sec->save
                    || ( $old && !$old->trash ) )
                {
                    my $objtag = $app->log_data_tag($obj);
                    $app->log( 'error' =>
                          "Config: Error replacing section page $objtag with $clonetag"
                    );
                    $statusbar->send_error();
                }
            }

        }
        $statusbar->send_error() if !defined $obj;
    }
    undef $rmap;    #don't need our section map anymore

    if (%page_map) {    #update related URLs
        $progress++;
        my $osite_id = $osite->id;
        my $nsite_id = $site->id;
        my $regmatch = qr{\Abm://\Q$osite_id\E/\d+\z};
        my $all_url  =
          BigMed::URL->select( { site => $site->id, url => $regmatch } )
          or $statusbar->send_error;
        my $num_urls = $all_url->count;
        my $count    = 0;
        my $url_obj;

        while ( $url_obj = $all_url->next ) {
            if ( $count % 25 == 0 ) {
                $statusbar->update_status(
                    message =>
                      ['CONFIG_Updating related URLs', "$count/$num_urls"],
                    progress => $progress,
                );
            }
            $count++;

            my $url_tag = $app->log_data_tag($url_obj);
            my $url = $url_obj->url || q{};
            my ($id) = ( $url =~ m{/(\d+)\z} );
            if ( !$id || !$page_map{$id} ) {
                $app->log( 'warn' =>
                      "Config: Removing URL object $url_tag; unknown page" );
                $url_obj->trash or $statusbar->send_error();
            }
            $url_obj->set_url("bm://$nsite_id/$page_map{$id}");
            $url_obj->save() or $statusbar->send_error();
            $app->log(
                'info' => "Config: Updated $url_tag for new internal URL" );
        }
        $statusbar->send_error() if !defined $url_obj;
    }
    %page_map = ();    #don't need it anymore

    #index pages for clone site
    $statusbar->update_status( progress => ++$progress );
    _index_all_pages( $site, $statusbar ) or $statusbar->send_error();

    #build all pages
    require BigMed::Builder;
    $statusbar->update_status(
        message  => 'CONFIG_Building site',
        progress => ++$progress,
    );
    my $rupdate_section = sub {
        my $builder = shift;
        my $cx      = $builder->context or return 1;
        my $count   = $cx->content->count;
        my $section = $cx->section || $cx->site->homepage_obj or return 1;
        my $name    = $section->name;
        return $statusbar->update_status(
            message => ['CONFIG_Building section', $name, $count], );
    };
    my $builder = BigMed::Builder->new( site => $site ) or return;
    $builder->add_trigger( 'fresh_section_context', $rupdate_section );
    $builder->add_trigger( 'level_midbuild',
        sub { return $statusbar->ping } );
    $statusbar->send_error() if !$builder->build();

    $statusbar->mark_done();
    $app->teardown();
    exit(0);
}

sub _index_all_pages {
    my ( $site, $statusbar ) = @_;

    my @active = ( $site->homepage_id, $site->all_active_descendants_ids() );
    my $all_active = BigMed::Content::Page->select(
        {   site       => $site->id,
            pub_status => 'published',
            sections   => \@active
        }
      )
      or return;

    require BigMed::Search;
    require BigMed::Format::HTML;    #for language pref
    my $lang     = $site->get_pref_value('html_htmlhead_lang') || 'en-us';
    my $sitename = $site->name;
    my $total    = $all_active->count;
    my $rping    = sub {
        my $indexer = shift;
        $statusbar->update_status(
            message => [
                'SEARCH_Updating search index', $sitename,
                $indexer->page_progress,        $total,
                $indexer->word_progress,
            ]
        );
        return 1;
    };
    my $search = BigMed::Search->new( locale => $lang ) or return;
    $search->indexer->add_trigger( 'mid_index', $rping );
    return $search->index_page($all_active);
}

sub _get_orig_id_for_clone {
    my ( $app, $site ) = @_;
    my ($oid) = $app->path_args;

    #target site cannot have any sections
    if ( $site->homepage_obj ) {
        return $app->set_error(
            head => 'CONFIG_HEAD_Cloned site has home',
            text => 'CONFIG_Cloned site has home',
        );
    }

    $oid =~ s/\D//g;
    $oid ||= 0;
    my $osite = $oid ? BigMed::Site->fetch($oid) : q{};
    if ( !$osite ) {
        return $app->set_error(
            head => 'BM_No such site',
            text => ['BM_TEXT_No such site', $oid],
        );
    }
    if ( $osite->id == $site->id ) {
        return $app->set_error(
            head => 'CONFIG_HEAD_Cannot clone same site',
            text => 'CONFIG_Cannot clone same site',
        );
    }
    return $osite;
}

sub rm_filetypes {
    my $app        = shift;
    my %options    = @_;
    my $file_types = $app->env('FILETYPES') || [];

    require BigMed::FileTypes;
    my %categories = BigMed::FileTypes->categories;
    my %has_cat    = BigMed::FileTypes->flag_categories($file_types);

    my @fieldsets;

    my $filesize = $app->prompt_field_ref(
        id        => 'filesize',
        prompt_as => 'kilobytes',
        value     => $app->env('ADMIN_DOCLIMIT') || 5 * 1024,
        label     => 'CONFIG_LABEL_FILETYPE Maximum File Size',
        required  => 1,
    );
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id       => 'bmfs_max_Size',
        fields   => [$filesize],
        title    => 'CONFIG_LABEL_FILETYPE Size Limit',
        pre_html =>
          $app->language('CONFIG_DESC_FILETYPE System max file size'),
        query     => $options{query},
        field_msg => $options{field_msg},
      );

    foreach my $cat ( sort keys %categories ) {
        my $rtypes  = $categories{$cat};
        my @options = sort keys %{$rtypes};
        my %icon    = map { $_ => $rtypes->{$_}->[0] } @options;
        my %labels  =
          map {
                $_ => qq|<span class="docIconSm_$icon{$_} docIconSm">|
              . $app->language("CONFIG_LABEL_FILETYPE $_")
              . '</span>'
          } @options;

        my $field = $app->prompt_field_ref(
            id        => "fieldtype_$cat",
            prompt_as => 'key_boolean',
            label     => 'CONFIG_LABEL Allowed file types',
            options   => \@options,
            labels    => \%labels,
            value     => $has_cat{$cat},
        );

        push @fieldsets,
          $app->prompt_fieldset_ref(
            id        => "bmfs_$cat",
            fields    => [$field],
            title     => "CONFIG_LABEL_FILETYPE $cat",
            query     => $options{query},
            field_msg => $options{field_msg},
          );
    }

    my @addl_values = $has_cat{_ADDL} ? sort keys %{ $has_cat{_ADDL} } : ();
    my $explanation =
        $app->language('CONFIG_LABEL_FILETYPE_DESC additional explanation')
      . '<br /><br />'
      . $app->language('CONFIG_LABEL_FILETYPE_DESC additional warning');
    my $additional = $app->prompt_field_ref(
        id          => 'fieldtype_additional',
        prompt_as   => 'value_freeform',
        value       => \@addl_values,
        label       => 'CONFIG_LABEL_FILETYPE additional extensions',
        description => 'CONFIG_LABEL_FILETYPE_DESC additional',
    );
    push @fieldsets,
      $app->prompt_fieldset_ref(
        id        => 'bmfs_additional',
        fields    => [$additional],
        title     => 'CONFIG_LABEL_FILETYPE additional',
        pre_html  => $explanation,
        query     => $options{query},
        field_msg => $options{field_msg},
      );

    #SUBMIT FIELDSET ----------------------------
    my $submit = $app->prompt_field_ref(
        id          => 'filetype_submit',
        prompt_as   => 'submit',
        value       => $app->language('BM_SUBMIT_LABEL_Save'),
        no_validate => 1,
    );
    push @fieldsets, $app->prompt_fieldset_ref( fields => [$submit], );

    #PAGE INFO AND DISPLAY ----------------------------
    my $title_unlocal = $options{head} || 'CONFIG_Filetypes_Title';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => 'CONFIG_Filetypes_Intro',
        title     => 'CONFIG_Filetypes_Title',
    );
    _set_breadcrumbs( $app, 'CONFIG_Filetypes_Title' );
    my $form_url = $options{form_url}
      || $app->build_url( script => 'bm-config.cgi', rm => 'save-filetypes' );

    return $app->html_template_screen(
        'screen_cp_generic.tmpl',
        bmcp_title => $title,
        form_url   => $form_url,
        message    => $message,
        fieldsets  => \@fieldsets,
    );
}

sub rm_save_filetypes {
    my $app = shift;

    #collect the field info
    my @fields = (
        {   id       => 'filesize',
            parse_as => 'kilobytes',
            required => 1,
        },
        {   id       => 'fieldtype_additional',
            parse_as => 'value_freeform',
        },

    );
    require BigMed::FileTypes;
    my %categories = BigMed::FileTypes->categories;
    foreach my $cat ( sort keys %categories ) {
        push @fields,
          { id       => "fieldtype_$cat",
            parse_as => 'key_boolean',
          };
    }
    my %field = $app->parse_submission(@fields);
    return $app->rm_filetypes( query => $app->query,
        field_msg => $field{_ERROR} )
      if $field{_ERROR};

    #additional types must be a-z only.
    my @accepted;
    foreach my $ext ( @{ $field{fieldtype_additional} } ) {
        $ext =~ s/[^a-zA-Z0-9]//msg;
        push( @accepted, lc $ext ) if $ext ne '';
    }

    #collect the checked items
    foreach my $cat ( sort keys %categories ) {
        foreach my $type ( sort keys %{ $field{"fieldtype_$cat"} } ) {
            push @accepted, @{ $categories{$cat}->{$type} };
        }
    }

    #remove dupes and sort
    my %final = map { $_ => 1 } @accepted;

    my $bm = $app->bigmed;
    $bm->set_env( 'ADMIN_DOCLIMIT', $field{filesize} );
    $bm->set_env( 'FILETYPES', [sort keys %final] );
    $bm->save_config or return $app->rm_filetypes( query => $app->query );

    return $app->_redirect_to_main_menu('BM_Your changes have been saved.');
}

sub rm_register {
    my $app     = shift;
    my %options = @_;
    $app->session->param('_BMLOGIN_first_login', 1);

    my $activate_url = $app->build_url(
        script => 'bm-config.cgi',
        site   => 'x',
        rm     => 'activate',
    );

    my @fields = (
        $app->prompt_field_ref(
            id          => 'number',
            label       => 'CONFIG_LABEL_License_Number',
            prompt_as   => 'simple_text',
            required    => 1,
            validate_as => 'license',
            description => 'CONFIG_DESC_License_Number',
            focus       => 1,
            value => $app->env('REGNUM'),    #stored value is encoded
        ),
        $app->prompt_field_ref(
            id    => 'email',
            label => 'CONFIG_LABEL_Register Email',
            ,
            prompt_as   => 'email',
            required    => 1,
            description => => 'CONFIG_DESC_Register Email',
            value       => $app->escape( $app->env('REGEMAIL') ),
        ),
        $app->prompt_field_ref(
            id        => 'activate_url',
            prompt_as => 'hidden',
            value     => $app->escape($activate_url),
        ),
        $app->prompt_field_ref(
            id        => 'version',
            prompt_as => 'hidden',
            value     => $app->escape( $app->bigmed->VERSION ),
        ),
    );
    my $post_html =
        '<div class="bmcpSupportText">'
      . $app->language('CONFIG_Register_Privacy_Notice')
      . '</div>';

    my $fieldset = $app->prompt_fieldset_ref(
        id        => 'bmfs_register_info',
        fields    => \@fields,
        title     => 'CONFIG_LABEL_Registration Info',
        query     => $options{query},
        field_msg => $options{field_msg},
        post_html => $post_html,
    );

    #submit fieldset
    my $submit = $app->prompt_field_ref(
        id        => 'register_submit',
        prompt_as => 'submit',
        value     => $app->language('BM_SUBMIT_LABEL_Continue'),
    );
    my $submit_fs = $app->prompt_fieldset_ref( fields => [$submit] );

    #PAGE INFO AND DISPLAY ----------------------------
    my $this_title   = 'CONFIG_REGISTER_Register Big Medium';
    my $this_message = 'CONFIG_REGISTER_registration_intro';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message} || $this_message,
        title     => $options{head} || $this_title,
    );
    _set_breadcrumbs( $app, 'CONFIG_REGISTER_Register Big Medium' );
    my $form_url = 'http://globalmoxie.com/cgi-bin/license/activate.cgi';

    return $app->html_template_screen(
        'screen_config_register.tmpl',
        bmcp_title => $title,
        form_url   => $form_url,
        message    => $message,
        fieldsets  => [$fieldset, $submit_fs,],
    );
}

sub rm_activate {
    my $app = shift;
    my %field = $app->parse_submission(
        {   id     => 'lic',
            parse_as => 'simple_text',
            required   => 1,
        },
        {   id     => 'e',
            parse_as => 'email',
            required   => 1,
        },
        {   id     => 'c',
            parse_as => 'simple_text',
            required   => 1,
        },
    );
    $app->session->param('_BMLOGIN_first_login', 1);
    if ($field{_ERROR} || length $field{lic} != 24 || length $field{c} != 32 ) {
        $app->error_set(
            head => 'CONFIG_Invalid activation response',
            text => 'CONFIG_TEXT_Invalid activation response',
        );
        return $app->error_stop;
    }
    my $bm = $app->bigmed;
    $bm->set_env(
        'REGNUM' => $field{lic},
        'REGEMAIL' => $field{e},
        'RCONF' => $field{c},
    );
    $bm->save_config() or $app->error_stop;
    $app->header_type('redirect');
    $app->header_props(
        -url => $app->build_url(
            script => 'bm-editor.cgi',
            rm => 'menu',
        ),
    );
    return 'Redirecting...';
}


sub rm_ajax_verify_dir {
    my $app   = shift;
    my %field = $app->parse_submission(
        {   id       => 'd',
            required => 1,
            parse_as => 'dir_path',
        },
        {   id       => 't',
            required => 1,
            parse_as => 'raw_text',
        },
    );
    my $dir = $field{d};
    my $type = $field{t} || q{};
    if ( $field{_ERROR} && $field{_ERROR}->{d} ) {
        return $app->ajax_error( $app->language( $field{_ERROR}->{d} ) );
    }

    if ( -e $dir && -d $dir ) {
        my ( $checkdir, $not_found, $checkfile );
        if ( $type eq 'moxiedata' ) {
            $checkdir = bm_file_path( $dir, 'templates', 'cp_templates' );
            $not_found = 'CONFIG_Directory not moxiedata';
        }
        elsif ( $type eq 'bmadmin' ) {
            $checkdir = bm_file_path( $dir, 'themes' );
            $not_found = 'CONFIG_Directory not bmadmin';
        }
        elsif ( $type eq 'v1moxiedata' ) {
            $checkfile = bm_file_path( $dir, 'sites', 'siteindex.cgi' );
            $not_found = 'CONFIG_Directory not moxiedata';
        }
        else {
            my $html =
              $app->html_template( 'wi_ajax_status_ok.tmpl',
                STATUS => $app->language('CONFIG_Directory confirmed') );
            return $app->ajax_html($html);
        }

        if (   ( $checkdir && -e $checkdir && -d $checkdir )
            || ( $checkfile && -e $checkfile ) )
        {
            my $html =
              $app->html_template( 'wi_ajax_status_ok.tmpl',
                STATUS => $app->language('CONFIG_Directory confirmed'), );
            return $app->ajax_html($html);
        }
        else {
            return $app->ajax_error( $app->language($not_found) );
        }

    }
    elsif ( -e $dir ) {
        return $app->ajax_error(
            $app->language('CONFIG_Directory not a directory') );
    }
    else {
        return $app->ajax_error(
            $app->language('CONFIG_Directory does not exist') );
    }
}

sub rm_ajax_sitename_available {
    return $_[0]->ajax_check_uniqueness(
        class  => 'BigMed::Site',
        column => 'name',
        name   => 'CONFIG_website name',
        type   => 'CONFIG_site',
    );
}

sub rm_ajax_make_htmltrace {
    my $app = shift;

    my $rfield = $app->_get_htmltrace_info();
    if ( ref $rfield ne 'HASH' ) {
        return $app->ajax_error($rfield);
    }
    my %field = %{$rfield};

    #check uniqueness
    my $site   = BigMed::Site->new();
    my $setter = 'set_' . $field{t};
    if ( !$site->can($setter) ) {
        return $app->ajax_error(
            $app->language(
                ['CONFIG_ERR_Unknown directory type', $field{t}]
            )
        );
    }
    $site->$setter( $field{d} );
    $site->set_id( $field{id} ) if $field{id};
    my $unique = $site->is_unique( $field{t} );
    if ( !defined $unique ) {    #caught an error
        $app->ajax_system_error();
    }
    elsif ( !$unique ) {
        return $app->ajax_error(
            $app->language(
                [   'BM_Not unique',
                    $app->language( 'CONFIG_' . $field{t} ),
                    $app->language('CONFIG_site'),
                ]
            )
        );
    }

    #can we create it?
    if ( !bm_confirm_dir( $field{d}, { build_path => 1 } ) ) {
        return $app->ajax_system_error();
    }

    #can we write to it?
    my $file = bm_file_path( $field{d}, 'bm_' . $field{t} . '.txt' );
    my $test_text = $field{t} . ' directory test.';
    if ( !bm_write_file( $file, $test_text ) ) {
        return $app->ajax_system_error();
    }

    return $app->ajax_json_response( { valid => 'OK' } );
}

sub rm_ajax_remove_htmltrace {
    my $app = shift;

    #remove the file created by htmltrace. No confirmation or error
    #is shown to user, so if we encounter a problem, just fail quietly...
    my $rfield = $app->_get_htmltrace_info();
    return $rfield if ref $rfield ne 'HASH';    #already ajax error format
    my %field = %{$rfield};

    #make sure that it's a valid type
    if ( !BigMed::Site->can( 'set_' . $field{t} ) ) {
        return $app->ajax_error("Bad directory type '$field{t}'");
    }
    my $file = bm_file_path( $field{d}, 'bm_' . $field{t} . '.txt' );
    $file = bm_untaint_filepath($file);
    if ( !$file ) {
        warn 'Config.pm received suspect filepath for ajax-remove-htmltrace'
          if $WARNING;
        return $app->ajax_error('Bad filepath');
    }
    if ( -e $file ) {
        if ( !unlink $file ) {
            warn "Config.pm could not unlink $file: $!" if $WARNING;
            return $app->ajax_error('Could not unlink file');
        }
        return $app->ajax_json_response( { valid => 'OK' } );
    }
    else {
        warn "Config.pm found no such file to unlink: $file" if $WARNING;
        return $app->ajax_error('No such file to unlink');
    }
}

sub rm_ajax_confirm_homepage {
    return $_[0]->_ajax_update_confirmation_image('homepage');
}

sub rm_ajax_confirm_html {
    return $_[0]->_ajax_update_confirmation_image('html');
}

sub _return_to_rm {
    my ( $app, $prev_screen_ref, %param ) = @_;
    return $prev_screen_ref->(
        $app,
        head => 'BM_Please_Review_Your_Entry',
        %param,
    );
}

sub _redirect_to_first_sections {
    my $app = shift;
    $app->header_type('redirect');
    $app->header_props(
        -url => $app->build_url(
            script => 'bm-sections.cgi',
            rm     => 'first-site-sections'
        )
    );
    return 'Redirecting...';
}

sub _redirect_to_new_sections {
    my $app = shift;
    $app->header_type('redirect');
    $app->header_props(
        -url => $app->build_url(
            script => 'bm-sections.cgi',
            rm     => 'new-site-sections'
        )
    );
    return 'Redirecting...';
}

sub _screen_confirm_site_dirs {
    my $app     = shift;
    my %options = @_;
    my $site    = $options{site} || $app->current_site
      or return $app->rm_login_site();

    #show test images with update form
    my %confirm_fs;
    foreach my $dirtype qw(homepage html) {
        my $url_method   = $dirtype . '_url';
        my $dir_method   = $dirtype . '_dir';
        my $random_field = $app->prompt_field_ref(
            id        => 'random_' . $dirtype,
            prompt_as => 'hidden',
            value     => $options{random},
        );
        my $new_url = $app->prompt_field_ref(
            data_class => 'BigMed::Site',
            column     => $url_method,
            value      => $app->escape( $site->$url_method ),
        );
        my $old_dir = $app->prompt_field_ref(
            id        => 'old_' . $dir_method,
            prompt_as => 'hidden',
            value     => $app->escape( $site->$dir_method ),
        );
        my $new_dir = $app->prompt_field_ref(
            data_class => 'BigMed::Site',
            column     => $dir_method,
            value      => $app->escape( $site->$dir_method ),
        );
        my $submit = $app->prompt_field_ref(
            id        => $dirtype . '_submit',
            prompt_as => 'submit',
            value     => $app->language('CONFIG_Try again'),
        );
        $confirm_fs{$dirtype} = [
            $app->prompt_fieldset_ref(
                id     => 'bmfs_' . $dirtype,
                fields =>
                  [$random_field, $new_url, $old_dir, $new_dir, $submit],
                title => "CONFIG_Update $dirtype directory",
            )
        ];
    }

    #PAGE INFO AND DISPLAY ----------------------------
    my $title_unlocal = $options{head} || 'CONFIG_Confirm Site Directories';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message} || 'CONFIG_Confirm directory message',
        title     => $title_unlocal,
    );
    _set_breadcrumbs( $app, $title_unlocal, $options{breadcrumbs} );

    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-config.js' );

    my $template = $options{template}
      || 'screen_config_confirm-site-dirs.tmpl';
    my %attr =
      $options{clone_site_id}
      ? ( rm => 'clone-menu', args => $options{clone_site_id} )
      : $options{is_new} ? ( rm => 'new-site-cleanup' )
      : ( rm => "site-cleanup" );
    $options{next_url} ||= { script => 'bm-config.cgi', %attr };

    my $next_url =
      $app->build_url( %{ $options{next_url} }, site => $site->id );
    return $app->html_template_screen(
        $template,
        bmcp_title     => $title,
        step4          => 1,                       #for setup wizard
        message        => $message,
        homepage       => $confirm_fs{homepage},
        html           => $confirm_fs{html},
        homepage_image => $site->homepage_url
          . "/homepage_dir$options{random}.jpg",
        html_image   => $site->html_url . "/html_dir$options{random}.jpg",
        homepage_url => $app->build_url(
            script => 'bm-config.cgi',
            rm     => 'ajax-confirm-homepage',
            site   => $site->id,
        ),
        html_url => $app->build_url(
            script => 'bm-config.cgi',
            rm     => 'ajax-confirm-html',
            site   => $site->id,
        ),
        next_url => $next_url,
    );
}

sub _ajax_update_confirmation_image {
    my $app        = shift;
    my $type       = shift;
    my $dir_method = $type . '_dir';
    my $url_method = $type . '_url';
    my $site     = $app->current_site or return $app->rm_ajax_login_badsite();
    my $orig_dir = $site->$dir_method;

    my %field = $app->parse_submission(
        {   data_class => 'BigMed::Site',
            column     => $url_method,
            required   => 1,
        },
        {   data_class => 'BigMed::Site',
            column     => $dir_method,
            required   => 1,
        },
        {   id       => "old_$dir_method",
            parse_as => 'dir_path',
            required => 1,
        },
        {   id       => "random_$type",
            required => 1,
            parse_as => 'number_integer_positive',
        },
    );
    return $app->ajax_parse_error( $field{_ERROR} ) if $field{_ERROR};

    #set the new values and check uniqueness
    my $setter = 'set_' . $dir_method;
    $site->$setter( $field{$dir_method} );
    $setter = 'set_' . $url_method;
    $site->$setter( $field{$url_method} );
    my $unique;
    if ( !defined( $unique = $site->is_unique($dir_method) ) ) {
        return $app->ajax_system_error();
    }
    if ( !$unique ) {
        return $app->ajax_error(
            $app->language(
                [   'BM_Not unique',
                    $app->language("site:$dir_method"),
                    $app->language('CONFIG_site')
                ]
            )
        );
    }

    #make sure the directory is good
    bm_confirm_dir( $field{$dir_method} ) or return $app->ajax_system_error();

    #remove any existing test image from original directory
    $orig_dir = bm_untaint_filepath($orig_dir);
    my $DIR;
    opendir $DIR, $orig_dir or next;
    my @files =
      map { bm_file_path( $orig_dir, $_ ) }
      grep { m{^\Q$type\E_dir\d+[.]jpg$}ms } readdir $DIR;
    foreach my $file (@files) {
        bm_delete_file($file) or next;
    }
    close $DIR;

    #copy the test image
    my %test_image = $app->_locate_test_images
      or return $app->ajax_system_error();
    my $random = int( rand(999_999) ) + 1;
    my $img    =
      bm_file_path( $field{$dir_method}, $dir_method . $random . '.jpg' );
    $img = bm_untaint_filepath($img) or return $app->ajax_system_error();

    if (!bm_copy_file(
            $test_image{ 's_' . $dir_method },
            bm_file_path( $field{$dir_method}, "$dir_method$random.jpg" )
        )
      )
    {
        return $app->ajax_system_error;
    }

    #data looks good
    $site->save or return $app->ajax_system_error();

    #move the public files if necessary
    if ($orig_dir) {
        $site->move_public_files( { "orig_${type}_dir" => $orig_dir } )
          or return $app->ajax_system_error();
    }

    #remove the old image (but don't complain if we can't do it)
    my $old_img = bm_file_path( $field{"old_$dir_method"},
        $dir_method . $field{"random_$type"} . '.jpg' );
    $old_img = bm_untaint_filepath($old_img);
    unlink $old_img;

    #return the info
    return $app->ajax_data(
        {   random => $random,
            url    => "$field{$url_method}/$dir_method$random.jpg"
        }
    );
}

sub _locate_test_images {
    my $app = shift;
    my %test_image;

    my $support_dir = bm_file_path( $app->env('MOXIEDATA'), 'support' );
    foreach my $dirtype (qw(homepage_dir html_dir)) {
        $test_image{ 's_' . $dirtype } =
          bm_file_path( $support_dir, $dirtype . '.jpg' );
        if ( !-e $test_image{ 's_' . $dirtype } ) {
            $app->set_error(
                head => 'CONFIG_Missing file',
                text => [
                    'CONFIG_Could not find files in moxiedata',
                    "support/$dirtype.jpg",
                ],
            );
            return;
        }
    }

    #make sure that the support files have correct permissions
    bm_dir_permissions($support_dir) or return;

    return %test_image;
}

sub _get_htmltrace_info {
    my $app   = shift;
    my %field = $app->parse_submission(
        {   id       => 't',
            required => 1,
            parse_as => 'raw_text',
        },
        {   id       => 'd',
            required => 1,
            parse_as => 'dir_path',
        },
        {   id       => 'id',
            parse_as => 'id',
        },
    );
    return $app->ajax_parse_error( $field{_ERROR} ) if $field{_ERROR};
    return \%field;
}

sub _set_breadcrumbs {
    my $app    = shift;    #could be Start.pm app, so handle gently
    my $title  = shift;
    my $ref_bc = shift;
    if ( $app->can('set_cp_breadcrumbs') ) {    #because of start.pm
        my @bc =
          ref $ref_bc eq 'ARRAY' ? @{$ref_bc}
          : (
            {   bc_label => 'BM_CP_NAVLABEL_Settings',
                bc_url   => $app->build_url(
                    script => 'bm-prefs.cgi',
                    rm     => 'main-menu',
                    site   => $app->current_site ? $app->current_site->id
                    : undef,
                ),
            },
            { bc_label => $title, },
          );
        $app->set_cp_breadcrumbs(@bc);
    }
    return;
}

sub _store_env_values {
    my ( $app, $rprev_screen, $rnext_screen ) = @_;
    if ( !defined $app->bigmed->save_config ) {    #error writing file
        my %error_text = $app->error_html_hash;
        my %param      = (
            head    => 'CONFIG_ERR_Could_not_write_configuration_file',
            message => $error_text{text},
            query   => $app->query,
        );
        return $rprev_screen->( $app, %param );
    }
    return $rnext_screen->($app);
}

sub _moxiedata_recommendation {
    my $app      = shift;
    my $doc_root = _document_root();
    ( my $above_root = $doc_root ) =~ s{ [/\\] [^/\\]+ [/\\]?$}{}msx;
    my $root_declaration =
      $doc_root =~ m{path[\\/]to[\\/]public_html_directory$}ms
      ? 'CONFIG_Moxiedata: Web root directory example'
      : 'CONFIG_Moxiedata: Web root directory';
    my $moxiedata_dir = bm_file_path( $above_root, 'moxiedata' );
    my $bmadmin_dir   = bm_file_path( $doc_root,   'bmadmin' );

    #root declaration starts the sentence to say where the root is,
    #root sentence finishes up to suggest where the moxiedata dir
    #should go.
    my $root_sentence = [
        'CONFIG_Moxiedata recommendation',
        $app->language( [$root_declaration, $doc_root] ),
        $above_root, $moxiedata_dir, $bmadmin_dir,
    ];
    return ( $moxiedata_dir, $bmadmin_dir, $root_sentence );
}

sub _document_root {
    my $doc_root = $ENV{DOCUMENT_ROOT};
    if ( !$doc_root && $OSNAME eq 'MSWin32' ) {
        $doc_root = 'E:\path\to\public_html_directory';
    }
    elsif ( !$doc_root ) {
        $doc_root = '/path/to/public_html_directory';
    }
    return $doc_root;
}

sub _domain_url {
    my $domain = $ENV{HTTP_HOST} || 'www.example.com';
    return 'http://' . $domain;
}

sub _moxiebin_url {
    my $domain_url = shift;
    my $cgi_uri    = $ENV{REQUEST_URI} || '/cgi-bin/moxiebin/bm-start.cgi';
    my $dir_path   = $cgi_uri =~ m{\A(.*)/bm-[a-z]+\.cgi}ms ? $1 : $cgi_uri;
    return $domain_url . $dir_path;
}

sub _sendmail_location {
    return -e '/usr/lib/sendmail' ? '/usr/lib/sendmail'
      : -e '/usr/sbin/sendmail'   ? '/usr/sbin/sendmail'
      : -e '/usr/bin/sendmail'    ? '/usr/bin/sendmail'
      : q{};
}

sub _smtp_server_name {
    my $domain = $ENV{HTTP_HOST} || 'www.example.com';
    $domain =~ s{\Awww\.}{}ms;
    if ( $domain =~ /[^.\d]/ms ) {
        return 'smtp.' . $domain;
    }
    else {
        return $domain;
    }
}

sub _redirect_to_main_menu {
    my ( $app, $message, $redirect ) = @_;
    $app->set_session_message($message);

    #don't always have a site id (for example, when call the about_us
    #run mode)
    my $site_id = $app->current_site ? $app->current_site->id : undef;

    $redirect ||= $app->build_url(
        script => 'bm-prefs.cgi',
        rm     => 'main-menu',
        site   => $site_id,
    );
    $app->header_type('redirect');
    $app->header_props( -url => $redirect );
    return "Redirecting to $redirect";
}

1;

__END__

=head1 NAME

BigMed::App::Web::Login - Big Medium application class for user verification

=head1 DESCRIPTION

BigMed::App::Web::Login is a subclass of BigMed::App::Web that requires
user verification via BigMed::User. BigMed::App::Web::Login can be subclassed
by Big Medium web application modules that require user credentials.

BigMed::App::Web::Login intercepts run mode requests via its C<cgiapp_prerun>
method and checks the user's session info for a valid login. If there isn't
one, or if the session has expired, BigMed::App::Web::Login diverts the
user to an appropriate login run mode.

=head2 USAGE

To subclass BigMed::App::Web::Login, use it as the base class for your
application module by including these lines at the top of your module:

    package MyApp;
    use base qw(BigMed::App::Web::Login);

In addition to this module's run modes and user-validation, you also inherit
all of the methods of BigMed::App::Web.

=head2 RUN MODES

=head3 login

The login run mode presents a welcome message and a login screen.

=head1 SEE ALSO

BigMed::App::Web

=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

