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

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

use base qw(BigMed::App::Web::CP);
use BigMed::Content;
use BigMed::Plugin;
use BigMed::JSON;
use BigMed::DiskUtil qw(bm_delete_file bm_file_path);
use BigMed::App::Web::PageNav;
use BigMed::URL;
use BigMed::PageAlert;
use BigMed::Status;
use BigMed::Search::Scheduler;

sub setup {
    my $app = shift;
    $app->start_mode('menu');
    $app->set_cp_selected_nav('Edit');
    $app->run_modes(
        'AUTOLOAD'         => sub { $_[0]->rm_menu() },
        'menu'             => 'rm_menu',
        'edit'             => 'rm_edit',
        'save'             => 'rm_save',
        'preview'          => 'rm_preview',
        'delete'           => 'rm_delete',
        'choose-sec'       => 'rm_choose_sec',
        'move-sec'         => 'rm_move_sec',
        'copy'             => 'rm_copy',
        'copyxsite'        => 'rm_copyxsite',
        'change-status'    => 'rm_change_status',
        'ajax-save-mini'   => 'rm_ajax_save_mini',
        'ajax-delete-mini' => 'rm_ajax_delete_mini',
        'ajax-miniobj'     => 'rm_ajax_miniobj',
        'ajax-pages'       => 'rm_ajax_pages',
        'ajax-copyxsite'   => 'rm_ajax_copyxsite',
        'ajax-diff'        => 'rm_ajax_diff',
    );
    return;
}

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

sub require_sections {
    my $app  = shift;
    my $site = shift;
    defined( my $homepage = $site->homepage_obj ) or $app->error_stop;
    return 1 if $homepage;

    my $url = $app->build_url(
        script => 'bm-sections.cgi',
        rm     => 'outline',
        site   => $site->id,
    );
    $app->set_error(
        head => 'EDITOR_No sections',
        text => ['EDITOR_TEXT_No sections', $url],
    );
    return $app->error_stop;    #actually, it exits
}

###########################################################
# CALLBACKS
###########################################################

BigMed::App::Web::Editor->add_trigger( 'editor_new_content',
    \&_content_added );
BigMed::App::Web::Editor->add_trigger( 'editor_status_change',
    \&_status_change );

sub _content_added {
    my ( $app, $obj, $site ) = @_;
    BigMed::PageAlert->notify( 'page_new', $obj, $site )
      if $obj->isa('BigMed::Content::Page');
    return 1;
}

sub _status_change {
    my ( $app, $obj, $site ) = @_;
    BigMed::PageAlert->notify( 'page_status', $obj, $site )
      if $obj->isa('BigMed::Content::Page');
    return 1;
}

