# 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: Account.pm 3137 2008-06-17 21:18:24Z josh $

package BigMed::App::Web::Account;
use strict;
use warnings;
use utf8;
use Carp;
$Carp::Verbose = 1;
use base qw(BigMed::App::Web::CP);
use BigMed::User;
use BigMed::Priv;
use BigMed::Email qw(send_email);
use BigMed::PageAlert;

my $PRIV_OPTIONS = [qw(2 3 4 5 6)];    #for now, only offer writer+ options
my $PRIV_LABELS;

sub setup {
    my $app = shift;
    $app->start_mode('find_route');
    $app->run_modes(
        'AUTOLOAD' => sub { $_[0]->rm_find_route() },
        'account'  => 'rm_edit', #redirect from Start.pm
        'edit'     => 'rm_edit',
        'new'      => 'rm_edit_account',
        'save'     => 'rm_save_account',
        'search'   => 'rm_search',
        'delete'   => 'rm_delete',
        'ajax-username-avail' => 'rm_ajax_username_available',
        'ajax-cancel-privs' => 'rm_ajax_cancel_privs',
        'ajax-add-privs' => 'rm_ajax_add_privs',
        'ajax-priv-form' => 'rm_ajax_priv_form',
        'ajax-save-privs' => 'rm_ajax_save_privs',
    );
    $PRIV_LABELS = {
        '1' => $app->language('BM_User_Priv1'),
        '2' => $app->language('BM_User_Priv2'),
        '3' => $app->language('BM_User_Priv3'),
        '4' => $app->language('BM_User_Priv4'),
        '5' => $app->language('BM_User_Priv5'),
        '6' => $app->language('BM_User_Priv6'),
    };
    return;
}

sub cgiapp_prerun {
    my $app = shift;
    $app->SUPER::cgiapp_prerun;
    $app->require_privilege_level(2);
    return;
}

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

sub rm_find_route {
    my $app  = shift;
    my $user = $app->current_user;
    if ( $user->privilege_level( $app->current_site ) > 4 ) {
        return $app->rm_menu;
    }
    else {
        return $app->rm_edit_account( user => $user );
    }
}

sub rm_menu {
    my $app     = shift;
    my %options = @_;
    my $user    = $app->current_user;
    my $site    = $app->current_site;
    return $app->rm_edit_account( user => $user )
      if $user->privilege_level($site) < 5;

    #sites where user has permission to edit accounts
    defined( my $user_sites = $user->allowed_site_selection(5) ) or return;
    my $s;
    my @sites = (q{});
    my %site_name = ( q{} => $app->language('ACCOUNT_All Sites') );
    while ( $s = $user_sites->next ) {
        push @sites, $s->id;
        $site_name{ $s->id } = $s->name;
    }

    #SEARCH FIELDSET ----------------------------
    my $name_field = $app->prompt_field_ref(
        data_class => 'BigMed::User',
        column     => 'name',
        value      => q{},
        focus      => 1,
    );
    my $email_field = $app->prompt_field_ref(
        data_class => 'BigMed::User',
        column     => 'email',
        value      => q{},
    );

    my @priv = ( q{}, @{$PRIV_OPTIONS} );
    pop @priv if $user->level < 6;    #only admins can search for admins
    my $privileges = $app->prompt_field_ref(
        data_class => 'BigMed::User',
        column     => 'level',
        prompt_as  => 'value_list',
        options    => [q{}, @{$PRIV_OPTIONS}],
        labels     =>
          { %{$PRIV_LABELS}, q{} => $app->language('ACCOUNT_All Levels'), },
        value => q{},
    );
    my $search_sites = $app->prompt_field_ref(
        id        => 'search_site',
        prompt_as => 'value_list',
        options   => \@sites,
        labels    => \%site_name,
        value     => $site->id,
        label     => 'ACCOUNT_Site',
    );

    #assemble the fieldsets
    my $search_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_user_search',
        fields    => [$name_field, $email_field, $privileges, $search_sites],
        title     => 'ACCT_LABEL_search',
        query     => $options{query},
        field_msg => $options{field_msg},
    );

    my $submit_fs = $app->prompt_fieldset_ref(
        fields => [
            $app->prompt_field_ref(
                id        => 'account_submit',
                prompt_as => 'submit',
                value     => $app->language('BM_SUBMIT_LABEL_Search'),
            )
        ]
    );

    #PAGE INFO AND DISPLAY ----------------------------
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $options{head} || 'ACCT_Account Menu',
    );

    $app->_set_breadcrumbs('ACCT_Account Menu');

    my $form_url = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'search',
        site   => $app->current_site->id,
    );
    my $new_url = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'new',
        site   => $app->current_site->id,
    );
    my $self_edit = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'edit',
        site   => $app->current_site->id,
        args   => [$user->id],
    );
    my $search_all = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'search',
        site   => $app->current_site->id,
        args   => ['all'],
    );
    return $app->html_template_screen(
        'screen_account_menu.tmpl',
        bmcp_title     => $title,
        form_url       => $form_url,
        message        => $message,
        fieldsets      => [$search_fs, $submit_fs],
        self_edit_url  => $self_edit,
        search_all_url => $search_all,
        new_url        => $new_url,
    );

}

