# 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: LibEditor.pm 3046 2008-03-31 16:42:06Z josh $

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

use base qw(BigMed::App::Web::CP);
use BigMed::Library;
use BigMed::App::Web::PageNav;
use BigMed::Archive;
use BigMed::JSON;
use BigMed::DiskUtil
  qw(bm_file_path bm_untaint_filepath bm_delete_dir bm_dir_permissions);
use BigMed::MD5 qw(md5_hex);
use BigMed::Status;

my $MAX_ITEMS               = 50;
my $MIN_LEVEL_TO_EDIT_USERS = 5;

sub setup {
    my $app = shift;
    $app->start_mode('menu');
    $app->set_cp_selected_nav('Library');
    $app->run_modes(
        'AUTOLOAD'      => sub { $_[0]->rm_menu() },
        'menu'          => 'rm_menu',
        'edit'          => 'rm_edit',
        'save'          => 'rm_save',
        'delete'        => 'rm_delete',
        'batch'         => 'rm_batch',
        'batch-save'    => 'rm_batch_save',
        'change-status' => 'rm_change_status',
        'ajax-saveview' => 'rm_ajax_saveview',
    );
    return;
}

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

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

sub rm_menu {
    my $app       = shift;
    my %options   = @_;
    my $user      = $app->current_user or return $app->rm_login;
    my $site      = $app->current_site or return $app->rm_login_site;
    my @path_args = $app->path_args;
    my $lib       = $options{libtype} || $path_args[0];
    my $offset = defined $options{offset} ? $options{offset} : $path_args[1];

    #enforce valid class, default to image
    my @libraries = BigMed::Library->library_types;
    {
        my %is_lib = map { $_ => 1 } @libraries;
        $lib = 'image' if !$lib || !$is_lib{$lib};
    }
    my $libclass = BigMed::Library->class_for_label($lib);

    #get basic search info
    my $s = $app->session;
    my ( $order, $filter, $non, $mine );
    if ( $app->utf8_param('s') ) {
        $offset = 0;    #for new search
        my %f = $app->parse_submission(
            { id => 's',    parse_as => 'raw_text' },
            { id => 'non',  parse_as => 'boolean' },
            { id => 'mine', parse_as => 'boolean' },
            { id => 'q',    parse_as => 'simple_text' },
        );
        $s->param( 'LIBRARY_order',  ( $order  = $f{'s'} ) );
        $s->param( 'LIBRARY_filter', ( $filter = $f{'q'} ) );
        $s->param( 'LIBRARY_non',    ( $non    = $f{'non'} ) );
        $s->param( 'LIBRARY_mine',   ( $mine   = $f{'mine'} || q{} ) );
    }
    elsif ( $s->param('LIBRARY_justsaved') ) {
        $s->clear( ['LIBRARY_justsaved', 'LIBRARY_filter'] );
        $s->param( 'LIBRARY_order', ( $order = 'recent' ) );
        $non  = $s->param('LIBRARY_non');
        $mine = $s->param('LIBRARY_mine');
    }
    elsif ( $s->param('LIBRARY_type') && $lib eq $s->param('LIBRARY_type') ) {
        $order  = $s->param('LIBRARY_order');
        $filter = $s->param('LIBRARY_filter');
        $non    = $s->param('LIBRARY_non');
        $mine   = $s->param('LIBRARY_mine');
    }
    else {
        $s->param( 'LIBRARY_type', $lib );
        $s->clear(
            [qw(LIBRARY_order LIBRARY_filter LIBRARY_non LIBRARY_mine)] );

    }
    $order  ||= 'alpha';
    $offset ||= '0';
    $offset =~ s/\D+//msg;
    $filter = q{} if !defined $filter;

    #build type tabs
    my @tabs;
    {
        my @types = grep { $_ ne 'image' } @libraries;
        my $menu_url = $app->build_url(
            script => 'bm-library.cgi',
            rm     => 'menu',
            site   => $site,
        );
        foreach my $tab ( 'image', @types ) {
            push @tabs,
              { text     => $app->language( 'TITLE_PL_' . $tab ),
                selected => $lib eq $tab,
                url      => "$menu_url/$tab",
              };
        }
    }

    #collect items
    my ( $select, $all_count );
    {

        #get all w/out limit/offset/order for full count
        my $all_items = $libclass->collect_lib(
            site       => $site,
            user       => $user,
            in_lib     => !$non,
            editable   => $mine,
            namesearch => $filter,
          )
          or $app->error_stop;
        $all_count = $all_items->count;

  #and then winnow...
  #(leave user/filter/in_lib/editable off, automatically collects all, faster)
        $select = $libclass->collect_lib(
            cache  => $all_items,
            site   => $site,
            limit  => $MAX_ITEMS,
            offset => $offset,
            order  => $order,
          )
          or $app->error_stop;

        if ( !$select->count && $all_count ) {    #back up to last page
            my $npages = int( $all_count / $MAX_ITEMS );
            $npages++ if $all_count % $MAX_ITEMS;
            $offset = ( $npages - 1 ) * $MAX_ITEMS;
            $select = $libclass->collect_lib(
                cache  => $all_items,
                site   => $site,
                limit  => $MAX_ITEMS,
                offset => $offset,
                order  => $order,
              )
              or $app->error_stop;
        }
    }

    #get nav menu
    my $nav_menu_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        site   => $site,
        args   => [$lib],
    );
    my ( $pagenav_menu, $browse_text ) = $app->appweb_pagenav_menu(
        offset        => $offset,
        this_total    => $select->count,
        total_records => $all_count,
        per_page      => $MAX_ITEMS,
        url_callback  => sub { "$nav_menu_url/$_[0]" },
    );

    #coderef for building owner names
    my %user_name;
    my $ruser = sub {
        my $id = shift or return q{--};
        return $user_name{$id} if $user_name{$id};
        my $obj = BigMed::User->fetch($id);
        $user_name{$id} = $obj ? $obj->name : q{--};    #in case of deletion
    };

    #get html preview of all items
    my %preview = $libclass->preview();
    my @html_items;
    my $obj;
    my $base_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'edit',
        site   => $site,
        args   => [$lib],
    );
    my $lock_img = $app->env('BMADMINURL') . '/img/bmcp_lock.gif';
    while ( $obj = $select->next ) {
        my $date =
          $app->format_time( $obj->mod_time,
            { site => $site, no_time => 1 } );
        my $edit_url =
          $obj->editable_by_user($user) ? "$base_url/" . $obj->id : q{};

        push @html_items,
          { preview_img => $obj->lib_preview_image($site),
            edit_url    => $edit_url,
            preview_url => $obj->lib_preview_url($site),
            title       => $obj->lib_preview_title($site),
            owner       => $ruser->( $obj->owner ),
            mod_date    => $date,
            lock_img    => $lock_img,
            cid         => $obj->id,
          };
    }
    $app->error_stop if !defined $obj;

    #view is stored in class name -- set via css class
    my $view = $app->session->param('LIBRARY_view') || 'imagetext';
    my $view_class =
        $view eq 'image' ? 'bmcp_libmenu libStyleImage'
      : $view eq 'text'  ? 'libStyleText'
      : 'bmcp_libmenu';

    #grab the user names if we have 'em, and put into json field ref
    my $json_users;
    if ( $user->privilege_level($site) >= $MIN_LEVEL_TO_EDIT_USERS ) {
        my @users   = ( $app->_users_for_site ) or $app->error_stop;
        my @options = (q{});
        my %labels  = ( q{} => $app->language('BM_No change') );
        foreach my $u (@users) {
            push @options, $u->[0];
            $labels{ $u->[0] } = $u->[1];
        }
        my $ref = [
            'value_list',
            'BMLIB_USERS',
            {   options => \@options,
                labels  => \%labels,
                label   => $app->language('BM_Owner')
            }
        ];
        $json_users = objToJson($ref);
    }

    #initialize javascript
    $app->js_add_code(qq{BM.libType = "$lib";});
    $app->js_add_code(qq{BM.Library.UserSelect=$json_users;}) if $json_users;
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-library.js' );
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-actionmenu.js' );
    $app->js_add_onload(
        'BM.ActionMenu.update("libraryActionMenu","libraryMenu");');

    #title and crumbs
    my $lex_title = "LIBRARY_${lib}_library";
    $app->_set_lib_breadcrumbs($lib);
    my $title_unlocal = $options{head} || $lex_title;
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );

    #remaining template params
    my $self_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$lib, $order],
    );
    my $add_text =
      $app->language(
        ['EDITOR_New Title', $app->language( 'TITLE_' . $lib )] );
    my ( $batch_url, $batch_text );
    if ( $libclass->can_batch_upload ) {
        $batch_url = $app->build_url(
            script => 'bm-library.cgi',
            rm     => 'batch',
            args   => [$lib],
        );
        $batch_text = $app->language('LIBEDITOR_Batch Upload');
    }

    return $app->html_template_screen(
        'screen_libeditor_menu.tmpl',
        bmcp_title => $title,
        message    => $message,
        tabs       => \@tabs,

        add_url    => $base_url,
        add_text   => $add_text,
        batch_url  => $batch_url,
        batch_text => $batch_text,

        self_url      => $self_url,
        filter_string => $app->escape($filter),
        sort_recent   => ( $order eq 'recent' ),
        non_lib       => $non,
        is_admin      => ( $user->privilege_level( $site->id ) >= 5 ),
        mine_only     => $mine,
        view_class    => $view_class,

        ITEMS        => \@html_items,
        navbar       => $pagenav_menu,
        BMCP_LEFTCOL => $browse_text,
    );
}

