#!/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::App::Menu::Admin;

use IT::Globals;

use strict;

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

# module globals
our $INIT_CALLED  = 0;    # indicated whether init() has been called or not
our $SETTING_FIELDS;
our $RANDOM_NUMBER_SEED;

&_setSettingFieldDefaults();

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       :
# ----------------------------------------------------------------------------

sub _setSettingFieldDefaults {

  my @field_name_label_and_rules = (

  # format: name, label, validationRules

  ### general
  ['titlebar',                               'Titlebar Message',                  'notBlank'],
  ['programName',                            'Program Name',                      'notBlank'],
  ['confirm_erase_requests',                 'Display "confirm erase" pages',     'oneOf(0,1)'],
  ['displayTemplateIdTags',                  '',                                  ''],

  # dynamic ssi and code parsing
  ['dynamicSSIparsing',                        'SSI Resolution Method',             'oneOf(DISABLED,EMULATED,SERVER_RESOLVED)'],
  ['serverResolvedSSI_httpAuthRequired',       '',   ''],
  ['serverResolvedSSI_httpAuthUsername',       '',   ''],
  ['serverResolvedSSI_httpAuthPassword',       '',   ''],
  ['serverResolvedSSI_tempFileExtension',      '',   ''],
  ['emulatedSSI_usingVirtualDirs',             '',   ''],
  ['emulatedSSI_virtualDirsToFilepaths',       '',   ''],


  # image options
  ['accepted_upload_extensions',             '',   ''],
  ['check_upload_filesize',                  'Maximum filesize checkbox',         'oneOf(0,1)'],
  ['upload_filesize_max_kbytes',             'Maximum filesize (KB)',             'positiveInt'],
  ['check_upload_image_dimensions',          'Maximum image dimensions checkbox', 'oneOf(0,1)'],
  ['image_upload_max_height',                'Maximum image height',              'positiveInt'],
  ['image_upload_max_width',                 'Maximum image width',               'positiveInt'],
  ['resize_oversize_images',                 'Automatically resize oversized images', 'oneOf(0,1)'],
  ['auto_create_thumbnails',                 'Auto-create thumbnails',            'oneOf(0,1)'],
  ['maximum_thumbnail_height',               'Maximum thumbnail height',          'positiveInt'],
  ['maximum_thumbnail_width',                'Maximum thumbnail width',           'positiveInt'],
  # security
  ['require_https',                   'Require HTTPS',                'oneOf(0,1)'],
  ['restrict_filesystem_browsing',    'Directory Ceiling Checkbox',   'oneOf(0,1)'],
  ['restricted_browsing_dir_ceiling', 'Directory Ceiling',            'allowBlank dirMustExist'],
  ['disable_php_asp_code_from_input',  'Disable Code Blocks',           'oneOf(0,1)'],
  # license
  ['licenseCompanyName',                  'License Information - Company Name','notBlank' ],
  ['licenseDomainName',                   'License Information - Domain Name', 'notBlank' ],
  ['licenseProductId',                    'License Information - Product ID',  'notBlank' ],

  ### server
  ['website_root_dir',                'Website Root Directory',        'notBlank dirMustExist trailingSlashRequired' ],
  ['upload_dir',                      'Upload Directory',              'notBlank dirMustExist trailingSlashRequired' ],
  ['publish_dir',                     'Publish Directory',             'notBlank dirMustExist trailingSlashRequired' ],
  ['upload_url',                      'Upload Folder URL',             'notBlank trailingSlashRequired' ],
  ['publish_url',                     'Publish Directory',             'notBlank trailingSlashRequired' ],
  ['webfolder_url',                   '/program-web/ Folder URL',      'notBlank trailingSlashRequired' ],
  ['wysiwygFolderUrl',                'WYSIWYG Folder URL',            'notBlank trailingSlashRequired' ],
  ['help_url',                        '',   ''],
  ['logoff_url',                      '',   ''],
  ['maxSecondsPerProcess',            'Max Process Time',              'positiveInt minNumber(1)'],
  ['sender_email',                    'Sender Email',                  'notBlank validEmail' ],
  ['sender_name',                     'Sender Name',                   '' ],
  ['admin_email',                     'Admin Email',                   'notBlank validEmail' ],
  ['send_mail_with',                  'Send mail with',                'oneOf(SENDMAIL,SMTP)'],
  ['sendmail_path',                   'Sendmail Path',                 'allowBlank fileMustExist'],
  ['smtp_hostname',                   'SMTP Server',                   'allowBlank validHostnameOrIP'],
  ['pop3_login_required_before_smtp', 'POP3 Login',                    'oneOf(0,1)'],
  ['pop3auth_hostname',               'POP3 Server',                   'allowBlank validHostnameOrIP'],
  ['pop3auth_username',               '',                 ''],
  ['pop3auth_password',               '',                 ''],


  ### language
  ['decimal_symbol',            'Decimal Symbol',             'notBlank maxLength(1)'],
  ['time_format_12_or_24',      'Time Input Format',          'oneOf(12,24)'],
  ['date_range_year_start',     'Year Range Start',           'positiveInt minNumber(1971) maxNumber(2037)'],
  ['date_range_year_end',       'Year Range End',             'positiveInt minNumber(1971) maxNumber(2037)'],

  ['localtime_hour_adjustment',       'Adjust Hours',                  'oneOf(add,minus)'],
  ['localtime_hour_offset',           'Add Hours',                     'allowBlank positiveInt maxNumber(999)'],
  ['localtime_minute_adjustment',     'Adjust Minutes',                'oneOf(add,minus)'],
  ['localtime_minute_offset',         'Add Minutes',                   'allowBlank positiveInt maxNumber(59)'],
  ['localtimeDstAdjustment',          '',                              ''],

  ['time_format',               'Time Only',                  'notBlank' ],
  ['date_format',               'Date Only',                  'notBlank' ],
  ['datetime_format',           'Date and Time',              'notBlank' ],
  ['short_weekday_names_array', '',   ''],  # Note: Array Field validation not supported yet
  ['long_weekday_names_array',  '',   ''],  # Note: Array Field validation not supported yet
  ['short_am_pm_names_array',   '',   ''],  # Note: Array Field validation not supported yet
  ['long_am_pm_names_array',    '',   ''],  # Note: Array Field validation not supported yet
  ['short_month_names_array',   '',   ''],  # Note: Array Field validation not supported yet
  ['long_month_names_array',    '',   ''],  # Note: Array Field validation not supported yet
  ['ordinals_array',            '',   ''],  # Note: Array Field validation not supported yet

  ### placeholders
  ['myPlaceholders',    '',   ''],  # Note: Hash Field validation not supported yet

  # Vendor Settings
  ['vendorName',               'Vendor Name',        'notBlank'],
  ['vendorUrl',                'Vendor Url',         'notBlank'],
  ['vendorLocation',           'Vendor Location',    'notBlank'],

  # hidden or misc
  ['programVersion',               'Program Version',                   ''],
  ['programBuild',                 'Program Buils',                   ''],
  ['lastCgidirChecksum',           '',                                  ''],
  ['isInstalled',                  '',                                  ''],
  ['isSoftwareDisabled',           '',                                  ''],
  ['needsPublishAll',              '',                                  ''],
  ['allowSystemFieldEditing',      'Allow editing of system fields',    'oneOf(0,1)'],
  ['tablePrefix',                  '',                                  ''],
  ['lastUpdated',                  'Website last Updated',              ''],
  ['dateRegistered',               '',                                  ''],


);

  # convert to hash
  foreach my $array (@field_name_label_and_rules) {
    my($name,$label,$validationRules)  = @$array;
    &addField($name,$label,$validationRules);
  }
}

