#!/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::CustomMenus::Publish;

# load modules
use strict;

use IT::Globals;

use vars qw($PUBLISH_TYPE $MINIMUM_PUBLISH_ACCESS_LEVEL $MINIMUM_PUBLISH_ALL_ACCESS_LEVEL $CACHED_CATEGORY_PLACEHOLDERS);
use vars qw($CACHED_PUBLISH_RULE_SINGLE_FILE_PLACEHOLDERS);
$MINIMUM_PUBLISH_ACCESS_LEVEL     = 3;
$MINIMUM_PUBLISH_ALL_ACCESS_LEVEL = 9;

# ------------------------------------------------------------------------------
# Function    : publish
# Description :
# Usage       : &IT::ArtMan::CustomMenus::Publish::publish($publishType, $nextAction);
# ------------------------------------------------------------------------------

sub publish {
  $PUBLISH_TYPE  = shift || die "no 'publishType' specified, must be 'all' or 'updated'";
  my $nextAction = shift || $FORM->{'nextAction'} || $FORM->{'_defaultAction'} || 'home';
  if ($SETTINGS->{'needsPublishAll'}) { $PUBLISH_TYPE = 'all'; }

  # error checking
  if ($CURRENT_USER->{'accessLevel'} < $MINIMUM_PUBLISH_ACCESS_LEVEL) { return; } # skip publishing if no access
  if ($PUBLISH_TYPE ne 'all' && $PUBLISH_TYPE ne 'updated') { die "invalid publishType '$PUBLISH_TYPE', must be 'all' or 'updated'!"; }
  if ($PUBLISH_TYPE eq 'all' && !$SETTINGS->{'needsPublishAll'} && $CURRENT_USER->{'accessLevel'} < $MINIMUM_PUBLISH_ALL_ACCESS_LEVEL) {
    die "Only admin users can 'publish all'!";
  }

  # clear caches and update globals - (changes may have been made to categories during edit/save/etc)
  &IT::ArtMan::CustomMenus::Category::clearAllCachedData();
  &IT::ArtMan::CustomMenus::Admin::PublishRules::clearAllCachedData();
  &main::defineGlobalPlaceholders();

  # display publishing page and begin/continue
  &IT::MultiStepOperations::doMultiStepOperation({
    'stages'      => &_createPublishingStages,
    'maxSeconds'  => $SETTINGS->{'maxSecondsPerProcess'},
    'displayPage' => sub { &_displayPublishingInProgressPage($nextAction); },
    'onTimeout'   => sub {
      &IT::App::alert("The server timed out and was unable to complete publishing.");
      &IT::App::alert("The last status message was: $FORM->{'_lastStatusMessage'}");
      &main::dispatchAction($nextAction);
    },
    'onComplete'  => sub {
      my($elapsedSeconds, $elapsedTime) = @_;

      # For debugging: die "HALT PUBLISH: Elapsed time: $elapsedTime!\n";

      &IT::MultiStepOperations::addStatusMessage("Publishing Complete, reloading browser...\n");
      &IT::Common::redirectBrowserToURL("$CGIURL?action=$nextAction&elapsedTime=$elapsedTime");
    },
  });
  exit;
}

# ------------------------------------------------------------------------------
# Function    : _createPublishingStages
# Description :
# Usage       : my $stages = &_createPublishingStages()
# ------------------------------------------------------------------------------

