<?php
/** @noinspection PhpMissingParamTypeInspection */
/** @noinspection PhpUnused */
/** @noinspection PhpIncludeInspection */
$FM_VERS = "10.03"; // script version

/* ex:set ts=4 sw=4 et:
 * FormMail PHP script from Tectite.com.
 * FormMail versions <= 8 support PHP 4.
 * FormMail version 9 supports PHP 5 through 7.
 * FormMail versions >= 10 supports PHP 8.
 *
 * Copyright (c) 2001-2022 Open Concepts (Vic) Pty Ltd
 * (ABN 12 130 429 248), Melbourne, Australia.
 * This script is free for all use as described in the "Copying and Use" and
 * "Warranty and Disclaimer" sections below.
 *
 * Visit us at http://www.tectite.com/ for updates and more information.
 *
 *** If you use Tectite FormMail, please support its development and other
 *** freeware products by putting the following link on your website:
 ***  Visit www.tectite.com for free <a href="http://www.tectite.com/">FormMail</a>.
 *
 * Author: Russell Robinson
 * First released: 2nd October 2001 
 *
 * Read This First
 * ~~~~~~~~~~~~~~~
 *  This script is very well documented and quite large!  It looks daunting,
 *  but really isn't.
 *  If you have experience with PHP or other scripting languages,
 *  here's what you *need* to read:
 *      - Configuration (TARGET_EMAIL & DEF_ALERT)
 *      - Creating Forms
 *  That's it!  (Alternatively, just read the Quick Start and/or
 *  Quicker Start section below).
 *  Full configuration documentation is here:
 *      http://www.tectite.com/fmdoc/index.php
 *
 *  NOTE: do not read or modify this script or any PHP script
 *  with DreamWeaver or FrontPage!
 *  Many versions of those programs silently corrupt PHP scripts.
 *
 * Purpose:
 * ~~~~~~~~
 *  To accept information from an HTML form via HTTP and mail it to recipients.
 *
 * What does this PHP script do?
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *  On your web site, you may have one or more HTML forms that accept
 *  information from people visiting your website.  Your aim is for your
 *  website to email that information to you and/or add it to a database.
 *  FormMail performs those functions.
 *
 * Quick Start
 * ~~~~~~~~~~~
 *  1. Edit this file and set TARGET_EMAIL for your requirements (near
 *      line 512 in this file - replace "yourhost\.com" with your mail server's
 *      name).  We also strongly recommend you set DEF_ALERT (the next
 *      configuration below TARGET_EMAIL).
 *  2. Install this file as formmail.php (or other name ending in .php)
 *     on your web server.
 *     Test alerts by using your browser to open a URL to the script:
 *          http://www.yourhost.com/formmail.php?testalert=1
 *     Alerts are the only way FormMail can tell you the details of
 *     errors or faults.
 *  3. Create an HTML form and:
 *      - specify a hidden field called "recipients" with the email address
 *        of the person to receive the form's results.
 *      - in the your form tag set the action attribute to
 *        the formmail.php you uploaded to your web server
 *
 *  Once you have FormMail working, you may be interested in some advanced
 *  usage and features.  We have HOW-TO guides at www.tectite.com which
 *  describe many of the advanced processing you can do with FormMail.
 *      http://www.tectite.com/fmhowto/guides.php
 *
 * Quicker Start
 * ~~~~~~~~~~~~~
 *  Use the FormMail Configuration Wizard here:
 *      http://www.tectite.com/wizards/fmconf.php
 *  By answering a few questions you'll get a configured FormMail and
 *  a sample HTML form ready to upload and use on your server.
 *
 * Features
 * ~~~~~~~~
 *  For a list of features go to: http://www.tectite.com/formmailpage.php
 *
 * Security
 * ~~~~~~~~
 *  Security is the primary concern in accepting data from your website
 *  visitors.
 *  Tectite FormMail has several security features designed into it.  Note,
 *  however, it requires configuration for your particular web site.
 *
 * Configuration
 * ~~~~~~~~~~~~~
 *  To configure this script, go to the section titled "CONFIGURATION"
 *  (after reading the legal stuff below).
 *
 *  There is only one mandatory setting: TARGET_EMAIL
 *  and one strongly recommended setting: DEF_ALERT
 *
 *  Full configuration information is available here:
 *      http://www.tectite.com/fmdoc/index.php
 *
 * Creating Forms
 * ~~~~~~~~~~~~~~
 *  Go to this URL to learn how to write HTML forms for use with
 *  Tectite FormMail: http://www.tectite.com/fmdoc/creating_forms.php
 *
 * Copying and Use (Software License)
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *  Tectite FormMail is provided free of charge and may be freely distributed
 *  and used provided that you:
 *      1. keep this header, including copyright and comments,
 *         in place and unmodified; and,
 *      2. do not charge a fee for distributing it, without an agreement
 *         in writing with Root Software allowing you to do so; and,
 *      3. if you modify FormMail before distributing it, you clearly
 *         identify:
 *              a) who you are
 *              b) how to contact you
 *              c) what changes you have made
 *              d) why you have made those changes.
 *
 *  By using any of our products, including this script, you are
 *  agreeing to our standard Terms and Conditions, available here:
 *      http://www.tectite.com/TermsAndConditions.pdf
 *
 *  This is free software and the Software License shown above
 *  is to be read in conjunction with our standard Terms and Conditions.
 *
 * Warranty and Disclaimer
 * ~~~~~~~~~~~~~~~~~~~~~~~
 *  Tectite FormMail is provided free-of-charge and with ABSOLUTELY NO WARRANTY.
 *  It has not been verified for use in critical applications, including,
 *  but not limited to, medicine, defense, aircraft, space exploration,
 *  or any other potentially dangerous activity.
 *
 *  By using Tectite FormMail you agree to indemnify Root Software and
 *  Open Concepts (Vic) Pty Ltd, their agents, employees, directors and
 *  associated companies and businesses from any liability whatsoever.
 *
 * We still care
 * ~~~~~~~~~~~~~
 *  If you find a bug or fault in FormMail, please report it to us.
 *  We will respond to your report and make endeavours to rectify any
 *  faults you've detected as soon as possible.
 *
 *  To contact us view our contact information:
 *      http://www.tectite.com/contacts.php
 *
 * Version History
 * ~~~~~~~~~~~~~~~
 *  Near the top of this file, you'll find its version. The version
 *  line looks like this:
 *       $FM_VERS = "N.MM";     /* script version ...
 *
 *  The version history used to be located within this file.  However,
 *  starting with Version 8.00 we've moved it...
 *
 *  You can read the complete version history of FormMail on our
 *  main website here:
 *   http://www.tectite.com/fmdoc/version_history.php
 */

/**
 * An alternative function for sending email.
 * You can override this variable in a hook script to use a different function.
 */
$ALT_MAIL_FUNCTION = '';

FMDebug('Submission to: ' . (isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '') . ' from: ' .
        (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''));

if (isset($_SERVER['REQUEST_METHOD']) && strtoupper($_SERVER['REQUEST_METHOD']) === 'OPTIONS') {
	FMDebug('CORS OPTIONS request');
	CORS_Response();
	FormMailExit();
}

//
// Capture the current date and time, for various purposes.
//
date_default_timezone_set('UTC'); /* prevent notice in PHP 5.1+ */
$lNow = time();

ini_set('track_errors',1); // enable $php_errormsg

$aAlertInfo = array();

$sLangID   = ""; // the language ID
$aMessages = array(); // all FormMail messages in the appropriate language

function FormMailExit($s_mesg = '')
{
	if (($incFile = hookFileExists("fmhookonexit.inc")) !== false) {
		@include($incFile);
	}
	exit($s_mesg);
}

/**
 * Interrogate and manage the execution environment.
 *
 * @package    tectite_formmail
 * @subpackage setup
 */
class ExecEnv
{
	/**
	 * Value of phpversion().
	 *
	 * @var string
	 */
	private $_sPHPVersionString;

	/**
	 * Array containing the elements of the PHP version
	 *
	 * @var array
	 */
	private $_aPHPVersion;

	/**
	 * The URL for the script.
	 *
	 * @var string
	 */
	private $_sScript;

	/**
	 * Construct the class, and check PHP version.
	 */
	function __construct()
	{
		$this->_Init();
		$this->_CheckVersion();
	}

	/**
	 * Initialise the object.
	 * Sets {@link $_aPHPVersion} and {@link $_sPHPVersionString}
	 */
	private function _Init()
	{
		$this->_sPHPVersionString = phpversion();
		$this->_aPHPVersion       = explode(".",$this->_ZapHyphenPart($this->_sPHPVersionString));
	}

	/**
	 * Remove any hyphenated component of a version string.
	 *
	 * @param $str
	 *
	 * @return false|string
	 */
	private function _ZapHyphenPart($str)
	{
		if (($i_pos = strpos($str,'-')) !== false) {
			return substr($str,0,$i_pos);
		} else {
			return $str;
		}
	}

	/**
	 * @return array
	 */
	public function getPHPVersion()
	{
		return $this->_aPHPVersion;
	}

	/**
	 * @return string
	 */
	public function getPHPVersionString()
	{
		return $this->_sPHPVersionString;
	}

	/**
	 * Check for old version of PHP - die with a message if too old.
	 *
	 * This is actually not required because PHP 4 won't even accept
	 * the syntax of a PHP 5 script. However, we might need some
	 * other version check in the future, so this is a useful method
	 * to have around in that case.
	 */
	private function _CheckVersion()
	{
		$s_req_string = "5.0.0"; // We only support PHP 5 onward.
		$a_too_old    = explode(".",$s_req_string);

		$i_cannot_use = ($a_too_old[0] * 10000) + ($a_too_old[1] * 100) + $a_too_old[2];

		$i_this_num = ($this->_aPHPVersion[0] * 10000) + ($this->_aPHPVersion[1] * 100) +
		              $this->_aPHPVersion[2];

		if ($i_this_num <= $i_cannot_use) {
			FormMailExit(GetMessage(MSG_SCRIPT_VERSION,
			                        array("PHPREQ" => $s_req_string,"PHPVERS" => $this->_sPHPVersionString)));
		}
	}

	/**
	 * Test PHP version against a particular version string.
	 *
	 * @param $s_vers
	 *
	 * @return boolean true if the PHP version is at or later than the version
	 *         specified
	 */
	public function IsPHPAtLeast($s_vers)
	{
		$a_test_version = explode(".",$s_vers);
		if (count($a_test_version) < 3) {
			return (false);
		}
		return ($this->_aPHPVersion[0] > $a_test_version[0] || ($this->_aPHPVersion[0] ==
		                                                        $a_test_version[0] &&
		                                                        ($this->_aPHPVersion[1] >
		                                                         $a_test_version[1] ||
		                                                         $this->_aPHPVersion[1] ==
		                                                         $a_test_version[1] &&
		                                                         $this->_aPHPVersion[2] >=
		                                                         $a_test_version[2])));
	}

	public function GetScript()
	{
		if (!isset($this->_sScript)) {
			if (isset($_SERVER["PHP_SELF"]) &&
			    !empty($_SERVER["PHP_SELF"]) &&
			    isset($_SERVER["SERVER_NAME"]) &&
			    !empty($_SERVER["SERVER_NAME"])
			) {
				if (isset($_SERVER["SERVER_PORT"]) &&
				    $_SERVER["SERVER_PORT"] != 80
				) {
					if ($_SERVER["SERVER_PORT"] == 443) // SSL port
						//
						// just use https prefix
						//
					{
						$this->_sScript = "https://" . $_SERVER["SERVER_NAME"] .
						                  $_SERVER["PHP_SELF"];
					} else
						//
						// use http with port number
						//
					{
						$this->_sScript = "http://" . $_SERVER["SERVER_NAME"] .
						                  ":" . $_SERVER["SERVER_PORT"] .
						                  $_SERVER["PHP_SELF"];
					}
				} else {
					$this->_sScript = "http://" . $_SERVER["SERVER_NAME"] .
					                  $_SERVER["PHP_SELF"];
				}
			} else {
				Error("no_php_self",GetMessage(MSG_NO_PHP_SELF),false,false);
			}
		}
		return ($this->_sScript);
	}

	/**
	 * Get a boolean PHP setting.
	 *
	 * @param string $s_name the name of the setting
	 *
	 * @return bool|null    the value of the setting
	 */
	public function getINIBool($s_name)
	{
		$m_val = ini_get($s_name);
		if ($m_val !== null) {
			if (is_numeric($m_val)) {
				$m_val = (int)$m_val;
			} elseif (is_string($m_val)) {
				$m_val = strtolower($m_val);
				switch ($m_val) {
					case "1":
					case "on":
					case "true":
						$m_val = true;
						break;
					default:
						$m_val = false;
						break;
				}
			}
		}
		return ($m_val);
	}

	/**
	 * Return whether the session can be passed in a URL.
	 *
	 * @return bool true if the session can be passed in a URL
	 */
	public function allowSessionURL()
	{
		$m_only_cookies = $this->getINIBool('session.use_only_cookies');
		FMDebug('only_cookies=' . $m_only_cookies);
		if ($m_only_cookies === null) {
			$m_only_cookies = $this->IsPHPAtLeast('5.3.0');
			FMDebug('php=' . $this->IsPHPAtLeast('5.3.0') . ',only_cookies=' . $m_only_cookies);
		}
		return (!$m_only_cookies);
	}

	public function getPostMaxSize()
	{
		$s_max_value = ini_get('post_max_size');
		$number      = (int)substr($s_max_value,0,-1);
		switch (strtoupper(substr($s_max_value,-1))) {
			case "K":
				return $number * 1024;
			case "M":
				return $number * pow(1024,2);
			case "G":
				return $number * pow(1024,3);
			default:
				return (int)$s_max_value;
		}
	}

	public function checkUploadSize()
	{
		if (isset($_SERVER['CONTENT_LENGTH'])) {
			$n_size = (int)$_SERVER['CONTENT_LENGTH'];
			$n_max  = $this->getPostMaxSize();
			//			echo "Size = $n_size, max = $n_max\n";
			if ($n_size > $n_max) {
				UserError("post_size_limit",GetMessage(MSG_POST_SIZE_LIMIT,array(),false,false));
			}
		}
	}

	public function checkFileUploadSize($a_file_vars)
	{
		if (Settings::get('FILEUPLOADS')) {
			$a_field_names = array_keys($a_file_vars);
			foreach ($a_field_names as $s_name => $a_upload) {
				if (isset($a_upload)) {
					switch (isset($a_upload['error']) ? $a_upload['error'] : 0) {
						case 0:     // no error
						case 4:     // no file uploaded
							break;
						default:
							UserError('file_upload_error',FieldManager::GetFileUploadErrorMesg($a_upload['error']));
					}
				}
			}
		}
	}
}

$ExecEnv = new ExecEnv();
if (!$ExecEnv->IsPHPAtLeast("5.3.0")) {
	//
	// disable this silly setting (usually not enabled)
	// it's also deprecated from PHP version 5.3.0
	//
	if (function_exists('set_magic_quotes_runtime')) {
		/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
		@set_magic_quotes_runtime(0);
	}
}

//
// We set references to the appropriate arrays to handle PHP version differences
// Session vars are selected after we start the session.
//
$aServerVars = &$_SERVER;
$aGetVars    = &$_GET;
$aFormVars   = &$_POST;
$aFileVars   = &$_FILES;
$aEnvVars    = &$_ENV;

$bIsGetMethod = false;
$bHasGetData  = false;

if (!isset($REAL_DOCUMENT_ROOT)) {
	SetRealDocumentRoot();
}

if (isset($aServerVars['SERVER_PORT'])) {
	$SCHEME = ($aServerVars['SERVER_PORT'] == 80) ? "http://" : "https://";
} else {
	$SCHEME = "";
}
if (isset($aServerVars['SERVER_NAME']) && $aServerVars['SERVER_NAME'] !== "") {
	$SERVER = $aServerVars['SERVER_NAME'];
} elseif (isset($aServerVars['SERVER_ADDR']) && $aServerVars['SERVER_ADDR'] !== "") {
	$SERVER = $aServerVars['SERVER_ADDR'];
} else {
	$SERVER = "";
}

/*
 * Load an optional include file before the configuration.
 * You can use this to set variables that can be used in the
 * configuration section.
 */
if (includeFileExists("formmail-preconfig.inc.php")) {
	@include("formmail-preconfig.inc.php");
}

/*****************************************************************************/
/* CONFIGURATION (do not alter this line in any way!!!)                      */
/*****************************************************************************
 * This is the *only* place where you need to modify things to use formmail.php
 * on your particular system.  This section finishes at "END OF CONFIGURATION".
 * Help for all settings can be found on our website:
 *  http://www.tectite.com/fmdoc/index.php
 *
 * Also, above each setting is a direct URL to the help information for the
 * setting.
 *****************************************************************************/

/* Help: http://www.tectite.com/fmdoc/email_name.php */
$EMAIL_NAME = "^[-a-z0-9._]+"; /* the '^' is an important security feature! */

/* Help: http://www.tectite.com/fmdoc/target_email.php */
$TARGET_EMAIL = array("^info@iocloudsolutions\.net$");

/* Help: http://www.tectite.com/fmdoc/email_addrs.php */
$EMAIL_ADDRS = array();

/* Help: http://www.tectite.com/fmdoc/def_alert.php */
$DEF_ALERT = "admin@bugzbunny.net";

/* Help: http://www.tectite.com/fmdoc/site_domain.php */
$SITE_DOMAIN = "https://www.iocloudsolutions.net"; /* your website domain name */

/* Help: http://www.tectite.com/fmdoc/set_real_document_root.php */
$SET_REAL_DOCUMENT_ROOT = ""; /* overrides the value set by SetRealDocumentRoot function */

//
// override $REAL_DOCUMENT_ROOT from the $SET_REAL_DOCUMENT_ROOT value (if any)
// Do not alter the following code (next 3 lines)!
//
if (isset($SET_REAL_DOCUMENT_ROOT) && $SET_REAL_DOCUMENT_ROOT !== "") {
	$REAL_DOCUMENT_ROOT = $SET_REAL_DOCUMENT_ROOT;
}

/* Help: http://www.tectite.com/fmdoc/config_check.php */
$CONFIG_CHECK = array("TARGET_EMAIL");

/* Help: http://www.tectite.com/fmdoc/at_mangle.php */
$AT_MANGLE = "";

/* Help: http://www.tectite.com/fmdoc/target_urls.php */
$TARGET_URLS = array(); /* default; no URLs allowed */

/* Help: http://www.tectite.com/fmdoc/head_crlf.php */
$HEAD_CRLF = "\r\n";

/* Help: http://www.tectite.com/fmdoc/body_lf.php */
$BODY_LF = "\r\n"; /* the new default: use this for CR+LF */
//$BODY_LF = "\n";       /* the old default: just LF */

/* Help: http://www.tectite.com/fmdoc/from_user.php */
$FROM_USER = ""; /* the default - setting not used */

/* Help: http://www.tectite.com/fmdoc/sendmail_f_option.php */
$SENDMAIL_F_OPTION      = false;
$SENDMAIL_F_OPTION_LINE = __LINE__ - 1; /* don't modify this line! */

/* Help: http://www.tectite.com/fmdoc/fixed_sender.php */
$FIXED_SENDER = "";

/* Help: http://www.tectite.com/fmdoc/set_sender_from_email.php */
$SET_SENDER_FROM_EMAIL = false;

/* Help: http://www.tectite.com/fmdoc/ini_set_from.php */
$INI_SET_FROM = false;

/* Help: http://www.tectite.com/fmdoc/logdir.php */
$LOGDIR = "/var/www/logs"; /* directory for log files; empty string to disallow log files */

/* Help: http://www.tectite.com/fmdoc/autorespondlog.php */
$AUTORESPONDLOG = ""; /* file name in $LOGDIR for the auto responder log; empty string for no auto responder log */

/* Help: http://www.tectite.com/fmdoc/csv_file_settings.php */
$CSVDIR    = ""; /* directory for csv files; empty string to disallow csv files */
$CSVSEP    = ","; /* comma separator between fields (columns) */
$CSVINTSEP = ";"; /* semicolon is the separator for fields (columns) with multiple values (checkboxes, etc.) */
$CSVQUOTE  = '"'; /* all fields in the CSV are quoted with this character; default is double quote.  You can change it to single quote or leave it empty for no quotes. */
//$CSVQUOTE = "'";		/* use this if you want single quotes */
$CSVOPEN = ""; /* set to "b" to force line terminations to be kept as $CSVLINE setting below, regardless of operating
	                    system.  Keep as empty string and leave $CSVLINE unchanged, to get text file
	                    terminations for your server's operating system. (Line feed on UNIX, carriage-return line feed on Windows). */
$CSVLINE = "\n"; /* line termination for CSV files.  The default is a single line feed, which may be modified for your
						server's operating system.  If you want to change
	                    this value, you *must* set $CSVOPEN = "b". */

/* Help: http://www.tectite.com/fmdoc/templatedir.php */
$TEMPLATEDIR = ""; /* directory for template files; empty string if you don't have any templates */

/* Help: http://www.tectite.com/fmdoc/templateurl.php */
$TEMPLATEURL = ""; /* default; no template URL */

/* Help: http://www.tectite.com/fmdoc/multiformdir.php */
$MULTIFORMDIR = ""; /* directory for multi-form template files; empty string if you're not using multi-forms */

/* Help: http://www.tectite.com/fmdoc/multiformurl.php */
$MULTIFORMURL = ""; /* default; no multi-forms templates URL */

/* Help: http://www.tectite.com/fmdoc/text_subs.php */
$TEXT_SUBS = array(
	array("srch" => "/\\\\r\\\\n/","repl" => "\r\n",),
	array("srch" => "/\\\\n/","repl" => "\n",),
	array("srch" => "/\\\\t/","repl" => "\t",),
	array("srch" => "/\\[NL\\]/","repl" => "\n",),
	array("srch" => "/\\[TAB\\]/","repl" => "\t",),
	array("srch" => "/\\[NBSP\\]/","repl" => "&nbsp;",),
	array("srch" => "/\\[DQUOT\\]/","repl" => '"',),
	array("srch" => "/\\[SQUOT\\]/","repl" => "'",),
	array("srch" => "/\\[COLON\\]/","repl" => ":",),
	array("srch" => "/\\[SLOSH\\]/","repl" => "\\",),
	array("srch" => "/\\[OPCURL\\]/","repl" => "{",),
	array("srch" => "/\\[CLCURL\\]/","repl" => "}",),
	array("srch" => "/(on[a-z]*|href|src)\\s*=\\s*/i","repl" => ""), /* strip html attributes that could be unsafe */
	array("srch" => "/<\\s*(table|tr|td|th|p|ul|ol|li|b|i|u|strong|pre|h[1-6]|em|dl|dd|dt|hr|span|br)(\\b[^>]*?)>/i",
	      "repl" => "<\$1\$2>",
	),
	array("srch" => "#<\\s*/\\s*(table|tr|td|th|p|ul|ol|li|b|i|u|strong|pre|h[1-6]|em|dl|dd|dt|hr|span|br)\\s*>#i",
	      "repl" => "</\$1>",
	),
);

/* Help: http://www.tectite.com/fmdoc/authentication_settings.php */
$AUTHENTICATE = "";
//$AUTHENTICATE = "Basic cnVzc2VsbHI6dGVzdA==";        // example
$AUTH_USER = "";
$AUTH_PW   = "";

/* Help: http://www.tectite.com/fmdoc/form_ini_file.php */
$FORM_INI_FILE = "/var/www/formmail.ini";

/* Help: http://www.tectite.com/fmdoc/moduledir.php */
$MODULEDIR = ".";

/* Help: http://www.tectite.com/fmdoc/fmcompute.php */
$FMCOMPUTE = "fmcompute.php";

/* Help: http://www.tectite.com/fmdoc/fmgeoip.php */
$FMGEOIP = "fmgeoip.php";

/* Help: http://www.tectite.com/fmdoc/advanced_templates.php */
$ADVANCED_TEMPLATES = false; /* set to true for advanced templates */

/* Help: http://www.tectite.com/fmdoc/limited_import.php */
$LIMITED_IMPORT = true; /* set to true if your database cannot handle escaped quotes or newlines within imported data.  Microsoft Access is one example. */

/* Help: http://www.tectite.com/fmdoc/valid_env.php */
$VALID_ENV = array('HTTP_REFERER','REMOTE_HOST','REMOTE_ADDR','REMOTE_USER',
                   'HTTP_USER_AGENT'
);

/* Help: http://www.tectite.com/fmdoc/fileuploads.php */
$FILEUPLOADS = false; /* set to true to allow file attachments */

/* Help: http://www.tectite.com/fmdoc/max_file_upload_size.php */
$MAX_FILE_UPLOAD_SIZE = 0; /* default of 0 means that other software */
// controls the maximum file upload size
// (FormMail doesn't test the file size)

/* Help: http://www.tectite.com/fmdoc/file_repository.php */
$FILE_REPOSITORY = "";

/* Help: http://www.tectite.com/fmdoc/file_mode.php */
$FILE_MODE = 0664; /* always precede with 0 to specify octal! */

/* Help: http://www.tectite.com/fmdoc/file_overwrite.php */
$FILE_OVERWRITE = true;

/* Help: http://www.tectite.com/fmdoc/next_num_file.php */
$NEXT_NUM_FILE = "";

/* Help: http://www.tectite.com/fmdoc/put_data_in_url.php */
$PUT_DATA_IN_URL = true; /* set to true to place data in the URL */
// for bad_url redirects

/* Help: http://www.tectite.com/fmdoc/allow_get_method.php */
$ALLOW_GET_METHOD = false;

/* Help: http://www.tectite.com/fmdoc/db_see_input.php */
$DB_SEE_INPUT = false; /* set to true to just see the input values */

/* Help: http://www.tectite.com/fmdoc/db_see_ini.php */
$DB_SEE_INI = false; /* set to true to just see the ini file */

/* Help: http://www.tectite.com/fmdoc/maxstring.php */
$MAXSTRING = 1024; /* maximum string length for a value */

/* Help: http://www.tectite.com/fmdoc/require_captcha.php */
$REQUIRE_CAPTCHA = ""; /* set to a message string if your forms */
// must provide a CAPTCHA string

/* Help: http://www.tectite.com/fmdoc/recaptcha_private_key.php */
$RECAPTCHA_PRIVATE_KEY = "";

/* Help: http://www.tectite.com/fmdoc/bshowmesgnumbers.php */
$bShowMesgNumbers = false;

/* Help: http://www.tectite.com/fmdoc/filters.php */
/* Note for Tectite personnel: the upgrade Wizard will merge new values
 * but be careful of $var usage and quoting in new entries.
 */
/** @noinspection PhpUndefinedVariableInspection */
$FILTERS = array("encode" => "$REAL_DOCUMENT_ROOT/cgi-bin/fmencoder -kpubkey.txt",
                 "null"   => "null",
                 "csv"    => "csv"
);

/* Help: http://www.tectite.com/fmdoc/socket_filters.php */
$SOCKET_FILTERS = array(
	"httpencode" => array("site"   => "YourSiteHere",
	                      "port"   => 80,
	                      "path"   => "/cgi-bin/fmencoder",
	                      "params" => array(array("name" => "key",
	                                              "file" => "$REAL_DOCUMENT_ROOT/cgi-bin/pubkey.txt"
	                                        )
	                      )
	),
	"sslencode"  => array("site"   => "ssl://YourSecureSiteHere",
	                      "port"   => 443,
	                      "path"   => "/cgi-bin/fmencoder",
	                      "params" => array(array("name" => "key",
	                                              "file" => "$REAL_DOCUMENT_ROOT/cgi-bin/pubkey.txt"
	                                        )
	                      )
	),
);

/* Help: http://www.tectite.com/fmdoc/filter_attribs.php */
$FILTER_ATTRIBS = array("encode"     => "Strips,MIME=application/vnd.fmencoded,Encrypts",
                        "httpencode" => "Strips,MIME=application/vnd.fmencoded,Encrypts",
                        "sslencode"  => "Strips,MIME=application/vnd.fmencoded,Encrypts",
                        "csv"        => "Strips,MIME=text/csv",
);

/* Help: http://www.tectite.com/fmdoc/check_for_new_version.php */
$CHECK_FOR_NEW_VERSION = true;
$CHECK_DAYS            = 30;

/* Help: http://www.tectite.com/fmdoc/scratch_pad.php */
$SCRATCH_PAD = "";

/* Help: http://www.tectite.com/fmdoc/cleanup_time.php */
$CLEANUP_TIME = 60; /* cleanup time in minutes */

/* Help: http://www.tectite.com/fmdoc/cleanup_chance.php */
$CLEANUP_CHANCE = 20; /* percentage probability that cleanup will be performed */

/* Help: http://www.tectite.com/fmdoc/pear_settings.php */
$PEAR_SMTP_HOST = "";
$PEAR_SMTP_PORT = 25;
$PEAR_SMTP_USER = "";
$PEAR_SMTP_PWD  = "";

/* Help: http://www.tectite.com/fmdoc/alert_on_user_error.php */
$ALERT_ON_USER_ERROR = true;

/* Help: http://www.tectite.com/fmdoc/enable_attack_detection.php */
$ENABLE_ATTACK_DETECTION = true;

/* Help: http://www.tectite.com/fmdoc/attack_detection_url.php */
$ATTACK_DETECTION_URL = "";

/* Help: http://www.tectite.com/fmdoc/alert_on_attack_detection.php */
$ALERT_ON_ATTACK_DETECTION = false;

/* Help: http://www.tectite.com/fmdoc/attack_detection_mime.php */
$ATTACK_DETECTION_MIME = true;

/* Help: http://www.tectite.com/fmdoc/attack_detection_junk.php */
$ATTACK_DETECTION_JUNK                   = true;
$ATTACK_DETECTION_JUNK_CONSONANTS        = "bcdfghjklmnpqrstvwxz";
$ATTACK_DETECTION_JUNK_VOWELS            = "aeiouy";
$ATTACK_DETECTION_JUNK_CONSEC_CONSONANTS = 5;
$ATTACK_DETECTION_JUNK_CONSEC_VOWELS     = 4;
$ATTACK_DETECTION_JUNK_TRIGGER           = 2;
$ATTACK_DETECTION_JUNK_LANG_STRIP        = array(
	"aiia", /* Hawaiian */
	"aeoa", /* palaeoanthropic */
	"aeoe", /* palaeoethnic */
	"ayou", /* layout */
	"ayee", /* payee */
	"buyout", /* buyout */
	"clayey", /* clayey */
	"hooey", /* hooey */
	"ioau", /* radioautograph */
	"opoeia", /* pharmacopoeia, onomatopoeia */
	"ooee", /* cooee */
	"oyee", /* employee */
	"ioau", /* radioautograph */
	"uaia", /* guaiac */
	"uaya", /* uruguayan */
	"ueou", /* aqueous */
	"uiou", /* obsequious */
	"uoya", /* buoyant */
	"queue", /* queue, queueing */
	"earth", /* earthquake, earthslide */
	"cks", /* jockstrap, backscratcher */
	"ngth", /* strengths, length */
	"ndths", /* thousandths */
	"ght", /* nightclub, knightsbridge */
	"phth", /* ophthalmology */
	"sch", /* rothschild */
	"shch", /* borshch */
	"scr", /* corkscrew */
	"spr", /* wingspread, offspring */
	"str", /* armstrong, songstress */
	"sts", /* bursts, postscript */
	"tch", /* catchphrase, scratchproof */
	"thst", /* northstar, birthstone */
	"http", /* https, http */
	"html", /* HTML, XHTML */
);
$ATTACK_DETECTION_JUNK_IGNORE_FIELDS     = array();

/* Help: http://www.tectite.com/fmdoc/attack_detection_dups.php */
$ATTACK_DETECTION_DUPS = array("realname","address1","address2","country","zip",
                               "phone","postcode","state","email"
);

/* Help: http://www.tectite.com/fmdoc/attack_detection_specials.php */
$ATTACK_DETECTION_SPECIALS = true;

/* Help: http://www.tectite.com/fmdoc/attack_detection_specials.php */
$ATTACK_DETECTION_SPECIALS_ONLY_EMAIL = array("derive_fields","required",
                                              "mail_options","good_url","bad_url","good_template",
                                              "bad_template"
);

/* Help: http://www.tectite.com/fmdoc/attack_detection_specials.php */
$ATTACK_DETECTION_SPECIALS_ANY_EMAIL = array("subject");

/* Help: http://www.tectite.com/fmdoc/attack_detection_many_urls.php */
$ATTACK_DETECTION_MANY_URLS = 3;

/* Help: http://www.tectite.com/fmdoc/attack_detection_many_url_fields.php */
$ATTACK_DETECTION_MANY_URL_FIELDS = 0;

/* Help: http://www.tectite.com/fmdoc/attack_detection_url_patterns.php */
$ATTACK_DETECTION_URL_PATTERNS = array(
	'(^|[^-a-z_.0-9]+)(?<!\S@)([-a-z0-9]+\.)+(aaa|aarp|abarth|abb|abbott|abbvie|abc|able|abogado|abudhabi|ac|academy|accenture|accountant|accountants|aco|actor|ad|adac|ads|adult|ae|aeg|aero|aetna|af|afl|africa|ag|agakhan|agency|ai|aig|airbus|airforce|airtel|akdn|al|alfaromeo|alibaba|alipay|allfinanz|allstate|ally|alsace|alstom|am|amazon|americanexpress|americanfamily|amex|amfam|amica|amsterdam|analytics|android|anquan|anz|ao|aol|apartments|app|apple|aq|aquarelle|ar|arab|aramco|archi|army|arpa|art|arte|as|asda|asia|associates|at|athleta|attorney|au|auction|audi|audible|audio|auspost|author|auto|autos|avianca|aw|aws|ax|axa|az|azure|ba|baby|baidu|banamex|bananarepublic|band|bank|bar|barcelona|barclaycard|barclays|barefoot|bargains|baseball|basketball|bauhaus|bayern|bb|bbc|bbt|bbva|bcg|bcn|bd|be|beats|beauty|beer|bentley|berlin|best|bestbuy|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|blockbuster|blog|bloomberg|blue|bm|bms|bmw|bn|bnpparibas|bo|boats|boehringer|bofa|bom|bond|boo|book|booking|bosch|bostik|boston|bot|boutique|box|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|calvinklein|cam|camera|camp|cancerresearch|canon|capetown|capital|capitalone|car|caravan|cards|care|career|careers|cars|casa|case|cash|casino|cat|catering|catholic|cba|cbn|cbre|cbs|cc|cd|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|charity|chase|chat|cheap|chintai|christmas|chrome|church|ci|cipriani|circle|cisco|citadel|citi|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|comcast|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cookingchannel|cool|coop|corsica|country|coupon|coupons|courses|cpa|cr|credit|creditcard|creditunion|cricket|crown|crs|cruise|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|data|date|dating|datsun|day|dclk|dds|de|deal|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|dhl|diamonds|diet|digital|direct|directory|discount|discover|dish|diy|dj|dk|dm|dnp|do|	docs|doctor|dog|domains|dot|download|drive|dtv|dubai|dunlop|dupont|durban|dvag|dvr|dz|earth|eat|ec|eco|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|ericsson|erni|es|esq|estate|et|etisalat|eu|eurovision|eus|events|exchange|expert|exposed|express|extraspace|fage|fail|fairwinds|faith|family|fan|fans|farm|farmers|fashion|fast|fedex|feedback|ferrari|ferrero|fi|fiat|fidelity|fido|film|final|finance|financial|fire|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|flir|florist|flowers|fly|fm|fo|foo|food|foodnetwork|football|ford|forex|forsale|forum|foundation|fox|fr|free|fresenius|frl|frogans|frontdoor|frontier|ftr|fujitsu|fun|fund|furniture|futbol|fyi|ga|gal|gallery|gallo|gallup|game|games|gap|garden|gay|gb|gbiz|gd|gdn|ge|gea|gent|genting|george|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|godaddy|gold|goldpoint|golf|goo|goodyear|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|grocery|group|gs|gt|gu|guardian|gucci|guge|guide|guitars|guru|gw|gy|hair|hamburg|hangout|haus|hbo|hdfc|hdfcbank|health|healthcare|help|helsinki|here|hermes|hgtv|hiphop|hisamitsu|hitachi|hiv|hk|hkt|hm|hn|hockey|holdings|holiday|homedepot|homegoods|homes|homesense|honda|horse|hospital|host|hosting|hot|hoteles|hotels|hotmail|house|how|hr|hsbc|ht|hu|hughes|hyatt|hyundai|ibm|icbc|ice|icu|id|ie|ieee|ifm|ikano|il|im|imamat|imdb|immo|immobilien|in|inc|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|intuit|investments|io|ipiranga|iq|ir|irish|is|ismaili|ist|istanbul|it|itau|itv|jaguar|java|jcb|je|jeep|jetzt|jewelry|jio|jll|jm|jmp|jnj|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|juniper|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kindle|kitchen|kiwi|km|kn|koeln|komatsu|kosher|kp|kpmg|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|lancia|land|landrover|lanxess|lasalle|lat|latino|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|lefrak|legal|lego|lexus|lgbt|li|lidl|life|lifeinsurance|lifestyle|lighting|like|lilly|limited|limo|lincoln|linde|link|lipsy|live|living|lk|llc|llp|loan|loans|locker|locus|loft|lol|london|lotte|lotto|love|lpl|lplfinancial|lr|ls|lt|ltd|ltda|lu|lundbeck|luxe|luxury|lv|ly|ma|macys|madrid|maif|maison|makeup|man|management|mango|map|market|marketing|markets|marriott|marshalls|maserati|mattel|mba|mc|mckinsey|md|me|med|media|meet|melbourne|meme|memorial|men|menu|merckmsd|mg|mh|miami|microsoft|mil|mini|mint|mit|mitsubishi|mk|ml|mlb|mls|mm|mma|mn|mo|mobi|mobile|moda|moe|moi|mom|monash|money|monster|mormon|mortgage|moscow|moto|motorcycles|mov|movie|mp|mq|mr|ms|msd|mt|mtn|mtr|mu|museum|music|mutual|mv|mw|mx|my|mz|na|nab|nagoya|name|natura|navy|nba|nc|ne|nec|net|netbank|netflix|network|neustar|new|news|next|nextdirect|nexus|nf|nfl|ng|ngo|nhk|ni|nico|nike|nikon|ninja|nissan|nissay|nl|no|nokia|northwesternmutual|norton|now|nowruz|nowtv|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|observer|office|okinawa|olayan|olayangroup|oldnavy|ollo|om|omega|one|ong|onl|online|ooo|open|oracle|orange|org|organic|origins|osaka|otsuka|ott|ovh|pa|page|panasonic|paris|pars|partners|parts|party|passagens|pay|pccw|pe|pet|pf|pfizer|pg|ph|pharmacy|phd|philips|phone|photo|photography|photos|physio|pics|pictet|pictures|pid|pin|ping|pink|pioneer|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pnc|pohl|poker|politie|porn|post|pr|pramerica|praxi|press|prime|pro|prod|productions|prof|progressive|promo|properties|property|protection|pru|prudential|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|radio|re|read|realestate|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|reliance|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|richardli|ricoh|ril|rio|rip|ro|rocher|rocks|rodeo|rogers|room|rs|rsvp|ru|rugby|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsclub|samsung|sandvik|sandvikcoromant|sanofi|sap|sarl|sas|save|saxo|sb|sbi|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scot|sd|se|search|seat|secure|security|seek|select|sener|services|ses|seven|sew|sex|sexy|sfr|sg|sh|shangrila|sharp|shaw|shell|shia|shiksha|shoes|shop|shopping|shouji|show|showtime|si|silk|sina|singles|site|sj|sk|ski|skin|sky|skype|sl|sling|sm|smart|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|spa|space|sport|spot|sr|srl|ss|st|stada|staples|star|statebank|statefarm|stc|stcgroup|stockholm|storage|store|stream|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|systems|sz|tab|taipei|talk|taobao|target|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|tdk|team|tech|technology|tel|temasek|tennis|teva|tf|tg|th|thd|theater|theatre|tiaa|tickets|tienda|tiffany|tips|tires|tirol|tj|tjmaxx|tjx|tk|tkmaxx|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tr|trade|trading|training|travel|travelchannel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubank|ubs|ug|uk|unicom|university|uno|uol|ups|us|uy|uz|va|vacations|vana|vanguard|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|vig|viking|villas|vin|vip|virgin|visa|vision|viva|vivo|vlaanderen|vn|vodka|volkswagen|volvo|vote|voting|voto|voyage|vu|vuelos|wales|walmart|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weibo|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|winners|wme|wolterskluwer|woodside|work|works|world|wow|ws|wtc|wtf|xbox|xerox|xfinity|xihuan|xin|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|you|youtube|yt|yun|za|zappos|zara|zero|zip|zm|zone|zuerich|zw)\b',
	'(^|[^-a-z_.0-9]+)(?<!\S@)([-a-z0-9]+\.)+(com{0,1}|org|net)\.[a-z][a-z]\b',
	'(^|[^-a-z_.0-9]+)(?<!\S@)([-a-z0-9]+\.)+(xn--[a-z0-9]+)\b',
	'\b(bit\.ly|goo\.gl|owl\.ly|deck\.ly|su\.pr|lnk\.co|fur\.ly)/'
);

/* Help: http://www.tectite.com/fmdoc/attack_detection_ignore_errors.php */
$ATTACK_DETECTION_IGNORE_ERRORS = false;

/* Help: http://www.tectite.com/fmdoc/attack_detection_reverse_captcha.php */
$ATTACK_DETECTION_REVERSE_CAPTCHA = array();

/* Help: http://www.tectite.com/fmdoc/geoip_lic.php */
$GEOIP_LIC = ""; /* default - no GeoIP */

/* Help: http://www.tectite.com/fmdoc/zero_is_empty.php */
$ZERO_IS_EMPTY = false;

/* Help: http://www.tectite.com/fmdoc/validate_email_domain.php */
$VALIDATE_EMAIL_DOMAIN = true;

/* Help: http://www.tectite.com/fmdoc/email_validation_max_monitoring_time_secs.php */
$EMAIL_VALIDATION_MAX_MONITORING_TIME_SECS = 0;

/* Help: http://www.tectite.com/fmdoc/alert_on_email_validation_failed.php */
$ALERT_ON_EMAIL_VALIDATION_FAILED = true;

/* Help: http://www.tectite.com/fmdoc/session_name.php */
$SESSION_NAME = "";

/* Help: http://www.tectite.com/fmdoc/session_access.php */
$SESSION_ACCESS = array();

/* Help: http://www.tectite.com/fmdoc/destroy_session.php */
$DESTROY_SESSION = true;

/* Help: http://www.tectite.com/fmdoc/hook_dir.php */
$HOOK_DIR = "";

/* Help: http://www.tectite.com/fmdoc/test_password.php */
$TEST_PASSWORD = "1";

/* UPGRADE CONTROL
**
** ATTACK_DETECTION_URL_PATTERNS:lt:9.26:no_keep:The ATTACK_DETECTION_URL_PATTERNS
** configuration has been modified to recognise all current top level domains.:
**
** FILTERS:lt:8.04:merge:The FILTERS configuration has
** been modified to include some new standard filters.:
**
** FILTER_ATTRIBS:lt:8.04:no_keep:The FILTER_ATTRIBS configuration has
** been modified to include new information about the standard filters.:
**
** ATTACK_DETECTION_URL_PATTERNS:eq:8.02:no_keep:The ATTACK_DETECTION_URL_PATTERNS
** configuration has been modified to fix a bug.:
**
** FILTER_ATTRIBS:lt:4.00:no_keep:The FILTER_ATTRIBS configuration has
** been modified to include new information about the standard filters.:
**
** SET_REAL_DOCUMENT_ROOT:gt:4.07:copy_from=REAL_DOCUMENT_ROOT:The
** REAL_DOCUMENT_ROOT configuration has been renamed to SET_REAL_DOCUMENT_ROOT.:
**
** EMAIL_NAME:lt:6.01:no_keep:The EMAIL_NAME configuration has
** been modified to match hyphens ('-') in email addresses.:
**
** ZERO_IS_EMPTY:le:6.01:set_to=true:ZERO_IS_EMPTY has been
** set to a value that duplicates previous behaviour.:
**
** TEXT_SUBS:lt:8.30:no_keep:The TEXT_SUBS configuration has
** been modified to be secure with new features released in this version.:
**
** EMAIL_NAME:lt:9.08:no_keep:The EMAIL_NAME configuration has
** been modified to match underscores ('_') in email addresses.:
** END OF CONTROL
*/

/*****************************************************************************/
/* END OF CONFIGURATION (do not alter this line in any way!!!)               */
/*****************************************************************************/

/**
 * Class to access and modify configuration settings.
 *
 * @author russellr
 */
class Settings
{
	/**
	 * Check that the configuration setting exists.
	 * If it doesn't exist, then the script dies immediately.
	 *
	 * @param string $s_name the name of the config setting
	 */
	private static function _check($s_name)
	{
		if (!array_key_exists($s_name,$GLOBALS)) {
			echo '<pre>';
			debug_print_backtrace();
			echo '</pre>';
			FormMailExit("No FormMail setting called '$s_name' exists.");
		}
	}

	/**
	 * Tests if the given config setting is empty.
	 *
	 * @param string $s_name the name of the config setting
	 *
	 * @return boolean          true if the config setting is empty
	 */
	public static function isEmpty($s_name)
	{
		Settings::_check($s_name);
		if (gettype($GLOBALS[$s_name]) == 'string') {
			return ($GLOBALS[$s_name] === '');
		} else {
			return (empty($GLOBALS[$s_name]));
		}
	}

	/**
	 * Returns the value of the given config setting.
	 *
	 * @param string $s_name the name of the config setting
	 *
	 * @return mixed            the value of the config setting
	 */
	public static function get($s_name)
	{
		Settings::_check($s_name);
		return ($GLOBALS[$s_name]);
	}

	/**
	 * Set the value of a config setting.
	 *
	 * @param string $s_name  the name of the config setting
	 * @param mixed  $m_value the value to set for the config setting
	 */
	public static function set($s_name,$m_value)
	{
		Settings::_check($s_name);
		if (($s_orig_type = gettype($GLOBALS[$s_name])) != ($s_new_type = gettype($m_value))) {
			echo '<pre>';
			debug_print_backtrace();
			echo '</pre>';
			FormMailExit("You cannot set FormMail setting '$s_name' to type '$s_new_type'.  It should be type '$s_orig_type'.");
		}
		$GLOBALS[$s_name] = $m_value;
	}
}

/*
 * For PHP v8 we need to check each include file for existence first.
 */
function includeFileExists($file_name)
{
	return (stream_resolve_include_path($file_name) !== false);
}

function hookFileExists($file_name)
{
	if (!Settings::isEmpty('HOOK_DIR')) {
		$hook_file = Settings::get('HOOK_DIR') . DIRECTORY_SEPARATOR . $file_name . ".php";
		if (includeFileExists($hook_file)) {
			return ($hook_file);
		}
		$hook_file = Settings::get('HOOK_DIR') . DIRECTORY_SEPARATOR . $file_name;
		if (includeFileExists($hook_file)) {
			return ($hook_file);
		}
	}
	return (false);
}

/*
 * Load an optional include file after the configuration.
 * You can use this to set variables or make adjustments
 * based on the results of the configuration section.
 */
if (includeFileExists("formmail-preconfigdefs.inc.php")) {
	@include("formmail-preconfigdefs.inc.php");
}

/*
 * Many configuration settings used to be constants.  However, constants
 * cannot be re-defined in PHP.  This limitation reduced the value
 * of "formmail - postconfig . inc . php" and other hook scripts.
 * Therefore, all configuration settings have been changed to be global
 * variables (no define's).
 *
 * The following defines are for backward-compatibility with any existing
 * hook scripts that are expecting the old constants.
 */
define("EMAIL_NAME",Settings::get("EMAIL_NAME"));
define("DEF_ALERT",Settings::get("DEF_ALERT"));
define("AT_MANGLE",Settings::get("AT_MANGLE"));
define("HEAD_CRLF",Settings::get("HEAD_CRLF"));
define("BODY_LF",Settings::get("BODY_LF"));
define("SENDMAIL_F_OPTION",Settings::get("SENDMAIL_F_OPTION"));
define("SENDMAIL_F_OPTION_LINE",Settings::get("SENDMAIL_F_OPTION_LINE"));
define("SET_SENDER_FROM_EMAIL",Settings::get("SET_SENDER_FROM_EMAIL"));
define("INI_SET_FROM",Settings::get("INI_SET_FROM"));
define("ADVANCED_TEMPLATES",Settings::get("ADVANCED_TEMPLATES"));
define("LIMITED_IMPORT",Settings::get("LIMITED_IMPORT"));
define("FILEUPLOADS",Settings::get("FILEUPLOADS"));
define("MAX_FILE_UPLOAD_SIZE",Settings::get("MAX_FILE_UPLOAD_SIZE"));
define("FILE_MODE",Settings::get("FILE_MODE"));
define("FILE_OVERWRITE",Settings::get("FILE_OVERWRITE"));
define("PUT_DATA_IN_URL",Settings::get("PUT_DATA_IN_URL"));
define("DB_SEE_INPUT",Settings::get("DB_SEE_INPUT"));
define("DB_SEE_INI",Settings::get("DB_SEE_INI"));
define("MAXSTRING",Settings::get("MAXSTRING"));
define("CHECK_FOR_NEW_VERSION",Settings::get("CHECK_FOR_NEW_VERSION"));
define("CHECK_DAYS",Settings::get("CHECK_DAYS"));
define("ALERT_ON_USER_ERROR",Settings::get("ALERT_ON_USER_ERROR"));
define("ENABLE_ATTACK_DETECTION",Settings::get("ENABLE_ATTACK_DETECTION"));
define("ATTACK_DETECTION_URL",Settings::get("ATTACK_DETECTION_URL"));
define("ALERT_ON_ATTACK_DETECTION",Settings::get("ALERT_ON_ATTACK_DETECTION"));
define("ATTACK_DETECTION_MIME",Settings::get("ATTACK_DETECTION_MIME"));
define("ATTACK_DETECTION_JUNK",Settings::get("ATTACK_DETECTION_JUNK"));
define("ATTACK_DETECTION_JUNK_CONSONANTS",Settings::get("ATTACK_DETECTION_JUNK_CONSONANTS"));
define("ATTACK_DETECTION_JUNK_VOWELS",Settings::get("ATTACK_DETECTION_JUNK_VOWELS"));
define("ATTACK_DETECTION_JUNK_CONSEC_CONSONANTS",Settings::get("ATTACK_DETECTION_JUNK_CONSEC_CONSONANTS"));
define("ATTACK_DETECTION_JUNK_CONSEC_VOWELS",Settings::get("ATTACK_DETECTION_JUNK_CONSEC_VOWELS"));
define("ATTACK_DETECTION_JUNK_TRIGGER",Settings::get("ATTACK_DETECTION_JUNK_TRIGGER"));
define("ATTACK_DETECTION_SPECIALS",Settings::get("ATTACK_DETECTION_SPECIALS"));
define("ATTACK_DETECTION_MANY_URLS",Settings::get("ATTACK_DETECTION_MANY_URLS"));
define("ATTACK_DETECTION_MANY_URL_FIELDS",Settings::get("ATTACK_DETECTION_MANY_URL_FIELDS"));
define("ATTACK_DETECTION_IGNORE_ERRORS",Settings::get("ATTACK_DETECTION_IGNORE_ERRORS"));
define("ZERO_IS_EMPTY",Settings::get("ZERO_IS_EMPTY"));
define("VALIDATE_EMAIL_DOMAIN",Settings::get("VALIDATE_EMAIL_DOMAIN"));
define("DESTROY_SESSION",Settings::get("DESTROY_SESSION"));

//
// for Ajax allow GET method for cross site JSONP
//
if (IsAjax()) {
	Settings::set('ALLOW_GET_METHOD',true);
}

/*
 * Load an optional include file after the configuration.
 * You can use this to set variables or make adjustments
 * based on the results of the configuration section.
 */
if (includeFileExists("formmail-postconfig.inc.php")) {
	@include("formmail-postconfig.inc.php");
}

// @formatter:off

//
// the following constants define all FormMail messages
//
// To re-align comments, use these search and replace patterns:
//      Search                  Replace
//      ^(\S{35,39})\s+//        $1\t//
//      ^(\S{31,35})\s+//        $1\t\t//
//      ^(\S{27,31})\s+//        $1\t\t\t//
//      ^(\S{23,27})\s+//        $1\t\t\t\t//
//      ^(\S{19,23})\s+//        $1\t\t\t\t\t//
//
define('MSG_SCRIPT_VERSION',0);				    // This script requires at least PHP version...
define('MSG_END_VERS_CHK',1);				    // If you're happy...
define('MSG_VERS_CHK',2);				        // A later version of FormMail is available...
define('MSG_CHK_FILE_ERROR',3);				    // Unable to create check file...
define('MSG_UNK_VALUE_SPEC',4);				    // derive_fields: unknown value specification...
define('MSG_INV_VALUE_SPEC',5);				    // derive_fields: invalid value specification...
define('MSG_DERIVED_INVALID',6);				// Some derive_fields specifications...
define('MSG_INT_FORM_ERROR',7);				    // Internal form error...
define('MSG_OPTIONS_INVALID',8);				// Some mail_options settings...
define('MSG_PLSWAIT_REDIR',9);				    // Please wait while you are redirected...
define('MSG_IFNOT_REDIR',10);				    // If you are not redirected...
define('MSG_PEAR_OBJ',11);				        // Failed to create PEAR Mail object...
define('MSG_PEAR_ERROR',12);				    // PEAR Mail error...
define('MSG_NO_FOPT_ADDR',13);				    // You have specified "SendMailFOption"...
define('MSG_MORE_INFO',14);				        // More information...
define('MSG_INFO_STOPPED',15);				    // Extra alert information suppressed...
define('MSG_FM_ALERT',16);				        // FormMail alert
define('MSG_FM_ERROR',17);				        // FormMail script error
define('MSG_FM_ERROR_LINE',18);				    // The following error occurred...
define('MSG_USERDATA_STOPPED',19);				// User data suppressed...
define('MSG_FILTERED',20);				        // This alert has been filtered...
define('MSG_TEMPLATES',21);				        // You must set either TEMPLATEDIR or TEMPLATEURL...
define('MSG_OPEN_TEMPLATE',22);				    // Failed to open template...
define('MSG_ERROR_PROC',23);				    // An error occurred while processing...
define('MSG_ALERT_DONE',24);				    // Our staff have been alerted...
define('MSG_PLS_CONTACT',25);				    // Please contact us directly...
define('MSG_APOLOGY',26);				        // We apologize for any inconvenience...
define('MSG_ABOUT_FORMMAIL',27);				// Your form submission was processed by...
define('MSG_PREG_FAILED',28);				    // preg_match_all failed in FindCRMFields...
define('MSG_URL_INVALID',29);				    // CRM URL "$URL" is not valid...
define('MSG_URL_OPEN',30);				        // Failed to open Customer Relationship...
define('MSG_CRM_FAILED',31);				    // Failure report from CRM...
define('MSG_CRM_FORM_ERROR',32);				// Your form submission was not...
define('MSG_OR',33);		                    // "$ITEM1" or "$ITEM2"
define('MSG_NOT_BOTH',34);				        // not both "$ITEM1" and "$ITEM2"
define('MSG_XOR',35);		                    // "$ITEM1" or "$ITEM2" (but not both)
define('MSG_IS_SAME_AS',36);				    // "$ITEM1" is the same as "$ITEM2"
define('MSG_IS_NOT_SAME_AS',37);				// "$ITEM1" is not the same as "$ITEM2"
define('MSG_REQD_OPER',38);				        // Operator "$OPER" is not valid for "required"
define('MSG_PAT_FAILED',39);				    // Pattern operator "$OPER" failed: pattern...
define('MSG_COND_OPER',40);				        // Operator "$OPER" is not valid...
define('MSG_INV_COND',41);				        // Invalid "conditions" field...
define('MSG_COND_CHARS',42);				    // The conditions field "$FLD" is not valid...
define('MSG_COND_INVALID',43);				    // The conditions field "$FLD" is not valid...
define('MSG_COND_TEST_LONG',44);				// Field "$FLD" has too many components...
define('MSG_COND_IF_SHORT',45);				    // Field "$FLD" has too few components for...
define('MSG_COND_IF_LONG',46);				    // Field "$FLD" has too many components for...
define('MSG_COND_UNK',47);				        // Field "$FLD" has an unknown command word...
define('MSG_MISSING',48);			    	    // Missing "$ITEM"...
define('MSG_NEED_ARRAY',49);				    // "$ITEM" must be an array...
define('MSG_SUBM_FAILED',50);				    // Your form submission has failed...
define('MSG_FILTER_WRONG',51);				    // Filter "$FILTER" is not properly...
define('MSG_FILTER_CONNECT',52);				// Could not connect to site "$SITE"...
define('MSG_FILTER_PARAM',53);				    // Filter "$FILTER" has invalid parameter...
define('MSG_FILTER_OPEN_FILE',54);				// Filter "$FILTER" cannot open file...
define('MSG_FILTER_FILE_ERROR',55);				// Filter "$FILTER": read error on file...
define('MSG_FILTER_READ_ERROR',56);				// Filter '$filter' failed: read error...
define('MSG_FILTER_NOT_OK',57);				    // Filter 'FILTER' failed...
define('MSG_FILTER_UNK',58);				    // Unknown filter...
define('MSG_FILTER_CHDIR',59);				    // Cannot chdir...
define('MSG_FILTER_NOTFOUND',60);				// Cannot execute...
define('MSG_FILTER_ERROR',61);				    // Filter "$FILTER" failed...
define('MSG_SPARE',62);		                    // this value is now spare
define('MSG_TEMPLATE_ERRORS',63);				// Template "$NAME" caused the...
define('MSG_TEMPLATE_FAILED',64);				// Failed to process template "$NAME"...
define('MSG_MIME_PREAMBLE',65);				    // (Your mail reader should not show this...
define('MSG_MIME_HTML',66);				        // This message has been generated by FormMail...
define('MSG_FILE_OPEN_ERROR',67);				// Failed to open file "$NAME"...
define('MSG_ATTACH_DATA',68);				    // Internal error: AttachFile requires...
define('MSG_PHP_HTML_TEMPLATES',69);			// HTMLTemplate option is only ...
define('MSG_PHP_FILE_UPLOADS',70);				// For security reasons, file upload...
define('MSG_FILE_UPLOAD',71);				    // File upload attempt ignored...
define('MSG_FILE_UPLOAD_ATTACK',72);			// Possible file upload attack...
define('MSG_PHP_PLAIN_TEMPLATES',73);			// PlainTemplate option is only...
define('MSG_ATTACH_NAME',74);				    // filter_options: Attach must contain a name...
define('MSG_PHP_BCC',75);				        // Warning: BCC is probably not supported...
define('MSG_CSVCOLUMNS',76);				    // The "csvcolumns" setting is not...
define('MSG_CSVFILE',77);				        // The "csvfile" setting is not...
define('MSG_TARG_EMAIL_PAT_START',78);			// Warning: Your TARGET_EMAIL pattern...
define('MSG_TARG_EMAIL_PAT_END',79);			// Warning: Your TARGET_EMAIL pattern...
define('MSG_CONFIG_WARN',80);				    // The following potential problems...
define('MSG_PHP_AUTORESP',81);				    // Autorespond is only supported...
define('MSG_ALERT',82);		                    // This is a test alert message...
define('MSG_NO_DEF_ALERT',83);				    // No DEF_ALERT value has been set....
define('MSG_TEST_SENT',84);				        // Test message sent.  Check your email.....
define('MSG_TEST_FAILED',85);					// FAILED to send alert message...
define('MSG_NO_DATA_PAGE',86);					// This URL is a Form submission program...
define('MSG_REQD_ERROR',87);					// The form required some values that you...
define('MSG_COND_ERROR',88);					// Some of the values you provided...
define('MSG_CRM_FAILURE',89);					// The form submission did not succeed...
define('MSG_FOPTION_WARN',90);					// Warning: You've used SendMailFOption in...
define('MSG_NO_ACTIONS',91);					// The form has an internal error...
define('MSG_NO_RECIP',92);				    	// The form has an internal error...
define('MSG_INV_EMAIL',93);				        // Invalid email addresses...
define('MSG_FAILED_SEND',94);					// Failed to send email...
define('MSG_ARESP_EMAIL',96);					// No "email" field was found. Autorespond...
define('MSG_ARESP_SUBJ',97);					// Your form submission...
define('MSG_LOG_NO_VERIMG',98);					// No VerifyImgString in session...
define('MSG_ARESP_NO_AUTH',99);					// Failed to obtain authorization...
define('MSG_LOG_NO_MATCH',100);					// User did not match image...
define('MSG_ARESP_NO_MATCH',101);				// Your entry did not match...
define('MSG_LOG_FAILED',102);				    // Failed
define('MSG_ARESP_FAILED',103);				    // Autoresponder failed
define('MSG_LOG_OK',104);				        // OK
define('MSG_THANKS_PAGE',105);				    // Thanks!  We've received your....
define('MSG_LOAD_MODULE',106);				    // Cannot load module....
define('MSG_LOAD_FMCOMPUTE',107);				// Cannot load FMCompute....
define('MSG_REGISTER_MODULE',108);				// Cannot register module....
define('MSG_COMP_PARSE',109);				    // These parse errors occurred....
define('MSG_COMP_REG_DATA',110);				// Failed to register data field....
define('MSG_COMP_ALERT',111);				    // The following alert messages....
define('MSG_COMP_DEBUG',112);				    // The following debug messages...
define('MSG_COMP_EXEC',113);				    // The following errors occurred....
define('MSG_REG_FMCOMPUTE',114);				// Cannot register function...
define('MSG_USER_ERRORS',115);				    // A number of errors occurred...
define('MSG_CALL_PARAM_COUNT',116);				// Invalid parameter count...
define('MSG_CALL_UNK_FUNC',117);				// Unknown function...
define('MSG_SAVE_FILE',118);				    // Failed to save file....
define('MSG_CHMOD',119);				        // Failed to chmod file....
define('MSG_VERIFY_MISSING',120);				// Image verification string missing...
define('MSG_VERIFY_MATCH',121);				    // Your entry did not match...
define('MSG_FILE_NAMES_INVALID',122);			// Some file_names specifications...
define('MSG_FILE_NAMES_NOT_FILE',123);			// Your file_names specification...
define('MSG_TEMPL_ALERT',124);				    // The following alert messages....
define('MSG_TEMPL_DEBUG',125);				    // The following debug messages...
define('MSG_TEMPL_PROC',126);				    // The following errors occurred....
define('MSG_SAVE_FILE_EXISTS',127);				// Cannot save file....
define('MSG_EMPTY_ADDRESSES',128);				// $COUNT empty addresses
define('MSG_CALL_INVALID_PARAM',129);			// Invalid parameter....
define('MSG_INI_PARSE_WARN',130);				// Warning: your INI
define('MSG_INI_PARSE_ERROR',131);				// The FormMail INI...
define('MSG_RECAPTCHA_MATCH',132);				// reCaptcha verification failed...

define('MSG_AND',133);		                    // "$ITEM1" and "$ITEM2"
define('MSG_NEXT_PLUS_GOOD',134);				// The form specifies both next_form and....
define('MSG_MULTIFORM',135);				    // You must set either MULTIFORMDIR or MULTIFORMURL...
define('MSG_MULTIFORM_FAILED',136);				// Failed to process multi-page form template "$NAME"...
define('MSG_NEED_THIS_FORM',137);				// Multi-page forms require "this_form" field...
define('MSG_NO_PHP_SELF',138);				    // PHP on the server is not providing "PHP_SELF"
define('MSG_RETURN_URL_INVALID',139);			// Return "$URL" is not valid...
define('MSG_GO_BACK',140);				        // Cannot 'go back' if not a multi-page form...
define('MSG_OPEN_URL',141);				        // Cannot open URL...
define('MSG_CANNOT_RETURN',142);				// Cannot return to page....
define('MSG_ATTACK_DETECTED',143);				// Server attack detected....
define('MSG_ATTACK_PAGE',144);				    // Your form submission....
define('MSG_ATTACK_MIME_INFO',145);				// The field "$FLD" contained...
define('MSG_ATTACK_DUP_INFO',146);				// The fields "$FLD1" and...
define('MSG_ATTACK_SPEC_INFO',147);				// Special field "$FLD"...
define('MSG_NEED_SCRATCH_PAD',148);				// You need to set SCRATCH_PAD...
define('MSG_MULTI_UPLOAD',149);				    // File upload processing failed during multi-page form processing.
define('MSG_OPEN_SCRATCH_PAD',150);				// Cannot open directory...
define('MSG_NO_NEXT_NUM_FILE',151);				// You cannot use the %nextnum% feature...
define('MSG_NEXT_NUM_FILE',152);				// Cannot process next number...
define('MSG_ATTACK_MANYURL_INFO',153);			// Field "$FLD"...
define('MSG_ATTACK_MANYFIELDS_INFO',154);		// $NUM fields have URLs....
define('MSG_REV_CAP',155);				        // ATTACK_DETECTION_REVERSE_CAPTCHA setting....
define('MSG_ATTACK_REV_CAP_INFO',156);			// The field "$FLD" contained...
define('MSG_ATTACK_JUNK_INFO',157);				// The field "$FLD" contained...
define('MSG_ARESP_EMPTY',158);				    // The autoresponse...
define('MSG_LOG_RECAPTCHA',159);				// reCaptcha process failed...

define('MSG_URL_PARSE',160);				    // URL parse failed
define('MSG_URL_SCHEME',161);				    // Unsupported URL scheme...
define('MSG_SOCKET',162);				        // Socket error ...
define('MSG_GETURL_OPEN',163);				    // Open URL failed: ...
define('MSG_RESOLVE',164);				        // Cannot resolve...

define('MSG_FORM_OK',170);				        // Form Submission Succeeded
define('MSG_FORM_ERROR',171);				    // Form Submission Error
define('MSG_GET_DISALLOWED',172);				// GET method has...
define('MSG_INVALID_SENDER',173);				// The form has specified an invalid sender
define('MSG_SET_SENDER_FROM_EMAIL',174);		// SET_SENDER_FROM_EMAIL is no longer supported 
//
// The following are PHP's file upload error messages
//
define('MSG_FILE_UPLOAD_ERR_UNK',180);			// Unknown error code.
define('MSG_FILE_UPLOAD_ERR1',181);				// The uploaded file exceeds the upload_max_filesize directive in php.ini.
define('MSG_FILE_UPLOAD_ERR2',182);				// The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the html form.
define('MSG_FILE_UPLOAD_ERR3',183);				// The uploaded file was only partially uploaded.
define('MSG_FILE_UPLOAD_ERR4',184);				// No file was uploaded.
define('MSG_FILE_UPLOAD_ERR6',186);				// Missing a temporary folder.
define('MSG_FILE_UPLOAD_ERR7',187);				// Failed to write file to disk.
define('MSG_FILE_UPLOAD_ERR8',188);				// File upload stopped by extension.
define('MSG_FILE_UPLOAD_SIZE',189);				// Uploaded file "$NAME" is too big... (not a PHP error code - internal maximum file size error)
define('MSG_POST_SIZE_LIMIT',190);			    // Your form submission exceeds the server's configured size limit.
//
// following are for derive_fields functions
//
define('MSG_DER_FUNC_ERROR',200);				// derive_fields: invalid function....
define('MSG_DER_FUNC_SIZE_FMT',201);			// function 'size' requires....
define('MSG_DER_FUNC_IF_FMT',202);				// function 'if' requires....
define('MSG_DER_FUNC_NEXTNUM_FMT',203);			// function 'nextnum' requires....
define('MSG_DER_FUNC_EXT_FMT',204);				// function 'ext' requires....
define('MSG_DER_FUNC1_FMT',205);				// function 'FUNC' requires....
define('MSG_DER_FUNC_SUBSTR_FMT',206);			// function 'substr' requires....

define('MSG_USER_ATTACK_JUNK',220);				// The following input ...
define('MSG_USER_ATTACK_REV_CAP',221);			// Your input ...
define('MSG_USER_ATTACK_DUP',222);				// You have ...
define('MSG_USER_ATTACK_MANY_URLS',223);		// Your input ...
define('MSG_USER_ATTACK_MANY_URL_FIELDS',224);	// Your input ...

define('MSG_INVALID_EMAIL',230);           // The email address...is invalid

//
// The following messages are no longer used
//
define('MSG_FLD_NOTFOUND', 900);


// @formatter:on
// <A NAME="MessageNumbers"> Jump to: <A HREF="#BuiltinMessages">

//
// Return true if using the built-in language
//
/** @noinspection PhpUnused */
function IsBuiltInLanguage()
{
	global $sLangID;

	return (strpos($sLangID,"builtin") !== false);
}

$sSavePath  = "";
$bPathSaved = false;
//
// Set include path to include the given directory.
//
function AddIncludePath($s_dir = ".")
{
	global $sSavePath,$bPathSaved;

	$s_path     = ini_get('include_path');
	$i_path_len = strlen($s_path);
	$s_sep      = IsServerWindows() ? ";" : ":"; // get path separator
	//
	// look for it in the include_path
	//
	$b_found = false;
	$i_pos   = 0;
	$i_len   = strlen($s_dir);
	while (!$b_found && ($i_pos = strpos($s_path,$s_dir,$i_pos)) !== false) {
		if ($i_pos == 0) {
			if ($i_len == $i_path_len) {
				$b_found = true;
			} // the path only has $s_dir
			elseif ($s_path[$i_len] == $s_sep) {
				$b_found = true;
			}
		} elseif ($s_path[$i_pos - 1] == $s_sep &&
		          ($i_pos + $i_len == $i_path_len ||
		           $s_path[$i_pos + $i_len] == $s_sep)
		) {
			$b_found = true;
		}
		if (!$b_found) {
			$i_pos++;
		}
	}
	if (!$b_found) {
		//
		// allow multiple calls, but only store the original path once
		//
		if (!$bPathSaved) {
			$sSavePath = $s_path;
		}
		if (empty($s_path)) {
			$s_path = $s_dir;
		} else
			//
			// prepend the directory
			//
		{
			$s_path = $s_dir . $s_sep . $s_path;
		}
		ini_set('include_path',$s_path);
		$bPathSaved = true;
	}
}

//
// Reset the include path after a call to AddIncludePath.
//
function ResetIncludePath()
{
	global $sSavePath,$bPathSaved;

	if ($bPathSaved) {
		ini_set('include_path',$sSavePath);
		$bPathSaved = false;
	}
}

//
// Load a language file
//
function LoadLanguageFile()
{
	global $aMessages,$sLangID,$sHTMLCharSet;

	AddIncludePath();
	if (includeFileExists("language.inc.php")) {
		include("language.inc.php");
	} else {
		if (includeFileExists("language.inc")) {
			include("language.inc");
		}
	}

	ResetIncludePath();
	if (isset($sHTMLCharSet) && $sHTMLCharSet !== "") {
		header("Content-Type: text/html; charset=$sHTMLCharSet");
	}
}

//
// Load the messages array from the default language, and then
// override with an optional language file.
// Note: all messages get the MNUM parameter sent which they can use.
// If they don't use it, the message number is appended.
//
function LoadBuiltinLanguage()
{
	global $aMessages,$sLangID;

	$sLangID = "English (builtin)";
	// MSG_SCRIPT_VERSION is shown if the PHP version is too old to run
	// FormMail
	// Parameters:
	//  $PHPREQ     is the minimum required PHP version
	//  $PHPVERS    is  the version the server currently has installed.
	$aMessages[MSG_SCRIPT_VERSION] = 'This script requires at least PHP version ' .
	                                 '$PHPREQ.  You have PHP version $PHPVERS.';

	// MSG_END_VERS_CHK is sent at the end of an Alert message when
	// FormMail detects that there's a newer version available
	// Parameters: none
	$aMessages[MSG_END_VERS_CHK] = '***************************************************\n' .
	                               'If you are happy with your current version and want\n' .
	                               'to stop these reminders, edit formmail.php and\n' .
	                               'set CHECK_FOR_NEW_VERSION to false.\n' .
	                               '***************************************************\n';

	// MSG_VERS_CHK is sent in an Alert message when
	// FormMail detects that there's a newer version available
	// Parameters:
	//  $TECTITE    the website to go to
	//  $FM_VERS    the current FormMail version
	//  $NEWVERS    the new FormMail version that's available
	$aMessages[MSG_VERS_CHK] = 'A later version of FormMail is available from $TECTITE.\n' .
	                           'You are currently using version $FM_VERS.\n' .
	                           'The new version available is $NEWVERS.\n';

	// MSG_CHK_FILE_ERROR is sent in an Alert message when
	// FormMail cannot create a file to record the time of version check.
	// Parameters:
	//  $FILE   the file name that could not be created
	//  $ERROR  the actual error message
	$aMessages[MSG_CHK_FILE_ERROR] = 'Unable to create check file "$FILE": $ERROR';

	// MSG_UNK_VALUE_SPEC is sent in an Alert message when
	// a form uses an unknown value specification in derive_fields.
	// Parameters:
	//  $SPEC   the unknown value specification
	//  $MSG    additional message
	$aMessages[MSG_UNK_VALUE_SPEC] = 'derive_fields: unknown value specification ' .
	                                 '"$SPEC"$MSG';

	// MSG_INV_VALUE_SPEC is sent in an Alert message when
	// a form uses a value specification in derive_fields that's
	// formatted incorrectly (missing terminating '%')
	// Parameters:
	//  $SPEC   the invalid value specification
	$aMessages[MSG_INV_VALUE_SPEC] = 'derive_fields: invalid value specification ' .
	                                 '"$SPEC" (possibly missing a "%")';

	// MSG_DERIVED_INVALID is sent in an Alert message when
	// a form's derive_fields setting has errors
	// Parameters: none
	// A list of errors is appended on separate lines
	$aMessages[MSG_DERIVED_INVALID] = 'Some derive_fields specifications are invalid $MNUM:\n';

	// MSG_INT_FORM_ERROR is sent in an Alert message and displayed
	// to the form user
	// Parameters: none
	$aMessages[MSG_INT_FORM_ERROR] = 'Internal form error';

	// MSG_OPTIONS_INVALID is sent in an Alert message when
	// a form's options settings are invalid.  This applies to
	// mail_options, filter_options, crm_options, and autorespond
	// Parameters:
	//  $OPT    the name of the options field
	// A list of errors is appended on separate lines
	$aMessages[MSG_OPTIONS_INVALID] = 'Some $OPT settings are undefined $MNUM:\n';

	// MSG_PLSWAIT_REDIR is shown to the user for a redirect
	// with JavaScript
	// Parameters: none
	$aMessages[MSG_PLSWAIT_REDIR] = 'Please wait while you are redirected...';

	// MSG_IFNOT_REDIR is shown to the user for a redirect
	// with JavaScript
	// Parameters:
	//  $URL    the URL to redirect to
	$aMessages[MSG_IFNOT_REDIR] = 'If you are not automatically redirected, ' .
	                              'please <a href="$URL">click here</a>.';

	// MSG_PEAR_OBJ is shown to the user if the PEAR Mail object
	// cannot be created
	// Parameters: none
	$aMessages[MSG_PEAR_OBJ] = 'Failed to create PEAR Mail object';

	// MSG_PEAR_ERROR is sent in an Alert message if the PEAR Mail processing
	// reports an error
	// Parameters:
	//  $MSG    the error message from PEAR
	$aMessages[MSG_PEAR_ERROR] = 'PEAR Mail error: $MSG';

	// MSG_NO_FOPT_ADDR is sent in an Alert message SendMailFOption is
	// specified in the form and no email address has been provided
	// Parameters: none
	$aMessages[MSG_NO_FOPT_ADDR] = 'You have specified "SendMailFOption" in your ' .
	                               'form, but there is no email address to use';

	// MSG_MORE_INFO is sent in an Alert message on a line by itself, just
	// before extra information about the FormMail processing that may have
	// led to the alert message
	// Parameters: none
	$aMessages[MSG_MORE_INFO] = 'More information:';

	// MSG_INFO_STOPPED is sent in an Alert message to say that extra
	// alert information has been suppressed because of potential security
	// problems with showing it.
	// Parameters: none
	$aMessages[MSG_INFO_STOPPED] = '(Extra alert information suppressed for ' .
	                               'security purposes. $MNUM)';

	// MSG_FM_ALERT is sent as the subject line of an Alert message
	// Parameters: none
	$aMessages[MSG_FM_ALERT] = 'FormMail alert';

	// MSG_FM_ERROR is sent as the subject line of an Alert message
	// Parameters: none
	$aMessages[MSG_FM_ERROR] = 'FormMail script error';

	// MSG_FM_ERROR_LINE is sent in an Alert message on a
	// separate line to introduce the actual error message
	// Parameters: none
	$aMessages[MSG_FM_ERROR_LINE] = 'The following error occurred in FormMail $MNUM:';

	// MSG_USERDATA_STOPPED is sent in an Alert message to say that the
	// user's data has been suppressed because of potential security
	// problems with showing it.
	// Parameters: none
	$aMessages[MSG_USERDATA_STOPPED] = '(User data suppressed for security ' .
	                                   'purposes. $MNUM)';

	// MSG_FILTERED is sent in an Alert message to show what filter
	// has been used on the message
	// Parameters:
	//  $FILTER     the name of the filter
	$aMessages[MSG_FILTERED] = 'This alert has been filtered through "$FILTER" ' .
	                           'for security purposes.';

	// MSG_TEMPLATES is sent in an Alert message when a form tries
	// to use a template, but templates have not been configured in
	// formmail.php
	// Parameters: none
	$aMessages[MSG_TEMPLATES] = 'You must set either TEMPLATEDIR or TEMPLATEURL ' .
	                            'in formmail.php before you can specify ' .
	                            'templates in your forms.';

	// MSG_OPEN_TEMPLATE is sent in an Alert message when FormMail cannot
	// open a template file
	// Parameters:
	//  $NAME   the name of the template file
	//  $ERROR  information about the error
	$aMessages[MSG_OPEN_TEMPLATE] = 'Failed to open template "$NAME" $MNUM: $ERROR';

	// MSG_ERROR_PROC is shown to the user as part of an error
	// page.  This message introduces the error.
	// Parameters: none
	$aMessages[MSG_ERROR_PROC] = 'An error occurred while processing the ' .
	                             'form $MNUM.\n\n';

	// MSG_ALERT_DONE is shown to the user as part of an error
	// page if an Alert message has been sent to the website owner.
	// Parameters:
	//  SERVER      the name of the server (website)
	$aMessages[MSG_ALERT_DONE] = 'The staff at $SERVER have been alerted to the error $MNUM.\n';

	// MSG_PLS_CONTACT is shown to the user as part of an error
	// page if an Alert message could *not* be sent to the website owner.
	// Parameters:
	//  SERVER      the name of the server (website)
	$aMessages[MSG_PLS_CONTACT] = 'Please contact us ($SERVER) directly since this form ' .
	                              'is not working $MNUM.\n';

	// MSG_APOLOGY is shown to the user as part of an error
	// page as an apology for a problem with the form.
	// Parameters:
	//  SERVER      the name of the server (website)
	$aMessages[MSG_APOLOGY] = '$SERVER apologizes for any inconvenience this error ' .
	                          'may have caused.';

	// MSG_ABOUT_FORMMAIL is shown to the user at the foot of pages
	// generated by FormMail (e.g. the default "Thanks" page and default
	// error page).
	// Parameters:
	//  $FM_VERS    the FormMail version number
	//  $TECTITE    www.tectite.com
	$aMessages[MSG_ABOUT_FORMMAIL] = 'Your form submission was processed by ' .
	                                 '<a href="http://$TECTITE/">FormMail</a> ' .
	                                 '($FM_VERS), a PHP script available from ' .
	                                 '<a href="http://$TECTITE/">$TECTITE</a>.';

	// MSG_PREG_FAILED is sent in an Alert message if the TectiteCRM
	// system failed to return the expected result.
	// Parameters: none
	$aMessages[MSG_PREG_FAILED] = 'preg_match_all failed in FindCRMFields';

	// MSG_URL_INVALID is sent in an Alert message if the specified
	// URL for TectiteCRM is not valid according to the TARGET_URLS
	// configuration setting
	// Parameters:
	//  $URL        the invalid URL
	$aMessages[MSG_URL_INVALID] = 'The URL "$URL" to access the Customer ' .
	                              'Relationship Management System is not valid ' .
	                              '(see TARGET_URLS in formmail.php)';

	// MSG_URL_OPEN is sent in an Alert message if the specified
	// URL for TectiteCRM cannot be opened
	// Parameters:
	//  $URL    the invalid URL
	//  $ERROR  information about the error
	$aMessages[MSG_URL_OPEN] = 'Failed to open Customer Relationship ' .
	                           'Management System URL "$URL" $MNUM: $ERROR';

	// MSG_CRM_FAILED is sent in an Alert message if the TectiteCRM
	// system doesn't return an OK message
	// Parameters:
	//  $URL    the invalid URL
	//  $MSG    more information
	$aMessages[MSG_CRM_FAILED] = 'Failure report from Customer Relationship ' .
	                             'Management System (url="$URL") $MNUM: $MSG';

	// MSG_CRM_FORM_ERROR is shown to the user if the information
	// passed to TectiteCRM was not accepted
	// Parameters: none
	$aMessages[MSG_CRM_FORM_ERROR] = 'Your form submission was not accepted';

	// MSG_AND is shown to the user; it shows two items separated
	// by "and"
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_AND] = '"$ITEM1" and "$ITEM2"';

	// MSG_OR is shown to the user; it shows two items separated
	// by "or"
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_OR] = '"$ITEM1" or "$ITEM2"';

	// MSG_NOT_BOTH is shown to the user; it shows two items that must
	// be specified together
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_NOT_BOTH] = 'not both "$ITEM1" and "$ITEM2"';

	// MSG_XOR is shown to the user; it shows two items that must
	// not be specified together
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_XOR] = '"$ITEM1" or "$ITEM2" (but not both)';

	// MSG_IS_SAME_AS is shown to the user; it shows two items that must
	// not be the same value
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_IS_SAME_AS] = '"$ITEM1" is the same as "$ITEM2"';

	// MSG_IS_NOT_SAME_AS is shown to the user; it shows two items that must
	// be the same value
	// Parameters:
	//  $ITEM1      the first item
	//  $ITEM2      the second item
	$aMessages[MSG_IS_NOT_SAME_AS] = '"$ITEM1" is not the same as "$ITEM2"';

	// MSG_REQD_OPER is sent in an Alert message when an unknown
	// operator has been used in a "required" specification
	// Parameters:
	//  $OPER       the unknown operator
	$aMessages[MSG_REQD_OPER] = 'Operator "$OPER" is not valid for "required"';

	// MSG_PAT_FAILED is sent in an Alert message when a "conditions" pattern
	// match has not matched anything (this isn't necessarily an error)
	// Parameters:
	//  $OPER       the "conditions" operator
	//  $PAT        the "conditions" pattern
	//  $VALUE      the value that was searched
	$aMessages[MSG_PAT_FAILED] = 'Pattern operator "$OPER" failed: pattern ' .
	                             '"$PAT", value searched was "$VALUE".';

	// MSG_COND_OPER is sent in an Alert message when a "conditions"
	// operator is not value
	// Parameters:
	//  $OPER       the "conditions" operator
	$aMessages[MSG_COND_OPER] = 'Operator "$OPER" is not valid for "conditions"';

	// MSG_INV_COND is sent in an Alert message when a "conditions"
	// field is not valid
	// Parameters:
	//  FLD     the field name
	$aMessages[MSG_INV_COND] = 'Invalid "conditions" field "$FLD" - not a string or array.';

	// MSG_COND_CHARS is sent in an Alert message when a "conditions"
	// field is missing the mandatory first 2 characters (the separators)
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	$aMessages[MSG_COND_CHARS] = 'The conditions field "$FLD" is not valid. ' .
	                             'You must provide the two separator ' .
	                             'characters at the beginning. You had "$COND".';

	// MSG_COND_INVALID is sent in an Alert message when a "conditions"
	// field has the wrong format
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	//  SEP     the internal separator character for the field.
	$aMessages[MSG_COND_INVALID] = 'The conditions field "$FLD" is not valid. ' .
	                               'There must be at least 5 components ' .
	                               'separated by "$SEP". Your value was "$COND".';

	// MSG_COND_TEST_LONG is sent in an Alert message when a "conditions"
	// TEST value has too many components
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	//  SEP     the list separator character for the field.
	$aMessages[MSG_COND_TEST_LONG] = 'Field "$FLD" has too many components for ' .
	                                 'a "TEST" command: "$COND".\nAre you missing ' .
	                                 'a "$SEP"?';

	// MSG_COND_IF_SHORT is sent in an Alert message when a "conditions"
	// IF value has too few components
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	//  SEP     the internal separator character for the field.
	$aMessages[MSG_COND_IF_SHORT] = 'Field "$FLD" has too few components for ' .
	                                'an "IF" command: "$COND".\nThere must be ' .
	                                'at least 6 components separated by "$SEP"';

	// MSG_COND_IF_LONG is sent in an Alert message when a "conditions"
	// IF value has too many components
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	//  SEP     the list separator character for the field.
	$aMessages[MSG_COND_IF_LONG] = 'Field "$FLD" has too many components for ' .
	                               'an "IF" command: "$COND".\nAre you missing ' .
	                               'a "$SEP"?';

	// MSG_COND_UNK is sent in an Alert message when a "conditions"
	// value has an unknown command
	// Parameters:
	//  FLD     the field name
	//  COND    the conditions field value
	//  CMD     the unknown command
	$aMessages[MSG_COND_UNK] = 'Field "$FLD" has an unknown command word ' .
	                           '"$CMD": "$COND".';

	// MSG_MISSING is sent in an Alert message when
	// a socket filter is incorrectly defined
	// Parameters:
	//  ITEM    the missing item
	$aMessages[MSG_MISSING] = 'Missing "$ITEM"';

	// MSG_NEED_ARRAY is sent in an Alert message when
	// a socket filter is incorrectly defined
	// Parameters:
	//  ITEM    the item that should be an array
	$aMessages[MSG_NEED_ARRAY] = '"$ITEM" must be an array';

	// MSG_SUBM_FAILED is shown to the user when an internal error
	// as occurred and that error is not to be shown
	// Parameters: none
	$aMessages[MSG_SUBM_FAILED] = 'Your form submission has failed due to ' .
	                              'an error on our server.';

	// MSG_FILTER_WRONG is sent in an Alert message when
	// a socket filter is incorrectly defined
	// Parameters:
	//  FILTER  the filter name
	//  ERRORS  a string containing a list of errors
	$aMessages[MSG_FILTER_WRONG] = 'Filter "$FILTER" is not properly defined: ' .
	                               '$ERRORS';

	// MSG_FILTER_CONNECT is sent in an Alert message when FormMail
	// cannot connect to a socket filter
	// Parameters:
	//  FILTER  the filter name
	//  SITE    the site
	//  ERRNUM  socket error number
	//  ERRSTR  socket error message
	$aMessages[MSG_FILTER_CONNECT] = 'Could not connect to site "$SITE" ' .
	                                 'for filter "$FILTER" ($ERRNUM): $ERRSTR';

	// MSG_FILTER_PARAM is sent in an Alert message when a socket
	// filter has an invalid parameter specification
	// Parameters:
	//  FILTER  the filter name
	//  NUM     parameter number
	//  NAME    parameter name
	$aMessages[MSG_FILTER_PARAM] = 'Filter "$FILTER" has invalid parameter ' .
	                               '#$NUM: no "$NAME"';

	// MSG_FILTER_OPEN_FILE is sent in an Alert message when a socket
	// filter cannot open the required file
	// Parameters:
	//  FILTER  the filter name
	//  FILE    the file that could not be opened
	//  ERROR   the error message
	$aMessages[MSG_FILTER_OPEN_FILE] = 'Filter "$FILTER" cannot open file ' .
	                                   '"$FILE": $ERROR';

	// MSG_FILTER_FILE_ERROR is sent in an Alert message when a socket
	// filter gets an error message during reading a file
	// Parameters:
	//  FILTER  the filter name
	//  FILE    the file that could not be opened
	//  ERROR   the error message
	//  NLINES  the number of lines that were read successfully
	$aMessages[MSG_FILTER_FILE_ERROR] = 'Filter "$FILTER": read error on file ' .
	                                    '"$FILE" after $NLINES lines: $ERROR';

	// MSG_FILTER_READ_ERROR is sent in an Alert message when a socket
	// filter gets an error during reading from the socket
	// Parameters:
	//  FILTER  the filter name
	//  ERROR   the error message
	$aMessages[MSG_FILTER_READ_ERROR] = 'Filter "$FILTER" failed: read error: ' .
	                                    '$ERROR';

	// MSG_FILTER_NOT_OK is sent in an Alert message when a socket
	// filter fails to return the agreed __OK__ indicator
	// Parameters:
	//  FILTER  the filter name
	//  DATA    the data returned from the filter
	$aMessages[MSG_FILTER_NOT_OK] = 'Filter "$FILTER" failed (missing ' .
	                                '__OK__ line): $DATA';

	// MSG_FILTER_UNK is sent in an Alert message
	// when an unknown filter is specified by a form
	// Parameters:
	//  FILTER  the filter name
	$aMessages[MSG_FILTER_UNK] = 'Unknown filter "$FILTER"';

	// MSG_FILTER_CHDIR is sent in an Alert message
	// when FormMail cannot change to the filter's directory
	// Parameters:
	//  FILTER  the filter name
	//  DIR     the directory name
	//  ERROR   an error message from the system
	$aMessages[MSG_FILTER_CHDIR] = 'Cannot chdir to "$DIR" to run filter ' .
	                               '"$FILTER": $ERROR';

	// MSG_FILTER_NOTFOUND is sent in an Alert message
	// when FormMail cannot execute the filter
	// Parameters:
	//  FILTER  the filter name
	//  CMD     the command line being executed
	//  ERROR   an error message from the system
	$aMessages[MSG_FILTER_NOTFOUND] = 'Cannot execute filter "$FILTER" with ' .
	                                  'command "$CMD": $ERROR';

	// MSG_FILTER_ERROR is sent in an Alert message
	// when a filter returns a non-zero status
	// Parameters:
	//  FILTER  the filter name
	//  ERROR   an error message from the system
	//  STATUS  the status return from the command
	$aMessages[MSG_FILTER_ERROR] = 'Filter "$FILTER" failed (status $STATUS): ' .
	                               '$ERROR';

	// MSG_SPARE is a spare message
	$aMessages[MSG_SPARE] = '';

	// MSG_TEMPLATE_ERRORS is sent as part of an Alert message
	// when a template has generated some errors.  The message
	// should end with a new line and the actual errors are
	// output after it.
	// Parameters:
	//  NAME    the template name
	$aMessages[MSG_TEMPLATE_ERRORS] = 'Template "$NAME" caused the ' .
	                                  'following errors $MNUM:\n';

	// MSG_TEMPLATE_FAILED is sent in an Alert message
	// when processing a template has failed.
	// Parameters:
	//  NAME    the template name
	$aMessages[MSG_TEMPLATE_FAILED] = 'Failed to process template "$NAME"';

	// MSG_MIME_PREAMBLE is sent in the preamble of MIME emails
	// Parameters: none
	$aMessages[MSG_MIME_PREAMBLE] = '(Your mail reader should not show this ' .
	                                'text.\nIf it does you may need to ' .
	                                'upgrade to more modern software.)';

	// MSG_MIME_HTML is sent in the preamble of HTML emails
	// Parameters:
	//  NAME    the template name
	$aMessages[MSG_MIME_HTML] = 'This message has been generated by FormMail ' .
	                            'using an HTML template\ncalled "$NAME". The ' .
	                            'raw text of the form results\nhas been ' .
	                            'included below, but your mail reader should ' .
	                            'display the HTML\nversion only (unless it\'s ' .
	                            'not capable of doing so).';

	// MSG_FILE_OPEN_ERROR is sent in an Alert message when FormMail
	// cannot open a file
	// Parameters:
	//  NAME    the file name
	//  TYPE    the type of file
	//  ERROR   the system error message
	$aMessages[MSG_FILE_OPEN_ERROR] = 'Failed to open $TYPE file "$NAME": $ERROR';

	// MSG_ATTACH_DATA is sent in an Alert message when the file
	// attachment through 'data' has gone wrong.
	// Parameters: none
	$aMessages[MSG_ATTACH_DATA] = 'Internal error: AttachFile requires ' .
	                              '"tmp_name" or "data"';

	// MSG_PHP_HTML_TEMPLATES is sent in an Alert message when an
	// HTML template is used but the PHP version is too old. (deprecated)
	// Parameters:
	//  $PHPVERS    the current PHP version
	$aMessages[MSG_PHP_HTML_TEMPLATES] = '';

	// MSG_PHP_FILE_UPLOADS is sent in an Alert message when
	// file upload is used but the PHP version is too old. (deprecated)
	// Parameters:
	//  $PHPVERS    the current PHP version
	$aMessages[MSG_PHP_FILE_UPLOADS] = '';

	// MSG_FILE_UPLOAD is sent in an Alert message when
	// file upload is attempted but FormMail is not configured to allow
	// it
	// Parameters: none
	$aMessages[MSG_FILE_UPLOAD] = 'File upload attempt ignored';

	// MSG_FILE_UPLOAD_ATTACK is sent in an Alert message when
	// possible file upload attack is detected
	// Parameters:
	//  NAME    file name
	//  TEMP    temporary file name
	//  FLD     name of the file upload field
	$aMessages[MSG_FILE_UPLOAD_ATTACK] = 'Possible file upload attack ' .
	                                     'detected: field="$FLD", name="$NAME" ' .
	                                     'temp name="$TEMP"';

	// MSG_PHP_PLAIN_TEMPLATES is sent in an Alert message when a
	// Plain template is used but the PHP version is too old. (deprecated)
	// Parameters:
	//  $PHPVERS    the current PHP version
	$aMessages[MSG_PHP_PLAIN_TEMPLATES] = '';

	// MSG_ATTACH_NAME is sent in an Alert message when a
	// the form uses the Attach feature without specifying a file name
	// Parameters: none
	$aMessages[MSG_ATTACH_NAME] = 'filter_options: Attach must contain a name ' .
	                              '(e.g. Attach=data.txt)';

	// MSG_PHP_BCC is sent in an Alert message when a
	// the form uses the BCC feature and the PHP version may not support it
	// (deprecated)
	// Parameters:
	//  $PHPVERS    the current PHP version
	$aMessages[MSG_PHP_BCC] = '';

	// MSG_CSVCOLUMNS is sent in an Alert message when a csvcolumns field
	// is not correct
	// Parameters:
	//  $VALUE  the csvcolumns field value
	$aMessages[MSG_CSVCOLUMNS] = 'The "csvcolumns" setting is not ' .
	                             'valid: "$VALUE"';

	// MSG_CSVFILE is sent in an Alert message when a csvfile field
	// is not correct
	// Parameters:
	//  $VALUE  the csvfile field value
	$aMessages[MSG_CSVFILE] = 'The "csvfile" setting is not valid: "$VALUE"';

	// MSG_TARG_EMAIL_PAT_START is sent in an Alert message when a
	// $TARGET_EMAIL pattern is insecure because of a missing '^'
	// at the beginning
	// Parameters:
	//  $PAT    the pattern
	$aMessages[MSG_TARG_EMAIL_PAT_START] = 'Warning: Your TARGET_EMAIL pattern ' .
	                                       '"$PAT" is missing a ^ at the ' .
	                                       'beginning.';

	// MSG_TARG_EMAIL_PAT_END is sent in an Alert message when a
	// $TARGET_EMAIL pattern is insecure because of a missing '$'
	// at the end
	// Parameters:
	//  $PAT    the pattern
	$aMessages[MSG_TARG_EMAIL_PAT_END] = 'Warning: Your TARGET_EMAIL pattern ' .
	                                     '"$PAT" is missing a $ at the end.';

	// MSG_CONFIG_WARN is sent in an Alert message when the FormMail
	// configuration may have some problems.  The messages are
	// passed on separate lines, so the line terminations below
	// are important.
	// Parameters:
	//  $MESGS  lines of messages
	$aMessages[MSG_CONFIG_WARN] = 'The following potential problems were found ' .
	                              'in your configuration:\n$MESGS\n\n' .
	                              'These are not necessarily errors, but you ' .
	                              'should review the documentation\n' .
	                              'inside formmail.php.  If you are sure your ' .
	                              'configuration is correct\n' .
	                              'you can disable the above messages by ' .
	                              'changing the CONFIG_CHECK settings.';

	// MSG_PHP_AUTORESP is sent in an Alert message when the PHP version
	// does not support autoresponding (deprecated)
	// Parameters:
	//  $PHPVERS    current PHP version
	$aMessages[MSG_PHP_AUTORESP] = '';

	// MSG_ALERT is the test alert message (formmail.php?testalert=1)
	// Parameters:
	//  $LANG               the language ID
	//  $PHPVERS            PHP version
	//  $FM_VERS            FormMail version
	//  $SERVER             server type
	//  $DOCUMENT_ROOT      PHP's DOCUMENT_ROOT value
	//  $SCRIPT_FILENAME    PHP's SCRIPT_FILENAME value
	//  $PATH_TRANSLATED    PHP's PATH_TRANSLATED value
	//  $REAL_DOCUMENT_ROOT the REAL_DOCUMENT_ROOT value
	$aMessages[MSG_ALERT] = 'This is a test alert message $MNUM\n' .
	                        'Loaded language is $LANG\n' .
	                        'PHP version is $PHPVERS\n' .
	                        'FormMail version is $FM_VERS\n' .
	                        'Server type: $SERVER\n' .
	                        '\n' .
	                        'DOCUMENT_ROOT: $DOCUMENT_ROOT\n' .
	                        'SCRIPT_FILENAME: $SCRIPT_FILENAME\n' .
	                        'PATH_TRANSLATED: $PATH_TRANSLATED\n' .
	                        'REAL_DOCUMENT_ROOT: $REAL_DOCUMENT_ROOT';

	// MSG_NO_DEF_ALERT is displayed if you use the testalert feature
	// and no DEF_ALERT setting has been provided.
	// Parameters: none
	$aMessages[MSG_NO_DEF_ALERT] = 'No DEF_ALERT value has been set.';

	// MSG_TEST_SENT is displayed if when use the testalert feature
	// Parameters: none
	$aMessages[MSG_TEST_SENT] = 'Test message sent.  Check your email.';

	// MSG_TEST_FAILED is displayed if when use the testalert feature
	// and the mail sending fails.
	// Parameters: none
	$aMessages[MSG_TEST_FAILED] = 'FAILED to send alert message.  Check your ' .
	                              'server error logs.';

	// MSG_NO_DATA_PAGE is the page that's displayed if the user
	// just opens the URL to FormMail directly.
	// Parameters: none
	$aMessages[MSG_NO_DATA_PAGE] = 'This URL is a Form submission program.\n' .
	                               'It appears the form is not working ' .
	                               'correctly as there was no data found.\n' .
	                               'You\'re not supposed to browse to this ' .
	                               'URL; it should be accessed from a form.';

	// MSG_REQD_ERROR is displayed to the user as a default error
	// message when they haven't supplied some required fields
	// Parameters: none
	$aMessages[MSG_REQD_ERROR] = 'The form required some values that you ' .
	                             'did not seem to provide.';

	// MSG_COND_ERROR is displayed to the user as a default error
	// message when some form conditions have failed
	// Parameters: none
	$aMessages[MSG_COND_ERROR] = 'Some of the values you provided are not valid.';

	// MSG_CRM_FAILURE is displayed to the user when submission
	// to the CRM has failed.
	// Parameters:
	//      $URL    the URL that was used
	//      $DATA   data returned from the CRM
	$aMessages[MSG_CRM_FAILURE] = 'The form submission did not succeed due to ' .
	                              'a CRM failure. URL was \'$URL\'. ' .
	                              'Returned CRM data:\n$DATA';

	// MSG_FOPTION_WARN is sent in an Alert message when the form
	// uses the superseded SendMailFOption feature
	// Parameters:
	//  $LINE   line number for SENDMAIL_F_OPTION
	$aMessages[MSG_FOPTION_WARN] = 'Warning: You\'ve used SendMailFOption in ' .
	                               '"mail_options" in your form. This has been ' .
	                               'superseded with a configuration setting ' .
	                               'inside formmail.php.  Please update your ' .
	                               'formmail.php configuration (look for ' .
	                               'SENDMAIL_F_OPTION on line $LINE) and set ' .
	                               'it to "true", then remove SendMailFOption ' .
	                               'from your form(s).';

	// MSG_NO_ACTIONS is sent in an Alert message when there is no
	// action to perform or email address to send to
	// Parameters: none
	$aMessages[MSG_NO_ACTIONS] = 'The form has an internal error - no actions ' .
	                             'or recipients were specified.';

	// MSG_NO_RECIP is sent in an Alert message when there are no
	// valid recipients to send to
	// Parameters: none
	$aMessages[MSG_NO_RECIP] = 'The form has an internal error - no valid ' .
	                           'recipients were specified.';

	// MSG_INV_EMAIL is sent in an Alert message when there are errors
	// in the email addresses specified in the form
	// Parameters:
	//  $ERRORS     list of errors
	$aMessages[MSG_INV_EMAIL] = 'Invalid email addresses were specified ' .
	                            'in the form $MNUM:\n$ERRORS';

	// MSG_FAILED_SEND is sent in an Alert message when the mail sending fails.
	// Parameters: none
	$aMessages[MSG_FAILED_SEND] = 'Failed to send email';

	// MSG_ARESP_EMAIL is sent in an Alert message when
	// no email address has been specified for an autoreponse
	// Parameters: none
	$aMessages[MSG_ARESP_EMAIL] = 'No "email" field was found. Autorespond ' .
	                              'requires the submitter\'s email address.';

	// MSG_ARESP_SUBJ is the default subject for the auto response email
	// Parameters: none
	$aMessages[MSG_ARESP_SUBJ] = 'Your form submission';

	// MSG_LOG_NO_VERIMG is written to the auto respond log file
	// if no VerifyImgString session variable was found
	// Parameters: none
	$aMessages[MSG_LOG_NO_VERIMG] = 'No VerifyImgString or turing_string in session, ' .
	                                'no reverse CAPTCHA, no reCaptcha';

	// MSG_ARESP_NO_AUTH is shown to the user
	// if no VerifyImgString session variable was found
	// Parameters: none
	$aMessages[MSG_ARESP_NO_AUTH] = 'Failed to obtain authorization to send ' .
	                                'you email. This is probably a fault on ' .
	                                'the server.';

	// MSG_LOG_NO_MATCH is written to the auto respond log file
	// if the user's entry did not match the image verification
	// Parameters: none
	$aMessages[MSG_LOG_NO_MATCH] = 'User did not match image';

	// MSG_LOG_RECAPTCHA is written to the auto respond log file
	// if the reCaptcha process fails
	// Parameters:
	//  ERR the reCaptcha error code
	$aMessages[MSG_LOG_RECAPTCHA] = 'reCaptcha process failed ($ERR)';

	// MSG_ARESP_NO_MATCH is shown to the user
	// if the user's entry did not match the image verification
	// Parameters: none
	$aMessages[MSG_ARESP_NO_MATCH] = 'Your entry did not match the image';

	// MSG_LOG_FAILED is written to the auto respond log file
	// if the autoresponding failed
	// Parameters: none
	$aMessages[MSG_LOG_FAILED] = 'Failed';

	// MSG_ARESP_FAILED is sent in an Alert message
	// if the autoresponding failed
	// Parameters: none
	$aMessages[MSG_ARESP_FAILED] = 'Autoresponder failed';

	// MSG_LOG_OK is written to the auto respond log file
	// if the autoresponding succeeded
	// Parameters: none
	$aMessages[MSG_LOG_OK] = 'OK';

	// MSG_THANKS_PAGE is the default page that's displayed if the
	// submission is successful
	// Parameters: none
	$aMessages[MSG_THANKS_PAGE] = 'Thanks!  We\'ve received your information ' .
	                              'and, if it\'s appropriate, we\'ll be in ' .
	                              'contact with you soon.';

	// MSG_LOAD_MODULE is sent in an alert message if a module
	// could not be loaded.
	// Parameters:
	//  $FILE    the file name
	//  $ERROR   the error message
	$aMessages[MSG_LOAD_MODULE] = 'Cannot load module from file \'$FILE\': $ERROR';

	// MSG_LOAD_FMCOMPUTE is sent in an alert message if the form
	// specifies at least one "fmcompute" field and the FMCompute
	// module cannot be loaded.
	// Parameters:
	//  $FILE    the file name
	//  $ERROR   the error message
	$aMessages[MSG_LOAD_FMCOMPUTE] = 'Cannot load FMCompute module from file ' .
	                                 '\'$FILE\': $ERROR';

	// MSG_REGISTER_MODULE is sent in an alert message if a module
	// could not register with FMCompute
	// Parameters:
	//  $NAME    the name of the module
	//  $ERROR   the error message
	$aMessages[MSG_REGISTER_MODULE] = 'Cannot register module $NAME with ' .
	                                  'FMCompute: $ERROR';

	// MSG_COMP_PARSE is sent in an alert message if a parse error
	// occurs in an fmcompute field
	// Parameters:
	//  $CODE    the code with an error
	//  $ERRORS  the error messages
	$aMessages[MSG_COMP_PARSE] = 'These parse errors occurred in the following ' .
	                             'code:\n$ERRORS\n$CODE';

	// MSG_COMP_REG_DATA is sent in an alert message if FormMail cannot
	// register a data field with the FMCompute module
	// Parameters:
	//  $NAME    the field name
	//  $ERROR  the error message
	$aMessages[MSG_COMP_REG_DATA] = 'Failed to register data field \'$NAME\': ' .
	                                '$ERROR';

	// MSG_COMP_ALERT is sent in an alert message if the FMCompute
	// module has generated some alert messages.
	// Parameters:
	//  $ALERTS  the alerts
	$aMessages[MSG_COMP_ALERT] = 'The following alert messages were reported ' .
	                             'from the FMCompute module: $ALERTS';

	// MSG_COMP_DEBUG is sent in an alert message if the FMCompute
	// module has generated some debug messages.
	// Parameters:
	//  $DEBUG  the alerts
	$aMessages[MSG_COMP_DEBUG] = 'The following debug messages were reported ' .
	                             'from the FMCompute module: $DEBUG';

	// MSG_COMP_EXEC is sent in an alert message if the FMCompute
	// module has generated some error messages during execution
	// Parameters:
	//  $ERRORS  the errors
	$aMessages[MSG_COMP_EXEC] = 'The following error messages were reported ' .
	                            'from the FMCompute module: $ERRORS';

	// MSG_TEMPL_ALERT is sent in an alert message if Advanced Template
	// Processing has generated some alert messages.
	// Parameters:
	//  $ALERTS  the alerts
	$aMessages[MSG_TEMPL_ALERT] = 'The following alert messages were reported ' .
	                              'from the Advanced Template Processor: $ALERTS';

	// MSG_TEMPL_DEBUG is sent in an alert message if Advanced Template
	// Processing has generated some debug messages.
	// Parameters:
	//  $DEBUG  the alerts
	$aMessages[MSG_TEMPL_DEBUG] = 'The following debug messages were reported ' .
	                              'from the Advanced Template Processor: $DEBUG';

	// MSG_TEMPL_PROC is sent in an alert message if Advanced Template Processing
	// has generated some error messages during processing
	// Parameters:
	//  $ERRORS  the errors
	$aMessages[MSG_TEMPL_PROC] = 'The following error messages were reported ' .
	                             'from the Advanced Template Processor: $ERRORS';

	// MSG_REG_FMCOMPUTE is sent in an Alert message when FormMail
	// cannot register an external function with FMCompute.
	// Parameters:
	//  FUNC    the function that could not be registered
	//  ERROR   the error message
	$aMessages[MSG_REG_FMCOMPUTE] = 'Cannot register function "$FUNC" with ' .
	                                'FMCompute: $ERROR';

	// MSG_USER_ERRORS is shown as part of a user error when an FMCompute
	// has called the "UserError" function one or more times.
	// Parameters:
	//  NONE
	$aMessages[MSG_USER_ERRORS] = 'One or more errors occurred in your form submission';

	// MSG_CALL_PARAM_COUNT is sent in an alert when a call to a FormMail
	// function from FMCompute has the wrong number of parameters
	// Parameters:
	//  FUNC    the function name
	//  COUNT   the actual number of parameters passed
	$aMessages[MSG_CALL_PARAM_COUNT] = 'FMCompute called FormMail function ' .
	                                   '\'$FUNC\' with wrong number of ' .
	                                   'parameters: $COUNT';

	// MSG_CALL_UNK_FUNC is sent in an alert when FMCompute calls an
	// unknown FormMail function
	// Parameters:
	//  FUNC    the function name
	$aMessages[MSG_CALL_UNK_FUNC] = 'FMCompute called unknown FormMail function ' .
	                                '\'$FUNC\'';

	// MSG_SAVE_FILE is sent in an alert when saving a file to
	// the server has failed
	// Parameters:
	//  FILE    the source file name (usually a temporary file name)
	//  DEST    the destination file name
	//  ERR     the error message
	$aMessages[MSG_SAVE_FILE] = 'Failed to save file \'$FILE\' to \'$DEST\': $ERR';

	// MSG_SAVE_FILE_EXISTS is sent as part of an alert when saving a file to
	// the repository ($FILE_REPOSITORY) has failed because the file
	// already exists and FILE_OVERWRITE is set to false.
	// Parameters:
	//  FILE    the destination file name
	$aMessages[MSG_SAVE_FILE_EXISTS] = 'Cannot save file to repository as this would ' .
	                                   'overwrite \'$FILE\' and you have ' .
	                                   'set FILE_OVERWRITE to false.';

	// MSG_EMPTY_ADDRESSES is sent as part of an alert when a number of empty
	// email addresses have been specified in recipients, cc, or bcc
	// *and* there are no valid addresses provided
	// in the list
	// Parameters:
	//  COUNT    the number of empty addresses
	$aMessages[MSG_EMPTY_ADDRESSES] = '$COUNT empty addresses';

	// MSG_CALL_INVALID_PARAM is sent in an alert when a call to a FormMail
	// function from FMCompute has an invalid parameter
	// Parameters:
	//  FUNC    the function name
	//  PARAM   the parameter number
	//  CORRECT information about correct values
	$aMessages[MSG_CALL_INVALID_PARAM] = 'FMCompute called FormMail function ' .
	                                     '\'$FUNC\' with an invalid parameter ' .
	                                     'number $PARAM. Correct values are: $CORRECT';

	// MSG_INI_PARSE_WARN is sent in an alert when the INI file
	// may have a syntax error and cannot be parsed.
	// Parameters:
	//  FILE    the file name
	$aMessages[MSG_INI_PARSE_WARN] = 'Warning: your INI file \'$FILE\' appears ' .
	                                 'to be empty.  This may indicate a syntax error.';

	// MSG_INI_PARSE_ERROR is shown as an error message when the INI file
	// has a syntax error and cannot be parsed.
	// Parameters:
	//  FILE    the file name
	$aMessages[MSG_INI_PARSE_ERROR] = 'The FormMail INI file \'$FILE\' has a syntax error';

	// MSG_CHMOD is sent in an alert when changing the protection
	// mode of a file to has failed
	// Parameters:
	//  FILE    the file name
	//  MODE    the mode
	//  ERR     the error message
	$aMessages[MSG_CHMOD] = 'Failed to change protection mode of file \'$FILE\' ' .
	                        'to $MODE: $ERR';

	// MSG_VERIFY_MISSING is shown to the user image verification string
	// was not found
	// Parameters: none
	$aMessages[MSG_VERIFY_MISSING] = 'Image verification string missing. This' .
	                                 ' is probably a fault on the server.';

	// MSG_VERIFY_MATCH is shown to the user
	// if the user's entry did not match the image verification for the
	// imgverify option
	// Parameters: none
	$aMessages[MSG_VERIFY_MATCH] = 'Your entry did not match the image';

	// MSG_RECAPTCHA_MATCH is shown to the user
	// if using the reCaptcha system and there was an error
	// Parameters:
	//  ERR the error code from the reCaptcha API
	$aMessages[MSG_RECAPTCHA_MATCH] = 'reCaptcha verification failed ($ERR)';

	// MSG_FILE_NAMES_INVALID is sent in an Alert message when
	// a form's file_names setting has errors
	// Parameters: none
	// A list of errors is appended on separate lines
	$aMessages[MSG_FILE_NAMES_INVALID] = 'Some file_names specifications are invalid $MNUM:\n';

	// MSG_FILE_NAMES_NOT_FILE is sent in an Alert message when
	// a form's file_names setting refers to a file field that doesn't
	// exist
	// Parameters:
	//      NAME    the name of the file field that doesn't exist
	$aMessages[MSG_FILE_NAMES_NOT_FILE] = 'Your file_names specification has ' .
	                                      'an error. \'$NAME\' is not the name ' .
	                                      'of a file upload field\n';

	// MSG_NEXT_PLUS_GOOD is sent in an alert message if the form is
	// ambiguous and specifies both "next_form" and "good_url" or
	// "good_template"
	// Parameters:
	//  $WHICH  the "good_" field that was specified
	$aMessages[MSG_NEXT_PLUS_GOOD] = 'The form has specified both "next_form" ' .
	                                 'and "$WHICH" fields - the action to ' .
	                                 'to perform is ambiguous';

	// MSG_MULTIFORM is sent in an Alert message when a form tries
	// to use a multi-form template, but templates have not been configured in
	// formmail.php
	// Parameters: none
	$aMessages[MSG_MULTIFORM] = 'You must set either MULTIFORMDIR or MULTIFORMURL ' .
	                            'in formmail.php before you can use ' .
	                            'multi-page forms.';

	// MSG_MULTIFORM_FAILED is sent in an Alert message
	// when processing a multi-page form template has failed.
	// Parameters:
	//  NAME    the template name
	$aMessages[MSG_MULTIFORM_FAILED] = 'Failed to process multi-page form template "$NAME"';

	// MSG_NEED_THIS_FORM is sent in an Alert message
	// when a multi-page form does not specify the "this_form" field.
	// Parameters:
	//  none
	$aMessages[MSG_NEED_THIS_FORM] = 'Multi-page forms require "this_form" field';

	// MSG_NO_PHP_SELF is sent in an Alert message
	// when FormMail requires the "PHP_SELF" server variable and PHP is not
	// providing it.
	// Parameters:
	//  none
	$aMessages[MSG_NO_PHP_SELF] = 'PHP on the server is not providing "PHP_SELF"';

	// MSG_RETURN_URL_INVALID is sent in an Alert message
	// when "this_form" is not a valid return URL.  This occurs for
	// multi-page forms.
	// Parameters:
	//  URL     the invalid URL
	$aMessages[MSG_RETURN_URL_INVALID] = 'Return URL "$URL" is not valid';

	// MSG_GO_BACK is sent in an Alert message
	// when "multi_go_back" has been submitted but this isn't part of a
	// multi-page form.
	// Parameters:
	//  none
	$aMessages[MSG_GO_BACK] = 'Cannot "go back" if not in a multi-page form ' .
	                          'sequence or at the first page of the form ' .
	                          'sequence';

	// MSG_OPEN_URL is sent in an Alert message when a URL cannot
	// be opened.
	// Parameters:
	//  URL     the invalid URL
	//  ERROR   error message
	$aMessages[MSG_OPEN_URL] = 'Cannot open URL "$URL": $ERROR';

	// MSG_CANNOT_RETURN is sent in an Alert message when an invalid return
	// request is made in a multi-page form sequence.
	// Parameters:
	//  TO          the requested page index
	//  TOPINDEX    the top page index
	$aMessages[MSG_CANNOT_RETURN] = 'Cannot return to page $TO.  The top page ' .
	                                'index is $TOPINDEX';

	// MSG_ATTACK_DETECTED is sent in an Alert message when an attack on
	// the server has been detected
	// Parameters:
	//  ATTACK      name or description of the attack
	//  INFO        more information about the attack
	$aMessages[MSG_ATTACK_DETECTED] = 'Server attack "$ATTACK" detected. ' .
	                                  'Your server is safe as FormMail is ' .
	                                  'invulnerable to this attack.  You can ' .
	                                  'disable these messages by setting ' .
	                                  'ALERT_ON_ATTACK_DETECTION to false ' .
	                                  'in FormMail\'s configuration section.' .
	                                  '\nMore information:\n$INFO';

	// MSG_ATTACK_PAGE is the contents of the browser page displayed to the
	// user when an attack is detected
	// Parameters:
	//  SERVER      the name of the server (website)
	//  USERINFO    details of the error
	$aMessages[MSG_ATTACK_PAGE] = 'Your form submission has been rejected ' .
	                              'as it appears to be an abuse of our server (' .
	                              '$SERVER).<br />' .
	                              'Our supplier of forms processing software has ' .
	                              'provided <a href="http://www.tectite.com/serverabuse.php" ' .
	                              ' target="_blank">more information about this error</a>.<br /><br />' .
	                              '$USERINFO';

	// MSG_ATTACK_MIME_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the MIME attack
	// Parameters:
	//  FLD     name of the field
	//  CONTENT the invalid content found in the field
	$aMessages[MSG_ATTACK_MIME_INFO] = 'The field "$FLD" contained invalid ' .
	                                   'content "$CONTENT"';

	// MSG_ATTACK_DUP_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the Duplicate Data attack
	// Parameters:
	//  FLD1     name of the first field
	//  FLD2     name of the second field
	$aMessages[MSG_ATTACK_DUP_INFO] = 'The fields "$FLD1" and "$FLD2" contained ' .
	                                  'duplicate data';

	// MSG_ATTACK_SPEC_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the Special Field attack
	// Parameters:
	//  FLD     name of the special field
	$aMessages[MSG_ATTACK_SPEC_INFO] = 'Special field "$FLD" contained an email address';

	// MSG_ATTACK_MANYURL_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the Many URLs attack
	// Parameters:
	//  FLD     name of the field
	//  NUM     number of URLs detected
	$aMessages[MSG_ATTACK_MANYURL_INFO] = 'Field "$FLD" contained $NUM URLs';

	// MSG_ATTACK_MANYFIELDS_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the Many Fields with URLs
	// attack
	// Parameters:
	//  NUM     number of fields detected with URLs
	//  FLDS    list of fields with URLs in them
	$aMessages[MSG_ATTACK_MANYFIELDS_INFO] = '$NUM fields contained URLs: $FLDS';

	// MSG_REV_CAP is an error regarding the setting of ATTACK_DETECTION_REVERSE_CAPTCHA
	// Parameters: none
	$aMessages[MSG_REV_CAP] = 'ATTACK_DETECTION_REVERSE_CAPTCHA is not set correctly, ' .
	                          'and will be ignored. Please refer to the documentation ' .
	                          'to make the correct setting.';

	// MSG_ATTACK_REV_CAP_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the Reverse Captcha attack
	// detection
	// Parameters:
	//  FLD     name of the field
	//  CONTENT the invalid content found in the field
	$aMessages[MSG_ATTACK_REV_CAP_INFO] = 'The field "$FLD" contained unexpected ' .
	                                      'content "$CONTENT".';

	// MSG_ATTACK_JUNK_INFO is the contents of the INFO parameter
	// to the MSG_ATTACK_DETECTED message for the JUNK attack
	// Parameters:
	//  FLD     name of the field
	//  JUNK    the junk that was found
	$aMessages[MSG_ATTACK_JUNK_INFO] = 'The field "$FLD" contained junk ' .
	                                   'data "$JUNK"';

	// MSG_ARESP_EMPTY is an alert message sent when
	// an autoresponse template or file is empty
	// Parameters:
	//  TYPE    the type of autoreponse requested
	$aMessages[MSG_ARESP_EMPTY] = 'The autoresponse is empty.  The form ' .
	                              'requested $TYPE';

	// MSG_NEED_SCRATCH_PAD is an alert message when processing requires
	// SCRATCH_PAD configuration for file upload processing.  This occurs
	// when you upload files in pages of a multi page form sequence.
	// Parameters:
	//  none
	$aMessages[MSG_NEED_SCRATCH_PAD] = 'You need to set SCRATCH_PAD in the ' .
	                                   'configuration section to process ' .
	                                   'uploaded files.';

	// MSG_OPEN_SCRATCH_PAD is an alert message when the SCRATCH_PAD
	// directory cannot be opened for cleanup.
	// Parameters:
	//  DIR     name of the directory
	//  ERR     more error information
	$aMessages[MSG_OPEN_SCRATCH_PAD] = 'Cannot open SCRATCH_PAD directory ' .
	                                   '"$DIR".  Open failed: $ERR';

	// MSG_NO_NEXT_NUM_FILE is an alert message when a form tries to
	// use the %nextnum% derivation feature but you haven't
	// setup FormMail's configuration correctly.
	// Parameters:
	//  none
	$aMessages[MSG_NO_NEXT_NUM_FILE] = 'You cannot use the %nextnum% feature: ' .
	                                   'you have not configured NEXT_NUM_FILE';

	// MSG_NEXT_NUM_FILE is an alert message when a form tries to
	// use the %nextnum% derivation feature but the next number file cannot
	// be processed.
	// Parameters:
	//  FILE    name of the file
	//  ACT     action
	//  ERR     more error information
	$aMessages[MSG_NEXT_NUM_FILE] = 'Cannot $ACT next number file ' .
	                                '\'$FILE\': $ERR';

	// MSG_MULTI_UPLOAD is an alert message when processing of uploaded
	// fails during a multi-page form sequence
	// Parameters:
	//  none
	$aMessages[MSG_MULTI_UPLOAD] = 'File upload processing failed during ' .
	                               'multi-page form processing.';

	// MSG_URL_PARSE is an error message when a URL to be opened
	// cannot be parsed
	// Parameters:
	//  none
	$aMessages[MSG_URL_PARSE] = 'Failed to parse URL';

	// MSG_URL_SCHEME is an error message when a URL to be opened
	// has an unsupported "scheme" value
	// Parameters:
	//  SCHEME     the scheme that was seen
	$aMessages[MSG_URL_SCHEME] = 'Unsupported URL scheme "$SCHEME"';

	// MSG_SOCKET is an error message when opening a socket for a URL
	// fails
	// Parameters:
	//  ERRNO     the error code
	//  ERRSTR    the error string
	//  PHPERR    the value of $php_errormsg
	$aMessages[MSG_SOCKET] = 'Socket error $ERRNO: $ERRSTR: $PHPERR';

	// MSG_GETURL_OPEN is an error message when the web server reports
	// a failure on opening a URL
	// Parameters:
	//  STATUS     the HTTP status value (number + string)
	$aMessages[MSG_GETURL_OPEN] = 'Open URL failed: $STATUS URL=$URL';

	// MSG_RESOLVE is an error message when a host name cannot be
	// resolved to an IP address
	// Parameters:
	//  NAME    the host name that could not be resolved
	$aMessages[MSG_RESOLVE] = 'Cannot resolve host name "$NAME"';

	// MSG_FORM_OK is the page title for the default
	// "thank you" page
	// Parameters:
	//  none
	$aMessages[MSG_FORM_OK] = 'Form Submission Succeeded';

	// MSG_FORM_ERROR is the page title for default
	// error pages
	// Parameters:
	//  none
	$aMessages[MSG_FORM_ERROR] = 'Form Submission Error';

	// MSG_GET_DISALLOWED is the message shown when GET method is used
	// but has been disabled.
	// Parameters:
	//  none
	$aMessages[MSG_GET_DISALLOWED] = 'GET method has been disabled.  Forms must use ' .
	                                 'the POST method. Alternatively, reconfigure ' .
	                                 'FormMail to allow the GET method.';

	// MSG_INVALID_SENDER is the alert message sent when an sender email address
	// has been specified that is not a known or valid email address
	// Parameters:
	//  LOC the location of the error
	//  EMAIL the invalid email address
	$aMessages[MSG_INVALID_SENDER] = 'The form has specified an invalid sender email address ($EMAIL) in $LOC.';

	// MSG_SET_SENDER_FROM_EMAIL is the configuration check message when SET_SENDER_FROM_EMAIL
	// has been set to true in the configuration section.  This feature is no longer available
	// as can lead to a security problem.
	// Parameters:
	//  none
	$aMessages[MSG_SET_SENDER_FROM_EMAIL] = 'SET_SENDER_FROM_EMAIL is no longer supported and will be ignored.';

	// MSG_FILE_UPLOAD_ERRn are the error messages corresponding to the
	// PHP file upload error code n.
	// Parameters:
	//  none
	$aMessages[MSG_FILE_UPLOAD_ERR1] = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
	$aMessages[MSG_FILE_UPLOAD_ERR2] = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the html form.';
	$aMessages[MSG_FILE_UPLOAD_ERR3] = 'The uploaded file was only partially uploaded.';
	$aMessages[MSG_FILE_UPLOAD_ERR4] = 'No file was uploaded.';
	$aMessages[MSG_FILE_UPLOAD_ERR6] = 'Missing a temporary folder.';
	$aMessages[MSG_FILE_UPLOAD_ERR7] = 'Failed to write file to disk.';
	$aMessages[MSG_FILE_UPLOAD_ERR8] = 'File upload stopped by extension.';

	// MSG_FILE_UPLOAD_ERR_UNK is displayed when an unknown error code
	// is provided by PHP for a file upload
	// Parameters:
	//  ERRNO   the error code
	$aMessages[MSG_FILE_UPLOAD_ERR_UNK] = 'Unknown file upload error code $ERRNO';

	// MSG_FILE_UPLOAD_SIZE is displayed when an uploaded file exceeds
	// the configured maximum size
	// Parameters:
	//  NAME   the uploaded file's name
	//  SIZE   the size of the uploaded file
	//  MAX    the maximum size that was exceeded
	$aMessages[MSG_FILE_UPLOAD_SIZE] = 'Uploaded file "$NAME" is too big (' .
	                                   '$SIZE bytes). The maximum permitted ' .
	                                   'size is $MAX kilobytes.';

	// MSG_POST_SIZE_LIMIT is displayed when a form submission is larger
	// than the configured maximum size in PHP
	// Parameters:
	//  NAME   the uploaded file's name
	//  SIZE   the size of the uploaded file
	//  MAX    the maximum size that was exceeded
	$aMessages[MSG_POST_SIZE_LIMIT] = 'Your form submission exceeds the server\'s configured size limit.';

	// MSG_DER_FUNC_ERROR is sent in an Alert message when
	// a form uses a derive_fields function that's
	// formatted incorrectly
	// Parameters:
	//  $SPEC   the invalid value specification
	//  $MSG    a message describing the error or providing an example
	$aMessages[MSG_DER_FUNC_ERROR] = 'derive_fields: invalid function specification ' .
	                                 '"$SPEC": $MSG';

	// MSG_DER_FUNC_SIZE_FMT describes the right syntax for the "size" function
	// Parameters:
	//  none
	$aMessages[MSG_DER_FUNC_SIZE_FMT] = '"size" function requires this format: ' .
	                                    'size(file_field)';

	// MSG_DER_FUNC_IF_FMT describes the right syntax for the "if" function
	// Parameters:
	//  none
	$aMessages[MSG_DER_FUNC_IF_FMT] = '"if" function requires this format: ' .
	                                  'if(field;spec;spec)';

	// MSG_DER_FUNC_NEXTNUM_FMT describes the right syntax for the "nextnum" function
	// Parameters:
	//  none
	$aMessages[MSG_DER_FUNC_NEXTNUM_FMT] = '"nextnum" function requires this format: ' .
	                                       'nextnum(pad) or nextnum(pad;base).  pad and base ' .
	                                       'must be numbers. base must be 2 to 36 inclusive';

	// MSG_DER_FUNC_EXT_FMT describes the right syntax for the "ext" function
	// Parameters:
	//  none
	$aMessages[MSG_DER_FUNC_EXT_FMT] = '"ext" function requires this format: ' .
	                                   'ext(file_field)';

	// MSG_DER_FUNC1_FMT describes the right syntax for a function
	// requiring one parameter
	// Parameters:
	//  FUNC    name of the function
	$aMessages[MSG_DER_FUNC1_FMT] = '"$FUNC" function requires this format: ' .
	                                '$FUNC(fieldname)';

	// MSG_DER_FUNC_SUBSTR_FMT describes the right syntax for the "substr" function
	// Parameters:
	//  none
	$aMessages[MSG_DER_FUNC_SUBSTR_FMT] = '"substr" function requires this format: ' .
	                                      'substr(fieldname;start) or ' .
	                                      'substr(fieldname;start;length) - ' .
	                                      'start and length must be numbers.';

	// MSG_USER_ATTACK_JUNK is a message shown to the user when a junk
	// attack has been detected
	// Parameters:
	//  INPUT   the data the user input
	$aMessages[MSG_USER_ATTACK_JUNK] = 'The following input looks like a junk attack ' .
	                                   'on our server.  Please avoid scientific ' .
	                                   'or technical terms with long sequences ' .
	                                   'of consonants or vowels: $INPUT';

	// MSG_USER_ATTACK_REV_CAP is a message shown to the user when a reverse
	// captcha attack has been detected
	// Parameters:
	//  none
	$aMessages[MSG_USER_ATTACK_REV_CAP] = 'Your input looks like an automated spambot ' .
	                                      'attacking our server.  Some automatic form ' .
	                                      'fillers can trigger this detection. Try ' .
	                                      'filling in our form manually. If you use the ' .
	                                      'back button to go back, make sure you ' .
	                                      'refresh the page before trying again.';

	// MSG_USER_ATTACK_DUP is a message shown to the user when a duplicate
	// data attack has been detected
	// Parameters:
	//  none
	$aMessages[MSG_USER_ATTACK_DUP] = 'You have input the same information in ' .
	                                  'several fields in the form. Please ' .
	                                  're-submit the form without duplication';

	// MSG_USER_ATTACK_MANY_URLS is a message shown to the user when a many urls
	// attack has been detected
	// Parameters:
	//  none
	$aMessages[MSG_USER_ATTACK_MANY_URLS] = 'Your input includes a number of URLs. ' .
	                                        'This server has been configured to reject ' .
	                                        'form submissions with too many URLs. ' .
	                                        'Please re-submit the form without URLs or ' .
	                                        'with fewer URLs.';

	// MSG_USER_ATTACK_MANY_URL_FIELDS is a message shown to the user when a many urls
	// attack has been detected
	// Parameters:
	//  none
	$aMessages[MSG_USER_ATTACK_MANY_URL_FIELDS] = $aMessages[MSG_USER_ATTACK_MANY_URLS];

	$aMessages[MSG_INVALID_EMAIL] = 'The email address "$EMAIL" is not valid: $REASON';
} // <A NAME="BuiltinMessages"> Jump to: <A HREF="#MessageNumbers">

//
// If the form submission was using the GET method, switch to the
// GET vars instead of the POST vars
//
if (isset($aServerVars["REQUEST_METHOD"]) && $aServerVars["REQUEST_METHOD"] === "GET") {
	$bIsGetMethod = true;
	if (Settings::get('ALLOW_GET_METHOD')) {
		$aFormVars = &$_GET;
	} elseif (count($_GET) > 0) {
		$bHasGetData = true;
	}
}

//
// Load the default language, and then override with an optional language file.
//
function LoadLanguage()
{
	LoadBuiltinLanguage();
	LoadLanguageFile();
}

//
// To return the value of a string or empty string if not set.
//
function CheckString($ss)
{
	return (isset($ss) ? $ss : "");
}

$aGetMessageSubstituteErrors   = array();
$aGetMessageSubstituteFound    = array();
$bGetMessageSubstituteNoErrors = false;

//
// Worker function for GetMessage's preg_replace_callback calls.
// Returns the value of the matched variable name.
// Variables are searched for in the global $aGetMessageValues.
// If no such variable exists, an empty string is returned and the
// global variable $aGetMessageSubstituteErrors lists the missing names.
//
function GetMessageSubstituteParam($a_matches)
{
	global $aGetMessageValues,$aGetMessageSubstituteErrors;
	global $aGetMessageSubstituteFound,$bGetMessageSubstituteNoErrors;

	$s_name                       = $a_matches[1];
	$aGetMessageSubstituteFound[] = $s_name;
	$s_value                      = "";
	if (isset($aGetMessageValues[$s_name])) {
		$s_value = $aGetMessageValues[$s_name];
	} elseif ($bGetMessageSubstituteNoErrors) {
		$s_value = '$' . $s_name;
	} else {
		$aGetMessageSubstituteErrors[] = $s_name;
	}
	return ($s_value);
}

//
// Returns message text from a message number, with optional parameters.
//
function GetMessage($i_msg_num,$a_params = array(),
                    $b_show_mnum = true,$b_no_errors = false)
{
	global $aMessages,$sLangID;

	if (count($aMessages) == 0) {
		LoadLanguage();
	}
	if (!isset($aMessages[$i_msg_num])) {
		SendAlert("Unknown Message Number $i_msg_num was used",false,true);
		$s_text = "<UNKNOWN MESSAGE NUMBER>";
	} else {
		$s_text = $aMessages[$i_msg_num];
	}
	$s_mno = Settings::get('bShowMesgNumbers') ? "[M$i_msg_num]" : "";

	$s_orig_text = $s_text;
	//
	// substitute parameters; only works with PHP version 4.0.5 or later
	//
	if (strpos($s_text,'$') !== false) {
		global $aGetMessageValues,$aGetMessageSubstituteErrors;
		global $aGetMessageSubstituteFound,$bGetMessageSubstituteNoErrors;

		$aGetMessageSubstituteErrors   = array();
		$aGetMessageSubstituteFound    = array();
		$aGetMessageValues             = HTMLEntitiesArray($a_params,true);
		$bGetMessageSubstituteNoErrors = $b_no_errors;
		$aGetMessageValues["MNUM"]     = $s_mno; // add the message number
		//
		// search for words in this form:
		//      $word
		// where word begins with an alphabetic character and
		// consists of alphanumeric and underscore
		//
		$s_text = preg_replace_callback('/\$([a-z][a-z0-9_]*)/i',
		                                'GetMessageSubstituteParam',$s_text);
		if (count($aGetMessageSubstituteErrors) > 0) {
			SendAlert("Message Number $i_msg_num ('$s_orig_text') in language $sLangID " .
			          "specified the following unsupported parameters: " .
			          implode(',',$aGetMessageSubstituteErrors));
		}
		if (!in_array("MNUM",$aGetMessageSubstituteFound))
			//
			// append the message number
			//
		{
			$s_text .= $b_show_mnum ? " $s_mno" : "";
		}
	} else
		//
		// append the message number
		//
	{
		$s_text .= $b_show_mnum ? " $s_mno" : "";
	}
	//
	// replace '\n' sequences with new lines
	//
	return (str_replace('\n',"\n",$s_text));
}

//
// Check if the server is Windows
//
function IsServerWindows()
{
	static $bGotAnswer = false;
	static $bAnswer;

	if (!$bGotAnswer) {
		if ((isset($_ENV["OS"]) && stristr($_ENV["OS"],"windows") !== false) ||
		    (isset($_SERVER["PATH"]) && stristr($_SERVER["PATH"],"winnt") !== false) ||
		    (isset($_SERVER["PATH"]) && stristr($_SERVER["PATH"],"windows") !== false) ||
		    (isset($_SERVER["SystemRoot"]) && stristr($_SERVER["SystemRoot"],"winnt") !== false) ||
		    (isset($_ENV["SystemRoot"]) && stristr($_ENV["SystemRoot"],"winnt") !== false) ||
		    (isset($_SERVER["SystemRoot"]) && stristr($_SERVER["SystemRoot"],"windows") !== false) ||
		    (isset($_ENV["SystemRoot"]) && stristr($_ENV["SystemRoot"],"windows") !== false) ||
		    (isset($_SERVER["Path"]) && stristr($_SERVER["Path"],"windows") !== false)
		) {
			$bAnswer = true;
		} else {
			$bAnswer = false;
		}
		$bGotAnswer = true;
	}
	return ($bAnswer);
}

//
// To return a temporary file name from $SCRATCH_PAD
//
function GetScratchPadFile($s_prefix)
{

	switch (substr(Settings::get('SCRATCH_PAD'),-1)) {
		case '/':
		case '\\':
			$s_dir = substr(Settings::get('SCRATCH_PAD'),0,-1);
			break;
		default:
			$s_dir = Settings::get('SCRATCH_PAD');
			break;
	}
	//
	// Ideally, we could use tempnam. But,
	// tempnam is system dependent and might not use the
	// SCRATCH_PAD directory even if we tell it to.
	// So, we'll force the file into SCRATCH_PAD.
	//
	// Note that we do *not* create the file, even though tempnam
	// does create it in PHP version 4.0.3 and above. (The reason is
	// we can't guarantee a non-race condition anyway.)
	//
	do {
		$i_rand = mt_rand(0,16777215); // 16777215 is FFFFFF in hex
		$s_name = $s_dir . "/" . $s_prefix . sprintf("%06X",$i_rand);
	} while (file_exists($s_name));
	return ($s_name);
}

//
// To return a temporary file name.
//
function GetTempName($s_prefix)
{
	if (!Settings::isEmpty('SCRATCH_PAD')) {
		$s_name = GetScratchPadFile($s_prefix);
	} else {
		$s_name = tempnam("/tmp",$s_prefix);
	}
	return ($s_name);
}

//
// To find a directory on the server for temporary files.
//
function GetTempDir()
{
	$s_name = GetTempName("fm");
	if (file_exists($s_name)) {
		unlink($s_name);
	}
	$s_dir = dirname($s_name);
	return ($s_dir);
}

define('DEBUG',false); // for production
//define('DEBUG',true);         // for development and debugging
define('RFCLINELEN',76); // recommend maximum line length from RFC 2822

//
// The user agent string to use when opening URLs
//
$sUserAgent = "FormMail/$FM_VERS (from www.tectite.com)";

if (DEBUG) {
	error_reporting(E_ALL); // trap everything!
	ini_set("display_errors","stdout");
	ini_set("display_startup_errors","1");
	assert_options(ASSERT_ACTIVE,true);
	assert_options(ASSERT_BAIL,true);
	LoadLanguage();
} else {
	$iOldLevel = error_reporting(E_ALL ^ E_WARNING);
	LoadLanguage();
	//
	// report everything except warnings and notices
	//
	error_reporting(E_ALL ^ E_WARNING ^ E_NOTICE);
}

function SetRealDocumentRoot()
{
	global $aServerVars,$REAL_DOCUMENT_ROOT;

	if (isset($aServerVars['SCRIPT_FILENAME'])) {
		$REAL_DOCUMENT_ROOT = $aServerVars['SCRIPT_FILENAME'];
	} elseif (isset($aServerVars['PATH_TRANSLATED'])) {
		$REAL_DOCUMENT_ROOT = $aServerVars['PATH_TRANSLATED'];
	} else {
		$REAL_DOCUMENT_ROOT = "";
	}
	//
	// look for 'www' or 'public_html' and strip back to that if found,
	// otherwise just get the directory name
	//
	if (($i_pos = strpos($REAL_DOCUMENT_ROOT,"/www/")) !== false) {
		$REAL_DOCUMENT_ROOT = substr($REAL_DOCUMENT_ROOT,0,$i_pos + 4);
	} elseif (($i_pos = strpos($REAL_DOCUMENT_ROOT,"/public_html/")) !== false) {
		$REAL_DOCUMENT_ROOT = substr($REAL_DOCUMENT_ROOT,0,$i_pos + 12);
	} elseif (!empty($REAL_DOCUMENT_ROOT)) {
		$REAL_DOCUMENT_ROOT = dirname($REAL_DOCUMENT_ROOT);
	} elseif (isset($aServerVars['DOCUMENT_ROOT']) &&
	          !empty($aServerVars['DOCUMENT_ROOT'])
	) {
		$REAL_DOCUMENT_ROOT = $aServerVars['DOCUMENT_ROOT'];
	}
}

//
// Hook system: before initialization (but after configuration)
//
if (($incFile = hookFileExists("fmhookpreinit.inc")) !== false) {
	@include($incFile);
}

if (!Settings::isEmpty('SESSION_NAME')) {
	session_name(Settings::get('SESSION_NAME'));
}

/**
 * Session data access
 *
 * @param $s_name
 *
 * @return mixed null session variable's value or null if not set
 */
function GetSession($s_name)
{
	return (isset($_SESSION) ? $_SESSION[$s_name] : null);
}

/**
 * Session data isset
 *
 * @param $s_name
 *
 * @return boolean true if the given session variable is set
 */
function IsSetSession($s_name)
{
	return (isset($_SESSION) && isset($_SESSION[$s_name]));
}

/**
 * Session data setting
 *
 * Sets the given session variable to the given value.
 *
 * @param $s_name  string
 *                 name of the session variable
 * @param $m_value mixed
 *                 value to set
 */
function SetSession($s_name,$m_value)
{
	$_SESSION[$s_name] = $m_value;
}

/**
 * Session data un-setting
 *
 * Unsets the given session variable.
 *
 * @param $s_name string
 *                name of the session variable
 */
function UnsetSession($s_name)
{
	$_SESSION[$s_name] = null;
	unset($_SESSION[$s_name]);
}

function ZapSession()
{
	global $aSessionVarNames;

	if (Settings::get('DESTROY_SESSION')) {
		if (session_id() != '') {
			session_destroy();
		}
	} else {
		foreach ($aSessionVarNames as $s_var_name) {
			UnsetSession($s_var_name);
		}
	}
}

$bReverseCaptchaCompleted = false; // records whether ATTACK_DETECTION_REVERSE_CAPTCHA has been completed successfully
session_start();

//
// Hook system: after session initialization
//
if (($incFile = hookFileExists("fmhookpostsess.inc")) !== false) {
	@include($incFile);
}

//
// This array lists the private variables used by FormMail.
// We use the names here to cleanup the session when FormMail has
// finished its processing.
//
$aSessionVarNames = array("FormError","FormErrorInfo","FormErrorCode",
                          "FormErrorItems","FormData","FormIsUserError",
                          "FormAlerted","FormSavedFiles","FormIndex",
                          "FormList","FormKeep","VerifyImgString",
                          "turing_string"
);

UnsetSession("FormError"); // start with no error
UnsetSession("FormErrorInfo"); // start with no error
UnsetSession("FormErrorCode"); // start with no error
UnsetSession("FormErrorItems"); // start with no error
UnsetSession("FormData"); // start with no data
UnsetSession("FormIsUserError"); // start with no data
UnsetSession("FormAlerted"); // start with no data

//
// Note that HTTP_REFERER is easily spoofed, so there's no point in
// using it for security.
//

//
// SPECIAL_FIELDS is the list of fields that formmail.php looks for to
// control its operation
//
$SPECIAL_FIELDS = array(
	"email", // email address of the person who filled in the form
	"realname", // the real name of the person who filled in the form
	"recipients", // comma-separated list of email addresses to which we'll send the results
	"cc", // comma-separated list of email addresses to which we'll CC the results
	"bcc", // comma-separated list of email addresses to which we'll BCC the results
	"replyto", // comma-separated list of email addresses to whom replies should be sent
	"required", // comma-separated list of fields that must be found in the input
	"conditions", // complex condition tests
	"fmcompute", // computations
	"fmmodules", // list of modules required
	"fmmode", // mode of operation
	"mail_options", // comma-separated list of options
	"good_url", // URL to go to on success
	"good_template", // template file to display on success
	"bad_url", // URL to go to on error
	"bad_template", // template file to display on error
	"template_list_sep", // separator when expanding lists in templates
	"this_form", // the URL of the form (can be used by bad_url)
	"subject", // subject for the email
	"env_report", // comma-separated list of environment variables to report
	"filter", // a supported filter to use
	"filter_options", // options for using the filter
	"filter_fields", // list of fields to filter (default is to filter all fields)
	"filter_files", // list of file fields to filter (default is to filter no file fields)
	"logfile", // log file to write to
	"csvfile", // file to write CSV records to
	"csvcolumns", // columns to save in the csvfile
	"crm_url", // URL for sending data to the CRM; note that the
	// value must have a valid prefix specified in TARGET_URLS
	"crm_spec", // CRM specification (field mapping)
	"crm_options", // comma-separated list of options to control CRM processing
	"derive_fields", // a list of fields to derive from other fields
	"file_names", // specifies names for files being uploaded
	"autorespond", // specification for auto-responding
	"arverify", // verification field to allow auto-responding
	"imgverify", // verification field to allow submission
	"multi_start", // set this field on the first page of a multi-page form sequence
	"multi_keep", // set this field on the pages of a multi-page form sequence
	// to the list of fields that should be kept when moving
	// forward after going backwards
	"next_form", // next form name or empty for last form
	"multi_go_back", // this field should be set when the user clicks the
	// back button or link in a multi-page form sequence
	"alert_to", // email address to send alerts (errors) to
	//
	// fields for reCaptcha implementation
	//
	"recaptcha_response_field", // verification field to allow submission
	"recaptcha_challenge_field", // challenge field
	//
	// reCaptcha version 2
	//
	"g-recaptcha-response",
	"email_validation_time_secs"        // set via the VALIDATE_EMAIL_DOMAIN process
);

//
// $SPECIAL_MULTI is the list of fields from $SPECIAL_FIELDS that can
// have multiple values, for example:
//      name="conditions1"
//      name="conditions2"
//
$SPECIAL_MULTI = array(
	"conditions",
	"fmcompute",
);

//
// $SPECIAL_ARRAYS is the list of fields from $SPECIAL_FIELDS that can
// be submitted as arrays of values, for example:
//      <select name="recipients[]">
//      <option value="sales">Sales</option>
//      <option value="service">Service</option>
//      </select>
//
$SPECIAL_ARRAYS = array(
	"recipients",
	"cc",
	"bcc",
	"replyto",
);

//
// $SPECIAL_NOSTRIP is the list of fields from $SPECIAL_FIELDS that
// should not be stripped (other than for magic_quotes_gpc reasons).
//
$SPECIAL_NOSTRIP = array(
	"conditions",
	"fmcompute",
	"recaptcha_response_field",
	"recaptcha_challenge_field",
	"g-recaptcha-response",
	"arverify",
	"imgverify",
);

//
// VALID_MAIL_OPTIONS lists the valid mail_options words
//
$VALID_MAIL_OPTIONS = array(
	"AlwaysEmailFiles" => true,
	"AlwaysList"       => true,
	"CharSet"          => true,
	"DupHeader"        => true,
	"Exclude"          => true,
	"FromAddr"         => true,
	"FromLineStyle"    => true,
	"HTMLTemplate"     => true,
	"KeepLines"        => true,
	"NoEmpty"          => true,
	"NoPlain"          => true,
	"PlainTemplate"    => true,
	"SendMailFOption"  => true,
	"StartLine"        => true,
	"TemplateMissing"  => true,
);

//
// VALID_CRM_OPTIONS lists the valid crm_options words
//
$VALID_CRM_OPTIONS = array(
	"ErrorOnFail" => true,
);

//
// VALID_AR_OPTIONS lists the valid autorespond words
//
$VALID_AR_OPTIONS = array(
	"Subject"         => true,
	"HTMLTemplate"    => true,
	"PlainTemplate"   => true,
	"TemplateMissing" => true,
	"PlainFile"       => true,
	"HTMLFile"        => true,
	"FromAddr"        => true,
	"FromName"        => true
);

//
// VALID_FILTER_OPTIONS lists the valid filter_options words
//
$VALID_FILTER_OPTIONS = array(
	"Attach"       => true,
	"KeepInLine"   => true,
	"CSVHeading"   => true,
	"CSVSep"       => true,
	"CSVIntSep"    => true,
	"CSVQuote"     => true,
	"CSVEscPolicy" => true,
	"CSVRaw"       => true,
);

//
// SPECIAL_VALUES is set to the value of the fields we've found
//  usage: $SPECIAL_VALUES["email"] is the value of the email field
//
$SPECIAL_VALUES = array();
//
// Array of mail options; set by the function 'ProcessMailOptions'
//
$MAIL_OPTS = array();
//
// Array of crm options; set by the function 'ProcessCRMOptions'
//
$CRM_OPTS = array();
//
// Array of autorespond options; set by the function 'ProcessAROptions'
//
$AR_OPTS = array();
//
// Array of filter options; set by the function 'ProcessFilterOptions'
//
$FILTER_OPTS = array();

//
// initialise $SPECIAL_VALUES so that we don't fail on using unset values
//
foreach ($SPECIAL_FIELDS as $sFieldName) {
	$SPECIAL_VALUES[$sFieldName] = "";
}

//
// Defaults for some special fields....
//
$SPECIAL_VALUES['template_list_sep'] = ",";

//
// FORMATTED_INPUT contains the input variables formatted nicely
// This is used for error reporting and debugging only.
//
$FORMATTED_INPUT = array();

//
// $FILTER_ATTRIBS_LOOKUP is the parsed $FILTER_ATTRIBS array
//
$FILTER_ATTRIBS_LOOKUP = array();

//
// BuiltinFunctions provides additional functions that can be called via derive_fields
//
class   BuiltinFunctions
{
	private $_aFunctions;

	function __construct()
	{
		$this->_aFunctions = array();
	}

	/**
	 * Adds a builtin function.
	 *
	 * @param string $s_name   name of the function
	 * @param string $n_params number of parameters the function expects
	 */
	public function Add($s_name,$n_params)
	{
		$this->_aFunctions[$s_name] = array('nparams' => $n_params);
	}

	/**
	 * Calls a builtin function.
	 *
	 * @param string  $s_name   name of the function
	 * @param array   $a_params list of parameters
	 * @param string &$s_result result of the function or error message
	 *
	 * @return boolean true on success, otherwise false
	 */
	public function Call($s_name,$a_params,&$s_result)
	{
		if (!isset($this->_aFunctions[$s_name])) {
			$s_result = "Function '$s_name' is not a builtin function";
			return (false);
		}
		$a_func = $this->_aFunctions[$s_name];
		if (count($a_params) != $a_func['nparams']) {
			$s_result = "Function '$s_name' expects " . $a_func['nparams'] . " parameters, " . count($a_params) .
			            " given.";
			return (false);
		}
		$s_result = call_user_func_array($s_name,$a_params);
		return (true);
	}
}

$BuiltinFunctions = new BuiltinFunctions();

$reCaptchaProcessor = null;
if (Settings::get('RECAPTCHA_PRIVATE_KEY') !== "") {
	//
	// Assume version 1 unless g-recaptcha-response is found in the form data.
	// If we cannot load recaptchalib, then we assume it's version 2.
	//
	$bRecaptchaVersion = 1;
	if (isset($aFormVars['g-recaptcha-response']) && $aFormVars['g-recaptcha-response'] != '') {
		$bRecaptchaVersion = 2;
	}
	if ($bRecaptchaVersion == 1 && (!includeFileExists("recaptchalib.php") || !@include_once("recaptchalib.php"))) {
		$bRecaptchaVersion = 2;
	}

	if ($bRecaptchaVersion == 2) {
		if (!function_exists('json_decode')) {
			SendAlert("reCaptcha version 2 requires PHP version 5.2.0 or later",false,false);
		}

		/*
		 * Class:       reCaptchaWrapper
		 * Description:
			 *  Wraps processing of reCaptcha version 2.
		 */

		class   reCaptchaWrapperV2
		{
			var $_sPrivate; // the private key
			var $_bDone; // true when done
			var $_Resp; // the response from reCaptcha

			/*
			 * Method:      reCaptchaWrapperV2 ctor
			 * Parameters:  $s_priv     the private key
			 * Returns:     n/a
			 * Description:
			 *  Initializes the wrapper ready to process reCaptcha.
			 */
			function __construct($s_priv)
			{
				$this->_sPrivate = $s_priv;
				$this->_bDone    = false;
			}

			/**
			 * Try to contact Google reCaptcha.
			 * PHP version 5.6.2 has problems with sockets, so this may fail with PHP 5.6.2.
			 *
			 * @param string $s_response the reCaptcha response.
			 */
			function _askGoogle($s_response)
			{
				$s_url = 'https://www.google.com/recaptcha/api/siteverify';

				$a_post_data = array(
					'secret'   => $this->_sPrivate,
					'response' => $s_response
				);
				if (isset($_SERVER['REMOTE_ADDR'])) {
					$a_post_data['remoteip'] = $_SERVER['REMOTE_ADDR'];
				}

				FMDebug('Posting to google reCaptcha');
				$recaptcha    = new HTTPPost($s_url);
				$a_resp_lines = $recaptcha->Post($a_post_data);

				if ($a_resp_lines === false) {
					FMDebug('reCaptcha via HTTPPost socket failed');
					$s_resp = '{"success":false,"error-codes":["reCaptcha failed"]}';
				} else {
					$s_resp = implode('',$a_resp_lines);
					FMDebug('reCaptcha via HTTPPost socket succeeded: ' . $s_resp);
				}
				/** @noinspection PhpComposerExtensionStubsInspection */
				$this->_Resp = json_decode($s_resp,true);
			}

			/*
			 * Method:      reCaptchaWrapper::Check
			 * Parameters:  $s_response the reCaptcha response value
			 *              $a_values   field values
			 *              $s_error    returns the reCaptcha error code
			 * Returns:     bool        true on success, otherwise false
			 * Description:
			 *  Performs the reCaptcha check and caches the result so it's
			 *  only done once.
			 */
			function Check($s_response,$a_values,&$s_error)
			{
				if (!$this->_bDone) {
					$this->_askGoogle($s_response);
				}
				$this->_bDone = true;
				$s_error      = "";
				if (!$this->_Resp['success']) {
					if (!isset($this->_Resp['error-codes']) || count($this->_Resp['error-codes']) == 0 ||
					    !$this->_Resp['error-codes'][0]
					) {
						$s_error = 'verification failed (error not specified)';
					} else {
						$s_error = $this->_Resp['error-codes'][0];
					}
				}
				return ($this->_Resp['success']);
			}
		}

		$reCaptchaProcessor = new reCaptchaWrapperV2(Settings::get('RECAPTCHA_PRIVATE_KEY'));
	} else {

		/*
		 * Class:       reCaptchaWrapper
		 * Description:
		 *  Wraps processing of reCaptcha version 1.
		 */

		class   reCaptchaWrapper
		{
			var $_sPrivate; // the private key
			var $_bDone; // true when done
			var $_Resp; // the response from reCaptcha

			/*
			 * Method:      reCaptchaWrapper ctor
			 * Parameters:  $s_priv     the private key
			 * Returns:     n/a
			 * Description:
			 *  Initializes the wrapper ready to process reCaptcha.
			 */
			function __construct($s_priv)
			{
				$this->_sPrivate = $s_priv;
				$this->_bDone    = false;
			}

			/*
			 * Method:      reCaptchaWrapper::Check
				 * Parameters:  $s_response the reCaptcha response value
			 *              $a_values   field values
			 *              $s_error    returns the reCaptcha error code
			 * Returns:     bool        true on success, otherwise false
			 * Description:
			 *  Performs the reCaptcha check and caches the result so it's
			 *  only done once.
			 */
			function Check($s_response,$a_values,&$s_error)
			{
				if (!$this->_bDone) {
					/** @noinspection PhpUndefinedFunctionInspection */
					$this->_Resp = recaptcha_check_answer($this->_sPrivate,
					                                      $_SERVER["REMOTE_ADDR"],
					                                      $a_values["recaptcha_challenge_field"],
					                                      $s_response);
				}
				$this->_bDone = true;
				$s_error      = "";
				if (!$this->_Resp->is_valid) {
					$s_error = $this->_Resp->error;
				}
				return ($this->_Resp->is_valid);
			}
		}

		$reCaptchaProcessor = new reCaptchaWrapper(Settings::get('RECAPTCHA_PRIVATE_KEY'));
	}
}

/**
 * Contains a list of valid email addresses and
 * email address patterns. Provides methods for checking the validity of an
 * email address.
 */
class EmailChecker
{

	var $_aAddresses; // valid email addresses (as keys)
	var $_aTargetPatterns; // valid email address patterns

	/**
	 * Constructs the object.
	 *
	 * @param array $a_aliases
	 * @param array $a_patterns an array of email address patterns
	 */
	function __construct($a_aliases = array(),$a_patterns = array())
	{
		$this->_aAddresses      = array();
		$this->_aTargetPatterns = $a_patterns;
		foreach ($a_aliases as $s_list) {
			$this->AddAddresses($s_list);
		}
	}

	/**
	 * Adds an email address to the list of valid * email addresses.
	 *
	 * @param string $s_addr an email address
	 */
	function AddAddress($s_addr)
	{
		$this->_aAddresses[$s_addr] = true;
	}

	/**
	 * Adds a comma-separated list of email addresses to the list of valid email addresses.
	 *
	 * @param string $s_list a list of email addresses
	 */
	function AddAddresses($s_list)
	{
		$a_addrs = TrimArray(explode(",",$s_list));
		foreach ($a_addrs as $s_addr) {
			$this->AddAddress($s_addr);
		}
	}

	/**
	 * Checks an email address for validity.
	 *
	 * @param string $s_email an email address
	 *
	 * @return boolean true if the address is valid, otherwise false
	 */
	function CheckAddress($s_email)
	{
		$b_is_valid = false;
		if (isset($this->_aAddresses[$s_email])) {
			$b_is_valid = true;
		} else {
			for ($ii = 0 ; $ii < count($this->_aTargetPatterns) ; $ii++) {
				//
				// prepend / with \
				//
				$s_pat = "/" . str_replace('/','\\/',$this->_aTargetPatterns[$ii]) . "/i";
				if (preg_match($s_pat,$s_email)) {
					$b_is_valid = true;
					break;
				}
			}
		}
		return ($b_is_valid);
	}
}

//
// Create the object for checking emails
//
$ValidEmails = new EmailChecker(Settings::get('EMAIL_ADDRS'),Settings::get('TARGET_EMAIL'));

/**
 * Class SpecialFieldsManager
 *
 * Encapsulate storage and access to special fields.
 *
 * NOTE: this is initial code implemented in version 9.17 and is not complete.
 * It's part of our transition to a more complete Object Oriented code base
 * which is targeted for version 9.??.
 */
class SpecialFieldsManager
{
	/**
	 * list of special field names
	 *
	 * @var array
	 */
	private $_aFieldNames;

	/**
	 * list of special field names that can have multiple values by a numeric suffic
	 *
	 * @var array
	 */
	private $_aMultiFieldNames;

	/**
	 * list of special field names that can be arrays
	 *
	 * @var array
	 */
	private $_aArrayFieldNames;

	/**
	 * list of special field names that must be used in raw format (not cleaned/stripped)
	 *
	 * @var array
	 */
	private $_aRawFieldNames;

	/**
	 * list of field values keyed by field name
	 *
	 * @var array
	 */
	private $_aValues;

	function __construct($a_field_names,$a_multi_field_names,$a_array_field_names,$a_raw_field_names)
	{
		$this->_aFieldNames      = $a_field_names;
		$this->_aMultiFieldNames = $a_multi_field_names;
		$this->_aArrayFieldNames = $a_array_field_names;
		$this->_aRawFieldNames   = $a_raw_field_names;
	}

	public static function getCleanEmail($s_email)
	{
		if (function_exists('filter_var')) {
			return filter_var($s_email,FILTER_VALIDATE_EMAIL) === false ? '' : $s_email;
		} else {
			// basic email pattern if filter_var is not available
			return preg_match('/^[a-z0-9]+@[a-z\.]+$/i',$s_email) ? $s_email : '';
		}
	}
}

// TODO implement this!
//$SpecialFields = new SpecialFieldsManager($SPECIAL_FIELDS,$SPECIAL_MULTI,$SPECIAL_ARRAYS,$SPECIAL_NOSTRIP);

/**
 * Class FieldManager
 *
 * Encapsulates storage and lookup of field data.
 *
 * NOTE: this is initial code implemented in version 8.27 and is not complete.
 * It's part of our transition to a more complete Object Oriented code base
 * which is targeted for version 9.??.
 */
class FieldManager
{

	/**
	 * list of fields keyed by field name
	 *
	 * @var array
	 */
	private $_aFields;

	/**
	 * list of file fields keyed by field name (not currently used)
	 *
	 * @var array
	 */
	private $_aFileFields;

	/**
	 * last array separator specified
	 *
	 * @var string
	 */
	private $_sArraySep;

	/**
	 * array separator to use (after substitutions)
	 *
	 * @var string
	 */
	private $_sArraySepValue;

	/**
	 * counter for unique generation
	 *
	 * @var int
	 */
	private $_nUnique;

	/**
	 * Constructs the object.
	 *
	 * @param array $a_fields      list of fields
	 * @param array $a_file_fields list of file fields
	 */
	public function __construct($a_fields = array(),$a_file_fields = array())
	{
		$this->_sArraySepValue = $this->_sArraySep = "";
		$this->_aFields        = $this->_aFileFields = array();
		$this->_nUnique        = 0;
		$this->Init($a_fields,$a_file_fields);
	}

	public function GetFields()
	{
		return $this->_aFields;
	}

	/**
	 * Initializes the object with the field data.
	 *
	 * @param array $a_fields      list of fields
	 * @param array $a_file_fields list of file fields
	 *
	 * @return void
	 */
	public function Init($a_fields = array(),$a_file_fields = array())
	{
		$this->_aFields     = $a_fields;
		$this->_aFileFields = $a_file_fields;
	}

	/**
	 * Return a field value.
	 * Empty string is returned if the field is
	 * not found. File fields return the original name of the uploaded file.
	 *
	 * @param string $s_fld       name of the field
	 * @param string $s_array_sep string to use to separate array values
	 *
	 * @return string the field's value
	 */
	public function GetFieldValue($s_fld,$s_array_sep = ";")
	{
		if (!isset($this->_aFields[$s_fld])) {
			if (($s_value = $this->GetFileName($s_fld)) === false) {
				$s_value = "";
			}
		} else {
			if (is_array($this->_aFields[$s_fld])) {
				$s_value = implode($this->_GetArraySep($s_array_sep),$this->_aFields[$s_fld]);
			} else {
				$s_value = (string)$this->_aFields[$s_fld];
			}
		}
		return ($s_value);
	}

	/**
	 * Return a field value.
	 * Empty string is returned if the field is
	 * not found. File fields return the original name of the uploaded file.
	 * The returned value is HTML-safe.
	 * b_text_subs performs text substitutions on the field value
	 * that are not affected by HTML-safety replacement. This means
	 *
	 * @param string $s_fld       name of the field
	 * @param bool   $b_text_subs perform text substitutions
	 * @param string $s_array_sep string to use to separate array values
	 *
	 * @return string the field's value
	 * @uses      $TEXT_SUBS can be used to force allowance of particular HTML tags. Note
	 *            that b_text_subs is not yet implemented for array * field
	 *            values.
	 *
	 */
	public function GetSafeFieldValue($s_fld,$b_text_subs = false,$s_array_sep = ";")
	{
		//
		// for array values, insert the array separator after making
		// the individual values HTML-safe
		// The equivalent logic up to and including version 8.24 used
		// htmlspecialchars not htmlentities.
		// The use of htmlentities broke UTF-8 template processing,
		// and this was reported in version 8.28.
		// By specifying the character set, we trigger the use of
		// htmlspecialchars
		// so the logic is equivalent to the old logic.
		//
		if (isset($this->_aFields[$s_fld]) && is_array($this->_aFields[$s_fld])) {
			$s_value = implode($this->_GetArraySep($s_array_sep),
			                   HTMLEntitiesArray($this->_aFields[$s_fld],false,
			                                     GetMailOption("CharSet")));
		} else {
			if (!isset($this->_aFields[$s_fld])) {
				if (($s_name = $this->GetFileName($s_fld)) === false) {
					$s_name = "";
				}
				$s_value = $s_name;
			} else {
				$s_value = (string)$this->_aFields[$s_fld];
			}
			if ($b_text_subs) {
				list ($s_value,$a_subs_data) = $this->_PrepareTextSubstitute($s_value);
			}
			$s_value = FixedHTMLEntities($s_value,GetMailOption("CharSet"));
			if ($b_text_subs) {
				$s_value = $this->_CompleteTextSubstitute($s_value,$a_subs_data);
			}
		}
		return ($s_value);
	}

	/**
	 * Prepares a value for text substitution using $TEXT_SUBS.
	 *
	 * @param string $s_value
	 *            the value to perform substitutions on
	 *
	 * @return array [0]=>the processed value, [1]=>array of substitution data
	 */
	private function _PrepareTextSubstitute($s_value)
	{
		$a_subs_data = array();
		$a_text_subs = Settings::get('TEXT_SUBS');
		for ($ii = 0 ; $ii < count($a_text_subs) ; $ii++) {
			$a_match_data = array();
			if (($n_matches = preg_match_all($a_text_subs[$ii]["srch"],$s_value,$a_matches,
			                                 PREG_OFFSET_CAPTURE)) !== false && $n_matches > 0
			) {
				$a_match_data["srch"] = $a_text_subs[$ii]["srch"];
				$a_match_data["repl"] = $a_text_subs[$ii]["repl"];
				$s_value              = $this->_HTMLSafeSubstitute($s_value,$a_matches,$a_match_data);
			}
			$a_subs_data[$ii] = $a_match_data;
		}
		return (array($s_value,$a_subs_data));
	}

	/**
	 * Completes text substitution started by _PrepareTextSubstitute.
	 *
	 * @param string $s_value
	 *            the value to perform substitutions on
	 * @param array  $a_subs_data
	 *            data that describes the substitutions to perform
	 *
	 * @return string the new value
	 */
	private function _CompleteTextSubstitute($s_value,$a_subs_data)
	{
		//
		// because later substitutions can capture earlier ones,
		// we have to process them all in reverse order
		//
		for ($ii = count($a_subs_data) ; --$ii >= 0 ;) {
			$a_subs_list = $a_subs_data[$ii];
			for ($jj = count($a_subs_list) ; --$jj >= 0 ;) {
				$s_code  = $a_subs_list[$jj]["code"];
				$s_subs  = $a_subs_list[$jj]["subs"];
				$s_value = str_replace($s_code,$s_subs,$s_value);
			}
		}
		return ($s_value);
	}

	/**
	 * Generates a unique string from a base string
	 *
	 * @param string $s_base
	 *            a base of the unique string
	 *
	 * @return string a unique string
	 */
	private function _MakeUniqueString($s_base)
	{
		$n_uniq = $this->_nUnique++;
		return ($s_base . "_" . str_pad("$n_uniq",5,"0",STR_PAD_LEFT));
	}

	/**
	 * Performs a temporary substitution on a string of the given matches with
	 * the given replacement specification.
	 * This makes the replacement using
	 * a special indicator string that can be substituted for the real value
	 * later. This allows non-replaced parts of the string to be processed
	 * and made safe for HTML entities, without affecting our actual
	 * replacements.
	 *
	 * @param string $s_value
	 *            the string to substitute
	 * @param array  $a_matches
	 *            list of matches and offsets from preg_match_all
	 * @param array  $a_match_data
	 *            contains some data, and returns replacement data for the
	 *            temporary substitution
	 *
	 * @return string the temporarily substituted string
	 */
	private function _HTMLSafeSubstitute($s_value,$a_matches,&$a_match_data)
	{
		$a_matches = $a_matches[0]; // we're only interested in the full pattern
		// matches
		$s_srch = $a_match_data["srch"];
		$s_repl = $a_match_data["repl"];

		//
		// to preserve offsets, we must process the string in reverse order
		// of the matches; since we don't assume the array is ordered
		// by ascending offset, we'll sort it now
		//
		usort($a_matches,function ($a,$b) {
			return $b[1] - $a[1];
		});
		$a_match_data = array();
		for ($ii = 0 ; $ii < count($a_matches) ; $ii++) {
			$s_match  = $a_matches[$ii][0];
			$i_offset = $a_matches[$ii][1];
			$i_len    = strlen($s_match);

			$s_subs = preg_replace($s_srch,$s_repl,$s_match);
			//
			// the code string must e HTML safe so it doesn't get altered
			// before we can replace it; we use ! at the edges so that
			// other patterns can successfully match word boundaries
			// An improvement would be to determine the type of characters
			// at the edges of the matched string, then choose the substitution
			// edges accordingly.
			//
			$s_code            = "!" . $this->_MakeUniqueString("SUBS") . "!";
			$a_match_data[$ii] = array("subs" => $s_subs,"code" => $s_code);

			$s_value = substr($s_value,0,$i_offset) . $s_code . substr($s_value,$i_offset + $i_len);
		}
		return ($s_value);
	}

	/**
	 * Test if a field is set in the $_aFields array or in the uploaded
	 * files.
	 *
	 * @param string $s_fld name of the field
	 *
	 * @return bool true if the field has a value
	 */
	public function IsFieldSet($s_fld)
	{
		if (isset($this->_aFields[$s_fld])) {
			return (true);
		}
		if (Settings::get('FILEUPLOADS')) {
			if (isset($this->_aFileFields[$s_fld])) {
				return (true);
			}
			if (IsSetSession("FormSavedFiles")) {
				$a_saved_files = GetSession("FormSavedFiles");
				if (isset($a_saved_files[$s_fld])) {
					return (true);
				}
			}
		}
		return (false);
	}

	public static function GetFileUploadErrorMesg($n_error)
	{
		$s_mesg = '';
		switch ($n_error) {
			case 0:     // no error
				break;
			case 1:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR1);
				break;
			case 2:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR2);
				break;
			case 3:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR3);
				break;
			case 4:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR4);
				break;
			case 6:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR6);
				break;
			case 7:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR7);
				break;
			case 8:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR8);
				break;
			default:
				$s_mesg = GetMessage(MSG_FILE_UPLOAD_ERR_UNK,array("ERRNO" => $n_error));
				break;
		}
		return $s_mesg;
	}

	/**
	 * Tests a field against the $_aFields array for emptiness.
	 * If the var isn't found there, then the POSTed files array is checked.
	 * Returns true if the field is empty (a specific error may
	 * be returned in the $s_mesg parameter).
	 *
	 * @param string $s_fld  name of the field
	 * @param string $s_mesg returns an error message, where possible
	 *
	 * @return bool true if the field is empty
	 */
	public function TestFieldEmpty($s_fld,&$s_mesg)
	{
		$s_mesg  = "";
		$b_empty = TRUE;
		if (!isset($this->_aFields[$s_fld])) {
			//
			// Each file var is an array with these elements:
			// "name" => The original name of the file on the client machine.
			// "type" => The mime type of the file, if the browser provided this
			// information.
			// "tmp_name" => The temporary filename of the file in which the
			// uploaded file was stored on the server.
			// "error" => The error code associated with this file upload.
			// NOTE: "error" was added in PHP 4.2.0
			// "size" => The size, in bytes, of the uploaded file.
			//
			// Error codes (the constants are only available from PHP 4.3.0 so
			// we have to use the raw numbers):
			// UPLOAD_ERR_OK
			// Value: 0; There is no error, the file uploaded with success.
			// UPLOAD_ERR_INI_SIZE
			// Value: 1; The uploaded file exceeds the upload_max_filesize
			// directive in php.ini.
			// UPLOAD_ERR_FORM_SIZE
			// Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive
			// that was specified in the html form.
			// UPLOAD_ERR_PARTIAL
			// Value: 3; The uploaded file was only partially uploaded.
			// UPLOAD_ERR_NO_FILE
			// Value: 4; No file was uploaded.
			//
			if (Settings::get('FILEUPLOADS')) {
				if (IsSetSession("FormSavedFiles")) {
					$a_saved_files = GetSession("FormSavedFiles");
					if (isset($a_saved_files[$s_fld])) {
						$a_upload = $a_saved_files[$s_fld];
					} elseif (isset($this->_aFileFields[$s_fld])) {
						$a_upload = $this->_aFileFields[$s_fld];
					}
				} elseif (isset($this->_aFileFields[$s_fld])) {
					$a_upload = $this->_aFileFields[$s_fld];
				}
			}
			if (isset($a_upload)) {
				if (isset($a_upload["tmp_name"]) && !empty($a_upload["tmp_name"]) &&
				    isset($a_upload["name"]) && !empty($a_upload["name"])
				) {
					if ($this->_IsUploadedFile($a_upload)) {
						$b_empty = false;
					}
				}
				if ($b_empty && isset($a_upload["error"])) {
					$s_mesg = FieldManager::GetFileUploadErrorMesg($a_upload["error"]);
				}
			}
		} else {
			$b_empty = FieldManager::IsEmpty($this->_aFields[$s_fld]);
		}
		return ($b_empty);
	}

	/**
	 * Same as "empty" but checks for true emptiness if ZERO_IS_EMPTY is
	 * set to false.
	 *
	 * @param string $s_value the value to test
	 *
	 * @return bool true if the value is "empty"
	 */
	public static function IsEmpty($s_value)
	{
		if (Settings::get('ZERO_IS_EMPTY') || is_array($s_value)) {
			return (empty($s_value));
		} else {
			return ($s_value === "");
		}
	}

	/**
	 * Makes substitutions on strings as specified in the configuration setting $TEXT_SUBS.
	 *
	 * @param string $s_str a string on which to perform substitutions
	 *
	 * @return string the string, substituted
	 */
	public static function Substitute($s_str)
	{

		$a_srch = $a_repl = array();
		foreach (Settings::get('TEXT_SUBS') as $a_sub) {
			if (isset($a_sub["srch"]) && isset($a_sub["repl"]) && $a_sub["srch"] !== "") {
				$a_srch[] = $a_sub["srch"];
				$a_repl[] = $a_sub["repl"];
			}
		}
		return (preg_replace($a_srch,$a_repl,$s_str));
	}

	/**
	 * Computes the array separation string.
	 * $s_sep is subject to
	 * substitutions specified in $TEXT_SUBS, and then returned.
	 *
	 * @param string $s_sep the string specified for array separations
	 *
	 * @return string the string to use for array separations
	 */
	private function _GetArraySep($s_sep)
	{
		//
		// check for cached (previously calculated) value
		//
		if ($s_sep !== $this->_sArraySep) {
			$this->_sArraySep      = $s_sep;
			$this->_sArraySepValue = FieldManager::Substitute($this->_sArraySep);
		}
		return ($this->_sArraySepValue);
	}

	/**
	 * Return the info for the uploaded file, or false on error.
	 *
	 * @param string $s_fld field name
	 *
	 * @return bool
	 */
	public function GetFileInfo($s_fld)
	{
		if (Settings::get('FILEUPLOADS')) {
			//
			// Must look at new file uploads first.
			//
			if (isset($this->_aFileFields[$s_fld]) && !empty($this->_aFileFields[$s_fld])) {
				$a_upload = $this->_aFileFields[$s_fld];
			} elseif (IsSetSession("FormSavedFiles")) {
				$a_saved_files = GetSession("FormSavedFiles");
				if (isset($a_saved_files[$s_fld])) {
					$a_upload = $a_saved_files[$s_fld];
				}
			}
		}
		if (isset($a_upload)) {
			if (isset($a_upload["tmp_name"]) && !empty($a_upload["tmp_name"]) &&
			    isset($a_upload["name"]) && !empty($a_upload["name"]) &&
			    $this->_IsUploadedFile($a_upload)
			) {
				return ($a_upload);
			}
		}
		return (false);
	}

	/**
	 * Return the original name of the uploaded file or false on error.
	 *
	 * @param string $s_fld field name
	 *
	 * @return bool
	 * @see \FormMail\UnitTests\GetFileNameTest
	 */
	public function GetFileName($s_fld)
	{
		if (($a_upload = $this->GetFileInfo($s_fld)) !== false) {
			return ($a_upload["name"]);
		}
		return (false);
	}

	/**
	 * Check if a file is a valid uploaded file.
	 *
	 * @param array $a_file_spec file specification
	 *
	 * @return bool
	 */
	public static function IsUploadedFile($a_file_spec)
	{
		//
		// $a_file_spec["moved"] is our own internal flag to say we've
		// saved the file
		//
		if (isset($a_file_spec["moved"]) && $a_file_spec["moved"]) {
			return (true);
		}
		return (is_uploaded_file($a_file_spec["tmp_name"]));
	}

	/**
	 * Internal access to static method.  This allows us to mock the operation in unit tests.
	 *
	 * @param $a_file_spec
	 *
	 * @return bool
	 */
	protected function _IsUploadedFile($a_file_spec)
	{
		return self::IsUploadedFile($a_file_spec);
	}

}

//
// Perform general line folding.
// This function can be used for RFC 2822 line folding, as well
// Quoted Printable soft line breaks (RFC 2045).
// $s_before lists the characters before which we should fold the line.
// $s_after lists the characters after which we should fold the line.
// $s_fold is the string to insert to fold the line.
//
function LineFolding($s_str,$i_max_line,$s_before,$s_after,$s_fold)
{
	$i_str_len  = strlen($s_str);
	$ii         = $i_start = 0;
	$i_line_len = 0;
	while ($ii < $i_str_len) {
		if ($i_line_len == $i_max_line) {
			//
			// fold this line:
			// search backwards for a character at which we can
			// fold the line
			//
			$b_done = false;
			for ($jj = $ii ; !$b_done && $jj > $i_start ; $jj--) {
				$b_found = false;
				if (strpos($s_before,$s_str[$jj]) !== false) {
					//
					// fold before this character
					//
					$b_found = true;
				} elseif (strpos($s_after,$s_str[$jj]) !== false) {
					//
					// fold after this character
					//
					$jj++;
					$b_found = true;
				}
				if ($b_found) {
					$s_str      = substr($s_str,0,$jj) . $s_fold . substr($s_str,$jj);
					$i_fold_len = strlen($s_fold);
					$i_str_len  += $i_fold_len; // the additional chars we inserted
					$i_start    = $jj + $i_fold_len; // start of the next line
					$b_done     = true;
				}
			}
			//
			// if we cannot fold and shorten the line,
			// ignore this and try for the next line
			//
			if ($b_done) {
				$ii = $i_start;
			} else {
				$i_start = $ii;
			}
			$i_line_len = 0;
		} elseif (substr($s_str,$ii,2) == "\r\n") {
			//
			// end of line found - reset counters
			//
			$i_line_len = 0;
			$ii         += 2;
			$i_start    = $ii;
		} else {
			$ii++;
			$i_line_len++;
		}
	}
	return ($s_str);
}

//
// Quoted Printable Encoding with soft line breaks.
//
// Process a string to fit the requirements of RFC2045 section 6.7. Note that
// this works, but replaces more characters than the minimum set.
// Prior to version 8.34, for readability the spaces were not encoded, which was
// WRONG (see http://tools.ietf.org/html/rfc2047#section-2).  Spaces must
// be encoded.
//
// Adapted from:
//  http://www.php.net/manual/en/function.quoted-printable-decode.php
// Note that we *must* split long lines because a QP string might not
// contain any Folding White Space (FWS). In this case, it would
// not be possible to fold the line according to RFC 2822.
// Therefore, we need to use Soft Line Breaks as defined by the
// Quoted Printable definition in RFC 2045.
//
// Set $i_max_line to -ve to skip the line folding.
//
function QPEncode($s_str,$i_max_line)
{
	//
	// According to RFC2045 section 6.7 point (4), we need to keep any
	// actual \r\n breaks encoded in the QP.  The original code replaced
	// them with CRLF pairs.
	//
	$s_str = str_replace('%','=',rawurlencode($s_str));
	if ($i_max_line < 0) {
		return ($s_str);
	} else {
		$s_before = "="; // characters before which we can fold the line
		return (LineFolding($s_str,$i_max_line,$s_before,"","=\r\n"));
	}
}

//
// Peform header line folding according to RFC 2822.
// $s_before lists the characters before which we should fold the line.
// $s_after lists the characters after which we should fold the line.
// Characters left out of folding:
//      [] are part of no-fold-literal
//      () are part of comments, and should work, but don't
//
function HeaderFolding($s_str,$i_max_line = RFCLINELEN,$s_before = "<",$s_after = ">;, ")
{
	return (LineFolding($s_str,$i_max_line,$s_before,$s_after,"\r\n "));
}

//
// Access the www.tectite.com website to get the current version.
//
function CheckVersion()
{
	global $FM_VERS,$php_errormsg;

	$http_get     = new HTTPGet("http://www.tectite.com/fmversion.txt");
	$php_errormsg = ""; // clear this out in case we get an error that doesn't set it
	FMDebug("CheckVersion");
	if (($a_lines = $http_get->Read()) !== false) {
		//
		// version file looks like this:
		//      Version=versionumber
		//      Message=a message to send in the alert
		//
		$s_version = "";
		$s_message = "";
		$s_line    = "";
		$b_in_mesg = false;
		foreach ($a_lines as $s_line) {
			if ($b_in_mesg) {
				$s_message .= $s_line;
			} else {
				$s_prefix = substr($s_line,0,8);
				if ($s_prefix == "Message=") {
					$s_message .= substr($s_line,8);
					$b_in_mesg = true;
				} elseif ($s_prefix == "Version=") {
					$s_version = substr($s_line,8);
				}
			}
		}
		$s_version   = str_replace("\r","",$s_version);
		$s_version   = str_replace("\n","",$s_version);
		$s_stop_mesg = GetMessage(MSG_END_VERS_CHK);
		FMDebug("CheckVersion: vers=$s_version");
		if ((float)$s_version > (float)$FM_VERS) {
			SendAlert(GetMessage(MSG_VERS_CHK,array(
				          "TECTITE" => "www.tectite.com",
				          "FM_VERS" => "$FM_VERS",
				          "NEWVERS" => $s_version,
			          )) .
			          "\n$s_message\n$s_stop_mesg",true,true);
		}
	}
}

//
// Check for new FormMail version
//
function Check4Update($s_chk_file,$s_id = "")
{
	global $lNow,$php_errormsg;

	@   $l_last_chk = filemtime($s_chk_file);
	if ($l_last_chk === false || $lNow - $l_last_chk >= (Settings::get('CHECK_DAYS') * 24 * 60 * 60)) {
		CheckVersion();
		//
		// update the check file's time stamp
		//
		@   $fp = fopen($s_chk_file,"w");
		if ($fp !== false) {
			fwrite($fp,"FormMail version check " .
			           (empty($s_id) ? "" : "for identifier '$s_id' ") .
			           "at " . date("H:i:s d-M-Y",$lNow) . "\n");
			fclose($fp);
		} else {
			SendAlert(GetMessage(MSG_CHK_FILE_ERROR,array("FILE"  => $s_chk_file,
			                                              "ERROR" => CheckString($php_errormsg)
			)));
		}
	}
}

//
// Perform various processing at the end of the script's execution.
//
function OnExit()
{
	FMDebug("OnExit");

	//
	// Check the www.tectite.com website for a new version, but only
	// do this check once every CHECK_DAYS days (or on server reboot).
	//
	if (Settings::get('CHECK_FOR_NEW_VERSION')) {
		global $SERVER;

		$a_targets = Settings::get('TARGET_EMAIL');
		if (isset($a_targets[0])) {
			//
			// use the first few characters of the MD5 of first email
			// address pattern from $TARGET_EMAIL to get a unique file
			// for the server
			//
			$s_id = "";
			if (isset($SERVER) && !empty($SERVER)) {
				$s_id = $SERVER;
			}
			$s_dir      = GetTempDir();
			$s_md5      = md5($a_targets[0]);
			$s_uniq     = substr($s_md5,0,6);
			$s_chk_file = "fm" . "$s_uniq" . ".txt";
			Check4Update($s_dir . "/" . $s_chk_file,$s_id);
		}
	}
}

register_shutdown_function('OnExit');

//
// Return the array with each string processed by htmlentities
//
function HTMLEntitiesArray($a_array,$b_equals_processing = false,$s_charset = NULL)
{
	foreach ($a_array as $m_key => $s_str) {
		//
		// only encode the value after the '='
		//
		if ($b_equals_processing && ($i_pos = strpos($s_str,'=')) !== false) {
			$a_array[$m_key] = substr($s_str,0,$i_pos + 1) .
			                   FixedHTMLEntities(substr($s_str,$i_pos + 1),$s_charset);
		} else {
			$a_array[$m_key] = FixedHTMLEntities($s_str,$s_charset);
		}
	}
	return ($a_array);
}

//
// Unfortunately, htmlentities (in some versions of PHP) gets
// some characters wrong and converts them even when the
// charset is provided.
// This function overcomes this problem.
//
function FixedHTMLEntities($s_str,$s_charset = NULL)
{
	global $sHTMLCharSet;

	if (isset($s_charset) && $s_charset != "") {
		return (htmlspecialchars($s_str,ENT_COMPAT,$s_charset));
	}
	if (isset($sHTMLCharSet) && $sHTMLCharSet != "") {
		return (htmlspecialchars($s_str,ENT_COMPAT,$sHTMLCharSet));
	}
	return (htmlentities($s_str));
}

//
// Return the array with each string urlencode'd.
//
function URLEncodeArray($a_array)
{
	foreach ($a_array as $m_key => $s_str) {
		//
		// only encode the value after the '='
		//
		if (($i_pos = strpos($s_str,'=')) !== false) {
			$a_array[$m_key] = substr($s_str,0,$i_pos + 1) .
			                   urlencode(substr($s_str,$i_pos + 1));
		} else {
			$a_array[$m_key] = urlencode($s_str);
		}
	}
	return ($a_array);
}

//
// Performs charset encoding for header line text.
// This operates according to RFC 2047, but without
// imposing the 75 character limit on an encoding.
// I haven't implemented that because of all the dramas
// with *trying* to obey the header line length rules that
// don't seem to work with PHP, the MTA, and email clients.
//
function EncodeHeaderText($s_text,$i_max_line = -1)
{
	global $sHTMLCharSet;

	//
	// RFCLINELEN is the RFC recommended maximum line length, but we don't know
	// what the front part of the line will be at this point.
	// So, we'll be conservative and reduce it.
	//
	if ($i_max_line == 0) {
		$i_max_line = RFCLINELEN - 15;
	}
	$s_charset = "";
	if (isset($sHTMLCharSet) && $sHTMLCharSet != "") {
		$s_charset = $sHTMLCharSet;
	} else {
		if (IsMailOptionSet("CharSet")) {
			$s_charset = GetMailOption("CharSet");
		}
	}
	if ($s_charset != "") {
		//
		// this is the (unused) code for base64 encoding.
		// quoted printable is a better choice for human readability
		//
		//return ("=?".$s_charset."?B?".base64_encode($s_text)."?=");
		$s_prefix = "=?" . $s_charset . "?Q?";
		$s_suffix = "?=";
		//
		// pick a line length that allows a line split with the prefix or suffix added
		// to be within the RFC maximum recommended line length
		//
		return ($s_prefix . QPEncode($s_text,$i_max_line - strlen($s_prefix)) . $s_suffix);
	} else {
		return ($s_text);
	}
}

//
// Add a parameter or list of parameters to a URL.
//
function AddURLParams($s_url,$m_params,$b_encode = true)
{
	if (!empty($m_params)) {
		if (!is_array($m_params)) {
			$m_params = array($m_params);
		}
		$s_anchor = "";
		if (($i_pos = strpos($s_url,'#')) !== false) {
			//
			// extract the anchor
			//
			$s_anchor = substr($s_url,$i_pos);
			$s_url    = substr($s_url,0,$i_pos);
		}
		if (strpos($s_url,'?') === false) {
			$s_url .= '?';
		} else {
			$s_url .= '&';
		}
		$s_url .= implode('&',($b_encode) ? URLEncodeArray($m_params) : $m_params);
		if ($s_anchor !== "") {
			$s_url .= "$s_anchor";
		}
	}
	return ($s_url);
}

//
// Recursively trim an array of strings (non string values are converted
// to a string first).
//
function TrimArray($a_list)
{
	foreach ($a_list as $m_key => $m_item) {
		if (is_array($m_item)) {
			$a_list[$m_key] = TrimArray($m_item);
		} elseif (is_scalar($m_item)) {
			$a_list[$m_key] = trim("$m_item");
		} else {
			$a_list[$m_key] = "";
		}
	}
	return ($a_list);
}

//
// Parse a derivation specification and return an array of
// field names and operators.
//
function ParseDerivation($s_fld_spec,$s_name,&$a_errors)
{
	$a_deriv = array();
	while (($i_len = strlen($s_fld_spec)) > 0) {
		//
		// we support the following operators:
		//      +   concatenate with a single space between, but skip the space
		//          if the next field is empty
		//      *   concatenate with a single space between
		//      .   concatenate with no space between
		//
		$i_span = strcspn($s_fld_spec,'+*.');
		if ($i_span == 0) {
			$a_errors[] = $s_name;
			return (false);
		}
		$a_deriv[] = trim(substr($s_fld_spec,0,$i_span));
		if ($i_span < $i_len) {
			$a_deriv[]  = substr($s_fld_spec,$i_span,1);
			$s_fld_spec = substr($s_fld_spec,$i_span + 1);
		} else {
			$s_fld_spec = "";
		}
	}
	return ($a_deriv);
}

//
// Test if a character is an alphabetic.
//
function IsAlpha($ch)
{
	return ($ch !== "" && strpos("abcdefghijklmnopqrstuvwxyz",strtolower($ch)) !== false);
}

//
// Test if a character is a digit.
//
function IsNumeric($ch)
{
	return ($ch !== "" && strpos("0123456789",$ch) !== false);
}

//
// Test if a character is an alphanumeric
//
function IsAlnum($ch)
{
	return (IsAlpha($ch) || IsNumeric($ch));
}

/**
 * Test if there is a number at the current position in a string.
 * (I had to write this because Badoo SoftMocks was crashing on the original
 * complex expression.)
 *
 * @param string $s_str the string
 * @param int    $i_pos the current position in the string
 * @param int    $i_len the length of the string
 *
 * @return bool
 */
function IsANumber($s_str,$i_pos,$i_len)
{
	$ch = $s_str[$i_pos];
	if (IsNumeric($ch)) {
		return true;
	}
	if ($ch == ".") {
		if (($i_pos + 1) <= ($i_len - 1)) {
			$ch = $s_str[$i_pos + 1];
			return IsNumeric($ch);
		}
	}
	return false;
}

//
// Return an array of tokens extracted from the given string.
// A token is:
//  - a word (begins with alpha or _, and is followed by any number
//    of alphanumerics or _ chars)
//  - a number (any number of consecutive digits with up to one period)
//  - a string enclosed in specified quotes (this can be disabled)
//  - any punctuation character
//
// Note that no string escapes (e.g. backslashes) are supported
// Anything not matching the above is silently ignored!
//
function GetTokens($s_str,$s_quotes = "'\"")
{
	$b_allow_strings = $s_quotes !== "";
	$ii              = 0;
	$i_len           = strlen($s_str);
	$a_toks          = array();

	while ($ii < $i_len) {
		switch ($ch = $s_str[$ii]) {
			case " ":
			case "\t":
			case "\n":
			case "\r":
				$ii++;
				continue 2;
		}
		//
		// start of a token
		//
		$i_start = $ii;
		if ($ch == "_" || IsAlpha($ch)) {
			//
			// a word
			//
			$i_count = 1;
			while (++$ii < $i_len &&
			       (($ch = $s_str[$ii]) == "_" || IsAlnum($ch))) {
				++$i_count;
			}
			$a_toks[] = substr($s_str,$i_start,$i_count);
		} elseif (IsANumber($s_str,$ii,$i_len)) {
			// note: $ch == $s_str[$ii]
			//
			// a number
			//
			$b_had_dot = ($ch == ".");
			$i_count   = 1;
			while (++$ii < $i_len) {
				$c_check = $s_str[$ii];
				if (IsNumeric($c_check)) {
					++$i_count;
				} elseif ($c_check == "." && !$b_had_dot) {
					++$i_count;
					$b_had_dot = true;
				} else {
					break;
				}
			}
			$a_toks[] = substr($s_str,$i_start,$i_count);
		} elseif ($b_allow_strings && strpos($s_quotes,$ch) !== false) {
			$c_quote = $ch;
			//
			// a quoted string
			//
			while (++$ii < $i_len) {
				if ($s_str[$ii] == $c_quote) {
					++$ii; // include the terminating quote
					break;
				}
			}
			$a_toks[] = substr($s_str,$i_start,$ii - $i_start);
		} else {
			$s_punct = "~!@#$%^&*()-+={}[]|:;<>,.?/`\\";
			if (!$b_allow_strings) {
				$s_punct .= "'\"";
			}
			if (strpos($s_punct,$ch) !== false) {
				$a_toks[] = $ch;
			}
			++$ii;
		}
	}
	return ($a_toks);
}

/**
 * Return the value from a derive_fields specification.
 * Specifications are in this format:
 * %info%
 * where info is a predefined word or a literal in quotes
 * (e.g. 'the time is ')
 *
 * @param string       $s_spec    the specification
 * @param FieldManager $field_mgr form fields manager
 * @param array        $a_errors  returns the list of errors
 *
 * @return string
 * @see \FormMail\UnitTests\ValueSpecTest
 */
function ValueSpec($s_spec,$field_mgr,&$a_errors)
{
	global $lNow;

	$s_value = "";
	switch (trim($s_spec)) {
		case 'date': // "standard" date format: DD-MMM-YYYY
			$s_value = date('d-M-Y',$lNow);
			break;
		case 'time': // "standard" time format: HH:MM:SS
			$s_value = date('H:i:s',$lNow);
			break;
		case 'ampm': // am or pm
			$s_value = date('a',$lNow);
			break;
		case 'AMPM': // AM or PM
			$s_value = date('A',$lNow);
			break;
		case 'dom0': // day of month with possible leading zero
			$s_value = date('d',$lNow);
			break;
		case 'dom': // day of month with no leading zero
			$s_value = date('j',$lNow);
			break;
		case 'day': // day name (abbreviated)
			$s_value = date('D',$lNow);
			break;
		case 'dayname': // day name (full)
			$s_value = date('l',$lNow);
			break;
		case 'daysuffix': // day number suffix for English (st for 1st, nd for 2nd, etc.)
			$s_value = date('S',$lNow);
			break;
		case 'moy0': // month of year with possible leading zero
			$s_value = date('m',$lNow);
			break;
		case 'moy': // month of year with no leading zero
			$s_value = date('n',$lNow);
			break;
		case 'month': // month name (abbreviated)
			$s_value = date('M',$lNow);
			break;
		case 'monthname': // month name (full)
			$s_value = date('F',$lNow);
			break;
		case 'year': // year (two digits)
			$s_value = date('y',$lNow);
			break;
		case 'fullyear': // year (full)
			$s_value = date('Y',$lNow);
			break;
		case 'rfcdate': // date formatted according to RFC 822
			$s_value = date('r',$lNow);
			break;
		case 'tzname': // timezone name
			$s_value = date('T',$lNow);
			break;
		case 'tz': // timezone difference from Greenwich +NNNN or -NNNN
			$s_value = date('O',$lNow);
			break;
		case 'hour120': // hour of day (01-12) with possible leading zero
			$s_value = date('h',$lNow);
			break;
		case 'hour240': // hour of day (00-23) with possible leading zero
			$s_value = date('H',$lNow);
			break;
		case 'hour12': // hour of day (1-12) with no leading zero
			$s_value = date('g',$lNow);
			break;
		case 'hour24': // hour of day (0-23) with no leading zero
			$s_value = date('G',$lNow);
			break;
		case 'min': // minute of hour (00-59)
			$s_value = date('i',$lNow);
			break;
		case 'sec': // seconds of minute (00-59)
			$s_value = date('s',$lNow);
			break;
		default:
			if ($s_spec[0] == "'") {
				//
				// to get a quote, use 3 quotes:
				//      '''
				//
				if ($s_spec == "'''") {
					$s_value = "'";
				} elseif (substr($s_spec,-1,1) == "'") {
					$s_value = substr($s_spec,1,-1);
				} else
					//
					// missing final quote is OK
					//
				{
					$s_value = substr($s_spec,1);
				}
			} elseif (strspn($s_spec,"0123456789ABCDEF") == 2) {
				//
				// insert the ASCII character corresponding to
				// the hexadecimal value
				//
				$i_val   = intval(substr($s_spec,0,2),16);
				$s_value = chr($i_val);
			} else {
				//
				// look for supported functions, start by getting all
				// the tokens
				//
				$a_toks = GetTokens($s_spec);
				if (count($a_toks) > 0) {
					switch ($a_toks[0]) {
						case "if":
							//
							// "if" function: test first field
							//       if not empty, then use second field
							//       else, use third field
							//
							// Example: if(fld1 ; fld2 ; fld3)
							//
							// tokens are:
							//      1               (
							//      2               the field name to test (first)
							//      3               ;
							//      4               the "then" spec (can be missing)
							//      5               ;
							//      6               the "else" spec (can be missing)
							//      7               )
							//
							if (($n_tok = count($a_toks)) < 6 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ";" ||
							    $a_toks[$n_tok - 1] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC_IF_FMT)
								                     )));
							} else {
								$b_ok        = true;
								$s_fld_name  = $a_toks[2];
								$s_then_spec = $s_else_spec = "";
								for ($ii = 4 ; $ii < $n_tok && $a_toks[$ii] != ';' ; $ii++) {
									$s_then_spec .= $a_toks[$ii];
								}
								if ($ii == $n_tok) {
									$b_ok = false;
								} else {
									//
									// Concatenate tokens until the ')'.
									// This provides the "else" spec.
									//
									for (; ++$ii < $n_tok && $a_toks[$ii] != ')' ;) {
										$s_else_spec .= $a_toks[$ii];
									}
									if ($ii == $n_tok) {
										$b_ok = false;
									}
								}
								$s_mesg = "";
								if ($b_ok) {
									if (!$field_mgr->TestFieldEmpty($s_fld_name,$s_mesg)) {
										$s_fld_spec = $s_then_spec;
									} else {
										$s_fld_spec = $s_else_spec;
									}
									$s_value = GetDerivedValue($field_mgr,$s_fld_spec,$a_errors);
								} else {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_IF_FMT)
									                     )));
								}
							}
							break;
						case "size":
							//
							// "size" function: return size of uploaded file
							//
							// Example: size(fieldname)
							//
							// tokens are:
							//      1               (
							//      2               the field name for the file upload
							//      3               )
							//
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC_SIZE_FMT)
								                     )));
							} elseif (($i_size = GetFileSize($a_toks[2])) !== false) {
								$s_value = "$i_size";
							}
							break;
						case "ext":
							//
							// "ext" function: return filename extension of uploaded file
							//
							// Example: ext(fieldname)
							//
							// tokens are:
							//      1               (
							//      2               the field name for the file upload
							//      3               )
							//
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC_EXT_FMT)
								                     )));
							} elseif (($s_name = GetFileName($a_toks[2])) !== false) {
								if (($i_pos = strrpos($s_name,".")) !== false) {
									$s_value = substr($s_name,$i_pos + 1);
								}
							}
							break;
						case "ucase":
						case "lcase":
							//
							// "ucase" and "lcase" functions: return field
							// converted to upper or lower case
							//
							// Example: lcase(fieldname)
							//
							// tokens are:
							//      1               (
							//      2               the field name to convert
							//      3               )
							//
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC1_FMT,
								                                                array("FUNC" => $a_toks[0]))
								                     )));
							} elseif ($a_toks[0] == "ucase") {
								$s_value = strtoupper($field_mgr->GetFieldValue($a_toks[2]));
							} else {
								$s_value = strtolower($field_mgr->GetFieldValue($a_toks[2]));
							}
							break;
						case "ltrim":
						case "rtrim":
						case "trim":
							//
							// trim functions: return field with whitespace removed
							// from left (ltrim), right (rtrim), or both (trim)
							// ends.
							//
							// Example: ltrim(fieldname)
							//
							// tokens are:
							//      1               (
							//      2               the field name to trim
							//      3               )
							//
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC1_FMT,
								                                                array("FUNC" => $a_toks[0]))
								                     )));
							} else {
								$s_value = $a_toks[0]($field_mgr->GetFieldValue($a_toks[2]));
							}
							break;
						case "ltrim0":
							//
							// ltrim0 function: return field with blanks and
							// leading 0's removed from the left.
							//
							// Example: ltrim0(fieldname)
							//
							// tokens are:
							//      1               (
							//      2               the field name to trim
							//      3               )
							//
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC1_FMT,
								                                                array("FUNC" => $a_toks[0]))
								                     )));
							} else {
								$s_value = $field_mgr->GetFieldValue($a_toks[2]);
								$s_value = ltrim($s_value); // trim blanks on left
								$i_len   = strspn($s_value,"0");
								//
								// if the whole string is zeroes, make sure we leave
								// one of them!
								//
								if ($i_len == strlen($s_value)) {
									if (--$i_len < 0) {
										$i_len = 0;
									}
								}
								$s_value = substr($s_value,$i_len);
							}
							break;
						case "nextnum":
							//
							// "nextnum" function: return a unique number (next number)
							//
							// Usage: nextnum[(pad[,base])]
							//
							// Examples:
							//      %nextnum%
							//      %nextnum(8)%
							//      %nextnum(5;16)%
							//
							// You can provide a padding amount. In this case, the
							// number is padded on the left with zeroes to the number
							// of digits specified.
							//
							// You can also provide a base for your numbers.  Valid
							// values for base are 2 to 36, inclusive.
							//
							// tokens are:
							//      1               (
							//      2               the padding amount
							//      3               ;
							//      4               the base
							//      5               )
							//
							$i_pad  = 0; // no padding
							$i_base = 10; // base 10
							if (($n_tok = count($a_toks)) > 1) {
								if (($n_tok != 4 && $n_tok != 6) ||
								    $a_toks[1] != "(" ||
								    $a_toks[$n_tok - 1] != ")"
								) {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_NEXTNUM_FMT) . " (T1)"
									                     )));
								}
								if ($n_tok == 6 && $a_toks[3] != ";") {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_NEXTNUM_FMT) . " (T2)"
									                     )));
								}
								if (!is_numeric($a_toks[2]) ||
								    ($n_tok == 6 && !is_numeric($a_toks[4]))
								) {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_NEXTNUM_FMT) . " (T3)"
									                     )));
								}
								$i_pad = intval($a_toks[2]);
								if ($n_tok == 6) {
									$i_base = intval($a_toks[4]);
									if ($i_base < 2 || $i_base > 36) {
										SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
										                     array("SPEC" => $s_spec,
										                           "MSG"  =>
											                           GetMessage(MSG_DER_FUNC_NEXTNUM_FMT) . " (T4)"
										                     )));
										$i_base = 10;
									}
								}
								$s_value = GetNextNum($i_pad,$i_base);
							} else {
								$s_value = GetNextNum($i_pad,$i_base);
							}
							break;
						case "substr":
							//
							// "substr" function: return part of a string
							//
							// Usage: substr(fieldname;start;length)
							//
							// Examples:
							//      %substr(field1;0;5)%
							//      %substr(field1;4)%
							//      %substr(field1;-4;-1)%
							//
							// The start is the number of characters to skip
							// at the beginning of the string, if positive.
							// If negative, the start is calculated from the
							// end of the string, and says how many characters
							// to go backward from the end.
							//
							// The length is the number of characters to return,
							// if positive.  If not specified, then the remainder
							// of the string is returned.  If negative, it
							// specifies the number of characters to be omitted
							// from the end of the string.
							//
							// Examples:
							//  Assume "f1" has the value "abcdef":
							//      %substr(f1;0;5)%    returns "abcde"
							//      %substr(f1;4)%      returns "ef"
							//      %substr(f1;1;-1)%   returns "bcde"
							//      %substr(f1;-4;-1)%  returns "cde"
							//
							// tokens are:
							//      1               (
							//      2               the field name
							//      3               ;
							//      4               the start
							//      5               ;
							//      6               the length
							//      7               )
							//
							$i_start = 0; // default start
							$i_len   = null; // no length
							if (($n_tok = count($a_toks)) > 1) {
								if (($n_tok != 6 && $n_tok != 8) ||
								    $a_toks[1] != "(" ||
								    $a_toks[$n_tok - 1] != ")"
								) {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_SUBSTR_FMT) . " (T1)"
									                     )));
								}
								if ($n_tok == 8 &&
								    ($a_toks[3] != ";" || $a_toks[5] != ";")
								) {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_SUBSTR_FMT) . " (T2)"
									                     )));
								}
								if ($n_tok == 6 && $a_toks[3] != ";") {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_SUBSTR_FMT) . " (T3)"
									                     )));
								}
								if (!is_numeric($a_toks[4]) ||
								    ($n_tok == 8 && !is_numeric($a_toks[6]))
								) {
									SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
									                     array("SPEC" => $s_spec,
									                           "MSG"  => GetMessage(MSG_DER_FUNC_SUBSTR_FMT) . " (T4)"
									                     )));
								}
								$i_start = intval($a_toks[4]);
								$s_value = $field_mgr->GetFieldValue($a_toks[2]);
								if ($n_tok == 8) {
									$i_len = intval($a_toks[6]);
								}
								if (isset($i_len)) {
									$s_value = substr($s_value,$i_start,$i_len);
								} else {
									$s_value = substr($s_value,$i_start);
								}
								if ($s_value === FALSE) {
									$s_value = "";
								}
							} else {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC_SUBSTR_FMT) . " (T5)"
								                     )));
							}
							break;
						case "call":
							//
							// "call" function: calls a builtin function
							//
							// Example: call(name ; p1 ; p2 ; ...)
							//
							// tokens are:
							//      1               (
							//      2               the function name
							//      3               ;
							//      4               first parameter
							//      5               ;
							//      6               second parameter
							//      ...             ...
							//      n               )
							//
							if (($n_tok = count($a_toks)) < 3 ||
							    $a_toks[1] != "(" ||
							    $a_toks[$n_tok - 1] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => 'Incorrect format for call function'
								                     )));
							} else {
								$b_ok        = true;
								$s_func_name = $a_toks[2];
								$a_params    = array();
								if (3 < $n_tok && $a_toks[3] == ';') {
									$a_params = CollectParams($a_toks,$n_tok,3);
								}
								for ($ii = 0 ; $ii < count($a_params) ; $ii++) {
									$a_params[$ii] = GetDerivedValue($field_mgr,$a_params[$ii],$a_errors);
								}
								global $BuiltinFunctions;

								if (!$BuiltinFunctions->Call($s_func_name,$a_params,$s_result)) {
									SendAlert($s_result);
								} else {
									$s_value = $s_result;
								}
							}
							break;
						case "urlencode":
							// code supplied by Jeff Shields
							if (count($a_toks) != 4 ||
							    $a_toks[1] != "(" ||
							    $a_toks[3] != ")"
							) {
								SendAlert(GetMessage(MSG_DER_FUNC_ERROR,
								                     array("SPEC" => $s_spec,
								                           "MSG"  => GetMessage(MSG_DER_FUNC1_FMT,
								                                                array("FUNC" => $a_toks[0]))
								                     )));
							} else {
								$s_value = urlencode($field_mgr->GetFieldValue($a_toks[2]));
							}
							break;
						default:
							SendAlertIgnoreSpam(GetMessage(MSG_UNK_VALUE_SPEC,
							                               array("SPEC" => $s_spec,"MSG" => "")));
							break;
					}
				} else {
					SendAlertIgnoreSpam(GetMessage(MSG_UNK_VALUE_SPEC,array("SPEC" => $s_spec,
					                                                        "MSG"  => ""
					)));
				}
			}
			break;
	}
	return ($s_value);
}

//
// Collect parameters for a builtin function call
//
function CollectParams($a_toks,$n_tok,$i_tok)
{
	$a_params = array();
	$i_param  = 0;
	do {
		$i_tok++;
		$a_params[$i_param] = '';
		for (; $i_tok < $n_tok && $a_toks[$i_tok] != ';' && $a_toks[$i_tok] != ')' ; $i_tok++) {
			$a_params[$i_param] .= $a_toks[$i_tok];
		}
		$i_param++;
	} while ($a_toks[$i_tok] != ')');
	return ($a_params);
}

//
// Return the next number or fail on error
//
function GetNextNum($i_pad,$i_base)
{
	global $php_errormsg;

	if (Settings::isEmpty('NEXT_NUM_FILE') || Settings::get('NEXT_NUM_FILE') === "") {
		ErrorWithIgnore("next_num_config",GetMessage(MSG_NO_NEXT_NUM_FILE));
		FormMailExit();
	}
	if (($fp = @fopen(Settings::get('NEXT_NUM_FILE'),"r+")) === false) {
		Error("next_num_file",GetMessage(MSG_NEXT_NUM_FILE,
		                                 array("FILE" => Settings::get('NEXT_NUM_FILE'),
		                                       "ACT"  => "open",
		                                       "ERR"  => $php_errormsg
		                                 )));
		FormMailExit();
	}
	if (!flock($fp,defined("LOCK_EX") ? LOCK_EX : 2)) {
		Error("next_num_file",GetMessage(MSG_NEXT_NUM_FILE,
		                                 array("FILE" => Settings::get('NEXT_NUM_FILE'),
		                                       "ACT"  => "flock",
		                                       "ERR"  => $php_errormsg
		                                 )));
		FormMailExit();
	}
	//
	// read the first line only
	//
	if (!feof($fp)) {
		if (($s_line = fread($fp,1024)) === false) {
			$i_next = 1;
		} elseif (($i_next = intval($s_line)) <= 0) {
			$i_next = 1;
		}
	} else {
		$i_next = 1;
	}
	if (rewind($fp) == 0) {
		Error("next_num_file",GetMessage(MSG_NEXT_NUM_FILE,
		                                 array("FILE" => Settings::get('NEXT_NUM_FILE'),
		                                       "ACT"  => "rewind",
		                                       "ERR"  => $php_errormsg
		                                 )));
		FormMailExit();
	}
	$s_ret = strval($i_next++);
	if (fputs($fp,"$i_next\r\n") <= 0) {
		Error("next_num_file",GetMessage(MSG_NEXT_NUM_FILE,
		                                 array("FILE" => Settings::get('NEXT_NUM_FILE'),
		                                       "ACT"  => "fputs",
		                                       "ERR"  => $php_errormsg
		                                 )));
		FormMailExit();
	}
	fclose($fp);
	if ($i_base != 10) {
		$s_ret = base_convert($s_ret,10,$i_base);
		$s_ret = strtoupper($s_ret); // always upper case if alphas are used
	}
	if ($i_pad != 0) {
		$s_ret = str_pad($s_ret,$i_pad,"0",STR_PAD_LEFT);
	}
	return ($s_ret);
}

//
// Return the value of an object or array as a string.
//
function GetObjectAsString($m_value)
{
	ob_start();
	print_r($m_value);
	$s_ret = ob_get_contents();
	ob_end_clean();
	return ($s_ret);
}

//
// Return a Server or Environment variable value.  Returns false if
// not found, otherwise a string value.
//
function GetEnvValue($s_name)
{
	global $aServerVars,$aEnvVars;

	if (isset($aEnvVars[$s_name])) {
		$m_value = $aEnvVars[$s_name];
	} elseif (isset($aServerVars[$s_name])) {
		$m_value = $aServerVars[$s_name];
	}
	//
	// some values might not be strings - so convert
	//
	if (isset($m_value) && !is_scalar($m_value)) {
		$m_value = GetObjectAsString($m_value);
	}
	return (isset($m_value) ? ((string)$m_value) : false);
}

/**
 * Test if a field is set in the given vars array or in the uploaded
 * files.
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param string $s_fld       the field name
 * @param array  $a_main_vars the field list array
 *
 * @return bool
 */
function IsFieldSet($s_fld,$a_main_vars)
{
	global $aFileVars;

	$mgr = new FieldManager($a_main_vars,$aFileVars);
	return ($mgr->IsFieldSet($s_fld));
}

/*
 * Function:    IsFileField
 * Parameters:  $s_fld  the field name
 * Returns:     bool    true if this is a file upload field
 * Description:
 *  Checks if a field is a file upload field (regardless of whether
 *  file uploads are being allowed, or whether the actual upload
 *  is valid in any way).
 */
/** @noinspection PhpUnused */
function IsFileField($s_fld)
{
	global $aFileVars;

	return (isset($aFileVars[$s_fld]));
}

//
// Delete the info for an uploaded file
//
function DeleteFileInfo($s_fld)
{
	global $aFileVars;
	global $aCleanedValues,$aRawDataValues,$aAllRawValues;

	if (Settings::get('FILEUPLOADS')) {
		if (IsSetSession("FormSavedFiles")) {
			$a_saved_files = GetSession("FormSavedFiles");
			unset($a_saved_files[$s_fld]);
			SetSession("FormSavedFiles",$a_saved_files);
		}
		if (isset($aFileVars[$s_fld])) {
			unset($aFileVars[$s_fld]);
		}
		//
		// zap any "name_of" field that has been created
		//
		$s_name = "name_of_$s_fld";
		unset($aCleanedValues[$s_name]);
		unset($aRawDataValues[$s_name]);
		unset($aAllRawValues[$s_name]);
	}
}

/**
 * Return the info for the uploaded file, or false on error.
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param string $s_fld field name
 *
 * @return bool
 * @see \FormMail\UnitTests\GetFieldValueTest
 */
function GetFileInfo($s_fld)
{
	global $aFileVars;

	$mgr = new FieldManager(array(),$aFileVars);
	return ($mgr->GetFileInfo($s_fld));
}

/**
 * Return the original name of the uploaded file or false on error.
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param string $s_fld field name
 *
 * @return bool
 * @see \FormMail\UnitTests\GetFileNameTest
 */
function GetFileName($s_fld)
{
	global $aFileVars;

	$mgr = new FieldManager(array(),$aFileVars);
	return ($mgr->GetFileName($s_fld));
}

//
// Return the size of the uploaded file or false on error.
//
function GetFileSize($s_fld)
{
	if (($a_upload = GetFileInfo($s_fld)) !== false) {
		return ($a_upload["size"]);
	}
	return (false);
}

/**
 * Return a field value.  Empty string is returned if the field is
 * not found. File fields return the original name of the uploaded file.
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param string $s_fld       field name
 * @param array  $a_main_vars the array of fields
 * @param string $s_array_sep a separator for joining array values
 *
 * @return bool|string
 * @see \FormMail\UnitTests\GetFieldValueTest
 */
function GetFieldValue($s_fld,$a_main_vars,$s_array_sep = ";")
{
	global $aFileVars;

	$mgr = new FieldManager($a_main_vars,$aFileVars);
	return ($mgr->GetFieldValue($s_fld,$s_array_sep));
}

/**
 * Tests a field against an array of vars for emptiness.
 * If the var isn't found there, then the POSTed files array is checked.
 * Returns true if the field is empty (a specific error may
 * be returned in the $s_mesg parameter).
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param string $s_fld       field name
 * @param array  $a_main_vars the array of fields
 * @param string $s_mesg      returned error message
 *
 * @return bool
 * @see \FormMail\UnitTests\TestFieldEmptyTest
 */
function TestFieldEmpty($s_fld,$a_main_vars,&$s_mesg)
{
	global $aFileVars;

	$mgr = new FieldManager($a_main_vars,$aFileVars);
	return ($mgr->TestFieldEmpty($s_fld,$s_mesg));
}

//
// Return a derived field value or value specification.
//
function GetDerivedValue($field_mgr,$s_word,&$a_errors)
{
	$s_value = "";
	//
	// a field name or a value specification
	// value specifications have the following format:
	//      %spec%
	//
	if (substr($s_word,0,1) == '%') {
		if (substr($s_word,-1,1) != '%') {
			SendAlert(GetMessage(MSG_INV_VALUE_SPEC,array("SPEC" => $s_word)));
			$s_value = $s_word;
		} else {
			$s_spec  = substr($s_word,1,-1);
			$s_value = ValueSpec($s_spec,$field_mgr,$a_errors);
		}
	} else {
		$s_fld_name = $s_word;
		//
		// try form data first, then the environment/server data
		//
		if ($field_mgr->IsFieldSet($s_fld_name)) {
			$s_value = $field_mgr->GetFieldValue($s_fld_name);
		} elseif (($s_value = GetEnvValue($s_fld_name)) === false) {
			$s_value = "";
		}
		$s_value = trim($s_value);
	}
	return ($s_value);
}

//
// Derive a value from the form data using the specification returned
// from ParseDerivation.
//
function DeriveValue($field_mgr,$a_value_spec,$s_name,&$a_errors)
{
	$s_value = "";
	for ($ii = 0 ; $ii < count($a_value_spec) ; $ii++) {
		switch ($a_value_spec[$ii]) {
			case '+':
				//
				// concatenate with a single space between, but skip the space
				// if the next field is empty
				//
				if ($ii < count($a_value_spec) - 1) {
					$s_temp = GetDerivedValue($field_mgr,$a_value_spec[$ii + 1],$a_errors);
					if (!FieldManager::IsEmpty($s_temp)) {
						$s_value .= ' ';
					}
				}
				break;
			case '.':
				//
				// concatenate with no space between
				//
				break;
			case '*':
				//
				// concatenate with a single space between
				//
				$s_value .= ' ';
				break;
			default:
				//
				// a field name or a value specification
				// value specifications have the following format:
				//      %name%
				//
				$s_value .= GetDerivedValue($field_mgr,$a_value_spec[$ii],$a_errors);
				break;
		}
	}
	return ($s_value);
}

//
// Create derived fields specified by the "derive_fields" value.
//
function CreateDerived($a_form_data)
{
	global $aFileVars;

	if (isset($a_form_data["derive_fields"])) {
		$a_errors = array();
		//
		// get the list of derived field specifications
		//
		$a_list    = TrimArray(explode(",",$a_form_data["derive_fields"]));
		$field_mgr = new FieldManager($a_form_data,$aFileVars);
		foreach ($a_list as $s_fld_spec) {
			if ($s_fld_spec === "")
				//
				// silently ignore empty derivations
				//
			{
				continue;
			}
			if (($i_pos = strpos($s_fld_spec,"=")) === false) {
				$a_errors[] = $s_fld_spec;
				continue;
			}
			$s_name     = trim(substr($s_fld_spec,0,$i_pos));
			$s_fld_spec = substr($s_fld_spec,$i_pos + 1);

			if (($a_value_spec = ParseDerivation($s_fld_spec,$s_name,$a_errors)) === false
			) {
				break;
			}
			$a_form_data[$s_name] = DeriveValue($field_mgr,$a_value_spec,$s_name,$a_errors);
			// we've added a new field, so we need a new FieldManager object
			$field_mgr = new FieldManager($a_form_data,$aFileVars);
		}
		if (count($a_errors) > 0) {
			SendAlertIgnoreSpam(GetMessage(MSG_DERIVED_INVALID) . implode("\n",$a_errors));
			ErrorWithIgnore("derivation_failure",GetMessage(MSG_INT_FORM_ERROR));
		}
	}
	return ($a_form_data);
}

//
// To process the name specification for files and update the
// array of file variables accordingly.
//
function SetFileNames($s_name_spec,$a_order,$a_fields,$a_raw_fields,$a_all_raw_values,$a_file_vars)
{
	$a_errors = array();
	//
	// get the list of file name derivations
	//
	$a_list = TrimArray(explode(",",$s_name_spec));
	foreach ($a_list as $s_fld_spec) {
		if ($s_fld_spec === "")
			//
			// silently ignore empty file name derivations
			//
		{
			continue;
		}
		if (($i_pos = strpos($s_fld_spec,"=")) === false) {
			$a_errors[] = $s_fld_spec;
			continue;
		}
		$s_name     = trim(substr($s_fld_spec,0,$i_pos));
		$s_fld_spec = substr($s_fld_spec,$i_pos + 1);

		if (($a_value_spec = ParseDerivation($s_fld_spec,$s_name,$a_errors)) === false
		) {
			break;
		}
		if (isset($a_file_vars[$s_name]) && FieldManager::IsUploadedFile($a_file_vars[$s_name])) {
			//
			// we create our own special entry in the file variable's data
			//
			$a_file_vars[$s_name]["new_name"] = DeriveValue(new FieldManager($a_raw_fields,$a_file_vars),
			                                                $a_value_spec,$s_name,$a_errors);
			//
			// we also create (derive) a new field called 'name_of_X'
			// where X is the file fields's name
			//
			ProcessField("name_of_$s_name",$a_file_vars[$s_name]["new_name"],
			             $a_order,$a_fields,$a_raw_fields);
			$a_all_raw_values["name_of_$s_name"] = $a_file_vars[$s_name]["new_name"];
		}
		/* This is annoying if a file upload is optional.  Just ignore missing
            file upload fields.
        else
            SendAlert(GetMessage(MSG_FILE_NAMES_NOT_FILE,
                                            array("NAME"=>$s_name)));*/
	}
	if (count($a_errors) > 0) {
		SendAlertIgnoreSpam(GetMessage(MSG_FILE_NAMES_INVALID) . implode("\n",$a_errors));
		ErrorWithIgnore("file_names_derivation_failure",GetMessage(MSG_INT_FORM_ERROR));
	}
	return (array($a_order,$a_fields,$a_raw_fields,$a_all_raw_values,$a_file_vars));
}

$aProcessSpecsFormData  = array();
$sProcessSpecsFieldName = "";

//
// Callback function for ProcessSpecs
//
function ProcessSpecsMatch($a_matches)
{
	global $aProcessSpecsFormData,$sProcessSpecsFieldName,$aFileVars;

	//
	// strip % at either end
	//
	$s_spec   = substr($a_matches[0],1,-1);
	$a_errors = array();
	$s_value  = ValueSpec($s_spec,new FieldManager($aProcessSpecsFormData,$aFileVars),$a_errors);
	return ($s_value);
}

//
// Process %...% specifications in a string.
// This function is used for processing options values, such as in "autorespond"
// and "mail_options".
//
function ProcessSpecs($s_fld_name,$a_form_data,$s_value)
{
	global $aProcessSpecsFormData,$sProcessSpecsFieldName;

	$aProcessSpecsFormData  = $a_form_data;
	$sProcessSpecsFieldName = $s_fld_name;
	//
	// un-greedy pattern match
	// Note that this means we can't use %..% within a %if(..)%, for example.
	//
	$s_value                = preg_replace_callback('/%.+?%/',"ProcessSpecsMatch",$s_value);
	$aProcessSpecsFormData  = array();
	$sProcessSpecsFieldName = "";
	return ($s_value);
}

//
// Process a list of attributes or options.
// Format for each attribute/option:
//      name
// or
//      name=value
//
// Values can be simple values or semicolon (;) separated lists:
//          avalue
//          value1;value2;value3;...
//
// Returns attribute/options in the associative array $a_attribs.
// Optionally, valid attributes can be provided in $a_valid_attribs
// (if empty, all attributes found are considered valid).
// Errors are returned in $a_errors.
//
function ProcessAttributeList($s_fld_name,$a_form_data,$a_list,&$a_attribs,&$a_errors,
                              $a_valid_attribs = array())
{
	$b_got_valid_list = (count($a_valid_attribs) > 0);
	foreach ($a_list as $s_attrib) {
		//
		// if the name begins with '.' then silently ignore it;
		// this allows you to temporarily disable an option without
		// getting an alert message
		//
		if (($i_pos = strpos($s_attrib,"=")) === false) {
			$s_name = trim($s_attrib);
			if (empty($s_name) || $s_name[0] == '.') {
				continue;
			}
			//
			// option is a simple "present" value
			//
			$a_attribs[$s_name] = true;
		} else {
			$s_name = trim(substr($s_attrib,0,$i_pos));
			if (empty($s_name) || $s_name[0] == '.') {
				continue;
			}
			$s_value_list = substr($s_attrib,$i_pos + 1);
			if (($i_pos = strpos($s_value_list,";")) === false)
				//
				// single value
				//
			{
				$a_attribs[$s_name] = ProcessSpecs($s_fld_name,$a_form_data,trim($s_value_list));
			} else
				//
				// list of values
				//
			{
				$a_attribs[$s_name] = TrimArray(explode(";",$s_value_list));
			}
		}
		if ($b_got_valid_list && !isset($a_valid_attribs[$s_name])) {
			$a_errors[] = $s_name;
		}
	}
}

//
// Process the options specified in the form.
// Options can be specified in this format:
//          option1,option2,option3,...
// Each option can be a simple word or a word and value:
//          name
//          name=value
// No name or value can contain a comma.
// Values can be simple values or semicolon (;) separated lists:
//          avalue
//          value1;value2;value3;...
// No value can contain a semicolon.
// Be careful of values beginning and ending with whitespace characters;
// they will be trimmed.
//
function ProcessOptions($s_name,$a_form_data,&$a_options,$a_valid_options)
{
	$a_errors  = array();
	$a_options = array();
	if (isset($a_form_data[$s_name])) {
		//
		// get the options list and trim each one
		//
		$a_list = TrimArray(explode(",",$a_form_data[$s_name]));
		ProcessAttributeList($s_name,$a_form_data,$a_list,$a_options,$a_errors,$a_valid_options);
	}
	if (count($a_errors) > 0) {
		SendAlertIgnoreSpam(GetMessage(MSG_OPTIONS_INVALID,array("OPT" => $s_name)) .
		                    implode("\n",$a_errors));
	}
}

//
// Process the mail_options specified in the form.
//
function ProcessMailOptions($a_form_data)
{
	global $MAIL_OPTS,$VALID_MAIL_OPTIONS;

	ProcessOptions("mail_options",$a_form_data,$MAIL_OPTS,$VALID_MAIL_OPTIONS);
}

//
// Check if an option is set
//
function IsMailOptionSet($s_name)
{
	global $MAIL_OPTS;

	return (isset($MAIL_OPTS[$s_name]));
}

//
// Return an option's value or NULL if not set.
//
function GetMailOption($s_name)
{
	global $MAIL_OPTS;

	return (isset($MAIL_OPTS[$s_name]) ? $MAIL_OPTS[$s_name] : NULL);
}

//
// Process the crm_options specified in the form.
//
function ProcessCRMOptions($a_form_data)
{
	global $CRM_OPTS,$VALID_CRM_OPTIONS;

	ProcessOptions("crm_options",$a_form_data,$CRM_OPTS,$VALID_CRM_OPTIONS);
}

//
// Check if an option is set
//
function IsCRMOptionSet($s_name)
{
	global $CRM_OPTS;

	return (isset($CRM_OPTS[$s_name]));
}

//
// Return an option's value or NULL if not set.
//
/** @noinspection PhpUnused */
function GetCRMOption($s_name)
{
	global $CRM_OPTS;

	return (isset($CRM_OPTS[$s_name]) ? $CRM_OPTS[$s_name] : NULL);
}

//
// Check if a field is in the mail exclusion list.
//
function IsMailExcluded($s_name)
{
	$a_list = GetMailOption("Exclude");
	if (!isset($a_list)) {
		return (false);
	}
	if (is_array($a_list)) {
		return (in_array($s_name,$a_list));
	} else {
		return ($s_name === $a_list);
	}
}

//
// Process the autorespond specified in the form.
//
function ProcessAROptions($a_form_data)
{
	global $AR_OPTS,$VALID_AR_OPTIONS;

	ProcessOptions("autorespond",$a_form_data,$AR_OPTS,$VALID_AR_OPTIONS);
}

//
// Check if an option is set
//
function IsAROptionSet($s_name)
{
	global $AR_OPTS;

	return (isset($AR_OPTS[$s_name]));
}

//
// Return an option's value or NULL if not set.
//
function GetAROption($s_name)
{
	global $AR_OPTS;

	return (isset($AR_OPTS[$s_name]) ? $AR_OPTS[$s_name] : NULL);
}

//
// Process the mail_options specified in the form.
//
function ProcessFilterOptions($a_form_data)
{
	global $FILTER_OPTS,$VALID_FILTER_OPTIONS;

	ProcessOptions("filter_options",$a_form_data,$FILTER_OPTS,$VALID_FILTER_OPTIONS);
}

//
// Check if an option is set
//
function IsFilterOptionSet($s_name)
{
	global $FILTER_OPTS;

	return (isset($FILTER_OPTS[$s_name]));
}

//
// Return an option's value or NULL if not set.
//
function GetFilterOption($s_name)
{
	global $FILTER_OPTS;

	return (isset($FILTER_OPTS[$s_name]) ? $FILTER_OPTS[$s_name] : NULL);
}

//
// Lookup a filter attribute for the given filter.
// Return it's value or false if not set.
//
function GetFilterAttrib($s_filter,$s_attrib)
{
	global $FILTER_ATTRIBS_LOOKUP;

	$a_attribs = Settings::get('FILTER_ATTRIBS');
	if (!isset($a_attribs[$s_filter]))
		//
		// no attributes for the filter
		//
	{
		return (false);
	}
	if (!isset($FILTER_ATTRIBS_LOOKUP[$s_filter])) {
		//
		// the attributes have not yet been parsed - create the lookup table
		//
		$a_list                           = TrimArray(explode(",",$a_attribs[$s_filter]));
		$FILTER_ATTRIBS_LOOKUP[$s_filter] = array();
		$a_errors                         = array();

		ProcessAttributeList('FILTER_ATTRIBS',array(),$a_list,$FILTER_ATTRIBS_LOOKUP[$s_filter],$a_errors);
	}
	//
	// perform the lookup and return the value
	//
	if (!isset($FILTER_ATTRIBS_LOOKUP[$s_filter][$s_attrib])) {
		return (false);
	}
	return ($FILTER_ATTRIBS_LOOKUP[$s_filter][$s_attrib]);
}

//
// Check the filter attributes for the given filter.
// Return true if the given attribute is set otherwise false.
//
function IsFilterAttribSet($s_filter,$s_attrib)
{
	return (GetFilterAttrib($s_filter,$s_attrib));
}

//
// Process the given .ini file.
//
function ProcessFormIniFile($s_file)
{
	global $EMAIL_ADDRS,$ValidEmails;

	$a_sections = parse_ini_file($s_file,TRUE);
	//
	// from PHP 5.2.7, parse_ini_file returns false on syntax problems
	// prior to that, an empty array.  So, on previous versions of PHP
	// we cannot detect an actual error (an empty array is perfectly valid).
	//
	if ($a_sections === false) {
		Error("bad_ini",GetMessage(MSG_INI_PARSE_ERROR,array("FILE" => $s_file)));
	} elseif (empty($a_sections)) {
		SendAlert(GetMessage(MSG_INI_PARSE_WARN,array("FILE" => $s_file)),false,true);
	}
	if (Settings::get('DB_SEE_INI')) {
		//
		// just display the ini file
		//
		$s_text = "<p><b>The following settings were found in the file '$s_file':</b></p>";
		foreach ($a_sections as $s_sect => $a_settings) {
			$s_text .= "<p>[$s_sect]\n";
			foreach ($a_settings as $s_name => $s_value) {
				$s_text .= "$s_name = \"$s_value\"\n";
			}
			$s_text .= "</p>";
		}
		CreatePage($s_text,"Debug Output - INI File Display");
		FormMailExit();
	}
	//
	// Load the email_addresses section.
	//
	if (isset($a_sections["email_addresses"])) {
		$a_addr_list = $a_sections["email_addresses"];
		//
		// make these addresses valid
		//
		foreach ($a_addr_list as $s_alias => $s_list) {
			$EMAIL_ADDRS[$s_alias] = $s_list;
			$ValidEmails->AddAddresses($s_list);
		}
	}
	//
	// Process special fields
	//
	if (isset($a_sections["special_fields"])) {
		foreach ($a_sections["special_fields"] as $s_name => $m_value) {
			if (IsSpecialField($s_name)) {
				ValidateSpecialField($s_name,$m_value,false);
				SetSpecialField($s_name,$m_value);
				//
				// if this is the recipients, cc, or bcc field,
				// make the addresses valid
				//
				if ($s_name === "recipients" || $s_name === "cc" || $s_name === "bcc")
					//
					// coming from the INI file, the values can only be strings
					//
				{
					if (is_string($m_value)) {
						$ValidEmails->AddAddresses($m_value);
					}
				}
			}
			//
			// check for multiple valued special fields
			//
			if (($a_multi_fld = IsSpecialMultiField($s_name)) !== false) {
				SetSpecialMultiField($a_multi_fld[0],$a_multi_fld[1],$m_value);
			}
		}
	}
}

/**
 * UnMangle an email address.  This means replacing AT_MANGLE in the given
 * string with the @ symbol.
 * It can also lookup an email alias and return the corresponding email address.
 * Email aliases are defined in the EMAIL_ADDRS configuration setting or in an INI file.
 *
 * @param string $s_email the email address to unmangle
 *
 * @return string the actual email address
 */
function UnMangle($s_email)
{
	$email_addrs = Settings::get('EMAIL_ADDRS');

	//
	// map from a name to the real email address; if it exists
	//
	if (isset($email_addrs[$s_email])) {
		$s_email = $email_addrs[$s_email];
	}
	//
	// unmangle
	//
	if (Settings::get('AT_MANGLE') != "") {
		$s_email = str_replace(Settings::get('AT_MANGLE'),"@",$s_email);
	}
	return ($s_email);
}

//
// Check a list of email addresses (comma separated); returns a list
// of valid email addresses (comma separated).
// The list can be an array of comma separated lists.
// The return value is true if there is at least one valid email address.
//
function CheckEmailAddress($m_addr,&$s_valid,&$s_invalid,$b_check = true)
{
	global $ValidEmails;

	$s_invalid = $s_valid = "";
	if (is_array($m_addr)) {
		$a_list = array();
		foreach ($m_addr as $s_addr_list) {
			$a_list = array_merge($a_list,TrimArray(explode(",",$s_addr_list)));
		}
	} else {
		$a_list = TrimArray(explode(",",$m_addr));
	}
	$a_invalid = array();
	$n_empty   = 0;
	for ($ii = 0 ; $ii < count($a_list) ; $ii++) {
		if ($a_list[$ii] === "") {
			//
			// ignore, but count empty addresses
			//
			$n_empty++;
			continue;
		}
		$s_email = UnMangle($a_list[$ii]);
		//
		// UnMangle works with INI files too, and a single
		// word can expand to a list of email addresses.
		//
		$a_this_list = TrimArray(explode(",",$s_email));
		foreach ($a_this_list as $s_email) {
			if ($s_email === "") {
				//
				// ignore, but count empty addresses
				//
				$n_empty++;
				continue;
			}
			if ($b_check) {
				$b_is_valid = $ValidEmails->CheckAddress($s_email);
			} else {
				$b_is_valid = true;
			}
			if ($b_is_valid) {
				if (empty($s_valid)) {
					$s_valid = $s_email;
				} else {
					$s_valid .= "," . $s_email;
				}
			} else {
				$a_invalid[] = $s_email;
			}
		}
	}
	//
	// just ignore empty recipients unless there are *no* valid recipients
	//
	if (empty($s_valid) && $n_empty > 0) {
		$a_invalid[] = GetMessage(MSG_EMPTY_ADDRESSES,array("COUNT" => $n_empty));
	}
	if (count($a_invalid) > 0) {
		$s_invalid = implode(",",$a_invalid);
	}
	return (!empty($s_valid));
}

//
// PHP 8 replacement for
//      filter_var($url,FILTER_SANITIZE_STRING)
// See: https://stackoverflow.com/questions/69207368/constant-filter-sanitize-string-is-deprecated
//
function filter_string_polyfill(string $string): string
{
	$str = preg_replace('/\x00|<[^>]*>?/','',$string);
	return str_replace(["'",'"'],['&#39;','&#34;'],$str);
}

//
// Redirect to another URL
//
function Redirect($url,$title)
{
	global $ExecEnv;

	//
	// for browsers without cookies enabled, append the Session ID
	//
	if ($ExecEnv->allowSessionURL()) {
		if (session_id() !== "") {
			$url = AddURLParams($url,session_name() . "=" . urlencode(session_id()));
		} elseif (defined("SID")) {
			$url = AddURLParams($url,SID);
		}
	}

	//FMDebug("Before redirecting, FormData = ".(isset($_SESSION["FormData"]) ? var_export($_SESSION["FormData"],true) : "NULL"));

	//
	// this is probably a good idea to ensure the session data
	// is written away
	//
	if (function_exists('session_write_close')) {
		session_write_close();
	}

	header("Location: $url");

	//
	// avoid XSS by sanitizing the URL
	//
	$url = stripJS(filter_string_polyfill($url));

	//
	// if the header doesn't work, try JavaScript.
	// if that doesn't work, provide a manual link
	//
	$s_text = GetMessage(MSG_PLSWAIT_REDIR) . "\n\n";
	$s_text .= "<script language=\"JavaScript\" type=\"text/javascript\">";
	$s_text .= "window.location.href = '$url';";
	$s_text .= "</script>";
	$s_text .= "\n\n" . GetMessage(MSG_IFNOT_REDIR,array("URL" => $url));
	CreatePage($s_text,$title);
	FormMailExit();
}

/**
 * Remove 'javascript:' from the given text.
 *
 * @param string $s_text the text to be modified
 *
 * @return string|string[]
 */
function stripJS($s_text)
{
	return str_ireplace('javascript:','',$s_text);
}

class   JSON
{
	function _Format($m_val)
	{
		if (is_bool($m_val)) {
			$s_value = ($m_val) ? "true" : "false";
		} elseif (is_string($m_val)) {
			//
			// convert literal line breaks into JavaScript escape sequences
			//
			$s_value = '"' . str_replace(array("\r","\n"),array('\\r','\\n'),addslashes($m_val)) . '"';
		} elseif (is_numeric($m_val)) {
			$s_value = $m_val;
		} elseif (is_array($m_val)) {
			$s_value = $this->_FormatArray($m_val);
		} else {
			$s_value = "null";
		}
		return ($s_value);
	}

	function _FormatArray($a_array)
	{
		if ($this->_IsNumericArray($a_array)) {
			$a_values = array();
			foreach ($a_array as $m_val) {
				$a_values[] = $this->_Format($m_val);
			}
			$s_value = "[" . implode(",",$a_values) . "]";
		} else {
			//
			// associative arrays are objects
			//
			$s_value = $this->MakeObject($a_array);
		}
		return ($s_value);
	}

	//
	// check if we have a numeric array or an associative array
	// numeric arrays may have holes; numeric array indexes must
	// be integers
	//
	function _IsNumericArray($a_data)
	{
		if (empty($a_data)) {
			return (true);
		} // empty array - treat as numeric
		//
		// check all the keys for numeric
		//
		$a_keys = array_keys($a_data);
		foreach ($a_keys as $m_index) {
			if (!is_int($m_index)) {
				return (false);
			}
		}
		return (true);
	}

	function MakeObject($a_data)
	{
		$a_members = array();
		foreach ($a_data as $s_key => $m_val) {
			$a_members[] = '"' . $s_key . '":' . $this->_Format($m_val);
		}
		return ("{" . implode(",",$a_members) . "}");
	}
}

function CORS_Response()
{
	header('Access-Control-Allow-Origin: *');
	header('Access-Control-Max-Age: 36000');
	header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
	header('Access-Control-Allow-Headers: X-Requested-With');
}

function JSON_Result($s_result,$a_data = array())
{
	global $aGetVars;

	FMDebug("Sending JSON_Result: $s_result");
	$a_data["Result"] = $s_result;
	$json             = new JSON();
	$s_ret            = $json->MakeObject($a_data);
	CORS_Response();
	//
	// handle JSONP request
	//
	if (isset($aGetVars['callback']) && $aGetVars['callback'] != '') {
		header('Content-Type: text/javascript; charset=utf-8');
		$s_ret = $aGetVars['callback'] . "($s_ret);";
		FMDebug('JSONP request callback=' . $aGetVars['callback']);
	} else {
		header('Content-Encoding: utf-8');
		header('Content-Type: application/json; charset=utf-8');
	}
	FMDebug("JSON_Result output: " . $s_ret);
	echo $s_ret;
}

//
// JoinLines is just like "implode" except that it checks
// the end of each array for the separator already being
// there. This allows us to join a mixture of mail
// header lines (already terminated) with body lines.
// This logic works if HEAD_CRLF, for example, is the same
// as BODY_LF (i.e. both "\r\n") or if BODY_LF is the
// same as the last character in HEAD_CRLF (i.e.
// HEAD_CRLF = "\r\n" and BODY_LF = "\n").
// Other value combinations may break things.
//
function JoinLines($s_sep,$a_lines)
{
	$s_str = "";
	if (($i_sep_len = strlen($s_sep)) == 0)
		//
		// no separator
		//
	{
		return (implode("",$a_lines));
	}
	$n_lines = count($a_lines);
	for ($ii = 0 ; $ii < $n_lines ; $ii++) {
		$s_line = $a_lines[$ii];
		if (substr($s_line,-$i_sep_len) == $s_sep) {
			$s_str .= $s_line;
		} else {
			$s_str .= $s_line;
			//
			// don't append a separator to the last line
			//
			if ($ii < $n_lines - 1) {
				$s_str .= $s_sep;
			}
		}
	}
	return ($s_str);
}

//
// Re-orders an array of email headers into the
// order recommended by RFC822, section 4.1:
//      It  is  recommended that,  if  present,
//      headers be sent in the order "Return-
//      Path", "Received", "Date",  "From",  "Subject",
//      "Sender", "To", "cc", etc.
//
// Note that RFC822 is obsoleted by RFC2822 and
// the latter states that field order doesn't
// matter (except for some tracing fields).
// However, a FormMail user reported that Yahoo doesn't like
// email where the CC header appears before the From
// header. So, as always, we try to work with broken
// servers too...
//
// Returns an array indexed by the require numerical
// order.  Each element is an array containing the
// header value (name,value pair).
//
function OrderHeaders($a_headers)
{
	//
	// we list the headers we're responsible for
	// in the order suggested
	//
	$a_ordering        = array("From","Subject","To","Cc","Bcc","Reply-To");
	$a_ordered_headers = array();
	foreach ($a_ordering as $s_name) {
		if (isset($a_headers[$s_name])) {
			$a_ordered_headers[] = array($s_name => $a_headers[$s_name]);
			unset($a_headers[$s_name]);
		}
	}
	//
	// now add in the remaining headers
	//
	foreach ($a_headers as $s_name => $s_value) {
		$a_ordered_headers[] = array($s_name => $a_headers[$s_name]);
	}
	return ($a_ordered_headers);
}

//
// Makes a mail header field body "safe".
// This simply places a backslash in front of every double-quote.
// There's probably more we could do if required, but this
// attempts to provide the same protection that was in the first
// version of FormMail.  In that version, every incoming
// field had double-quotes replaced with single quotes.
// That processing is no longer performed, and this
// function is used to protect against potential attacks in
// header fields - not by replacing double quotes with single quotes,
// but by using the backslash "quoting" feature of RFC2822.
//
// This code could be improved by parsing the header and rewriting
// it to be valid, possibly removing junk.
//
// That's a lot of code, though!
//
function SafeHeader($s_str)
{
	return (str_replace('"','\\"',$s_str));
}

//
// makes a string safe to put as words in a header
//
function SafeHeaderWords($s_str)
{
	//
	// We zap various characters and replace them with a question mark.
	// Also, we don't handle quoted strings, which are valid words.
	//
	$s_specials = '()<>@,;:\\".[]'; // special characters defined by RFC822
	$s_str      = preg_replace('/[[:cntrl:]]+/',"?",$s_str); // zap all control chars
	$s_str      = preg_replace("/[" . preg_quote($s_specials,"/") . "]/","?",$s_str); // zap all specials
	return ($s_str);
}

//
// makes a string safe to put as a quoted string in a header
//
function SafeHeaderQString($s_str)
{
	return (str_replace('"','\\"',
	                    str_replace("\\","\\\\",
	                                str_replace("\r"," ",
	                                            str_replace("\r\n"," ",$s_str)))));
}

//
// makes a string safe to put in a header comment
//
function SafeHeaderComment($s_str)
{
	return (str_replace("(","\\(",
	                    str_replace(")","\\)",
	                                str_replace("\\","\\\\",
	                                            str_replace("\r"," ",
	                                                        str_replace("\r\n"," ",$s_str))))));
}

//
// makes a string safe to put in a header as an email address
//
function SafeHeaderEmail($s_str)
{
	//
	// An email address is made up of local and domain parts
	// each of these is made up of "words" separated by "."
	// each "word" can be a sequence of characters excluding
	// specials, space and control characters OR it can be
	// a quoted string.
	//
	// The correct processing would be to completely
	// parse the address, strip junk, double-quote
	// words that need to be turned into quote strings,
	// and return a well-formed email address.
	//
	// That's a lot of code!
	//
	// So, instead, we opt for stripping out control characters.
	//
	$s_str = preg_replace('/[[:cntrl:]]+/',"",$s_str); // zap all control chars
	return ($s_str);
}

//
// Expands an array of mail headers into mail header lines.
//
function ExpandMailHeaders($a_headers,$b_fold = false)
{
	$s_hdrs            = "";
	$a_ordered_headers = OrderHeaders($a_headers);
	for ($ii = 0 ; $ii < count($a_ordered_headers) ; $ii++) {
		foreach ($a_ordered_headers[$ii] as $s_name => $s_value) {
			if ($s_name != "") {
				if ($s_hdrs != "") {
					$s_hdrs .= Settings::get('HEAD_CRLF');
				}
				if ($b_fold) {
					$s_hdrs .= HeaderFolding($s_name . ": " . $s_value);
				} else {
					$s_hdrs .= $s_name . ": " . $s_value;
				}
			}
		}
	}
	//FMDebug("Headers are: $s_hdrs");
	return ($s_hdrs);
}

//
// Expands an array of mail headers into an array containing header lines.
//
function ExpandMailHeadersArray($a_headers)
{
	$a_hdrs            = array();
	$a_ordered_headers = OrderHeaders($a_headers);
	for ($ii = 0 ; $ii < count($a_ordered_headers) ; $ii++) {
		foreach ($a_ordered_headers[$ii] as $s_name => $s_value) {
			if ($s_name != "") {
				$a_hdrs[] = $s_name . ": " . $s_value . Settings::get('HEAD_CRLF');
			}
		}
	}
	return ($a_hdrs);
}

//
// Low-level email send function; either calls PHP's mail function
// or uses the PEAR Mail object.
// NOTE: for some errors, there's no point trying to email
// an alert message!  So, in these cases, we just display the error to
// the user.
// $s_options are ignored for PEAR sending.
//
function DoMail($s_to,$s_subject,$s_mesg,$a_headers,$s_options)
{
	global $ALT_MAIL_FUNCTION;

	if ($ALT_MAIL_FUNCTION !== '') {
		return ($ALT_MAIL_FUNCTION($s_to,$s_subject,$s_mesg,$a_headers,$s_options));
	} else {
		//
		// Encode the subject line.
		// Ideally, we want to encode the relevant parts of To, From, Cc,
		// Reply-To, and this is the right place to do it.
		// However, it's another 1000 lines of code!
		// So, we must compromise the code quality because of this cost.
		// We encode subject here, and we encode the From line where it's
		// created.  The rest remain for a future version where code size
		// can be controlled.
		//
		$s_subject = EncodeHeaderText($s_subject);
		if (!Settings::isEmpty('PEAR_SMTP_HOST')) {
			//
			// Note that PEAR Mail seems to take responsibility for header line folding
			//
			require_once("Mail.php");

			$a_params = array("host" => Settings::get('PEAR_SMTP_HOST'),
			                  "port" => Settings::get('PEAR_SMTP_PORT')
			);
			if (!Settings::isEmpty('PEAR_SMTP_USER')) {
				$a_params["auth"]     = TRUE;
				$a_params["username"] = Settings::get('PEAR_SMTP_USER');
				$a_params["password"] = Settings::get('PEAR_SMTP_PWD');
			}
			$mailer = Mail::factory("smtp",$a_params);
			if (!is_object($mailer)) {
				ShowError("pear_error",GetMessage(MSG_PEAR_OBJ),FALSE,FALSE);
				FormMailExit();
			}
			if (strtolower(get_class($mailer)) === 'pear_error') {
				ShowError("pear_error",$mailer->getMessage(),FALSE,FALSE);
				FormMailExit();
			}
			if (!isset($a_headers['To']) && !isset($a_headers['to'])) {
				$a_headers['To'] = SafeHeader($s_to);
			}
			if (!isset($a_headers['Subject']) && !isset($a_headers['subject'])) {
				$a_headers['Subject'] = SafeHeader($s_subject);
			}
			$res = $mailer->send($s_to,$a_headers,$s_mesg);
			if ($res === TRUE) {
				return (TRUE);
			}

			global $aAlertInfo;

			$aAlertInfo[] = GetMessage(MSG_PEAR_ERROR,array("MSG" => $res->getMessage()));
			return (FALSE);
		} else {
			//$s_subject = HeaderFolding($s_subject,RFCLINELEN-10);   // "Subject: " is about 10 chars
			//
			// Notes from Feb 2010....
			//
			// PHP's mail function (tested in version 5.2.6) does folding of the
			// To line and the Subject line.
			// If we do it, then things break.
			//
			// This area is quite confusing.  It's not clear whether the script
			// should be folding header lines or whether the MTA should do it.
			// We *do know* (as stated above) that folding To and Subject breaks things.
			//
			// But folding other header lines properly, seems to be OK.
			//
			// However, for years FormMail never did header line folding (except for the
			// soft line breaks inserted by the quoted_printable_encode function we had used),
			// and we didn't seem to get any reports of breakage (except for problems
			// with the quoted_printable_encode soft line breaks!).
			//
			// So, even though we've implemented all the code for header line folding,
			// we'll not use it.
			// No header line folding will be performed in version 8.22 onwards.
			//
			if ($s_options !== "") {
				return (mail($s_to,$s_subject,$s_mesg,ExpandMailHeaders($a_headers),$s_options));
			} else {
				return (mail($s_to,$s_subject,$s_mesg,ExpandMailHeaders($a_headers)));
			}
		}
	}
}

//
// Send an email
//
function SendCheckedMail($to,$subject,$mesg,$sender,$a_headers = array())
{

	$b_f_option    = false;
	$b_form_option = IsMailOptionSet("SendMailFOption"); // this is superseded, but still supported
	if (Settings::get('SENDMAIL_F_OPTION') || $b_form_option) {
		if (empty($sender)) {
			//
			// SENDMAIL_F_OPTION with no sender is silently ignored
			//
			if ($b_form_option) {
				//
				// form has specified SendMailFOption, but there's no
				// sender address
				//
				static $b_in_here = false;
				global $SERVER;

				if (!$b_in_here) // prevent infinite recursion
				{
					$b_in_here = true;
					SendAlert(GetMessage(MSG_NO_FOPT_ADDR));
					$b_in_here = false;
				}
				//
				// if there's no from address, create a dummy one
				//
				$sender            = "dummy@" . (isset($SERVER) ? $SERVER : "UnknownServer");
				$a_headers['From'] = $sender;
				$b_f_option        = true;
			}
		} else {
			$b_f_option = true;
		}
	}
	if (Settings::get('INI_SET_FROM') && !empty($sender)) {
		ini_set('sendmail_from',$sender);
	}

	return (DoMail($to,$subject,$mesg,$a_headers,($b_f_option ? "-f$sender" : "")));
}

//
// Send an alert email, but not if ATTACK_DETECTION_IGNORE_ERRORS is true.
//
function SendAlertIgnoreSpam($s_error,$b_filter = true,$b_non_error = false)
{
	if (!Settings::get('ATTACK_DETECTION_IGNORE_ERRORS')) {
		SendAlert($s_error,$b_filter,$b_non_error);
	}
}

//
// Send an alert email
//
function SendAlert($s_error,$b_filter = true,$b_non_error = false,$s_addl_subject = '')
{
	global $SPECIAL_VALUES,$FORMATTED_INPUT,$aServerVars,$aStrippedFormVars;
	global $aAlertInfo,$aCleanedValues,$aFieldOrder,$sHTMLCharSet;

	//
	// The following initialisations are so that ShowError can run from fmhookpreinit scripts
	//
	$aSpecialValues         = isset($SPECIAL_VALUES) ? $SPECIAL_VALUES : array();
	$aFormattedInputValues  = isset($FORMATTED_INPUT) ? $FORMATTED_INPUT : array();
	$aServerVarValues       = isset($aServerVars) ? $aServerVars : array();
	$aStrippedFormVarValues = isset($aStrippedFormVars) ? $aStrippedFormVars : array();
	$aAlertInfoValues       = isset($aAlertInfo) ? $aAlertInfo : array();
	$aCleaned               = isset($aCleanedValues) ? $aCleanedValues : array();
	$aFieldOrderValues      = isset($aFieldOrder) ? $aFieldOrder : array();

	$s_error      = str_replace("\n",Settings::get('BODY_LF'),$s_error);
	$b_got_filter = GetFilterSpec($s_filter_name,$a_filter_list);

	//
	// if there is a filter specified and we're not sending the alert
	// through the filter, don't show the user's data.  This is
	// on the assumption that the filter is an encryption program; so,
	// we don't want to send the user's data in clear text inside the
	// alerts.
	//
	$b_show_data = true;
	if ($b_got_filter && !$b_filter) {
		$b_show_data = false;
	}

	$s_form_subject = $s_alert_to = "";
	$b_check        = true;
	//
	// might be too early to have $aSpecialValues set, so
	// look in the form vars too
	//
	if (isset($aSpecialValues["alert_to"])) {
		$s_alert_to = trim($aSpecialValues["alert_to"]);
	}
	if (empty($s_alert_to) && isset($aStrippedFormVarValues["alert_to"])) {
		$s_alert_to = trim($aStrippedFormVarValues["alert_to"]);
	}

	if (isset($aSpecialValues["subject"])) {
		$s_form_subject = trim($aSpecialValues["subject"]);
	}
	if (empty($s_form_subject) && isset($aStrippedFormVarValues["subject"])) {
		$s_form_subject = trim($aStrippedFormVarValues["subject"]);
	}

	if (empty($s_alert_to)) {
		$s_alert_to = Settings::get('DEF_ALERT');
		$b_check    = false;
	}
	if (!empty($s_alert_to)) {
		$s_from_addr = $s_from = "";
		$a_headers   = array();
		if (!Settings::isEmpty('FROM_USER')) {
			if (Settings::get('FROM_USER') != "NONE") {
				$a_headers['From'] = Settings::get('FROM_USER');
				$s_from_addr       = Settings::get('FROM_USER');
				$s_from            = "From: $s_from_addr";
			}
		} else {
			global $SERVER;

			$s_from_addr       = "FormMail@" . $SERVER;
			$a_headers['From'] = $s_from_addr;
			$s_from            = "From: $s_from_addr";
		}
		$s_mesg = "To: " . UnMangle($s_alert_to) . Settings::get('BODY_LF');
		//
		// if a language pack has been included, a lot of error messages
		// may need the character set to be provided.
		// If that's available from the language pack, use it,
		// otherwise, if it's a mail_option, use it from there.
		//
		$s_charset = "";
		if (isset($sHTMLCharSet) && $sHTMLCharSet !== "") {
			$s_charset = $sHTMLCharSet;
		} else {
			if (IsMailOptionSet("CharSet")) {
				$s_charset = GetMailOption("CharSet");
			}
		}

		//
		// Alerts are plain text emails, so convert any HTML entities
		// back to their original characters.  Note, this will only work on PHP
		// version 4.3.0 and above.
		//
		if (function_exists("html_entity_decode")) {
			$s_error = @html_entity_decode($s_error,ENT_COMPAT,$s_charset);
		}

		if ($s_charset !== "") {
			$a_headers['Content-Type'] = SafeHeader("text/plain; charset=$s_charset");
		}

		if (!empty($s_from)) {
			$s_mesg .= $s_from . Settings::get('BODY_LF');
		}
		$s_mesg .= Settings::get('BODY_LF');
		if (count($aAlertInfoValues) > 0) {
			if ($b_show_data) {
				$s_error .= Settings::get('BODY_LF') . GetMessage(MSG_MORE_INFO) . Settings::get('BODY_LF');
				$s_error .= implode(Settings::get('BODY_LF'),$aAlertInfoValues);
			} else {
				$s_error .= Settings::get('BODY_LF') . GetMessage(MSG_INFO_STOPPED) . Settings::get('BODY_LF');
			}
		}
		//
		// some fields aren't security issues - show those in the alert
		//
		$a_safe_fields = array(
			"email: " . isset($aSpecialValues["email"]) ? $aSpecialValues["email"] : '',
			"realname: " . isset($aSpecialValues["realname"]) ? $aSpecialValues["realname"] : '',
		);
		$s_safe_data   = implode(Settings::get('BODY_LF'),$a_safe_fields);

		if ($b_non_error) {
			$s_preamble = $s_error . Settings::get('BODY_LF') . Settings::get('BODY_LF');
			$s_mesg     .= $s_preamble;
			$s_subj     = GetMessage(MSG_FM_ALERT) . ($s_addl_subject !== '' ? " - $s_addl_subject" : '');
			if (!empty($s_form_subject)) {
				$s_subj .= " ($s_form_subject)";
			}
		} else {
			$s_preamble = GetMessage(MSG_FM_ERROR_LINE) . Settings::get('BODY_LF') .
			              $s_error . Settings::get('BODY_LF') . Settings::get('BODY_LF');
			$s_mesg     .= $s_preamble;
			$s_subj     = GetMessage(MSG_FM_ERROR) . ($s_addl_subject !== '' ? " - $s_addl_subject" : '');
			if (!empty($s_form_subject)) {
				$s_subj .= " ($s_form_subject)";
			}
			$s_mesg .= $s_safe_data;
			$s_mesg .= Settings::get('BODY_LF') . Settings::get('BODY_LF');
			if ($b_show_data) {
				$s_mesg .= implode(Settings::get('BODY_LF'),$aFormattedInputValues);
			} else {
				$s_mesg .= GetMessage(MSG_USERDATA_STOPPED);
			}
		}

		/*
             * We only need to filter the form fields if the filter that
             * is specified is an encrypting filter.
             */
		if ($b_filter && $b_got_filter &&
		    IsFilterAttribSet($aSpecialValues["filter"],"Encrypts")
		) {
			$s_new_mesg = $s_preamble . $s_safe_data;
			$s_new_mesg .= Settings::get('BODY_LF') . Settings::get('BODY_LF');

			if ($a_filter_list !== false) {
				//
				// just filter the critical fields
				//
				list($s_unfiltered,$s_filtered_results) =
					GetFilteredOutput($aFieldOrderValues,$aCleaned,
					                  $s_filter_name,$a_filter_list);
				$s_new_mesg .= $s_unfiltered;
			} else {
				//
				// filter everything
				//
				$s_filtered_results = Filter($s_filter_name,$s_mesg);
			}
			$s_new_mesg .= GetMessage(MSG_FILTERED,array("FILTER" => $s_filter_name)) .
			               Settings::get('BODY_LF') . Settings::get('BODY_LF') .
			               $s_filtered_results;
			$s_mesg     = $s_new_mesg;
		}
		$s_mesg .= Settings::get('BODY_LF');

		if (isset($aServerVarValues['HTTP_REFERER'])) {
			$s_mesg .= "Referring page was " . $aServerVarValues['HTTP_REFERER'];
		} elseif (isset($aSpecialValues['this_form']) && $aSpecialValues['this_form'] !== "") {
			$s_mesg .= "Referring form was " . $aSpecialValues['this_form'];
		}

		$s_mesg .= Settings::get('BODY_LF');

		if (isset($aServerVarValues['SERVER_NAME'])) {
			$s_mesg .= "SERVER_NAME was " . $aServerVarValues['SERVER_NAME'] . Settings::get('BODY_LF');
		}
		if (isset($aServerVarValues['REQUEST_URI'])) {
			$s_mesg .= "REQUEST_URI was " . $aServerVarValues['REQUEST_URI'] . Settings::get('BODY_LF');
		}

		$s_mesg .= Settings::get('BODY_LF');

		if (isset($aServerVarValues['REMOTE_ADDR'])) {
			$s_mesg .= "User IP address was " . $aServerVarValues['REMOTE_ADDR'] . Settings::get('BODY_LF');
		}
		if (isset($aServerVarValues['HTTP_USER_AGENT'])) {
			$s_mesg .= "User agent was " . $aServerVarValues['HTTP_USER_AGENT'] . Settings::get('BODY_LF');
		}

		if ($b_check) {
			if (CheckEmailAddress($s_alert_to,$s_valid,$s_invalid)) {
				return (SendCheckedMail($s_valid,$s_subj,$s_mesg,$s_from_addr,$a_headers));
			}
		} else {
			return (SendCheckedMail($s_alert_to,$s_subj,$s_mesg,$s_from_addr,$a_headers));
		}
	}
	return (false);
}

//
// Read the lines in a file and return an array.
// Each line is stripped of line termination characters.
//
function ReadLines($fp)
{
	$a_lines = array();
	while (!feof($fp)) {
		$s_line = fgets($fp,4096);
		//
		// strip carriage returns and line feeds
		//
		$s_line    = str_replace("\r","",$s_line);
		$s_line    = str_replace("\n","",$s_line);
		$a_lines[] = $s_line;
	}
	return ($a_lines);
}

//
// Open a URL and return the data from it as a string or array of lines.
// Returns false on failure ($s_error has the error string)
//
function GetURL($s_url,&$s_error,$b_ret_lines = false,$n_depth = 0)
{
	global $php_errormsg,$aServerVars,$sUserAgent,$ExecEnv;

	//
	// open the URL with the same session as we have
	//
	if ($ExecEnv->allowSessionURL()) {
		if (session_id() !== "") {
			$s_url = AddURLParams($s_url,session_name() . "=" . urlencode(session_id()));
		}
		if (defined("SID")) {
			$s_url = AddURLParams($s_url,SID);
		}
	}

	$http_get = new HTTPGet($s_url);

	//
	// Determine authentication requirements
	//
	if (Settings::get('AUTHENTICATE') !== "" || Settings::get('AUTH_USER') !== "" || Settings::get('AUTH_PW') !== "") {
		if (Settings::get('AUTHENTICATE') === "") {
			$http_get->SetAuthentication("Basic",Settings::get('AUTH_USER'),Settings::get('AUTH_PW'));
		} else {
			$http_get->SetAuthenticationLine(Settings::get('AUTHENTICATE'));
		}
	} else {
		$a_parts = $http_get->GetURLSplit();
		if (isset($a_parts["user"]) || isset($a_parts["pass"])) {
			$s_auth_user = isset($a_parts["user"]) ? $a_parts["user"] : "";
			$s_auth_pass = isset($a_parts["pass"]) ? $a_parts["pass"] : "";
		} else {
			$s_auth_type = isset($aServerVars["PHP_AUTH_TYPE"]) ? $aServerVars["PHP_AUTH_TYPE"] : "";
			$s_auth_user = isset($aServerVars["PHP_AUTH_USER"]) ? $aServerVars["PHP_AUTH_USER"] : "";
			$s_auth_pass = isset($aServerVars["PHP_AUTH_PW"]) ? $aServerVars["PHP_AUTH_PW"] : "";
		}
		if (!isset($s_auth_type) || $s_auth_type === "") {
			$s_auth_type = "Basic";
		}
		if ($s_auth_user !== "" || $s_auth_pass !== "") {
			$http_get->SetAuthentication($s_auth_type,$s_auth_user,$s_auth_pass);
		}
	}
	//
	// set the user agent
	//
	$http_get->SetAgent($sUserAgent);
	//
	// resolve the name now so the DNS cache can be written to the session
	//
	$http_get->Resolve();

	//
	// Since we might be opening a URL within the same session, we can
	// get locks.  So, close the session for writing to prevent this.
	//
	$b_closed = false;
	if (function_exists('session_write_close')) {
		session_write_close();
		$b_closed = true;
		//ob_flush();             // this prevents automatic redirects if $TEMPLATEURL
		// is in use and JavaScript is switched off
	}

	$m_buf = FALSE;

	//FMDebug("Begin read");
	if (($a_lines = $http_get->Read()) === FALSE) {
		$http_get->Close();
		//
		// get the error code and send the appropriate alert
		//
		list($i_error,$i_sys_err,$s_sys_msg) = $http_get->GetError();
		switch ($i_error) {
			case $http_get->nErrParse:
				$s_error = GetMessage(MSG_URL_PARSE);
				break;
			case $http_get->nErrScheme:
				$a_parts = $http_get->GetURLSplit();
				$s_error = GetMessage(MSG_URL_SCHEME,array("SCHEME" => $a_parts["scheme"]));
				break;
			default:
				$s_error = GetMessage(MSG_SOCKET,
				                      array("ERRNO"  => $i_sys_err,
				                            "ERRSTR" => $s_sys_msg,
				                            "PHPERR" => isset($php_errormsg) ? $php_errormsg : ""
				                      ));
				break;
		}
	} else {
		$http_get->Close();
		//
		// check the HTTP response for actual status.  Anything outside
		// 200-299 is a failure, but we also handle redirects.
		//
		list($i_http_code,$s_http_status) = $http_get->GetHTTPStatus();

		if ($i_http_code < 200 || $i_http_code > 299) {
			switch ($i_http_code) {
				case 300: // multiple choices (we'll take the first)
				case 301: // moved permanently
				case 302: // found
				case 303: // see other
					/** @noinspection PhpMissingBreakStatementInspection */
				case 307: // temporary redirect
					//
					// a "location" header must be present for us to continue
					// In the case of infinite redirects, we need to stop.
					// So, we limit to a maximum of 10 redirects.
					//
					if ($n_depth < 10) {
						if (($s_location = $http_get->FindHeader("location")) !== false) {
							FMDebug("Redirect from '$s_url' to '$s_location'");
							$m_buf    = GetURL($s_location,$s_error,$b_ret_lines,$n_depth + 1);
							$b_closed = false;
							break;
						}
						FMDebug("Redirect FAILED - no location header");
					} else {
						FMDebug("Redirect FAILED depth=$n_depth");
					}
				// FALL THRU
				default:
					$s_error = GetMessage(MSG_GETURL_OPEN,array("STATUS" => $s_http_status,"URL" => $s_url));
					break;
			}
		} elseif ($b_ret_lines) {
			$m_buf = $a_lines;
		} else
			//
			// return lines as one big string buffer
			//
		{
			$m_buf = implode("",$a_lines);
		}
	}
	//
	// re-open our session
	//
	if ($b_closed) {
		session_start();
	}

	return ($m_buf);
}

//
// Write to the debug log if it exists and is writable.
//
function FMDebug($s_mesg)
{
	static $fDebug = NULL;

	if (!isset($fDebug)) {
		$fDebug    = false; // only initialize once
		$s_db_file = "fmdebug.log"; // look for debug file in current directory
		//
		// we only open an existing file - we don't create one
		//
		if (file_exists($s_db_file)) {
			if (($fDebug = fopen($s_db_file,"a")) === false) {
				return;
			}
		}
	}
	if ($fDebug !== false) {
		fwrite($fDebug,date('r') . ": " . $s_mesg . "\n");
		fflush($fDebug);
	}
}

/*
 * Class:       NetIO
 * Description:
 *  A class to provide internet input/output capabilities.
 *  Use as a base class for more specific functions.
 */

class   NetIO
{
	var $_sHost;
	var $_iPort;
	var $_sPrefix;

	var $_iConnTimeout;
	var $_fSock;

	var $_aIPs;

	var $_iError = 0;
	var $_iSysErr;
	var $_sSysMesg;

	var $nErrInit       = -1; // not initialized
	var $nErrRead       = -2; // read error
	var $nErrWrite      = -3; // write error
	var $nErrWriteShort = -4; // failed to write all bytes

	var $nErrSocket = -100; // error in socket open

	function __construct($s_host = NULL,$i_port = NULL,$s_prefix = "")
	{
		if (isset($s_host)) {
			$this->_sHost = $s_host;
		}
		if (isset($i_port)) {
			$this->_iPort = $i_port;
		}
		$this->_sPrefix      = $s_prefix;
		$this->_iConnTimeout = 30;
		$this->_iSysErr      = 0;
		$this->_sSysMesg     = "";
	}

	function _SetError($i_error,$i_sys_err = 0,$s_sys_mesg = "")
	{
		$this->_iError   = $i_error;
		$this->_iSysErr  = $i_sys_err;
		$this->_sSysMesg = $s_sys_mesg;
		return (FALSE);
	}

	function IsError()
	{
		return $this->_iError !== 0;
	}

	function ClearError()
	{
		$this->_SetError(0);
	}

	function GetError()
	{
		return (array($this->_iError,$this->_iSysErr,$this->_sSysMesg));
	}

	function SetHost($s_host)
	{
		$this->_sHost = $s_host;
	}

	function SetPort($i_port)
	{
		$this->_iPort = $i_port;
	}

	function SetConnectionTimeout($i_secs)
	{
		$this->_iConnTimeout = $i_secs;
	}

	function SetPrefix($s_prefix)
	{
		$this->_sPrefix = $s_prefix;
	}

	function GetHost()
	{
		return (isset($this->_sHost) ? $this->_sHost : "");
	}

	function GetPort()
	{
		return (isset($this->_iPort) ? $this->_iPort : 0);
	}

	function GetPrefix()
	{
		return ($this->_sPrefix);
	}

	function GetConnectionTimeout()
	{
		return ($this->_iConnTimeout);
	}

	function _CacheIt()
	{
		FMDebug("Caching " . implode(",",$this->_aIPs));
		if (IsSetSession("FormNetIODNSCache")) {
			$a_cache = GetSession("FormNetIODNSCache");
		} else {
			$a_cache = array();
		}
		$a_cache[$this->_sHost] = $this->_aIPs;
		SetSession("FormNetIODNSCache",$a_cache);
	}

	/*
     * Some versions of PHP seem to have a major slowdown when resolving
     * names with gethostbyname (5 seconds with PHP 4.3.9).
     * So, in the case of multi-page forms using MULTIFORMURL, we get a big speed up
     * by caching the IP address of the server.
     */
	function _CheckCache()
	{
		if (!IsSetSession("FormNetIODNSCache")) {
			return (FALSE);
		}
		$a_cache = GetSession("FormNetIODNSCache");
		if (!is_array($a_cache) || !isset($a_cache[$this->_sHost]) || !is_array($a_cache[$this->_sHost])) {
			return (FALSE);
		}
		$this->_aIPs = $a_cache[$this->_sHost];
		return (TRUE);
	}

	function Resolve()
	{
		$this->ClearError();
		if (!isset($this->_sHost)) {
			return ($this->_SetError($this->nErrInit));
		}
		if ($this->_CheckCache()) {
			return (TRUE);
		}
		FMDebug("Start resolve of " . $this->_sHost);
		//
		// if host is an actual IP address, then it is returned unchanged, which is good!
		//
		if (($a_ip_list = gethostbynamel($this->_sHost)) === FALSE) {
			FMDebug("Resolve failed");
			return ($this->_SetError($this->nErrInit,0,
			                         GetMessage(MSG_RESOLVE,array("NAME" => $this->_sHost))));
		}
		FMDebug("Done resolve: " . implode(",",$a_ip_list));
		$this->_aIPs = $a_ip_list;
		$this->_CacheIt();
		return (TRUE);
	}

	function _SSLOpen($s_ip,&$errno,&$errstr,$i_timeout)
	{
		global $ExecEnv;

		FMDebug("Using _SSLOpen (stream_socket_client), SNI, host=" . $this->GetHost());
		$context = stream_context_create();
		$result  = stream_context_set_option($context,'ssl','verify_host',true);
		$result  = stream_context_set_option($context,'ssl','verify_peer',false);
		$result  = stream_context_set_option($context,'ssl','allow_self_signed',true);
		$result  = stream_context_set_option($context,'ssl','SNI_enabled',true);
		if ($ExecEnv->IsPHPAtLeast("5.6.0")) {
			$result = stream_context_set_option($context,'ssl','peer_name',$this->GetHost());
		} else {
			$result = stream_context_set_option($context,'ssl','SNI_server_name',$this->GetHost());
		}
		//
		// Note that even if SNI fails, the socket will still open, but the
		// web server should send a 400 error.
		//
		return (stream_socket_client($this->GetPrefix() . $s_ip . ":" . $this->GetPort(),
		                             $errno,$errstr,$i_timeout,STREAM_CLIENT_CONNECT,$context));
	}

	/** @noinspection PhpUndefinedVariableInspection */
	function Open()
	{
		$this->ClearError();
		if (!isset($this->_sHost) || !isset($this->_iPort)) {
			return ($this->_SetError($this->nErrInit));
		}
		if (!$this->Resolve()) {
			return (FALSE);
		}
		FMDebug("Starting socket open");
		$f_sock = FALSE;
		//
		// Now, run through the list of IPs until we find one that connects.
		// However, this can cause problems with SNI in SSL/TLS connections.
		// If there is only one IP address, use the host name.
		// Otherwise, if we can specify SNI and it's an SSL connection
		// use streams, otherwise try each IP individually.
		//
		if (count($this->_aIPs) == 1) {
			FMDebug("Trying host " . $this->_sHost . ", timeout " . $this->GetConnectionTimeout());
			$f_sock = @fsockopen($this->GetPrefix() . $this->_sHost,$this->GetPort(),
			                     $errno,$errstr,$this->GetConnectionTimeout());
		} else {
			foreach ($this->_aIPs as $s_ip) {
				global $ExecEnv;

				FMDebug("Trying IP $s_ip, timeout " . $this->GetConnectionTimeout());
				if ($ExecEnv->IsPHPAtLeast("5.3.2") && substr($this->GetPrefix(),0,3) == "ssl") {
					if (($f_sock = $this->_SSLOpen($s_ip,$errno,$errstr,
					                               $this->GetConnectionTimeout())) !== FALSE
					) {
						break;
					}
				} elseif (($f_sock = @fsockopen($this->GetPrefix() . $s_ip,$this->GetPort(),
				                                $errno,$errstr,$this->GetConnectionTimeout())) !== FALSE
				) {
					break;
				}
			}
		}
		if ($f_sock === FALSE) {
			FMDebug("open failed: $errno $errstr");
			return ($this->_SetError($this->nErrSocket,$errno,$errstr));
		}
		$this->_fSock = $f_sock;
		FMDebug("Done socket open");
		return (TRUE);
	}

	function Read()
	{
		$this->ClearError();
		$a_lines = array();
		while (($s_line = fgets($this->_fSock)) !== FALSE) {
			$a_lines[] = $s_line;
		}
		FMDebug("Read " . count($a_lines) . " lines");
		return ($a_lines);
	}

	function Write($s_str,$b_flush = TRUE)
	{
		$this->ClearError();
		if (!isset($this->_fSock)) {
			return ($this->_SetError($this->nErrInit));
		}
		if (($n_write = fwrite($this->_fSock,$s_str)) === FALSE) {
			return ($this->_SetError($this->nErrWrite));
		}
		if ($n_write != strlen($s_str)) {
			return ($this->_SetError($this->nErrWriteShort));
		}
		if ($b_flush) {
			if (fflush($this->_fSock) === FALSE) {
				return ($this->_SetError($this->nErrWriteShort));
			}
		}
		return (TRUE);
	}

	function Close()
	{
		if (isset($this->_fSock)) {
			fclose($this->_fSock);
			unset($this->_fSock);
		}
	}
}

/*
 * Class:       HTTPGet
 * Description:
 *  A class that implements HTTP GET method.
 */

class   HTTPGet extends NetIO
{
	var $_sURL;
	var $_aURLSplit;

	var $_sRequest;
	var $_aResponse;
	var $_aRespHeaders;

	var $_sAuthLine;
	var $_sAuthType;
	var $_sAuthUser;
	var $_sAuthPass;

	var $_sAgent;

	var $nErrParse  = -1000; // failed to parse URL
	var $nErrScheme = -1001; // unsupported URL scheme

	function __construct($s_url = "")
	{
		parent::__construct();
		$this->_aURLSplit = array();
		if (($this->_sURL = $s_url) !== "") {
			$this->_SplitURL();
		}
	}

	function _SplitURL()
	{
		FMDebug("URL: " . $this->_sURL);
		if (($this->_aURLSplit = parse_url($this->_sURL)) === FALSE) {
			$this->_aURLSplit = array();
			return ($this->_SetError($this->nErrParse));
		}
		return (TRUE);
	}

	function GetURLSplit()
	{
		return ($this->_aURLSplit);
	}

	function SetURL($s_url)
	{
		$this->_aURLSplit = array();
		$this->_sURL      = $s_url;
		return ($this->_SplitURL());
	}

	function _Init()
	{
		if (!isset($this->_aURLSplit["host"])) {
			return ($this->_SetError($this->nErrInit));
		}
		$this->SetHost($this->_aURLSplit["host"]);
		$i_port    = 80;
		$b_use_ssl = false;
		if (isset($this->_aURLSplit["scheme"])) {
			switch (strtolower($this->_aURLSplit["scheme"])) {
				case "http":
					break;
				case "https":
					$b_use_ssl = true;
					$i_port    = 443;
					break;
				default:
					return ($this->_SetError($this->nErrScheme));
			}
		}
		if (isset($this->_aURLSplit["port"])) {
			$i_port = $this->_aURLSplit["port"];
		}
		if ($b_use_ssl)
			//
			// we require ssl:// for port 443
			//
		{
			$this->SetPrefix("ssl://");
		}
		$this->SetPort($i_port);
		return (TRUE);
	}

	function _SendRequest()
	{
		$this->_PrepareRequest();
		return (parent::Write($this->_sRequest));
	}

	function _PrepareRequest($s_method = 'GET')
	{
		FMDebug("Path: " . $this->_aURLSplit["path"]);
		if (!isset($this->_aURLSplit["path"]) || $this->_aURLSplit["path"] === "") {
			$s_path = "/";
		} // default path
		else {
			$s_path = $this->_aURLSplit["path"];
		}
		if (isset($this->_aURLSplit["query"])) {
			//
			// add the query to the path
			// Note that parse_url decodes the query string (urldecode), so
			// we need to split it into its component parameters
			// are re-encode their values.  Calling urlencode($this->_aURLSplit["query"])
			// encodes the '=' between parameters and this breaks things.
			//
			$a_params = explode('&',$this->_aURLSplit["query"]);
			foreach ($a_params as $i_idx => $s_param) {
				if (($i_pos = strpos($s_param,"=")) === false) {
					$a_params[$i_idx] = urlencode($s_param);
				} else {
					$a_params[$i_idx] = substr($s_param,0,$i_pos) . '=' .
					                    urlencode(substr($s_param,$i_pos + 1));
				}
			}
			$s_path .= "?" . implode('&',$a_params);
		}
		//
		// add the fragment to the path.
		//
		if (isset($this->_aURLSplit["fragment"])) {
			$s_path .= '#' . urlencode($this->_aURLSplit["fragment"]);
		}
		//
		// build the request
		//
		$s_req = $s_method . " $s_path HTTP/1.0\r\n";
		//
		// Add authentication
		//
		if (isset($this->_sAuthLine)) {
			$s_req .= "Authorization: $this->_sAuthLine\r\n";
		} elseif (isset($this->_sAuthType)) {
			$s_req .= "Authorization: " . $this->_sAuthType . " " .
			          base64_encode($this->_sAuthUser . ":" . $this->_sAuthPass) . "\r\n";
		}
		//
		// Specify the host name
		//
		$s_req .= "Host: " . $this->GetHost() . "\r\n";
		//
		// Specify the user agent
		//
		if (isset($this->_sAgent)) {
			$s_req .= "User-Agent: " . $this->_sAgent . "\r\n";
		}
		//
		// Accept any output
		// use of concatenation to avoid problems with IDE syntax highlighting.
		//
		$s_req .= "Accept: */" . "*\r\n";
		$s_req .= $this->_AdditionalHeaders();
		//
		// End of request headers
		//
		$s_req           .= "\r\n";
		$this->_sRequest = $s_req;
	}

	function _AdditionalHeaders()
	{
		return ('');
	}

	function _GetResponse()
	{
		FMDebug("Reading");
		if (($a_lines = parent::Read()) === FALSE) {
			return (FALSE);
		}

		$this->_aRespHeaders = $this->_aResponse = array();
		$b_body              = FALSE;
		for ($ii = 0 ; $ii < count($a_lines) ; $ii++) {
			if ($b_body) {
				//FMDebug("Body line: ".rtrim($a_lines[$ii]));
				$this->_aResponse[] = $a_lines[$ii];
			} elseif ($a_lines[$ii] == "\r\n" || $a_lines[$ii] == "\n") {
				$b_body = TRUE;
			} else {
				//FMDebug("Header line: ".rtrim($a_lines[$ii]));
				$this->_aRespHeaders[] = $a_lines[$ii];
			}
		}
		return (TRUE);
	}

	function GetResponseHeaders()
	{
		return ($this->_aRespHeaders);
	}

	function FindHeader($s_name)
	{
		$s_name = strtolower($s_name);
		$i_len  = strlen($s_name);
		for ($ii = 0 ; $ii < count($this->_aRespHeaders) ; $ii++) {
			$s_line = $this->_aRespHeaders[$ii];
			if (($s_hdr = substr($s_line,0,$i_len)) !== false) {
				$s_hdr = strtolower($s_hdr);
				if ($s_hdr === $s_name && substr($s_line,$i_len,1) === ":") {
					return (trim(substr($s_line,$i_len + 1)));
				}
			}
		}
		return (false);
	}

	function GetHTTPStatus()
	{
		$i_http_code = 0;
		$s_status    = "";
		for ($ii = 0 ; $ii < count($this->_aRespHeaders) ; $ii++) {
			$s_line = $this->_aRespHeaders[$ii];
			if (substr($s_line,0,4) == "HTTP") {
				$i_pos     = strpos($s_line," ");
				$s_status  = substr($s_line,$i_pos + 1);
				$i_end_pos = strpos($s_status," ");
				if ($i_end_pos === false) {
					$i_end_pos = strlen($s_status);
				}
				$i_http_code = (int)substr($s_status,0,$i_end_pos);
			}
		}
		return (array($i_http_code,$s_status));
	}

	function Resolve()
	{
		if (!$this->_Init()) {
			return (FALSE);
		}
		return (parent::Resolve());
	}

	function Read()
	{
		if (!$this->_Init()) {
			return (FALSE);
		}
		FMDebug("Init done");
		if (!$this->Open()) {
			return (FALSE);
		}
		FMDebug("Open done");
		if (!$this->_SendRequest()) {
			return (FALSE);
		}
		FMDebug("Send done");
		if (!$this->_GetResponse()) {
			return (FALSE);
		}
		FMDebug("Get done");
		$this->Close();
		return ($this->_aResponse);
	}

	function SetAuthenticationLine($s_auth)
	{
		$this->_sAuthLine = $s_auth;
	}

	function SetAuthentication($s_type,$s_user,$s_pass)
	{
		$this->_sAuthType = $s_type;
		$this->_sAuthUser = $s_user;
		$this->_sAuthPass = $s_pass;
	}

	function SetAgent($s_agent)
	{
		$this->_sAgent = $s_agent;
	}
}

/*
 * Class:       HTTPPost
 * Description:
 *  A class that implements HTTP POST method.
 */

class   HTTPPost extends HTTPGet
{
	var $_sPostData; /* data to POST */

	function __construct($s_url = "")
	{
		$this->_sPostData = '';
		parent::__construct($s_url);
	}

	function _SendRequest()
	{
		$this->_PrepareRequest();
		return (NetIO::Write($this->_sRequest));
	}

	function _PrepareRequest($s_method = 'POST')
	{
		parent::_PrepareRequest($s_method);
		$this->_AddData();
	}

	function _AdditionalHeaders()
	{
		//
		// we don't handle file uploads yet
		//
		$a_hdrs = array(
			'Content-Type: application/x-www-form-urlencoded',
			'Content-Length: ' . strlen($this->_sPostData),
		);
		return (implode("\r\n",$a_hdrs));
	}

	function _AddData()
	{
		$this->_sRequest .= "\r\n"; // blank line after headers
		$this->_sRequest .= $this->_sPostData;
	}

	function _EncodeData($a_fields)
	{
		$s_data = '';
		foreach ($a_fields as $s_name => $s_value) {
			if ($s_data != '') {
				$s_data .= '&';
			}
			if (is_string($s_value)) {
				$s_data .= urlencode($s_name) . '=' . urlencode($s_value);
			} else {
				$s_data .= urlencode($s_name) . '=' . urlencode(serialize($s_value));
			}
		}
		return ($s_data);
	}

	function Post($a_fields)
	{
		//
		// we don't handle file uploads yet
		//
		$this->_sPostData = $this->_EncodeData($a_fields);
		return ($this->Read());
	}
}

//
// Load a template file into a string.
//
function LoadTemplate($s_name,$s_dir,$s_url,$b_ret_lines = false)
{
	global $php_errormsg;

	$s_buf   = "";
	$a_lines = array();
	if (!empty($s_dir)) {
		$s_name = "$s_dir/" . basename($s_name);
		@       $fp = fopen($s_name,"r");
		if ($fp === false) {
			SendAlert(GetMessage(MSG_OPEN_TEMPLATE,array("NAME"  => $s_name,
			                                             "ERROR" => CheckString($php_errormsg)
			)));
			return (false);
		}
		if ($b_ret_lines) {
			$a_lines = ReadLines($fp);
		} else
			//
			// load the whole template into a string
			//
		{
			$s_buf = fread($fp,filesize($s_name));
		}
		fclose($fp);
	} else {
		if (substr($s_url,-1) == '/') {
			$s_name = "$s_url" . basename($s_name);
		} else {
			$s_name = "$s_url/" . basename($s_name);
		}
		if (($m_data = GetURL($s_name,$s_error,$b_ret_lines)) === false) {
			SendAlert($s_error);
			return (false);
		}
		if ($b_ret_lines) {
			$a_lines = $m_data;
			//
			// strip line terminations from each line
			//
			for ($ii = count($a_lines) ; --$ii >= 0 ;) {
				$s_line       = $a_lines[$ii];
				$s_line       = str_replace("\r","",$s_line);
				$s_line       = str_replace("\n","",$s_line);
				$a_lines[$ii] = $s_line;
			}
		} else {
			$s_buf = $m_data;
		}
	}
	return ($b_ret_lines ? $a_lines : $s_buf);
}

//
// To show an error template.  The template must be HTML and, for security
// reasons, must be a file on the server in the directory specified
// by $TEMPLATEDIR or $TEMPLATEURL.
// $a_specs is an array of substitutions to perform, as follows:
//      tag-name    replacement string
//
// For example:
//      "fmerror"=>"An error message"
//
function ShowErrorTemplate($s_name,$a_specs,$b_user_error)
{
	if (function_exists('FMHookShowErrorTemplate')) {
		if (FMHookShowErrorTemplate($s_name,$a_specs,$b_user_error)) {
			return (true);
		}
	}
	if (Settings::isEmpty('TEMPLATEDIR') && Settings::isEmpty('TEMPLATEURL')) {
		SendAlert(GetMessage(MSG_TEMPLATES));
		return (false);
	}
	if (($s_buf = LoadTemplate($s_name,Settings::get('TEMPLATEDIR'),Settings::get('TEMPLATEURL'))) === false) {
		return (false);
	}

	//
	// now look for the tags to replace
	//
	foreach ($a_specs as $s_tag => $s_value)
		//
		// search for
		//      <tagname/>
		// or
		//      [tagname/]
		// with optional whitespace
		//
	{
		$s_buf = preg_replace('/[<\[]\s*' . preg_quote($s_tag,"/") . '\s*\/\s*[>\]]/ims',
		                      nl2br($s_value),$s_buf);
	}
	if ($b_user_error) {
		// strip any <fmusererror> and </fmusererror> tags
		// or [fmusererrors] and [/fmusererror] tags
		//
		// You can show information that's specific to user
		// errors between these special tags.
		//
		$s_buf = preg_replace('/[<\[]\s*\/?\s*fmusererror\s*[>\]]/ims','',$s_buf);
		//
		// since this isn't a system error, strip anything between
		// <fmsyserror> and </fmsyserror>
		// or [fmsyserrors] and [/fmsyserror] tags
		//
		$s_buf = preg_replace('/[<\[]\s*fmsyserror\s*[>\]].*[<\[]\s*\/\s*fmsyserror\s*[>\]]/ims','',$s_buf);
	} else {
		// strip any <fmsyserror> and </fmsyserror> tags
		// or [fmsyserrors] and [/fmsyserror] tags
		//
		// You can show information that's specific to system
		// errors between these special tags.
		//
		$s_buf = preg_replace('/[<\[]\s*\/?\s*fmsyserror\s*[>\]]/ims','',$s_buf);
		//
		// since this isn't a user error, strip anything between
		// <fmusererror> and </fmusererror>
		// or [fmusererrors] and [/fmusererror] tags
		//
		$s_buf = preg_replace('/[<\[]\s*fmusererror\s*[>\]].*[<\[]\s*\/\s*fmusererror\s*[>\]]/ims','',$s_buf);
	}
	//
	// just output the page
	//
	echo $s_buf;
	return (true);
}

//
// To show an error to the user.
//
function ShowError($error_code,$error_mesg,$b_user_error,
                   $b_alerted = false,$a_item_list = array(),$s_extra_info = "")
{
	global $SPECIAL_FIELDS,$SPECIAL_MULTI,$SPECIAL_VALUES;
	global $aServerVars,$aStrippedFormVars;

	//
	// The following initialisations are so that ShowError can run from fmhookpreinit scripts
	//
	$aSpecialFieldValues    = isset($SPECIAL_FIELDS) ? $SPECIAL_FIELDS : array();
	$aSpecialValues         = isset($SPECIAL_VALUES) ? $SPECIAL_VALUES : array();
	$aSpecialMultiValues    = isset($SPECIAL_MULTI) ? $SPECIAL_MULTI : array();
	$aServerVarValues       = isset($aServerVars) ? $aServerVars : array();
	$aStrippedFormVarValues = isset($aStrippedFormVars) ? $aStrippedFormVars : array();

	//
	// Testing with PHP 4.0.6 indicates that sessions don't always work.
	// So, we'll also add the error to the URL, unless
	// PUT_DATA_IN_URL is false.
	//
	SetSession("FormError",$error_mesg);
	SetSession("FormErrorInfo",$s_extra_info);
	SetSession("FormErrorCode",$error_code);
	SetSession("FormErrorItems",$a_item_list);
	SetSession("FormIsUserError",$b_user_error);
	SetSession("FormAlerted",$b_alerted);
	SetSession("FormData",array());

	$bad_url      = isset($aSpecialValues["bad_url"]) ? $aSpecialValues["bad_url"] : '';
	$bad_template = isset($aSpecialValues["bad_template"]) ? $aSpecialValues["bad_template"] : '';
	$this_form    = isset($aSpecialValues["this_form"]) ? $aSpecialValues["this_form"] : '';
	if (IsAjax()) {
		JSON_Result("ERROR",array("ErrorCode"  => $error_code,
		                          "UserError"  => $b_user_error,
		                          "ErrorMesg"  => $error_mesg,
		                          "Alerted"    => $b_alerted,
		                          "ErrorItems" => $a_item_list
		));
		ZapSession();
	} elseif (!empty($bad_url)) {
		$a_params = array();
		if (Settings::get('PUT_DATA_IN_URL')) {
			$a_params[] = "this_form=" . urlencode("$this_form");
			$a_params[] = "bad_template=" . urlencode("$bad_template");
			$a_params[] = "error=" . urlencode("$error_mesg");
			$a_params[] = "extra=" . urlencode("$s_extra_info");
			$a_params[] = "errcode=" . urlencode("$error_code");
			$a_params[] = "isusererror=" . ($b_user_error ? "1" : "0");
			$a_params[] = "alerted=" . ($b_alerted ? "1" : "0");
			$i_count    = 1;
			foreach ($a_item_list as $s_item) {
				$a_params[] = "erroritem$i_count=" . urlencode("$s_item");
				$i_count++;
			}
		} else {
			$a_sess_data                 = GetSession("FormData");
			$a_sess_data["this_form"]    = "$this_form";
			$a_sess_data["bad_template"] = "$bad_template";
			SetSession("FormData",$a_sess_data);
			//
			// tell the bad_url to look in the session only
			//
			$a_params[] = "insession=1";
		}
		//
		// Add the posted data to the URL so that an intelligent
		// $bad_url can call the form again
		//
		foreach ($aStrippedFormVarValues as $s_name => $m_value) {
			//
			// skip special fields
			//
			$b_special = false;
			if (in_array($s_name,$aSpecialFieldValues)) {
				$b_special = true;
			} else {
				foreach ($aSpecialMultiValues as $s_multi_fld) {
					$i_len = strlen($s_multi_fld);
					if (substr($s_name,0,$i_len) == $s_multi_fld) {
						$i_index = (int)substr($s_name,$i_len);
						if ($i_index > 0) {
							$b_special = true;
							break;
						}
					}
				}
			}
			if (!$b_special) {
				if (Settings::get('PUT_DATA_IN_URL')) {
					if (is_array($m_value)) {
						foreach ($m_value as $s_value) {
							$a_params[] = "$s_name" . '[]=' .
							              urlencode(substr($s_value,0,Settings::get('MAXSTRING')));
						}
					} else {
						$a_params[] = "$s_name=" . urlencode(substr($m_value,0,Settings::get('MAXSTRING')));
					}
				} else {
					$a_sess_data = GetSession("FormData");
					if (is_array($m_value)) {
						$a_sess_data["$s_name"] = $m_value;
					} else {
						$a_sess_data["$s_name"] = substr($m_value,0,Settings::get('MAXSTRING'));
					}
					SetSession("FormData",$a_sess_data);
				}
			}
		}
		//
		// Now add the authentication data, if any
		//
		if ((isset($aServerVarValues["PHP_AUTH_USER"]) &&
		     $aServerVarValues["PHP_AUTH_USER"] !== "") ||
		    (isset($aServerVarValues["PHP_AUTH_PW"]) &&
		     $aServerVarValues["PHP_AUTH_PW"] !== "")
		) {
			if (Settings::get('PUT_DATA_IN_URL')) {
				if (isset($aServerVarValues["PHP_AUTH_USER"])) {
					$a_params[] = "PHP_AUTH_USER=" . urlencode($aServerVarValues["PHP_AUTH_USER"]);
				}

				if (isset($aServerVarValues["PHP_AUTH_PW"])) {
					$a_params[] = "PHP_AUTH_PW=" . urlencode($aServerVarValues["PHP_AUTH_PW"]);
				}

				if (isset($aServerVarValues["PHP_AUTH_TYPE"])) {
					$a_params[] = "PHP_AUTH_TYPE=" . urlencode($aServerVarValues["PHP_AUTH_TYPE"]);
				}
			} else {
				$a_sess_data = GetSession("FormData");
				if (isset($aServerVarValues["PHP_AUTH_USER"])) {
					$a_sess_data["PHP_AUTH_USER"] = $aServerVarValues["PHP_AUTH_USER"];
				}

				if (isset($aServerVarValues["PHP_AUTH_PW"])) {
					$a_sess_data["PHP_AUTH_PW"] = $aServerVarValues["PHP_AUTH_PW"];
				}

				if (isset($aServerVarValues["PHP_AUTH_TYPE"])) {
					$a_sess_data["PHP_AUTH_TYPE"] = $aServerVarValues["PHP_AUTH_TYPE"];
				}
				SetSession("FormData",$a_sess_data);
			}
		}
		$bad_url = AddURLParams($bad_url,$a_params,false);
		Redirect($bad_url,GetMessage(MSG_FORM_ERROR));
	} else {
		if (!empty($bad_template)) {
			$a_specs = array("fmerror"      => htmlspecialchars("$error_mesg"),
			                 "fmerrorcode"  => htmlspecialchars("$error_code"),
			                 "fmfullerror"  => htmlspecialchars("$error_mesg") . "\n" .
			                                   htmlspecialchars("$s_extra_info"),
			                 "fmerrorextra" => htmlspecialchars("$s_extra_info"),
			);
			for ($i_count = 1 ; $i_count <= 20 ; $i_count++) {
				$a_specs["fmerroritem$i_count"] = "";
			}
			$i_count = 1;
			foreach ($a_item_list as $s_item) {
				$a_specs["fmerroritem$i_count"] = htmlspecialchars($s_item);
				$i_count++;
			}
			$s_list = "";
			foreach ($a_item_list as $s_item) {
				$s_list .= "<li>" . htmlspecialchars($s_item) . "</li>";
			}
			$a_specs["fmerroritemlist"] = $s_list;
			if (ShowErrorTemplate($bad_template,$a_specs,$b_user_error)) {
				return;
			}
		}
		$s_text = GetMessage(MSG_ERROR_PROC);
		if ($b_user_error) {
			$s_text .= $error_mesg . "\n" . FixedHTMLEntities($s_extra_info);
		} else {
			global $SERVER;

			if ($b_alerted) {
				$s_text .= GetMessage(MSG_ALERT_DONE,array("SERVER" => $SERVER));
			} else {
				$s_text .= GetMessage(MSG_PLS_CONTACT,array("SERVER" => $SERVER));
			}
			$s_text .= GetMessage(MSG_APOLOGY,array("SERVER" => $SERVER));
		}
		CreatePage($s_text,GetMessage(MSG_FORM_ERROR),false);
		//
		// the session data is not needed now
		//
		ZapSession();
	}
}

//
// Report an error.  Same as Error, but implements
// ATTACK_DETECTION_IGNORE_ERRORS.
//
function ErrorWithIgnore($error_code,$error_mesg,$b_filter = true,$show = true,$int_mesg = "")
{
	if (function_exists('FMHookErrorWithIgnore')) {
		FMHookErrorWithIgnore($error_code,$error_mesg,$b_filter,$show,$int_mesg);
	}

	$b_alerted = false;
	if (!Settings::get('ATTACK_DETECTION_IGNORE_ERRORS')) {
		if (SendAlert("$error_code\n *****$int_mesg*****\nError=$error_mesg\n",$b_filter)) {
			$b_alerted = true;
		}
	}
	if ($show) {
		ShowError($error_code,$error_mesg,false,$b_alerted);
	} else
		//
		// show something to the user
		//
	{
		ShowError($error_code,GetMessage(MSG_SUBM_FAILED),false,$b_alerted);
	}
	FormMailExit();
}

//
// Report an error
//
function Error($error_code,$error_mesg,$b_filter = true,$show = true,$int_mesg = "")
{
	if (function_exists('FMHookError')) {
		FMHookError($error_code,$error_mesg,$b_filter,$show,$int_mesg);
	}

	$b_alerted = false;
	if (SendAlert("$error_code\n *****$int_mesg*****\nError=$error_mesg\n",$b_filter)) {
		$b_alerted = true;
	}
	if ($show) {
		ShowError($error_code,$error_mesg,false,$b_alerted);
	} else
		//
		// show something to the user
		//
	{
		ShowError($error_code,GetMessage(MSG_SUBM_FAILED),false,$b_alerted);
	}
	FormMailExit();
}

//
// Report a user error
//
function UserError($s_error_code,$s_error_mesg,
                   $s_extra_info = "",$a_item_list = array())
{
	if (function_exists('FMHookUserError')) {
		FMHookUserError($s_error_code,$s_error_mesg,$s_extra_info,$a_item_list);
	}
	$b_alerted = false;
	if (Settings::get('ALERT_ON_USER_ERROR') ||
	    ($s_error_code === "invalid_email" && Settings::get('ALERT_ON_EMAIL_VALIDATION_FAILED'))) {
		$s_addl_subject = $s_error_code === "invalid_email" ? 'Invalid Email Domain' : '';
		if (SendAlert("$s_error_code\nError=$s_error_mesg\n$s_extra_info\n",true,false,$s_addl_subject)) {
			$b_alerted = true;
		}
	}
	ShowError($s_error_code,$s_error_mesg,true,$b_alerted,$a_item_list,$s_extra_info);
	FormMailExit();
}

//
// Create a simple page with the given text.
//
function CreatePage($text,$title = "",$b_show_about = true)
{
	global $FM_VERS,$sHTMLCharSet;

	if (IsAjax()) {
		//
		// CreatePage should not be called in Ajax mode.
		// If it is, it must be an error or debugging state.
		//
		JSON_Result("ERROR",array("ErrorCode" => $title,
		                          "ErrorMesg" => $text
		));
	} else {
		echo
			'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' .
			"\n";
		echo '<html xmlns="http://www.w3.org/1999/xhtml">' . "\n";
		echo "<head>\n";
		if (isset($sHTMLCharSet) && $sHTMLCharSet !== "") {
			echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=$sHTMLCharSet\" />\n";
		}
		if ($title != "") {
			echo "<title>" . FixedHTMLEntities($title) . "</title>\n";
		}
		echo "</head>\n";
		echo "<body>\n";
		echo nl2br($text);
		echo "<p />";
		if ($b_show_about) {
			echo "<p><small>\n";
			echo GetMessage(MSG_ABOUT_FORMMAIL,array("FM_VERS" => $FM_VERS,
			                                         "TECTITE" => "www.tectite.com"
			));
			echo "</small></p>\n";
		}
		echo "</body>\n";
		echo "</html>\n";
	}
}

function GetMagicQuotesGPC()
{
	global $ExecEnv;
	$b_result = false;

	// the magic_quotes_gpc setting was removed in PHP 5.4.0
	// get_magic_quotes_gpc was deprecated in PHP 7.4.0 and removed in PHP 8.0.0
	if (function_exists('get_magic_quotes_gpc')) {
		if (!$ExecEnv->IsPHPAtLeast("5.4.0")) {
			$b_result = (get_magic_quotes_gpc() != 0);
		}
	}
	return $b_result;
}

//
// Strip slashes if magic_quotes_gpc is set.
//
function StripGPC($s_value)
{
	global $ExecEnv;

	if (GetMagicQuotesGPC()) {
		$s_value = stripslashes($s_value);
	}
	return ($s_value);
}

//
// return an array, stripped of slashes if magic_quotes_gpc is set
//
function StripGPCArray($a_values)
{
	global $ExecEnv;

	if (GetMagicQuotesGPC()) {
		foreach ($a_values as $m_key => $m_value) {
			if (is_array($m_value))
				//
				// strip arrays recursively
				//
			{
				$a_values[$m_key] = StripGPCArray($m_value);
			} else
				//
				// convert scalar to string and strip
				//
			{
				$a_values[$m_key] = stripslashes("$m_value");
			}
		}
	}

	return ($a_values);
}

//
// Strip a value of unwanted characters, which might be hacks.
// The stripping of \r and \n is a *critical* security feature.
//
function Strip($value)
{
	//
	// When working with character sets such as UTF-8, stripping
	// control characters is a *really bad idea* and breaks things.
	// From version 8.22, FormMail only strips \r and \n as these
	// are really the only characters that can cause header hacks
	// to be inserted. (Strip means replace with a single space).
	// We also handle multiple spaces.
	//
	$value = preg_replace('/[ \r\n]+/'," ",$value); // zap all CRLF and multiple blanks
	return ($value);
}

//
// Clean a value.  This means:
//  1. convert to string
//  2. truncate to maximum length
//  3. strip the value of unwanted or dangerous characters (hacks)
//  4. trim both ends of whitespace
// Each element of an array is cleaned as above.  This process occurs
// recursively, so arrays of arrays work OK too (though there's no
// need for that in this program).
//
// Non-scalar values are changed to the string "<X>" where X is the type.
// In general, FormMail won't receive non-scalar non-array values, so this
// is more a future-proofing measure.
//
function CleanValue($m_value)
{
	if (is_array($m_value)) {
		foreach ($m_value as $m_key => $m_item) {
			$m_value[$m_key] = CleanValue($m_item);
		}
	} elseif (!is_scalar($m_value)) {
		$m_value = "<" . gettype($m_value) . ">";
	} else {
		//
		// convert to string and truncate
		//
		$m_value = substr("$m_value",0,Settings::get('MAXSTRING'));
		//
		// strip unwanted chars and trim
		//
		$m_value = trim(Strip($m_value));
	}
	return ($m_value);
}

//
// Clean a special value.  Special values listed in $SPECIAL_NOSTRIP
// will not be cleaned.
//
function SpecialCleanValue($s_name,$m_value)
{
	global $SPECIAL_NOSTRIP;

	if (!in_array($s_name,$SPECIAL_NOSTRIP)) {
		$m_value = CleanValue($m_value);
	}
	return ($m_value);
}

//
// Return the fields and their values in a string containing one
// field per line.
//
function MakeFieldOutput($a_order,$a_fields,$s_line_feed = null)
{
	if ($s_line_feed === null) {
		$s_line_feed = Settings::get('BODY_LF');
	}
	$n_order  = count($a_order);
	$s_output = "";
	for ($ii = 0 ; $ii < $n_order ; $ii++) {
		$s_name = $a_order[$ii];
		if (isset($a_fields[$s_name])) {
			$s_output .= "$s_name: " . $a_fields[$s_name] . $s_line_feed;
		}
	}
	return ($s_output);
}

//
// Check if a field name is special. Returns true if it is.
//
function IsSpecialField($s_name)
{
	global $SPECIAL_FIELDS;

	return (in_array($s_name,$SPECIAL_FIELDS));
}

//
// Set a special field value.
//
function SetSpecialField($s_name,$m_value)
{
	global $SPECIAL_VALUES;

	//
	// most special values cannot be arrays; ignore them if they are
	//
	if (is_array($m_value)) {
		global $SPECIAL_ARRAYS;

		if (!in_array($s_name,$SPECIAL_ARRAYS)) {
			return;
		}
	}
	$SPECIAL_VALUES[$s_name] = SpecialCleanValue($s_name,$m_value);
}

//
// Check if a field name is a special "multi" field.
// A multi field is the name plus a sequence number.  For example,
//      conditions1
//      conditions2
// Returns a list (array) if it is, otherwise false if not.
// The list contains:
//      1. the name of the special multi field
//      2. the index number for multi field
//
function IsSpecialMultiField($s_name)
{
	global $SPECIAL_MULTI;

	foreach ($SPECIAL_MULTI as $s_multi_fld) {
		$i_len = strlen($s_multi_fld);
		//
		// look for nameN where N is a number starting from 1
		//
		if (substr($s_name,0,$i_len) == $s_multi_fld) {
			$i_index = (int)substr($s_name,$i_len);
			if ($i_index > 0) {
				//
				// re-index to zero
				//
				--$i_index;
				return (array($s_multi_fld,$i_index));
			}
		}
	}
	return (false);
}

//
// Set a multi field value.
//
function SetSpecialMultiField($s_name,$i_index,$m_value)
{
	global $SPECIAL_VALUES;

	//
	// these special fields cannot be arrays - ignore if it is
	//
	if (!is_array($m_value)) {
		//
		// Convert the special field itself into an array so it can be correctly
		// indexed.
		//
		if (!is_array($SPECIAL_VALUES[$s_name])) {
			$SPECIAL_VALUES[$s_name] = array();
		}
		$SPECIAL_VALUES[$s_name][$i_index] = SpecialCleanValue($s_name,$m_value);
	}
}

//
// Check if a field is part of Reverse Captcha processing.
//
function IsReverseCaptchaField($s_name)
{
	$a_rev_captcha = Settings::get('ATTACK_DETECTION_REVERSE_CAPTCHA');
	return (isset($a_rev_captcha[$s_name]));
}

/**
 * Validate an email address.
 * Generate a UserError if $b_user_error is true.
 *
 * @param $s_name
 * @param $s_value
 * @param $b_user_error
 */
function ValidateEmailAddress($s_name,$s_value,$b_user_error)
{
	$b_error_on_invalid = true;
	$b_valid            = true;
	$s_reason           = '';
	$a_components       = explode('@',$s_value,2);
	if (count($a_components) !== 2) {
		$b_valid  = false;
		$s_reason = "missing '@'";
	} else {
		if ($a_components[0] === '' || $a_components[1] === '') {
			$b_valid  = false;
			$s_reason = "missing name or domain name";
		} else {
			if (Settings::get('VALIDATE_EMAIL_DOMAIN')) {
				$n_max_monitoring_secs = Settings::get('EMAIL_VALIDATION_MAX_MONITORING_TIME_SECS');
				if ($n_max_monitoring_secs > 0) {
					$f_start_time = microtime(true);
				}
				if (!checkdnsrr($a_components[1])) {
					$b_valid  = false;
					$s_reason = "invalid domain name '" . $a_components[1] . "'";
				}
				if ($n_max_monitoring_secs > 0) {
					$f_end_time = microtime(true);
					$f_diff     = $f_end_time - $f_start_time;
					SetSpecialField("email_validation_time_secs",number_format($f_diff,2));
					if ($f_diff >= $n_max_monitoring_secs) {
						//
						// let the submission go through because the time is >= EMAIL_VALIDATION_MAX_MONITORING_TIME_SECS
						//
						$b_error_on_invalid = false;
					}
				}
			}
		}
	}
	if (!$b_valid) {
		if ($b_error_on_invalid) {
			$s_error_mesg = GetMessage(MSG_INVALID_EMAIL,
			                           array("EMAIL"  => $s_value,
			                                 "REASON" => $s_reason
			                           ));
			if ($b_user_error) {
				UserError("invalid_email",$s_error_mesg,'',array($s_name => $s_error_mesg));
			} else {
				SendAlert($s_error_mesg);
			}
		}
	}
}

/**
 * Perform some basic validation on a special field before setting it.
 * Generate a UserError if $b_user_error is true.
 *
 * @param $s_name
 * @param $s_value
 * @param $b_user_error
 */
function ValidateSpecialField($s_name,$s_value,$b_user_error)
{
	switch ($s_name) {
		case "email":
			if ($s_value !== '') {
				ValidateEmailAddress($s_name,$s_value,$b_user_error);
			}
			break;
	}
}

//
// Process a field
//
function ProcessField($s_name,$raw_value,&$a_order,&$a_fields,&$a_raw_fields)
{
	global $FORMATTED_INPUT;

	//
	// fields go into an array of special values or an array of other values
	// or get completely ignored.
	//
	$b_ignore = $b_special = false;
	if (IsSpecialField($s_name)) {
		ValidateSpecialField($s_name,$raw_value,true);
		SetSpecialField($s_name,$raw_value);
		$b_special = true;
	}
	//
	// check for multiple valued special fields
	//
	if (($a_multi_fld = IsSpecialMultiField($s_name)) !== false) {
		SetSpecialMultiField($a_multi_fld[0],$a_multi_fld[1],$raw_value);
		$b_special = true;
	}
	if (!$b_special) {
		if (IsReverseCaptchaField($s_name)) {
			$b_ignore = true;
		}
	}
	if (!$b_special && !$b_ignore) {
		//
		// return the raw value unchanged in the $a_raw_fields array
		//
		$a_raw_fields[$s_name] = $raw_value;
		//
		// handle checkboxes and multiple-selection lists
		// Thanks go to Theodore Boardman for the suggestion
		// and initial working code.
		//
		if (is_array($raw_value)) {
			if (empty($raw_value)) {
				$s_cleaned_value = "";
			} else {
				$a_cleaned_values = CleanValue($raw_value);
				//
				// the output is a comma separated list of values for the
				// checkbox.  For example,
				//          events: Diving,Cycling,Running
				//
				// Set the clean value to the list of cleaned checkbox
				// values.
				// First, remove any commas in the values themselves.
				//
				$a_cleaned_values = str_replace(",","",$a_cleaned_values);
				$s_cleaned_value  = implode(",",$a_cleaned_values);
			}
		} else {
			//
			// NOTE: there is a minor bug here now that we support
			// $FORM_INI_FILE. The INI file is processed at the end
			// so if you set the mail_options below in the INI file they
			// won't get processed here.  This means you must set
			// the following mail_options in the HTML form for now.
			// (To be fixed at a later date.  RJR 17-Feb-06).
			//

			//
			// if the form specifies the "KeepLines" option,
			// don't strip new lines
			//
			if (IsMailOptionSet("KeepLines") && strpos($raw_value,"\n") !== false) {
				//
				// truncate first
				//
				$s_truncated = substr("$raw_value",0,Settings::get('MAXSTRING'));
				//
				// split into lines, clean each individual line,
				// then put it back together again
				//
				$a_lines         = explode("\n",$s_truncated);
				$a_lines         = CleanValue($a_lines);
				$s_cleaned_value = implode(Settings::get('BODY_LF'),$a_lines);
				//
				// and, for this special case, prepend a new line
				// so that the value is shown on a fresh line
				//
				$s_cleaned_value = Settings::get('BODY_LF') . $s_cleaned_value;
			} else {
				$s_cleaned_value = CleanValue($raw_value);
			}
		}
		//
		// if the form specifies the "NoEmpty" option, skip
		// empty values.
		//
		if (!IsMailOptionSet("NoEmpty") || !FieldManager::IsEmpty($s_cleaned_value)) {
			if (!IsMailExcluded($s_name)) {
				//
				// by default, we maintain the order as passed in
				// the HTTP request
				//
				$a_order[]         = $s_name;
				$a_fields[$s_name] = $s_cleaned_value;
			}
		}

		//
		// add to the $FORMATTED_INPUT array for debugging and
		// error reporting
		//
		array_push($FORMATTED_INPUT,"$s_name: '$s_cleaned_value'");
	}
}

//
// Parse the input variables and return:
//  1. an array that specifies the required field order in the output
//  2. an array containing the non-special cleaned field values indexed
//     by field name.
//  3. an array containing the non-special raw field values indexed by
//     field name.
//
function ParseInput($a_vars)
{
	$a_order      = array();
	$a_fields     = array();
	$a_raw_fields = array();
	//
	// scan the array of values passed in (name-value pairs) and
	// produce slightly formatted (not HTML) textual output
	// and extract any special values found.
	//
	foreach ($a_vars as $s_name => $raw_value) {
		ProcessField($s_name,$raw_value,$a_order,$a_fields,$a_raw_fields);
	}

	return (array($a_order,$a_fields,$a_raw_fields));
}

//
// Get the URL for sending to the CRM.
//
function GetCRMURL($spec,$vars,$url)
{
	$bad  = false;
	$list = TrimArray(explode(",",$spec));
	$map  = array();
	for ($ii = 0 ; $ii < count($list) ; $ii++) {
		$name = $list[$ii];
		if ($name) {
			//
			// the specification must be in this format:
			//      form-field-name:CRM-field-name
			//
			if (($i_crm_name_pos = strpos($name,":")) > 0) {
				$s_crm_name = substr($name,$i_crm_name_pos + 1);
				$name       = substr($name,0,$i_crm_name_pos);
				if (isset($vars[$name])) {
					$map[] = $s_crm_name . "=" . urlencode($vars[$name]);
					$map[] = "Orig_" . $s_crm_name . "=" . urlencode($name);
				}
			} else {
				//
				// not the right format, so just include as a parameter
				// check for name=value format to choose encoding
				//
				$a_values = explode("=",$name);
				if (count($a_values) > 1) {
					$map[] = urlencode($a_values[0]) . "=" . urlencode($a_values[1]);
				} else {
					$map[] = urlencode($a_values[0]);
				}
			}
		}
	}
	if (count($map) == 0) {
		return ("");
	}
	return (AddURLParams($url,$map,false));
}

//
// strip the HTML from a string or array of strings
//
function StripHTML($m_value,$s_line_feed = "\n")
{
	if (is_array($m_value)) {
		foreach ($m_value as $m_key => $s_str) {
			$m_value[$m_key] = StripHTML($s_str);
		}
		return ($m_value);
	}
	$s_str = $m_value;
	//
	// strip HTML comments (s option means include new lines in matches)
	//
	$s_str = preg_replace('/<!--([^-]*([^-]|-([^-]|-[^>])))*-->/s','',$s_str);
	//
	// strip any scripts (i option means case-insensitive)
	//
	$s_str = preg_replace('/<script[^>]*?>.*?<\/script[^>]*?>/si','',$s_str);
	//
	// replace paragraphs with new lines (line feeds)
	//
	$s_str = preg_replace('/<p[^>]*?>/i',$s_line_feed,$s_str);
	//
	// replace breaks with new lines (line feeds)
	//
	$s_str = preg_replace('/<br[[:space:]]*\/?[[:space:]]*>/i',$s_line_feed,$s_str);
	//
	// overcome this bug: http://bugs.php.net/bug.php?id=21311
	//
	$s_str = preg_replace('/<![^>]*>/s','',$s_str);
	//
	// get rid of all HTML tags
	//
	$s_str = strip_tags($s_str);
	return ($s_str);
}

//
// Check for valid URL in TARGET_URLS
//
function CheckValidURL($s_url)
{

	foreach (Settings::get('TARGET_URLS') as $s_prefix) {
		if (!empty($s_prefix) &&
		    strtolower(substr($s_url,0,strlen($s_prefix))) ==
		    strtolower($s_prefix)
		) {
			return (true);
		}
	}
	return (false);
}

//
// Scan the given data for fields returned from the CRM.
// A field has this following format:
//      __FIELDNAME__=value
// terminated by a line feed.
//
function FindCRMFields($s_data)
{
	$a_ret = array();
	if (preg_match_all('/^__([A-Za-z][A-Za-z0-9_]*)__=(.*)$/m',$s_data,$a_matches) === false) {
		SendAlert(GetMessage(MSG_PREG_FAILED));
	} else {
		$n_matches = count($a_matches[0]);
		//  SendAlert("$n_matches on '$s_data'");
		for ($ii = 0 ; $ii < $n_matches ; $ii++) {
			if (isset($a_matches[1][$ii]) && isset($a_matches[2][$ii])) {
				$a_ret[$a_matches[1][$ii]] = $a_matches[2][$ii];
			}
		}
	}
	return ($a_ret);
}

//
// open the given URL to send data to it, we expect the response
// to contain at least '__OK__=' followed by true or false
//
function SendToCRM($s_url,&$a_data)
{
	global $php_errormsg;

	if (!CheckValidURL($s_url)) {
		SendAlert(GetMessage(MSG_URL_INVALID,array("URL" => $s_url)));
		return (false);
	}
	@   $fp = fopen($s_url,"r"); // RJR: TO DO: re-implement using NetIO
	if ($fp === false) {
		SendAlert(GetMessage(MSG_URL_OPEN,array("URL"   => $s_url,
		                                        "ERROR" => CheckString($php_errormsg)
		)));
		return (false);
	}
	$s_mesg = "";
	while (!feof($fp)) {
		$s_line = fgets($fp,4096);
		$s_mesg .= $s_line;
	}
	fclose($fp);
	$s_mesg   = StripHTML($s_mesg);
	$s_result = preg_match('/__OK__=(.*)/',$s_mesg,$a_matches);
	if (count($a_matches) < 2 || $a_matches[1] === "") {
		//
		// no agreed __OK__ value returned - assume system error
		//
		SendAlert(GetMessage(MSG_CRM_FAILED,array("URL" => $s_url,
		                                          "MSG" => $s_mesg
		)));
		return (false);
	}
	//
	// look for fields to return
	//
	$a_data = FindCRMFields($s_mesg);
	//
	// check for success or user error
	//
	switch (strtolower($a_matches[1])) {
		case "true":
			break;
		case "false":
			//
			// check for user error
			//
			if (isset($a_data["USERERRORCODE"])) {
				$s_error_code = "crm_error";
				$s_error_mesg = GetMessage(MSG_CRM_FORM_ERROR);
				$s_error_code .= $a_data["USERERRORCODE"];
				if (isset($a_data["USERERRORMESG"])) {
					$s_error_mesg = $a_data["USERERRORMESG"];
				}
				UserError($s_error_code,$s_error_mesg);
				// no return
			}
			return (false);
	}
	return (true);
}

//
// Split into field name and friendly name; returns an array with
// two elements.
// Format is:
//      fieldname:Nice printable name for displaying
//
function GetFriendlyName($s_name)
{
	if (($i_pos = strpos($s_name,':')) === false) {
		return (array(trim($s_name),trim($s_name)));
	}
	return (array(trim(substr($s_name,0,$i_pos)),trim(substr($s_name,$i_pos + 1))));
}

define("REQUIREDOPS","|^!="); // operand characters for advanced required processing

//
// Perform a field comparison test.
//
/** @noinspection PhpStatementHasEmptyBodyInspection */
function FieldTest($s_oper,$s_fld1,$s_fld2,$a_vars,&$s_error_mesg,
                   $s_friendly1 = "",$s_friendly2 = "")
{
	$b_ok     = true;
	$s_empty1 = $s_empty2 = "";
	//
	// perform the test
	//
	switch ($s_oper) {
		case '&': // both fields must be present
			if (!TestFieldEmpty($s_fld1,$a_vars,$s_empty1) &&
			    !TestFieldEmpty($s_fld2,$a_vars,$s_empty2)
			) {
			} // OK
			else {
				//
				// failed
				//
				$s_error_mesg = GetMessage(MSG_AND,array("ITEM1" => $s_friendly1,
				                                         "ITEM2" => $s_friendly2
				));
				$b_ok         = false;
			}
			break;
		case '|': // either field or both must be present
			if (!TestFieldEmpty($s_fld1,$a_vars,$s_empty1) ||
			    !TestFieldEmpty($s_fld2,$a_vars,$s_empty2)
			) {
			} // OK
			else {
				//
				// failed
				//
				$s_error_mesg = GetMessage(MSG_OR,array("ITEM1" => $s_friendly1,
				                                        "ITEM2" => $s_friendly2
				));
				$b_ok         = false;
			}
			break;
		case '^': // either field but not both must be present
			$b_got1 = !TestFieldEmpty($s_fld1,$a_vars,$s_empty1);
			$b_got2 = !TestFieldEmpty($s_fld2,$a_vars,$s_empty2);
			if ($b_got1 || $b_got2) {
				if ($b_got1 && $b_got2) {
					//
					// failed
					//
					$s_error_mesg = GetMessage(MSG_NOT_BOTH,
					                           array("ITEM1" => $s_friendly1,
					                                 "ITEM2" => $s_friendly2
					                           ));
					$b_ok         = false;
				}
			} else {
				//
				// failed
				//
				$s_error_mesg = GetMessage(MSG_XOR,
				                           array("ITEM1" => $s_friendly1,
				                                 "ITEM2" => $s_friendly2
				                           ));
				$b_ok         = false;
			}
			break;
		case '!=':
		case '=':
			$b_got1 = !TestFieldEmpty($s_fld1,$a_vars,$s_empty1);
			$b_got2 = !TestFieldEmpty($s_fld2,$a_vars,$s_empty2);
			if ($b_got1 && $b_got2) {
				$b_match = (GetFieldValue($s_fld1,$a_vars) ==
				            GetFieldValue($s_fld2,$a_vars));
			} elseif (!$b_got1 && !$b_got2)
				//
				// haven't got either value - they match
				//
			{
				$b_match = true;
			} else
				//
				// got one value, but not the other - they don't match
				//
			{
				$b_match = false;
			}
			if ($s_oper != '=') {
				//
				// != operator
				//
				$b_match = !$b_match;
				$s_desc  = GetMessage(MSG_IS_SAME_AS,
				                      array("ITEM1" => $s_friendly1,
				                            "ITEM2" => $s_friendly2
				                      ));
			} else {
				$s_desc = GetMessage(MSG_IS_NOT_SAME_AS,
				                     array("ITEM1" => $s_friendly1,
				                           "ITEM2" => $s_friendly2
				                     ));
			}
			if (!$b_match) {
				//
				// failed
				//
				$s_error_mesg = $s_desc;
				$b_ok         = false;
			}
			break;
	}
	return ($b_ok);
}

//
// Process advanced "required" conditionals
//
function AdvancedRequired($s_cond,$i_span,$a_vars,&$s_missing,&$a_missing_list)
{
	$b_ok = true;
	//
	// get first field name
	//
	list($s_fld1,$s_friendly1) = GetFriendlyName(substr($s_cond,0,$i_span));
	//
	// get the operator
	//
	$s_rem  = substr($s_cond,$i_span);
	$i_span = strspn($s_rem,REQUIREDOPS);
	$s_oper = substr($s_rem,0,$i_span);
	switch ($s_oper) {
		case '|':
		case '^':
		case '=':
		case '!=':
			//
			// second component is a field name
			//
			list($s_fld2,$s_friendly2) = GetFriendlyName(substr($s_rem,$i_span));
			if (!FieldTest($s_oper,$s_fld1,$s_fld2,$a_vars,$s_error_mesg,
			               $s_friendly1,$s_friendly2)
			) {
				//
				// failed
				//
				$s_missing               .= "$s_error_mesg\n";
				$a_missing_list[$s_fld1] = "$s_error_mesg";
				$b_ok                    = false;
			}
			break;
		default:
			SendAlert(GetMessage(MSG_REQD_OPER,array("OPER" => $s_oper)));
			break;
	}
	return ($b_ok);
}

//
// Check the input for required values.  The list of required fields
// is a comma-separated list of field names or conditionals
//
function CheckRequired($s_reqd,$a_vars,&$s_missing,&$a_missing_list)
{
	global $reCaptchaProcessor;

	$b_bad          = false;
	$a_list         = TrimArray(explode(",",$s_reqd));
	$s_missing      = "";
	$a_missing_list = array();
	$s_mesg         = "";
	for ($ii = 0 ; $ii < count($a_list) ; $ii++) {
		$s_cond = $a_list[$ii];
		$i_len  = strlen($s_cond);
		if ($i_len <= 0) {
			continue;
		}
		if (($i_span = strcspn($s_cond,REQUIREDOPS)) >= $i_len) {
			//
			// no advanced operator; just a field name
			//
			list($s_fld,$s_friendly) = GetFriendlyName($s_cond);
			if (TestFieldEmpty($s_fld,$a_vars,$s_mesg)) {
				if ($s_mesg === "") {
					$s_mesg = "$s_friendly";
				} else {
					$s_mesg = "$s_friendly ($s_mesg)";
				}
				$b_bad                  = true;
				$s_missing              .= "$s_mesg\n";
				$a_missing_list[$s_fld] = "$s_mesg";
			}
		} elseif (!AdvancedRequired($s_cond,$i_span,$a_vars,
		                            $s_missing,$a_missing_list)
		) {
			$b_bad = true;
		}
	}

	global $SPECIAL_VALUES;

	//
	// implement REQUIRE_CAPTCHA feature
	//
	if (!Settings::isEmpty('REQUIRE_CAPTCHA')) {
		if ($SPECIAL_VALUES["imgverify"] === "") {
			$s_missing                   .= Settings::get('REQUIRE_CAPTCHA') . "\n";
			$a_missing_list['imgverify'] = Settings::get('REQUIRE_CAPTCHA');
			$b_bad                       = true;
		}
	}
	return (!$b_bad);
}

/**
 * Class Conditions
 * Implements "conditions" processing.
 */
class Conditions
{
	/** @var array|string the conditions to process */
	private $_mConditions;      //
	/** @var string the "conditions" field being processed */
	private $_sField;
	/** @var string */
	private $_sMissing;
	/** @var array */
	private $_aMissingList;
	/** @var FieldManager */
	private $_FldManager;

	/**
	 * @param array|string $m_conditions   the conditions to process
	 * @param string       $s_missing      returns the message from the condition's failure
	 * @param array        $a_missing_list appended with the field name(s) that failed
	 */
	function __construct($m_conditions,&$s_missing,&$a_missing_list)
	{
		$this->_mConditions  = $m_conditions;
		$this->_sMissing     = &$s_missing;
		$this->_aMissingList = &$a_missing_list;
		$this->_FldManager   = new FieldManager();
	}

	/**
	 * Run the given field logic.
	 *
	 * @param string $s_test a string containing the field logic to run
	 *
	 * @return string|bool true if the logic evaluates to true, otherwise name of a field if the logic evaluates to false
	 */
	private function _runLogic($s_test)
	{
		global $aAlertInfo;

		$s_op_chars = "&|^!=~#<>"; // these are the characters for the operators
		$i_len      = strlen($s_test);
		$b_ok       = true;
		$s_mesg     = "";
		$s_fld_name = "";
		/** @noinspection PhpStatementHasEmptyBodyInspection */
		if ($i_len <= 0)
			//
			// empty test - true
			//
		{
		} elseif ($s_test == "!")
			//
			// test asserts false
			//
		{
			$b_ok = false;
		} elseif (($i_span = strcspn($s_test,$s_op_chars)) >= $i_len)
			//
			// no operator - just check field presence
			//
		{
			$s_fld_name = $s_test;
			$b_ok       = !$this->_FldManager->TestFieldEmpty($s_test,$s_mesg);
		} else {
			//
			// get first field name
			//
			$s_fld_name = $s_fld1 = trim(substr($s_test,0,$i_span));
			//
			// get the operator
			//
			$s_rem  = substr($s_test,$i_span);
			$i_span = strspn($s_rem,$s_op_chars);
			$s_oper = substr($s_rem,0,$i_span);
			switch ($s_oper) {
				case '&':
				case '|':
				case '^':
				case '=':
				case '!=':
					//
					// get the second field name
					//
					$s_fld2 = trim(substr($s_rem,$i_span));
					$b_ok   = FieldTest($s_oper,$s_fld1,$s_fld2,$this->_FldManager->GetFields(),$s_error_mesg);
					break;
				case '~':
				case '!~':
					//
					// get the regular expression
					//
					$s_pat = trim(substr($s_rem,$i_span));
					if (!$this->_FldManager->TestFieldEmpty($s_fld1,$s_mesg)) {
						$s_value = $this->_FldManager->GetFieldValue($s_fld1);
					} else {
						$s_value = "";
					}
					//echo "<p>Pattern: '".htmlspecialchars($s_pat)."': count=".preg_match($s_pat,$s_value)."<br /></p>";
					//
					// match the regular expression
					//
					if (preg_match($s_pat,$s_value) > 0) {
						$b_ok = ($s_oper == '~');
					} else {
						$b_ok = ($s_oper == '!~');
					}
					if (!$b_ok) {
						$aAlertInfo[] = GetMessage(MSG_PAT_FAILED,array("OPER"  => $s_oper,
						                                                "PAT"   => $s_pat,
						                                                "VALUE" => $s_value
						));
					}
					break;
				case '#=':
				case '#!=':
				case '#<':
				case '#>':
				case '#<=':
				case '#>=':
					//
					// numeric tests
					//
					$s_num = trim(substr($s_rem,$i_span));
					//
					// if this is a file field, get the size of the file for
					// numeric tests
					//
					if (($s_value = GetFileSize($s_fld1)) === false) {
						$s_value = $this->_FldManager->GetFieldValue($s_fld1);
					}
					if (strpos($s_num,'.') === false) {
						//
						// treat as integer
						//
						$m_num = (int)$s_num;
						$m_fld = (int)$s_value;
					} else {
						//
						// treat as floating point
						//
						$m_num = (float)$s_num;
						$m_fld = (float)$s_value;
					}
					switch ($s_oper) {
						case '#=':
							$b_ok = ($m_fld == $m_num);
							break;
						case '#!=':
							$b_ok = ($m_fld != $m_num);
							break;
						case '#<':
							$b_ok = ($m_fld < $m_num);
							break;
						case '#>':
							$b_ok = ($m_fld > $m_num);
							break;
						case '#<=':
							$b_ok = ($m_fld <= $m_num);
							break;
						case '#>=':
							$b_ok = ($m_fld >= $m_num);
							break;
					}
					break;
				default:
					SendAlert(GetMessage(MSG_COND_OPER,array("OPER" => $s_oper)));
					break;
			}
		}
		return $b_ok ? true : $s_fld_name;
	}

	//
	// Check the input for condition tests.
	//
	public function Check($a_vars,$a_file_vars = array())
	{
		$this->_FldManager->Init($a_vars,$a_file_vars);
		//
		// handle a list of conditions in an array
		//
		if (is_array($this->_mConditions)) {
			//
			// Sort the conditions by their numeric value.  This ensures
			// conditions are executed in the right order.
			//
			ksort($this->_mConditions,SORT_NUMERIC);
			foreach ($this->_mConditions as $m_key => $s_cond) {
				if (!$this->_checkString($s_cond,$m_key)) {
					return (false);
				}
			}
			return (true);
		} else {
			//
			// handle one set of conditions in a string
			//
			return $this->_checkString($this->_mConditions);
		}
	}

	/**
	 * Test a condition represented in a string.
	 *
	 * @param string   $s_cond the condition
	 * @param bool|int $m_id   ID of the condition being processed
	 *
	 * @return bool true if the condition passed, otherwise false
	 */
	private function _checkString($s_cond,$m_id = false)
	{
		$this->_sField = "conditions" . ($m_id === false ? "" : ($m_id + 1));
		if (!is_string($s_cond)) {
			SendAlert(GetMessage(MSG_INV_COND,array("FLD" => $this->_sField)));
			//
			// invalid conditions are simple "true"
			//
			return (true);
		}
		//
		// empty conditions are simply "true"
		//
		if ($s_cond == "") {
			return (true);
		}
		//
		// extract the separator characters
		//
		if (strlen($s_cond) < 2) {
			SendAlert(GetMessage(MSG_COND_CHARS,
			                     array("FLD" => $this->_sField,"COND" => $s_cond)));
			//
			// invalid conditions are simple "true"
			//
			return (true);
		}
		return $this->_checkString2($s_cond);
	}

	/**
	 * Test a condition represented in a string.  This method is called by _checkString to do the real
	 * work after some initial sanity checks.
	 *
	 * @param string $s_cond the condition
	 *
	 * @return bool true if the condition passed, otherwise false
	 */
	private function _checkString2($s_cond)
	{
		$s_list_sep  = $s_cond[0];
		$s_int_sep   = $s_cond[1];
		$s_full_cond = $s_cond = substr($s_cond,2);
		$b_bad       = false;
		$a_list      = TrimArray(explode($s_list_sep,$s_cond));
		for ($ii = 0 ; $ii < count($a_list) ; $ii++) {
			$s_curr_cond = $a_list[$ii];
			$i_len       = strlen($s_curr_cond);
			if ($i_len <= 0) {
				continue;
			}
			//
			// split the condition into its internal components
			//
			$a_components = TrimArray(explode($s_int_sep,$s_curr_cond));
			if (count($a_components) < 5) {
				SendAlert(GetMessage(MSG_COND_INVALID,
				                     array("FLD" => $this->_sField,"COND" => $s_curr_cond,
				                           "SEP" => $s_int_sep
				                     )));
				//
				// the smallest condition has 5 components
				//
				continue;
			}
			//
			// first component is ignored (it's blank)
			//
			$a_components = array_slice($a_components,1);
			switch ($a_components[0]) {
				case "TEST":
					if (!$this->_doTest($s_curr_cond,$a_components,$s_list_sep)) {
						$b_bad = true;
					}
					break;
				case "IF":
					if (!$this->_doIf($s_curr_cond,$a_components,$s_int_sep,$s_list_sep)) {
						$b_bad = true;
					}
					break;
				default:
					SendAlert(GetMessage(MSG_COND_UNK,
					                     array("FLD" => $this->_sField,"COND" => $s_curr_cond,
					                           "CMD" => $a_components[0]
					                     )));
					break;
			}
		}
		return (!$b_bad);
	}

	/**
	 * Process a TEST condition.
	 *
	 * @param string $s_cond       the condition
	 * @param array  $a_components components of the TEST condition
	 * @param string $s_list_sep   the separator for lists in the condition
	 *
	 * @return bool true if the condition passed, otherwise false
	 */
	private function _doTest($s_cond,$a_components,$s_list_sep)
	{
		//
		// sanity check - if failed, just pass the TEST
		//
		if (count($a_components) > 5) {
			SendAlert(GetMessage(MSG_COND_TEST_LONG,
			                     array("FLD" => $this->_sField,"COND" => $s_cond,
			                           "SEP" => $s_list_sep
			                     )));
			return true;
		} elseif (($m_test_result = $this->_runLogic($a_components[1])) === true) {
			return true;
		} else {
			$this->_recordField($m_test_result,$a_components[2]);
			return false;
		}
	}

	/**
	 * Process an IF condition.
	 *
	 * @param string $s_cond       the condition
	 * @param array  $a_components components of the TEST condition
	 * @param string $s_int_sep    the internal separator for the condition
	 * @param string $s_list_sep   the separator for lists in the condition
	 *
	 * @return bool true if the condition passed, otherwise false
	 */
	private function _doIf($s_cond,$a_components,$s_int_sep,$s_list_sep)
	{
		//
		// sanity checks - if failed, just pass the IF
		//
		if (count($a_components) < 6) {
			SendAlert(GetMessage(MSG_COND_IF_SHORT,
			                     array("FLD" => $this->_sField,"COND" => $s_cond,
			                           "SEP" => $s_int_sep
			                     )));
			return true;
		}
		if (count($a_components) > 7) {
			SendAlert(GetMessage(MSG_COND_IF_LONG,
			                     array("FLD" => $this->_sField,"COND" => $s_cond,
			                           "SEP" => $s_list_sep
			                     )));
			return true;
		}
		if (($m_test_result = $this->_runLogic($a_components[1])) === true) {
			$m_test_result = $this->_runLogic($a_components[2]);
		} else {
			$m_test_result = $this->_runLogic($a_components[3]);
		}
		if ($m_test_result !== true) {
			$this->_recordField($m_test_result,$a_components[4]);
			return false;
		} else {
			return true;
		}
	}

	private function _recordField($s_fld_name,$s_mesg)
	{
		$this->_sMissing                  .= $s_mesg . "\n";
		$this->_aMissingList[$s_fld_name] = $s_mesg;
	}

}

/**
 * Original interface to processing conditions.
 * This function is only kept for backwards compatibility with any existing hook code.
 *
 * @param mixed    $m_conditions   a single condition or an array of them
 * @param array    $a_vars         field values
 * @param string   $s_missing      returns the message from the condition's failure
 * @param array    $a_missing_list appended with the field name(s) that failed
 * @param bool|int $m_id           was never used - deprecated
 *
 * @return bool true if the condition passed, otherwise false
 * @noinspection PhpUnused
 */
function CheckConditions($m_conditions,$a_vars,&$s_missing,&$a_missing_list,$m_id = false)
{
	global $aFileVars;

	$cond = new Conditions($m_conditions,$s_missing,$a_missing_list);

	return $cond->Check($a_vars,$aFileVars);
}

//
// Return a formatted list of the given environment variables.
//
function GetEnvVars($list,$s_line_feed)
{
	global $aServerVars;

	$output = "";
	for ($ii = 0 ; $ii < count($list) ; $ii++) {
		$name = $list[$ii];
		if ($name && array_search($name,Settings::get('VALID_ENV'),true) !== false) {
			//
			// if the environment variable is empty or non-existent, try
			// looking for the value in the server vars.
			//
			if (($s_value = getenv($name)) === "" || $s_value === false) {
				if (isset($aServerVars[$name])) {
					$s_value = $aServerVars[$name];
				} else {
					$s_value = "";
				}
			}
			$output .= $name . "=" . $s_value . $s_line_feed;
		}
	}
	return ($output);
}

//
// open a socket connection to a filter and post the data there
// RJR: TO DO: re-implement using NetIO
//
function SocketFilter($filter,$a_filter_info,$m_data)
{
	static $b_in_here = false;
	global $php_errormsg;

	//
	// prevent recursive errors
	//
	if ($b_in_here) {
		return ("<DATA DISCARDED>");
	}
	$b_in_here = true;

	$a_errors = array();
	if (!isset($a_filter_info["site"])) {
		$a_errors[] = GetMessage(MSG_MISSING,array("ITEM" => "site"));
	} else {
		$s_site = $a_filter_info["site"];
	}

	if (!isset($a_filter_info["port"])) {
		$a_errors[] = GetMessage(MSG_MISSING,array("ITEM" => "port"));
	} else {
		$i_port = (int)$a_filter_info["port"];
	}

	if (!isset($a_filter_info["path"])) {
		$a_errors[] = GetMessage(MSG_MISSING,array("ITEM" => "path"));
	} else {
		$s_path = $a_filter_info["path"];
	}

	if (!isset($a_filter_info["params"])) {
		$a_params = array();
	} elseif (!is_array($a_filter_info["params"])) {
		$a_errors[] = GetMessage(MSG_NEED_ARRAY,array("ITEM" => "params"));
	} else {
		$a_params = $a_filter_info["params"];
	}

	if (!empty($a_errors)) {
		Error("bad_filter",GetMessage(MSG_FILTER_WRONG,array(
			"FILTER" => $filter,
			"ERRORS" => implode(', ',$a_errors)
		)),   false,false);
		FormMailExit();
	}

	//
	// ready to build the socket - we need a longer time limit for the
	// script if we're doing this; we allow 30 seconds for the connection
	// (should be instantaneous, especially if it's the same domain)
	//
	set_time_limit(60);
	/** @noinspection PhpUndefinedVariableInspection */
	/** @noinspection PhpUndefinedVariableInspection */
	@   $f_sock = fsockopen($s_site,$i_port,$i_errno,$s_errstr,30);
	if ($f_sock === false) {
		Error("filter_connect",GetMessage(MSG_FILTER_CONNECT,array(
			"FILTER" => $filter,
			"SITE"   => $s_site,
			"ERRNUM" => $i_errno,
			"ERRSTR" => "$s_errstr (" . CheckString($php_errormsg) . ")"
		)),
		      false,false);
		FormMailExit();
	}
	//
	// build the data to send
	//
	$m_request_data = array();
	$i_count        = 0;
	/** @noinspection PhpUndefinedVariableInspection */
	foreach ($a_params as $m_var) {
		$i_count++;
		//
		// if the parameter spec is an array, process it specially;
		// it must have "name" and "file" elements
		//
		if (is_array($m_var)) {
			if (!isset($m_var["name"])) {
				Error("bad_filter",GetMessage(MSG_FILTER_PARAM,
				                              array("FILTER" => $filter,
				                                    "NUM"    => $i_count,
				                                    "NAME"   => "name"
				                              )),false,false);
				fclose($f_sock);
				FormMailExit();
			}
			$s_name = $m_var["name"];
			if (!isset($m_var["file"])) {
				Error("bad_filter",GetMessage(MSG_FILTER_PARAM,
				                              array("FILTER" => $filter,
				                                    "NUM"    => $i_count,
				                                    "NAME"   => "file"
				                              )),false,false);
				fclose($f_sock);
				FormMailExit();
			}
			//
			// open the file and read its contents
			//
			@           $fp = fopen($m_var["file"],"r");
			if ($fp === false) {
				Error("filter_error",GetMessage(MSG_FILTER_OPEN_FILE,
				                                array("FILTER" => $filter,
				                                      "FILE"   => $m_var["file"],
				                                      "ERROR"  => CheckString($php_errormsg)
				                                )),false,false);
				fclose($f_sock);
				FormMailExit();
			}
			$s_data  = "";
			$n_lines = 0;
			while (!feof($fp)) {
				if (($s_line = fgets($fp,2048)) === false) {
					if (feof($fp)) {
						break;
					} else {
						Error("filter_error",GetMessage(MSG_FILTER_FILE_ERROR,
						                                array("FILTER" => $filter,
						                                      "FILE"   => $m_var["file"],
						                                      "ERROR"  => CheckString($php_errormsg),
						                                      "NLINES" => $n_lines
						                                )),false,false);
						fclose($f_sock);
						FormMailExit();
					}
				}
				$s_data .= $s_line;
				$n_lines++;
			}

			fclose($fp);
			$m_request_data[] = "$s_name=" . urlencode($s_data);
		} else {
			$m_request_data[] = (string)$m_var;
		}
	}
	//
	// add the data
	//
	if (is_array($m_data)) {
		$m_request_data[] = "data=" . urlencode(implode(Settings::get('BODY_LF'),$m_data));
	} else {
		$m_request_data[] = "data=" . urlencode($m_data);
	}
	$s_request = implode("&",$m_request_data);

	if (($i_pos = strpos($s_site,"://")) !== false) {
		$s_site_name = substr($s_site,$i_pos + 3);
	} else {
		$s_site_name = $s_site;
	}

	/** @noinspection PhpUndefinedVariableInspection */
	fputs($f_sock,"POST $s_path HTTP/1.0\r\n");
	fputs($f_sock,"Host: $s_site_name\r\n");
	fputs($f_sock,"Content-Type: application/x-www-form-urlencoded\r\n");
	fputs($f_sock,"Content-Length: " . strlen($s_request) . "\r\n");
	fputs($f_sock,"\r\n");
	fputs($f_sock,"$s_request\r\n");

	//
	// now read the response
	//
	$m_hdr    = "";
	$m_data   = "";
	$b_in_hdr = true;
	$b_ok     = false;
	while (!feof($f_sock)) {
		if (($s_line = fgets($f_sock,2048)) === false) {
			if (feof($f_sock)) {
				break;
			} else {
				Error("filter_failed",GetMessage(MSG_FILTER_READ_ERROR,
				                                 array("FILTER" => $filter,
				                                       "ERROR"  => CheckString($php_errormsg)
				                                 )),false,false);
				fclose($f_sock);
				FormMailExit();
			}
		}
		//
		// look for an "__OK__" line
		//
		if (trim($s_line) == "__OK__") {
			$b_ok = true;
		} elseif ($b_in_hdr) {
			//
			// blank line signals end of header
			//
			if (trim($s_line) == "") {
				$b_in_hdr = false;
			} else {
				$m_hdr .= $s_line;
			}
		} else {
			$m_data .= $s_line;
		}
	}
	//
	// if not OK, then report error
	//
	if (!$b_ok) {
		Error("filter_failed",GetMessage(MSG_FILTER_NOT_OK,
		                                 array("FILTER" => $filter,
		                                       "DATA"   => $m_data
		                                 )),false,false);
		fclose($f_sock);
		FormMailExit();
	}
	fclose($f_sock);
	$b_in_here = false;
	return ($m_data);
}

//
// run data through a supported filter
//
/** @noinspection PhpStatementHasEmptyBodyInspection */
function Filter($filter,$m_data)
{

	global $php_errormsg;
	static $b_in_here = false;

	//
	// prevent recursive errors
	//
	if ($b_in_here) {
		return ("<DATA DISCARDED>");
	}
	$b_in_here = true;

	//
	// Any errors sent in an alert are flagged to not run through the
	// filter - this also means the user's data won't be included in the
	// alert.
	// The reason for this is that the Filter is typically an encryption
	// program. If the filter fails, then sending the user's data in
	// clear text in an alert breaks the security of having encryption
	// in the first place!
	//

	//
	// find the filter
	//
	$a_filters = Settings::get('FILTERS');
	if (!isset($a_filters[$filter]) || $a_filters[$filter] == "") {
		//
		// check for SOCKET_FILTERS
		//
		$a_filters = Settings::get('SOCKET_FILTERS');
		if (!isset($a_filters[$filter]) || $a_filters[$filter] == "") {
			ErrorWithIgnore("bad_filter",GetMessage(MSG_FILTER_UNK,
			                                        array("FILTER" => $filter)),false,false);
			FormMailExit();
		}
		$m_data = SocketFilter($filter,$a_filters[$filter],$m_data);
	} elseif ($a_filters[$filter] == "null")
		//
		// do nothing - just return the original data unchanged
		//
	{
	} elseif ($a_filters[$filter] == "csv") {
		$m_data = BuiltinFilterCSV();
	} else {
		$cmd = $a_filters[$filter];
		//
		// get the program name - assumed to be the first blank-separated word
		//
		$a_words = preg_split('/\s+/',$cmd);
		$prog    = $a_words[0];

		$s_cwd = getcwd();
		//
		// change to the directory that contains the filter program
		//
		$dirname = dirname($prog);
		if ($dirname != "" && $dirname != "." && !chdir($dirname)) {
			Error("chdir_filter",
			      GetMessage(MSG_FILTER_CHDIR,
			                 array("DIR"    => $dirname,
			                       "FILTER" => $filter,
			                       "ERROR"  => CheckString($php_errormsg)
			                 )),
			      false,false);
			FormMailExit();
		}

		//
		// the output of the filter goes to a temporary file; this works
		// OK on Windows too, even with the Unix shell syntax.
		//
		$temp_file       = GetTempName("FMF");
		$temp_error_file = GetTempName("FME");
		$cmd             = "$cmd >$temp_file 2>$temp_error_file";
		//
		// start the filter
		//
		$pipe = popen($cmd,"w");
		if ($pipe === false) {
			$s_sv_err = CheckString($php_errormsg);
			$err      = join('',file($temp_error_file));
			unlink($temp_file);
			unlink($temp_error_file);
			Error("filter_not_found",
			      GetMessage(MSG_FILTER_NOTFOUND,
			                 array("CMD"    => $cmd,
			                       "FILTER" => $filter,
			                       "ERROR"  => $s_sv_err
			                 )),
			      false,
			      false,
			      $err);
			FormMailExit();
		}
		//
		// write the data to the filter
		//
		if (is_array($m_data)) {
			fwrite($pipe,implode(Settings::get('BODY_LF'),$m_data));
		} else {
			fwrite($pipe,$m_data);
		}
		if (($i_st = pclose($pipe)) != 0) {
			$s_sv_err = CheckString($php_errormsg);
			$err      = join('',file($temp_error_file));
			unlink($temp_file);
			unlink($temp_error_file);
			Error("filter_failed",GetMessage(MSG_FILTER_ERROR,
			                                 array("FILTER" => $filter,
			                                       "ERROR"  => $s_sv_err,
			                                       "STATUS" => $i_st
			                                 )),false,false,$err);
			FormMailExit();
		}
		//
		// read in the filter's output and return as the data to be sent
		//
		$m_data = join('',file($temp_file));
		unlink($temp_error_file);
		unlink($temp_file);

		//
		// return to previous directory
		//
		chdir($s_cwd);
	}
	$b_in_here = false;
	return ($m_data);
}

/*
 * Function:    FilterFiles
 * Parameters:  $a_files    list of file uploads to filter
 * Returns:     void
 * Description:
 *  Run the given files through any filter for which they are specified.
 */
function FilterFiles(&$a_files)
{
	global $SPECIAL_VALUES;

	FMDebug("FilterFiles " . count($a_files));
	if (!GetFilterSpec($s_filter,$a_filter_list,true) || $a_filter_list === false)
		//
		// no filter or file fields to filter
		//
	{
		return;
	}
	if (($s_mime = GetFilterAttrib($s_filter,"MIME")) === false) {
		$s_mime = "";
	}
	//
	// now, for each file, check if it is specified in the filter list
	//
	foreach ($a_files as $s_fld => $a_upload) {
		FMDebug("Checking $s_fld");
		if (!FieldManager::IsUploadedFile($a_upload)) {
			FMDebug("Not uploaded");
			//
			// failed security check
			//
			continue;
		}
		if (!in_array($s_fld,$a_filter_list,true)) {
			FMDebug("Not to be filtered");
			continue;
		}
		/*** not sure what to do with this....
		 * if (isset($a_upload["error"]))
		 * //
		 * // there was an upload error
		 * //
		 * continue;
		 ***/
		//
		// this file upload has been specified for filtering
		//
		$s_file_name = $a_upload["tmp_name"];
		//
		// check if the file has been saved elsewhere
		//
		if (isset($a_upload["saved_as"]) && !empty($a_upload["saved_as"])) {
			$s_file_name = $a_upload["saved_as"];
		}
		FMDebug("File name is $s_file_name");
		//
		// read in the file
		//
		if (($s_data = ReadInFile($s_file_name,"upload")) === false) {
			Error("filter_files",GetMessage(MSG_FILE_UPLOAD_ERR_UNK,array("ERRNO" => "reading $s_fld")),false,
			      false);
		}
		//
		// filter and write it back out to the same file
		//
		$s_data = Filter($s_filter,$s_data);
		if (!WriteOutFile($s_file_name,$s_data,"upload")) {
			Error("filter_files",GetMessage(MSG_FILE_UPLOAD_ERR_UNK,array("ERRNO" => "writing $s_fld")),false,
			      false);
		}
		//
		// update size and MIME type for this upload
		//
		$a_upload["size"] = strlen($s_data);
		if ($s_mime !== "") {
			$a_upload["type"] = $s_mime;
		}
		$a_files[$s_fld] = $a_upload;
	}
}

/*
 * Function:    ReadInFile
 * Parameters:  $s_file_name    the name of the file
 *              $s_file_error_type type of file for any error message
 *              $b_text         if true, read file as text
 * Returns:     mixed           the entire contents of the file
 *                              as a string, or false on error
 * Description:
 *  Reads the contents of a file into a string.
 */
function ReadInFile($s_file_name,$s_file_error_type,$b_text = false)
{
	global $php_errormsg;

	if (($fp = @fopen($s_file_name,"r" . ($b_text ? "t" : "b"))) === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $s_file_name,
		                                               "TYPE"  => "read " . $s_file_error_type,
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return (false);
	}
	$s_data = "";
	while (!feof($fp)) {
		$s_data .= fread($fp,8192);
	}
	@fclose($fp);
	return ($s_data);
}

/*
 * Function:    WriteOutFile
 * Parameters:  $s_file_name    the name of the file
 *              $s_data         the data to write
 *              $s_file_error_type type of file for any error message
 *              $b_text         if true, read file as text
 * Returns:     bool            true on success, otherwise false
 * Description:
 *  Writes the contents of a file from a string.
 */
function WriteOutFile($s_file_name,$s_data,$s_file_error_type,$b_text = false)
{
	global $php_errormsg;

	if (($fp = @fopen($s_file_name,"w" . ($b_text ? "t" : "b"))) === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $s_file_name,
		                                               "TYPE"  => "write " . $s_file_error_type,
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return (false);
	}
	if (fwrite($fp,$s_data) < strlen($s_data)) {
		@fclose($fp);
		return (false);
	}
	@fclose($fp);
	return (true);
}

/*
 * Class:       CSVFormat
 * Description:
 *  Manages formatting of CSV content.
 */

class   CSVFormat
{
	var $_cSep; /* field separator character */
	var $_cQuote; /* field quote character */
	var $_cIntSep; /* internal separator character (for lists) */
	var $_sEscPolicy; /* escape processing policy */
	var $_sCleanFunc; /* cleaning function for fields */

	/*
     * Method:      CSVFormat ctor
     * Parameters:  $c_sep          the field separator
     *              $c_quote        the quote character to use
     *              $c_int_sep      the internal field separator to use
     *              $s_esc_policy   escape processing policy to use
     *              $s_clean_func   a cleaning function
     * Returns:     n/a
     * Description:
     *  Constructs the object.
     */
	function __construct($c_sep = ',',$c_quote = '"',$c_int_sep = ';',
	                     $s_esc_policy = "backslash",$s_clean_func = NULL)
	{
		$this->SetSep($c_sep);
		$this->SetQuote($c_quote);
		$this->SetIntSep($c_int_sep);
		$this->SetEscPolicy($s_esc_policy);
		$this->SetCleanFunc($s_clean_func);
	}

	/*
     * Method:      SetEscPolicy
     * Parameters:  $s_esc_policy   a string specifying the escape processing
     *                              policy to use
     * Returns:     void
     * Description: 
     *  Set the escape processing policy.
     */
	function SetEscPolicy($s_esc_policy)
	{
		switch ($s_esc_policy) {
			default: /* should generate a warning */
			case "backslash":
				$this->_sEscPolicy = "b";
				break;
			case "double":
				$this->_sEscPolicy = "d";
				break;
			case "strip":
				$this->_sEscPolicy = "s";
				break;
			case "conv":
				$this->_sEscPolicy = "c";
				break;
		}
	}

	/*
     * Method:      SetSep
     * Parameters:  $c_sep      the separator character to use
     * Returns:     void
     * Description: 
     *  Set the separator character for between fields.
     */
	function SetSep($c_sep)
	{
		$this->_cSep = $c_sep;
	}

	/*
     * Method:      SetQuote
     * Parameters:  $c_quote      the quote character to use
     * Returns:     void
     * Description: 
     *  Set the quote character for quoting fields.
     */
	function SetQuote($c_quote)
	{
		$this->_cQuote = $c_quote;
	}

	/*
     * Method:      SetIntSep
     * Parameters:  $c_int_sep      the internal separator character to use
     * Returns:     void
     * Description: 
     *  Set the internal separator character for inside fields.
     */
	function SetIntSep($c_int_sep)
	{
		$this->_cIntSep = $c_int_sep;
	}

	/*
     * Method:      SetCleanFunc
     * Parameters:  $s_clean_func   the name of a cleaning function (can be NULL)
     * Returns:     void
     * Description: 
     *  Set the cleaning function for fields.
     */
	function SetCleanFunc($s_clean_func)
	{
		$this->_sCleanFunc = $s_clean_func;
	}

	/*
     * Method:      _Escape
     * Parameters:  $m_value    the field value; string or array of strings 
     * Returns:     mixed       the field value escaped according to the
     *                          escape processing policy
     * Description: 
     *  Escapes a field value according to the configured requirements.
     */
	function _Escape($m_value)
	{
		switch ($this->_sEscPolicy) {
			default: /* should generate an error */
			case "b":
				/*
                     * 'backslash' escape policy: replace \ with \\ and
                     * " with \"
                     */
				$m_value = str_replace("\\","\\\\",$m_value);
				$m_value = str_replace($this->_cQuote,"\\" . $this->_cQuote,
				                       $m_value);
				break;
			case "d":
				/*
                     * 'double' escape policy: replace " with ""
                     * This is suitable for Microsoft apps such as Excel
                     * and Access. It also meets the specification of
                     * RFC4180, though this RFC only specified double
                     * quotes whereas we handle any quote character.
                     */
				$m_value = str_replace($this->_cQuote,
				                       $this->_cQuote . $this->_cQuote,$m_value);
				break;
			case "s":
				/*
                     * 'strip' escape policy: strip quotes
                     */
				$m_value = str_replace($this->_cQuote,"",$m_value);
				break;
			case "c":
				/*
                     * 'conv' escape policy: convert quotes to the other quotes
                     */
				switch ($this->_cQuote) {
					case '"':
						/*
                         * convert double quotes in the data to single quotes
                         */
						$m_value = str_replace("\"","'",$m_value);
						break;
					case '\'':
						/*
                         * convert single quotes in the data to double quotes
                         */
						$m_value = str_replace("'","\"",$m_value);
						break;
					default:
						/*
                         * otherwise, leave the data unchanged
                         */
						break;
				}
				break;
		}
		return ($m_value);
	}

	/*
     * Method:      _Format
     * Parameters:  $s_value    the string value to format
     *              $s_format   a format specification
     *                          a list of characters:
     *                              c   CleanValue
     *                              s   force to be a string
     *                              r   remove carriage returns
     * Returns:     string      the formatted value
     * Description: 
     *  Formats a value.
     */
	function _Format($s_value,$s_format = "")
	{
		$s_value  = $this->_Escape($s_value);
		$s_prefix = "";
		/*
             * now implement any special formatting to overcome
             * problems with importing
             */
		$i_len = strlen($s_format);
		for ($ii = 0 ; $ii < $i_len ; $ii++) {
			switch ($s_format[$ii]) {
				case "c":
					/*
                     * implement "c" formatting - CleanValue
                     */
					$s_value = CleanValue($s_value);
					break;
				case "r":
					/*
                     * implement "r" formatting - remove
                     * carriage returns.  Useful for Microsoft Excel
                     */
					$s_value = str_replace("\r","",$s_value);
					break;
				case "s":
					/*
                     * implement "s" formatting - force
                     * a value to be a string (by making it a string
                     * formula).  Useful for Microsoft Excel and OpenOffice
                     * spreadsheet, which don't understand numeric phone
                     * numbers, for example.
                     */
					if (strlen($s_value) > 0) {
						$s_prefix = "=";
					}
					break;
			}
		}

		return ($s_prefix . $this->_cQuote . $s_value . $this->_cQuote);
	}

	/*
     * Method:      _GetColumn
     * Parameters:  $s_col_spec a column specification
     * Returns:     array       the column name and the format specifier, if
     *                          any
     * Description: 
     *  Returns the column name and any format specifier.
     */
	function _GetColumn($s_col_spec)
	{
		$s_format = "";
		if (($i_pos = strpos($s_col_spec,":")) !== false) {
			$s_col_name = trim(substr($s_col_spec,0,$i_pos));
			$s_format   = trim(substr($s_col_spec,$i_pos + 1));
		} else {
			$s_col_name = $s_col_spec;
		}
		return (array($s_col_name,$s_format));
	}

	/*
     * Method:      MakeCSVRecord
     * Parameters:  $a_column_list  a list of column names (field names) to
     *                              include; can include format specifiers
     *              $a_vars         raw data array indexed by column name
     *                              (field name).
     *                              A data value can be a string or an array
     *                              of strings.
     * Returns:     string          the comma-separated value    
     * Description: 
     *  Creates a single CSV record for a list of columns.
     */
	function MakeCSVRecord($a_column_list,$a_vars)
	{
		$s_rec     = "";
		$n_columns = count($a_column_list);
		for ($ii = 0 ; $ii < $n_columns ; $ii++) {
			list($s_col_name,$s_format) = $this->_GetColumn($a_column_list[$ii]);
			/*
                 * if a column is specified it must be included, even if there
                 * is no data for it.
                 */
			$s_value = GetFieldValue($s_col_name,$a_vars,$this->_cIntSep);
			if (isset($this->_sCleanFunc)) {
				$s_func  = $this->_sCleanFunc;
				$s_value = $s_func($s_value);
			}

			$s_value = $this->_Format($s_value,$s_format);
			if ($ii > 0) /*
                     * prepend the separator from the second field onwards
                     */ {
				$s_rec .= $this->_cSep;
			}
			$s_rec .= $s_value;
		}
		return ($s_rec);
	}

	/*
     * Method:      MakeHeading
     * Parameters:  $a_column_list  a list of column names (field names) to
     *                              include
     * Returns:     string          the comma-separated heading record    
     * Description: 
     *  Creates a heading record for the CSV data.
     */
	function MakeHeading($a_column_list)
	{
		$s_rec     = "";
		$n_columns = count($a_column_list);
		for ($ii = 0 ; $ii < $n_columns ; $ii++) {
			list($s_col_name,$s_format) = $this->_GetColumn($a_column_list[$ii]);
			$s_value = $this->_Format($s_col_name);
			if ($ii > 0) /*
                     * prepend the separator from the second field onwards
                     */ {
				$s_rec .= $this->_cSep;
			}
			$s_rec .= $s_value;
		}
		return ($s_rec);
	}
}

/*
 * Built-in filter.  Generates CSV (comma separated values) content from
 * the submitted fields. The special field "filter_fields" determines
 * which fields to include in the CSV content.
 * The following options are support in "filter_options":
 *      CSVHeading  if set, includes a heading line first with the field names
 *      CSVSep      specifies a separator character instead of comma
 *      CSVIntSep   specifies an internal separator character for lists
 *      CSVQuote    specifies the character to use to quote each column; default
 *                  is double quotes
 *      CSVEscPolicy controls the way quotes are escaped in the data.  Supported
 *                  values are: backslash (the default),double,strip
 *      CSVRaw      if set, then the fields are recorded as raw values and
 *                  are *not* cleaned according to FormMail's normal field
 *                  cleaning process.
 * If the "filter_fields" field does not exist, then the "csvcolumns" field is
 * used instead.  If neither exist, then all fields are included along with
 * a Heading line.
 */
function BuiltinFilterCSV()
{
	global $aAllRawValues,$aRawDataValues,$SPECIAL_VALUES;

	$b_heading     = false;
	$a_column_list = array();
	$s_cols        = $SPECIAL_VALUES["filter_fields"];
	if (!isset($s_cols) || empty($s_cols) || !is_string($s_cols)) {
		$s_cols = $SPECIAL_VALUES["csvcolumns"];
		if (!isset($s_cols) || empty($s_cols) || !is_string($s_cols)) {
			/*
			 * neither filter_fields nor csvcolumns defined - get all columns
			 */
			$s_cols = "";
			/*
			 * special case - include these two special fields
			 */
			$a_column_list = array("email","realname");
			/*
			 * now include all the data fields
			 */
			$a_column_list = array_merge($a_column_list,
			                             array_keys($aRawDataValues));
			$b_heading     = true;
		}
	}
	if (empty($a_column_list)) {
		$a_column_list = TrimArray(explode(",",$s_cols));
	}

	$csv_format = new CSVFormat();

	/*
	 * get the various options and set them
	 */
	$m_temp = GetFilterOption("CSVQuote");
	if (isset($m_temp)) {
		$csv_format->SetQuote($m_temp);
	}
	$m_temp = GetFilterOption("CSVSep");
	if (isset($m_temp)) {
		$csv_format->SetSep($m_temp);
	}
	$m_temp = GetFilterOption("CSVIntSep");
	if (isset($m_temp)) {
		$csv_format->SetIntSep($m_temp);
	}
	$m_temp = GetFilterOption("CSVEscPolicy");
	if (isset($m_temp)) {
		$csv_format->SetEscPolicy($m_temp);
	}
	$m_temp = GetFilterOption("CSVHeading");
	if (isset($m_temp)) {
		$b_heading = true;
	}

	/*
	 * clean fields unless CSVRaw is specified
	 */
	$m_temp = GetFilterOption("CSVRaw");
	if (!isset($m_temp)) {
		$csv_format->SetCleanFunc(function ($m_value) {
			return CleanValue($m_value);
		});
	}

	$s_csv = $csv_format->MakeCSVRecord($a_column_list,$aAllRawValues);

	if ($b_heading) {
		$s_head = $csv_format->MakeHeading($a_column_list);
		/*
		 * return the heading and the record with $CSVLINE as record separator
		 */
		return ($s_head . Settings::get('CSVLINE') . $s_csv . Settings::get('CSVLINE'));
	} else {
		/*
		* return this record with $CSVLINE appended
		*/
		return ($s_csv . Settings::get('CSVLINE'));
	}
}

$aSubstituteErrors  = array();
$SubstituteFields   = NULL;
$sSubstituteMissing = NULL;

//
// Run htmlspecialchars on every value in an array.
//
function ArrayHTMLSpecialChars($a_list)
{
	$a_new = array();
	foreach ($a_list as $m_key => $m_value) {
		if (is_array($m_value)) {
			$a_new[$m_key] = ArrayHTMLSpecialChars($m_value);
		} else {
			$a_new[$m_key] = htmlspecialchars($m_value);
		}
	}
	return ($a_new);
}

//
// Truncate a value based on the specified maximums.
//
function Truncate($s_value,$n_max_chars,$n_max_lines)
{
	if ($n_max_lines > 0) {
		$a_lines = explode("\n",$s_value);
		if (count($a_lines) > $n_max_lines) {
			$a_lines = array_slice($a_lines,0,$n_max_lines);
			$s_value = implode("\n",$a_lines);
			$s_value .= "...";
		}
	}
	if ($n_max_chars > 0) {
		$a_lines = explode("\n",$s_value);
		for ($ii = 0 ; $ii < count($a_lines) ; $ii++) {
			$n_len = strlen($a_lines[$ii]);
			$s_eol = "";
			if (substr($a_lines[$ii],-1) == "\n") {
				$n_len--;
				$s_eol = "\n";
			}
			if ($n_len > $n_max_chars) {
				$a_lines[$ii] = substr($a_lines[$ii],0,$n_max_chars) . "..." . $s_eol;
			}
		}
		$s_value = implode("\n",$a_lines);
	}
	return ($s_value);
}

//
// Worker function for SubstituteValue and SubstituteValueForPage.
// Returns the value of the matched variable name.
// Variables are searched for in the global $SubstituteFields.
// If no such variable exists, an error is reported or the given
// replacement string is used.
// Errors are stored in the global $aSubstituteErrors.
//
function SubstituteValueWorker($a_matches,$s_repl,$b_html = true)
{
	/**
	 * @global FieldManager $SubstituteFields
	 */
	global $aSubstituteErrors,$SubstituteFields,$SPECIAL_VALUES;

	$b_insert_br = true; // option to put "<br />" tags before newlines in HTML templates
	$n_max_chars = 0;
	$n_max_lines = 0;
	$s_list_sep  = $SPECIAL_VALUES['template_list_sep'];
	$b_text_subs = false;

	$s_name = $a_matches[0];
	assert(strlen($s_name) > 1 && $s_name[0] == '$');
	$s_name = substr($s_name,1);
	if (($i_len = strlen($s_name)) > 0 && $s_name[0] == '{') {
		assert($s_name[$i_len - 1] == '}');
		$s_name = substr($s_name,1,-1);
		//
		// grab any processing options
		//
		$a_args = explode(":",$s_name);
		$s_name = $a_args[0];
		if (($n_args = count($a_args)) > 1) {
			for ($ii = 1 ; $ii < $n_args ; $ii++) {
				//
				// some options are followed by =X
				// where X is a value
				//
				$s_param = "";
				if (($i_pos = strpos($a_args[$ii],'=')) !== false) {
					$s_param = substr($a_args[$ii],$i_pos + 1);
					$s_opt   = substr($a_args[$ii],0,$i_pos);
				} else {
					$s_opt = $a_args[$ii];
				}
				switch ($s_opt) {
					case "nobr":
						$b_insert_br = false;
						break;
					case "chars":
						if ($s_param !== "") {
							$n_max_chars = (int)$s_param;
						}
						break;
					case "lines":
						if ($s_param !== "") {
							$n_max_lines = (int)$s_param;
						}
						break;
					case "sep":
						if ($s_param !== "") {
							$s_list_sep = $s_param;
						}
						break;
					case "subs":
						$b_text_subs = true;
						break;
				}
			}
		}
	}
	$s_value = "";
	$s_mesg  = "";
	if ($SubstituteFields->IsFieldSet($s_name) &&
	    !$SubstituteFields->TestFieldEmpty($s_name,$s_mesg)
	) {
		if ($b_html)
			//
			// Up to and including version 8.24, the code used
			// htmlspecialchars.  Version 8.28 caused UTF-8 template
			// processing to break, because it started using htmlentities
			// without specifying the charset.
			//
		{
			$s_value = $SubstituteFields->GetSafeFieldValue($s_name,$b_text_subs,$s_list_sep);
		} else {
			$s_value = $SubstituteFields->GetFieldValue($s_name,$s_list_sep);
		}
		$s_value = Truncate($s_value,$n_max_chars,$n_max_lines);
		if ($b_html && $b_insert_br)
			//
			// Insert HTML line breaks before newlines.
			//
		{
			$s_value = nl2br($s_value);
		}
	} elseif (isset($SPECIAL_VALUES[$s_name])) {
		$s_value = $b_html ?
			htmlspecialchars((string)$SPECIAL_VALUES[$s_name]) :
			(string)$SPECIAL_VALUES[$s_name];
		$s_value = Truncate($s_value,$n_max_chars,$n_max_lines);
	} elseif (isset($s_repl))
		//
		// If a replacement value has been specified use it, and
		// don't call htmlspecialchars.  This allows the use
		// of HTML tags in a replacement string.
		//
	{
		$s_value = $s_repl;
	} else {
		$s_value = "";
	}
	return ($s_value);
}

//
// Callback function for preg_replace_callback.  Returns the value
// of the matched variable name.
// Variables are searched for in the global $SubstituteFields.
// If no such variable exists, an error is reported or an special
// replacement string is used.
// Errors are stored in the global $aSubstituteErrors.
//
/** @noinspection PhpUnused */
function SubstituteValue($a_matches)
{
	global $sSubstituteMissing;

	return (SubstituteValueWorker($a_matches,$sSubstituteMissing));
}

//
// Callback function for preg_replace_callback.  Returns the value
// of the matched variable name.
// Variables are searched for in the global $SubstituteFields.
// If no such variable exists, an error is reported or an special
// replacement string is used.
// Errors are stored in the global $aSubstituteErrors.
//
/** @noinspection PhpUnused */
function SubstituteValuePlain($a_matches)
{
	global $sSubstituteMissing;

	return (SubstituteValueWorker($a_matches,$sSubstituteMissing,false));
}

//
// Callback function for preg_replace_callback.  Returns the value
// of the matched variable name.
// Variables are searched for in the global $SubstituteFields.
// If no such variable exists, the empty string is substituted.
// Errors are stored in the global $aSubstituteErrors.
//
/** @noinspection PhpUnused */
function SubstituteValueForPage($a_matches)
{
	return (SubstituteValueWorker($a_matches,""));
}

//
// Callback function for preg_replace_callback.  Returns
// exactly what was matched.
//
/** @noinspection PhpUnused */
function SubstituteValueDummy($a_matches)
{
	return ($a_matches[0]);
}

//
// Process the given HTML template and fill the fields.
//
function DoProcessTemplate($s_dir,$s_url,$s_template,&$a_lines,
                           $a_values,$s_missing,$s_subs_func)
{
	global $aSubstituteErrors,$SubstituteFields,$sSubstituteMissing,$aFileVars;

	if (($a_template_lines = LoadTemplate($s_template,$s_dir,
	                                      $s_url,true)) === false
	) {
		return (false);
	}
	FMDebug("Template '$s_template' contains " . count($a_template_lines) . " lines");

	$b_ok = true;
	//
	// initialize the errors list
	//
	$aSubstituteErrors = array();
	//
	// initialize the values
	//
	$SubstituteFields   = new FieldManager($a_values,$aFileVars);
	$sSubstituteMissing = $s_missing;

	foreach ($a_template_lines as $s_line) {
		//
		// search for words in these forms:
		//      $word
		//      ${word:options}
		// where word begins with an alphabetic character and
		// consists of alphanumeric and underscore
		//
		$a_lines[] = preg_replace_callback('/\$[a-z][a-z0-9_]*|\$\{[a-z][a-z0-9_]*(:[^\}]*)*\}/i',
		                                   $s_subs_func,$s_line);
	}

	FMDebug("DoProcessTemplate error count=" . count($aSubstituteErrors));
	if (count($aSubstituteErrors) != 0) {
		SendAlert(GetMessage(MSG_TEMPLATE_ERRORS,array("NAME" => $s_template)) .
		          implode("\n",$aSubstituteErrors));
		$b_ok = false;
	}
	global $FMCTemplProc;

	//
	// note that it's possible for an old version of FormMail Computation
	// module to get loaded which doesn't provide FMCTemplProc
	//
	if ($b_ok && Settings::get('ADVANCED_TEMPLATES') && isset($FMCTemplProc)) {
		$s_buf = implode("\n",$a_lines);
		/*
             * Look for a string that means we can skip advanced template
             * processing on this template.  The string is "FormMail-Basic-Template".
             */
		if (strpos($s_buf,"FormMail-Basic-Template") === FALSE) {
			$a_mesgs = array();
			/*foreach ($a_lines as $i_lno=>$s_line)
                if (strpos($s_line,"\n") !== false)
                    SendAlert("Line $i_lno has a newline");*/
			set_time_limit(60);
			if (($m_result = $FMCTemplProc->Process($s_buf,$a_mesgs)) === false) {
				$s_msgs = "\n";
				foreach ($a_mesgs as $a_msg) {
					$s_msgs .= "Line " . $a_msg["LINE"];
					$s_msgs .= ", position " . $a_msg["CHAR"] . ": ";
					$s_msgs .= $a_msg["MSG"] . "\n";
				}
				Error("fmadvtemplates",GetMessage(MSG_TEMPL_PROC,
				                                  array("ERRORS" => $s_msgs)),false,false);
				$b_ok = false;
			} else {
				/*foreach ($m_result as $i_lno=>$s_line)
                    if (($nn = substr_count($s_line,"\n")) > 1)
                        SendAlert("Result line $i_lno has $nn newlines");*/
				//
				// strip the new lines
				//
				$a_lines = explode("\n",implode("",$m_result));
			}
			$a_alerts = $FMCTemplProc->GetAlerts();
			if (count($a_alerts) > 0) {
				SendAlert(GetMessage(MSG_TEMPL_ALERT,
				                     array("ALERTS" => implode("\n",$a_alerts))));
			}
			$a_debug = $FMCTemplProc->GetDebug();
			if (count($a_debug) > 0) {
				SendAlert(GetMessage(MSG_TEMPL_DEBUG,
				                     array("DEBUG" => implode("\n",$a_debug))));
			}
		}
	}

	return ($b_ok);
}

//
// Process the given HTML template and fill the fields.
//
function ProcessTemplate($s_template,&$a_lines,$a_values,$s_missing = NULL,
                         $s_subs_func = 'SubstituteValue')
{

	if (Settings::isEmpty('TEMPLATEDIR') && Settings::isEmpty('TEMPLATEURL')) {
		SendAlert(GetMessage(MSG_TEMPLATES));
		return (false);
	}
	return (DoProcessTemplate(Settings::get('TEMPLATEDIR'),Settings::get('TEMPLATEURL'),$s_template,$a_lines,
	                          $a_values,$s_missing,$s_subs_func));
}

//
// Output the given HTML template after filling in the fields.
//
function OutputTemplate($s_template,$a_values)
{
	$a_lines = array();
	if (!ProcessTemplate($s_template,$a_lines,$a_values,"",'SubstituteValueForPage')) {
		Error("template_failed",GetMessage(MSG_TEMPLATE_FAILED,
		                                   array("NAME" => $s_template)),false,false);
	} else {
		for ($ii = 0 ; $ii < count($a_lines) ; $ii++) {
			echo $a_lines[$ii] . "\n";
		}
	}
}

//
// This function handles input type fields.
//
function RemoveFieldValue($s_name,$s_buf)
{
	//
	// we search for:
	//  <input ... name="thename" ... >
	// and change it to:
	//  <!-- disabled by FormMail: input ... name="thename" ... -->
	//

	// handle name attribute first
	$s_pat = '/<(\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*)>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'<!-- disabled by FormMail: $1 -->',$s_buf);

	return ($s_buf);
}

//
// Quote special characters in a replacement expression
// for preg_replace.
//
function RegReplaceQuote($s_value)
{
	return (str_replace('$','\\$',str_replace('\\','\\\\',$s_value)));
}

//
// This function handles input type "text" and "password"
//
function FixInputText($s_name,$s_value,$s_buf)
{
	//
	// we search for:
	//  <input type="text" name="thename"...
	// and change it to:
	//  <input type="text" name="thename" value="thevalue" ...
	//
	// Note that the value attribute must appear *after* the
	// type and name attributes.
	//

	//
	// first strip any current value attribute for the field
	//

	//
	// (?:) is a grouping subpattern that does no capturing
	//

	// handle type attribute first
	$s_pat = '/(<\s*input[^>]*type="(?:text|password|email|tel|search|number|date|month|datetime|datetime-local|time|week|range|url|color)"[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*)(value="[^"]*")([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	// handle name attribute first
	$s_pat = '/(<\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*type="(?:text|password|email|tel|search|number|date|month|datetime|datetime-local|time|week|range|url|color)"[^>]*)(value="[^"]*")([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	//
	// now add in the new value
	//
	$s_repl = '$1 value="' . htmlspecialchars(RegReplaceQuote($s_value)) . '" $2>';

	// handle type attribute first
	$s_pat = '/(<\s*input[^>]*type="(?:text|password|email|tel|search|number|date|month|datetime|datetime-local|time|week|range|url|color)"[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,$s_repl,$s_buf);

	// handle name attribute first
	$s_pat = '/(<\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*type="(?:text|password|email|tel|search|number|date|month|datetime|datetime-local|time|week|range|url|color)"[^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,$s_repl,$s_buf);

	return ($s_buf);
}

//
// This function handles textareas.
//
function FixTextArea($s_name,$s_value,$s_buf)
{
	//
	// we search for:
	//  <textarea name="thename"...>value</textarea>
	// and change it to:
	//  <textarea name="thename"...>new value</textarea>
	//

	$s_pat = '/(<\s*textarea[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*)>.*?<\s*\/\s*textarea\s*>';
	$s_pat .= '/ims';
	//
	// we exclude the closing '>' from the match above so that
	// we can put it below.  We need to do this so that the replacement
	// string is not faulty if the value begins with a digit:
	//      $19 Some Street
	//
	$s_repl = '$1>' . htmlspecialchars(RegReplaceQuote($s_value)) . '</textarea>';
	$s_buf  = preg_replace($s_pat,$s_repl,$s_buf);

	return ($s_buf);
}

//
// This function handles radio buttons and non-array checkboxes.
//
function FixButton($s_name,$s_value,$s_buf)
{
	//
	// we search for:
	//  <input type="radio" name="thename" value="thevalue" ...
	// and change it to:
	//  <input type="radio" name="thename" value="thevalue" checked="checked"
	//
	// Note that the value attribute must appear *after* the
	// type and name attributes.
	//

	//
	// first strip any current checked attributes
	//

	//
	// (?:) is a grouping subpattern that does no capturing
	//

	// handle type attribute first
	// match: input tag with type 'radio' or 'checkbox' with attribute
	// 'checked' or 'checked="checked"'
	//              <A NAME="PatternInfo">
	//      [^>]*?[^"\w] matches up to a word boundary starting with
	//      'checked' but not '"checked'
	//      (="checked"|(?=[^"\w]))? this matches:
	//              nothing
	//              ="checked"
	//              any character except a word character or " (without
	//              consuming it)
	//
	$s_pat = '/(<\s*input[^>]*type="(?:radio|checkbox)"[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*?[^"\w])checked(="checked"|(?=[^"\w]))?([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	// handle name attribute first
	$s_pat = '/(<\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*type="(?:radio|checkbox)"[^>]*?[^"\w])checked(="checked"|(?=[^"\w]))?([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	// handle type attribute first
	$s_pat = '/(<\s*input[^>]*type="(?:radio|checkbox)"[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*value="';
	$s_pat .= preg_quote($s_value,"/");
	$s_pat .= '")([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$2 checked="checked" $3>',$s_buf);

	// handle name attribute first
	$s_pat = '/(<\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '"[^>]*type="(?:radio|checkbox)"[^>]*value="';
	$s_pat .= preg_quote($s_value,"/");
	$s_pat .= '")([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$2 checked="checked" $3>',$s_buf);

	return ($s_buf);
}

//
// This function handles checkboxes as an array of values.
//
function FixCheckboxes($s_name,$a_values,$s_buf)
{
	//global $aDebug;

	//
	// we search for:
	//  <input type="checkbox" name="thename" value="thevalue" ...
	// and change it to:
	//  <input type="checkbox" name="thename" value="thevalue" checked
	//
	// Note that the value attribute must appear *after* the
	// type and name attributes.
	//

	//
	// first strip any current checked attributes
	//
	//$aDebug[] = "FixCheckboxes: Name='$s_name'";

	// handle type attribute first
	// see <A HREF="fmbadhandler.php#PatternInfo">
	$s_pat = '/(<\s*input[^>]*type="checkbox"[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '\[]"[^>]*?[^"\w])checked(="checked"|(?=[^"\w]))?([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	// handle name attribute first
	$s_pat = '/(<\s*input[^>]*name="';
	$s_pat .= preg_quote($s_name,"/");
	$s_pat .= '\[]"[^>]*type="checkbox"[^>]*?[^"\w])checked(="checked"|(?=[^"\w]))?([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	foreach ($a_values as $s_value) {
		// handle type attribute first
		$s_pat = '/(<\s*input[^>]*type="checkbox"[^>]*name="';
		$s_pat .= preg_quote($s_name,"/");
		$s_pat .= '\[\]"[^>]*value="';
		$s_pat .= preg_quote($s_value,"/");
		$s_pat .= '")([^>]*?)(\s*\/\s*)?>';
		$s_pat .= '/ims';
		$s_buf = preg_replace($s_pat,'$1$2 checked="checked"$3>',$s_buf);
		//$aDebug[] = "Name='$s_name', pat='$s_pat'";

		// handle name attribute first
		$s_pat = '/(<\s*input[^>]*name="';
		$s_pat .= preg_quote($s_name,"/");
		$s_pat .= '\[\]"[^>]*type="checkbox"[^>]*value="';
		$s_pat .= preg_quote($s_value,"/");
		$s_pat .= '")([^>]*?)(\s*\/\s*)?>';
		$s_pat .= '/ims';
		$s_buf = preg_replace($s_pat,'$1$2 checked="checked">',$s_buf);
	}
	return ($s_buf);
}

//
// This function handles selects.
//
function FixSelect($s_name,$s_value,$s_buf)
{
	//
	// we search for:
	//  <select name="thename"...>
	//  <option value="thevalue">...</option>
	//  </select>
	//

	$s_pat  = '/(<\s*select[^>]*name="';
	$s_pat  .= preg_quote($s_name,"/");
	$s_pat  .= '".*?<\s*option[^>]*value="';
	$s_pat  .= preg_quote($s_value,"/");
	$s_pat  .= '"[^>]*)>';
	$s_pat  .= '/ims';
	$s_repl = '$1 selected="selected">';
	//  echo "<p>pat: ".htmlspecialchars($s_pat);
	$s_buf = preg_replace($s_pat,$s_repl,$s_buf);

	return ($s_buf);
}

//
// This function handles multiple selects.
//
function FixMultiSelect($s_name,$a_values,$s_buf)
{
	//
	// we search for:
	//  <select name="thename"...>
	//  <option value="thevalue">...</option>
	//  </select>
	//

	foreach ($a_values as $s_value) {
		$s_pat  = '/(<\s*select[^>]*name="';
		$s_pat  .= preg_quote($s_name,"/");
		$s_pat  .= '\[\]".*?<\s*option[^>]*value="';
		$s_pat  .= preg_quote($s_value,"/");
		$s_pat  .= '"[^>]*)>';
		$s_pat  .= '/ims';
		$s_repl = '$1 selected="selected">';
		//  echo "<p>pat: ".htmlspecialchars($s_pat);
		$s_buf = preg_replace($s_pat,$s_repl,$s_buf);
	}
	return ($s_buf);
}

//
// This function unchecks all checkboxes and select options.
//
function UnCheckStuff($s_buf)
{
	global $php_errormsg;

	//
	// we search for:
	//  <input type="checkbox" ... checked
	// and remove "checked" (checked="checked" is OK too)
	//
	// Note that the check attribute must appear *after* the
	// type attribute.
	// see <A HREF="fmbadhandler.php#PatternInfo">
	//

	$s_pat = '/(<\s*input[^>]*type="checkbox"[^>]*?[^"\w])checked(="checked"|(?=[^"\w]))?([^>]*?)(\s*\/\s*)?>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3$4>',$s_buf);

	//
	// we search for:
	//  <option... selected
	// and remove "selected" (selected="selected" is OK too)
	// see <A HREF="fmbadhandler.php#PatternInfo">
	//

	$s_pat = '/(<\s*option[^>]*?[^"\w])selected(="selected"|(?=[^"\w]))?([^>]*)>';
	$s_pat .= '/ims';
	$s_buf = preg_replace($s_pat,'$1$3>',$s_buf);

	return ($s_buf);
}

//
// Add the user agent to the url as a parameter called USER_AGENT.
// This allows dynamic web sites to know what the user's browser is.
//
function AddUserAgent($s_url)
{
	global $aServerVars,$aGetVars;

	//
	// check if the USER_AGENT has been passed as a URL parameter,
	// if so, use it
	//
	$s_agent = "";
	if (isset($aGetVars['USER_AGENT']) && $aGetVars['USER_AGENT'] !== "") {
		$s_agent = $aGetVars['USER_AGENT'];
		//
		// check for URL encoding, and if not, then encode it
		//
		if (!IsURLEncoded($s_agent)) {
			$s_agent = urlencode($s_agent);
		}
	} elseif (isset($aServerVars['HTTP_USER_AGENT'])) {
		$s_agent = urlencode($aServerVars['HTTP_USER_AGENT']);
	}

	if ($s_agent !== "") {
		return (AddURLParams($s_url,"USER_AGENT=$s_agent",false));
	} else {
		return ($s_url);
	}
}

//
// Try to determine if the given string is already URL-encoded
//
function IsURLEncoded($s_str)
{
	//
	// the only non-alphanumeric characters we'd expect
	// to see are defined as safe or extra in RFC 1738:
	//  safe           = "$" | "-" | "_" | "." | "+"
	//  extra          = "!" | "*" | "'" | "(" | ")" | ","
	// plus the encoding character %
	//
	if (preg_match('/[^a-z0-9$_.+!*\'(),%-]/i',$s_str,$a_matches)) {
		FMDebug("IsURLEncoded: '$s_str' matched '" . $a_matches[0] . "' and is therefore not URL-encoded");
		return (false);
	}
	return (true);
}

//
// Sets previous values in a form.
//
function SetPreviousValues($s_form_buf,$a_values,$a_strip = array())
{
	//
	// Uncheck any checkboxes and select options
	//
	$s_form_buf = UnCheckStuff($s_form_buf);
	foreach ($a_values as $s_name => $m_value) {
		if (is_array($m_value)) {
			//
			// note that if no values are selected for a field,
			// then we will never get here for that field
			//
			$s_form_buf = FixCheckboxes($s_name,$m_value,$s_form_buf);
			$s_form_buf = FixMultiSelect($s_name,$m_value,$s_form_buf);
		} else {
			//
			// Fix the field if it's an input type "text" or "password".
			//
			$s_form_buf = FixInputText($s_name,$m_value,$s_form_buf);
			//
			// Fix the field if it's radio button.
			//
			$s_form_buf = FixButton($s_name,$m_value,$s_form_buf);
			//
			// Fix the field if it's a "textarea".
			//
			$s_form_buf = FixTextArea($s_name,$m_value,$s_form_buf);
			//
			// Fix the field if it's a "select".
			//
			$s_form_buf = FixSelect($s_name,$m_value,$s_form_buf);
		}
	}
	//
	// Now strip particular field values.
	//
	foreach ($a_strip as $s_name) {
		$s_form_buf = RemoveFieldValue($s_name,$s_form_buf);
	}
	return ($s_form_buf);
}

//
// Open a URL, do value substitutions, and send to browser.
// The a_strip array provides a list of fields (usually
// hidden fields) to remove from the form (their values are
// set to empty).
//
function ProcessReturnToForm($s_url,$a_values,$a_strip = array())
{
	global $php_errormsg;

	//
	// read the original form, and modify it to provide values
	// for the fields
	//
	if (!CheckValidURL($s_url)) {
		Error("invalid_url",GetMessage(MSG_RETURN_URL_INVALID,
		                               array("URL" => $s_url)),false,false);
	}

	$s_form_url = AddUserAgent($s_url);
	$s_error    = "";
	$s_form_buf = GetURL($s_form_url,$s_error);
	if ($s_form_buf === false) {
		Error("invalid_url",
		      GetMessage(MSG_OPEN_URL,
		                 array("URL"   => $s_form_url,
		                       "ERROR" => $s_error .
		                                  ": " .
		                                  (isset($php_errormsg) ?
			                                  $php_errormsg : "")
		                 )),
		      false,
		      false);
	}

	//
	// Next, we replace or set actual field values.
	//
	echo SetPreviousValues($s_form_buf,$a_values,$a_strip);
}

//
// To return the URL for returning to a particular multi-page form URL.
//
function GetReturnLink($s_this_script,$i_form_index)
{
	global $aServerVars;

	if (!CheckValidURL($s_this_script)) {
		Error("not_valid_url",GetMessage(MSG_RETURN_URL_INVALID,
		                                 array("URL" => $s_this_script)),false,false);
	}

	$a_params   = array();
	$a_params[] = "return=$i_form_index";
	if (isset($aServerVars["QUERY_STRING"])) {
		$a_params[] = $aServerVars["QUERY_STRING"];
	}
	$a_params[] = session_name() . "=" . session_id();
	return (AddURLParams($s_this_script,$a_params));
}

//
// Process a multi-page form template.
//
function ProcessMultiFormTemplate($s_template,$a_values,&$a_lines)
{
	global $SPECIAL_VALUES;

	if (Settings::isEmpty('MULTIFORMDIR') && Settings::isEmpty('MULTIFORMURL')) {
		SendAlert(GetMessage(MSG_MULTIFORM));
		return (false);
	}
	//
	// create the "this_form_url" field
	//

	$i_index                   = GetSession("FormIndex");
	$a_list                    = GetSession("FormList");
	$a_values["this_form_url"] = $a_list[$i_index]["URL"];
	//
	// get the persistent file fields
	//
	$a_values = GetSavedFileNames($a_values);
	//$a_values["prev_form"] = GetReturnLink($SPECIAL_VALUES["this_form"]);
	return (DoProcessTemplate(Settings::get('MULTIFORMDIR'),Settings::get('MULTIFORMURL'),$s_template,$a_lines,
	                          $a_values,"",'SubstituteValueForPage'));
}

//
// Output the multi-form template after filling in the fields.
//
function OutputMultiFormTemplate($s_template,$a_values)
{
	$a_lines = array();
	if (!ProcessMultiFormTemplate($s_template,$a_values,$a_lines)) {
		Error("multi_form_failed",GetMessage(MSG_MULTIFORM_FAILED,
		                                     array("NAME" => $s_template)),false,false);
	} else {
		$n_lines = count($a_lines);
		$s_buf   = "";
		for ($ii = 0 ; $ii < $n_lines ; $ii++) {
			$s_buf .= $a_lines[$ii] . "\n";
			unset($a_lines[$ii]); // free memory (hopefully)
		}
		unset($a_lines); // free memory (hopefully)

		if (IsSetSession("FormKeep"))
			//
			// put in any values that are being forward-remembered
			//
		{
			echo SetPreviousValues($s_buf,GetSession("FormKeep"));
		} else {
			echo $s_buf;
		}
	}
}

//
// Insert a preamble into a MIME message.
//
function MimePreamble(&$a_lines,$a_mesg = array())
{
	$a_preamble = explode("\n",GetMessage(MSG_MIME_PREAMBLE));
	foreach ($a_preamble as $s_line) {
		$a_lines[] = $s_line . Settings::get('HEAD_CRLF');
	}

	$a_lines[]    = Settings::get('HEAD_CRLF'); // blank line
	$b_need_blank = false;
	foreach ($a_mesg as $s_line) {
		$a_lines[] = $s_line . Settings::get('HEAD_CRLF');
		if (!empty($s_line)) {
			$b_need_blank = true;
		}
	}
	if ($b_need_blank) {
		$a_lines[] = Settings::get('HEAD_CRLF');
	} // blank line
}

//
// Create the HTML mail
//
function HTMLMail(&$a_lines,&$a_headers,$s_body,$s_template,$s_missing,$s_filter,
                  $s_boundary,$a_raw_fields,$b_no_plain,$b_process_template)
{
	$s_charset = GetMailOption("CharSet");
	if (!isset($s_charset)) {
		$s_charset = "ISO-8859-1";
	}
	if ($b_no_plain) {
		$b_multi = false;
		//
		// don't provide a plain text version - just the HTML
		//
		$a_headers['Content-Type'] = SafeHeader("text/html; charset=$s_charset");
	} else {
		$b_multi                   = true;
		$a_headers['Content-Type'] = "multipart/alternative; boundary=\"$s_boundary\"";

		$a_pre_lines = explode("\n",GetMessage(MSG_MIME_HTML,
		                                       array("NAME" => $s_template)));

		MimePreamble($a_lines,$a_pre_lines);

		//
		// first part - the text version only
		//
		$a_lines[] = "--$s_boundary" . Settings::get('HEAD_CRLF');
		$a_lines[] = "Content-Type: text/plain; charset=$s_charset" . Settings::get('HEAD_CRLF');
		$a_lines[] = Settings::get('HEAD_CRLF'); // blank line
		//
		// treat the body like one line, even though it isn't
		//
		$a_lines[] = $s_body;
		$a_lines[] = Settings::get('HEAD_CRLF'); // blank line
		//
		// second part - the HTML version
		//
		$a_lines[] = "--$s_boundary" . Settings::get('HEAD_CRLF');
		$a_lines[] = "Content-Type: text/html; charset=$s_charset" . Settings::get('HEAD_CRLF');
		$a_lines[] = Settings::get('HEAD_CRLF'); // blank line
	}

	$a_html_lines = array();
	if (!$b_process_template) {
		if (!ProcessTemplate($s_template,$a_html_lines,$a_raw_fields,NULL,'SubstituteValueDummy')) {
			return (false);
		}
	} elseif (!ProcessTemplate($s_template,$a_html_lines,$a_raw_fields,$s_missing)) {
		return (false);
	}

	if (!empty($s_filter))
		//
		// treat the data like one line, even though it isn't
		//
	{
		$a_lines[] = Filter($s_filter,$a_html_lines);
	} else {
		foreach ($a_html_lines as $s_line) {
			$a_lines[] = $s_line;
		}
	}

	if ($b_multi) {
		//
		// end
		//
		$a_lines[] = "--$s_boundary--" . Settings::get('HEAD_CRLF');
		$a_lines[] = Settings::get('HEAD_CRLF'); // blank line
	}
	return (true);
}

//
// Add the contents of a file in base64 encoding.
//
function AddFile(&$a_lines,$s_file_name,$i_file_size,$b_remove = true)
{
	global $php_errormsg;

	@   $fp = fopen($s_file_name,"rb");
	if ($fp === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $s_file_name,
		                                               "TYPE"  => "attachment",
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return (false);
	}
	//
	// PHP under IIS has problems with the filesize function when
	// the file is on another drive.  So, we replaced a call
	// to filesize with the $i_file_size parameter (this occurred
	// in version 3.01).
	//
	$s_contents = fread($fp,$i_file_size);
	//
	// treat as a single line, even though it isn't
	//
	$a_lines[] = chunk_split(base64_encode($s_contents));
	fclose($fp);
	if ($b_remove) {
		@unlink($s_file_name);
	}
	return (true);
}

//
// Add the contents of a string in base64 encoding.
//
function AddData(&$a_lines,$s_data)
{
	//
	// treat as a single line, even though it isn't
	//
	$a_lines[] = chunk_split(base64_encode($s_data));
	return (true);
}

/**
 * Check if a file is a valid uploaded file.
 *
 * This function is obsolete and is only kept for use by
 * existing hook files.  Do not use this function in new code.
 *
 * @param array $a_file_spec file specification
 *
 * @return bool
 */
function IsUploadedFile($a_file_spec)
{
	return FieldManager::IsUploadedFile($a_file_spec);
}

//
// Save an uploaded file to the repository directory.
//
function SaveFileInRepository(&$a_file_spec)
{
	global $php_errormsg;

	//
	// if a replacement name has been specified, use that, otherwise
	// use the original name
	//
	if (isset($a_file_spec["new_name"])) {
		$s_file_name = basename($a_file_spec["new_name"]);
	} else {
		$s_file_name = basename($a_file_spec["name"]);
	}
	$s_dest = Settings::get('FILE_REPOSITORY') . "/" . $s_file_name;

	$b_ok    = true;
	$s_error = "";

	if (isset($a_file_spec["saved_as"]) && !empty($a_file_spec["saved_as"])) {
		FMDebug("SaveFileInRepository: saved_as");
		$s_srce = $a_file_spec["saved_as"];
	} else {
		$s_srce = $a_file_spec["tmp_name"];
	}

	FMDebug("SaveFileInRepository: $s_srce");
	if (!Settings::get('FILE_OVERWRITE')) {
		clearstatcache();
		if (@file_exists($s_dest)) {
			$b_ok    = false;
			$s_error = GetMessage(MSG_SAVE_FILE_EXISTS,array("FILE" => $s_dest));
		}
	}
	if (Settings::get('MAX_FILE_UPLOAD_SIZE') != 0 &&
	    $a_file_spec["size"] > Settings::get('MAX_FILE_UPLOAD_SIZE') * 1024
	)
		//
		// this exits
		//
	{
		UserError("upload_size",GetMessage(MSG_FILE_UPLOAD_SIZE,
		                                   array("NAME" => $a_file_spec["name"],
		                                         "SIZE" => $a_file_spec["size"],
		                                         "MAX"  => Settings::get('MAX_FILE_UPLOAD_SIZE')
		                                   )));
	}
	if ($b_ok) {
		if (isset($a_file_spec["saved_as"]) && !empty($a_file_spec["saved_as"])) {
			if (!copy($s_srce,$s_dest) || !@unlink($s_srce)) {
				$b_ok = false;
			}
		} else {
			if (!move_uploaded_file($s_srce,$s_dest)) {
				$b_ok = false;
			}
		}
		if ($b_ok) {
			//
			// Flag to say it's been put in the repository.
			//
			$a_file_spec["in_repository"] = true;
			//
			// Its new location
			//
			$a_file_spec["saved_as"] = $s_dest;
			//
			// Now that the file has been saved, "is_uploaded_file"
			// will return false.  So, we create a flag to say it was
			// valid.
			//
			$a_file_spec["moved"] = true;
		} else {
			$s_error = $php_errormsg;
		}
	}
	if (!$b_ok) {
		SendAlert(GetMessage(MSG_SAVE_FILE,array(
			"FILE" => $s_srce,
			"DEST" => $s_dest,
			"ERR"  => $s_error
		)));
		return (false);
	}
	//
	// ignore chmod fails (other than reporting them)
	//
	if (Settings::get('FILE_MODE') != 0 && !chmod($s_dest,Settings::get('FILE_MODE'))) {
		SendAlert(GetMessage(MSG_CHMOD,array(
			"FILE" => $s_dest,
			"MODE" => Settings::get('FILE_MODE'),
			"ERR"  => $s_error
		)));
	}
	return (true);
}

//
// Save all uploaded files to the repository directory.
//
function SaveAllFilesToRepository()
{
	global $aFileVars;

	if (!Settings::get('FILEUPLOADS') || Settings::get('FILE_REPOSITORY') === "")
		//
		// nothing to do
		//
	{
		return (true);
	}

	foreach ($aFileVars as $m_file_key => $a_upload) {
		//
		// One customer reported:
		//  Possible file upload attack detected: name='' temp name='none'
		// on PHP 4.1.2 on RAQ4.
		// So, we now also test for "name".
		//
		if (!isset($a_upload["tmp_name"]) || empty($a_upload["tmp_name"]) ||
		    !isset($a_upload["name"]) || empty($a_upload["name"])
		) {
			continue;
		}
		if (isset($a_upload["in_repository"]) && $a_upload["in_repository"])
			//
			// already saved
			//
		{
			continue;
		}
		if (!FieldManager::IsUploadedFile($a_upload)) {
			SendAlert(GetMessage(MSG_FILE_UPLOAD_ATTACK,
			                     array("NAME" => $a_upload["name"],
			                           "TEMP" => $a_upload["tmp_name"],
			                           "FLD"  => $m_file_key
			                     )));
			continue;
		}
		if (!SaveFileInRepository($aFileVars[$m_file_key])) {
			return (false);
		}
		//
		// Now the file has been saved in the repository, make
		// the field persistent through all further processing
		// (e.g. all movements in a multi-page form)
		//
		if (IsSetSession("FormSavedFiles")) {
			$a_saved_files = GetSession("FormSavedFiles");
		} else {
			$a_saved_files = array();
		}
		$a_saved_files["repository_" . $m_file_key] = $aFileVars[$m_file_key];
		SetSession("FormSavedFiles",$a_saved_files);
	}
	return (true);
}

//
// Delete an uploaded file from the repository directory.
// For security reasons, only the field name can be used.  This
// uniquely identifies an uploaded file by this form process.
//
function DeleteFileFromRepository($s_fld)
{
	global $aFileVars;

	if (!Settings::get('FILEUPLOADS') || Settings::get('FILE_REPOSITORY') === "")
		//
		// nothing to do
		//
	{
		return (false);
	}

	if (($a_upload = GetFileInfo($s_fld)) === false) {
		return (false);
	}

	if (isset($a_upload["in_repository"]) && $a_upload["in_repository"]) {
		if (isset($a_upload["saved_as"]) && !empty($a_upload["saved_as"])) {
			@unlink($a_upload["saved_as"]);
		}
	}
	DeleteFileInfo($s_fld);
	return (true);
}

//
// Save an uploaded file for later processing.
//
function SaveUploadedFile(&$a_file_spec,$s_prefix)
{
	global $php_errormsg;

	FMDebug("SaveUploadedFile");
	$s_dest = GetScratchPadFile($s_prefix);
	if (!move_uploaded_file($a_file_spec["tmp_name"],$s_dest)) {
		SendAlert(GetMessage(MSG_SAVE_FILE,array(
			"FILE" => $a_file_spec["tmp_name"],
			"DEST" => $s_dest,
			"ERR"  => $php_errormsg
		)));
		return (false);
	}
	$a_file_spec["saved_as"] = $s_dest;
	$a_file_spec["moved"]    = true;
	return (true);
}

//
// Remove old files from the scratchpad directory.
//
function CleanScratchPad($s_prefix = "")
{
	global $lNow;
	global $php_errormsg;

	if (Settings::isEmpty('SCRATCH_PAD'))
		//
		// no scratchpad to cleanup!
		//
	{
		return;
	}
	if (Settings::get('CLEANUP_TIME') <= 0)
		//
		// cleanup disabled
		//
	{
		return;
	}
	//
	// compute chance of cleanup
	//
	if (Settings::get('CLEANUP_CHANCE') < 100) {
		$i_rand = mt_rand(1,100);
		if ($i_rand > Settings::get('CLEANUP_CHANCE')) {
			return;
		}
	}
	if (($f_dir = @opendir(Settings::get('SCRATCH_PAD'))) === false) {
		Error("open_scratch_pad",GetMessage(MSG_OPEN_SCRATCH_PAD,array(
			"DIR" => Settings::get('SCRATCH_PAD'),
			"ERR" => $php_errormsg
		)),   false,false);
		return;
	}
	$i_len = strlen($s_prefix);
	while (($s_file = readdir($f_dir)) !== false) {
		$s_path = Settings::get('SCRATCH_PAD') . "/" . $s_file;
		if (is_file($s_path) && ($i_len == 0 || substr($s_file,0,$i_len) == $s_prefix)) {
			if (($a_stat = @stat($s_path)) !== false) {
				if (isset($a_stat['mtime'])) {
					$l_time = $a_stat['mtime'];
				} else {
					$l_time = $a_stat[9];
				}
				if (($lNow - $l_time) / 60 >= Settings::get('CLEANUP_TIME')) {
					@unlink($s_path);
				}
			}
		}
	}
	closedir($f_dir);
}

//
// Save all uploaded files for later processing.
//
function SaveAllUploadedFiles(&$a_file_vars)
{
	global $php_errormsg;

	$s_prefix = "UPLD";
	if (Settings::isEmpty('SCRATCH_PAD')) {
		Error("need_scratch_pad",GetMessage(MSG_NEED_SCRATCH_PAD),false,false);
		return (false);
	}

	//
	// remove old uploaded files that have not been moved out.
	//
	CleanScratchPad($s_prefix);

	foreach (array_keys($a_file_vars) as $m_file_key) {
		$a_upload = &$a_file_vars[$m_file_key];
		//
		// One customer reported:
		//  Possible file upload attack detected: name='' temp name='none'
		// on PHP 4.1.2 on RAQ4.
		// So, we now also test for "name".
		//
		if (!isset($a_upload["tmp_name"]) || empty($a_upload["tmp_name"]) ||
		    !isset($a_upload["name"]) || empty($a_upload["name"])
		) {
			continue;
		}
		//
		// ensure we don't move the file more than once
		//
		if (!isset($a_upload["saved_as"]) || empty($a_upload["saved_as"])) {
			if (!FieldManager::IsUploadedFile($a_upload)) {
				SendAlert(GetMessage(MSG_FILE_UPLOAD_ATTACK,
				                     array("NAME" => $a_upload["name"],
				                           "TEMP" => $a_upload["tmp_name"],
				                           "FLD"  => $m_file_key
				                     )));
			} elseif (!SaveUploadedFile($a_upload,$s_prefix)) {
				return (false);
			}
		}
	}
	return (true);
}

//
// Attach a file to the body of a MIME formatted email.  $a_lines is the
// current body, and is modified to include the file.
// $a_file_spec must have the following values (just like an uploaded
// file specification):
//      name        the name of the file
//      type        the mime type
//      tmp_name    the name of the temporary file
//      size        the size of the temporary file
//
// Alternatively, you supply the following instead of tmp_name and size:
//      data        the data to attach
//
function AttachFile(&$a_lines,$s_att_boundary,$a_file_spec,$s_charset,$b_remove = true)
{
	$a_lines[] = "--$s_att_boundary" . Settings::get('HEAD_CRLF');
	//
	// if a replacement name has been specified, use that, otherwise
	// use the original name
	//
	if (isset($a_file_spec["new_name"])) {
		$s_file_name = $a_file_spec["new_name"];
	} else {
		$s_file_name = $a_file_spec["name"];
	}
	$s_file_name = str_replace('"','',$s_file_name);
	$s_mime_type = $a_file_spec["type"];
	//
	// The following says that the data is encoded in
	// base64 and is an attachment and that once decoded the
	// character set of the decoded data is $s_charset.
	// (See RFC 1521 Section 5.)
	//
	$a_lines[] = "Content-Type: $s_mime_type; name=\"$s_file_name\"; charset=$s_charset" . Settings::get('HEAD_CRLF');
	$a_lines[] = "Content-Transfer-Encoding: base64" . Settings::get('HEAD_CRLF');
	$a_lines[] = "Content-Disposition: attachment; filename=\"$s_file_name\"" . Settings::get('HEAD_CRLF');
	$a_lines[] = Settings::get('HEAD_CRLF'); // blank line
	if (isset($a_file_spec["tmp_name"]) && isset($a_file_spec["size"])) {
		$s_srce = $a_file_spec["tmp_name"];
		//
		// check if the file has been saved elsewhere
		//
		if (isset($a_file_spec["saved_as"]) && !empty($a_file_spec["saved_as"])) {
			$s_srce = $a_file_spec["saved_as"];
		}
		FMDebug("AttachFile: $s_srce");
		return (AddFile($a_lines,$s_srce,$a_file_spec["size"],$b_remove));
	}
	if (!isset($a_file_spec["data"])) {
		SendAlert(GetMessage(MSG_ATTACH_DATA));
		return (false);
	}
	return (AddData($a_lines,$a_file_spec["data"]));
}

//
// Reformat the email to be in MIME format.
// Process file attachments and and fill out any
// specified HTML template.
//
function MakeMimeMail(&$s_body,&$a_headers,$a_raw_fields,$s_template = "",
                      $s_missing = NULL,$b_no_plain = false,
                      $s_filter = "",$a_file_vars = array(),
                      $a_attach_spec = array(),$b_process_template = true)
{
	global $FM_VERS;
	global $SPECIAL_VALUES;

	$s_charset = GetMailOption("CharSet");
	if (!isset($s_charset)) {
		$s_charset = "ISO-8859-1";
	}
	$b_att        = $b_html = false;
	$b_got_filter = (isset($s_filter) && !empty($s_filter));
	if (isset($s_template) && !empty($s_template)) {
		$b_html = true;
	}
	if (count($a_file_vars) > 0) {
		if (!Settings::get('FILEUPLOADS')) {
			SendAlert(GetMessage(MSG_FILE_UPLOAD));
			//
			// if storing files in the server repository, don't attach
			// unless the mail_options insist
			//
		} elseif (Settings::get('FILE_REPOSITORY') === "" || IsMailOptionSet("AlwaysEmailFiles")) {
			foreach ($a_file_vars as $a_upload) {
				//
				// One customer reported:
				//  Possible file upload attack detected: name='' temp name='none'
				// on PHP 4.1.2 on RAQ4.
				// So, we now also test for "name".
				//
				if (isset($a_upload["tmp_name"]) && !empty($a_upload["tmp_name"]) &&
				    isset($a_upload["name"]) && !empty($a_upload["name"])
				) {
					$b_att = true;
					break;
				}
			}
		}
	}
	//
	// check for an internally-generated attachment
	//
	if (isset($a_attach_spec["Data"])) {
		$b_att = true;
	}

	$s_uniq                    = md5($s_body);
	$s_body_boundary           = "BODY$s_uniq";
	$s_att_boundary            = "PART$s_uniq";
	$a_headers['MIME-Version'] = "1.0 (produced by FormMail $FM_VERS from www.tectite.com)";

	//
	// if the filter strips formatting, then we'll only have plain text
	// to send, even after the template has been used
	//
	if ($b_got_filter && IsFilterAttribSet($s_filter,"Strips"))
		//
		// no HTML if the filter strips the formatting
		//
	{
		$b_html = false;
	}
	$a_new = array();
	if ($b_att) {
		$a_headers['Content-Type'] = "multipart/mixed; boundary=\"$s_att_boundary\"";

		MimePreamble($a_new);
		//
		// add the body of the email
		//
		$a_new[] = "--$s_att_boundary" . Settings::get('HEAD_CRLF');
		if ($b_html) {
			$a_lines = $a_local_headers = array();
			if (!HTMLMail($a_lines,$a_local_headers,$s_body,$s_template,
			              $s_missing,($b_got_filter) ? $s_filter : "",
			              $s_body_boundary,$a_raw_fields,$b_no_plain,
			              $b_process_template)
			) {
				return (false);
			}
			$a_new   = array_merge($a_new,ExpandMailHeadersArray($a_local_headers));
			$a_new[] = Settings::get('HEAD_CRLF'); // blank line after header
			$a_new   = array_merge($a_new,$a_lines);
		} else {
			$a_new[] = "Content-Type: text/plain; charset=$s_charset" . Settings::get('HEAD_CRLF');
			$a_new[] = Settings::get('HEAD_CRLF'); // blank line
			//
			// treat the body like one line, even though it isn't
			//
			$a_new[] = $s_body;
		}
		//
		// now add the attachments or save to the $FILE_REPOSITORY
		//
		if (Settings::get('FILEUPLOADS') &&
		    (Settings::get('FILE_REPOSITORY') === "" || IsMailOptionSet("AlwaysEmailFiles"))
		) {
			foreach ($a_file_vars as $m_file_key => $a_upload) {
				//
				// One customer reported:
				//  Possible file upload attack detected: name='' temp name='none'
				// on PHP 4.1.2 on RAQ4.
				// So, we now also test for "name".
				//
				if (!isset($a_upload["tmp_name"]) || empty($a_upload["tmp_name"]) ||
				    !isset($a_upload["name"]) || empty($a_upload["name"])
				) {
					continue;
				}
				if (!FieldManager::IsUploadedFile($a_upload)) {
					SendAlert(GetMessage(MSG_FILE_UPLOAD_ATTACK,
					                     array("NAME" => $a_upload["name"],
					                           "TEMP" => $a_upload["tmp_name"],
					                           "FLD"  => $m_file_key
					                     )));
					continue;
				}
				if (Settings::get('MAX_FILE_UPLOAD_SIZE') != 0 &&
				    $a_upload["size"] > Settings::get('MAX_FILE_UPLOAD_SIZE') * 1024
				) {
					UserError("upload_size",GetMessage(MSG_FILE_UPLOAD_SIZE,
					                                   array("NAME" => $a_upload["name"],
					                                         "SIZE" => $a_upload["size"],
					                                         "MAX"  => Settings::get('MAX_FILE_UPLOAD_SIZE')
					                                   )));
				}
				if (!AttachFile($a_new,$s_att_boundary,$a_upload,$s_charset,
				                Settings::get('FILE_REPOSITORY') === "")
				) {
					return (false);
				}
			}
		}
		if (isset($a_attach_spec["Data"])) {
			//
			// build a specification similar to a file upload
			//
			$a_file_spec["name"] = isset($a_attach_spec["Name"]) ?
				$a_attach_spec["Name"] :
				"attachment.dat";
			$a_file_spec["type"] = isset($a_attach_spec["MIME"]) ?
				$a_attach_spec["MIME"] :
				"text/plain";
			$a_file_spec["data"] = $a_attach_spec["Data"];
			if (!AttachFile($a_new,$s_att_boundary,$a_file_spec,
			                isset($a_attach_spec["CharSet"]) ?
				                $a_attach_spec["CharSet"] :
				                $s_charset)
			) {
				return (false);
			}
		}
		$a_new[] = "--$s_att_boundary--" . Settings::get('HEAD_CRLF'); // the end
		$a_new[] = Settings::get('HEAD_CRLF'); // blank line
	} elseif ($b_html) {
		if (!HTMLMail($a_new,$a_headers,$s_body,$s_template,
		              $s_missing,($b_got_filter) ? $s_filter : "",
		              $s_body_boundary,$a_raw_fields,$b_no_plain,
		              $b_process_template)
		) {
			return (false);
		}
	} else {
		$a_headers['Content-Type'] = SafeHeader("text/plain; charset=$s_charset");
		//
		// treat the body like one line, even though it isn't
		//
		$a_new[] = $s_body;
	}

	$s_body = JoinLines(Settings::get('BODY_LF'),$a_new);
	return (true);
}

//
// to make a From line for the email
//
function MakeFromLine($s_email,$s_name)
{
	$s_style = GetMailOption("FromLineStyle");
	$s_line  = "";
	if (!isset($s_style)) {
		$s_style = "";
	}
	//
	// the following From line styles are in accordance with RFC 822
	//
	switch ($s_style) {
		default:
		case "":
		case "default":
		case "AddrSpecName":
			//
			// this is the original From line style that FormMail produced
			// e.g.
			//      jack@nowhere.com (Jack Smith)
			// this is an addr-spec with a trailing comment with the name
			//
			if (!empty($s_email)) {
				$s_line .= SafeHeaderEmail($s_email) . " ";
			}
			if (!empty($s_name)) {
				$s_line .= "(" . SafeHeaderComment(EncodeHeaderText($s_name)) . ")";
			}
			break;
		case "NameAddrSpec":
			//
			// email address as an addr-spec preceded by a comment with the name
			// e.g.
			//      (Jack Smith) jack@nowhere.com
			//
			if (!empty($s_name)) {
				$s_line .= "(" . SafeHeaderComment(EncodeHeaderText($s_name)) . ") ";
			}
			if (!empty($s_email)) {
				$s_line .= SafeHeaderEmail($s_email);
			}
			break;
		case "RouteAddr":
			//
			// just the email address as a route-addr
			// e.g.
			//      <jack@nowhere.com>
			//
			if (!empty($s_email)) {
				$s_line .= "<" . SafeHeaderEmail($s_email) . ">";
			}
			break;
		case "QuotedNameRouteAddr":
			//
			// email address as a route-addr preceded
			// by the name of the user as a quoted string
			// e.g.
			//      "Jack Smith" <jack@nowhere.com>
			//
			if (!empty($s_name)) {
				$s_line .= '"' . SafeHeaderQString(EncodeHeaderText($s_name)) . '" ';
			}
			if (!empty($s_email)) {
				$s_line .= "<" . SafeHeaderEmail($s_email) . ">";
			}
			break;
		case "NameRouteAddr":
			//
			// email address as a route-addr preceded
			// by the name of the user as words
			// e.g.
			//      Jack Smith <jack@nowhere.com>
			//
			if (!empty($s_name)) {
				$s_line .= SafeHeaderWords(EncodeHeaderText($s_name)) . ' ';
			}
			if (!empty($s_email)) {
				$s_line .= "<" . SafeHeaderEmail($s_email) . ">";
			}
			break;
	}
	return ($s_line);
}

//
// Return two sets of plain text output: the filtered fields and the
// non-filtered fields.
//
function GetFilteredOutput($a_fld_order,$a_clean_fields,$s_filter,$a_filter_list)
{
	//
	// find the non-filtered fields and make unfiltered text from them
	//
	$a_unfiltered_list = array();
	$n_flds            = count($a_fld_order);
	for ($ii = 0 ; $ii < $n_flds ; $ii++) {
		if (!in_array($a_fld_order[$ii],$a_filter_list)) {
			$a_unfiltered_list[] = $a_fld_order[$ii];
		}
	}
	$s_unfiltered_results = MakeFieldOutput($a_unfiltered_list,$a_clean_fields);
	//
	// filter the specified fields only
	//
	$s_filtered_results = MakeFieldOutput($a_filter_list,$a_clean_fields);
	$s_filtered_results = Filter($s_filter,$s_filtered_results);
	return (array($s_unfiltered_results,$s_filtered_results));
}

//
// Make a plain text email body
//
function MakePlainEmail($a_fld_order,$a_clean_fields,
                        $s_to,$s_cc,$s_bcc,$a_raw_fields,$s_filter,$a_filter_list)
{
	global $SPECIAL_VALUES;

	$s_unfiltered_results = $s_filtered_results = "";
	$b_got_filter         = (isset($s_filter) && !empty($s_filter));
	if ($b_got_filter) {
		if (isset($a_filter_list) && count($a_filter_list) > 0) {
			$b_limited_filter = true;
		} else {
			$b_limited_filter = false;
		}
	}
	$b_used_template = false;
	if (IsMailOptionSet("PlainTemplate")) {
		$s_template = GetMailOption("PlainTemplate");
		if (ProcessTemplate($s_template,$a_lines,$a_raw_fields,GetMailOption('TemplateMissing'),
		                    'SubstituteValuePlain')
		) {
			$b_used_template      = true;
			$s_unfiltered_results = implode(Settings::get('BODY_LF'),$a_lines);
			if ($b_got_filter) {
				//
				// with a limited filter, the template goes unfiltered
				// and the named fields get filtered
				//
				if ($b_limited_filter) {
					list ($s_discard,$s_filtered_results) = GetFilteredOutput($a_fld_order,
					                                                          $a_clean_fields,
					                                                          $s_filter,$a_filter_list);
				} else {
					$s_filtered_results   = Filter($s_filter,$s_unfiltered_results);
					$s_unfiltered_results = "";
				}
			}
		}
	}
	if (!$b_used_template) {
		$res_hdr = "";

		if (IsMailOptionSet("DupHeader")) {
			//
			// write some standard mail headers
			//
			$res_hdr = "To: $s_to" . Settings::get('BODY_LF');
			if (!empty($s_cc)) {
				$res_hdr .= "Cc: $s_cc" . Settings::get('BODY_LF');
			}
			if (!empty($SPECIAL_VALUES["email"])) {
				$res_hdr .= "From: " . MakeFromLine($SPECIAL_VALUES["email"],
				                                    $SPECIAL_VALUES["realname"]) . Settings::get('BODY_LF');
			}
			$res_hdr .= Settings::get('BODY_LF');
			if (IsMailOptionSet("StartLine")) {
				$res_hdr .= "--START--" . Settings::get('BODY_LF');
			} // signals the beginning of the text to filter
		}

		//
		// put the realname and the email address at the top of the results
		// (if not excluded)
		//
		if (!IsMailExcluded("realname")) {
			array_unshift($a_fld_order,"realname");
			$a_clean_fields["realname"] = $SPECIAL_VALUES["realname"];
		}
		if (!IsMailExcluded("email")) {
			array_unshift($a_fld_order,"email");
			$a_clean_fields["email"] = $SPECIAL_VALUES["email"];
		}
		if ($b_got_filter) {
			if ($b_limited_filter) {
				list($s_unfiltered_results,$s_filtered_results) =
					GetFilteredOutput($a_fld_order,$a_clean_fields,
					                  $s_filter,$a_filter_list);
			} else {
				//
				// make text output and filter it (filter all fields)
				//
				$s_filtered_results = MakeFieldOutput($a_fld_order,$a_clean_fields);
				$s_filtered_results = Filter($s_filter,$s_filtered_results);
			}
		} else {
			//SendAlert("There are ".count($a_fld_order)." fields in the order array");
			//SendAlert("Here is the clean fields array:\r\n".var_export($a_clean_fields,true));
			$s_unfiltered_results = MakeFieldOutput($a_fld_order,$a_clean_fields);
		}
		$s_unfiltered_results = $res_hdr . $s_unfiltered_results;
	}
	$s_results = $s_unfiltered_results;
	if ($b_got_filter && !empty($s_filtered_results)) {
		if (!empty($s_results)) {
			$s_results .= Settings::get('BODY_LF');
		}
		$s_results .= $s_filtered_results;
	}
	//
	// append the environment variables report
	//
	if (isset($SPECIAL_VALUES["env_report"]) && !empty($SPECIAL_VALUES["env_report"])) {
		$s_results .= Settings::get('BODY_LF') . "==================================" . Settings::get('BODY_LF');
		$s_results .= Settings::get('BODY_LF') . GetEnvVars(TrimArray(explode(",",$SPECIAL_VALUES["env_report"])),
		                                                    Settings::get('BODY_LF'));
	}
	return (array($s_results,$s_unfiltered_results,$s_filtered_results));
}

//
// Return the list of fields to be filtered, FALSE if no list provided.
//
function GetFilterList($b_file_fields)
{
	global $SPECIAL_VALUES;

	//
	// no filter means no list of fields
	//
	if (!empty($SPECIAL_VALUES["filter"])) {
		if ($b_file_fields) {
			if (isset($SPECIAL_VALUES["filter_files"]) && !empty($SPECIAL_VALUES["filter_files"])) {
				return (TrimArray(explode(",",$SPECIAL_VALUES["filter_files"])));
			}
		} else {
			if (isset($SPECIAL_VALUES["filter_fields"]) && !empty($SPECIAL_VALUES["filter_fields"])) {
				return (TrimArray(explode(",",$SPECIAL_VALUES["filter_fields"])));
			}
		}
	}
	return (false);
}

/* 
 * Function:    GetFilterSpec
 * Parameters:  $s_filter       returns the filter name
 *              $m_filter_list  returns the list of fields to filter (an array)
 *                              or is set to false if there is no filter list
 *              $b_file_fields  if true, return file fields, otherwise return non-file fields
 * Returns:     bool            true if filtering a list of fields of the specified type
 * Description:     
 *  Checks whether the form has specified to filter a list of 
 *  fields of the specified type (file fields or non-file fields).
 */
function GetFilterSpec(&$s_filter,&$m_filter_list,$b_file_fields = false)
{
	global $SPECIAL_VALUES;

	if (isset($SPECIAL_VALUES["filter"]) && !empty($SPECIAL_VALUES["filter"])) {
		$s_filter      = $SPECIAL_VALUES["filter"];
		$m_filter_list = GetFilterList($b_file_fields);
		return (true);
	}
	return (false);
}

//
// send the given results to the given email addresses
//
function SendResults($a_fld_order,$a_clean_fields,$s_to,$s_cc,$s_bcc,$a_raw_fields)
{
	global $SPECIAL_VALUES,$aFileVars,$ValidEmails;

	//
	// check for a filter and how to use it
	//
	$b_filter_attach = false;
	$a_attach_spec   = array();
	$s_filter        = "";
	$a_filter_list   = array();
	if ($b_got_filter = GetFilterSpec($s_filter,$a_filter_list)) {
		if ($a_filter_list === false) {
			//
			// not a limited filter, so filter all fields
			//
			$b_limited_filter = false;
			$a_filter_list    = array();
		} else {
			$b_limited_filter = true;
		}
		FMDebug("SendResults: got filter '$s_filter', limited=$b_limited_filter");
		$s_filter_attach_name = GetFilterOption("Attach");
		if (isset($s_filter_attach_name)) {
			if (!is_string($s_filter_attach_name) || empty($s_filter_attach_name)) {
				SendAlert(GetMessage(MSG_ATTACH_NAME));
			} else {
				$b_filter_attach = true;
				$a_attach_spec   = array("Name" => $s_filter_attach_name);
				if (($s_mime = GetFilterAttrib($s_filter,"MIME")) !== false) {
					$a_attach_spec["MIME"] = $s_mime;
				}
				//
				// Regarding the character set...
				// A filter will not generally change the character set
				// of the message, however, if it does, then we
				// provide that information to the MIME encoder.
				// Remember: this character set specification refers
				// to the data *after* the effect of the filter
				// has been reversed (e.g. an encrypted message
				// in UTF-8 is in UTF-8 when it is decrypted).
				//
				if (($s_cset = GetFilterAttrib($s_filter,"CharSet")) !== false) {
					$a_attach_spec["CharSet"] = $s_cset;
				}
			}
		}
	}

	//
	// check the need for MIME formatted mail
	//
	$b_mime_mail = (IsMailOptionSet("HTMLTemplate") || count($aFileVars) > 0 ||
	                $b_filter_attach);

	//
	// create the email header lines - CC, BCC, From, and Reply-To
	//
	$a_headers = array();
	if (!empty($s_cc)) {
		$a_headers['Cc'] = SafeHeader($s_cc);
	}
	if (!empty($SPECIAL_VALUES["replyto"])) {
		//
		// expand replyto list
		//
		CheckEmailAddress($SPECIAL_VALUES["replyto"],$s_list,$s_invalid,false);
		if (!empty($s_list)) {
			$a_headers['Reply-To'] = SafeHeader($s_list);
		}
	}
	if (!empty($s_bcc)) {
		$a_headers['Bcc'] = SafeHeader($s_bcc);
	}
	//
	// create the From address
	//
	// Some servers won't let you set the email address to the
	// submitter of the form.  Therefore, use FromAddr if it's been
	// specified to set the sender and the "From" address.
	//
	$s_sender = GetMailOption("FromAddr");
	if (!isset($s_sender)) {
		$s_sender = "";
		if (!empty($SPECIAL_VALUES["email"])) {
			$a_headers['From'] = MakeFromLine($SPECIAL_VALUES["email"],
			                                  $SPECIAL_VALUES["realname"]);
		}
	} elseif ($s_sender !== "") {
		$s_sender = UnMangle($s_sender);
		if ($ValidEmails->CheckAddress($s_sender)) {
			$s_sender = $a_headers['From'] = SafeHeader($s_sender);
		} else {
			SendAlert(GetMessage(MSG_INVALID_SENDER,array("LOC" => "FromAddr in mail_options","EMAIL" => $s_sender)));
			$s_sender = "";
		}
	}

	/* 
     * Override sender if $FIXED_SENDER is set.
     */
	if (Settings::get('FIXED_SENDER') !== "") {
		$s_sender = Settings::get('FIXED_SENDER');
	}

	//
	// special case: if there is only one non-special string value, then
	// format it as an email (unless an option says not to)
	//
	$a_keys = array_keys($a_raw_fields);
	if (count($a_keys) == 1 && is_string($a_raw_fields[$a_keys[0]]) &&
	    !IsMailOptionSet("AlwaysList") && !IsMailOptionSet("DupHeader")
	) {
		if (IsMailExcluded($a_keys[0])) {
			SendAlert("Exclusion of single field '" . $a_keys[0] . "' ignored");
		}
		$s_value = $a_raw_fields[$a_keys[0]];
		//
		// replace carriage return/linefeeds with <br>
		//
		$s_value = str_replace("\r\n",'<br />',$s_value);
		//
		// replace lone linefeeds with <br>
		//
		$s_value = str_replace("\n",'<br />',$s_value);
		//
		// remove lone carriage returns
		//
		$s_value = str_replace("\r","",$s_value);
		//
		// replace all control chars with <br />
		//
		$s_value = preg_replace('/[[:cntrl:]]+/','<br />',$s_value);
		//
		// strip HTML (note that all the <br> above will now be
		// replaced with BODY_LF)
		//
		$s_value = StripHTML($s_value,Settings::get('BODY_LF'));

		if ($b_mime_mail) {
			if ($b_got_filter) {
				//
				// filter the whole value (ignore filter_fields for this
				// special case) if a filter has been specified
				//
				$s_results = Filter($s_filter,$s_value);
				if ($b_filter_attach) {
					$a_attach_spec["Data"] = $s_results;
					//
					// KeepInLine keeps the filtered version inline as well
					// as an attachment
					//
					if (!IsFilterOptionSet("KeepInLine")) {
						$s_results = "";
					}
					$s_filter = ""; // no more filtering
				}
			} else {
				$s_results = $s_value;
			}

			//
			// send this single value off to get formatted in a MIME
			// email
			//
			if (!MakeMimeMail($s_results,$a_headers,$a_raw_fields,
			                  GetMailOption('HTMLTemplate'),
			                  GetMailOption('TemplateMissing'),
			                  IsMailOptionSet("NoPlain"),
			                  $s_filter,$aFileVars,$a_attach_spec)
			) {
				return (false);
			}
		} elseif ($b_got_filter)
			//
			// filter the whole value (ignore filter_fields for this special case)
			// if a filter has been specified
			//
		{
			$s_results = Filter($s_filter,$s_value);
		} else {
			$s_results = $s_value;
			if (IsMailOptionSet("CharSet"))
				//
				// sending plain text email, and the CharSet has been
				// specified; include a header
				//
			{
				$a_headers['Content-Type'] = "text/plain; charset=" . SafeHeader(GetMailOption("CharSet"));
			}
		}
	} else {
		if ($b_mime_mail) {
			//
			// get the plain text version of the email then send it
			// to get MIME formatted
			//
			list($s_results,$s_unfiltered_results,$s_filtered_results) =
				MakePlainEmail($a_fld_order,$a_clean_fields,
				               $s_to,$s_cc,$s_bcc,$a_raw_fields,$s_filter,
				               $a_filter_list);
			if ($b_filter_attach) {
				//
				// attached the filtered results
				//
				$a_attach_spec["Data"] = $s_filtered_results;
				//
				// KeepInLine keeps the filtered version inline as well
				// as an attachment
				//
				if (!IsFilterOptionSet("KeepInLine"))
					//
					// put the unfiltered results in the body of the message
					//
				{
					$s_results = $s_unfiltered_results;
				}
				$s_filter = ""; // no more filtering
			}
			if (!MakeMimeMail($s_results,$a_headers,$a_raw_fields,
			                  GetMailOption('HTMLTemplate'),
			                  GetMailOption('TemplateMissing'),
			                  IsMailOptionSet("NoPlain"),
			                  $s_filter,$aFileVars,$a_attach_spec)
			) {
				return (false);
			}
		} else {
			list($s_results,$s_unfiltered_results,$s_filtered_results) =
				MakePlainEmail($a_fld_order,$a_clean_fields,
				               $s_to,$s_cc,$s_bcc,$a_raw_fields,$s_filter,
				               $a_filter_list);
			if (!$b_got_filter && IsMailOptionSet("CharSet"))
				//
				// sending plain text email, and the CharSet has been
				// specified; include a header
				//
			{
				$a_headers['Content-Type'] = "text/plain; charset=" . SafeHeader(GetMailOption("CharSet"));
			}
		}
	}

	//
	// now save uploaded files to the repository
	//
	if (Settings::get('FILEUPLOADS') && Settings::get('FILE_REPOSITORY') !== "") {
		if (!SaveAllFilesToRepository()) {
			return (false);
		}
	}

	//
	// send the mail - assumes the email addresses have already been checked
	//
	return (SendCheckedMail($s_to,$SPECIAL_VALUES["subject"],$s_results,
	                        $s_sender,$a_headers));
}

//
// append an entry to a log file
//
function WriteLog($log_file)
{
	global $SPECIAL_VALUES,$php_errormsg;

	@   $log_fp = fopen($log_file,"a");
	if ($log_fp === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $log_file,
		                                               "TYPE"  => "log",
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return;
	}
	$date  = gmdate("H:i:s d-M-y T");
	$entry = $date . ":" . $SPECIAL_VALUES["email"] . "," .
	         $SPECIAL_VALUES["realname"] . "," . $SPECIAL_VALUES["subject"] . "\n";
	fwrite($log_fp,$entry);
	fclose($log_fp);
}

//
// write the data to a comma-separated-values file
//
function WriteCSVFile($s_csv_file,$a_vars)
{
	global $SPECIAL_VALUES;

	//
	// create an array of column values in the order specified
	// in $SPECIAL_VALUES["csvcolumns"]
	//
	$a_column_list = $SPECIAL_VALUES["csvcolumns"];
	if (!isset($a_column_list) || empty($a_column_list) || !is_string($a_column_list)) {
		SendAlert(GetMessage(MSG_CSVCOLUMNS,array("VALUE" => $a_column_list)));
		return;
	}
	if (!isset($s_csv_file) || empty($s_csv_file) || !is_string($s_csv_file)) {
		SendAlert(GetMessage(MSG_CSVFILE,array("VALUE" => $s_csv_file)));
		return;
	}

	@   $fp = fopen($s_csv_file,"a" . Settings::get('CSVOPEN'));
	if ($fp === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $s_csv_file,
		                                               "TYPE"  => "CSV",
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return;
	}

	//
	// convert the column list to an array, trim the names too
	//
	$a_column_list = TrimArray(explode(",",$a_column_list));
	$n_columns     = count($a_column_list);

	//
	// if the file is currently empty, put the column names in the first line
	//
	$b_heading = false;
	if (filesize($s_csv_file) == 0) {
		$b_heading = true;
	}

	$csv_format = new CSVFormat();

	//
	// now configure the CSVFormat object
	// according to FormMail's configuration settings
	//
	$csv_format->SetQuote(Settings::get('CSVQUOTE'));
	$csv_format->SetEscPolicy("conv");
	$csv_format->SetSep(Settings::get('CSVSEP'));
	$csv_format->SetIntSep(Settings::get('CSVINTSEP'));
	if (Settings::get('LIMITED_IMPORT')) {
		$csv_format->SetCleanFunc(function ($m_value) {
			return CleanValue($m_value);
		});
	}

	$s_csv = $csv_format->MakeCSVRecord($a_column_list,$a_vars);

	if ($b_heading) {
		fwrite($fp,$csv_format->MakeHeading($a_column_list) . Settings::get('CSVLINE'));
	}

	fwrite($fp,$s_csv . Settings::get('CSVLINE'));
	fclose($fp);
	//  CreatePage($debug);
	//  FormMailExit();
}

function CheckConfig()
{
	$a_mesgs = array();
	if (in_array("TARGET_EMAIL",Settings::get('CONFIG_CHECK'))) {
		//
		// $TARGET_EMAIL values should begin with ^ and end with $
		//
		$a_target_email = Settings::get('TARGET_EMAIL');
		for ($ii = 0 ; $ii < count($a_target_email) ; $ii++) {
			$s_pattern = $a_target_email[$ii];
			if (substr($s_pattern,0,1) != '^') {
				$a_mesgs[] = GetMessage(MSG_TARG_EMAIL_PAT_START,
				                        array("PAT" => $s_pattern));
			}
			if (substr($s_pattern,-1) != '$') {
				$a_mesgs[] = GetMessage(MSG_TARG_EMAIL_PAT_END,
				                        array("PAT" => $s_pattern));
			}
		}
	}
	if (Settings::get('SET_SENDER_FROM_EMAIL')) {
		$a_mesgs[] = GetMessage(MSG_SET_SENDER_FROM_EMAIL);
	}
	if (count($a_mesgs) > 0) {
		SendAlert(GetMessage(MSG_CONFIG_WARN,
		                     array("MESGS" => implode("\n",$a_mesgs))),false,true);
	}
}

//
// append an entry to the Auto Responder log file
//
function WriteARLog($s_to,$s_subj,$s_info)
{
	global $aServerVars,$php_errormsg;

	if (Settings::isEmpty('LOGDIR') || Settings::isEmpty('AUTORESPONDLOG')) {
		return;
	}

	$log_file = Settings::get('LOGDIR') . "/" . Settings::get('AUTORESPONDLOG');
	@   $log_fp = fopen($log_file,"a");
	if ($log_fp === false) {
		SendAlert(GetMessage(MSG_FILE_OPEN_ERROR,array("NAME"  => $log_file,
		                                               "TYPE"  => "log",
		                                               "ERROR" => CheckString($php_errormsg)
		)));
		return;
	}
	$a_entry   = array();
	$a_entry[] = gmdate("H:i:s d-M-y T"); // date/time in GMT
	$a_entry[] = $aServerVars['REMOTE_ADDR']; // remote IP address
	$a_entry[] = $s_to; // target email address
	$a_entry[] = $s_subj; // subject line
	$a_entry[] = $s_info; // information

	$s_log_entry = implode(",",$a_entry) . "\n";
	fwrite($log_fp,$s_log_entry);
	fclose($log_fp);
}

/*
 * The main logic starts here....
 */

//
// First, a special case; if formmail.php is called like this:
//  http://.../formmail.php?testalert=1
// it sends a test message to the default alert address with some
// information about your PHP version and the DOCUMENT_ROOT.
//
if (isset($aGetVars["testalert"])) {
	if ($aGetVars["testalert"] == Settings::get('TEST_PASSWORD')) {
		function ShowServerVar($s_name)
		{
			global $aServerVars;

			return (isset($aServerVars[$s_name]) ? $aServerVars[$s_name] : "-not set-");
		}

		$sAlert = GetMessage(MSG_ALERT,
		                     array("LANG"               => $sLangID,
		                           "PHPVERS"            => $ExecEnv->getPHPVersionString(),
		                           "FM_VERS"            => $FM_VERS,
		                           "SERVER"             => (IsServerWindows() ? "Windows" : "non-Windows"),
		                           "DOCUMENT_ROOT"      => ShowServerVar('DOCUMENT_ROOT'),
		                           "SCRIPT_FILENAME"    => ShowServerVar('SCRIPT_FILENAME'),
		                           "PATH_TRANSLATED"    => ShowServerVar('PATH_TRANSLATED'),
		                           "REAL_DOCUMENT_ROOT" => CheckString($REAL_DOCUMENT_ROOT),
		                     ));

		if (Settings::get('DEF_ALERT') == "") {
			echo "<p>" . GetMessage(MSG_NO_DEF_ALERT) . "</p>";
		} elseif (SendAlert($sAlert,false,true)) {
			echo "<p>" . GetMessage(MSG_TEST_SENT) . "</p>";
		} else {
			echo "<p>" . GetMessage(MSG_TEST_FAILED) . "</p>";
		}
		FormMailExit();
	} else {
		echo "<p>Wrong password for testalert!</p>";
		FormMailExit();
	}
}

if (isset($aGetVars["testlang"]) && $aGetVars["testlang"] == 1) {

	function ShowMessages()
	{
		global $aMessages,$sLangID,$aGetVars,$sHTMLCharSet;

		//
		// force message numbers on unless "mnums=no"
		//
		if (isset($aGetVars["mnums"]) && $aGetVars["mnums"] == "no") {
			Settings::set('bShowMesgNumbers',false);
		} else {
			Settings::set('bShowMesgNumbers',true);
		}
		LoadBuiltinLanguage();

		$s_def_lang  = $sLangID;
		$a_def_mesgs = $aMessages;

		LoadLanguageFile();

		$s_active_lang  = $sLangID;
		$a_active_mesgs = $aMessages;

		$a_list = get_defined_constants();

		echo "<html>\n";
		echo "<head>\n";
		if (isset($sHTMLCharSet) && $sHTMLCharSet !== "") {
			echo "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=$sHTMLCharSet\">\n";
		}
		echo "</head>\n";
		echo "<body>\n";

		echo "<table border=\"1\" cellpadding=\"10\" width=\"95%\">\n";
		echo "<tr>\n";
		echo "<th>\n";
		echo "Message Number";
		echo "</th>\n";
		echo "<th>\n";
		echo "$s_def_lang";
		echo "</th>\n";
		echo "<th>\n";
		echo "$s_active_lang";
		echo "</th>\n";
		echo "</tr>\n";
		foreach ($a_list as $s_name => $i_value) {
			if (substr($s_name,0,4) == "MSG_") {
				//
				// some PHP constants begin with MSG_, so we try to skip them
				// too
				//
				switch ($s_name) {
					case "MSG_IPC_NOWAIT":
					case "MSG_EAGAIN":
					case "MSG_ENOMSG":
					case "MSG_NOERROR":
					case "MSG_EXCEPT":
					case "MSG_OOB":
					case "MSG_PEEK":
					case "MSG_DONTROUTE":
					case "MSG_EOR":
						continue 2;
				}
				if ($i_value >= 256) {
					continue;
				}
				echo "<tr>\n";
				echo "<td valign=\"top\">\n";
				echo "$s_name ($i_value)";
				echo "</td>\n";
				echo "<td valign=\"top\">\n";
				$aMessages = $a_def_mesgs;
				$s_def_msg = GetMessage((int)$i_value,array(),true,true);
				echo nl2br(htmlentities($s_def_msg)); // English - don't need
				// FixedHTMLEntities
				echo "</td>\n";
				echo "<td valign=\"top\">\n";
				$aMessages = $a_active_mesgs;
				$s_act_msg = GetMessage((int)$i_value,array(),true,true);
				if ($s_def_msg == $s_act_msg) {
					echo "<i>identical</i>\n";
				} else {
					echo nl2br(FixedHTMLEntities($s_act_msg));
				}
				echo "</td>\n";
				echo "</tr>\n";
			}
		}
		echo "</table>\n";

		echo "</body>\n";
		echo "</html>\n";
	}

	ShowMessages();
	FormMailExit();
}

//
// For saved files, add in the "new_name" values to the given
// array.
//
function GetSavedFileNames($a_values)
{
	if (IsSetSession("FormSavedFiles")) {
		$a_saved_files = GetSession("FormSavedFiles");
		foreach ($a_saved_files as $s_fld => $a_upload) {
			if (isset($a_upload["name"])) {
				$a_values[$s_fld] = $a_upload["name"];
			}
			if (isset($a_upload["new_name"])) {
				$a_values["name_of_$s_fld"] = $a_upload["new_name"];
			}
		}
	}
	return ($a_values);
}

//
// Scan the Multi form sequence values and build up the values that
// were submitted to the given form index.
//
function GetMultiValues($a_form_list,$i_form_index,$a_order = array(),
                        $a_clean = array(),
                        $a_raw_data = array(),
                        $a_all_data = array(),
                        $a_file_data = array())
{
	$a_ret_clean = $a_ret_raw = $a_ret_all = $a_ret_files = array();
	for ($ii = 0 ; $ii < $i_form_index ; $ii++) {
		//
		// only add a field to the order if it's not already there
		//
		$a_form_order = $a_form_list[$ii]["ORDER"];
		$n_order      = count($a_form_order);
		for ($jj = 0 ; $jj < $n_order ; $jj++) {
			if (array_search($a_form_order[$jj],$a_order) === false) {
				$a_order[] = $a_form_order[$jj];
			}
		}
		$a_ret_clean = array_merge($a_ret_clean,$a_form_list[$ii]["CLEAN"]);
		$a_ret_raw   = array_merge($a_ret_raw,$a_form_list[$ii]["RAWDATA"]);
		$a_ret_all   = array_merge($a_ret_all,$a_form_list[$ii]["ALLDATA"]);
		$a_ret_files = array_merge($a_ret_files,$a_form_list[$ii]["FILES"]);
	}
	//
	// later values must take precedence to earlier values,
	// so merge in the passed-in data last
	//
	$a_ret_clean = array_merge($a_ret_clean,$a_clean);
	$a_ret_raw   = array_merge($a_ret_raw,$a_raw_data);
	$a_ret_all   = array_merge($a_ret_all,$a_all_data);
	$a_ret_files = array_merge($a_ret_files,$a_file_data);
	return (array($a_order,$a_ret_clean,$a_ret_raw,$a_ret_all,$a_ret_files));
}

$bMultiForm = false;

//
// Multi-page form sequencing is complicated....
// Requirements:
//      1. The first page in a multi-page form sequence must provide:
//          - multi_start field set to 1
//          - this_form field (it's own URL)
//          - next_form field (multi-page form template name)
//      2. Subsequent pages must provide either:
//          next_form (the next template name in the sequence)
//         OR
//          good_url or good_template or neither; this terminates
//          the multi-page processing and provides the final URL.
// Logic:
//      1. We create session variables to contain information about
//         the sequence.
//      2. On the first submission from the starting form, we record
//         its "this_form" URL and the data submitted.
//         We also create a URL (to FormMail) that will allow return
//         to the *next* form in the sequence.
//      3. On submission from other forms in the sequence, we record
//         the data that was submitted so we can return to that
//         form with the user's data re-filled into the form.
//         We also create a URL (to FormMail) that will allow return
//         to the *next* form in the sequence.
//      4. A return URL contains "return=index" where index is the
//         form sequence index number.  This is a URL to FormMail.
//         FormMail gets the template name or URL (URL only for the
//         starting form) and outputs the requested HTML form.  It also
//         truncates the session data for forms after the returned-to
//         one.
//

//
// Return to a previous form in a sequence.
//
function MultiFormReturn($i_return_to)
{
	global $iFormIndex;
	global $SessionAccessor;

	if (!IsSetSession("FormList") ||
	    !IsSetSession("FormIndex") ||
	    $i_return_to < 0 ||
	    $i_return_to > GetSession("FormIndex")
	) {
		Error("cannot_return",GetMessage(MSG_CANNOT_RETURN,
		                                 array("TO"       => $i_return_to,
		                                       "TOPINDEX" => (
		                                       IsSetSession("FormIndex") ?
			                                       GetSession("FormIndex") :
			                                       "<undefined>")
		                                 )),
		      false,false);
	}
	$a_list = GetSession("FormList");
	assert($i_return_to < count($a_list));
	$a_form_def = $a_list[$i_return_to];
	SetSession("FormList",$a_list = array_slice($a_list,0,$i_return_to + 1));
	SetSession("FormIndex",$iFormIndex = $i_return_to);
	if (isset($a_form_def["FORM"])) {
		//
		// get the values that were originally submitted to this form
		//
		list(,,$a_values,,) = GetMultiValues($a_list,$i_return_to);
		//
		// get the session values
		//
		$SessionAccessor->CopyIn($a_values,true);
		$a_lines = array();
		//
		// process the page as a template
		//
		if (ProcessMultiFormTemplate($a_form_def["FORM"],$a_values,$a_lines)) {
			$n_lines = count($a_lines);
			$s_buf   = "";
			for ($ii = 0 ; $ii < $n_lines ; $ii++) {
				$s_buf .= $a_lines[$ii] . "\n";
				unset($a_lines[$ii]); // free memory (hopefully)
			}
			unset($a_lines); // free memory (hopefully)
			//
			// put in the values that the user previously submitted
			// to this form
			//
			echo SetPreviousValues($s_buf,$a_form_def["RAWDATA"]);
		} else {
			Error("multi_form_failed",GetMessage(MSG_MULTIFORM_FAILED,
			                                     array("NAME" => $a_form_def["FORM"])),false,false);
		}
	} else
		//
		// we probably should include 
		//  $SessionAccessor->CopyIn(...,true);
		// at some stage in the future to get the session values....need to think about this
		// and run some case studies.
		//
	{
		ProcessReturnToForm($a_form_def["URL"],$a_form_def["RAWDATA"],array("multi_start"));
	}
	//echo "Returned to $i_return_to";
}

//
// Store any data just submitted and specified as "multi_keep".
//
function MultiKeep()
{
	global $SPECIAL_VALUES,$aRawDataValues;

	if (isset($SPECIAL_VALUES["multi_keep"]) &&
	    !empty($SPECIAL_VALUES["multi_keep"])
	) {
		$a_list = TrimArray(explode(",",$SPECIAL_VALUES["multi_keep"]));
		if (IsSetSession("FormKeep")) {
			$a_keep = GetSession("FormKeep");
		} else {
			$a_keep = array();
		}
		//
		// For each data field specified in "multi_keep" store its
		// value in the FormKeep session variable.
		// If a field is specified and does not exist in the
		// recent submission, its value is discarded.
		//
		foreach ($a_list as $s_fld_name) {
			if (!empty($s_fld_name)) {
				if (isset($aRawDataValues[$s_fld_name])) {
					$a_keep[$s_fld_name] = $aRawDataValues[$s_fld_name];
				} else {
					unset($a_keep[$s_fld_name]);
				}
			}
		}
		SetSession("FormKeep",$a_keep);
	}
}

//
// Perform Logic for Multi-Page form sequences
//
function MultiFormLogic()
{
	global $bMultiForm,$SPECIAL_VALUES,$aServerVars,$aFileVars,$ExecEnv;
	global $sFormMailScript,$bGotGoBack,$bGotNextForm,$iFormIndex;
	global $aFieldOrder,$aCleanedValues,$aRawDataValues,$aAllRawValues;

	if ($SPECIAL_VALUES["multi_start"] == 1) {
		if (empty($SPECIAL_VALUES["this_form"])) {
			ErrorWithIgnore("need_this_form",GetMessage(MSG_NEED_THIS_FORM),false,false);
		}

		$bMultiForm = true;
		//
		// Start of multi-page form sequence
		//
		$a_list     = array();
		$a_list[0]  = array("URL"     => $SPECIAL_VALUES["this_form"],
		                    "ORDER"   => $aFieldOrder,
		                    "CLEAN"   => $aCleanedValues,
		                    "RAWDATA" => $aRawDataValues,
		                    "ALLDATA" => $aAllRawValues,
		                    "FILES"   => $aFileVars
		);
		$iFormIndex = 0; // zero is the first form, which was just submitted
		SetSession("FormList",$a_list);
		SetSession("FormIndex",$iFormIndex);
		//
		// this is a fresh session, so remove any remembered values
		//
		UnsetSession("FormSavedFiles");
		UnsetSession("FormKeep");
	} elseif (IsSetSession("FormList")) {
		$bMultiForm = true;
	}

	if ($bMultiForm) {
		$sFormMailScript = $ExecEnv->GetScript();
		$iFormIndex      = GetSession("FormIndex");
	}

	//
	// If we're going forward in a multi-page form sequence,
	// compute a URL to return to the form we're about to display.
	//
	if ($bMultiForm && !$bGotGoBack) {
		//
		// record the data that was just submitted by the previous form
		//
		$iFormIndex                     = GetSession("FormIndex");
		$a_list                         = GetSession("FormList");
		$a_list[$iFormIndex]["ORDER"]   = $aFieldOrder;
		$a_list[$iFormIndex]["CLEAN"]   = $aCleanedValues;
		$a_list[$iFormIndex]["RAWDATA"] = $aRawDataValues;
		$a_list[$iFormIndex]["ALLDATA"] = $aAllRawValues;
		if (count($aFileVars) > 0 && !Settings::get('FILEUPLOADS')) {
			SendAlert(GetMessage(MSG_FILE_UPLOAD));
		} elseif (count($aFileVars) > 0 && !SaveAllUploadedFiles($aFileVars)) {
			Error("upload_save_failed",GetMessage(MSG_MULTI_UPLOAD),false,false);
		}
		$a_list[$iFormIndex]["FILES"] = $aFileVars;
		$iFormIndex++;
		$s_url               = GetReturnLink($sFormMailScript,$iFormIndex);
		$a_list[$iFormIndex] = array("URL"     => $s_url,
		                             "FORM"    => $SPECIAL_VALUES["next_form"],
		                             "ORDER"   => $aFieldOrder,
		                             "CLEAN"   => $aCleanedValues,
		                             "RAWDATA" => $aRawDataValues,
		                             "ALLDATA" => $aAllRawValues,
		                             "FILES"   => $aFileVars
		);
		SetSession("FormList",$a_list);
		SetSession("FormIndex",$iFormIndex);
		MultiKeep();
	}
}

//
// Check for the MIME Attack
//
function DetectMimeAttack($a_fields,&$s_attack,&$s_info,&$s_user_info)
{
	//
	// if any of the recipient fields contain "MIME-Version" or
	// "Content-Type" then this is the MIME attack
	// Now also checks the subject field.
	//
	$a_rec_flds = array("recipients","cc","bcc","replyto","subject");
	foreach ($a_rec_flds as $s_fld) {
		if (isset($a_fields[$s_fld])) {
			//
			// some of the fields can be arrays
			//
			if (is_array($a_fields[$s_fld])) {
				$s_data = implode(",",$a_fields[$s_fld]);
			} else {
				$s_data = $a_fields[$s_fld];
			}
			$s_data = strtolower($s_data);
			if (($i_mime = strpos($s_data,"mime-version")) !== false ||
			    ($i_cont = strpos($s_data,"content-type")) !== false
			) {
				$s_attack = "MIME";
				$s_info   = GetMessage(MSG_ATTACK_MIME_INFO,
				                       array("FLD"     => $s_fld,
				                             "CONTENT" => ($i_mime !==
				                                           false) ?
					                             "mime-version" :
					                             "content-type"
				                       ),
				                       false);
				return (true);
			}
		}
	}
	return (false);
}

//
// Strip common language sequences from the data
// that might otherwise look like a junk attack.
// We replace them with a space so that the stripping
// cannot create more junk accidentally.
//
function AttackDetectionStripLang($s_data)
{

	foreach (Settings::get('ATTACK_DETECTION_JUNK_LANG_STRIP') as $s_seq) {
		$s_data = str_replace($s_seq," ",$s_data);
	}
	return ($s_data);
}

//
// Find strings of $n_consec characters from the alphabet of $s_alpha
// in $s_data.  Return the count of instances found.
//
function AttackDetectionFindJunk($s_data,$s_alpha,$n_consec,&$a_matches)
{
	$s_pat = "/[" . preg_quote($s_alpha,"/") . "]{" . "$n_consec,$n_consec" . "}/";
	if (($n_count = preg_match_all($s_pat,$s_data,$a_matches)) === false) {
		$n_count   = 0;
		$a_matches = array();
	} else {
		$a_matches = $a_matches[0];
	}
	return ($n_count);
}

//
// Test if a field name is a technical field.
// This means a field that is likely to contain junk values.
// Any special field and certain other field names are counted as technical.
//
function IsTechnicalField($s_fld)
{
	$b_is_technical = false;
	if (IsSpecialField($s_fld) || IsSpecialMultiField($s_fld)) {
		$b_is_technical = true;
	} else {
		// case doesn't matter
		$s_lower_fld = strtolower($s_fld);

		$a_prefix_list    = array('email');
		$a_prefix_found   = array_filter($a_prefix_list,function ($s_prefix) use ($s_lower_fld) {
			return substr($s_lower_fld,strlen($s_prefix)) === $s_prefix;
		});
		$a_suffix_list    = array('check','url','email');
		$a_suffix_found   = array_filter($a_suffix_list,function ($s_suffix) use ($s_lower_fld) {
			return substr($s_lower_fld,-strlen($s_suffix)) === $s_suffix;
		});
		$a_fullname_list  = array('tectiteformid');
		$b_fullname_match = array_search($s_lower_fld,$a_fullname_list) !== false;
		$b_is_technical   = $b_fullname_match || count($a_prefix_found) > 0 || count($a_suffix_found) > 0;
	}
	return $b_is_technical;
}

//
// Check for the Junk Attack
//
function DetectJunkAttack($a_fields,&$s_attack,&$s_info,&$s_user_info)
{
	//
	// If any field contains junk data, trigger detection.
	//
	$n_count        = 0;
	$a_matches      = array();
	$a_user_matches = array();
	foreach ($a_fields as $s_fld => $m_value) {
		if (IsTechnicalField($s_fld)) {
			//
			// Skip special fields because they don't contain
			// normal user input.
			// But, some special fields do contain normal user
			// input, so we don't skip them.
			//
			$b_skip = true;
			switch ($s_fld) {
				case "realname":
				case "subject":
					$b_skip = false;
					break;
			}
			if ($b_skip) {
				continue;
			}
		}
		//
		// Ignore fields that are specified to contain
		// technical information.
		//
		if (in_array($s_fld,Settings::get('ATTACK_DETECTION_JUNK_IGNORE_FIELDS'),true)) {
			continue;
		}
		if (isset($m_value) && !FieldManager::IsEmpty($m_value)) {
			if (!is_array($m_value)) {
				$m_value = array($m_value);
			}
			foreach ($m_value as $s_data) {
				$s_orig_data = $s_data = strtolower($s_data);
				//
				// Strip out sequences that might be common to
				// the user's language.
				// For example, in English, there are a lot of common
				// words that can easily be protected from our
				// tests. For example, "thousandths" has 5 consecutive consonants
				// but is a reasonable word.
				// Similarly, "queue" has 4 consecutive vowels.
				//
				$s_data = AttackDetectionStripLang($s_data);
				//
				// Look for strings of too many vowels or too many consonants.
				// For safety, we must detect more than one instance, because there
				// are a lot of normal words that may be caught by this test.
				// The number of instances is controlled by configuration.
				//
				$n_match = AttackDetectionFindJunk($s_data,Settings::get('ATTACK_DETECTION_JUNK_CONSONANTS'),
				                                   Settings::get('ATTACK_DETECTION_JUNK_CONSEC_CONSONANTS'),
				                                   $a_match_cons);
				if ($n_match > 0) {
					$a_user_matches[] = $s_orig_data;
				}
				$n_count += $n_match;
				$n_match = AttackDetectionFindJunk($s_data,Settings::get('ATTACK_DETECTION_JUNK_VOWELS'),
				                                   Settings::get('ATTACK_DETECTION_JUNK_CONSEC_VOWELS'),
				                                   $a_match_vow);
				if ($n_match > 0) {
					$a_user_matches[] = $s_orig_data;
				}
				$n_count += $n_match;
				if ($n_count >= Settings::get('ATTACK_DETECTION_JUNK_TRIGGER')) {
					$a_matches   = array_merge($a_matches,$a_match_cons,$a_match_vow);
					$s_user_info = GetMessage(MSG_USER_ATTACK_JUNK,
					                          array("INPUT" => implode(", ",$a_user_matches)),false);
					$s_attack    = "JUNK";
					$s_info      = GetMessage(MSG_ATTACK_JUNK_INFO,
					                          array("FLD"  => $s_fld,
					                                "JUNK" => implode(" ",
					                                                  $a_matches)
					                          ),
					                          false);
					return (true);
				}
			}
		}
	}
	return (false);
}

//
// Check for the duplicate data attack
//
function DetectDupAttack($a_fields,&$s_attack,&$s_info,&$s_user_info)
{
	//
	// if any of the configured fields contain duplicate data,
	// then this lame attack has been detected
	//

	$a_data_map = array();
	foreach (Settings::get('ATTACK_DETECTION_DUPS') as $s_fld) {
		if (isset($a_fields[$s_fld]) &&
		    is_scalar($a_fields[$s_fld]) && // can only work with string data
		    !empty($a_fields[$s_fld])
		) {
			$s_data = (string)$a_fields[$s_fld];
			if (isset($a_data_map[$s_data])) {
				//
				// duplicate found!
				//
				$s_attack    = "Duplicate Fields";
				$s_info      = GetMessage(MSG_ATTACK_DUP_INFO,
					array("FLD1" => $a_data_map[$s_data],
					      "FLD2" => $s_fld
					),false);
				$s_user_info = GetMessage(MSG_USER_ATTACK_DUP,array(),false);
				return (true);
			}
			$a_data_map[$s_data] = $s_fld;
		}
	}
	return (false);
}

//
// Check for the email addresses in specials attack
//
function DetectSpecialsAttack($a_fields,&$s_attack,&$s_info,&$s_user_info)
{
	//
	// look for email addresses in whole fields
	//
	foreach (Settings::get('ATTACK_DETECTION_SPECIALS_ONLY_EMAIL') as $s_fld) {
		if (isset($a_fields[$s_fld]) &&
		    is_scalar($a_fields[$s_fld]) && // can only work with string data
		    !empty($a_fields[$s_fld])
		) {
			$s_data = $a_fields[$s_fld];
			if (preg_match("/^\b[-a-z0-9._%]+@[-.%_a-z0-9]+\.[a-z]{2,9}\b$/i",
			               $s_data) === 1
			) {
				//
				// email address found in wrong field
				//
				$s_attack = "Special Fields Only";
				$s_info   = GetMessage(MSG_ATTACK_SPEC_INFO,
				                       array("FLD" => $s_fld),false);
				return (true);
			}
		}
	}
	//
	// look for email addresses in any part of fields
	//
	foreach (Settings::get('ATTACK_DETECTION_SPECIALS_ANY_EMAIL') as $s_fld) {
		if (isset($a_fields[$s_fld]) &&
		    is_scalar($a_fields[$s_fld]) && // can only work with string data
		    !empty($a_fields[$s_fld])
		) {
			$s_data = $a_fields[$s_fld];
			if (preg_match("/\b[-a-z0-9._%]+@[-.%_a-z0-9]+\.[a-z]{2,9}\b/i",
			               $s_data) > 0
			) {
				//
				// email address found in wrong field
				//
				$s_attack = "Special Fields Any";
				$s_info   = GetMessage(MSG_ATTACK_SPEC_INFO,
				                       array("FLD" => $s_fld),false);
				return (true);
			}
		}
	}
	return (false);
}

//
// Check for "many URLs in a field" and "many fields with URLs" attacks
//
function DetectManyURLsAttack($a_fields,&$s_attack,&$s_info,&$s_user_info)
{
	//
	// This suffix to patterns ensure that a domain name is not actually the name component
	// of an email address.  For example, 'ss.game@bigpond.com' would get incorrectly recognised as a URL
	// without this additional pattern.
	//
	$s_not_email_name = '(?!@[a-z]+\.)';

	$a_fld_names = array();
	//
	// actual URL link patterns
	// 21-Jul-18: We've removed looking for alphanumerics here because it doesn't
	// detect non-English chars.  The UTF-8 options for preg_match did not help.
	//
	$s_srch = '(\bhttps{0,1}:\/\/|<\s*a\s+href=)';

	//
	// now add configurable patterns
	//
	if (!Settings::isEmpty('ATTACK_DETECTION_URL_PATTERNS') &&
	    is_array(Settings::get('ATTACK_DETECTION_URL_PATTERNS'))
	) {
		//
		// escape / characters and build up a string
		// of alternate patterns
		//
		foreach (Settings::get('ATTACK_DETECTION_URL_PATTERNS') as $s_pat) {
			if ($s_pat == "") {
				continue;
			}
			$s_srch .= "|" . str_replace('/','\/',$s_pat) . $s_not_email_name;
		}
	}
	if (Settings::get('SITE_DOMAIN')) {
		if (strpos(Settings::get('SITE_DOMAIN'),'://') !== false) {
			$a_url_parts = parse_url(Settings::get('SITE_DOMAIN'));
			$own_domain  = $a_url_parts['host'];
		} else {
			$own_domain = Settings::get('SITE_DOMAIN');
		}
		$own_domain = str_ireplace('www.','',$own_domain);
	} else {
		$own_domain = '';
	}
	foreach ($a_fields as $s_fld => $s_data) {
		if (IsSpecialField($s_fld) || IsSpecialMultiField($s_fld))
			//
			// skip special fields because some are supposed to
			// have URLs in them
			//
		{
			continue;
		}
		if (isset($s_data) &&
		    is_scalar($s_data) && // can only work with string data
		    !empty($s_data)
		) {
			$n_match = preg_match_all("/$s_srch/msi",$s_data,$a_matches);
			if (!is_int($n_match)) {
				$n_match = 0;
			}
			// allow links to SITE_DOMAIN
			if ($own_domain) {
				$n_site_match = preg_match('/\b' . $own_domain . '\b/msi',$s_data);
				if (is_int($n_site_match) && $n_site_match > 0) {
					$n_match -= $n_site_match;
				}
			}
			/*
             * debugging code....
             if ($n_match > 0)
                echo "Pattern is '".htmlentities($s_srch)."' with '".
                    htmlentities($s_data)."' field '$s_fld', matched=$n_match<br />";
             */
			if (Settings::get('ATTACK_DETECTION_MANY_URLS') > 0) {
				if ($n_match >= Settings::get('ATTACK_DETECTION_MANY_URLS')) {
					$s_attack    = "Many URLS in a field";
					$s_info      = GetMessage(MSG_ATTACK_MANYURL_INFO,
					                          array("FLD" => $s_fld,"NUM" => ($n_match)),false);
					$s_user_info = GetMessage(MSG_USER_ATTACK_MANY_URLS,array(),false);
					return (true);
				}
			}
			if ($n_match > 0) {
				$a_fld_names[] = $s_fld;
			}
		}
	}
	if (Settings::get('ATTACK_DETECTION_MANY_URL_FIELDS') > 0) {
		if (count($a_fld_names) >= Settings::get('ATTACK_DETECTION_MANY_URL_FIELDS')) {
			$s_attack    = "Many fields with URLs";
			$s_info      = GetMessage(MSG_ATTACK_MANYFIELDS_INFO,
				array("FLDS" => implode(",",$a_fld_names),
				      "NUM"  => (count($a_fld_names))
				),false);
			$s_user_info = GetMessage(MSG_USER_ATTACK_MANY_URL_FIELDS,array(),false);
			return (true);
		}
	}
	return (false);
}

function IsAjax()
{
	global $SPECIAL_VALUES,$aFormVars,$aGetVars;

	//
	// this may be called too early to have SPECIAL_VALUES loaded,
	// so we check the submitted form vars too.
	//
	if (isset($SPECIAL_VALUES) && $SPECIAL_VALUES["fmmode"] == "ajax") {
		return (true);
	}
	if (isset($aFormVars["fmmode"])) {
		return ($aFormVars["fmmode"] == "ajax");
	}
	if (isset($aGetVars["fmmode"])) {
		return ($aGetVars["fmmode"] == "ajax");
	}
	return (false);
}

//
// Detect annoying attacks and prevent them from sending spurious
// alert messages.
//
function DetectAttacks($a_fields)
{

	$s_info      = $s_attack = "";
	$b_attacked  = false;
	$s_user_info = "";
	if (Settings::get('ATTACK_DETECTION_MIME')) {
		if (DetectMimeAttack($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}
	if (!$b_attacked && !Settings::isEmpty('ATTACK_DETECTION_DUPS')) {
		if (DetectDupAttack($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}
	if (!$b_attacked && Settings::get('ATTACK_DETECTION_SPECIALS')) {
		if (DetectSpecialsAttack($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}
	if (!$b_attacked && (Settings::get('ATTACK_DETECTION_MANY_URLS') ||
	                     Settings::get('ATTACK_DETECTION_MANY_URL_FIELDS'))
	) {
		if (DetectManyURLsAttack($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}
	if (Settings::get('ATTACK_DETECTION_JUNK')) {
		if (DetectJunkAttack($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}

	if (!$b_attacked && !Settings::isEmpty('ATTACK_DETECTION_REVERSE_CAPTCHA')) {
		if (DetectRevCaptchaAttack(Settings::get('ATTACK_DETECTION_REVERSE_CAPTCHA'),$a_fields,$s_attack,$s_info,
		                           $s_user_info)
		) {
			$b_attacked = true;
		}
	}
	if (function_exists('FMHookDetectAttacks')) {
		if (FMHookDetectAttacks($a_fields,$s_attack,$s_info,$s_user_info)) {
			$b_attacked = true;
		}
	}

	if ($b_attacked) {
		if (function_exists('FMHookAttacked')) {
			FMHookAttacked($s_attack);
		}
		if (Settings::get('ALERT_ON_ATTACK_DETECTION')) {
			SendAlert(GetMessage(MSG_ATTACK_DETECTED,
			                     array("ATTACK" => $s_attack,
			                           "INFO"   => $s_info,
			                     )),
			          false);
		}
		if (!IsAjax() && Settings::get('ATTACK_DETECTION_URL') !== "") {
			Redirect(Settings::get('ATTACK_DETECTION_URL'),GetMessage(MSG_FORM_ERROR));
		} else {
			global $SERVER;

			CreatePage(GetMessage(MSG_ATTACK_PAGE,array("SERVER" => $SERVER,"USERINFO" => $s_user_info)),
			           GetMessage(MSG_FORM_ERROR));
		}
		FormMailExit();
	}
}

//
// Implement reverse captcha.  At least two fields must be provided.
// At least one of the fields should have a non-empty value.
//
function DetectRevCaptchaAttack($a_revcap_spec,$a_form_data,&$s_attack,&$s_info,&$s_user_info)
{
	global $bReverseCaptchaCompleted;

	if (count($a_revcap_spec) < 2) {
		SendAlert(GetMessage(MSG_REV_CAP));
		return (false);
	}
	//
	// check the reverse captcha fields
	//
	$n_empty    = $n_non_empty = 0;
	$b_attacked = false;
	$s_info     = "";
	foreach ($a_revcap_spec as $s_fld_name => $s_value) {
		if ($s_value === "") {
			$n_empty++;
			if (isset($a_form_data[$s_fld_name]) &&
			    $a_form_data[$s_fld_name] !== ""
			) {
				$b_attacked = true;
				$s_info     .= "\n" . GetMessage(MSG_ATTACK_REV_CAP_INFO,
				                                 array("FLD"     => $s_fld_name,
				                                       "CONTENT" => $a_form_data[$s_fld_name]
				                                 ),
				                                 false);
			}
		} else {
			$n_non_empty++;
			if (!isset($a_form_data[$s_fld_name]) ||
			    $a_form_data[$s_fld_name] !== $s_value
			) {
				$b_attacked = true;
				$s_info     .= "\n" . GetMessage(MSG_ATTACK_REV_CAP_INFO,
				                                 array("FLD"     => $s_fld_name,
				                                       "CONTENT" =>
					                                       isset($a_form_data[$s_fld_name]) ?
						                                       $a_form_data[$s_fld_name] :
						                                       ""
				                                 ),
				                                 false);
			}
		}
	}
	//
	// check that the rev captcha specification is correct:
	//      at least two fields, at least one empty
	//      and at least one non-empty
	//
	if ($n_empty + $n_non_empty < 2 ||
	    $n_empty == 0 || $n_non_empty == 0
	) {
		SendAlert(GetMessage(MSG_REV_CAP));
		return (false);
	}
	if ($b_attacked) {
		$s_attack    = "Reverse Captcha";
		$s_user_info = GetMessage(MSG_USER_ATTACK_REV_CAP,array(),false);
	}
	$bReverseCaptchaCompleted = !$b_attacked;
	FMDebug('RevCaptcha: ' . $s_info);
	//FMDebug("RevCaptcha done: ".($bReverseCaptchaCompleted ? "success" : "failure"));
	return ($b_attacked);
}

//
// Check whether a form submission is allowed based on any Captcha provided
//
function CheckCaptchaSubmit()
{
	global $SPECIAL_VALUES,$reCaptchaProcessor;

	if ($SPECIAL_VALUES["imgverify"] !== "") {
		//
		// implement reCaptcha
		//
		if (isset($reCaptchaProcessor)) {
			$s_error = '';
			if (!$reCaptchaProcessor->Check($SPECIAL_VALUES["imgverify"],$SPECIAL_VALUES,$s_error)) {
				$s_error_mesg = GetMessage(MSG_RECAPTCHA_MATCH,array("ERR" => $s_error));
				UserError("recaptcha",$s_error_mesg,'',array('imgverify' => $s_error_mesg));
			}
		}
		//
		// implement other CAPTCHA
		//
		else {
			//
			// VerifyImgString is from Tectite's simple verifyimg.php CAPTCHA
			// turing_string is from Captcha Creator
			//
			if (!IsSetSession("VerifyImgString") &&
			    !IsSetSession("turing_string")
			) {
				ErrorWithIgnore("verify_failed",GetMessage(MSG_VERIFY_MISSING),false);
			}
			//
			// the user's entry must match the value in the session; allow
			// spaces in the user's input
			//
			if (IsSetSession("VerifyImgString")) {
				if (strtoupper(str_replace(" ","",$SPECIAL_VALUES["imgverify"])) !==
				    strtoupper(GetSession("VerifyImgString"))
				) {
					$s_error_mesg = GetMessage(MSG_VERIFY_MATCH);
					UserError("img_verify",$s_error_mesg,'',array('imgverify' => $s_error_mesg));
				}
			} else {
				if (strtoupper(str_replace(" ","",$SPECIAL_VALUES["imgverify"])) !==
				    strtoupper(GetSession("turing_string"))
				) {
					$s_error_mesg = GetMessage(MSG_VERIFY_MATCH);
					UserError("img_verify",$s_error_mesg,'',array('imgverify' => $s_error_mesg));
				}
			}
		}
	}
}

/*
 * Class:       AutoResponder
 * Description:     
 *  Implements the auto responding feature of FormMail.
 *  The object must only be created after special fields have been
 *  processed.
 */

class   AutoResponder
{
	var $_bRequested; // true if requested by the form
	var $_sTo; // to-address for auto response
	var $_sSubject; // subject for auto response

	var $_iNone = 0; // must be zero - initializes iType and iCaptchaType

	var $_iCaptchaType; // type of CAPTCHA that's been successfully processed
	var $_bCaptchaOK; // true if CAPTCHA processing is OK, otherwise false

	//
	// values of _iCaptchaType
	//
	var $_iFull = 1; // full captcha
	var $_iRev  = 2; // reverse captcha

	var $_iType; // type of autoresponse (template or plain)

	//
	// values of _iType
	//
	var $_iSendTemplate = 1; // send a template
	var $_iSendPlain    = 2; // send a plain file

	/*
     * Method:      AutoResponder ctor
     * Parameters:  void
     * Returns:     n/a 
     * Description: 
     *  Constructs the object.
     */
	function __construct()
	{
		global $SPECIAL_VALUES;

		$this->_bCaptchaOK   = $this->_bRequested = false;
		$this->_sTo          = "";
		$this->_sSubject     = "";
		$this->_iType        = $this->_iNone;
		$this->_iCaptchaType = $this->_iNone;

		//
		// An autoreponse is sometimes optional in the sense the user
		// can choose whether they want it.
		// It can also be mandatory, and enforced by the form owner.
		// Here are the rules:
		//  1.  If there is no return email address (email field), no autoresponse.
		//      In this case, if autoresponse has been requested by the form, send
		//      an alert message to the form owner, but otherwise ignore the problem
		//      (it's optional).
		//  2.  If no autoresponse has been requested by the form, skip.
		//  3.  If HTMLTemplate or PlainTemplate is set:
		//      3a. If full CAPTCHA has been performed, send autoresponse.
		//      3b  If no full CAPTCHA has been performed, no autoresponse.
		//  4.  If HTMLTemplate and PlainTemplate are *not* set and either
		//      PlainFile or HTMLFile is set:
		//      4a. If full CAPTCHA has been performed, send autoresponse.
		//      4b. If Reverse CAPTCHA has been performed, send autoresponse.
		//      4c. Otherwise, no autoresponse.
		//  5.  If full CAPTCHA is attempted but fails (e.g. wrong input), display
		//      error.
		//
		// The form owner can enforce autoresponse by making "email" required,
		// and making "arverify" or "recaptcha_response_field" required (for templates)
		// or by ensuring Reverse CAPTCHA is performed (for PlainFile or HTMLFile).
		//
		if (IsAROptionSet('HTMLTemplate') ||
		    IsAROptionSet('PlainTemplate')
		) {
			$this->_iType = $this->_iSendTemplate;
		}
		if (IsAROptionSet('PlainFile') ||
		    IsAROptionSet('HTMLFile')
		) {
			$this->_iType = $this->_iSendPlain;
		}
		if ($this->_iType) {
			//
			// the form has requested autoresponse....
			// we need an email address to send to
			//
			if (!isset($SPECIAL_VALUES["email"]) || empty($SPECIAL_VALUES["email"])) {
				SendAlert(GetMessage(MSG_ARESP_EMAIL));
			} else {
				$this->_bRequested = true;
				$this->_sTo        = $SPECIAL_VALUES["email"];
				if (IsAROptionSet('Subject')) {
					$this->_sSubject = GetAROption('Subject');
				} else {
					$this->_sSubject = GetMessage(MSG_ARESP_SUBJ,array(),false);
				}
			}
		}
	}

	/*
     * Method:      AutoResponder::IsRequested
     * Parameters:  void
     * Returns:     bool    true if autoresponding has been requested
     * Description: 
     *  Determines if autoresponding has been requested by the HTML.  
     */
	function IsRequested()
	{
		return ($this->_bRequested);
	}

	/*
     * Method:      AutoResponder::Process
     * Parameters:  $b_check_only   if true, perform checks but do not send
     * Returns:     void
     * Description: 
     *  Processes the autorespond.
     */
	function Process($b_check_only = false)
	{
		global $SPECIAL_VALUES;

		FMDebug("AutoResponder::Process: check=" . ($b_check_only ? "Y" : "N"));
		if ($this->IsRequested()) {
			FMDebug("AutoResponder::Process: requested");
			$b_done = false;
			//
			// You can create a function called SpecialAutoResponderProcess in a hook script.
			// It gets passed the AutoResponder object and the $b_check_only flag.
			// If the function returns true, then the standard process is not performed.
			// Otherwise the standard process continues as-is.
			// Your special function can call any of the public methods of this class including:
			//  StandarProcess
			//  Send
			//
			if (function_exists('SpecialAutoResponderProcess')) {
				$b_done = SpecialAutoResponderProcess($this,$b_check_only);
			}
			if (!$b_done) {
				$this->StandardProcess($b_check_only);
			}
		}
	}

	function StandardProcess($b_check_only)
	{
		//
		// verify CAPTCHA or that Reverse CAPTCHA has been completed
		// (unless we've already done that)
		//
		$this->_CheckCaptcha();
		if (!$b_check_only && $this->_bCaptchaOK) {
			FMDebug("AutoResponder::Process: proceeding, type=" . $this->_iType);
			//
			// for a template, full CAPTCHA must have been processed
			//
			if ($this->_iType == $this->_iSendTemplate) {
				if ($this->_iCaptchaType == $this->_iFull) {
					$this->Send(true);
				}
			}
			//
			// for a plain file, reverse CAPTCHA is sufficient, any CAPTCHA is OK
			//
			elseif ($this->_iType == $this->_iSendPlain) {
				if ($this->_iCaptchaType) {
					$this->Send(false);
				}
			}
		}
	}

	/*
     * Method:      AutoResponder::_CheckCaptcha
     * Parameters:  void
     * Returns:     void 
     * Description: 
     *  Checks the type of CAPTCHA that has been processed.
     *  This method should only be called if autoresponse has been requested
     *  by the form (i.e. IsRequested returns true).
     */
	function _CheckCaptcha()
	{
		global $SPECIAL_VALUES,$bReverseCaptchaCompleted;
		global $reCaptchaProcessor;

		//
		// only check for CAPTCHA once
		//
		if (!$this->_iCaptchaType) {
			//
			// check for full CAPTCHA attempt
			// first, check for reCaptcha
			//
			if (isset($reCaptchaProcessor) && $SPECIAL_VALUES["arverify"] !== "") {
				$this->_iCaptchaType = $this->_iFull;
				$s_error             = '';
				if ($reCaptchaProcessor->Check($SPECIAL_VALUES["arverify"],$SPECIAL_VALUES,$s_error)) {
					$this->_bCaptchaOK = true;
				} else {
					$this->_bCaptchaOK = false;
					//
					// report the error
					//
					WriteARLog($this->_sTo,$this->_sSubject,
					           GetMessage(MSG_LOG_RECAPTCHA,array("ERR" => $s_error),false));
					UserError("recaptcha",GetMessage(MSG_RECAPTCHA_MATCH,array("ERR" => $s_error)));
				}
			}
			//
			// now check for our verifyimg or Captcha Creator
			//
			elseif ($SPECIAL_VALUES["arverify"] !== "") {
				//
				// allow spaces in the user's input, except for reCaptcha
				//
				$s_arverify          = str_replace(" ","",$SPECIAL_VALUES["arverify"]);
				$this->_iCaptchaType = $this->_iFull;
				//
				// full CAPTCHA has been attempted
				// VerifyImgString is from Tectite's simple verifyimg.php CAPTCHA.
				// turing_string is from Captcha Creator
				//
				if (IsSetSession("VerifyImgString") || IsSetSession("turing_string")) {
					$b_match = false;
					//
					// the user's entry must match the value in the session
					//
					if (IsSetSession("VerifyImgString")) {
						if (strtoupper($s_arverify) === strtoupper(GetSession("VerifyImgString"))) {
							$b_match = true;
						}
					} else {
						if (strtoupper($s_arverify) === strtoupper(GetSession("turing_string"))) {
							$b_match = true;
						}
					}
					if ($b_match) {
						$this->_bCaptchaOK = true;
					} else {
						WriteARLog($this->_sTo,$this->_sSubject,
						           GetMessage(MSG_LOG_NO_MATCH,array(),false));
						UserError("ar_verify",GetMessage(MSG_ARESP_NO_MATCH));
					}
				} else {
					//
					// ...and it has failed because there's no session data
					//
					WriteARLog($this->_sTo,$this->_sSubject,
					           GetMessage(MSG_LOG_NO_VERIMG,array(),false));
					ErrorWithIgnore("verify_failed",GetMessage(MSG_ARESP_NO_AUTH),true);
				}
			} elseif (Settings::get('ENABLE_ATTACK_DETECTION') &&
			          !Settings::isEmpty('ATTACK_DETECTION_REVERSE_CAPTCHA')
			) {
				//
				// Reverse CAPTCHA has been configured
				//
				$this->_iCaptchaType = $this->_iRev;
				$this->_bCaptchaOK   = $bReverseCaptchaCompleted;
			}
		}
	}

	/*
     * Method:      AutoResponder::_Send
     * Parameters:  $b_use_template  if true, allow template, otherwise file
     * Returns:     void
     * Description: 
     *  Sends an autoreponse using a template.
     */
	function Send($b_use_template)
	{
		global $SPECIAL_VALUES;
		//
		// declare some global vars for the hook system calls below
		//
		global $aFieldOrder,$aCleanedValues,$aRawDataValues,$aAllRawValues,$aFileVars;

		FMDebug("Sending auto response: " . ($b_use_template ? "template" : "plain"));

		//
		// Hook system: before sending auto response
		//
		if (($incFile = hookFileExists("fmhookprearesp.inc")) !== false) {
			@include($incFile);
		}
		if (!$this->_SendEmail($this->_sTo,$this->_sSubject,$aRawDataValues,$b_use_template)) {
			WriteARLog($this->_sTo,$this->_sSubject,
			           GetMessage(MSG_LOG_FAILED,array(),false));
			SendAlert(GetMessage(MSG_ARESP_FAILED));
		} else {
			WriteARLog($this->_sTo,$this->_sSubject,
			           GetMessage(MSG_LOG_OK,array(),false));
			//
			// Hook system: after sending auto response
			//
			if (($incFile = hookFileExists("fmhookpostaresp.inc")) !== false) {
				@include($incFile);
			}
		}
	}

	/*
     * Method:      AutoResponder::_SendEmail
     * Parameters:  $s_to       the email address to send to
     *              $s_subj     the subject line
     *              $a_values   field values to access
     *              $b_use_template if true, use a template, otherwise plain file
     * Returns:     bool        true on success, otherwise false
     * Description: 
     *  Sends an autoresponse email to the user.
     */
	function _SendEmail($s_to,$s_subj,$a_values,$b_use_template)
	{
		global $ValidEmails;

		$a_headers   = array();
		$s_mail_text = "";
		$s_from_addr = GetAROption("FromAddr");

		if (isset($s_from_addr)) {
			$s_from_addr = UnMangle($s_from_addr);
			if (!$ValidEmails->CheckAddress($s_from_addr)) {
				SendAlert(GetMessage(MSG_INVALID_SENDER,
				                     array("LOC" => "FromAddr in autorespond","EMAIL" => $s_from_addr)));
				unset($s_from_addr);
			}
		}
		if (!isset($s_from_addr)) {
			$s_from_addr = "";
			if (!Settings::isEmpty('FROM_USER')) {
				if (Settings::get('FROM_USER') != "NONE") {
					$s_from_addr = Settings::get('FROM_USER');
				}
			} else {
				global $SERVER;

				$s_from_addr = "FormMail@" . $SERVER;
			}
		}

		if (!empty($s_from_addr)) {
			$s_from_name       = GetAROption('FromName');
			$a_headers['From'] = MakeFromLine($s_from_addr,$s_from_name);
		}
		if (Settings::get('FIXED_SENDER') !== "") {
			$s_sender = Settings::get('FIXED_SENDER');
		} else {
			$s_sender = $s_from_addr;
		}
		$s_type = "";
		if ($b_use_template) {
			if (IsAROptionSet('PlainTemplate')) {
				$s_type     .= "PlainTemplate ";
				$s_template = GetAROption("PlainTemplate");
				if (!ProcessTemplate($s_template,$a_lines,$a_values,
				                     GetAROption('TemplateMissing'),
				                     'SubstituteValuePlain')
				) {
					return (false);
				}
				FMDebug("AutoRespond: PlainTemplate " . count($a_lines) . " lines");
				$s_mail_text = implode(Settings::get('BODY_LF'),$a_lines);
			}
			if (IsAROptionSet("HTMLTemplate")) {
				$s_type .= "HTMLTemplate ";
				if (!MakeMimeMail($s_mail_text,$a_headers,$a_values,
				                  GetAROption("HTMLTemplate"),
				                  GetAROption('TemplateMissing'))
				) {
					return (false);
				}
				FMDebug("AutoRespond: HTMLTemplate " . strlen($s_mail_text) . " bytes");
			}
		} else {

			if (IsAROptionSet('PlainFile')) {
				$s_type .= "PlainFile ";
				//
				// load the plain text file from the templates area
				//
				if (Settings::isEmpty('TEMPLATEDIR') && Settings::isEmpty('TEMPLATEURL')) {
					SendAlert(GetMessage(MSG_TEMPLATES));
					return (false);
				}
				$s_file = GetAROption("PlainFile");
				if (($a_lines = LoadTemplate($s_file,Settings::get('TEMPLATEDIR'),Settings::get('TEMPLATEURL'),
				                             true)) === false
				) {
					return (false);
				}
				$s_mail_text = implode(Settings::get('BODY_LF'),$a_lines);
				FMDebug("AutoRespond: PlainFile " . count($a_lines) . " lines");
			}
			if (IsAROptionSet("HTMLFile")) {
				$s_type .= "HTMLFile ";
				if (!MakeMimeMail($s_mail_text,$a_headers,$a_values,
				                  GetAROption("HTMLFile"),"",
				                  false,"",array(),array(),false)
				) {
					return (false);
				}
				FMDebug("AutoRespond: HTMLTemplate " . strlen($s_mail_text) . " bytes");
			}
		}
		if (strlen($s_mail_text) == 0) {
			SendAlert(GetMessage(MSG_ARESP_EMPTY),array("TYPE" => $s_type));
		}
		FMDebug("AutoRespond: message is " . strlen($s_mail_text) . " bytes");
		return (SendCheckedMail($s_to,$s_subj,$s_mail_text,$s_sender,$a_headers));
	}
}

/* 
 * Class:       SessionAccess
 * Description:     
 *  Implements access to the general PHP session.
 *  This provides a secure way of copy data to and from the
 *  user's PHP session.
 */

class   SessionAccess
{
	var $_aAccessList;

	/*
     * Method:      SessionAccess ctor
     * Parameters:  $a_access_list      list of variables that can be accessed
     * Returns:     n/a
     * Description: 
     *  Constructs the object.
     */
	function __construct($a_access_list)
	{
		$this->_aAccessList = $a_access_list;
	}

	/*
     * Method:      SessionAccess::CopyIn
     * Parameters:  $a_vars     reference to an array of values (keyed on name)
     *              $b_overwrite_empty  if true, the session value will overwrite
     *                          an empty array variable
     * Returns:     int         number of values copied
     * Description: 
     *  Copies in the list of variables from the session to the given array.
     */
	function CopyIn(&$a_vars,$b_overwrite_empty)
	{
		//$s_db = "Session CopyIn:\n";
		$n_copied = 0;
		foreach ($this->_aAccessList as $s_var_name) {
			if (IsSetSession($s_var_name)) {
				if (!isset($a_vars[$s_var_name]) ||
				    ($b_overwrite_empty &&
				     FieldManager::IsEmpty($a_vars[$s_var_name]))
				) {
					$a_vars[$s_var_name] = GetSession($s_var_name);
					//$s_db .= "$s_var_name='".$a_vars[$s_var_name]."'\n";
					$n_copied++;
				}
			}
		}
		//SendAlert($s_db);
		return ($n_copied);
	}

	/*
     * Method:      SessionAccess::CopyOut
     * Parameters:  $a_vars     reference to an array of values (keyed on name)
     *              $a_fields   an array of fields to copy
     * Returns:     int         number of values copied
     * Description: 
     *  Copies the variables from the given array into the session.
     *  The list of fields to copy is specified in _aAccessList.
     *  If $a_fields is provided, it contains a list of fields to copy, which
     *  limits the fields selected from _aAccessList, otherwise all fields
     *  listed in _aAccessList are copied.
     */
	function CopyOut($a_vars,$a_fields = array())
	{
		//$s_db = "Session CopyOut:\n";

		$n_copied = 0;
		foreach ($this->_aAccessList as $s_var_name) {
			if (isset($a_vars[$s_var_name])) {
				if (empty($a_fields) || in_array($s_var_name,$a_fields)) {
					SetSession($s_var_name,$a_vars[$s_var_name]);
					//$s_db .= "$s_var_name='".GetSession($s_var_name)."'\n";
					$n_copied++;
				}
			}
		}
		//SendAlert($s_db);
		return ($n_copied);
	}
}

$SessionAccessor = new SessionAccess(Settings::get('SESSION_ACCESS'));

$bAdvTemplates = false;
if (Settings::get('ADVANCED_TEMPLATES') && (
		!Settings::isEmpty('TEMPLATEDIR')
		|| !Settings::isEmpty('TEMPLATEURL')
		|| !Settings::isEmpty('MULTIFORMDIR')
		|| !Settings::isEmpty('MULTIFORMURL')
	)) {
	$bAdvTemplates = true;
}

//
// Hook system: after configuring Advanced Templates
//
if (($incFile = hookFileExists("fmhookpostadvtemplates.inc")) !== false) {
	@include($incFile);
}

//
// Hook system: before return check and processing
//
if (($incFile = hookFileExists("fmhookprereturnform.inc")) !== false) {
	@include($incFile);
}

if (isset($aGetVars["return"]) && is_numeric($aGetVars["return"])) {
	//
	// if advanced templates are in use, we will need them for
	// performing a multi-page form return
	//
	if ($bAdvTemplates) {
		$FMCTEMPLATE_PROC = true;
		if (!include_once("$MODULEDIR/$FMCOMPUTE")) {
			Error("load_fmcompute",
			      GetMessage(MSG_LOAD_FMCOMPUTE,
			                 array("FILE"  => "$MODULEDIR/$FMCOMPUTE",
			                       "ERROR" => $php_errormsg
			                 )),
			      false,
			      false);
		}
	}
	MultiFormReturn($aGetVars["return"]);
	//
	// Hook system: after multi-page return to form output
	//
	if (($incFile = hookFileExists("fmhookpostreturnform.inc")) !== false) {
		@include($incFile);
	}
	FormMailExit();
}

//
// Hook system: after initialization
//
if (($incFile = hookFileExists("fmhookpostinit.inc")) !== false) {
	@include($incFile);
}

$ExecEnv->checkUploadSize();
$ExecEnv->checkFileUploadSize($aFileVars);
//
// check configuration values for potential security problems
//
CheckConfig();

//
// otherwise, do the real processing of FormMail
//
$aStrippedFormVars = $aAllRawValues = StripGPCArray($aFormVars);

if (Settings::get('ENABLE_ATTACK_DETECTION')) {
	DetectAttacks($aAllRawValues);
}

//
// Copy in configured session variables, overwriting any
// fields that are empty.
//
$SessionAccessor->CopyIn($aAllRawValues,true);
$SessionAccessor->CopyIn($aStrippedFormVars,true);

//
// process the options
//
ProcessMailOptions($aAllRawValues);
ProcessCRMOptions($aAllRawValues);
ProcessAROptions($aAllRawValues);
ProcessFilterOptions($aAllRawValues);

//
// create any derived fields
// NOTE: it's important that derivation occurs before cleaned values
// are created.  If derivation occurred after cleaning or it created
// fields that were assumed to be clean, then an attack could create
// forged email headers using derive_fields.  The code is safe with
// the logic sequence below.
//
$aAllRawValues = CreateDerived($aAllRawValues);

list($aFieldOrder,$aCleanedValues,$aRawDataValues) = ParseInput($aAllRawValues);

FilterFiles($aFileVars);

//
// if we're processing multi-forms, then merge in the raw data from previous
// forms unless this is the first page of the form sequence
//
if (IsSetSession("FormList") && $SPECIAL_VALUES["multi_start"] != 1) {
	list($aFieldOrder,$aCleanedValues,
		$aRawDataValues,$aAllRawValues,$aFileVars) = GetMultiValues(
		GetSession("FormList"),
		GetSession("FormIndex"),
		$aFieldOrder,$aCleanedValues,
		$aRawDataValues,$aAllRawValues,
		$aFileVars);
}

if ($SPECIAL_VALUES["file_names"] !== "") {
	list($aFieldOrder,$aCleanedValues,$aRawDataValues,$aAllRawValues,$aFileVars) =
		SetFileNames($SPECIAL_VALUES["file_names"],$aFieldOrder,
		             $aCleanedValues,$aRawDataValues,$aAllRawValues,$aFileVars);
}

//
// Hook system: after loading and processing data
//
if (($incFile = hookFileExists("fmhookload.inc")) !== false) {
	@include($incFile);
}

if (Settings::get('FORM_INI_FILE') !== "") {
	ProcessFormIniFile(Settings::get('FORM_INI_FILE'));
	//
	// Hook system: after processing INI file
	//
	if (($incFile = hookFileExists("fmhookinifile.inc")) !== false) {
		@include($incFile);
	}
}

$bDoneSomething = false;
if (Settings::get('DB_SEE_INPUT')) {
	/***
	 * echo "<pre>";
	 * print_r($aFormVars);
	 * print_r($aServerVars);
	 * echo "</pre>";
	 * exit;
	 ****/
	CreatePage(implode("\n",$FORMATTED_INPUT),"Debug Output - Fields Submitted");
	ZapSession();
	FormMailExit();
}

if (!empty($SPECIAL_VALUES["fmcompute"]) || $bAdvTemplates) {
	$FM_UserErrors = array();
	//
	// Generalized interface between FMCompute and FormMail functions
	//
	/** @noinspection PhpUnused */
	function FM_CallFunction($s_func,$a_params,&$m_return,
	                         &$s_mesg,&$a_debug,&$a_alerts)
	{
		switch ($s_func) {
			case "FMFatalError":
				SendComputeAlerts();
				if (count($a_params) < 3) {
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					Error("fmcompute_error",$a_params[0],$a_params[1],$a_params[2]);
				}
				break;

			case "FMFatalUserError":
				SendComputeAlerts();
				if (count($a_params) < 1) {
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					UserError("fmcompute_usererror",$a_params[0]);
				}
				break;

			case "FMUserError":
				if (count($a_params) < 1) {
					SendComputeAlerts();
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					global $FM_UserErrors;

					$FM_UserErrors[] = $a_params[0];
				}
				break;

			case "FMSaveAllFilesToRepository":
				if (count($a_params) != 0) {
					SendComputeAlerts();
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					$m_return = SaveAllFilesToRepository();
				}
				break;

			case "FMDeleteFileFromRepository":
				if (count($a_params) != 1) {
					SendComputeAlerts();
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					$m_return = DeleteFileFromRepository($a_params[0]);
				}
				break;

			case "FMNextNum":
				if (count($a_params) != 2) {
					SendComputeAlerts();
					Error("fmcompute_call",GetMessage(MSG_CALL_PARAM_COUNT,
					                                  array("FUNC"  => $s_func,
					                                        "COUNT" => count($a_params)
					                                  )),
					      false,false);
				} else {
					$i_pad  = $a_params[0];
					$i_base = $a_params[1];
					if ($i_base < 2 || $i_base > 36) {
						Error("fmcompute_call",GetMessage(MSG_CALL_INVALID_PARAM,
						                                  array("FUNC"    => $s_func,
						                                        "PARAM"   => 2,
						                                        "CORRECT" => 2. . .36
						                                  )),
						      false,false);
					}
					$m_return = GetNextNum($i_pad,$i_base);
				}
				break;

			default:
				$s_mesg = GetMessage(MSG_CALL_UNK_FUNC,array("FUNC" => $s_func));
				return (false);
		}
		return (true);
	}

	//
	// register useful FormMail functions with FMCompute module
	//
	function RegisterFormMailFunctions($fmc)
	{
		//
		// Allows the user to call "Error" from within a computation
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","void",
		                                             "FMFatalError",
		                                             array("string","bool","bool"),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMFatalError",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}
		//
		// Allows the user to call "UserError" from within a computation
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","void",
		                                             "FMFatalUserError",
		                                             array("string"),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMFatalUserError",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}

		//
		// Allows the user to record an error to be displayed
		// from within a computation.
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","void",
		                                             "FMUserError",
		                                             array("string"),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMUserError",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}

		//
		// Saves files to the repository.
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","bool",
		                                             "FMSaveAllFilesToRepository",
		                                             array(),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMSaveAllFilesToRepository",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}

		//
		// Delete a file from the repository.
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","bool",
		                                             "FMDeleteFileFromRepository",
		                                             array("string"),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMDeleteFileFromRepository",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}

		//
		// Generate a Next Number
		//
		if (($s_msg = $fmc->RegisterExternalFunction("PHP","string",
		                                             "FMNextNum",
		                                             array("int","int"),
		                                             "FM_CallFunction")) !== true
		) {
			Error("fmcompute_reg",
			      GetMessage(MSG_REG_FMCOMPUTE,
			                 array("FUNC"  => "FMNextNum",
			                       "ERROR" => $s_msg
			                 )),
			      false,false);
		}
	}

	//
	// load the fmcompute module
	//
	if (!empty($SPECIAL_VALUES["fmcompute"])) {
		$FMCOMPUTE_CLASS   = true;
		$FMCOMPUTE_NODEBUG = true;
	}
	if ($bAdvTemplates) {
		$FMCTEMPLATE_PROC = true;
	}
	if (!include_once("$MODULEDIR/$FMCOMPUTE")) {
		Error("load_fmcompute",
		      GetMessage(MSG_LOAD_FMCOMPUTE,
		                 array("FILE"  => "$MODULEDIR/$FMCOMPUTE",
		                       "ERROR" => $php_errormsg
		                 )),
		      false,false);
	}
	if (!empty($SPECIAL_VALUES["fmcompute"])) {
		/** @noinspection PhpUndefinedVariableInspection */
		RegisterFormMailFunctions($FMCalc);

		//
		// if GeoIP support is specified, load that module now
		//
		if (!Settings::isEmpty('GEOIP_LIC')) {
			$FMMODULE_LOAD = true; // signal module load
			if (!include_once("$MODULEDIR/$FMGEOIP")) {
				Error("load_module",
				      GetMessage(MSG_LOAD_MODULE,
				                 array("FILE"  => "$MODULEDIR/$FMGEOIP",
				                       "ERROR" => $php_errormsg
				                 )),
				      false,false);
			}
			//
			// load the license and register the module
			//
			/** @noinspection PhpUndefinedClassInspection */
			$GeoIP = new FMGeoIP(Settings::get('GEOIP_LIC'));
			/** @noinspection PhpUndefinedMethodInspection */
			if (!$GeoIP->RegisterModule($FMCalc)) {
				/** @noinspection PhpUndefinedMethodInspection */
				Error("reg_module",
				      GetMessage(MSG_REGISTER_MODULE,
				                 array("NAME"  => "FMGeoIP",
				                       "ERROR" => $GeoIP->GetError()
				                 )),
				      false,false);
			}
		}
	}
}

if (isset($SPECIAL_VALUES["multi_go_back"]) && !empty($SPECIAL_VALUES["multi_go_back"])) {
	if (!IsSetSession("FormList") || GetSession("FormIndex") == 0) {
		ErrorWithIgnore("go_back",GetMessage(MSG_GO_BACK),false,false);
	}
	MultiKeep(); // store any "multi_keep" data just submitted
	//
	// save back to the session any variables that have been specified in
	// multi_keep
	//
	if (isset($SPECIAL_VALUES["multi_keep"]) && !empty($SPECIAL_VALUES["multi_keep"])) {
		$SessionAccessor->CopyOut($aAllRawValues,$SPECIAL_VALUES["multi_keep"]);
	}
	MultiFormReturn(GetSession("FormIndex") - 1);
	//    echo "Form index = ".GetSession("FormIndex");
	//
	// Hook system: after multi-page return to form output
	//
	if (($incFile = hookFileExists("fmhookpostreturnform.inc")) !== false) {
		@include($incFile);
	}
	FormMailExit();
}

//
// This is the check for spiders; I can't imagine a spider will
// ever use the POST method.
//
if ($bIsGetMethod && count($aFormVars) == 0) {
	if (!Settings::get('ALLOW_GET_METHOD') && $bHasGetData) {
		CreatePage(GetMessage(MSG_GET_DISALLOWED),GetMessage(MSG_FORM_ERROR));
	} else {
		CreatePage(GetMessage(MSG_NO_DATA_PAGE),GetMessage(MSG_FORM_ERROR));
	}
	ZapSession();
	FormMailExit();
}

//
// Hook system: before performing required and conditions etc.
//
if (($incFile = hookFileExists("fmhookprechecks.inc")) !== false) {
	@include($incFile);
}

//
// check for required fields
//
if (!CheckRequired($SPECIAL_VALUES["required"],$aAllRawValues,$sMissing,$aMissingList)) {
	UserError("missing_fields",GetMessage(MSG_REQD_ERROR),$sMissing,$aMissingList);
}

//
// check complex conditions
//
$fmConditions = new Conditions($SPECIAL_VALUES["conditions"],$sMissing,$aMissingList);
if (!$fmConditions->Check($aAllRawValues,$aFileVars)) {
	UserError("failed_conditions",GetMessage(MSG_COND_ERROR),$sMissing,$aMissingList);
}

//
// check CAPTCHA
//
CheckCaptchaSubmit();

//
// Hook system: after performing required and conditions etc.
//
if (($incFile = hookFileExists("fmhookchecks.inc")) !== false) {
	@include($incFile);
}

if (!empty($SPECIAL_VALUES["fmmodules"])) {
	$aModuleList   = TrimArray(explode(",",$SPECIAL_VALUES["fmmodules"]));
	$FMMODULE_LOAD = true; // signal module load
	foreach ($aModuleList as $sModule) {
		if (!include_once("$MODULEDIR/$sModule")) {
			Error("load_module",
			      GetMessage(MSG_LOAD_MODULE,
			                 array("FILE"  => "$MODULEDIR/$sModule",
			                       "ERROR" => $php_errormsg
			                 )),
			      false,false);
		}
	}
}

if (!empty($SPECIAL_VALUES["fmcompute"])) {
	//
	// Callback function for preg_replace_callback to add line
	// numbers on each match
	//
	function AddLineNumbersCallback($a_matches)
	{
		global $iAddLineNumbersCounter;

		return (sprintf("%d:",++$iAddLineNumbersCounter) . $a_matches[0]);
	}

	//
	// Add line numbers to some code
	//
	function AddLineNumbers($s_code)
	{
		global $iAddLineNumbersCounter;

		$iAddLineNumbersCounter = 0;
		return (preg_replace_callback('/^/m','AddLineNumbersCallback',$s_code));
	}

	//
	// Load some more code into FMCalc
	//
	function Load($s_code)
	{
		global $FMCalc;

		$a_mesgs = array();
		// echo "Loading '$s_code'";
		if ($FMCalc->Parse($s_code,$a_mesgs) === false) {
			$s_msgs = "";
			foreach ($a_mesgs as $a_msg) {
				$s_msgs .= "Line " . $a_msg["LINE"];
				$s_msgs .= ", position " . $a_msg["CHAR"] . ": ";
				$s_msgs .= $a_msg["MSG"] . "\n";
			}
			Error("fmcompute_parse",
			      GetMessage(MSG_COMP_PARSE,
			                 array("CODE"   => AddLineNumbers($s_code),
			                       "ERRORS" => $s_msgs
			                 )),
			      false,
			      false);
		}
	}

	//
	// Send any alerts found in FMCalc
	//
	function SendComputeAlerts()
	{
		global $FMCalc;

		$a_alerts = $FMCalc->GetAlerts();
		if (count($a_alerts) > 0) {
			SendAlert(GetMessage(MSG_COMP_ALERT,
			                     array("ALERTS" => implode("\n",StripHTML($a_alerts)))));
		}
		$a_debug = $FMCalc->GetDebug();
		if (count($a_debug) > 0) {
			SendAlert(GetMessage(MSG_COMP_DEBUG,
			                     array("DEBUG" => implode("\n",StripHTML($a_debug)))));
		}
	}

	//
	// Perform the computations in FMCalc
	//
	function Compute(&$a_field_order,&$a_cleaned_values,&$a_raw_data_values,
	                 &$a_values)
	{
		global $FMCalc,$FM_UserErrors;

		$a_mesgs       = array();
		$FM_UserErrors = array();
		if (($a_flds = $FMCalc->Execute($a_mesgs)) !== false) {
			SendComputeAlerts();
			foreach ($a_flds as $s_name => $s_value) {
				$a_values[$s_name] = $s_value;
				ProcessField($s_name,$s_value,$a_field_order,
				             $a_cleaned_values,$a_raw_data_values);
			}
			if (count($FM_UserErrors) > 0) {
				UserError("fmcompute_usererrors",GetMessage(MSG_USER_ERRORS),
				          "",$FM_UserErrors);
			}
		} else {
			SendComputeAlerts();
			Error("fmcompute_exec",GetMessage(MSG_COMP_EXEC,
			                                  array("ERRORS" => implode("\n",$a_mesgs))),false,false);
		}
	}

	//
	// Register all fields; a future improvement should use a call-back
	// function so as not to copy all the data into the FMCompute object
	//
	function RegisterData($a_form_data,$a_file_vars)
	{
		global $FMCalc;

		foreach ($a_form_data as $s_name => $s_value) {
			if (isset($s_name) && isset($s_value)) {
				if (($s_msg = $FMCalc->RegisterExternalData("PHP","string",
				                                            $s_name,"c",$s_value)) !== true
				) {
					Error("fmcompute_regdata",GetMessage(MSG_COMP_REG_DATA,
					                                     array("NAME" => $s_name,"ERROR" => $s_msg)),false,false);
				}
			}
		}

		foreach ($a_file_vars as $s_fld_name => $a_file_spec) {
			if (FieldManager::IsUploadedFile($a_file_spec)) {
				if (isset($a_file_spec["new_name"])) {
					//
					// we ignore errors here, because name_of_ field often already
					// exists from the above loop
					//
					$FMCalc->RegisterExternalData("PHP","string",
					                              "name_of_" . $s_fld_name,"c",$a_file_spec["new_name"]);
				}
				$s_value = $a_file_spec["name"];
			} else {
				$s_value = "";
			}
			if (($s_msg = $FMCalc->RegisterExternalData("PHP","string",
			                                            $s_fld_name,"c",$s_value)) !== true
			) {
				Error("fmcompute_regdata",GetMessage(MSG_COMP_REG_DATA,
				                                     array("NAME" => $s_fld_name,"ERROR" => $s_msg)),false,false);
			}
		}
	}

	/* 
     * Function:    MergeFileArrays
     * Parameters:  $a_new_files    the list of files just submitted by the form
     *              $a_saved_files  the list of files that have been previously saved (can be NULL)
     * Returns:     array           a merged array
     * Description:     
     *  Intelligently merges two arrays of file definitions.
     *  If a file has been newly uploaded, its definition takes precedence.
     */
	function MergeFileArrays($a_new_files,$a_saved_files)
	{
		if (isset($a_saved_files)) {
			foreach ($a_saved_files as $s_key => $a_def) {
				if (isset($a_new_files[$s_key])) {
					if (!FieldManager::IsUploadedFile($a_new_files[$s_key])) {
						$a_new_files[$s_key] = $a_def;
					}
				} else {
					$a_new_files[$s_key] = $a_def;
				}
			}
		}
		return ($a_new_files);
	}

	RegisterData($aAllRawValues,MergeFileArrays($aFileVars,IsSetSession("FormSavedFiles") ?
		GetSession("FormSavedFiles") : array()));
	//
	// parse all fmcompute fields
	//
	if (is_array($SPECIAL_VALUES["fmcompute"])) {
		$nCompute = count($SPECIAL_VALUES["fmcompute"]);
		for ($iCompute = 0 ; $iCompute < $nCompute ; $iCompute++) {
			Load($SPECIAL_VALUES["fmcompute"][$iCompute]);
		}
	} else {
		Load($SPECIAL_VALUES["fmcompute"]);
	}

	//
	// run computations
	//
	Compute($aFieldOrder,$aCleanedValues,$aRawDataValues,$aAllRawValues);
	//
	// Hook system: after computations
	//
	if (($incFile = hookFileExists("fmhookcompute.inc")) !== false) {
		@include($incFile);
	}
}

$bGotGoBack = $bGotNextForm = $bGotGoodTemplate = $bGotGoodUrl = false;
if (isset($SPECIAL_VALUES["good_url"]) &&
    !empty($SPECIAL_VALUES["good_url"])
) {
	$bGotGoodUrl = true;
}

if (isset($SPECIAL_VALUES["good_template"]) &&
    !empty($SPECIAL_VALUES["good_template"])
) {
	$bGotGoodTemplate = true;
}

if (isset($SPECIAL_VALUES["next_form"]) &&
    !empty($SPECIAL_VALUES["next_form"])
) {
	$bGotNextForm = true;
}

if (isset($SPECIAL_VALUES["multi_go_back"]) &&
    !empty($SPECIAL_VALUES["multi_go_back"])
) {
	$bGotGoBack = true;
}

//
// it's not valid to specify "next_form" as well as "good_url" or
// "good_template"; this is because it's ambiguous - do we go to the
// next form or the final 'thank you' page?
//
if ($bGotNextForm && ($bGotGoodTemplate || $bGotGoodUrl)) {
	ErrorWithIgnore("next_plus_good",GetMessage(MSG_NEXT_PLUS_GOOD,array("WHICH" =>
		                                                                     ($bGotGoodUrl ? "good_url" :
			                                                                     "good_template")
	)),             false,false);
}

MultiFormLogic();

//
// Hook system: after multi-page form logic
//
if (($incFile = hookFileExists("fmhookmulti.inc")) !== false) {
	@include($incFile);
}

//
// write to the CSV database
//
if (!Settings::isEmpty('CSVDIR') && isset($SPECIAL_VALUES["csvfile"]) &&
    !empty($SPECIAL_VALUES["csvfile"])
) {
	//
	// Hook system: before writing CSV file
	//
	if (($incFile = hookFileExists("fmhookprecsv.inc")) !== false) {
		@include($incFile);
	}
	WriteCSVFile(Settings::get('CSVDIR') . "/" . basename($SPECIAL_VALUES["csvfile"]),$aAllRawValues);
	//
	// Hook system: after writing CSV file
	//
	if (($incFile = hookFileExists("fmhookpostcsv.inc")) !== false) {
		@include($incFile);
	}
	$bDoneSomething = true;
}

//
// write to the log file
//
if (!Settings::isEmpty('LOGDIR') && isset($SPECIAL_VALUES["logfile"]) && !empty($SPECIAL_VALUES["logfile"])) {
	//
	// Hook system: before writing log file
	//
	if (($incFile = hookFileExists("fmhookprelog.inc")) !== false) {
		@include($incFile);
	}
	WriteLog(Settings::get('LOGDIR') . "/" . basename($SPECIAL_VALUES["logfile"]));
	//
	// Hook system: after writing log file
	//
	if (($incFile = hookFileExists("fmhookpostlog.inc")) !== false) {
		@include($incFile);
	}
	$bDoneSomething = true;
}

//
// send to the CRM
//
if (isset($SPECIAL_VALUES["crm_url"]) && isset($SPECIAL_VALUES["crm_spec"]) &&
    !empty($SPECIAL_VALUES["crm_url"]) && !empty($SPECIAL_VALUES["crm_spec"])
) {
	$sCRM = GetCRMURL($SPECIAL_VALUES["crm_spec"],$aAllRawValues,$SPECIAL_VALUES["crm_url"]);
	if (!empty($sCRM)) {
		$aCRMReturnData = array();
		//
		// Hook system: before sending to CRM
		//
		if (($incFile = hookFileExists("fmhookprecrm.inc")) !== false) {
			@include($incFile);
		}
		if (!SendToCRM($sCRM,$aCRMReturnData)) {
			//
			// CRM interface failed, check if the form wants an error
			// displayed
			//
			if (IsCRMOptionSet("ErrorOnFail")) {
				Error("crm_failed",GetMessage(MSG_CRM_FAILURE,
				                              array("URL"  => $sCRM,
				                                    "DATA" => GetObjectAsString($aCRMReturnData)
				                              )),
				      false,false);
			}
		} else
			//
			// append the returned data to the raw data values of the form
			//
		{
			$aRawDataValues = array_merge($aRawDataValues,$aCRMReturnData);
		}
		//
		// Hook system: after sending to CRM
		//
		if (($incFile = hookFileExists("fmhookpostcrm.inc")) !== false) {
			@include($incFile);
		}
		$bDoneSomething = true;
	}
}

//
// Check obsolete SendMailFOption
//
if (IsMailOptionSet("SendMailFOption")) {
	SendAlert(GetMessage(MSG_FOPTION_WARN,array("LINE" => Settings::get('SENDMAIL_F_OPTION_LINE'))),
	          false,true);
}

$AutoResp = new AutoResponder();
//
// check for autoresponse problems
//
$AutoResp->Process(true);

//
// Hook system: before completion
//
if (($incFile = hookFileExists("fmhookprecomplete.inc")) !== false) {
	@include($incFile);
}

//
// send email
//
if (!isset($SPECIAL_VALUES["recipients"]) || empty($SPECIAL_VALUES["recipients"])) {
	//
	// No recipients - don't email anyone...
	// If nothing has been done above (CSV, logging, or CRM),
	// then report an error.
	//
	if (!$bDoneSomething) {
		if (!$bGotGoBack && !$bGotNextForm) {
			ErrorWithIgnore("no_recipients",GetMessage(MSG_NO_ACTIONS));
		}
	}
} else {
	//
	// Hook system: before sending emails
	//
	if (($incFile = hookFileExists("fmhookpreemail.inc")) !== false) {
		@include($incFile);
	}
	$s_invalid = $s_invalid_cc = $s_invalid_bcc = "";
	if (!CheckEmailAddress($SPECIAL_VALUES["recipients"],$s_valid_recipients,$s_invalid)) {
		ErrorWithIgnore("no_valid_recipients",GetMessage(MSG_NO_RECIP));
	} else {
		$s_valid_cc = $s_valid_bcc = "";

		//
		// check CC and BCC addresses
		//
		if (isset($SPECIAL_VALUES["cc"]) && !empty($SPECIAL_VALUES["cc"])) {
			CheckEmailAddress($SPECIAL_VALUES["cc"],$s_valid_cc,$s_invalid_cc);
		}
		if (isset($SPECIAL_VALUES["bcc"]) && !empty($SPECIAL_VALUES["bcc"])) {
			CheckEmailAddress($SPECIAL_VALUES["bcc"],$s_valid_bcc,$s_invalid_bcc);
		}

		//
		// send an alert for invalid addresses
		//
		$s_error = "";
		if (!empty($s_invalid)) {
			$s_error .= "recipients: $s_invalid\r\n";
		}
		if (!empty($s_invalid_cc)) {
			$s_error .= "cc: $s_invalid_cc\r\n";
		}
		if (!empty($s_invalid_bcc)) {
			$s_error .= "bcc: $s_invalid_bcc\r\n";
		}
		if (!empty($s_error)) {
			SendAlert(GetMessage(MSG_INV_EMAIL,array("ERRORS" => $s_error)));
		}

		//
		// send the actual results
		//
		if (!SendResults($aFieldOrder,$aCleanedValues,$s_valid_recipients,$s_valid_cc,
		                 $s_valid_bcc,$aRawDataValues)
		) {
			Error("mail_failed",GetMessage(MSG_FAILED_SEND));
		}
		//
		// Hook system: after sending emails
		//
		if (($incFile = hookFileExists("fmhookpostemail.inc")) !== false) {
			@include($incFile);
		}
	}
}

//
// Process autoresponse
//
$AutoResp->Process();

$SessionAccessor->CopyOut($aAllRawValues);
//
// multi-form processing
//
if ($bGotNextForm) {
	OutputMultiFormTemplate($SPECIAL_VALUES["next_form"],$aRawDataValues);
	//    echo "Form index = ".GetSession("FormIndex");
	//
	// Hook system: after multi-page next form output
	//
	if (($incFile = hookFileExists("fmhookpostnextform.inc")) !== false) {
		@include($incFile);
	}
} else {
	//
	// Hook system: before finishing
	//
	if (($incFile = hookFileExists("fmhookprefinish.inc")) !== false) {
		@include($incFile);
	}
	//
	// redirect to the good URL page, or create a default page;
	// we're no longer processing a multi-page form sequence
	//
	UnsetSession("FormList");
	UnsetSession("FormIndex");
	UnsetSession("FormKeep");
	if (!$bGotGoodUrl) {
		if (IsAjax()) {
			JSON_Result("OK");
		} else {
			if ($bGotGoodTemplate) {
				OutputTemplate($SPECIAL_VALUES["good_template"],$aRawDataValues);
			} else {
				CreatePage(GetMessage(MSG_THANKS_PAGE),GetMessage(MSG_FORM_OK));
			}
		}
		//
		// Hook system: after finishing (before session is cleared)
		//
		if (($incFile = hookFileExists("fmhookpostfinish.inc")) !== false) {
			@include($incFile);
		}
		//
		// everything's good, so we don't need the session any more
		//
		ZapSession();
	} elseif (IsAjax()) {
		JSON_Result("OK");
	} else

		//
		// Note that a redirect terminates the script and prevents
		// the processing of fmhookpostfinish.inc.  Also, the session
		// is left intact (which the good_url can use).
		//
	{
		Redirect($SPECIAL_VALUES["good_url"],GetMessage(MSG_FORM_OK));
	}
}