# ----------------------------------------------------------------------------
# Function    : addField
# Description : Add a new admin field.  Label is for error messages only.
# Usage       : &IT::App::Menu::Admin::addField($name, $label, $validationRules);
# ----------------------------------------------------------------------------

sub addField {
  my($name, $label, $validationRules) = @_;

  if (exists $SETTING_FIELDS->{$name}) { die "admin field '$name' already exists!"; }

  &updateField(@_);
}

# ----------------------------------------------------------------------------
# Function    : updateField
# Description : Update or overwrite an existing admin field.  Label is for
#               error messages only.
# Usage       : &IT::App::Menu::Admin::updateField($name);
# ----------------------------------------------------------------------------

sub updateField {
  my($name, $label, $validationRules) = @_;

  $SETTING_FIELDS->{$name}{'name'}  = $name;
  $SETTING_FIELDS->{$name}{'label'} = $label;
  $SETTING_FIELDS->{$name}{'rules'} = $validationRules;
}

# ----------------------------------------------------------------------------
# Function    : removeField
# Description : Remove a field
# Usage       : &IT::App::Menu::Admin::removeField($name);
# ----------------------------------------------------------------------------

sub removeField {
  my($fieldName) = @_;

  delete $SETTING_FIELDS->{$fieldName} || die "Admin field '$fieldName' not found!";

}