sub rm_edit {
    my $app     = shift;
    my %options = @_;
    my $user    = $app->current_user or return $app->rm_login;
    my $site    = $app->current_site or return $app->rm_login_site;

    my $obj = $options{obj} || $app->_load_object_from_url($site)
      or return $app->rm_menu;

    if ( !$obj->editable_by_user($user) ) {
        $app->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text =>
              'CONTENT_TEXT_You do not have permission to edit that content',
        );
        return $app->rm_menu();
    }

    my $is_new = !$obj->id;
    $obj->update_id() or return $app->rm_menu;
    my $libtype  = $obj->data_label;
    my $libclass = ref $obj;

    #build the edit fields
    my @fields;
    foreach my $rfield_hash ( $obj->editor_fields ) {
        my $value;
        my $col = $rfield_hash->{column};
        if ($col) {
            $rfield_hash->{data_class} = $libclass;
            my $ref_getter = "_ref_$col";
            $value = $obj->$ref_getter;
        }
        my %addl = ( value => $value );
        my $callback = $rfield_hash->{prompt_callback};
        %addl = ( %addl, $callback->( $app, $obj ) ) if $callback;
        push @fields, $app->prompt_field_ref( %{$rfield_hash}, %addl );
    }
    if ( $user->privilege_level($site) >= $MIN_LEVEL_TO_EDIT_USERS ) {
        push @fields,
          $app->prompt_field_ref( %{ $app->_user_field_info($obj) } );
    }

    my @fieldsets = (
        $app->prompt_fieldset_ref(
            fields    => \@fields,
            query     => $options{query},
            field_msg => $options{field_msg},
        )
    );

    my $submit = $app->prompt_field_ref(
        id        => 'libeditor_submit',
        prompt_as => 'submit',
        value     => $app->language('BM_SUBMIT_LABEL_Save'),
    );
    push @fieldsets, $app->prompt_fieldset_ref( fields => [$submit], );

    my $leftcol = $app->edit_screen_leftcol($obj);
    return $app->rm_menu( offset => 0 ) if !defined $leftcol;

    my $form_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'save',
        args   => [$libtype, $obj->id],
    );

    #headline/message text
    my $edit_label    = $app->language( 'TITLE_' . $libtype );
    my $action        = $is_new ? 'EDITOR_New Title' : 'EDITOR_Edit Title';
    my $title_unlocal = $options{head} || [$action, $edit_label];
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );
    $app->_set_lib_breadcrumbs( $libtype, [$action, $edit_label] );

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

}

