* Copyright 2001-2012 Strangecode, LLC * * This file is part of The Strangecode Codebase. * * The Strangecode Codebase is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * The Strangecode Codebase is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * The Strangecode Codebase. If not, see . */ /** * Utilities.inc.php */ require_once dirname(__FILE__) . '/App.inc.php'; /** * Convert all PHP data types to human-readable string representations. * * @param mixed $var * @return mixed */ function exhibitData($var, $verbose=false) { static $finfo = null; if (null === $var) { return $verbose ? '(null)' : ''; } if (true === $var) { return $verbose ? '(bool: true)' : 'true'; } if (false === $var) { return $verbose ? '(bool: false)' : 'false'; } if (is_array($var)) { return array_map('exhibitData', $var); } if (is_object($var)) { try { $obj = clone $var; foreach (get_object_vars($obj) as $key => $value) { $obj->$key = exhibitData($value); } return $obj; } catch (Exception $e) { return sprintf('(unclonable object: %s)', get_class($var)); } } if (is_string($var)) { if ('' === $var) { return $verbose ? '(empty string)' : ''; } if (preg_match('/^\s+$/', $var)) { return $verbose ? '(only white space)' : ''; } if (!mb_check_encoding($var, 'UTF-8')) { if (null === $finfo) { $finfo = new finfo(FILEINFO_MIME_TYPE); } $type = $finfo->buffer($var); if ('application/octet-stream' === $type) { return $verbose ? '(binary data)' : '…'; } if (false === $type) { return $verbose ? '(binary data: unknown)' : '…'; } return $verbose ? sprintf('(binary data: %s)', $type) : '…'; } if (preg_match('/[\p{C}]/u', $var)) { return preg_replace_callback('/[\p{C}]/u', function($m) { return sprintf('\\x%02x', ord($m[0])); }, $var); } return $var; } if ($var instanceof Closure) { return $verbose ? '(closure)' : '…'; } if (is_resource($var)) { return $verbose ? '(resource: ' . get_resource_type($var) . ')' : '…'; } return $var; } /** * Print variable dump. * * @param mixed $var The variable to dump. * @param bool $display Print the dump in
 tags or hide it in html comments (non-CLI only).
 * @param  const    $dump_method    Dump method. See SC_DUMP_* constants.
 * @param  string   $file           Value of __FILE__.
 * @param  string   $line           Value of __LINE__
 */
define('SC_DUMP_PRINT_R', 0);
define('SC_DUMP_VAR_DUMP', 1);
define('SC_DUMP_VAR_EXPORT', 2);
define('SC_DUMP_JSON', 3);
function dump($var, $display=false, $dump_method=SC_DUMP_JSON, $file='', $line='')
{
    $app =& App::getInstance();

    if ($app->isCLI()) {
        echo ('' != $file . $line) ? "DUMP FROM: $file $line\n" : "DUMP:\n";
    } else {
        echo $display ? "\n
DUMP $file $line
\n" : "\n\n";
    }
}