sub rm_search {
    my $app  = shift;
    my $user = $app->current_user;
    my $site = $app->current_site;
    return $app->rm_edit_account( user => $user )
      if $user->privilege_level($site) < 5;

    my ( $name, $email, $priv, $searchsite );
    my $search_all = ( $app->path_args() )[0];
    my $q          = $app->query;
    if ( !$search_all || $search_all ne 'all' ) {    #gather inputs
        my %field = $app->parse_submission(
            {   data_class => 'BigMed::User',
                column     => 'name',
            },
            {   data_class => 'BigMed::User',
                column     => 'email',
            },
            {   data_class => 'BigMed::User',
                column     => 'level',
            },
            {   id       => 'search_site',
                parse_as => 'value_list',
            },
        );
        return $app->rm_menu( field_msg => $field{_ERROR}, query => $q )
          if $field{_ERROR};
        $field{search_site} =~ s/\D//msg;
        ( $name, $email, $priv, $searchsite ) =
          @field{qw(name email level search_site)};
    }

    #non-admins can't look up admins. if we get that request, just
    #discard it (the interface doesn't offer it)
    my $user_level = $user->privilege_level( $app->current_site );
    undef $priv if $priv && $priv > 5 && $user_level < 6;
    if ( $priv && $priv == 6 ) {    #admin
        undef $searchsite;
    }

    #start off by finding users who match non-site-based criteria
    my (%user_criteria);
    $user_criteria{name}  = $name  if defined $name  && $name  ne q{};
    $user_criteria{email} = $email if defined $email && $email ne q{};
    if ( !$searchsite && $priv ) {
        $user_criteria{level} = $priv;
    }
    if ( !$priv && $user_level < 6 ) {    #find only non-admins
        $user_criteria{level} = { from => 1, to => 5 };
    }
    my $user_set = BigMed::User->select( \%user_criteria )
      or return $app->rm_menu( query => $q );
    if ( !$user_set->count ) {
        return $app->rm_menu(
            query   => $q,
            message => 'ACCOUNT_No matching results'
        );
    }

    my @users;

    #winnow based on site permissions
    if ($searchsite) {
        $user_set = $user_set->join_has(
            { join => 'BigMed::Priv', key => 'user', unique => 1 },
            { site => $searchsite },
          )
          or $app->rm_menu( query => $q );

        #note that the join_has strips out any admins because they don't
        #have privilege objects; they need to be added back in later if
        #appropriate

        if ($priv) {
            my $u;
            while ( $u = $user_set->next ) {
                push @users, $u if $u->privilege_level($searchsite) == $priv;
            }
            return $app->rm_menu( query => $q ) if !defined $u;
        }
        else {
            @users = $user_set->fetch();
            if ( $user_level > 5 ) {    #add back admins
                my @admins =
                  BigMed::User->fetch( { %user_criteria, level => 6 } );
                return $app->rm_menu( query => $q ) if $app->error;
                push @users, @admins;    #should not be dupes
            }
        }
    }
    else {
        @users = $user_set->fetch();
    }

    #sort 'em by user name and make sure that each is definitely editable
    #by the editor
    my $base_url = $app->build_url(
        script => 'bm-account.cgi',
        site   => $app->current_site->id,
        rm     => 'edit',
    );
    my @rows;
    my $user_id = $user->id;
    foreach my $u ( sort { $a->name cmp $b->name } @users ) {
        next if !$user->can_edit_user($u);
        push @rows,
          { name     => $u->name,
            edit_url => "$base_url/" . $u->id,
            level    => $PRIV_LABELS->{ $u->privilege_level($searchsite) },
            email    => $u->email,
            cid      => $user_id == $u->id ? undef: $u->id,
          };
    }
    if ( !@rows ) {
        return $app->rm_menu(
            query   => $q,
            message => 'ACCOUNT_No matching results'
        );
    }

    my ( $title, $message ) = $app->title_and_message(
        message => ['ACCOUNT_Found matches', scalar @rows],
        title   => 'ACCT_Search Results',
    );
    $app->_set_breadcrumbs( q{}, 'ACCT_Search Results' );

    $app->js_add_onload(
        'BM.ActionMenu.update("accountActionMenu","accountMenu");');
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-actionmenu.js' );

    my $search_url = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'menu',
        site   => $app->current_site->id,
    );
    return $app->html_template_screen(
        'screen_account_search_results.tmpl',
        bmcp_title => $title,
        message    => $message,
        users      => \@rows,
        search_url => $search_url,
    );
}

sub rm_edit {
    my $app = shift;
    my $uid = ( $app->path_args() )[0] || q{};
    $uid =~ s/\D//msg;
    my $user;
    if ($uid) {
        defined( $user = BigMed::User->fetch($uid) )
          or $app->error_stop;
        if ( !$user ) {
            $app->set_error(
                head => 'ACCOUNT_ERR_No such user',
                text => 'ACCOUNT_ERR_TEXT_No such user',
            );
            $app->error_stop;
        }
    }
    else {
        $user = $app->current_user;
    }
    return $app->rm_edit_account( user => $user );
}