sub rm_delete {
    my $app = shift;
    $app->require_post() or $app->error_stop;
    my $site     = $app->current_site;
    my $lib      = ( $app->path_args )[0] || q{};
    my $libclass = $app->_libclass_from_type($lib)
      or return $app->rm_menu( offset => 0 );

    my ( $rids, $dgroup ) = $app->_gather_checkbox_objects($libclass);
    return $app->rm_menu( offset => 0 ) if !$rids;    #error, or nothing to do

    #cannot call $libclass->build_dependent_pages because trashing the
    #library items deletes the pointers, so no pages get rebuilt.
    #have to use a custom workaround instead.

    my $rcontent =
      $libclass->dependent_content( { site => $site, id => $rids } )
      or return $app->rm_menu( offset => 0 );
    $dgroup->trash_all() or return $app->rm_menu( offset => 0 );
    $app->_build_dependent_pages_dropin($libclass, $rcontent)
      or return $app->rm_menu( offset => 0 );

    my $menu = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$lib],
    );
    return $app->_redirect( $menu,
        'EDITOR_Items deleted, changes made on live site' );
}


sub _build_dependent_pages_dropin {
    my ($app, $libclass, $rcontent) = @_;
    my $site = $app->current_site;
    
    #this stands in for Library->build_dependent_pages which
    #is no good for building dependent pages for deleted library items.
    #this is nasty cut-n-paste code, and I should probably build it
    #into the Library class instead, at least as an internal
    #method, so that it's not just duplicated code...
    
    if ( @{ $rcontent->{section} } ) {
        if ( @{ $rcontent->{index_id} } ) {
            require BigMed::Search::Scheduler;
            import BigMed::Search::Scheduler;
            schedule_index( $site, $rcontent->{index_id} )
              or return;
        }

        require BigMed::Builder;
        my $builder = BigMed::Builder->new( site => $site ) or return;
        $builder->build(
            pages          => $rcontent->{page_id},
            sections       => $rcontent->{section},
            defer_overflow => 1,
          )
          or return;
    }
    return 1;
}

sub rm_save {
    my $app  = shift;
    my $user = $app->current_user or return $app->rm_login;
    my $site = $app->current_site or return $app->rm_login_site;

    my $obj = $app->_load_object_from_url($site)
      or return $app->rm_edit( query => $app->query );

    if ( $user->privilege_level($site) >= $MIN_LEVEL_TO_EDIT_USERS ) {
        my %field = $app->parse_submission( $app->_user_field_info($obj) );
        return $app->rm_edit(
            query     => $app->query,
            field_msg => $field{_ERROR}
          )
          if $field{_ERROR};
        $obj->set_owner( $field{owner} );
    }

    $obj->save_submission( user => $user )
      or return $app->rm_edit( query => $app->query );
    $obj->build_dependent_pages( { site => $site } )
      or return $app->rm_edit( query => $app->query );

    my $menu = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$obj->data_label],
    );
    $app->session->param( 'LIBRARY_justsaved', '1' );
    return $app->_redirect( $menu, 'BM_Your changes have been saved.' );
}

