#!/usr/bin/perl
# ----------------------------------------------------------------------------
# This software is Copyright (C) 2007, All Rights Reserved
# ----------------------------------------------------------------------------
# This program is protected by local and international copyright laws. Any
# use of this program is subject to the the terms of the license agreement
# included as part of this distribution archive. Any other uses are strictly
# prohibited without the written permission of the Vendor and all
# other rights are reserved.
# -----------------------------------------------------------------------------
# Warning : Modifying this program violates your license agreement!
# -----------------------------------------------------------------------------

package IT::ArtMan::Upgrade::From141;

  # load modules
  use strict;
  use Socket qw(:DEFAULT :crlf);

  use IT::Globals;

use vars qw($SETUP_OPTIONS_FIELDS $ACCOUNT_FIELDS $CATEGORY_FIELDS $ARTICLE_FIELDS $ARCHIVE_FIELDS);

$SETUP_OPTIONS_FIELDS = [qw(num titlebar confirm_erase confirm_saved confirm_erased setup_warning uploadlimit uploadmax upload_height upload_width uploadext
lhour lhour_offset lmin lmin_offset reg1 reg2 reg3 d c i dir_base dir_upload dir_publish url_image url_help url_upload url_publish url_logoff
search_url default_autoformat allow_html allow_art_types auto_publish publish_index_fileprefix publish_headline_fileprefix publish_cat_fileprefix
publish_cat_headline_fileprefix publish_art_fileprefix publish_art_printer_fileprefix publish_cat_list_fileprefix publish_sampler_fileprefix
publish_fileext publish_index_qty publish_headline_qty publish_cat_qty publish_cat_headline_qty publish_sampler_qty publish_index publish_headlines
publish_cat publish_cat_headlines publish_article publish_printer publish_list_of_cats publish_sampler publish_search_form_fileprefix
publish_search_form gcat_name global_updated sfield_name1 sfield_name2 sfield_name3 sfield_name4 sfield_name5 sfield_name6 sfield_name7
sfield_name8 sfield_name9 sfield_name10 sfield_name11 sfield_name12 sfield_order1 sfield_order2 sfield_order3 sfield_order4 sfield_order5
sfield_order6 sfield_order7 sfield_order8 sfield_order9 sfield_order10 sfield_order11 sfield_order12 sfield_type1 sfield_type2 sfield_type3
sfield_type4 sfield_type5 sfield_type6 sfield_type7 sfield_type8 sfield_type9 sfield_type10 sfield_type11 sfield_type12 publish_syndicate_fileprefix
publish_syndicate_qty publish_syndicate webfeed_gen_active v branding footer static_archive_pages viewer_url viewer_pathinfo allow_art_filename
sfield_name13 sfield_name14 sfield_name15 sfield_name16 sfield_name17 sfield_name18 sfield_name19 sfield_name20 sfield_order13 sfield_order14
sfield_order15 sfield_order16 sfield_order17 sfield_order18 sfield_order19 sfield_order20 sfield_type13 sfield_type14 sfield_type15 sfield_type16
sfield_type17 sfield_type18 sfield_type19 sfield_type20 sort1field sort1reverse sort1numeric sort2field sort2reverse sort2numeric sort3field
sort3reverse sort3numeric summary_name content_name summary_type content_type summary_enabled content_enabled sfield_enabled1 sfield_enabled2
sfield_enabled3 sfield_enabled4 sfield_enabled5 sfield_enabled6 sfield_enabled7 sfield_enabled8 sfield_enabled9 sfield_enabled10 sfield_enabled11
sfield_enabled12 sfield_enabled13 sfield_enabled14 sfield_enabled15 sfield_enabled16 sfield_enabled17 sfield_enabled18 sfield_enabled19
sfield_enabled20 publish_hidden_index email_notification_enabled mail_sender_email mail_sender_name mail_mailer mail_sendmail mail_smtp mail_pop3auth
mail_pop3 mail_pop3user mail_pop3pass parse_ssi search_program_active view_program_active ssi_http_auth_username ssi_http_auth_password
include_subcat_articles max_subcat_depth subcat_format)];

$ACCOUNT_FIELDS = [qw(num created created_by updated updated_by name id pw acl notes cats all_cats email_cats email_all_cats email_notification_events email)];

$CATEGORY_FIELDS = [qw(num created created_by updated updated_by name sorder_deprecated template_file article_template_file headline_template_file
ufile_image pub_fileprefix pub_fileprefix_specify perpage_index perpage_headlines hidden printer_template_file imagewidth_image imageheight_image
pub_perpage_specify dir_publish url_publish publish_specify dir_upload url_upload display_only parent global_order sibling_order depth lineage)];

$ARTICLE_FIELDS = [qw(num created created_by updated updated_by mon day year name summary content summary_format content_format cat ufile_image1
ufile_image2 ufile_image3 ufile_image4 ufile_image5 ufile_image6 ufile_image7 ufile_image8 ufile_image9 ufile_image10 caption1 caption2 caption3
caption4 caption5 caption6 caption7 caption8 caption9 caption10 hour min ampm owner link art_type ufile_document hidden imagename1 imagename2
imagename3 imagename4 imagename5 imagename6 imagename7 imagename8 imagename9 imagename10 imagewidth_image1 imagewidth_image2 imagewidth_image3
imagewidth_image4 imagewidth_image5 imagewidth_image6 imagewidth_image7 imagewidth_image8 imagewidth_image9 imagewidth_image10 imageheight_image1
imageheight_image2 imageheight_image3 imageheight_image4 imageheight_image5 imageheight_image6 imageheight_image7 imageheight_image8 imageheight_image9
imageheight_image10 status spare1 spare2 spare3 spare4 spare5 spare6 spare7 spare8 spare9 spare10 spare11 spare12 stamp ufile_image11 ufile_image12
ufile_image13 ufile_image14 ufile_image15 ufile_image16 ufile_image17 ufile_image18 ufile_image19 ufile_image20 ufile_image21 ufile_image22 ufile_image23
ufile_image24 ufile_image25 imagename11 imagename12 imagename13 imagename14 imagename15 imagename16 imagename17 imagename18 imagename19 imagename20
imagename21 imagename22 imagename23 imagename24 imagename25 imagewidth_image11 imagewidth_image12 imagewidth_image13 imagewidth_image14 imagewidth_image15
imagewidth_image16 imagewidth_image17 imagewidth_image18 imagewidth_image19 imagewidth_image20 imagewidth_image21 imagewidth_image22 imagewidth_image23
imagewidth_image24 imagewidth_image25 imageheight_image11 imageheight_image12 imageheight_image13 imageheight_image14 imageheight_image15 imageheight_image16
imageheight_image17 imageheight_image18 imageheight_image19 imageheight_image20 imageheight_image21 imageheight_image22 imageheight_image23 imageheight_image24
imageheight_image25 filename spare13 spare14 spare15 spare16 spare17 spare18 spare19 spare20 caption11 caption12 caption13 caption14 caption15 caption16 caption17
caption18 caption19 caption20 caption21 caption22 caption23 caption24 caption25 format_spare1 format_spare2 format_spare3 format_spare4 format_spare5 format_spare6
format_spare7 format_spare8 format_spare9 format_spare10 format_spare11 format_spare12 format_spare13 format_spare14 format_spare15 format_spare16 format_spare17
format_spare18 format_spare19 format_spare20)];

$ARCHIVE_FIELDS = [qw(num created created_by updated updated_by name count)];

use vars qw($OLD_SETUP_OPTIONS $OLD_CATEGORIES $CATEGORY_FH $ARTICLE_FH $ARCHIVE_FH $CURRENT_ARCHIVE $ACCOUNTS_FH);


# ------------------------------------------------------------------------------
# Function    : _openDataFile
# Usage       : my $fh = &_openDataFile($filepath);
# ------------------------------------------------------------------------------

sub _openDataFile {
  my $filepath = shift || die "no filepath specified!";

  # error checking
  unless (-e $filepath) { die "'$filepath' doesn't exist!"; }
  unless (-f $filepath) { die "'$filepath' isn't a file!"; }

  # open file
  my $fh = local(*FH);
  open($fh, "<$filepath") || die "Couldn't open filepath '$filepath' : $!";

  #
  return $fh;
}

# ------------------------------------------------------------------------------
# Function    : _closeDataFile
# Usage       : &_closeDataFile($fh);
# ------------------------------------------------------------------------------

sub _closeDataFile {
  my $fh = shift;
  unless (ref $fh eq 'GLOB') { die "\$fh must be a glob reference!"; }

  close(*$fh);
  undef *$fh;
}

# ------------------------------------------------------------------------------
# Function    : _getRecordCount
# Description :
# Usage       : my $recordCount = &_getRecordCount($filepath);
# ------------------------------------------------------------------------------

sub _getRecordCount {
  my $filepath = shift || die "no filepath specified!";

  # get record count
  my $recordCount = 0;
  my $fh = &_openDataFile($filepath);
  open($fh, "<$filepath") || die "Couldn't open filepath '$filepath' : $!";
  while (my $line = <$fh>) {
    if ($line =~ /^\d+\Ą/) { $recordCount++; }
  }
  &_closeDataFile(\$fh);

  #
  return $recordCount;
}



# ------------------------------------------------------------------------------
# Function    : _getNextRecord
# Description : return next record as hash from old database.  returns undef
#               on end of file
# Usage       : my $fh         = &_openDataFile($filepath);
#               my $recordHash = &_getNextRecord($fh, \@fieldnames);
# ------------------------------------------------------------------------------

sub _getNextRecord {
  my $FH         = shift || die "no filehandle!";
  my $fieldNames = shift || die "no fieldnames specified!";

  # error checking
  unless (fileno $FH) { die "Invalid filehandle!"; }

  # get next record
  my $recordLine;
  while (<$FH>) {
    if (!/^\d+\Ą/) { next; }  # skip no records
    s/[^Ą]+$//;               # remove return/nextline
    $recordLine = $_;
    last;
  }
  unless ($recordLine) { return; }

  # split recordLine into fields
  my $record = {};
  my @fieldValues = split(/\Ą/, $recordLine);            # split record into fields
  for my $index (0..$#fieldValues) {
    my $fieldName  = $fieldNames->[$index];
    my $fieldValue = $fieldValues[$index];
    $fieldValue =~ s/ż([A-F0-9]{2})/pack('C',hex($1))/egix; # decode field value
    $record->{$fieldName} = $fieldValue;
  }

  return $record;
}


# ------------------------------------------------------------------------------
# Function    : _skipRecords
# Description : skip specified number of records.  returns undef on end of file.
#               Doesn't check that records start with number, but this shouldn't
#               matter because if we've
#               &_skipRecords($fh, $skipCount);
# ------------------------------------------------------------------------------

sub _skipRecords {
  my $FH    = shift || die "no filehandle!";
  my $count = shift;

  # error checking
  unless (fileno $FH) { die "Invalid filehandle!"; }
  if (ref $count)     { die "Count can't be a reference!"; }
  if (!$count) { return; }

  # get next record
  my $isPastHeader = 0;
  my $skippedCount = 0;
  while (<$FH>) {

    # skip past header
    if (not $isPastHeader) {    # skip past header
      if (!/^\d+\Ą/) { next; }  # skip no records
      $isPastHeader = 1;
    }

    # skip over record
    $skippedCount++;
    if ($skippedCount >= $count) { last; }
  }

}

# ------------------------------------------------------------------------------
# Function    : loadSetupOptions
# Usage       : my $setupOptions = &loadSetupOptions($filepath);
# ------------------------------------------------------------------------------

sub loadSetupOptions {
  my $filepath = shift || die "no filepath specified!";

  # load setup options
  unless ($OLD_SETUP_OPTIONS) {
    my $fh             = &_openDataFile($filepath);
    $OLD_SETUP_OPTIONS = &_getNextRecord($fh, $SETUP_OPTIONS_FIELDS) || die "couldn't load setup options!";
    &_closeDataFile(\$fh);
  }

  return $OLD_SETUP_OPTIONS;
}

# ------------------------------------------------------------------------------
# Function    : loadOldCategoriesHash
# Usage       : my $oldCategories = &loadOldCategoriesHash($oldDataDir);
# ------------------------------------------------------------------------------

sub loadOldCategoriesHash {
  my $oldDataDir = shift || die "no old data dir specified!";
  my $filepath   = "$oldDataDir/category.dat";

  # load categories
  unless ($OLD_CATEGORIES) {
    $OLD_CATEGORIES = {};
    my $fh             = &_openDataFile($filepath);
    while (my $record = &_getNextRecord($fh, $CATEGORY_FIELDS)) {
      $OLD_CATEGORIES->{ $record->{'num'} } = $record;
    }
    &_closeDataFile(\$fh);
  }

  return $OLD_CATEGORIES;
}

# ------------------------------------------------------------------------------
# Function    : _createUploadFieldDataString
# Description : Note, the order images are passed in is important as that's the
#               image number they are assigned.  It must be the same so ***image1*** tags work
# Usage       : my $imageFieldDataString = &_createUploadFieldDataString( [ [$pathFromUploadDir, $height, $width], ... ] )
# ------------------------------------------------------------------------------

sub _createUploadFieldDataString {
  my $imagesArray = shift || return "";

  my $listStruct   = &_createUploadFieldDataHash( $imagesArray );
  if (!$listStruct) { return; }

  my $listString = &IT::DhtmlFieldLists::convertListStructToString($listStruct);

  return $listString;
}

