# 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: ImportV1.pm 3305 2008-09-03 12:22:16Z josh $

package BigMed::ImportV1;
use strict;
use warnings;
use utf8;
use Carp;
use BigMed;
use BigMed::Log;
use BigMed::Status;
use BigMed::DiskUtil
  qw(bm_file_path bm_load_file bm_confirm_dir bm_write_file
  bm_delete_file bm_copy_dir bm_copy_file bm_untaint_filepath);
use BigMed::Plugin;
use BigMed::Media::Image;
use BigMed::Theme;
use BigMed::User;
use BigMed::Site;
use BigMed::Section;
use BigMed::Template;
use BigMed::Content::Page;
use BigMed::Content::Tip;
use BigMed::Content::Annc;
use BigMed::Builder;
use BigMed::MD5 qw(md5_hex);
use BigMed::Search;

sub new {
    my $class        = shift;
    my $v1_moxiedata = shift;
    croak 'BigMed::ImportV1 object creation requires valid v1 moxiedata path'
      if !$v1_moxiedata || !-e $v1_moxiedata || !-d $v1_moxiedata;

    BigMed::Plugin->load_formats;

    my $v2_moxiedata = BigMed->bigmed->env('MOXIEDATA');
    my $status_dir = bm_file_path( $v2_moxiedata, 'importv1' );
    bm_confirm_dir( $status_dir, { data => 1 } ) or return;
    my $lockfile = bm_file_path( $status_dir, 'updating.txt' );

    my $self = {
        v1_moxiedata => $v1_moxiedata,
        v1_userdir   => bm_file_path( $v1_moxiedata, 'user_data', 'members' ),
        status_dir   => $status_dir,
        lockfile     => $lockfile,
        v2_moxiedata => $v2_moxiedata,
    };
    bless $self, $class;

    return $self;
}

# MAIN IMPORT ROUTINE
# -----------------------------------------------------------------

sub import_v1_data {
    my $self = shift;
    my %opt  = @_;

    #if there's a lock file and it was updated within the last 2 minutes,
    #then we'll assume that there's an update underway
    my $lockfile = $self->lockfile;
    if ( -e $lockfile && -M $lockfile < 0.0014 ) {
        $self->log( notice =>
              'ImportV1: Import requested, but another is already underway' );
        return BigMed->set_error(
            head => 'IMPORTV1_HEAD_Import Already Underway',
            text => 'IMPORTV1_Import Already Underway',
        );
    }
    $self->update_lockfile('Starting import') or return;

    my ( $rdone, $rerror );
    if ( $opt{statusbar} ) {
        $self->{statusbar} = $opt{statusbar};
        $rerror = sub {
            $self->{statusbar}->send_error( $_[0] );
        };
        $rdone = sub {
            $self->{statusbar}->mark_done();
            exit(0)
        };
    }
    else {
        delete $self->{statusbar};
        $rdone  = sub { 1 };
        $rerror = sub { return };
    }

    $self->import_users( skip_dupes => $opt{merge_usernames} )
      or return $rerror->();
    $self->import_sites(%opt) or return $rerror->();
    return 1 if $opt{_DEBUG};

    $self->clear_status_logs or return $rerror->();
    $self->log( notice => 'ImportV1: Import complete' );
    return $rdone->();
}

# ACCESSORS
# -----------------------------------------------------------------

sub v1_moxiedata {
    return $_[0]->{v1_moxiedata};
}

sub v2_moxiedata {
    return $_[0]->{v2_moxiedata};
}

sub v1_userdir {
    return $_[0]->{v1_userdir};
}

sub v1_sitedir {
    my ( $self, $siteid ) = @_;
    return bm_file_path( $self->v1_moxiedata, 'sites', $siteid );
}

sub v1_site_ids {
    my $self  = shift;
    my $dir   = $self->v1_moxiedata;
    my @sites =
      bm_load_file( bm_file_path( $dir, 'sites', 'siteindex.cgi' ) );
    return map { ( split(/_!!_/ms) )[0] } @sites;
}

sub v1_site_hash {
    my $self  = shift;
    my $dir   = $self->v1_moxiedata;
    my @sites =
      bm_load_file( bm_file_path( $dir, 'sites', 'siteindex.cgi' ) );
    return map { split(/_!!_/ms) } @sites;
}

sub v1_user_ids {
    my $self  = shift;
    my $dir   = $self->v1_userdir;
    my @users = bm_load_file( bm_file_path( $dir, 'memberslist.cgi' ) );
    return map { ( split(/_!!_/ms) )[0] } @users;
}

# STATUS LOGGERS
# -----------------------------------------------------------------

sub status_dir {
    return $_[0]->{status_dir};
}

sub usermap_path {
    return bm_file_path( $_[0]->status_dir, 'v1_usermap.cgi' );
}

sub sitemap_path {
    return bm_file_path( $_[0]->status_dir, 'v1_sitemap.cgi' );
}

sub sectionmap_path {
    my ( $self, $old_id ) = @_;
    croak 'sectionmap_path requires old site id argument' if !$old_id;
    return bm_file_path( $self->status_dir, "v1_sectionmap-$old_id.cgi" );
}

sub pagemap_path {
    my ( $self, $old_id ) = @_;
    croak 'pagemap_path requires old site id argument' if !$old_id;
    return bm_file_path( $self->status_dir, "v1_pagemap-$old_id.cgi" );
}

sub lockfile {
    return $_[0]->{lockfile};
}

sub update_lockfile {
    my $self = shift;
    defined( my $message = shift )
      or croak 'update_lockfile requires message argument';
    $message = 'Importing from ' . $self->v1_moxiedata . "\n$message";
    return bm_write_file( $self->lockfile, $message );
}

sub clear_status_logs {
    my $self = shift;
    $self->trash_user_map             or return;
    $self->trash_site_status          or return;
    bm_delete_file( $self->lockfile ) or return;

    return 1;
}

sub update_user_map {
    my $self         = shift;
    my %reverse_id   = reverse %{ $self->{userid_map} };
    my %reverse_name = reverse %{ $self->{username_map} };

    my @lines =
      map  { "${_}:::$reverse_id{$_}:::$reverse_name{$_}" }
      sort { $a <=> $b } keys %reverse_id;
    return bm_write_file( $self->usermap_path, join( "\n", @lines ) );
}

sub load_user_map {
    my $self    = shift;
    my @entries = bm_load_file( $self->usermap_path );
    foreach my $e (@entries) {
        my ( $id, $old_id, $old_name ) = split( /:::/ms, $e );
        $self->{userid_map}->{$old_id}     = $id;
        $self->{username_map}->{$old_name} = $id;
    }
    return 1;
}

sub trash_user_map {
    my $self = shift;
    $self->{userid_map}   = {};
    $self->{username_map} = {};
    return bm_delete_file( $self->usermap_path );
}

sub update_site_status {
    my ( $self, $site, $status, $old_id ) = @_;
    croak 'update_site_status requires site argument'   if !$site;
    croak 'update_site_status requires status argument' if !$status;
    my $sid = $site->id;
    if ($old_id) {
        $self->{site_map}->{$sid} = $old_id;
    }
    elsif ( !( $old_id = $self->{site_map}->{$sid} ) ) {
        croak 'update_site_status: Unknown site requires old id arg';
    }

    $self->{old_site_status}->{$old_id} = $status;
    $self->{site_status}->{$sid}        = $status;

    my %site_map = %{ $self->{site_map} };

    my @lines =
      map  { "${_}:::$site_map{$_}:::" . $self->{site_status}->{$_} }
      sort { $a <=> $b } keys %site_map;

    return bm_write_file( $self->sitemap_path, join( "\n", @lines ) );
}

sub load_site_status {
    my $self = shift;
    $self->{old_site_status} = {};
    $self->{site_status}     = {};
    $self->{site_map}        = {};
    foreach my $e ( bm_load_file( $self->sitemap_path ) ) {
        my ( $id, $old_id, $status ) = split( /:::/ms, $e );
        $self->{old_site_status}->{$old_id} = $status;
        $self->{site_status}->{$id}         = $status;
        $self->{site_map}->{$id}            = $old_id;
    }
    return 1;
}

sub trash_site_status {
    my $self = shift;
    $self->{old_site_status} = {};
    $self->{site_status}     = {};
    $self->{site_map}        = {};
    return bm_delete_file( $self->sitemap_path );
}

sub update_section_map {
    my ( $self, $section, $old_siteid ) = @_;
    croak 'update_section_map requires section argument'
      if !$section || !ref $section || !$section->isa('BigMed::Section');
    croak 'update_section_map requires old site id argument' if !$old_siteid;

    my $old_sec = $section->stash('old_id')
      or croak 'section object has no id in the old_id stash';
    $self->{section_map}->{$old_sec} = $section->id;

    my %section_map = %{ $self->{section_map} };
    my @lines       =
      map { "$section_map{$_}:::${_}" }
      keys %section_map;
    return bm_write_file( $self->sectionmap_path($old_siteid),
        join( "\n", @lines ) );
}

sub load_section_map {
    my ( $self, $old_siteid ) = @_;
    croak 'load_section_map requires old site id argument' if !$old_siteid;
    my @entries = bm_load_file( $self->sectionmap_path($old_siteid) );
    $self->{section_map} = {};
    foreach my $e (@entries) {
        my ( $id, $old_id ) = split( /:::/ms, $e );
        $self->{section_map}->{$old_id} = $id;
    }
    return 1;
}

sub trash_section_map {
    my ( $self, $old_siteid ) = @_;
    croak 'trash_section_map requires old site id argument' if !$old_siteid;
    $self->{section_map} = {};
    return bm_delete_file( $self->sectionmap_path($old_siteid) );
}

sub update_page_map {
    my ( $self, $old_siteid, $old_page_id, $new_id ) = @_;
    croak 'update_page_map requires old site id argument' if !$old_siteid;
    croak 'update_page_map requires old page id argument' if !$old_page_id;
    croak 'update_page_map requires new page id argument' if !$new_id;

    $self->{page_map}->{$old_page_id} = $new_id;

    my %page_map = %{ $self->{page_map} };
    my @lines    =
      map { "$page_map{$_}:::${_}" }
      keys %page_map;
    return bm_write_file( $self->pagemap_path($old_siteid),
        join( "\n", @lines ) );
}

sub load_page_map {
    my ( $self, $old_siteid ) = @_;
    croak 'load_page_map requires old site id argument' if !$old_siteid;
    my @entries = bm_load_file( $self->pagemap_path($old_siteid) );
    $self->{page_map} = {};
    foreach my $e (@entries) {
        my ( $id, $old_id ) = split( /:::/ms, $e );
        $self->{page_map}->{$old_id} = $id;
    }
    return 1;
}

sub trash_page_map {
    my ( $self, $old_siteid ) = @_;
    croak 'trash_page_map requires old site id argument' if !$old_siteid;
    $self->{page_map} = {};
    return bm_delete_file( $self->pagemap_path($old_siteid) );
}

# USER ROUTINES
# -----------------------------------------------------------------

sub import_users {
    my $self    = shift;
    my %options = @_;

    #avoid duplicating in case we're resuming
    $self->load_user_map or return;

    my @uids = $self->v1_user_ids;
    if ( $self->{statusbar} ) {
        $self->{statusbar}->update_status(
            progress => 0,
            steps    => scalar(@uids),
            message  => 'IMPORTV1_Importing accounts',
        );
    }

    my $progress = 0;
    foreach my $uid ( $self->v1_user_ids ) {
        $progress++;
        $self->{statusbar}->update_status( progress => $progress )
          if $self->{statusbar};
        next if $self->{userid_map}->{$uid};    #already have it
        defined( my $user = $self->fetch_v1_user($uid) ) or return;
        next if !$user;                         #no such user or file

        my $oname = $user->name;
        $self->log( info => "ImportV1: Importing user '$oname' ($uid)" );
        if ( $options{skip_dupes} && !$user->is_unique('name') ) {
            defined( my $exist_user =
                  BigMed::User->fetch( { name => $oname } ) )
              or return;
            if ($exist_user) {
                $self->{userid_map}->{$uid}     = $exist_user->id;
                $self->{username_map}->{$oname} = $exist_user->id;
                $self->update_user_map or return;
            }
            $self->log( info => 'ImportV1: Skipping (username exists)' );
            next;
        }
        elsif ( !$options{skip_dupes} ) {    #update name til it's unique
            _make_field_unique( $user, 'name' ) or return;
        }

        $user->save or return;
        my $tag = $self->log_data_tag($user);
        $self->log( info => "ImportV1: Saved as $tag" );

        $self->{userid_map}->{$uid}     = $user->id;
        $self->{username_map}->{$oname} = $user->id;
        $self->update_user_map or return;
    }
    $self->update_lockfile('Users imported') or return;
    return 1;
}

my %PRIV_MAP = (
    'Wri'   => 2,
    'Ed'    => 3,
    'Pub'   => 4,
    'Web'   => 5,
    'Admin' => 6,
);

sub fetch_v1_user {
    my ( $self, $uid ) = @_;
    my $path = bm_file_path( $self->v1_userdir, "$uid.cgi" );
    my @userdata = bm_load_file($path) or return q{};

    my $user = BigMed::User->new();
    $user->set_name( $userdata[0] );

    my $passw = $userdata[1];
    if ( length $passw != 32 || $passw =~ /[^a-fA-F0-9]/ms ) {    #legacy pass
        $passw = md5_hex($passw);
    }
    $user->set_password($passw);
    $user->set_email( $userdata[2] );
    $user->set_level( $PRIV_MAP{ $userdata[3] } || 0 );

    return $user;
}

sub import_privileges {
    my ( $self, $site, $old_id ) = @_;
    croak 'import_privileges requires site object arg'
      if !$site || !ref $site || !$site->isa('BigMed::Site');
    croak 'import_privileges requires old site id' if !$old_id;

    #file exists only if non-admins have permissions, and that's ok
    my $path = bm_file_path( $self->v1_sitedir($old_id), 'users.cgi' );
    foreach my $old_id ( bm_load_file($path) ) {
        my $uid = $self->{userid_map}->{$old_id} or next;
        defined( my $user = BigMed::User->fetch($uid) ) or return;
        next if !$user;
        $user->set_site_privileges( site => $site->id ) or return;
    }

    return 1;
}

# SITE ROUTINES
# -----------------------------------------------------------------

sub import_sites {
    my ( $self, %opt ) = @_;
    $self->load_site_status();
    my %lookup = reverse %{ $self->{site_map} };
    my @old_ids = $opt{sites} ? @{ $opt{sites} } : $self->v1_site_ids;
    foreach my $old_site_id (@old_ids) {
        my $new_id = $lookup{$old_site_id};
        my $status;
        if ($new_id) {
            $status = $self->{site_status}->{$new_id};
            next if $status eq 'DONE';
        }
        $self->fetch_v1_site( $old_site_id,
            { %opt, status => $status, id => $new_id } )
          or return;
        return 1 if $opt{_DEBUG};
        $self->update_lockfile("Site $old_site_id imported") or return;
    }
    $self->trash_site_status or return;
    return 1;
}

my %STATUS_MAP = (
    'SITE'      => 1,
    'PREFS'     => 2,
    'ASSETS'    => 3,
    'PRIVS'     => 4,
    'SECTIONS'  => 5,
    'TEMPLATES' => 6,
    'STYLES'    => 7,
    'PAGES'     => 8,
    'TIPS'      => 9,
    'ANNOUNCE'  => 10,
    'INDEX'     => 11,
);