sub _createPublishingStages {

  # get selected publish rules (defaults to all)
  my $allPublishRules         = &IT::ArtMan::CustomMenus::Admin::PublishRules::getSortedArrayOfPublishRules();
  my %selectedRuleNumbersHash = map { $_ => 1 } split /,/, $FORM->{'selectedRuleNumbers'};
  my $selectedPublishRules    = [];
  if (keys %selectedRuleNumbersHash) {
    foreach my $publishRule (@$allPublishRules) {
      unless ($selectedRuleNumbersHash{ $publishRule->{'num'} }) { next; }
      push @$selectedPublishRules, $publishRule;
    }
  }
  else { $selectedPublishRules = $allPublishRules }

  # add publish rule stages
  my @stages;
  foreach my $thisRule (@$selectedPublishRules) {

    my $codeRef;
    my $skipDisabledRules  = $thisRule->{'disabled'} && !$FORM->{'selectedRuleNumbers'};
    my $publishUpdatedOnly = $PUBLISH_TYPE eq 'updated' && !$thisRule->{'alwaysPublish'};

    if    ($skipDisabledRules)                    { next; }
    elsif ($thisRule->{'type'} eq 'articlePages') { $codeRef = sub { &IT::ArtMan::CustomMenus::Publish::ArticlePages::publish($thisRule, $publishUpdatedOnly, @_)}; }
    elsif ($thisRule->{'type'} eq 'articleList')  { $codeRef = sub { &IT::ArtMan::CustomMenus::Publish::ArticleLists::publish($thisRule, $publishUpdatedOnly, @_)}; }
    elsif ($thisRule->{'type'} eq 'categoryMenu') { $codeRef = sub { &IT::ArtMan::CustomMenus::Publish::CategoryMenus::publish($thisRule, $publishUpdatedOnly, @_)}; }
    elsif ($thisRule->{'type'} eq 'singlePage')   { $codeRef = sub { &IT::ArtMan::CustomMenus::Publish::SinglePages::publish($thisRule, $publishUpdatedOnly, @_)}; }
    elsif ($thisRule->{'type'} eq 'externalUrl')  { $codeRef = sub { &IT::ArtMan::CustomMenus::Publish::ExternalUrls::request($thisRule, $publishUpdatedOnly, @_)}; }
    else                                          { die "unknown rule type of '$thisRule->{'type'}' for '$thisRule->{'name'}'"; }

    push(@stages, $codeRef);
  }

  # add manual stages to end
  my $publishUpdatedOnly = $PUBLISH_TYPE eq 'updated';

  unless ($FORM->{'selectedRuleNumbers'}) {
    push @stages, \&_resetNeedsPublishFields;
  }

  #
  return \@stages;
}

# ------------------------------------------------------------------------------
# Function    : _displayPublishingInProgressPage
# Description :
# Usage       : &_displayPublishingInProgressPage();
# ------------------------------------------------------------------------------

sub _displayPublishingInProgressPage {
  my $nextAction = shift || die "no nextAction defined!";
  my $template   = "admin/menus/publish.html";

  # display alerts
  &IT::App::alert('<font color="red"><b>PUBLISHING IN PROGRESS</b></font>', 'keepHTML');
  &IT::App::alert('Please, do not stop or close your browser!');

  # define placeholders
  my $placeholders = {};
  $placeholders = &IT::MultiStepOperations::getMultiStepPlaceholders($placeholders);
  $placeholders->{'publishType'}         = $PUBLISH_TYPE;
  $placeholders->{'nextAction'}          = $nextAction;
  $placeholders->{'selectedRuleNumbers'} = $FORM->{'selectedRuleNumbers'};

  # Display page
  &IT::App::displayInterface({
    'template'           => $template,
    'templatecell'       => 'publishInProgress',
    'placeholders'       => $placeholders,
    'selectedMenuOption' => 'none',
    'doNotExit'          => 1,
  });

}

# ------------------------------------------------------------------------------
# Function    : _resetNeedsPublishFields
# Description :
# Usage       : &_resetNeedsPublishFields()
# ------------------------------------------------------------------------------

sub _resetNeedsPublishFields {

  &IT::MultiStepOperations::addStatusMessage("Flagging all records as published... \n");

  ### reset all 'needsPublish' fields (on records that haven't been updated since publish started!)
  &IT::FileLock::getLock('exclusive');

  # update articles
  my $safeStartTime = int $FORM->{'_startTime'} || time();
  my $articleWhereQuery = "lastUpdatedTime <= '$safeStartTime' AND needsPublish != '0'";
  my $articleUpdateHash = { 'needsPublish' => '0' };
  my $updatedArticles   = &IT::DB::update($TABLE_PREFIX . "article",  $articleWhereQuery, $articleUpdateHash);

  # update categories
  my $categoryWhereQuery = "lastUpdatedTime <= '$safeStartTime' AND (needsPublish != '0' OR needsPublishForArticleLists != '0')";
  my $categoryUpdateHash = { 'needsPublish' => '0', 'needsPublishForArticleLists' => '0' };
  my $updatedCategories  = &IT::DB::update($TABLE_PREFIX . "category", $categoryWhereQuery, $categoryUpdateHash);

  # upset admin settings
  if ($SETTINGS->{'needsPublishAll'} && $SETTINGS->{'needsPublishAll'}) {
    $SETTINGS->{'needsPublishAll'} = 0;
    &IT::App::Menu::Admin::saveAdminSettingsToFile($SETTINGS);
  }

  &IT::FileLock::releaseLock();
  return 1; # stage complete
}