###########################################################
# 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;
    $app->require_sections($site);

    my $site_id = $site->id;
    my ( $class, $type, $subtype ) = $app->_class_type_and_subtype_from_url();
    $app->error_stop if !$class;

    #get sort info
    my %default_sort = (
        'mod_time' => 'descend',
        'pub_time' => 'descend',
        'title'    => 'ascend',
        'priority' => 'descend',
    );
    my $session = $app->session;
    my ( $sort, $order, $offset ) = ( $app->path_args )[1 .. 3];
    my ( $rsort, $rorder );

    if ( !$sort && $session->param('EDITOR_MENU_SORT') ) {
        $rsort  = $session->param('EDITOR_MENU_SORT');
        $rorder = $session->param('EDITOR_MENU_ORDER');
    }
    else {
        $sort  ||= q{};
        $order ||= q{};
        my @sort  = split( /[.]/ms, $sort );
        my @order = split( /[.]/ms, $order );
        foreach my $s (@sort) {
            next if !$default_sort{$s};    #not a supported order
            push @{$rsort}, $s;
            my $o = shift @order;
            $o = $default_sort{$s}
              if !$o || ( $o ne 'ascend' && $o ne 'descend' );
            push @{$rorder}, $o;
        }
    }
    if ( !$rsort ) {    #couldn't come up with anything meaningful from url
        $rsort  = ['mod_time', 'priority', 'pub_time', 'title'];
        $rorder = ['descend',  'descend',  'descend',  'ascend'];
    }
    $session->param( 'EDITOR_MENU_SORT',  $rsort );
    $session->param( 'EDITOR_MENU_ORDER', $rorder );

    #fetch search criteria, including filter info; get *all* matches to start
    my $rcriteria = $app->_get_menu_search_criteria( $class, $subtype );
    my $selection = $class->select($rcriteria) or $app->error_stop;
    my $full_count = $selection->count;

    #winnow and sort this collection according to the current offset
    $offset ||= q{};
    $offset =~ s/\D//msg;
    $offset ||= 0;
    my $per_page = $app->env('MENUNUMTODISPLAY') || 100;
    my %param = (
        offset => $offset,
        limit  => $per_page,
        order  => $rorder,
        sort   => $rsort,
    );
    $selection = $selection->select( undef, \%param ) or $app->error_stop;
    if ( !$selection->count && $full_count ) {    #back up to last page
        my $npages = int( $full_count / $per_page );
        $npages++ if $full_count % $per_page;
        $offset = $param{offset} = ( $npages - 1 ) * $per_page;
        $selection = $class->select( $rcriteria, \%param )
          or $app->error_stop;
    }

    #navigation bar and browse text
    my $nav_url =
      $app->menu_navbar_base_url( $type, $subtype, $rsort, $rorder );

    my ( $pagenav_menu, $browse_text ) = $app->appweb_pagenav_menu(
        offset        => $offset,
        this_total    => $selection->count,
        total_records => $full_count,
        per_page      => $per_page,
        url_callback  => sub { return "$nav_url/$_[0]" },
    );

    #find/cache pointers; pointers are used here for authors and for link
    #previews, so we can winnow the pointer list just to that set of
    #relationships. need to find out what relationships are used for links.
    my $rcache;
    {
        my @menu_relations =
          ( 'author', BigMed::Content::Page->url_relationships );
        my $pointers = BigMed::Pointer->select(
            { site => $site_id, type => \@menu_relations } )
          or $app->error_stop;
        $rcache->{'BigMed::Pointer'} = $pointers;
    }

    #header row urls
    my %normal_sort = (
        'mod_time' =>
          'mod_time.priority.pub_time.title/descend.descend.descend.ascend',
        'pub_time' =>
          'pub_time.auto_pub_time.mod_time/descend.descend.descend',
        'title' => 'title.priority.mod_time/ascend.descend.descend',
        'priority' =>
          'priority.mod_time.pub_time.title/descend.descend.descend.ascend',
    );
    my %flip_sort = (
        'mod_time' =>
          'mod_time.priority.pub_time.title/ascend.descend.ascend.ascend',
        'pub_time' => 'auto_pub_time.pub_time.mod_time/ascend.ascend.ascend',
        'title'    => 'title.priority.mod_time/descend.descend.descend',
        'priority' =>
          'priority.mod_time.pub_time.title/ascend.descend.ascend.ascend',
    );
    my %sort_opposite = (
        'ascend'  => 'descend',
        'descend' => 'ascend',
    );
    my $header_url = $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'menu',
        args   => ( $subtype ? "$type.$subtype" : $type ),
    );
    my %header_params;
    foreach my $col ( keys %normal_sort ) {
        my $sort;
        if ( $rsort->[0] eq $col && $rorder->[0] ne $default_sort{$col} ) {
            $sort = $normal_sort{$col};
            my $direction = $sort_opposite{ $default_sort{$col} };
            $header_params{"${col}_class"} = "sort_$direction selected";
        }
        elsif ( $rsort->[0] eq $col ) {
            $sort = $flip_sort{$col};
            $header_params{"${col}_class"} =
              "sort_$default_sort{$col} selected";
        }
        else {
            $sort = $normal_sort{$col};
            $header_params{"${col}_class"} = "sort_$default_sort{$col}";
        }
        $header_params{"${col}_url"} = "$header_url/$sort/0";
    }

    #build the coderefs for building user names, summaries, bylines, sections
    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
    };
    my $rsummary = $app->menu_summary_coderef($class);
    my $rbyline =
      $class->isa('BigMed::Content::Page')
      ? sub { return $_[0]->authors($rcache); }
      : sub { return q{} };
    my $rsections = sub {
        my @names;
        foreach my $sec_id ( $_[0]->sections ) {
            defined( my $section = $site->section_obj_by_id($sec_id) )
              or $app->error_stop;
            push @names, $section->name if $section;
        }
        join( ',<br />', @names );
    };

    #build each row for table display
    my $obj;
    my @items;
    my $base_url = $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'edit',
        site   => $site_id,
        args   => [$type],
    );
    my $base_preview = $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'preview',
        site   => $site_id,
        args   => [$type],
    );
    my %stat_text = (
        'draft'     => $app->language('CONTENT_pub_status_draft'),
        'edit'      => $app->language('CONTENT_pub_status_edit'),
        'ready'     => $app->language('CONTENT_pub_status_ready'),
        'published' => $app->language('CONTENT_pub_status_published'),
    );
    while ( $obj = $selection->next ) {
        my $pub_status = $obj->pub_status || 'draft';
        my $mod_time =
          $obj->mod_time ? $app->format_time( $obj->mod_time ) : q{};

        my $byline = $rbyline->($obj);
        $byline &&= $app->language( ['BM_Byline by', "%BM$byline%"] );

        my $view_url;
        $view_url = $obj->active_page_url( $site, { rcache => $rcache } )
          || "$base_preview/" . $obj->id
          if $class->isa('BigMed::Content::Page');

        my $is_section = $obj->subtype && $obj->subtype eq 'section';

        my $editor = $obj->last_editor ? $ruser->( $obj->last_editor ) : q{};
        $editor = q{} if $editor eq q{--};
        $editor &&= $app->language(
            ['BM_Byline by', q{%BM} . $ruser->( $obj->last_editor ) . q{%}] );

        my $sec_name = $rsections->($obj);
        my $title =
          ( $is_section && $sec_name ne $obj->title )
          ? $sec_name . ' (' . $obj->title . ')'
          : $obj->title;
        my $slug_path = $is_section ? menu_slug_path( $site, $obj ) : q{};

        push @items,
          { CID      => $obj->data_label . $obj->id,
            CAN_EDIT => $obj->editable_by_user($user),
            EDIT_URL => "$base_url/" . $obj->id,
            TITLE    => $title,
            SUMMARY  => $rsummary->($obj),
            BYLINE   => $byline,
            OWNER    => $ruser->( $obj->owner ),
            SECTIONS => $sec_name,
            PUB_TIME => $app->published_text($obj),
            MOD_TIME => "$mod_time $editor",
            PRIORITY => defined $obj->priority ? $obj->priority : 500,
            $pub_status =>
              qq~<span title="$stat_text{$pub_status}">&times;</span>~,
            NO_OPTS    => $is_section,
            VIEW_URL   => $view_url,
            SLUGPATH   => $slug_path,
            IS_SECTION => $is_section,
          };
    }

    #if obj is undefined here, we could have an error... let it slide,
    #since it will be displayed as an error message on the menu page itself

    #page title and message
    my $edit_type = $subtype || $type;
    my $title_unlocal = $options{head}
      || ['EDITOR_Type Edit Menu', $app->language( 'TITLE_' . $edit_type )];
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );

    $app->_set_breadcrumbs(
        class     => $class,
        type      => $type,
        subtype   => $subtype,
        this_page => $subtype ? "$type:$subtype" : $type,
    );

    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-actionmenu.js' );
    my $template = $options{template} || 'screen_editor_menu.tmpl';
    $app->js_add_onload(
        'BM.ActionMenu.update("editorActionMenu","editorMenu");');
    my $page_type = ( $app->path_args )[0] || 'page.all';
    $app->js_add_code(
        q{BM.EditType="} . $app->js_escape($page_type) . q{";} );

    my $plevel = $user->privilege_level($site_id);
    if ( $plevel > 2 ) {    #better than writer
        $app->js_add_code('BM.canPublish=1;') if $plevel > 3;    #publisher +

        #can edit users
        my @users = sort { $a->[2] cmp $b->[2] }
          map { [$_->id, $_->name, lc $_->name] } $app->current_site->users();
        my %uname = ( 'x' => $app->language('BM_No change') );
        my @uid = ('x');
        foreach my $u (@users) {
            push @uid, $u->[0];
            $uname{ $u->[0] } = $u->[1];
        }
        $app->js_add_code( 'BM.EditUsers='
              . objToJson( { uid => \@uid, uname => \%uname } ) );
    }

    #get section pulldown
    my $section_pulldown = $app->prompt(
        'select_section',
        'sec',
        {   site                => $site,
            user                => $user,
            field_class         => 'inline',
            value               => $session->param('EDITOR_MENU_SEC'),
            all_sections_option => 1,
        },
    )->{FIELD_HTML};

    #see if there are other sites available to offer copy-to-sites option
    my $all_sites = $user->allowed_site_selection(2) or $app->error_stop;
    my $copy2site = $all_sites->count > 1;

    return $app->html_template_screen(
        $template,
        bmcp_title   => $title,
        message      => $message,
        tabs         => $app->menu_page_tabs( $type, $site_id ),
        subtabs      => $app->menu_page_subtabs( $class, $subtype, $site_id ),
        new_links    => $app->menu_new_links( $class, $subtype, $site_id ),
        items        => \@items,
        BMCP_LEFTCOL => $browse_text,
        navbar       => $pagenav_menu,
        sections     => $section_pulldown,
        filter_string => $session->param('EDITOR_MENU_STRING'),
        filter_sub    => $session->param('EDITOR_MENU_SUB'),
        self_url      => $header_url,
        copy2site     => $copy2site,
        %header_params,
    );
}

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;
    $app->require_sections($site);
    my $content = $options{obj} || $app->_load_object_from_url($site)
      or $app->error_stop;
    my $class_label = $content->data_label;

    if ( !$content->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();
    }

    #new object if all data columns except id/site/subtype are undefined
    my $is_new = 1;
    foreach my $col ( $content->data_columns ) {
        my $getter = "_ref_$col";
        next
          if $col eq 'id'
              || $col eq 'site'
              || $col eq 'subtype'
              || !defined $content->$getter;
        undef $is_new;
        last;
    }
    $content->set_owner( $user->id )
      if !$content->owner
          && ( !$content->subtype || $content->subtype ne 'section' );

    #make sure section pages are associated with valid section;
    #shouldn't ever be the case, but it's happened in the wild after
    #partial section deletion. if so, delete section page and show message
    if ( $content->subtype && $content->subtype eq 'section' ) {
        my $sec = ( $content->sections )[0];
        if ( !$sec || !$site->section_obj_by_id($sec) ) {
            $content->trash;
            $app->set_error(
                head => 'EDITOR_Orphan section page',
                text => 'EDITOR_TEXT_Orphan section page',
            );
            return $app->rm_menu();
        }
    }

    $app->set_cp_selected_nav('NewPage') if $is_new;

    #build the edit fields
    my @fieldsets = $app->build_content_fieldsets( $content, %options );
    my $submit_text = $options{submit_text} || 'BM_SUBMIT_LABEL_Save';
    my $submit = $app->prompt_field_ref(
        id        => 'editor_submit',
        prompt_as => 'submit',
        value     => $app->language($submit_text),
    );
    my $label_field = $app->prompt_field_ref(    #minicontent js needs this
        id        => 'BM_CONTENT_LABEL',
        prompt_as => 'hidden',
        value     => $class_label,
    );
    my $id_field = $app->prompt_field_ref(       #minicontent js needs this
        id        => 'BM_CONTENT_ID',
        prompt_as => 'hidden',
        value     => $content->id,
    );
    my $subtype_field = $app->prompt_field_ref(    #minicontent js needs this
        id        => 'BM_CONTENT_SUBTYPE',
        prompt_as => 'hidden',
        value     => $content->subtype,
    );
    push @fieldsets,
      $app->prompt_fieldset_ref(
        fields => [$submit, $label_field, $id_field, $subtype_field], );

    #headline/message text
    my $edit_label = $content->subtype || $class_label;
    $edit_label = $app->language( 'TITLE_' . $edit_label );
    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,
    );

    #form url
    $class_label .= q{.} . $content->subtype if $content->subtype;
    my $form_url = $app->build_url(
        script => 'bm-editor.cgi',
        site   => $site->id,
        rm     => 'save',
        args   => [$class_label, $content->id]
    );

    $app->_set_breadcrumbs(
        class     => ref $content,
        type      => $content->data_label,
        subtype   => $content->subtype,
        title     => [$action, $edit_label],
        this_page => $class_label,
    );
    my $template = $options{template} || 'screen_cp_generic.tmpl';
    $app->js_add_script( $app->env('BMADMINURL') . '/js/diff.js' );
    $app->js_add_code(
        'BM.htmlDir="' . $site->html_url . '"; BM.confirmLeave = true;' );
    return $app->html_template_screen(
        $template,
        form_url     => $form_url,
        form_id      => 'EditorScreen',
        bmcp_title   => $title,
        message      => $message,
        fieldsets    => \@fieldsets,
        BMCP_LEFTCOL => $app->left_col_page_status($content),
    );
}

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;
    $app->require_sections($site);
    my $content     = $app->_load_object_from_url($site) or $app->error_stop;
    my $class_label = $content->data_label;
    my $level       = $user->privilege_level( $site, [$content->sections] );

    if ( !$content->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_edit( query => $app->query, obj => $content );
    }

    #cache some original settings
    my $is_new     = !$content->mod_time;
    my $orig_pub   = $content->pub_status || 'draft';
    my $orig_title = $content->title || q{};
    my $orig_slug  = $content->can('slug') ? ( $content->slug || q{} ) : q{};
    my @orig_sec   = $content->sections;
    my @orig_tags;
    if ( $content->isa('BigMed::Content::Page') ) {
        @orig_tags = map { $_->[1] } $content->load_related_objects('tag');
    }

    #populate the basic object column values
    $content->set_owner( $user->id )
      if !$content->owner
          && ( !$content->subtype || $content->subtype ne 'section' );
    $content->set_last_editor( $user->id );
    my $version = $content->version || 0;
    $content->set_version( $version + 1 );
    $content->set_pub_status('draft') if !$content->pub_status;

    #collect fields to edit; special handling for pub-scheduling fields
    my @fields;
    my $class = ref $content;
    my @relations;
    foreach my $rfieldset ( $content->editor_fields() ) {
        next if !$rfieldset->{fields};
        foreach my $rfield_hash ( @{ $rfieldset->{fields} } ) {
            my %field_opt = %{$rfield_hash};
            if ( $field_opt{relation} ) {    #handle later
                push @relations, $field_opt{relation};
                next;
            }
            my $id = $field_opt{id} || $field_opt{column} || q{};
            next
              if ( $id eq 'auto_pub_time' || $id eq 'auto_unpub_time' )
              && !$content->publishable_by_user($user);
            next if $id eq 'owner' && $level < 3;

            my $col = $field_opt{column};
            $field_opt{data_class} = $class if $col;
            $field_opt{parse_as} = $field_opt{prompt_as};
            my $callback = $field_opt{parse_callback};
            %field_opt = ( %field_opt, $callback->( $app, $content ) )
              if $callback;
            push @fields, \%field_opt;
        }
    }
    my %fvalue = $app->parse_submission(@fields);
    $content->post_parse( \%fvalue );
    if ( $fvalue{_ERROR} ) {
        return $app->rm_edit(
            field_msg => $fvalue{_ERROR},
            query     => $app->query
        );
    }

    #pour the fields into the content object
    foreach my $f (@fields) {
        my $col = $f->{column};
        if ( $col && $content->isa( $f->{data_class} ) ) {
            my $setter = 'set_' . $col;
            $content->$setter( $fvalue{$col} );
        }
    }
    if ( $content->can('slug') ) {
        $content->generate_slug
          or return $app->rm_edit( query => $app->query, obj => $content );
    }

    #check permission issues (sections and publishing)
    if ( $level < 2 ) {
        $app->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text => 'EDITOR_TEXT_Inadequate section permissions',
        );
    }
    elsif ( $content->pub_status eq 'published'
        && !$content->publishable_by_user($user) )
    {
        $app->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text => 'EDITOR_TEXT_Inadequate publishing permissions',
        );
    }
    return $app->rm_edit( query => $app->query, obj => $content )
      if $app->error;

    #allow backdating via auto_pub_time
    my $time_now = BigMed->bigmed_time();
    my $auto_pub = $content->auto_pub_time;
    if ( $auto_pub && $auto_pub lt $time_now ) {
        $content->set_pub_status('published');
        $content->set_pub_time($auto_pub);
        $content->set_auto_pub_time(undef);
    }

    #publish time and details
    my $pub_status = $content->pub_status;
    if ( $pub_status eq 'published' && !$content->pub_time ) {
        $content->set_pub_time($time_now);
    }
    elsif ( $pub_status ne 'published' ) {
        $content->set_pub_time(undef);
    }

    #note any custom_save relations, check required relationships
    my %edit_relation = map { $_ => 1 } @relations;
    my @custom_relations = ();
    foreach my $relation ( $content->data_relationships ) {
        my %info = $content->relationship_info($relation);
        push @custom_relations, [$relation, \%info]
          if $info{custom_save} && $edit_relation{$relation};
        next if !$info{required};
        my @related = $content->load_related_objects($relation);
        if ( !@related ) {
            $app->set_error(
                head => 'EDITOR_Missing Item',
                text => [
                    'EDITOR_Please supply a relationship item',
                    $app->language( 'CONTENT_RELATION_' . $relation ),
                ],
            );
        }
    }
    return $app->rm_edit( query => $app->query, obj => $content )
      if $app->error;

    ##DONE!
    $app->call_trigger( 'editor_before_save', $content );
    $content->save
      or return $app->rm_edit( query => $app->query, obj => $content );

    $app->log( warning => 'Editor: '
          . $app->log_data_tag($user)
          . ' saved changes to '
          . $app->log_data_tag($content) );

    #now see if we have any sortable relationships to update
    $app->_update_sortable_relations($content)
      or return $app->rm_edit( query => $app->query, obj => $content );

    #save any custom relationships (most are handled via ajax, but some
    #like tags, are inline and have custom_parse callbacks)
    foreach my $rpair (@custom_relations) {
        my ( $relation, $rinfo ) = @{$rpair};
        $content->load_related_classes($relation)
          or return $app->rm_edit( query => $app->query, obj => $content );
        $rinfo->{custom_save}->( $app, $content )
          or return $app->rm_edit( query => $app->query, obj => $content );
    }

    #get inital batch of sections to build/remove
    my %has_sec = map { $_ => 1 } $content->sections;
    my @remove = grep { !$has_sec{$_} } @orig_sec;
    my @build_secs = $content->sections;
    my @build_pages;

    #update live pages only if published or changing pub_status
    if ( $orig_pub eq 'published' || $content->pub_status eq 'published' ) {
        my ( $kill, $no_detail, $build_page );
        require BigMed::Builder;
        my $builder = BigMed::Builder->new( site => $site )
          or return $app->rm_edit( query => $app->query, obj => $content );

        if ( $content->isa('BigMed::Content::Page') ) {
            if ( $content->pub_status ne 'published' ) {    #just unpublished
                push @remove, $content->sections;
                schedule_deindex( $site, $content ) or return;
            }
            elsif ( $content->active_page_url($site) ) { #live, published page
                schedule_index( $site, $content ) or return;
            }
            else {    #published page with no active url
                schedule_deindex( $site, $content );
            }
            $kill = { sections => \@remove, pages => $content->id }
              if @remove;
            @build_pages = ( $content->id );

            #remove old pages if the slug has changed
            $content->set_slug(q{}) if !$content->slug;
            my $slug_changed;
            if ( $orig_slug && $orig_slug ne $content->slug ) {
                $slug_changed = 1;
                $builder->remove_old_files( $orig_slug, \@orig_sec )
                  or return $app->rm_edit(
                    query => $app->query,
                    obj   => $content
                  );
            }

            #check if adding sections
            my %had_sec = map { $_ => 1 } @orig_sec;
            my $new_sec;
            foreach my $sid ( $content->sections ) {
                $new_sec = 1, last if !$had_sec{$sid};
            }

            #if it's not new, and the slug, pub status, title  or section has
            #changed, update other content items that link to this page.
            if ($orig_slug
                && (   $slug_changed
                    || $orig_pub   ne $content->pub_status
                    || $orig_title ne $content->title
                    || scalar(@remove)
                    || $new_sec )
              )
            {
                my $url = q{bm://} . $site->id . q{/} . $content->id;
                my $links =
                  BigMed::URL->select( { site => $site->id, url => $url } )
                  or return $app->rm_edit(
                    query => $app->query,
                    obj   => $content
                  );
                my ( $rpages, $rsections ) =
                  _get_link_sources_and_sections($links);
                push( @build_pages, @{$rpages} );
                push( @build_secs,  @{$rsections} );
            }
        }
        else {    #build only section pages
            $no_detail = 1;
        }

        $builder->build(
            pages => ( @build_pages ? \@build_pages : undef ),
            sections       => [@build_secs, @remove],
            remove_detail  => $kill,
            no_detail      => $no_detail,
            defer_overflow => 1,
            force_tags => ( @orig_tags ? \@orig_tags : undef ),
        ) or return $app->rm_edit( query => $app->query, obj => $content );
    }

    #do callbacks
    $app->call_trigger( 'editor_new_content', $content, $site ) if $is_new;
    $app->call_trigger( 'editor_status_change', $content, $site )
      if $orig_pub ne $content->pub_status;

    return $app->redirect_to_menu( ( $app->path_args )[0],
        'BM_Your changes have been saved.' );
}

