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

use strict;

use IT::Globals;

# user defined globals (set by Init call)
our @REQUIRED_USER_DEFINED_GLOBALS = qw(TEMPLATE_DIRS OUTPUT_ID_COMMENTS);
our $TEMPLATE_DIRS;
our $OUTPUT_ID_COMMENTS;

# library globals
our $INIT_CALLED          = 0;    # indicated whether init() has been called or not
our $GLOBAL_PLACEHOLDERS  = {};
our $TEMPLATE_CACHE       = {};
our $TEMPLATECELL_CACHE   = {};

our $CURRENT_TEMPLATE     = '';
our $CURRENT_TEMPLATECELL = '';
our $PLACEHOLDER_WHITELIST;
our $INSIDE_UPLOAD_ROW    = 0;      # don't allow nesting of upload placeholders

# define regexps
our $MATCH_PLACEHOLDER_NAME   = '[\w\.]+';
our $MATCH_MODIFIER_NAME      = '\s*:(\w+)';
our $MATCH_MODIFIER_ARGUMENTS = '(?:\((.*?)\)(?=\s*:|\$))?';
our $MATCH_MODIFIER           = $MATCH_MODIFIER_NAME . $MATCH_MODIFIER_ARGUMENTS;
our $MATCH_PLACEHOLDER        = "(\\\$($MATCH_PLACEHOLDER_NAME)(?:$MATCH_MODIFIER)*\\\$)";
our %TEMPLATE_IF_OPERATORS    = map { lc $_ => 1 } qw(eq ne == != < <= > >= isMultipleOf isNotMultipleOf);

# ----------------------------------------------------------------------------
# Function    : init
# Description : define filepaths and globals required by library
# Usage       : &init({
#                 'ARGUMENT' => "$value",
#               });
#
# Note: Don't call any functions that call &_die_unless_init_called()
#       from this function because then we'll get a confusing error about the init
#       function not being called instead of the actual error.
# ----------------------------------------------------------------------------

sub init {
  my $args = shift;
  if (!$args)                     { die("no arguments specified!"); }
  if (ref $args ne 'HASH')        { die("args must be passed by hash ref!"); }

  # error checking
  my($thisSubroutine) = (caller(0))[3];
  foreach (@REQUIRED_USER_DEFINED_GLOBALS) {
    unless (exists $args->{$_}) { &IT::Error::dieAsCaller("required $thisSubroutine argument '$_' not defined"); }
  }

  # set user defined globals
  $TEMPLATE_DIRS      = $args->{'TEMPLATE_DIRS'};
  $OUTPUT_ID_COMMENTS = $args->{'OUTPUT_ID_COMMENTS'};

  # error checking
  if (ref $TEMPLATE_DIRS ne 'ARRAY') { die "TEMPLATE_DIRS must be an array ref!"; }
  foreach my $templateDir (@$TEMPLATE_DIRS) {
    unless (-d $templateDir) { die "template dir '$templateDir' isn't a valid directory!"; }
  }

  # set sub globals
  $INIT_CALLED = 1;

}

# ----------------------------------------------------------------------------
# Function    : _die_unless_init_called()
# Description : Dies unless &init() has been called.  This is called
#               first by many public function to make sure the init function
#               for the module has been called.
# Usage       : &_die_unless_init_called();
# ----------------------------------------------------------------------------

sub _die_unless_init_called {
  if ($INIT_CALLED) { return; }

  my($thisPackage) = (caller(0))[0];
  my($subroutine) = (caller(1))[3];
  &IT::Error::dieAsCaller("You must call &${thisPackage}::init() before calling $subroutine",2);

}

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       : $html = &template($pathFromTemplateDir, \%placeholders);
# ----------------------------------------------------------------------------

sub template {
  &_die_unless_init_called();
  my $relativeTemplatePath = shift || die "no template specified!";
  my $placeholderValues    = shift || die "no placeholders specified!";
  $placeholderValues       = &_lowercasePlaceholderNames($placeholderValues);

  ### set globals
  $CURRENT_TEMPLATE = $relativeTemplatePath;
  $CURRENT_TEMPLATECELL = '';

  ### load template
  my $content;

  # first, check for template in cache
  if (exists $TEMPLATE_CACHE->{$relativeTemplatePath}) {
    $content = $TEMPLATE_CACHE->{$relativeTemplatePath}
  }

  # if not in cache, load and parse template file
  else {

    # load template content from file
    my $fullTemplatePath;
    my $foundTemplate = 0;
    foreach my $templateDir (@$TEMPLATE_DIRS) {
      $fullTemplatePath = "$templateDir/$relativeTemplatePath";
      if (-f $fullTemplatePath) { $foundTemplate = 1; last; }
    }
    unless ($foundTemplate) { die "template file '$relativeTemplatePath' doesn't exist!\n"; }

    $content = &IT::File::readFile($fullTemplatePath, 'WITHOUT_FILELOCKING');

    # remove template comments
    $content =~ s/<!--\s*template\s*comment\s*-->.*?<!--\s*\/template\s*comment\s*-->//gis;

    # remove template insert 'wrappers' and replace with tag content
    $content =~ s/<!--\s*template\s*insert\s*:\s*(.*?)\s*-->/$1/gis;

    # cache and remove templatecells
    my @cellnames = ($content =~ m/<!--\s*template\s*cell\s*:\s*([\w\.]+)\s*-->/gi);
    my %seenCell;
    foreach my $cellname (reverse @cellnames) { # parse in reverse so nested cells get cached and removed first (instead of last when they wouldn't exist anymore!)
      if ($seenCell{$cellname}++) { die "template '$relativeTemplatePath' has more than one templatecell named '$cellname'!\n"; }
      if ($content =~ s/<!--\s*template\s*cell\s*:\s*$cellname\s*-->(?:\r\n|\n)?(.*?)(?:\r\n|\n)?<!--\s*\/\s*template\s*cell\s*:\s*$cellname\s*-->//is) {
        &addTemplateCellToCache($relativeTemplatePath, $cellname, $1);
      }
    }

    # add template to cache (we're adding the pre-parsed content)
    $TEMPLATE_CACHE->{$relativeTemplatePath} = $content;
  }

  ### insert templatePlaceholderList
  $content =~ s/<!--\s*template\s*placeholder\s*list\s*-->/&_createHtmlPlaceholderList($placeholderValues)/gise;

  $content = &_parseTemplateIfTags($content, $placeholderValues);

  ### replace placeholders
  my $output = &_replacePlaceholders($content, $placeholderValues);

  # add template ID tags (templateDebugMode)
  $output = &_wrapOutputInTemplateIdTags($output, $relativeTemplatePath, '');

  return $output;
}