my $NUM_ITEMS = 3;

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

    my $user = $app->current_user or return $app->rm_login;
    my $site = $app->current_site or return $app->rm_login_site;
    my $lib = ( $app->path_args )[0] || 'image';
    my $libclass = $app->_get_batch_upload_class($lib)
      or return $app->rm_menu( libtype => $lib, offset => 0 );

    my ( $rshared,      $rfields ) = $app->_get_batch_upload_cols($libclass);
    my ( $shared_field, @fields )  =
      map { $app->prompt_field_ref( %{$_} ) } ( $rshared, @{$rfields} );

    my $num_field = $app->prompt_field_ref(
        id        => 'num_uploads',
        prompt_as => 'hidden',
        value     => $NUM_ITEMS,
    );
    my @global_fieldset = (
        $app->prompt_fieldset_ref(
            fields    => [$shared_field, $num_field],
            query     => $options{query},
            field_msg => $options{field_msg},
        ),
    );

    my $max_upload = $site->site_doclimit || $app->env('ADMIN_DOCLIMIT');
    $max_upload =
      $max_upload >= 1024
      ? sprintf( '%.1f', ( $max_upload / 1024 ) ) . ' MB'
      : "$max_upload KB";

    my ( $archive_intro, @archive_fieldset );
    my $can_extract = bma_can_extract();
    if ($can_extract) {
        my $compressed_field = $app->prompt_field_ref(
            id          => 'compressed',
            prompt_as   => 'document',
            label       => 'LIBEDITOR_Archive File',
            description => 'LIBEDITOR_DESC_Archive File',
            required    => 1,
        );
        $archive_intro = $app->language(
            [   'LIBEDITOR_About compressed file',
                $max_upload,
                join( ', ', bma_all_extract_types() )
            ]
        );
        push @archive_fieldset,
          $app->prompt_fieldset_ref(
            fields    => [$compressed_field],
            query     => $options{query},
            field_msg => $options{field_msg},
          );
    }

    #create several fieldsets for each item to upload, each set preceded
    #with BATCHn_
    my $edit_title = $app->language( 'TITLE_' . $lib );
    my @indiv_fieldsets;
    foreach my $n ( 1 .. $NUM_ITEMS ) {
        my @numbered_fields;
        foreach my $rfield (@fields) {
            my @trivalue = @{$rfield};
            $trivalue[1] = "BATCH${n}_$trivalue[1]";
            push @numbered_fields, \@trivalue;
        }
        push @indiv_fieldsets,
          $app->prompt_fieldset_ref(
            fields    => \@numbered_fields,
            query     => $options{query},
            field_msg => $options{field_msg},
            title     => ['LIBEDITOR_Upload num', $edit_title, $n],
          );
    }

    #submit fieldset
    my $submit = $app->prompt_field_ref(
        id        => 'libeditor_submit',
        prompt_as => 'submit',
        value     => $app->language('BM_SUBMIT_LABEL_Save'),
    );
    my @submit_fieldset =
      ( $app->prompt_fieldset_ref( fields => [$submit] ) );

    my $form_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'batch-save',
        args   => [$lib],
    );

    my $edit_label = $app->language("TITLE_PL_$lib");
    my $title_unlocal = ['LIBEDITOR_Batch Upload', $edit_label];
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );
    $app->_set_lib_breadcrumbs( $lib, $title_unlocal );

    #set javascript
    my $rjson_attr =
      BigMed::JSON::prep_class_attributes( $app, $libclass, $rfields );
    my $json_field = objToJson( $rjson_attr->{schema_fields} );
    my $js_type    = $app->js_escape($edit_title);
    $app->js_add_code(
        "BM.Library.bName='$js_type';BM.Library.bFields=$json_field;");
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-library.js' );
    $app->js_init_radio_tabs() if $can_extract;

    return $app->html_template_screen(
        'screen_libeditor_batch.tmpl',
        bmcp_title      => $title,
        message         => $message,
        form_url        => $form_url,
        global_fieldset => \@global_fieldset,
        indiv_fieldsets => \@indiv_fieldsets,
        indiv_intro     =>
          $app->language( ['LIBEDITOR_About individual', $max_upload] ),
        batch_total      => $NUM_ITEMS,
        archive_fieldset => \@archive_fieldset,
        archive_intro    => $archive_intro,
        submit_fieldset  => \@submit_fieldset,
    );

}

sub rm_batch_save {
    my $app      = shift;
    my $user     = $app->current_user or return $app->rm_login;
    my $site     = $app->current_site or return $app->rm_login_site;
    my $lib      = ( $app->path_args )[0] || q{};
    my $libclass = $app->_get_batch_upload_class($lib)
      or return $app->rm_menu( libtype => $lib, offset => 0 );
    my %type = $app->parse_submission(
        {   id       => 'uploadtype',
            parse_as => 'radio_toggle',
        }
    );
    return $app->rm_batch( query => $app->query, field_msg => $type{_ERROR} )
      if $type{_ERROR};

    #display the bulk of the page
    my $edit_label = $app->language("TITLE_PL_$lib");
    my $title_unlocal = ['LIBEDITOR_Batch Save', $edit_label];
    my ( $title, $message ) =
      $app->title_and_message( title => $title_unlocal, );
    $app->_set_lib_breadcrumbs( $lib, $title_unlocal );
    print $app->_send_headers();
    print $app->html_template_screen(
        'screen_libeditor_batch_save.tmpl',
        container  => $app->{container},
        bmcp_title => $title,
        message    => $message,
    );

    my $nsaved =
        $type{uploadtype} eq 'Archive'
      ? $app->_save_archive_batch($libclass)
      : $app->_save_indiv_batch($libclass);
    my @message;
    if ($nsaved) {
        @message = ( ['LIBEDITOR_Saved items to library', $nsaved] );
        $app->session->param( 'LIBRARY_justsaved', '1' );
    }
    elsif ( !defined $nsaved ) {
        my %err = $app->error_html_hash;
        @message = ( $err{text}, 'error' );
        $app->session->param( 'LIBRARY_justsaved', '1' );
    }
    $app->set_session_message(@message);

    #wrap up and redirect
    my $menu = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$lib],
    );
    print $app->html_template( 'wi_libeditor_batch_save_bottom.tmpl',
        url => $menu, );

    $app->teardown;
    exit;
}