sub fetch_v1_site {
    my ( $self, $siteid, $ropt ) = @_;    #8-digit legacy format id
    my $dir = $self->v1_sitedir($siteid);
    my $setup = bm_file_path( $dir, 'site_setup.cgi' );
    $setup = bm_untaint_filepath($setup) or return;
    if (!do $setup) {
        return BigMed->set_error(
            head => 'IMPORTV1_HEAD_Could not read site settings',
            text => ['IMPORTV!_TEXT_Could not read site settings',$siteid],
        );
    }

    my $debug = $ropt->{_DEBUG} || q{};

    my $sitename = $bmSetup::SiteName;
    my ( $num_sections, $num_pages );
    if ( $self->{statusbar} ) {           #figure out how many steps

        {
            my @sections =
              bm_load_file(
                bm_file_path( $dir, 'templates', 'Sections.setup' ) );
            $num_sections = scalar @sections;
        }
        {
            my @pages =
              bm_load_file(
                bm_file_path( $dir, 'articles', 'masterindex.cgi' ) );
            $num_pages = scalar @pages;
        }

        #9 base steps: prefs, assets, privs, templates, styles, tips, annc,
        #              search index, build -- plus number of sections and
        #              number of pages
        $self->{statusbar}->update_status(
            progress => 0,
            steps    => 9 + $num_sections + $num_pages,
            message  => ['IMPORTV1_Preferences', $sitename],
        );
    }

    my $resume_state =
      ( $ropt->{status} && $STATUS_MAP{ $ropt->{status} } )
      ? $STATUS_MAP{ $ropt->{status} }
      : 0;

    #even if resuming, go through the site and prefs stages to load
    #the basic site info and cached v1 locations; but don't update site
    #status if we're resuming

    #CREATE SITE OBJECT --------------------------------
    my $site = BigMed::Site->new();
    $site->set_id( $ropt->{id} ) if $ropt->{id};
    $site->set_date_format( date_format_v1() );
    $site->set_time_offset( time_offset_v1() );
    my $closer =
      ( $bmSetup::HTMLtype && $bmSetup::HTMLtype =~ /xhtml/msi) ? q{ /} : q{};
    $site->set_stash( 'v1_closer', $closer );
    $site->save or return;
    if ( $resume_state < $STATUS_MAP{'SITE'} ) {
        $self->update_site_status( $site, 'SITE', $siteid ) or return;
    }
    $self->log(
        info => "ImportV1: Created site for $siteid (" . $site->id . ')' );
    return 1 if $debug eq 'SITE';

    #SITE PREFS ----------------------------------------
    $self->load_site_from_globals($site) or return;
    $site->set_stash( 'v1_htmldir', bm_file_path( $site->html_dir ) );
    $site->set_stash( 'v1_images',
        bm_file_path( $site->html_dir, 'moxiepix' ) );
        
    #don't use doc_path for v1_docs, it's the *old* directory!
    $site->set_stash( 'v1_docs', bm_file_path( $site->html_dir, 'bm~doc' ) );
    $site->set_stash( 'v1_unpubdocs',
        bm_file_path( $self->v1_sitedir($siteid), 'unpubdocs' ) );

    #require uniqueness for name and dirs
    _make_field_unique( $site, 'name' ) or return;
    if ( $ropt->{newdir} ) {
        make_new_dirs($site) or return;
    }
    elsif (!$site->is_unique('html_dir')
        || !$site->is_unique('homepage_dir') )
    {
        make_new_dirs($site) or return;
    }

    $self->import_site_prefs($site) or return;
    $site->save                     or return;

    if ( $resume_state < $STATUS_MAP{'PREFS'} ) {
        $self->update_site_status( $site, 'PREFS' ) or return;
    }
    my $data_tag = $self->log_data_tag($site);
    $self->log( info => "ImportV1: All site prefs imported for $data_tag" );
    $self->{statusbar}->update_status(
        progress => 1,
        message  => ['IMPORTV1_Site assets', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'PREFS';

    #SITE ASSETS ---------------------------------------
    if ( $resume_state < $STATUS_MAP{'ASSETS'} ) {
        my $orig_assets =
          bm_file_path( $self->v2_moxiedata, 'support', 'assets' );
        my $site_assets = bm_file_path( $site->html_dir, 'bm.assets' );
        bm_copy_dir( $orig_assets, $site_assets ) or return;
        $self->update_site_status( $site, 'ASSETS' ) or return;
        $self->log( info => "ImportV1: Site assets copied for $data_tag" );
    }
    $self->{statusbar}->update_status(
        progress => 2,
        message  => ['IMPORTV1_Privileges', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'ASSETS';

    #USER PERMISSIONS ----------------------------------
    if ( $resume_state < $STATUS_MAP{'PRIVS'} ) {
        $self->import_privileges( $site, $siteid ) or return;
        $self->update_site_status( $site, 'PRIVS' ) or return;
        $self->log(
            info => "ImportV1: Site privileges assigned for $data_tag" );
    }
    return 1 if $debug eq 'PRIVS';

    #SECTIONS ------------------------------------------
    $self->load_section_map($siteid);

    #always load sections even if status map was already here, because the
    #section objects need to be updated with the old_id stash info
    $self->import_sections( $site, $siteid, $ropt->{_SECTIONSTOP} )
      or return;
    return 1 if $ropt->{_SECTIONSTOP};
    if ( $resume_state < $STATUS_MAP{'SECTIONS'} ) {
        $self->update_site_status( $site, 'SECTIONS' ) or return;
    }
    $self->log( info => "ImportV1: All sections imported for $data_tag" );
    $self->update_lockfile("Sections imported for site $siteid")
      or return;
    $self->{statusbar}->update_status(
        progress => 3 + $num_sections,
        message  => ['IMPORTV1_Templates', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'SECTIONS';

    #TEMPLATES -----------------------------------------
    if ( $resume_state < $STATUS_MAP{'TEMPLATES'} ) {
        $self->import_templates( $site, $siteid ) or return;
        $self->update_site_status( $site, 'TEMPLATES' ) or return;
        $self->log(
            info => "ImportV1: All templates imported for $data_tag" );
    }
    $self->{statusbar}->update_status(
        progress => 4 + $num_sections,
        message  => ['IMPORTV1_Styles', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'TEMPLATES';

    #CSS SETTINGS --------------------------------------
    if ( $resume_state < $STATUS_MAP{'STYLES'} ) {
        $self->transfer_v1_styles( $site, $siteid ) or return;
        $self->update_site_status( $site, 'STYLES' ) or return;
        $self->log( info => "ImportV1: All styles imported for $data_tag" );
    }
    return 1 if $debug eq 'STYLES';

    #PAGES ---------------------------------------------
    if ( $resume_state < $STATUS_MAP{'PAGES'} ) {
        $self->import_pages( $site, $siteid, $ropt->{_PAGESTOP} ) or return;
        return 1 if $ropt->{_PAGESTOP};
        $self->update_site_status( $site, 'PAGES' ) or return;
        $self->log( info => "ImportV1: All pages imported for $data_tag" );
        $self->update_lockfile("Pages imported for site $siteid") or return;
    }
    $self->{statusbar}->update_status(
        progress => 5 + $num_sections + $num_pages,
        message  => ['IMPORTV1_Tips', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'PAGES';

    #TIPS ----------------------------------------------
    if ( $resume_state < $STATUS_MAP{'TIPS'} ) {
        $self->import_tips( $site, $siteid ) or return;
        $self->update_site_status( $site, 'TIPS' ) or return;
        $self->log( info => "ImportV1: All tips imported for $data_tag" );
    }
    $self->{statusbar}->update_status(
        progress => 6 + $num_sections + $num_pages,
        message  => ['IMPORTV1_Announcements', $sitename],
      )
      if $self->{statusbar};
    return 1 if $debug eq 'TIPS';

    #ANNOUNCEMENTS -------------------------------------
    if ( $resume_state < $STATUS_MAP{'ANNOUNCE'} ) {
        $self->import_announcements( $site, $siteid ) or return;
        $self->update_site_status( $site, 'ANNOUNCE' ) or return;
        $self->log(
            info => "ImportV1: All announcements imported for $data_tag" );
    }
    $self->{statusbar}->update_status( #message updated in index_pages
        progress => 7 + $num_sections + $num_pages,
      )
      if $self->{statusbar};
    return 1 if $debug eq 'ANNOUNCE';

    #BUILD SEARCH INDEX --------------------------------
    if ( $resume_state < $STATUS_MAP{'INDEX'} ) {
        $self->index_pages($site) or return;
        $self->update_site_status( $site, 'INDEX' ) or return;
        $self->log(
            info => "ImportV1: All pages indexed for $data_tag" );
    }
    $self->{statusbar}->update_status(
        progress => 8 + $num_sections + $num_pages,
        message  => ['IMPORTV1_Building', $sitename],
      )
      if $self->{statusbar};

    #BUILD ---------------------------------------------
    my $builder = BigMed::Builder->new( site => $site ) or return;
    
    #handle statusbar and lockfile updates
    if ($self->{statusbar}) {
        my $rstatbar = sub {
            my $builder = shift;
            my $count   = $builder->context->content->count;
            my $name    = $builder->context->section->name;
            $self->{statusbar}->update_status( message =>
                  ['IMPORTV1_Building section', $sitename, $name, $count], );
            return 1;
        };
        $builder->add_trigger('fresh_section_context', $rstatbar);
        
        my $rping = sub {
            $self->{statusbar}->ping();
            return 1;
        };
        $builder->add_trigger('level_midbuild', $rping);
    }
    
    my $rlockfile = sub {
        my $builder = shift;
        my $name = $builder->context->section->name;
        return $self->update_lockfile("Building pages for '$name'");        
    };
    $builder->add_trigger('level_midbuild', $rlockfile);
    $builder->add_trigger('fresh_section_context', $rlockfile);
    $builder->add_trigger('before_detail_build', $rlockfile);

    $builder->build() or return;
    $self->update_site_status( $site, 'DONE' ) or return;
    $self->log( info => "ImportV1: All pages built for $data_tag" );

    #CLEAN UP ----------------------------------
    $self->trash_section_map($siteid) or return;

    undef $bmSetup::AboutUs;
    undef $bmSetup::AddMoreMain;
    undef $bmSetup::CSS2site;
    undef $bmSetup::DateBracket;
    undef $bmSetup::DataDir;
    undef $bmSetup::EngineInclude;
    undef $bmSetup::FillBrowseMore;
    undef $bmSetup::GMTOffset;
    undef $bmSetup::HideStyles;
    undef $bmSetup::HImageTable;
    undef $bmSetup::HSImageTable;
    undef $bmSetup::HSTextTable;
    undef $bmSetup::HTextTable;
    undef $bmSetup::ImageDir;
    undef $bmSetup::ImageURL;
    undef $bmSetup::VImageTable;
    undef $bmSetup::VSImageTable;
    undef $bmSetup::VSTextTable;
    undef $bmSetup::VTextTable;

    return $site;
}

sub load_site_from_globals {
    my $self = shift;
    my $site = shift or croak 'load_site_from_globals requires site argument';

    # I'm ignoring a few of the global vars:
    # TipJS (include javascript tip feed; we don't do that anymore)
    # OneClickFormat (now irrelevant)

    my %settings_map = (

        #site object settings
        htmlDir       => sub { $site->set_html_dir(shift) },
        htmlURL       => sub { $site->set_html_url(shift) },
        HomepageDir   => sub { $site->set_homepage_dir(shift) },
        HomepageURL   => sub { $site->set_homepage_url(shift) },
        site_doclimit => sub {
            my $limit = shift || 5120;
            $site->set_site_doclimit($limit);
        },
        SiteName      => sub { $site->set_name(shift) },

        #preference settings
        IncludeSubhead => sub {
            $site->store_pref( 'html_headline_subhead', make_boolean(shift) );
        },
        RequireSpotlightImage => sub {
            $site->store_pref( 'html_spotlight_needimage',
                make_boolean(shift) );
        },
        LatestArtStop => sub {
            $site->store_pref( 'html_links_exclude_self',
                make_boolean(shift) );
        },
        TipRandom => sub {
            $site->store_pref( 'html_tip_randomize', make_boolean(shift) );
        },
        BreadComplete => sub {
            $site->store_pref( 'html_breadcrumbs_full', make_boolean(shift) );
        },
        BreadLC => sub {
            $site->store_pref( 'html_breadcrumbs_lc', make_boolean(shift) );
        },
        JSfeed => sub {
            $site->store_pref( 'js_enable_feed', make_boolean(shift) );
        },
        RSSfeed => sub {
            $site->store_pref( 'rss_enable_feed', make_boolean(shift) );
        },
        HomeNavigation => sub {
            $site->store_pref( 'html_navigation_includehome',
                make_boolean(shift) );
        },

        #now those with default values

        HeadInsert => sub {
            $site->store_pref( 'html_htmlhead_xtrahtml',
                make_pref_value( shift, 'html_htmlhead_xtrahtml' ) );
        },
        TipDesc => sub {
            $site->store_pref( 'html_tip_pagetitle',
                make_pref_value( shift, 'html_tip_pagetitle' ) );
        },
        HTMLlang => sub {
            $site->store_pref( 'html_htmlhead_lang',
                make_pref_value( shift, 'html_htmlhead_lang' ) );
        },
        MainNumToDisplay => sub {
            $site->store_pref( 'html_links_numdisplay',
                make_pref_value( shift, 'html_links_numdisplay' ) );
        },
        MainShortToDisplay => sub {
            $site->store_pref( 'html_morelinks_numdisplay',
                make_pref_value( shift, 'html_morelinks_numdisplay' ) );
        },
        QuickNumToDisplay => sub {
            $site->store_pref( 'html_quicktease_numdisplay',
                make_pref_value( shift, 'html_quicktease_numdisplay' ) );
        },
        LatestArtNum => sub {
            $site->store_pref( 'html_latest_numdisplay',
                make_pref_value( shift, 'html_latest_numdisplay' ) );
        },
        SubNumToDisplay => sub {
            $site->store_pref( 'html_overflow_numdisplay',
                make_pref_value( shift, 'html_overflow_numdisplay' ) );
        },
        HomeNewsToDisplay => sub {
            $site->store_pref( 'html_news_numdisplay',
                make_pref_value( shift, 'html_news_numdisplay' ) );
        },
        TipNumToDisplay => sub {
            $site->store_pref( 'html_tip_numdisplay',
                make_pref_value( shift, 'html_tip_numdisplay' ) );
        },
        BreadcrumbSep => sub {
            $site->store_pref( 'html_breadcrumbs_separator',
                make_pref_value( shift, 'html_breadcrumbs_separator' ) );
        },
        RSSNum => sub {
            $site->store_pref( 'rss_display_num',
                make_pref_value( shift, 'rss_display_num' ) );
        },
        InternalDomains => sub {
            $site->store_pref( 'html_links_window_intdomains',
                make_pref_value( shift, 'html_links_window_intdomains' ) );
        },
        HTMLtype => sub {
            $site->store_pref( 'html_htmlhead_doctype',
                make_pref_value( shift, 'html_htmlhead_doctype' ) );
        },
    );

    my $tip_text = 'More ' . ( $bmSetup::TipDesc || 'Tips' );
    $site->store_pref( 'html_tip_linktext', $tip_text ) or return;

    {
        no strict 'refs';
        foreach my $var_name ( keys %settings_map ) {
            $settings_map{$var_name}->( ${"bmSetup::$var_name"} ) or return;
            undef ${"bmSetup::$var_name"};
        }
    }

    return $site;
}

my %DATE_FORMAT = (
    'USshort'     => '%b %e, %Y',
    'USshortday'  => '%a, %b %e, %Y',
    'USnum'       => '%n-%e-%Y',
    'USnumday'    => '%a %n-%e-%Y',
    'USlongday'   => '%A, %B %e, %Y',
    'EUshort'     => '%e %b, %Y',
    'EUshortday'  => '%a, %e %b, %Y',
    'EUnum'       => '%e-%n-%Y',
    'EUnumday'    => '%a %e-%n-%Y',
    'EUlongday'   => '%A, %e %B, %Y',
    'EUdotnum'    => '%e.%n.%Y',
    'EUdotnumday' => '%a %e.%n.%Y',

    'USlong'       => '%B %e, %Y',
    'EUlong'       => '%e %B, %Y',
    'USdotnum'     => '%n.%e.%Y',
    'USdotnumday'  => '%a %n.%e.%Y',
    'USyear'       => '%Y-%m-%d',
    'USyearday'    => '%a %Y-%m-%d',
    'EUyear'       => '%Y-%d-%m',
    'EUyearday'    => '%a %Y-%d-%m',
    'USdotyear'    => '%Y.%m.%d',
    'USdotyearday' => '%a %Y.%m.%d',
    'EUdotyear'    => '%Y.%d.%m',
    'EUdotyearday' => '%a %Y.%d.%m',
);

sub date_format_v1 {
    my $fmt = $bmSetup::DateFormat || $bmSetup::ArtDateFormat || 'USshort';
    undef $bmSetup::DateFormat;
    undef $bmSetup::ArtDateFormat;
    undef $bmSetup::SpotDateFormat;
    undef $bmSetup::PromoDateFormat;

    return $DATE_FORMAT{$fmt} || $DATE_FORMAT{'USshort'};
}

sub time_offset_v1 {

    #should have a gmt time but in case we're upgrading from an earlier
    #version, get it from timezoneoffset.
    if ( !defined $bmSetup::GMTOffset && defined $bmSetup::TimeZoneOffset ) {
        my $local_time = time() + ( $bmSetup::TimeZoneOffset * 3600 );
        my ( $hour, $day, $mon, $year ) = ( localtime($local_time) )[2 .. 5];
        my ( $gmt_hour, $gmt_day, $gmt_mon, $gmt_year ) =
          ( gmtime(time) )[2 .. 5];
        if (   $gmt_year < $year
            || ( $gmt_year == $year && $gmt_mon < $mon )
            || ( $gmt_year == $year && $gmt_mon == $mon && $gmt_day < $day ) )
        {

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

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

        #amount to add/subtract to get GMT time
        $bmSetup::GMTOffset = $gmt_hour - $hour;
    }

    #the v1 offset is actually the reverse of the v2 offset
    my $v1offset = $bmSetup::GMTOffset || 0;
    $v1offset *= -1;
    undef $bmSetup::TimeZoneOffset;
    undef $bmSetup::GMTOffset;

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

sub make_new_dirs {
    my $site = shift;
    croak 'make_new_dirs requires site object'
      if !$site || !$site->isa('BigMed::Site');
    my $orig = $site->html_dir
      or croak 'Site ' . $site->id . ' does not have a html directory';

    #if the html url is a web root, put it in a new directory
    my $html_url = $site->html_url;
    if ( $html_url =~ m{\Ahttps?://[^/]+\z}ms ) {
        $site->set_html_dir( bm_file_path( $orig, 'site-bm2' ) );
    }
    else {    #make it a sibling site
        $site->set_html_dir("$orig-bm2");
    }
    _make_field_unique( $site, 'html_dir' ) or return;

    my $new_dir = $site->html_dir;
    bm_confirm_dir( $new_dir, { build_path => 1 } ) or return;

    if ( $new_dir =~ /\A\Q$orig\E(.+)\z/ms ) {
        my $ext = $1;
        $site->set_html_url( $site->html_url . $ext );
    }
    elsif ( $new_dir ne $orig && $new_dir =~ /\A.*-bm2-(\d+)\z/ims ) {

        #original directory ended in -bm2 or -bm2-\d
        my $num = $1;
        my $url = $site->html_url;
        $url =~ s/bm2(-\d+)?$/bm2-$num/ims;
        $site->set_html_url($url);
    }

    #make homepage the same as page dir unless it's not a unique homepage dir
    $site->set_homepage_dir($new_dir);
    $orig = $new_dir;
    _make_field_unique( $site, 'homepage_dir' ) or return;
    $new_dir = $site->homepage_dir;
    bm_confirm_dir( $new_dir, { build_path => 1 } ) or return;

    if ( $new_dir ne $orig && $new_dir =~ /\A.*-bm2-(\d+)\z/ims ) {

        #original directory ended in -bm2 or -bm2-\d
        my $num = $1;
        my $url = $site->html_url;
        $url =~ s/bm2(-\d+)?$/bm2-$num/ims;
        $site->set_homepage_url($url);
    }
    else {
        $site->set_homepage_url( $site->html_url );
    }

    return 1;
}

sub import_site_prefs {
    my ( $self, $site ) = @_;

    #set the preferences that require some massaging

    ## HTMLHEAD FORMATS
    my $dir      = $self->v1_sitedir( $site->id );
    my @sections =
      bm_load_file( bm_file_path( $dir, 'templates', 'Sections.setup' ) );
    $site->store_pref( 'html_htmlhead_titlehome',
        home_titletag_v1( $site->name, $sections[0] ) )
      or return;
    $site->store_pref( 'html_htmlhead_titlepage', page_titletag_v1() )
      or return;
    $site->store_pref( 'html_htmlhead_robot', robot_behavior_v1() )
      or return;

    ## LINK FORMATS
    $site->store_pref( 'html_link_elements', link_elements_v1('Promo') )
      or return;
    $site->store_pref( 'html_spotlight_elements', link_elements_v1('Spot') )
      or return;
    $site->store_pref( 'html_morelinks_elements', alt_elements_v1('Short') )
      or return;
    $site->store_pref( 'html_latest_elements', alt_elements_v1('LatestArt') )
      or return;
    $site->store_pref( 'html_quicktease_elements',
        alt_elements_v1('QuickHit') )
      or return;
    $site->store_pref( 'html_news_elements', alt_elements_v1('TopNews') )
      or return;
    $site->store_pref( 'html_quicktease_textheading',
        '[In &lt;%section%&gt;]' )
      or return;
    $site->store_pref( 'html_links_desclb', q{} ) or return;

    #thumbnail alignment
    my $thumb_align = link_thumb_align_v1();
    my $long_align  = longlink_thumb_v1() ? $thumb_align : 'none';
    my $short_align = shortlink_thumb_v1() ? $thumb_align : 'none';
    $site->store_pref( 'html_link_imagepos', $long_align )
      or return;
    $site->store_pref( 'html_morelinks_imagepos', $short_align )
      or return;
    $site->store_pref( 'html_spotlight_imagepos', spot_thumb_align_v1() )
      or return;
    foreach my $type (qw(quicktease latest news )) {
        my @elements = $site->get_pref_value("html_${type}_elements");
        my $align = @elements == 1 ? $short_align : $long_align;
        $site->store_pref( "html_${type}_imagepos", $align ) or return;
    }

    #new windows
    $site->store_pref( 'html_links_window', new_window_v1() )
      or return;

    ## SHOW RELATED LINKS? -------
    my $show_related = related_links_v1();
    $site->store_pref( 'html_links_includerelated', $show_related )
      or return;
    if ($show_related) {    #default is to show 'em; suppress for shortlinks
        $site->store_pref( 'html_morelinks_includerelated', q{} )
          or return;
        foreach my $type (qw(quicktease latest news)) {
            my @elements = $site->get_pref_value("html_${type}_elements");
            $site->store_pref( "html_${type}_includerelated", q{} )
              if @elements == 1;
        }
    }

    ## PAGE FOOTER
    $site->store_pref( 'html_footer_aboutline', aboutus_v1($site) )
      or return;

    ## QUICKTEASE SUPPRESSION
    $site->store_pref( 'html_quicktease_hideonhome',
        quicktease_value('QuickNoHome') )
      or return;
    $site->store_pref( 'html_quicktease_hideonmain',
        quicktease_value('QuickNo') )
      or return;

    ## DETAIL PAGES
    $site->store_pref( 'html_byline_linkto', byline_linkto_v1() )
      or return;
    $site->store_pref( 'html_tools_emailicon', icon_v1('Mail') )
      or return;
    $site->store_pref( 'html_tools_printicon', icon_v1('Print') )
      or return;

    ## IMAGE PREFS
    save_image_prefs($site) or return;

    return 1;
}

sub aboutus_v1 {
    my $site = shift or croak 'aboutus_v1 requires site argument';
    my $footer = q{RawHTML:};
    $footer .= $bmSetup::AboutUsRaw if defined $bmSetup::AboutUsRaw;
    if ($bmSetup::Copyright) {
        my $closer = $site->stash('v1_closer') || q{};
        $footer .= "\n<br$closer>\n" if $bmSetup::AboutUsRaw;
        $footer .= $bmSetup::Copyright;
    }
    undef $bmSetup::AboutUsRaw;
    undef $bmSetup::Copyright;
    return $footer;
}

sub page_titletag_v1 {
    $bmSetup::PageTitleFormat = q{} if !defined $bmSetup::PageTitleFormat;
    return if !$bmSetup::PageTitleFormat;

    my %space_map = (
        q{.} => [q{.}, 'sp'],
        q{:} => ['sp', q{:}, 'sp'],
        q{|} => ['sp', q{|}, 'sp'],
        q{-} => ['sp', q{-}, 'sp'],
        q{o} => ['sp', 'o'],
    );
    my @elements = map { $space_map{$_} ? @{ $space_map{$_} } : $_ }
      split( //ms, $bmSetup::PageTitleFormat );
    undef $bmSetup::PageTitleFormat;
    return \@elements;
}

sub home_titletag_v1 {
    my ( $site_name, $home_line ) = @_;
    my $home_title = q{};
    my @home       = $home_line ? split( /_!!_/ms, $home_line ) : ();
    my %title      = (
        's' => $site_name ? $site_name : q{},
        'k' => $home[3]   ? $home[3]   : q{},
        'd' => $home[2]   ? $home[2]   : q{},
        q{.} => q{: },
        q{:} => q{ :: },
        q{|} => q{ | },
        q{-} => q{ - },
        'o'  => ' (',
        'c'  => ')',
    );
    $bmSetup::HomeTitleFormat = q{} unless defined $bmSetup::HomeTitleFormat;
    my @home_elements = split( //ms, $bmSetup::HomeTitleFormat );
    @home_elements = ( $home_elements[0] )
      unless ( $home_elements[2] && $title{ $home_elements[2] } )
      || @home_elements == 0;
    $home_title = join( q{}, map { $title{$_} } @home_elements );
    undef $bmSetup::HomeTitleFormat;
    return $home_title;
}

sub new_window_v1 {
    my $new_window = make_boolean($bmSetup::NewWinLink)
      || make_boolean($bmSetup::NewWinCode);
    undef $bmSetup::NewWinRelated;
    undef $bmSetup::NewWinLink;
    undef $bmSetup::NewWinCode;
    undef $bmSetup::NewWinStop;

    return $new_window;
}

sub robot_behavior_v1 {
    my $behavior = 'index,follow';
    if ( defined $bmSetup::RobotBehavior
        && index( $bmSetup::RobotBehavior, 'noindex' ) >= 0 )
    {
        $behavior = 'noindex,nofollow';
    }
    undef $bmSetup::RobotBehavior;
    return $behavior;
}

sub quicktease_value {
    my $loc = shift;
    if ( !$loc || ( $loc ne 'QuickNoHome' && $loc ne 'QuickNo' ) ) {
        croak '_quicktease_value requires "QuickNoHome" or "QuickNo" arg';
    }
    my $value = q{};

    no strict 'refs';
    foreach my $type (qw(Spot Long Short)) {
        my $var = "bmSetup::$loc$type";
        $value = '1' if ${$var} && ${$var} eq 'Y';
        undef ${$var};
    }
    return $value;
}

sub byline_linkto_v1 {
    my $type =
      ( $bmSetup::MailMethod && $bmSetup::MailMethod eq 'form' )
      ? 'form'
      : 'email';
    undef $bmSetup::MailMethod;
    return $type;
}

sub icon_v1 {
    my $type = shift;
    if ( !$type || ( $type ne 'Mail' && $type ne 'Print' ) ) {
        croak 'icon_v1 requires type argument ("Mail" or "Print")';
    }
    no strict 'refs';
    my $var = "bmSetup::${type}Icon";
    my $icon = ${$var} || q{};
    undef ${$var};
    $icon = ( $icon =~ m!^<img src="[^"]*/([^"]+)"!ms ? $1 : undef );
    $icon =~ s/^(email|print)/${1}icon/ms if $icon;
    return $icon;
}

sub link_elements_v1 {
    my $type = shift;
    if ( !$type || ( $type ne 'Promo' && $type ne 'Spot' ) ) {
        croak 'link_elements_v1 requires Promo or Spot type argument';
    }
    my $var = "bmSetup::${type}Order";
    no strict 'refs';
    my $elements = ${$var} ? [split( /-/ms, ${$var} )] : undef;
    undef ${$var};
    return $elements;
}

sub alt_elements_v1 {
    my $type = shift;
    if (!$type
        || ($type    ne 'Short'     #not v1 var; used as morelinks placeholder
            && $type ne 'LatestArt'
            && $type ne 'QuickHit'
            && $type ne 'TopNews'
        )
      )
    {
        croak 'alt_elements_v1 requires valid type argument';
    }
    no strict 'refs';
    my $var = "bmSetup::${type}Desc";
    my $value = ${$var} && ${$var} eq 'full' ? undef: ['head'];
    undef ${$var};
    return $value;
}

sub related_links_v1 {
    my $promolinks = make_boolean($bmSetup::PromoRelatedLinks);
    undef $bmSetup::PromoRelatedLinks;
    return $promolinks;
}

sub link_thumb_align_v1 {
    my $align = $bmSetup::ThumbImageAlign || 'above';
    undef $bmSetup::ThumbImageAlign;
    return $align;
}

sub spot_thumb_align_v1 {
    my $align = $bmSetup::SpotImageAlign || 'above';
    undef $bmSetup::SpotImageAlign;
    return $align;
}

sub longlink_thumb_v1 {
    my $longlink_thumb = defined $bmSetup::ThumbIncludeLong
      && $bmSetup::ThumbIncludeLong =~ /y/ims;
    undef $bmSetup::ThumbIncludeLong;
    return $longlink_thumb;
}

sub shortlink_thumb_v1 {
    my $shortlink_thumb = defined $bmSetup::ThumbIncludeShort
      && $bmSetup::ThumbIncludeShort =~ /y/ims;
    undef $bmSetup::ThumbIncludeShort;
    return $shortlink_thumb;
}

my $MAIN_IMAGE_DFLT = 'small';

sub save_image_prefs {
    my $site = shift or croak 'save_image_prefs requires site obj argument';
    my %default_img = BigMed::Media::Image->image_actions($site);
    my %custom_img;
    my $image_dim;

    #spotlight
    if ( $image_dim = image_dimensions_v1('Promo') ) {
        $site->store_pref( 'html_spotlight_imagesize', $image_dim ) or return;
        $custom_img{$image_dim} = 'squeeze' if !$default_img{$image_dim};
    }

    #link thumbnail
    if ( $image_dim = image_dimensions_v1('Thumb') ) {
        $site->store_pref( 'html_link_imagesize', $image_dim ) or return;
        $custom_img{$image_dim} = 'squeeze-crop' if !$default_img{$image_dim};
    }

    #body image
    if ( $image_dim = image_dimensions_v1('Int') ) {
        $site->store_pref( 'html_image_size', $image_dim ) or return;
        $custom_img{$image_dim} = 'squeeze' if !$default_img{$image_dim};
    }

    #main article image
    if ( ( $image_dim = image_dimensions_v1('Article') )
        && !$default_img{$image_dim} )
    {
        $custom_img{$image_dim} = 'squeeze';
    }

    #stow this image dimension for later use (template updates)
    $image_dim ||= $MAIN_IMAGE_DFLT;
    $site->set_stash( 'main_image_size', $image_dim );

    my %image_action = ( %default_img, %custom_img );
    $site->store_pref( 'image_actions', \%image_action ) or return;

    return 1;
}

sub image_dimensions_v1 {
    my $type = shift;
    if (!$type
        || (   $type ne 'Promo'
            && $type ne 'Article'
            && $type ne 'Thumb'
            && $type ne 'Int' )
      )
    {
        croak 'image_dimensions_v1 requires valid type argument';
    }

    my $var = "bmSetup::${type}HTML";
    my ( $height, $width );

    {
        no strict 'refs';
        my $val = ${$var} || q{};
        undef ${$var};
        if ( $val =~ /width="(\d+)"/ms ) {
            $width = $1;
        }
        if ( $val =~ /height="(\d+)"/ms ) {
            $height = $1;
        }
    }

    my $dim =
        ( $width && $height ) ? "${width}x$height"
      : ($width)  ? "${width}x" . ( $width * 2 )
      : ($height) ? "${height}x$height"
      : q{};
    return $dim;
}

# CSS STYLES
# -----------------------------------------------------------------

my %CSS_MAP = (
    'body'                                           => 'all',
    'a'                                              => 'link',
    'h2.bmw_headline'                                => 'headline',
    'h3.bmc_subhead'                                 => 'description',
    'div.bmw_byline'                                 => 'byline',
    'span.bmw_pubdate'                               => 'date',
    'span.bmw_modified'                              => 'date',
    'div.bmc_caption'                                => 'caption',
    'blockquote.bmc_bigPullquote'                    => 'bigpull',
    'blockquote.bmc_smallPullquote'                  => 'smallpull',
    'div.bmw_footer'                                 => 'bottom',
    'div.bmw_breadcrumbs a'                          => 'breadLink',
    'div.bmw_breadcrumbs'                            => 'breadtext',
    'div.bmw_link a.bma_head'                        => 'headLink',
    'span.bma_byline'                                => 'promoByline',
    'div.bmw_link a.bma_section'                     => 'quickLink',
    'div.bmw_quickteaseLinks h3.bma_heading'         => 'quickLink',
    'div.bmw_spotlightLinks div.bmw_link a.bma_head' => 'spotLink',
    'h3.bmw_tips'                                    => 'tiphead',
    'div.bmw_tips_tip'                               => 'tiptext',
    'div.bmw_navigation a'                           => 'navLink',
);

my %CUSTOM_MAP = (    #defunct and non-style-editor styles
    'a.bmw_parentlink'      => 'parentLink',
    'a.bmw_mainsectionlink' => 'parentLink',
    'div.bmw_pagetools a'   => 'toolLink',
    'div.bmc_related a'     => 'relatedLink',
    'span.bma_date'         => 'promoDate',
);

my %ALL_CSS_MAP = ( %CSS_MAP, %CUSTOM_MAP );

sub transfer_v1_styles {
    my ( $self, $site, $old_id ) = @_;
    my $site_id = $site->id;
    $self->css_globals_v1($old_id);
    require BigMed::CSS;
    BigMed::CSS->init_css;

    my $css_all = BigMed::CSS->select( { site => $site_id, section => 0 } );
    my $css;

    #basic default, text and link styles
    foreach my $selector ( keys %CSS_MAP ) {
        my %attr = get_css_attributes($selector);
        if ( $selector eq 'body' && $attr{'background-image'} ) {
            $attr{'background-image'} =
              handle_v1_skin_files( $site, $attr{'background-image'} );
        }
        if ( $selector eq 'div.bmw_quickteaseLinks h3.bma_heading' ) {
            delete $attr{color};
            $attr{'margin-bottom'} = '0px';
        }

        if ( keys %attr ) {
            save_css_entry( $selector, $css_all, \%attr, $site_id ) or return;
        }

        if ( is_css_link($selector) ) {
            my $visited = get_link_color( $selector, 'Visit' );
            if ($visited) {
                save_css_entry(
                    "$selector:visited", $css_all,
                    { color => $visited }, $site_id
                  )
                  or return;
            }
            my $hover = get_link_color( $selector, 'Hover' );
            if ($hover) {
                save_css_entry(
                    "$selector:hover", $css_all,
                    { color => $hover }, $site_id
                  )
                  or return;
            }
        }
    }

    #NON-STANDARD STYLES -----------------------------------
    transfer_v1_nav( $site ) or return;
    transfer_v1_theme_css($site) or return;

    #CLEAN UP GLOBALS --------------------------------------
    undef $bmSetup::NavHighlightColor;
    undef $bmSetup::ButtonPadding;
    undef $bmSetup::IntPullWidth;
    undef $bmSetup::ToolWidth;
    undef $bmSetup::ToolAlign;
    undef $bmSetup::AboutUsAlign;
    undef $bmStyle::StyleInsert;
    undef %bmStyle::font;
    undef %bmStyle::color;
    undef %bmStyle::weight;
    undef %bmStyle::size;
    undef %bmStyle::background;
    undef %bmStyle::bgcolor;
    undef %bmStyle::repeat;

    #generate the css file
    BigMed::CSS->build_sheet($site) or return;
    return 1;
}

sub css_globals_v1 {
    my $self       = shift;
    my $old_id     = shift or croak 'css_globals_v1 requires old id argument';
    my $old_styles =
      bm_file_path( $self->v1_sitedir($old_id), 'templates', 'styles.pl' );
    $old_styles = bm_untaint_filepath($old_styles) or return;
    return do $old_styles;
}

sub is_css_link {
    my $selector = shift;
    my $is_link = substr( $selector, 0, 2 ) eq 'a.'
      || $selector eq 'a'
      || $selector =~ / a([.]\S+)?\z/ms;
    return $is_link;
}

sub get_css_attributes {
    my $selector = shift or croak 'get_css_attributes requires selector arg';
    my $oldid    = $ALL_CSS_MAP{$selector}
      or croak "Unknown css selector $selector";
    my %attr     = (
        'font-family' => $bmStyle::font{$oldid},
        'font-size'   => $bmStyle::size{$oldid},
        'font-weight' => $bmStyle::weight{$oldid},
        'color'       => $bmStyle::color{$oldid},
    );
    my $is_link = is_css_link($selector);

    #headers should always get an explicit size
    if ( $selector =~ /h\d/ms && !$attr{'font-size'} ) {
        $attr{'font-size'} = $bmStyle::size{'all'};
    }

    if ( is_css_link($selector) && $selector ne 'a' ) {
        massage_link_css( $selector, \%attr );
    }

    if (   $selector eq 'blockquote.bmc_bigPullquote'
        || $selector eq 'blockquote.bmc_smallPullquote' )
    {
        $attr{'border-color'}   = $bmStyle::color{ $ALL_CSS_MAP{$selector} };
        $attr{'border-style'}   = 'dotted';
        $attr{'border-width'}   = '4px';
        $attr{'padding-top'}    = '10px';
        $attr{'padding-right'}  = '10px';
        $attr{'padding-bottom'} = '10px';
        $attr{'padding-left'}   = '10px';

        $bmSetup::IntPullWidth =~ s/^width://ms;
        $attr{'width'} = $bmSetup::IntPullWidth;
    }

    if ( $selector eq 'body' ) {
        my $bg = $bmStyle::background{'body'};
        $attr{'background-image'} = "url($bg)"
          if $bg && $bg ne 'http://' && $bg ne 'none';
        $attr{'background-color'}  = $bmStyle::bgcolor{'body'};
        $attr{'background-repeat'} = $bmStyle::repeat{'body'};
    }

    if ( $selector eq 'div.bmw_footer' ) {
        $attr{'text-align'} = $bmSetup::AboutUsAlign;
    }

    my @keys = keys %attr;
    foreach my $k (@keys) {
        delete $attr{$k} if !defined $attr{$k} || $attr{$k} eq q{};
    }

    return %attr;
}

sub massage_link_css {
    my $selector = shift;
    my $rattr    = shift;
    my $old_var  = $ALL_CSS_MAP{$selector};

    if (   $bmStyle::font{$old_var} eq $bmStyle::font{'link'}
        || $bmStyle::font{$old_var} eq q{} )
    {
        delete $rattr->{'font-family'};
        $bmStyle::font{$old_var} = q{};
    }

    if ( $bmStyle::size{$old_var} eq $bmStyle::size{'link'} ) {
        delete $rattr->{'font-size'};
        $bmStyle::size{$old_var} = q{};
    }

    if ( $bmStyle::color{$old_var} eq $bmStyle::color{'link'} ) {
        delete $rattr->{'color'};
        $bmStyle::color{$old_var} = q{};
    }

    if ( $bmStyle::weight{$old_var} eq $bmStyle::weight{'link'} ) {
        delete $rattr->{'font-weight'};
        $bmStyle::weight{$old_var} = q{};
    }

    return;
}

sub get_link_color {
    my ( $selector, $mod ) = @_;

    #regular links should always get the full visited/hover.

    #other links that have same base value as generic link will have an
    #empty link color; those links should get hover or visited only if
    #different than generic link.

    #links with a different base color than generic links should always
    #get the visited/hover color if it's different than the base color

    #all links should get the visited/hover color if it's different
    #than the generic links' visited/hover color

    if ($selector eq 'a'
        || (   $bmStyle::color{ $ALL_CSS_MAP{$selector} }
            && $bmStyle::color{ $ALL_CSS_MAP{$selector} } ne
            $bmStyle::color{ $ALL_CSS_MAP{$selector} . $mod } )
        || ( $bmStyle::color{"link$mod"} ne
            $bmStyle::color{ $ALL_CSS_MAP{$selector} . $mod } )
      )
    {
        return $bmStyle::color{ $ALL_CSS_MAP{$selector} . $mod };
    }

    return;
}

sub transfer_v1_nav {
    my ( $site ) = @_;
    croak 'transfer_v1_nav requires site object'
      if !$site || !ref $site || !$site->isa('BigMed::Site');

    my $site_id = $site->id;

    if ($bmSetup::NavHighlightColor) {

        #navigation: background colors
        save_css_entry(
            'div.bmw_navigation li.bmn_hover, div.bmw_navigation li:hover',
            undef,    #no css_all, or we can get dupes
            { 'background-color' => $bmSetup::NavHighlightColor },
            $site_id,
            'extend',            
          )
          or return;
        save_css_entry(
            'div.bmw_navigation li.bmn_active',
            undef,    #no css_all, or we can get dupes
            { 'background-color' => $bmSetup::NavHighlightColor }, $site_id,
            'extend',            

          )
          or return;
    }
    
    #default background color should always be transparent
    save_css_entry(
        'div.bmw_navigation li',
        undef, #no css_all, or we can get dupes
        { 'background-color' => 'transparent' },
        $site_id,
            'extend',            
      )
      or return;

    if ( defined $bmSetup::ButtonPadding ) {

        #navigation: button padding
        my $pad = $bmSetup::ButtonPadding . 'px';
        save_css_entry(
            'div.bmw_navigation a',
            undef, #no css_all, or we can get dupes
            {   'padding-left'   => $pad,
                'padding-right'  => $pad,
                'padding-top'    => $pad,
                'padding-bottom' => $pad,
            },
            $site_id,
            'extend',            
          )
          or return;
    }

    return 1;
}

sub transfer_v1_theme_css {
    my $site = shift;
    croak 'transfer_v1_theme_css requires site object'
      if !$site || !ref $site || !$site->isa('BigMed::Site');

    my $pixels;
    if ( $bmSetup::ToolWidth =~ /(\d+)px/ms ) {
        $pixels = $1;
    }
    else {
        $pixels = 200;
    }
    my $align_width = ( $pixels - 10 ) . 'px';

    my $theme = <<"TOOL_PANEL_ALL";
/* STYLES FOR SITES IMPORTED FROM BIG MEDIUM v1 ----------------- */

/* v1 toolpanel styles */
div.bmv1ToolPanel {
    margin-top: 1em;
    text-align: left;
    margin: 0px auto;
    $bmSetup::ToolWidth;
TOOL_PANEL_ALL

    if ( $bmSetup::ToolAlign eq 'left' ) {
        $theme .= <<"TOOL_PANEL_LEFT";
    padding: 1em 10px 5px 0px;
    float: left;
    voice-family:"\\"}\\"";voice-family:inherit;
    width: $align_width;
TOOL_PANEL_LEFT
    }
    elsif ( $bmSetup::ToolAlign eq 'right' ) {
        $theme .= <<"TOOL_PANEL_RIGHT";
    padding: 1em 0px 5px 10px;
    float: right;
    voice-family:"\\"}\\"";voice-family:inherit;
    width: $align_width;
TOOL_PANEL_RIGHT
    }
    else {
        $theme .= <<'TOOL_PANEL_CENTER';
    margin: 0px auto;
    padding: 1em 0px 0px 0px;
TOOL_PANEL_CENTER
    }

    $theme .= <<'BOTTOM_TOOL_CSS';
}
div.bmv1ToolPanel div.bmc_image {
    padding: 0;
}

/*v1 bottomtools styles */
div.bmv1BottomTools {
    clear:both;
    overflow:hidden;
    zoom:1;
    width:100%;
}
div.bmv1BotRelated {
    float:left;
    width:45%;
    text-align:left;
}
div.bmv1BotEmailPrint {
    float:right;
    width:45%;
    text-align:right;
}
BOTTOM_TOOL_CSS

    if ( $site->stash( 'has_browsemore', 1 ) ) {
        $theme .= <<'BROWSEMORE_STYLES';

/* update sections styles to approximate v1 browsemore */
div.bmw_sections div.bmw_link {
    margin:0;
}
BROWSEMORE_STYLES
    }

    if ( $bmSetup::AboutUsAlign eq 'right' ) {
        $theme .= <<'BIGMEDIUM_ALIGN';

/* alignment of bigmedium from v1 */
a.bmw_bigmedium { margin-left: auto; margin-right: 0; }
BIGMEDIUM_ALIGN
    }
    elsif ( $bmSetup::AboutUsAlign eq 'center' ) {
        $theme .= <<'BIGMEDIUM_ALIGN';

/* alignment of bigmedium from v1 */
a.bmw_bigmedium { margin:0 auto; }
BIGMEDIUM_ALIGN
    }

    $theme .= <<"IMAGENAV_STYLES";

/* v1 imagenav widgets */
.bmv1_imagenav img.bmv1-imgroll,
.bmv1_imagenav a:hover img.bmv1-imgnav,
.bmv1_imagenav a.bmv1-ACTIVE img.bmv1-imgnav {
    display: none;
}
.bmv1_imagenav a:hover img.bmv1-imgroll,
.bmv1_imagenav a.bmv1-ACTIVE img.bmv1-imgroll,
.bmv1_imagenav img.bmv1-imgnav,
.bmv1_imagenav a:hover {
    display: inline;
    border: none; /*ie needs this */
}
IMAGENAV_STYLES

    foreach my $selector ( keys %CUSTOM_MAP ) {
        my %attr = get_css_attributes($selector);
        if ( keys %attr ) {
            $theme .= "\n$selector {";
            foreach my $a ( keys %attr ) {
                next if !defined $attr{$a} || $attr{$a} eq q{};
                $theme .= "\n    $a: $attr{$a};";
            }
            $theme .= "\n}\n";
        }

        if ( is_css_link($selector) ) {
            my $visited = get_link_color( $selector, 'Visit' );
            $theme .= "\n$selector:visited { color: $visited }\n" if $visited;
            my $hover = get_link_color( $selector, 'Hover' );
            $theme .= "\n$selector:hover { color: $hover }\n" if $hover;
        }
    }

    my $custom_css = $bmStyle::StyleInsert || q{};
    if (index($custom_css, 'localnews-img')) { #derivative of news skin
        $theme .= "\ndiv.bmn_vsubnav {\n    width: 98\%;\n}"; #for ie subnav
    }

    #news theme has a p style that can screw up some v2 spacing
    $custom_css =~ s/^p\s+\{\s+margin: 1em 0 0 0;\s+\}\s+//msg;

    $custom_css = handle_v1_skin_files( $site, $custom_css );
    $custom_css = update_v1_custom_styles($custom_css);
    $theme .= "\n\n/* START ORIGINAL CUSTOM STYLES FROM V1 SITE "
      . "----------------- */\n$custom_css";

    my $theme_obj = BigMed::Theme->new();
    return $theme_obj->save_site_css( $site, $theme );
}

sub save_css_entry {
    my ( $selector, $all_css, $rattributes, $site_id, $extend ) = @_;

    $all_css ||= 'BigMed::CSS';
    my $css = $all_css->fetch( { selector => $selector, site => $site_id } );
    return if !defined $css;
    $css ||= BigMed::CSS->new();

    $css->set_selector($selector);
    $css->set_site($site_id);
    $css->set_section('0');
    my %attributes =
      $extend ? ( $css->attributes, %{ $rattributes } ) : %{ $rattributes };
    $css->set_attributes(\%attributes);
    return $css->save;
}

sub handle_v1_skin_files {    #copy skin directories if we can sort it out
    my ( $site, $html_or_css ) = @_;
    my $htmldir = $site->html_dir or return $html_or_css;

    my $regex_start = q{(https?://[^"\)]+?|\+\+ADMINDIRURL\+\+)/tempimg/};
    my $demo_regex  = qq{$regex_start(1demo-img)/};
    my $news_regex  = qq{$regex_start(localnews-img)/};

    foreach my $regex ( $demo_regex, $news_regex ) {
        if ( $html_or_css =~ m{$regex}ms ) {
            my $bmadmin  = $1;
            my $skindir  = $2;
            my $themedir = bm_file_path( $htmldir, 'bm.theme' );
            my $test_file = bm_file_path($themedir, 'bmBig.gif');
            if ( !-e $test_file ) {
                my $path = bm_file_path( BigMed->bigmed->env('MOXIEDATA'),
                    'support', 'v1assets', $skindir );
                next if !-e $path;
                bm_copy_dir( $path, $themedir, { build_path => 1 } ) or next;
            }

            my $theme_url;
            if ( $bmadmin =~ s{\Ahttps?://[^/]+}{}ms ) {    #full url in css
                $theme_url = 'bm.theme';
            }
            elsif ( $bmadmin =~ m{\A\+\+ADMINDIRURL\+\+}ms ) {    #template
                $theme_url = '<%pagedirurl%>/bm.theme';
            }
            $html_or_css =~ s{$regex}{$theme_url/}msg;
        }
    }
    return $html_or_css;
}

#css changes; run in reverse sort order to do div|td first
my %CSS_UPDATE = (
    '(div|td).bmNavigation' => 'div.bmw_navigation li',
    '(div|td).bmActiveNav'  =>
      "div.bmw_navigation li.bmn_hover,\ndiv.bmw_navigation li:hover,\n"
      . 'div.bmw_navigation li.bmn_active',
    '(\S*)[.]bmToolLink'                => 'div.bmw_pagetools a',
    '(\S*)[.]bmTool(Left|Right|Center)' => 'div.bmv1ToolPanel',
    '(\S*)[.]bmTipText'                 => 'div.bmw_tips_tip',
    '(\S*)[.]bmTipHead'                 => 'h3.bmw_tips',
    '(\S*)[.]bmSubhead'                 => 'h3.bmc_subhead',
    '(\S*)[.]bmSpotHeadline'            =>
      'div.bmw_spotlightLinks div.bmw_link a.bma_head',
    '(\S*)[.]bmSmallPull'      => 'blockquote.bmc_smallPullquote',
    '(\S*)[.]bmRightImage'     => 'div.bmc_rightContentImage img',
    '(\S*)[.]bmRelatedLink'    => 'div.bmc_related a',
    '(\S*)[.]bmQuickHead'      => 'div.bmw_link a.bma_section',
    '(\S*)[.]bmPullRight'      => 'blockquote.bmc_rightPullquote',
    '(\S*)[.]bmPullLeft'       => 'blockquote.bmc_leftPullquote',
    '(\S*)[.]bmPullCenter'     => 'blockquote.bmc_centerPullquote',
    '(\S*)[.]bmpromoDate'      => 'span.bma_date',
    '(\S*)[.]bmpromoByline'    => 'span.bma_byline',
    '(\S*)[.]bmParent'         => '.bmw_parentlink, .bmw_mainsectionlink',
    '(\S*)[.]bmNavigation'     => 'div.bmw_navigation a',
    '(\S*)[.]bmLinkright'      => 'img.bma_rightthumb',
    '(\S*)[.]bmLinkleft'       => 'img.bma_leftthumb',
    '(\S*)[.]bmLeftImage'      => 'div.bmc_leftContentImage img',
    '(\S*)[.]bmHeadlineLink'   => 'div.bmw_link a.bma_head',
    '(\S*)[.]bmCenterImage'    => 'div.bmc_centerContentImage img',
    '(\S*)[.]bmCaption'        => 'div.bmc_caption',
    '(\S*)[.]bmBreadcrumbText' => 'div.bmw_breadcrumbs',
    '(\S*)[.]bmBreadcrumbs'    => 'div.bmw_breadcrumbs a',
    '(\S*)[.]bmBottomText'     => 'div.bmw_footer',
    '(\S*)[.]bmBigPull'        => 'blockquote.bmc_bigPullquote',
    '(\S*)[.]bmarticleDate'    => 'span.bmw_pubdate',
    '(\S*)[.]bmarticleByline'  => 'div.bmw_byline',
    '(\S*)[.]bmActiveNav'      =>
      "div.bmw_navigation li.bmn_hover,\ndiv.bmw_navigation li:hover,\n"
      . 'div.bmw_navigation li.bmn_active',
);

my @V1_CSS = sort { $b cmp $a } keys %CSS_UPDATE;

sub update_v1_custom_styles {
    my $css = shift;
    foreach my $sel (@V1_CSS) {
        $css =~ s/$sel/$CSS_UPDATE{$sel}/msig;
    }
    $css =~ s/(^|,)\s*h1(\s*[,{])/${1}h1, h2.bmw_headline$2/msig;

    return $css;
}

# SECTIONS
# -----------------------------------------------------------------

sub import_sections {
    my ( $self, $site, $old_siteid, $debug_stop ) = @_;
    croak 'import_sections requires site object in first argument'
      if ref $site ne 'BigMed::Site';
    croak 'import_sections requires old site id in second argument'
      if !$old_siteid;

    my $sdir     = $self->v1_sitedir($old_siteid);
    my @sections =
      bm_load_file( bm_file_path( $sdir, 'templates', 'Sections.setup' ) );
    if (!@sections) { #totally empty; fake a homepage
        @sections = ('h_!!_Home');
    }

    my $sitename = $site->name;
    my @parents;
    my $depth;
    my $secnum         = 0;
    my $totalnum       = scalar @sections;
    my $sub_numdisplay = $site->get_pref_value('html_overflow_numdisplay');
    foreach my $secline (@sections) {
        $secnum++;
        last if $debug_stop && $secnum > $debug_stop;
        next unless $secline;
        my $section = $self->fetch_v1_section( $secline, $site ) or return;

        $self->{statusbar}->update_status(
            progress => 3 + $secnum,
            message  => ['IMPORTV1_Sections', $sitename, "$secnum/$totalnum"],
          )
          if $self->{statusbar};

        #set parents and update parent's kids list
        $depth =
            $section->is_homepage ? 0
          : index( $section->stash('old_id'), q{_} ) < 0 ? 1
          : 2;
        $parents[$depth] = $section->id;
        if ( $depth > 0 ) {

            #if already have parents, that means we're resuming a previous
            #import; can skip parent assignment
            my @gotp = $section->parents;
            if ( !@gotp ) {
                $section->set_parents( [@parents[0 .. $depth - 1]] );
                $section->save or return;
            }

            #even though sections save themselves into parents automatically
            #on save, it won't be updated in the section object within
            #site cache; so do it here to make sure it's captured in the
            #site object, otherwise sections won't get built.
            #BUT... have to make sure that we don't double up (which can
            #happen if we're resuming an import).
            my $parent = $site->section_obj_by_id( $parents[$depth - 1] );
            my %sib = map { $_ => 1 } $parent->kids;
            if ( !$sib{ $section->id } ) {
                $parent->set_kids( [$parent->kids, $section->id] );
                $parent->save or return;
            }

            if ( $depth > 1 ) {    #subsection, set number of links
                $site->store_pref( 'html_links_numdisplay', $sub_numdisplay,
                    $section->id )
                  or return;
            }
        }
        $self->update_section_map( $section, $old_siteid ) or return;
        my $tag = $self->log_data_tag($section);
        $self->log( info => "ImportV1: Imported and saved section $tag" );
    }
    if ($bmSetup::TopNewsSection) {
        my $news_sec = $self->{section_map}->{$bmSetup::TopNewsSection};
        $site->store_pref( 'html_news_section', $news_sec ) or return;
    }
    return 1;
}

my %SEC_FLAG = (
    'h' => 'html_nohome',
    'n' => 'html_nonav',
    'm' => 'html_noparent',
    's' => 'nofeed',
    'p' => 'nospot',
);

sub fetch_v1_section {
    my ( $self, $secline, $site ) = @_;
    croak 'load_v1_section requires a site object'
      if ref $site ne 'BigMed::Site';

    my @data = split( /_!!_/ms, $secline );
    my $name =
      index( $data[1], '>' ) >= 0
      ? ( split( />/ms, $data[1] ) )[1]
      : $data[1];

    #create the section object
    my $old_id = $data[0];
    my $section;
    if ( $self->{section_map}->{$old_id} ) {    #make sure that we have it
        $section =
          $site->section_obj_by_id( $self->{section_map}->{$old_id} );
        return if !defined $section;
        if ($section) {
            $section->set_stash( 'old_id' => $old_id );
            stash_section_navimages( $site, $section, @data ) or return;
            return $section;
        }
    }
    $section = BigMed::Section->new();
    my $site_id = $site->id;

    #get homepage status and add slug/id info to parent section object
    if ( $old_id eq 'h' ) {
        homepage_settings_v1( $section, $site ) or return;
    }
    else {
        $section->set_name($name);
        $section->set_active( make_boolean( $data[4] ) );
        $section->set_slug( $data[5] );
        $site->add_section($section) or return;
        _make_field_unique($section, 'slug') or return;
    }
    stash_section_navimages( $site, $section, @data ) or return;

    #gather flags
    my $suppress_string = $data[6] || q{};
    my %flags;
    foreach my $f ( split( //ms, $suppress_string ) ) {
        $flags{ $SEC_FLAG{$f} } = 1 if $SEC_FLAG{$f};
    }
    if ( $flags{nofeed} ) {
        delete $flags{nofeed};
        $flags{'rss_disable_feed'} = 1;
        $flags{'js_disable_feed'}  = 1;
    }
    if ( $flags{nospot} ) {
        delete $flags{nospot};
        $site->store_pref( 'html_spotlight_numdisplay', 0, $section->id );
    }

    #update the remaining info, except parents/kids handled w/load_sections_v1
    $section->set_alias( $data[15] ) if $data[15];
    $section->set_flags( \%flags );

    #stash the section map and other id info
    $section->set_stash( 'old_id' => $old_id );
    $section->save or return;

    #update page info
    my $page = $section->section_page_obj or return;
    $page->set_meta_description( $data[2] );
    $page->set_meta_keywords( $data[3] );
    $page->save or return;

    return $section;
}

sub homepage_settings_v1 {
    my ( $section, $site ) = @_;
    croak 'homepage_settings_v1 requires section object'
      if ref $section ne 'BigMed::Section';
    croak 'homepage_settings_v1 requires site object'
      if ref $site ne 'BigMed::Site';

    $section->make_homepage;
    $section->set_name( $bmSetup::HomeLabel || 'Home' );
    $section->set_active(1);
    $site->add_section($section) or return;    #have to add now to save prefs

    #homepage html_links_numdisplay gets defined only if diff than site's
    my $home_id = $section->id;
    my $link_display = $site->get_pref_value('html_links_numdisplay') || 0;
    my $custom_link;
    if ( defined $bmSetup::HomeNumToDisplay
        && $link_display != $bmSetup::HomeNumToDisplay )
    {
        $site->store_pref( 'html_links_numdisplay',
            $bmSetup::HomeNumToDisplay, $home_id )
          or return;
        $custom_link = 1;
    }

    #homepage html_morelinks_numdisplay gets defined only if there is a
    #different custom link or a different sitewide morelinks
    my $morelink_display = $site->get_pref_value('html_morelinks_numdisplay');
    my $home_short       = $bmSetup::HomeShortToDisplay;
    my $custom_short;
    if ( defined $home_short ) {
        $custom_short =
          ( $custom_link && $home_short != $bmSetup::HomeNumToDisplay )
          || $link_display != $home_short;
    }

    if ($custom_short) {
        $site->store_pref( 'html_morelinks_numdisplay', $home_short,
            $home_id )
          or return;
    }

    undef $bmSetup::HomeNumToDisplay;
    undef $bmSetup::HomeShortToDisplay;
    undef $bmSetup::HomeNewsDesc;    #not relevant in v2
    undef $bmSetup::HomeLabel;

    return 1;
}

sub stash_section_navimages {
    my ( $site, $section, @data ) = @_;

    #horizontal
    _do_navstash( $site, $section, 'h', 'nav', $data[8], $data[7] ) or return;
    if ( $section->stash('v1_hnav') ) {
        _do_navstash( $site, $section, 'h', 'roll', $data[10], $data[9] )
          or return;
    }

    #vertical
    _do_navstash( $site, $section, 'v', 'nav', $data[12], $data[11] )
      or return;
    if ( $section->stash('v1_vnav') ) {
        _do_navstash( $site, $section, 'v', 'roll', $data[14], $data[13] )
          or return;
    }
    return 1;
}

sub _do_navstash {
    my ( $site, $section, $direction, $type, $url, $file ) = @_;
    my $stash_name = "v1_$direction$type";
    if ($url) {
        $section->set_stash( $stash_name, $url );
    }
    elsif ($file) {
        my $v1_path = bm_file_path( $site->stash('v1_images'), $file );
        my $suffix = $file =~ /.*([.][^.]+)$/ ? $1 : q{};
        my $slug = defined $section->slug ? $section->slug : '__HOME';
        my $dot = BigMed->bigmed->env('DOT');
        my $v2_file = "$stash_name${dot}sec-$slug$suffix";
        my $v2_path = bm_file_path( $site->html_dir, 'bm.theme', $v2_file );
        if ( -e $v1_path ) {
            bm_copy_file( $v1_path, $v2_path, { build_path => 1 } ) or return;
            $section->set_stash( $stash_name,
                "<\%pagedirurl\%>/bm.theme/$v2_file" );
        }
    }
    return 1;
}

# TEMPLATE UPDATES
# -----------------------------------------------------------------

my %WIDGET_MAP = (

    #mainimage and toolpanel are set in import_templates

    'HTMLHEAD'       => '<%htmlhead%>',
    'HTMLEND'        => '</html>',
    'ABOUTUS'        => qq{<\%footer\%>\n<\%bigmedium\%>\n<\%sitemap\%>},
    'HNAV'           => '<%navigation direction="horizontal"%>',
    'VNAV'           => '<%navigation direction="vertical"%>',
    'HSUBNAV'        => '<%subnavigation direction="horizontal" main="1"%>',
    'VSUBNAV'        => '<%subnavigation direction="vertical" main="1"%>',
    'BREADCRUMBS'    => '<%breadcrumbs%>',
    'PULLDOWN'       => '<%pulldown%>',
    'MOREMAIN'       => '<%overflow%>',
    'SITENAMELINK'   => '<%sitenamelink%>',
    'HOMELINK'       => '<%homelink%>',
    'PARENTLINK'     => '<%parentlink%>',
    'MAINPARENTLINK' => '<%mainsectionlink%>',
    'SITENAME'       => '<%sitename%>',
    'HEADLINE'       => '<%headline%>',
    'TITLE'          => '<%title%>',
    'DESCRIPTION'    => '<%description%>',
    'PARENTNAME'     => '<%parentname%>',
    'MAINPARENTNAME' => '<%mainsectionname%>',
    'SPOTLIGHT'      => '<%spotlight%>',
    'SPOTLIGHTTEXT'  => '<%spotlighttext%>',
    'SPOTLIGHTIMAGE' => '<%spotlightimage%>',
    'LONGLINKS'      => '<%links%>',
    'SHORTLINKS'     => '<%morelinks%>',
    'INDEX'          => qq{<\%links\%>\n<\%overflow\%>},
    'LATEST'         => '<%latest%>',
    'CONTENTS'       => '<%content%>',
    'BYLINE'         => '<%byline%>',
    'AUTHORNAME'     => '<%authorname%>',
    'AUTHOREMAIL'    => '<%authoremail%>',
    'AUTHORLINK'     => '<%authorlink%>',
    'PUBDATE'        => '<%pubdate%>',
    'BYLINEDATE'     => qq{<\%byline\%>\n<p><\%pubdate\%></p>},
    'MAINIMAGETEXT'  => q{ },
    'EMAILPAGE'      => '<%emailpage%>',
    'PRINTPAGE'      => '<%printpage%>',
    'LINETOOLS'      => '<%pagetools%>',
    'RELATED'        => '<%related%>',
    'TOPNEWS'        => '<%news%>',
    'HOMEPAGENEWS'   => '<%news%>',
    'TIP'            => '<%tips%>',
    'ANNOUNCE'       => '<%announcements%>',
    'NEWSFEED'       => '<%feeds%>',
    'URL'            => '<%url%>',
    'HOMEDIRURL'     => '<%homedirurl%>',
    'PAGEDIRURL'     => '<%pagedirurl%>',
    'HOMEDIRPATH'    => '<%homedirpath%>',
    'PAGEDIRPATH'    => '<%pagedirpath%>',
    'ARTICLESLUG'    => '<%pageslug%>',
    'SECTIONSLUG'    => '<%sectionslug%>',
    'PARENTSLUG'     => '<%parentslug%>',
    'PARENTURL'      => '<%parenturl%>',
    'MAINPARENTSLUG' => '<%mainsectionslug%>',
    'MAINPARENTURL'  => '<%mainsectionurl%>',
    'ADMINDIRURL'    => '<%admindirurl%>',
    'BOTTOMTOOLS'    =>
      '<div class="bmv1BottomTools"><div class="bmv1BotRelated"><%related%>'
      . '</div><div class="bmv1BotEmailPrint"><%pagetools%></div></div>',
);

my @V1_TEMPLATES = qw( .template _0.template sub.template ind.template );

sub import_templates {
    my ( $self, $site, $old_siteid ) = @_;
    croak 'import_templates requires site object'
      if ref $site ne 'BigMed::Site';
    croak 'import_templates requires old site id' if !$old_siteid;

    my $v1_dir = bm_file_path( $self->v1_sitedir($old_siteid), 'templates' );
    my %section_map = %{ $self->{section_map} };
    my $tmpl_obj    = BigMed::Template->new($site);

    #sort out main image and toolpanel settings for the site
    my $main_img = $site->stash('main_image_size') || $MAIN_IMAGE_DFLT;
    my $main_wi =
        '<%images limit="other" size="'
      . $main_img
      . '" caption="1" direction="vertical"%>';
    my $toolpanel = q{<div class="bmv1ToolPanel">};
    $toolpanel .= "\n$main_wi\n"
      if index( $bmSetup::ToolOrder, 'image' ) >= 0;
    $toolpanel .= "\n<\%pagetools\%>\n"
      if index( $bmSetup::ToolOrder, 'tool' ) >= 0;
    $toolpanel .= "\n<\%related\%>\n"
      if index( $bmSetup::ToolOrder, 'links' ) >= 0;
    $toolpanel .= '</div>';
    $WIDGET_MAP{TOOLPANEL} = $toolpanel;
    $WIDGET_MAP{MAINIMAGE} = $main_wi;
    undef $bmSetup::ToolOrder;

    #cope with automatic addition of overflow links to browsemore
    if ($bmSetup::AddMoreMain) {
        $WIDGET_MAP{BROWSEMORE} =
          qq{<\%overflow\%>\nBrowse more...\n<\%sections main="1"\%>};
        $site->set_stash( 'has_browsemore', 1 );
    }
    else {
        $WIDGET_MAP{BROWSEMORE} = qq{Browse more...\n<\%sections main="1"\%>};
    }
    undef $bmSetup::AddMoreMain;

    #do homepage template
    my $home =
      update_v1_template( $site, bm_file_path( $v1_dir, 'home.template' ) );
    if ($home) {
        $home = update_imagenav( $home, $site, $site->homepage_id );
        $tmpl_obj->save_template(
            text   => $home,
            format => 'HTML',
            type   => 'home',
          )
          or return;
    }

    my ( $section_dflt, $page_dflt, $utility_dflt );
    my %default_sub;

    foreach my $old_sec ( sort keys %section_map ) {
        next if $old_sec eq 'h';    #already got homepage
        foreach my $suffix (@V1_TEMPLATES) {

            #load and update the v1 template, and skip it if not found
            my $template =
              update_v1_template( $site,
                bm_file_path( $v1_dir, "$old_sec$suffix" ) );
            $template &&= update_imagenav( $template, $site,
                $self->{section_map}->{$old_sec} );

            my $parent = $old_sec =~ /(\d+)_/ms ? $1 : undef;
            if (   !$template
                && $suffix eq '.template'
                && $parent )
            {
                $template = $default_sub{$parent};
            }
            next if !$template;
            my $type = $suffix eq 'sub.template' ? 'page' : 'section';

            #skip it if it already matches the current inherited template
            my $can_inherit = $type eq 'page'
              || $suffix eq '.template'       #subsection page
              || $suffix eq '_0.template';    #main section page
            if ($can_inherit) {
                my $default = $tmpl_obj->template_text(
                    section => $self->{section_map}->{$old_sec},
                    format  => 'HTML',
                    type    => $type,
                );
                return if !defined $default;

                #ignore white space for the purpose of the test
                $default =~ s/\s+//msg;
                ( my $test = $template ) =~ s/\s+//msg;
                next if $test eq $default;
            }

            #handle default templates (main section templates only)
            if ( !$parent && $suffix eq 'ind.template' ) {    #default sub
                $default_sub{$old_sec} = $template;
                $utility_dflt =
                  ( save_utility_template( $tmpl_obj, $template ) || return )
                  if !$utility_dflt;
                next;    #ind.template doesnt' get saved for main sections
            }
            elsif ( $suffix eq '_0.template' && !$section_dflt ) {   #main sec
                $section_dflt = 1;
                $tmpl_obj->save_template(
                    text   => $template,
                    format => 'HTML',
                    type   => 'section',
                  )
                  or return;
                next;    #don't save custom, it's the default
            }
            elsif ( !$parent && $type eq 'page' && !$page_dflt ) {
                $page_dflt = 1;
                $tmpl_obj->save_template(
                    text   => $template,
                    format => 'HTML',
                    type   => 'page',
                  )
                  or return;
                next;    #don't save custom, it's the default
            }

            #looks good, save it...
            if ( $parent || $suffix ne 'ind.template' ) {
                $tmpl_obj->save_template(
                    text    => $template,
                    section => $self->{section_map}->{$old_sec},
                    format  => 'HTML',
                    type    => $type,
                  )
                  or return;
            }
        }
    }
    return 1;
}

sub save_utility_template {
    my ( $tmpl_obj, $template ) = @_;
    my $email = $template;
    my $em    = qq{<\%description\%>\n<\%emailform\%>};
    $email =~ s/\Q$WIDGET_MAP{INDEX}\E/$em/ms;
    $tmpl_obj->save_template(
        text   => $email,
        format => 'HTML',
        type   => 'tool_email',
      )
      or return;

    my $feed = $template;
    my $fd   =
        qq{<\%feedintro\%>\n<\%fullfeedlink\%>\n<\%podcastlink\%>\n}
      . qq{<%newsgadget%>\n<%sectionfeeds%>};
    $feed =~ s/\Q$WIDGET_MAP{INDEX}\E/$fd/ms;
    $feed =~ s/<\%headline\%>/<\%feedtitle\%>/msg;
    $tmpl_obj->save_template(
        text   => $feed,
        format => 'HTML',
        type   => 'tool_feeds',
      )
      or return;

    my $utility = $template;
    $utility =~ s/\Q$WIDGET_MAP{INDEX}\E/<\%content\%>/ms;
    $tmpl_obj->save_template(
        text   => $utility,
        format => 'HTML',
        type   => 'utility',
      )
      or return;

    return 1;
}

my %TMPL_CLASS_UPDATE = (
    'bmParent'         => 'bmw_parentlink',
    'bmCaption'        => 'bmc_caption',
    'bmBreadcrumbText' => 'bmw_breadcrumbs',
);

sub update_v1_template {
    my ( $site, $path ) = @_;
    my $template = join( "\n", bm_load_file($path) );
    $template =~ s/\A\s+//ms;
    $template =~ s/\s+\z//ms;
    return if !$template;

    $template = handle_v1_skin_files( $site, $template );
    foreach my $class ( keys %TMPL_CLASS_UPDATE ) {
        $template =~ s/$class/$TMPL_CLASS_UPDATE{$class}/msg;
    }
    $template =~ s/\+\+([^+]+)\+\+/$WIDGET_MAP{$1} || "++$1++"/msge;

    #quicktease needs extra help
    $template =~ s/\+\+QUICKTEASE:([^+]+)\+\+/<\%quicktease slug="$1"\%>/msg;

    return $template;
}

sub update_imagenav {
    my ( $template, $site, $sec_id ) = @_;
    my $section;
    foreach my $wi (qw(HNAV VNAV HSUBNAV VSUBNAV)) {
        if ( index( $template, "++${wi}IMAGE++" ) >= 0 ) {
            $section = $site->section_obj_by_id($sec_id) if !$section;
            my $html = _imagenav_html( $wi, $site, $section ) || q{};
            $template =~ s/\+\+${wi}IMAGE\+\+/$html/msg;
        }
    }
    return $template;
}

sub _imagenav_html {
    my ( $type, $site, $section ) = @_;

    my @parents = $section->parents;
    my $main;
    if ( !$parents[0] ) {    #homepage
        $main = $section;    #homepage
    }
    else {
        shift @parents;      #get rid of home
        $main =
          $parents[0] ? $site->section_obj_by_id( $parents[0] ) : $section;
    }
    my ( $html, $active_slug );
    if ( $type eq 'HNAV' || $type eq 'VNAV' ) {
        my @ids = $site->homepage_obj->kids;
        unshift @ids, $site->homepage_id
          if $site->get_pref_value('html_navigation_includehome');
        $site->set_stash( $type, _build_imagenav_html( $site, $type, @ids ) )
          if !$site->stash($type);
        $html = $site->stash($type);
        $active_slug = defined $main->slug ? $main->slug : '__HOME';
    }

    else {
        return q{} if $main->is_homepage;
        $main->set_stash( $type,
            _build_imagenav_html( $site, $type, $main->kids ) )
          if !$main->stash($type);
        $html = $main->stash($type);
        $active_slug = defined $section->slug ? $section->slug : '__HOME';
    }
    $html =~ s/(bmv1nav-\Q$active_slug\E)"/$1 bmv1-ACTIVE"/msg;
    return $html;
}

sub _build_imagenav_html {
    my ( $site, $type, @sec_ids ) = @_;
    my ( $div, $nav_stash, $roll_stash );
    my $closer = $site->stash('v1_closer') || q{};
    if ( index( $type, 'H' ) == 0 ) {
        $div        = q{};
        $nav_stash  = 'v1_hnav';
        $roll_stash = 'v1_hroll';
    }
    else {
        $div        = qq{<br$closer>};
        $nav_stash  = 'v1_vnav';
        $roll_stash = 'v1_vroll';
    }
    my $parentslug;
    my @images;
    foreach my $sid (@sec_ids) {
        my $sec = $site->section_obj_by_id($sid) or next;
        next if !$sec->active;
        my $nav = $sec->stash($nav_stash)        or next;
        my $roll = $sec->stash($roll_stash);
        my $url;
        if ( $sec->is_homepage ) {
            $url = '<%homedirurl%>/index.shtml';
        }
        elsif ( $sec->alias ) {
            $url = $sec->alias;
        }
        else {
            my @parents = $sec->parents;
            my $slugdex = $sec->slug . '/index.shtml';
            $url =
              $parents[1]
              ? '<%pagedirurl%>/<%mainsectionslug%>/' . $slugdex
              : '<%pagedirurl%>/' . $slugdex;
        }
        my $img_html;
        my $name = $sec->name;
        if ($roll) {
            $img_html =
                qq{<img src="$roll" alt="$name" class="bmv1-imgroll"$closer>}
              . qq{<img src="$nav" alt="$name" class="bmv1-imgnav"$closer>};
        }
        else {
            $img_html = qq{<img src="$nav" alt="$name"$closer>};
        }

        my $slug = defined $sec->slug ? $sec->slug : '__HOME';
        push @images, qq{<a href="$url" class="bmv1nav-$slug">$img_html</a>};
    }
    return '<span class="bmv1_imagenav">' . join( $div, @images ) . '</span>';
}

# IMPORT ARTICLES/PAGES
# -----------------------------------------------------------------

sub import_pages {
    my ( $self, $site, $old_siteid, $debug_stop ) = @_;
    my $index_path = bm_file_path( $self->v1_sitedir($old_siteid),
        'articles', 'masterindex.cgi' );

    #load all relationships for all subtypes
    foreach my $sub ( undef, BigMed::Content::Page->content_subtypes() ) {
        foreach my $rel ( BigMed::Content::Page->data_relationships($sub) ) {
            BigMed::Content::Page->load_related_classes( $rel, $sub );
        }
    }

    $self->load_page_map($old_siteid);
    my $progress = $self->{statusbar} ? $self->{statusbar}->progress + 1 : 0;
    my $sitename = $site->name;

    my @articles = bm_load_file($index_path);
    my $totalnum = scalar @articles;
    my $pagenum  = 0;
    foreach my $line (@articles) {
        $pagenum++;
        return 1 if $debug_stop && $pagenum > $debug_stop;
        next if !$line;
        my $v1_id = ( split( /_!!_/ms, $line ) )[0];
        next if $self->{page_map}->{$v1_id};
        my $page = $self->fetch_v1_article( $site, $old_siteid, $v1_id );
        return if !defined $page;
        next if !$page;

        #update logs and statusbars
        $self->{statusbar}->update_status(
            progress => $progress + $pagenum,
            message  => ['IMPORTV1_Pages', $sitename, "$pagenum/$totalnum"],
          )
          if $self->{statusbar};
        $self->update_page_map( $old_siteid, $v1_id, $page->id ) or return;
        $self->log( info => "ImportV1: v1 page $v1_id imported as page "
              . $page->id );
        $self->update_lockfile("Page $v1_id imported for site $old_siteid")
          or return;
    }
    $self->trash_page_map($old_siteid) or return;
    return 1;
}

my %FLAG_MAP = (
    'h' => 'html_nohome',
    'p' => 'html_nospothome',
    'n' => 'html_nonews',
    'q' => 'html_noqt',
    'm' => 'html_nomain',
    'f' => 'html_nospotsec',
    's' => 'html_nosec',
);

my @NAME_SUFFIX = qw(sr[.]? jr[.]? md m[.]d[.] phd esq ii iii iv$ v$);

sub fetch_v1_article {
    my ( $self, $site, $old_site, $v1_pid ) = @_;

    my $path =
      bm_file_path( $self->v1_sitedir($old_site), 'articles', "$v1_pid.cgi" );
    return q{} if !-e $path;

    my %v1_page;
    (   $v1_page{id},              $v1_page{title},
        $v1_page{author_name},     $v1_page{author_email},
        $v1_page{description},     $v1_page{keywords},
        $v1_page{sections},        $v1_page{remote},
        $v1_page{url},             $v1_page{htmltext},
        $v1_page{content},         $v1_page{promoimage},
        $v1_page{promoimageurl},   $v1_page{articleimage},
        $v1_page{articleimageurl}, $v1_page{imagecaption},
        $v1_page{pubdate},         $v1_page{suppress},
        $v1_page{published},       $v1_page{related1},
        $v1_page{relatedurl1},     $v1_page{related2},
        $v1_page{relatedurl2},     $v1_page{related3},
        $v1_page{relatedurl3},     $v1_page{related4},
        $v1_page{relatedurl4},     $v1_page{related5},
        $v1_page{relatedurl5},     $v1_page{lasteditor},
        $v1_page{creator},         $v1_page{create_date},
        $v1_page{slug},            $v1_page{thumbimage},
        $v1_page{thumbimageurl},   $v1_page{internal_images},
        $v1_page{pullquotes}
      )
      = bm_load_file($path);
    return q{} if !$v1_page{id} || $v1_page{id} != $v1_pid;

    my $page = BigMed::Content::Page->new();
    $page->set_priority(500);
    $page->set_site( $site->id );
    $page->set_version(1);

    #title and slug
    $v1_page{title} = 'Untitled'
      if !defined $v1_page{title} || $v1_page{title} eq q{};
    $page->set_title( $v1_page{title} );
    $page->set_slug( $v1_page{slug} );
    defined( $page->generate_slug ) or return;

    #update sections to v2 values
    my @sections;
    foreach my $old_sid ( split( /_!!_/ms, $v1_page{sections} ) ) {
        next if !$self->{section_map}->{$old_sid};
        push @sections, $self->{section_map}->{$old_sid};
    }    
    #handle the case of no matching sections below, with pubstatus
    $page->set_sections( \@sections );

    #page type
    my ( $link_url, $doc_filename );
    if ( $v1_page{remote} eq 'Link' ) {
        $page->set_subtype('link');
        $v1_page{content} = q{};
        $link_url = $v1_page{url};
    }
    elsif ( $v1_page{remote} eq 'Doc' ) {
        $page->set_subtype('download');
        $v1_page{content} = q{};
        $doc_filename = $v1_page{url};
    }
    else {
        $page->set_subtype('article');
    }

    #massage the content
    $v1_page{content} ||= q{};
    $v1_page{content} =~ s/_!!_/\n/msg;
    if ( $v1_page{htmltext} eq 'HTML' ) {
        $v1_page{content} = 'RawHTML:' . $v1_page{content};
    }
    else {
        $v1_page{content} = 'RichText:' . $v1_page{content};
    }
    $page->set_content( $v1_page{content} );

    #metadata
    $page->set_meta_keywords( $v1_page{keywords} );
    $v1_page{description} = q{} if !defined $v1_page{description};
    $v1_page{description} =~ s/&&/\n/msg;
    $page->set_description( 'RichText:' . $v1_page{description} );

    #creation time and pub date
    $page->set_create_time( convert_v1_time( $site, $v1_page{create_date} ) );
    my ( $pub_mon, $pub_day, $pub_yr ) = jdate( $v1_page{pubdate} );
    my $pub_time = BigMed->bigmed_time(
        month  => $pub_mon,
        day    => $pub_day,
        year   => $pub_yr,
        hour   => 12,
        offset => $site->time_offset,
    );
    $page->set_pub_time($pub_time);

    my $pubstatus =
      ( $v1_page{published} && $v1_page{published} ) eq 'pub'
      ? 'published'
      : 'draft';
    
    if (!@sections) { #there were no matching sections
        $pubstatus = 'draft';
        my $first_main = ($site->homepage_obj->kids)[0];
        $page->set_sections( [$first_main] ) if $first_main;
    }
    
    $page->set_pub_status($pubstatus);

    #owner and editor
    $page->set_owner( $self->{username_map}->{ $v1_page{creator} } )
      if $v1_page{creator};
    if ( $v1_page{lasteditor} ) {
        my ($editor) = split( /&&/ms, $v1_page{lasteditor} );
        $page->set_last_editor( $self->{username_map}->{$editor} );
    }

    #suppress
    if ( $v1_page{suppress} ) {
        my %flag;
        foreach my $f ( split( //ms, $v1_page{suppress} ) ) {
            $flag{ $FLAG_MAP{$f} } = 1 if $FLAG_MAP{$f};
        }
        $page->set_flags( \%flag );
    }

    #done w/changes to the page object; the rest is related objects
    $page->save or return;

    #related links
    foreach my $i ( 1 .. 5 ) {
        next if !$v1_page{"relatedurl$i"};

        my $url = BigMed::URL->new();
        $url->set_url( $v1_page{"relatedurl$i"} );
        $url->set_text( $v1_page{"related$i"} );
        $url->set_priority( 501 - $i );
        $url->set_new_win('default');

        $page->save_object_link(
            linked_obj => $url,
            type       => 'related_link',
          )
          or return;
    }

    #pullquotes
    my @pquotes;
    @pquotes = reverse split( /_!!_/ms, $v1_page{pullquotes} )
      if $v1_page{pullquotes};
    foreach my $p (@pquotes) {
        my @fields = split( /&&/ms, $p );
        next if !$fields[4];    #content field

        my $pquote = BigMed::Pullquote->new();
        $fields[4] =~ s/~\!\!~/\n/msg;
        my $filter = $fields[5] eq 'html' ? 'RawHTML' : 'RichText';
        $pquote->set_text("$filter:$fields[4]");

        $fields[1] ||= 1;
        $pquote->set_position("block:$fields[1]");

        $pquote->set_align( $fields[2] || 'default' );

        my $size = $fields[6] && $fields[6] eq 's' ? 'small' : 'big';
        $pquote->set_size($size);

        $page->save_object_link(
            linked_obj => $pquote,
            type       => 'pullquote',
          )
          or return;
    }

    #author_name and author_email
    if ( $v1_page{author_name} ) {
        my ( $firstname, $lastname ) =
          parse_v1_names( $v1_page{author_name} );
        my $email = $v1_page{author_email} ||= q{};

        my $person = BigMed::Person->fetch(
            {   site       => $site->id,
                first_name => $firstname,
                last_name  => $lastname
            }
        );
        return if !defined $person;
        undef $person if $person && !$lastname && $person->last_name;
        if ( $person && !$person->email && $email ) {
            $person->set_email($email);
            $person->save or return;
        }
        elsif ( $person && $person->email && $email ne $person->email ) {
            undef $person;    #different email, create a new object
        }

        if ( !$person ) {
            $person = BigMed::Person->new();
            $person->set_last_name($lastname);
            $person->set_first_name($firstname);
            $person->set_email($email) if $email;
            $person->set_shared(1);
            $person->set_in_lib(1);
            $person->set_owner( $page->owner );
        }
        $page->save_object_link(
            linked_obj => $person,
            type       => 'author',
          )
          or return;
    }

    #link object for link page types
    if ($link_url) {
        my $extlink = BigMed::ExtURL->new();
        $extlink->set_text( $page->title );
        $extlink->set_url($link_url);
        $extlink->set_link_to('url');
        $extlink->set_new_win('default');
        $page->save_object_link(
            linked_obj => $extlink,
            type       => 'link_url',
          )
          or return;
    }

    #document object for link page types
    if ($doc_filename) {
        my $v1_path;
        if ( $page->pub_status eq 'published' ) {
            $v1_path = bm_file_path( $site->stash('v1_docs'), $doc_filename );
            $v1_path =
              bm_file_path( $site->stash('v1_unpubdocs'), $doc_filename )
              if !-e $v1_path;
        }
        else {
            $v1_path =
              bm_file_path( $site->stash('v1_unpubdocs'), $doc_filename );
            $v1_path = bm_file_path( $site->stash('v1_docs'), $doc_filename )
              if !-e $v1_path;
        }

        my $v2_path =
          bm_file_path( $site->doc_path, $doc_filename );
        if ( -e $v1_path ) {
            my $download;
            if ( -e $v2_path ) { #could happen if pub/unpub docs; use existing
                $download = BigMed::Media::Document->fetch(
                    {   site     => $page->site,
                        filename => $doc_filename,
                    }
                );
                return if !defined $download;
            }
            elsif ( $v1_path ne $v2_path ) {
                bm_copy_file( $v1_path, $v2_path, { build_path => 1 } )
                  or return;
            }

            if ( !$download ) {
                $download = BigMed::Media::Document->new();
                $download->set_filename($doc_filename);
                $download->set_title( $page->title );
                $download->set_shared(1);
                $download->set_in_lib(1);
                $download->set_owner( $page->owner );
                $download->set_site( $page->site );
                _make_field_unique( $download, 'title' );
            }
            $page->save_object_link(
                linked_obj => $download,
                type       => 'download',
                metadata   => { 'link_to' => 'document' },
              )
              or return;
        }
        else {
            warn "No document directory found at $v1_path";
        }
    }

    my %image_cache;
    
    #promoimage/promoimageurl (spotlight)
    import_v1_image(
        site => $site,
        page => $page,
        url  => $v1_page{promoimageurl},
        file => $v1_page{promoimage},
        link => 'spot',
        cache => \%image_cache,
      )
      or return;

    #articleimage/articleimageurl (main article image)
    import_v1_image(
        site     => $site,
        page     => $page,
        url      => $v1_page{articleimageurl},
        file     => $v1_page{articleimage},
        position => 'other',
        caption  => $v1_page{imagecaption},
        cache => \%image_cache,
      )
      or return;

    #thumbimage/thumbimageurl (thumbnail image)
    import_v1_image(
        site => $site,
        page => $page,
        url  => $v1_page{thumbimageurl},
        file => $v1_page{thumbimage},
        link => 'links',
        cache => \%image_cache,
      )
      or return;

    #internal_images (body images)
    my @bimages;
    @bimages = reverse split( /_!!_/ms, $v1_page{internal_images} )
      if $v1_page{internal_images};
    foreach my $b (@bimages) {
        my @fields = split( /&&/ms, $b );
        my $caption = $fields[4] || q{};
        $caption =~ s/~!!~/\n/msg;
        import_v1_image(
            site     => $site,
            page     => $page,
            url      => $fields[6],
            file     => $fields[5],
            position => $fields[1],
            caption  => $caption,
            align    => $fields[2],
            hotlink  => $fields[8],
            cache => \%image_cache,
          )
          or return;
    }

    return $page;
}

sub jdate {
    my ($jd) = @_;
    my ($jdate_tmp);
    my ( $m, $d, $y, $wkday );
    $wkday     = ( $jd + 1 ) % 7;    # calculate weekday (0=Sun,6=Sat)
    $jdate_tmp = $jd - 1_721_119;
    $y         = int( ( 4 * $jdate_tmp - 1 ) / 146_097 );
    $jdate_tmp = 4 * $jdate_tmp - 1 - 146_097 * $y;
    $d         = int( $jdate_tmp / 4 );
    $jdate_tmp = int( ( 4 * $d + 3 ) / 1461 );
    $d         = 4 * $d + 3 - 1461 * $jdate_tmp;
    $d         = int( ( $d + 4 ) / 4 );
    $m         = int( ( 5 * $d - 3 ) / 153 );
    $d         = 5 * $d - 3 - 153 * $m;
    $d         = int( ( $d + 5 ) / 5 );
    $y         = 100 * $y + $jdate_tmp;
    if ( $m < 10 ) {
        $m += 3;
    }
    else {
        $m -= 9;
        ++$y;
    }
    return ( $m, $d, $y, $wkday );
}

sub convert_v1_time {
    my ( $site, $v1_time ) = @_;
    my ( $time, $month, $day, $year ) = split( /[-]/ms, $v1_time );
    my ( $hour, $minute, $ampm );
    if ($time) {
        $time =~ s/\s+//msg;
        ( $hour, $minute, $ampm ) = split( /:/ms, $time );
        $hour += 12 if $ampm && $hour < 12 && $ampm eq 'pm';
    }
    else {
        ( $hour, $minute ) = ( 12, 0 );
    }
    return BigMed->bigmed_time(
        month  => $month,
        day    => $day,
        year   => $year,
        hour   => $hour,
        minute => $minute,
        offset => $site->time_offset,
    );
}

sub parse_v1_names {
    my $full_name = shift;
    my @names = split( /\s+/ms, $full_name );
    my ( $firstname, $lastname );
    if ( @names == 2 ) {
        $firstname = $names[0];
        $lastname  = $names[1];
    }
    elsif ( @names > 2 ) {
        my $is_suff;
        my $test = $names[-1];
        foreach my $suff (@NAME_SUFFIX) {
            $is_suff = 1, last if $test =~ /^$suff/msi;
        }
        if ($is_suff) {
            my $suff = pop @names;
            $lastname = ( pop @names ) . " $suff";
        }
        else {
            $lastname = pop @names;
        }
        $firstname = join( q{ }, @names );
    }
    else {    #just one name
        $firstname = $names[0];
        $lastname  = q{};
    }
    return ( $firstname, $lastname );
}

sub import_v1_image {
    my %opt = @_;
    return 1 if !$opt{url} && !$opt{file};    #nothing to add

    my $site = $opt{site};
    croak 'import_v1_image requires site object param'
      if ref $site ne 'BigMed::Site';
    my $page = $opt{page};
    croak 'import_v1_image requires page object param'
      if ref $page ne 'BigMed::Content::Page';

    my $image = BigMed::Media::Image->new();
    $image->set_site( $page->site );
    $image->set_owner( $page->owner );
    $image->set_shared(1);
    $image->set_in_lib(1);

    my %actions = $image->image_actions($site);
    my $dupe_key;
    if ( $opt{url} ) {
        my $url = $dupe_key = "url:$opt{url}";
        return 1 if _update_dupe_image(%opt, dupe_key => $dupe_key);
        my %format = map { $_ => $url } ( 'orig', keys %actions );
        $image->set_formats( \%format );
    }
    else {    #file
        my $v1_path = bm_file_path( $site->stash('v1_images'), $opt{file} );
        if ( !-e $v1_path || !( $dupe_key = -s $v1_path) ) {
            warn "No v1 image to import at $v1_path";
            return 1;
        }
        return 1 if _update_dupe_image(%opt, dupe_key => $dupe_key);
        
        my $v2_path = bm_file_path( $site->image_path, $opt{file} );
        if ( $v1_path ne $v2_path ) {
            bm_copy_file( $v1_path, $v2_path, { build_path => 1 } ) or return;
        }

        if ( $image->can_thumbnail ) {
            $image->set_formats( { orig => $opt{file} } );
            if ( !$image->generate_thumbnails($site) ) { #thumbnail error
                my %error = BigMed->error_html_hash();
                my $error_msg;
                if ( $error{text} ) {
                    $error_msg = BigMed->bigmed->language( $error{text} ) ;
                }
                else {
                    $error_msg = "Unknown image error: $v2_path";
                }
                BigMed->clear_error();
                warn "Skipping image: $error_msg";
                return 1;
            }
        }
        else {
            my %format = ( orig => $opt{file} );
            if ( $opt{file} =~ /^(.*?)[.](jpg|gif|jpeg)$/msi ) {
                my $basename = $1;
                my $suffix   = $2;
                my $dot = BigMed->bigmed->env('DOT');
                foreach my $size ( keys %actions ) {
                    my $file = "$basename${dot}s$size.$suffix";
                    my $path =
                      bm_file_path( $site->image_path, $file );
                    bm_copy_file( $v2_path, $path ) or return;
                    $format{$size} = $file;
                }
            }
            $image->set_formats( \%format );
        }
    }

    $image->choose_preview_image();

    #link metadata
    my %meta;
    $opt{link} ||= q{};
    $meta{link_position} =
      ( $opt{link} eq 'spot' || $opt{link} eq 'links' ) ? $opt{link} : 'none';
    $meta{position} =
        !$opt{position} ? 'hidden'
      : $opt{position} eq 'other' ? 'other'
      : "block:$opt{position}";
    $meta{align}    = $opt{align}    || 'default';
    $meta{priority} = $opt{priority} || 500;

    if ( $opt{caption} ) {
        ( $meta{caption} = "RawHTML:$opt{caption}" ) =~ s/_!!_/\n/msg;
    }
    $meta{hotlink} = $opt{hotlink};

    #link type and title
    my $type = $meta{link_position} eq 'spot' ? '-Spotlight'
             : $meta{link_position} eq 'links' ? '-Link'
             : $meta{position} eq 'other' ? '-Main'
             : index($meta{position}, 'block') == 0 ? '-Body'
             : q{};
    $image->set_title( $page->title . $type );
    $image->update_id or return;
    while (!$image->is_unique('title')) {
        my $title = $image->title;
        if ($title =~ /(.*)-(\d+)$/) {
            my $num = $2 + 1;
            $image->set_title("$1-$num");
        }
        else {
            $image->set_title("$title-2");
        }
    }
    
    my $rpair = $page->save_object_link(
        linked_obj => $image,
        type       => 'media',
        metadata   => \%meta,
    ) or return;
    $opt{cache}->{$dupe_key} = $rpair->[0];
    return 1;
}

sub _update_dupe_image {
    my %opt = @_;
    my $pointer = $opt{cache}->{ $opt{dupe_key} } or return;
    my %meta = $pointer->metadata;
    
    #an image can have only one body position
    return
      if $meta{position} && $meta{position} ne 'hidden' && $opt{position};
    
    if ( $opt{link} ) {
        $meta{link_position} =
          ( $meta{link_position} && $meta{link_position} ne 'none' )
          ? 'all'
          : $opt{link};
    }
    elsif ( $opt{position} ) {
        $meta{position} =
          $opt{position} eq 'other' ? 'other' : "block:$opt{position}";
        $meta{align} = $opt{align} || 'default';
    }
    $meta{priority} ||= $opt{priority} || 500;
    $pointer->set_metadata(\%meta);
    return $pointer->save;
}

# IMPORT ANNOUNCEMENTS
# -----------------------------------------------------------------

sub import_announcements {
    my ( $self, $site, $old_siteid ) = @_;
    croak 'import_announcements requires site object in first argument'
      if ref $site ne 'BigMed::Site';
    croak 'import_announcements requires old site id in second argument'
      if !$old_siteid;

    $site->set_stash( 'section_annc', {} );

    #get home announcement
    if ( $site->stash('v1_htmldir') ) {
        my $path =
          bm_file_path( $site->stash('v1_htmldir'), 'announcement.txt' );
        my $annc = join( "\n", bm_load_file($path) );
        my $home = $site->homepage_obj;
        save_announcement( $site, $home, $annc ) or return;
    }

    my $annc_dir =
      bm_file_path( $self->v1_sitedir($old_siteid), 'templates' );
    my %lookup = reverse %{ $self->{section_map} };
    foreach my $sid ( $site->all_descendants_ids() ) {
        my $orig = $lookup{$sid};
        my $path;
        if ( index( $orig, '_' ) < 0 ) {
            $orig .= '_0';    #main section
            $path = bm_file_path( $annc_dir, "$orig.annc" );
        }
        else {
            $path = bm_file_path( $annc_dir, "$orig.annc" );
            if ( !-e $path ) {
                my ($pid) = split( /_/ms, $orig );
                $path = bm_file_path( $annc_dir, "${pid}sub.annc" );
            }
        }
        my $annc = join( "\n", bm_load_file($path) );
        my $section = $site->section_obj_by_id($sid);
        save_announcement( $site, $section, $annc ) or return;
    }

    return 1;
}

sub save_announcement {
    my ( $site, $section, $annc ) = @_;
    croak 'save_announcement requires site object in first argument'
      if ref $site ne 'BigMed::Site';
    croak 'save_announcement requires section object in second argument'
      if ref $section ne 'BigMed::Section';

    $annc =~ s/\A\s+//ms;
    $annc =~ s/\s+\z//ms;
    return 1 if !$annc;

    #avoid dupes; strip out whitespace and use md5 hash as unique key
    ( my $annc2 = $annc ) =~ s/\s+//msg;
    $annc2 = md5_hex($annc2);
    my $rsection_annc = $site->stash('section_annc');
    my $dupe          = $rsection_annc->{$annc2};
    if ($dupe) {
        my $dupe_obj =
          BigMed::Content::Annc->fetch( { site => $site->id, id => $dupe } );
        $dupe_obj->set_sections( [$dupe_obj->sections, $section->id] );
        return $dupe_obj->save;
    }

    my $annc_obj = BigMed::Content::Annc->new();
    $annc_obj->set_title('Announcement');
    $annc_obj->set_sections( [$section->id] );
    $annc_obj->set_content( 'RawHTML:' . $annc );
    $annc_obj->set_pub_status('published');
    $annc_obj->set_version(1);
    $annc_obj->set_site( $site->id );
    $annc_obj->set_pub_time( BigMed->bigmed_time );
    $annc_obj->save or return;

    $rsection_annc->{$annc2} = $annc_obj->id;
    return 1;
}

# IMPORT TIPS
# -----------------------------------------------------------------

sub import_tips {
    my ( $self, $site, $old_siteid ) = @_;
    croak 'import_tips requires site object in first argument'
      if ref $site ne 'BigMed::Site';
    croak 'import_tips requires old site id in second argument'
      if !$old_siteid;

    my $index_path =
      bm_file_path( $self->v1_sitedir($old_siteid), 'tips', 'tipindex.txt' );
    my @all_secs = ( $site->homepage_id, $site->all_descendants_ids );

    foreach my $line ( reverse bm_load_file($index_path) ) {
        next if !$line;
        my @fields = split( /_!!_/ms, $line );

        my $tip = BigMed::Content::Tip->new();
        $tip->set_title( $fields[1] );
        $tip->set_sections( \@all_secs );
        $fields[2] =~ s/_\!\!\!_//msg;    #legacy
        $tip->set_content( 'RichText:' . $fields[2] );
        $tip->set_pub_status('published');

        my $user = $self->{user_map}->{ $fields[3] };
        $tip->set_owner($user);
        $tip->set_last_editor($user);
        $tip->set_version(1);
        $tip->set_site( $site->id );

        #pubdate
        my ( $m, $d, $y );
        if ( $fields[4] =~ /\-/ms ) {
            ( $y, $m, $d ) = split( /-/ms, $fields[4] );
        }
        else {
            ( $m, $d, $y ) = jdate( $fields[4] );
        }
        my $pub_time = BigMed->bigmed_time(
            month  => $m,
            day    => $d,
            year   => $y,
            hour   => 12,
            offset => $site->time_offset,
        );
        $tip->set_pub_time($pub_time);
        $tip->set_create_time($pub_time);
        $tip->save or return;

    }
    return 1;
}

# UTILITY ROUTINES
# -----------------------------------------------------------------

sub index_pages {
    my ($self, $site) = @_;

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

    require BigMed::Format::HTML; #for language pref
    my $lang = $site->get_pref_value('html_htmlhead_lang') || 'en-us';
    my $search = BigMed::Search->new( locale => $lang ) or return;

    my $sitename = $site->name;
    my $total = $all_active->count;
    if ($self->{statusbar}) {
        my $rping = sub {
            my $indexer = shift;
            $self->update_lockfile("Building search index for '$sitename'")
              or return;
            $self->{statusbar}->update_status(
                message => [
                    'SEARCH_Updating search index',
                    $sitename,
                    $indexer->page_progress,
                    $total,
                    $indexer->word_progress,
                ]
            );
            return 1;
        };
        $search->indexer->add_trigger( 'mid_index', $rping );
    }
    return $search->index_page($all_active);
}

sub _make_field_unique {
    my ( $obj, $colname ) = @_;
    while ( my $not_unique = !$obj->is_unique($colname) ) {
        return if !defined $not_unique;    #i/o error from driver
        my $field = $obj->$colname;
        if ( $field =~ /\-(\d+)$/ms ) {
            my $suffix = $1;
            $suffix++;
            $field =~ s/\-\d+$/\-$suffix/ms;
        }
        else {
            $field .= '-2';
        }
        my $setter = "set_$colname";
        $obj->$setter($field);
    }
    return 1;
}

sub make_boolean {
    my $value = shift;
    return ( $value && ( $value =~ /^on$/msi || $value =~ /^y/msi ) )
      ? 1
      : q{};
}

sub make_pref_value {
    my $value     = shift;
    my $pref_name = shift;
    return defined $value ? $value : BigMed::Prefs->pref_default($pref_name);
}

1;

__END__

=head1 NAME

BigMed::ImportV1 - Import Big Medium 1.x data into Big Medium 2.0

=head1 DESCRIPTION

BigMed::ImportV1 imports all data (users, sites, content, images, documents)
into Big Medium 2.0. The import leaves the original data untouched,
copying it into the 2.0 data store.

=head1 USAGE

=head2 Construct and Import

=head3 C<BigMed::ImportV1->new( '/path/to/v1/moxiedata' )>

Returns a new ImportV1 object (or undef if there's an error along the way)

=head3 C<<$import->import_v1_data( %options )>>

Imports user accounts and sites based on the parameters in the
hash argument. The options:

=over 4

=item * statusbar => 1

If true, the method will print a HTML response to STDOUT appropriate
for returning as response to Big Medium's StatusDriver JavaScript
object for displaying status bars.

=item * merge_usernames => 1

If true, the import will not import v1 accounts whose usernames
already exist in the v2 installation; permissions and page ownership
from imported sites will be applied to the existing v2 accounts.
If false, the import will import v1 accounts, updating them to have
unique usernames. Default is false.

=item * sites => \@old_site_ids

An optional reference to an array of v1 eight-digit site ids. If
provided, the import will be limited to the named sites; if not,
all sites will be imported.

=back

The method also creates a lockfile to indicate that an import is
underway. If the method is called when a lockfile exists that has
been modified within the last minute, the method will return an
"import underway" error message.

=head2 Accessors

=head3 C<<$import->v1_moxiedata>>

Returns the path to the v1 moxiedata directory.

=head3 C<<$import->v2_moxiedata>>

Returns the path to the v2 moxiedata directory.

=head3 C<<$import->v1_userdir>>

Returns the path to the v1 user_data/members directory.

=head3 C<<$import->v1_sitedir( $v1_site_id )>>

Returns the path to the v1 data directory for the site whose v1 id
is specified in the argument. (The method does not do any verification
or validation of the path, it just returns the path for where it
*should* be, based on the site id).

=head3 C<<$import->v1_site_ids()>>

Returns an array of all site IDs in that installation. The IDs are
formatted in the 8-digit v1.x format. Returns an empty array if no
sites are found.

=head3 C<<$import->v1_site_hash()>>

Returns a hash keyed to v1 site ids and values as the site names.

=head3 C<<$import->v1_user_ids()>>

Returns an array of all user IDs in that installation. The IDs are
formatted in the 8-digit v1.x format. Returns an empty array if no
users are found.

=head2 Importing Users

=head3 C<<$import->import_users()>>

Imports all users from the v1 installation and saves them in the v2
installation. Accepts an optional hash of arguments to set parameters:

=over 4

=item * C<<skip_dupes => 1>>

If true, skips any users whose names already exist in the v2 installation.
If false, updates the name of the new user so that it's unique (by
appending a number to the username). Default is false.

=back

The method returns true on success or false on error.

=head3 C<<$import->fetch_v1_user( $v1_user_id )>>

Returns a v2 user object stocked with the data from the v1 user whose ID
is specified in the argument. Returns an empty string if no such user
exists.

The method does not save the user object; it's only in memory until
you save it later.

=head2 Importing Sites

=head3 C<<$import->import_sites( %options )>>

Imports sites, and returns true on success. The method detects if a previous
import stalled out in the middle and resumes if appropriate, skipping
already-imported sites and not repeating pages/sections that were
already imported.

The optional argument hash lets you set parameters:

=over 4

=item * sites => \@v1_site_ids

A reference to an array of v1 eight-digit site ids to import. If no
array reference is provided, all sites will be imported.

=back

=head3 C<<$import->fetch_v1_site( $v1_site_id, \%options )>>

Creates, saves and returns a site object with all data imported from
the v1 site whose id is in the first argument. Returns true on success.

The second argument is an optional hash reference for parameter values:

=over 4

=item * C<<newdir => 1>>

If true, sets the new site's html and page directory to a new location,
separate from the original v1 site so that the new site is built in
a location that does not interfere with the original. See C<make_new_dirs>
for details on the how and where.

=item * C<<id => $id>>

An optional element that lets you specify the v2 id of the site to create
(used internally by C<import_sites> to resume a site import).

=item * C<<status => $status>>

An optional element that lets you indicate the stage of a previously
interrupted import (used internally by C<import_sites to resume a site
import).

=back

=head3 C<<make_new_dirs( $site )>>

Used internally by C<fetch_v1_site> to locate, create and set
new alternate homepage/html directories and URLs for the site
object in the first argument.

Both the homepage directory and html directory are set to the same
directory. If the original html directory is the root directory
for the domain, a new directory named site-bm2 is created inside
that root directory. Otherwise, a new directory is created at the
same level as the original html directory; the directory has the same
name but has '-bm2' appended to it.

The site object's homepage_url, homepage_dir, html_url and html_dir
attributes are updated to reflect the change (these changes are not
saved, only made to the in-memory object).

The routine returns true on success.

=head3 C<<$self->import_privileges($site, $old_id)>>

Used internally by C<fetch_v1_site>.
Imports user privileges from the old site to the new site. Users should
be loaded via C<import_users> before calling this method. Returns true
on success.

=head2 Status/Logging Methods

The importer maintains several files to mark its progress, allowing
it to pick up the trail later in case the process is interrupted.
These methods are responsible for keeping those files current.

=head3 C<<$import->status_dir>>

The path of the directory where the importer's status files are stored.

=head3 C<<$import->lockfile>>

The path to the importer's lock/status file. The importer will not allow
another import operation to proceed while an import is in progress and
this file is the gatekeeper.

=head3 C<<$import->sitemap_path>>

The path to the data file where info about old/new site id mappings
and status of each site import is kept.

=head3 C<<$import->sectionmap_path( $old_site_id )>>

The path to the data file where info about old/new section id mappings
are kept. The original site's eight-digit id is the argument.

=head3 C<<$import->usermap_path>>

The path to the data file where info about how ids and names of users
in the v1 installation map to new users in the v2 installation.

=head3 C<<$import->pagemap_path( $old_site_id )>>

The path to the data file where info about how page ids
in the v1 installation map to new pages in the v2 installation.

=head3 C<<$import->update_lockfile( $message )>>

Updates the lock file with the contents of the required message string
argument. Returns true if successful, undef on error.

=head3 C<<$import->clear_status_logs()>>

Deletes all log/status files from the importer's status directory.
Returns true on success, undef on error.

=head3 C<<$import->update_site_status()>>

This method is used internally to build a data file that
maps old v1 site IDs and to new v2 site IDs, along with a status
label for each site to indicate where it is in the import process.
This method updates the importv1 object's internal site id map
and clears internal site map data. Returns true on success, undef
on error.

=head3 C<<$import->load_site_status()>>

Loads data from the site map file into the importer's internal data
structure. Returns true on success.

=head3 C<<$import->trash_site_status()>>

Deletes the importer's site map file. Returns true on success, false on
error.

=head3 C<<$import->update_section_map($section_obj, $old_site_id)>>

This method is used internally to build a data file that
maps old v1 section IDs to new v2 section IDs.
This method updates the importv1 object's internal section id map.
Returns true on success, undef on error.

=head3 C<<$import->load_section_map($old_site_id)>>

Loads data from the section map file into the importer's internal data
structure, replacing any existing internal section maps.
Returns true on success.

=head3 C<<$import->trash_section_map($old_site_id)>>

Deletes the importer's section map file for the v1 site whose id
is in the first argument, and clears internal section
map data. Returns true on success, false on error.

=head3 C<<$import->update_user_map()>>

This method is used internally by import_users to build a data file that
maps old v1 user IDs and user names to new v2 user IDs. This method
updates the user map using internal importer data gathered in import_users.
Returns true on success, undef on error.

=head3 C<<$import->load_user_map()>>

Loads data from the user map file into the importer's internal data structure.
Returns true on success.

=head3 C<<$import->trash_user_map()>>

Deletes the importer's user map file. Returns true on success, false on
error.

=head3 C<<$import->update_page_map($old_sid, $old_pid, $v2_pid)>>

This method is used internally to build a data file that
maps old v1 page IDs to new v2 page IDs.
This method updates the importv1 object's internal page id map.
Returns true on success, undef on error.

=head3 C<<$import->load_page_map($old_site_id)>>

Loads data from the page map file into the importer's internal data
structure, replacing any existing internal page map.
Returns true on success.

=head3 C<<$import->trash_page_map($old_site_id)>>

Deletes the importer's page map file for the v1 site whose id
is in the first argument, and clears internal page
map data. Returns true on success, false on error.

=head2 Import CSS Styles

=head3 C<<$importer->transfer_v1_styles( $site_obj, $v1_site_id )>>

Used internally by C<fetch_v1_site>.
Imports all style settings from the original v1 site whose id is in the
second argument into the site whose object is in the first argument.
Style prefs are saved as BigMed::CSS objects, and the custom styles
from the v1 site are saved as the BigMed::Theme styles for the site.
The routine also builds the style sheets in the site's html directory.

Returns true on success and undefines all global CSS variables when it's
done.

=head3 C<<$importer->css_globals_v1( $v1_site_id )>>

Used internally by C<transfer_v1_styles>.
Loads the global CSS varaibles for the v1 site named in the argument.

=head3 C<<is_css_link($selector)>>

Returns true if the link styles an "a" anchor link.

=head3 C<<get_css_attributes($selector)>>

    my %attr = get_css_attributes('body');

Returns a hash with CSS attributes keyed to CSS values. The argument
must be a v2 selector; if the selector is mapped to a v1 value, those
values are used to populate the hash of attributes.

=head3 C<<massage_link_css($selector, \%attributes)>>

For link selectors, tidies up the attributes to remove attributes
that unnecesssarily duplicate the entries for generic links. Changes
are made directly to the \%attributes hash reference in the second
argument.

=head3 C<<get_link_color($selector, $mod)>>

Returns a color to set for the visited or hover value of the selector
in the first argument. The second argument should be either 'Visit'
or 'Hover'. If no color is necessary, returns undef.

=head3 C<<transfer_v1_nav($site_obj, $css_all)>>

Saves CSS values for navigation related styles (background color for
hover and active navigation, and padding for navigation cells).
The first argument is the v2 site object, and the second argument
is a BigMed::CSS selection object. Returns true on success.

=head3 C<<transfer_v1_theme_css($site_obj)>>

Saves custom CSS from v1 site, as well as defunct or non-style-editor
styles, as the site's theme CSS. Returns true on success.

=head3 C<<handle_v1_skin_files( $site, $custom_css )>>

Used internally by transfer_v1_theme_css. Searches the css in the
second argument for URLs that use v1 skin files. If it finds them,
tries to guess location of the directory and copy them to the
new site. Updates the CSS to reflect the change and returns the
updated CSS.

=head3 C<<update_v1_custom_styles( $css )>>

Converts v1 class names in the css string into v2 selectors. Returns
the modified CSS.

=head3 C<<save_css_entry($selector, $all_css, \%attributes, $site_id, $ext)>>

Saves a BigMed::CSS object with the selector, attributes and site id named in
the arguments. An existing CSS object for the selector will be loaded and
reused if it exists. Otherwise, a new object will be created.

The $all_css argument is an optional selection object that may be used to
speed duplicate-checks for saving. If the $ext flag is true, any existing
object will be extended (instead of outright replaced) with the attribute
values.

Returns true on success.

=head2 Importing Sections

=head3 C<<$self->import_sections($site_obj, $old_site_id)>>

Imports sections from the v1 site indicated in the second
argument and saves them into the site whose object is in the first
argument. Updates the internal section map as it goes and logs
its progress through the sections.

Returns true on success.

=head3 C<<$self->fetch_v1_section($v1_section_line, $site_obj)>>

Used internally by the import_sections method.
Parses the string in the first argument (a line from the v1
Sections.setup file) and creates/saves/returns a section object
associated with the site whose object is in the second argument.
Returns false on error, otherwise the section object.

=head3 C<<$self->homepage_settings_v1($section_obj, $site_obj)>>

Converts the section object in the first argument into the homepage
object for the site whose object is in the second argument. Saves
relevant preferences for the section (html_links_numdisplay and 
html_morelinks_numdisplay). Returns true on success.

=head3 C<<stash_section_navimages($site, $section, @data)>>

Used internally by C<import_sites> to import navigation graphic
files and stash the URLs in the section object (this info
is later used by C<update_imagenav> to build imagenav replacement.
Returns true on success.

The @data array is the split array from the section's v1 section
data line.

=head2 Importing Templates

=head3 C<<$self->import_templates($site_obj, $old_site_id)>>

Imports and updates the templates from the v1 site whose id is in
the second argument. Returns true on success.

Uses the first main section's section/page templates as site defaults
and imports other section templates only if they differ. The first
section's default subsection template is used as the basis for the
email/feed/utility templates.

=head3 C<<save_utility_template( $tmpl_obj, $subsec_tmpl_text )>>

Used internally by C<import_templates>. Sets the default email/feed/utility
templates via the BigMed::Template object in the first argument and based
on the template text in the second argument. The template text should
be a v2-converted subsection template (sub.template). Returns true
on success.

=head3 C<<update_v1_template($site_obj, $path_to_v1_template)>>

Used internally by C<import_templates>.
Returns a v2-formatted template (with all v1 widgets and classes
updated), from the file located at the path in the second argument.
Returns an empty string if no such template exists.

=head3 C<<update_imagenav( $template_text, $site, $section_id )>>

Used internally by C<import_templates> to update the template in the
first argument, replacing imagenav widgets with hard-coded navigation.
Relies on stash information generated by the C<stash_section_navimages>
routine.

=head2 Importing Content

=head3 C<<$self->import_pages($site_obj, $old_site_id)>>

Imports and updates the pages from the v1 site whose id is in
the second argument. Returns true on success.

If resuming an import from a previously interrupted import, skips
any pages already imported.

=head3 C<<$self->fetch_v1_article( $site, $old_site_id, $v1_pageid )>>

Used internally by C<import_pages>. Loads/saves/returns a page object
imported from the v1 site/pid in the second and third arguments. Along
the way, creates and saves the following related objects, if applicable:

=over 4

=item * Related links

=item * Pullquotes

=item * Author

=item * Link object for link page types

=item * Document object for document page types

=item * Images (spotlight, link, main and body)

=back

=head3 C<<$import->import_announcements($site, $old_siteid)>>

Imports and updates the announcements from the v1 site whose id is in
the second argument. Returns true on success. 

If announcements have duplicate content, they are merged into a single
object.

=head3 C<<save_announcement($site, $section_obj, $annc_text)>>

Used internally by C<import_announcements>. Compares the announcement
text to other announcements in the site; if there's a match, adds
the section to the announcement. Otherwise, creates and saves a new
announcement object. Returns true on success.

=head3 C<<$import->import_tips($site, $old_siteid)>>

Imports and updates the tips from the v1 site whose id is in
the second argument. Returns true on success. 

=head3 C<<import_v1_image( %params )>>

Used internally by C<fetch_v1_article>. Imports and populates all image
sizes (if it can thumbnail, generates the thumbs; otherwise puts the original
into all sizes). Returns true on success.

Set parameters in the hash argument:

=over 4

=item * site => $site_obj

=item * page => $page_obj

=item * url => $url

Takes precedence over a file; if present, this is the url for the image

=item * file => $file

The filename of the image file inside the v1 moxiepix directory.

=item * link => spot | links | none

Default is 'none'

=item * align => left | right | center | default

Default is 'default'

=item * position => $paragraph_num | 'other'

If no value provided, position is set to 'hidden'

=item * priority => $num

Default is 500

=item * caption => $caption

=item * hotlink => $url

=back

=head3 C<<parse_v1_names( $full_names )>>

Splits a name into first and last, returning ($first, $last).

=head3 C<<convert_v1_time( $site_obj, $v1_time )>>

Converts a HH::MMpm-mm-dd-yyyy format time and returns a bigmed system
time string.

=head3 C<<jdate( $julian_date )>>

Returns (month, day, four-digit year, zero-based weekday) for the julian
date number in the argument.

=head2 Global Variable Utilities

These routines convert global v1 site variables into v2 formats and are used
internally by C<fetch_v1_site>.

All require that the variables already be loaded into memory from the
site's site_setup.cgi library before calling them.

=head3 C<<$import->load_site_from_globals( $v2_site_obj )>>

Reads the simplest of the v1 site-specific global variables and
stows them in the object and/or preferences of the v2 site object
in the argument. Returns the site on success, or false on error.

The method handles the import of these v1 variables (undefining
them after it's done with them):

=over 4

=item * $bmSetup::htmlDir

Stowed in $site->html_dir.

=item * $bmSetup::htmlURL

Stowed in $site->html_url.

=item * $bmSetup::HomepageDir

Stowed in $site->homepage_dir.

=item * $bmSetup::HomepageURL

Stowed in $site->homepage_url.

=item * $bmSetup::site_doclimit

Stowed in $site->site_doclimit.

=item * $bmSetup::SiteName

Stowed in $site->name.

=item * $bmSetup::IncludeSubhead

Stowed in html_headline_subhead pref.

=item * $bmSetup::RequireSpotlightImage

Stowed in html_spotlight_needimage pref.

=item * $bmSetup::LatestArtStop

Stowed in html_links_exclude_self pref.

=item * $bmSetup::TipRandom

Stowed in html_tip_randomize pref.

=item * $bmSetup::BreadComplete

Stowed in html_breadcrumbs_full pref.

=item * $bmSetup::BreadLC

Stowed in html_breadcrumbs_lc pref.

=item * $bmSetup::RSSfeed

Stowed in rss_enable_feed pref.

=item * $bmSetup::HeadInsert

Stowed in html_htmlhead_xtrahtml pref.

=item * $bmSetup::HTMLlang

Stowed in html_htmlhead_lang pref.

=item * $bmSetup::MainNumToDisplay

Stowed in html_links_numdisplay pref.

=item * $bmSetup::MainShortToDisplay

Stowed in html_morelinks_numdisplay pref.

=item * $bmSetup::QuickNumToDisplay

Stowed in html_quicktease_numdisplay pref.

=item * $bmSetup::LatestArtNum

Stowed in html_latest_numdisplay pref.

=item * $bmSetup::SubNumToDisplay

Stowed in html_overflow_numdisplay pref.

=item * $bmSetup::HomeNewsToDisplay

Stowed in html_news_numdisplay pref.

=item * $bmSetup::TipNumToDisplay

Stowed in html_tip_numdisplay pref.

=item * $bmSetup::BreadcrumbSep

Stowed in html_breadcrumbs_separator pref.

=item * $bmSetup::RSSNum

Stowed in rss_display_num pref.

=item * $bmSetup::InternalDomains

Stowed in html_links_window_intdomains pref.

=item * $bmSetup::HTMLtype

Stowed in html_htmlhead_doctype pref.

=back

=head3 C<<$importer->import_site_prefs($site)>>

Imports global variables and saves as v2 BigMed::Prefs objects, massaging
the data as necessary. Returns true on success.

=head3 c<date_format_v1()>

Returns a v2-formatted date_format string based on the value of the
$bmSetup::DateFormat variable or, if that's not defined, the
$bmSetup::ArtDateFormat variable. If neither is available,
returns the default '%b %e, %Y' (USshort) value. Undefines all of the
date-format variables when it's done.

=head3 C<time_offset_v1()>

Returns a v2-formatted timezone-offset string based on the value of the
$bmSetup::TimeZoneOffset value (which it undefines when finished).

=head3 C<aboutus_v1($site)>

Returns a v2 RawHTML-formatted rich-text string based on the values
of the $bmSetup::AboutUsRaw and $bmSetup::Copyright values
(which it undefines when finished).

=head3 C<page_titletag_v1()>

Returns the array reference expected to save into the site's
html_htmlhead_titlepage preference, based on the value of the
$bmSetup::PageTitleFormat variable (which it undefines when finished).
If $bmSetup::PageTitleFormat is undefined or empty, it returns
undef (which, when saved to the preference, will prompt Big Medium
to use the default value for the preference).

=head3 C<home_titletag_v1( $site_name, $home_section_line)>

Returns a title string for the html_htmlhead_titlehome site preference
based on the value of the $bmSetup::HomeTitleFormat variable
(which it undefines when finished) and the site name and section-line
arguments. (The section line is the home record from the v1 site's
Sections.setup file.)

=head3 C<new_window_v1()>

Returns a boolean value for the html_links_new_window site preference,
based on the value of the $bmSetup::NewWinLink and $bmSetup::NewWinCode
variables. If either is true, the routine returns a true value. Otherwise,
false.

The routine also undefines the NewWinLink, NewWinCode, NewWinRelated and
NewWinStop variables when it's done.

=head3 C<robot_behavior_v1()>

Returns a string for the html_htmlhead_robot site preference, based on
the value of the $bmSetup::RobotBehavior variable which the routine
undefines when it's done.

=head3 C<quicktease_value( $location )>

Returns a boolean value indicating whether Big Medium should suppress (true)
or allow (false) quicktease links when the link also appears on the location
indicated by the location argument (either "QuickNoHome" for the homepage
or "QuickNoMain" for the main sections).

The value is based on the QuickNo(Home)Spot, QuickNo(Home)Long and
QuickNo(Home)Short v1 variables. If any of those are true, the routine
returns true, otherwise false.

The routine undefines the original v1 variables when it's done.

=head3 C<byline_linkto_v1()>

Returns a value for the site's html_byline_linkto pref: 'form' if
$bmSetup::MailMethod is 'form', otherwise 'email'.

=head3 C<icon_v1($type)>

Returns the filename of the email or print icon (or undef if not defined
or filename could not be discovered). Appropriate for the html_tools_emailicon
and html_tools_printicon prefs.

=head3 C<link_elements_v1($type)>

Returns an array ref of elements for saving in either the html_link_elements
or html_spotlight_elements preference, based on the value of the
$bmSetup::PromoOrder (for html_link_elements) or $bmSetup::SpotOrder
(for html_spotlight_elements). Which variable is used depends on the
$type argument which must be either Promo or Spot.

Returns undef if no value is defined for the relevant variable.

=head3 C<alt_elements_v1($type)>

Returns either a single-item array ref (['head']) or undef,
depending on whether the specified link type used long-format (undef)
or short-format (['head']) links. The result may be saved in that
link type's v2 _elements pref. Specify the value to retrieve in the
$type argument:

=over 4

=item * Short: html_morelinks_elements

=item * LatestArt: html_latest_elements

=item * QuickHit: html_quicktease_elements

=item * TopNews: html_topnews_elements

=back

=head3 C<related_links_v1()>

Returns boolean value depending on value of v1 $bmSetup::PromoRelatedLinks var.

=head3 C<link_num_display_v1($type)>

Returns number of links to display based on the global variable indicated
in the $type argument. The variable is undefined when the routine is done.
The types:

=over 4

=item * 'links' checks $bmSetup::MainNumToDisplay

=item * 'morelinks' checks MainShortToDisplay

=item * 'quicktease' checks QuickNumToDisplay

=item * 'latest' checks LatestArtNum

=item * 'overflow' checks SubNumToDisplay

=item * 'news' checks HomeNewsToDisplay

=back

=head3 C<spot_thumb_align_v1>

Returns the alignment setting to plug into the html_spotlight_imagepos pref.

=head3 C<link_thumb_align_v1>

Returns the alignment setting to plug into the _imagepos pref for link
types that should include thumbnail images

=head3 C<longlink_thumb_v1>

Returns true if long-format links should include thumbnails.

=head3 C<shortlink_thumb_v1>

Returns true if short-format links should include thumbnails.

=head3 C<save_image_prefs($site)>

Saves any custom image sizes into the site's image_actions preference,
and sets the html_spotlight_imagesize, html_link_imagesize and
html_image_size preferences. Returns true on success or false on error.

=head3 C<image_dimensions_v1($type)>

Used internally by C<save_image_prefs>, this routine returns the widthxheight
image dimensions to use for the image type specified in the type argument:

=over 4

=item * Promo: Spotlight image

=item * Thumb: Link thumbnail

=item * Article: Main article image

=item * Int: Embedded detail page image

=back

=head1 AUTHOR & COPYRIGHTS

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

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

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

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

=cut


1;