sub rm_preview {
    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;

    #load the content; because we preview in a new window, show any errors
    #via error_stop instead of sending back to the menu.
    my ( $class, $subtype, $id ) = $app->_object_info_from_url();
    return $app->error_stop() if !$class;
    if ( !$class->isa('BigMed::Content::Page') ) {
        $app->set_error(
            head => 'EDITOR_Preview Available for Pages Only',
            text => 'EDITOR_TEXT_Preview Available for Pages Only',
        );
        return $app->error_stop();
    }

    defined( my $content = $class->fetch( { site => $site->id, id => $id } ) )
      or return $app->error_stop();
    if ( !$content ) {    #no such content
        my $type = $subtype || $class->data_label;
        $app->set_error(
            head => 'EDITOR_Unknown Content',
            text => [
                'EDITOR_TEXT_Unknown Content',
                $app->language("TITLE_$type"),
                $id
            ],
        );
        return $app->error_stop();
    }

    my $section = ( $content->sections )[0];
    $subtype = $content->subtype || $class->default_subtype;
    my ( $template, $level, $preview );
    if ( $subtype ne 'section' ) {
        ( $template, $level ) = qw(page detail);
        $preview = { page_id => $content->id };
    }
    elsif ( $section != $site->homepage_id ) {
        ( $template, $level ) = qw(section section);
        $preview = { section_id => $section };
    }
    else {
        ( $template, $level ) = qw(home top);
    }

    require BigMed::Builder;
    my $builder = BigMed::Builder->new( site => $site, preview => $preview )
      or return $app->error_stop();
    my $html = $builder->preview(
        format_class => 'BigMed::Format::HTML',
        section      => $section,
        template     => $template,
        level        => $level,
        content_obj  => $content,
    );

    #TO DO: Replace server-side includes, and any relative or non-domain
    #absolute URLs and SRCs.

    return $html;
}

sub _get_checkbox_items {
    my $app   = shift;
    my @items = $app->utf8_param('c');

    #collect each object type into its own array; *should* be only one type
    #at a time, but we can be flexible here in case things change down
    #the road.
    my %type;
    foreach my $item (@items) {
        $item =~ /\A([a-zA-Z]+?)(\d+)\z/ms or next;
        my $class = BigMed::Content->class_for_label($1) or next;
        $class->isa('BigMed::Content') or next;
        push @{ $type{$class} }, $2;
    }
    return %type;
}

sub rm_change_status {
    my $app  = shift;
    my $site = $app->current_site or return $app->rm_login;
    my $user = $app->current_user or return $app->rm_login_site;
    $app->require_post() or $app->error_stop;

    my ( $url_type, $new_status, $priority, $owner ) =
      map { $_ || q{} } ( $app->path_args )[0 .. 3];
    if (   $new_status ne 'draft'
        && $new_status ne 'edit'
        && $new_status ne 'ready'
        && $new_status ne 'published' )
    {
        $new_status = q{};
    }
    $priority =~ s/\D//msg;
    $priority ||= 0;
    $priority = 1000 if $priority > 1000;
    $owner =~ s/\D//msg;
    $owner ||= 0;
    $owner = 0 if $user->privilege_level($site) < 3;
    if ($owner) {    #confirm exists
        my $o = BigMed::User->fetch($owner);
        $owner = 0 if !$o;
    }

    my %type = $app->_get_checkbox_items()
      or return $app->redirect_to_menu($url_type);
    my %build_sections;
    my ( @change_pages, @remove );
    my ( @add_index,    @del_index );
    my $time = $new_status eq 'published' ? BigMed->bigmed_time : undef;
    foreach my $class ( keys %type ) {
        my $select =
          $class->select( { site => $site->id, id => $type{$class} } )
          or return $app->redirect_to_menu($url_type);
        my $item;
        my @change_ids;
        my $is_page = $class->isa('BigMed::Content::Page');
        my $rk;
        while ( $item = $select->next ) {
            next if $is_page && $item->subtype && $item->subtype eq 'section';

            #ignore unchanged items
            my $old_status = $item->pub_status || q{};
            my $old_priority = $item->priority;
            $old_priority = 500 if !defined $old_priority;
            my $old_owner = $item->owner || 0;
            my $status_change = $new_status && $old_status ne $new_status;
            my $pri_change = $old_priority != $priority;
            next
              if !$status_change
                  && !$pri_change
                  && ( !$owner || $old_owner == $owner );

            #ignore requests to publish items without permission
            next
              if !$item->editable_by_user($user)
                  || ( $new_status eq 'published'
                      && !$item->publishable_by_user($user) );

            if ($status_change) {
                $item->set_pub_status($new_status);
                $item->set_pub_time($time);
                $app->call_trigger( 'editor_status_change', $item, $site );
                if ($is_page) {
                    if ( $old_status eq 'published' ) {
                        push @remove, [$item->slug, [$item->sections]];
                        push @del_index, $item->id;
                    }
                    elsif ( $new_status eq 'published' ) {
                        $rk ||= { map { $_ => 1 }
                              $site->all_active_descendants_ids() };
                        my $url =
                          $item->active_page_url( $site, { rkids => $rk } );
                        if ($url) {
                            push @add_index, $item->id;
                        }
                        else {
                            push @del_index, $item->id;
                        }
                    }
                }
            }
            $item->set_owner($owner) if $owner;
            $item->set_priority($priority);
            $item->set_last_editor( $user->id );
            $item->save or return $app->redirect_to_menu($url_type);

            next
              if ( $old_status ne 'published' && $new_status ne 'published' )
              || ( !$status_change && !$pri_change );

            #requires a change on the live site, queue for building
            push @change_pages, $item->id if $is_page;
            $build_sections{$_} = 1 for ( $item->sections );
        }
    }

    if (@change_pages) {    #collect any links to the changed pages
        my $sid   = $site->id;
        my @url   = map { "bm://$sid/$_" } @change_pages;
        my $links = BigMed::URL->select( { site => $sid, url => \@url } )
          or return $app->redirect_to_menu($url_type);
        my ( $rpages, $rsections ) = _get_link_sources_and_sections($links);
        push( @change_pages, @{$rpages} );
        $build_sections{$_} = 1 for @{$rsections};
    }

    if (%build_sections) {    #make changes on live site
        require BigMed::Builder;
        my $builder = BigMed::Builder->new( site => $site )
          or return $app->redirect_to_menu($url_type);

        my @build_sections = ( keys %build_sections );
        my ( $no_detail, $pages );
        if (@change_pages) {
            $pages = \@change_pages;
            schedule_index( $site, \@add_index ) or return;
            schedule_deindex( $site, \@del_index ) or return;
        }
        else {
            $no_detail = 1;
        }
        $builder->build(
            pages          => $pages,
            sections       => \@build_sections,
            no_detail      => $no_detail,
            defer_overflow => 1,
        ) or return $app->redirect_to_menu($url_type);

        foreach my $section_info (@remove) {
            $builder->remove_old_files( @{$section_info} )
              or return $app->redirect_to_menu($url_type);
        }
    }
    return $app->redirect_to_menu( $url_type,
        'BM_Your changes have been saved.' );
}