# ------------------------------------------------------------------------------
# Function    : getWhereForPublishRuleConditionFilter
# Description : Make sure to test if $rule->{'useRecordFilter'} is set before calling/using
# Usage       : my $conditionalFilter = &IT::ArtMan::CustomMenus::Publish::getWhereForPublishRuleConditionFilter($rule);
# ------------------------------------------------------------------------------

sub getWhereForPublishRuleConditionFilter {
  my $rule = shift || die "no rule specified!\n";

  # this feature isn't completed yet
  return '';

  # create filter where
  my $filterWhere;
  my $joinOperator = ($rule->{'filterAllOrAny'} eq 'all') ? 'AND' : 'OR';
  for my $num (1..3) {
    my $fieldname  = $rule->{"filterField$num"}    || next;
    my $dbOperator = $rule->{"filterOperator$num"} || next;
    my $value      = $rule->{"filterValue$num"};
    $filterWhere  .= " $joinOperator " if $filterWhere;
    $filterWhere  .= "article.$fieldname $dbOperator $value";
  }
  $filterWhere = "($filterWhere)" if $filterWhere;

  #
  return $filterWhere;
}

# ------------------------------------------------------------------------------
# Function    : getWhereForFilteringByCategoryField
# Description :
# Usage       : my $categoryWhere = &IT::ArtMan::CustomMenus::Publish::getWhereForFilteringByCategoryField(\@categoryNums);
# ------------------------------------------------------------------------------

sub getWhereForFilteringByCategoryField {
  my $categoryNums = shift || die "no category nums specified!";

  # error checking
  if (ref $categoryNums ne 'ARRAY') { die "categoryNums must be an array reference!"; }
  if (@$categoryNums == 0)          { die "categoryNums does not contain any categories!"; }

  # create where
  my $categoryWhere;
  my $categoryFieldnames = &IT::ArtMan::CustomFields::ArticleEditor::PrimaryCategory::getArticleCategoryFieldnames('all');
  my $categoryNumsAsCSV  = join(",", @$categoryNums);
  foreach my $categoryFieldname (@$categoryFieldnames) {
    $categoryWhere .= " OR " if $categoryWhere;
    $categoryWhere .= "article.";
    $categoryWhere .= "$categoryFieldname IN ($categoryNumsAsCSV)"
  }
  $categoryWhere = "($categoryWhere)" if $categoryWhere
}

# ------------------------------------------------------------------------------
# Function    : getCategoryNumsWithPublishFlag
# Description :
# Usage       : my $categoryNumsThatNeedPublish = &IT::ArtMan::CustomMenus::Publish::getCategoryNumsWithPublishFlag(\@selectedCategoryNums, $flagType);
# ------------------------------------------------------------------------------

sub getCategoryNumsWithPublishFlag {
  if (@_ != 2) { die "getCategoryNumsWithPublishFlag requires at least 2 arguments!"; }
  my $selectedCategoryNums = shift || die "no category nums specified!";
  my $flagType             = shift || die "no flagType specified!";
  my @categoryNumsThatNeedPublish;

  # error checking
  if ($flagType ne 'any' && $flagType ne 'needsPublish') {
    die "\$flagType must be 'any' or 'needsPublish' not '$flagType'!";
  }

  #
  my $categoryRecords = &IT::ArtMan::CustomMenus::Category::getHashOfCategoryRecords();
  foreach my $categoryNum (@$selectedCategoryNums) {
    my $needsPublish                = $categoryRecords->{$categoryNum}{'needsPublish'};
    my $needsPublishForArticleLists = $categoryRecords->{$categoryNum}{'needsPublishForArticleLists'};
    my $categoryUpdated = 0;
    if    ($flagType eq 'any'          && ($needsPublish || $needsPublishForArticleLists)) { $categoryUpdated = 1; }
    elsif ($flagType eq 'needsPublish' && $needsPublish)                                   { $categoryUpdated = 1; }

    if ($categoryUpdated) {
      push @categoryNumsThatNeedPublish, $categoryNum;
    }
  }

  #
  return \@categoryNumsThatNeedPublish;
}