sub rm_edit_account {    #this is where the action actually happens
    my ( $app, %options ) = @_;
    my $user   = $options{user} || BigMed::User->new();
    my $is_new = !$user->id;
    my $editor = $options{first_account} ? undef: $app->current_user;

    #can't use _can_edit method as an object method because of Start.pm
    #so call it as a regular subroutine
    $app->error_stop if !_can_edit( $app, $editor, $user, \%options );

    #new accounts start off with writer permissions at current site
    if ( !$options{first_account} && !$user->id ) {
        $user->update_id;
        $user->set_level(2);
        $user->set_site_privileges( site => $app->current_site )
          or $app->error_stop;
    }

    #PERSONAL INFO FIELDSET ----------------------------
    my $name_field = $app->prompt_field_ref(
        data_class  => 'BigMed::User',
        column      => 'name',
        required    => 1,
        ajax_status => !$options{first_account},
        value       => $user->name || q{},         #already html-escaped
        focus       => 1,
        description => 'ACCT_DESC_Username',
    );
    my $email_field = $app->prompt_field_ref(
        data_class  => 'BigMed::User',
        column      => 'email',
        required    => 1,
        value       => $user->email ? $app->escape( $user->email ) : q{},
        description => 'ACCT_DESC_Email',
    );
    my $user_id = $app->prompt_field_ref(          #for ajax check
        id        => 'user_id',
        prompt_as => 'hidden',
        value     => $user->id,
    );

    #assemble the fieldset
    my $pinfo_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_personal_info',
        fields    => [$name_field, $email_field, $user_id],
        title     => 'ACCT_LABEL_personal info',
        query     => $options{query},
        field_msg => $options{field_msg},
    );

    #PASSWORD INFO FIELDSET ----------------------------
    my $label = $user->password ? 'new_password' : 'password';
    my $password = $app->prompt_field_ref(
        data_class => 'BigMed::User',
        column     => 'password',
        id         => 'newpassword',            #otherwise auto-fill gets it
        label      => 'ACCT_LABEL_' . $label,
        required   => !$user->password,
        value      => q{},
    );
    my $confirm_password = $app->prompt_field_ref(
        data_class  => 'BigMed::User',
        column      => 'password',
        id          => 'confirmPass',
        validate_as => 'password_confirm',
        label       => 'ACCT_LABEL_confirm_password',
        required    => !$user->password,
        value       => q{},
    );

    #assemble the fieldset
    my $password_fs = $app->prompt_fieldset_ref(
        id        => 'bmfs_password',
        fields    => [$password, $confirm_password],
        title     => 'ACCT_LABEL_' . $label . '_fieldset',
        query     => $options{query},
        field_msg => $options{field_msg},
    );

    #SITE PRIVILEGES FIELDSET ----------------------------
    #display site privileges only if it's not a new account and it's not
    #the editor's own account
    my %site_privs;
    %site_privs = $app->_site_priv_fields( $user, $editor )
      if _can_see_privileges( $app, $editor, $user->id );

    #ALERT INFO ------------------------------------------

    my $alert_fieldset;
    if ( !$is_new ) {    #for existing accounts only
        defined( $alert_fieldset = $app->_user_alert_params($user) )
          or $app->error_stop;
    }

    #SUBMIT FIELDSET ----------------------------
    my $emailconfirm_field = $app->prompt_field_ref(
        id           => 'mailGreeting',
        label        => 'ACCT_LABEL_send_greeting',
        option_label => 'ACCT_LABEL_email confirmation message',
        prompt_as    => 'boolean',
        value        => 1,
    );

    my $submit_text = $options{submit_text} || 'BM_SUBMIT_LABEL_Save';
    my $submit = $app->prompt_field_ref(
        id        => 'account_submit',
        prompt_as => 'submit',
        value     => $app->language($submit_text),
    );

    my $submit_fields =
      $user->password ? [$submit] : [$emailconfirm_field, $submit];
    my $submit_fs = $app->prompt_fieldset_ref( fields => $submit_fields );

    #PAGE INFO AND DISPLAY ----------------------------
    my $this_title = $is_new ? 'ACCOUNT_New account' : 'ACCT_Edit Account';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $options{head} || $this_title,
    );

    my $site = $options{first_account} ? q{} : $app->current_site;

    if ( $app->can('_set_breadcrumbs') ) {    #can be a start.pm object
        my @title =
            ( !$editor || $editor->privilege_level($site) < 5 )
          ? ($this_title)
          : ( q{}, $this_title );
        $app->_set_breadcrumbs(@title);
    }

    my $form_url = $options{form_url}
      || $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'save',
        site   => $site,
        args   => [$user->id],
      );
    my $template = $options{template} || 'screen_account_edituser.tmpl';

    $app->js_make_toggle( 'BM_ACCT_ADD_SITE', 'BM_ACCT_ADD_SITE_TOG',
        'slide' );
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-account.js' );

    my @bottom_fields = ($submit_fs);
    unshift @bottom_fields, $alert_fieldset if $alert_fieldset;
    return $app->html_template_screen(
        $template,
        bmcp_title => $title,
        form_url   => $form_url,
        message    => $message,

        #for start.pm
        fieldsets => [$pinfo_fs, $password_fs,            $submit_fs],
        step3     => 1,          #for benefit of start.cgi

        #for all others
        topfields    => [$pinfo_fs, $password_fs],
        bottomfields => \@bottom_fields,
        %site_privs,
    );
}