# ------------------------------------------------------------------------------
# Function    : _createUploadFieldDataHash
# Description : Note, the order images are passed in is important as that's the
#               image number they are assigned.  It must be the same so ***image1*** tags work.
#               Returns undef if no images
# Usage       : my $imageFieldDataHash = &_createUploadFieldDataHash( [ [$pathFromUploadDir, $height, $width, $title, $caption], ... ] )
# ------------------------------------------------------------------------------

sub _createUploadFieldDataHash {
  my $imagesArray = shift || return "";

  my $imageHash = {};
  my $imageNum = 0;
  foreach my $imagePathHeightWidthEtc (@$imagesArray) {
    if (!$imagePathHeightWidthEtc->[0]) { next; }  # skip if no path

    my $pathFromUploadDir = $imagePathHeightWidthEtc->[0];
    my $height            = $imagePathHeightWidthEtc->[1];
    my $width             = $imagePathHeightWidthEtc->[2];
    my $filename          = $pathFromUploadDir;
    $filename             =~ s|^.*[\\/](.*?)$|$1|s; # remove all but filename
    my $extension         = $filename;
    $extension            =~ s|^.*\.(\w+)$|$1|s;    # remove all but extension
    my $isImage           = ($extension =~ m/^gif|jpg|jpeg|png$/i);
    my $title             = $imagePathHeightWidthEtc->[3] || $filename;
    my $caption           = $imagePathHeightWidthEtc->[4];
    my $num               = $imagePathHeightWidthEtc->[5] || ++$imageNum;
    my $order             = $num;

    if ($imageHash->{'config'}{'highest_record_number'} < $num) {
      $imageHash->{'config'}{'highest_record_number'} = $num;
    }

    $imageHash->{'rows'}{$num} = {
      '_erased'                    => '0',
      '_num'                       => $num,
      '_order'                     => $order,
      'title'                      => $title,
      'align'                      => 'right',
      'caption'                    => $caption,
      'extension'                  => $extension,
      'filename'                   => $filename,
      'hasThumbnail'               => '0',
      'height'                     => $height,
      'width'                      => $width,
      'isImage'                    => $isImage,
      'linkTo'                     => '',
      'linkUrl'                    => '',
      'openLinkInNewWindow'        => '0',
      'pathFromUploadDir'          => $pathFromUploadDir,
      'thumbnailFilename'          => $filename,
      'thumbnailPathFromUploadDir' => $pathFromUploadDir,
      'thumbnailHeight'            => $height,
      'thumbnailWidth'             => $width,
      'useThumbnail'               => '0',
      'wrapText'                   => '0',
    };
  }

  # if no images
  unless (keys %{$imageHash->{'rows'}}) { return; }

  return $imageHash;

  my $imageDataString = &IT::DhtmlFieldLists::convertListStructToString($imageHash);

  return $imageDataString;
}



# ------------------------------------------------------------------------------
# Function    : _convertContentFields
# Description : Modifes imageHash in place
# Usage       : my $content = &_convertContentFields($content, $imageHash, $autoFormat);  # modifies imagehash in place
# ------------------------------------------------------------------------------

sub _convertContentFields {
  my $content           = shift;
  my $imageHash         = shift;
  my $isFieldAutoFormat = shift;
  my $oldDataDir        = shift || die "no old data dir specified!";

  # load setup options
  my $setupOptionsPath    = "$oldDataDir/setup.dat.cgi";
  my $setupOptions        = &loadSetupOptions($setupOptionsPath);
  my $isDefaultAutoFormat = $setupOptions->{"default_autoformat"};

  # autoformat
  if ($isFieldAutoFormat) {
    $content =~ s|\n|<br/>\n|g;
  }

  # image placeholders - remove suffixes (except :url) and save removed alignment data
  while ($content =~ s/\*\*\*image(\d+):(right|left|center)\*\*\*/***image${1}***/i) {
    my $imageNum = $1;
    my $modifier = $2;

    if (!exists $imageHash->{'rows'}{$imageNum}) { next; }  # if no image, skip
    $imageHash->{'rows'}{$imageNum}{'align'} = $modifier;   # set alignment on image
  }

  return $content;
}

# ------------------------------------------------------------------------------
# Function    : importSetupOptions
# Description : remove previously imported art_field### fields and import extra
#               article fields defined in setup options
# Usage       : &IT::ArtMan::Upgrade::From141::importSetupOptions($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importSetupOptions {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $setupOptionsPath     = "$oldDataDir/setup.dat.cgi";

  # load old setup options
  my $setupOptions = &loadSetupOptions($setupOptionsPath);

  ### update Setup Options
  &{$displayStatusMessage}("Copying settings from Setup Options");
  $SETTINGS->{'confirm_erase_requests'}                 = $setupOptions->{'confirm_erase'} ? '1' : '0';
  $SETTINGS->{'admin_email'}                            ||= $setupOptions->{'mail_sender_email'};
  $SETTINGS->{'sender_email'}                           ||= $setupOptions->{'mail_sender_email'};
  $SETTINGS->{'sender_name'}                            ||= $setupOptions->{'mail_sender_name'};
  $SETTINGS->{'send_mail_with'}                         ||= uc $setupOptions->{'mail_mailer'};  # SENDMAIL or SMTP
  $SETTINGS->{'sendmail_path'}                          ||= $setupOptions->{'mail_sendmail'};
  $SETTINGS->{'smtp_hostname'}                          ||= $setupOptions->{'mail_smtp'};
  $SETTINGS->{'pop3_login_required_before_smtp'}        ||= $setupOptions->{'mail_pop3auth'} ? '1' : '0';
  $SETTINGS->{'pop3auth_hostname'}                      ||= $setupOptions->{'mail_pop3'};
  $SETTINGS->{'pop3auth_username'}                      ||= $setupOptions->{'mail_pop3pass'};
  $SETTINGS->{'pop3auth_password'}                      ||= $setupOptions->{'mail_pop3user'};
  $SETTINGS->{'maxCategoryDepth'}                       = $setupOptions->{'max_subcat_depth'};
  $SETTINGS->{'dynamicSSIparsing'}                    = $setupOptions->{'parse_ssi'} ? 'SERVER_RESOLVED' : 'EMULATED';
  $SETTINGS->{'serverResolvedSSI_httpAuthRequired'}     = $setupOptions->{'ssi_http_auth_username'} || $setupOptions->{'ssi_http_auth_password'};
  $SETTINGS->{'serverResolvedSSI_httpAuthUsername'}     = $setupOptions->{'ssi_http_auth_username'};
  $SETTINGS->{'serverResolvedSSI_httpAuthPassword'}     = $setupOptions->{'ssi_http_auth_password'};
  $SETTINGS->{'serverResolvedSSI_tempFileExtension'}    = $setupOptions->{'publish_fileext'};
  $SETTINGS->{'publishFileExtension'}                   = $setupOptions->{'publish_fileext'};
  $SETTINGS->{'titlebar'}                               = $setupOptions->{'titlebar'};
  $SETTINGS->{'check_upload_filesize'}                  = $setupOptions->{'uploadlimit'} ? '1' : '0';
  $SETTINGS->{'upload_filesize_max_kbytes'}             = $setupOptions->{'uploadmax'}     if $setupOptions->{'uploadmax'};
  $SETTINGS->{'image_upload_max_height'}                = $setupOptions->{'upload_height'} if $setupOptions->{'upload_height'};
  $SETTINGS->{'image_upload_max_width'}                 = $setupOptions->{'upload_width'}  if $setupOptions->{'upload_width'};
  $SETTINGS->{'accepted_upload_extensions'}             = $setupOptions->{'uploadext'};
  $SETTINGS->{'myPlaceholders'}                 ||= {};
  $SETTINGS->{'myPlaceholders'}{'homepageName'} = $setupOptions->{'gcat_name'};

  $SETTINGS->{'needsPublishAll'}                        = '1';

  &IT::App::Menu::Admin::saveAdminSettingsToFile($SETTINGS);


  ### Update Article sorting settings
  &{$displayStatusMessage}("Updating article sorting settings");
  my $articleTable  = $TABLE_PREFIX . "article";
  my @currentFields = &IT::DB::Schema::getFieldNames($articleTable);
  my %validFields = map { $_ => 1 } @currentFields;
  {
    my $sortFieldsArray = [];
    for my $num (1..3) {
      my $fieldname = $setupOptions->{"sort${num}field"} || next;
      if ($fieldname eq 'name')                 { $fieldname = "title"; }
      if ($fieldname =~ /^spare(\d+)/)          { $fieldname = "art_field$1"; }
      if ($fieldname eq 'stamp')                { $fieldname = "date"; }
      unless ($validFields{$fieldname})         { next; } # skip fields that aren't in schema
      if ($setupOptions->{"sort${num}reverse"}) { $fieldname = "-$fieldname"; }
      push @$sortFieldsArray, $fieldname;
    }

    $sortFieldsArray         = scalar(@$sortFieldsArray) ? $sortFieldsArray : ['-date', 'title'];
    &IT::DB::Schema::setTableAttribute($articleTable, 'defaultSort', $sortFieldsArray);
    &IT::FileLock::getLock('exclusive');
    &IT::DB::Schema::saveSchema($SCHEMA_FILEPATH);
    &IT::FileLock::releaseLock();
  }

  return 1;
}