# ------------------------------------------------------------------------------
# Function    : eraseOldPublishedFilesFor
# Description :
# Usage       : &IT::ArtMan::CustomMenus::Publish::eraseOldPublishedFilesFor($articleOrCategory, $attr);
# ------------------------------------------------------------------------------

sub eraseOldPublishedFilesFor {
  my $articleOrCategory = shift || die "no articleOrCategory value specified!";
  my $attr              = shift || die "no attributes specified!";
  my $isCategory        = ($articleOrCategory eq 'category');

  # restrict publishing access
  if ($CURRENT_USER->{'accessLevel'} < $IT::ArtMan::CustomMenus::Publish::MINIMUM_PUBLISH_ACCESS_LEVEL) { return; }

  # error checking
  unless ($articleOrCategory eq 'category' || $articleOrCategory eq 'article') { die "articleOrCategory must have a value of 'article' or 'category' not '$articleOrCategory'!"; }

  # when erasing record
  my $isErasingRecord = exists $attr->{'selectedRecords'};  # from IT::App::Menu::Erase
  if ($isErasingRecord) {
    foreach my $record (@{$attr->{'selectedRecords'}}) {
      my $filepaths = &_getAllPublishFilepathsFor($articleOrCategory, $record);
      unlink @$filepaths;
    }
  }

  # when saving old records (NOT when saving NEW records - they won't have any published files we need to remove yet)
  my $isSavingExistingRecord = keys %{$attr->{'originalRecord'}};
  if ($isSavingExistingRecord) {
    my %oldRecord        = %{$attr->{'originalRecord'}};
    my %newRecord        = ( %{$attr->{'originalRecord'}}, %{$attr->{'newOrUpdatedFields'}} );
    my $oldFilepaths     = &_getAllPublishFilepathsFor($articleOrCategory, \%oldRecord);
    my $newFilepaths     = &_getAllPublishFilepathsFor($articleOrCategory, \%newRecord);
    my %newFilepathsHash = map { $_ => 1 } @$newFilepaths;

    foreach my $filepath (@$oldFilepaths) {
      if ($newFilepathsHash{$filepath}) { next; }
      unlink $filepath; # unlink old paths that will no longer be generated for new records
    }
  }
}

# ------------------------------------------------------------------------------
# Function    : eraseAllPublishedFilesFor
# Description :
# Usage       : &IT::ArtMan::CustomMenus::Publish::eraseAllPublishedFilesFor($articleOrCategory, $record);
# ------------------------------------------------------------------------------

sub eraseAllPublishedFilesFor {
  my $articleOrCategory = shift || die "no articleOrCategory value specified!";
  my $record            = shift || die "no record specified!";

  # restrict publishing access
  if ($CURRENT_USER->{'accessLevel'} < $IT::ArtMan::CustomMenus::Publish::MINIMUM_PUBLISH_ACCESS_LEVEL) { return; }

  # error checking
  unless ($articleOrCategory eq 'category' || $articleOrCategory eq 'article') { die "articleOrCategory must have a value of 'article' or 'category' not '$articleOrCategory'!"; }

  # erase files
  my $filepaths = &_getAllPublishFilepathsFor($articleOrCategory, $record);
  foreach my $filepath (@$filepaths) {
    unlink $filepath;
  }

}

# ------------------------------------------------------------------------------
# Function    : _getAllPublishFilepathsFor
# Description : return all publish filepaths that can be created for
#               specified article or category record.
# Usage       : &IT::ArtMan::CustomMenus::Publish::_getAllPublishFilepathsFor($articleOrCategory, $articleOrCategoryRecord);
# ------------------------------------------------------------------------------