sub rm_change_status {
    my $app = shift;
    $app->require_post() or $app->error_stop;
    my $site     = $app->current_site;
    my $user     = $app->current_user;
    my $lib      = ( $app->path_args )[0] || q{};
    my $libclass = $app->_libclass_from_type($lib)
      or return $app->rm_menu( offset => 0 );

    #get field values
    my @fields = (
        {   id       => 'BMLIB_in_lib',
            parse_as => 'value_list',
            options  => [q{}, '1', '0'],
        },
        {   id       => 'BMLIB_shared',
            parse_as => 'value_list',
            options  => [q{}, '1', '0'],
        },
    );
    if ( $user->privilege_level($site) >= $MIN_LEVEL_TO_EDIT_USERS ) {
        my @users = $app->_users_for_site;
        return $app->rm_menu( offset => 0 ) if !@users;
        push @fields,
          { id       => 'BMLIB_USERS',
            parse_as => 'value_list',
            options  => [q{}, map { $_->[0] } @users],
          };
    }
    my %field = $app->parse_submission(@fields);
    return $app->rm_menu( offset => 0 ) if $field{_ERROR};    #fail silently

    #get setter values
    my %setter;
    $setter{set_in_lib} = $field{BMLIB_in_lib} + 0
      if $field{BMLIB_in_lib} ne q{};
    $setter{set_shared} = $field{BMLIB_shared} + 0
      if $field{BMLIB_shared} ne q{};
    $setter{set_owner} = $field{BMLIB_USERS} + 0 if $field{BMLIB_USERS};
    return $app->rm_menu( offset => 0 ) if !%setter;    #no values to set

    #get checkbox objects
    my ( $rids, $dgroup ) = $app->_gather_checkbox_objects($libclass);
    return $app->rm_menu( offset => 0 ) if !$rids;    #error, or nothing to do

    my $obj;
    while ( $obj = $dgroup->next ) {
        while ( my ( $method, $v ) = each %setter ) {
            $obj->$method($v);
        }
        $obj->save or return $app->rm_menu( offset => 0 );
    }
    return $app->rm_menu( offset => 0 ) if !defined $obj;

    my $menu = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$lib],
    );
    return $app->_redirect( $menu, 'BM_Your changes have been saved.' );
}

sub rm_ajax_saveview {
    my $app = shift;
    $app->require_post() or return $app->ajax_system_error;

    #response doesn't really matter, since the browser just fires
    #off the update and forgets about it. But we do error handling
    #just so we have it handy in case things change
    my $site = $app->current_site or return $app->rm_ajax_login_badsite;
    my $user = $app->current_user or return $app->rm_ajax_login;

    my $view = $app->utf8_param('view') || q{};
    if ( $view ne 'imagetext' && $view ne 'image' && $view ne 'text' ) {
        return $app->ajax_error('unknown view value');
    }
    $app->session->param( 'LIBRARY_view', $view );
    return $app->ajax_data( { valid => 'OK' } );
}

sub _libclass_from_type {
    my ( $app, $lib ) = @_;
    my @libraries = BigMed::Library->library_types;
    my %is_lib = map { $_ => 1 } @libraries;
    if ( !$is_lib{$lib} ) {
        return $app->set_error(
            head => 'EDITOR_Unknown Content Type',
            text => ['EDITOR_TEXT_Unknown Content Type', $lib],
        );
    }
    return BigMed::Library->class_for_label($lib);
}

sub _user_field_info {
    my ( $app, $obj ) = @_;
    my $owner_id = $obj ? $obj->owner : undef;
    my @users = $app->_users_for_site;
    return $app->rm_menu( offset => 0 ) if !@users;
    return {
        id        => 'owner',
        prompt_as => 'value_list',
        options   => [map { $_->[0] } @users],
        labels    => { map { $_->[0] => $_->[1] } @users },
        required  => 1,
        value => ( $owner_id || $app->current_user->id ),
        label => 'BM_Owner',
    };
}

sub _users_for_site {
    my $app       = shift;
    return
        map { [$_->[0], $_->[1]] }
        sort { $a->[2] cmp $b->[2] }
        map { [$_->id, $_->name, lc $_->name] }
        $app->current_site->users();
}

