source: trunk/lib/Utilities.inc.php

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

Add safeTrim() function

File size: 72.5 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
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.
13 *
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.
18 *
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/**
24 * Utilities.inc.php
25 */
26
27require_once dirname(__FILE__) . '/App.inc.php';
28
29/**
30 * Print variable dump.
31 *
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__
37 */
38define('SC_DUMP_PRINT_R', 0);
39define('SC_DUMP_VAR_DUMP', 1);
40define('SC_DUMP_VAR_EXPORT', 2);
41define('SC_DUMP_JSON', 3);
42function dump($var, $display=false, $dump_method=SC_DUMP_JSON, $file='', $line='')
43{
44    $app =& App::getInstance();
45
46    if ($app->isCLI()) {
47        echo ('' != $file . $line) ? "DUMP FROM: $file $line\n" : "DUMP:\n";
48    } else {
49        echo $display ? "\n<br />DUMP <strong>$file $line</strong><br /><pre>\n" : "\n<!-- DUMP $file $line\n";
50    }
51
52    switch ($dump_method) {
53    case SC_DUMP_PRINT_R:
54    default:
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        }
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;
78
79    case SC_DUMP_JSON:
80        echo json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
81        break;
82    }
83
84    if ($app->isCLI()) {
85        echo "\n";
86    } else {
87        echo $display ? "\n</pre><br />\n" : "\n-->\n";
88    }
89}
90
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        ?>
106        <script type="text/javascript">
107        /* <![CDATA[ */
108        console.log('<?php printf('%s: %s (on line %s of %s)', $prefix, str_replace("'", "\\'", getDump($var, true)), $line, $file); ?>');
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
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.
123* @author   Quinn Comendant <quinn@strangecode.com>
124*/
125function getDump($var, $serialize=false, $dump_method=SC_DUMP_JSON)
126{
127    $app =& App::getInstance();
128
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:
165    default:
166        $json_flags = $serialize ? 0 : JSON_PRETTY_PRINT;
167        return json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | $json_flags);
168    }
169    return $serialize ? preg_replace('/\s+/m' . $app->getParam('preg_u'), ' ', $d) : $d;
170}
171
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)
186{
187    $app =& App::getInstance();
188
189    $indent = trim($indent, ' ') . ' ';
190
191    $indent_str = str_repeat($indent, $depth);
192    $output = '';
193    if (is_array($var)) {
194        foreach ($var as $k=>$v) {
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)));
206            if (is_array($v)) {
207                $output .= sprintf("\n%s%s:\n%s\n", $indent_str, $k, fancyDump($v, $indent, $depth+1));
208            } else {
209                $output .= sprintf("%s%s: %s\n", $indent_str, $k, $v);
210            }
211        }
212    } else {
213        $output .= sprintf("%s%s\n", $indent_str, $var);
214    }
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);
225}
226
227/**
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/**
244 * Returns text with appropriate html translations (a smart wrapper for htmlspecialchars()).
245 *
246 * @param  string $text             Text to clean.
247 * @param  bool   $preserve_html    If set to true, oTxt will not translate <, >, ", or '
248 *                                  characters into HTML entities. This allows HTML to pass through undisturbed.
249 * @return string                   HTML-safe text.
250 */
251function oTxt($text, $preserve_html=false)
252{
253    $app =& App::getInstance();
254
255    if ('' == $text) {
256        return '';
257    }
258
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.
263    $search['retain_ampersand']     = '/&amp;/';
264    $replace['retain_ampersand']    = '&';
265
266    if ($preserve_html) {
267        // Convert characters that must remain non-entities for displaying HTML.
268        $search['retain_left_angle']       = '/&lt;/';
269        $replace['retain_left_angle']      = '<';
270
271        $search['retain_right_angle']      = '/&gt;/';
272        $replace['retain_right_angle']     = '>';
273
274        $search['retain_single_quote']     = '/&#039;/';
275        $replace['retain_single_quote']    = "'";
276
277        $search['retain_double_quote']     = '/&quot;/';
278        $replace['retain_double_quote']    = '"';
279    }
280
281    // & becomes &amp;. Exclude any occurrence where the & is followed by a alphanum or unicode character.
282    $search['ampersand']        = '/&(?![\w\d#]{1,10};)/';
283    $replace['ampersand']       = '&amp;';
284
285    return preg_replace($search, $replace, htmlspecialchars($text, ENT_QUOTES, $app->getParam('character_set')));
286}
287
288/**
289 * Returns text with stylistic modifications. Warning: this will break some HTML attributes!
290 * TODO: Allow a string such as this to be passed: <a href="javascript:openPopup('/foo/bar.php')">Click here</a>
291 *
292 * @param  string   $text Text to clean.
293 * @return string         Cleaned text.
294 */
295function fancyTxt($text, $extra_search=null, $extra_replace=null)
296{
297    $search = array();
298    $replace = array();
299
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”';
303
304    // text's apostrophes  →  text’s apostrophes (except foot marks: 6'3")
305    $search['_apostrophe']       = '/(?<=[a-z])(?:\'|&#0?39;)(?=\w)/imsu';
306    $replace['_apostrophe']      = '’';
307
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’';
311
312    // plural posessives' apostrophes  →  posessives’  (except foot marks: 6')
313    $search['_apostrophes']      = '/(?<=s)(?:\'|&#0?39;|&rsquo;)(?=\s)/imsu';
314    $replace['_apostrophes']     = '’';
315
316    // double--hyphens  →  en – dashes
317    $search['_em_dash']          = '/(?<=[\w\s"\'”’)])--(?=[\w\s“”‘"\'(?])/imsu';
318    $replace['_em_dash']         = ' – ';
319
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));
331}
332
333/*
334* Finds all URLs in text and hyperlinks them.
335*
336* @access   public
337* @param    string  $text   Text to search for URLs.
338* @param    bool    $strict True to only include URLs starting with a scheme (http:// ftp:// im://), or false to include URLs starting with 'www.'.
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>
343* @version  2.2
344* @since    22 Mar 2015 23:29:04
345*/
346function hyperlinkTxt($text, $strict=false, $length=null, $delim='
')
347{
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
351    // Capture the full URL into the first match and only the first X characters into the second match.
352    // This will match URLs not preceded by " ' or = (URLs inside an attribute) or ` (Markdown quoted) or double-scheme (http://http://www.asdf.com)
353    // https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid/1547940#1547940
354    $regex = '@
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
362            )
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
365        )
366        @Suxi
367    ';
368    $regex = sprintf($regex,
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.)
372    );
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];
378        $truncated_url = $m[2] . $m[3];
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.
382            // Remove http schemas, and any single trailing / to make the display URL.
383            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], $url);
384            return sprintf('<a href="%s">%s</a>', oTxt($absolute_url), oTxt($display_url));
385        } else {
386            // Truncated URL.
387            // Remove http schemas, and any single trailing / to make the display URL.
388            $display_url = preg_replace(['!^https?://!u', '!^([^/]+)/$!u'], ['', '$1'], trim($truncated_url));
389            return sprintf('<a href="%s">%s%s</a>', oTxt($absolute_url), oTxt($display_url), $delim);
390        }
391    }, $text);
392}
393
394/**
395 * Applies a class to search terms to highlight them ala google results.
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{
404    $app =& App::getInstance();
405
406    $words = preg_split('/[^\w]/', $search, -1, PREG_SPLIT_NO_EMPTY);
407
408    $search = array();
409    $replace = array();
410
411    foreach ($words as $w) {
412        if ('' != trim($w)) {
413            $search[] = '/\b(' . preg_quote($w) . ')\b/i' . $app->getParam('preg_u');
414            $replace[] = '<span class="' . oTxt($class) . '">$1</span>';
415        }
416    }
417
418    return empty($replace) ? $text : preg_replace($search, $replace, $text);
419}
420
421/**
422 * Generates a hexadecimal html color based on provided word.
423 *
424 * @access public
425 * @param  string $text  A string for which to convert to color.
426 * @param  float  $n     Brightness value between 0-1.
427 * @return string        A hexadecimal html color.
428 */
429function getTextColor($text, $method=1, $n=0.87)
430{
431    $hash = md5($text);
432    $rgb = array(
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),
439    );
440
441    switch ($method) {
442    case 1 :
443    default :
444        // Reduce all hex values slightly to avoid all white.
445        array_walk($rgb, function (&$v) use ($n) {
446            $v = dechex(round(hexdec($v) * $n));
447        });
448        break;
449
450    case 2 :
451        foreach ($rgb as $i => $v) {
452            if (hexdec($v) > hexdec('c')) {
453                $rgb[$i] = dechex(hexdec('f') - hexdec($v));
454            }
455        }
456        break;
457    }
458
459    return join('', $rgb);
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{
472    $output = '';
473    $num = mb_strlen($text);
474    for ($i=0; $i<$num; $i++) {
475        $output .= sprintf('&#%03s', ord($text[$i]));
476    }
477    return $output;
478}
479
480/**
481 * Encodes an email into a "user at domain dot com" format.
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 */
489function encodeEmail($email, $at=' at ', $dot=' dot ')
490{
491    $app =& App::getInstance();
492
493    $search = array('/@/' . $app->getParam('preg_u'), '/\./' . $app->getParam('preg_u'));
494    $replace = array($at, $dot);
495    return preg_replace($search, $replace, $email);
496}
497
498/**
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
".
503 *
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 *
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'.
511 * @param   string  $delim  The delimiter to print where content is truncated.
512 * @return  string          Truncated output string.
513 * @author  Quinn Comendant <quinn@strangecode.com>
514 * @since   29 Mar 2006 13:48:49
515 */
516function truncate($str, $len=50, $where='end', $delim='
')
517{
518    $app =& App::getInstance();
519
520    $dlen = mb_strlen($delim);
521    if ($len <= $dlen || mb_strlen($str) <= $dlen) {
522        return substr($str, 0, $len);
523    }
524    $part1 = floor(($len - $dlen) / 2);
525    $part2 = ceil(($len - $dlen) / 2);
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
533    switch ($where) {
534    case 'start' :
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);
536
537    case 'middle' :
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);
539
540    case 'end' :
541    default :
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);
543    }
544}
545
546/*
547* A substitution for the missing mb_ucfirst function.
548*
549* @access   public
550* @param    string  $string The string
551* @return   string          String with upper-cased first character.
552* @author   Quinn Comendant <quinn@strangecode.com>
553* @version  1.0
554* @since    06 Dec 2008 17:04:01
555*/
556if (!function_exists('mb_ucfirst')) {
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
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*/
575if (!function_exists('mb_strtr')) {
576    function mb_strtr($string, $from, $to)
577    {
578        return str_replace(mb_split('.', $from), mb_split('.', $to), $string);
579    }
580}
581
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
602/**
603 * Return a human readable disk space measurement. Input value measured in bytes.
604 *
605 * @param       int    $size        Size in bytes.
606 * @param       int    $unit        The maximum unit
607 * @param       int    $format      The return string format
608 * @author      Aidan Lister <aidan@php.net>
609 * @author      Quinn Comendant <quinn@strangecode.com>
610 * @version     1.2.0
611 */
612function humanFileSize($size, $format='%01.2f %s', $max_unit=null, $multiplier=1024)
613{
614    // Units
615    $units = array('B', 'KB', 'MB', 'GB', 'TB');
616    $ii = count($units) - 1;
617
618    // Max unit
619    $max_unit = array_search((string) $max_unit, $units);
620    if ($max_unit === null || $max_unit === false) {
621        $max_unit = $ii;
622    }
623
624    // Loop
625    $i = 0;
626    while ($max_unit != $i && $size >= $multiplier && $i < $ii) {
627        $size /= $multiplier;
628        $i++;
629    }
630
631    return sprintf($format, $size, $units[$i]);
632}
633
634/*
635* Returns a human readable amount of time for the given amount of seconds.
636*
637* 45 seconds
638* 12 minutes
639* 3.5 hours
640* 2 days
641* 1 week
642* 4 months
643*
644* Months are calculated using the real number of days in a year: 365.2422 / 12.
645*
646* @access   public
647* @param    int $seconds Seconds of time.
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*/
655function humanTime($seconds, $max_unit=null, $format='%01.1f')
656{
657    // Units: array of seconds in the unit, singular and plural unit names.
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")),
667        'century' => array(3155692608, _("century"), _("centuries")),
668    );
669
670    // Max unit to calculate.
671    $max_unit = isset($units[$max_unit]) ? $max_unit : 'year';
672
673    $final_time = $seconds;
674    $final_unit = 'second';
675    foreach ($units as $k => $v) {
676        if ($seconds >= $v[0]) {
677            $final_time = $seconds / $v[0];
678            $final_unit = $k;
679        }
680        if ($max_unit == $final_unit) {
681            break;
682        }
683    }
684    $final_time = sprintf($format, $final_time);
685    return sprintf('%s %s', $final_time, (1 == $final_time ? $units[$final_unit][1] : $units[$final_unit][2]));
686}
687
688/*
689* Calculate a prorated amount for the duration between two dates.
690*
691* @access   public
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.
697* @author   Quinn Comendant <quinn@strangecode.com>
698* @since    03 Nov 2021 22:44:30
699*/
700function prorate($amount, $duration, $start_date, $end_date)
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/*
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([
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'),
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{
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))));
784}
785
786/**
787 * Converts a string of text into a safe file name by removing non-ASCII characters and non-word characters.
788 *
789 * @access  public
790 * @param   string  $file_name  A name of a file.
791 * @param   string  $separator  The_separator_used_to_delimit_filename_parts.
792 * @return  string              The same name, but cleaned.
793 */
794function cleanFileName($file_name, $separator='_')
795{
796    $app =& App::getInstance();
797
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));
805    return mb_substr($file_name, 0, 250);
806}
807
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
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 != '') {
835        $unit = strtolower($val[mb_strlen($val) - 1]);
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        }
847    }
848
849    return (int)$val;
850}
851
852/**
853 * Tests the existence of a file anywhere in the include path.
854 * Replaced by stream_resolve_include_path() in PHP 5 >= 5.3.2
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();
864
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/**
878 * Returns stats of a file from the include path.
879 *
880 * @param   string  $file   File in include path.
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.
883 * @author  Quinn Comendant <quinn@strangecode.com>
884 * @since   03 Dec 2005 14:23:26
885 */
886function statIncludePath($file, $stat=null)
887{
888    // Open file pointer read-only using include path.
889    if ($fp = fopen($file, 'r', true)) {
890        // File opened successfully, get stats.
891        $stats = fstat($fp);
892        fclose($fp);
893        // Return specified stats.
894        return is_null($stat) ? $stats : $stats[$stat];
895    } else {
896        return false;
897    }
898}
899
900/*
901* Writes content to the specified file. This function emulates the functionality of file_put_contents from PHP 5.
902* It makes an exclusive lock on the file while writing.
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{
913    $app =& App::getInstance();
914
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
920    // Open file for writing and truncate to zero length.
921    if ($fp = fopen($filename, 'w')) {
922        if (flock($fp, LOCK_EX)) {
923            if (!fwrite($fp, (string)$content)) {
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
944/**
945 * If $var is net set or null, set it to $default. Otherwise leave it alone.
946 * Returns the final value of $var. Use to find a default value of one is not available.
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.
950 * @return mixed            The resulting value of $var.
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
966 * @param  array $delim    optional character that will also be escaped.
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 *
986 * @param  mixed $data        An array to transverse recursively, or a string
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 */
992function urlEncodeArray($data, $prefix='', $_return=true)
993{
994    // Data is stored in static variable.
995    static $args = array();
996
997    if (is_array($data)) {
998        foreach ($data as $key => $val) {
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".
1001            $new_prefix = ('' == $prefix) ? urlencode($key) : $prefix . '[' . urlencode($key) . ']';
1002            // Enter recursion.
1003            urlEncodeArray($val, $new_prefix, false);
1004        }
1005    } else {
1006        // We've come to the last dimension of the array, save the "array" and its value.
1007        $args[$prefix] = urlencode($data);
1008    }
1009
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 *
1022 * Todo: probably update to use the built-in http_build_query().
1023 *
1024 * @param  mixed $data        An array to transverse recursively, or a string
1025 *                            to use directly to create url arguments.
1026 * @param  string $prefix     The name of the first dimension of the array.
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 */
1030function urlEncodeArrayToString($data, $prefix='')
1031{
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
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*/
1051function base64EncodeURL($string) {
1052    return str_replace(['+','/','='], ['-','_',''], base64_encode($string));
1053}
1054function base64DecodeURL($string) {
1055    return base64_decode(str_replace(['-','_'], ['+','/'], $string));
1056}
1057
1058/**
1059 * Fills an array with the result from a multiple ereg search.
1060 * Courtesy of Bruno - rbronosky@mac.com - 10-May-2001
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 {
1069        if (!mb_ereg($pattern, $string, $temp)) {
1070             continue;
1071        }
1072        $string = str_replace($temp[0], '', $string);
1073        $results[] = $temp;
1074    } while (mb_ereg($pattern, $string, $temp));
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,
1081 * used for printing the word "checked" in a checkbox form input.
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,
1107 * otherwise prints nothing, used for printing the word "checked" in a
1108 * select form input
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/**
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.
1136 *
1137 *
1138 * @param  array $in      Array to convert.
1139 * @return string         Comma list of array values.
1140 */
1141function escapedList($in, $separator="', '")
1142{
1143    require_once dirname(__FILE__) . '/DB.inc.php';
1144    $db =& DB::getInstance();
1145
1146    if (is_array($in) && !empty($in)) {
1147        return join($separator, array_map(array($db, 'escapeString'), $in));
1148    } else {
1149        return $db->escapeString($in);
1150    }
1151}
1152
1153/**
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.
1157 *
1158 * @param  array $date     String date to convert.
1159 * @param  array $format   Date format to pass to date(). Default produces MySQL datetime: YYYY-MM-DD hh:mm:ss
1160 * @return string          SQL-safe date.
1161 */
1162function strToSQLDate($date, $format='Y-m-d H:i:s')
1163{
1164    require_once dirname(__FILE__) . '/DB.inc.php';
1165    $db =& DB::getInstance();
1166    $pdo =& \Strangecode\Codebase\PDO::getInstance();
1167
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
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];
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];
1180    } else {
1181        $zero_y = '0000';
1182        $zero_m = '00';
1183        $zero_d = '00';
1184    }
1185    // Translate the human string date into SQL-safe date format.
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) {
1187        // Return a string of zero time, formatted the same as $format.
1188        return strtr($format, array(
1189            'Y' => $zero_y,
1190            'm' => $zero_m,
1191            'd' => $zero_d,
1192            'H' => '00',
1193            'i' => '00',
1194            's' => '00',
1195        ));
1196    } else {
1197        return date($format, strtotime($date));
1198    }
1199}
1200
1201/**
1202 * If magic_quotes_gpc is in use, run stripslashes() on $var. If $var is an
1203 * array, stripslashes is run on each value, recursively, and the stripped
1204 * array is returned.
1205 *
1206 * @param  mixed $var   The string or array to un-quote, if necessary.
1207 * @return mixed        $var, minus any magic quotes.
1208 */
1209function dispelMagicQuotes($var, $always=false)
1210{
1211    static $magic_quotes_gpc;
1212
1213    if (!isset($magic_quotes_gpc)) {
1214        $magic_quotes_gpc = version_compare(PHP_VERSION, '5.4.0', '<') ? get_magic_quotes_gpc() : false;
1215    }
1216
1217    if ($always || $magic_quotes_gpc) {
1218        if (!is_array($var)) {
1219            $var = stripslashes($var);
1220        } else {
1221            foreach ($var as $key=>$val) {
1222                if (is_array($val)) {
1223                    $var[$key] = dispelMagicQuotes($val, $always);
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 *
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).
1239 * @return mixed      A cleaned GET or POST array if no key specified.
1240 * @return string     A cleaned form value if set, or $default.
1241 */
1242function getFormData($key=null, $default=null)
1243{
1244    $app =& App::getInstance();
1245
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        }
1258    }
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;
1275    } else {
1276        return $default;
1277    }
1278}
1279
1280function getPost($key=null, $default=null)
1281{
1282    $app =& App::getInstance();
1283
1284    if (null === $key) {
1285        return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes'));
1286    }
1287    if (isset($_POST[$key])) {
1288        return dispelMagicQuotes($_POST[$key], $app->getParam('always_dispel_magicquotes'));
1289    } else {
1290        return $default;
1291    }
1292}
1293
1294function getGet($key=null, $default=null)
1295{
1296    $app =& App::getInstance();
1297
1298    if (null === $key) {
1299        return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes'));
1300    }
1301    if (isset($_GET[$key])) {
1302        return dispelMagicQuotes($_GET[$key], $app->getParam('always_dispel_magicquotes'));
1303    } else {
1304        return $default;
1305    }
1306}
1307
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{
1321    switch (strtoupper(getenv('REQUEST_METHOD'))) {
1322    case 'POST':
1323        $_POST[$key] = $val;
1324        break;
1325
1326    case 'GET':
1327        $_GET[$key] = $val;
1328        break;
1329    }
1330
1331    $_REQUEST[$key] = $val;
1332}
1333
1334/*
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/*
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{
1371    $app =& App::getInstance();
1372
1373    return mb_substr(preg_replace('/[^\w]/' . $app->getParam('preg_u'), '', base64_encode(hash('sha512', $string, true))), 0, $length);
1374}
1375
1376/**
1377 * Signs a value using md5 and a simple text key. In order for this
1378 * function to be useful (i.e. secure) the salt must be kept secret, which
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.
1384 * @param   string  $salt   (Optional) A text key to use for computing the signature.
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.
1386 * @return  string  The original value with a signature appended.
1387 */
1388function addSignature($val, $salt=null, $length=18)
1389{
1390    $app =& App::getInstance();
1391
1392    if ('' == trim($val)) {
1393        $app->logMsg(sprintf('Cannot add signature to an empty string.', null), LOG_INFO, __FILE__, __LINE__);
1394        return '';
1395    }
1396
1397    if (!isset($salt)) {
1398        $salt = $app->getParam('signing_key');
1399    }
1400
1401    switch ($app->getParam('signing_method')) {
1402    case 'sha512+base64':
1403        return $val . '-' . mb_substr(preg_replace('/[^\w]/' . $app->getParam('preg_u'), '', base64_encode(hash('sha512', $val . $salt, true))), 0, $length);
1404
1405    case 'md5':
1406    default:
1407        return $val . '-' . mb_strtolower(mb_substr(md5($salt . md5($val . $salt)), 0, $length));
1408    }
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{
1420    if (empty($signed_val) || mb_strpos($signed_val, '-') === false) {
1421        return '';
1422    }
1423    return mb_substr($signed_val, 0, mb_strrpos($signed_val, '-'));
1424}
1425
1426/**
1427 * Verifies a signature appended to a value by addSignature().
1428 *
1429 * @access  public
1430 * @param   string  $signed_val A value with appended signature.
1431 * @param   string  $salt       (Optional) A text key to use for computing the signature.
1432 * @param   string  $length (Optional) The length of the added signature.
1433 * @return  bool    True if the signature matches the var.
1434 */
1435function verifySignature($signed_val, $salt=null, $length=18)
1436{
1437    $app =& App::getInstance();
1438
1439    // Strip the value from the signed value.
1440    $val = removeSignature($signed_val);
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    }
1446    // If the signed value matches the original signed value we consider the value safe.
1447    if ('' != $signed_val && $signed_val == addSignature($val, $salt, $length)) {
1448        // Signature verified.
1449        return true;
1450    } else {
1451        // A signature mismatch might occur if the signing_key is not the same across all environments, apache, cli, etc.
1452        $app->logMsg(sprintf('Invalid signature (%s should be %s).', $signed_val, addSignature($val, $salt, $length)), LOG_DEBUG, __FILE__, __LINE__);
1453        return false;
1454    }
1455}
1456
1457/**
1458 * Sends empty output to the browser and flushes the php buffer so the client
1459 * will see data before the page is finished processing.
1460 */
1461function flushBuffer()
1462{
1463    echo str_repeat('          ', 205);
1464    flush();
1465}
1466
1467/**
1468 * A stub for apps that still use this function.
1469 *
1470 * @access  public
1471 * @return  void
1472 */
1473function mailmanAddMember($email, $list, $send_welcome_message=false)
1474{
1475    $app =& App::getInstance();
1476    $app->logMsg(sprintf('mailmanAddMember called and ignored: %s, %s, %s', $email, $list, $send_welcome_message), LOG_WARNING, __FILE__, __LINE__);
1477}
1478
1479/**
1480 * A stub for apps that still use this function.
1481 *
1482 * @access  public
1483 * @return  void
1484 */
1485function mailmanRemoveMember($email, $list, $send_user_ack=false)
1486{
1487    $app =& App::getInstance();
1488    $app->logMsg(sprintf('mailmanRemoveMember called and ignored: %s, %s, %s', $email, $list, $send_user_ack), LOG_WARNING, __FILE__, __LINE__);
1489}
1490
1491/*
1492* Returns the remote IP address, taking into consideration proxy servers.
1493*
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']]).
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)
1508{
1509    $app =& App::getInstance();
1510
1511    if (!isset($_SERVER['REMOTE_ADDR'])) {
1512        // In some cases this won't be set, e.g., CLI scripts.
1513        return '';
1514    }
1515
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)) {
1520        // Then it's probably safe to use an IP address value set in an HTTP header.
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
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]) {
1528                foreach (explode(',', $_SERVER[$key]) as $addr) {
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);
1531                    $addr = canonicalIPAddr(trim($addr));
1532                    // Exclude invalid, private, or reserved IP addresses (a proxy server may be using a private IP).
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)) {
1534                        return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr;
1535                    }
1536                }
1537            }
1538        }
1539    }
1540
1541    $addr = canonicalIPAddr(trim($_SERVER['REMOTE_ADDR']));
1542    if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4)) {
1543        return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr;
1544    }
1545
1546    return '';
1547}
1548
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{
1562    if (!preg_match('/^([0-9a-f:]+|[0-9.])$/', $addr)) {
1563        // Definitely not an IPv6 or IPv4 address.
1564        return $addr;
1565    }
1566
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
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 */
1593function ipInRange($addr, $networks)
1594{
1595    if (null == $addr || '' == trim($addr)) {
1596        return false;
1597    }
1598
1599    if (!is_array($networks)) {
1600        $networks = array($networks);
1601    }
1602
1603    $addr_binary = sprintf('%032b', ip2long($addr));
1604    foreach ($networks as $network) {
1605        if (mb_strpos($network, '/') !== false) {
1606            // IP is in CIDR notation.
1607            list($cidr_ip, $cidr_bitmask) = explode('/', $network);
1608            $cidr_ip_binary = sprintf('%032b', ip2long($cidr_ip));
1609            if (mb_substr($addr_binary, 0, $cidr_bitmask) === mb_substr($cidr_ip_binary, 0, $cidr_bitmask)) {
1610               // IP address is within the specified IP range.
1611               return $network;
1612            }
1613        } else {
1614            if ($addr === $network) {
1615               // IP address exactly matches.
1616               return $network;
1617            }
1618        }
1619    }
1620
1621    return false;
1622}
1623
1624/**
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])) {
1637        if (!preg_match('!^https?://!i', $url)) {
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 {
1641            $urls[$url] = preg_match('!^https?://' . preg_quote(getenv('HTTP_HOST'), '!') . '!i', $url);
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{
1655    $app =& App::getInstance();
1656
1657    return preg_replace('/[?#].*$/' . $app->getParam('preg_u'), '', $url);
1658}
1659
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']           : ''),
1685        (isset($u['path'])      && '' != $u['path']     ? $u['path']           : ''),
1686        (isset($u['query'])     && '' != $u['query']    ? '?' . $u['query']    : ''),
1687        (isset($u['fragment'])  && '' != $u['fragment'] ? '#' . $u['fragment'] : '')
1688    );
1689}
1690
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',
1706        'gad_source', 'gclid', 'gbraid', 'wbraid', 'dclid', 'fbclid', 'msclkid', 'awc', 'pclk', 'mc_eid', 'twclid', 'igshid',
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
1729/**
1730 * Returns a fully qualified URL to the current script, including the query. If you don't need the scheme://, use REQUEST_URI instead.
1731 *
1732 * @return string    a full url to the current script
1733 */
1734function absoluteMe()
1735{
1736    $app =& App::getInstance();
1737
1738    $safe_http_host = preg_replace('/[^a-z\d.:-]/' . $app->getParam('preg_u'), '', getenv('HTTP_HOST'));
1739    return sprintf('%s://%s%s', (getenv('HTTPS') ? 'https' : 'http'), $safe_http_host, getenv('REQUEST_URI'));
1740}
1741
1742/**
1743 * Compares the current url with the referring url.
1744 *
1745 * @param  bool $exclude_query  Remove the query string first before comparing.
1746 * @return bool                 True if the current URL is the same as the referring URL, false otherwise.
1747 */
1748function refererIsMe($exclude_query=false)
1749{
1750    $current_url = absoluteMe();
1751    $referrer_url = getenv('HTTP_REFERER');
1752
1753    // If either is empty, don't continue with a comparison.
1754    if ('' == $current_url || '' == $referrer_url) {
1755        return false;
1756    }
1757
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))) {
1760        $current_url = preg_replace('@^https?://[^/]+@u', '', $current_url);
1761        $referrer_url = preg_replace('@^https?://[^/]+@u', '', $referrer_url);
1762    }
1763
1764    if ($exclude_query) {
1765        return (stripQuery($current_url) == stripQuery($referrer_url));
1766    } else {
1767        $app =& App::getInstance();
1768        $app->logMsg(sprintf('refererIsMe comparison: %s == %s', $current_url, $referrer_url), LOG_DEBUG, __FILE__, __LINE__);
1769        return ($current_url == $referrer_url);
1770    }
1771}
1772
1773/*
1774* Returns true if the given URL resolves to a resource with a HTTP 2xx or 3xx header response.
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.
1778*
1779* @access   public
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.
1783* @author   Quinn Comendant <quinn@strangecode.com>
1784* @version  2.0
1785* @since    02 May 2015 15:10:09
1786*/
1787function httpExists($url, $timeout=5)
1788{
1789    $ch = curl_init($url);
1790    curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
1791    curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
1792    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1793    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
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");
1795    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Don't pass through data to the browser.
1796    curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Frequent progress function calls.
1797    curl_setopt($ch, CURLOPT_NOPROGRESS, false); // Required to use CURLOPT_PROGRESSFUNCTION.
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    }
1812    curl_exec($ch);
1813    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1814    return preg_match('/^[23]\d\d$/', $http_code);
1815}
1816
1817/*
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*/
1828function getHttpHeader($url, $key=null, Array $valid_response_codes=[200], $method='GET')
1829{
1830    $context = stream_context_create(['http' => ['method' => $method]]);
1831    $headers = get_headers($url, 1, $context);
1832    $app =& \App::getInstance();
1833    $app->logMsg(sprintf('HTTP response headers for %s %s: %s', strtoupper($method), $url, getDump($headers, true, SC_DUMP_JSON)), LOG_DEBUG, __FILE__, __LINE__);
1834    if (empty($headers)) {
1835        return false;
1836    }
1837
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`
1842    $app->logMsg(sprintf('Last HTTP status code: %s', $final_http_status), LOG_DEBUG, __FILE__, __LINE__);
1843    if ($headers && preg_match(sprintf('/\b(%s)\b/', join('|', $valid_response_codes)), $final_http_status)) {
1844        $headers = array_change_key_case($headers, CASE_LOWER);
1845        if (!isset($key)) {
1846            return $headers;
1847        }
1848        $key = strtolower($key);
1849        if (isset($headers[$key])) {
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];
1852        }
1853    }
1854
1855    return false;
1856}
1857
1858/*
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
1874    if (false === ($resolved_filename = stream_resolve_include_path($filename))) {
1875        $app->logMsg(sprintf('JSON file "%s" not found in path "%s"', $filename, get_include_path()), LOG_ERR, __FILE__, __LINE__);
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
1884    if (null === ($data = json_decode(file_get_contents($resolved_filename), $assoc, $depth, $options))) {
1885        $app->logMsg(sprintf('JSON is unparsable: %s', $resolved_filename), LOG_ERR, __FILE__, __LINE__);
1886        return null;
1887    }
1888
1889    return $data;
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.
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.
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:
1928        $app->logMsg(sprintf('IP Intelligence timeout', null), LOG_NOTICE, __FILE__, __LINE__);
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:
1946        $app->logMsg('IP Intelligence: Unroutable or private address', LOG_NOTICE, __FILE__, __LINE__);
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:
1952        $app->logMsg('IP Intelligence: Banned: exceeded query limits, no permission, or invalid email address', LOG_WARNING, __FILE__, __LINE__);
1953        return false;
1954    case -6:
1955        $app->logMsg('IP Intelligence: Invalid contact information', LOG_WARNING, __FILE__, __LINE__);
1956        return false;
1957    default:
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) {
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    }
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);
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.