# ----------------------------------------------------------------------------
# Function    :
# Description : dies if templatecell isn't defined
# Usage       : &templateCell($relativeTemplatePath, $cellname, $placeholderValues);
# ----------------------------------------------------------------------------

sub templateCell {
  &_die_unless_init_called();
  my $relativeTemplatePath = shift || die "no template specified!";
  my $cellname             = shift || die "no templateCell specified!";
  my $placeholderValues    = shift || die "no placeholders specified!";

  $placeholderValues       = &_lowercasePlaceholderNames($placeholderValues);

  ### set globals
  $CURRENT_TEMPLATE     = $relativeTemplatePath;
  $CURRENT_TEMPLATECELL = $cellname;

  # cache template placeholders if needed
  unless (exists $TEMPLATECELL_CACHE->{$relativeTemplatePath}) { &template($relativeTemplatePath, {}); }

  # error checking
  unless (exists $TEMPLATECELL_CACHE->{$relativeTemplatePath}{lc $cellname}) {
    die "templatecell '$cellname' wasn't found in '$relativeTemplatePath'!\n";
  }

  # load templatecell from cache
  my $lcCellName = lc $cellname;
  my $content    = $TEMPLATECELL_CACHE->{$relativeTemplatePath}{$lcCellName};

  ### insert templatePlaceholderList

  $content =~ s/<!--\s*template\s*placeholder\s*list\s*-->/&_createHtmlPlaceholderList($placeholderValues)/gise;

  $content = &_parseTemplateIfTags($content, $placeholderValues);

  ### replace placeholders
  my $output = &_replacePlaceholders($content, $placeholderValues);

  # add template ID tags (templateDebugMode)
  $output = &_wrapOutputInTemplateIdTags($output, $relativeTemplatePath, $cellname);

  return $output;
}

# ----------------------------------------------------------------------------
# Function    : templateExists
# Description : return 0 or full template path
# Usage       : &IT::Template::templateExists($pathFromTemplateDir);
# ----------------------------------------------------------------------------

sub templateExists {
  &_die_unless_init_called();
  my $relativeTemplatePath = shift || die "no template specified!";

  # look for template file in template dirs
  my $fullTemplatePath;
  my $foundTemplate = 0;
  foreach my $templateDir (@$TEMPLATE_DIRS) {
    $fullTemplatePath = "$templateDir/$relativeTemplatePath";
    if (-f $fullTemplatePath) { $foundTemplate = 1; last; }
  }


  if    ($foundTemplate) { return $fullTemplatePath; }
  else                   { return 0; }
}

# ----------------------------------------------------------------------------
# Function    :
# Description : dies on non-existant template file
# Usage       : &IT::Template::templateCellExists($relativeTemplatePath, $cellname);
# ----------------------------------------------------------------------------