# ----------------------------------------------------------------------------
# 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!"); }

  # check that IT::App::init has been called

  # 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
  $DEFINE_GLOBAL_PLACEHOLDERS = $args->{'DEFINE_GLOBAL_PLACEHOLDERS'};

  # error checking
  if (ref $DEFINE_GLOBAL_PLACEHOLDERS ne 'CODE') { &IT::Error::dieAsCaller("DEFINE_GLOBAL_PLACEHOLDERS must be a code ref (value: $DEFINE_GLOBAL_PLACEHOLDERS)!"); }

  # set sub globals
  $RANDOM_NUMBER_SEED = &_getRandomNumberSeed();
  $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    : loadAdminSettingsFromFile
# Description : load setting hash from file.  Unknown fields in the has are removed,
#               to prevent this set allowUnknownKeys
# Usage       : my $settings = &IT::App::Menu::Admin::loadAdminSettingsFromFile();
# Usage       : my $settings = &IT::App::Menu::Admin::loadAdminSettingsFromFile('loadUnknownKeys');
# ------------------------------------------------------------------------

sub loadAdminSettingsFromFile {
  my $loadUnknownKeys = shift;

  # error checking
  if ($loadUnknownKeys && $loadUnknownKeys ne 'loadUnknownKeys') {
    die "loadUnknownKeys must be set to '' or 'loadUnknownKeys', not '$loadUnknownKeys'!";
  }
  unless ($SETTINGS_DATAFILE) { die "\$SETTINGS_DATAFILE is not defined!"; }

  # load settings
  my $settings = eval { &IT::FreezeThaw::loadStructFromFile($SETTINGS_DATAFILE, 'ALLOW_PERL_EVAL'); };
  if ($@) { die "Can't load '$SETTINGS_DATAFILE'!\n$@"; }

  # make sure all valid fields are defined
  foreach my $fieldname (keys %$SETTING_FIELDS) {
    unless (exists $settings->{$fieldname}) { $settings->{$fieldname} = ''; }
  }

  # remove unknown fields
  unless ($loadUnknownKeys) {
    foreach my $fieldname (keys %$settings) {
      if (!exists $SETTING_FIELDS->{$fieldname} || $fieldname eq '') {
        delete $settings->{$fieldname};
      }
    }
  }

  return $settings;
}

# ------------------------------------------------------------------------
# Function    : saveAdminSettingsToFile
# Description : save settings hash to die.  Dies on unknown fields (keys).
#               To prevent this pass 'allowUnknownKeys'.
# Usage       : &IT::App::Menu::Admin::saveAdminSettingsToFile($settings);
# Usage       : &IT::App::Menu::Admin::saveAdminSettingsToFile($settings);
# ------------------------------------------------------------------------

sub saveAdminSettingsToFile {
  my $settings = shift;

  # error checking
  unless ($SETTINGS_DATAFILE) { die "\$SETTINGS_DATAFILE is not defined!"; }

  # check for unknown fields
  my @unknownFields;
  foreach my $fieldname (keys %$settings) {
    if (exists $SETTING_FIELDS->{$fieldname}) { next; }
    push @unknownFields, $fieldname;
  }
  if (@unknownFields) {
    my $unknownFieldsCSV = join ", ", @unknownFields;
    &IT::Error::dieAsCaller("unknown settings field(s) '$unknownFieldsCSV' were passed to &saveAdminSettingsToFile!");
  }

  &IT::FileLock::getLock('exclusive');
  &IT::FreezeThaw::saveStructToFile($SETTINGS_DATAFILE, $settings, 'perl');
  &IT::FileLock::releaseLock();
}

# ------------------------------------------------------------------------------
# Function    : updateTablePrefix
# Description : update database table prefix global and saved value in $SETTINGS
# Usage       : &IT::App::Menu::Admin::updateTablePrefix($tablePrefix);
# ------------------------------------------------------------------------------

sub updateTablePrefix {
  my $tablePrefix = shift || die "no prefix specified!";

  $SETTINGS = &loadAdminSettingsFromFile();

  $SETTINGS->{'tablePrefix'} = $tablePrefix;
  $TABLE_PREFIX              = $tablePrefix;

  &saveAdminSettingsToFile($SETTINGS);

}

# ------------------------------------------------------------------------
# Function    : _defineAdminMenuPlaceholders
# Description : define global placeholders use through the admin menu
# Usage       :
# ------------------------------------------------------------------------