sub edit_screen_leftcol {
    my ( $app, $obj ) = @_;
    my $user = $app->current_user;

    my $mod_time = $obj->mod_time || q{};
    $mod_time &&= $app->format_time($mod_time);
    my $create_time = $obj->create_time || q{};
    $create_time &&= $app->format_time($create_time);

    my @page_links;
    if ($create_time) {    #not new
        @page_links = $app->_get_linking_page_info($obj);
    }

    my $owner_id = $obj->owner || 0;
    my $owner_name;
    if ( !$owner_id || $owner_id == $user->id ) {
        $owner_name = $user->name;
    }
    else {
        defined( my $owner = BigMed::User->fetch($owner_id) )
          or $app->error_stop;
        $owner_name = $owner ? $owner->name : q{--};
    }

    my $heading = $app->language(
        [   'LIBEDITOR_About_this',
            $app->language( 'TITLE_' . $obj->data_label )
        ]
    );
    return $app->html_template(
        'wi_libeditor_edit_leftcol.tmpl',
        page_links    => \@page_links,
        owner         => $owner_name,
        mod_time      => $mod_time,
        create_time   => $create_time,
        about_heading => $heading,
    );
}

sub _gather_checkbox_objects {
    my ( $app, $libclass ) = @_;
    my $site = $app->current_site;
    my $user = $app->current_user;

    #gather and vet ids
    my @id;
    {
        my @items = $app->utf8_param('c');
        foreach my $c (@items) {
            $c =~ s/\D+//msg;
            push @id, $c if $c;
        }
    }
    return if !@id;

    #get the group and then winnow for editable (we just silently
    #ignore items that cannot be edited).
    my $sid = $site->id;
    my $group = $libclass->select( { site => $sid, id => \@id } )
      or return;
    return \@id,
      $libclass->collect_lib(
        cache    => $group,
        user     => $user,
        site     => $sid,
        editable => 1,
      );
}

sub _save_indiv_batch {
    my ( $app, $data_class ) = @_;
    my $user = $app->current_user or return;
    my $site = $app->current_site or return;

    my $num = $app->utf8_param('BATCH_total') || q{};
    $num =~ s/\D//msg;
    $num ||= $NUM_ITEMS;

    my $sbar = BigMed::Status->new(
        {   container => 'bmLibEditorStatus',
            inline    => 1,
            steps     => $num,
        }
    );
    $sbar->update_status();

    my ( $shared_field, $rfields ) =
      $app->_get_batch_upload_cols($data_class);
    my @edit_fields = @{$rfields};
    my $is_shared   = $app->utf8_param('shared');
    my $uid         = $user->id;
    my $sid         = $site->id;
    my @errors;
    my $nsaved = 0;
    my $q = $app->query;
  OBJ: foreach my $n ( 1 .. $num ) {
        my $prefix = "BATCH${n}_";
        $sbar->update_status(
            progress => $n - 1,
            message  => ['LIBEDITOR_Processing file', $n, $num],
        );

        #make sure that we have all fields and a value in the upload field;
        #IMPORTANT: Have to use param (not app->utf8_param) to check field
        #existence, because of file/upload issues
        foreach my $f (@edit_fields) {
            my $id = $f->{column} || $f->{id};
            if ( $f->{prompt_as} && $f->{prompt_as} eq 'document' ) {
                next OBJ if !$q->param("$prefix$id");    #no document upload
            }
            next OBJ if !defined $q->param("$prefix$id");
        }

        my $obj = $data_class->new();
        $obj->set_shared($is_shared);
        $obj->set_in_lib(1);
        $obj->set_owner($uid);
        $obj->set_site($sid);
        my $saved = $obj->save_submission(
            use_fields   => $rfields,
            field_prefix => $prefix,
            user         => $user,
        );

        #populate a missing title with the filename
        my $title_col = $obj->can('name') ? 'name' : 'title';
        if ( $saved && !$obj->$title_col ) {
            my $setter = "set_$title_col";
            $obj->$setter( $obj->batch_upload_title );
            $saved = $obj->save;
        }

        if ( !$saved ) {    #caught an error along the way.
            my %err = $obj->error_html_hash;
            my $msg = $err{text} || 'BM_Unknown error';
            push @errors, $app->language(
                ['LIBEDITOR_Item error', $n, $app->language($msg)] );
            $obj->clear_error;
        }
        else {
            $nsaved++;
        }

    }

    $sbar->mark_done;
    if (@errors) {
        my $err_html = join( '<br /><br />', @errors );
        my $err_code = $nsaved
          ? ['LIBEDITOR_Partial save', $nsaved, $err_html]
          : ['LIBEDITOR_None saved', $err_html];
        return $app->set_error(
            head => 'LIBEDITOR_Batch Save Incomplete',
            text => $err_code,
        );
    }
    return $nsaved;
}

