1<?php
2// base.php -- HotCRP base helper functions
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5// string helpers
6
7function str_starts_with($haystack, $needle) {
8    $nl = strlen($needle);
9    return $nl <= strlen($haystack) && substr($haystack, 0, $nl) === $needle;
10}
11
12function str_ends_with($haystack, $needle) {
13    $p = strlen($haystack) - strlen($needle);
14    return $p >= 0 && substr($haystack, $p) === $needle;
15}
16
17function stri_ends_with($haystack, $needle) {
18    $p = strlen($haystack) - strlen($needle);
19    return $p >= 0 && strcasecmp(substr($haystack, $p), $needle) == 0;
20}
21
22function preg_matchpos($pattern, $subject) {
23    if (preg_match($pattern, $subject, $m, PREG_OFFSET_CAPTURE))
24        return $m[0][1];
25    else
26        return false;
27}
28
29function cleannl($text) {
30    if (substr($text, 0, 3) === "\xEF\xBB\xBF")
31        $text = substr($text, 3);
32    if (strpos($text, "\r") !== false) {
33        $text = str_replace("\r\n", "\n", $text);
34        $text = strtr($text, "\r", "\n");
35    }
36    if (strlen($text) && $text[strlen($text) - 1] !== "\n")
37        $text .= "\n";
38    return $text;
39}
40
41function space_join(/* $str_or_array, ... */) {
42    $t = "";
43    foreach (func_get_args() as $arg)
44        if (is_array($arg)) {
45            foreach ($arg as $x)
46                if ($x !== "" && $x !== false && $x !== null)
47                    $t .= ($t === "" ? "" : " ") . $x;
48        } else if ($arg !== "" && $arg !== false && $arg !== null)
49            $t .= ($t === "" ? "" : " ") . $arg;
50    return $t;
51}
52
53function is_valid_utf8($str) {
54    return !!preg_match('//u', $str);
55}
56
57if (function_exists("iconv")) {
58    function windows_1252_to_utf8($str) {
59        return iconv("Windows-1252", "UTF-8//IGNORE", $str);
60    }
61    function mac_os_roman_to_utf8($str) {
62        return iconv("Mac", "UTF-8//IGNORE", $str);
63    }
64} else if (function_exists("mb_convert_encoding")) {
65    function windows_1252_to_utf8($str) {
66        return mb_convert_encoding($str, "UTF-8", "Windows-1252");
67    }
68}
69if (!function_exists("windows_1252_to_utf8")) {
70    function windows_1252_to_utf8($str) {
71        return UnicodeHelper::windows_1252_to_utf8($str);
72    }
73}
74if (!function_exists("mac_os_roman_to_utf8")) {
75    function mac_os_roman_to_utf8($str) {
76        return UnicodeHelper::mac_os_roman_to_utf8($str);
77    }
78}
79
80function convert_to_utf8($str) {
81    if (substr($str, 0, 3) === "\xEF\xBB\xBF")
82        $str = substr($str, 3);
83    if (is_valid_utf8($str))
84        return $str;
85    $pfx = substr($str, 0, 5000);
86    if (substr_count($pfx, "\r") > 1.5 * substr_count($pfx, "\n"))
87        return mac_os_roman_to_utf8($str);
88    else
89        return windows_1252_to_utf8($str);
90}
91
92function simplify_whitespace($x) {
93    // Replace invisible Unicode space-type characters with true spaces,
94    // including control characters and DEL.
95    return trim(preg_replace('/(?:[\x00-\x20\x7F]|\xC2[\x80-\xA0]|\xE2\x80[\x80-\x8A\xA8\xA9\xAF]|\xE2\x81\x9F|\xE3\x80\x80)+/', " ", $x));
96}
97
98function prefix_word_wrap($prefix, $text, $indent = 18, $totWidth = 75) {
99    if (is_int($indent)) {
100        $indentlen = $indent;
101        $indent = str_pad("", $indent);
102    } else
103        $indentlen = strlen($indent);
104
105    $out = "";
106    if ($prefix !== false) {
107        while ($text !== "" && ctype_space($text[0])) {
108            $out .= $text[0];
109            $text = substr($text, 1);
110        }
111    } else if (($line = UnicodeHelper::utf8_line_break($text, $totWidth)) !== false)
112        $out .= $line . "\n";
113
114    while (($line = UnicodeHelper::utf8_line_break($text, $totWidth - $indentlen)) !== false)
115        $out .= $indent . preg_replace('/^\pZ+/u', '', $line) . "\n";
116
117    if ($prefix === false)
118        /* skip */;
119    else if (strlen($prefix) <= $indentlen) {
120        $prefix = str_pad($prefix, $indentlen, " ", STR_PAD_LEFT);
121        $out = $prefix . substr($out, $indentlen);
122    } else
123        $out = $prefix . "\n" . $out;
124
125    if (!str_ends_with($out, "\n"))
126        $out .= "\n";
127    return $out;
128}
129
130function center_word_wrap($text, $totWidth = 75, $multi_center = false) {
131    if (strlen($text) <= $totWidth && !preg_match('/[\200-\377]/', $text))
132        return str_pad($text, (int) (($totWidth + strlen($text)) / 2), " ", STR_PAD_LEFT) . "\n";
133    $out = "";
134    while (($line = UnicodeHelper::utf8_line_break($text, $totWidth)) !== false) {
135        $linelen = UnicodeHelper::utf8_glyphlen($line);
136        $out .= str_pad($line, (int) (($totWidth + $linelen) / 2), " ", STR_PAD_LEFT) . "\n";
137    }
138    return $out;
139}
140
141function count_words($text) {
142    return preg_match_all('/[^-\s.,;:<>!?*_~`#|]\S*/', $text);
143}
144
145function friendly_boolean($x) {
146    if (is_bool($x))
147        return $x;
148    else if (is_string($x) || is_int($x))
149        return filter_var($x, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
150    else
151        return null;
152}
153
154interface Abbreviator {
155    public function abbreviations_for($name, $data);
156}
157
158
159// email and MIME helpers
160
161function validate_email($email) {
162    // Allow @_.com email addresses.  Simpler than RFC822 validation.
163    if (!preg_match(':\A[-!#$%&\'*+./0-9=?A-Z^_`a-z{|}~]+@(.+)\z:', $email, $m))
164        return false;
165    if ($m[1][0] === "_")
166        return preg_match(':\A_\.[0-9A-Za-z]+\z:', $m[1]);
167    else
168        return preg_match(':\A([-0-9A-Za-z]+\.)+[0-9A-Za-z]+\z:', $m[1]);
169}
170
171function mime_quote_string($word) {
172    return '"' . preg_replace('_(?=[\x00-\x1F\\"])_', '\\', $word) . '"';
173}
174
175function mime_token_quote($word) {
176    if (preg_match('_\A[^][\x00-\x20\x80-\xFF()<>@,;:\\"/?=]+\z_', $word))
177        return $word;
178    else
179        return mime_quote_string($word);
180}
181
182function rfc2822_words_quote($words) {
183    if (preg_match(':\A[-A-Za-z0-9!#$%&\'*+/=?^_`{|}~ \t]*\z:', $words))
184        return $words;
185    else
186        return mime_quote_string($words);
187}
188
189
190// encoders and decoders
191
192function html_id_encode($text) {
193    $x = preg_split('_([^-a-zA-Z0-9])_', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
194    for ($i = 1; $i < count($x); $i += 2)
195        $x[$i] = "_" . dechex(ord($x[$i]));
196    return join("", $x);
197}
198
199function html_id_decode($text) {
200    $x = preg_split(',(_[0-9A-Fa-f][0-9A-Fa-f]),', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
201    for ($i = 1; $i < count($x); $i += 2)
202        $x[$i] = chr(hexdec(substr($x[$i], 1)));
203    return join("", $x);
204}
205
206function base64url_encode($text) {
207    return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
208}
209
210function base64url_decode($data) {
211    return base64_decode(strtr($text, '-_', '+/'));
212}
213
214
215// JSON encoding helpers
216
217if (!function_exists("json_encode") || !function_exists("json_decode"))
218    require_once("$ConfSitePATH/lib/json.php");
219if (!function_exists("json_last_error_msg")) {
220    function json_last_error_msg() {
221        return false;
222    }
223}
224if (defined("JSON_UNESCAPED_LINE_TERMINATORS")) {
225    // JSON_UNESCAPED_UNICODE is only safe to send to the browser if
226    // JSON_UNESCAPED_LINE_TERMINATORS is defined.
227    function json_encode_browser($x, $flags = 0) {
228        return json_encode($x, $flags | JSON_UNESCAPED_UNICODE);
229    }
230} else {
231    function json_encode_browser($x, $flags = 0) {
232        return json_encode($x, $flags);
233    }
234}
235function json_encode_db($x, $flags = 0) {
236    return json_encode($x, $flags | JSON_UNESCAPED_UNICODE);
237}
238
239
240// array and object helpers
241
242function get($var, $idx, $default = null) {
243    if (is_array($var))
244        return array_key_exists($idx, $var) ? $var[$idx] : $default;
245    else if (is_object($var))
246        return property_exists($var, $idx) ? $var->$idx : $default;
247    else if ($var === null)
248        return $default;
249    else {
250        error_log("inappropriate get: " . var_export($var, true) . ": " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
251        return $default;
252    }
253}
254
255function get_s($var, $idx, $default = null) {
256    return (string) get($var, $idx, $default);
257}
258
259function get_i($var, $idx, $default = null) {
260    return (int) get($var, $idx, $default);
261}
262
263function get_f($var, $idx, $default = null) {
264    return (float) get($var, $idx, $default);
265}
266
267function opt($idx, $default = null) {
268    global $Conf, $Opt;
269    return get($Conf ? $Conf->opt : $Opt, $idx, $default);
270}
271
272function uploaded_file_error($finfo) {
273    $e = $finfo["error"];
274    $name = get($finfo, "name") ? "<span class=\"lineno\">" . htmlspecialchars($finfo["name"]) . ":</span> " : "";
275    if ($e == UPLOAD_ERR_INI_SIZE || $e == UPLOAD_ERR_FORM_SIZE)
276        return $name . "Uploaded file too big. The maximum upload size is " . ini_get("upload_max_filesize") . "B.";
277    else if ($e == UPLOAD_ERR_PARTIAL)
278        return $name . "Upload process interrupted.";
279    else if ($e != UPLOAD_ERR_NO_FILE)
280        return $name . "Unknown upload error.";
281    else
282        return false;
283}
284
285function make_qreq() {
286    $qreq = new Qrequest($_SERVER["REQUEST_METHOD"]);
287    foreach ($_GET as $k => $v)
288        $qreq->set_req($k, $v);
289    foreach ($_POST as $k => $v)
290        $qreq->set_req($k, $v);
291    if (empty($_POST))
292        $qreq->set_post_empty();
293
294    // $_FILES requires special processing since we want error messages.
295    $errors = [];
296    foreach ($_FILES as $nx => $fix) {
297        if (is_array($fix["error"])) {
298            $fis = [];
299            foreach (array_keys($fix["error"]) as $i) {
300                $fis[$i ? "$nx.$i" : $nx] = ["name" => $fix["name"][$i], "type" => $fix["type"][$i], "size" => $fix["size"][$i], "tmp_name" => $fix["tmp_name"][$i], "error" => $fix["error"][$i]];
301            }
302        } else {
303            $fis = [$nx => $fix];
304        }
305        foreach ($fis as $n => $fi) {
306            if ($fi["error"] == UPLOAD_ERR_OK) {
307                if (is_uploaded_file($fi["tmp_name"]))
308                    $qreq->set_file($n, $fi);
309            } else if (($err = uploaded_file_error($fi)))
310                $errors[] = $err;
311        }
312    }
313    if (!empty($errors) && Conf::$g)
314        Conf::msg_error("<div class=\"parseerr\"><p>" . join("</p>\n<p>", $errors) . "</p></div>");
315
316    return $qreq;
317}
318
319function defval($var, $idx, $defval = null) {
320    if (is_array($var))
321        return (isset($var[$idx]) ? $var[$idx] : $defval);
322    else
323        return (isset($var->$idx) ? $var->$idx : $defval);
324}
325
326function is_associative_array($a) {
327    // this method is suprisingly fast
328    return is_array($a) && array_values($a) !== $a;
329}
330
331function array_to_object_recursive($a) {
332    if (is_associative_array($a)) {
333        $o = (object) array();
334        foreach ($a as $k => $v)
335            if ($k !== "")
336                $o->$k = array_to_object_recursive($v);
337        return $o;
338    } else
339        return $a;
340}
341
342function object_replace($a, $b) {
343    foreach (is_object($b) ? get_object_vars($b) : $b as $k => $v)
344        if ($v === null)
345            unset($a->$k);
346        else
347            $a->$k = $v;
348}
349
350function object_replace_recursive($a, $b) {
351    foreach (is_object($b) ? get_object_vars($b) : $b as $k => $v)
352        if ($v === null)
353            unset($a->$k);
354        else if (!property_exists($a, $k)
355                 || !is_object($a->$k)
356                 || !is_object($v))
357            $a->$k = $v;
358        else
359            object_replace_recursive($a->$k, $v);
360}
361
362function json_object_replace($j, $updates, $nullable = false) {
363    if ($j === null)
364        $j = (object) [];
365    else if (is_array($j))
366        $j = (object) $j;
367    object_replace($j, $updates);
368    if ($nullable) {
369        $x = get_object_vars($j);
370        if (empty($x))
371            $j = null;
372    }
373    return $j;
374}
375
376
377// debug helpers
378
379function caller_landmark($position = 1, $skipfunction_re = null) {
380    if (is_string($position))
381        list($position, $skipfunction_re) = array(1, $position);
382    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
383    $fname = null;
384    for (++$position; isset($trace[$position]); ++$position) {
385        $fname = get_s($trace[$position], "class");
386        $fname .= ($fname ? "::" : "") . $trace[$position]["function"];
387        if ((!$skipfunction_re || !preg_match($skipfunction_re, $fname))
388            && ($fname !== "call_user_func" || get($trace[$position - 1], "file")))
389            break;
390    }
391    $t = "";
392    if ($position > 0 && ($pi = $trace[$position - 1]) && isset($pi["file"]))
393        $t = $pi["file"] . ":" . $pi["line"];
394    if ($fname)
395        $t .= ($t ? ":" : "") . $fname;
396    return $t ? : "<unknown>";
397}
398
399function assert_callback() {
400    trigger_error("Assertion backtrace: " . json_encode(array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 2)), E_USER_WARNING);
401}
402//assert_options(ASSERT_CALLBACK, "assert_callback");
403
404
405// pcntl helpers
406
407if (function_exists("pcntl_wifexited") && pcntl_wifexited(0) !== null) {
408    function pcntl_wifexitedsuccess($status) {
409        return pcntl_wifexited($status) && pcntl_wexitstatus($status) == 0;
410    }
411} else {
412    function pcntl_wifexitedsuccess($status) {
413        return ($status & 0x7f) == 0 && (($status & 0xff00) >> 8) == 0;
414    }
415}
416