sub rm_save_account {
    my ( $app, %options ) = @_;
    my $editor = $options{first_account} ? undef: $app->current_user;
    my $uid    = ( $app->path_args() )[0];
    my %field  = $app->parse_submission(
        {   data_class => 'BigMed::User',
            column     => 'name',
            required   => 1,
        },
        {   data_class => 'BigMed::User',
            column     => 'email',
            required   => 1,
        },
        {   data_class => 'BigMed::User',
            column     => 'password',
            id         => 'newpassword',
            required   => $options{first_account},
        },
        {   id         => 'confirmPass',
            data_class => 'BigMed::User',
            column     => 'password',
            required   => $options{first_account},
        },
    );
    if ( $field{confirmPass} ne $field{newpassword} ) {
        $field{_ERROR}->{confirmPass} =
          'ACCT_ERR_confirmation password must match';
    }

    #get mode-specific fields
    my $user_level;
    if ( $options{first_account} ) {
        $user_level = 6;
        $field{'mailGreeting'} = $app->utf8_param('mailGreeting');
    }
    elsif ( _can_see_privileges( $app, $editor, $uid ) ) {
        %field = (
            %field,
            $app->parse_submission(
                {   data_class => 'BigMed::User',
                    column     => 'level',
                    parse_as   => 'value_list',
                    options    => $PRIV_OPTIONS,
                    required   => 1,
                },
                {   id       => 'mailGreeting',
                    parse_as => 'boolean',
                },
            ),
        );
        $user_level = $field{level};
        undef $user_level
          if $user_level > 5
          && $editor->privilege_level( $app->current_site ) < 6;
    }

    $options{prev_screen_ref} ||= sub { rm_edit_account(@_) };

    #get the user object, if any, or create a new one and validate the id
    defined( my $user = $uid ? BigMed::User->fetch($uid) : q{} )
      or return $options{prev_screen_ref}->( $app, query => $app->query );

    my %err_param = (
        field_msg => $field{_ERROR},
        query     => $app->query,
        user      => $user,
    );
    my $is_new;
    if ( !$user ) {
        $is_new = 1;
        $user   = BigMed::User->new();
        $user->set_id($uid);
        if ( $uid && !$user->has_valid_id() ) {
            $app->set_error(
                head => 'CONTENT_Invalid content ID',
                text => [
                    'CONTENT_TEXT_Invalid content ID', $user->data_label, $uid
                ],
            );
            return $options{prev_screen_ref}->( $app, %err_param );
        }
        else {
            $user->update_id()
              or return $options{prev_screen_ref}->( $app, %err_param );
        }
    }

    if ( $field{_ERROR} ) {
        return $options{prev_screen_ref}->( $app, %err_param );
    }

    #make sure we have permission to edit; as above in the edit run mode,
    #have to call the _can_edit method in procedural form instead of as
    #object method to help Start.pm.
    return $options{prev_screen_ref}->( $app, %err_param )
      if !_can_edit( $app, $editor, $user, \%options );

    #save alert prefs
    if ( !$is_new ) {
        $app->_save_alert_prefs($user)
          or return $options{prev_screen_ref}->( $app, %err_param );
    }

    #save the user info
    $user->set_name( $field{name} );
    $user->set_email( $field{email} );
    $user->set_password( $user->encode_password( $field{newpassword} ) )
      if $field{newpassword} ne q{};
    $user->set_level($user_level) if defined $user_level;
    $user->save
      or return $options{prev_screen_ref}->( $app, %err_param );

    if ( $user_level && $user_level > 5 ) {    #clear privs for admin
        defined( my $admin_priv =
              BigMed::Priv->select( { user => $user->id } ) )
          or return $options{prev_screen_ref}->( $app, %err_param );
        if ($admin_priv) {
            $admin_priv->trash_all
              or return $options{prev_screen_ref}->( $app, %err_param );
        }
    }

    my $confirmation = $options{message}
      || 'BM_Your changes have been saved.';

    #mail the greeting message if we have one
    if ( $field{mailGreeting} ) {
        my $sent = send_email(
            to        => $field{email},
            to_name   => $field{name},
            from      => $app->env('ADMINEMAIL'),
            from_name => $app->language('BM_bm administrator'),
            subject   => $app->language('ACCOUNT_EMAIL_Your account info'),
            body      => $app->language(
                [   'ACCT_EMAIL_New account text', $field{name},
                    $field{newpassword},           $field{email},
                    $app->env('BMADMINURL') . '/index.html',
                ]
            ),
        );
        if ( !$sent ) {    #caught an error; append message to confirmation
            my %error_msg = $app->error_html_hash();
            $confirmation = [
                'ACCT_Saved but could not send email',
                q{%BM} . $app->language($confirmation) . q{%},
                q{%BM} . $app->language( $error_msg{text} ) . q{%},
            ];
            $app->clear_error;
        }
    }

    $options{message} = $confirmation;
    my $next_screen = $options{next_screen_ref} || sub { _after_save(@_) };
    return $next_screen->( $app, %options, user => $user );
}

sub rm_delete {
    my $app  = shift;
    my $site = $app->current_site;
    my $user = $app->current_user;
    return $app->rm_edit_account( user => $user )
      if $user->privilege_level($site) < 5;
    my @id = $app->utf8_param('c');
    return $app->rm_menu if !@id;

    defined( my $select = BigMed::User->select( { id => \@id } ) )
      or return $app->rm_menu;
    my $u;
    while ( $u = $select->next ) {
        if ( !$user->can_edit_user($u) ) {
            $app->set_error(
                head => 'CONTENT_Not Allowed To Do That',
                text => [
                    'ACCOUNT_TEXT_No permission to delete',
                    q{%BM} . $u->name . q{%},
                ],
            );
            return $app->rm_menu;
        }
        if ( $u->id == $user->id ) {
            $app->set_error(
                head => 'ACCOUNT_Cannot Delete Yourself',
                text => 'ACCOUNT_TEXT_Cannot Delete Yourself'
            );
            return $app->rm_menu;
        }
    }
    return $app->rm_menu if !defined $u;

    #toss 'em
    $select->trash_all or return $app->rm_menu;
    return $app->rm_menu(
        message => 'ACCOUNT_Deleted the requested accounts' );
}