sub _getAllPublishFilepathsFor {
  my $articleOrCategory = shift || die "no articleOrCategory value specified!";
  my $record            = shift || die "no record value specified!";
  my $isCategory        = $articleOrCategory eq 'category';
  my $isArticle         = $articleOrCategory eq 'article';
  my $filepaths         = [];

  # error checking
  unless ($isCategory || $isArticle) { die "articleOrCategory must have a value of 'article' or 'category' not '$articleOrCategory'!"; }

  # skip hidden records (return empty array ref)
  if ($isArticle  && $record->{'status'} ne 'visible')  { return []; }
  if ($isCategory && $record->{'type'}   eq 'disabled') { return []; }

  # get list of publish filepaths
  my $publishRules    = &IT::ArtMan::CustomMenus::Admin::PublishRules::getSortedArrayOfPublishRules();
  my $categoryRecords = &IT::ArtMan::CustomMenus::Category::getHashOfCategoryRecords();
  foreach my $rule (@$publishRules) {

    # skip rules that aren't specific to specified article or category
    my $skipCategoryMenu = (!$isCategory || $rule->{'categoryListPublishTopOrCategory'} eq 'top');
    my $skipArticleList  = (!$isCategory || $rule->{'articleListPublishGlobalOrCategoryIndexes'} eq 'global');
    if    ($rule->{'disabled'})                                     { next; }
    elsif ($rule->{'type'} eq 'singlePage')                         { next; }
    elsif ($rule->{'type'} eq 'categoryMenu' && $skipCategoryMenu)  { next; }
    elsif ($rule->{'type'} eq 'articleList'  && $skipArticleList)   { next; }
    elsif ($rule->{'type'} eq 'articlePages' && !$isArticle)        { next; }
    elsif ($rule->{'type'} eq 'externalUrl')                        { next; }

    # get current article and category records
    my $currentArticle  = ($isArticle)  ? $record : {};
    my $currentCategory = ($isCategory) ? $record : $categoryRecords->{ $currentArticle->{'categoryNum'} };

    # get publish filepath
    my($publishDir, $publishUrl)    = &IT::ArtMan::CustomMenus::Admin::PublishRules::getPublishDirAndUrl($rule);
    my $filename                    = &_getPublishFilename($rule, $currentCategory, $currentArticle);
    my $filepath                    = "$publishDir$filename";

    # add to list
    push @$filepaths, $filepath;
  }

  # return filepath list
  return $filepaths;
}

# ------------------------------------------------------------------------------
# Function    : getFilePathAndCreateDirFor
# Description :
# Usage       : my $filepath = &IT::ArtMan::CustomMenus::Publish::getFilePathAndCreateDirFor($rule, $category, $article);
# ------------------------------------------------------------------------------

sub getFilePathAndCreateDirFor {
  my $rule     = shift || die "no rule specified";
  my $category = shift || die "no category specified!";
  my $article  = shift || die "no article specified!";

  # get filepath
  my($publishDir, $publishUrl) = &IT::ArtMan::CustomMenus::Admin::PublishRules::getPublishDirAndUrl($rule);
  my $filename = &_getPublishFilename($rule, $category, $article);
  my $filepath = "$publishDir$filename";

  # create dir if needed
  my $parentDir = &IT::File::getParentDirectory($filepath);
  unless (-d $parentDir) {
    &IT::File::createDirPath($parentDir) || die "Couldn't create '$parentDir' : $!";
  }

  #
  return $filepath;

}

# ------------------------------------------------------------------------------
# Function    : _getPublishFilename
# Description :
# Usage       : my $fileName = &_getPublishFilename($publishRule, [$categoryRecord], [$articleRecord]);
# ------------------------------------------------------------------------------

sub _getPublishFilename {
  my $rule           = shift || die "no rule specified!";
  my $categoryRecord = shift || {};
  my $articleRecord  = shift || {};

  # get filename format
  my($filenameFormat, $singleOrMultiple) = &IT::ArtMan::CustomMenus::Admin::PublishRules::getFilenameFormatAndType($rule);

  # replace filename <placeholders>
  my $filename = $filenameFormat;
  $filename =~ s/<ext>/$SETTINGS->{'publishFileExtension'}/gi;
  $filename =~ s/<artNum>/$articleRecord->{'num'}/gi;
  $filename =~ s/<catNum>/$categoryRecord->{'num'}/gi;
  $filename =~ s/<catFile>/$categoryRecord->{'filename'}/gi;  # do this last so user can't insert filename placeholders in their filename
  $filename =~ s/<artFile>/$articleRecord->{'filename'}/gi;   # do this last so user can't insert filename placeholders in their filename

  #
  return $filename;
}

# ------------------------------------------------------------------------------
# Function    : isExistingFilesWithConflictingNamesFor
# Description :
# Usage       : &IT::ArtMan::CustomMenus::Publish::isExistingFilesWithConflictingNamesFor($articleOrCategory, $record, $oldRecord);
# ------------------------------------------------------------------------------

