xref: /web-bugs/include/functions.php (revision 4792d120)
1<?php
2
3/* User flags */
4define('BUGS_NORMAL_USER',  1<<0);
5define('BUGS_DEV_USER',     1<<1);
6define('BUGS_TRUSTED_DEV',  1<<2);
7define('BUGS_SECURITY_DEV', 1<<3);
8
9/* Contains functions and variables used throughout the bug system */
10
11// used in mail_bug_updates(), below, and class for search results
12$tla = [
13    'Open'          => 'Opn',
14    'Not a bug'     => 'Nab',
15    'Feedback'      => 'Fbk',
16    'No Feedback'   => 'NoF',
17    'Wont fix'      => 'Wfx',
18    'Duplicate'     => 'Dup',
19    'Critical'      => 'Ctl',
20    'Assigned'      => 'Asn',
21    'Analyzed'      => 'Ana',
22    'Verified'      => 'Ver',
23    'Suspended'     => 'Sus',
24    'Closed'        => 'Csd',
25    'Spam'          => 'Spm',
26    'Re-Opened'     => 'ReO',
27];
28
29$bug_types = [
30    'Bug'                      => 'Bug',
31    'Feature/Change Request'   => 'Req',
32    'Documentation Problem'    => 'Doc',
33    'Security'                 => 'Sec Bug'
34];
35
36// Used in show_state_options()
37$state_types = [
38    'Open'          => 2,
39    'Closed'        => 2,
40    'Re-Opened'     => 1,
41    'Duplicate'     => 1,
42    'Critical'      => 1,
43    'Assigned'      => 2,
44    'Not Assigned'  => 0,
45    'Analyzed'      => 1,
46    'Verified'      => 1,
47    'Suspended'     => 1,
48    'Wont fix'      => 1,
49    'No Feedback'   => 1,
50    'Feedback'      => 1,
51    'Old Feedback'  => 0,
52    'Stale'         => 0,
53    'Fresh'         => 0,
54    'Not a bug'     => 1,
55    'Spam'          => 1,
56    'All'           => 0,
57];
58
59/**
60 * Authentication
61 */
62function verify_user_password($user, $pass)
63{
64    global $errors;
65
66    $post = http_build_query(
67        [
68            'token' => getenv('AUTH_TOKEN'),
69            'username' => $user,
70            'password' => $pass,
71        ]
72    );
73
74    $opts = [
75        'method'    => 'POST',
76        'header'    => 'Content-type: application/x-www-form-urlencoded',
77        'content'    => $post,
78    ];
79
80    $ctx = stream_context_create(['http' => $opts]);
81
82    $s = file_get_contents('https://main.php.net/fetch/cvsauth.php', false, $ctx);
83
84    $a = @unserialize($s);
85    if (!is_array($a)) {
86        $errors[] = "Failed to get authentication information.\nMaybe master is down?\n";
87        return false;
88    }
89    if (isset($a['errno'])) {
90        $errors[] = "Authentication failed: {$a['errstr']}\n";
91        return false;
92    }
93
94    $_SESSION["user"] = $user;
95
96    return true;
97}
98
99function bugs_has_access ($bug_id, $bug, $pw, $user_flags)
100{
101    global $auth_user;
102
103    if ($bug['private'] != 'Y') {
104        return true;
105    }
106
107    // When the bug is private, only the submitter, trusted devs, security devs and assigned dev
108    // should see the report info
109    if ($user_flags & (BUGS_SECURITY_DEV | BUGS_TRUSTED_DEV)) {
110        // trusted and security dev
111        return true;
112    } else if (($user_flags == BUGS_NORMAL_USER) && $pw != '' && verify_bug_passwd($bug_id, bugs_get_hash($pw))) {
113        // The submitter
114        return true;
115    } else if (($user_flags & BUGS_DEV_USER) && $bug['reporter_name'] != '' &&
116        strtolower($bug['reporter_name']) == strtolower($auth_user->handle)) {
117        // The submitter (php developer)
118        return true;
119    } else if (($user_flags & BUGS_DEV_USER) && $bug['assign'] != '' &&
120        strtolower($bug['assign']) == strtolower($auth_user->handle)) {
121        // The assigned dev
122        return true;
123    }
124
125    return false;
126}
127
128function bugs_authenticate (&$user, &$pw, &$logged_in, &$user_flags)
129{
130    global $auth_user, $ROOT_DIR;
131
132    // Default values
133    $user = '';
134    $pw = '';
135    $logged_in = false;
136
137    $user_flags = BUGS_NORMAL_USER;
138
139    // Set username and password
140    if (!empty($_POST['pw'])) {
141        if (empty($_POST['user'])) {
142            $user = '';
143        } else {
144            $user = htmlspecialchars($_POST['user']);
145        }
146        $user = strtolower($user);
147        $pw = $_POST['pw'];
148    } elseif (isset($auth_user) && is_object($auth_user) && $auth_user->handle) {
149        $user = $auth_user->handle;
150        $pw = $auth_user->password;
151    }
152
153    // Authentication and user level check
154    // User levels are: reader (0), commenter/patcher/etc. (edit = 3), submitter (edit = 2), developer (edit = 1)
155    if (!empty($_SESSION["user"])) {
156        $user = $_SESSION["user"];
157        $user_flags = BUGS_DEV_USER;
158        $logged_in = 'developer';
159        $auth_user = new stdClass;
160        $auth_user->handle = $user;
161        $auth_user->email = "{$user}@php.net";
162        $auth_user->name = $user;
163    } elseif ($user != '' && $pw != '' && verify_user_password($user, $pw)) {
164        $user_flags = BUGS_DEV_USER;
165        $logged_in = 'developer';
166        $auth_user = new stdClass;
167        $auth_user->handle = $user;
168        $auth_user->email = "{$user}@php.net";
169        $auth_user->name = $user;
170    } else {
171        $auth_user = new stdClass;
172        $auth_user->email = isset($_POST['in']['email']) ? $_POST['in']['email'] : '';
173        $auth_user->handle = '';
174        $auth_user->name = '';
175    }
176
177    // Check if developer is trusted
178    if ($logged_in == 'developer') {
179        require_once "{$ROOT_DIR}/include/trusted-devs.php";
180
181        if (in_array(strtolower($user), $trusted_developers)) {
182            $user_flags |= BUGS_TRUSTED_DEV;
183        }
184        if (in_array(strtolower($user), $security_developers)) {
185            $user_flags |= BUGS_SECURITY_DEV;
186        }
187    }
188}
189
190/* Primitive check for SPAM. Add more later. */
191function is_spam($string)
192{
193    // @php.net users are given permission to spam... we gotta eat! See also bug #48126
194    if (!empty($GLOBALS['auth_user']->handle)) {
195        return false;
196    }
197
198    if (preg_match_all('/https?:\/\/(\S+)/', $string, $matches)) {
199        foreach ($matches[1] as $match) {
200            if (!preg_match('/^[^\/)]*(php\.net|github\.com)/', $match)) {
201                return "Due to large amounts of spam, only links to php.net and github.com (including subdomains like gist.github.com) are allowed.";
202            }
203        }
204    }
205
206    $keywords = [
207        'spy',
208        'bdsm',
209        'massage',
210        'mortage',
211        'sex',
212        '11nong',
213        'oxycontin',
214        'distance-education',
215        'sismatech',
216        'justiceplan',
217        'prednisolone',
218        'baclofen',
219        'diflucan',
220        'unbra.se',
221        'objectis',
222        'angosso',
223        'colchicine',
224        'zovirax',
225        'korsbest',
226        'coachbags',
227        'chaneljpoutlet',
228        '\/Members\/',
229        'michaelkorsshop',
230        'mkmichaelkors',
231        'Burberrysale4u',
232        'gadboisphotos',
233        'oakleysunglasseslol',
234        'partydressuk',
235        'leslunettesdesoleil',
236        'PaulRGuthrie',
237        '[a-z]*?fuck[a-z]*?',
238        'jerseys',
239        'wholesale',
240        'fashionretailshop01',
241        'amoxicillin',
242        'helpdeskaustralia',
243        'porn',
244        'aarinkaur',
245        'lildurk',
246        'tvfun',
247    ];
248
249    if (preg_match('/\b('. implode('|', $keywords) . ')\b/i', $string)) {
250        return "Comment contains spam word, consider rewording.";
251    }
252
253    return false;
254}
255
256/* Primitive check for SPAMmy user. Add more later. */
257function is_spam_user($email)
258{
259    if (preg_match("/(rhsoft|reindl|phpbugreports|bugreprtsz|bugreports\d*@gmail|training365)/i", $email)) {
260        return true;
261    }
262    return false;
263}
264
265/**
266 * Obfuscates email addresses to hinder spammer's spiders
267 *
268 * Turns "@" into character entities that get interpreted as "at" and
269 * turns "." into character entities that get interpreted as "dot".
270 *
271 * @param string $txt        the email address to be obfuscated
272 * @param string $format    how the output will be displayed ('html', 'text', 'reverse')
273 *
274 * @return string    the altered email address
275 */
276function spam_protect($txt, $format = 'html')
277{
278    /* php.net addresses are not protected! */
279    if (preg_match('/^(.+)@php\.net$/i', $txt)) {
280        return $txt;
281    }
282    if ($format == 'html') {
283        $translate = [
284            '@' => ' &#x61;&#116; ',
285            '.' => ' &#x64;&#111;&#x74; ',
286        ];
287    } else {
288        $translate = [
289            '@' => ' at ',
290            '.' => ' dot ',
291        ];
292        if ($format == 'reverse') {
293            $translate = array_flip($translate);
294        }
295    }
296    return strtr($txt, $translate);
297}
298
299/**
300 * Goes through each variable submitted and returns the value
301 * from the first variable which has a non-empty value
302 *
303 * Handy function for when you're dealing with user input or a default.
304 *
305 * @param mixed        as many variables as you wish to check
306 *
307 * @return mixed    the value, if any
308 *
309 * @see field(), txfield()
310 */
311function oneof()
312{
313    foreach (func_get_args() as $arg) {
314        if ($arg) {
315            return $arg;
316        }
317    }
318}
319
320/**
321 * Returns the data from the field requested and sanitizes
322 * it for use as HTML
323 *
324 * If the data from a form submission exists, that is used.
325 * But if that's not there, the info is obtained from the database.
326 *
327 * @param string $n        the name of the field to be looked for
328 *
329 * @return mixed        the data requested
330 *
331 * @see oneof(), txfield()
332 */
333function field($n)
334{
335    return oneof(isset($_POST['in']) ?
336        htmlspecialchars(isset($_POST['in'][$n]) ? $_POST['in'][$n] : '') : null,
337            htmlspecialchars($GLOBALS['bug'][$n] ?? ''));
338}
339
340/**
341 * Escape string so it can be used as HTML
342 *
343 * @param string $in    the string to be sanitized
344 *
345 * @return string        the sanitized string
346 *
347 * @see txfield()
348 */
349function clean($in)
350{
351    return mb_encode_numericentity($in,
352        [
353            0x0, 0x8, 0, 0xffffff,
354            0xb, 0xc, 0, 0xffffff,
355            0xe, 0x1f, 0, 0xffffff,
356            0x22, 0x22, 0, 0xffffff,
357            0x26, 0x27, 0, 0xffffff,
358            0x3c, 0x3c, 0, 0xffffff,
359            0x3e, 0x3e, 0, 0xffffff,
360            0x7f, 0x84, 0, 0xffffff,
361            0x86, 0x9f, 0, 0xffffff,
362            0xfdd0, 0xfdef, 0, 0xffffff,
363            0x1fffe, 0x1ffff, 0, 0xffffff,
364            0x2fffe, 0x2ffff, 0, 0xffffff,
365            0x3fffe, 0x3ffff, 0, 0xffffff,
366            0x4fffe, 0x4ffff, 0, 0xffffff,
367            0x5fffe, 0x5ffff, 0, 0xffffff,
368            0x6fffe, 0x6ffff, 0, 0xffffff,
369            0x7fffe, 0x7ffff, 0, 0xffffff,
370            0x8fffe, 0x8ffff, 0, 0xffffff,
371            0x9fffe, 0x9ffff, 0, 0xffffff,
372            0xafffe, 0xaffff, 0, 0xffffff,
373            0xbfffe, 0xbffff, 0, 0xffffff,
374            0xcfffe, 0xcffff, 0, 0xffffff,
375            0xdfffe, 0xdffff, 0, 0xffffff,
376            0xefffe, 0xeffff, 0, 0xffffff,
377            0xffffe, 0xfffff, 0, 0xffffff,
378            0x10fffe, 0x10ffff, 0, 0xffffff,
379        ],
380    'UTF-8');
381}
382
383/**
384 * Returns the data from the field requested and sanitizes
385 * it for use as plain text
386 *
387 * If the data from a form submission exists, that is used.
388 * But if that's not there, the info is obtained from the database.
389 *
390 * @param string $n        the name of the field to be looked for
391 *
392 * @return mixed        the data requested
393 *
394 * @see clean()
395 */
396function txfield($n, $bug = null, $in = null)
397{
398    $one = (isset($in) && isset($in[$n])) ? $in[$n] : false;
399    if ($one) {
400        return $one;
401    }
402
403    $two = (isset($bug) && isset($bug[$n])) ? $bug[$n] : false;
404    if ($two) {
405        return $two;
406    }
407}
408
409/**
410 * Prints age <option>'s for use in a <select>
411 *
412 * @param string $current    the field's current value
413 *
414 * @return void
415 */
416function show_byage_options($current)
417{
418    $opts = [
419        '0' => 'the beginning',
420        '1'    => 'yesterday',
421        '7'    => '7 days ago',
422        '15' => '15 days ago',
423        '30' => '30 days ago',
424        '90' => '90 days ago',
425    ];
426    foreach ($opts as $k => $v) {
427        echo "<option value=\"$k\"", ($current==$k ? ' selected="selected"' : ''), ">$v</option>\n";
428    }
429}
430
431/**
432 * Prints a list of <option>'s for use in a <select> element
433 * asking how many bugs to display
434 *
435 * @param int $limit    the presently selected limit to be used as the default
436 *
437 * @return void
438 */
439function show_limit_options($limit = 30)
440{
441    for ($i = 10; $i < 100; $i += 10) {
442        echo '<option value="' . $i . '"';
443        if ($limit == $i) {
444            echo ' selected="selected"';
445        }
446        echo ">$i bugs</option>\n";
447    }
448
449    echo '<option value="All"';
450    if ($limit == 'All') {
451        echo ' selected="selected"';
452    }
453    echo ">All</option>\n";
454}
455
456/**
457 * Prints bug type <option>'s for use in a <select>
458 *
459 * Options include "Bug", "Documentation Problem" and "Feature/Change Request."
460 *
461 * @param string    $current    bug's current type
462 * @param bool      $deprecated whether or not deprecated types should be shown
463 * @param bool      $all        whether or not 'All' should be an option
464 *
465 * @retun void
466 */
467function show_type_options($current, $deprecated, $all = false)
468{
469    global $bug_types;
470
471    if ($all) {
472        if (!$current) {
473            $current = 'All';
474        }
475        echo '<option value="All"';
476        if ($current == 'All') {
477            echo ' selected="selected"';
478        }
479        echo ">All</option>\n";
480    } elseif (!$current) {
481        $current = 'bug';
482    }
483
484    foreach ($bug_types as $k => $v) {
485        if ($k !== 'Security' && !$deprecated) {
486            continue;
487        }
488        $selected = strcasecmp($current, $k) ? '' : ' selected="selected"';
489        $k = htmlentities($k, ENT_QUOTES);
490        echo "<option value=\"$k\"$selected>$k</option>";
491    }
492}
493
494/**
495 * Prints bug state <option>'s for use in a <select> list
496 *
497 * @param string $state        the bug's present state
498 * @param int    $user_mode    the 'edit' mode
499 * @param string $default    the default value
500 *
501 * @return void
502 */
503function show_state_options($state, $user_mode = 0, $default = '', $assigned = 0)
504{
505    global $state_types, $tla;
506
507    if (!$state && !$default) {
508        $state = $assigned ? 'Assigned' : 'Open';
509    } elseif (!$state) {
510        $state = $default;
511    }
512
513    /* regular users can only pick states with type 2 for unclosed bugs */
514    if ($state != 'All' && isset($state_types[$state]) && $state_types[$state] == 1 && $user_mode == 2) {
515        switch ($state)
516        {
517            /* If state was 'Feedback', set state automatically to 'Assigned' if the bug was
518             * assigned to someone before it to be set to 'Feedback', otherwise set it to 'Open'.
519             */
520            case 'Feedback':
521                if ($assigned) {
522                    echo '<option class="'.$tla['Assigned'].'">Assigned</option>'."\n";
523                } else {
524                    echo '<option class="'.$tla['Open'].'">Open</option>'."\n";
525                }
526                break;
527            case 'No Feedback':
528                echo '<option class="'.$tla['Re-Opened'].'">Re-Opened</option>'."\n";
529                break;
530            default:
531                echo '<option';
532                if (isset($tla[$state])) {
533                    echo ' class="'.$tla[$state].'"';
534                }
535                echo '>'.$state.'</option>'."\n";
536                break;
537        }
538        /* Allow state 'Closed' always when current state is not 'Not a bug' */
539        if ($state != 'Not a bug') {
540            echo '<option class="'.$tla['Closed'].'">Closed</option>'."\n";
541        }
542    } else {
543        foreach($state_types as $type => $mode) {
544            if (($state == 'Closed' && $type == 'Open')
545                || ($state == 'Open' && $type == 'Re-Opened')) {
546                continue;
547            }
548            if ($mode >= $user_mode) {
549                echo '<option';
550                if (isset($tla[$type])) {
551                    echo ' class="'.$tla[$type].'"';
552                }
553                if ($type == $state) {
554                    echo ' selected="selected"';
555                }
556                echo ">$type</option>\n";
557            }
558        }
559    }
560}
561
562/**
563 * Prints bug resolution <option>'s for use in a <select> list
564 *
565 * @param string $current    the bug's present state
566 * @param int    $expande    whether or not a longer explanation should be displayed
567 *
568 * @return void
569 */
570function show_reason_types($current = '', $expanded = 0)
571{
572    global $RESOLVE_REASONS;
573
574    if ($expanded) {
575        echo '<option value=""></option>' . "\n";
576    }
577    foreach ($RESOLVE_REASONS as $val)
578    {
579        if (empty($val['package_name'])) {
580            $sel = ($current == $val['name']) ? " selected='selected'" : '';
581            echo "<option value='{$val['name']}' {$sel} >{$val['title']}";
582            if ($expanded) {
583                echo " ({$val['status']})";
584            }
585            echo "</option>\n";
586        }
587    }
588}
589
590/**
591 * Prints PHP version number <option>'s for use in a <select> list
592 *
593 * @param string $current    the bug's current version number
594 *
595 * @return void
596 */
597function show_version_options($current)
598{
599    global $ROOT_DIR, $versions;
600
601    $use = 0;
602
603    echo '<option value="">--Please Select--</option>' , "\n";
604    foreach($versions as $v) {
605        echo '<option';
606        if ($current == $v) {
607            echo ' selected="selected"';
608        }
609        echo '>' , htmlspecialchars($v) , "</option>\n";
610        if ($current == $v) {
611            $use++;
612        }
613    }
614    if (!$use && $current) {
615        echo '<option selected="selected">' , htmlspecialchars($current) , "</option>\n";
616    }
617    echo '<option value="earlier">Earlier? Upgrade first!</option>', "\n";
618}
619
620/**
621 * Prints package name <option>'s for use in a <select> list
622 *
623 * @param string $current    the bug's present state
624 * @param int    $show_any    whether or not 'Any' should be an option. 'Any'
625 *                            will only be printed if no $current value exists.
626 * @param string $default     the default value
627 *
628 * @return void
629 */
630function show_package_options($current, $show_any, $default = '')
631{
632    global $pseudo_pkgs;
633    $disabled_style = ' style="background-color:#eee;"';
634    static $bug_groups;
635
636    if (!isset($bug_groups)) {
637        $bug_groups = array_filter(
638            $pseudo_pkgs,
639            function ($value) {
640                return is_array($value[2]);
641            }
642        );
643    }
644
645    if (!$current && (!$default || $default == 'none') && !$show_any) {
646        echo "<option value=\"none\">--Please Select--</option>\n";
647    } elseif (!$current && $show_any == 1) {
648        $current = 'Any';
649    } elseif (!$current) {
650        $current = $default;
651    }
652
653    if (!is_array($bug_groups)) {
654        return;
655    }
656
657
658    foreach ($bug_groups as $key => $bug_group) {
659        echo "<optgroup label=\"{$bug_group[0]}\"" .
660            (($bug_group[1]) ? $disabled_style : ''), "\n>";
661
662        array_unshift($bug_group[2], $key);
663        foreach ($bug_group[2] as $name) {
664            $child = $pseudo_pkgs[$name];
665            if ($show_any == 1 || $key != 'Any') {
666                echo "<option value=\"$name\"";
667                if ((is_array($current) && in_array($name, $current)) || ($name == $current)) {
668                    echo ' selected="selected"';
669                }
670                // Show disabled categories with different background color in listing
671                echo (($child[1]) ? $disabled_style : ''), ">{$child[0]}</option>\n";
672            }
673        }
674        echo "</optgroup>\n";
675    }
676}
677
678/**
679 * Prints a series of radio inputs to determine how the search
680 * term should be looked for
681 *
682 * @param string $current    the users present selection
683 *
684 * @return void
685 */
686function show_boolean_options($current)
687{
688    $options = ['any', 'all', 'raw'];
689    foreach ($options as $val => $type) {
690        echo '<input type="radio" id="boolean' . $val . '" name="boolean" value="', $val, '"';
691        if ($val === $current) {
692            echo ' checked="checked"';
693        }
694        echo '><label for="boolean' . $val . '">'.$type.'</label>';
695    }
696}
697
698/**
699 * Display errors or warnings as a <ul> inside a <div>
700 *
701 * Here's what happens depending on $in:
702 *     + string:    value is printed
703 *     + array:     looped through and each value is printed.
704 *                If array is empty, nothing is displayed.
705 *
706 * @param string|array $in see long description
707 * @param string $class        name of the HTML class for the <div> tag. ("errors", "warnings")
708 * @param string $head        string to be put above the message
709 *
710 * @return bool        true if errors were submitted, false if not
711 */
712function display_bug_error($in, $class = 'errors', $head = 'ERROR:')
713{
714    if (!is_array($in)) {
715        $in = [$in];
716    } elseif (!count($in)) {
717        return false;
718    }
719
720    echo "<div class='{$class}'>{$head}<ul>";
721    foreach ($in as $msg) {
722        echo '<li>' , htmlspecialchars($msg) , "</li>\n";
723    }
724    echo "</ul></div>\n";
725    return true;
726}
727
728/**
729 * Returns array of changes going to be made
730 */
731function bug_diff($bug, $in)
732{
733    $changed = [];
734
735    if (!empty($in['email']) && (trim($in['email']) != trim($bug['email']))) {
736        $changed['reported_by']['from'] = spam_protect($bug['email'], 'text');
737        $changed['reported_by']['to'] = spam_protect(txfield('email', $bug, $in), 'text');
738    }
739
740    $fields = [
741        'sdesc'                => 'Summary',
742        'status'            => 'Status',
743        'bug_type'            => 'Type',
744        'package_name'        => 'Package',
745        'php_os'            => 'Operating System',
746        'php_version'        => 'PHP Version',
747        'assign'            => 'Assigned To',
748        'block_user_comment' => 'Block user comment',
749        'private'            => 'Private report',
750        'cve_id'            => 'CVE-ID'
751    ];
752
753    foreach (array_keys($fields) as $name) {
754        if (array_key_exists($name, $in) && array_key_exists($name, $bug)) {
755            $to   = trim($in[$name]);
756            $from = trim($bug[$name]);
757            if ($from != $to) {
758                if (in_array($name, ['private', 'block_user_comment'])) {
759                    $from = $from == 'Y' ? 'Yes' : 'No';
760                    $to = $to == 'Y' ? 'Yes' : 'No';
761                }
762                $changed[$name]['from'] = $from;
763                $changed[$name]['to'] = $to;
764            }
765        }
766    }
767
768    return $changed;
769}
770
771function bug_diff_render_html($diff)
772{
773    $fields = [
774        'sdesc'                => 'Summary',
775        'status'            => 'Status',
776        'bug_type'            => 'Type',
777        'package_name'        => 'Package',
778        'php_os'            => 'Operating System',
779        'php_version'        => 'PHP Version',
780        'assign'            => 'Assigned To',
781        'block_user_comment' => 'Block user comment',
782        'private'            => 'Private report',
783        'cve_id'            => 'CVE-ID'
784    ];
785
786    // make diff output aligned
787    $actlength = $maxlength = 0;
788    foreach (array_keys($diff) as $v) {
789        $actlength = strlen($fields[$v]) + 2;
790        $maxlength = ($maxlength < $actlength) ? $actlength : $maxlength;
791    }
792
793    $changes = '<div class="changeset">' . "\n";
794    $spaces = str_repeat(' ', $maxlength + 1);
795    foreach ($diff as $name => $content) {
796        // align header content with headers (if a header contains
797        // more than one line, wrap it intelligently)
798        $field = str_pad($fields[$name] . ':', $maxlength);
799        $from = wordwrap('-'.$field.$content['from'], 72 - $maxlength, "\n$spaces"); // wrap and indent
800        $from = rtrim($from); // wordwrap may add spacer to last line
801        $to    = wordwrap('+'.$field.$content['to'], 72 - $maxlength, "\n$spaces"); // wrap and indent
802        $to    = rtrim($to); // wordwrap may add spacer to last line
803        $changes .= '<span class="removed">' . clean($from) . '</span>' . "\n";
804        $changes .= '<span class="added">' . clean($to) . '</span>' . "\n";
805    }
806    $changes .= '</div>';
807
808    return $changes;
809}
810
811/**
812 * Send an email notice about bug aditions and edits
813 *
814 * @param
815 *
816 * @return void
817 */
818function mail_bug_updates($bug, $in, $from, $ncomment, $edit = 1, $id = false)
819{
820    global $tla, $bug_types, $siteBig, $site_method, $site_url, $basedir;
821
822    $text = [];
823    $headers = [];
824    $changed = bug_diff($bug, $in);
825    $from = str_replace(["\n", "\r"], '', $from);
826
827    /* Default addresses */
828    list($mailto, $mailfrom, $bcc, $params) = get_package_mail(oneof(@$in['package_name'], $bug['package_name']), $id, oneof(@$in['bug_type'], $bug['bug_type']));
829
830    $headers[] = [' ID', $bug['id']];
831
832    switch ($edit) {
833        case 4:
834            $headers[] = [' Patch added by', $from];
835            $from = "\"{$from}\" <{$mailfrom}>";
836            break;
837        case 3:
838            $headers[] = [' Comment by', $from];
839            $from = "\"{$from}\" <{$mailfrom}>";
840            break;
841        case 2:
842            $from = spam_protect(txfield('email', $bug, $in), 'text');
843            $headers[] = [' User updated by', $from];
844            $from = "\"{$from}\" <{$mailfrom}>";
845            break;
846        default:
847            $headers[] = [' Updated by', $from];
848    }
849
850    $fields = [
851        'email'                => 'Reported by',
852        'sdesc'                => 'Summary',
853        'status'            => 'Status',
854        'bug_type'            => 'Type',
855        'package_name'        => 'Package',
856        'php_os'            => 'Operating System',
857        'php_version'        => 'PHP Version',
858        'assign'            => 'Assigned To',
859        'block_user_comment' => 'Block user comment',
860        'private'            => 'Private report',
861        'cve_id'            => 'CVE-ID',
862    ];
863
864    foreach ($fields as $name => $desc) {
865        $prefix = ' ';
866        if (isset($changed[$name])) {
867            $headers[] = ["-{$desc}", $changed[$name]['from']];
868            $prefix = '+';
869        }
870
871        /* only fields that are set get added. */
872        if ($f = txfield($name, $bug, $in)) {
873            if ($name == 'email') {
874                $f = spam_protect($f, 'text');
875            }
876            $foo = isset($changed[$name]['to']) ? $changed[$name]['to'] : $f;
877            $headers[] = [$prefix.$desc, $foo];
878        }
879    }
880
881    /* Make header output aligned */
882    $maxlength = 0;
883    $actlength = 0;
884    foreach ($headers as $v) {
885        $actlength = strlen($v[0]) + 1;
886        $maxlength = (($maxlength < $actlength) ? $actlength : $maxlength);
887    }
888
889    /* Align header content with headers (if a header contains more than one line, wrap it intelligently) */
890    $header_text = '';
891
892    $spaces = str_repeat(' ', $maxlength + 1);
893    foreach ($headers as $v) {
894        $hcontent = wordwrap($v[1], 72 - $maxlength, "\n{$spaces}"); // wrap and indent
895        $hcontent = rtrim($hcontent); // wordwrap may add spacer to last line
896        $header_text .= str_pad($v[0] . ':', $maxlength) . " {$hcontent}\n";
897    }
898
899    if ($ncomment) {
900#        $ncomment = preg_replace('#<div class="changeset">(.*)</div>#sUe', "ltrim(strip_tags('\\1'))", $ncomment);
901        $ncomment = preg_replace_callback('#<div class="changeset">(.*)</div>#sU', function ($m) { return ltrim(strip_tags($m[0])); }, $ncomment);
902
903        $text[] = " New Comment:\n\n{$ncomment}";
904    }
905
906    $old_comments = get_old_comments($bug['id'], empty($ncomment));
907#    $old_comments = preg_replace('#<div class="changeset">(.*)</div>#sUe', "ltrim(strip_tags('\\1'))", $old_comments);
908    $old_comments = preg_replace_callback('#<div class="changeset">(.*)</div>#sU', function ($m) { return ltrim(strip_tags($m[0])); }, $old_comments);
909
910    $text[] = $old_comments;
911
912    $wrapped_text = join("\n", $text);
913
914    /* user text with attention, headers and previous messages */
915    $user_text = <<< USER_TEXT
916ATTENTION! Do NOT reply to this email!
917To reply, use the web interface found at
918{$site_method}://{$site_url}{$basedir}/bug.php?id={$bug['id']}&edit=2
919
920{$header_text}
921{$wrapped_text}
922USER_TEXT;
923
924    /* developer text with headers, previous messages, and edit link */
925    $dev_text = <<< DEV_TEXT
926Edit report at {$site_method}://{$site_url}{$basedir}/bug.php?id={$bug['id']}&edit=1
927
928{$header_text}
929{$wrapped_text}
930
931--
932Edit this bug report at {$site_method}://{$site_url}{$basedir}/bug.php?id={$bug['id']}&edit=1
933DEV_TEXT;
934
935    if (preg_match('/.*@php\.net\z/', $bug['email'])) {
936        $user_text = $dev_text;
937    }
938
939    // Defaults
940    $subj = $bug_types[$bug['bug_type']];
941    $sdesc = txfield('sdesc', $bug, $in);
942
943    /* send mail if status was changed, there is a comment, private turned on/off or the bug type was changed to/from Security */
944    if (empty($in['status']) || $in['status'] != $bug['status'] || $ncomment != '' ||
945        (isset($in['private']) && $in['private'] != $bug['private']) ||
946        (isset($in['bug_type']) && $in['bug_type'] != $bug['bug_type'] &&
947            ($in['bug_type'] == 'Security' || $bug['bug_type'] == 'Security'))) {
948        if (isset($in['bug_type']) && $in['bug_type'] != $bug['bug_type']) {
949            $subj = $bug_types[$bug['bug_type']] . '->' . $bug_types[$in['bug_type']];
950        }
951
952        $old_status = $bug['status'];
953        $new_status = $bug['status'];
954
955        if (isset($in['status']) && $in['status'] != $bug['status'] && $edit != 3) {    /* status changed */
956            $new_status = $in['status'];
957            $subj .= " #{$bug['id']} [{$tla[$old_status]}->{$tla[$new_status]}]";
958        } elseif ($edit == 4) {    /* patch */
959            $subj .= " #{$bug['id']} [PATCH]";
960        } elseif ($edit == 3) {    /* comment */
961            $subj .= " #{$bug['id']} [Com]";
962        } else {    /* status did not change and not comment */
963            $subj .= " #{$bug['id']} [{$tla[$bug['status']]}]";
964        }
965
966        // the user gets sent mail with an envelope sender that ignores bounces
967        bugs_mail(
968            $bug['email'],
969            "{$subj}: {$sdesc}",
970            $user_text,
971            "From: {$siteBig} Bug Database <{$mailfrom}>\r\n" .
972            "Bcc: {$bcc}\r\n" .
973            "X-PHP-Bug: {$bug['id']}\r\n" .
974            "X-PHP-Site: {$siteBig}\r\n" .
975            "In-Reply-To: <bug-{$bug['id']}@{$site_url}>"
976        );
977
978        // Spam protection
979        $tmp = $edit != 3 ? $in : $bug;
980        $tmp['new_status'] = $new_status;
981        $tmp['old_status'] = $old_status;
982        foreach (['bug_type', 'php_version', 'package_name', 'php_os'] as $field) {
983            $tmp[$field] = strtok($tmp[$field], "\r\n");
984        }
985
986        // but we go ahead and let the default sender get used for the list
987        bugs_mail(
988            $mailto,
989            "{$subj}: {$sdesc}",
990            $dev_text,
991            "From: {$from}\r\n".
992            "X-PHP-Bug: {$bug['id']}\r\n" .
993            "X-PHP-Site: {$siteBig}\r\n" .
994            "X-PHP-Type: {$tmp['bug_type']}\r\n" .
995            "X-PHP-Version: {$tmp['php_version']}\r\n" .
996            "X-PHP-Category: {$tmp['package_name']}\r\n" .
997            "X-PHP-OS: {$tmp['php_os']}\r\n" .
998            "X-PHP-Status: {$tmp['new_status']}\r\n" .
999            "X-PHP-Old-Status: {$tmp['old_status']}\r\n" .
1000            "In-Reply-To: <bug-{$bug['id']}@{$site_url}>",
1001            $params
1002        );
1003    }
1004
1005    /* if a developer assigns someone else, let that other person know about it */
1006    if ($edit == 1 && $in['assign'] && $in['assign'] != $bug['assign']) {
1007
1008        $email = $in['assign'] . '@php.net';
1009
1010        // If the developer assigns him self then skip
1011        if ($email == $from) {
1012            return;
1013        }
1014
1015        bugs_mail(
1016            $email,
1017            $bug_types[$bug['bug_type']] . ' #' . $bug['id'] . ' ' . txfield('sdesc', $bug, $in),
1018            "{$in['assign']} you have just been assigned to this bug by {$from}\n\n{$dev_text}",
1019            "From: {$from}\r\n" .
1020            "X-PHP-Bug: {$bug['id']}\r\n" .
1021            "In-Reply-To: <bug-{$bug['id']}@{$site_url}>"
1022        );
1023    }
1024}
1025
1026/**
1027 * Turns a unix timestamp into a uniformly formatted date
1028 *
1029 * If the date is during the current year, the year is omitted.
1030 *
1031 * @param int $ts            the unix timestamp to be formatted
1032 * @param string $format    format to use
1033 *
1034 * @return string    the formatted date
1035 */
1036function format_date($ts = null, $format = 'Y-m-d H:i e')
1037{
1038    if (!$ts) {
1039        $ts = time();
1040    }
1041    return gmdate($format, (int)$ts - date('Z', (int)$ts));
1042}
1043
1044/**
1045 * Produces a string containing the bug's prior comments
1046 *
1047 * @param int $bug_id    the bug's id number
1048 * @param int $all        should all existing comments be returned?
1049 *
1050 * @return string    the comments
1051 */
1052function get_old_comments($bug_id, $all = 0)
1053{
1054    global $dbh, $site_method, $site_url, $basedir;
1055
1056    $divider = str_repeat('-', 72);
1057    $max_message_length = 10 * 1024;
1058    $max_comments = 5;
1059    $output = '';
1060    $count = 0;
1061
1062    $res = $dbh->prepare("
1063        SELECT ts, email, comment
1064        FROM bugdb_comments
1065        WHERE bug = ? AND comment_type != 'log'
1066        ORDER BY ts DESC
1067    ")->execute([$bug_id]);
1068
1069    // skip the most recent unless the caller wanted all comments
1070    if (!$all) {
1071        $row = $res->fetch(\PDO::FETCH_NUM);
1072        if (!$row) {
1073            return '';
1074        }
1075    }
1076
1077    while (($row = $res->fetch(\PDO::FETCH_NUM)) && strlen($output) < $max_message_length && $count++ < $max_comments) {
1078        $email = spam_protect($row[1], 'text');
1079        $output .= "[{$row[0]}] {$email}\n\n{$row[2]}\n\n{$divider}\n";
1080    }
1081
1082    if (strlen($output) < $max_message_length && $count < $max_comments) {
1083        $res = $dbh->prepare("SELECT ts1, email, ldesc FROM bugdb WHERE id = ?")->execute([$bug_id]);
1084        if (!$res) {
1085            return $output;
1086        }
1087        $row = $res->fetch(\PDO::FETCH_NUM);
1088        if (!$row) {
1089            return $output;
1090        }
1091        $email = spam_protect($row[1], 'text');
1092        return ("
1093
1094Previous Comments:
1095{$divider}
1096{$output}[{$row[0]}] {$email}
1097
1098{$row[2]}
1099
1100{$divider}
1101
1102");
1103    } else {
1104        return "
1105
1106Previous Comments:
1107{$divider}
1108{$output}
1109
1110The remainder of the comments for this report are too long. To view
1111the rest of the comments, please view the bug report online at
1112
1113    {$site_method}://{$site_url}{$basedir}/bug.php?id={$bug_id}
1114";
1115    }
1116
1117    return '';
1118}
1119
1120/**
1121 * Converts any URI's found in the string to hyperlinks
1122 *
1123 * @param string $text    the text to be examined
1124 *
1125 * @return string    the converted string
1126 */
1127function addlinks($text)
1128{
1129    $text = htmlspecialchars($text);
1130    $text = preg_replace("/((mailto|http|https|ftp|nntp|news):.+?)(&gt;|\\s|\\)|\\.\\s|,\\s|$)/i","<a href=\"\\1\" rel=\"nofollow\">\\1</a>\\3",$text);
1131
1132    # what the heck is this for?
1133    $text = preg_replace("/[.,]?-=-\"/", '"', $text);
1134    return $text;
1135}
1136
1137/**
1138 * Determine if the given package name is legitimate
1139 *
1140 * @param string $package_name    the name of the package
1141 *
1142 * @return bool
1143 */
1144function package_exists($package_name)
1145{
1146    global $pseudo_pkgs;
1147
1148    return isset($pseudo_pkgs[$package_name]);
1149}
1150
1151/**
1152 * Validate an email address
1153 */
1154function is_valid_email($email, $phpnet_allowed = true)
1155{
1156    if (!$phpnet_allowed) {
1157        if (false !== stripos($email, '@php.net')) {
1158            return false;
1159        }
1160    }
1161    return (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
1162}
1163
1164/**
1165 * Validate an incoming bug report
1166 *
1167 * @param mixed $in usually $_POST['in']
1168 * @param bool $initial
1169 * @param bool $logged_in
1170 *
1171 * @return array
1172 */
1173function incoming_details_are_valid($in, $initial = 0, $logged_in = false)
1174{
1175    global $bug, $dbh, $bug_types, $versions;
1176
1177    $errors = [];
1178    if (!is_array($in)) {
1179        $errors[] = 'Invalid data submitted!';
1180        return $errors;
1181    }
1182    if ($initial || (!empty($in['email']) && $bug['email'] != $in['email'])) {
1183        if (!is_valid_email($in['email'])) {
1184            $errors[] = 'Please provide a valid email address.';
1185        }
1186    }
1187
1188    if (!$logged_in && $initial && (empty($in['passwd']) || !is_string($in['passwd']))) {
1189        $errors[] = 'Please provide a password for this bug report.';
1190    }
1191
1192    if (isset($in['php_version']) && $in['php_version'] == 'earlier') {
1193        $errors[] = 'Please select a valid PHP version. If your PHP version is too old, please upgrade first and see if the problem has not already been fixed.';
1194    }
1195
1196    if (empty($in['php_version']) || !is_string($in['php_version']) || ($initial && !in_array($in['php_version'], $versions))) {
1197        $errors[] = 'Please select a valid PHP version.';
1198    }
1199
1200    if (empty($in['package_name']) || !is_string($in['package_name']) || $in['package_name'] == 'none') {
1201        $errors[] = 'Please select an appropriate package.';
1202    } else if (!package_exists($in['package_name'])) {
1203        $errors[] = 'Please select an appropriate package.';
1204    }
1205
1206    if (empty($in['bug_type']) || !is_string($in['bug_type']) || !array_key_exists($in['bug_type'], $bug_types)) {
1207        $errors[] = 'Please select a valid bug type.';
1208    }
1209
1210    if (empty($in['sdesc']) || !is_string($in['sdesc'])) {
1211        $errors[] = 'You must supply a short description of the bug you are reporting.';
1212    }
1213
1214    if ($initial && (empty($in['ldesc']) || !is_string($in['ldesc']))) {
1215        $errors[] = 'You must supply a long description of the bug you are reporting.';
1216    }
1217
1218    return $errors;
1219}
1220
1221/**
1222 * Produces an array of email addresses the report should go to
1223 *
1224 * @param string $package_name    the package's name
1225 *
1226 * @return array        an array of email addresses
1227 */
1228function get_package_mail($package_name, $bug_id = false, $bug_type = 'Bug')
1229{
1230    global $dbh, $bugEmail, $docBugEmail, $secBugEmail, $security_distro_people;
1231
1232    $to = [];
1233    $params = '-f noreply@php.net';
1234    $mailfrom = $bugEmail;
1235
1236    if ($bug_type === 'Documentation Problem') {
1237        // Documentation problems *always* go to the doc team
1238        $to[] = $docBugEmail;
1239    } else if ($bug_type == 'Security') {
1240        // Security problems *always* go to the sec team
1241        $to[] = $secBugEmail;
1242        foreach ($security_distro_people as $user) {
1243            $to[] = "{$user}@php.net";
1244        }
1245        $params = '-f bounce-no-user@php.net';
1246    }
1247    else {
1248        /* Get package mailing list address */
1249        $res = $dbh->prepare('
1250            SELECT list_email, project
1251            FROM bugdb_pseudo_packages
1252            WHERE name = ?
1253        ')->execute([$package_name]);
1254
1255        list($list_email, $project) = $res->fetch(\PDO::FETCH_NUM);
1256
1257        if ($project == 'pecl') {
1258            $mailfrom = 'pecl-dev@lists.php.net';
1259        }
1260
1261        if ($list_email) {
1262            if ($list_email == 'systems@php.net') {
1263                $params = '-f bounce-no-user@php.net';
1264            }
1265            $to[] = $list_email;
1266        } else {
1267            // Get the maintainers handle
1268            if ($project == 'pecl') {
1269                $handles = $dbh->prepare("SELECT GROUP_CONCAT(handle) FROM bugdb_packages_maintainers WHERE package_name = ?")->execute([$package_name])->fetch(\PDO::FETCH_NUM)[0];
1270
1271                if ($handles) {
1272                    foreach (explode(',', $handles) as $handle) {
1273                        $to[] = $handle .'@php.net';
1274                    }
1275                } else {
1276                    $to[] = $mailfrom;
1277                }
1278            } else {
1279                // Fall back to default mailing list
1280                $to[] = $bugEmail;
1281            }
1282        }
1283    }
1284
1285    /* Include assigned to To list and subscribers in Bcc list */
1286    if ($bug_id) {
1287        $bug_id = (int) $bug_id;
1288
1289        $assigned = $dbh->prepare("SELECT assign FROM bugdb WHERE id= ? ")->execute([$bug_id])->fetch(\PDO::FETCH_NUM)[0];
1290        if ($assigned) {
1291            $assigned .= '@php.net';
1292            if ($assigned && !in_array($assigned, $to)) {
1293                $to[] = $assigned;
1294            }
1295        }
1296        $bcc = $dbh->prepare("SELECT email FROM bugdb_subscribe WHERE bug_id=?")->execute([$bug_id])->fetchAll();
1297
1298        $bcc = array_unique($bcc);
1299        return [implode(', ', $to), $mailfrom, implode(', ', $bcc), $params];
1300    } else {
1301        return [implode(', ', $to), $mailfrom, '', $params];
1302    }
1303}
1304
1305/**
1306 * Prepare a query string with the search terms
1307 *
1308 * @param string $search    the term to be searched for
1309 *
1310 * @return array
1311 */
1312function format_search_string($search, $boolean_search = false)
1313{
1314    global $dbh;
1315
1316    // Function will be updated to make results more relevant.
1317    // Quick hack for indicating ignored words.
1318    $min_word_len=3;
1319
1320    $words = preg_split("/\s+/", $search);
1321    $ignored = $used = [];
1322    foreach($words as $match)
1323    {
1324        if (strlen($match) < $min_word_len) {
1325            array_push($ignored, $match);
1326        } else {
1327            array_push($used, $match);
1328        }
1329    }
1330
1331    if ($boolean_search) {
1332        // require all used words (all)
1333        if ($boolean_search === 1) {
1334            $newsearch = '';
1335            foreach ($used as $word) {
1336                $newsearch .= "+$word ";
1337            }
1338            return [" AND MATCH (bugdb.email,sdesc,ldesc) AGAINST (" . $dbh->quote($newsearch) . " IN BOOLEAN MODE)", $ignored];
1339
1340        // allow custom boolean search (raw)
1341        } elseif ($boolean_search === 2) {
1342            return [" AND MATCH (bugdb.email,sdesc,ldesc) AGAINST (" . $dbh->quote($search) . " IN BOOLEAN MODE)", $ignored];
1343        }
1344    }
1345    // require any of the words (any)
1346    return [" AND MATCH (bugdb.email,sdesc,ldesc) AGAINST (" . $dbh->quote($search) . ")", $ignored];
1347}
1348
1349/**
1350 * Send the confirmation mail to confirm a subscription removal
1351 *
1352 * @param integer    bug ID
1353 * @param string    email to remove
1354 * @param array        bug data
1355 *
1356 * @return void
1357 */
1358function unsubscribe_hash($bug_id, $email)
1359{
1360    global $dbh, $siteBig, $site_method, $site_url, $bugEmail;
1361
1362    $now = time();
1363    $hash = crypt($email . $bug_id, $now);
1364
1365    $query = "
1366        UPDATE bugdb_subscribe
1367        SET unsubscribe_date = '{$now}',
1368            unsubscribe_hash = ?
1369        WHERE bug_id = ? AND email = ?
1370    ";
1371
1372    $affected = $dbh->prepare($query, null, null)->execute([$hash, $bug_id, $email]);
1373
1374    if ($affected > 0) {
1375        $hash = urlencode($hash);
1376        /* user text with attention, headers and previous messages */
1377        $user_text = <<< USER_TEXT
1378ATTENTION! Do NOT reply to this email!
1379
1380A request has been made to remove your subscription to
1381{$siteBig} bug #{$bug_id}
1382
1383To view the bug in question please use this link:
1384{$site_method}://{$site_url}{$basedir}/bug.php?id={$bug_id}
1385
1386To confirm the removal please use this link:
1387{$site_method}://{$site_url}{$basedir}/bug.php?id={$bug_id}&unsubscribe=1&t={$hash}
1388
1389
1390USER_TEXT;
1391
1392        bugs_mail(
1393            $email,
1394            "[$siteBig-BUG-unsubscribe] #{$bug_id}",
1395            $user_text,
1396            "From: {$siteBig} Bug Database <{$bugEmail}>\r\n".
1397            "X-PHP-Bug: {$bug_id}\r\n".
1398            "In-Reply-To: <bug-{$bug_id}@{$site_url}>"
1399        );
1400    }
1401}
1402
1403
1404/**
1405 * Remove a subscribtion
1406 *
1407 * @param integer    bug ID
1408 * @param string    hash
1409 *
1410 * @return void
1411 */
1412function unsubscribe($bug_id, $hash)
1413{
1414    global $dbh;
1415
1416    $bug_id = (int) $bug_id;
1417
1418    $query = "
1419        SELECT bug_id, email, unsubscribe_date, unsubscribe_hash
1420        FROM bugdb_subscribe
1421        WHERE bug_id = ? AND unsubscribe_hash = ? LIMIT 1
1422    ";
1423
1424    $sub = $dbh->prepare($query)->execute([$bug_id, $hash])->fetch();
1425
1426    if (!$sub) {
1427        return false;
1428    }
1429
1430    $now = time();
1431    $requested_on = $sub['unsubscribe_date'];
1432    /* 24hours delay to answer the mail */
1433    if (($now - $requested_on) > 86400) {
1434        return false;
1435    }
1436
1437    $query = "
1438        DELETE FROM bugdb_subscribe
1439        WHERE bug_id = ? AND unsubscribe_hash = ? AND email = ?
1440    ";
1441    $dbh->prepare($query)->execute([$bug_id, $hash, $sub['email']]);
1442    return true;
1443}
1444
1445/**
1446 * Add bug comment
1447 */
1448function bugs_add_comment($bug_id, $from, $from_name, $comment, $type = 'comment')
1449{
1450    global $dbh;
1451
1452    return $dbh->prepare("
1453        INSERT INTO bugdb_comments (bug, email, reporter_name, comment, comment_type, ts)
1454        VALUES (?, ?, ?, ?, ?, NOW())
1455    ")->execute([
1456        $bug_id, $from, $from_name, $comment, $type
1457    ]);
1458}
1459
1460/**
1461 * Change bug status
1462 */
1463function bugs_status_change($bug_id, $new_status)
1464{
1465    global $dbh;
1466
1467    return $dbh->prepare("
1468        UPDATE bugdb SET status = ? WHERE id = ? LIMIT 1
1469    ")->execute([$new_status, $bug_id]);
1470}
1471
1472/**
1473 * Verify bug password
1474 *
1475 * @return bool
1476 */
1477
1478function verify_bug_passwd($bug_id, $passwd)
1479{
1480    global $dbh;
1481
1482    return (bool) $dbh->prepare('SELECT 1 FROM bugdb WHERE id = ? AND passwd = ?')->execute([$bug_id, $passwd])->fetch(\PDO::FETCH_NUM)[0];
1483}
1484
1485/**
1486 * Mailer function. When DEVBOX is defined, this only outputs the parameters as-is.
1487 *
1488 * @return bool
1489 *
1490 */
1491function bugs_mail($to, $subject, $message, $headers = '', $params = '')
1492{
1493    if (empty($params)) {
1494        $params = '-f noreply@php.net';
1495    }
1496    if (DEVBOX === true) {
1497        if (defined('DEBUG_MAILS')) {
1498            echo '<pre>';
1499            var_dump(htmlspecialchars($to), htmlspecialchars($subject), htmlspecialchars($message), htmlspecialchars($headers));
1500            echo '</pre>';
1501        }
1502        return true;
1503    }
1504    return @mail($to, $subject, $message, $headers, $params);
1505}
1506
1507/**
1508 * Prints out the XHTML headers and top of the page.
1509 *
1510 * @param string $title    a string to go into the header's <title>
1511 * @return void
1512 */
1513function response_header($title, $extraHeaders = '')
1514{
1515    global $_header_done, $self, $auth_user, $logged_in, $siteBig, $site_method, $site_url, $basedir;
1516
1517    $is_logged = false;
1518
1519    if ($_header_done) {
1520        return;
1521    }
1522
1523    if ($logged_in === 'developer') {
1524        $is_logged = true;
1525        $username = $auth_user->handle;
1526    } else if (!empty($_SESSION['user'])) {
1527        $is_logged = true;
1528        $username = $_SESSION['user'];
1529    }
1530
1531    $_header_done = true;
1532
1533    header('Content-Type: text/html; charset=UTF-8');
1534    header('X-Frame-Options: SAMEORIGIN');
1535?>
1536<!DOCTYPE html>
1537<html lang="en">
1538<head>
1539    <meta charset="utf-8">
1540    <?php echo $extraHeaders; ?>
1541    <base href="<?php echo $site_method?>://<?php echo $site_url, $basedir; ?>/">
1542    <title><?php echo $siteBig; ?> :: <?php echo $title; ?></title>
1543    <link rel="shortcut icon" href="<?php echo $site_method?>://<?php echo $site_url, $basedir; ?>/images/favicon.ico">
1544    <link rel="stylesheet" href="<?php echo $site_method?>://<?php echo $site_url, $basedir; ?>/css/style.css">
1545</head>
1546
1547<body>
1548
1549<table id="top" class="head" cellspacing="0" cellpadding="0">
1550    <tr>
1551        <td class="head-logo">
1552            <a href="/"><img src="images/logo.png" alt="Bugs" vspace="2" hspace="2"></a>
1553        </td>
1554
1555        <td class="head-menu">
1556            <a href="https://php.net/">php.net</a>&nbsp;|&nbsp;
1557            <a href="https://php.net/support.php">support</a>&nbsp;|&nbsp;
1558            <a href="https://php.net/docs.php">documentation</a>&nbsp;|&nbsp;
1559            <a href="report.php">report a bug</a>&nbsp;|&nbsp;
1560            <a href="search.php">advanced search</a>&nbsp;|&nbsp;
1561            <a href="search-howto.php">search howto</a>&nbsp;|&nbsp;
1562            <a href="stats.php">statistics</a>&nbsp;|&nbsp;
1563            <a href="random">random bug</a>&nbsp;|&nbsp;
1564<?php if ($is_logged) { ?>
1565            <a href="search.php?cmd=display&amp;assign=<?php echo $username;?>">my bugs</a>&nbsp;|&nbsp;
1566<?php if ($logged_in === 'developer') { ?>
1567            <a href="/admin/">admin</a>&nbsp;|&nbsp;
1568<?php } ?>
1569            <a href="logout.php">logout</a>
1570<?php } else { ?>
1571            <a href="login.php">login</a>
1572<?php } ?>
1573        </td>
1574    </tr>
1575
1576    <tr>
1577        <td class="head-search" colspan="2">
1578            <form method="get" action="search.php">
1579                <p class="head-search">
1580                    <input type="hidden" name="cmd" value="display">
1581                    <small>go to bug id or search bugs for</small>
1582                    <input class="small" type="text" name="search_for" value="<?php print isset($_GET['search_for']) ? htmlspecialchars($_GET['search_for']) : ''; ?>" size="30">
1583                    <input type="image" src="images/small_submit_white.gif" alt="search" style="vertical-align: middle;">
1584                </p>
1585            </form>
1586        </td>
1587    </tr>
1588</table>
1589
1590<table class="middle" cellspacing="0" cellpadding="0">
1591    <tr>
1592        <td class="content">
1593<?php
1594}
1595
1596
1597function response_footer($extra_html = '')
1598{
1599    global $_footer_done, $LAST_UPDATED, $basedir;
1600
1601    if ($_footer_done) {
1602        return;
1603    }
1604    $_footer_done = true;
1605?>
1606        </td>
1607    </tr>
1608</table>
1609
1610<?php echo $extra_html; ?>
1611
1612<table class="foot" cellspacing="0" cellpadding="0">
1613    <tr>
1614        <td class="foot-bar" colspan="2">&nbsp;</td>
1615    </tr>
1616
1617    <tr>
1618        <td class="foot-copy">
1619            <small>
1620                <a href="https://php.net/"><img src="images/logo-small.gif" align="left" valign="middle" hspace="3" alt="PHP"></a>
1621                <a href="https://php.net/copyright.php">Copyright &copy; 2001-<?php echo date('Y'); ?> The PHP Group</a><br>
1622                All rights reserved.
1623            </small>
1624        </td>
1625        <td class="foot-source">
1626            <small>Last updated: <?php echo $LAST_UPDATED; ?></small>
1627        </td>
1628    </tr>
1629</table>
1630</body>
1631</html>
1632<?php
1633}
1634
1635
1636/**
1637 * Redirects to the given full or partial URL.
1638 *
1639 * @param string $url Full/partial url to redirect to
1640 */
1641function redirect($url)
1642{
1643    header("Location: {$url}");
1644    exit;
1645}
1646
1647
1648/**
1649 * Turns the provided email address into a "mailto:" hyperlink.
1650 *
1651 * The link and link text are obfuscated by alternating Ord and Hex
1652 * entities.
1653 *
1654 * @param string $email        the email address to make the link for
1655 * @param string $linktext    a string for the visible part of the link.
1656 *                            If not provided, the email address is used.
1657 * @param string $extras    a string of extra attributes for the <a> element
1658 *
1659 * @return string            the HTML hyperlink of an email address
1660 */
1661function make_mailto_link($email, $linktext = '', $extras = '')
1662{
1663    $tmp = '';
1664    for ($i = 0, $l = strlen($email); $i<$l; $i++) {
1665        if ($i % 2) {
1666            $tmp .= '&#' . ord($email[$i]) . ';';
1667        } else {
1668            $tmp .= '&#x' . dechex(ord($email[$i])) . ';';
1669        }
1670    }
1671
1672    return "<a {$extras} href='&#x6d;&#97;&#x69;&#108;&#x74;&#111;&#x3a;{$tmp}'>" . ($linktext != '' ? $linktext : $tmp) . '</a>';
1673}
1674
1675/**
1676 * Turns bug/feature request numbers into hyperlinks
1677 *
1678 * @param string $text    the text to check for bug numbers
1679 *
1680 * @return string the string with bug numbers hyperlinked
1681 */
1682function make_ticket_links($text)
1683{
1684    return preg_replace(
1685        '/(?<![>a-z])(bug(?:fix)?|feat(?:ure)?|doc(?:umentation)?|req(?:uest)?|duplicated of)\s+#?([0-9]+)/i',
1686        "<a href='bug.php?id=\\2'>\\0</a>",
1687        $text
1688    );
1689}
1690
1691function get_ticket_links($text)
1692{
1693    $matches = [];
1694
1695    preg_match_all('/(?<![>a-z])(?:bug(?:fix)?|feat(?:ure)?|doc(?:umentation)?|req(?:uest)?|duplicated of)\s+#?([0-9]+)/i', $text, $matches);
1696
1697    return $matches[1];
1698}
1699
1700/**
1701 * Generates a random password
1702 */
1703function bugs_gen_passwd($length = 8)
1704{
1705    return substr(md5(uniqid(time(), true)), 0, $length);
1706}
1707
1708function bugs_get_hash($passwd)
1709{
1710    return hash_hmac('sha256', $passwd, getenv('USER_PWD_SALT'));
1711}
1712