sub _after_save {
    my $app    = shift;
    my %option = @_;
    my $editor = $app->current_user;
    my $user   = $option{user};
    $app->set_current_user($user) if $user->id == $editor->id;
    if ( $editor->privilege_level( $app->current_site ) >= 5 ) {
        return $app->rm_menu( message => $option{message} );
    }
    else {
        $app->set_session_message( $option{message} );
        my $redirect = $app->build_url(
            script => 'bm-editor.cgi',
            rm     => 'menu',
            site   => $app->current_site->id,
        );
        $app->header_type('redirect');
        $app->header_props( -url => $redirect );
        return "Redirecting to $redirect";
    }
}

sub _user_alert_params {
    my ( $app, $user ) = @_;
    my $uid     = $user->id;
    my $ualerts = BigMed::PageAlert->select( { user => $uid } ) or return;

    #gather site information
    my $my_sites = $user->allowed_site_selection() or return q{};
    my $s;
    my @spair;
    my %sname;
    while ( $s = $my_sites->next ) {
        $sname{ $s->id } = $s->name;
        push @spair, [$s->id, lc $s->name];
    }
    return $app->error_stop if !defined $s;
    my @sites = map { $_->[0] } sort { $a->[1] cmp $b->[1] } @spair;

    #get alert settings for all sites
    my $mine = $user->level < 3;    #writers always get "mine" set
    my %active;
    my $all;
    my %site;
    my @fields;
    foreach my $type ( BigMed::PageAlert->alert_types ) {
        next if BigMed::PageAlert->min_level($type) > $user->level;
        my $all_type = $ualerts->select( { alert => $type } );
        my $obj;
        while ( $obj = $all_type->next ) {
            $active{$type} = 1, $site{ $obj->site } = 1;
            $all  = 1 if !$obj->site;    #zero means all sites
            $mine = 1 if $obj->mine;     #for now, one-size-fits-all
        }
        $app->error_stop if !defined $obj;
        push @fields,
          $app->prompt_field_ref(
            id           => "${type}__ACTIVE",
            prompt_as    => 'boolean',
            value        => $active{$type},
            option_label => "ACCT_ACTIVATE_$type",
            label        => "ACCT_LABEL_$type",
          );
    }
    $all = 1 if !%active;                #default to active

    my $site_select = $app->prompt_field_ref(
        id          => 'email_alert_sites',
        prompt_as   => 'value_list',
        multiple    => 5,
        label       => 'ACCOUNT_Selected Sites',
        description => 'BM_Multi-select',
        options     => \@sites,
        labels      => \%sname,
        value       => $all ? [] : [keys %site],
    );
    my $all_field = $app->prompt_field_ref(
        id        => 'email_alert_all',
        prompt_as => 'hidden',
        value     => '0',
    );
    push @fields,
      ( $app->prompt_field_ref(
            id          => 'email_alert_choice',
            prompt_as   => 'radio_toggle',
            label       => 'ACCT_LABEL_Alert for sites',
            force_right => 1,
            css_class   => 'fieldsBoxed',
            value       => [
                {   id      => 'alert_tab_all',
                    label   => 'ACCOUNT_All Sites',
                    value   => 'all',
                    checked => $all,
                    field   => $all_field,
                },
                {   id      => 'alert_tab_choice',
                    label   => 'ACCOUNT_Selected Sites',
                    value   => 'some',
                    checked => !$all,
                    field   => $site_select,
                },
            ],
        ),
        $app->prompt_field_ref(
            id           => 'email_alert_mine',
            prompt_as    => $user->level > 2 ? 'boolean' : 'hidden',
            value        => $mine,
            option_label => 'ACCT_ALERT_Only for pages owned by me',
            label        => 'ACCT_LABEL_Owned pages only',
        ),
      );
    return $app->prompt_fieldset_ref(
        title  => 'ACCT_LABEL_email alerts',
        fields => \@fields,
        query  => $app->query,
    );
}

sub _site_priv_fields {
    my ( $app, $user, $editor ) = @_;
    my $site         = $app->current_site;
    my $uid          = $user->id;

    my @sites;
    my %has_site;
    my $site_select;
    if ($user->level < 6 ) { #admins have no privilege levels
        defined( $site_select = $user->allowed_site_selection() )
          or return ();
    }
    if ($site_select) {
        my $site;
        while ( $site = $site_select->next ) {
            my $rparam = $app->_privilege_tmpl_params($user, $site) or return ();
            push @sites, $rparam;
            $has_site{ $site->id } = 1;
        }
        return () if !defined $site;
    }

    #gather the sites to which the editor has permission to grant privileges
    my @editor_sites;
    defined( my $editor_select = $editor->allowed_site_selection(5) )
      or return ();
    if ($editor_select) {
        my $site;
        while ( $site = $editor_select->next ) {
            next if $has_site{ $site->id };
            push @editor_sites,
              { id       => $site->id,
                name     => $site->name,
              };
        }
        return () if !defined $site;
    }
    my @priv = ( q{}, @{$PRIV_OPTIONS} );
    pop @priv
      if $editor->privilege_level($site) < 6;    #only admins can set admins
    my $level = $app->prompt_field_ref(
        data_class => 'BigMed::User',
        column     => 'level',
        prompt_as  => 'value_list',
        options    => \@priv,
        labels     => $PRIV_LABELS,
        ,
        required    => 1,
        value       => $user->level,
        description => 'ACCOUNT_DESC_level',
        css_class   => 'chooseLevel',
    );
    my $show_fields = $user->level < 6;

    return (
        PRIV_ITEMS => \@sites,
        ADD_LINK     => $app->language('BM_Add site'),
        EDITOR_SITES => \@editor_sites,
        LEGEND       => $app->language('ACCOUNT_Site privileges'),
        LEVEL_FIELD  => [$app->prompt( @{$level} )],
        SHOW_FIELDS  => $show_fields,
        ADMIN_MSG    =>
          $app->language('ACCOUNT_Admins have privileges at all sites'),
    );
}