sub isExistingFilesWithConflictingNamesFor {
  my $articleOrCategory = shift || die "no articleOrCategory value specified!";
  my $record            = shift || die "no record specified!";
  my $oldRecord         = shift;

  # get old filepaths
  my $oldFilepaths     = [];
  my %oldFilepathsHash;
  if ($oldRecord) {
    $oldFilepaths     = &_getAllPublishFilepathsFor($articleOrCategory, $oldRecord);
    %oldFilepathsHash = map { $_ => 1 } @$oldFilepaths;
  }

  # get new filepaths
  my $filepaths = &_getAllPublishFilepathsFor($articleOrCategory, $record);
  foreach my $filepath (@$filepaths) {
    if ($oldFilepathsHash{$filepath}) { next; }
    if (-e $filepath || -e lc $filepath) { return $filepath; }
  }

  return 0;
}

# ------------------------------------------------------------------------------
# Function    : createPlaceholders
# Description : Create placeholders for category (if defined), article (if defined),
#               and publish.* placeholders
# Usage       : $placeholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholders($category, $article, $template, $ruleNum);
# ------------------------------------------------------------------------------

sub createPlaceholders {
  my $category      = shift || die "no category specified!";
  my $article       = shift || die "no article specified!";
  my $template      = shift || die "no template specified!";
  my $ruleNum       = shift;

  # define article.* placeholders
  my $articlePlaceholders = {};
  if (keys %$article) {
    $articlePlaceholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForArticles($article, $template);

    # get category lists
    &IT::Template::addPlaceholderToWhitelist('article.categoryList');
    $articlePlaceholders->{'article.categoryList'} = &IT::ArtMan::CustomMenus::Publish::getArticleCategoryListHtml($article, $template, $articlePlaceholders);
  }

  # define category.* placeholders
  my $categoryPlaceholders = {};
  if ($category->{'num'}) {
    $categoryPlaceholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForCategories($template, $category->{'num'});
  }

  # define published.* placeholders
  my $publishedPlaceholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForAllPublishRules($category, $article, $ruleNum);

  # merge placeholders
  my %placeholders          = ( %$categoryPlaceholders, %$articlePlaceholders, %$publishedPlaceholders );
  return \%placeholders;
}

# ------------------------------------------------------------------------------
# Function    : createPlaceholdersForCategories
# Description : Create category.* placeholders for articles
# Usage       : $placeholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForCategories($template, $categoryNum);
# ------------------------------------------------------------------------------

sub createPlaceholdersForCategories {
  if (@_ != 2) { die "function takes 3 arguments!"; }
  my $template     = shift || die "no template specified!";
  my $categoryNum  = shift || die "no categorynum specified!";

  ### Cache ALL category placeholders (on first call only)
  my $categoryTable = "${TABLE_PREFIX}category";
  unless ($CACHED_CATEGORY_PLACEHOLDERS) {
    my $categoryRecords   = &IT::ArtMan::CustomMenus::Category::getHashOfCategoryRecords();
    foreach my $categoryNum (keys %$categoryRecords) {
      my $categoryRecord = $categoryRecords->{$categoryNum};
      my $placeholders   = &IT::App::getPlaceholdersFromRecordFields($categoryTable, $categoryRecord);
      $CACHED_CATEGORY_PLACEHOLDERS->{ $categoryRecord->{'num'} } = $placeholders
    }
  }

  ### define category.* placeholders (for categoryMenus, etc)
  my $placeholders = {};
  my $categoryRecords    = &IT::ArtMan::CustomMenus::Category::getHashOfCategoryRecords();
  my $category           = $categoryRecords->{$categoryNum} || next;
  my $cachedPlaceholders = $CACHED_CATEGORY_PLACEHOLDERS->{ $categoryNum };

  foreach my $key (keys %$cachedPlaceholders) {
    $placeholders->{"category.$key"} = $cachedPlaceholders->{$key};
  }

  $placeholders = &IT::App::replaceMediaPlaceholders($categoryTable, $category, $placeholders, $template);

  # define category field defaults
  $placeholders->{'category.num'} ||= '';
  $placeholders->{'categoryNums'}   = $categoryNum;


  #
  return $placeholders;
}