/*
* Log a PHP variable to javascript console. Relies on getDump(), below.
*
* @access   public
* @param    mixed   $var      The variable to dump.
* @param    string  $prefix   A short note to print before the output to make identifying output easier.
* @param    string  $file     The value of __FILE__.
* @param    string  $line     The value of __LINE__.
* @return   null
* @author   Quinn Comendant 
*/
function jsDump($var, $prefix='jsDump', $file='-', $line='-')
{
    if (!empty($var)) {
        ?>
        
        
*/
function getDump($var, $serialize=false, $dump_method=SC_DUMP_JSON)
{
    $app =& App::getInstance();

    switch ($dump_method) {
    case SC_DUMP_PRINT_R:
        ob_start();
        print_r(exhibitData($var, true));
        $d = ob_get_contents();
        ob_end_clean();
        break;

    case SC_DUMP_VAR_DUMP:
        ob_start();
        var_dump($var);
        $d = ob_get_contents();
        ob_end_clean();
        break;

    case SC_DUMP_VAR_EXPORT:
        ob_start();
        var_export($var);
        $d = ob_get_contents();
        ob_end_clean();
        break;

    case SC_DUMP_JSON:
    default:
        $json_flags = $serialize ? 0 : JSON_PRETTY_PRINT;
        return json_encode(exhibitData($var, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | $json_flags);
    }
    return $serialize ? preg_replace('/\s+/m' . $app->getParam('preg_u'), ' ', $d) : $d;
}

/*
* Return dump as cleaned text. Useful for dumping data into emails or output from CLI scripts.
* To output tab-style lists set $indent to "\t" and $depth to 0;
* To output markdown-style lists set $indent to '- ' and $depth to 1;
* Also see yaml_emit() https://secure.php.net/manual/en/function.yaml-emit.php
*
* @param  array    $var        Variable to dump.
* @param  string   $indent     A string to prepend indented lines.
* @param  string   $depth      Starting depth of this iteration of recursion (set to 0 to have no initial indentation).
* @return string               Pretty dump of $var.
* @author   Quinn Comendant 
* @version 2.0
*/
function fancyDump($var, $indent='- ', $depth=1)
{
    $app =& App::getInstance();

    $indent = trim($indent, ' ') . ' ';

    $indent_str = str_repeat($indent, $depth);
    $output = '';
    $var = exhibitData($var);
    if (is_array($var)) {
        foreach ($var as $k=>$v) {
            $k = ucfirst(preg_replace([
                '/_/',
                '/ {2,}/',
                '/\bip\b/',
                '/\bid\b/',
            ], [
                ' ',
                ' ',
                'IP',
                'ID',
            ], mb_strtolower($k)));
            if (is_array($v)) {
                $output .= sprintf("\n%s%s:\n%s\n", $indent_str, $k, fancyDump($v, $indent, $depth+1));
            } else {
                $output .= sprintf("%s%s: %s\n", $indent_str, $k, $v);
            }
        }
    } else if (is_scalar($var)) {
        $output .= sprintf("%s%s\n", $indent_str, (string)$var);
    } else {
        $output .= sprintf("%s(non-scalar data)\n", $indent_str);
    }

    return preg_replace([
        '/^[ \t]+$/' . $app->getParam('preg_u'),
        '/\n\n+/' . $app->getParam('preg_u'),
        sprintf('/^(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(?:%1$s( ))?(%1$s )/m%2$s', preg_quote(trim($indent, ' '), '/'), $app->getParam('preg_u')),
    ], [
        '',
        "\n",
        '$1$1$2$2$3$3$4$4$5$5$6$6$7$7$8$8$9'
    ], $output);
}

/**
 * @param string|mixed $value A string to UTF8-encode.
 *
 * @returns string|mixed The UTF8-encoded string, or the object passed in if
 *    it wasn't a string.
 */
function conditionalUTF8Encode($value)
{
  if (is_string($value) && mb_detect_encoding($value, 'UTF-8', true) != 'UTF-8') {
    return utf8_encode($value);
  } else {
    return $value;
  }
}


/**
 * Returns text with appropriate html translations (a smart wrapper for htmlspecialchars()).
 *
 * @param  string $text             Text to clean.
 * @param  bool   $preserve_html    If set to true, oTxt will not translate <, >, ", or '
 *                                  characters into HTML entities. This allows HTML to pass through undisturbed.
 * @return string                   HTML-safe text.
 */
function oTxt($text, $preserve_html=false)
{
    $app =& App::getInstance();

    if (!is_scalar($text)) {
        $app->logMsg(sprintf('oTxt() non-scalar value: %s', getDump($text)), LOG_NOTICE, __FILE__, __LINE__);
        return '';
    }

    $text = (string)$text;
    $search = array();
    $replace = array();

    // Make converted ampersand entities into normal ampersands (they will be done manually later) to retain HTML entities.
    $search['retain_ampersand']     = '/&/';
    $replace['retain_ampersand']    = '&';

    if ($preserve_html) {
        // Convert characters that must remain non-entities for displaying HTML.
        $search['retain_left_angle']       = '/</';
        $replace['retain_left_angle']      = '<';

        $search['retain_right_angle']      = '/>/';
        $replace['retain_right_angle']     = '>';

        $search['retain_single_quote']     = '/'/';
        $replace['retain_single_quote']    = "'";

        $search['retain_double_quote']     = '/"/';
        $replace['retain_double_quote']    = '"';
    }

    // & becomes &. Exclude any occurrence where the & is followed by a alphanum or unicode character.
    $search['ampersand']        = '/&(?![\w\d#]{1,10};)/';
    $replace['ampersand']       = '&';

    return preg_replace($search, $replace, htmlspecialchars($text, ENT_QUOTES, $app->getParam('character_set')));
}

/**
 * Returns text with stylistic modifications. Warning: this will break some HTML attributes!
 * TODO: Allow a string such as this to be passed: Click here
 *
 * @param  string   $text Text to clean.
 * @return string         Cleaned text.
 */
function fancyTxt($text, $extra_search=null, $extra_replace=null)
{
    if (!is_scalar($text)) {
        $app =& App::getInstance();
        $app->logMsg(sprintf('fancyTxt() non-scalar value: %s', getDump($text)), LOG_NOTICE, __FILE__, __LINE__);
        return '';
    }

    $text = (string)$text;

    if ('' === $text) {
        return '';
    }

    $search = array();
    $replace = array();

    // "double quoted text"  →  “double quoted text”
    $search['_double_quotes']    = '/(?<=^|[^\w=(])(?:"|"|�?34;|"|“)([\w\'.…(—–-][^"]*?)(?:"|"|�?34;|"|”)(?=[^)\w]|$)/imsu'; // " is the same as " and " and " and "
    $replace['_double_quotes']   = '“$1”';

    // text's apostrophes  →  text’s apostrophes (except foot marks: 6'3")
    $search['_apostrophe']       = '/(?<=[a-z])(?:\'|�?39;)(?=\w)/imsu';
    $replace['_apostrophe']      = '’';

    // 'single quoted text'  →  ‘single quoted text’
    $search['_single_quotes']    = '/(?<=^|[^\w=(])(?:\'|�?39;|‘)([\w"][^\']+?)(?:\'|�?39;|’)(?=[^)\w]|$)/imsu';
    $replace['_single_quotes']   = '‘$1’';

    // plural posessives' apostrophes  →  posessives’  (except foot marks: 6')
    $search['_apostrophes']      = '/(?<=s)(?:\'|�?39;|’)(?=\s)/imsu';
    $replace['_apostrophes']     = '’';

    // double--hyphens  →  en – dashes
    $search['_em_dash']          = '/(?<=[\w\s"\'”’)])--(?=[\w\s“”‘"\'(?])/imsu';
    $replace['_em_dash']         = ' – ';

    // ...  →  …
    $search['_elipsis']          = '/(?<=^|[^.])\.\.\.(?=[^.]|$)/imsu';
    $replace['_elipsis']         = '…';

    if (is_array($extra_search) && is_array($extra_replace) && sizeof($extra_search) == sizeof($extra_replace)) {
        // Append additional search replacements.
        $search = array_merge($search, $extra_search);
        $replace = array_merge($replace, $extra_replace);
    }

    return trim(preg_replace($search, $replace, $text));
}

/*
* Finds all URLs in text and hyperlinks them.
*
* @access   public
* @param    string  $text   Text to search for URLs.
* @param    bool    $strict True to only include URLs starting with a scheme (http:// ftp:// im://), or false to include URLs starting with 'www.'.
* @param    mixed   $length Number of characters to truncate URL, or NULL to disable truncating.
* @param    string  $delim  Delimiter to append, indicate truncation.
* @return   string          Same input text, but URLs hyperlinked.
* @author   Quinn Comendant 
* @version  2.2
* @since    22 Mar 2015 23:29:04
*/
function hyperlinkTxt($text, $strict=false, $length=null, $delim='…')
{
    if (!is_scalar($text)) {
        $app =& App::getInstance();
        $app->logMsg(sprintf('hyperlinkTxt() non-scalar value: %s', getDump($text)), LOG_NOTICE, __FILE__, __LINE__);
        return '';
    }

    $text = (string)$text;

    if ('' === $text) {
        return '';
    }

    // A list of schemes we allow at the beginning of a URL.
    $schemes = 'mailto:|tel:|skype:|callto:|facetime:|bitcoin:|geo:|magnet:\?|sip:|sms:|xmpp:|view-source:(?:https?://)?|[\w-]{2,}://';

    // Capture the full URL into the first match and only the first X characters into the second match.
    // This will match URLs not preceded by " ' or = (URLs inside an attribute) or ` (Markdown quoted) or double-scheme (http://http://www.asdf.com)
    // https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid/1547940#1547940
    $regex = '@
        \b                                 # Start with a word-boundary.
        (?|`|\]\(|\[\d\] |[:/]/) # Negative look-behind to exclude URLs already in  tag, beween, `Markdown quoted`, [Markdown](link), [1] www.markdown.footnotes, and avoid broken:/ and doubled://schemes://
        (                                  # Begin match 1
            (                              # Begin match 2
                (?:%s)                     # URL starts with known scheme or www. if strict = false
                [^\s/$.?#]+                # Any domain-valid characters
                [^\s"`<>]{1,%s}            # Match 2 is limited to a maximum of LENGTH valid URL characters
            )
            [^\s"`<>]*                     # Match 1 continues with any further valid URL characters
            ([^\P{Any}\s…<>«»"—–%s])       # Final character not a space or common end-of-sentence punctuation (.,:;?!, etc). Using double negation set, see http://stackoverflow.com/a/4786560/277303
        )
        @Suxi
    ';
    $regex = sprintf($regex,
        ($strict ? $schemes : $schemes . '|www\.'), // Strict=false adds "www." to the list of allowed start-of-URL.
        ($length ? $length : ''),
        ($strict ? '' : '?!.,:;)\'-') // Strict=false excludes some "URL-valid" characters from the last character of URL. (Hyphen must remain last character in this class.)
    );

    // Use a callback function to decide when to append the delim.
    // Also encode special chars with oTxt().
    return preg_replace_callback($regex, function ($m) use ($length, $delim) {
        $url = $m[1];
        $truncated_url = $m[2] . $m[3];
        $absolute_url = preg_replace('!^www\.!', 'http://www.', $url);
        if (is_null($length) || $url == $truncated_url) {
            // If not truncating, or URL was not truncated.
            // Remove http schemas, and any single trailing / to make the display URL.
            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], $url);
            return sprintf('%s', oTxt($absolute_url), oTxt($display_url));
        } else {
            // Truncated URL.
            // Remove http schemas, and any single trailing / to make the display URL.
            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], trim($truncated_url));
            return sprintf('%s%s', oTxt($absolute_url), oTxt($display_url), $delim);
        }
    }, $text);
}