sub _save_alert_prefs {
    my ( $app, $user ) = @_;
    my $uid = $user->id;

    my @alerts =
      map { { id => $_ . '__ACTIVE', parse_as => 'boolean', } }
      BigMed::PageAlert->alert_types( $user->level );

    my %alert = $app->parse_submission(
        @alerts,
        {   id       => 'email_alert_mine',
            parse_as => 'boolean',
        },
        {   id       => 'email_alert_choice',
            parse_as => 'raw_text',
        },
    );
    my %site;
    if ( $alert{email_alert_choice} eq 'all' ) {
        $site{'0'} = 1;
    }
    else {
        my @sites = $app->utf8_param('email_alert_sites');

        my %site_ok;
        my $all_site = $user->allowed_site_selection() or return;
        my $s;
        while ( $s = $all_site->next ) {
            $site_ok{ $s->id } = 1;
        }
        return if !defined $s;
        %site = map { $_ => 1 } grep { $site_ok{$_} } @sites;
    }

    my $mine = $user->level < 3 || $alert{'email_alert_mine'};
    my $ualert = BigMed::PageAlert->select( { user => $uid } ) or return;
    foreach my $type ( BigMed::PageAlert->alert_types() ) {
        my $select = $ualert->select( { alert => $type } ) or return;
        if ( !$alert{"${type}__ACTIVE"} ) {
            $select->trash_all or return;
            next;
        }
        my %check_site = %site;

        #update existing objects and trash out-of-date
        my $alert;
        while ( $alert = $select->next ) {
            my $sid = $alert->site;
            if ( !$check_site{$sid} ) {
                $alert->trash or return;
                next;
            }
            $alert->set_mine($mine);
            $alert->save or return;
            delete $check_site{ $alert->site };
            next;
        }
        return if !defined $alert;

        #add objects for any remaining sites
        foreach my $sid ( keys %check_site ) {
            my $alert = BigMed::PageAlert->new();
            $alert->set_alert($type);
            $alert->set_user($uid);
            $alert->set_site($sid);
            $alert->set_mine($mine);
            $alert->save or return;
        }
    }
    return 1;
}

sub rm_ajax_username_available {
    return $_[0]->ajax_check_uniqueness(
        class      => 'BigMed::User',
        column     => 'name',
        field_name => 'ACCT_user name',
        type       => 'ACCT_account',
    );
}

sub rm_ajax_add_privs {
    my $app    = shift;
    my $editor = $app->current_user;
    $app->require_post() or return $app->ajax_system_error();
    my ($uid) = $app->path_args() || q{};

    $uid =~ s/\D+//msg;
    if ( !$uid ) {
        $app->set_error(
            head => 'ACCOUNT_ERR_Missing data',
            text => 'ACCOUNT_ERR_TEXT_Missing data',
        );
        return $app->ajax_system_error();
    }
    if ( $uid == $editor->id ) {
        $app->set_error(
            head => 'ACCOUNT_Cannot Edit Own Privs',
            text => 'ACCOUNT_TEXT_Cannot Edit Own Privs',
        );
        return $app->ajax_system_error();
    }
    my $user = $app->_get_validated_user($uid)
      or return $app->ajax_system_error();

    #winnow submitted sites to those where editor has admin or webmaster privs
    my @sites;
    my $editor_level = $editor->level || 0;
    if ( $editor_level > 5 ) {    #admins
        @sites =
          grep { /\A\d+\z/ } $app->utf8_param('BM_ACCT_ADD_SITE_SELECT');
    }
    else {                        #collect sites with webmaster+ privs
        defined( my $webm_sites = $editor->allowed_site_selection(5) )
          or return $app->ajax_system_error();
        if ($webm_sites) {
            my $index;
            my %can_site;
            while ( $index = $webm_sites->next_index ) {
                $can_site{ $index->{id} } = 1;
            }
            @sites =
              grep { $can_site{$_} }
              $app->utf8_param('BM_ACCT_ADD_SITE_SELECT');
        }
    }

    my @tmpl_params;
    foreach my $sid (@sites) {
        next if $user->privilege_level($sid);   #ignore sites w/existing privs
        defined( my $site = BigMed::Site->fetch($sid) )
          or return $app->ajax_system_error();
        next if !$site;
        $user->set_site_privileges( site => $sid )
          or return $app->ajax_system_error();
        my $rparam = $app->_privilege_tmpl_params( $user, $site )
          or return ();
        push @tmpl_params, $rparam;
    }

    my $html = $app->html_template_screen( 'wi_account_privilege_item.tmpl',
        priv_items => \@tmpl_params, );

    return $app->ajax_html($html);
}