sub _save_archive_batch {
    my ( $app, $data_class ) = @_;
    my $user = $app->current_user or return;
    my $site = $app->current_site or return;

    my $sbar = BigMed::Status->new(
        {   message   => ('LIBEDITOR_Extracting files...'),
            container => 'bmLibEditorStatus',
            inline    => 1,
        }
    );
    $sbar->update_status();

    my %field = $app->parse_submission(
        {   id       => 'shared',
            parse_as => 'boolean',
        },
        {   id         => 'compressed',
            parse_as   => 'document',
            file_types => [bma_all_extract_types(), 'gz'],
        },
    );
    if ( $field{_ERROR} && $field{_ERROR}->{compressed} ) {
        $app->set_error(
            head => 'BM_Trouble_processing_form',
            text => $field{_ERROR}->{compressed}
        );
        $sbar->send_error();
    }

    #extract the archive to $tdir
    my ( $archfilename, $archpath, $ARCH ) = @{ $field{compressed} };

    my $archtype = bma_archive_type($archfilename);
    my $target   = substr( md5_hex( $user->name . time . rand ), 15 );
    my $tdir     =
      bm_file_path( $app->env('MOXIEDATA'), 'worktemp', 'extract', $target );
    my $extracted = bma_extract( $archpath, $tdir, $archtype );
    close $ARCH;
    if ( !$extracted ) {
        bm_delete_dir($tdir);
        $sbar->send_error();
    }
    if ( !bm_dir_permissions($tdir) ) {
        $sbar->send_error();
    }

    #collect all file paths
    my @files = $app->_find_nested_files($tdir);
    if ( $app->error ) {
        bm_delete_dir($tdir);
        $sbar->send_error();
    }

    #figure out the data format and build a coderef to handle it
    my $file_col = $data_class->batch_upload_column;
    my %prop     = $data_class->properties;
    my $rdo_format;    # @_ = ($safe_file, $filepath, $handle)
    if ( $prop{$file_col}->{type} eq 'image_file' ) {
        $rdo_format = sub {
            return [
                {   filename   => $_[0],
                    tempfile   => $_[1],
                    filehandle => $_[2],
                    dimensions => 'orig',
                    manual     => undef,
                }
            ];
          }
    }
    else {    #regular document
        $rdo_format = sub { return [@_]; };
    }

    #process all objects from these paths
    my $uid       = $user->id;
    my $sid       = $site->id;
    my $is_shared = $field{shared};
    my $file_set  = "set_$file_col";
    my $title_set = $data_class->can('set_name') ? 'set_name' : 'set_title';
    my %allowed   = map { ( $_ => 1 ) } @{ $app->env('FILETYPES') };
    my @errors;
    my $nsaved = 0;

    my $nfiles = scalar @files;
    my $fcount = 0;
    foreach my $f (@files) {

        $sbar->update_status(
            steps    => $nfiles,
            progress => $fcount,
            message  => ['LIBEDITOR_Processing file', $fcount + 1, $nfiles],
        );
        $fcount++;

        my $obj = $data_class->new();
        $obj->set_shared($is_shared);
        $obj->set_in_lib(1);
        $obj->set_owner($uid);
        $obj->set_site($sid);

        my $newf = BigMed::App::Web::Parse::_safe_filename_from_path($f);

        #check that it's an approved file type
        my $bad_type;
        my $suffix = ( $f =~ /[.]([a-zA-Z0-9]+)$/ms ) ? lc $1 : q{};
        if ( !$allowed{$suffix} ) {
            my $msg = ['PARSE_Not an approved file type', $suffix];
            push @errors, $app->language(
                ['LIBEDITOR_Filename error', $newf, $app->language($msg)] );
            next;
        }
        my $FILE;
        open( $FILE, '<', $f ) or warn "Could not open $f: $!", next;
        my $rvalue = { $file_col => $rdo_format->( $newf, $f, $FILE ) };
        $obj->post_parse($rvalue);

        my $success;
        if ( !$rvalue->{_ERROR} && !$app->error ) {
            $obj->$file_set( $rvalue->{$file_col} );
            $obj->$title_set( $obj->batch_upload_title );
            if ( $success = $obj->save ) {
                $nsaved++;
            }
        }

        if ( !$success ) {
            if ( $app->error ) {
                my %err = $obj->error_html_hash;
                my $msg = $err{text} || 'BM_Unknown error';
                push @errors,
                  $app->language(
                    ['LIBEDITOR_Filename error', $newf, $app->language($msg)]
                  );
                $obj->clear_error;
            }
            if ( $rvalue->{_ERROR} ) {
                my $msg = $rvalue->{_ERROR}->{$file_col};
                push @errors,
                  $app->language(
                    ['LIBEDITOR_Filename error', $newf, $app->language($msg)]
                  )
                  if $msg;

            }
        }

    }
    if ( !bm_delete_dir($tdir) ) {
        $sbar->send_error;
    }
    $sbar->mark_done;

    if (@errors) {
        my $err_html = join( '<br /><br />', @errors );
        my $err_code = $nsaved
          ? ['LIBEDITOR_Partial save', $nsaved, $err_html]
          : ['LIBEDITOR_None saved', $err_html];
        return $app->set_error(
            head => 'LIBEDITOR_Batch Save Incomplete',
            text => $err_code,
        );
    }
    return $nsaved;
}