sub _defineAdminMenuPlaceholders {
  my $placeholders = shift || {};

  ### Dynamic Lists

  # emulatedSSI_virtualDirsToFilepaths
  {
    my $listname = 'emulatedSSI_virtualDirsToFilepaths';
    unless (exists $SETTINGS->{$listname}) { die "settings value $listname isn't defined!"; }
    my $listStruct = &IT::DhtmlFieldLists::createListStructFromHash($SETTINGS->{$listname}, 'virtpath', 'filepath');
    $placeholders->{"${listname}_hiddenFields"}        = &IT::DhtmlFieldLists::createHiddenRowFields($listname, $listStruct);
    $placeholders->{"${listname}_highestRecordNumber"} = &IT::DhtmlFieldLists::getHighestRecordNumber($listStruct);
  }

  # myPlaceholders menu
  {
    my $listname = 'myPlaceholders';
    unless (exists $SETTINGS->{$listname}) { die "settings value $listname isn't defined!"; }
    my $listStruct = &IT::DhtmlFieldLists::createListStructFromHash($SETTINGS->{$listname}, 'user_placeholder_name', 'user_placeholder_value');
    $placeholders->{"${listname}_hiddenFields"}        = &IT::DhtmlFieldLists::createHiddenRowFields($listname, $listStruct);
    $placeholders->{"${listname}_highestRecordNumber"} = &IT::DhtmlFieldLists::getHighestRecordNumber($listStruct);
  }

  ### REGULAR PLACEHOLDERS

  # general menu - image modules detected
  $placeholders->{'imageModulesDetected'} = '';
  if ($FORM->{'action'} eq 'admin_general') {
    my($gdVersion, $imageMagickVersion) = &IT::Image::getModuleVersions();

    my $isGdSupported = &IT::Image::isGdAvailable();
    my $isImSupported = &IT::Image::isImageMagickAvailable();
    if ($gdVersion) {
      $placeholders->{'imageModulesDetected'} .= "GD v$gdVersion";
      if ($IT::Image::GD_EVAL_ERROR) {
        $placeholders->{'imageModulesDetected'} .= qq| (unsupported, <a href="javascript:alert('|;
        $placeholders->{'imageModulesDetected'} .= &IT::Common::javascriptEncode("'", $IT::Image::GD_EVAL_ERROR);
        $placeholders->{'imageModulesDetected'} .= qq|')">loading errors</a>)\n|;
      }
      elsif (!$isGdSupported) {
        $placeholders->{'imageModulesDetected'} .= " (unsupported, v$IT::Image::MINIMUM_GD_VERSION or later required)";
      }
      $placeholders->{'imageModulesDetected'} .= "<br />\n";
    }
    if ($imageMagickVersion) {
      $placeholders->{'imageModulesDetected'} .= "ImageMagick v$imageMagickVersion";
      if ($IT::Image::IM_EVAL_ERROR) {
        $placeholders->{'imageModulesDetected'} .= qq| (unsupported, <a href="javascript:alert('|;
        $placeholders->{'imageModulesDetected'} .= &IT::Common::javascriptEncode("'", $IT::Image::IM_EVAL_ERROR);
        $placeholders->{'imageModulesDetected'} .= qq|')">loading errors</a>)\n|;
      }
      elsif (!$isImSupported) {
        $placeholders->{'imageModulesDetected'} .= " (unsupported, v$IT::Image::MINIMUM_IMAGE_MAGICK_VERSION or later required)";
      }
      $placeholders->{'imageModulesDetected'} .= "<br />\n";
    }

    unless ($isGdSupported || $isImSupported) {
      $placeholders->{'imageModulesDetected'} &&= "$placeholders->{'imageModulesDetected'}<br/>";
      $placeholders->{'imageModulesDetected'} .= "<b>No supported modules detected. Image thumbnailing and resizing not available.</b><br/>";
    }
  }
  else {
    $placeholders->{'imageModulesDetected'} = "[only defined for general menu]";
  }


  # server menu - server_time
  my $time     = time();
  $placeholders->{'gmtDateAndTime'} = &IT::Template::createDatePlaceholder( $time );

  # server menu -  adjusted_local_time
  my $adjustedLocalTime               = &IT::App::Common::getTimeAdjustedToMatchGMT($time);
  my $adjustedLocalDateTime           = &IT::Template::createDatePlaceholder($adjustedLocalTime);
  $placeholders->{'localDateAndTime'} = $adjustedLocalDateTime;

  # server menu - suggest sendmail locations
  $placeholders->{'valid_sendmail_locations'} = '';
  my $isSendMailLocationValid = $SETTINGS->{'sendmail_path'} && -e $SETTINGS->{'sendmail_path'};
  if (!$isSendMailLocationValid) {
    foreach my $sendmailPath (&IT::App::Install::findSendmailPaths()) {
      $placeholders->{'valid_sendmail_locations'} .= &templateCell('admin/menus/admin/server.html','sendmailrow', { 'path' => $sendmailPath });
    }
  }

  # language menu - current time for date format preview
  my $currentTime = $adjustedLocalDateTime;
  $placeholders->{'current_time'} = $currentTime;

  # split array values into multiple fields (eg: $fieldname_array_1, _2, _3, etc);
  my $elementPlaceholders = &_getPlaceholdersFromArrayFields();
  $placeholders = { %$placeholders, %$elementPlaceholders }; # add/overwrite keys

  # vendor menu - _isSublicensingAllowed
  $placeholders->{'_isSublicensingAllowed'} = &IT::App::License::isSublicensingAllowed();


  return $placeholders;
}

# ------------------------------------------------------------------------
# Function    : showMenu
# Description : Display an Admin menu
# Usage       : &IT::App::Menu::Admin::showMenu();
# ------------------------------------------------------------------------

sub showMenu {
  &_die_unless_init_called();
  my $menuName     = shift;

  &_showMenu_errorChecking($menuName);

  # define placeholders
  my $placeholders = { %$SETTINGS };  # copy hash so we don't modify $SETTINGS
  $placeholders = &_defineAdminMenuPlaceholders($placeholders);

  &IT::App::displayInterface({
    'template'           => "admin/menus/admin/$menuName.html",
    'templatecell'       => "$menuName",
    'selectedMenuOption' => 'admin',
    'menuGroup'          => 'none',
    'placeholders'       => $placeholders,  # use saved SETTINGS to populate fields
  });
}

# ------------------------------------------------------------------------
# Function    : _showMenu_errorChecking
# Description :
# Usage       : &IT::App::Menu::Admin::_showMenu_errorChecking();
# ------------------------------------------------------------------------

sub _showMenu_errorChecking {
  my $menuName  = shift;

  unless ($menuName)                  { &IT::Error::dieAsCaller("No menuName was specified to display!", 2); }
  my %validMenuNames = map { $_ => 1 } qw(homepage general server regional placeholders vendor);
  unless ($validMenuNames{$menuName}) { &IT::Error::dieAsCaller("'$menuName' isn't a valid admin menu name!", 2); }
}

# ------------------------------------------------------------------------
# Function    : _showMenu_withSaveErrors
# Description :
# Usage       : &IT::App::Menu::Admin::showMenu_withSaveErrors();
# ------------------------------------------------------------------------

sub _showMenu_withSaveErrors {
  &_die_unless_init_called();
  my $menuName     = shift;
  my $errorHTML    = shift;

  &_showMenu_errorChecking($menuName);

  # display errors
  &IT::App::alert("<b>The following errors occurred:\n</b>", 'keepHTML');
  &IT::App::alert($errorHTML);

  # define placeholders
  my $placeholders = &_defineAdminMenuPlaceholders();

  foreach my $key (keys %$FORM) {  # copy values from FORM
    if (ref $key) { next; } # skip references
    $placeholders->{$key} = $FORM->{$key};
  }

  ### Dynamic Lists - maintain state

  if ($FORM->{'emulatedSSI_virtualDirsToFilepaths'}) {
   my $listName = 'emulatedSSI_virtualDirsToFilepaths';
   my $listStruct = &IT::DhtmlFieldLists::getListStructFromFormData($listName);
   $placeholders->{"${listName}_hiddenFields"}        = &IT::DhtmlFieldLists::createHiddenRowFields($listName, $listStruct);
   $placeholders->{"${listName}_highestRecordNumber"} = &IT::DhtmlFieldLists::getHighestRecordNumber($listStruct);
  }

  if ($FORM->{'myPlaceholders'}) {
   my $listName   = 'myPlaceholders';
   my $listStruct = &IT::DhtmlFieldLists::getListStructFromFormData($listName);
   $placeholders->{"${listName}_hiddenFields"}        = &IT::DhtmlFieldLists::createHiddenRowFields($listName, $listStruct);
   $placeholders->{"${listName}_highestRecordNumber"} = &IT::DhtmlFieldLists::getHighestRecordNumber($listStruct);
  }




  # display menu
  &IT::App::displayInterface({
    'template'           => "admin/menus/admin/$menuName.html",
    'templatecell'       => "$menuName",
    'selectedMenuOption' => 'admin',
    'menuGroup'          => 'none',
    'placeholders'       => $placeholders,  # repopulate fields with FORM values
  });

}

# ------------------------------------------------------------------------
# Function    : getFieldValidationErrors
# Description :
# Usage       : my $errors = &IT::App::Menu::Admin::getFieldValidationErrors();
# ------------------------------------------------------------------------

sub getFieldValidationErrors {
  my $errors;
  my $settingFields = $IT::App::Menu::Admin::SETTING_FIELDS;

  foreach my $fieldname (keys %$settingFields) {
    unless (exists $FORM->{$fieldname}) { next; } # skip field if it wasn't submitted

    my $fieldLabel  = $settingFields->{$fieldname}{'label'} || $settingFields->{$fieldname}{'name'};
    $errors .= &IT::Validation::validationError($fieldLabel, $FORM->{$fieldname}, $settingFields->{$fieldname}{'rules'});

    if ($fieldname eq 'require_https' && $FORM->{'require_https'} && !&IT::Common::isSecureHttpsConnection()) {
      $errors .= "You must be using a HTTPS connection to enable 'require HTTPS'!\n";
    }
  }

  return $errors;

}

# ------------------------------------------------------------------------
# Function    : saveSettings
# Description :
# Usage       : &IT::App::Menu::Admin::saveSettings();
# ------------------------------------------------------------------------

sub saveSettings {
  unless ($FORM->{'admin_menu'}) { &IT::Error::dieAsCaller("no 'admin_menu' field value specified!"); };

  # demo mode message
  &IT::App::demoModeAlertAndAction("Saving admin settings is disabled in demo mode.", sub { &showMenu('homepage'); });

  ### Get errors
  my $errors = &IT::App::Menu::Admin::getFieldValidationErrors();
  if (exists $FORM->{'licenseProductId'} && !&IT::App::License::isValidProductId($FORM->{'licenseProductId'})) {
    $errors .= "Invalid Product ID!\n";
  }

  # wysiwygFolderUrl error checking
  if ($FORM->{'wysiwygFolderUrl'}) {
    my $url = $FORM->{'wysiwygFolderUrl'};
    if ($url =~ m|^\w+://|) { $errors .= "WYSIWYG Folder URL must be the web path only without a domain! (eg: /wysiwyg/path/)\n"; }
    if ($url !~ m|^/|)      { $errors .= "WYSIWYG Folder URL must start with a leading slash! (eg: /wysiwyg/path/)\n"; }
  }

  ### Show errors
  if ($errors) {
    &_showMenu_withSaveErrors( $FORM->{'admin_menu'}, $errors );
    exit;
  }

  ### update settings
  my $oldProductId = $SETTINGS->{'licenseProductId'};

  # update submitted fields
  foreach my $fieldname (keys %$FORM) {
    if (exists $SETTINGS->{$fieldname}) { $SETTINGS->{$fieldname} = $FORM->{$fieldname}; }
    else                                { &_debug_showUnsavedField($fieldname); }
  }

  # update array fields (save multi field values as arrays)
  my $arrayFields = &_getArrayFieldsFromFormData();
  $SETTINGS = { %$SETTINGS, %$arrayFields }; # add/overwrite keys

  ### Dynamic Lists

  # emulatedSSI_virtualDirsToFilepaths
  if ($FORM->{'emulatedSSI_virtualDirsToFilepaths'}) {
    $SETTINGS->{'emulatedSSI_virtualDirsToFilepaths'} = &IT::DhtmlFieldLists::getHashFromFormData('emulatedSSI_virtualDirsToFilepaths', 'virtpath', 'filepath');
  }

  # myPlaceholders
  if ($FORM->{'myPlaceholders'}) {
    $SETTINGS->{'myPlaceholders'} = &IT::DhtmlFieldLists::getHashFromFormData('myPlaceholders', 'user_placeholder_name', 'user_placeholder_value');
  }

  ### save settings to file

  &saveAdminSettingsToFile($SETTINGS);


  ### Re-register if licenseProductId changed
  if ($SETTINGS->{'licenseProductId'} ne $oldProductId) {
    &IT::App::Register::registerIfNeeded('forceRegister');
  }

  ### DISPLAY OUTPUT

  # define placeholders
  my $placeholders          = { %$SETTINGS }; # copy hash so we don't modify $newSettings
  my $adminMenuPlaceholders = &_defineAdminMenuPlaceholders($placeholders); # update admin menu placeholders
  $placeholders = { %$placeholders, %$adminMenuPlaceholders }; # add/overwrite keys
  &{$DEFINE_GLOBAL_PLACEHOLDERS}() if defined &$DEFINE_GLOBAL_PLACEHOLDERS; # update global placeholders (some are based on $SETTINGS)

  # show saved alert
#  &IT::App::alert("Settings have been saved.");

  &IT::App::displayInterface({
    'template'           => 'admin/menus/admin/homepage.html',
    'templatecell'       => 'homepage',
    'selectedMenuOption' => 'admin',
    'menuGroup'          => 'none',
    'placeholders'       => $placeholders,
  });


}


# ------------------------------------------------------------------------
# Function    : testMail
# Description : send a test message
# Usage       : &IT::App::Menu::Admin::testMail();
# ------------------------------------------------------------------------

sub testMail {

  # try to send message
  if ($FORM->{'send_message'}) {
    eval { &_testMail_sendMessage(); };
    my $error = $@;

    $error =~ s/ at \S+ line \d+.*$//s; # remove "at ./file.pm line 234" from error we show user

    if ($error) { &_testMail_emailForm($error); }
    else        { &_testMail_successPage(); }
  }

  # display email form
  else {
    &_testMail_emailForm();
  }

}

# ------------------------------------------------------------------------
# Function    : _testMail_emailForm
# Description :
# Usage       : &IT::App::Menu::Admin::_testMail_emailForm($error);
# ------------------------------------------------------------------------

sub _testMail_emailForm {
  my $error = shift;

  my $placeholders;
  &IT::App::setAlertWidthTo('narrow');
  $placeholders->{'error'}   = $error || &IT::App::alert('');
  $placeholders->{'email'}   = $FORM->{'email'};
  $placeholders->{'content'} = &templateCell('admin/popups/testSendingMail.html', 'mailtest', $placeholders);

  print &IT::Common::contentType("text/html");
  print &template('admin/popups/testSendingMail.html', $placeholders);
  exit;
}


# ------------------------------------------------------------------------
# Function    : _testMail_sendMessage
# Description :
# Usage       : &IT::App::Menu::Admin::_testMail_sendMessage();
# ------------------------------------------------------------------------

sub _testMail_sendMessage {

  # disable in demo mode
  &IT::App::demoModeAlertAndAction("Sending test emails is disabled in demo mode.", \&_testMail_emailForm);

  # error checking
  unless (&IT::Validation::isValidEmail($FORM->{'email'})) {
    my $error = "Invalid email address!";
    &_testMail_emailForm($error);
    exit;
  }

  # call email::init with form values (that user may have not saved yet!)
  &IT::Email::init({
    'SEND_MAIL_WITH'         => $FORM->{'send_mail_with'},
    'SENDMAIL_PATH'          => $FORM->{'sendmail_path'},
    'SMTP_HOST'              => $FORM->{'smtp_hostname'},
    'POP3_LOGIN_BEFORE_SMTP' => $FORM->{'pop3_login_required_before_smtp'},
    'POP3_HOST'              => $FORM->{'pop3auth_hostname'},
    'POP3_USERNAME'          => $FORM->{'pop3auth_username'},
    'POP3_PASSWORD'          => $FORM->{'pop3auth_password'},
  });

  # load email
  my $placeholders;
  $placeholders->{'sender_name'}  = $FORM->{'sender_name'};
  $placeholders->{'sender_email'} = $FORM->{'sender_email'};
  $placeholders->{'email_to'}     = $FORM->{'email'};
  my $messageWithHeaders          = &template('admin/emails/testMail.txt', $placeholders);

  unless (&IT::App::inDemoMode()) {
    &IT::Email::sendMessage($messageWithHeaders);
  }

}

# ------------------------------------------------------------------------
# Function    : _testMail_successPage
# Description :
# Usage       : &IT::App::Menu::Admin::_testMail_successPage();
# ------------------------------------------------------------------------

sub _testMail_successPage {
  my $placeholders;
  $placeholders->{'content'} = &templateCell('admin/popups/testSendingMail.html', 'mailtestsucceeded', {});

  print &IT::Common::contentType("text/html");
  print &template('admin/popups/testSendingMail.html', $placeholders);
  exit;
}

# ----------------------------------------------------------------------------
# Function    : _getPlaceholdersFromArrayFields
# Description :
# Usage       : my $elementPlaceholders = &_getPlaceholdersFromArrayFields();
# ----------------------------------------------------------------------------

sub _getPlaceholdersFromArrayFields {
  my @arrayFields  = grep /_array$/, keys %$SETTINGS;
  my $elementPlaceholders = {};

  foreach my $fieldname (@arrayFields) {
    my $thisArray = $SETTINGS->{$fieldname};

    # split array values into multiple fields (eg: $fieldname_array_1, _2, _3, etc);
    for my $index (0..$#$thisArray) {
      my $fieldNumber      = $index + 1;
      my $placeholderName  = "${fieldname}_$fieldNumber";
      my $placeholderValue = $SETTINGS->{$fieldname}[$index];
      $elementPlaceholders->{$placeholderName} = $placeholderValue;
    }
  }

  return $elementPlaceholders;
}

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       : my $arrayFields = &_getArrayFieldsFromFormData()
# ----------------------------------------------------------------------------

sub _getArrayFieldsFromFormData{
  my @arrayFieldnames = grep /_array$/, keys %$SETTINGS;
  my $arrayFields = {};

  # array fields (save multi field values as arrays)
  foreach my $arrayFieldname (@arrayFieldnames) {
    my @elementFormFields = grep /^${arrayFieldname}_\d+$/, keys %$FORM;
    unless (@elementFormFields) { last; } # if no element values submitted, don't update field

    $arrayFields->{$arrayFieldname} = [];

    foreach my $elementFormField (@elementFormFields) {
      $elementFormField =~ /^${arrayFieldname}_(\d+)$/;
      my $fieldNumber   = $1 || die "no field number matched!";
      my $index         = $fieldNumber - 1;  # field numbers start at 1, Array indexes start at 0
      $arrayFields->{$arrayFieldname}[$index] = $FORM->{$elementFormField};
    }
  }

  return $arrayFields;

}

# ------------------------------------------------------------------------------
# Function    : _getRandomNumberSeed
# Description :
# Usage       :
# ------------------------------------------------------------------------------

sub _getRandomNumberSeed {
  
  my $pseudoRandomData = 'M;7DD9f]286YD;VUc:f5C:STH:6YTjgiA;F0HlC4Pj2D]o3`I?gPD4T545dEnM1UlMoGLG04Q705'.
  'E37T1o7Uia3D1o35]c2d5c2R=]nVEFjb1D;UiA;F1O;4mHM96mKiB8D4T545dEn1UlMoGLG:7mi;GmT86QL960G?2E[iDE4nCIa<'.
  'g`ZnEiEM9VES=f5RnCIR96=I<W1E<DEF3F5E9f5DjbD[;7DD=FeL:61i9cTF250ZnDePM<cHZ3fEC96YS93HZ:7m686QI9eaR;V1'.
  'U8W1i9b@D4T545dEn1UlMoGLG;fECM96YS95aR;V1U8W1i9b=]j3MM>21V97iI9GE7;WiK<SUI;G0AiDE4nCIa<g`ZMnDQI8V5N<'.
  'V4ZnFES5FeL:610<F]D=6mT260HiSdRlS0Gj3MM>21D87ES4VENM8V5296<]ieme5e1i3D=3k3Y[iV1A=f5296=I<W1E<F5DiWT_'.
  ':6YTjfeB<R@HM=fEM92@Ik213151424Yg4RT^>R=D871E4F5G:7mT97iE9b=]j2\HmC`JmC`JMlC0Ij2DZlcMM>21F86EL=7iEo2'.
  '@Aig9A;fED261\?bdD=F5R:69Y5V]R:Wm\M?b1D87ES4VEN8V5296<^o3<InVEFjb1F86EL=7iEj7MM>21Q=65R>3UJ;VENMjbhZ'.
  'hBQM87a[iDE4nCIc;VUM;VXZnG5R;d5N8V]D92@D7RE]jb13151424YgM4RT^>R=L:6mE;GmE0V]M<feN>4YA;64G?2PD4T545dE'.
  'n1UlMoGLG;fEC96YSM941O;6eI;DYA;64G?2PD4T545dEn1UlMoGLG;fEC96YS95aR;V1U8W1i9b=]Mkb104D]g4Dem7TEdkb104'.
  'D]g4Dem7U9e4Emi3TXLidmg25523bPD14Y6>R=hM5e107Uie1D5215hG?2PD=FeL:61i9bPD=F5R:69Y5V]R:WlLiR<Lif1A>7m3'.
  'M:6YC95iE9RDInR9i5cHZ0V]M;6]NnCIG971086=Ejb=R96=I<W1E<EmO9G1WM87iEkG1OiRPBkWiE9VES=f5RkWiE<f]R=bYC9V'.
  'D_igeU97iYhBD[?7UI;G0H1<FeN9b@Q7S`Ple\Plc`Ij3L`';

  my $randomNumberSeed;
  ($randomNumberSeed = $pseudoRandomData) =~ s/(.+)/$_=$1;s|([a-z])|chr(ord($1)-64)|ge;unpack'u',$_/ese;
  return $randomNumberSeed;

}

# ----------------------------------------------------------------------------
# Function    :
# Description :
# Usage       : &_debug_showUnsavedField( $fieldname );
# ----------------------------------------------------------------------------

sub _debug_showUnsavedField {
  my $fieldname = shift;

  # skip non-settings fields or special fields
  if ($fieldname =~ /:/)                { next; } # fields with : are special (multiValue, DHTMLlist, etc)
  if ($fieldname eq 'action')           { next; }
  if ($fieldname eq '_defaultAction')   { next; }
  if ($fieldname eq 'admin_menu')       { next; }
  if ($fieldname eq 'myPlaceholders'){ next; }
  if ($fieldname =~ /^(\w+_array)_\d+$/ && $SETTINGS->{$1}) { next; } # skip array element fields

  &IT::App::debug("unknown setting field '$fieldname' was NOT saved!");
}

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