sub rm_ajax_cancel_privs {
    my $app = shift;
    $app->require_post() or return $app->ajax_system_error();
    my $editor = $app->current_user;
    my ( $user, $sid ) = $app->_get_userobj_and_siteid();
    return $app->ajax_system_error if !$user;
    if ( $user->id == $editor->id ) {
        $app->set_error(
            head => 'ACCOUNT_Cannot Edit Own Privs',
            text => 'ACCOUNT_TEXT_Cannot Edit Own Privs',
        );
        return $app->ajax_system_error();
    }

    my $priv = BigMed::Priv->select( { site => $sid, user => $user->id } )
      or return $app->ajax_system_error();
    $priv->trash_all or return $app->ajax_system_error();
    return $app->ajax_json_response( { 'valid' => 1 } );
}

sub rm_ajax_priv_form {
    my $app = shift;
    $app->require_post() or return $app->ajax_system_error();
    my $editor = $app->current_user;
    my ( $user, $sid ) = $app->_get_userobj_and_siteid();
    return $app->ajax_system_error if !$user;
    
    my ($priv, $site) = $app->_priv_and_site_to_edit($user,$sid);
    return $app->ajax_system_error() if !$priv;    
    
    my $limited = scalar( $priv->sections );
    my $level = $priv->level || $user->level || 2;
    my @l_options = (2..5);
    my %l_labels = map { $_ => $app->language("BM_User_Priv$_") } @l_options;
    my %has_priv = $user->allowed_section_hash($site);

    my $level_field = $app->prompt_field_ref(
        id        => "BM_ACCT_PRIVLEVEL",
        prompt_as => 'value_list',
        label     => 'user:level',
        description => 'ACCOUNT_Customize privilege level',
        required  => 1,
        value     => $level,
        options   => \@l_options,
        labels    => \%l_labels,
    );
    my $checkbox = $app->prompt_field_ref(
        id           => "BM_ACCT_ALLSECTIONS",
        prompt_as    => 'boolean',
        label        => 'ACCOUNTS_All sections',
        description  => 'ACCOUNTS_INFO_All sections',
        option_label => 'ACCOUNTS_DESC_All sections',
        value        => !$limited,
    );
    my $secref = $app->prompt_field_ref(
        id           => "BM_ACCT_SECTIONS_$sid",
        prompt_as    => 'select_section',
        site         => $site,
        label        => 'page:sections',
        description  => 'ACCOUNT_DESC_privilege_sections',
        multiple     => 8,
        container_id => 'bm_acct_sections_toggle',
        hidden       => !$limited,
        value        => [ keys %has_priv ],
    );
    my $sid_field = $app->prompt_field_ref(
        id           => "BM_ACCT_PRIVSITE",
        prompt_as    => 'hidden',
        value        => $sid,
    );
    my @fieldsets = (
        $app->prompt_fieldset_ref(
            title => ['ACCOUNT_Site privileges:Site', $site->name],
            fields => [$level_field, $checkbox, $secref, $sid_field,],
        ),
    );

    my $url = $app->build_url(
        script => 'bm-account.cgi',
        rm     => 'ajax-save-privs',
        args   => [$user->id, $sid],
    );
    my $html = $app->html_template_screen(
        'wi_account_priv_form.tmpl',
        form_url => $url,
        fieldsets   => \@fieldsets,
    );
    return $app->ajax_html($html);
}

sub rm_ajax_save_privs {
    my $app = shift;
    $app->require_post() or return $app->ajax_system_error();
    my $editor = $app->current_user;
    my ( $user, $sid ) = $app->_get_userobj_and_siteid();
    return $app->ajax_system_error if !$user;

    #don't operate directly on this priv, just returned by validator.
    #should use user->set_site_privileges
    my ( $priv, $site ) = $app->_priv_and_site_to_edit( $user, $sid );
    return $app->ajax_system_error() if !$priv;

    my %field = $app->parse_submission(
        {   id       => 'BM_ACCT_PRIVLEVEL',
            parse_as => 'value_list',
            required => 1,
            options  => [2, 3, 4, 5],
        },
        {   id       => 'BM_ACCT_ALLSECTIONS',
            parse_as => 'boolean',
        },
        {   id       => "BM_ACCT_SECTIONS_$sid",
            parse_as => 'select_section',
            site     => $site,
            multiple => 8,
        },
    );
    return $app->ajax_parse_error( $field{_ERROR} ) if $field{_ERROR};

    if ( $field{'BM_ACCT_PRIVLEVEL'} == 5 || $field{'BM_ACCT_ALLSECTIONS'} ) {
        undef $field{"BM_ACCT_SECTIONS_$sid"};
    }
    $user->set_site_privileges(
        site     => $site,
        sections => $field{"BM_ACCT_SECTIONS_$sid"},
        level    => $field{'BM_ACCT_PRIVLEVEL'}
      )
      or return $app->ajax_system_error;

    my $rparam = $app->_privilege_tmpl_params( $user, $site ) or return ();
    my $html = $app->html_template_screen( 'wi_account_privilege_item.tmpl',
        priv_items => [$rparam], );
    return $app->ajax_html($html);
}

sub _get_userobj_and_siteid {    #pull from path args[0,1]
    my $app = shift;
    my ( $uid, $sid ) = $app->path_args;
    foreach my $d ( $uid, $sid ) {
        $d ||= q{};
        $d =~ s/\D+//msg;
        next if $d;
        return $app->set_error(
            head => 'ACCOUNT_ERR_Missing data',
            text => 'ACCOUNT_ERR_TEXT_Missing data',
        );
    }
    my $user = $app->_get_validated_user($uid) or return;
    return ( $user, $sid );
}