# ------------------------------------------------------------------------------
# Function    : createPlaceholdersForArticles
# Description : Create article.* placeholders for articles
# Usage       : $placeholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForArticles($article, $template);
# ------------------------------------------------------------------------------

sub createPlaceholdersForArticles {
  if (@_ != 2) { die "function takes 3 arguments!"; }
  my $article       = shift || die "no article specified!";
  my $template      = shift || die "no template specified!";

  my $placeholders = {};
  if (keys %$article) {
    my $articleTable      = $TABLE_PREFIX . 'article';
    my $placeholderPrefix = 'article.';
    $placeholders  = &IT::App::getPlaceholdersFromRecordFields($articleTable, $article, $placeholderPrefix);
    $placeholders  = &IT::App::replaceMediaPlaceholders($articleTable, $article, $placeholders, $template);
  }

  return $placeholders;
}

# ------------------------------------------------------------------------------
# Function    : createPlaceholdersForAllPublishRules
# Description : Create published.* placeholders for publish rules
# Usage       : $placeholders = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForAllPublishRules($category, $article, $ruleNum);
# ------------------------------------------------------------------------------

sub createPlaceholdersForAllPublishRules {
  if (@_ != 3) { die "function takes 3 arguments!"; }
  my $category      = shift || die "no category specified!";
  my $article       = shift || die "no article specified!";
  my $ruleNum       = shift;

  # get publish placeholders
  my $placeholders = {};
  my $publishRules = &IT::ArtMan::CustomMenus::Admin::PublishRules::getSortedArrayOfPublishRules();
  foreach my $rule (@$publishRules) {

    # get rule placeholders
    $placeholders = _createPlaceholdersForPublishRule($category, $article, $rule, $placeholders);

    # set currentPage placeholders
    if ($ruleNum && $ruleNum == $rule->{'num'}) {
      $placeholders->{"published.currentPage.url"}      = $placeholders->{"published.$rule->{'placeholder'}.url"};
      $placeholders->{"published.currentPage.webpath"}  = $placeholders->{"published.$rule->{'placeholder'}.webpath"};
      $placeholders->{"published.currentPage.filepath"} = $placeholders->{"published.$rule->{'placeholder'}.filepath"};
      $placeholders->{"published.currentPage.filename"} = $placeholders->{"published.$rule->{'placeholder'}.filename"};
    }

  }

  # assign default values
  $placeholders->{"published.currentPage.url"}      ||= '';
  $placeholders->{"published.currentPage.webpath"}  ||= '';
  $placeholders->{"published.currentPage.filepath"} ||= '';
  $placeholders->{"published.currentPage.filename"} ||= '';

  #
  return $placeholders;
};

# ------------------------------------------------------------------------------
# Function    : _createPlaceholdersForPublishRule
# Description : Create published.* placeholders for publish rules
# Usage       : $placeholders = _createPlaceholdersForPublishRule($category, $article, $rule, $placeholders);
# ------------------------------------------------------------------------------

