<?PHP
/**
 * @file StdLib.php
 * Standard library functions for CWIS.
 */

/**
* Determine if the user is logged in.
* @return bool TRUE if the user is logged in and FALSE otherwise
* @deprecated
*/
function UserIsLoggedIn()
{
    return $GLOBALS["G_User"]->IsLoggedIn();
}

/**
 * Determine if a local function exists as defined in an SPT--Local.php file.
 * @param string $FunctionBaseName Base name of function to look for.
 * @return bool TRUE if the local function exists or FALSE otherwise
 */
function LocalFunctionExists($FunctionBaseName)
{
    # try to load SPT--Local.php from its current location
    if (file_exists("local/misc/SPT--Local.php"))
    {
        require_once "local/misc/SPT--Local.php";
    }

    # try to load SPT--Local.php from its old location
    else if (file_exists("include/SPT--Local.php"))
    {
        require_once "include/SPT--Local.php";
    }

    return function_exists("Local_".$FunctionBaseName);
}

/**
* Get the name of the directory of the current theme.
* @return string the name of the directory of the current theme
* @deprecated
*/
function GetCurrentThemeDir()
{
    # return configured theme
    return $GLOBALS["SysConfig"]->CurrentTheme();
}

/**
 * Check whether the user is authorized to view the current page. If the current
 * user does not have at least one of the specified privileges, then a hook is
 * set to cause the "Unauthorized Access" HTML file to display instead of the
 * normal HTML file.
 * @param mixed $AuthFlag Privilege required (or array of possible privileges).
 * @return TRUE if user has one of the specified privileges, otherwise FALSE.
 * @see CheckAuthorization_SignalHook()
*/
function CheckAuthorization($AuthFlag = NULL)
{
    $Privileges = is_array($AuthFlag) ? $AuthFlag : func_get_args();

    # if the user is logged in and no privileges were given or the user has at
    # least one of the specified privileges
    if ($GLOBALS["G_User"]->IsLoggedIn()
        && ((is_null($AuthFlag)) || $GLOBALS["G_User"]->HasPriv($Privileges)))
    {
        return TRUE;
    }

    # the user is not logged in or doesn't have at least one of the specified
    # privileges
    else
    {
        DisplayUnauthorizedAccessPage();
        return FALSE;
    }
}

/**
* Display "Unauthorized Access" HTML file.  This method is intended to be called
* from within a PHP "page" file when a page has been reached which the current
* user does not have the required privileges to access.
*/
function DisplayUnauthorizedAccessPage()
{
    $GLOBALS["AF"]->HookEvent(
            "EVENT_HTML_FILE_LOAD", "DisplayUnauthorizedAccessPage_SignalHook");
}

/**
 * Hook used to display the "Unauthorized Access" page in conjunction with the
 * DisplayUnauthorizedAccessPage() function.
 * @param string $PageName Page name
 * @return array modified hook parameters
 * @see DisplayUnauthorizedAccessPage()
 */
function DisplayUnauthorizedAccessPage_SignalHook($PageName)
{
    return array("PageName" => "UnauthorizedAccess");
}

/**
 * Print all error messages within a <ul>, if any.
 * @return void
 */
function PrintErrorMessageList()
{
    # make sure error messages are set before attempting to access them
    if (isset($GLOBALS["ErrorMessages"]))
    {
        $Errors = $GLOBALS["ErrorMessages"];

        # if dealing with an error list and it has error messages
        if ($Errors instanceof ErrorList && $Errors->HasErrors())
        {
            print $Errors->GetMessagesAsUList();
        }
    }
}

/**
 * Get or set the title of page as displayed in the title bar of a user's
 * web browser.
 * @param string|null $NewTitle The new page title or NULL to leave it as-is
 * @param bool $IncludePortalName TRUE to automatically prepend the portal name
 * @return string the new page title, including the portal name if applicable
 */
function PageTitle($NewTitle = NULL, $IncludePortalName = TRUE)
{
    static $Title;

    # save a new page title if given one
    if (!is_null($NewTitle))
    {
        $Title = $NewTitle;
    }

    # the portal name should be prepended before returning the title...
    if ($IncludePortalName && strlen($GLOBALS["SysConfig"]->PortalName()))
    {
        return $GLOBALS["SysConfig"]->PortalName() . " - " . $Title;
    }

    # ... otherwise just return the page title
    return $Title;
}

/**
 * Get the path to the interface directory that contains the fast user rating
 * images/icons, if any.
 * @return string the path to the interface containing the rating stars
 */
function GetFastRatingInterfaceDirectory()
{
    if (preg_match('/(.*)\/images\/BigStars--0_0\.gif$/',
        $GLOBALS["AF"]->GUIFile("BigStars--0_0.gif"), $Matches))
    {
        return $GLOBALS['AF']->ActiveUserInterface();
    }
    else
    {
        return "default";
    }

}

/**
 * Get a string of options for the possible user interfaces.
 * @param string $Selected The user interface that's currently selected
 * @return string options (HTML) for the possible user interfaces
 */
function GetActiveUIOptionList($Selected)
{
    # start out with an empty list
    $Text = "";

    foreach ($GLOBALS["AF"]->GetUserInterfaces() as $CanonicalName => $Label)
    {
        # add the beginning of the opening tag and value
        $Text .= '<option value="'.defaulthtmlentities($CanonicalName).'"';

        # add a selected attribute if the interface is the selected one
        if ($CanonicalName == $Selected)
        {
            $Text .= ' selected="selected"';
        }

        # close off the beginning tag, add the label, and add the closing tag
        $Text .= '>'.defaulthtmlentities($Label).'</option>';
    }

    # return list to caller
    return $Text;
}