sub _get_validated_user {
    my ( $app, $uid ) = @_;
    croak '_get_validated_user requires user id' if !$uid;

    #fetch and validate user and editor's privs to edit
    defined( my $user = BigMed::User->fetch($uid) )
      or return $app->ajax_system_error;
    if ( !$user ) { # could be a new user account
        $user = BigMed::User->new;
        $user->set_id($uid);
        return $app->set_error(
            head => 'ACCOUNT_ERR_No such user',
            text => 'ACCOUNT_ERR_TEXT_No such user',
        ) if !$user->has_valid_id;
    }

    my $editor = $app->current_user;
    if ( !$editor->can_edit_user($user) ) {
        return $app->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text =>
              'ACCOUNT_TEXT_You do not have permission to edit that account',
        );
    }
    return $user;
}

sub _priv_and_site_to_edit {
    my ($app, $user, $sid) = @_;
    my $editor = $app->current_user;    
    my $uid = $user->id;
    if ( $uid == $editor->id ) {
        return $app->set_error(
            head => 'ACCOUNT_Cannot Edit Own Privs',
            text => 'ACCOUNT_TEXT_Cannot Edit Own Privs',
        );
    }
    defined( my $site = BigMed::Site->fetch($sid) ) or return;
    if ( !$site ) {
        return $app->set_error(
            head => 'BM_No such site',
            text => ['BM_TEXT_No such site', $sid],
        );
    }
    defined ( my $priv = BigMed::Priv->fetch({site=>$sid, user=>$uid}) )
      or return;
    if (!$priv) {
        return $app->set_error(
            head => 'ACCOUNT_No privileges at site',
            text => 'ACCOUNT_TEXT_No privileges at site',
        );
    }
    return ($priv, $site);
}

sub _privilege_tmpl_params {
    my ( $app, $user, $site ) = @_;
    my $uid          = $user->id;
    my $edit         = $app->language('ACCOUNT_Customize');
    my $cancel       = $app->language('ACCOUNT_Cancel privileges');
    my $all_sections = $app->language('ACCOUNT_all sections');
    my $limited      = $app->language('ACCOUNT_limited_sections');
    my $section_text;

    my $ulevel = $user->level || 0;
    if ( $ulevel > 5 ) {    #admin
        $section_text = $all_sections;
    }
    else {
        my $priv = BigMed::Priv->fetch( { user => $uid, site => $site->id } );
        return if !defined $priv;
        next   if !$priv;
        $section_text = scalar( $priv->sections ) ? $limited : $all_sections;
        my $plevel = $priv->level;
        $plevel = $ulevel if !defined $plevel;
        $section_text .= ' (' . $app->language( "BM_User_Priv$plevel" ) . ')'
          if $plevel != $ulevel;
    }
    return {
        name           => $site->name,
        id             => $site->id,
        url            => $site->homepage_url . '/index.shtml',
        section_status => $section_text,
        edit           => $edit,
        cancel         => $cancel,
    };
}

sub _can_see_privileges {
    my ( $app, $editor, $uid ) = @_;
    
    #no editor if creating first account; also cannot edit self
    return if !$editor || $editor->id == $uid;
    
    #be careful, this can be Start.pm which doesn't have current_site
    my $site;
    if ( $app->can('current_site') ) {
        defined ( $site = $app->current_site ) or return;        
    }
    my $plevel = $site ? $editor->privilege_level($site) : $editor->level;
    
    #have to be at least webmaster at the current site
    return $plevel > 4;
}

sub _can_edit {
    my ( $app, $editor, $user, $roptions ) = @_;

    #is admin or webmaster level?
    my $is_admin = $roptions->{first_account}
      || $editor->level( $app->current_site ) >= 5;

    #a new user either has no id or no sites
    my $new_user;
    if ( !$user->id ) {
        $new_user = 1;
    }
    else {
        defined( my $user_sites = $user->allowed_site_selection() ) or return;
        $new_user = !$user_sites;
    }

    # can edit if it's the first account, if it's a new user and editor is
    # webmaster+ at current site, or it's an existing user where editor has
    # adequate powers.

    my $can_edit = $roptions->{first_account}
      || ( $new_user  && $is_admin )
      || ( !$new_user && $editor->can_edit_user($user) );

    return $can_edit || $app->set_error(
        head => 'CONTENT_Not Allowed To Do That',
        text =>
          'ACCOUNT_TEXT_You do not have permission to edit that account',
    );
}

sub _set_breadcrumbs {
    my $app        = shift;
    my $main_title = shift || 'ACCT_Account Menu';
    my $title      = shift;
    my $ref_bc     = shift;
    my @bc;

    if ( ref $ref_bc eq 'ARRAY' ) {
        @bc = @{$ref_bc};
    }
    elsif ($title) {
        my $site = $app->current_site ? $app->current_site->id : undef;
        @bc = (
            {   bc_label => $main_title,
                bc_url   => $app->build_url(
                    script => 'bm-account.cgi',
                    rm     => 'find-route',
                    site   => $site,
                ),
            },
            { bc_label => $title, },
        );
    }
    else {
        @bc = ( { bc_label => $main_title } );
    }

    return $app->set_cp_breadcrumbs(@bc);
}

1;