sub _find_nested_files {
    my ( $app, $dir ) = @_;

    #would be nice to use File::Find but it doesn't do untainting
    $dir = bm_untaint_filepath($dir) or return;
    my ( $DIR, $file, @files );
    opendir( $DIR, $dir )
      or return $app->set_io_error( $DIR, 'opendir', $dir, $! );
    while ( defined( $file = readdir($DIR) ) ) {
        next if index( $file, q{.} ) == 0;
        my $path = bm_file_path( $dir, $file );
        if ( -d $path ) {
            push @files, $app->_find_nested_files($path);
        }
        else {
            push @files, $path;
        }
    }
    closedir($DIR);
    return @files;
}

sub _get_batch_upload_cols {
    my ( $app, $libclass ) = @_;
    my $upload_field = $libclass->batch_upload_column;

    my ( @fields, $shared_field );
    foreach my $rfield ( $libclass->editor_fields ) {
        my %field = %{$rfield};
        my $id = $field{column} || $field{id};
        $field{data_class} = $libclass;
        delete $field{required};
        if ( $id eq 'name' || $id eq 'title' ) {
            push @fields, \%field;
        }
        elsif ( $id eq $upload_field ) {

            #make some exceptions for image, which has quite a thorny
            #collection of fields
            if ( $libclass->isa('BigMed::Media::Image') ) {
                push @fields,
                  ( {   id        => "${id}__AUTO",
                        value     => 1,
                        prompt_as => 'hidden',
                    },
                    {   id        => "${id}__MSTR",
                        label     => 'image:formats',
                        prompt_as => 'document'
                    },
                    {   column     => $id,
                        data_class => $libclass,
                        prompt_as  => 'hidden'
                    },
                  );
            }
            else {
                $field{prompt_as} = 'document';
                push @fields, \%field;
            }
        }
        elsif ( $id eq 'shared' ) {
            $field{option_label} = 'LIBRARY_Allow others to use these items';
            $shared_field = \%field;
        }
    }
    return ( $shared_field, \@fields );
}

sub _get_batch_upload_class {
    my ( $app, $lib ) = @_;
    my %check_type = map { $_ => 1 } BigMed::Library->library_types;
    if ( !$check_type{$lib} ) {
        return $app->set_error(
            head => 'EDITOR_Unknown Content Type',
            text => ['EDITOR_TEXT_Unknown Content Type', $lib],
        );
    }
    my $libclass = BigMed::Library->class_for_label($lib);
    if ( !$libclass->can_batch_upload ) {
        return $app->set_error(
            head => 'LIBEDITOR_Cannot batch upload',
            text => [
                'LIBEDITOR_TEXT_Cannot batch upload',
                $app->language("TITLE_PL_$lib")
            ],
        );
    }
    return $libclass;
}

sub _load_object_from_url {
    my ( $app,     $site ) = @_;
    my ( $libtype, $id )   = $app->path_args;
    $libtype ||= q{not specified};
    $id      ||= q{};
    $id =~ s/\D//msg;
    my $class = $app->_libclass_from_type($libtype) or return;

    my $obj;
    if ($id) {
        $obj =
          $class->fetch( { site => $site->id, id => $id }, { limit => 1 } );
        return if !defined $obj;
    }
    if ( !$obj ) {    #no such object, or new
        $obj = $class->new();
        $obj->set_site( $site->id );
        if ($id) {
            $obj->set_id($id);

            #prevent arbitrary advancement of the counter into the future
            #has to be an id that we've seen before
            $obj->has_valid_id()
              or return $class->set_error(
                head => 'CONTENT_Invalid content ID',
                text => ['CONTENT_TEXT_Invalid content ID', $libtype, $id],
              );
        }
    }
    return $obj;
}

sub _get_linking_page_info {
    my ( $app, $obj ) = @_;
    my $site = $app->current_site;
    my $sid  = $site->id;
    my $user = $app->current_user;

    require BigMed::Content::Page;
    my $pages = BigMed::Content::Page->join_points_to(
        {   join   => ref $obj,
            site   => $site->id,
            unique => 1,
            limit  => 10,
        },
        { id   => $obj->id },
        { sort => 'mod_time', order => 'descend' }
    );
    return if !$pages || !$pages->count;

    my $preview = $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'preview',
        site   => $sid,
        args   => 'page',
    );

    my ( $p, @page_links );
    while ( $p = $pages->next ) {
        my $url = $p->active_page_url($site)
          || ( $p->editable_by_user($user) ? "$preview/" . $p->id : undef );
        push @page_links,
          { title => $p->title,
            url   => $url,
          };
    }
    return if !defined $p;
    return @page_links;
}

sub _set_lib_breadcrumbs {
    my ( $app, $lib, $this_title ) = @_;
    my $lex_title = "LIBRARY_${lib}_library";
    if ( !$this_title ) {
        return $app->set_cp_breadcrumbs( { bc_label => $lex_title } );
    }

    my $base_url = $app->build_url(
        script => 'bm-library.cgi',
        rm     => 'menu',
        args   => [$lib],
    );

    return $app->set_cp_breadcrumbs(
        { bc_label => $lex_title, bc_url => $base_url },
        { bc_label => $this_title },
    );
}

sub _redirect {
    my ( $app, $url, $message, $err ) = @_;
    $app->set_session_message( $message, $err ) if $message;
    $app->header_type('redirect');
    $app->header_props( -url => $url );
    return "Redirecting to $url";
}

1;

__END__