sub templateCellExists {
  &_die_unless_init_called();
  my $relativeTemplatePath = shift;
  my $cellname             = shift;

  # if templateCell dies it means the templatecell isn't defined
  eval {
    my $placeholderValues = {};
    &templateCell($relativeTemplatePath, $cellname, $placeholderValues);
  };
  my $evalError = $@;

  # we need to reset this or else if we die without an error $@ won't be reset and will
  # be displayed by eval catch around main::main() (because we're technically inside two eval blocks)
  undef $@;

  if    (!$evalError)                                               { return 1; } # no errors, templatecell exists
  elsif ($evalError =~ /^templatecell '.*?' wasn't found in '.*?'/) { return 0; } # templatecell not found error
  else                                                              { die $evalError;   } # die on other error messages

}

# ------------------------------------------------------------------------------
# Function    : getTemplateList
# Description : return array of relative template paths from template directories
# Usage       : my $templateArray = &IT::Template::getTemplateList($pathType);
# ------------------------------------------------------------------------------

sub getTemplateList {
  my $pathType        = shift || 'relativePaths';
  my $isRelativePaths = $pathType eq 'relativePaths';
  my $isAbsolutePaths = $pathType eq 'absolutePaths';

  # error checking
  if (!$isRelativePaths && !$isAbsolutePaths) {
    die "getTemplateList: Invalid pathType '$pathType', must be 'absolutePaths' or 'relativePaths'!\n";
  }

  # get template list
  my %templateList;
  foreach my $templateDir (@$TEMPLATE_DIRS) {
    my @filesAndDirs = &IT::File::dirTree($templateDir);

    foreach my $filepath (@filesAndDirs) {
      if (-d $filepath) { next; }
      if ($filepath =~ m|/_vti_cnf|) { next; } # skip MS Frontpage dirs
      if ($filepath =~ m|/create_this_folder.txt|) { next; } # skip install slug files meant to ensure folders get created

      if ($isAbsolutePaths) {
        $templateList{$filepath} = 1;
      }
      elsif ($isRelativePaths) {
        my $relativePath = $filepath;
        $relativePath =~ s|^$templateDir[\\\/]*||;
        $templateList{$relativePath} = 1;
      }

    }
  }

  my @templateList = sort keys %templateList;
  return \@templateList;
}

# ------------------------------------------------------------------------------
# Function    : isValidTemplate
# Description : check if specified template exists in templateList.
#               NOTE:  Checking that template exists in template list is MUCH
#               more secure than checking if template starts with template dir
#               path, since .. and lots of other tricks can traverse out of
#               template dir.
# Usage       : &IT::Template::isValidTemplate($relativeTemplatePath);
# ------------------------------------------------------------------------------

sub isValidTemplate {
  my $template = shift || die "no template specified!";

  my $templateArray  = &IT::Template::getTemplateList();
  my %validTemplates = map { $_ => 1 } @$templateArray;

  if ($validTemplates{$template}) { return 1; }

  return 0;
}

# ------------------------------------------------------------------------------
# Function    : _lowercasePlaceholderNames
# Description : replaces non-lowercase hash keys with lowercase ones.
# Usage       : my $lcPlaceholders = &_lowercasePlaceholderNames($placeholders);
# ------------------------------------------------------------------------------

sub _lowercasePlaceholderNames {
  my $mixedCaseplaceholders = shift || die "no placeholders specified!";
  my $lowercasePlaceholders = {};

  # lowercase placeholder names
  foreach my $name (keys %$mixedCaseplaceholders) {
    $lowercasePlaceholders->{ lc $name } = $mixedCaseplaceholders->{$name};
  }

  return $lowercasePlaceholders;
}

# ------------------------------------------------------------------------------
# Function    : _createHtmlPlaceholderList
# Description :
# Usage       : my $html = &_createHtmlPlaceholderList($placeholderValues);
# ------------------------------------------------------------------------------

sub _createHtmlPlaceholderList {
  my $placeholderValues = shift;

  # limit exec time
  my $maxExecTime = 5;
  my $execTime    = time() - $^T;
  if ($execTime > $maxExecTime) {
    return "[templatePlaceholderList halted after 5 seconds.  Try using less lists in one page.]";
  }

  # create javascript hash of names and values
  my %combinedPlaceholderNames = map { $_ => 1 } (keys %$placeholderValues, keys %$GLOBAL_PLACEHOLDERS);
  my $javascriptPlaceholderHash = '';
  foreach my $name (sort keys %combinedPlaceholderNames) {
    my $value = (exists $GLOBAL_PLACEHOLDERS->{$name}) ? $GLOBAL_PLACEHOLDERS->{$name} : $placeholderValues->{$name};
    if ($name =~ m/:/) { next; }  # skip things like 'form.attachments:highest_record_number' used by dhtml lists

    # hide value for sensitive names
    my $isSensitiveValue  = ($name =~ /pop3auth/i ||
                             $name =~ /http_auth/i ||
                             $name =~ /password/i ||
                             $name =~ /licenseProductId/i);
#    my $encodedName        = &IT::Common::htmlEncode( $name );
#    my $doubleEncodedValue = &IT::Common::javascriptEncode("'", &IT::Common::htmlEncode( $value ));

    $javascriptPlaceholderHash .= ",\n" if $javascriptPlaceholderHash;
    if ($isSensitiveValue) { $javascriptPlaceholderHash .= "'$name': \"(value not displayed)\""; }
    else                   { $javascriptPlaceholderHash .= "'$name': \"\$$name:maxChars('1000','... (truncated)'):htmlEncoded:javascriptSafe(doubleQuote)\$\""; }
  }

  # create html
  my $html = <<__HTML__;
  <script type="text/javascript" src="\$_weburl\$javascript/templatePlaceholderList.js" language="JavaScript1.2"></script>

<script>
var placeholderList = {
$javascriptPlaceholderHash
};

document.write( createPlaceholderListTableHTML(placeholderList) );
</script>
__HTML__

  return $html;

}

# ------------------------------------------------------------------------------
# Function    : _wrapOutputInTemplateIdTags
# Description :
# Usage       : my $output = &_wrapOutputInTemplateIdTags($content, $template, $templateCell);
# ------------------------------------------------------------------------------

sub _wrapOutputInTemplateIdTags {
  my $content      = shift;
  my $template     = shift || die "no template specified!";
  my $templateCell = shift;

  # skip if disabled
  unless ($OUTPUT_ID_COMMENTS) { return $content; }

  # skip non-html files
  if ($template !~ /\.html$/) { return $content; }

  # create id tags
  my $tagIdName = "templateDebugMode";
  my($startTag, $endTag);
  my $bumpedTag = "<!-- $tagIdName: template starts above, but this tag is listed here so header tags aren't modified -->";
  if ($templateCell) {
    $startTag = "<!-- $tagIdName: start template: $template - templateCell: $templateCell -->\n";
    $endTag   = "\n<!-- /$tagIdName: end template: $template - templateCell: $templateCell -->\n";
  }
  else {
    $startTag = "<!-- $tagIdName: start template: $template -->\n";
    $endTag   = "\n<!-- /$tagIdName: end template: $template -->\n";
  }

  ### add tags to content

  # html pages - add BELOW <html> tag so we don't change the doctype
  if ($content =~ s/(<html[^>]*>)/$1\n$startTag$bumpedTag/is) {
    $content = $content . $endTag;
  }
  else {  # all other pages
    $content =~ s/^\s+//gs; # remove leading whitespace
    $content =~ s/\s+$//gs; # remove trailing whitespace
    $content = $startTag . $content . $endTag;
  }

  #
  return $content;
}



# ------------------------------------------------------------------------------
# Function    : addTemplateCellToCache
# Description : Add templateCell content to cache.  Filepath is used to lookup
#               templatecell and can be fake, eg: 'internal'
# Usage       : &IT::Template::addTemplateCellToCache($relativeTemplatePath, $cellName, $html);
# ------------------------------------------------------------------------------

sub addTemplateCellToCache {
  unless (@_ == 3) { die "Function takes 3 arguments"; }
  my $relativeTemplatePath = shift || die "no filepath specified!";
  my $cellName             = shift || die "no template cell name specified!";
  my $html                 = shift;

  # remove template comments
  $html =~ s/<!--\s*template\s*comment\s*-->.*?<!--\s*\/template\s*comment\s*-->//gis;

  # remove template insert 'wrappers' and replace with tag content
  $html =~ s/<!--\s*template\s*insert\s*:\s*(.*?)\s*-->/$1/gis;

  $TEMPLATECELL_CACHE->{$relativeTemplatePath}->{lc $cellName} = $html;

}

# ------------------------------------------------------------------------------
# Function    : addPlaceholderToWhitelist
# Description : don't filter this placeholder for ssis
# Usage       : &IT::Template::addPlaceholderToWhitelist($placeholderName);
# ------------------------------------------------------------------------------

sub addPlaceholderToWhitelist {
  my $name = shift || die "no placeholder name specified";

  $PLACEHOLDER_WHITELIST ||= {};
  $PLACEHOLDER_WHITELIST->{lc $name} = 1;

}

# ------------------------------------------------------------------------------
# Function    : isPlaceholderWhitelisted
# Description : whitelisted placeholders don't have ssis removed and aren't
#               checked for ssi content
# Usage       : &IT::Template::isPlaceholderWhitelisted($placeholderName);
# ------------------------------------------------------------------------------

sub isPlaceholderWhitelisted {
  my $name = shift || die "no placeholder name specified";

  if ($INI->{'allowSSIsInInput'}) { return 1; }

  if ($PLACEHOLDER_WHITELIST->{lc $name}) { return 1; }

  return 0;
}

# ------------------------------------------------------------------------------
# Function    : _parseTemplateIfTags
# Description :
# Usage       : $html = &_parseTemplateIfTags($html, $placeholders);
# ------------------------------------------------------------------------------

sub _parseTemplateIfTags {
  my $html         = shift;
  my $placeholders = shift;

  ### NOTE:  This function is highly optimized.
  ###        Use benchmark.pm to test the speed of any changes.

  ### Assign numbers to templateIf tag pairs to make matching pairs easier
  ### ... (eg: <!-- templateIf42: $one$ eq "1" -->...<!-- /templateIf42 -->)
  my $commentTagRegexp = '(<!--\s*(\/?\s*)templateIf)((?:\s*:\s*.*?)?\s*-->)';
  my $textTagRegexp    = '(\\[ \s*(\/?\s*)templateIf)((?:\s*:\s*.*?)?\s*\\])';
  my(@ifNumberStack, $highestIfNumber);

  ### NOTE: Having these in a foreach loop seems to cause perl to silently die sometimes.
  ###       This is why they are not duplicated. The bug looks like it was perl related
  ###       are hard to track down.  Adding/removing unrelated code made it appear/disappear.

  $html =~ s{$commentTagRegexp}{
    # optimized for speed with benchmark.pm
    #my $firstHalfOfTag = $1;
    #my $isClosingTag   = $2;
    #my $lastHalfOfTag  = $3;

    if (!$2) {
      push @ifNumberStack, ++$highestIfNumber;
      $1 . $highestIfNumber . $3;
    }
    elsif ($2) {
      $1 . pop(@ifNumberStack) . $3;
    }
  }isgex;

  $html =~ s{$textTagRegexp}{
    # optimized for speed with benchmark.pm
    #my $firstHalfOfTag = $1;
    #my $isClosingTag   = $2;
    #my $lastHalfOfTag  = $3;

    if (!$2) {
      push @ifNumberStack, ++$highestIfNumber;
      $1 . $highestIfNumber . $3;
    }
    elsif ($2) {
      $1 . pop(@ifNumberStack) . $3;
    }
  }isgex;



  ### Evaluate templateIf blocks
  my $tagBlockRegexp  = '<!--\s*templateIf(\d+)\b\s*:\s*(.*?)\s*-->(.*?)<!--\s*/\s*templateIf\1\b\s*-->';
  my $textBlockRegexp = '\\[ \s*templateIf(\d+)\b\s*:\s*(.*?)\s*\\](.*?)\\[ \s*/\s*templateIf\1\b\s*\\]';

  ### NOTE: Having these in a foreach loop seems to cause perl to silently die sometimes.
  ###       This is why they are not duplicated. The bug looks like it was perl related
  ###       are hard to track down.  Adding/removing unrelated code made it appear/disappear.

  while ($html =~ s{$tagBlockRegexp}{
    # optimized for speed with benchmark.pm
    #my $ifNumber     = $1;
    #my $expression   = $2;
    #my $blockContent = $3;

    &__getTemplateIfResult($2, $3, $placeholders);  # return value
  }isgex) { 1 };

  while ($html =~ s{$textBlockRegexp}{
    # optimized for speed with benchmark.pm
    #my $ifNumber     = $1;
    #my $expression   = $2;
    #my $blockContent = $3;

    &__getTemplateIfResult($2, $3, $placeholders);  # return value
  }isgex) { 1 };

  # return html
  return $html;
}



# ------------------------------------------------------------------------------
# Function    : _getTemplateIfResult
# Description :
# Usage       : my $returnValue = &__getTemplateIfResult($ifExpression, $blockContent, $placeholders);
# ------------------------------------------------------------------------------

sub __getTemplateIfResult {
  my $expression         = shift;
  my $blockContent       = shift;
  my $placeholders       = shift;
  my $originalExpression = $expression;

  ### get expression elements
  my $matchPlaceholder  = qq|$MATCH_PLACEHOLDER|;
  my $matchQuotedString = q|(["'])([^\1]*?)\1|;
  my $matchOperator     = q|([^"'\$\s]+)|;
  my($value1, $operator, $value2);

  # optimization: test most common case first (placeholder operator quotedValue)
  my $matchPlaceholderOperatorQuotedString = $MATCH_PLACEHOLDER .'\s*'. $matchOperator .'\s*' . q|(["'])([^\6]*?)\6|;
  if ($expression =~ s/^\s*$matchPlaceholderOperatorQuotedString//s) {
    ($value1, $operator, $value2) = ($1, $5, $7);
  }
  # otherwise parse individual elements with error checking
  else {
    if    ($expression =~ s/^\s*$matchPlaceholder//s)  { $value1 = $1; }
    elsif ($expression =~ s/^\s*$matchQuotedString//s) { $value1 = $2; }
    else                                               { return "[TemplateIf Error: First value must be a \$<i>placeholder</i>\$ or \"quoted string\"]"; }
    if    ($expression =~ s/^\s*$matchOperator//s)     { $operator = $1; }
    else                                               { return "[TemplateIf Error: Can't find operator in templateIf expression!]"; }
    if    ($expression =~ s/^\s*$matchPlaceholder//s)  { $value2 = $1; }
    elsif ($expression =~ s/^\s*$matchQuotedString//s) { $value2 = $2; }
    else                                               { return "[TemplateIf Error: Second value must be a \$<i>placeholder</i>\$ or \"quoted string\"]"; }
  }
  if    ($expression =~ /\S/)                          { return "[TemplateIf Error: Expression '$originalExpression' isn't in the format 'value operator value']"; }

  # replace placeholders in values
  if ($value1 =~ m/\$/) { $value1 = &IT::Template::_replacePlaceholders($value1, $placeholders); }
  if ($value2 =~ m/\$/) { $value2 = &IT::Template::_replacePlaceholders($value2, $placeholders); }

  # evaluate expression
  my $isTrue = 0;
  my $lcOperator = lc $operator;
  if    ($lcOperator eq 'eq')              { $isTrue = $value1 eq $value2; }
  elsif ($lcOperator eq 'ne')              { $isTrue = $value1 ne $value2; }
  elsif ($operator eq '==')                { $isTrue = $value1 == $value2; }
  elsif ($operator eq '!=')                { $isTrue = $value1 != $value2; }
  elsif ($operator eq '<' )                { $isTrue = $value1 <  $value2; }
  elsif ($operator eq '<=')                { $isTrue = $value1 <= $value2; }
  elsif ($operator eq '>' )                { $isTrue = $value1 >  $value2; }
  elsif ($operator eq '>=')                { $isTrue = $value1 >= $value2; }
  elsif ($lcOperator eq 'ismultipleof')    { $isTrue = (($value1 % $value2) == 0); }
  elsif ($lcOperator eq 'isnotmultipleof') { $isTrue = (($value1 % $value2) != 0); }
  else {
    my $validOperators = join ", ", keys %TEMPLATE_IF_OPERATORS;
    return "[TemplateIf Error: Unknown operator '$operator'.  Valid Operators are $validOperators]";
  }

  # return value
  my $returnValue = '';
  if ($isTrue) {
    $returnValue = $blockContent;
    $returnValue =~ s/(^\r\n|^\n|^\r\n$|\n$)//g;  # remove leading and trailing nextlines
  }
  return $returnValue;
}

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       : my $output = &_replacePlaceholders($content, $placeholderValues);
# ----------------------------------------------------------------------------

sub _replacePlaceholders {
  my($content, $placeholderValues) = @_;
  $placeholderValues = { %$placeholderValues }; # copy placeholder values so we don't modify original ref

  # optimization: return if no placeholder chars in content
  if ($content !~ /\$/) { return $content; }

  # count SSIs before replacements (also counted in whitelisted placeholders below)
  my $countBeforeReplace = 0;
  while ($content =~ /<!--\s*#.*?-->/sg) { $countBeforeReplace++; }

  #
  $content =~ s{$MATCH_PLACEHOLDER}{
    my $placeholderWithModifiers = $1;
    my $placeholderName = $2;
    my $placeholderNameLc = lc $placeholderName;

    my $value = &_getPlaceholderValue($placeholderWithModifiers, $placeholderName, $placeholderValues);

    # count ssi's in whitelisted placeholders
    if (&isPlaceholderWhitelisted($placeholderName)) {
      while ($value =~ /<!--\s*#.*?-->/sg) { $countBeforeReplace++; }
    }


    # return placeholder value
    $value;
  }ge;

  # count SSIs after replacements
  my $countAfterReplace = 0;
  while ($content =~ /<!--\s*#.*?-->/sg) { $countAfterReplace++; }

  # error checking - make sure user didn't construct SSI by insert parts of it in side by side placeholders ($var1$$var2$)
  if ($countBeforeReplace != $countAfterReplace) {
    $content = "[placeholder content contains SSIs!]";
  }

  return $content;
}



# ------------------------------------------------------------------------------
# Function    : _getPlaceholderValue
# Description :
# Usage       : my $value = &_getPlaceholderValue($placeholderWithModifiers, $placeholderNameOnly, $placeholderValues);
# ------------------------------------------------------------------------------

sub _getPlaceholderValue {
  my $placeholderWithModifiers = shift;
  my $placeholderName          = shift;
  my $placeholders             = shift;

  # get placeholder value
  my $originalValue;
  my $placeholderNameLc = lc $placeholderName;
  my $isDefined         = 1;
  if    (exists $GLOBAL_PLACEHOLDERS->{$placeholderNameLc}) { $originalValue = $GLOBAL_PLACEHOLDERS->{$placeholderNameLc}; }
  elsif (exists $placeholders->{$placeholderNameLc})        { $originalValue = $placeholders->{$placeholderNameLc}; }
  else {
    $isDefined = 0;
    $originalValue = "[unknown placeholder \$$placeholderName\$]";
  }

  # get default value for special placeholders
  my $modifiedValue = $originalValue;
  if (ref $modifiedValue) {
    my $placeholderType   = $modifiedValue->{'type'};
    my $modifierArguments = '';
    if    ($placeholderType eq 'date')   { $modifiedValue = IT::Template::PlaceholderModifiers::dateFormat($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($placeholderType eq 'list')   { $modifiedValue = IT::Template::PlaceholderModifiers::listSeparator($placeholderName, $originalValue, $modifiedValue, undef); }
    elsif ($placeholderType eq 'upload') { $modifiedValue = __createUploadListHtml($placeholderName, $originalValue, $modifiedValue, $modifierArguments, $placeholders); }
    else {
      die "no default formatting specified for placeholder '$placeholderName' of type '$placeholderType'!\n";
    }
  }

  # Apply placeholder modifiers
  while ($placeholderWithModifiers =~ /$MATCH_MODIFIER/g) {  # modifiers must end in ): or )$ so those aren't allowed in modifiers
    my($modifierName, $modifierArguments) = ($1, $2);

    # apply defaultValue modifier to all placeholders (even undefined ones)
    if ($modifierName eq 'defaultValue') {
      $modifiedValue = &IT::Template::PlaceholderModifiers::defaultValue($placeholderName, $originalValue, $modifiedValue, $modifierArguments, $isDefined);
      $isDefined     = 1;
      next;
    }

    # skip other modifiers for undefined placeholders
    if (!$isDefined) { next; }

    # apply these modifiers for defined placeholders
    if    ($modifierName eq 'format')             { $modifiedValue = &IT::Template::PlaceholderModifiers::dateFormat($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'separator')          { $modifiedValue = &IT::Template::PlaceholderModifiers::listSeparator($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'fileAttribute')      { $modifiedValue = &IT::Template::PlaceholderModifiers::uploadFileAttribute($placeholderName, $originalValue, $modifiedValue, $modifierArguments, $placeholders); }
    elsif ($modifierName eq 'uppercase')          { $modifiedValue = &IT::Template::PlaceholderModifiers::uppercase($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'lowercase')          { $modifiedValue = &IT::Template::PlaceholderModifiers::lowercase($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'urlEncoded')         { $modifiedValue = &IT::Template::PlaceholderModifiers::urlEncoded($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'javascriptSafe')     { $modifiedValue = &IT::Template::PlaceholderModifiers::javascriptSafe($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'checkedIf')          { $modifiedValue = &IT::Template::PlaceholderModifiers::checkedIf($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'selectedIf')         { $modifiedValue = &IT::Template::PlaceholderModifiers::selectedIf($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'spacesToNbsp')       { $modifiedValue = &IT::Template::PlaceholderModifiers::spacesToNbsp($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'limitParagraphs')    { $modifiedValue = &IT::Template::PlaceholderModifiers::limitParagraphs($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'maxWords')           { $modifiedValue = &IT::Template::PlaceholderModifiers::maxWords($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'textOnly')           { $modifiedValue = &IT::Template::PlaceholderModifiers::textOnly($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'htmlEncoded')        { $modifiedValue = &IT::Template::PlaceholderModifiers::htmlEncoded($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'maxChars')           { $modifiedValue = &IT::Template::PlaceholderModifiers::maxChars($placeholderName, $originalValue, $modifiedValue, $modifierArguments); } # UNDOCUMENTED, see header
    elsif ($modifierName eq 'replace')            { $modifiedValue = &IT::Template::PlaceholderModifiers::replace($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    elsif ($modifierName eq 'repeat')             { $modifiedValue = &IT::Template::PlaceholderModifiers::repeat($placeholderName, $originalValue, $modifiedValue, $modifierArguments); } # UNDOCUMENTED, see header
    elsif ($modifierName eq 'getFieldValueByNum') { $modifiedValue = &IT::Template::PlaceholderModifiers::getFieldValueByNum($placeholderName, $originalValue, $modifiedValue, $modifierArguments); }
    else                                          { $modifiedValue = "[placeholder \$$placeholderName\$ has unknown modifier: $modifierName]"; }
  }
  my $outputValue = $modifiedValue;


  # remove SSIs from placeholder content (unless placeholder is whitelisted - meaning program generated and safe from potentially malicious user input)
  unless (&IT::Template::isPlaceholderWhitelisted($placeholderNameLc)) {
    while ($outputValue =~ s/<!--\s*#.*?-->/<!-- ssi removed by template module -->/sg) { next; }
    # NOTE: while with global replace is intentional.  Otherwise things like <!--#ssi--><!--#<!--#ssi-->ssi--> would create an ssi by filtering out only two!
  }

  # if SSIs allowed in input, remove excess whitespace that may have been added by wysiwyg
  if ($INI->{'allowSSIsInInput'}) {
    $outputValue =~ s/<!--\s*#\s*(.*?)\s*-->/<!--#$1 -->/sg;
  }


  # return placeholder value
  return $outputValue;

}

# ------------------------------------------------------------------------------
# Function    : __createUploadListHtml
# Description :
# Usage       : my $html = __createUploadListHtml();
# ------------------------------------------------------------------------------

sub __createUploadListHtml {
  my $placeholderName   = shift || die "no placeholder name specified!";
  my $originalValue     = shift;
  my $modifiedValue     = shift;
  my $modifierArguments = shift;
  my $placeholders      = shift || die "no placeholders specified!";
  my $templateCell    = "$placeholderName.row";

  # error checking
  if ($INSIDE_UPLOAD_ROW)                                                      { return "[Nesting upload placeholders isn't allowed.]"; }
  unless (&templateCellExists($IT::Template::CURRENT_TEMPLATE, $templateCell)) { return "[\$$placeholderName requires a templateCell called '<b>$templateCell</b>' in '$IT::Template::CURRENT_TEMPLATE']"; }

  # create uploadListHtml
  my $uploadListHTML;
  my @sortedUploadRows = sort { $a->{'_order'} <=> $b->{'_order'} } @{$originalValue->{'rows'}};
  my $totalRows        = scalar @sortedUploadRows;
  my $rowNumber        = 0;
  $INSIDE_UPLOAD_ROW++;
  foreach my $uploadHash (@sortedUploadRows) {
    my $cellPlaceholders = &IT::App::createUploadFilePlaceholders($placeholders, $uploadHash);
    $cellPlaceholders->{"$placeholderName.rowNumber"} = ++$rowNumber;
    $cellPlaceholders->{"$placeholderName.totalRows"} = $totalRows;
    $uploadListHTML .= &templateCell($IT::Template::CURRENT_TEMPLATE, $templateCell, $cellPlaceholders);
  }
  $INSIDE_UPLOAD_ROW--;

  #
  return $uploadListHTML;
}

# ----------------------------------------------------------------------------
# Function    : createDatePlaceholder
# Description :
# NOTE        : Don't forget to call &IT::App::Common::getTimeAdjustedToMatchGMT();
#               on value!
# Usage       : my $datePlaceholder = &IT::Template::createDatePlaceholder($epoch_time);
# ----------------------------------------------------------------------------

sub createDatePlaceholder {
  my $time = shift;
  if ($time ne int $time) { &IT::Error::dieAsCaller("'$time' isn't an integer"); }

  # NOTE: Don't forget to call &IT::App::Common::getTimeAdjustedToMatchGMT(); on time!

  my $datePlaceholder = {
    'type'          => 'date',
    'time'          => $time,
  };

  return $datePlaceholder;
}

# ----------------------------------------------------------------------------
# Function    : createListPlaceholder
# Description :
# Usage       : my $listPlaceholder = &createListPlaceholder($tabDelimitedValues);
# ----------------------------------------------------------------------------

sub createListPlaceholder {
  my $tabDelimitedValues = shift;

  my @listValues = split /\t/, $tabDelimitedValues;

  my $listPlaceholder = {
    'type'          => 'list',
    'listValues'    => \@listValues,
  };

  return $listPlaceholder
}

# ----------------------------------------------------------------------------
# Function    : createUploadPlaceholder
# Description :
# Usage       : my $uploadPlaceholder = &IT::Template::createUploadPlaceholder($dhtmlListString);
# ----------------------------------------------------------------------------

sub createUploadPlaceholder {
  my $listString = shift;
  my $listStruct   = &IT::DhtmlFieldLists::convertListStringToStruct($listString);
  my @rows       = values %{$listStruct->{'rows'}};

  my $listPlaceholder = {
    'type'   => 'upload',
    'config' => $listStruct->{'config'},
    'rows'   => \@rows,
  };

  return $listPlaceholder
}

# ----------------------------------------------------------------------------
# Function    : getPlaceholderType
# Description :
# Usage       : my $placeholderType = &IT::Template::getPlaceholderType($placeholderValue);
# ----------------------------------------------------------------------------

sub getPlaceholderType {
  my $placeholder = shift;

  if (ref $placeholder eq 'HASH') { return $placeholder->{'type'}; }
  else                            { return ''; }
}

# ----------------------------------------------------------------------------
# Function    : createPrefixedGlobalPlaceholderSet
# Description :
# Usage       : &createPrefixedGlobalPlaceholderSet($prefix, $placeholders);
# ----------------------------------------------------------------------------

sub createPrefixedGlobalPlaceholderSet {
  my $prefix       = shift || &IT::Error::dieAsCaller("no prefix specified!");
  my $placeholders = shift || &IT::Error::dieAsCaller("no placeholders specified!");

  # error checking
  &_die_unless_init_called();
  if (ref $placeholders ne 'HASH') { &IT::Error::dieAsCaller('placeholder must be a hashref'); }
  if ($prefix !~ /(\.)$/)             { &IT::Error::dieAsCaller('prefix must end in a "."'); }

  # remove old placeholder set
  foreach my $key (keys %$GLOBAL_PLACEHOLDERS) {
    if ($key =~ /^$prefix/) { delete $GLOBAL_PLACEHOLDERS->{$key}; }
  }

  # create new user defined values
  foreach my $key (keys %$placeholders) {
    my $name = $key;
    my $value = $placeholders->{$name};
    if (ref $value) { next; }                 # skip keys that are references (shouldn't normally happen);
    &setGlobalPlaceholder("$prefix$name", $value);
  }

}

# ----------------------------------------------------------------------------
# Function    :
# Description : overwrites existing placeholders of the same name
# Usage       : &IT::Template::setGlobalPlaceholder($name, $value);
# ----------------------------------------------------------------------------

sub setGlobalPlaceholder {
  &_die_unless_init_called();
  my $name = shift;
  my $value = shift;

  # error checking
  if ($name eq '') { &IT::Error::dieAsCaller('no placeholder name specified!'); }

  $GLOBAL_PLACEHOLDERS->{lc $name} = $value;
}

# ----------------------------------------------------------------------------
# Function    : appendToGlobalPlaceholder
# Description : Create or append to global placeholder
# Usage       : &IT::Template::appendToGlobalPlaceholder($name, $value);
# ----------------------------------------------------------------------------

sub appendToGlobalPlaceholder {
  &_die_unless_init_called();
  my $name = shift;
  my $value = shift;

  # error checking
  if ($name eq '') { &IT::Error::dieAsCaller('no placeholder name specified!'); }

  $GLOBAL_PLACEHOLDERS->{lc $name} .= $value;

}

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       : &IT::Template::removeGlobalPlaceholder($name);
# ----------------------------------------------------------------------------

sub removeGlobalPlaceholder {
  &_die_unless_init_called();
  my $name = shift;

  delete $GLOBAL_PLACEHOLDERS->{lc $name};
}

# ----------------------------------------------------------------------------
# Function    : getGlobalPlaceholder
# Description :
# Usage       : my $value = &IT::Template::getGlobalPlaceholder($name);
# ----------------------------------------------------------------------------

sub getGlobalPlaceholder {
  &_die_unless_init_called();
  my $name = shift;

  my $value = $GLOBAL_PLACEHOLDERS->{lc $name};

  return $value;
}

# ------------------------------------------------------------------------------
# Function    : searchAndReplace
# Description :
# Usage       : my($replacementsMade, $templatesModified) = &IT::Template::searchAndReplace($templateFileRegexp, $matchRegexp, $replacement);
# ------------------------------------------------------------------------------

sub searchAndReplace {
  my $templateFilter = shift || die "no template filter regexp defined!";
  my $matchRegexp    = shift || die "no match regexp defined";
  my $replacement    = shift;

  # get list of templates
  my $templatePaths = &IT::Template::getTemplateList('absolutePaths');

  ### search and replace template content
  my $totalTemplatesModified = 0;
  my $totalReplacementsMade  = 0;
  foreach my $templatePath (sort @$templatePaths) {
    if ($templateFilter && $templatePath !~ /$templateFilter/) { next; }
    if (!-e $templatePath) { die "Template file '$templatePath' doesn't exist!\n"; }
    if (!-f $templatePath) { die "Template file '$templatePath' isn't a file!\n"; }
    if (!-w $templatePath) { die "Template file '$templatePath' isn't writable!\n"; }

    # load content
    &IT::FileLock::getLock('exclusive');
    my $html = &IT::File::readFile($templatePath);

    # replace content
    my $regexpCode = "\$html =~ s/$matchRegexp/$replacement/gis;";
    my $replacementsMade = eval $regexpCode;
    if ($@) { die "Template SearchAndReplace Error: $@\n"; }

    if ($replacementsMade) {
      $totalTemplatesModified += 1;
      $totalReplacementsMade  += $replacementsMade;
    }

    # save content
    if ($replacementsMade) {
      &IT::File::writeFile("$templatePath", $html) || die "Error writing $templatePath! Check permissions!";
    }
    &IT::FileLock::releaseLock();
  }

  return($totalReplacementsMade, $totalTemplatesModified);

}

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