sub _createPlaceholdersForPublishRule {
  if (@_ != 4) { die "function takes 4 arguments!"; }
  my $category     = shift || die "no category specified!";
  my $article      = shift || die "no article specified!";
  my $rule         = shift || die "no rule specified!";
  my $placeholders = shift || die "no placeholder specified!";

  # skip non-publishing rules
  if ($rule->{'type'} eq 'externalUrl') { return $placeholders; }

  # get files published
  my $isMultiFileRule = ($rule->{'type'} eq 'categoryMenu' && $rule->{'categoryListPublishTopOrCategory'} eq 'category') ||
                        ($rule->{'type'} eq 'articleList'  && $rule->{'articleListPublishGlobalOrCategoryIndexes'} eq 'category') ||
                        ($rule->{'type'} eq 'articlePages');
  my $isSingleFileRule = !$isMultiFileRule;

  # singleFileRules - loaded cached value if available
  my $cachedPlaceholders = $CACHED_PUBLISH_RULE_SINGLE_FILE_PLACEHOLDERS->{ $rule->{'num'} };
  if ($isSingleFileRule && $cachedPlaceholders) {
    @$placeholders{keys %$cachedPlaceholders} = values %$cachedPlaceholders;  # fast hash merge using hash slice
    return $placeholders
  }

  # multiFileRules - skip rule if it doesn't apply to this category
  if ($isMultiFileRule && $category->{'num'} && $rule->{'useCategoryFilter'}) {
    my $selectedCategoryNumsArray = &IT::ArtMan::CustomMenus::Admin::PublishRules::getSelectedCategoryNumsForPublishRule($rule);
    my %selectedCategoryNums      = map { $_ => 1 } @$selectedCategoryNumsArray;
    unless ($selectedCategoryNums{ $category->{'num'} }) {
      return $placeholders;
    }
  }

  # define placeholders
  my($publishDir, $publishUrl) = &IT::ArtMan::CustomMenus::Admin::PublishRules::getPublishDirAndUrl($rule);
  my($fileName)                = &_getPublishFilename($rule, $category, $article);
  my $publishUrlWithoutDomain  = $publishUrl;
  $publishUrlWithoutDomain     =~ s|^\w+://[^/]+||; # remove protocol and domain

  $placeholders->{"published.$rule->{'placeholder'}.url"}      = $publishUrl . $fileName;
  $placeholders->{"published.$rule->{'placeholder'}.webpath"}  = $publishUrlWithoutDomain . $fileName;
  $placeholders->{"published.$rule->{'placeholder'}.filepath"} = $publishDir . $fileName;
  $placeholders->{"published.$rule->{'placeholder'}.filename"} = $fileName;

  # cache values for single file rules
  if ($isSingleFileRule) {
    my $cachedPlaceholders = $CACHED_PUBLISH_RULE_SINGLE_FILE_PLACEHOLDERS->{ $rule->{'num'} } = {};
    $cachedPlaceholders->{"published.$rule->{'placeholder'}.url"}      = $placeholders->{"published.$rule->{'placeholder'}.url"};
    $cachedPlaceholders->{"published.$rule->{'placeholder'}.webpath"}  = $placeholders->{"published.$rule->{'placeholder'}.webpath"};
    $cachedPlaceholders->{"published.$rule->{'placeholder'}.filepath"} = $placeholders->{"published.$rule->{'placeholder'}.filepath"};
    $cachedPlaceholders->{"published.$rule->{'placeholder'}.filename"} = $placeholders->{"published.$rule->{'placeholder'}.filename"};
  }

  #
  return $placeholders;

}

# ------------------------------------------------------------------------------
# Function    : getArticleCategoryListHtml
# Description : Don't forget to whitelist placeholder name!
# Usage       : $placeholders->{'article.categoryList'} = &IT::ArtMan::CustomMenus::Publish::getArticleCategoryListHtml($article, $template, $placeholders);
# ------------------------------------------------------------------------------

sub getArticleCategoryListHtml {
  my $article             = shift || die "no article specified";
  my $template            = shift || die "no template specified!";
  my $articlePlaceholders = shift;
  my $templateCell = "categoryRow";

  # skip if no templatcell defined
  unless (&IT::Template::templateCellExists($template, $templateCell)) { return ''; }

  # get category nums
  my @categoryNums;
  my $categoryFieldnames = &IT::ArtMan::CustomFields::ArticleEditor::PrimaryCategory::getArticleCategoryFieldnames('all');
  foreach my $fieldname (@$categoryFieldnames) {
    my $categoryNum = $article->{$fieldname} || next;
    push @categoryNums, $categoryNum;
  }

  # get category list html;
  my $categoryListHtml;
  my $categoryRecords = &IT::ArtMan::CustomMenus::Category::getHashOfCategoryRecords();
  foreach my $index (0..$#categoryNums) {
    my $categoryNum                   = $categoryNums[$index];
    my $category                      = $categoryRecords->{$categoryNum} || next;
    my $categoryPlaceholders          = &IT::ArtMan::CustomMenus::Publish::createPlaceholdersForCategories($template, $categoryNum);
    my $publishedPlaceholders         = &createPlaceholdersForAllPublishRules($category, $article, '');
    my $placeholders                  = { %$articlePlaceholders, %$categoryPlaceholders, %$publishedPlaceholders }; # copy article placeholders so we don't modify that hash by ref
    $placeholders->{'isLastCategory'} = int ($index == $#categoryNums);
    $categoryListHtml                .= &IT::Template::templateCell($template, $templateCell, $placeholders);
  }

  #
  return $categoryListHtml;

}

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