/**
* Determine whether the current page is part of the metadata tool.
* @return bool TRUE if the page is part of the metadata tool or FALSE otherwise
* @deprecated
*/
function InMetadataTool()
{
    if ((strstr($_SERVER["SCRIPT_NAME"], "/MetadataTool/")
                    && (strstr($_SERVER["SCRIPT_NAME"],
                     "SPT--ImportData") == FALSE)
                    && (strstr($_SERVER["SCRIPT_NAME"],
                     "SPT--EditControlledNameTypes") == FALSE))
            || (strstr($_SERVER["SCRIPT_NAME"],
                    "SPT--BrowseResources.php") && $GLOBALS["Editing"])
            || (strstr($_SERVER["SCRIPT_NAME"],
                    "SPT--Advanced.php") && $GLOBALS["Editing"])
            || (strstr($_SERVER["SCRIPT_NAME"],
                    "SPT--AdvancedSearch.php") && $GLOBALS["Editing"]))
    {
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

/**
 * Add a closing tag to the given text if at least one is missing.
 * @param string $Tag HTML tag to check for
 * @param string $Text HTML to inspect
 * @return string the text with the tag closed if it was necessary
 */
function CloseTags($Tag, $Text)
{
    # if there are a greater number of opening tags than closing tags
    if (preg_match_all("/<".$Tag.">/i", $Text, $Dummy) >
        preg_match_all("/<\\/".$Tag.">/i", $Text, $Dummy))
    {
        # add closing tag to text
        $Text .= "</".$Tag.">";
    }

    # return (possibly) revised text to caller
    return $Text;
}

/**
 * Translate special characters within the value to HTML entities.
 * @param string $Value Value to translate
 * @return string The value with special characters translated to HTML entities
 */
function GetHtmlTranslatedString($Value)
{
    return defaulthtmlspecialchars($Value);
}

/**
 * Debugging output utility function. This should not be used in production
 * code.
 * @param string $VarName The variable name
 * @param mixed $VarValue The value of the variable
 * @see var_dump()
 */
function PrintForDebug($VarName, $VarValue)
{
    # print the variable name
    print "<pre>" . $VarName . ": ";

    # print a newline if not printing a scalar value
    if (!is_scalar($VarValue))
    {
        print "\n";
    }

    # use PHP's built-in variable dumping function if available
    if (function_exists("var_dump"))
    {
        ini_set('xdebug.var_display_max_depth', 5);
        ini_set('xdebug.var_display_max_children', 256);
        ini_set('xdebug.var_display_max_data', 1024);
        var_dump($VarValue);
    }

    # otherwise use the next best thing
    else
    {
        print_r($VarValue);
    }

    # print the closing tag
    print "</pre>\n";
}

/**
 * Return the HTTP protocol that is currently in use.
 * @return string the HTTP protocol currently in use ("http:" or "https:")
 * @see OurUrl()
 * @see OurBaseUrl()
 * @see OurBasePath()
 */
function OurHttpProtocol()
{
    return isset($_SERVER["HTTPS"]) ? "https:" : "http:";
}

/**
 * Get the URL to the current file being executed.
 * @return string the URL to the current file being executed
 * @see OurHttpProtocol()
 * @see OurBaseUrl()
 * @see OurBasePath()
 */
function OurUrl()
{
    $Protocol = OurHttpProtocol() . "//";
    $Url = $Protocol.(($_SERVER["SERVER_NAME"] != "127.0.0.1")
                ? $_SERVER["SERVER_NAME"]
                : $_SERVER["HTTP_HOST"])
            .$_SERVER["SCRIPT_NAME"];
    return $Url;
}

/**
 * Get the URL to the root of the site with a trailing slash.
 * @return string the URL to the root of the site with a trailing slash
 * @see OurHttpProtocol()
 * @see OurUrl()
 * @see OurBasePath()
 */
function OurBaseUrl()
{
    $Protocol = OurHttpProtocol() . "//";
    $BaseUrl = $Protocol.(($_SERVER["SERVER_NAME"] != "127.0.0.1")
                ? $_SERVER["SERVER_NAME"]
                : $_SERVER["HTTP_HOST"])
            .dirname($_SERVER["SCRIPT_NAME"]);
    if (substr($BaseUrl, -1) != "/")
    {
        $BaseUrl .= "/";
    }
    return $BaseUrl;
}

/**
 * Get the path part of the URL to the root of the site with a trailing slash.
 * @return string the path part of the URL to the root of the site
 * @see OurHttpProtocol()
 * @see OurUrl()
 * @see OurBaseUrl()
 */
function OurBasePath()
{
    $BasePath = dirname($_SERVER["SCRIPT_NAME"]);

    if (substr($BasePath, -1) != "/")
    {
        $BasePath .= "/";
    }

    return $BasePath;
}

/**
 * Determine whether the given URL is safe to redirect to, i.e., if a protocol
 * and host are specified, make sure the host is the same as the server's host.
 * This is meant to protect from malicious redirects
 * @param string $Url URL to check
 * @return bool TRUE if the URL is safe and FALSE otherwise
 */
function IsSafeRedirectUrl($Url)
{
    $ParsedUrl = parse_url($Url);
    $Protocol = GetArrayValue($ParsedUrl, "scheme");
    $Host = GetArrayValue($ParsedUrl, "host");

    # if a protocol and host are specified, make sure the host is equal to the
    # server's host to protect from malicious redirects
    return !$Protocol || $Host == $_SERVER["SERVER_NAME"];
}

/**
 * Attempt to truncate a string as neatly as possible with respect to word
 * breaks, punctuation, and HTML tags.
 * @param string $String String to truncate
 * @param int $MaxLength The maximum length of the truncated string
 * @param bool $BreakAnywhere TRUE to break exactly at the maximum length
 * @return the (possibly) truncated string
 */
function NeatlyTruncateString($String, $MaxLength, $BreakAnywhere=FALSE)
{
    $strlen = (strcasecmp($GLOBALS["SysConfig"]->DefaultCharacterSet(), "UTF-8") === 0)
        ? "mb_strlen" : "strlen";
    $substr = (strcasecmp($GLOBALS["SysConfig"]->DefaultCharacterSet(), "UTF-8") === 0)
        ? "mb_substr" : "substr";

    # if the string is too long
    if ($MaxLength < $strlen($String))
    {
        # iterate through the string one char at a time
        #    * counting printing characters (eg, the ones not inside HTML tags)
        #    * marking location of the last space or punct character where
        #        the string could be neatly broken
        $CurLen=0;            # number of printing characters seen so far
        $Cur=0;               # current position in the string
        $LastBreakpoint=NULL; # most recently seen breaking place
        $TagStack = array();  # tags that need closing
        $BeginsWithUrl = $substr(strip_tags($String), 0, 7) == "http://" ||
                $substr(strip_tags($String), 0, 8) == "https://";

        $StringLength = $strlen($String);
        while ($CurLen < $MaxLength && $Cur < $StringLength)
        {
            if ($substr($String, $Cur, 1) == "<")
            {
                # eat the contents of tags
                while ($substr($String, $Cur, 1) != ">" && $Cur < $StringLength)
                {
                    $Cur++;
                }
                # ends of tags are good breakpoints
                $LastBreakpoint = $Cur;
            }
            else
            {
                # otherwise, we are just out in printed text land
                # look for point where we can break the string
                # and store the last occurrence of one
                if (strstr(",;: ", $substr($String, $Cur, 1)) !== FALSE ||
                    $BreakAnywhere || $BeginsWithUrl)
                {
                    $LastBreakpoint = $Cur;
                }
                $CurLen++;
            }
            $Cur++;
        }

        # now we've reached the end of the portion of the string we
        # will display, but we'll continue to the end of the string
        # and track which closing tags were skipped so that they can
        # still be output
        while ($Cur < $StringLength)
        {
            if ($substr($String, $Cur, 1) == "<")
            {
                # if we were not in a tag, but one starts here
                $ThisTag = "";

                # eat the start char and leading spaces
                while (strstr("< ", $substr($String, $Cur, 1)) !== FALSE &&
                       $Cur < $StringLength)
                {
                    $Cur++;
                }

                # eat the tag up to the first space or tag-end
                while (strstr(" >", $substr($String, $Cur, 1)) === FALSE &&
                       $Cur < $StringLength)
                {
                    $ThisTag .= $substr($String, $Cur, 1);
                    $Cur++;
                }

                # if the tag found was an end tag, add it to our list
                if ($strlen($ThisTag) && $substr($ThisTag, 0, 1) == "/")
                {
                    array_push($TagStack, $ThisTag);
                }
            }
            else
            {
                $Cur++;
            }
        }

        # if we found a good breakpoint, chop the string there
        #   otherwise, chop at max length
        $String = $substr(
            $String, 0, ($LastBreakpoint === NULL ? $MaxLength : $LastBreakpoint) );

        # close all the open tags
        while (count($TagStack)>0)
        {
            $String .= "<".array_pop($TagStack).">";
        }

        # And tack on the ellipsis
        $String .= "...";
    }

    return $String;
}

/**
 * Get the value from an array with the given index or a default value if it
 * does not exist.
 * @param array $Array Array to search
 * @param mixed $Key Index of the value to retrieve
 * @param mixed $Default Value to return if the key does not exist
 * @return the value at the given index or the default value if it doesn't exist
 */
function GetArrayValue(array $Array, $Key, $Default=NULL)
{
    return array_key_exists($Key, $Array) ? $Array[$Key] : $Default;
}

/**
 * Extract all the values with the given keys from the given array, setting the
 * values to a default one if they don't exist and a default is given or NULL if
 * one is not given.
 * @param array $Array Array to extract values from
 * @param array $Keys Array of keys of the values to extract
 * @param array $Defaults Array of default values used if a value doesn't exist
 * @return array extracted values
 */
function GetArrayValues(array $Array, array $Keys, array $Defaults=array())
{
    $Values = array();

    foreach ($Keys as $Key)
    {
        $Default = GetArrayValue($Defaults, $Key);
        $Values[$Key] = GetArrayValue($Array, $Key, $Default);
    }

    return $Values;
}

/**
 * Retrieve the host name for the given IP address. Returns the IP address if
 * no host name can be found.
 * @param string $IpAddress IP address as a string
 * @return the host name for the IP address or the address if one doesn't exist
 */
function GetHostNameForAddr($IpAddress)
{
    static $HostNames;
    if (!isset($HostNames[$IpAddress]))
    {
        $HostNames[$IpAddress] = preg_match("/\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/",
                $IpAddress) ? gethostbyaddr($IpAddress)
                : (($IpAddress == "::1") ? "localhost" : $IpAddress);
    }
    return $HostNames[$IpAddress];
}

/**
 * Strips a string of any tags and attributes that are not provided as
 * exceptions. Stripping of tags or attributes can be disabled by options.
 * Uses the \f (form feed) character as a token, so this will fail in the
 * unlikely event that someone manages to get a \f character into the input
 * string to this function.
 *
 * Options are:
 *     "StripTags" => set to FALSE to disable tag stripping
 *     "StripAttributes" => set to FALSE to disable attribute stripping
 *     "Tags" => string of allowed tags, whitespace delimited (e.g., "a b i")
 *     "Attributes" => string of allowed attributes, whitespace
 *                     delimited (e.g., "href target")
 *
 * @param string $String String to parse
 * @param array $Options Options (see above)
 * @return string the parsed string
 * @see StripXSSThreats()
 * @see StripUnsafeProtocols()
 */
function StripTagsAttributes($String, $Options=array())
{
    # make sure have values for the predicate options
    if (!is_array($Options)) {  $Options = array();  }

    $Options["StripTags"] = GetArrayValue($Options, "StripTags", TRUE);
    $Options["StripAttributes"] = GetArrayValue($Options, "StripAttributes", TRUE);

    # phase 1: strip invalid tags if necessary
    if ($Options["StripTags"])
    {
        $Tags = trim(GetArrayValue($Options, "Tags"));

        # escape allowed tags if any were given
        if (strlen($Tags))
        {
            # strip invalid characters and ready the names for the subpattern
            $Tags = preg_replace('/[^a-z0-9 ]/i', '', $Options["Tags"]);
            $Tags = preg_replace('/\s+/', "|", $Tags);

            # and finally escape the tags
            $String = preg_replace(
                  '/<('.$Tags.')(\s[^>]*)?(\/?)>|<(\/)('.$Tags.')(\s[^>]*)?>/i',
                  sprintf('%c$1$2$3$4$5$6%c', 12, 12),
                  $String);
        }

        # remove all other tags and then unescape allowed tags
        $String = preg_replace('/<[^>]*>/', '', $String);
        $String = preg_replace(sprintf('/%c([^%c]*)%c/', 12, 12, 12),
                '<$1>', $String);
    }

    # phase 2: strip attributes if necessary
    if ($Options["StripAttributes"])
    {
        $Attributes = trim(GetArrayValue($Options, "Attributes"));

        # move all of the attributes into separate contexts for validation
        $String = preg_replace('/<([a-z0-9]+)\s+([^>]*[^>\/])(\/)?>/i',
            sprintf('<$1$3>%c$2%c', 12, 12), $String);

        if (strlen($Attributes))
        {
            # remove bad chars and split by whitespace
            $Attributes = preg_replace('/[^a-zA-z0-9 ]/i', '', $Options["Attributes"]);
            $Attributes = preg_split('/\s+/', $Attributes);

            $AttributesCount = count($Attributes);

            # extract each allowed attribute from its context
            for ($i = 0; $i < $AttributesCount; $i++)
            {
                if ($i < strlen($Attributes[$i]))
                {
                      $String = preg_replace(
                            sprintf('/<([A-Za-z0-9]+)(\s[^>]*[^>\/])?(\/)?>'.
                            '%c([^%c]*\s*)'.$Attributes[$i].'=("[^"]*"|\'[^\']*\')'.
                            '([^%c]*\s*)%c/i', 12, 12, 12, 12),
                            sprintf("<$1$2 ".$Attributes[$i]."=$5$3>%c$4$6%c", 12, 12),
                            $String);
                }
                else
                {
                    return "";
                }
            }
        }

        # destroy all the contexts created, deleting any attributes that aren't
        # allowed, and make well-formed singleton tags
        $String = preg_replace(
            array(sprintf('/%c[^%c]*%c/', 12, 12, 12), '/<([^>]+)\/>/'),
            array('', '<$1 />'),
            $String);
    }

    # return the string
    return $String;
}

/**
 * Strip potentially unsafe tags, attributes, and protocols from the given
 * string to remove any XSS threats.
 * @param string $String The string to strip
 * @return string the string stripped of potentially unsafe text
 * @see StripTagsAttributes()
 * @see StripUnsafeProtocols()
 */
function StripXSSThreats($String)
{
    $Options["Tags"] = "
        a abbr b blockquote br caption cite code dd dl dt del div em h1 h2 h3 h4
        h5 h6 hr i ins kbd ol ul li mark p pre q s samp small span strike strong
        sub sup table tbody td tfoot th thead time tr u var";
    $Options["Attributes"] = "href id name summary";
    return StripUnsafeProtocols(StripTagsAttributes($String, $Options));
}

/**
 * Strip any attributes with potentially unsafe protocols and data in the given
 * HTML. This does not strip out unsafe data in attributes where potentially
 * unsafe data is expected, e.g., the onclick and onhover attributes. Those
 * attributes should be removed entirely if their data cannot be trusted.
 * @param string $Html HTML to strip
 * @return string stripped HTML
 * @see StripTagsAttributes()
 */
function StripUnsafeProtocols($Html)
{
    # the attributes that potentially allow unsafe protocols, depending on the
    # user agent
    $CheckedAttributes = array(
        "action", "cite", "data", "for", "formaction", "formtarget", "href",
        "poster", "src", "srcdoc", "target");

    # the protocols that are considered unsafe
    $UnsafeProtocols = join("|", array("data", "javascript", "vbscript"));

    # remove unsafe protocols from each checked attribute
    foreach ($CheckedAttributes as $CheckedAttribute)
    {
        $Html = preg_replace(
            '/<([a-z0-9]+)\s+([^>]*)'.$CheckedAttribute
                .'\s*=\s*(["\'])\s*('.$UnsafeProtocols
                .')\s*:.*?[^\\\]\3\s?(.*?)>/i',
            '<\1 \2\5>',
            $Html);
    }

    # return the stripped HTML
    return $Html;
}

/**
 * Version-agnostic htmlentities() function.
 * See: http://www.php.net/manual/en/function.htmlentities.php
 * @param string $string The input string
 * @param int $quote_style Defines what is done with quotes
 * @param strin $charset Defines the character set used in conversion
 * @param bool $double_encode When off, PHP won't encode existing HTML entities (>= 5.2.3)
 * @return string the encoded string
 * @see htmlentities()
 * @see htmlspecialchars()
 * @see safehtmlspecialchars()
 * @see defaulthtmlentities()
 * @see defaulthtmlspecialchars()
 */
function safehtmlentities($string, $quote_style=ENT_COMPAT, $charset="ISO-8859-1",
                          $double_encode=TRUE)
{
    # ignore $double_encode param if php version < 5.2.3
    if (version_compare(phpversion(), "5.2.3", "<"))
    {
        return htmlentities($string, $quote_style, $charset);
    }

    return htmlentities($string, $quote_style, $charset, $double_encode);
}

/**
 * Uses the default character set from the system configuration along with the
 * version-agnostic HTML special characters translation function to escape HTML
 * special characters in the given string in the same manner as htmlentities().
 * @param string $String String to translate
 * @return string the translated string
 * @see htmlentities()
 * @see htmlspecialchars()
 * @see safehtmlentities()
 * @see safehtmlspecialchars()
 * @see defaulthtmlspecialchars()
 */
function defaulthtmlentities($String)
{
    $CharacterSet = $GLOBALS["SysConfig"]->DefaultCharacterSet();

    return safehtmlentities($String, ENT_QUOTES, $CharacterSet, FALSE);
}

/**
 * Version-agnostic htmlspecialchars() function.
 * See: http://www.php.net/manual/en/function.htmlspecialchars.php
 * @param string $string The input string
 * @param int $quote_style Defines what is done with quotes
 * @param string $charset Defines the character set used in conversion
 * @param bool $double_encode When off, PHP won't encode existing HTML entities (>= 5.2.3)
 * @return string the encoded string
 * @see htmlspecialchars()
 * @see htmlentities()
 * @see safehtmlentities()
 * @see defaulthtmlspecialchars()
 * @see defaulthtmlentities()
 */
function safehtmlspecialchars($string, $quote_style=ENT_COMPAT, $charset="ISO-8859-1",
                              $double_encode=TRUE)
{
    # ignore $double_encode param if php version < 5.2.3
    if (version_compare(phpversion(), "5.2.3", "<"))
    {
        return htmlspecialchars($string, $quote_style, $charset);
    }

    return htmlspecialchars($string, $quote_style, $charset, $double_encode);
}

/**
 * Uses the default character set from the system configuration along with the
 * version-agnostic HTML special characters translation function to escape HTML
 * special characters in the given string in the same manner as
 * htmlspecialchars().
 * @param string $String String to translate
 * @return string the translated string
 * @see htmlspecialchars()
 * @see htmlentities()
 * @see safehtmlspecialchars()
 * @see safehtmlentities()
 * @see defaulthtmlentities()
 */
function defaulthtmlspecialchars($String)
{
    $CharacterSet = $GLOBALS["SysConfig"]->DefaultCharacterSet();

    return safehtmlspecialchars($String, ENT_QUOTES, $CharacterSet, FALSE);
}

/**
 * Retreive PHP configuration settings via a call to phpinfo().
 * @return array an array of PHP configuration settings
 * @see phpinfo()
 */
function GetPhpInfo()
{
    # grab PHP info page
    ob_start();
    phpinfo();
    $InfoPage = ob_get_contents();
    ob_end_clean();

    # start by assuming that no info is available
    $Info = array();

    # for each section on page
    $PageChunks = explode("<h2", $InfoPage);
    foreach ($PageChunks as $PageChunk)
    {
        # look for module/section name
        preg_match("/<a name=\"module_([^<>]*)\">/", $PageChunk, $Piece);

        # if we found module/section
        if (count($Piece) > 1)
        {
            # save module/section name
            $ModuleName = trim($Piece[1]);
        }
        else
        {
            # assume no module/section name
            $ModuleName = "";
        }

        # pull out info values from HTML tables
        preg_match_all("/<tr[^>]*><td[^>]*>(.*)<\/td><td[^>]*>(.*)<\/td>/Ux",
                       $PageChunk, $LocalValue);
        preg_match_all(
                "/<tr[^>]*><td[^>]*>(.*)<\/td><td[^>]*>(.*)<\/td><td[^>]*>(.*)<\/td>/Ux",
                 $PageChunk, $MasterValue);

        # store "local" info values
        foreach ($LocalValue[0] as $MatchString => $Dummy)
        {
            $MatchString = trim($MatchString);
            $Info[$ModuleName][trim(strip_tags($LocalValue[1][$MatchString]))] =
                    array(trim(strip_tags($LocalValue[2][$MatchString])));
        }

        # store "master" info values
        foreach ($MasterValue[0] as $MatchString => $Dummy)
        {
            $MatchString = trim($MatchString);
            $Info[$ModuleName][trim(strip_tags($MasterValue[1][$MatchString]))] =
                  array(trim(strip_tags($MasterValue[2][$MatchString])),
                  trim(strip_tags($MasterValue[3][$MatchString])));
        }
    }

    # return info to caller
    return $Info;
}

/**
 * Munges an email address to try to fool web-scraping robots. Inserts two
 * strings of random characters into the email address, wrapped in spans which
 * will hide them from users whose browsers properly implement CSS.  Also wraps
 * the whole thing in a span.EMungeAddr so that some javascript in
 * SPT--EmailMunge.js can convert these fuzzed addresses back into clickable
 * mailtos.
 * @param string $String An email address to obfuscate
 * @return string the obfuscated email address
 */
function MungeEmailAddress($String)
{
    $FuzzOne = substr(md5(mt_rand()), 0, rand(8, 32));
    $FuzzTwo = substr(md5(mt_rand()), 0, rand(8, 32));
    return '<span class="EMungeAddr">'.preg_replace(
        '/@/',
        '<span style="display:none;"> '.htmlentities($FuzzOne).' </span>'
        .'&#64;'
        .'<span style="display:none;"> '.htmlentities($FuzzTwo).' </span>',
        $String).'</span>';
}

/**
 * Converts a simpleXML element into an array. Preserves attributes and
 * everything. You can choose to get your elements either flattened, or stored
 * in a custom index that you define.
 * For example, for a given element
 * <field name="someName" type="someType"/>
 * if you choose to flatten attributes, you would get:
 * $array['field']['name'] = 'someName';
 * $array['field']['type'] = 'someType';
 * If you choose not to flatten, you get:
 * $array['field']['@attributes']['name'] = 'someName';
 * _____________________________________
 * Repeating fields are stored in indexed arrays. so for a markup such as:
 * <parent>
 * <child>a</child>
 * <child>b</child>
 * <child>c</child>
 * </parent>
 * you array would be:
 * $array['parent']['child'][0] = 'a';
 * $array['parent']['child'][1] = 'b';
 * ...And so on.
 * _____________________________________
 * @param SimpleXMLElement $Xml XML to convert
 * @param boolean $FlattenValues Whether to flatten values
 *       or to set them under a particular index.  Defaults to TRUE;
 * @param boolean $FlattenAttributes Whether to flatten attributes
 *       or to set them under a particular index. Defaults to TRUE;
 * @param boolean $FlattenChildren Whether to flatten children
 *       or to set them under a particular index. Defaults to TRUE;
 * @param string $ValueKey Index for values, in case $FlattenValues was
 *       set to FALSE. Defaults to "@value"
 * @param string $AttributesKey Index for attributes, in case
 *       $FlattenAttributes was set to FALSE. Defaults to "@attributes"
 * @param string $ChildrenKey Index for children, in case $FlattenChildren
 *       was set to FALSE. Defaults to "@children"
 * @return array The resulting array.
 */
function SimpleXMLToArray($Xml,
        $FlattenValues = TRUE,
        $FlattenAttributes = TRUE,
        $FlattenChildren = TRUE,
        $ValueKey = "@values",
        $AttributesKey = "@attributes",
        $ChildrenKey = "@children")
{
    $Array = array();
    $XMLClassName = "SimpleXMLElement";
    if (!($Xml instanceof $XMLClassName)) {  return $Array;  }

    $Name = $Xml->getName();
    $Value = trim((string)$Xml);
    if (!strlen($Value)) {  $Value = NULL;  }

    if ($Value !== NULL)
    {
        if ($FlattenValues) {  $Array = $Value;  }
        else {  $Array[$ValueKey] = $Value;  }
    }

    $Children = array();
    foreach ($Xml->children() as $ElementName => $Child)
    {
        $Value = SimpleXMLToArray($Child, $FlattenValues, $FlattenAttributes,
                $FlattenChildren, $ValueKey, $AttributesKey, $ChildrenKey);

        if (isset($Children[$ElementName]))
        {
            if (!isset($MultipleMembers[$ElementName]))
            {
                $Temp = $Children[$ElementName];
                unset($Children[$ElementName]);
                $Children[$ElementName][] = $Temp;
                $MultipleMembers[$ElementName] = TRUE;
            }
            $Children[$ElementName][] = $Value;
        }
        else
        {
            $Children[$ElementName] = $Value;
        }
    }
    if (count($Children))
    {
        if ($FlattenChildren) {  $Array = array_merge($Array, $Children);  }
        else {  $Array[$ChildrenKey] = $Children;  }
    }

    $Attribs = array();
    foreach ($Xml->attributes() as $Name => $Value)
    {
        $Attribs[$Name] = trim($Value);
    }
    if (count($Attribs))
    {
        if (!$FlattenAttributes) {  $Array[$AttributesKey] = $Attribs;  }
        else {  $Array = array_merge($Array, $Attribs);  }
    }

    return $Array;
}

/**
 * Checks an SQL statement for potentially destructive keywords.
 * @param string $SqlStatement An SQL statement
 * @return bool FALSE for safe-looking statements and TRUE otherwise
 */
function ContainsDangerousSQL($SqlStatement)
{
    $EvilKeywords = array(
        "ALTER","RENAME","TRUNCATE","CREATE","START","COMMIT",
        "ROLLBACK", "SET", "BACKUP", "OPTIMIZE", "REPAIR", "RESTORE",
        "SET", "GRANT", "USE", "UPDATE", "INSERT", "DELETE", "DROP");

    return preg_match("/(^|\() *(".implode("|", $EvilKeywords).") /i",
                           $SqlStatement) === 1;
}

/**
 * Retrieve a form value from $_POST or $_GET, if available, otherwise returns a
 * default value
 * @param mixed $Key Key into $_POST or $_GET to check for value
 * @param mixed $Default Value to return if nothing is found in $_POST or $_GET
 *       (OPTIONAL, defaults to NULL)
 * @return mixed the form value or default if nothing is found
 * @see GetArrayValue()
 */
function GetFormValue($Key, $Default = NULL)
{
    return GetArrayValue($_POST, $Key, GetArrayValue($_GET, $Key, $Default));
}

/**
 * Remove the file or directory with the given path. Recursively removes
 * directories.
 * @param string $Path Path of the file or directory to remove
 * @return bool TRUE if successful or FALSE otherwise
 */
function RemoveFromFilesystem($Path)
{
    # the path is a directory
    if (is_dir($Path))
    {
        # recursively delete directories
        foreach (glob($Path."/*") as $Item)
        {
            if (!RemoveFromFilesystem($Item))
            {
                return FALSE;
            }
        }

        # and then remove this directory
        return @rmdir($Path);
    }

    # the path is a file
    else
    {
        return @unlink($Path);
    }
}

/**
* Converts a timestamp into a user-friendly printable format.
* @param mixed $Timestamp Date/time value to print, as a timestamp or in
*      any format parseable by strtotime().
* @param bool $Verbose Whether to be verbose about date.  (OPTIONAL, defaults
*       to FALSE)
* @param string $BadTimeString String to display if date/time appears invalid.
*       (OPTIONAL, defaults to "-")
* @param bool $IncludeOldTimes Whether to include time when date is more than
*       a week in the past.  (OPTIONAL, defaults to TRUE)
* @return string A string containing a nicely-formatted timestamp value.
*/
function GetPrettyTimestamp($Timestamp, $Verbose = FALSE,
        $BadTimeString = "-", $IncludeOldTimes = TRUE)
{
    # convert timestamp to seconds if necessary
    $TStamp = preg_match("/^[0-9]+$/", $Timestamp) ? $Timestamp
            : strtotime($Timestamp);

    # if time was invalid
    if (($TStamp === FALSE) || ($TStamp < 0))
    {
        $Pretty = $BadTimeString;
    }
    # else if timestamp is today use format "1:23pm"
    elseif (date("z Y", $TStamp) == date("z Y"))
    {
        $Pretty = date("g:ia", $TStamp);
    }
    # else if timestamp is yesterday use format "Yesterday 1:23pm"
    elseif (date("n/j/Y", ($TStamp + (24 * 60 * 60))) == date("n/j/Y"))
    {
        $Pretty = "Yesterday "
                .($Verbose ? "at " : "")
                .date("g:ia", $TStamp);
    }
    # else if timestamp is tomorrow use format "Tomorrow 1:23pm"
    elseif (date("n/j/Y", ($TStamp - (24 * 60 * 60))) == date("n/j/Y"))
    {
        $Pretty = "Tomorrow "
                .($Verbose ? "at " : "")
                .date("g:ia", $TStamp);
    }
    # else if timestamp is this week use format "Monday 1:23pm"
    # (adjust timestamp by a day because "W" begins the week on monday)
    elseif (date("W/o/Y", ($TStamp + (24*60*60))) == date("W/o/Y"))
    {
        $Pretty = $Verbose ? date('l \a\t g:ia', $TStamp)
                : date("D g:ia", $TStamp);
    }
    # else if timestamp is this year use format "1/31 1:23pm"
    elseif (date("Y", $TStamp) == date("Y"))
    {
        $Pretty = date(($Verbose
                ? ($IncludeOldTimes ? 'F jS \a\t g:ia' : 'F jS')
                : ($IncludeOldTimes ? "n/j g:ia" : "n/j")
                ), $TStamp);
    }
    # else use format "1/31/99 1:23pm"
    else
    {
        $Pretty = date(($Verbose
                ? ($IncludeOldTimes ? 'F jS, Y \a\t g:ia' : 'F jS, Y')
                : ($IncludeOldTimes ? "n/j/y g:ia" : "n/j/y")
                ), $TStamp);
    }

    # return nicely-formatted timestamp to caller
    return $Pretty;
}

/**
* Converts a date into a user-friendly printable format.
* @param mixed $Date Date value to print, in any format parseable by strtotime().
* @param bool $Verbose Whether to be verbose about date.  (OPTIONAL, defaults to FALSE)
* @param string $BadDateString String to display if date appears invalid.  (OPTIONAL,
*       defaults to "-")
* @return string Returns a string containing a nicely-formatted date value.
*/
function GetPrettyDate($Date, $Verbose = FALSE, $BadDateString = "-")
{
    # convert date to seconds
    $TStamp = strtotime($Date);

    # if time was invalid
    if (($TStamp === FALSE) || ($TStamp < 0))
    {
        return $BadDateString;
    }

    # if timestamp was today use "Today"
    if (date("z Y", $TStamp) == date("z Y"))
    {
        return "Today";
    }

    # if timestamp was yesterday use "Yesterday"
    if (date("n/j/Y", ($TStamp - (24 * 60 * 60))) == date("n/j/Y"))
    {
        return "Yesterday";
    }

    # if timestamp was this week use format "Monday" or "Mon"
    # (adjust timestamp by a day because "W" begins the week on monday)
    if (date("W/o/Y", ($TStamp + (24*60*60))) == date("W/o/Y"))
    {
        $Format = $Verbose ? "l" : "D";
        return date($Format, $TStamp);
    }

    # if timestamp was this year use format "January 31st" or "1/31"
    if (date("Y", $TStamp) == date("Y"))
    {
        $Format = $Verbose ? "F jS" : "n/j";
        return date($Format, $TStamp);
    }

    # just use format "January 31st, 1999" or "1/31/99"
    $Format = $Verbose ? "F jS, Y" : "n/j/y";
    return date($Format, $TStamp);
}

/**
* Convert a date range into a user-friendly printable format.
* @param string $StartDate Starting date, in any format parseable by strtotime().
* @param string $EndDate Ending date, in any format parseable by strtotime().
* @param bool $Verbose Whether to be verbose about dates.  (OPTIONAL, defaults to
*       TRUE)
* @param string $BadDateString String to display if start date appears invalid.
*       (OPTIONAL, defaults to "-")
* @return string Returns a string containing a nicely-formatted date value.
*/
function GetPrettyDateRange($StartDate, $EndDate, $Verbose = TRUE, $BadDateString = "-")
{
    # convert dates to seconds
    $Start = strtotime($StartDate);
    $End = strtotime($EndDate);

    # return bad date string if start date was invalid
    if (($Start === FALSE) || ($Start < 0))
    {
        return $BadDateString;
    }

    # return pretty printed date if end date was invalid or same as start date
    if (($Start === FALSE) || ($Start < 0) || ($End == $Start))
    {
        return GetPrettyDate($EndDate, $Verbose, $BadDateString);
    }

    # use short or long month names based on verbosity setting
    $MChar = $Verbose ? "F" : "M";

    # if start and end month are the same use "January 1-10"
    $AddYear = TRUE;
    if (date("MY", $Start) == date("MY", $End))
    {
        $Range = date($MChar." j-", $Start).date("j", $End);
    }
    # else if start and end year are the same use "January 21 - February 3"
    elseif (date("Y", $Start) == date("Y", $End))
    {
        $Range = date($MChar." j - ", $Start).date($MChar." j", $End);
    }
    # else use "December 21, 2013 - January 3, 2014"
    else
    {
        $Range = date($MChar." j, Y - ", $Start).date($MChar." j, Y", $End);
        $AddYear = FALSE;
    }

    # if end year is not current year and we haven't already added it
    if ((date("Y", $End) != date("Y")) && $AddYear)
    {
        # add end year to date
        $Range .= date(", Y", $End);
    }

    # return pretty date range to caller
    return $Range;
}

/**
* Convert a date range into a user-friendly printable format broken into pieces,
* useful when adding a date to a page with semantic markup.
* @param string $StartDate Starting date, in any format parseable by strtotime().
* @param string $EndDate Ending date, in any format parseable by strtotime().
* @param bool $Verbose Whether to be verbose about dates.  (OPTIONAL, defaults to
*       TRUE)
* @return array Returns an associative array with "Start" and "End" elements,
*       each portions of a string containing a nicely-formatted date value, or
*       NULL if the date range was invalid.
*/
function GetPrettyDateRangeInParts($StartDate, $EndDate, $Verbose = TRUE)
{
    # generate pretty date range string
    $RangeString = GetPrettyDateRange($StartDate, $EndDate, $Verbose, NULL);

    # return NULL if date was not valid
    if ($RangeString === NULL) {  return NULL;  }

    # break range string into pieces
    $Pieces = explode("-", $RangeString, 2);
    $RangeParts["Start"] = trim($Pieces[0]);
    $RangeParts["End"] = (count($Pieces) > 1) ? trim($Pieces[1]) : "";

    # return pieces to caller
    return $RangeParts;
}

/**
* Parse a query string into variables like parse_str(). This function differs
* from parse_str() in two important ways:
* - Any slashes added by magic quotes are removed.
* - The variables are always returned in an array rather than setting variables
*   in the scope in which the function was called. Use extract() to set
*   variables in the local scope using an array, e.g.,
* @code
* extract(ParseQueryString("..."));
* @endcode
* @param string $QueryString The query string to parse.
* @return Returns the parsed query string as an array of variables.
* @see parse_str()
* @see extract()
*/
function ParseQueryString($QueryString)
{
    # parse the query string into an array
    parse_str($QueryString, $QueryVariables);

    # strip slashes from the query variables if magic quotes are enabled
    if (get_magic_quotes_gpc())
    {
        array_walk_recursive($QueryVariables, "ParseQueryString_Aux");
    }

    # return the query variables
    return $QueryVariables;
}

/**
* Auxiliary function for ParseQueryString(). Is used to strip slashes from the
* values in an array.
* @param mixed $Value Reference to the value in the array.
* @param mixed $Key Key for the value in the array.
*/
function ParseQueryString_Aux(&$Value, $Key)
{
    $Value = stripslashes($Value);
}


/**
* Get MIME type for specified file.  If the MIME type cannot be
* determined, a type of "application/octet-stream" will be returned.
* @param string $FileName File name with path.
* @return string MIME type for file.
*/
function GetMimeType($FileName)
{
    # if Fileinfo PECL package is available (standard for PHP >= 5.3)
    if (function_exists("finfo_open"))
    {
        # if file info database is available
        $FInfoHandle = finfo_open(FILEINFO_MIME);
        if ($FInfoHandle)
        {
            # attempt to retrieve MIME type
            $FInfoMime = finfo_file($FInfoHandle, $FileName);
            finfo_close($FInfoHandle);

            # if MIME type was found
            if ($FInfoMime)
            {
                # use type found
                $MimeType = $FInfoMime;
            }
        }
    }

    # if type not yet found and old MIME type library function is available
    if (!isset($MimeType) && function_exists("mime_content_type"))
    {
        # attempt to retrieve MIME type
        $ContentType = mime_content_type($TempFileName);

        # if MIME type was found
        if ($ContentType)
        {
            # use type found
            $MimeType = $ContentType;
        }
    }

    # use default if we can't find another type
    if (!isset($MimeType)) {  $MimeType = "application/octet-stream";  }

    # return type to caller
    return $MimeType;
}

/**
* Function to check if the request to a page is an AJAX request;
* depends upon using jQuery to handle AJAX for
* response type to be correctly set.
* @return bool TRUE if page was requested with AJAX, otherwise FALSE.
*/
function ReachedWithAjax()
{
    return (isset($_SERVER['HTTP_X_REQUESTED_WITH'])
            && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
}

/**
* Function to start an AJAX response and avoid caching errors
*/
function BeginAjaxResponse()
{
    # how long until the cached results become stale
    $MaxAgeInSeconds = 30;

    # set headers to control caching
    header("Expires: ".gmdate("D, d M Y H:i:s \G\M\T", time()+$MaxAgeInSeconds));
    header("Cache-Control: private, max-age=".$MaxAgeInSeconds);
    header("Pragma:");

    # PHP sessions cause AJAX calls to block, so make the session readonly from
    # this point on to avoid latency
    session_write_close();
    $GLOBALS['AF']->SetJumpToPage(NULL);
}


/**
* Get a specified number of random alphanumeric characters.
* @param int $NumChars Number of characters to get.
* @param strint $ExcludePattern PCRE pattern to exclude undesired characters
*    (OPTIONAL, default [^A-Za-z0-9]).
* @return string Random characters.
*/
function GetRandomCharacters($NumChars, $ExcludePattern="/[^A-Za-z0-9]/")
{
    $rc = '';

    while (strlen($rc) < $NumChars)
    {
        # append random alphanumerics
        $rc .= preg_replace(
            $ExcludePattern, "",
            base64_encode(openssl_random_pseudo_bytes(3*$NumChars)));
    }

    return substr($rc, 0, $NumChars);
}

/**
* Determine if a string contains serialized php data.
* @param string $String String to test
* @return TRUE when the string represents data
*/
function IsSerializedData($String)
{
    # only strings are serialized data
    if (!is_string($String))
    {
        return FALSE;
    }

    # if the string encodes 'FALSE', then it's serialized
    if ($String == serialize(FALSE))
    {
        return TRUE;
    }

    # otherwise attempt to unserialize, will get FALSE on error
    $Data = @unserialize($String);
    return ($Data !== FALSE);
}
