source: trunk/lib/Utilities.inc.php

Last change on this file was 814, checked in by anonymous, 7 weeks ago

Add safeTrim() function

File size: 72.5 KB
RevLine 
[1]1<?php
2/**
[362]3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
[396]5 * Copyright 2001-2012 Strangecode, LLC
[454]6 *
[362]7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
[454]13 *
[362]14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
[454]18 *
[362]19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
[1]24 * Utilities.inc.php
25 */
26
[807]27require_once dirname(__FILE__) . '/App.inc.php';
[1]28
29/**
30 * Print variable dump.
31 *
[781]32 * @param  mixed    $var            The variable to dump.
33 * @param  bool     $display        Print the dump in <pre> tags or hide it in html comments (non-CLI only).
34 * @param  const    $dump_method    Dump method. See SC_DUMP_* constants.
35 * @param  string   $file           Value of __FILE__.
36 * @param  string   $line           Value of __LINE__
[1]37 */
[613]38define('SC_DUMP_PRINT_R', 0);
39define('SC_DUMP_VAR_DUMP', 1);
40define('SC_DUMP_VAR_EXPORT', 2);
[743]41define('SC_DUMP_JSON', 3);
[793]42function dump($var, $display=false, $dump_method=SC_DUMP_JSON, $file='', $line='')
[1]43{
[548]44    $app =& App::getInstance();
45
[665]46    if ($app->isCLI()) {
[750]47        echo ('' != $file . $line) ? "DUMP FROM: $file $line\n" : "DUMP:\n";
[454]48    } else {
[477]49        echo $display ? "\n<br />DUMP <strong>$file $line</strong><br /><pre>\n" : "\n<!-- DUMP $file $line\n";
[380]50    }
[613]51
52    switch ($dump_method) {
53    case SC_DUMP_PRINT_R:
54    default:
[479]55        // Print human-readable descriptions of invisible types.
56        if (null === $var) {
57            echo '(null)';
58        } else if (true === $var) {
59            echo '(bool: true)';
60        } else if (false === $var) {
61            echo '(bool: false)';
62        } else if (is_scalar($var) && '' === $var) {
63            echo '(empty string)';
64        } else if (is_scalar($var) && preg_match('/^\s+$/', $var)) {
65            echo '(only white space)';
66        } else {
67            print_r($var);
68        }
[613]69        break;
70
71    case SC_DUMP_VAR_DUMP:
72        var_dump($var);
73        break;
74
75    case SC_DUMP_VAR_EXPORT:
76        var_export($var);
77        break;
[743]78
79    case SC_DUMP_JSON:
[793]80        echo json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
[743]81        break;
[1]82    }
[613]83
[665]84    if ($app->isCLI()) {
[380]85        echo "\n";
[454]86    } else {
[477]87        echo $display ? "\n</pre><br />\n" : "\n-->\n";
[380]88    }
[1]89}
90
[464]91/*
92* Log a PHP variable to javascript console. Relies on getDump(), below.
93*
94* @access   public
95* @param    mixed   $var      The variable to dump.
96* @param    string  $prefix   A short note to print before the output to make identifying output easier.
97* @param    string  $file     The value of __FILE__.
98* @param    string  $line     The value of __LINE__.
99* @return   null
100* @author   Quinn Comendant <quinn@strangecode.com>
101*/
102function jsDump($var, $prefix='jsDump', $file='-', $line='-')
103{
104    if (!empty($var)) {
105        ?>
[518]106        <script type="text/javascript">
[464]107        /* <![CDATA[ */
[518]108        console.log('<?php printf('%s: %s (on line %s of %s)', $prefix, str_replace("'", "\\'", getDump($var, true)), $line, $file); ?>');
[464]109        /* ]]> */
110        </script>
111        <?php
112    }
113}
114
115/*
116* Return a string version of any variable, optionally serialized on one line.
117*
118* @access   public
[781]119* @param    mixed   $var            The variable to dump.
120* @param    bool    $serialize      If true, remove line-endings. Useful for logging variables.
121* @param    const   $dump_method    Dump method. See SC_DUMP_* constants.
122* @return   string                  The dumped variable.
[464]123* @author   Quinn Comendant <quinn@strangecode.com>
124*/
[789]125function getDump($var, $serialize=false, $dump_method=SC_DUMP_JSON)
[1]126{
[724]127    $app =& App::getInstance();
128
[765]129    switch ($dump_method) {
130    case SC_DUMP_PRINT_R:
131        // Print human-readable descriptions of invisible types.
132        if (null === $var) {
133            $d = '(null)';
134        } else if (true === $var) {
135            $d = '(bool: true)';
136        } else if (false === $var) {
137            $d = '(bool: false)';
138        } else if (is_scalar($var) && '' === $var) {
139            $d = '(empty string)';
140        } else if (is_scalar($var) && preg_match('/^\s+$/', $var)) {
141            $d = '(only white space)';
142        } else {
143            ob_start();
144            print_r($var);
145            $d = ob_get_contents();
146            ob_end_clean();
147        }
148        break;
149
150    case SC_DUMP_VAR_DUMP:
151        ob_start();
152        print_r($var);
153        var_dump($var);
154        ob_end_clean();
155        break;
156
157    case SC_DUMP_VAR_EXPORT:
158        ob_start();
159        print_r($var);
160        var_export($var);
161        ob_end_clean();
162        break;
163
164    case SC_DUMP_JSON:
[789]165    default:
[781]166        $json_flags = $serialize ? 0 : JSON_PRETTY_PRINT;
167        return json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | $json_flags);
[765]168    }
[724]169    return $serialize ? preg_replace('/\s+/m' . $app->getParam('preg_u'), ' ', $d) : $d;
[1]170}
171
[652]172/*
173* Return dump as cleaned text. Useful for dumping data into emails or output from CLI scripts.
174* To output tab-style lists set $indent to "\t" and $depth to 0;
175* To output markdown-style lists set $indent to '- ' and $depth to 1;
176* Also see yaml_emit() https://secure.php.net/manual/en/function.yaml-emit.php
177*
178* @param  array    $var        Variable to dump.
179* @param  string   $indent     A string to prepend indented lines.
180* @param  string   $depth      Starting depth of this iteration of recursion (set to 0 to have no initial indentation).
181* @return string               Pretty dump of $var.
182* @author   Quinn Comendant <quinn@strangecode.com>
183* @version 2.0
184*/
185function fancyDump($var, $indent='- ', $depth=1)
[1]186{
[724]187    $app =& App::getInstance();
188
[807]189    $indent = trim($indent, ' ') . ' ';
190
[652]191    $indent_str = str_repeat($indent, $depth);
[1]192    $output = '';
193    if (is_array($var)) {
194        foreach ($var as $k=>$v) {
[811]195            $k = ucfirst(preg_replace([
196                '/_/',
197                '/ {2,}/',
198                '/\bip\b/',
199                '/\bid\b/',
200            ], [
201                ' ',
202                ' ',
203                'IP',
204                'ID',
205            ], mb_strtolower($k)));
[1]206            if (is_array($v)) {
[652]207                $output .= sprintf("\n%s%s:\n%s\n", $indent_str, $k, fancyDump($v, $indent, $depth+1));
[1]208            } else {
[652]209                $output .= sprintf("%s%s: %s\n", $indent_str, $k, $v);
[1]210            }
211        }
212    } else {
[652]213        $output .= sprintf("%s%s\n", $indent_str, $var);
[1]214    }
[807]215
216    return preg_replace([
217        '/^[ \t]+$/' . $app->getParam('preg_u'),
218        '/\n\n+/' . $app->getParam('preg_u'),
219        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')),
220    ], [
221        '',
222        "\n",
223        '$1$1$2$2$3$3$4$4$5$5$6$6$7$7$8$8$9'
224    ], $output);
[1]225}
226
227/**
[605]228 * @param string|mixed $value A string to UTF8-encode.
229 *
230 * @returns string|mixed The UTF8-encoded string, or the object passed in if
231 *    it wasn't a string.
232 */
233function conditionalUTF8Encode($value)
234{
235  if (is_string($value) && mb_detect_encoding($value, 'UTF-8', true) != 'UTF-8') {
236    return utf8_encode($value);
237  } else {
238    return $value;
239  }
240}
241
242
243/**
[505]244 * Returns text with appropriate html translations (a smart wrapper for htmlspecialchars()).
[1]245 *
[257]246 * @param  string $text             Text to clean.
[334]247 * @param  bool   $preserve_html    If set to true, oTxt will not translate <, >, ", or '
[485]248 *                                  characters into HTML entities. This allows HTML to pass through undisturbed.
249 * @return string                   HTML-safe text.
[1]250 */
[257]251function oTxt($text, $preserve_html=false)
[1]252{
[479]253    $app =& App::getInstance();
[136]254
[785]255    if ('' == $text) {
256        return '';
257    }
258
[1]259    $search = array();
260    $replace = array();
261
262    // Make converted ampersand entities into normal ampersands (they will be done manually later) to retain HTML entities.
[723]263    $search['retain_ampersand']     = '/&amp;/';
[1]264    $replace['retain_ampersand']    = '&';
265
266    if ($preserve_html) {
267        // Convert characters that must remain non-entities for displaying HTML.
[723]268        $search['retain_left_angle']       = '/&lt;/';
[1]269        $replace['retain_left_angle']      = '<';
[42]270
[723]271        $search['retain_right_angle']      = '/&gt;/';
[1]272        $replace['retain_right_angle']     = '>';
[42]273
[723]274        $search['retain_single_quote']     = '/&#039;/';
[1]275        $replace['retain_single_quote']    = "'";
[42]276
[723]277        $search['retain_double_quote']     = '/&quot;/';
[1]278        $replace['retain_double_quote']    = '"';
279    }
280
[334]281    // & becomes &amp;. Exclude any occurrence where the & is followed by a alphanum or unicode character.
[32]282    $search['ampersand']        = '/&(?![\w\d#]{1,10};)/';
283    $replace['ampersand']       = '&amp;';
[1]284
[334]285    return preg_replace($search, $replace, htmlspecialchars($text, ENT_QUOTES, $app->getParam('character_set')));
[1]286}
287
288/**
[334]289 * Returns text with stylistic modifications. Warning: this will break some HTML attributes!
[320]290 * TODO: Allow a string such as this to be passed: <a href="javascript:openPopup('/foo/bar.php')">Click here</a>
[1]291 *
[257]292 * @param  string   $text Text to clean.
[1]293 * @return string         Cleaned text.
294 */
[653]295function fancyTxt($text, $extra_search=null, $extra_replace=null)
[1]296{
[103]297    $search = array();
298    $replace = array();
299
[653]300    // "double quoted text"  →  “double quoted text”
301    $search['_double_quotes']    = '/(?<=^|[^\w=(])(?:"|&quot;|&#0?34;|&#x22;|&ldquo;)([\w\'.
(—–-][^"]*?)(?:"|&quot;|&#0?34;|&#x22;|&rdquo;)(?=[^)\w]|$)/imsu'; // " is the same as &quot; and &#34; and &#034; and &#x22;
302    $replace['_double_quotes']   = '“$1”';
[103]303
[653]304    // text's apostrophes  →  text’s apostrophes (except foot marks: 6'3")
305    $search['_apostrophe']       = '/(?<=[a-z])(?:\'|&#0?39;)(?=\w)/imsu';
306    $replace['_apostrophe']      = '’';
[103]307
[653]308    // 'single quoted text'  →  ‘single quoted text’
309    $search['_single_quotes']    = '/(?<=^|[^\w=(])(?:\'|&#0?39;|&lsquo;)([\w"][^\']+?)(?:\'|&#0?39;|&rsquo;)(?=[^)\w]|$)/imsu';
310    $replace['_single_quotes']   = '‘$1’';
[103]311
[653]312    // plural posessives' apostrophes  →  posessives’  (except foot marks: 6')
313    $search['_apostrophes']      = '/(?<=s)(?:\'|&#0?39;|&rsquo;)(?=\s)/imsu';
314    $replace['_apostrophes']     = '’';
[103]315
[774]316    // double--hyphens  →  en – dashes
[653]317    $search['_em_dash']          = '/(?<=[\w\s"\'”’)])--(?=[\w\s“”‘"\'(?])/imsu';
318    $replace['_em_dash']         = ' – ';
[103]319
[653]320    // ...  →  

321    $search['_elipsis']          = '/(?<=^|[^.])\.\.\.(?=[^.]|$)/imsu';
322    $replace['_elipsis']         = '
';
323
324    if (is_array($extra_search) && is_array($extra_replace) && sizeof($extra_search) == sizeof($extra_replace)) {
325        // Append additional search replacements.
326        $search = array_merge($search, $extra_search);
327        $replace = array_merge($replace, $extra_replace);
328    }
329
330    return trim(preg_replace($search, $replace, $text));
[1]331}
332
[505]333/*
334* Finds all URLs in text and hyperlinks them.
335*
336* @access   public
337* @param    string  $text   Text to search for URLs.
[541]338* @param    bool    $strict True to only include URLs starting with a scheme (http:// ftp:// im://), or false to include URLs starting with 'www.'.
[505]339* @param    mixed   $length Number of characters to truncate URL, or NULL to disable truncating.
340* @param    string  $delim  Delimiter to append, indicate truncation.
341* @return   string          Same input text, but URLs hyperlinked.
342* @author   Quinn Comendant <quinn@strangecode.com>
[647]343* @version  2.2
[505]344* @since    22 Mar 2015 23:29:04
345*/
[541]346function hyperlinkTxt($text, $strict=false, $length=null, $delim='
')
[505]347{
[545]348    // A list of schemes we allow at the beginning of a URL.
349    $schemes = 'mailto:|tel:|skype:|callto:|facetime:|bitcoin:|geo:|magnet:\?|sip:|sms:|xmpp:|view-source:(?:https?://)?|[\w-]{2,}://';
350
[541]351    // Capture the full URL into the first match and only the first X characters into the second match.
[647]352    // This will match URLs not preceded by " ' or = (URLs inside an attribute) or ` (Markdown quoted) or double-scheme (http://http://www.asdf.com)
[715]353    // https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid/1547940#1547940
[541]354    $regex = '@
[647]355        \b                                 # Start with a word-boundary.
356        (?<!"|\'|=|>|`|\]\(|\[\d\] |[:/]/) # Negative look-behind to exclude URLs already in <a> tag, <tags>beween</tags>, `Markdown quoted`, [Markdown](link), [1] www.markdown.footnotes, and avoid broken:/ and doubled://schemes://
357        (                                  # Begin match 1
358            (                              # Begin match 2
359                (?:%s)                     # URL starts with known scheme or www. if strict = false
360                [^\s/$.?#]+                # Any domain-valid characters
361                [^\s"`<>]{1,%s}            # Match 2 is limited to a maximum of LENGTH valid URL characters
[541]362            )
[647]363            [^\s"`<>]*                     # Match 1 continues with any further valid URL characters
364            ([^\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
[541]365        )
366        @Suxi
367    ';
368    $regex = sprintf($regex,
[545]369        ($strict ? $schemes : $schemes . '|www\.'), // Strict=false adds "www." to the list of allowed start-of-URL.
370        ($length ? $length : ''),
371        ($strict ? '' : '?!.,:;)\'-') // Strict=false excludes some "URL-valid" characters from the last character of URL. (Hyphen must remain last character in this class.)
[505]372    );
[541]373
374    // Use a callback function to decide when to append the delim.
375    // Also encode special chars with oTxt().
376    return preg_replace_callback($regex, function ($m) use ($length, $delim) {
377        $url = $m[1];
[545]378        $truncated_url = $m[2] . $m[3];
[541]379        $absolute_url = preg_replace('!^www\.!', 'http://www.', $url);
380        if (is_null($length) || $url == $truncated_url) {
381            // If not truncating, or URL was not truncated.
[545]382            // Remove http schemas, and any single trailing / to make the display URL.
[696]383            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], $url);
[763]384            return sprintf('<a href="%s">%s</a>', oTxt($absolute_url), oTxt($display_url));
[541]385        } else {
386            // Truncated URL.
[545]387            // Remove http schemas, and any single trailing / to make the display URL.
[696]388            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], trim($truncated_url));
[763]389            return sprintf('<a href="%s">%s%s</a>', oTxt($absolute_url), oTxt($display_url), $delim);
[541]390        }
391    }, $text);
[505]392}
393
[257]394/**
[334]395 * Applies a class to search terms to highlight them ala google results.
[257]396 *
397 * @param  string   $text   Input text to search.
398 * @param  string   $search String of word(s) that will be highlighted.
399 * @param  string   $class  CSS class to apply.
400 * @return string           Text with searched words wrapped in <span>.
401 */
402function highlightWords($text, $search, $class='sc-highlightwords')
403{
[724]404    $app =& App::getInstance();
405
[257]406    $words = preg_split('/[^\w]/', $search, -1, PREG_SPLIT_NO_EMPTY);
[454]407
[257]408    $search = array();
409    $replace = array();
[454]410
[257]411    foreach ($words as $w) {
[258]412        if ('' != trim($w)) {
[724]413            $search[] = '/\b(' . preg_quote($w) . ')\b/i' . $app->getParam('preg_u');
[763]414            $replace[] = '<span class="' . oTxt($class) . '">$1</span>';
[258]415        }
[257]416    }
[42]417
[258]418    return empty($replace) ? $text : preg_replace($search, $replace, $text);
[257]419}
420
[1]421/**
[334]422 * Generates a hexadecimal html color based on provided word.
[1]423 *
424 * @access public
425 * @param  string $text  A string for which to convert to color.
[759]426 * @param  float  $n     Brightness value between 0-1.
427 * @return string        A hexadecimal html color.
[1]428 */
[534]429function getTextColor($text, $method=1, $n=0.87)
[1]430{
[235]431    $hash = md5($text);
432    $rgb = array(
[247]433        mb_substr($hash, 0, 1),
434        mb_substr($hash, 1, 1),
435        mb_substr($hash, 2, 1),
436        mb_substr($hash, 3, 1),
437        mb_substr($hash, 4, 1),
438        mb_substr($hash, 5, 1),
[235]439    );
[1]440
441    switch ($method) {
[235]442    case 1 :
443    default :
[334]444        // Reduce all hex values slightly to avoid all white.
[696]445        array_walk($rgb, function (&$v) use ($n) {
446            $v = dechex(round(hexdec($v) * $n));
447        });
[235]448        break;
[696]449
[1]450    case 2 :
[235]451        foreach ($rgb as $i => $v) {
452            if (hexdec($v) > hexdec('c')) {
453                $rgb[$i] = dechex(hexdec('f') - hexdec($v));
454            }
[1]455        }
456        break;
457    }
458
[235]459    return join('', $rgb);
[1]460}
461
462/**
463 * Encodes a string into unicode values 128-255.
464 * Useful for hiding an email address from spambots.
465 *
466 * @access  public
467 * @param   string   $text   A line of text to encode.
468 * @return  string   Encoded text.
469 */
470function encodeAscii($text)
471{
[255]472    $output = '';
[247]473    $num = mb_strlen($text);
[1]474    for ($i=0; $i<$num; $i++) {
[729]475        $output .= sprintf('&#%03s', ord($text[$i]));
[1]476    }
477    return $output;
478}
479
480/**
[84]481 * Encodes an email into a "user at domain dot com" format.
[9]482 *
483 * @access  public
484 * @param   string   $email   An email to encode.
485 * @param   string   $at      Replaces the @.
486 * @param   string   $dot     Replaces the ..
487 * @return  string   Encoded email.
488 */
[53]489function encodeEmail($email, $at=' at ', $dot=' dot ')
[9]490{
[724]491    $app =& App::getInstance();
492
493    $search = array('/@/' . $app->getParam('preg_u'), '/\./' . $app->getParam('preg_u'));
[9]494    $replace = array($at, $dot);
495    return preg_replace($search, $replace, $email);
496}
497
498/**
[454]499 * Truncates "a really long string" into a string of specified length
500 * at the beginning: "
long string"
501 * at the middle: "a rea
string"
502 * or at the end: "a really
".
[84]503 *
[454]504 * The regular expressions below first match and replace the string to the specified length and position,
505 * and secondly they remove any whitespace from around the delimiter (to avoid "this 
 " from happening).
506 *
[84]507 * @access  public
508 * @param   string  $str    Input string
509 * @param   int     $len    Maximum string length.
510 * @param   string  $where  Where to cut the string. One of: 'start', 'middle', or 'end'.
[808]511 * @param   string  $delim  The delimiter to print where content is truncated.
[454]512 * @return  string          Truncated output string.
[84]513 * @author  Quinn Comendant <quinn@strangecode.com>
514 * @since   29 Mar 2006 13:48:49
515 */
[454]516function truncate($str, $len=50, $where='end', $delim='
')
[84]517{
[724]518    $app =& App::getInstance();
519
[454]520    $dlen = mb_strlen($delim);
521    if ($len <= $dlen || mb_strlen($str) <= $dlen) {
522        return substr($str, 0, $len);
[240]523    }
[454]524    $part1 = floor(($len - $dlen) / 2);
525    $part2 = ceil(($len - $dlen) / 2);
[531]526
527    if ($len > ini_get('pcre.backtrack_limit')) {
528        $app =& App::getInstance();
529        $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__);
530        ini_set('pcre.backtrack_limit', $len);
531    }
532
[84]533    switch ($where) {
534    case 'start' :
[724]535        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);
[454]536
[84]537    case 'middle' :
[724]538        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);
[454]539
[84]540    case 'end' :
[454]541    default :
[724]542        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);
[84]543    }
544}
545
[340]546/*
547* A substitution for the missing mb_ucfirst function.
548*
549* @access   public
[414]550* @param    string  $string The string
551* @return   string          String with upper-cased first character.
[340]552* @author   Quinn Comendant <quinn@strangecode.com>
553* @version  1.0
554* @since    06 Dec 2008 17:04:01
555*/
[454]556if (!function_exists('mb_ucfirst')) {
[340]557    function mb_ucfirst($string)
558    {
559        return mb_strtoupper(mb_substr($string, 0, 1)) . mb_substr($string, 1, mb_strlen($string));
560    }
561}
562
[414]563/*
564* A substitution for the missing mb_strtr function.
565*
566* @access   public
567* @param    string  $string The string
568* @param    string  $from   String of characters to translate from
569* @param    string  $to     String of characters to translate to
570* @return   string          String with translated characters.
571* @author   Quinn Comendant <quinn@strangecode.com>
572* @version  1.0
573* @since    20 Jan 2013 12:33:26
574*/
[454]575if (!function_exists('mb_strtr')) {
[414]576    function mb_strtr($string, $from, $to)
577    {
578        return str_replace(mb_split('.', $from), mb_split('.', $to), $string);
579    }
580}
581
[474]582/*
583* A substitution for the missing mb_str_pad function.
584*
585* @access   public
586* @param    string  $input      The string that receives padding.
587* @param    string  $pad_length Total length of resultant string.
588* @param    string  $pad_string The string to use for padding
589* @param    string  $pad_type   Flags STR_PAD_RIGHT or STR_PAD_LEFT or STR_PAD_BOTH
590* @return   string          String with translated characters.
591* @author   Quinn Comendant <quinn@strangecode.com>
592* @version  1.0
593* @since    20 Jan 2013 12:33:26
594*/
595if (!function_exists('mb_str_pad')) {
596    function mb_str_pad($input, $pad_length, $pad_string=' ', $pad_type=STR_PAD_RIGHT) {
597        $diff = strlen($input) - mb_strlen($input);
598        return str_pad($input, $pad_length + $diff, $pad_string, $pad_type);
599    }
600}
601
[84]602/**
[338]603 * Return a human readable disk space measurement. Input value measured in bytes.
[1]604 *
[338]605 * @param       int    $size        Size in bytes.
[1]606 * @param       int    $unit        The maximum unit
607 * @param       int    $format      The return string format
608 * @author      Aidan Lister <aidan@php.net>
[362]609 * @author      Quinn Comendant <quinn@strangecode.com>
610 * @version     1.2.0
[1]611 */
[338]612function humanFileSize($size, $format='%01.2f %s', $max_unit=null, $multiplier=1024)
[1]613{
614    // Units
615    $units = array('B', 'KB', 'MB', 'GB', 'TB');
616    $ii = count($units) - 1;
[42]617
[1]618    // Max unit
[154]619    $max_unit = array_search((string) $max_unit, $units);
620    if ($max_unit === null || $max_unit === false) {
621        $max_unit = $ii;
[1]622    }
[42]623
[1]624    // Loop
625    $i = 0;
[338]626    while ($max_unit != $i && $size >= $multiplier && $i < $ii) {
627        $size /= $multiplier;
[1]628        $i++;
629    }
[42]630
[1]631    return sprintf($format, $size, $units[$i]);
632}
633
[180]634/*
[189]635* Returns a human readable amount of time for the given amount of seconds.
[454]636*
[180]637* 45 seconds
638* 12 minutes
639* 3.5 hours
640* 2 days
641* 1 week
642* 4 months
[454]643*
[180]644* Months are calculated using the real number of days in a year: 365.2422 / 12.
645*
646* @access   public
[189]647* @param    int $seconds Seconds of time.
[180]648* @param    string $max_unit Key value from the $units array.
649* @param    string $format Sprintf formatting string.
650* @return   string Value of units elapsed.
651* @author   Quinn Comendant <quinn@strangecode.com>
652* @version  1.0
653* @since    23 Jun 2006 12:15:19
654*/
[189]655function humanTime($seconds, $max_unit=null, $format='%01.1f')
[180]656{
[202]657    // Units: array of seconds in the unit, singular and plural unit names.
[180]658    $units = array(
659        'second' => array(1, _("second"), _("seconds")),
660        'minute' => array(60, _("minute"), _("minutes")),
661        'hour' => array(3600, _("hour"), _("hours")),
662        'day' => array(86400, _("day"), _("days")),
663        'week' => array(604800, _("week"), _("weeks")),
664        'month' => array(2629743.84, _("month"), _("months")),
665        'year' => array(31556926.08, _("year"), _("years")),
666        'decade' => array(315569260.8, _("decade"), _("decades")),
[362]667        'century' => array(3155692608, _("century"), _("centuries")),
[180]668    );
[454]669
[202]670    // Max unit to calculate.
[362]671    $max_unit = isset($units[$max_unit]) ? $max_unit : 'year';
[180]672
[189]673    $final_time = $seconds;
[363]674    $final_unit = 'second';
[180]675    foreach ($units as $k => $v) {
[363]676        if ($seconds >= $v[0]) {
[189]677            $final_time = $seconds / $v[0];
[363]678            $final_unit = $k;
[180]679        }
[363]680        if ($max_unit == $final_unit) {
681            break;
682        }
[180]683    }
[189]684    $final_time = sprintf($format, $final_time);
[454]685    return sprintf('%s %s', $final_time, (1 == $final_time ? $units[$final_unit][1] : $units[$final_unit][2]));
[180]686}
687
[722]688/*
[757]689* Calculate a prorated amount for the duration between two dates.
[752]690*
691* @access   public
[757]692* @param    float   $amount     Original price per duration.
693* @param    string  $duration   Unit of time for the original price (`year`, `quarter`, `month`, or `day`).
694* @param    string  $start_date Start date of prorated period (strtotime-compatible date).
695* @param    string  $end_date   End date of prorated period (strtotime-compatible date).
696* @return   float               The prorated amount.
[752]697* @author   Quinn Comendant <quinn@strangecode.com>
698* @since    03 Nov 2021 22:44:30
699*/
[757]700function prorate($amount, $duration, $start_date, $end_date)
[752]701{
702    $app =& App::getInstance();
703
704    switch ($duration) {
705    case 'yr':
706    case 'year':
707        $amount_per_day = $amount / 365;
708        break;
709
710    case 'quarter':
711        $amount_per_day = $amount / 91.25;
712        break;
713
714    case 'mo':
715    case 'month':
716        $amount_per_day = $amount / 30.4167;
717        break;
718
719    case 'week':
720        $amount_per_day = $amount / 7;
721        break;
722
723    case 'day':
724        $amount_per_day = $amount;
725        break;
726
727    default:
728        $app->logMsg(sprintf('Unknown prorate duration “%s”. Please use one of: year, yr, quarter, month, mo, week, day.', $duration), LOG_ERR, __FILE__, __LINE__);
729        return false;
730    }
731
732    $diff_time = strtotime($end_date) - strtotime($start_date);
733    $days = $diff_time / (60 * 60 * 24);
734    return $amount_per_day * $days;
735}
736
737/*
[722]738* 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&cent;` which becomes `5cent`.
739*
740* @access   public
741* @param    string  $str    Input string of text containing accents.
742* @return   string          String with accented characters converted to ASCII equivalents.
743* @author   Quinn Comendant <quinn@strangecode.com>
744* @since    30 Apr 2020 21:29:16
745*/
746function simplifyAccents($str)
747{
748    $app =& App::getInstance();
749
750    return preg_replace([
[724]751        '/&amp;(?=[\w\d#]{1,10};)/i' . $app->getParam('preg_u'),
752        '/&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml|caron);/i' . $app->getParam('preg_u'),
753        '/&(?:ndash|mdash|horbar);/i' . $app->getParam('preg_u'),
754        '/&(?:nbsp);/i' . $app->getParam('preg_u'),
755        '/&(?:bdquo|ldquo|ldquor|lsquo|lsquor|rdquo|rdquor|rsquo|rsquor|sbquo|lsaquo|rsaquo);/i' . $app->getParam('preg_u'),
756        '/&(?:amp);/i' . $app->getParam('preg_u'), // This replacement must come after matching all other entities.
757        '/[&;]+/' . $app->getParam('preg_u'),
[722]758    ], [
759        '&',
760        '$1',
761        '-',
762        ' ',
763        '',
764        'and',
765        '',
766    ], htmlentities($str, ENT_NOQUOTES | ENT_IGNORE, $app->getParam('character_set')));
767}
768
769/*
770* Converts a string into a URL-safe slug, removing spaces and non word characters.
771*
772* @access   public
773* @param    string  $str    String to convert.
774* @return   string          URL-safe slug.
775* @author   Quinn Comendant <quinn@strangecode.com>
776* @version  1.0
777* @since    18 Aug 2014 12:54:29
778*/
779function URLSlug($str)
780{
[724]781    $app =& App::getInstance();
782
783    return strtolower(urlencode(preg_replace(['/[-\s–—.:;?!@#=+_\/\\\]+|(?:&nbsp;|&#160;|&ndash;|&#8211;|&mdash;|&#8212;|%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))));
[722]784}
785
[518]786/**
[722]787 * Converts a string of text into a safe file name by removing non-ASCII characters and non-word characters.
[518]788 *
789 * @access  public
790 * @param   string  $file_name  A name of a file.
[773]791 * @param   string  $separator  The_separator_used_to_delimit_filename_parts.
[518]792 * @return  string              The same name, but cleaned.
793 */
[773]794function cleanFileName($file_name, $separator='_')
[518]795{
796    $app =& App::getInstance();
797
[773]798    $file_name = preg_replace([
799        sprintf('/[^a-zA-Z0-9()@._=+-]+/%s', $app->getParam('preg_u')),
800        sprintf('/^%1$s+|%1$s+$/%2$s', $separator, $app->getParam('preg_u')),
801    ], [
802        $separator,
803        ''
804    ], simplifyAccents($file_name));
[518]805    return mb_substr($file_name, 0, 250);
806}
807
[519]808/**
809 * Returns the extension of a file name, or an empty string if none exists.
810 *
811 * @access  public
812 * @param   string  $file_name  A name of a file, with extension after a dot.
813 * @return  string              The value found after the dot
814 */
815function getFilenameExtension($file_name)
816{
817    preg_match('/.*?\.(\w+)$/i', trim($file_name), $ext);
818    return isset($ext[1]) ? $ext[1] : '';
819}
820
[487]821/*
822* Convert a php.ini value (8M, 512K, etc), into integer value of bytes.
823*
824* @access   public
825* @param    string  $val    Value from php config, e.g., upload_max_filesize.
826* @return   int             Value converted to bytes as an integer.
827* @author   Quinn Comendant <quinn@strangecode.com>
828* @version  1.0
829* @since    20 Aug 2014 14:32:41
830*/
831function phpIniGetBytes($val)
832{
833    $val = trim(ini_get($val));
834    if ($val != '') {
[729]835        $unit = strtolower($val[mb_strlen($val) - 1]);
[718]836        $val = preg_replace('/\D/', '', $val);
837
838        switch ($unit) {
839            // No `break`, so these multiplications are cumulative.
840            case 'g':
841                $val *= 1024;
842            case 'm':
843                $val *= 1024;
844            case 'k':
845                $val *= 1024;
846        }
[487]847    }
848
849    return (int)$val;
850}
851
[1]852/**
[334]853 * Tests the existence of a file anywhere in the include path.
[523]854 * Replaced by stream_resolve_include_path() in PHP 5 >= 5.3.2
[258]855 *
856 * @param   string  $file   File in include path.
857 * @return  mixed           False if file not found, the path of the file if it is found.
858 * @author  Quinn Comendant <quinn@strangecode.com>
859 * @since   03 Dec 2005 14:23:26
860 */
861function fileExistsIncludePath($file)
862{
863    $app =& App::getInstance();
[454]864
[258]865    foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) {
866        $fullpath = $path . DIRECTORY_SEPARATOR . $file;
867        if (file_exists($fullpath)) {
868            $app->logMsg(sprintf('Found file "%s" at path: %s', $file, $fullpath), LOG_DEBUG, __FILE__, __LINE__);
869            return $fullpath;
870        } else {
871            $app->logMsg(sprintf('File "%s" not found in include_path: %s', $file, get_include_path()), LOG_DEBUG, __FILE__, __LINE__);
872            return false;
873        }
874    }
875}
876
877/**
[26]878 * Returns stats of a file from the include path.
879 *
880 * @param   string  $file   File in include path.
[258]881 * @param   mixed   $stat   Which statistic to return (or null to return all).
882 * @return  mixed           Value of requested key from fstat(), or false on error.
[26]883 * @author  Quinn Comendant <quinn@strangecode.com>
884 * @since   03 Dec 2005 14:23:26
885 */
[241]886function statIncludePath($file, $stat=null)
[26]887{
888    // Open file pointer read-only using include path.
889    if ($fp = fopen($file, 'r', true)) {
[258]890        // File opened successfully, get stats.
[26]891        $stats = fstat($fp);
892        fclose($fp);
893        // Return specified stats.
[241]894        return is_null($stat) ? $stats : $stats[$stat];
[26]895    } else {
896        return false;
897    }
898}
899
[330]900/*
901* Writes content to the specified file. This function emulates the functionality of file_put_contents from PHP 5.
[400]902* It makes an exclusive lock on the file while writing.
[330]903*
904* @access   public
905* @param    string  $filename   Path to file.
906* @param    string  $content    Data to write into file.
907* @return   bool                Success or failure.
908* @author   Quinn Comendant <quinn@strangecode.com>
909* @since    11 Apr 2006 22:48:30
910*/
911function filePutContents($filename, $content)
912{
[479]913    $app =& App::getInstance();
[330]914
[789]915    if (is_null($content) || is_bool($content) || is_object($content) || is_array($content)) {
916        $app->logMsg(sprintf("Failed writing to file '%s'. Content is not a string.", $filename), LOG_WARNING, __FILE__, __LINE__);
917        return false;
918    }
919
[330]920    // Open file for writing and truncate to zero length.
921    if ($fp = fopen($filename, 'w')) {
922        if (flock($fp, LOCK_EX)) {
[789]923            if (!fwrite($fp, (string)$content)) {
[330]924                $app->logMsg(sprintf('Failed writing to file: %s', $filename), LOG_ERR, __FILE__, __LINE__);
925                fclose($fp);
926                return false;
927            }
928            flock($fp, LOCK_UN);
929        } else {
930            $app->logMsg(sprintf('Could not lock file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
931            fclose($fp);
932            return false;
933        }
934        fclose($fp);
935        // Success!
936        $app->logMsg(sprintf('Wrote to file: %s', $filename), LOG_DEBUG, __FILE__, __LINE__);
937        return true;
938    } else {
939        $app->logMsg(sprintf('Could not open file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
940        return false;
941    }
942}
943
[26]944/**
[1]945 * If $var is net set or null, set it to $default. Otherwise leave it alone.
[334]946 * Returns the final value of $var. Use to find a default value of one is not available.
[1]947 *
948 * @param  mixed $var       The variable that is being set.
949 * @param  mixed $default   What to set it to if $val is not currently set.
[42]950 * @return mixed            The resulting value of $var.
[1]951 */
952function setDefault(&$var, $default='')
953{
954    if (!isset($var)) {
955        $var = $default;
956    }
957    return $var;
958}
959
960/**
961 * Like preg_quote() except for arrays, it takes an array of strings and puts
962 * a backslash in front of every character that is part of the regular
963 * expression syntax.
964 *
965 * @param  array $array    input array
[334]966 * @param  array $delim    optional character that will also be escaped.
[1]967 * @return array    an array with the same values as $array1 but shuffled
968 */
969function pregQuoteArray($array, $delim='/')
970{
971    if (!empty($array)) {
972        if (is_array($array)) {
973            foreach ($array as $key=>$val) {
974                $quoted_array[$key] = preg_quote($val, $delim);
975            }
976            return $quoted_array;
977        } else {
978            return preg_quote($array, $delim);
979        }
980    }
981}
982
983/**
984 * Converts a PHP Array into encoded URL arguments and return them as an array.
985 *
[334]986 * @param  mixed $data        An array to transverse recursively, or a string
[1]987 *                            to use directly to create url arguments.
988 * @param  string $prefix     The name of the first dimension of the array.
989 *                            If not specified, the first keys of the array will be used.
990 * @return array              URL with array elements as URL key=value arguments.
991 */
[235]992function urlEncodeArray($data, $prefix='', $_return=true)
993{
[1]994    // Data is stored in static variable.
[590]995    static $args = array();
[42]996
[1]997    if (is_array($data)) {
998        foreach ($data as $key => $val) {
[334]999            // If the prefix is empty, use the $key as the name of the first dimension of the "array".
1000            // ...otherwise, append the key as a new dimension of the "array".
[1]1001            $new_prefix = ('' == $prefix) ? urlencode($key) : $prefix . '[' . urlencode($key) . ']';
1002            // Enter recursion.
1003            urlEncodeArray($val, $new_prefix, false);
1004        }
1005    } else {
[334]1006        // We've come to the last dimension of the array, save the "array" and its value.
[1]1007        $args[$prefix] = urlencode($data);
1008    }
[42]1009
[1]1010    if ($_return) {
1011        // This is not a recursive execution. All recursion is complete.
1012        // Reset static var and return the result.
1013        $ret = $args;
1014        $args = array();
1015        return is_array($ret) ? $ret : array();
1016    }
1017}
1018
1019/**
1020 * Converts a PHP Array into encoded URL arguments and return them in a string.
1021 *
[580]1022 * Todo: probably update to use the built-in http_build_query().
1023 *
[334]1024 * @param  mixed $data        An array to transverse recursively, or a string
[1]1025 *                            to use directly to create url arguments.
[334]1026 * @param  string $prefix     The name of the first dimension of the array.
[1]1027 *                            If not specified, the first keys of the array will be used.
1028 * @return string url         A string ready to append to a url.
1029 */
[235]1030function urlEncodeArrayToString($data, $prefix='')
1031{
[1]1032    $array_args = urlEncodeArray($data, $prefix);
1033    $url_args = '';
1034    $delim = '';
1035    foreach ($array_args as $key=>$val) {
1036        $url_args .= $delim . $key . '=' . $val;
1037        $delim = ini_get('arg_separator.output');
1038    }
1039    return $url_args;
1040}
1041
[768]1042/*
1043* Encode/decode a string that is safe for URLs.
1044*
1045* @access   public
1046* @param    string   $string    Input string
1047* @return   string              Encoded/decoded string.
1048* @author   Rasmus Schultz <https://www.php.net/manual/en/function.base64-encode.php#123098>
1049* @since    09 Jun 2022 07:50:49
1050*/
[783]1051function base64EncodeURL($string) {
[768]1052    return str_replace(['+','/','='], ['-','_',''], base64_encode($string));
1053}
[783]1054function base64DecodeURL($string) {
[768]1055    return base64_decode(str_replace(['-','_'], ['+','/'], $string));
1056}
1057
[1]1058/**
[334]1059 * Fills an array with the result from a multiple ereg search.
1060 * Courtesy of Bruno - rbronosky@mac.com - 10-May-2001
[1]1061 *
1062 * @param  mixed $pattern   regular expression needle
1063 * @param  mixed $string   haystack
1064 * @return array    populated with each found result
1065 */
1066function eregAll($pattern, $string)
1067{
1068    do {
[247]1069        if (!mb_ereg($pattern, $string, $temp)) {
[1]1070             continue;
1071        }
1072        $string = str_replace($temp[0], '', $string);
1073        $results[] = $temp;
[247]1074    } while (mb_ereg($pattern, $string, $temp));
[1]1075    return $results;
1076}
1077
1078/**
1079 * Prints the word "checked" if a variable is set, and optionally matches
1080 * the desired value, otherwise prints nothing,
[42]1081 * used for printing the word "checked" in a checkbox form input.
[1]1082 *
1083 * @param  mixed $var     the variable to compare
1084 * @param  mixed $value   optional, what to compare with if a specific value is required.
1085 */
1086function frmChecked($var, $value=null)
1087{
1088    if (func_num_args() == 1 && $var) {
1089        // 'Checked' if var is true.
1090        echo ' checked="checked" ';
1091    } else if (func_num_args() == 2 && $var == $value) {
1092        // 'Checked' if var and value match.
1093        echo ' checked="checked" ';
1094    } else if (func_num_args() == 2 && is_array($var)) {
1095        // 'Checked' if the value is in the key or the value of an array.
1096        if (isset($var[$value])) {
1097            echo ' checked="checked" ';
1098        } else if (in_array($value, $var)) {
1099            echo ' checked="checked" ';
1100        }
1101    }
1102}
1103
1104/**
1105 * prints the word "selected" if a variable is set, and optionally matches
1106 * the desired value, otherwise prints nothing,
[42]1107 * otherwise prints nothing, used for printing the word "checked" in a
1108 * select form input
[1]1109 *
1110 * @param  mixed $var     the variable to compare
1111 * @param  mixed $value   optional, what to compare with if a specific value is required.
1112 */
1113function frmSelected($var, $value=null)
1114{
1115    if (func_num_args() == 1 && $var) {
1116        // 'selected' if var is true.
1117        echo ' selected="selected" ';
1118    } else if (func_num_args() == 2 && $var == $value) {
1119        // 'selected' if var and value match.
1120        echo ' selected="selected" ';
1121    } else if (func_num_args() == 2 && is_array($var)) {
1122        // 'selected' if the value is in the key or the value of an array.
1123        if (isset($var[$value])) {
1124            echo ' selected="selected" ';
1125        } else if (in_array($value, $var)) {
1126            echo ' selected="selected" ';
1127        }
1128    }
1129}
1130
1131/**
[111]1132 * Adds slashes to values of an array and converts the array to a comma
1133 * delimited list. If value provided is a string return the string
1134 * escaped.  This is useful for putting values coming in from posted
1135 * checkboxes into a SET column of a database.
[1]1136 *
[454]1137 *
[111]1138 * @param  array $in      Array to convert.
[1]1139 * @return string         Comma list of array values.
1140 */
[224]1141function escapedList($in, $separator="', '")
[1]1142{
[600]1143    require_once dirname(__FILE__) . '/DB.inc.php';
[479]1144    $db =& DB::getInstance();
[454]1145
[111]1146    if (is_array($in) && !empty($in)) {
[224]1147        return join($separator, array_map(array($db, 'escapeString'), $in));
[111]1148    } else {
[136]1149        return $db->escapeString($in);
[1]1150    }
1151}
1152
1153/**
[111]1154 * Converts a human string date into a SQL-safe date.  Dates nearing
1155 * infinity use the date 2038-01-01 so conversion to unix time format
1156 * remain within valid range.
[1]1157 *
1158 * @param  array $date     String date to convert.
[600]1159 * @param  array $format   Date format to pass to date(). Default produces MySQL datetime: YYYY-MM-DD hh:mm:ss
[1]1160 * @return string          SQL-safe date.
1161 */
1162function strToSQLDate($date, $format='Y-m-d H:i:s')
1163{
[600]1164    require_once dirname(__FILE__) . '/DB.inc.php';
1165    $db =& DB::getInstance();
[788]1166    $pdo =& \Strangecode\Codebase\PDO::getInstance();
[600]1167
[788]1168    // Mysql version >= 5.7.4 stopped allowing a "zero" date of 0000-00-00.
1169    // https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_zero_date
[601]1170    if ($db->isConnected() && mb_strpos($db->getParam('zero_date'), '-') !== false) {
1171        $zero_date_parts = explode('-', $db->getParam('zero_date'));
1172        $zero_y = $zero_date_parts[0];
1173        $zero_m = $zero_date_parts[1];
1174        $zero_d = $zero_date_parts[2];
[788]1175    } else if ($pdo->isConnected() && mb_strpos($pdo->getParam('zero_date'), '-') !== false) {
1176        $zero_date_parts = explode('-', $pdo->getParam('zero_date'));
1177        $zero_y = $zero_date_parts[0];
1178        $zero_m = $zero_date_parts[1];
1179        $zero_d = $zero_date_parts[2];
[600]1180    } else {
1181        $zero_y = '0000';
1182        $zero_m = '00';
1183        $zero_d = '00';
1184    }
[1]1185    // Translate the human string date into SQL-safe date format.
[600]1186    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) {
[224]1187        // Return a string of zero time, formatted the same as $format.
1188        return strtr($format, array(
[600]1189            'Y' => $zero_y,
1190            'm' => $zero_m,
1191            'd' => $zero_d,
[224]1192            'H' => '00',
1193            'i' => '00',
1194            's' => '00',
1195        ));
[1]1196    } else {
[219]1197        return date($format, strtotime($date));
[1]1198    }
1199}
1200
1201/**
1202 * If magic_quotes_gpc is in use, run stripslashes() on $var. If $var is an
[334]1203 * array, stripslashes is run on each value, recursively, and the stripped
[51]1204 * array is returned.
[1]1205 *
1206 * @param  mixed $var   The string or array to un-quote, if necessary.
1207 * @return mixed        $var, minus any magic quotes.
1208 */
[523]1209function dispelMagicQuotes($var, $always=false)
[1]1210{
1211    static $magic_quotes_gpc;
[42]1212
[1]1213    if (!isset($magic_quotes_gpc)) {
[738]1214        $magic_quotes_gpc = version_compare(PHP_VERSION, '5.4.0', '<') ? get_magic_quotes_gpc() : false;
[1]1215    }
[42]1216
[523]1217    if ($always || $magic_quotes_gpc) {
[1]1218        if (!is_array($var)) {
1219            $var = stripslashes($var);
1220        } else {
1221            foreach ($var as $key=>$val) {
1222                if (is_array($val)) {
[523]1223                    $var[$key] = dispelMagicQuotes($val, $always);
[1]1224                } else {
1225                    $var[$key] = stripslashes($val);
1226                }
1227            }
1228        }
1229    }
1230    return $var;
1231}
1232
1233/**
1234 * Get a form variable from GET or POST data, stripped of magic
1235 * quotes if necessary.
1236 *
[747]1237 * @param string $key       The name of a $_REQUEST key (optional).
1238 * @param string $default   The value to return if the variable is set (optional).
[701]1239 * @return mixed      A cleaned GET or POST array if no key specified.
[747]1240 * @return string     A cleaned form value if set, or $default.
[1]1241 */
[701]1242function getFormData($key=null, $default=null)
[1]1243{
[523]1244    $app =& App::getInstance();
1245
[718]1246    if (null === $key) {
1247        // Return entire array.
1248        switch (strtoupper(getenv('REQUEST_METHOD'))) {
1249        case 'POST':
1250            return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes'));
1251
1252        case 'GET':
1253            return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes'));
1254
1255        default:
1256            return dispelMagicQuotes($_REQUEST, $app->getParam('always_dispel_magicquotes'));
1257        }
[1]1258    }
[701]1259
1260    if (isset($_REQUEST[$key])) {
1261        // $key is found in the flat array of REQUEST.
1262        return dispelMagicQuotes($_REQUEST[$key], $app->getParam('always_dispel_magicquotes'));
1263    } else if (mb_strpos($key, '[') !== false && isset($_REQUEST[strtok($key, '[')]) && preg_match_all('/\[([a-z0-9._~-]+)\]/', $key, $matches)) {
1264        // $key is formatted with sub-keys, e.g., getFormData('foo[bar][baz]') and top level key (`foo`) exists in REQUEST.
1265        // Extract these as sub-keys and access REQUEST as a multi-dimensional array, e.g., $_REQUEST[foo][bar][baz].
1266        $leaf = $_REQUEST[strtok($key, '[')];
1267        foreach ($matches[1] as $subkey) {
1268            if (is_array($leaf) && isset($leaf[$subkey])) {
1269                $leaf = $leaf[$subkey];
1270            } else {
1271                $leaf = null;
1272            }
1273        }
1274        return $leaf;
[1]1275    } else {
1276        return $default;
1277    }
1278}
[523]1279
[701]1280function getPost($key=null, $default=null)
[1]1281{
[523]1282    $app =& App::getInstance();
1283
[701]1284    if (null === $key) {
[523]1285        return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes'));
[1]1286    }
[701]1287    if (isset($_POST[$key])) {
1288        return dispelMagicQuotes($_POST[$key], $app->getParam('always_dispel_magicquotes'));
[1]1289    } else {
1290        return $default;
1291    }
1292}
[523]1293
[701]1294function getGet($key=null, $default=null)
[1]1295{
[523]1296    $app =& App::getInstance();
[701]1297
1298    if (null === $key) {
[523]1299        return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes'));
[1]1300    }
[701]1301    if (isset($_GET[$key])) {
1302        return dispelMagicQuotes($_GET[$key], $app->getParam('always_dispel_magicquotes'));
[1]1303    } else {
1304        return $default;
1305    }
1306}
1307
[361]1308/*
1309* Sets a $_GET or $_POST variable.
1310*
1311* @access   public
1312* @param    string  $key    The key of the request array to set.
1313* @param    mixed   $val    The value to save in the request array.
1314* @return   void
1315* @author   Quinn Comendant <quinn@strangecode.com>
1316* @version  1.0
1317* @since    01 Nov 2009 12:25:29
1318*/
1319function putFormData($key, $val)
1320{
[560]1321    switch (strtoupper(getenv('REQUEST_METHOD'))) {
1322    case 'POST':
[361]1323        $_POST[$key] = $val;
[560]1324        break;
1325
1326    case 'GET':
[361]1327        $_GET[$key] = $val;
[560]1328        break;
[361]1329    }
[718]1330
1331    $_REQUEST[$key] = $val;
[361]1332}
1333
[580]1334/*
[804]1335* Trims whitespace from request data.
1336*
1337* @access   public
1338* @return   void
1339* @author   Quinn Comendant <quinn@strangecode.com>
1340* @version  1.0
1341* @since    12 Jan 2024 13:15:02
1342*/
1343function trimFormData()
1344{
1345    switch (strtoupper(getenv('REQUEST_METHOD'))) {
1346    case 'POST':
1347        array_walk_recursive($_POST, function(&$v) { if (isset($v)) { $v = trim($v); } });
1348        break;
1349
1350    case 'GET':
1351        array_walk_recursive($_GET, function(&$v) { if (isset($v)) { $v = trim($v); } });
1352        break;
1353    }
1354
1355    array_walk_recursive($_REQUEST, function(&$v) { if (isset($v)) { $v = trim($v); } });
1356}
1357
1358/*
[580]1359* Generates a base-65-encoded sha512 hash of $string truncated to $length.
1360*
1361* @access   public
1362* @param    string  $string Input string to hash.
1363* @param    int     $length Length of output hash string.
1364* @return   string          String of hash.
1365* @author   Quinn Comendant <quinn@strangecode.com>
1366* @version  1.0
1367* @since    03 Apr 2016 19:48:49
1368*/
1369function hash64($string, $length=18)
1370{
[724]1371    $app =& App::getInstance();
1372
1373    return mb_substr(preg_replace('/[^\w]/' . $app->getParam('preg_u'), '', base64_encode(hash('sha512', $string, true))), 0, $length);
[580]1374}
1375
[1]1376/**
1377 * Signs a value using md5 and a simple text key. In order for this
[502]1378 * function to be useful (i.e. secure) the salt must be kept secret, which
[1]1379 * means keeping it as safe as database credentials. Putting it into an
1380 * environment variable set in httpd.conf is a good place.
1381 *
1382 * @access  public
1383 * @param   string  $val    The string to sign.
[159]1384 * @param   string  $salt   (Optional) A text key to use for computing the signature.
[282]1385 * @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.
[1]1386 * @return  string  The original value with a signature appended.
1387 */
[282]1388function addSignature($val, $salt=null, $length=18)
[1]1389{
[159]1390    $app =& App::getInstance();
[454]1391
[159]1392    if ('' == trim($val)) {
[201]1393        $app->logMsg(sprintf('Cannot add signature to an empty string.', null), LOG_INFO, __FILE__, __LINE__);
[159]1394        return '';
[1]1395    }
[42]1396
[159]1397    if (!isset($salt)) {
1398        $salt = $app->getParam('signing_key');
[1]1399    }
[454]1400
[500]1401    switch ($app->getParam('signing_method')) {
1402    case 'sha512+base64':
[724]1403        return $val . '-' . mb_substr(preg_replace('/[^\w]/' . $app->getParam('preg_u'), '', base64_encode(hash('sha512', $val . $salt, true))), 0, $length);
[500]1404
1405    case 'md5':
1406    default:
1407        return $val . '-' . mb_strtolower(mb_substr(md5($salt . md5($val . $salt)), 0, $length));
1408    }
[1]1409}
1410
1411/**
1412 * Strips off the signature appended by addSignature().
1413 *
1414 * @access  public
1415 * @param   string  $signed_val     The string to sign.
1416 * @return  string  The original value with a signature removed.
1417 */
1418function removeSignature($signed_val)
1419{
[249]1420    if (empty($signed_val) || mb_strpos($signed_val, '-') === false) {
1421        return '';
1422    }
[247]1423    return mb_substr($signed_val, 0, mb_strrpos($signed_val, '-'));
[1]1424}
1425
1426/**
[500]1427 * Verifies a signature appended to a value by addSignature().
[1]1428 *
1429 * @access  public
1430 * @param   string  $signed_val A value with appended signature.
[159]1431 * @param   string  $salt       (Optional) A text key to use for computing the signature.
[502]1432 * @param   string  $length (Optional) The length of the added signature.
[1]1433 * @return  bool    True if the signature matches the var.
1434 */
[282]1435function verifySignature($signed_val, $salt=null, $length=18)
[1]1436{
[783]1437    $app =& App::getInstance();
1438
[1]1439    // Strip the value from the signed value.
[22]1440    $val = removeSignature($signed_val);
[783]1441    if ('' == $val) {
1442        // Removing the signature failed because it was empty or did not contain a hyphen.
1443        $app->logMsg(sprintf('Invalid signature ("%s" is not a valid signed value).', $signed_val), LOG_DEBUG, __FILE__, __LINE__);
1444        return false;
1445    }
[1]1446    // If the signed value matches the original signed value we consider the value safe.
[532]1447    if ('' != $signed_val && $signed_val == addSignature($val, $salt, $length)) {
[1]1448        // Signature verified.
1449        return true;
1450    } else {
[770]1451        // A signature mismatch might occur if the signing_key is not the same across all environments, apache, cli, etc.
[783]1452        $app->logMsg(sprintf('Invalid signature (%s should be %s).', $signed_val, addSignature($val, $salt, $length)), LOG_DEBUG, __FILE__, __LINE__);
[1]1453        return false;
1454    }
1455}
1456
1457/**
1458 * Sends empty output to the browser and flushes the php buffer so the client
[42]1459 * will see data before the page is finished processing.
[1]1460 */
[235]1461function flushBuffer()
1462{
[1]1463    echo str_repeat('          ', 205);
1464    flush();
1465}
1466
1467/**
[667]1468 * A stub for apps that still use this function.
[1]1469 *
1470 * @access  public
[667]1471 * @return  void
[1]1472 */
1473function mailmanAddMember($email, $list, $send_welcome_message=false)
1474{
[479]1475    $app =& App::getInstance();
[667]1476    $app->logMsg(sprintf('mailmanAddMember called and ignored: %s, %s, %s', $email, $list, $send_welcome_message), LOG_WARNING, __FILE__, __LINE__);
[1]1477}
1478
1479/**
[667]1480 * A stub for apps that still use this function.
[1]1481 *
1482 * @access  public
[667]1483 * @return  void
[1]1484 */
1485function mailmanRemoveMember($email, $list, $send_user_ack=false)
1486{
[479]1487    $app =& App::getInstance();
[667]1488    $app->logMsg(sprintf('mailmanRemoveMember called and ignored: %s, %s, %s', $email, $list, $send_user_ack), LOG_WARNING, __FILE__, __LINE__);
[1]1489}
1490
[497]1491/*
1492* Returns the remote IP address, taking into consideration proxy servers.
1493*
[808]1494* If strict checking is enabled, we will only trust REMOTE_ADDR or an HTTP header value if
1495* REMOTE_ADDR is a trusted proxy (configured as an array via $app->setParam(['trusted_proxies' => ['1.2.3.4', '5.6.7.8']]).
[497]1496*
1497* @access   public
1498* @param    bool $dolookup            Resolve to IP to a hostname?
1499* @param    bool $trust_all_proxies   Should we trust any IP address set in HTTP_* variables? Set to FALSE for secure usage.
1500* @return   mixed Canonicalized IP address (or a corresponding hostname if $dolookup is true), or false if no IP was found.
1501* @author   Alix Axel <http://stackoverflow.com/a/2031935/277303>
1502* @author   Corey Ballou <http://blackbe.lt/advanced-method-to-obtain-the-client-ip-in-php/>
1503* @author   Quinn Comendant <quinn@strangecode.com>
1504* @version  1.0
1505* @since    12 Sep 2014 19:07:46
1506*/
1507function getRemoteAddr($dolookup=false, $trust_all_proxies=true)
[1]1508{
[808]1509    $app =& App::getInstance();
[497]1510
1511    if (!isset($_SERVER['REMOTE_ADDR'])) {
[507]1512        // In some cases this won't be set, e.g., CLI scripts.
[783]1513        return '';
[497]1514    }
1515
[808]1516    // Use an HTTP header value only if $trust_all_proxies is true or when REMOTE_ADDR is in our $trusted_proxies array.
1517    // $trusted_proxies is an array of proxy server addresses we expect to see in REMOTE_ADDR.
1518    $trusted_proxies = $app->getParam('trusted_proxies', []);
1519    if ($trust_all_proxies || is_array($trusted_proxies) && in_array($_SERVER['REMOTE_ADDR'], $trusted_proxies, true)) {
[497]1520        // Then it's probably safe to use an IP address value set in an HTTP header.
[706]1521        // Loop through possible IP address headers from those most likely to contain the correct value first.
1522        // HTTP_CLIENT_IP: set by Apache Module mod_remoteip
1523        // HTTP_REAL_IP: set by Nginx Module ngx_http_realip_module
1524        // HTTP_CF_CONNECTING_IP: set by Cloudflare proxy
1525        // HTTP_X_FORWARDED_FOR: defacto standard for web proxies
[808]1526        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) {
1527            if (isset($_SERVER[$key]) && '' != $_SERVER[$key]) {
[497]1528                foreach (explode(',', $_SERVER[$key]) as $addr) {
[598]1529                    // Strip non-address data to avoid "PHP Warning:  inet_pton(): Unrecognized address for=189.211.197.173 in ./Utilities.inc.php on line 1293"
1530                    $addr = preg_replace('/[^=]=/', '', $addr);
[497]1531                    $addr = canonicalIPAddr(trim($addr));
[808]1532                    // Exclude invalid, private, or reserved IP addresses (a proxy server may be using a private IP).
[706]1533                    if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
[497]1534                        return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr;
1535                    }
1536                }
1537            }
[290]1538        }
[1]1539    }
[497]1540
1541    $addr = canonicalIPAddr(trim($_SERVER['REMOTE_ADDR']));
[808]1542    if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4)) {
[807]1543        return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr;
1544    }
1545
1546    return '';
[1]1547}
1548
[497]1549/*
1550* Converts an ipv4 IP address in hexadecimal form into canonical form (i.e., it removes the prefix).
1551*
1552* @access   public
1553* @param    string  $addr   IP address.
1554* @return   string          Canonical IP address.
1555* @author   Sander Steffann <http://stackoverflow.com/a/12436099/277303>
1556* @author   Quinn Comendant <quinn@strangecode.com>
1557* @version  1.0
1558* @since    15 Sep 2012
1559*/
1560function canonicalIPAddr($addr)
1561{
[706]1562    if (!preg_match('/^([0-9a-f:]+|[0-9.])$/', $addr)) {
1563        // Definitely not an IPv6 or IPv4 address.
1564        return $addr;
1565    }
1566
[497]1567    // Known prefix
1568    $v4mapped_prefix_bin = pack('H*', '00000000000000000000ffff');
1569
1570    // Parse
1571    $addr_bin = inet_pton($addr);
1572
1573    // Check prefix
1574    if (substr($addr_bin, 0, strlen($v4mapped_prefix_bin)) == $v4mapped_prefix_bin) {
1575        // Strip prefix
1576        $addr_bin = substr($addr_bin, strlen($v4mapped_prefix_bin));
1577    }
1578
1579    // Convert back to printable address in canonical form
1580    return inet_ntop($addr_bin);
1581}
1582
[1]1583/**
1584 * Tests whether a given IP address can be found in an array of IP address networks.
1585 * Elements of networks array can be single IP addresses or an IP address range in CIDR notation
1586 * See: http://en.wikipedia.org/wiki/Classless_inter-domain_routing
1587 *
1588 * @access  public
1589 * @param   string  IP address to search for.
1590 * @param   array   Array of networks to search within.
1591 * @return  mixed   Returns the network that matched on success, false on failure.
1592 */
[497]1593function ipInRange($addr, $networks)
[1]1594{
[765]1595    if (null == $addr || '' == trim($addr)) {
1596        return false;
1597    }
1598
[1]1599    if (!is_array($networks)) {
1600        $networks = array($networks);
1601    }
[42]1602
[497]1603    $addr_binary = sprintf('%032b', ip2long($addr));
[1]1604    foreach ($networks as $network) {
[715]1605        if (mb_strpos($network, '/') !== false) {
[1]1606            // IP is in CIDR notation.
[247]1607            list($cidr_ip, $cidr_bitmask) = explode('/', $network);
[1]1608            $cidr_ip_binary = sprintf('%032b', ip2long($cidr_ip));
[497]1609            if (mb_substr($addr_binary, 0, $cidr_bitmask) === mb_substr($cidr_ip_binary, 0, $cidr_bitmask)) {
[1]1610               // IP address is within the specified IP range.
1611               return $network;
1612            }
1613        } else {
[497]1614            if ($addr === $network) {
[1]1615               // IP address exactly matches.
1616               return $network;
1617            }
1618        }
1619    }
[42]1620
[1]1621    return false;
1622}
1623
1624/**
[159]1625 * If the given $url is on the same web site, return true. This can be used to
1626 * prevent from sending sensitive info in a get query (like the SID) to another
1627 * domain.
1628 *
1629 * @param  string $url    the URI to test.
1630 * @return bool True if given $url is our domain or has no domain (is a relative url), false if it's another.
1631 */
1632function isMyDomain($url)
1633{
1634    static $urls = array();
1635
1636    if (!isset($urls[$url])) {
[670]1637        if (!preg_match('!^https?://!i', $url)) {
[159]1638            // If we can't find a domain we assume the URL is local (i.e. "/my/url/path/" or "../img/file.jpg").
1639            $urls[$url] = true;
1640        } else {
[670]1641            $urls[$url] = preg_match('!^https?://' . preg_quote(getenv('HTTP_HOST'), '!') . '!i', $url);
[159]1642        }
1643    }
1644    return $urls[$url];
1645}
1646
1647/**
1648 * Takes a URL and returns it without the query or anchor portion
1649 *
1650 * @param  string $url   any kind of URI
1651 * @return string        the URI with ? or # and everything after removed
1652 */
1653function stripQuery($url)
1654{
[724]1655    $app =& App::getInstance();
1656
1657    return preg_replace('/[?#].*$/' . $app->getParam('preg_u'), '', $url);
[159]1658}
1659
[742]1660/*
1661* Merge query arguments into a URL.
1662* Usage:
1663* Add ?lang=it or replace an existing ?lang= argument:
1664* $url = urlMerge('https://example.com/?lang=en', ['lang' => 'it']).
1665*
1666* @access   public
1667* @param    string  $url        Original URL.
1668* @param    array   $new_args   New/modified query arguments.
1669* @return   string              Modified URL.
1670* @author   Quinn Comendant <quinn@strangecode.com>
1671* @since    20 Feb 2021 21:21:53
1672*/
1673function urlMergeQuery($url, Array $new_args)
1674{
1675    $u = parse_url($url);
1676    if (isset($u['query']) && '' != $u['query']) {
1677        parse_str($u['query'], $args);
1678    } else {
1679        $args = [];
1680    }
1681    $u['query'] = http_build_query(array_merge($args, $new_args));
1682    return sprintf('%s%s%s%s%s',
1683        (isset($u['scheme'])    && '' != $u['scheme']   ? $u['scheme'] . '://' : ''),
1684        (isset($u['host'])      && '' != $u['host']     ? $u['host']           : ''),
[808]1685        (isset($u['path'])      && '' != $u['path']     ? $u['path']           : ''),
[742]1686        (isset($u['query'])     && '' != $u['query']    ? '?' . $u['query']    : ''),
1687        (isset($u['fragment'])  && '' != $u['fragment'] ? '#' . $u['fragment'] : '')
1688    );
1689}
1690
[808]1691/*
1692* Strip tracking query parameters from a URL.
1693*
1694* @access   public
1695* @param string $url                URL which may contain query parameters.
1696* @param mixed  $tracking_params    An array of tracking parameters to remove, or null to use a default set.
1697* @return string The URL with query params removed.
1698* @author   Quinn Comendant <quinn@strangecode.com>
1699* @since    02 Mar 2024 16:11:27
1700*/
1701function removeURLTrackingParameters($url, $tracking_params=null)
1702{
1703    // Use a default set of tracking params if not specified.
1704    $tracking_params = isset($tracking_params) ? $tracking_params : [
1705        'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_source_platform', 'utm_marketing_tactic', 'utm_creative_format',
[809]1706        'gad_source', 'gclid', 'gbraid', 'wbraid', 'dclid', 'fbclid', 'msclkid', 'awc', 'pclk', 'mc_eid', 'twclid', 'igshid',
[808]1707    ];
1708
1709    $u = parse_url($url);
1710    if (isset($u['query']) && '' != $u['query']) {
1711        parse_str($u['query'], $params);
1712        foreach ($tracking_params as $p) {
1713            unset($params[$p]);
1714        }
1715        $u['query'] = http_build_query($params);
1716
1717        return sprintf('%s%s%s%s%s',
1718            (isset($u['scheme'])    && '' != $u['scheme']   ? $u['scheme'] . '://' : ''),
1719            (isset($u['host'])      && '' != $u['host']     ? $u['host']           : ''),
1720            (isset($u['path'])      && '' != $u['path']     ? $u['path']           : ''),
1721            (isset($u['query'])     && '' != $u['query']    ? '?' . $u['query']    : ''),
1722            (isset($u['fragment'])  && '' != $u['fragment'] ? '#' . $u['fragment'] : '')
1723        );
1724    }
1725
1726    return $url;
1727}
1728
[159]1729/**
[690]1730 * Returns a fully qualified URL to the current script, including the query. If you don't need the scheme://, use REQUEST_URI instead.
[1]1731 *
1732 * @return string    a full url to the current script
1733 */
1734function absoluteMe()
1735{
[724]1736    $app =& App::getInstance();
1737
1738    $safe_http_host = preg_replace('/[^a-z\d.:-]/' . $app->getParam('preg_u'), '', getenv('HTTP_HOST'));
[670]1739    return sprintf('%s://%s%s', (getenv('HTTPS') ? 'https' : 'http'), $safe_http_host, getenv('REQUEST_URI'));
[1]1740}
1741
1742/**
1743 * Compares the current url with the referring url.
1744 *
[159]1745 * @param  bool $exclude_query  Remove the query string first before comparing.
[334]1746 * @return bool                 True if the current URL is the same as the referring URL, false otherwise.
[1]1747 */
1748function refererIsMe($exclude_query=false)
1749{
[580]1750    $current_url = absoluteMe();
[598]1751    $referrer_url = getenv('HTTP_REFERER');
[580]1752
[806]1753    // If either is empty, don't continue with a comparison.
1754    if ('' == $current_url || '' == $referrer_url) {
1755        return false;
1756    }
1757
[580]1758    // If one of the hostnames is an IP address, compare only the path of both.
1759    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))) {
[696]1760        $current_url = preg_replace('@^https?://[^/]+@u', '', $current_url);
1761        $referrer_url = preg_replace('@^https?://[^/]+@u', '', $referrer_url);
[580]1762    }
1763
[1]1764    if ($exclude_query) {
[598]1765        return (stripQuery($current_url) == stripQuery($referrer_url));
[1]1766    } else {
[580]1767        $app =& App::getInstance();
[598]1768        $app->logMsg(sprintf('refererIsMe comparison: %s == %s', $current_url, $referrer_url), LOG_DEBUG, __FILE__, __LINE__);
1769        return ($current_url == $referrer_url);
[1]1770    }
1771}
[520]1772
1773/*
[591]1774* Returns true if the given URL resolves to a resource with a HTTP 2xx or 3xx header response.
[606]1775* The download will abort if it retrieves >= 10KB of data to avoid downloading large files.
1776* We couldn't use CURLOPT_NOBODY (a HEAD request) because some services don't behave without a GET request (ahem, BBC).
1777* This function may not be very portable, if the server doesn't support CURLOPT_PROGRESSFUNCTION.
[520]1778*
1779* @access   public
[743]1780* @param    string  $url     URL to a file.
1781* @param    int     $timeout The maximum number of seconds to allow the HTTP query to execute.
1782* @return   bool             True if the resource exists, false otherwise.
[520]1783* @author   Quinn Comendant <quinn@strangecode.com>
[606]1784* @version  2.0
[520]1785* @since    02 May 2015 15:10:09
1786*/
[735]1787function httpExists($url, $timeout=5)
[520]1788{
1789    $ch = curl_init($url);
[735]1790    curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
[679]1791    curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
[520]1792    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
[679]1793    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
[606]1794    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");
[672]1795    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Don't pass through data to the browser.
[606]1796    curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Frequent progress function calls.
1797    curl_setopt($ch, CURLOPT_NOPROGRESS, false); // Required to use CURLOPT_PROGRESSFUNCTION.
[607]1798    // Function arguments for CURLOPT_PROGRESSFUNCTION changed with php 5.5.0.
1799    if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
1800        curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($ch, $dltot, $dlcur, $ultot, $ulcur){
1801            // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error
1802            // 10KB should be enough to catch a few 302 redirect headers and get to the actual content.
1803            return ($dlcur > 10*1024) ? 1 : 0;
1804        });
1805    } else {
1806        curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($dltot, $dlcur, $ultot, $ulcur){
1807            // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error
1808            // 10KB should be enough to catch a few 302 redirect headers and get to the actual content.
1809            return ($dlcur > 10*1024) ? 1 : 0;
1810        });
1811    }
[520]1812    curl_exec($ch);
[591]1813    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1814    return preg_match('/^[23]\d\d$/', $http_code);
[520]1815}
[704]1816
1817/*
[735]1818* Get a HTTP response header.
1819*
1820* @access   public
1821* @param    string  $url    URL to hit.
1822* @param    string  $key    Name of the header to return.
1823* @param    array   $valid_response_codes   Array of acceptable HTTP return codes.
1824* @return   string  Value of the http header.
1825* @author   Quinn Comendant <quinn@strangecode.com>
1826* @since    28 Oct 2020 20:00:36
1827*/
[787]1828function getHttpHeader($url, $key=null, Array $valid_response_codes=[200], $method='GET')
[735]1829{
[787]1830    $context = stream_context_create(['http' => ['method' => $method]]);
1831    $headers = get_headers($url, 1, $context);
1832    $app =& \App::getInstance();
[796]1833    $app->logMsg(sprintf('HTTP response headers for %s %s: %s', strtoupper($method), $url, getDump($headers, true, SC_DUMP_JSON)), LOG_DEBUG, __FILE__, __LINE__);
[787]1834    if (empty($headers)) {
1835        return false;
1836    }
[735]1837
[787]1838    // Status lines are found in numeric-indexed keys.
1839    $http_status_keys = preg_grep('/^\d$/', array_keys($headers)); // [0] => "HTTP/1.1 302 Found", [1] => "HTTP/1.1 200 OK",
1840    $final_http_status_key = end($http_status_keys); // E.g., `1`
1841    $final_http_status = $headers[$final_http_status_key]; // E.g., `HTTP/1.1 200 OK`
[796]1842    $app->logMsg(sprintf('Last HTTP status code: %s', $final_http_status), LOG_DEBUG, __FILE__, __LINE__);
[787]1843    if ($headers && preg_match(sprintf('/\b(%s)\b/', join('|', $valid_response_codes)), $final_http_status)) {
[735]1844        $headers = array_change_key_case($headers, CASE_LOWER);
[787]1845        if (!isset($key)) {
1846            return $headers;
1847        }
[735]1848        $key = strtolower($key);
1849        if (isset($headers[$key])) {
[787]1850            // If multiple redirects, the header key is an array; return only the last one.
1851            return is_array($headers[$key]) && isset($headers[$key][$final_http_status_key]) ? $headers[$key][$final_http_status_key] : $headers[$key];
[735]1852        }
1853    }
1854
1855    return false;
1856}
1857
1858/*
[704]1859* Load JSON data from a file and return it as an array (as specified by the json_decode options passed below.)
1860*
1861* @access   public
1862* @param    string  $filename   Name of the file to load. Just exist in the include path.
1863* @param    bool    $assoc      When TRUE, returned objects will be converted into associative arrays.
1864* @param    int     $depth      Recursion depth.
1865* @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.
1866* @return   array               Array of data from the file, or null if there was a problem.
1867* @author   Quinn Comendant <quinn@strangecode.com>
1868* @since    09 Oct 2019 21:32:47
1869*/
1870function jsonDecodeFile($filename, $assoc=true, $depth=512, $options=0)
1871{
1872    $app =& App::getInstance();
1873
[746]1874    if (false === ($resolved_filename = stream_resolve_include_path($filename))) {
[705]1875        $app->logMsg(sprintf('JSON file "%s" not found in path "%s"', $filename, get_include_path()), LOG_ERR, __FILE__, __LINE__);
[704]1876        return null;
1877    }
1878
1879    if (!is_readable($resolved_filename)) {
1880        $app->logMsg(sprintf('JSON file is unreadable: %s', $resolved_filename), LOG_ERR, __FILE__, __LINE__);
1881        return null;
1882    }
1883
[746]1884    if (null === ($data = json_decode(file_get_contents($resolved_filename), $assoc, $depth, $options))) {
[704]1885        $app->logMsg(sprintf('JSON is unparsable: %s', $resolved_filename), LOG_ERR, __FILE__, __LINE__);
1886        return null;
1887    }
1888
1889    return $data;
[706]1890}
1891
1892/*
1893* Get IP address status from IP Intelligence. https://getipintel.net/free-proxy-vpn-tor-detection-api/#expected_output
1894*
1895* @access   public
1896* @param    string  $ip         IP address to check.
1897* @param    float   $threshold  Return true if the IP score is above this threshold (0-1).
1898* @param    string  $email      Requester email address.
[707]1899* @return   boolean             True if the IP address appears to be a robot, proxy, or VPN.
1900*                               False if the IP address is a residential or business IP address, or the API failed to return a valid response.
[706]1901* @author   Quinn Comendant <quinn@strangecode.com>
1902* @since    26 Oct 2019 15:39:17
1903*/
1904function IPIntelligenceBadIP($ip, $threshold=0.95, $email='hello@strangecode.com')
1905{
1906    $app =& App::getInstance();
1907
1908    $ch = curl_init(sprintf('http://check.getipintel.net/check.php?ip=%s&contact=%s', urlencode($ip), urlencode($email)));
1909    curl_setopt($ch, CURLOPT_TIMEOUT, 2);
1910    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1911    $response = curl_exec($ch);
1912    $errorno = curl_errno($ch);
1913    $error = curl_error($ch);
1914    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1915    curl_close($ch);
1916
1917    if ($errorno == CURLE_OPERATION_TIMEOUTED) {
1918        $http_code = 408;
1919    }
1920
1921    switch ($http_code) {
1922    case 200:
1923    case 400:
1924        // Check response value, below.
1925        break;
1926
1927    case 408:
[787]1928        $app->logMsg(sprintf('IP Intelligence timeout', null), LOG_NOTICE, __FILE__, __LINE__);
[706]1929        return false;
1930    case 429:
1931        $app->logMsg(sprintf('IP Intelligence number of allowed queries exceeded (rate limit 15 requests/minute)', null), LOG_WARNING, __FILE__, __LINE__);
1932        return false;
1933    default:
1934        $app->logMsg(sprintf('IP Intelligence unexpected response (%s): %s: %s', $http_code, $error, $response), LOG_ERR, __FILE__, __LINE__);
1935        return false;
1936    }
1937
1938    switch ($response) {
1939    case -1:
1940        $app->logMsg('IP Intelligence: Invalid no input', LOG_WARNING, __FILE__, __LINE__);
1941        return false;
1942    case -2:
1943        $app->logMsg('IP Intelligence: Invalid IP address', LOG_WARNING, __FILE__, __LINE__);
1944        return false;
1945    case -3:
[787]1946        $app->logMsg('IP Intelligence: Unroutable or private address', LOG_NOTICE, __FILE__, __LINE__);
[706]1947        return false;
1948    case -4:
1949        $app->logMsg('IP Intelligence: Unable to reach database', LOG_WARNING, __FILE__, __LINE__);
1950        return false;
1951    case -5:
[746]1952        $app->logMsg('IP Intelligence: Banned: exceeded query limits, no permission, or invalid email address', LOG_WARNING, __FILE__, __LINE__);
[706]1953        return false;
1954    case -6:
1955        $app->logMsg('IP Intelligence: Invalid contact information', LOG_WARNING, __FILE__, __LINE__);
1956        return false;
1957    default:
[746]1958        if (!is_numeric($response) || $response < 0) {
1959            $app->logMsg(sprintf('IP Intelligence: Unknown status for IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__);
1960            return false;
1961        }
1962        if ($response >= $threshold) {
[706]1963            $app->logMsg(sprintf('IP Intelligence: Bad IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__);
1964            return true;
1965        }
1966        $app->logMsg(sprintf('IP Intelligence: Good IP (%s): %s', $response, $ip), LOG_NOTICE, __FILE__, __LINE__);
1967        return false;
1968    }
[741]1969}
1970
1971/*
1972* Test if a string is valid json.
1973* https://stackoverflow.com/questions/6041741/fastest-way-to-check-if-a-string-is-json-in-php
1974*
1975* @access   public
1976* @param    string  $str  The string to test.
1977* @return   boolean       True if the string is valid json.
1978* @author   Quinn Comendant <quinn@strangecode.com>
1979* @since    06 Dec 2020 18:41:51
1980*/
1981function isJSON($str)
1982{
1983    json_decode($str);
1984    return (json_last_error() === JSON_ERROR_NONE);
[814]1985}
1986
1987/*
1988* Trim strings. Arrays of strings are trimmed recursively. Other data types remain untouched.
1989*
1990* @access   public
1991* @param    mixed  $var A variable of any data type, which may be a multidimensional array.
1992* @return   mixed       String values trimmed, other data types returned as-is.
1993* @author   Quinn Comendant <quinn@strangecode.com>
1994* @since    28 Mar 2024 20:32:29
1995*/
1996function safeTrim($var)
1997{
1998    // Directly trim if it's a string.
1999    if (is_string($var)) {
2000        return trim($var);
2001    }
2002
2003    // Recursively trim elements if it's an array.
2004    if (is_array($var)) {
2005        array_walk_recursive($var, function (&$v) {
2006            if (is_string($v)) {
2007                $v = trim($v);
2008            }
2009        });
2010        return $var;
2011    }
2012
2013    // Return the value as-is for other data types.
2014    return $var;
2015}
Note: See TracBrowser for help on using the repository browser.