# ------------------------------------------------------------------------------
# Function    : createArtman1ArticleFields
# Description : added needed fields and remove unneeded fields
# Usage       : &IT::ArtMan::Upgrade::From141::createArtman1ArticleFields($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub createArtman1ArticleFields {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $articleTable         = $TABLE_PREFIX . "article";

  ### Add Artman 1 fields
  &{$displayStatusMessage}("Create v1.41 article fields");
  &IT::FileLock::getLock('exclusive');

  # drop existing table
  if (&IT::DB::Schema::existsTable($articleTable))                   { &IT::DB::Schema::dropTable($articleTable); }
  if (&IT::DB::existsTable($CURRENT_CONNECTION_NAME, $articleTable)) { &IT::DB::dropTable($CURRENT_CONNECTION_NAME, $articleTable); }

  # define fields
  my $fieldDefs = [
    { 'appDisplayOrder' => '10',  'name' => 'num', 'appDisplayOnly' => '1', 'appFieldLabel' => 'Record Number', 'appFieldType' => 'none', 'appHiddenField' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '20',  'name' => 'createdTime', 'appDisplayOnly' => '1', 'appFieldLabel' => 'Created', 'appFieldOptions' => { 'customFieldHtml' => ' <tr> <td>Created</td> <td>$createdTime$ (by $createdByUserNum:getFieldValueByNum(accounts, username, Unknown User)$)</td> </tr> ', 'useCustomFieldHtml' => '1' }, 'appFieldType' => 'date', 'type' => 'int' },
    { 'appDisplayOrder' => '30',  'name' => 'createdByUserNum', 'appDisplayOnly' => '1', 'appFieldLabel' => 'Created By', 'appFieldType' => 'none', 'appHiddenField' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '40',  'name' => 'lastUpdatedTime', 'appDisplayOnly' => '1', 'appFieldLabel' => 'Last Updated', 'appFieldOptions' => { 'customFieldHtml' => ' <tr> <td>Last Updated</td> <td>$lastUpdatedTime$ (by $lastUpdatedByUserNum:getFieldValueByNum(accounts, username, Unknown User)$)</td> </tr>', 'useCustomFieldHtml' => '1' },  'appFieldType' => 'date', 'type' => 'int' },
    { 'appDisplayOrder' => '50',  'name' => 'lastUpdatedByUserNum', 'appDisplayOnly' => '1', 'appFieldLabel' => 'Last Updated By', 'appFieldType' => 'none', 'appHiddenField' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '60',  'name' => 'needsPublish', 'appDisplayOnly' => '1', 'appFieldLabel' => 'needsPublish', 'appFieldType' => 'none', 'appHiddenField' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '70',  'name' => '___separator1___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '80',  'name' => 'title', 'appFieldLabel' => 'Title', 'appFieldOptions' => { 'customFieldHtml' => ' <tr> <td style="padding-right:5px">$_fieldLabel$</td> <td><input type="text" name="$_fieldName:htmlEncoded$" value="$_fieldValue:htmlEncoded$" size="40" onchange="setDefaultFilenameValueTo(this.value)"/> </td> </tr> ', 'useCustomFieldHtml' => '1' },  'appFieldType' => 'textfield', 'appFieldValidationRules' => { 'valueIsRequired' => '1' },  'type' => 'string' },
    { 'appDisplayOrder' => '90',  'name' => 'filename', 'appFieldLabel' => 'Filename', 'appFieldOptions' => { 'customFieldHtml' => ' <tr> <td style="padding-right:5px">$_fieldLabel$</td> <td><input type="text" name="$_fieldName:htmlEncoded$" id="$_fieldName:htmlEncoded$" value="$_fieldValue:htmlEncoded$" size="40" onChange="this.value = replaceInvalidFilenameCharsWithUnderscores(this.value)" />.$settings.publishfileextension$</td> </tr> ', 'useCustomFieldHtml' => '1' },  'appFieldType' => 'textfield', 'appFieldValidationRules' => { 'valueIsRequired' => '1', 'valueMustBeUnique' => '1' },  'type' => 'string' },
#   User may have already defined featured and author fields
#    { 'appDisplayOrder' => '100', 'name' => 'author', 'appFieldLabel' => 'Author', 'appFieldType' => 'textfield', 'appUserEditable' => '1', 'type' => 'string' },
#    { 'appDisplayOrder' => '110', 'name' => 'featured', 'appFieldLabel' => 'Featured', 'appFieldOptions' => { 'checkbox_checkedValue' => 'Yes', 'checkbox_description' => 'Show this article first in lists' }, 'appFieldType' => 'checkbox', 'appUserEditable' => '1', 'type' => 'string' },
    { 'appDisplayOrder' => '120', 'name' => 'status', 'appCustomFieldType' => 'articleEditorArticleStatus', 'appFieldLabel' => 'Status', 'appFieldType' => 'custom', 'type' => 'string' },
    { 'appDisplayOrder' => '130', 'name' => '___separator7___', 'appFieldSeparator' => { 'separatorHTML' => '<tr> <td>&nbsp;</td> <td>&nbsp;</td> </tr> ', 'separatorHeaderText' => '', 'separatorType' => 'blankLine' }, 'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '140', 'name' => 'categoryNum', 'appCustomFieldType' => 'articleEditorPrimaryCategory', 'appFieldLabel' => 'Categories', 'appFieldType' => 'custom', 'type' => 'int' },
    { 'appDisplayOrder' => '150', 'name' => 'category2Num', 'appCustomFieldType' => 'articleEditorSecondaryCategory', 'appFieldType' => 'custom', 'appUserEditable' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '160', 'name' => 'category3Num', 'appCustomFieldType' => 'articleEditorSecondaryCategory', 'appFieldType' => 'custom', 'appUserEditable' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '170', 'name' => 'category4Num', 'appCustomFieldType' => 'articleEditorSecondaryCategory', 'appFieldType' => 'custom', 'appUserEditable' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '180', 'name' => 'category5Num', 'appCustomFieldType' => 'articleEditorSecondaryCategory', 'appFieldType' => 'custom', 'appUserEditable' => '1', 'type' => 'int' },
    { 'appDisplayOrder' => '190', 'name' => '___separator2___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => 'Article Content', 'separatorType' => 'headerBar' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '200', 'name' => 'date', 'appFieldLabel' => 'Date', 'appFieldOptions' => { 'date_showHourMinFields' => '1', 'date_showSecondsField' => '1' }, 'appFieldType' => 'date', 'appFieldValidationRules' => { }, 'type' => 'int' },
    { 'appDisplayOrder' => '210', 'name' => '___separator3___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '220', 'name' => 'summary', 'appFieldLabel' => 'Summary', 'appFieldOptions' => { 'textbox_heightInPixels' => '150', 'textbox_isWysiwyg' => '1' },  'appFieldType' => 'textbox', 'appUserEditable' => '1', 'type' => 'memo' },
    { 'appDisplayOrder' => '230', 'name' => '___separator4___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '240', 'name' => 'content', 'appFieldLabel' => 'Content', 'appFieldOptions' => { 'textbox_heightInPixels' => '350', 'textbox_isWysiwyg' => '1' }, 'appFieldType' => 'textbox', 'appUserEditable' => '1', 'type' => 'memo' },
    { 'appDisplayOrder' => '250', 'name' => 'image', 'appFieldLabel' => 'Upload Images', 'appFieldOptions' => { 'upload_maxUploads' => '25', 'upload_type' => 'media' },  'appFieldType' => 'upload', 'appUserEditable' => '1', 'type' => 'memo' },
    { 'appDisplayOrder' => '260', 'name' => '___separator5___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    { 'appDisplayOrder' => '270', 'name' => 'attachments', 'appFieldLabel' => 'File Attachments', 'appFieldOptions' => { 'upload_maxUploads' => '10', 'upload_type' => 'attachments' },  'appFieldType' => 'upload', 'appUserEditable' => '1', 'type' => 'memo' },
    { 'appDisplayOrder' => '280', 'name' => '___separator6___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },

    # v1.41 upgrade fields - these are only defined for v1.41 upgraders to import old data
    { 'appDisplayOrder' => '290', 'name' => 'archiveNum',   'appDisplayOnly' => '1', 'appFieldLabel' => 'archiveNum', 'type' => 'int', 'appUserEditable' => '1', 'appHiddenField' => '1', 'appFieldType' => 'textfield' },
    { 'appDisplayOrder' => '300', 'name' => 'articleType',  'appDisplayOnly' => '1', 'appFieldLabel' => 'articleType', 'type' => 'string', 'appUserEditable' => '1', 'appHiddenField' => '1', 'appFieldType' => 'textfield' },
    { 'appDisplayOrder' => '310', 'name' => 'externalLink',  'appFieldLabel' => 'External Link', 'type' => 'string', 'appFieldValidationRules' => { 'validationRuleType' => 'element', 'validationRule_validElementName' => 'url'}, 'appUserEditable' => '1', 'appFieldType' => 'textfield' },
    { 'appDisplayOrder' => '320', 'name' => '___separator8___', 'appFieldLabel' => 'separator', 'appFieldSeparator' => { 'separatorHTML' => undef, 'separatorHeaderText' => undef, 'separatorType' => 'blankLine' },  'appFieldType' => 'separator', 'appUserEditable' => '1', 'type' => 'none' },
    # /v1.41 upgrade fields
  ];


  # create new table
  &IT::DB::Schema::createTable($CURRENT_CONNECTION_NAME, $articleTable, $fieldDefs);
  &IT::DB::createTable($CURRENT_CONNECTION_NAME, $articleTable, $fieldDefs);



  ### Adjust Summary/Content Fields
  &{$displayStatusMessage}("Adjusting summary and content fields");
  my $setupOptionsPath = "$oldDataDir/setup.dat.cgi";
  my $setupOptions     = &loadSetupOptions($setupOptionsPath);

  # hide fields that aren't enabled
  &IT::DB::Schema::setFieldAttribute($articleTable, 'summary', 'appHiddenField',  '1') if !$setupOptions->{'summary_enabled'};
  &IT::DB::Schema::setFieldAttribute($articleTable, 'content', 'appHiddenField',  '1') if !$setupOptions->{'content_enabled'};

  # set field names
  &IT::DB::Schema::setFieldAttribute($articleTable, 'summary', 'appFieldLabel',  $setupOptions->{'summary_name'}) if $setupOptions->{'summary_name'};
  &IT::DB::Schema::setFieldAttribute($articleTable, 'content', 'appFieldLabel',  $setupOptions->{'content_name'}) if $setupOptions->{'content_name'};

  # change field types
  foreach my $fieldname (qw(summary content)) {
    my $isWysiwyg = $setupOptions->{"${fieldname}_type"} ? '1' : '0';
    my($appFieldOptions) = &IT::DB::Schema::getFieldAttributes($articleTable, $fieldname, 'appFieldOptions');
    $appFieldOptions->{'textbox_isWysiwyg'} = $isWysiwyg;
    &IT::DB::Schema::setFieldAttribute($articleTable, $fieldname, 'appFieldOptions', $appFieldOptions);
  }

  # set table description
  &IT::DB::Schema::setTableAttribute($articleTable, 'appTableDescription', 'Stores Article Data');

  # set article search fields
  &IT::DB::Schema::setTableAttribute($articleTable, 'appDefaultSearchFields', ['title', 'summary', 'content']);

  ### Save Schema
  &IT::DB::Schema::saveSchema($SCHEMA_FILEPATH);
  &IT::FileLock::releaseLock();

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : importUserDefinedArticleFields
# Description : remove previously imported art_field### fields and import extra
#               article fields defined in setup options
# Usage       : &IT::ArtMan::Upgrade::From141::importUserDefinedArticleFields($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importUserDefinedArticleFields {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $setupOptionsPath     = "$oldDataDir/setup.dat.cgi";
  my $articleTable         = $TABLE_PREFIX . "article";

  # error checking
  unless (ref $displayStatusMessage eq 'CODE') { die "displayStatusMessage must be a code ref!"; }

  ### remove previously imported fields from schema
  my @currentFields  = &IT::DB::Schema::getFieldNames($articleTable);
  my @importedFields = grep /^art_field/, @currentFields;

  if (@importedFields) {
    &IT::FileLock::getLock('exclusive');
    &{$displayStatusMessage}("Removing previously imported fields:");
    foreach my $fieldname (@importedFields) {
      &IT::DB::dropField($articleTable, $fieldname);
      &IT::DB::Schema::dropField($articleTable, $fieldname);
      &{$displayStatusMessage}("- $fieldname");
    }
    &IT::DB::Schema::saveSchema($SCHEMA_FILEPATH);
    &IT::FileLock::releaseLock();
  }


  # load old setup options
  my $setupOptions = &loadSetupOptions($setupOptionsPath);

  ### Add fields to schema
  &{$displayStatusMessage}("Importing Extra Article Fields from Setup Options");
  &IT::FileLock::getLock('exclusive');
  for my $num (1..20) {
    if ($setupOptions->{"sfield_enabled$num"}) {
      my $oldFieldOrder           = $setupOptions->{"sfield_order$num"};
      my $oldFieldLabelAndOptions = $setupOptions->{"sfield_name$num"};
      my $oldFieldType            = $setupOptions->{"sfield_type$num"};
      my $oldAutoFormatting       = $setupOptions->{"default_autoformat"};

      my $dbFieldType        = 'memo';
      my $fieldOrder         = $oldFieldOrder + 1000;
      my $fieldName          = "art_field$num";
      my $fieldLabel         = $oldFieldLabelAndOptions;
      my $appFieldType       = "";
      my $appFieldOptions    = {};

      if    ($oldFieldType == 1) {      # textfield
        $appFieldType = "textfield";
      }
      elsif ($oldFieldType == 2) {      # list
        $appFieldType = "list";
        $appFieldOptions->{'list_singleOrMultipleChoice'} = "single";
        $appFieldOptions->{'list_singleType'}             = "pulldown";
        $appFieldOptions->{'list_options'}                = [];

        # add options
        my $optionsAsCSV;
        if ($oldFieldLabelAndOptions =~ /([^(]+)\s+\((.+)\)/) {
          $fieldLabel   = $1;
          $optionsAsCSV = $2;
        }
        $optionsAsCSV =~ s/(^|[^\\]),/$1 ,/g;             # space out split commas "," to " ,"
        foreach my $option (split(/ ,/, $optionsAsCSV)) { # split on commas " ,"
          $option =~ s/\\,/,/go;                          # unbackslash real commas
          $option =~ s/(^\s+|\s+$)//go;                   # remove leading & trailing whitespace
          push @{$appFieldOptions->{'list_options'}}, {
            label             => $option,
            selectedByDefault => '',
            'value'           => $option,
          };
        }
      }
      elsif ($oldFieldType == 3) {      # textbox
        $appFieldType = "textbox";
        $appFieldOptions->{'textbox_noAutoFormat'} = $oldAutoFormatting ? 0 : 1;
      }
      elsif ($oldFieldType == 4) {      # checkbox
        $appFieldType = "checkbox";
        $appFieldOptions->{'checkbox_checkedValue'}   = "1";
        $appFieldOptions->{'checkbox_uncheckedValue'} = "0";
        if ($oldFieldLabelAndOptions =~ /([^(]+)\s+\((.+)\)/) {
           $fieldLabel                                = $1;
           $appFieldOptions->{'checkbox_description'} = $2;
        }
        $dbFieldType = "int";
      }
      elsif ($oldFieldType == 5) {      # textbox (wysiwyg)
        $appFieldType = "textbox";
        $appFieldOptions->{'textbox_isWysiwyg'} = 1;
      }

      # create new field
      &{$displayStatusMessage}("- $fieldName");
      &IT::DB::createField($articleTable, $fieldName, $dbFieldType);
      &IT::DB::Schema::createField($articleTable, $fieldName, $dbFieldType);
      &IT::DB::Schema::setFieldAttribute($articleTable, $fieldName, 'appDisplayOrder',         $fieldOrder);
      &IT::DB::Schema::setFieldAttribute($articleTable, $fieldName, 'appFieldLabel',           $fieldLabel);
      &IT::DB::Schema::setFieldAttribute($articleTable, $fieldName, 'appFieldType',            $appFieldType);
      &IT::DB::Schema::setFieldAttribute($articleTable, $fieldName, 'appUserEditable',         1);
      &IT::DB::Schema::setFieldAttribute($articleTable, $fieldName, 'appFieldOptions',         $appFieldOptions);
    }
  }

  ### Save Schema
  &IT::DB::Schema::saveSchema($SCHEMA_FILEPATH);
  &IT::FileLock::releaseLock();


  return 1;
}


# ------------------------------------------------------------------------------
# Function    : createPublishRules
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::createPublishRules($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub createPublishRules {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";

  ### Add Artman 1 fields
  &{$displayStatusMessage}("Creating publish rules");

  # load artman 1 settings
  my $setupOptionsPath     = "$oldDataDir/setup.dat.cgi";
  my $setupOptions = &loadSetupOptions($setupOptionsPath);

  # get category list format
  my $oldCategoryListFormat = $setupOptions->{'subcat_format'};
  my $newCategoryListFormat = '';
  if    ($oldCategoryListFormat eq 'show_all')             { $newCategoryListFormat = 'showAll'; }
  elsif ($oldCategoryListFormat eq 'one_level')            { $newCategoryListFormat = 'oneLevel'; }
  elsif ($oldCategoryListFormat eq 'two_level')            { $newCategoryListFormat = 'twoLevel'; }
  elsif ($oldCategoryListFormat eq 'selected_branch_only') { $newCategoryListFormat = 'selectedBranchOnly'; }
  elsif ($oldCategoryListFormat eq 'subcats_only')         { $newCategoryListFormat = 'subcatsOnly'; }
  else                                                     { $newCategoryListFormat = 'showAll'; }

  # get publish filenames (skip this for now, v1.41 had fixed filenaming based on whether filename was entered or not which can't be easily recreated)
  #my $globalIndexFilename         = $setupOptions->{'publish_index_fileprefix'};
  #my $categoryIndexFilenames      = $setupOptions->{'publish_cat_fileprefix'};
  #my $articlePageFilenames        = $setupOptions->{'publish_art_fileprefix'};
  #my $articlePrinterPageFilenames = $setupOptions->{'publish_art_printer_fileprefix'};

  # define publish rule records
  my @records = (
    { 'name' => 'Articles for Grouped Headlines', 'limitResultsRangeEnd' => "$setupOptions->{'publish_sampler_qty'}", 'categoryListFormat' => "showAll", 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>/include_articlesForGroupedHeadlines', 'articleListGlobalFileExt' => '', 'articleListGlobalFileName' => '', 'articleListPublishGlobalOrCategoryIndexes' => 'category', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147127505', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'included by Grouped Headlines', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '1', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157653', 'num' => '9', 'placeholder' => 'articlesForGroupedHeadlines', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/articlesForGroupedHeadlines.html', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Category Headlines',             'limitResultsRangeEnd' => "$setupOptions->{'publish_cat_headline_qty'}", 'categoryListFormat' => "showAll", 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>/include_categoryHeadlines', 'articleListGlobalFileExt' => '', 'articleListGlobalFileName' => '', 'articleListPublishGlobalOrCategoryIndexes' => 'category', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => '', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1149877553', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'one for each category', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '1', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157668', 'num' => '7', 'placeholder' => 'categoryHeadlines', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/categoryHeadlines.html', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Category Indexes',               'limitResultsRangeEnd' => "$setupOptions->{'publish_cat_qty'}", 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>/index', 'articleListGlobalFileExt' => '', 'articleListGlobalFileName' => '', 'articleListPublishGlobalOrCategoryIndexes' => 'category', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147108366', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'the main page for each category', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '1', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157684', 'num' => '2', 'placeholder' => 'categoryIndex', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/categoryIndex.html', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Global Headlines',               'limitResultsRangeEnd' => "$setupOptions->{'publish_headline_qty'}", 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '', 'articleListCategoryFileName' => '', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'include_globalHeadlines', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => '', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1160089071', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'from all categories', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157698', 'num' => '15', 'placeholder' => 'globalHeadlines', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/globalHeadlines.html', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Global Index',                   'limitResultsRangeEnd' => "$setupOptions->{'publish_index_qty'}", 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '', 'articleListCategoryFileName' => '', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'oneLevel', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147108233', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'the site "home page"', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157719', 'num' => '1', 'placeholder' => 'globalIndex', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/globalIndex.html', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Global RSS Feed',                'limitResultsRangeEnd' => '20', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '', 'articleListCategoryFileName' => '', 'articleListGlobalFileExt' => 'xml', 'articleListGlobalFileName' => 'rss', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147196196', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => 'date', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '1', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'article syndication through RSS', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157735', 'num' => '11', 'placeholder' => 'globalRSS', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articleLists/globalRSS.xml', 'type' => 'articleList', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '1', 'useRecordFilter' => '0' },
    { 'name' => 'Article Pages',                  'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<catFile>/<artFile>', 'articlePagesUseRuleForPreviews' => '1', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147108691', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'main article detail pages', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160154492', 'num' => '5', 'placeholder' => 'articlePage', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articlePages/articlePage.html', 'type' => 'articlePages', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Article Printer Pages',          'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<catFile>/<artFile>_printer', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147108754', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'printer-friendly versions of each article', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160155779', 'num' => '6', 'placeholder' => 'articlePrinterPage', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'articlePages/articlePrinterPage.html', 'type' => 'articlePages', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Category Breadcrumbs',           'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'breadcrumbs', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => '<catFile>/include_categoryBreadcrumb', 'categoryListPublishTopOrCategory' => 'category', 'categoryListTopMenuFileExt' => '', 'categoryListTopMenuFileName' => '', 'createdByUserNum' => '3', 'createdTime' => '1150230400', 'customMenuChildDepthVisible' => '0', 'customMenuDepthVisibleFromRoot' => '0', 'customMenuParentVisibility' => 'parentsOnly', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'one for each category', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157764', 'num' => '12', 'placeholder' => 'categoryBreadcrumb', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'categoryMenus/categoryBreadcrumb.html', 'type' => 'categoryMenu', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Category Menus',                 'categoryListFormat'   => "$newCategoryListFormat", 'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => '<catFile>/include_categoryMenu', 'categoryListPublishTopOrCategory' => 'category', 'categoryListTopMenuFileExt' => '', 'categoryListTopMenuFileName' => '', 'createdByUserNum' => '3', 'createdTime' => '1147108550', 'customMenuChildDepthVisible' => '1', 'customMenuDepthVisibleFromRoot' => '0', 'customMenuParentVisibility' => 'parentsOnly', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'current category highlighted', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157777', 'num' => '3', 'placeholder' => 'categoryMenu', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'categoryMenus/categoryMenu.html', 'type' => 'categoryMenu', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Global Menu',                    'categoryListFormat'   => "$newCategoryListFormat", 'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListMenuFileExt' => '', 'categoryListMenuFileName' => '', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'include_globalMenu', 'createdByUserNum' => '3', 'createdTime' => '1147108550', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => '3', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'default menu', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157790', 'num' => '4', 'placeholder' => 'globalMenu', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'categoryMenus/categoryMenu.html', 'type' => 'categoryMenu', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Grouped Headlines',              'limitResultsRangeEnd' => '10', 'alwaysPublish' => '0', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'custom', 'categoryListMenuFileExt' => '', 'categoryListMenuFileName' => '', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'include_groupedHeadlines', 'createdByUserNum' => '3', 'createdTime' => '1147127290', 'customMenuChildDepthVisible' => '0', 'customMenuDepthVisibleFromRoot' => '1', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'grouped by category', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157815', 'limitResultsRangeStart' => '1', 'num' => '8', 'placeholder' => 'groupedHeadlines', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'pageName', 'template' => 'categoryMenus/groupedHeadlines.html', 'type' => 'categoryMenu', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
    { 'name' => 'Search Box',                     'limitResultsRangeEnd' => '10', 'limitResultsRangeStart' => '1', 'alwaysPublish' => '1', 'articleListCategoryFileExt' => '<ext>', 'articleListCategoryFileName' => '<catFile>', 'articleListGlobalFileExt' => '<ext>', 'articleListGlobalFileName' => 'index', 'articleListPublishGlobalOrCategoryIndexes' => 'global', 'articlePagesFileExt' => '<ext>', 'articlePagesFileName' => '<artFile>', 'articlePagesUseRuleForPreviews' => '0', 'categoryFilterIncludeOrExclude' => 'include', 'categoryFilterSelections' => '', 'categoryListFormat' => 'showAll', 'categoryListMenuFileExt' => '<ext>', 'categoryListMenuFileName' => 'menu_<catFile>', 'categoryListPublishTopOrCategory' => 'top', 'categoryListTopMenuFileExt' => '<ext>', 'categoryListTopMenuFileName' => 'menu_top', 'createdByUserNum' => '3', 'createdTime' => '1147108947', 'customMenuChildDepthVisible' => 'all', 'customMenuDepthVisibleFromRoot' => 'all', 'customMenuParentVisibility' => 'nothing', 'customMenuShowSelectedCategory' => '1', 'customPublishDir' => '', 'customPublishUrl' => '', 'customSortField1' => '', 'customSortField2' => '', 'customSortField3' => '', 'customSortReverse1' => '0', 'customSortReverse2' => '0', 'customSortReverse3' => '0', 'description' => 'a basic search box', 'disabled' => '0', 'filterAllOrAny' => 'all', 'filterField1' => '', 'filterField2' => '', 'filterField3' => '', 'filterOperator1' => '=', 'filterOperator2' => '=', 'filterOperator3' => '=', 'filterValue1' => '', 'filterValue2' => '', 'filterValue3' => '', 'includeChildArticles' => '0', 'lastUpdatedByUserNum' => '3', 'lastUpdatedTime' => '1160157912', 'num' => '10', 'placeholder' => 'searchbox', 'singlePageFileExt' => '<ext>', 'singlePageFileName' => 'include_searchbox', 'template' => 'singlePages/basicSearchBox.html', 'type' => 'singlePage', 'useCategoryFilter' => '0', 'useCustomPublishDir' => '0', 'useCustomSorting' => '0', 'useRecordFilter' => '0' },
  );

  # insert records
  my $publishRulesTable = $TABLE_PREFIX . "publishrules";
  &IT::FileLock::getLock('exclusive');
  &IT::DB::disableAutoSequencing($publishRulesTable);
  &IT::DB::delete($publishRulesTable, 'num > "0"');   # remove all existing records
  &IT::DB::insert($publishRulesTable, \@records);
  &IT::DB::enableAutoSequencing($publishRulesTable);
  &IT::FileLock::releaseLock();

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : removeCurrentCategories
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::removeCurrentCategories($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub removeCurrentCategories {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";

  my $categoryFilepath     = "$oldDataDir/category.dat";
  my $categoryTable        = $TABLE_PREFIX . "category";

  ### remove current categories
  &{$displayStatusMessage}("Removing current categories");
  &IT::FileLock::getLock('exclusive');
  &IT::DB::delete($categoryTable, 'num > "0"');
  &IT::FileLock::releaseLock();

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : importCategories
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::importCategories($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importCategories {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $categoryFilepath     = "$oldDataDir/category.dat";
  my $categoryTable        = $TABLE_PREFIX . "category";

  ### On first function call
  if (!$CATEGORY_FH) {

    # display header
    &{$displayStatusMessage}("Importing Categories");

    # load file
    $CATEGORY_FH = &_openDataFile($categoryFilepath);

   # Skip to current record
   &_skipRecords($CATEGORY_FH, $$currentStep);        # skip to current record
  }

  ### Load/Import Next record
  my $oldCategory = &_getNextRecord($CATEGORY_FH, $CATEGORY_FIELDS);
  if ($oldCategory) {

    # display message
    &{$displayStatusMessage}("- Category $oldCategory->{'num'}: $oldCategory->{'name'}");

    # create new record
    my $newRecord = {};
    $newRecord->{'num'}                         = $oldCategory->{'num'};
    $newRecord->{'createdTime'}                 = $oldCategory->{'created'};
    $newRecord->{'createdByUserNum'}            = $oldCategory->{'created_by'};
    $newRecord->{'lastUpdatedTime'}             = $oldCategory->{'updated'};
    $newRecord->{'lastUpdatedByUserNum'}        = $oldCategory->{'updated_by'};
    $newRecord->{'needsPublish'}                = 1;
    $newRecord->{'needsPublishForArticleLists'} = 1;
    $newRecord->{'globalOrder'}                 = $oldCategory->{'global_order'};
    $newRecord->{'siblingOrder'}                = $oldCategory->{'sibling_order'};
    $newRecord->{'lineage'}                     = ''; # updated below by IT::ArtMan::CustomMenus::Category::updateDepthOrderAndLineageFields
    $newRecord->{'depth'}                       = ''; # updated below by IT::ArtMan::CustomMenus::Category::updateDepthOrderAndLineageFields
    $newRecord->{'name'}                        = $oldCategory->{'name'};

    if ($oldCategory->{'pub_fileprefix_specify'}) { $newRecord->{'filename'} = $oldCategory->{'pub_fileprefix'}; }
    else                                          { $newRecord->{'filename'} = &_generateFilenameFromTitle($oldCategory->{'name'}, $oldCategory->{'num'}); };

    $newRecord->{'parentNum'}                   = $oldCategory->{'parent'};
    $newRecord->{'type'}                        = $oldCategory->{'hidden'} ? 'unlinked' : 'regular';
    $newRecord->{'locked'}                      = $oldCategory->{'display_only'};
    $newRecord->{'hideOnSearchForm'}            = 0;

    # copy image
    my $oldSetupOptions = &loadSetupOptions("$oldDataDir/setup.dat.cgi");
    my $oldUploadDir    = $oldSetupOptions->{'dir_upload'};
    my $oldFilename     = $oldCategory->{'ufile_image'};
    my $sourcePath      = "$oldUploadDir/$oldFilename";
    my $newFileName     = $oldFilename;
    my $imageExists     = $oldFilename && -f $sourcePath;
    if ($imageExists) {
      my $newUploadDir = "$SETTINGS->{'upload_dir'}/1";
      my $targetPath   = "$newUploadDir/$newFileName";
      while (-e $targetPath) {  # get unused filename
        $newFileName = &IT::File::incrementFileNameOrPath($newFileName);
        $targetPath  = "$newUploadDir/$newFileName";
      }
      &{$displayStatusMessage}("    - Copying $oldFilename");
      &IT::File::createDirPath($newUploadDir) || die "Couldn't create '$newUploadDir' : $!";
      &IT::File::copyFile($sourcePath, $targetPath) || die "Error copying file: $!";
    }

    # set upload field value
    $newRecord->{'image'} = '';
    if ($imageExists) {
      my $newFilepathFromUploadDir = "1/$newFileName";
      $newRecord->{'image'} = &_createUploadFieldDataString( [[$newFilepathFromUploadDir, $oldCategory->{'imageheight_image'}, $oldCategory->{'imagewidth_image'}]]);
    }

    # add new record
    &IT::FileLock::getLock('exclusive');
    &IT::DB::disableAutoSequencing($categoryTable);
    &IT::DB::insert($categoryTable, [ $newRecord ]);
    &IT::DB::enableAutoSequencing($categoryTable);
    &IT::FileLock::releaseLock();

    ${$currentStep}++;
    return 0;
  }

  ### If no more records
  if (!$oldCategory) {
    &_closeDataFile(\$CATEGORY_FH);

    ### Update meta field on categories
    &IT::ArtMan::CustomMenus::Category::updateDepthOrderAndLineageFields();

    return 1; # return true, stage complete
  }

}

# ------------------------------------------------------------------------------
# Function    : removeCurrentArticles
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::removeCurrentArticles($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub removeCurrentArticles {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $articleTable        = $TABLE_PREFIX . "article";

  ### remove current categories
  &{$displayStatusMessage}("Removing current articles");
  &IT::FileLock::getLock('exclusive');
  &IT::DB::delete($articleTable, 'num > "0"');
  &IT::FileLock::releaseLock();

  return 1;
}



# ------------------------------------------------------------------------------
# Function    : removeCurrentUploads
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::removeCurrentUploads($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub removeCurrentUploads {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";

  &{$displayStatusMessage}("Removing current uploads");

  # error checking
  unless ($SETTINGS->{'upload_dir'})    { die "no upload dir defined!"; }
  unless (-d $SETTINGS->{'upload_dir'}) { die "upload dir isn't a directory!"; }

  #
  my $count = 0;
  my @dirTree = &IT::File::dirTree($SETTINGS->{'upload_dir'});
  foreach my $file (@dirTree) {
    if (-d $file) { next; } # skip dirs
    if ($file =~ /unsavedUploadExpiryTimes\.dat/) { next; } # skip data file
    if ($file =~ m|/_vti_cnf|) { next; }                   # skip MS Frontpage dirs
    unlink $file;
    $count++;
  }

  &{$displayStatusMessage}("Removed $count files from upload dir");
  return 1;
}



# ------------------------------------------------------------------------------
# Function    : importArticles
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::importArticles($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importArticles {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $articleFilepath     = "$oldDataDir/article.dat";
  my $articleTable        = $TABLE_PREFIX . "article";

  ### On first function call
  if (!$ARTICLE_FH) {

    # display header
    &{$displayStatusMessage}("Importing Articles");

    # load file
    $ARTICLE_FH = &_openDataFile($articleFilepath);

   # Skip to current record
   &_skipRecords($ARTICLE_FH, $$currentStep);        # skip to current record
  }


  ### Load/Import Next record
  my $oldArticle = &_getNextRecord($ARTICLE_FH, $ARTICLE_FIELDS);
  if ($oldArticle) {

    &{$displayStatusMessage}("- Article $oldArticle->{'num'}: $oldArticle->{'name'}");
    &_importOldArticleRecord($oldArticle, $oldDataDir, $displayStatusMessage);

    ${$currentStep}++;
    return 0;
  }

  ### If no more records
  if (!$oldArticle) {
    &_closeDataFile(\$ARTICLE_FH);

    return 1; # return true, stage complete
  }

}

# ------------------------------------------------------------------------------
# Function    : importArchives
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::importArchives($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importArchives {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";

  ### Load Archive - On first function call
  if (!$ARCHIVE_FH) {
    &{$displayStatusMessage}("Importing Archives...");                               # display header
    my $archiveFilepath = "$oldDataDir/archive.dat";
    $ARCHIVE_FH = &_openDataFile($archiveFilepath);                               # load file
    &_skipRecords($ARCHIVE_FH, $$currentStep);     # skip to current record
  }

  # Get next archive
  if (!$CURRENT_ARCHIVE || $$currentSubstep == 0) {
    $CURRENT_ARCHIVE = &_getNextRecord($ARCHIVE_FH, $ARCHIVE_FIELDS);

    # if no more archives, stage complete
    if (!$CURRENT_ARCHIVE) {
      &_closeDataFile(\$ARCHIVE_FH);
      undef $CURRENT_ARCHIVE;
      return 1;
    }

    &{$displayStatusMessage}("Importing Archive: $CURRENT_ARCHIVE->{'name'}");

    # skip empty archives
    if ($CURRENT_ARCHIVE->{'count'} == 0) {
      &{$displayStatusMessage}("- no articles");
      sleep 1;
      ${$currentStep}++;
      ${$currentSubstep} = 0;
      return 0;
    }
  }


  ### Load article
  if (!$ARTICLE_FH) {
    # load current archive article
    my $archiveNum      = $CURRENT_ARCHIVE->{'num'} || die "No archive num!";
    my $articleFilepath = "$oldDataDir/archive${archiveNum}.dat";
    $ARTICLE_FH          = &_openDataFile($articleFilepath);
    &_skipRecords($ARTICLE_FH, $$currentSubstep);     # skip to current record
  }

  ### Load/Import Archive Articles
  my $oldArticle = &_getNextRecord($ARTICLE_FH, $ARTICLE_FIELDS);
  if ($oldArticle) {
    $oldArticle->{'archiveNum'} = $CURRENT_ARCHIVE->{'num'};

    &{$displayStatusMessage}("- Article $oldArticle->{'num'}: $oldArticle->{'name'}");
    &_importOldArticleRecord($oldArticle, $oldDataDir, $displayStatusMessage);

    ${$currentSubstep}++; # move to next article
    return 0;             # stage not yet complete
  }


  ### If no more articles in this file
  if (!$oldArticle) {
    &_closeDataFile(\$ARTICLE_FH);
    ${$currentStep}++;   # move to next archive if no more articles in this one
    ${$currentSubstep} = 0;
    return 0;            # stage not yet complete
  }

}

# ------------------------------------------------------------------------------
# Function    : _importOldArticleRecord
# Description :
# Usage       : &{$displayStatusMessage}("- Article $oldArticle->{'num'}: $oldArticle->{'name'}");
# Usage       : &_importOldArticleRecord(\$oldArticleRecord);
# ------------------------------------------------------------------------------

sub _importOldArticleRecord {
  my $oldArticle   = shift || die "no old article record specified!";
  my $oldDataDir           = shift || die "no old data dir specified!";
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $articleTable = $TABLE_PREFIX . "article";

  # create new record
  my $statusNumToName = { '1' => 'pending', '2' => 'visible', '3' => 'hidden' };
  my $newRecord       = {};
  $newRecord->{'num'}                  = $oldArticle->{'num'};
  $newRecord->{'createdTime'}          = $oldArticle->{'created'};
  $newRecord->{'createdByUserNum'}     = $oldArticle->{'created_by'};
  $newRecord->{'lastUpdatedTime'}      = $oldArticle->{'updated'};
  $newRecord->{'lastUpdatedByUserNum'} = $oldArticle->{'updated_by'};
  $newRecord->{'needsPublish'}         = 1;
  $newRecord->{'title'}                = $oldArticle->{'name'};
  $newRecord->{'filename'}             = $oldArticle->{'filename'} || &_generateFilenameFromTitle($oldArticle->{'name'}, $oldArticle->{'num'});
  $newRecord->{'categoryNum'}          = $oldArticle->{'cat'};

  my $hour = $oldArticle->{'hour'} || 12; # artman 1 stores 12 as 0
  my $sec = 0;
  $newRecord->{'date'}                 = &IT::Date::getEpochTimeFromDateValues($oldArticle->{'year'}, $oldArticle->{'mon'}, $oldArticle->{'day'}, $hour, $oldArticle->{'min'}, $sec, $oldArticle->{'ampm'});
  $newRecord->{'status'}               = $statusNumToName->{$oldArticle->{'status'}};
  $newRecord->{'archiveNum'}           = $oldArticle->{'archiveNum'};
  $newRecord->{'articleType'}          = $oldArticle->{'art_type'}; # not used but stored if case user wants to use info
  $newRecord->{'externalLink'}         = $oldArticle->{'link'};
  $newRecord->{'attachments'}          = &_createUploadFieldDataString( [[$oldArticle->{'ufile_document'}, '', '']]);

  # copy uploads and get uploadStruct
  my @imageArray;
  for my $num (1..25) {
    my $oldFilename = $oldArticle->{"ufile_image$num"};
    if (!$oldFilename) { next; }

    # copy image to new upload dir
    my $oldUploadDir = &getOldUploadDirectory($oldDataDir, $oldArticle->{'cat'});
    my $oldFilePath  = "$oldUploadDir/$oldFilename";
    my $newUploadDir = "$SETTINGS->{'upload_dir'}/1";
    my $newFileName  = $oldFilename;
    if (-f $oldFilePath) {
      my $sourcePath = $oldFilePath;
      my $targetPath = "$newUploadDir/$newFileName";
      while (-e $targetPath) {  # get unused filename
        $newFileName = &IT::File::incrementFileNameOrPath($newFileName);
        $targetPath  = "$newUploadDir/$newFileName";
      }

      &{$displayStatusMessage}("    - Copying $oldFilename");
      &IT::File::createDirPath($newUploadDir) || die "Couldn't create '$newUploadDir' : $!";
      &IT::File::copyFile($sourcePath, $targetPath) || die "Error copying file: $!";
    }

    # add image to new article record
    my $newFilepathFromUploadDir = "1/$newFileName";
    push @imageArray, [$newFilepathFromUploadDir, $oldArticle->{"imageheight_image$num"}, $oldArticle->{"imagewidth_image$num"}, $oldArticle->{"imagename$num"}, $oldArticle->{"caption$num"}, $num ];
  }
  my $uploadListHash = &_createUploadFieldDataHash(\@imageArray);

  # get content fields
  $newRecord->{'summary'} = &_convertContentFields($oldArticle->{'summary'}, $uploadListHash, $oldArticle->{'summary_format'}, $oldDataDir);
  $newRecord->{'content'} = &_convertContentFields($oldArticle->{'content'}, $uploadListHash, $oldArticle->{'content_format'}, $oldDataDir);
  for my $num (1..20) {
    my $oldFieldname  = "spare$num";
    my $newFieldname  = "art_field$num";
    my $autoFormat    = $oldArticle->{"format_$oldFieldname"};
    unless (&IT::DB::Schema::existsField($articleTable, $newFieldname)) { next; } # skip if field not in use (and thus, not imported)

    $newRecord->{$newFieldname} = &_convertContentFields($oldArticle->{$oldFieldname}, $uploadListHash, $autoFormat, $oldDataDir);
  }

  # get images field content (_convertContentFields modifies $uploadListHash so do this last)
  $newRecord->{'image'} = $uploadListHash ? &IT::DhtmlFieldLists::convertListStructToString($uploadListHash) : '';

  ### add new record
  &IT::FileLock::getLock('exclusive');
  &IT::DB::disableAutoSequencing($articleTable);

  eval { &IT::DB::insert($articleTable, [ $newRecord ]); };

  # increment record number and try again on record number collisions
  if ($@) {
    my $recordNumberCollision = ($@ =~ /insert records have sequence numbers that already exist/i);
    if ($recordNumberCollision) {
      my $highestRecordNumber = &IT::DB::getSequence($articleTable);
      $newRecord->{'num'}     = $highestRecordNumber + 1;
      &IT::DB::insert($articleTable, [ $newRecord ]);
    }
    else { die $@; }
  }

  &IT::DB::enableAutoSequencing($articleTable);
  &IT::FileLock::releaseLock();

}

# ------------------------------------------------------------------------------
# Function    : _generateFilenameFromTitle
# Description :
# Usage       : my $generatedName = &_generateFilenameFromTitle($title, $recordNum);
# ------------------------------------------------------------------------------

sub _generateFilenameFromTitle {
  my $title     = shift;
  my $recordNum = shift || die "no record number specified!";

  ### generate filename
  my $generatedFilename = $title;
  $generatedFilename   =~ s/[^a-zA-Z0-9\-\_]+/_/g;                      # replace non-alphanumeric chars with underscores
  $generatedFilename   =~ s/_+/_/g;                                     # remove double underscores
  $generatedFilename   =~ s/(^_+|_+$)//g;                               # remove leading and trailing underscores

  # truncate filename
  while (length $generatedFilename > 50) { chop $generatedFilename; }

  # add record number suffic to ensure it's unique
  $generatedFilename .= "_$recordNum";

  return $generatedFilename;
}

# ------------------------------------------------------------------------------
# Function    : getOldUploadDirectory
# Description :
# Usage       : my $oldUploadDir = &IT::ArtMan::Upgrade::From141::getOldUploadDirectory($oldDataDir, $articleCategoryNum);
# ------------------------------------------------------------------------------

sub getOldUploadDirectory {
  my $oldDataDir         = shift || die "no oldDataDir value specified!";
  my $articleCategoryNum = shift;

  # get old upload dir
  my $oldUploadDir;
  my $oldSetupOptions    = &loadSetupOptions("$oldDataDir/setup.dat.cgi");
  my $oldCategories      = &loadOldCategoriesHash($oldDataDir);
  if ($oldCategories->{$articleCategoryNum}{'publish_specify'}) { $oldUploadDir = $oldCategories->{$articleCategoryNum}{'dir_upload'}; }
  else                                                          { $oldUploadDir = $oldSetupOptions->{'dir_upload'}; }

  return $oldUploadDir;
}

# ------------------------------------------------------------------------------
# Function    : doesAccountExistInVersion1
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::doesAccountExistInVersion1($oldDataDir, $accountName);
# ------------------------------------------------------------------------------

sub doesAccountExistInVersion1 {
  my $oldDataDir  = shift || die "no old datadir specified!";
  my $accountName = shift || die "no account name specified!";

  # check for duplicate account
  my $isDuplicateAccounts = 0;
  my $accountsFilepath = "$oldDataDir/account.dat.cgi";
  $ACCOUNTS_FH         = &_openDataFile($accountsFilepath);
  while (my $record = &_getNextRecord($ACCOUNTS_FH, $ACCOUNT_FIELDS)) {
    if (lc $record->{'id'} eq lc $COOKIE->{'adminUsername'}) {
      $isDuplicateAccounts = 1;
      last;
    }
  }
  &_closeDataFile(\$ACCOUNTS_FH);

  return $isDuplicateAccounts;
}

# ------------------------------------------------------------------------------
# Function    : removeAllAccountsExceptCurrentUser
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::removeAllAccountsExceptCurrentUser($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub removeAllAccountsExceptCurrentUser {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $accountsTable        = $TABLE_PREFIX . "accounts";

  # error checking
  unless ($COOKIE->{'adminUsername'}) { die "No adminUsername cookie defined!"; }

  ### remove current accounts
  &{$displayStatusMessage}("Removing accounts (except for $COOKIE->{'adminUsername'})");

  &IT::FileLock::getLock('exclusive');
  my $escapedUsername = &IT::DB::getEscapedValue($COOKIE->{'adminUsername'});
  &IT::DB::delete($accountsTable, "username != '$escapedUsername'");
  &IT::FileLock::releaseLock();

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : reNumberCurrentUser
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::reNumberCurrentUser($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub reNumberCurrentUser {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $accountsTable        = $TABLE_PREFIX . "accounts";

  # get highest used record number (in old version)
  my $highestRecordNumber = 0;
  my $accountsFilepath = "$oldDataDir/account.dat.cgi";
  $ACCOUNTS_FH         = &_openDataFile($accountsFilepath);
  while (my $record = &_getNextRecord($ACCOUNTS_FH, $ACCOUNT_FIELDS)) {
    if ($record->{'num'} > $highestRecordNumber) { $highestRecordNumber = $record->{'num'}; }
  }
  &_closeDataFile(\$ACCOUNTS_FH);

  # get current user record number (in new version)
  my $oldNumber;
  &IT::FileLock::getLock('exclusive');
  my $escapedUsername = &IT::DB::getEscapedValue($COOKIE->{'adminUsername'}) || die "no username cookie found!";
  my $records = &IT::DB::select({
    'tableName'  => $TABLE_PREFIX . 'accounts',
    'whereQuery' => "username = '$escapedUsername'",
    'limit'      => [0,1],
  });
  my $oldRecord = $records->[0] || die "Couldn't find current user in database!";
  $oldNumber = $oldRecord->{'num'};
  &IT::FileLock::releaseLock();

  # renumber current user
  my $newNumber = $highestRecordNumber + 1;
  if ($newNumber != $oldNumber) {
    &{$displayStatusMessage}("Renumbering admin account ($COOKIE->{'adminUsername'})");
    my $newRecord = $oldRecord;
    $newRecord->{'num'} = $newNumber;
    &IT::FileLock::getLock('exclusive');
    &IT::DB::disableAutoSequencing($accountsTable);
    &IT::DB::insert($accountsTable, [ $newRecord ]);        # insert duplicate record with new number
    &IT::DB::delete($accountsTable, "num = '$oldNumber'") || die "Couldn't erase old numbered admin account (num = '$oldNumber')"; # erase old record
    &IT::DB::enableAutoSequencing($accountsTable);
    &IT::FileLock::releaseLock();
  }

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : importAccounts
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::importAccounts($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importAccounts {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $accountsFilepath     = "$oldDataDir/account.dat.cgi";
  my $accountsTable        = $TABLE_PREFIX . "accounts";

  ### On first function call
  if (!$ACCOUNTS_FH) {

    # display header
    &{$displayStatusMessage}("Importing Accounts");

    # load file
    $ACCOUNTS_FH = &_openDataFile($accountsFilepath);

    # Skip to current record
    &_skipRecords($ACCOUNTS_FH, $$currentStep);     # skip to current record
  }

  ### Load/Import Next record
  my $oldAccount = &_getNextRecord($ACCOUNTS_FH, $ACCOUNT_FIELDS);
  if ($oldAccount) {

    # display message
    &{$displayStatusMessage}("- $oldAccount->{'name'}");

    # create new record
    my $newRecord = {};
    $newRecord->{'num'}                         = $oldAccount->{'num'};
    $newRecord->{'createdTime'}                 = $oldAccount->{'created'};
    $newRecord->{'createdByUserNum'}            = $oldAccount->{'created_by'};
    $newRecord->{'lastUpdatedTime'}             = $oldAccount->{'updated'};
    $newRecord->{'lastUpdatedByUserNum'}        = $oldAccount->{'updated_by'};

    if ($oldAccount->{'acl'} == 1) { $newRecord->{'accessLevel'} = 0; } # disabled users now have access level of 0 instead of 1
    else                           { $newRecord->{'accessLevel'} = $oldAccount->{'acl'} }

    $newRecord->{'username'}                    = $oldAccount->{'id'};
    $newRecord->{'password'}                    = $oldAccount->{'pw'};
    $newRecord->{'fullname'}                    = $oldAccount->{'name'};
    $newRecord->{'email'}                       = $oldAccount->{'email'};
    $newRecord->{'notes'}                       = $oldAccount->{'notes'};

    my %emailNotificationEvents = map { $_ => 1 } split(/,/, $oldAccount->{'email_notification_events'});
    $newRecord->{'emailOnArticleCreated'}       = $emailNotificationEvents{'1'};
    $newRecord->{'emailOnArticlePublish'}       = $emailNotificationEvents{'2'};
    $newRecord->{'emailOnArticleModified'}      = $emailNotificationEvents{'3'};
    $newRecord->{'emailOnArticleErased'}        = $emailNotificationEvents{'4'};

    # get category access rights
    my @categoryAccessRights = split(/,/, $oldAccount->{'cats'});
    push(@categoryAccessRights, 'all') if $oldAccount->{'all_cats'};
    my @categoryEmailRights  = split(/,/, $oldAccount->{'email_cats'});
    push(@categoryEmailRights, 'all')  if $oldAccount->{'email_all_cats'};

    $newRecord->{'categoryRights'}             .= join ",", @categoryAccessRights;
    $newRecord->{'categoryRights'}             .= "\t";
    $newRecord->{'categoryRights'}             .= join ",", @categoryEmailRights;

    # error checking
    if ($COOKIE->{'adminUsername'} &&
        lc $newRecord->{'username'} eq lc $COOKIE->{'adminUsername'}) {
      die "Current username ($COOKIE->{'adminUsername'}) exists in both versions!  Rename user in new version and try again!";
    }

    # add new record
    &IT::FileLock::getLock('exclusive');
    &IT::DB::disableAutoSequencing($accountsTable);
    &IT::DB::insert($accountsTable, [ $newRecord ]);
    &IT::DB::enableAutoSequencing($accountsTable);
    &IT::FileLock::releaseLock();

    ${$currentStep}++;
    return 0;
  }

  ### If no more records
  if (!$oldAccount) {
    &_closeDataFile(\$ACCOUNTS_FH);

    return 1; # return true, stage complete
  }

}




# ------------------------------------------------------------------------------
# Function    : importTemplates
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::importTemplates($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub importTemplates {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";

  &{$displayStatusMessage}("Copying standard artman 1 templates");

  # error checking
  my $newTemplateDir = &IT::File::cleanPath("$CGIDIR/../templates/");
  unless (-e $newTemplateDir) { die "Couldn't find template dir ($newTemplateDir)!"; }
  unless (-d $newTemplateDir) { die "Template dir isn't a directory ($newTemplateDir)!"; }

  # get list of old templates
  my $oldTemplateDir = &IT::File::cleanPath("$oldDataDir/../templates/");
  my @oldTemplates;
  push @oldTemplates, &IT::File::dirList("$oldTemplateDir/");
  push @oldTemplates, &IT::File::dirList("$oldTemplateDir/article/");
  push @oldTemplates, &IT::File::dirList("$oldTemplateDir/headlines/");
  push @oldTemplates, &IT::File::dirList("$oldTemplateDir/index/");
  push @oldTemplates, &IT::File::dirList("$oldTemplateDir/search/");

  foreach my $sourcePath (@oldTemplates) {
    if (-d $sourcePath)               { next; }
    if ($sourcePath =~ m|/_vti_cnf|) { next; } # skip MS Frontpage dirs


    my $oldPathFromTemplates = $sourcePath;
    $oldPathFromTemplates =~ s|^.*?/templates/||si || die "Couldn't remove old template path!";
    my $oldFilename          = $oldPathFromTemplates;
    $oldFilename             =~ s|^.*?([^\\\/]+)$|$1|si || die "Couldn't remove old template relative path!";

    # get new target path
    my ($targetPath1, $targetPath2);
    if    ($oldPathFromTemplates =~ m|article/default.html$|i)         { $targetPath1 = "$newTemplateDir/articlePages/articlePage.html"; }
    elsif ($oldPathFromTemplates =~ m|article/default_printer.html$|i) { $targetPath1 = "$newTemplateDir/articlePages/articlePrinterPage.html"; }
    elsif ($oldPathFromTemplates =~ m|category_breadcrumb.html$|i)     { $targetPath1 = "$newTemplateDir/categoryMenus/categoryBreadcrumb.html"; }
    elsif ($oldPathFromTemplates =~ m|category_list.html$|i)           { $targetPath1 = "$newTemplateDir/categoryMenus/categoryMenu.html"; }
    elsif ($oldPathFromTemplates =~ m|search/search_form.html$|i)      { $targetPath1 = "$newTemplateDir/singlePages/basicSearchBox.html"; }
    elsif ($oldPathFromTemplates =~ m|search/advanced_search.html$|i)  { $targetPath1 = "$newTemplateDir/searchEngine/searchForm.html"; }
    elsif ($oldPathFromTemplates =~ m|search/search_results.html$|i)   { $targetPath1 = "$newTemplateDir/searchEngine/searchResults.html"; }
    elsif ($oldPathFromTemplates =~ m|index/default.html$|i)           { $targetPath1 = "$newTemplateDir/articleLists/globalIndex.html";
                                                                         $targetPath2 = "$newTemplateDir/articleLists/categoryIndex.html"; }
    elsif ($oldPathFromTemplates =~ m|headlines/default.html$|i)       { $targetPath1 = "$newTemplateDir/articleLists/globalHeadlines.html";
                                                                         $targetPath2 = "$newTemplateDir/articleLists/categoryHeadlines.html"; }
    elsif ($oldPathFromTemplates =~ m|headlines_bycat.html$|i)         { $targetPath1 = "$newTemplateDir/articleLists/articlesForGroupedHeadlines.html";
                                                                         $targetPath2 = "$newTemplateDir/categoryMenus/groupedHeadlines.html"; }
    elsif ($oldPathFromTemplates =~ m|^article/|i)                     { $targetPath1 = "$newTemplateDir/articlePages/$oldFilename"; }
    elsif ($oldPathFromTemplates =~ m|^headlines/|i)                   { $targetPath1 = "$newTemplateDir/articleLists/$oldFilename"; }
    elsif ($oldPathFromTemplates =~ m|^index/|i)                       { $targetPath1 = "$newTemplateDir/articleLists/$oldFilename"; }
    elsif ($oldPathFromTemplates =~ m|^search/|i)                      { $targetPath1 = "$newTemplateDir/searchEngine/$oldFilename"; }
    elsif ($oldPathFromTemplates =~ m|article_redirect|i)              { next; }
    else                                                               { $targetPath1 = "$newTemplateDir/$oldPathFromTemplates"; }

    # copy template
    foreach my $targetPath ($targetPath1, $targetPath2) {
      unless ($targetPath) { next; }
      my $newPathFromTemplates = $targetPath;
      $newPathFromTemplates =~ s|^.*?/templates/?||si || die "Couldn't remove new template path on ($newPathFromTemplates)!";

      &{$displayStatusMessage}("- Importing $oldPathFromTemplates");
      &{$displayStatusMessage}("         as $newPathFromTemplates");

      # load file
      my $html = &IT::File::readFile($sourcePath, 'WITHOUT_FILELOCKING');

      # rewrite file
      $html = &_rewriteTemplate($oldPathFromTemplates, $newPathFromTemplates, $html, $oldDataDir);

      # create target directory
      my $parentDir = &IT::File::getParentDirectory($targetPath);
      unless (-d $parentDir) { &IT::File::createDirPath($parentDir); }

      # save file
      &IT::File::writeFileFast($targetPath, $html);
    }

  }

  return 1;
}

# ------------------------------------------------------------------------------
# Function    : _rewriteTemplate
# Description :
# Usage       : $html = &_rewriteTemplate($oldPathFromTemplates, $newPathFromTemplates, $html);
# ------------------------------------------------------------------------------

sub _rewriteTemplate {
  my $oldRelativePath = shift || die "no old relative path specified!";
  my $newRelativePath = shift || die "no new relative path specified!";
  my $html            = shift;
  my $oldDataDir      = shift || die "no old data dir specified!";

  # all templates
  $html = &_importImgCellsAsEmbeddedMediaCells($html);
  $html = &_replaceTemplateModifiers($html);
  $html = &_renamePlaceholders($html, $oldDataDir);


  # category breadcrumb templates
  if (&_templateCellExists($html, 'breadcrumb_link')) {
    my $unselectedLink = &_getTemplateCellContents($html, 'breadcrumb_link');
    my $divider        = &_getTemplateCellContents($html, 'breadcrumb_divider');
    my $selectedLink   = &_getTemplateCellContents($html, 'breadcrumb_link_selected');

    $html = "";
    $html .= qq|<!-- template insert : \$categoryList\$ -->\n|;
    $html .= qq|  <!-- templatecell : categoryRow -->\n|;
    $html .= qq|    <!-- templateIf : \$isSelectedCategory\$ == "0" -->$unselectedLink $divider <!-- /templateIf  -->\n|;
    $html .= qq|    <!-- templateIf : \$isSelectedCategory\$ == "1" -->$selectedLink <!-- /templateIf  -->\n|;
    $html .= qq|  <!-- /templatecell : categoryRow -->\n|;
  }

  # categoryMenu templates
  if (&_templateCellExists($html, 'row_depth1')) {
    for my $num (1..20) {
      my $numLessOne = $num - 1;
      $html = &_replaceTemplateCell($html, "row_depth$num",          "<!-- templateIf : \$category.depth\$ == '$numLessOne' --><!-- templateIf : \$isSelectedCategory\$ == '0' -->", undef, '<!-- /templateIf --><!-- /templateIf -->');
      $html = &_replaceTemplateCell($html, "row_selected_depth$num", "<!-- templateIf : \$category.depth\$ == '$numLessOne' --><!-- templateIf : \$isSelectedCategory\$ == '1' -->", undef, '<!-- /templateIf --><!-- /templateIf -->');
    }
    $html = &_replaceTemplateCell($html, "not_found", "<!-- templateIf : \$categoryList\$ eq '' -->", undef, '<!-- /templateIf -->');
    $html =~ s|(<!-- templateIf .*<!-- /templateIf --><!-- /templateIf -->)|<!-- templatecell : categoryRow -->\n\n$1\n\n<!-- /templatecell : categoryRow -->\n|is;
  }

  # search box
  if ($newRelativePath =~ m/basicSearchBox.html|searchForm.html|searchResults.html/) {
    $html =~ s/\b(cat)\b/categoryNum/gsi;
    $html =~ s/\b(search_subcats)\b/includeSubcats/gsi;
    $html =~ s/(<form[^>]*>)/$1\n<input type="hidden" name="action" value="search">\n
      <!--
      <input type="hidden" name="sortBy" value="fieldname, -fieldnameReverseOrder, fieldname">
      <input type="hidden" name="keywordSearchFields" value="title, author">
      -->\n/gsi;
    $html =~ s/([\$"'])mon2/${1}endMon/gsi;
    $html =~ s/([\$"'])day2/${1}endDay/gsi;
    $html =~ s/([\$"'])year2/${1}endYear/gsi;
    $html =~ s/([\$"'])mon/${1}startMon/gsi;
    $html =~ s/([\$"'])day/${1}startDay/gsi;
    $html =~ s/([\$"'])year/${1}startYear/gsi;
    $html =~ s|<option>|<option value="">|gsi;
    #next/prev links specific to search engine templates
    $html =~ s/\s*<!--\s*template\s*insert\s*:\s*\$(prev|next|more)\$\s*-->//gsi;
    $html =~ s/\$prev_url\$/\$settings.searchurl\$?action=search&page=\$articleList.prevPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&categoryNums=\$categoryNums\$&includeSubcats=\$articleList.includeSubcats\$&keyword=\$search.keyword:urlEncoded\$&keywordSearchFields=\$search.keywordSearchFields:urlEncoded\$/gsi;
    $html =~ s/\$next_url\$/\$settings.searchurl\$?action=search&page=\$articleList.nextPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&categoryNums=\$categoryNums\$&includeSubcats=\$articleList.includeSubcats\$&keyword=\$search.keyword:urlEncoded\$&keywordSearchFields=\$search.keywordSearchFields:urlEncoded\$/gsi;

  }
  if ($newRelativePath =~ m/searchForm.html/) {
    $html =~ s/published.categoryMenu./published.globalMenu./gs;
    $html =~ s/published.categoryHeadlines./published.globalHeadlines./gs;
  }

  # for global indexes only
  if ($newRelativePath =~ m/searchResults.html|global/i) {
    my $breadcrumbInclude = &_getSSIorPhpInclude($oldDataDir, 'categoryBreadcrumb');
    $html = &_makeReplacementsOutsideOfTemplateCells($html, quotemeta($breadcrumbInclude),              '$my.homepageName$');
    $html = &_makeReplacementsOutsideOfTemplateCells($html, '\$published.categoryBreadcrumb.webpath\$', '$my.homepageName$');
    $html = &_makeReplacementsOutsideOfTemplateCells($html, '\$category.name[^$]*\$',                   '$my.homepageName$');
    $html =~ s/published.categoryMenu./published.globalMenu./gs;
    #next/prev links specific to global indexes
    $html =~ s/\s*<!--\s*template\s*insert\s*:\s*\$(prev|next|more)\$\s*-->//gsi;
    $html =~ s/\$prev_url\$/\$settings.searchurl\$?action=search&page=\$articleList.prevPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&includeSubcats=\$articleList.includeSubcats\$/gsi;
    $html =~ s/\$next_url\$/\$settings.searchurl\$?action=search&page=\$articleList.nextPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&includeSubcats=\$articleList.includeSubcats\$/gsi;

  }

  # articleList templates
  if ($newRelativePath =~ m/articleList/ ||
      (&_templateCellExists($html, 'row_summary') && !&_templateCellExists($html, 'header'))
     ) {

    # replace article rows
    my $newRowContent;
    unless ($newRelativePath =~ /headline/i) {
      $newRowContent   .= '<!-- templateIf : $article.attachments$ ne "" -->' . "\n";
      $newRowContent   .= '<div class="summary_attachments"> <b style="color: #666666;">Download attachment(s):</b> $article.attachments$' . "\n";
      $newRowContent   .= "\n". '  <!-- templatecell : article.attachments.row -->[ <a href="$file.webUrl$">$file.title$</a> ]<br><br clear="all"><!-- /templatecell : article.attachments.row -->' . "\n";
      $newRowContent   .= '  </div>' . "\n";
      $newRowContent   .= '<!-- /templateIf -->' . "\n";
      $newRowContent   .= "\n";
      $newRowContent   .= '<!-- templateIf : $article.externalLink$ ne "" -->[ <a href="$article.externalLink$">Visit&nbsp;Website</a> ]<br><br clear="all"><!-- /templateIf -->' . "\n";
    }
    $html = &_replaceTemplateCell($html, "row",         '<!-- templatecell : articleRow -->', undef, "$newRowContent\n<!-- /templatecell : articleRow -->");
    $html = &_replaceTemplateCell($html, "row_summary", '', '', '');
    $html = &_replaceTemplateCell($html, "row_link",    '', '', '');
    $html = &_replaceTemplateCell($html, "row_ufile",   '', '', '');
    unless ($newRelativePath =~ /articlesForGroupedHeadlines/) {
      $html = &_replaceTemplateCell($html, "not_found",   '<!-- templateIf : $articleList$ eq "" -->', undef, '<!-- /templateIf -->');
    }

    # replace prev, next page links
    $html =~ s/\s*<!--\s*template\s*insert\s*:\s*\$(prev|next|more)\$\s*-->//gsi;
    $html =~ s/\$prev_url\$/\$settings.searchurl\$?action=search&page=\$articleList.prevPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&categoryNums=\$categoryNums\$&includeSubcats=\$articleList.includeSubcats\$/gsi;
    $html =~ s/\$next_url\$/\$settings.searchurl\$?action=search&page=\$articleList.nextPageNumber\$&perpage=\$articleList.perpage\$&template=\$articleList.template\$&categoryNums=\$categoryNums\$&includeSubcats=\$articleList.includeSubcats\$/gsi;
    $html = &_replaceTemplateCell($html, 'no_prev', '<!-- templateIf : $articleList.prevPageNumber$ eq "" -->', undef, '<!-- /templateIf -->');
    $html = &_replaceTemplateCell($html, 'prev',    '<!-- templateIf : $articleList.prevPageNumber$ ne "" -->', undef, '<!-- /templateIf -->');
    $html = &_replaceTemplateCell($html, 'no_next', '<!-- templateIf : $articleList.nextPageNumber$ <= $articleList.thisPageNumber$ -->', undef, '<!-- /templateIf -->');
    $html = &_replaceTemplateCell($html, 'next',    '<!-- templateIf : $articleList.nextPageNumber$ > $articleList.thisPageNumber$ -->', undef, '<!-- /templateIf -->');
    $html = &_replaceTemplateCell($html, 'more', '', '', '');
    $html = &_replaceTemplateCell($html, 'no_more', '', '', '');
  }

  # groupedHeadlines
  if ($newRelativePath =~ /groupedHeadlines/) {

    my $header = &_getTemplateCellContents($html, 'header');
    my $footer = &_getTemplateCellContents($html, 'footer');

    $html = &_replaceTemplateCell($html, 'img_caption',   '', '', '');
    $html = &_replaceTemplateCell($html, 'img_nocaption', '', '', '');
    $html = &_replaceTemplateCell($html, "row", '', '', '');
    $html = &_replaceTemplateCell($html, "row_summary", '', '', '');
    $html = &_replaceTemplateCell($html, "row_link",    '', '', '');
    $html = &_replaceTemplateCell($html, "row_ufile",   '', '', '');
    $html = &_replaceTemplateCell($html, "divider",   '', '', '');
    $html = &_replaceTemplateCell($html, "not_found",   '<!-- templateIf : $categoryList$ eq "" -->', undef, '<!-- /templateIf -->');
    $html = &_replaceTemplateCell($html, "footer", '', '', '');

    $html = &_replaceTemplateCell($html, "header",
                                         "<!-- templatecell : categoryRow -->\n<!-- templateIf : \$category.depth\$ == '0' -->",
                                         $header . &_getSSIorPhpInclude($oldDataDir, 'articlesForGroupedHeadlines') . $footer,
                                         "<!-- /templateIf -->\n<!-- /templatecell : categoryRow -->");

  }

  # articlesForGroupedHeadlines
  if ($newRelativePath =~ /articlesForGroupedHeadlines/) {
    my $articleRow = &_getTemplateCellContents($html, 'articleRow');
    my $notFound   = &_getTemplateCellContents($html, 'not_found');

    $html = "\n";
    $html .= "<!-- template insert : \$articleList\$ -->\n";
    $html .= "<!-- templatecell : articleRow -->\n$articleRow\n<!-- /templatecell : articleRow -->\n";
    $html .= "<!-- templateIf : \$categoryList\$ eq '' -->\n$notFound\n<!-- /templateIf -->\n";
  }


  $html =~ s/\r\n/\n/g;
  return $html;

}

# ------------------------------------------------------------------------------
# Function    : _replaceTemplateModifiers
# Description :
# Usage       : $html = &_replaceTemplateModifiers($html);
# ------------------------------------------------------------------------------

sub _replaceTemplateModifiers {
  my $content = shift;

  $content =~ s[\$(\w+?)((?:_ue|_he|_nbsp|_je)+)\$][
    my $name = $1;
    my $oldModifiers = $2;
    my $newModifiers = $oldModifiers;

    $newModifiers =~ s/_ue/:urlEncoded/g;
    $newModifiers =~ s/_he/:htmlEncoded/g;
    $newModifiers =~ s/_nbsp/:spacesToNbsp/g;
    $newModifiers =~ s/_je/:javascriptSafe/g;

    my $newPlaceholder = '$' . $name . $newModifiers . '$';
    $newPlaceholder;
  ]ge;

  return $content;
}


# ------------------------------------------------------------------------------
# Function    : _renamePlaceholders
# Description :
# Usage       : $html = &_renamePlaceholders($html);
# ------------------------------------------------------------------------------

sub _renamePlaceholders {
  my $content = shift;
  my $oldDataDir = shift || die "no old data dir specified!";

  ### multi placeholder replacements (do these first since they need to match original placeholder names)
  $content =~ s|\$publishdir_virtualpath\$/*\$file_gcategories\b|\$published.globalMenu.webpath|g;
  $content =~ s|\$publishdir_virtualpath\$/*\$file_gheadlines\b|\$published.globalHeadlines.webpath|g;
  $content =~ s|\$publishdir_virtualpath\$/*\$file_headlines_bycat\b|\$published.groupedHeadlines.webpath|g;
  $content =~ s|\$publishdir_virtualpath\$/*\$file_search\b|\$published.searchbox.webpath|g;
  $content =~ s|\$publishdir_virtualpath\$/*\$file_categories\b|\$published.categoryMenu.webpath|g;
  $content =~ s|\$publishdir_virtualpath\$/*\$file_headlines\b|\$published.categoryHeadlines.webpath|g;
  $content =~ s|\$art_date\$, \$art_time\$|\$article.date\$|g;

  ### global placeholders
  $content =~ s|\$_imageurl([^\$]*)\$|\$_weburl$1\$v141_images/|g;
  $content =~ s|\$url_gindex\b|\$published.globalIndex.url|g;
  $content =~ s|\$url_publish\b|\$settings.publish_url|g;
  $content =~ s|\$url_search\b|\$settings.searchUrl|g;
  $content =~ s|\$global_updated\b|\$_lastUpdated|g;
  $content =~ s|\$gcat_name\b|\$my.homepageName|g;
  $content =~ s|\$publishdir_filepath\b|\$_publishDir|g;
  $content =~ s|\$publishdir_virtualpath\b|\$_publishWebPath|g;
  $content =~ s|\$url_thispage\b|\$published.currentPage.url|g;

  $content =~ s/_(\w+)_selected([_:\$])/:selectedIf($1)$2/gsi;
  $content =~ s/_(\w+)_checked([_:\$])/:checkedIf($1)$2/gsi;

  ### search placeholders
  $content =~ s|\$search_cpage\b|\$articleList.thisPageNumber|g;    # current page number
  $content =~ s|\$search_tpage\b|\$articleList.totalPages|g;        # total page numbers
  $content =~ s|\$search_match\b|\$articleList.totalResults|g;      # total matched records
  $content =~ s|\$search_lnum\b|\$articleList.firstResultNumber|g;  # first result number on page
  $content =~ s|\$search_hnum\b|\$articleList.lastResultNumber|g;   # last result number on page

  ### publish placeholders
  $content =~ s|\$file_gcategories\b|\$published.globalMenu.filename|g;
  $content =~ s|\$file_gheadlines\b|\$published.globalHeadlines.filename|g;
  $content =~ s|\$file_headlines_bycat\b|\$published.groupedHeadlines.filename|g;
  $content =~ s|\$file_search\b|\$published.searchbox.filename|g;
  $content =~ s|\$file_categories\b|\$published.categoryMenu.filename|g;
  $content =~ s|\$file_headlines\b|\$published.categoryHeadlines.filename|g;

  ### article placeholders
  $content =~ s|\$ampm|\$article.date:format(tt)|g;
  $content =~ s|\$art_num\b|\$article.num|g;
  $content =~ s|\$art_name\b|\$article.title|g;
  $content =~ s|\$art_date\b|\$article.date:format(date_only)|g;
  $content =~ s|\$art_time\b|\$article.date:format(time_only)|g;
  $content =~ s|\$art_day\b|\$article.date:format(d)|g;
  $content =~ s|\$art_mon\b|\$article.date:format(M)|g;
  $content =~ s|\$art_year\b|\$article.date:format(yyyy)|g;
  $content =~ s|\$art_hour\b|\$article.date:format(h)|g;
  $content =~ s|\$art_min\b|\$article.date:format(m)|g;

  $content =~ s|\$art_ampm|\$article.date:format(tt)|g;
  $content =~ s|\$art_summary\b|\$article.summary|g;
  $content =~ s|\$art_content\b|\$article.content|g;
  $content =~ s|\$art_field(\d+)\b|\$article.art_field$1|g;
  $content =~ s|\$url_index\b|\$published.categoryIndex.url|g;
  $content =~ s|\$detail_link\b|\$published.articlePage.url|g;
  $content =~ s|\$art_printer_url\b|\$published.articlePrinterPage.url|g;
  $content =~ s|\$article_list\b|\$articleList|g;

  $content =~ s|\$art_caption(\d+)\b|\$article.image:fileAttribute($1, caption)|g;
  $content =~ s|\$art_ufile_image(\d+)\b|\$article.image:fileAttribute($1, pathFromUploadDir)|g;
  $content =~ s|\$art_image_url(\d+)\b|\$article.image:fileAttribute($1, webUrl)|g;
  $content =~ s|\$art_imageheight_image(\d+)\b|\$article.image:fileAttribute($1, height)|g;
  $content =~ s|\$art_imagewidth_image(\d+)\b|\$article.image:fileAttribute($1, width)|g;
  $content =~ s|\$art_imagename(\d+)\b|\$article.image:fileAttribute($1, title)|g;

  # category placeholders

  $content =~ s|\$cat_num\b|\$category.num|g;
  $content =~ s|\$cat_name\b|\$category.name|g;
  $content =~ s|\$cat_updated\b|\$category.lastUpdatedTime|g;

  my $breadcrumbInclude = &_getSSIorPhpInclude($oldDataDir, 'categoryBreadcrumb');
  $content =~ s|\$cat_breadcrumb[^\$]*?\$|$breadcrumbInclude|g;

  $content =~ s|(<([^>]*?)\$cat_image([^>]*?)>)|
    my $replacement = "\n\$category.image\$\n<!-- templateCell : category.image.row -->\n$1\n<!-- /templateCell : category.image.row -->\n";
    $replacement =~ s/\$cat_image\b/\$file.webUrl/g;
    $replacement =~ s/\$cat_image_height\b/\$file.height/g;
    $replacement =~ s/\$cat_image_width\b/\$file.width/g;
    $replacement;
  |ge;
  $content =~ s|\$category_list\b|\$categoryList|g;
  $content =~ s|\$category_pulldown\b|\$category.optionListForSearchEngineAsBranches|g;

  # misc placeholders
  $content =~ s|\$img_height\b|\$file.height|g;
  $content =~ s|\$img_width\b|\$file.width|g;
  $content =~ s|\$img_caption\b|\$file.caption|g;
  $content =~ s|\$img_url\b|\$file.webUrl|g;
  $content =~ s|\$img_align\b|\$file.align|g;
  $content =~ s|\$virtual_categories\b|\$published.globalMenu.webpath|g;
  $content =~ s|\$virtual_headlines\b|\$published.categoryHeadlines.webpath|g;
  $content =~ s|\$virtual_search\b|\$published.searchbox.webpath|g;
  $content =~ s|\$virtual_headlines_bycat\b|\$published.groupedHeadlines.webpath|g;

  return $content;

}

# ------------------------------------------------------------------------------
# Function    : _replaceTemplateCell
# Description : Replace named templatecell with startTag, cellContent and EndTag
#               and return html.  specify undef for startTag, cellContent, or
#               endTag to leave value unchanged.
# Usage       : $html = &_replaceTemplateCells($html, $cellname, $startTag, $cellContent, $endTag);
# ------------------------------------------------------------------------------

sub _replaceTemplateCell {
  my $content             = shift;
  my $cellname            = shift || die "no templateCellName specified!";
  my $replacementStartTag = shift;
  my $replacementCellBody = shift;
  my $replacementEndTag   = shift;

  $content =~ s/(<!--\s*template\s*cell\s*:\s*$cellname\s*-->)(.*?)(<!--\s*\/\s*template\s*cell\s*:\s*$cellname\s*-->)/
    my $replacement;
    $replacement .= defined $replacementStartTag ? $replacementStartTag : $1;
    $replacement .= defined $replacementCellBody ? $replacementCellBody : $2;
    $replacement .= defined $replacementEndTag   ? $replacementEndTag : $3;
    $replacement;
  /ise;

  return $content;
}

# ------------------------------------------------------------------------------
# Function    : _getTemplateCellContents
# Description :
# Usage       : $html = &_getTemplateCellContents($html, $cellname);
# ------------------------------------------------------------------------------

sub _getTemplateCellContents {
  my $content             = shift;
  my $cellname            = shift || die "no templateCellName specified!";

  if ($content =~ m/(<!--\s*template\s*cell\s*:\s*$cellname\s*-->)(.*?)(<!--\s*\/\s*template\s*cell\s*:\s*$cellname\s*-->)/is) {
    return $2;
  }

  die "Couldn't find templatecell '$cellname' in content!";
}

# ------------------------------------------------------------------------------
# Function    : _templateCellExists
# Description :
# Usage       : $html = &_templateCellExists($html, $cellname);
# ------------------------------------------------------------------------------

sub _templateCellExists {
  my $content             = shift;
  my $cellname            = shift || die "no templateCellName specified!";

  my $cellExists = ($content =~ m/(<!--\s*template\s*cell\s*:\s*$cellname\s*-->)(.*?)(<!--\s*\/\s*template\s*cell\s*:\s*$cellname\s*-->)/is);

  return $cellExists;
}

# ------------------------------------------------------------------------------
# Function    : _makeReplacementsOutsideOfTemplateCells
# Description :
# Usage       : $html = &_makeReplacementsOutsideOfTemplateCells($html, $matchRegexp, $replacement);
# ------------------------------------------------------------------------------

sub _makeReplacementsOutsideOfTemplateCells {
  my $content     = shift;
  my $regexp      = shift || die "no regexp specified!";
  my $replacement = shift;

  my $matchTemplateCell = '<!--\s*template\s*cell\s*:\s*(\S+)\s*-->).*?(<!--\s*\/\s*template\s*cell\s*:\s*\1\s*-->)';

  # remove templateCells
  my %templateCells;
  my $counter = 1;
  while ($content =~ s/(<!--\s*template\s*cell\s*:\s*(\S+)\s*-->.*?<!--\s*\/\s*template\s*cell\s*:\s*\2\s*-->)/### TEMPLATECELL $counter ###/is) {
    $templateCells{$counter} = $1;
    $counter++;
  }

  # make replacements
  $content =~ s/$regexp/$replacement/gsi;

  # replace templatecells
  $content =~ s/### TEMPLATECELL (\d+) ###/$templateCells{$1}/iseg;

  return $content;

}

# ------------------------------------------------------------------------------
# Function    : _importImgCellsAsEmbeddedMediaCells
# Description :
# Usage       : $html = &_importImgCellsAsEmbeddedMediaCells($html);
# ------------------------------------------------------------------------------

sub _importImgCellsAsEmbeddedMediaCells {
  my $html = shift;

  # return html if no cells
  unless (&_templateCellExists($html, 'img_caption')) { return $html; }

  # get cell contents
  my $imgCaptionHtml   = &_getTemplateCellContents($html, 'img_caption');
  my $imgNoCaptionHtml = &_getTemplateCellContents($html, 'img_nocaption');
  foreach ($imgNoCaptionHtml, $imgCaptionHtml) {
    s/^\s+//s; # remove leading white space
    s/\s+$//s; # remove trailing whitespace
  }

  # replace placeholders

  # get new pageDefault.embeddedMedia html
  my $newCell;
  $newCell .= qq|\n<!-- templateCell : pageDefault.embeddedMedia -->\n|;
  $newCell .= qq|\n|;
  $newCell .= qq|  <!-- templateIf : \$file.caption\$ ne '' -->\n|;
  $newCell .= qq|    $imgCaptionHtml\n| if $imgCaptionHtml;
  $newCell .= qq|  <!-- /templateIf -->\n|;
  $newCell .= qq|\n|;
  $newCell .= qq|  <!-- templateIf : \$file.caption\$ eq '' -->\n|;
  $newCell .= qq|    $imgNoCaptionHtml\n| if $imgNoCaptionHtml;
  $newCell .= qq|  <!-- /templateIf -->\n|;
  $newCell .= qq|\n|;
  $newCell .= qq|<!-- /templateCell : pageDefault.embeddedMedia -->\n|;

  # replace cells with pageDefault.embeddedMedia
  $html = &_replaceTemplateCell($html, 'img_caption', '', $newCell, '');
  $html = &_replaceTemplateCell($html, 'img_nocaption', '', '', '');

  #
  return $html;

}


# ------------------------------------------------------------------------------
# Function    : copyOldImages
# Description :
# Usage       : &IT::ArtMan::Upgrade::From141::copyOldImages($currentStep, $currentSubstep, \&displayStatusMessage, $oldDataDir);
# ------------------------------------------------------------------------------

sub copyOldImages {
  my $currentStep          = shift;
  my $currentSubstep       = shift;
  my $displayStatusMessage = shift || die "no displayStatusMessage coderef passed!";
  my $oldDataDir           = shift || die "no old datadir specified!";
  my $oldImagesDir         = shift || die "no old images dir specified!";

  $oldImagesDir    = &IT::File::cleanPath($oldImagesDir);
  my $newImagesDir = &IT::File::cleanPath("$SETTINGS->{'upload_dir'}/../v141_images/");

  &{$displayStatusMessage}("Copying old /images/ files");

  # create new images dir
  &IT::File::createDirPath($newImagesDir) || die "Couldn't create dir $newImagesDir!";

  # get list of old images files
  my @allFiles = &IT::File::dirTree("$oldImagesDir");
  my @files;
  foreach my $filepath (@allFiles) {
    if ($filepath =~ m|/wysiwyg|)  { next; }                    # skip wysiwyg files
    if ($filepath =~ m|/_vti_cnf|) { next; }                    # skip MS Frontpage dirs
    push @files, $filepath;
  }

  # copy files
  foreach my $sourcePath (sort @files) {

    my $oldPathFromImages = &IT::File::cleanPath($sourcePath);
    $oldPathFromImages   =~ s|\Q$oldImagesDir\E||si || die "Couldn't remove old images dir from path!";
    my $targetPath       = "$newImagesDir/$oldPathFromImages";

    &{$displayStatusMessage}("- $oldPathFromImages");
    if (-e $targetPath) { next; }

    # create directories
    if (-d $sourcePath) { &IT::File::createDirPath($targetPath); }

    # copy files
    if (-f $sourcePath) {
      &IT::File::copyFile($sourcePath, $targetPath);
    }
  }

  return 1;
}


# ------------------------------------------------------------------------------
# Function    : _getSSIorPhpInclude
# Description : Check old settings publish extension and return PHP include for php
#               and SSI for anything else
# Usage       : my $includeCode = &_getSSIorPhpInclude($oldDataDir, $publishPlaceholderName);
# ------------------------------------------------------------------------------

sub _getSSIorPhpInclude {
  my $oldDataDir             = shift || die "no old datadir specified!\n";
  my $publishPlaceholderName = shift || die "no publish placeholder specified!\n";
  my $setupOptionsPath       = "$oldDataDir/setup.dat.cgi";

  my $setupOptions = &loadSetupOptions($setupOptionsPath);
  my $publishExt   = $setupOptions->{'publish_fileext'};

  my $includeCode;
  if ($publishExt =~ /php/i) {
    $includeCode = qq||;
    $includeCode = qq|<?php include('\$published.$publishPlaceholderName.filepath\$'); ?>|;
  }
  else {
    $includeCode = qq|<!--#include virtual="\$published.$publishPlaceholderName.webpath\$" -->|;
  }

  return $includeCode;
}

# ----------------------------------------------------------------------------
# end of file
1;