sub rm_delete {
    my $app = shift;
    $app->require_post() or $app->error_stop;
    my $site = $app->current_site;
    my $user = $app->current_user;

    my %type = $app->_get_checkbox_items();
    return $app->rm_menu if !%type;

    foreach my $class ( keys %type ) {
        my $select =
          $class->select( { site => $site->id, id => $type{$class} } )
          or return $app->rm_menu();
        my $item;

        #make sure has permission to delete all
        while ( $item = $select->next ) {
            if ( !$item->editable_by_user($user) ) {
                $app->set_error(
                    head => 'CONTENT_Not Allowed To Do That',
                    text => [
                        'EDITOR_TEXT_No permission to delete',
                        q{%BM} . $item->title . q{%}
                    ],
                );
                return $app->rm_menu();
            }
            $app->log( warning => 'Editor: '
                  . $app->log_data_tag($user)
                  . ' requested deletion of '
                  . $app->log_data_tag($item) );
        }
        return $app->rm_menu() if !defined $item;    #i/o error
        my $count = $select->count;
        $select->trash_all() or return $app->rm_menu();
        $app->log( warning => 'Editor: '
              . $app->log_data_tag($user)
              . " deleted $count item(s)" );
    }
    return $app->rm_menu(
        message => 'EDITOR_Items deleted, changes made on live site' );
}

sub rm_copy {
    my $app = shift;
    $app->require_post() or $app->error_stop;
    my $site = $app->current_site;
    my $user = $app->current_user;
    my %type = $app->_get_checkbox_items();
    return $app->rm_menu if !%type;

    my $copytext = $app->language('BM_Copy') . ': ';
    my $user_tag = $app->log_data_tag($user);
    foreach my $class ( keys %type ) {
        my $select =
          $class->select( { site => $site->id, id => $type{$class} } )
          or return $app->rm_menu();
        my $item;
        while ( $item = $select->next ) {
            next if !$item->editable_by_user($user);    #ignore
            my $clone = $item->copy(
                {   source_site => $site,
                    target_site => $site,
                    omit_rel    => ['comment']
                }
            ) or return $app->rm_menu();
            $clone->set_title( $copytext . ( $clone->title || 'Untitled' ) );
            $clone->set_pub_status('draft');
            $clone->set_pub_time(undef);
            $clone->set_owner( $user->id );
            $clone->set_last_editor( $user->id );
            $clone->save or return $app->rm_menu();
            $app->call_trigger( 'editor_new_content', $clone, $site );
            my ( $clone_tag, $item_tag ) =
              ( $app->log_data_tag($clone), $app->log_data_tag($item) );
            $app->log( warning =>
                  "Editor: $user_tag copied $item_tag to $clone_tag" );
        }
        return $app->rm_menu() if !defined $item;    #i/o error
    }
    return $app->rm_menu( message => 'BM_Your changes have been saved.' );
}