/*
* Wrap lines separated by double newlines with 

tags. * * @access public * @param string $text Input text. * @return string Input text with HTML

tags. * @author Quinn Comendant * @since 10 Jan 2025 01:52:42 */ function AddHtmlParagraphTags($text) { $paragraphs = preg_split('/([\n\r]\s*?){2,}/', $text); return implode("\n", array_map(function($p) { // Skip paragraphs containing these block-level elements. if (preg_match('/<(p|div|table|ul|ol|blockquote|figure|address|section|article|aside|nav|header|footer|h[1-6])[^>]*>/i', $p)) { return $p; } return '

' . trim($p) . '

'; }, $paragraphs)); } /** * Applies a class to search terms to highlight them ala google results. * * @param string $text Input text to search. * @param string $search String of word(s) that will be highlighted. * @param string $class CSS class to apply. * @return string Text with searched words wrapped in . */ function highlightWords($text, $search, $class='sc-highlightwords') { $app =& App::getInstance(); if (!is_scalar($text)) { $app->logMsg(sprintf('highlightWords() non-scalar value: %s', getDump($text)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $text = (string)$text; if ('' === $text) { return ''; } $words = preg_split('/[^\w]/', $search, -1, PREG_SPLIT_NO_EMPTY); $search = array(); $replace = array(); foreach ($words as $w) { if ('' != trim($w)) { $search[] = '/\b(' . preg_quote($w) . ')\b/i' . $app->getParam('preg_u'); $replace[] = '$1'; } } return empty($replace) ? $text : preg_replace($search, $replace, $text); } /** * Generates a hexadecimal html color based on provided word. * * @access public * @param string $text A string for which to convert to color. * @param float $n Brightness value between 0-1. * @return string A hexadecimal html color. */ function getTextColor($text, $method=1, $n=0.87) { $hash = md5($text); $rgb = array( mb_substr($hash, 0, 1), mb_substr($hash, 1, 1), mb_substr($hash, 2, 1), mb_substr($hash, 3, 1), mb_substr($hash, 4, 1), mb_substr($hash, 5, 1), ); switch ($method) { case 1 : default : // Reduce all hex values slightly to avoid all white. array_walk($rgb, function (&$v) use ($n) { $v = dechex(round(hexdec($v) * $n)); }); break; case 2 : foreach ($rgb as $i => $v) { if (hexdec($v) > hexdec('c')) { $rgb[$i] = dechex(hexdec('f') - hexdec($v)); } } break; } return join('', $rgb); } /** * Encodes a string into unicode values 128-255. * Useful for hiding an email address from spambots. * * @access public * @param string $text A line of text to encode. * @return string Encoded text. */ function encodeAscii($text) { if (!is_scalar($text)) { $app =& App::getInstance(); $app->logMsg(sprintf('encodeAscii() non-scalar value: %s', getDump($text)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $text = (string)$text; if ('' === $text) { return ''; } $output = ''; $num = mb_strlen($text); for ($i=0; $i<$num; $i++) { $output .= sprintf('&#%03s', ord($text[$i])); } return $output; } /** * Encodes an email into a "user at domain dot com" format. * * @access public * @param string $email An email to encode. * @param string $at Replaces the @. * @param string $dot Replaces the .. * @return string Encoded email. */ function encodeEmail($email, $at=' at ', $dot=' dot ') { $app =& App::getInstance(); if (!is_scalar($email)) { $app->logMsg(sprintf('encodeEmail() non-scalar value: %s', getDump($email)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $email = (string)$email; if ('' === $email) { return ''; } $search = array('/@/' . $app->getParam('preg_u'), '/\./' . $app->getParam('preg_u')); $replace = array($at, $dot); return preg_replace($search, $replace, $email); } /** * Truncates "a really long string" into a string of specified length * at the beginning: "…long string" * at the middle: "a rea…string" * or at the end: "a really…". * * The regular expressions below first match and replace the string to the specified length and position, * and secondly they remove any whitespace from around the delimiter (to avoid "this … " from happening). * * @access public * @param string $str Input string * @param int $len Maximum string length. * @param string $where Where to cut the string. One of: 'start', 'middle', or 'end'. * @param string $delim The delimiter to print where content is truncated. * @return string Truncated output string. * @author Quinn Comendant * @since 29 Mar 2006 13:48:49 */ function truncate($str, $len=50, $where='end', $delim='…') { $app =& App::getInstance(); if (!is_scalar($str)) { $app->logMsg(sprintf('truncate() non-scalar value: %s', getDump($str)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $str = (string)$str; if ('' === $str) { return ''; } $dlen = mb_strlen($delim); if ($len <= $dlen || mb_strlen($str) <= $dlen) { return substr($str, 0, $len); } $part1 = floor(($len - $dlen) / 2); $part2 = ceil(($len - $dlen) / 2); if ($len > ini_get('pcre.backtrack_limit')) { $app =& App::getInstance(); $app->logMsg(sprintf('Asked to truncate string len of %s > pcre.backtrack_limit of %s', $len, ini_get('pcre.backtrack_limit')), LOG_DEBUG, __FILE__, __LINE__); ini_set('pcre.backtrack_limit', $len); } switch ($where) { case 'start' : return preg_replace(array(sprintf('/^.{%s,}(.{%s})$/s' . $app->getParam('preg_u'), $dlen + 1, $part1 + $part2), sprintf('/\s*%s{%s,}\s*/s' . $app->getParam('preg_u'), preg_quote($delim), $dlen)), array($delim . '$1', $delim), $str); case 'middle' : return preg_replace(array(sprintf('/^(.{%s}).{%s,}(.{%s})$/s' . $app->getParam('preg_u'), $part1, $dlen + 1, $part2), sprintf('/\s*%s{%s,}\s*/s' . $app->getParam('preg_u'), preg_quote($delim), $dlen)), array('$1' . $delim . '$2', $delim), $str); case 'end' : default : return preg_replace(array(sprintf('/^(.{%s}).{%s,}$/s' . $app->getParam('preg_u'), $part1 + $part2, $dlen + 1), sprintf('/\s*%s{%s,}\s*/s' . $app->getParam('preg_u'), preg_quote($delim), $dlen)), array('$1' . $delim, $delim), $str); } } /* * A substitution for the missing mb_ucfirst function. * * @access public * @param string $string The string * @return string String with upper-cased first character. * @author Quinn Comendant * @version 1.0 * @since 06 Dec 2008 17:04:01 */ if (!function_exists('mb_ucfirst')) { function mb_ucfirst($string) { if (!is_scalar($string)) { $app =& App::getInstance(); $app->logMsg(sprintf('mb_ucfirst() non-scalar value: %s', getDump($string)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $string = (string)$string; if ('' === $string) { return ''; } return mb_strtoupper(mb_substr($string, 0, 1)) . mb_substr($string, 1, mb_strlen($string)); } } /* * A substitution for the missing mb_strtr function. * * @access public * @param string $string The string * @param string $from String of characters to translate from * @param string $to String of characters to translate to * @return string String with translated characters. * @author Quinn Comendant * @version 1.0 * @since 20 Jan 2013 12:33:26 */ if (!function_exists('mb_strtr')) { function mb_strtr($string, $from, $to) { if (!is_scalar($string)) { $app =& App::getInstance(); $app->logMsg(sprintf('mb_strtr() non-scalar value: %s', getDump($string)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $string = (string)$string; if ('' === $string) { return ''; } return str_replace(mb_split('.', $from), mb_split('.', $to), $string); } } /* * A substitution for the missing mb_str_pad function. * * @access public * @param string $input The string that receives padding. * @param string $pad_length Total length of resultant string. * @param string $pad_string The string to use for padding * @param string $pad_type Flags STR_PAD_RIGHT or STR_PAD_LEFT or STR_PAD_BOTH * @return string String with translated characters. * @author Quinn Comendant * @version 1.0 * @since 20 Jan 2013 12:33:26 */ if (!function_exists('mb_str_pad')) { function mb_str_pad($input, $pad_length, $pad_string=' ', $pad_type=STR_PAD_RIGHT) { $diff = strlen($input) - mb_strlen($input); return str_pad($input, $pad_length + $diff, $pad_string, $pad_type); } } /** * Return a human readable disk space measurement. Input value measured in bytes. * * @param int $size Size in bytes. * @param int $unit The maximum unit * @param int $format The return string format * @author Aidan Lister * @author Quinn Comendant * @version 1.2.0 */ function humanFileSize($size, $format='%01.2f %s', $max_unit=null, $multiplier=1024) { // Units $units = array('B', 'KB', 'MB', 'GB', 'TB'); $ii = count($units) - 1; // Max unit $max_unit = array_search((string) $max_unit, $units); if ($max_unit === null || $max_unit === false) { $max_unit = $ii; } // Loop $i = 0; while ($max_unit != $i && $size >= $multiplier && $i < $ii) { $size /= $multiplier; $i++; } return sprintf($format, $size, $units[$i]); } /* * Returns a human readable amount of time for the given amount of seconds. * * 45 seconds * 12 minutes * 3.5 hours * 2 days * 1 week * 4 months * * Months are calculated using the real number of days in a year: 365.2422 / 12. * * @access public * @param int $seconds Seconds of time. * @param string $max_unit Key value from the $units array. * @param string $format Sprintf formatting string. * @return string Value of units elapsed. * @author Quinn Comendant * @version 1.0 * @since 23 Jun 2006 12:15:19 */ function humanTime($seconds, $max_unit=null, $format='%01.1f') { // Units: array of seconds in the unit, singular and plural unit names. $units = array( 'second' => array(1, _("second"), _("seconds")), 'minute' => array(60, _("minute"), _("minutes")), 'hour' => array(3600, _("hour"), _("hours")), 'day' => array(86400, _("day"), _("days")), 'week' => array(604800, _("week"), _("weeks")), 'month' => array(2629743.84, _("month"), _("months")), 'year' => array(31556926.08, _("year"), _("years")), 'decade' => array(315569260.8, _("decade"), _("decades")), 'century' => array(3155692608, _("century"), _("centuries")), ); // Max unit to calculate. $max_unit = isset($units[$max_unit]) ? $max_unit : 'year'; $final_time = $seconds; $final_unit = 'second'; foreach ($units as $k => $v) { if ($seconds >= $v[0]) { $final_time = $seconds / $v[0]; $final_unit = $k; } if ($max_unit == $final_unit) { break; } } $final_time = sprintf($format, $final_time); return sprintf('%s %s', $final_time, (1 == $final_time ? $units[$final_unit][1] : $units[$final_unit][2])); } /* * Calculate a prorated amount for the duration between two dates. * * @access public * @param float $amount Original price per duration. * @param string $duration Unit of time for the original price (`year`, `quarter`, `month`, or `day`). * @param string $start_date Start date of prorated period (strtotime-compatible date). * @param string $end_date End date of prorated period (strtotime-compatible date). * @return float The prorated amount. * @author Quinn Comendant * @since 03 Nov 2021 22:44:30 */ function prorate($amount, $duration, $start_date, $end_date) { $app =& App::getInstance(); switch ($duration) { case 'yr': case 'year': $amount_per_day = $amount / 365; break; case 'quarter': $amount_per_day = $amount / 91.25; break; case 'mo': case 'month': $amount_per_day = $amount / 30.4167; break; case 'week': $amount_per_day = $amount / 7; break; case 'day': $amount_per_day = $amount; break; default: $app->logMsg(sprintf('Unknown prorate duration “%s”. Please use one of: year, yr, quarter, month, mo, week, day.', $duration), LOG_ERR, __FILE__, __LINE__); return false; } $diff_time = strtotime($end_date) - strtotime($start_date); $days = $diff_time / (60 * 60 * 24); return $amount_per_day * $days; } /* * Converts strange characters into ASCII using a htmlentities hack. If a character does not have a specific rule, it will remain as its entity name, e.g., `5¢` becomes `5¢` which becomes `5cent`. * * @access public * @param string $str Input string of text containing accents. * @return string String with accented characters converted to ASCII equivalents. * @author Quinn Comendant * @since 30 Apr 2020 21:29:16 */ function simplifyAccents($str) { $app =& App::getInstance(); if (!is_scalar($str)) { $app->logMsg(sprintf('simplifyAccents() non-scalar value: %s', getDump($str)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $str = (string)$str; if ('' === $str) { return ''; } return preg_replace([ '/&(?=[\w\d#]{1,10};)/i' . $app->getParam('preg_u'), '/&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml|caron);/i' . $app->getParam('preg_u'), '/&(?:ndash|mdash|horbar);/i' . $app->getParam('preg_u'), '/&(?:nbsp);/i' . $app->getParam('preg_u'), '/&(?:bdquo|ldquo|ldquor|lsquo|lsquor|rdquo|rdquor|rsquo|rsquor|sbquo|lsaquo|rsaquo);/i' . $app->getParam('preg_u'), '/&(?:amp);/i' . $app->getParam('preg_u'), // This replacement must come after matching all other entities. '/[&;]+/' . $app->getParam('preg_u'), ], [ '&', '$1', '-', ' ', '', 'and', '', ], htmlentities($str, ENT_NOQUOTES | ENT_IGNORE, $app->getParam('character_set'))); } /* * Converts a string into a URL-safe slug, removing spaces and non word characters. * * @access public * @param string $str String to convert. * @return string URL-safe slug. * @author Quinn Comendant * @version 1.0 * @since 18 Aug 2014 12:54:29 */ function URLSlug($str) { $app =& App::getInstance(); if (!is_scalar($str)) { $app->logMsg(sprintf('URLSlug() non-scalar value: %s', getDump($str)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $str = (string)$str; if ('' === $str) { return ''; } return strtolower(urlencode(preg_replace(['/[-\s–—.:;?!@#=+_\/\\\]+|(?: | |–|–|—|—|%c2%a0|%e2%80%93|%e2%80%9)+/' . $app->getParam('preg_u'), '/-+/' . $app->getParam('preg_u'), '/[^\w-]+/' . $app->getParam('preg_u'), '/^-+|-+$/' . $app->getParam('preg_u')], ['-', '-', '', ''], simplifyAccents($str)))); } /** * Converts a string of text into a safe file name by removing non-ASCII characters and non-word characters. * * @access public * @param string $file_name A name of a file. * @param string $separator The_separator_used_to_delimit_filename_parts. * @return string The same name, but cleaned. */ function cleanFileName($file_name, $separator='_') { $app =& App::getInstance(); $file_name = preg_replace([ sprintf('/[^a-zA-Z0-9()@._=+-]+/%s', $app->getParam('preg_u')), sprintf('/^%1$s+|%1$s+$/%2$s', $separator, $app->getParam('preg_u')), ], [ $separator, '' ], simplifyAccents($file_name)); return mb_substr($file_name, 0, 250); } /** * Returns the extension of a file name, or an empty string if none exists. * * @access public * @param string $file_name A name of a file, with extension after a dot. * @return string The value found after the dot */ function getFilenameExtension($file_name) { preg_match('/.*?\.(\w+)$/i', trim($file_name), $ext); return isset($ext[1]) ? $ext[1] : ''; } /* * Convert a php.ini value (8M, 512K, etc), into integer value of bytes. * * @access public * @param string $val Value from php config, e.g., upload_max_filesize. * @return int Value converted to bytes as an integer. * @author Quinn Comendant * @version 1.0 * @since 20 Aug 2014 14:32:41 */ function phpIniGetBytes($val) { $val = trim(ini_get($val)); if ($val != '') { $unit = strtolower($val[mb_strlen($val) - 1]); $val = preg_replace('/\D/', '', $val); switch ($unit) { // No `break`, so these multiplications are cumulative. case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } } return (int)$val; } /** * Tests the existence of a file anywhere in the include path. * Replaced by stream_resolve_include_path() in PHP 5 >= 5.3.2 * * @param string $file File in include path. * @return mixed False if file not found, the path of the file if it is found. * @author Quinn Comendant * @since 03 Dec 2005 14:23:26 */ function fileExistsIncludePath($file) { $app =& App::getInstance(); foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) { $fullpath = $path . DIRECTORY_SEPARATOR . $file; if (file_exists($fullpath)) { $app->logMsg(sprintf('Found file "%s" at path: %s', $file, $fullpath), LOG_DEBUG, __FILE__, __LINE__); return $fullpath; } else { $app->logMsg(sprintf('File "%s" not found in include_path: %s', $file, get_include_path()), LOG_DEBUG, __FILE__, __LINE__); return false; } } } /** * Returns stats of a file from the include path. * * @param string $file File in include path. * @param mixed $stat Which statistic to return (or null to return all). * @return mixed Value of requested key from fstat(), or false on error. * @author Quinn Comendant * @since 03 Dec 2005 14:23:26 */ function statIncludePath($file, $stat=null) { // Open file pointer read-only using include path. if ($fp = fopen($file, 'r', true)) { // File opened successfully, get stats. $stats = fstat($fp); fclose($fp); // Return specified stats. return is_null($stat) ? $stats : $stats[$stat]; } else { return false; } } /* * Writes content to the specified file. This function emulates the functionality of file_put_contents from PHP 5. * It makes an exclusive lock on the file while writing. * * @access public * @param string $filename Path to file. * @param string $content Data to write into file. * @return bool Success or failure. * @author Quinn Comendant * @since 11 Apr 2006 22:48:30 */ function filePutContents($filename, $content) { $app =& App::getInstance(); if (is_null($content) || is_bool($content) || is_object($content) || is_array($content)) { $app->logMsg(sprintf("Failed writing to file '%s'. Content is not a string.", $filename), LOG_WARNING, __FILE__, __LINE__); return false; } // Open file for writing and truncate to zero length. if ($fp = fopen($filename, 'w')) { if (flock($fp, LOCK_EX)) { if (!fwrite($fp, (string)$content)) { $app->logMsg(sprintf('Failed writing to file: %s', $filename), LOG_ERR, __FILE__, __LINE__); fclose($fp); return false; } flock($fp, LOCK_UN); } else { $app->logMsg(sprintf('Could not lock file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__); fclose($fp); return false; } fclose($fp); // Success! $app->logMsg(sprintf('Wrote to file: %s', $filename), LOG_DEBUG, __FILE__, __LINE__); return true; } else { $app->logMsg(sprintf('Could not open file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__); return false; } } /** * If $var is net set or null, set it to $default. Otherwise leave it alone. * Returns the final value of $var. Use to find a default value of one is not available. * * @param mixed $var The variable that is being set. * @param mixed $default What to set it to if $val is not currently set. * @return mixed The resulting value of $var. */ function setDefault(&$var, $default='') { if (!isset($var)) { $var = $default; } return $var; } /** * Like preg_quote() except for arrays, it takes an array of strings and puts * a backslash in front of every character that is part of the regular * expression syntax. * * @param array $array input array * @param array $delim optional character that will also be escaped. * @return array an array with the same values as $array1 but shuffled */ function pregQuoteArray($array, $delim='/') { if (!empty($array)) { if (is_array($array)) { foreach ($array as $key=>$val) { $quoted_array[$key] = preg_quote($val, $delim); } return $quoted_array; } else { return preg_quote($array, $delim); } } } /** * Converts a PHP Array into encoded URL arguments and return them as an array. * * @param mixed $data An array to transverse recursively, or a string * to use directly to create url arguments. * @param string $prefix The name of the first dimension of the array. * If not specified, the first keys of the array will be used. * @return array URL with array elements as URL key=value arguments. */ function urlEncodeArray($data, $prefix='', $_return=true) { // Data is stored in static variable. static $args = array(); if (is_array($data)) { foreach ($data as $key => $val) { // If the prefix is empty, use the $key as the name of the first dimension of the "array". // ...otherwise, append the key as a new dimension of the "array". $new_prefix = ('' === $prefix) ? urlencode($key) : $prefix . '[' . urlencode($key) . ']'; // Enter recursion. urlEncodeArray($val, $new_prefix, false); } } else { // We've come to the last dimension of the array, save the "array" and its value. $args[$prefix] = urlencode($data); } if ($_return) { // This is not a recursive execution. All recursion is complete. // Reset static var and return the result. $ret = $args; $args = array(); return is_array($ret) ? $ret : array(); } } /** * Converts a PHP Array into encoded URL arguments and return them in a string. * * Todo: probably update to use the built-in http_build_query(). * * @param mixed $data An array to transverse recursively, or a string * to use directly to create url arguments. * @param string $prefix The name of the first dimension of the array. * If not specified, the first keys of the array will be used. * @return string url A string ready to append to a url. */ function urlEncodeArrayToString($data, $prefix='') { $array_args = urlEncodeArray($data, $prefix); $url_args = ''; $delim = ''; foreach ($array_args as $key=>$val) { $url_args .= $delim . $key . '=' . $val; $delim = ini_get('arg_separator.output'); } return $url_args; } /* * Encode/decode a string that is safe for URLs. * * @access public * @param string $string Input string * @return string Encoded/decoded string. * @author Rasmus Schultz * @since 09 Jun 2022 07:50:49 */ function base64EncodeURL($data) { return str_replace(['+','/','='], ['-','_',''], base64_encode($data)); } function base64DecodeURL($string) { if (!is_string($string)) { $app =& App::getInstance(); $app->logMsg(sprintf('base64DecodeURL() non-string value: %s', getDump($string)), LOG_NOTICE, __FILE__, __LINE__); return ''; } if ('' === $string) { return ''; } return base64_decode(str_replace(['-','_'], ['+','/'], $string)); } /** * Fills an array with the result from a multiple ereg search. * Courtesy of Bruno - rbronosky@mac.com - 10-May-2001 * * @param mixed $pattern regular expression needle * @param mixed $string haystack * @return array populated with each found result */ function eregAll($pattern, $string) { do { if (!mb_ereg($pattern, $string, $temp)) { continue; } $string = str_replace($temp[0], '', $string); $results[] = $temp; } while (mb_ereg($pattern, $string, $temp)); return $results; } /** * Prints the word "checked" if a variable is set, and optionally matches * the desired value, otherwise prints nothing, * used for printing the word "checked" in a checkbox form input. * * @param mixed $var the variable to compare * @param mixed $value optional, what to compare with if a specific value is required. */ function frmChecked($var, $value=null) { if (func_num_args() == 1 && $var) { // 'Checked' if var is true. echo ' checked="checked" '; } else if (func_num_args() == 2 && $var == $value) { // 'Checked' if var and value match. echo ' checked="checked" '; } else if (func_num_args() == 2 && is_array($var)) { // 'Checked' if the value is in the key or the value of an array. if (isset($var[$value])) { echo ' checked="checked" '; } else if (in_array($value, $var)) { echo ' checked="checked" '; } } } /** * prints the word "selected" if a variable is set, and optionally matches * the desired value, otherwise prints nothing, * otherwise prints nothing, used for printing the word "checked" in a * select form input * * @param mixed $var the variable to compare * @param mixed $value optional, what to compare with if a specific value is required. */ function frmSelected($var, $value=null) { if (func_num_args() == 1 && $var) { // 'selected' if var is true. echo ' selected="selected" '; } else if (func_num_args() == 2 && $var == $value) { // 'selected' if var and value match. echo ' selected="selected" '; } else if (func_num_args() == 2 && is_array($var)) { // 'selected' if the value is in the key or the value of an array. if (isset($var[$value])) { echo ' selected="selected" '; } else if (in_array($value, $var)) { echo ' selected="selected" '; } } } /** * Adds slashes to values of an array and converts the array to a comma * delimited list. If value provided is a string return the string * escaped. This is useful for putting values coming in from posted * checkboxes into a SET column of a database. * * * @param array $in Array to convert. * @return string Comma list of array values. */ function escapedList($in, $separator="', '") { require_once dirname(__FILE__) . '/DB.inc.php'; $db =& DB::getInstance(); if (is_array($in) && !empty($in)) { return join($separator, array_map(array($db, 'escapeString'), $in)); } else { return $db->escapeString($in); } } /** * Converts a human string date into a SQL-safe date. Dates nearing * infinity use the date 2038-01-01 so conversion to unix time format * remain within valid range. * * @param array $date String date to convert. * @param array $format Date format to pass to date(). Default produces MySQL datetime: YYYY-MM-DD hh:mm:ss * @return string SQL-safe date. */ function strToSQLDate($date, $format='Y-m-d H:i:s') { require_once dirname(__FILE__) . '/DB.inc.php'; $db =& DB::getInstance(); $pdo =& \Strangecode\Codebase\PDO::getInstance(); // Mysql version >= 5.7.4 stopped allowing a "zero" date of 0000-00-00. // https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_zero_date if ($db->isConnected() && mb_strpos($db->getParam('zero_date'), '-') !== false) { $zero_date_parts = explode('-', $db->getParam('zero_date')); $zero_y = $zero_date_parts[0]; $zero_m = $zero_date_parts[1]; $zero_d = $zero_date_parts[2]; } else if ($pdo->isConnected() && mb_strpos($pdo->getParam('zero_date'), '-') !== false) { $zero_date_parts = explode('-', $pdo->getParam('zero_date')); $zero_y = $zero_date_parts[0]; $zero_m = $zero_date_parts[1]; $zero_d = $zero_date_parts[2]; } else { $zero_y = '0000'; $zero_m = '00'; $zero_d = '00'; } // Translate the human string date into SQL-safe date format. if (empty($date) || mb_strpos($date, sprintf('%s-%s-%s', $zero_y, $zero_m, $zero_d)) !== false || strtotime($date) === -1 || strtotime($date) === false || strtotime($date) === null) { // Return a string of zero time, formatted the same as $format. return strtr($format, array( 'Y' => $zero_y, 'm' => $zero_m, 'd' => $zero_d, 'H' => '00', 'i' => '00', 's' => '00', )); } else { return date($format, strtotime($date)); } } /** * If magic_quotes_gpc is in use, run stripslashes() on $var. If $var is an * array, stripslashes is run on each value, recursively, and the stripped * array is returned. * * @param mixed $var The string or array to un-quote, if necessary. * @return mixed $var, minus any magic quotes. */ function dispelMagicQuotes($var, $always=false) { static $magic_quotes_gpc; if (!isset($magic_quotes_gpc)) { $magic_quotes_gpc = version_compare(PHP_VERSION, '5.4.0', '<') ? get_magic_quotes_gpc() : false; } if ($always || $magic_quotes_gpc) { if (!is_array($var)) { $var = stripslashes($var); } else { foreach ($var as $key=>$val) { if (is_array($val)) { $var[$key] = dispelMagicQuotes($val, $always); } else { $var[$key] = stripslashes($val); } } } } return $var; } /** * Get a form variable from GET or POST data, stripped of magic * quotes if necessary. * * @param string $key The name of a $_REQUEST key (optional). * @param string $default The value to return if the variable is set (optional). * @return mixed A cleaned GET or POST array if no key specified. * @return string A cleaned form value if set, or $default. */ function getFormData($key=null, $default=null) { $app =& App::getInstance(); if (null === $key) { // Return entire array. switch (strtoupper(getenv('REQUEST_METHOD'))) { case 'POST': return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes')); case 'GET': return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes')); default: return dispelMagicQuotes($_REQUEST, $app->getParam('always_dispel_magicquotes')); } } if (isset($_REQUEST[$key])) { // $key is found in the flat array of REQUEST. return dispelMagicQuotes($_REQUEST[$key], $app->getParam('always_dispel_magicquotes')); } else if (mb_strpos($key, '[') !== false && isset($_REQUEST[strtok($key, '[')]) && preg_match_all('/\[([a-z0-9._~-]+)\]/', $key, $matches)) { // $key is formatted with sub-keys, e.g., getFormData('foo[bar][baz]') and top level key (`foo`) exists in REQUEST. // Extract these as sub-keys and access REQUEST as a multi-dimensional array, e.g., $_REQUEST[foo][bar][baz]. $leaf = $_REQUEST[strtok($key, '[')]; foreach ($matches[1] as $subkey) { if (is_array($leaf) && isset($leaf[$subkey])) { $leaf = $leaf[$subkey]; } else { $leaf = null; } } return $leaf; } else { return $default; } } function getPost($key=null, $default=null) { $app =& App::getInstance(); if (null === $key) { return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes')); } if (isset($_POST[$key])) { return dispelMagicQuotes($_POST[$key], $app->getParam('always_dispel_magicquotes')); } else { return $default; } } function getGet($key=null, $default=null) { $app =& App::getInstance(); if (null === $key) { return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes')); } if (isset($_GET[$key])) { return dispelMagicQuotes($_GET[$key], $app->getParam('always_dispel_magicquotes')); } else { return $default; } } /* * Sets a $_GET or $_POST variable. * * @access public * @param string $key The key of the request array to set. * @param mixed $val The value to save in the request array. * @return void * @author Quinn Comendant * @version 1.0 * @since 01 Nov 2009 12:25:29 */ function putFormData($key, $val) { switch (strtoupper(getenv('REQUEST_METHOD'))) { case 'POST': $_POST[$key] = $val; break; case 'GET': $_GET[$key] = $val; break; } $_REQUEST[$key] = $val; } /* * Trims whitespace from request data. * * @access public * @return void * @author Quinn Comendant * @version 1.0 * @since 12 Jan 2024 13:15:02 */ function trimFormData() { switch (strtoupper(getenv('REQUEST_METHOD'))) { case 'POST': array_walk_recursive($_POST, function(&$v) { if (isset($v)) { $v = trim($v); } }); break; case 'GET': array_walk_recursive($_GET, function(&$v) { if (isset($v)) { $v = trim($v); } }); break; } array_walk_recursive($_REQUEST, function(&$v) { if (isset($v)) { $v = trim($v); } }); } /* * Generates a base-64-encoded sha512 hash of $data truncated to $length. * * @access public * @param string $data Input value to hash. * @param int $length Length of output hash string. * @return string String of hash. * @author Quinn Comendant * @version 1.0 * @since 03 Apr 2016 19:48:49 */ function hash64($data, $length=18) { return mb_substr(preg_replace('/[^\w]/', '', base64_encode(hash('sha512', $data, true))), 0, $length); } /** * Signs a value using md5 and a simple text key. In order for this * function to be useful (i.e. secure) the salt must be kept secret, which * means keeping it as safe as database credentials. Putting it into an * environment variable set in httpd.conf is a good place. * * @access public * @param string $str The string to sign. * @param string $salt (Optional) A text key to use for computing the signature. * @param string $length (Optional) The length of the added signature. Longer signatures are safer. Must match the length passed to verifySignature() for the signatures to match. * @return string The original value with a signature appended. */ function addSignature($str, $salt=null, $length=18) { $app =& App::getInstance(); if (!is_scalar($str)) { $app =& App::getInstance(); $app->logMsg(sprintf('addSignature() non-scalar value: %s', getDump($str)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $str = (string)$str; if ('' === trim($str)) { $app->logMsg(sprintf('Cannot add signature to an empty string.', null), LOG_INFO, __FILE__, __LINE__); return ''; } if (!isset($salt)) { $salt = $app->getParam('signing_key'); } switch ($app->getParam('signing_method')) { case 'sha512+base64': return $str . '-' . mb_substr(preg_replace('/[^\w]/', '', base64_encode(hash('sha512', $str . $salt, true))), 0, $length); case 'md5': default: return $str . '-' . mb_strtolower(mb_substr(md5($salt . md5($str . $salt)), 0, $length)); } } /** * Strips off the signature appended by addSignature(). * * @access public * @param string $signed_val The string to sign. * @return string The original value with a signature removed. */ function removeSignature($signed_val) { if (!is_scalar($signed_val)) { $app =& App::getInstance(); $app->logMsg(sprintf('removeSignature() non-scalar value: %s', getDump($signed_val)), LOG_NOTICE, __FILE__, __LINE__); return ''; } $signed_val = (string)$signed_val; if ('' === $signed_val || mb_strpos($signed_val, '-') === false) { return ''; } return mb_substr($signed_val, 0, mb_strrpos($signed_val, '-')); } /** * Verifies a signature appended to a value by addSignature(). * * @access public * @param string $signed_val A value with appended signature. * @param string $salt (Optional) A text key to use for computing the signature. * @param string $length (Optional) The length of the added signature. * @return bool True if the signature matches the var. */ function verifySignature($signed_val, $salt=null, $length=18) { $app =& App::getInstance(); // Strip the value from the signed value. $str = removeSignature($signed_val); if ('' === $str) { // Removing the signature failed because it was empty or did not contain a hyphen. $app->logMsg(sprintf('Invalid signature ("%s" is not a valid signed value).', $signed_val), LOG_DEBUG, __FILE__, __LINE__); return false; } // If the signed value matches the original signed value we consider the value safe. if ('' != $signed_val && $signed_val == addSignature($str, $salt, $length)) { // Signature verified. return true; } else { // A signature mismatch might occur if the signing_key is not the same across all environments, apache, cli, etc. $app->logMsg(sprintf('Invalid signature (%s should be %s).', $signed_val, addSignature($str, $salt, $length)), LOG_DEBUG, __FILE__, __LINE__); return false; } } /** * Sends empty output to the browser and flushes the php buffer so the client * will see data before the page is finished processing. */ function flushBuffer() { echo str_repeat(' ', 205); flush(); } /** * A stub for apps that still use this function. * * @access public * @return void */ function mailmanAddMember($email, $list, $send_welcome_message=false) { $app =& App::getInstance(); $app->logMsg(sprintf('mailmanAddMember called and ignored: %s, %s, %s', $email, $list, $send_welcome_message), LOG_WARNING, __FILE__, __LINE__); } /** * A stub for apps that still use this function. * * @access public * @return void */ function mailmanRemoveMember($email, $list, $send_user_ack=false) { $app =& App::getInstance(); $app->logMsg(sprintf('mailmanRemoveMember called and ignored: %s, %s, %s', $email, $list, $send_user_ack), LOG_WARNING, __FILE__, __LINE__); } /* * Returns the remote IP address, taking into consideration proxy servers. * * If strict checking is enabled, we will only trust REMOTE_ADDR or an HTTP header value if * REMOTE_ADDR is a trusted proxy (configured as an array via $app->setParam(['trusted_proxies' => ['1.2.3.4', '5.6.7.8']]). * * @access public * @param bool $dolookup Resolve to IP to a hostname? * @param bool $trust_all_proxies Should we trust any IP address set in HTTP_* variables? Set to FALSE for secure usage. * @return mixed Canonicalized IP address (or a corresponding hostname if $dolookup is true), or false if no IP was found. * @author Alix Axel * @author Corey Ballou * @author Quinn Comendant * @version 1.0 * @since 12 Sep 2014 19:07:46 */ function getRemoteAddr($dolookup=false, $trust_all_proxies=true) { $app =& App::getInstance(); if (!isset($_SERVER['REMOTE_ADDR'])) { // In some cases this won't be set, e.g., CLI scripts. return ''; } // Use an HTTP header value only if $trust_all_proxies is true or when REMOTE_ADDR is in our $trusted_proxies array. // $trusted_proxies is an array of proxy server addresses we expect to see in REMOTE_ADDR. $trusted_proxies = $app->getParam('trusted_proxies', []); if ($trust_all_proxies || is_array($trusted_proxies) && in_array($_SERVER['REMOTE_ADDR'], $trusted_proxies, true)) { // Then it's probably safe to use an IP address value set in an HTTP header. // Loop through possible IP address headers from those most likely to contain the correct value first. // HTTP_CLIENT_IP: set by Apache Module mod_remoteip // HTTP_REAL_IP: set by Nginx Module ngx_http_realip_module // HTTP_CF_CONNECTING_IP: set by Cloudflare proxy // HTTP_X_FORWARDED_FOR: defacto standard for web proxies foreach (['HTTP_CLIENT_IP', 'HTTP_REAL_IP', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED'] as $key) { if (isset($_SERVER[$key]) && '' != $_SERVER[$key]) { foreach (explode(',', $_SERVER[$key]) as $addr) { // Strip non-address data to avoid "PHP Warning: inet_pton(): Unrecognized address for=189.211.197.173 in ./Utilities.inc.php on line 1293" $addr = preg_replace('/[^=]=/', '', $addr); $addr = canonicalIPAddr(trim($addr)); // Exclude invalid, private, or reserved IP addresses (a proxy server may be using a private IP). if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr; } } } } } $addr = canonicalIPAddr(trim($_SERVER['REMOTE_ADDR'])); if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4)) { return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr; } return ''; } /* * Converts an ipv4 IP address in hexadecimal form into canonical form (i.e., it removes the prefix). * * @access public * @param string $addr IP address. * @return string Canonical IP address. * @author Sander Steffann * @author Quinn Comendant * @version 1.0 * @since 15 Sep 2012 */ function canonicalIPAddr($addr) { if (!preg_match('/^([0-9a-f:]+|[0-9.])$/', $addr)) { // Definitely not an IPv6 or IPv4 address. return $addr; } // Known prefix $v4mapped_prefix_bin = pack('H*', '00000000000000000000ffff'); // Parse $addr_bin = inet_pton($addr); // Check prefix if (substr($addr_bin, 0, strlen($v4mapped_prefix_bin)) == $v4mapped_prefix_bin) { // Strip prefix $addr_bin = substr($addr_bin, strlen($v4mapped_prefix_bin)); } // Convert back to printable address in canonical form return inet_ntop($addr_bin); } /** * Tests whether a given IP address can be found in an array of IP address networks. * Elements of networks array can be single IP addresses or an IP address range in CIDR notation * See: http://en.wikipedia.org/wiki/Classless_inter-domain_routing * * @access public * @param string IP address to search for. * @param array Array of networks to search within. * @return mixed Returns the network that matched on success, false on failure. */ function ipInRange($addr, $networks) { if (null == $addr || '' === trim($addr)) { return false; } if (!is_array($networks)) { $networks = array($networks); } $addr_binary = sprintf('%032b', ip2long($addr)); foreach ($networks as $network) { if (mb_strpos($network, '/') !== false) { // IP is in CIDR notation. list($cidr_ip, $cidr_bitmask) = explode('/', $network); $cidr_ip_binary = sprintf('%032b', ip2long($cidr_ip)); if (mb_substr($addr_binary, 0, $cidr_bitmask) === mb_substr($cidr_ip_binary, 0, $cidr_bitmask)) { // IP address is within the specified IP range. return $network; } } else { if ($addr === $network) { // IP address exactly matches. return $network; } } } return false; } /** * If the given $url is on the same web site, return true. This can be used to * prevent from sending sensitive info in a get query (like the SID) to another * domain. * * @param string $url the URI to test. * @return bool True if given $url is our domain or has no domain (is a relative url), false if it's another. */ function isMyDomain($url) { static $urls = array(); if (!isset($urls[$url])) { if (!preg_match('!^https?://!i', $url)) { // If we can't find a domain we assume the URL is local (i.e. "/my/url/path/" or "../img/file.jpg"). $urls[$url] = true; } else { $urls[$url] = preg_match('!^https?://' . preg_quote(getenv('HTTP_HOST'), '!') . '!i', $url); } } return $urls[$url]; } /** * Takes a URL and returns it without the query or anchor portion * * @param string $url any kind of URI * @return string the URI with ? or # and everything after removed */ function stripQuery($url) { $app =& App::getInstance(); return preg_replace('/[?#].*$/' . $app->getParam('preg_u'), '', $url); } /* * Merge query arguments into a URL. * Usage: * Add ?lang=it or replace an existing ?lang= argument: * $url = urlMerge('https://example.com/?lang=en', ['lang' => 'it']). * * @access public * @param string $url Original URL. * @param array $new_args New/modified query arguments. * @return string Modified URL. * @author Quinn Comendant * @since 20 Feb 2021 21:21:53 */ function urlMergeQuery($url, Array $new_args) { $u = parse_url($url); if (isset($u['query']) && '' != $u['query']) { parse_str($u['query'], $args); } else { $args = []; } $u['query'] = http_build_query(array_merge($args, $new_args)); return sprintf('%s%s%s%s%s', (isset($u['scheme']) && '' != $u['scheme'] ? $u['scheme'] . '://' : ''), (isset($u['host']) && '' != $u['host'] ? $u['host'] : ''), (isset($u['path']) && '' != $u['path'] ? $u['path'] : ''), (isset($u['query']) && '' != $u['query'] ? '?' . $u['query'] : ''), (isset($u['fragment']) && '' != $u['fragment'] ? '#' . $u['fragment'] : '') ); } /* * Strip tracking query parameters from a URL. * * @access public * @param string $url URL which may contain query parameters. * @param mixed $tracking_params An array of tracking parameters to remove, or null to use a default set. * @return string The URL with query params removed. * @author Quinn Comendant * @since 02 Mar 2024 16:11:27 */ function removeURLTrackingParameters($url, $tracking_params=null) { // Use a default set of tracking params if not specified. $tracking_params = isset($tracking_params) ? $tracking_params : [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_source_platform', 'utm_marketing_tactic', 'utm_creative_format', 'gad_source', 'gclid', 'gbraid', 'wbraid', 'dclid', 'fbclid', 'msclkid', 'awc', 'pclk', 'mc_eid', 'twclid', 'igshid', ]; $u = parse_url($url); if (isset($u['query']) && '' != $u['query']) { parse_str($u['query'], $params); foreach ($tracking_params as $p) { unset($params[$p]); } $u['query'] = http_build_query($params); return sprintf('%s%s%s%s%s', (isset($u['scheme']) && '' != $u['scheme'] ? $u['scheme'] . '://' : ''), (isset($u['host']) && '' != $u['host'] ? $u['host'] : ''), (isset($u['path']) && '' != $u['path'] ? $u['path'] : ''), (isset($u['query']) && '' != $u['query'] ? '?' . $u['query'] : ''), (isset($u['fragment']) && '' != $u['fragment'] ? '#' . $u['fragment'] : '') ); } return $url; } /** * Returns a fully qualified URL to the current script, including the query. If you don't need the scheme://, use REQUEST_URI instead. * * @return string a full url to the current script */ function absoluteMe() { $app =& App::getInstance(); $safe_http_host = preg_replace('/[^a-z\d.:-]/' . $app->getParam('preg_u'), '', getenv('HTTP_HOST')); return sprintf('%s://%s%s', (getenv('HTTPS') ? 'https' : 'http'), $safe_http_host, getenv('REQUEST_URI')); } /** * Compares the current url with the referring url. * * @param bool $exclude_query Remove the query string first before comparing. * @return bool True if the current URL is the same as the referring URL, false otherwise. */ function refererIsMe($exclude_query=false) { $current_url = absoluteMe(); $referrer_url = getenv('HTTP_REFERER'); // If either is empty, don't continue with a comparison. if ('' == $current_url || '' == $referrer_url) { return false; } // If one of the hostnames is an IP address, compare only the path of both. if (preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', parse_url($current_url, PHP_URL_HOST)) || preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', parse_url($referrer_url, PHP_URL_HOST))) { $current_url = preg_replace('@^https?://[^/]+@u', '', $current_url); $referrer_url = preg_replace('@^https?://[^/]+@u', '', $referrer_url); } if ($exclude_query) { return (stripQuery($current_url) == stripQuery($referrer_url)); } else { $app =& App::getInstance(); $app->logMsg(sprintf('refererIsMe comparison: %s == %s', $current_url, $referrer_url), LOG_DEBUG, __FILE__, __LINE__); return ($current_url == $referrer_url); } } /* * Returns true if the given URL resolves to a resource with a HTTP 2xx or 3xx header response. * The download will abort if it retrieves >= 10KB of data to avoid downloading large files. * We couldn't use CURLOPT_NOBODY (a HEAD request) because some services don't behave without a GET request (ahem, BBC). * This function may not be very portable, if the server doesn't support CURLOPT_PROGRESSFUNCTION. * * @access public * @param string $url URL to a file. * @param int $timeout The maximum number of seconds to allow the HTTP query to execute. * @return bool True if the resource exists, false otherwise. * @author Quinn Comendant * @version 2.0 * @since 02 May 2015 15:10:09 */ function httpExists($url, $timeout=5) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Don't pass through data to the browser. curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Frequent progress function calls. curl_setopt($ch, CURLOPT_NOPROGRESS, false); // Required to use CURLOPT_PROGRESSFUNCTION. // Function arguments for CURLOPT_PROGRESSFUNCTION changed with php 5.5.0. if (version_compare(PHP_VERSION, '5.5.0', '>=')) { curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($ch, $dltot, $dlcur, $ultot, $ulcur){ // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error // 10KB should be enough to catch a few 302 redirect headers and get to the actual content. return ($dlcur > 10*1024) ? 1 : 0; }); } else { curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($dltot, $dlcur, $ultot, $ulcur){ // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error // 10KB should be enough to catch a few 302 redirect headers and get to the actual content. return ($dlcur > 10*1024) ? 1 : 0; }); } curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); return preg_match('/^[23]\d\d$/', $http_code); } /* * Get a HTTP response header. * * @access public * @param string $url URL to hit. * @param string $key Name of the header to return. * @param array $valid_response_codes Array of acceptable HTTP return codes. * @return string Value of the http header. * @author Quinn Comendant * @since 28 Oct 2020 20:00:36 */ function getHttpHeader($url, $key=null, Array $valid_response_codes=[200], $method='GET') { $context = stream_context_create(['http' => ['method' => $method]]); // Silence warning: PHP Warning: get_headers() expects at most 2 parameters, 3 given $headers = @get_headers($url, 1, $context); $app =& \App::getInstance(); $app->logMsg(sprintf('HTTP response headers for %s %s: %s', strtoupper($method), $url, getDump($headers, true, SC_DUMP_JSON)), LOG_DEBUG, __FILE__, __LINE__); if (empty($headers)) { return false; } // Status lines are found in numeric-indexed keys. $http_status_keys = preg_grep('/^\d$/', array_keys($headers)); // [0] => "HTTP/1.1 302 Found", [1] => "HTTP/1.1 200 OK", $final_http_status_key = end($http_status_keys); // E.g., `1` $final_http_status = $headers[$final_http_status_key]; // E.g., `HTTP/1.1 200 OK` $app->logMsg(sprintf('Last HTTP status code: %s', $final_http_status), LOG_DEBUG, __FILE__, __LINE__); if ($headers && preg_match(sprintf('/\b(%s)\b/', join('|', $valid_response_codes)), $final_http_status)) { $headers = array_change_key_case($headers, CASE_LOWER); if (!isset($key)) { return $headers; } $key = strtolower($key); if (isset($headers[$key])) { // If multiple redirects, the header key is an array; return only the last one. return is_array($headers[$key]) && isset($headers[$key][$final_http_status_key]) ? $headers[$key][$final_http_status_key] : $headers[$key]; } } return false; } /* * Load JSON data from a file and return it as an array (as specified by the json_decode options passed below.) * * @access public * @param string $filename Name of the file to load. Just exist in the include path. * @param bool $assoc When TRUE, returned objects will be converted into associative arrays. * @param int $depth Recursion depth. * @param const $options Bitmask of JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR. * @return array Array of data from the file, or null if there was a problem. * @author Quinn Comendant * @since 09 Oct 2019 21:32:47 */ function jsonDecodeFile($filename, $assoc=true, $depth=512, $options=0) { $app =& App::getInstance(); if (false === ($resolved_filename = stream_resolve_include_path($filename))) { $app->logMsg(sprintf('JSON file "%s" not found in path "%s"', $filename, get_include_path()), LOG_ERR, __FILE__, __LINE__); return null; } if (!is_readable($resolved_filename)) { $app->logMsg(sprintf('JSON file is unreadable: %s', $resolved_filename), LOG_ERR, __FILE__, __LINE__); return null; } if (null === ($data = json_decode(file_get_contents($resolved_filename), $assoc, $depth, $options))) { $app->logMsg(sprintf('JSON is unparsable: %s', $resolved_filename), LOG_ERR, __FILE__, __LINE__); return null; } return $data; } /* * Get IP address status from IP Intelligence. https://getipintel.net/free-proxy-vpn-tor-detection-api/#expected_output * * @access public * @param string $ip IP address to check. * @param float $threshold Return true if the IP score is above this threshold (0-1). * @param string $email Requester email address. * @return boolean True if the IP address appears to be a robot, proxy, or VPN. * False if the IP address is a residential or business IP address, or the API failed to return a valid response. * @author Quinn Comendant * @since 26 Oct 2019 15:39:17 */ function IPIntelligenceBadIP($ip, $threshold=0.95, $email='hello@strangecode.com') { $app =& App::getInstance(); $ch = curl_init(sprintf('http://check.getipintel.net/check.php?ip=%s&contact=%s', urlencode($ip), urlencode($email))); curl_setopt($ch, CURLOPT_TIMEOUT, 2); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $errorno = curl_errno($ch); $error = curl_error($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errorno == CURLE_OPERATION_TIMEOUTED) { $http_code = 408; } switch ($http_code) { case 200: case 400: // Check response value, below. break; case 408: $app->logMsg(sprintf('IP Intelligence timeout', null), LOG_NOTICE, __FILE__, __LINE__); return false; case 429: $app->logMsg(sprintf('IP Intelligence number of allowed queries exceeded (rate limit 15 requests/minute)', null), LOG_WARNING, __FILE__, __LINE__); return false; default: $app->logMsg(sprintf('IP Intelligence unexpected response (%s): %s: %s', $http_code, $error, $response), LOG_ERR, __FILE__, __LINE__); return false; } switch ($response) { case -1: $app->logMsg('IP Intelligence: Invalid no input', LOG_WARNING, __FILE__, __LINE__); return false; case -2: $app->logMsg('IP Intelligence: Invalid IP address', LOG_WARNING, __FILE__, __LINE__); return false; case -3: $app->logMsg('IP Intelligence: Unroutable or private address', LOG_NOTICE, __FILE__, __LINE__); return false; case -4: $app->logMsg('IP Intelligence: Unable to reach database', LOG_WARNING, __FILE__, __LINE__); return false; case -5: $app->logMsg('IP Intelligence: Banned: exceeded query limits, no permission, or invalid email address', LOG_WARNING, __FILE__, __LINE__); return false; case -6: $app->logMsg('IP Intelligence: Invalid contact information', LOG_WARNING, __FILE__, __LINE__); return false; default: if (!is_numeric($response) || $response < 0) { $app->logMsg(sprintf('IP Intelligence: Unknown status for IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__); return false; } if ($response >= $threshold) { $app->logMsg(sprintf('IP Intelligence: Bad IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__); return true; } $app->logMsg(sprintf('IP Intelligence: Good IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__); return false; } } /* * Test if a string is valid json. * https://stackoverflow.com/questions/6041741/fastest-way-to-check-if-a-string-is-json-in-php * * @access public * @param string $str The string to test. * @return boolean True if the string is valid json. * @author Quinn Comendant * @since 06 Dec 2020 18:41:51 */ function isJSON($str) { json_decode($str); return (json_last_error() === JSON_ERROR_NONE); } /* * Trim strings. Arrays of strings are trimmed recursively. Other data types remain untouched. * * @access public * @param mixed $var A variable of any data type, which may be a multidimensional array. * @return mixed String values trimmed, other data types returned as-is. * @author Quinn Comendant * @since 28 Mar 2024 20:32:29 */ function safeTrim($var) { // Directly trim if it's a string. if (is_string($var)) { return trim($var); } // Recursively trim elements if it's an array. if (is_array($var)) { array_walk_recursive($var, function (&$v) { if (is_string($v)) { $v = trim($v); } }); return $var; } // Return the value as-is for other data types. return $var; }