sub rm_copyxsite {
    my $app     = shift;
    my %options = @_;
    $app->require_post() or $app->error_stop;
    my $site    = $app->current_site;
    my $this_id = $site->id;
    my $user    = $app->current_user;

    my %type  = $app->_get_checkbox_items();
    my $class = ( keys %type )[0];             #should be just one
    return $app->rm_menu if !$class;
    my $cboxes = $app->_rebuild_checkbox_field( 'BM_Copy', \%type )
      or return $app->rm_menu;

    #gather site info for *other* sites where we have writer+ permission
    my $all_site = $user->allowed_site_selection(2) or return $app->rm_menu;
    my $just_one = $all_site->count == 2;      #one besides current site
    my ( @sid, %sname, $s, $pick_site );
    while ( $s = $all_site->next ) {
        my $sid = $s->id;
        next if $this_id == $sid;
        push @sid, $sid;
        $sname{$sid} = $s->name;
        $pick_site = $sid if $just_one;
    }
    return $app->rm_menu if !defined $site;
    my $site_field = $app->prompt_field_ref(
        id          => 'site',
        required    => 1,
        prompt_as   => 'value_list',
        multiple    => 5,
        options     => \@sid,
        labels      => \%sname,
        label       => 'EDITOR_Target Site(s)',
        description => 'EDITOR_DESC_Target Site(s)',
        value       => $pick_site,
    );

    #pub status
    my @pub_status = qw(draft edit ready);
    push @pub_status, 'published' if $user->privilege_level($site) > 3;
    my %pub_label = (
        'draft'     => $app->language('CONTENT_pub_status_draft'),
        'edit'      => $app->language('CONTENT_pub_status_edit'),
        'ready'     => $app->language('CONTENT_pub_status_ready'),
        'published' => $app->language('CONTENT_pub_status_published'),
    );
    my $status_field = $app->prompt_field_ref(
        id        => 'status',
        prompt_as => 'value_list',
        required  => 1,
        options   => \@pub_status,
        labels    => \%pub_label,
        label     => 'page:pub_status',
    );

    my $submit = $app->prompt_field_ref(
        id        => 'copyxsite_submit',
        value     => $app->language('BM_Copy'),
        prompt_as => 'submit',
    );

    my @fieldsets = (
        $app->prompt_fieldset_ref(
            fields => [$site_field, $status_field, $cboxes],
        ),
        $app->prompt_fieldset_ref( fields => [$submit], ),
    );

    #headline/message text
    my $title_unlocal = $options{head} || 'EDITOR_Copy To Other Sites';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message},
        title     => $title_unlocal,
    );

    #form url
    my $form_url = $app->build_url(
        script => 'bm-editor.cgi',
        site   => $site->id,
        rm     => 'ajax-copyxsite',
    );

    $app->_set_breadcrumbs(
        class => $class,
        type  => $class->data_label,
        title => $title_unlocal,
    );
    $app->js_add_script( $app->env('BMADMINURL') . '/js/bm-copyxsite.js' );
    $app->js_add_code( 'BM.EditType="' . $class->data_label . q{"} );

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

sub rm_ajax_copyxsite {
    my $app  = shift;
    my $site = $app->current_site;
    my $user = $app->current_user;

    my $sbar = BigMed::Status->new( { container => 'bmCopyStatus', } );
    my $rerr = sub {
        $sbar->send_error();
    };
    my $rdone = sub {
        $sbar->mark_done();
        $app->teardown();
        exit();
    };

    my @pub_status = qw(draft edit ready);
    push @pub_status, 'published' if $user->privilege_level($site) > 3;
    my %param = $app->parse_submission(
        {   id       => 'site',
            parse_as => 'value_list',
            required => 1,
            multiple => 1,
        },
        {   id       => 'status',
            parse_as => 'value_list',
            options  => \@pub_status,
            required => 1,
        },
    );
    return $app->ajax_parse_error( $param{_ERROR} ) if $param{_ERROR};
    my @sid   = grep { /\A\d+\z/ms } @{ $param{site} };
    my %type  = $app->_get_checkbox_items();
    my $class = ( keys %type )[0];
    $rdone->() if !@sid || !$class;

    #gather the sites and objects, figure number of steps
    my $c_select =
         $class->select( { site => $site->id, id => $type{$class} } )
      or $rerr->();
    my $sites = BigMed::Site->select( { id => \@sid } ) or $rerr->();
    $rdone->() if !$c_select->count || !$sites->count;
    my $per_site = $c_select->count + 1;
    my $stotal   = $sites->count;
    my $steps    = $per_site * $stotal;

    #do the copying
    my $progress = 0;
    my $scount   = 0;
    my $user_tag = $app->log_data_tag($user);
    my $s;
    while ( $s = $sites->next ) {
        $scount++;
        my $plevel = $user->privilege_level($s);
        if ( $plevel < 2 ) {    #inadequate privileges
            $progress += $per_site;
            next;
        }
        my $this_stat = $param{status};
        $this_stat = 'draft' if $plevel < 3 && $this_stat eq 'published';
        my $pub_time =
          $this_stat eq 'published' ? BigMed->bigmed_time() : undef;
        my $log_site = $app->log_data_tag($s);

        $c_select->set_index(0);
        my ( $orig, $rkids, %build_sec, @build_page, @index_page );
        my $msg = ['EDITOR_Copying to site', "$scount/$stotal"];
        my %has_priv = $user->allowed_section_hash($s);
        while ( $orig = $c_select->next ) {
            my $this_stat = $this_stat;    #in case no sections w/privs
            $sbar->update_status(
                steps    => $steps,
                progress => $progress++,
                message  => $msg,
            );
            my $clone = $orig->copy(
                {   source_site => $site,
                    target_site => $s,
                    omit_rel    => ['comment']
                }
            ) or $sbar->send_error();

            #vet sections
            my @sections = grep { $has_priv{$_} } $clone->sections;
            if ( !@sections ) {    #get first section with privs
                foreach my $secid ( $s->all_descendants_ids ) {
                    next if !$has_priv{$secid};
                    @sections = ($secid);
                    last;
                }

                #should always have privileges to at least one
                #section, but just in case...
                $this_stat = 'draft' if !@sections;    #local $this_stat
            }
            my $this_time = $this_stat eq 'published' ? $pub_time : undef;
            $clone->set_pub_time($this_time);
            $clone->set_sections( \@sections );
            $clone->set_pub_status($this_stat);
            $clone->set_owner( $user->id );
            $clone->set_last_editor( $user->id );
            $clone->save or $rerr->();

            my ( $clone_tag, $orig_tag ) =
              ( $app->log_data_tag($clone), $app->log_data_tag($orig) );
            $app->log( warning =>
                  "Editor: $user_tag copied $orig_tag to $clone_tag" );

            $app->call_trigger( 'editor_new_content', $clone, $s );
            next if $this_stat ne 'published';

            foreach my $sec ( $clone->sections ) {
                $build_sec{$sec} = 1;
            }
            if ( $clone->isa('BigMed::Content::Page') ) {
                push @build_page, $clone->id;
                $rkids
                  ||= { map { $_ => 1 } $site->all_active_descendants_ids() };
                push @index_page, $clone->id
                  if $clone->active_page_url( $s, { rkids => $rkids } );
            }
        }
        $sbar->update_status( progress => $progress++, );
        next if $this_stat ne 'published';

        if (@index_page) {
            schedule_index( $s, \@index_page ) or return;
        }

        require BigMed::Builder;
        my $builder = BigMed::Builder->new( site => $s ) or $rerr->();
        $builder->build(
            pages          => \@build_page,
            no_detail      => ( scalar @build_page == 0 ),
            sections       => [keys %build_sec],
            defer_overflow => 1,
        ) or $rerr->();
    }
    $rerr->() if !defined $s;
    return $rdone->();
}

sub rm_choose_sec {
    my $app      = shift;
    my %options  = @_;
    my $site     = $app->current_site;
    my $sid      = $site->id;
    my $user     = $app->current_user;
    my $url_type = ( $app->path_args )[0];

    my %type  = $app->_get_checkbox_items();
    my $class = ( keys %type )[0];             #should be just one
    return $app->redirect_to_menu($url_type) if !$class;

    #build fields
    my $cboxes =
      $app->_rebuild_checkbox_field( 'EDITOR_Apply change to...', \%type )
      or return $app->redirect_to_menu($url_type);
    my $sections = $app->prompt_field_ref(
        id          => 'sections',
        prompt_as   => 'select_section',
        multiple    => 5,
        user        => $user,
        site        => $site,
        label       => 'page:sections',
        description => 'CONTENT_DESC_Section',
        required    => 1,
    );
    my $submit = $app->prompt_field_ref(
        id        => 'choosesec_submit',
        value     => $app->language('BM_SUBMIT_LABEL_Save'),
        prompt_as => 'submit',
    );
    my @fieldsets = (
        $app->prompt_fieldset_ref(
            fields    => [$sections, $cboxes],
            query     => $options{query},
            field_msg => $options{field_msg},
        ),
        $app->prompt_fieldset_ref( fields => [$submit], ),
    );

    #headline/message text
    my $title_unlocal = $options{head} || 'EDITOR_Change Section';
    my ( $title, $message ) = $app->title_and_message(
        field_msg => $options{field_msg},
        message   => $options{message} || 'EDITOR_DESC_Change Section',
        title     => $title_unlocal,
    );

    #form url
    my $form_url = $app->build_url(
        script => 'bm-editor.cgi',
        site   => $site->id,
        rm     => 'move-sec',
        args   => [$url_type],

    );

    $app->_set_breadcrumbs(
        class => $class,
        type  => $class->data_label,
        title => $title_unlocal,
    );

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

sub rm_move_sec {
    my $app = shift;
    $app->require_post() or $app->error_stop;
    my $site     = $app->current_site;
    my $user     = $app->current_user;
    my $url_type = ( $app->path_args )[0];

    my %type  = $app->_get_checkbox_items();
    my $class = ( keys %type )[0];             #should be just one
    return $app->redirect_to_menu($url_type) if !$class;

    #user has permission to all sections we get back (parser validates)
    my %field = $app->parse_submission(
        {   id       => 'sections',
            parse_as => 'select_section',
            user     => $user,
            site     => $site,
            required => 1,
            multiple => 1,
        }
    );
    return $app->rm_choose_sec(
        field_msg => $field{_ERROR},
        query     => $app->query
    ) if $field{_ERROR};
    my $rsections = $field{sections};
    my @sections  = @{$rsections};

    #update items and gather build info
    my $select = $class->select( { site => $site->id, id => $type{$class} } )
      or return $app->rm_choose_sec( query => $app->query );
    my $is_page = $class->isa('BigMed::Content::Page');
    my %new_sec = map { $_ => 1 } @sections;
    my $utag    = $app->log_data_tag($user);
    my ( @build_pages, @build_secs, %kill, $rk );
    my ( @add_index, @del_index );
    my $item;

    while ( $item = $select->next ) {
        next if $is_page && $item->subtype && $item->subtype eq 'section';
        next if !$item->editable_by_user($user);

        my @orig_sec = $item->sections;
        $item->set_sections($rsections);
        $item->save or return $app->rm_choose_sec( query => $app->query );
        $app->log( warning => "Editor: $utag moved "
              . $app->log_data_tag($item)
              . 'to new section(s)' );
        next if $item->pub_status ne 'published';

        #gather stuff to build
        my %old_sec = map  { $_ => 1 } @orig_sec;
        my @add     = grep { !$old_sec{$_} } @sections;
        my @remove  = grep { !$new_sec{$_} } @orig_sec;
        next if !@add && !@remove;    #no section changes, nothing to build
        push @build_secs, @add, @remove;

        if ($is_page) {
            push @build_pages, $item->id;
            if (@remove) {
                push @{ $kill{sections} }, @remove;
                push @{ $kill{pages} },    $item->id;
            }

            #update pages that link here
            my $url = q{bm://} . $site->id . q{/} . $item->id;
            my $links =
              BigMed::URL->select( { site => $site->id, url => $url } )
              or return $app->rm_choose_sec( query => $app->query );
            my ( $rpages, $rsections ) =
              _get_link_sources_and_sections($links);
            push( @build_pages, @{$rpages} );
            push( @build_secs,  @{$rsections} );

            $rk ||= { map { $_ => 1 } $site->all_active_descendants_ids() };
            if ( $item->active_page_url( $site, { rkids => $rk } ) ) {
                push @add_index, $item->id;
            }
            else {
                push @del_index, $item->id;
            }
        }
    }
    return $app->rm_choose_sec( query => $app->query ) if !defined $item;

    #no need to build anything if no sections flagged
    return $app->redirect_to_menu( $url_type,
        'BM_Your changes have been saved.' )
      if !@build_secs;

    schedule_index( $site, \@add_index ) or return;
    schedule_deindex( $site, \@del_index ) or return;

    require BigMed::Builder;
    my $builder = BigMed::Builder->new( site => $site )
      or return $app->rm_choose_sec( query => $app->query );
    $builder->build(
        pages => ( @build_pages ? \@build_pages : undef ),
        sections       => \@build_secs,
        remove_detail  => ( %kill ? \%kill : undef ),
        no_detail      => ( @build_pages == 0 ),
        defer_overflow => 1,
    ) or return $app->rm_choose_sec( query => $app->query );

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

sub _rebuild_checkbox_field {
    my ( $app, $field_label, $rtype ) = @_;
    my $user  = $app->current_user;
    my $site  = $app->current_site;
    my %type  = $rtype ? %{$rtype} : $app->_get_checkbox_items();
    my $class = ( keys %type )[0];
    my $label = $class->data_label;

    my ( %ctitle, @cid, @csort );
    my $c_select =
      $class->select( { site => $site->id, id => $type{$class} } )
      or return $app->rm_menu();
    my $item;
    while ( $item = $c_select->next ) {
        next if !$item->editable_by_user($user);    #ignore
        my $title = $item->title || 'Untitled';
        my $id = $label . $item->id;
        $ctitle{$id} = $title;
        push( @csort, [$id, lc $title] );
    }
    return if !defined $item || !@csort;
    @cid = map { $_->[0] } sort { $a->[1] cmp $b->[1] } @csort;

    return $app->prompt_field_ref(
        id        => 'c',
        prompt_as => 'key_boolean',
        value     => { map { $_ => 1 } @cid },
        options   => \@cid,
        labels    => \%ctitle,
        label     => $field_label,
    );
}

###########################################################
# AJAX RUN MODES
###########################################################

sub rm_ajax_save_mini {
    my $app  = shift;
    my $site = $app->current_site or return $app->rm_ajax_login_badsite;
    my $user = $app->current_user or return $app->rm_ajax_login;

    #collect and require path info
    my ( $content_label, $cid, $relation, $mini_type, $mini_id ) =
      $app->path_args();
    return $app->ajax_system_error()
      if !$app->_has_required_path_info(
        'EDITOR_content type' => $content_label,
        'EDITOR_content id'   => $cid,
        'EDITOR_relation'     => $relation,
        'EDITOR_mini_type'    => $mini_type,
      );
    my $subtype;   #content_label subtype info where applicable - page.article
    ( $content_label, $subtype ) = split( /[.]/ms, $content_label );

    #validate primary content type and retrieve existing or new object
    my $c_class = BigMed::Content->class_for_label($content_label);
    if ( !$c_class || !BigMed::Content->content_class_exists($c_class) ) {
        return $app->_ajax_unknown_type_err($content_label);
    }
    my $content = $c_class->fetch_or_create_content_obj(
        site    => $site,
        id      => $cid,
        subtype => $subtype,
    ) or return $app->ajax_system_error();

    my $linked_obj = $content->save_related_info(
        relation_name => $relation,
        data_type     => $mini_type,
        data_id       => $mini_id,
        parser        => sub { $app->parse_submission(@_) },
        user          => $user,
    ) or return $app->ajax_system_error();

    my $obj_class = ref $linked_obj;
    my ( $obj, $meta, $pointer_id, $relation_id );
    if ( $obj_class eq 'ARRAY' ) {    #pointer/obj pair
        my $pointer = $linked_obj->[0];
        $obj        = $linked_obj->[1];
        $obj_class  = ref $obj;
        $pointer_id = $pointer->id;
        $relation_id =
          $relation . q{-} . $obj->data_label . q{-} . $pointer->id;

        if ( !$obj->stash('CONTENT_no_edit') ) {

            #the edit form was open and could have included a change
            #to the library object itself.
            #find any other pages that link to this object and update them;

            #if it's new, then there are no other pages to build
            my $time_obj = BigMed->time_obj;
            $time_obj->shift_time('-5S');
            my $new = $time_obj->bigmed_time lt $obj->create_time;

            #routine returns undef if there was an error, but we're not going
            #to report them for this ajax routine
            $obj->build_dependent_pages( { site => $site } ) if !$new;
        }
    }
    else {
        $obj         = $linked_obj;
        $relation_id = $relation . q{-} . $obj->data_label . q{-} . $obj->id;
    }
    $app->log( warning => 'Editor: '
          . $app->log_data_tag($user)
          . ' edited/added '
          . $app->log_data_tag($obj) );

    my %relationship_info = $content->relationship_info($relation);
    $obj->set_stash( 'can_edit', $obj->editable_by_user($user) );
    my $pre_preview = $obj->pre_preview;
    $pre_preview->( $app, [$linked_obj] ) if $pre_preview;

    my %json_info = json_object_info(
        app           => $app,
        objects       => [$linked_obj],
        relation_info => \%relationship_info,
    );

    #get the revised preview html for the item and metadata if we have it
    my $preview_html = q{};
    my %preview      = $obj->preview();
    my $meta_callback;
    $meta_callback = $relationship_info{preview}->{html}
      if $relationship_info{preview};
    if ( $preview{html} ) {
        my %obj_preview = $preview{html}->( $app, $obj, {} );
        my %meta_preview =
            $meta_callback
          ? $meta_callback->( $app, $linked_obj, \%obj_preview )
          : ();
        my $del_text = $relationship_info{points} ? 'BM_Remove' : 'BM_Delete';
        $preview_html = $app->html_template_screen(
            'wi_minicontent_item.tmpl',
            %obj_preview,
            %meta_preview,
            RELATION_ID => $relation_id,
            SORTABLE    => $relationship_info{sortable},
            DELETE      => $app->language($del_text),
        );
    }

    #format as json and return
    my $label  = $json_info{labels}->[0];
    my %result = (
        type         => qq~"$label"~,
        obj          => $json_info{objects}->{$label},
        relationName => q{"} . $app->js_escape($relation) . q{"},
        preview      => q{"} . $app->js_escape($preview_html) . q{"},
    );
    if ($pointer_id) {
        $result{pointer_id} = $pointer_id;
        $result{relation}   = $json_info{relation};
    }
    return $app->ajax_data_prefab_json( \%result );
}

sub rm_ajax_delete_mini {
    my $app = shift;
    $app->require_post() or return $app->ajax_system_error;
    my $site = $app->current_site or return $app->rm_ajax_login_badsite;
    my $user = $app->current_user or return $app->rm_ajax_login;

    #collect and require path info
    my ( $content_label, $cid, $relation, $mini_type, $mini_id ) =
      $app->path_args();
    return $app->ajax_system_error()
      if !$app->_has_required_path_info(
        'EDITOR_content type' => $content_label,
        'EDITOR_content id'   => $cid,
        'EDITOR_relation'     => $relation,
        'EDITOR_mini_type'    => $mini_type,
        'EDITOR_mini_id'      => $mini_id,
      );
    my $subtype;   #content_label subtype info where applicable - page.article
    ( $content_label, $subtype ) = split( /[.]/ms, $content_label );
    my $c_class = BigMed::Content->class_for_label($content_label);
    if ( !$c_class || !BigMed::Content->content_class_exists($c_class) ) {
        return $app->_ajax_unknown_type_err($content_label);
    }
    my $content = $c_class->fetch_or_create_content_obj(
        site    => $site,
        id      => $cid,
        subtype => $subtype,
    ) or return $app->ajax_system_error();
    if ( !$content->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->ajax_system_error();
    }
    my %relation = $c_class->relationship_info( $relation, $subtype );
    if ( !%relation ) {
        my $page_type = $subtype || $content_label;
        $content->set_error(
            head => 'CONTENT_Unknown Data Relationship',
            text => [
                'CONTENT_TEXT_Unknown Data Relationship', $relation,
                $page_type,
            ],
        );
        return $app->ajax_system_error();
    }

    my $success = { valid => 1 };
    if ( $relation{points} ) {
        defined( my $pt =
              BigMed::Pointer->fetch( { site => $site->id, id => $mini_id } )
        ) or return $app->ajax_system_error();
        return $app->ajax_json_response($success) if !$pt;    #already gone
        if (   $pt->source_id != $cid
            || $pt->source_table ne $content->data_source )
        {
            $content->set_error(
                head => 'CONTENT_Related object mismatch',
                text => 'CONTENT_TEXT_Related object mismatch',
                ,
            );
            return $app->ajax_system_error();
        }
        $pt->trash or return $app->ajax_system_error();
    }
    else {
        my $data_class = $content->related_obj_class( $relation, $mini_type )
          or return $app->ajax_system_error();
        defined( my $obj =
              $data_class->fetch( { site => $site->id, id => $mini_id } ) )
          or return $app->ajax_system_error();
        return $app->ajax_json_response($success) if !$obj;
        my $key = $relation{id_column};
        if ( $obj->$key != $content->id ) {
            $content->set_error(
                head => 'CONTENT_Related object mismatch',
                text => 'CONTENT_TEXT_Related object mismatch',
                ,
            );
            return $app->ajax_system_error();
        }
        $obj->trash or return $app->ajax_system_error();
    }
    return $app->ajax_json_response($success);
}

sub rm_ajax_pages {
    my $app  = shift;
    my $site = $app->current_site or return $app->rm_ajax_login_badsite;
    my $user = $app->current_user or return $app->rm_ajax_login;
    my ( $order, $range, $namesearch ) = $app->path_args();

    my ( $sort, $sortorder );
    if ( !$order || $order ne 'recent' ) {   #default is alphabetical by title
        $sort      = ['title',  'mod_time'];
        $sortorder = ['ascend', 'descend'];
    }
    else {
        $sort      = 'mod_time';
        $sortorder = 'descend';
    }

    #convert namesearch into simple_text by putting into query
    #object and pulling it back out via parse_submission
    $namesearch = q{} if !defined $namesearch;
    my $name_regex;
    if ($namesearch) {
        $app->query->param( 'namesearch', $namesearch );
        my %scrub = $app->parse_submission(
            { id => 'namesearch', parse_as => 'simple_text' } );
        ( $namesearch = $scrub{namesearch} ) =~ s/[.,"]/ /msg;
        $namesearch =~ s/&quot;/ /msg;
        $name_regex =
          join( q{|}, map { "\Q$_\E" } ( $namesearch =~ /(\S+)/msg ) );
        $name_regex = qr/$name_regex/msi;
    }

    #get all for total count
    my $selection = BigMed::Content::Page->select(
        { site => $site->id, title => $name_regex },
        {   sort  => $sort,
            order => $sortorder,
        },
    ) or return $app->ajax_system_error();
    my $count = $selection->count;

    $range ||= q{};
    my ( $start, $end ) =
      $range =~ /\A(\d+)\-(\d+)\z/ms ? ( $1, $2 ) : ( 1, $count );
    my $offset = $start - 1;
    my $limit  = $end - $offset;
    $selection =
      $selection->select( {}, { offset => $offset, limit => $limit } );

    my ( $obj, @items );
    while ( $obj = $selection->next ) {
        push @items,
          { id         => $obj->id,
            title      => $obj->title,
            mod_time   => $app->format_time( $obj->mod_time ),
            pub_status => $obj->pub_status
          };
    }
    return $app->ajax_system_error if !defined $obj;
    return $app->ajax_data( { totalRecords => $count, records => \@items } );
}

sub rm_ajax_miniobj {
    my $app  = shift;
    my $site = $app->current_site or return $app->rm_ajax_login_badsite;
    my $user = $app->current_user or return $app->rm_ajax_login;

    my ( $c_label, $relation, $obj_type, $order, $range, $namesearch ) =
      $app->path_args();
    return $app->ajax_system_error()
      if !$app->_has_required_path_info(
        'EDITOR_content type' => $c_label,
        'EDITOR_relation'     => $relation,
        'EDITOR_mini_type'    => $obj_type,
      );

    #confirm relationship and figure out the data class
    my $subtype;    #c_label subtype info where applicable - page.article
    ( $c_label, $subtype ) = split( /[.]/ms, $c_label );
    my $c_class = BigMed::Content->class_for_label($c_label);
    if ( !$c_class || !BigMed::Content->content_class_exists($c_class) ) {
        return $app->_ajax_unknown_type_err($c_label);
    }

    my %relation = $c_class->relationship_info( $relation, $subtype );
    if ( !%relation ) {
        my $page_type = $subtype || $c_label;
        $app->set_error(
            head => 'CONTENT_Unknown Data Relationship',
            text => [
                'CONTENT_TEXT_Unknown Data Relationship', $relation,
                $page_type,
            ],
        );
        return $app->ajax_system_error();
    }
    my $data_class =
      $c_class->related_obj_class( $relation, $obj_type, $subtype )
      or return $app->ajax_system_error();

    #get range
    $range ||= q{};
    my ( $offset, $limit );
    if ( $range =~ /\A(\d+)\-(\d+)\z/ms ) {
        my ( $start, $end ) = ( $1, $2 );
        $offset = $start - 1;
        $offset = 0 if $offset < 0;
        $limit  = $end - $start + 1;
        $limit  = 1 if $limit < 1;
    }

    #convert namesearch into simple_text by putting into query
    #object and pulling it back out via parse_submission
    $namesearch = q{} if !defined $namesearch;
    $app->query->param( 'namesearch', $namesearch );
    my %scrub = $app->parse_submission(
        { id => 'namesearch', parse_as => 'simple_text' } );
    $namesearch = $scrub{namesearch};

    #get all objects for count
    my $select = $data_class->collect_lib(
        site       => $site,
        user       => $user,
        in_lib     => 1,
        order      => $order,
        namesearch => $namesearch,
    ) or return $app->ajax_system_error;
    my $total = $select->count;

    #now winnow to requested objects
    $select = $select->select( {}, { offset => $offset, limit => $limit } )
      or return $app->ajax_system_error;

    #figure out the fields to include in json output
    my @incl_cols = ('id');
    foreach my $field ( $data_class->editor_abbr ) {
        push @incl_cols, $field->{column} if $field->{column};
    }
    my %props = $data_class->properties;

    my ( $obj, @items );
    while ( $obj = $select->next ) {
        my %item = json_format_cols( $app, $obj, \@incl_cols, \%props );
        $item{mod_time} = $app->format_time( $obj->mod_time );
        push @items, \%item;
    }
    return $app->ajax_system_error if !defined $obj;
    return $app->ajax_data( { totalRecords => $total, records => \@items } );
}

sub rm_ajax_diff {
    my $app  = shift;
    my $user = $app->current_user;
    my $sid  = $app->current_site->id;
    my ( $type, $pid, $version ) =
      $app->path_args;    #type is always page for now
    $pid     =~ s/\D//msg;
    $version =~ s/\D//msg;
    if ( !$pid || !$version ) {
        $app->set_error(
            head => 'ACCOUNT_ERR_Missing data',
            text => 'ACCOUNT_ERR_TEXT_Missing data'
        );
        return $app->ajax_system_error();
    }

    #confirm permission to edit requested content
    require BigMed::Content::Page;
    my $page = BigMed::Content::Page->fetch( { site => $sid, id => $pid } );
    return $app->ajax_system_error if !defined $page;
    if ( !$page ) {
        $app->set_error(
            head => 'EDITOR_Unknown Content',
            text =>
              ['EDITOR_TEXT_Unknown Content', $app->language('page'), $pid],
        );
        return $app->ajax_system_error();
    }
    if ( !$page->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->ajax_system_error();
    }

    #fetch the version info
    my $pv = BigMed::PageVersion->fetch(
        { site => $sid, page => $pid, version => $version } );
    return $app->ajax_system_error if !defined $pv;
    if ( !$pv ) {
        $app->set_error(
            head => 'EDITOR_Unknown Content',
            text => [
                'EDITOR_TEXT_Unknown Content', $app->language('page'),
                "${pid}v$version"
            ],
        );
        return $app->ajax_system_error();
    }

    #get the text to compare
    my %field =
      $app->parse_submission( { id => 'difftext', parse_as => 'raw_text' } );
    return $app->ajax_parse_error( $field{_ERROR} ) if $field{_ERROR};
    require BigMed::Filter;
    my $ntext = BigMed::Filter->filter( $field{difftext} );
    my $otext = BigMed::Filter->filter( $pv->content );
    my ( $revert_type, $revert_text ) =
      BigMed::Filter->extract_filter_and_text( $pv->content );
    if ( !BigMed::Filter->browser_supports( $app, $revert_type ) ) {

        #get final html to present as rawhtml or markdown
        $revert_text = BigMed::Filter->inline_single_graf($otext);
        $revert_type =
          ( index( $revert_text, '<' ) >= 0 ) ? 'RawHTML' : 'Markdown';
    }
    $revert_text = $otext if $revert_type eq 'RichText';

    #convert to plain text
    foreach my $t ( $ntext, $otext ) {

        #mild attempt to remove contents of bad containing tags
        $t =~ s/<\s*
          (script|style|select|head|textarea)
          [^>]*>.+?<\/\s*\1[^>]*>
          //msxg;

        #replace block tags with line breaks
        $t =~ s/\s+/ /msg;
        $t =~ s/ *<(p|address|blockquote|h\d|li)[^>]*> */\n\n/msgi;
        $t =~ s/ *<(br|center|div|pre|td)[^>]*> */\n/msgi;

        $t = $app->strip_html_tags($t);
        $t =~ s/&nbsp;|&#160;/ /msg;
        $t = $app->unescape($t);
        $t =~ s/\A\s+//g;
        $t =~ s/\s+\z//g;
    }
    if ( $ntext eq $otext ) {
        return $app->ajax_html(
            $app->html_template_screen('wi_editor_diffsame.tmpl') );
    }

    my $intro = $app->html_template_screen(
        'wi_editor_diffintro.tmpl',
        changetext => $app->language(
            [   'EDITOR_Changes are shown in green and red',
                $app->format_time( $pv->create_time )
            ]
        ),
    );

    return $app->ajax_data(
        {   o     => $otext,
            n     => $ntext,
            intro => $intro,
            rtype => $revert_type,
            rtext => $revert_text
        }
    );
}

###########################################################
# MENU PAGE SUPPORT ROUTINES
###########################################################

sub redirect_to_menu {
    my ( $app, $url_type, $message, $is_error ) = @_;
    $url_type ||= 'page';
    my $new_url = $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'menu',
        site   => $app->current_site,
        args   => [$url_type]
    );
    if ( $app->error ) {
        my %error = $app->error_html_hash;
        $message  = $error{text};
        $is_error = 1;
    }
    $app->set_session_message( $message, $is_error ) if $message;
    $app->header_type('redirect');
    $app->header_props( -url => $new_url );
    return "Redirecting to $new_url";
}

sub menu_navbar_base_url {
    my ( $app, $type, $subtype, $rsort, $rorder ) = @_;
    $type .= ".$subtype" if $subtype;
    return $app->build_url(
        script => 'bm-editor.cgi',
        site   => $app->current_site->id,
        rm     => 'menu',
        args   => [$type, join( q{.}, @{$rsort} ), join( q{.}, @{$rorder} )],
    );
}

sub menu_page_tabs {
    my ( $app, $type, $site_id ) = @_;
    my @types = grep { $_ ne 'page' } BigMed::Content->content_types;
    my @tabs;
    foreach my $tab ( 'page', @types ) {
        push @tabs,
          { text     => $app->language( 'TITLE_PL_' . $tab ),
            selected => $type eq $tab,
            url      => $app->build_url(
                script => 'bm-editor.cgi',
                rm     => 'menu',
                site   => $site_id,
                args   => [$tab],
            ),
          };
    }
    return \@tabs;
}

sub menu_page_subtabs {
    my ( $app, $class, $subtype, $site_id ) = @_;
    my $label    = $class->data_label;
    my @subtypes = $class->content_subtypes;
    my @subtabs;
    if ( @subtypes > 1 ) {
        $subtype ||= 'all';
        foreach my $subtab ( 'all', @subtypes ) {
            push @subtabs,
              { text     => $app->language( 'TITLE_PL_' . $subtab ),
                selected => $subtype eq $subtab,
                url      => $app->build_url(
                    script => 'bm-editor.cgi',
                    rm     => 'menu',
                    site   => $site_id,
                    args   => ["$label.$subtab"],
                ),
              };
        }

    }
    return \@subtabs;
}

sub menu_new_links {
    my ( $app, $class, $subtype, $site_id ) = @_;
    my $label    = $class->data_label;
    my @subtypes = $class->content_subtypes;
    my @new_links;
    if ( @subtypes > 1 ) {
        foreach my $type (@subtypes) {
            next if $type eq 'section' and $label eq 'page';
            my $name = q{%BM} . $app->language( 'TITLE_' . $type ) . q{%};
            push @new_links,
              { text => $app->language( ['EDITOR_New Title', $name] ),
                url => $app->build_url(
                    script => 'bm-editor.cgi',
                    rm     => 'edit',
                    site   => $site_id,
                    args   => ["$label.$type"],
                ),
              };
        }
    }
    else {
        my $name = q{%BM} . $app->language( 'TITLE_' . $label ) . q{%};
        @new_links = (
            {   text => $app->language( ['EDITOR_New Title', $name] ),
                url => $app->build_url(
                    script => 'bm-editor.cgi',
                    rm     => 'edit',
                    site   => $site_id,
                    args   => [$label],
                ),
            }
        );
    }
    return \@new_links;
}

sub menu_summary_coderef {
    my $app   = shift;
    my $class = shift;
    my $coderef;
    if ( $class->can('description') ) {
        require BigMed::Filter;
        $coderef = sub {
            my $text = BigMed::Filter->filter( $_[0]->description )
              || $_[0]->title;
            $text = $app->escape( $app->strip_html_tags($text) );
            $text =~ s/\s+/ /msg;
            if ( length $text > 200 ) {
                $text = substr( $text, 0, 200 ) . '...';
            }
            $text;
        };
    }
    else {
        $coderef = sub {
            my $text = $_[0]->title;
            $text = $app->escape( $app->strip_html_tags($text) );
            if ( length $text > 200 ) {
                $text = substr( $text, 0, 200 ) . '...';
            }
            $text;
        };
    }
    return $coderef;
}

sub menu_slug_path {    #for section pages only
    my ( $site, $obj ) = @_;
    my $sec = $site->section_obj_by_id( ( $obj->sections )[0] )
      or return q{};
    return q{} if $sec->is_homepage;
    my @slug;
    my @p = $sec->parents;
    shift @p;
    foreach my $p ( map { $site->section_obj_by_id($_) } @p ) {
        next if !$p;
        push @slug, $p->slug;
    }
    push @slug, $sec->slug;
    return join( ' &gt; ', @slug );
}

sub published_text {
    my $app     = shift;
    my $content = shift;
    my $site    = $app->current_site;
    my $pub_time;
    if ( $content->pub_time ) {
        $pub_time = $app->format_time( $content->pub_time );
    }
    elsif ( $content->auto_pub_time ) {
        $pub_time = $app->language(
            [   'EDITOR_Scheduled',
                $app->format_time( $content->auto_pub_time )
            ]
        );
    }
    else {
        $pub_time = $app->language('BM_Unpublished');
    }
    return $pub_time;
}

###########################################################
# EDIT PAGE SUPPORT ROUTINES
###########################################################

sub build_content_fieldsets {
    my $app     = shift;
    my $content = shift;
    my %options = @_;
    my @fieldsets;
    foreach my $rfieldset ( $content->editor_fields() ) {
        next if !$rfieldset->{fields};
        my @fields;
        foreach my $rfield_hash ( @{ $rfieldset->{fields} } ) {
            if ( $rfield_hash->{relation} ) {
                push @fields,
                  $app->build_relation_fields( $content,
                    $rfield_hash->{relation},
                  );
            }
            else {    #regular field
                my $value;
                my $col = $rfield_hash->{column};
                if ($col) {
                    $rfield_hash->{data_class} = ref $content;
                    my $ref_getter = "_ref_$col";
                    $value = $content->$ref_getter;
                }
                my %addl = ( value => $value );
                my $callback = $rfield_hash->{prompt_callback};
                %addl = ( %addl, $callback->( $app, $content ) ) if $callback;
                push @fields,
                  $app->prompt_field_ref( %{$rfield_hash}, %addl );
            }
        }

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

sub build_relation_fields {
    my $app      = shift;
    my $content  = shift;
    my $relation = shift;
    my $subtype  = $content->subtype;    #force scalar value
    my @classes = $content->load_related_classes($relation) or return ();
    my %relationship = $content->relationship_info( $relation, $subtype )
      or return ();

    #if @objects is empty, could be an error; just keep marching along,
    #the error messages will be displayed on the edit page...
    my @objects =
      ref $relationship{sort} eq 'CODE'
      ? $relationship{sort}->( $content->load_related_objects($relation) )
      : $content->load_related_objects($relation);

    if ( $relationship{points} ) {       #mark whether they're editable
        my $user = $app->current_user;
        foreach my $o (@objects) {
            $o->[1]
              ->set_stash( 'can_edit', $o->[1]->editable_by_user($user) );
        }
    }

    my $sortable = $relationship{sortable};    #do this to force scalar
    return $app->prompt_field_ref(
        prompt_as => ( $relationship{custom_prompt} || 'module_objects' ),
        id => 'BM_MINI_' . $relation,
        relation_name    => $relation,
        relation_class   => \@classes,
        relation_points  => $relationship{points},
        relation_info    => \%relationship,
        relation_preview => $relationship{preview},
        objects          => \@objects,
        sortable         => $sortable,
        hide_label       => 1,
        site             => $app->current_site,
    );
}

sub left_col_page_status {
    my $app      = shift;
    my $content  = shift;
    my $user     = $app->current_user or return;
    my $mod_time = $content->mod_time || q{};
    $mod_time &&= $app->format_time($mod_time);

    my $version = $content->version || 0;
    $version++;

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

    #get owner name
    my %uname;
    my $owner_name;
    my $own_id = $content->owner || 0;
    if ( !$own_id || $own_id == $user->id ) {
        $owner_name = ( $content->subtype && $content->subtype eq 'section' )
          ? q{--}    #sections have no owners
          : $user->name;
    }
    else {
        defined( my $owner = BigMed::User->fetch($own_id) )
          or $app->error_stop;
        $uname{$own_id} = $owner_name = ( $owner ? $owner->name : q{--} );
    }

    #get last editor name
    my $last_editor;
    my $ed_id = $content->last_editor;
    if ($ed_id) {
        if ( $ed_id == $own_id ) {
            $last_editor = $owner_name;
        }
        elsif ( $ed_id == $user->id ) {
            $uname{$ed_id} = $last_editor = $user->name;
        }
        else {
            defined( my $editor = BigMed::User->fetch($ed_id) )
              or $app->error_stop;
            $uname{$ed_id} = $last_editor = ( $editor ? $editor->name : q{} );
        }
    }

    my @previous;
    if ( $content->isa('BigMed::Content::Page') ) {
        my $pv_select = BigMed::PageVersion->select(
            { site => $content->site, page  => $content->id },
            { sort => 'version',      order => 'descend' }
        ) or return;

        my $vurl = $app->build_url(
            script => 'bm-editor.cgi',
            site   => $content->site,
            rm     => 'ajax-diff',
            args   => ['page', $content->id],
        );
        my $pv;
        while ( $pv = $pv_select->next ) {
            my $pv_ed = $pv->editor;
            if ( $pv_ed && !$uname{$pv_ed} ) {
                defined( my $u = BigMed::User->fetch($pv_ed) )
                  or $app->error_stop;
                $uname{$pv_ed} = $u ? $u->name : q{--};
            }
            my $editor = $pv_ed ? $uname{$pv_ed} : q{--};
            my $v = $app->language( ['EDITOR_Version #', $pv->version] );
            push @previous,
              { 'url'     => "$vurl/v" . $pv->version,
                'version' => $v,
                'editor'  => $editor,
                'time'    => $app->format_time( $pv->create_time ),
              };
        }
    }

    return $app->html_template(
        'wi_editor_edit_leftcol.tmpl',
        version     => $version,
        mod_time    => $mod_time,
        owner       => $owner_name,
        last_editor => $last_editor,
        pub_time    => $app->published_text($content),
        create_time => $create_time,
        previous    => \@previous,
    );
}

###########################################################
# MISC UTILITY ROUTINES
###########################################################

sub _get_menu_search_criteria {
    my ( $app, $class, $subtype ) = @_;
    my $user = $app->current_user or return $app->rm_login;
    my $site = $app->current_site or return $app->rm_login_site;
    my %filter = $app->_get_menu_filters();

    #identify requested sections
    my @req_secs;
    if ( $filter{'sec'} && $filter{'sub'} ) {
        @req_secs =
          ( $filter{'sec'}, $site->all_descendants_ids( $filter{'sec'} ) );
    }
    elsif ( $filter{'sec'} ) {
        @req_secs = ( $filter{'sec'} );
    }

    #winnow sections according to privileges
    my @filter_secs;
    my $site_id    = $site->id;
    my $user_level = $user->privilege_level($site_id);
    if ( $user_level < 5 ) {    #non-admin
        my %site_priv = $user->allowed_section_hash($site);
        if (@req_secs) {
            @filter_secs = grep { $site_priv{$_} } @req_secs;
            @filter_secs = keys %site_priv if !@filter_secs;
        }
        else {
            @filter_secs = keys %site_priv;
        }
    }
    else {
        @filter_secs = @req_secs;    #admin
    }

    #build criteria and get *all* matching items for count
    my %criteria = ( site => $site_id );
    $criteria{subtype} = $subtype if $subtype;
    if ( defined $filter{'q'} && $filter{'q'} ne q{} ) {    #OR title search
        $filter{'q'} =~ s/[.,"]/ /msg;
        $filter{'q'} =~ s/&quot;/ /msg;

        #build the regex; use blocks of anything other than
        #marks, punctuation and separators. Originally split the
        #words by using a regex paren collector:
        #    ( $filter{'q'} =~ /([^\p{M}\p{P}\p{Z}]+)/msg )
        #...but Perl 5.6 mangles that. Splitting does the same thing
        #but works under 5.6.
        #there is still a weird 5.8.0 bug that won't match a utf8 string
        #at the very end of a string. not a prob in 5.6.1 or 5.8.6.
        #looks like a utf8 regex bug in 5.8.0.
        my $find = join( q{|},
            map    { "\Q$_\E" }
              grep { length $_ }
              ( split( /[\p{M}\p{P}\p{Z}]+/, $filter{'q'} ) ) );
        $criteria{title} = qr/$find/i;
    }
    $criteria{sections} = @filter_secs < 2 ? $filter_secs[0] : \@filter_secs;

    #writers limited to their own items
    $criteria{owner} = $user->id if $user_level < 3 && $class->can('owner');

    return \%criteria;
}

sub _get_menu_filters {
    my $app = shift;
    my $s   = $app->session;
    my %f   = $app->parse_submission(
        { id => 'sec', parse_as => 'id' },
        { id => 'sub', parse_as => 'boolean' },
        { id => 'q',   parse_as => 'simple_text' },
    );
    if ( !defined $f{'sec'} ) {    #no query provided
        @f{qw(sec sub q)} = (
            $s->param('EDITOR_MENU_SEC'),
            $s->param('EDITOR_MENU_SUB'),
            $s->param('EDITOR_MENU_STRING'),
        );
        if ( $f{sec} && !$app->current_site->section_obj_by_id( $f{sec} ) ) {
            $f{sec} = undef;
            $s->param( 'EDITOR_MENU_SEC', undef );
        }
    }
    else {
        $s->param( 'EDITOR_MENU_SEC',    $f{'sec'} );
        $s->param( 'EDITOR_MENU_SUB',    $f{'sub'} );
        $s->param( 'EDITOR_MENU_STRING', $f{'q'} );
    }
    return %f;
}

sub _get_link_sources_and_sections {
    my $selection = shift;    #link selection

    #returns two array references: page ids and section ids of *published*
    #source objects from the link selection. if the source objects are not
    #pages, only the section ids get added. All repeats are removed.
    my $link;
    my %page;
    my %section;
    while ( $link = $selection->next ) {
        my $sclass = BigMed::Content->source_class( $link->linktype );
        my $source =
          $sclass->fetch( { site => $link->site, id => $link->linkid } )
          or next;
        next if $source->pub_status ne 'published';
        $page{ $source->id } = 1 if $source->isa('BigMed::Content::Page');
        $section{$_} = 1 for ( $source->sections );
    }
    return ( [keys %page], [keys %section] );
}

sub _update_sortable_relations {
    my ( $app, $content ) = @_;
    foreach my $rel ( $content->data_relationships ) {
        my %info = $content->relationship_info($rel);
        next if !$info{sortable};
        my $order = $app->utf8_param("BM_MINI_SORT_ORDER_$rel") or next;
        my @classes = $content->load_related_classes($rel) or return ();
        my %sort_info =
          ref $info{sortable} eq 'HASH' ? %{ $info{sortable} } : ();
        my $do_value = $sort_info{do_value} || sub { 1000 - $_[0] };
        my $col = $sort_info{column} || 'priority';
        my %map;
        my $count = 0;

        while ( $order =~ m{\Q$rel\E(.+?)(\d+)}msg ) {
            $count++;
            $map{$2} = $do_value->($count);
        }
        return 1 if !%map;
        my @obj = $content->load_related_objects($rel);
        if ( $info{points} ) {    #the sort applies to the pointer metadata
            foreach my $o (@obj) {
                my $pointer = $o->[0];
                my $id      = $pointer->id;
                my %meta    = $pointer->metadata;
                if ( defined $map{$id}
                    && ( !defined $meta{$col} || $map{$id} ne $meta{$col} ) )
                {
                    $meta{$col} = $map{$id};
                    $pointer->set_metadata( \%meta );
                    $pointer->save or return;
                }
            }
        }
        else {    #the point applies to the has object itself
            my $setter = "set_$col";
            foreach my $o (@obj) {
                my $id = $o->id;
                if ( defined $map{$id}
                    && ( !defined $o->$col || $o->$col ne $map{$id} ) )
                {
                    $o->$setter( $map{$id} );
                    $o->save or return;
                }
            }
        }
    }
    return 1;
}

sub _set_breadcrumbs {
    my $app   = shift;
    my %param = @_;
    my ( $class, $title, $type, $subtype, $this_page ) =
      @param{qw(class title type subtype this_page)};
    $this_page ||= q{};
    $type      ||= q{};

    #sets these breadcrumbs up to appropriate depth; no link if you're on
    #the page itself
    #1) link to the editor menu for the content type
    #2) link to the editor menu for the subtype
    #   (suppress if no subtype or just one subtype)
    #3) title supplied in title routine.

    #this_page should be set to same as type if on the main type menu page.
    #this_page should be set to "type.subtype" if on subtype menu page.
    #title should be set only when not on a menu page.

    #suppress subtype link if one or less subtypes
    my @subtypes = $class->content_subtypes();
    if ( @subtypes < 2 ) {
        undef $subtype;
        $this_page = $type if $this_page;
    }

    my $type_name = $app->language( 'TITLE_' . $type );
    my $type_url = ( $this_page eq $type ) ? undef : $app->build_url(
        script => 'bm-editor.cgi',
        rm     => 'menu',
        args   => [$type],
    );
    my @bc = (
        {   bc_label => ['EDITOR_Type Edit Menu', $type_name],
            bc_url   => $type_url,
        }
    );

    if ($subtype) {
        my $subtype_name = $app->language( 'TITLE_' . $subtype );
        my $subtype_url =
          ( $this_page eq "$type:$subtype" ) ? undef : $app->build_url(
            script => 'bm-editor.cgi',
            rm     => 'menu',
            args   => ["$type.$subtype"],
          );
        push @bc,
          { bc_label => ['EDITOR_Type Edit Menu', $subtype_name],
            bc_url   => $subtype_url,
          };
    }
    push @bc, { bc_label => $title } if $title;
    $app->set_cp_breadcrumbs(@bc);
    return;
}

sub _load_object_from_url {
    my ( $app, $site ) = @_;
    my ( $class, $subtype, $id ) = $app->_object_info_from_url();
    return if !$class;
    return $class->fetch_or_create_content_obj(
        id      => $id,
        site    => $site->id,
        subtype => $subtype
    );
}

sub _object_info_from_url {
    my $app = shift;
    my ( $class, $class_label, $subtype ) =
      $app->_class_type_and_subtype_from_url();
    return () if !$class;
    $subtype = $class->default_subtype if !$subtype;
    my @path_args = $app->path_args;     # label/id
    my $id = ( $path_args[1] || q{} );
    $id =~ s/\D//msg;
    return ( $class, $subtype, $id );
}

sub _class_type_and_subtype_from_url {
    my $app = shift;
    my $class_label = ( $app->path_args )[0] || 'page';
    my $subtype;
    ( $class_label, $subtype ) = split( /[.]/ms, $class_label );
    my $class = BigMed::Content->class_for_label($class_label);
    return $app->set_error(
        head => 'EDITOR_Unknown Content Type',
        text => ['EDITOR_TEXT_Unknown Content Type', $class_label],
    ) if !$class;
    undef $subtype if ( $subtype && !$class->subtype_exists($subtype) );
    return ( $class, $class_label, $subtype );
}

sub _has_required_path_info {
    my $app      = shift;
    my %required = @_;
    while ( my ( $key, $value ) = each(%required) ) {
        next if $value;
        $app->set_error(
            head => 'EDITOR_HEAD_Could Not Resolve Content Info',
            text => [
                'EDITOR_TEXT_Could Not Resolve Content Info',
                $app->language($key)
            ],
        );
    }
    return $app->error ? undef : 1;
}

sub _ajax_unknown_type_err {
    my $app  = shift;
    my $type = shift;
    $app->set_error(
        head => 'EDITOR_Unknown Content Type',
        text => [
            'EDITOR_Could not save mini because of unknown content type',
            $type
        ],
    );
    return $app->ajax_system_error();
}

1;

__END__

