Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.66% covered (danger)
33.66%
586 / 1741
30.51% covered (danger)
30.51%
18 / 59
CRAP
n/a
0 / 0
gen_rand_string
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
gen_rand_string_friendly
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
unique_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
phpbb_mt_rand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
phpbb_gmgetdate
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
get_formatted_filesize
98.00% covered (success)
98.00%
49 / 50
0.00% covered (danger)
0.00%
0 / 1
10
still_on_time
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
phpbb_version_compare
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
phpbb_language_select
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
style_select
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
phpbb_format_timezone_offset
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
phpbb_tz_select_compare
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
phpbb_get_timezone_identifiers
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
phpbb_timezone_select
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
90
markread
44.34% covered (danger)
44.34%
94 / 212
0.00% covered (danger)
0.00%
0 / 1
556.84
get_topic_tracking
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
get_complete_topic_tracking
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
506
get_unread_topics
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
380
update_forum_tracking_info
61.70% covered (warning)
61.70%
29 / 47
0.00% covered (danger)
0.00%
0 / 1
42.47
tracking_serialize
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
tracking_unserialize
81.82% covered (warning)
81.82%
54 / 66
0.00% covered (danger)
0.00%
0 / 1
24.91
append_sid
95.45% covered (success)
95.45%
42 / 44
0.00% covered (danger)
0.00%
0 / 1
33
generate_board_url
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
17.56
redirect
78.38% covered (warning)
78.38%
29 / 37
0.00% covered (danger)
0.00%
0 / 1
25.46
phpbb_get_install_redirect
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
reapply_sid
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
build_url
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
meta_refresh
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
send_status_line
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
phpbb_request_http_version
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
56
generate_link_hash
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
check_link_hash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
add_form_key
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
check_form_key
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
confirm_box
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
650
login_box
0.00% covered (danger)
0.00%
0 / 126
0.00% covered (danger)
0.00%
0 / 1
2352
login_forum_box
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
42
_build_hidden_fields
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
build_hidden_fields
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
get_backtrace
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
get_preg_expression
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
15
get_censor_preg_expression
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
short_ipv6
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
phpbb_ip_normalise
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
msg_handler
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 1
3906
phpbb_filter_root_path
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
obtain_guest_count
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
3.12
obtain_users_online
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
6
obtain_users_online_string
100.00% covered (success)
100.00%
59 / 59
100.00% covered (success)
100.00%
1 / 1
13
phpbb_optionget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
phpbb_optionset
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
5.20
phpbb_quoteattr
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
page_header
0.00% covered (danger)
0.00%
0 / 238
0.00% covered (danger)
0.00%
0 / 1
7482
phpbb_generate_debug_output
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
132
page_footer
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
garbage_collection
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
exit_handler
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
phpbb_get_board_contact
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
phpbb_get_board_contact_link
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14/**
15* @ignore
16*/
17if (!defined('IN_PHPBB'))
18{
19    exit;
20}
21
22// Common global functions
23/**
24* Generates an alphanumeric random string of given length
25*
26* @param int $num_chars Length of random string, defaults to 8.
27* This number should be less or equal than 64.
28*
29* @return string
30*/
31function gen_rand_string($num_chars = 8)
32{
33    $range = array_merge(range('A', 'Z'), range(0, 9));
34    $size = count($range);
35
36    $output = '';
37    for ($i = 0; $i < $num_chars; $i++)
38    {
39        $rand = random_int(0, $size - 1);
40        $output .= $range[$rand];
41    }
42
43    return $output;
44}
45
46/**
47* Generates a user-friendly alphanumeric random string of given length
48* We remove 0 and O so users cannot confuse those in passwords etc.
49*
50* @param int $num_chars Length of random string, defaults to 8.
51* This number should be less or equal than 64.
52*
53* @return string
54*/
55function gen_rand_string_friendly($num_chars = 8)
56{
57    $range = array_merge(range('A', 'N'), range('P', 'Z'), range(1, 9));
58    $size = count($range);
59
60    $output = '';
61    for ($i = 0; $i < $num_chars; $i++)
62    {
63        $rand = random_int(0, $size-1);
64        $output .= $range[$rand];
65    }
66
67    return $output;
68}
69
70/**
71* Return unique id
72*/
73function unique_id()
74{
75    return strtolower(gen_rand_string(16));
76}
77
78/**
79* Wrapper for mt_rand() which allows swapping $min and $max parameters.
80*
81* PHP does not allow us to swap the order of the arguments for mt_rand() anymore.
82* (since PHP 5.3.4, see http://bugs.php.net/46587)
83*
84* @param int $min        Lowest value to be returned
85* @param int $max        Highest value to be returned
86*
87* @return int            Random integer between $min and $max (or $max and $min)
88*/
89function phpbb_mt_rand($min, $max)
90{
91    return ($min > $max) ? mt_rand($max, $min) : mt_rand($min, $max);
92}
93
94/**
95* Wrapper for getdate() which returns the equivalent array for UTC timestamps.
96*
97* @param int $time        Unix timestamp (optional)
98*
99* @return array            Returns an associative array of information related to the timestamp.
100*                        See http://www.php.net/manual/en/function.getdate.php
101*/
102function phpbb_gmgetdate($time = false)
103{
104    if ($time === false)
105    {
106        $time = time();
107    }
108
109    // getdate() interprets timestamps in local time.
110    // What follows uses the fact that getdate() and
111    // date('Z') balance each other out.
112    return getdate($time - date('Z'));
113}
114
115/**
116* Return formatted string for filesizes
117*
118* @param mixed        $value            filesize in bytes
119*                                (non-negative number; int, float or string)
120* @param bool        $string_only    true if language string should be returned
121* @param array|null $allowed_units    only allow these units (data array indexes)
122*
123* @return array|string                    data array if $string_only is false
124*/
125function get_formatted_filesize($value, bool $string_only = true, array $allowed_units = null)
126{
127    global $user;
128
129    $available_units = array(
130        'tb' => array(
131            'min'         => 1099511627776, // pow(2, 40)
132            'index'        => 4,
133            'si_unit'    => 'TB',
134            'iec_unit'    => 'TIB',
135        ),
136        'gb' => array(
137            'min'         => 1073741824, // pow(2, 30)
138            'index'        => 3,
139            'si_unit'    => 'GB',
140            'iec_unit'    => 'GIB',
141        ),
142        'mb' => array(
143            'min'        => 1048576, // pow(2, 20)
144            'index'        => 2,
145            'si_unit'    => 'MB',
146            'iec_unit'    => 'MIB',
147        ),
148        'kb' => array(
149            'min'        => 1024, // pow(2, 10)
150            'index'        => 1,
151            'si_unit'    => 'KB',
152            'iec_unit'    => 'KIB',
153        ),
154        'b' => array(
155            'min'        => 0,
156            'index'        => 0,
157            'si_unit'    => 'BYTES', // Language index
158            'iec_unit'    => 'BYTES',  // Language index
159        ),
160    );
161
162    foreach ($available_units as $si_identifier => $unit_info)
163    {
164        if (is_array($allowed_units) && $si_identifier != 'b' && !in_array($si_identifier, $allowed_units))
165        {
166            continue;
167        }
168
169        if ($value >= $unit_info['min'])
170        {
171            $unit_info['si_identifier'] = $si_identifier;
172
173            break;
174        }
175    }
176    unset($available_units);
177
178    for ($i = 0; $i < $unit_info['index']; $i++)
179    {
180        $value /= 1024;
181    }
182    $value = round($value, 2);
183
184    // Lookup units in language dictionary
185    $unit_info['si_unit'] = (isset($user->lang[$unit_info['si_unit']])) ? $user->lang[$unit_info['si_unit']] : $unit_info['si_unit'];
186    $unit_info['iec_unit'] = (isset($user->lang[$unit_info['iec_unit']])) ? $user->lang[$unit_info['iec_unit']] : $unit_info['iec_unit'];
187
188    // Default to IEC
189    $unit_info['unit'] = $unit_info['iec_unit'];
190
191    if (!$string_only)
192    {
193        $unit_info['value'] = $value;
194
195        return $unit_info;
196    }
197
198    return $value  . ' ' . $unit_info['unit'];
199}
200
201/**
202* Determine whether we are approaching the maximum execution time. Should be called once
203* at the beginning of the script in which it's used.
204* @return    bool    Either true if the maximum execution time is nearly reached, or false
205*                    if some time is still left.
206*/
207function still_on_time($extra_time = 15)
208{
209    static $max_execution_time, $start_time;
210
211    $current_time = microtime(true);
212
213    if (empty($max_execution_time))
214    {
215        $max_execution_time = (function_exists('ini_get')) ? (int) @ini_get('max_execution_time') : (int) @get_cfg_var('max_execution_time');
216
217        // If zero, then set to something higher to not let the user catch the ten seconds barrier.
218        if ($max_execution_time === 0)
219        {
220            $max_execution_time = 50 + $extra_time;
221        }
222
223        $max_execution_time = min(max(10, ($max_execution_time - $extra_time)), 50);
224
225        // For debugging purposes
226        // $max_execution_time = 10;
227
228        global $starttime;
229        $start_time = (empty($starttime)) ? $current_time : $starttime;
230    }
231
232    return (ceil($current_time - $start_time) < $max_execution_time) ? true : false;
233}
234
235/**
236* Wrapper for version_compare() that allows using uppercase A and B
237* for alpha and beta releases.
238*
239* See http://www.php.net/manual/en/function.version-compare.php
240*
241* @param string            $version1        First version number
242* @param string            $version2        Second version number
243* @param string|null    $operator        Comparison operator (optional)
244*
245* @return mixed                Boolean (true, false) if comparison operator is specified.
246*                            Integer (-1, 0, 1) otherwise.
247*/
248function phpbb_version_compare(string $version1, string $version2, string $operator = null)
249{
250    $version1 = strtolower($version1);
251    $version2 = strtolower($version2);
252
253    if (is_null($operator))
254    {
255        return version_compare($version1, $version2);
256    }
257    else
258    {
259        return version_compare($version1, $version2, $operator);
260    }
261}
262
263// functions used for building option fields
264
265/**
266 * Pick a language, any language ...
267 *
268 * @param \phpbb\db\driver\driver_interface $db DBAL driver
269 * @param string $default    Language ISO code to be selected by default in the dropdown list
270 * @param array $langdata    Language data in format of array(array('lang_iso' => string, lang_local_name => string), ...)
271 */
272function phpbb_language_select(\phpbb\db\driver\driver_interface $db, string $default = '', array $langdata = []): array
273{
274    if (empty($langdata))
275    {
276        $sql = 'SELECT lang_iso, lang_local_name
277            FROM ' . LANG_TABLE . '
278            ORDER BY lang_english_name';
279        $result = $db->sql_query($sql);
280        $langdata = (array) $db->sql_fetchrowset($result);
281        $db->sql_freeresult($result);
282    }
283
284    $lang_options = [];
285    foreach ($langdata as $row)
286    {
287        $lang_options[] = [
288            'value'        => $row['lang_iso'],
289            'label'        => $row['lang_local_name'],
290            'selected'    => $row['lang_iso'] === $default,
291        ];
292    }
293
294    return $lang_options;
295}
296
297/**
298 * Pick a template/theme combo
299 *
300 * @param string $default    Style ID to be selected by default in the dropdown list
301 * @param bool $all            Flag indicating if all styles data including inactive ones should be fetched
302 * @param array $styledata    Style data in format of array(array('style_id' => int, style_name => string), ...)
303 *
304 * @return string            HTML options for style selection dropdown list.
305 */
306function style_select($default = '', $all = false, array $styledata = [])
307{
308    global $db;
309
310    if (empty($styledata))
311    {
312        $sql_where = (!$all) ? 'WHERE style_active = 1 ' : '';
313        $sql = 'SELECT style_id, style_name
314            FROM ' . STYLES_TABLE . "
315            $sql_where
316            ORDER BY style_name";
317        $result = $db->sql_query($sql);
318        $styledata = (array) $db->sql_fetchrowset($result);
319        $db->sql_freeresult($result);
320    }
321
322    $style_options = [];
323    foreach ($styledata as $row)
324    {
325        $style_options[] = [
326            'value'     => $row['style_id'],
327            'selected'    => $row['style_id'] == $default,
328            'label'        => $row['style_name'],
329        ];
330    }
331
332    return $style_options;
333}
334
335/**
336* Format the timezone offset with hours and minutes
337*
338* @param    int        $tz_offset    Timezone offset in seconds
339* @param    bool    $show_null    Whether null offsets should be shown
340* @return    string        Normalized offset string:    -7200 => -02:00
341*                                                    16200 => +04:30
342*/
343function phpbb_format_timezone_offset($tz_offset, $show_null = false)
344{
345    $sign = ($tz_offset < 0) ? '-' : '+';
346    $time_offset = abs($tz_offset);
347
348    if ($time_offset == 0 && $show_null == false)
349    {
350        return '';
351    }
352
353    $offset_seconds    = $time_offset % 3600;
354    $offset_minutes    = $offset_seconds / 60;
355    $offset_hours    = ($time_offset - $offset_seconds) / 3600;
356
357    $offset_string    = sprintf("%s%02d:%02d", $sign, $offset_hours, $offset_minutes);
358    return $offset_string;
359}
360
361/**
362* Compares two time zone labels.
363* Arranges them in increasing order by timezone offset.
364* Places UTC before other timezones in the same offset.
365*/
366function phpbb_tz_select_compare($a, $b)
367{
368    $a_sign = $a[3];
369    $b_sign = $b[3];
370    if ($a_sign != $b_sign)
371    {
372        return $a_sign == '-' ? -1 : 1;
373    }
374
375    $a_offset = substr($a, 4, 5);
376    $b_offset = substr($b, 4, 5);
377    if ($a_offset == $b_offset)
378    {
379        $a_name = substr($a, 12);
380        $b_name = substr($b, 12);
381        if ($a_name == $b_name)
382        {
383            return 0;
384        }
385        else if ($a_name == 'UTC')
386        {
387            return -1;
388        }
389        else if ($b_name == 'UTC')
390        {
391            return 1;
392        }
393        else
394        {
395            return $a_name < $b_name ? -1 : 1;
396        }
397    }
398    else
399    {
400        if ($a_sign == '-')
401        {
402            return $a_offset > $b_offset ? -1 : 1;
403        }
404        else
405        {
406            return $a_offset < $b_offset ? -1 : 1;
407        }
408    }
409}
410
411/**
412* Return list of timezone identifiers
413* We also add the selected timezone if we can create an object with it.
414* DateTimeZone::listIdentifiers seems to not add all identifiers to the list,
415* because some are only kept for backward compatible reasons. If the user has
416* a deprecated value, we add it here, so it can still be kept. Once the user
417* changed his value, there is no way back to deprecated values.
418*
419* @param    string        $selected_timezone        Additional timezone that shall
420*                                                be added to the list of identiers
421* @return        array        DateTimeZone::listIdentifiers and additional
422*                            selected_timezone if it is a valid timezone.
423*/
424function phpbb_get_timezone_identifiers($selected_timezone)
425{
426    $timezones = DateTimeZone::listIdentifiers();
427
428    if (!in_array($selected_timezone, $timezones))
429    {
430        try
431        {
432            // Add valid timezones that are currently selected but not returned
433            // by DateTimeZone::listIdentifiers
434            $validate_timezone = new DateTimeZone($selected_timezone);
435            $timezones[] = $selected_timezone;
436        }
437        catch (\Exception $e)
438        {
439        }
440    }
441
442    return $timezones;
443}
444
445/**
446* Options to pick a timezone and date/time
447*
448* @param    \phpbb\user    $user                Object of the current user
449* @param    string        $default            A timezone to select
450* @param    boolean        $truncate            Shall we truncate the options text
451*
452* @return    string        Returns an array containing the options for the time selector.
453*/
454function phpbb_timezone_select($user, $default = '', $truncate = false)
455{
456    static $timezones;
457
458    $default_offset = '';
459    if (!isset($timezones))
460    {
461        $unsorted_timezones = phpbb_get_timezone_identifiers($default);
462
463        $timezones = array();
464        foreach ($unsorted_timezones as $timezone)
465        {
466            $tz = new DateTimeZone($timezone);
467            $dt = $user->create_datetime('now', $tz);
468            $offset = $dt->getOffset();
469            $current_time = $dt->format($user->lang['DATETIME_FORMAT'], true);
470            $offset_string = phpbb_format_timezone_offset($offset, true);
471            $timezones['UTC' . $offset_string . ' - ' . $timezone] = array(
472                'tz'        => $timezone,
473                'offset'    => $offset_string,
474                'current'    => $current_time,
475            );
476            if ($timezone === $default)
477            {
478                $default_offset = 'UTC' . $offset_string;
479            }
480        }
481        unset($unsorted_timezones);
482
483        uksort($timezones, 'phpbb_tz_select_compare');
484    }
485
486    $opt_group = '';
487    $tz_data = [];
488
489    foreach ($timezones as $key => $timezone)
490    {
491        if ($opt_group != $timezone['offset'])
492        {
493            $tz_data[$timezone['offset']] = [
494                'label'        => $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $timezone['current']),
495                'value'        => $key . ' - ' . $timezone['current'],
496                'options'    => [],
497                'selected'    => !empty($default_offset) && strpos($key, $default_offset) !== false,
498                'data'        => ['tz-value'    => $key . ' - ' . $timezone['current']],
499            ];
500
501            $opt_group = $timezone['offset'];
502        }
503
504        $label = $timezone['tz'];
505        if (isset($user->lang['timezones'][$label]))
506        {
507            $label = $user->lang['timezones'][$label];
508        }
509        $title = $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $label);
510
511        if ($truncate)
512        {
513            $label = truncate_string($label, 50, 255, false, '...');
514        }
515
516        $tz_data[$timezone['offset']]['options'][] = [
517            'TITLE'            => $title,
518            'value'            => $timezone['tz'],
519            'selected'        => $timezone['tz'] === $default,
520            'label'            => $label,
521        ];
522    }
523
524    return $tz_data;
525}
526
527// Functions handling topic/post tracking/marking
528
529/**
530* Marks a topic/forum as read
531* Marks a topic as posted to
532*
533* @param string $mode (all, topics, topic, post)
534* @param int|bool $forum_id Used in all, topics, and topic mode
535* @param int|bool $topic_id Used in topic and post mode
536* @param int $post_time 0 means current time(), otherwise to set a specific mark time
537* @param int $user_id can only be used with $mode == 'post'
538*/
539function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $user_id = 0)
540{
541    global $db, $user, $config;
542    global $request, $phpbb_container, $phpbb_dispatcher;
543
544    $post_time = ($post_time === 0 || $post_time > time()) ? time() : (int) $post_time;
545
546    $should_markread = true;
547
548    /**
549     * This event is used for performing actions directly before marking forums,
550     * topics or posts as read.
551     *
552     * It is also possible to prevent the marking. For that, the $should_markread parameter
553     * should be set to FALSE.
554     *
555     * @event core.markread_before
556     * @var    string    mode                Variable containing marking mode value
557     * @var    mixed    forum_id            Variable containing forum id, or false
558     * @var    mixed    topic_id            Variable containing topic id, or false
559     * @var    int        post_time            Variable containing post time
560     * @var    int        user_id                Variable containing the user id
561     * @var    bool    should_markread        Flag indicating if the markread should be done or not.
562     * @since 3.1.4-RC1
563     */
564    $vars = array(
565        'mode',
566        'forum_id',
567        'topic_id',
568        'post_time',
569        'user_id',
570        'should_markread',
571    );
572    extract($phpbb_dispatcher->trigger_event('core.markread_before', compact($vars)));
573
574    if (!$should_markread)
575    {
576        return;
577    }
578
579    if ($mode == 'all')
580    {
581        if (empty($forum_id))
582        {
583            // Mark all forums read (index page)
584            /* @var $phpbb_notifications \phpbb\notification\manager */
585            $phpbb_notifications = $phpbb_container->get('notification_manager');
586
587            // Mark all topic notifications read for this user
588            $phpbb_notifications->mark_notifications(array(
589                'notification.type.topic',
590                'notification.type.mention',
591                'notification.type.quote',
592                'notification.type.bookmark',
593                'notification.type.post',
594                'notification.type.approve_topic',
595                'notification.type.approve_post',
596                'notification.type.forum',
597            ), false, $user->data['user_id'], $post_time);
598
599            if ($config['load_db_lastread'] && $user->data['is_registered'])
600            {
601                // Mark all forums read (index page)
602                $tables = array(TOPICS_TRACK_TABLE, FORUMS_TRACK_TABLE);
603                foreach ($tables as $table)
604                {
605                    $sql = 'DELETE FROM ' . $table . "
606                        WHERE user_id = {$user->data['user_id']}
607                            AND mark_time < $post_time";
608                    $db->sql_query($sql);
609                }
610
611                $sql = 'UPDATE ' . USERS_TABLE . "
612                    SET user_lastmark = $post_time
613                    WHERE user_id = {$user->data['user_id']}
614                        AND user_lastmark < $post_time";
615                $db->sql_query($sql);
616            }
617            else if ($config['load_anon_lastread'] || $user->data['is_registered'])
618            {
619                $tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
620                $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
621
622                unset($tracking_topics['tf']);
623                unset($tracking_topics['t']);
624                unset($tracking_topics['f']);
625                $tracking_topics['l'] = base_convert($post_time - $config['board_startdate'], 10, 36);
626
627                $user->set_cookie('track', tracking_serialize($tracking_topics), $post_time + 31536000);
628                $request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking_topics), \phpbb\request\request_interface::COOKIE);
629
630                unset($tracking_topics);
631
632                if ($user->data['is_registered'])
633                {
634                    $sql = 'UPDATE ' . USERS_TABLE . "
635                        SET user_lastmark = $post_time
636                        WHERE user_id = {$user->data['user_id']}
637                            AND user_lastmark < $post_time";
638                    $db->sql_query($sql);
639                }
640            }
641        }
642    }
643    else if ($mode == 'topics')
644    {
645        // Mark all topics in forums read
646        if (!is_array($forum_id))
647        {
648            $forum_id = array($forum_id);
649        }
650        else
651        {
652            $forum_id = array_unique($forum_id);
653        }
654
655        /* @var $phpbb_notifications \phpbb\notification\manager */
656        $phpbb_notifications = $phpbb_container->get('notification_manager');
657
658        $phpbb_notifications->mark_notifications_by_parent(array(
659            'notification.type.topic',
660            'notification.type.approve_topic',
661        ), $forum_id, $user->data['user_id'], $post_time);
662
663        // Mark all post/quote notifications read for this user in this forum
664        $topic_ids = array();
665        $sql = 'SELECT topic_id
666            FROM ' . TOPICS_TABLE . '
667            WHERE ' . $db->sql_in_set('forum_id', $forum_id);
668        $result = $db->sql_query($sql);
669        while ($row = $db->sql_fetchrow($result))
670        {
671            $topic_ids[] = $row['topic_id'];
672        }
673        $db->sql_freeresult($result);
674
675        $phpbb_notifications->mark_notifications_by_parent(array(
676            'notification.type.mention',
677            'notification.type.quote',
678            'notification.type.bookmark',
679            'notification.type.post',
680            'notification.type.approve_post',
681            'notification.type.forum',
682        ), $topic_ids, $user->data['user_id'], $post_time);
683
684        // Add 0 to forums array to mark global announcements correctly
685        // $forum_id[] = 0;
686
687        if ($config['load_db_lastread'] && $user->data['is_registered'])
688        {
689            $sql = 'DELETE FROM ' . TOPICS_TRACK_TABLE . "
690                WHERE user_id = {$user->data['user_id']}
691                    AND mark_time < $post_time
692                    AND " . $db->sql_in_set('forum_id', $forum_id);
693            $db->sql_query($sql);
694
695            $sql = 'SELECT forum_id
696                FROM ' . FORUMS_TRACK_TABLE . "
697                WHERE user_id = {$user->data['user_id']}
698                    AND " . $db->sql_in_set('forum_id', $forum_id);
699            $result = $db->sql_query($sql);
700
701            $sql_update = array();
702            while ($row = $db->sql_fetchrow($result))
703            {
704                $sql_update[] = (int) $row['forum_id'];
705            }
706            $db->sql_freeresult($result);
707
708            if (count($sql_update))
709            {
710                $sql = 'UPDATE ' . FORUMS_TRACK_TABLE . "
711                    SET mark_time = $post_time
712                    WHERE user_id = {$user->data['user_id']}
713                        AND mark_time < $post_time
714                        AND " . $db->sql_in_set('forum_id', $sql_update);
715                $db->sql_query($sql);
716            }
717
718            if ($sql_insert = array_diff($forum_id, $sql_update))
719            {
720                $sql_ary = array();
721                foreach ($sql_insert as $f_id)
722                {
723                    $sql_ary[] = array(
724                        'user_id'    => (int) $user->data['user_id'],
725                        'forum_id'    => (int) $f_id,
726                        'mark_time'    => $post_time,
727                    );
728                }
729
730                $db->sql_multi_insert(FORUMS_TRACK_TABLE, $sql_ary);
731            }
732        }
733        else if ($config['load_anon_lastread'] || $user->data['is_registered'])
734        {
735            $tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
736            $tracking = ($tracking) ? tracking_unserialize($tracking) : array();
737
738            foreach ($forum_id as $f_id)
739            {
740                $topic_ids36 = (isset($tracking['tf'][$f_id])) ? $tracking['tf'][$f_id] : array();
741
742                if (isset($tracking['tf'][$f_id]))
743                {
744                    unset($tracking['tf'][$f_id]);
745                }
746
747                foreach ($topic_ids36 as $topic_id36)
748                {
749                    unset($tracking['t'][$topic_id36]);
750                }
751
752                if (isset($tracking['f'][$f_id]))
753                {
754                    unset($tracking['f'][$f_id]);
755                }
756
757                $tracking['f'][$f_id] = base_convert($post_time - $config['board_startdate'], 10, 36);
758            }
759
760            if (isset($tracking['tf']) && empty($tracking['tf']))
761            {
762                unset($tracking['tf']);
763            }
764
765            $user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
766            $request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
767
768            unset($tracking);
769        }
770    }
771    else if ($mode == 'topic')
772    {
773        if ($topic_id === false || $forum_id === false)
774        {
775            return;
776        }
777
778        /* @var $phpbb_notifications \phpbb\notification\manager */
779        $phpbb_notifications = $phpbb_container->get('notification_manager');
780
781        // Mark post notifications read for this user in this topic
782        $phpbb_notifications->mark_notifications(array(
783            'notification.type.topic',
784            'notification.type.approve_topic',
785        ), $topic_id, $user->data['user_id'], $post_time);
786
787        $phpbb_notifications->mark_notifications_by_parent(array(
788            'notification.type.mention',
789            'notification.type.quote',
790            'notification.type.bookmark',
791            'notification.type.post',
792            'notification.type.approve_post',
793            'notification.type.forum',
794        ), $topic_id, $user->data['user_id'], $post_time);
795
796        if ($config['load_db_lastread'] && $user->data['is_registered'])
797        {
798            $sql = 'UPDATE ' . TOPICS_TRACK_TABLE . "
799                SET mark_time = $post_time
800                WHERE user_id = {$user->data['user_id']}
801                    AND mark_time < $post_time
802                    AND topic_id = $topic_id";
803            $db->sql_query($sql);
804
805            // insert row
806            if (!$db->sql_affectedrows())
807            {
808                $db->sql_return_on_error(true);
809
810                $sql_ary = array(
811                    'user_id'        => (int) $user->data['user_id'],
812                    'topic_id'        => (int) $topic_id,
813                    'forum_id'        => (int) $forum_id,
814                    'mark_time'        => $post_time,
815                );
816
817                $db->sql_query('INSERT INTO ' . TOPICS_TRACK_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
818
819                $db->sql_return_on_error(false);
820            }
821        }
822        else if ($config['load_anon_lastread'] || $user->data['is_registered'])
823        {
824            $tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
825            $tracking = ($tracking) ? tracking_unserialize($tracking) : array();
826
827            $topic_id36 = base_convert($topic_id, 10, 36);
828
829            if (!isset($tracking['t'][$topic_id36]))
830            {
831                $tracking['tf'][$forum_id][$topic_id36] = true;
832            }
833
834            $tracking['t'][$topic_id36] = base_convert($post_time - (int) $config['board_startdate'], 10, 36);
835
836            // If the cookie grows larger than 10000 characters we will remove the smallest value
837            // This can result in old topics being unread - but most of the time it should be accurate...
838            if (strlen($request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE)) > 10000)
839            {
840                //echo 'Cookie grown too large' . print_r($tracking, true);
841
842                // We get the ten most minimum stored time offsets and its associated topic ids
843                $time_keys = array();
844                for ($i = 0; $i < 10 && count($tracking['t']); $i++)
845                {
846                    $min_value = min($tracking['t']);
847                    $m_tkey = array_search($min_value, $tracking['t']);
848                    unset($tracking['t'][$m_tkey]);
849
850                    $time_keys[$m_tkey] = $min_value;
851                }
852
853                // Now remove the topic ids from the array...
854                foreach ($tracking['tf'] as $f_id => $topic_id_ary)
855                {
856                    foreach ($time_keys as $m_tkey => $min_value)
857                    {
858                        if (isset($topic_id_ary[$m_tkey]))
859                        {
860                            $tracking['f'][$f_id] = $min_value;
861                            unset($tracking['tf'][$f_id][$m_tkey]);
862                        }
863                    }
864                }
865
866                if ($user->data['is_registered'])
867                {
868                    $user->data['user_lastmark'] = intval(base_convert(max($time_keys) + $config['board_startdate'], 36, 10));
869
870                    $sql = 'UPDATE ' . USERS_TABLE . "
871                        SET user_lastmark = $post_time
872                        WHERE user_id = {$user->data['user_id']}
873                            AND mark_time < $post_time";
874                    $db->sql_query($sql);
875                }
876                else
877                {
878                    $tracking['l'] = max($time_keys);
879                }
880            }
881
882            $user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
883            $request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
884        }
885    }
886    else if ($mode == 'post')
887    {
888        if ($topic_id === false)
889        {
890            return;
891        }
892
893        $use_user_id = (!$user_id) ? $user->data['user_id'] : $user_id;
894
895        if ($config['load_db_track'] && $use_user_id != ANONYMOUS)
896        {
897            $db->sql_return_on_error(true);
898
899            $sql_ary = array(
900                'user_id'        => (int) $use_user_id,
901                'topic_id'        => (int) $topic_id,
902                'topic_posted'    => 1,
903            );
904
905            $db->sql_query('INSERT INTO ' . TOPICS_POSTED_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
906
907            $db->sql_return_on_error(false);
908        }
909    }
910
911    /**
912     * This event is used for performing actions directly after forums,
913     * topics or posts have been marked as read.
914     *
915     * @event core.markread_after
916     * @var    string        mode                Variable containing marking mode value
917     * @var    mixed        forum_id            Variable containing forum id, or false
918     * @var    mixed        topic_id            Variable containing topic id, or false
919     * @var    int            post_time            Variable containing post time
920     * @var    int            user_id                Variable containing the user id
921     * @since 3.2.6-RC1
922     */
923    $vars = array(
924        'mode',
925        'forum_id',
926        'topic_id',
927        'post_time',
928        'user_id',
929    );
930    extract($phpbb_dispatcher->trigger_event('core.markread_after', compact($vars)));
931}
932
933/**
934* Get topic tracking info by using already fetched info
935*/
936function get_topic_tracking($forum_id, $topic_ids, &$rowset, $forum_mark_time)
937{
938    global $user;
939
940    $last_read = array();
941
942    if (!is_array($topic_ids))
943    {
944        $topic_ids = array($topic_ids);
945    }
946
947    foreach ($topic_ids as $topic_id)
948    {
949        if (!empty($rowset[$topic_id]['mark_time']))
950        {
951            $last_read[$topic_id] = $rowset[$topic_id]['mark_time'];
952        }
953    }
954
955    $topic_ids = array_diff($topic_ids, array_keys($last_read));
956
957    if (count($topic_ids))
958    {
959        $mark_time = array();
960
961        if (!empty($forum_mark_time[$forum_id]) && $forum_mark_time[$forum_id] !== false)
962        {
963            $mark_time[$forum_id] = $forum_mark_time[$forum_id];
964        }
965
966        $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
967
968        foreach ($topic_ids as $topic_id)
969        {
970            $last_read[$topic_id] = $user_lastmark;
971        }
972    }
973
974    return $last_read;
975}
976
977/**
978* Get topic tracking info from db (for cookie based tracking only this function is used)
979*/
980function get_complete_topic_tracking($forum_id, $topic_ids)
981{
982    global $config, $user, $request;
983
984    $last_read = array();
985
986    if (!is_array($topic_ids))
987    {
988        $topic_ids = array($topic_ids);
989    }
990
991    if ($config['load_db_lastread'] && $user->data['is_registered'])
992    {
993        global $db;
994
995        $sql = 'SELECT topic_id, mark_time
996            FROM ' . TOPICS_TRACK_TABLE . "
997            WHERE user_id = {$user->data['user_id']}
998                AND " . $db->sql_in_set('topic_id', $topic_ids);
999        $result = $db->sql_query($sql);
1000
1001        while ($row = $db->sql_fetchrow($result))
1002        {
1003            $last_read[$row['topic_id']] = $row['mark_time'];
1004        }
1005        $db->sql_freeresult($result);
1006
1007        $topic_ids = array_diff($topic_ids, array_keys($last_read));
1008
1009        if (count($topic_ids))
1010        {
1011            $sql = 'SELECT forum_id, mark_time
1012                FROM ' . FORUMS_TRACK_TABLE . "
1013                WHERE user_id = {$user->data['user_id']}
1014                    AND forum_id = $forum_id";
1015            $result = $db->sql_query($sql);
1016
1017            $mark_time = array();
1018            while ($row = $db->sql_fetchrow($result))
1019            {
1020                $mark_time[$row['forum_id']] = $row['mark_time'];
1021            }
1022            $db->sql_freeresult($result);
1023
1024            $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
1025
1026            foreach ($topic_ids as $topic_id)
1027            {
1028                $last_read[$topic_id] = $user_lastmark;
1029            }
1030        }
1031    }
1032    else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1033    {
1034        global $tracking_topics;
1035
1036        if (!isset($tracking_topics) || !count($tracking_topics))
1037        {
1038            $tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
1039            $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1040        }
1041
1042        if (!$user->data['is_registered'])
1043        {
1044            $user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
1045        }
1046        else
1047        {
1048            $user_lastmark = $user->data['user_lastmark'];
1049        }
1050
1051        foreach ($topic_ids as $topic_id)
1052        {
1053            $topic_id36 = base_convert($topic_id, 10, 36);
1054
1055            if (isset($tracking_topics['t'][$topic_id36]))
1056            {
1057                $last_read[$topic_id] = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
1058            }
1059        }
1060
1061        $topic_ids = array_diff($topic_ids, array_keys($last_read));
1062
1063        if (count($topic_ids))
1064        {
1065            $mark_time = array();
1066
1067            if (isset($tracking_topics['f'][$forum_id]))
1068            {
1069                $mark_time[$forum_id] = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
1070            }
1071
1072            $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user_lastmark;
1073
1074            foreach ($topic_ids as $topic_id)
1075            {
1076                $last_read[$topic_id] = $user_lastmark;
1077            }
1078        }
1079    }
1080
1081    return $last_read;
1082}
1083
1084/**
1085* Get list of unread topics
1086*
1087* @param int $user_id            User ID (or false for current user)
1088* @param string $sql_extra        Extra WHERE SQL statement
1089* @param string $sql_sort        ORDER BY SQL sorting statement
1090* @param string $sql_limit        Limits the size of unread topics list, 0 for unlimited query
1091* @param string $sql_limit_offset  Sets the offset of the first row to search, 0 to search from the start
1092*
1093* @return int[]        Topic ids as keys, mark_time of topic as value
1094*/
1095function get_unread_topics($user_id = false, $sql_extra = '', $sql_sort = '', $sql_limit = 1001, $sql_limit_offset = 0)
1096{
1097    global $config, $db, $user, $request;
1098    global $phpbb_dispatcher;
1099
1100    $user_id = ($user_id === false) ? (int) $user->data['user_id'] : (int) $user_id;
1101
1102    // Data array we're going to return
1103    $unread_topics = array();
1104
1105    if (empty($sql_sort))
1106    {
1107        $sql_sort = 'ORDER BY t.topic_last_post_time DESC, t.topic_last_post_id DESC';
1108    }
1109
1110    if ($config['load_db_lastread'] && $user->data['is_registered'])
1111    {
1112        // Get list of the unread topics
1113        $last_mark = (int) $user->data['user_lastmark'];
1114
1115        $sql_array = array(
1116            'SELECT'        => 't.topic_id, t.topic_last_post_time, tt.mark_time as topic_mark_time, ft.mark_time as forum_mark_time',
1117
1118            'FROM'            => array(TOPICS_TABLE => 't'),
1119
1120            'LEFT_JOIN'        => array(
1121                array(
1122                    'FROM'    => array(TOPICS_TRACK_TABLE => 'tt'),
1123                    'ON'    => "tt.user_id = $user_id AND t.topic_id = tt.topic_id",
1124                ),
1125                array(
1126                    'FROM'    => array(FORUMS_TRACK_TABLE => 'ft'),
1127                    'ON'    => "ft.user_id = $user_id AND t.forum_id = ft.forum_id",
1128                ),
1129            ),
1130
1131            'WHERE'            => "
1132                 t.topic_last_post_time > $last_mark AND
1133                (
1134                (tt.mark_time IS NOT NULL AND t.topic_last_post_time > tt.mark_time) OR
1135                (tt.mark_time IS NULL AND ft.mark_time IS NOT NULL AND t.topic_last_post_time > ft.mark_time) OR
1136                (tt.mark_time IS NULL AND ft.mark_time IS NULL)
1137                )
1138                $sql_extra
1139                $sql_sort",
1140        );
1141
1142        /**
1143         * Change SQL query for fetching unread topics data
1144         *
1145         * @event core.get_unread_topics_modify_sql
1146         * @var array     sql_array    Fully assembled SQL query with keys SELECT, FROM, LEFT_JOIN, WHERE
1147         * @var int       last_mark    User's last_mark time
1148         * @var string    sql_extra    Extra WHERE SQL statement
1149         * @var string    sql_sort     ORDER BY SQL sorting statement
1150         * @since 3.1.4-RC1
1151         */
1152        $vars = array(
1153            'sql_array',
1154            'last_mark',
1155            'sql_extra',
1156            'sql_sort',
1157        );
1158        extract($phpbb_dispatcher->trigger_event('core.get_unread_topics_modify_sql', compact($vars)));
1159
1160        $sql = $db->sql_build_query('SELECT', $sql_array);
1161        $result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
1162
1163        while ($row = $db->sql_fetchrow($result))
1164        {
1165            $topic_id = (int) $row['topic_id'];
1166            $unread_topics[$topic_id] = ($row['topic_mark_time']) ? (int) $row['topic_mark_time'] : (($row['forum_mark_time']) ? (int) $row['forum_mark_time'] : $last_mark);
1167        }
1168        $db->sql_freeresult($result);
1169    }
1170    else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1171    {
1172        global $tracking_topics;
1173
1174        if (empty($tracking_topics))
1175        {
1176            $tracking_topics = $request->variable($config['cookie_name'] . '_track', '', false, \phpbb\request\request_interface::COOKIE);
1177            $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1178        }
1179
1180        if (!$user->data['is_registered'])
1181        {
1182            $user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
1183        }
1184        else
1185        {
1186            $user_lastmark = (int) $user->data['user_lastmark'];
1187        }
1188
1189        $sql = 'SELECT t.topic_id, t.forum_id, t.topic_last_post_time
1190            FROM ' . TOPICS_TABLE . ' t
1191            WHERE t.topic_last_post_time > ' . $user_lastmark . "
1192            $sql_extra
1193            $sql_sort";
1194        $result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
1195
1196        while ($row = $db->sql_fetchrow($result))
1197        {
1198            $forum_id = (int) $row['forum_id'];
1199            $topic_id = (int) $row['topic_id'];
1200            $topic_id36 = base_convert($topic_id, 10, 36);
1201
1202            if (isset($tracking_topics['t'][$topic_id36]))
1203            {
1204                $last_read = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
1205
1206                if ($row['topic_last_post_time'] > $last_read)
1207                {
1208                    $unread_topics[$topic_id] = $last_read;
1209                }
1210            }
1211            else if (isset($tracking_topics['f'][$forum_id]))
1212            {
1213                $mark_time = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
1214
1215                if ($row['topic_last_post_time'] > $mark_time)
1216                {
1217                    $unread_topics[$topic_id] = $mark_time;
1218                }
1219            }
1220            else
1221            {
1222                $unread_topics[$topic_id] = $user_lastmark;
1223            }
1224        }
1225        $db->sql_freeresult($result);
1226    }
1227
1228    return $unread_topics;
1229}
1230
1231/**
1232* Check for read forums and update topic tracking info accordingly
1233*
1234* @param int $forum_id the forum id to check
1235* @param int $forum_last_post_time the forums last post time
1236* @param int $f_mark_time the forums last mark time if user is registered and load_db_lastread enabled
1237* @param int $mark_time_forum false if the mark time needs to be obtained, else the last users forum mark time
1238*
1239* @return true if complete forum got marked read, else false.
1240*/
1241function update_forum_tracking_info($forum_id, $forum_last_post_time, $f_mark_time = false, $mark_time_forum = false)
1242{
1243    global $db, $tracking_topics, $user, $config, $request, $phpbb_container;
1244
1245    // Determine the users last forum mark time if not given.
1246    if ($mark_time_forum === false)
1247    {
1248        if ($config['load_db_lastread'] && $user->data['is_registered'])
1249        {
1250            $mark_time_forum = (!empty($f_mark_time)) ? $f_mark_time : $user->data['user_lastmark'];
1251        }
1252        else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1253        {
1254            $tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
1255            $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1256
1257            if (!$user->data['is_registered'])
1258            {
1259                $user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate']) : 0;
1260            }
1261
1262            $mark_time_forum = (isset($tracking_topics['f'][$forum_id])) ? (int) (base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate']) : $user->data['user_lastmark'];
1263        }
1264    }
1265
1266    // Handle update of unapproved topics info.
1267    // Only update for moderators having m_approve permission for the forum.
1268    /* @var $phpbb_content_visibility \phpbb\content_visibility */
1269    $phpbb_content_visibility = $phpbb_container->get('content.visibility');
1270
1271    // Check the forum for any left unread topics.
1272    // If there are none, we mark the forum as read.
1273    if ($config['load_db_lastread'] && $user->data['is_registered'])
1274    {
1275        if ($mark_time_forum >= $forum_last_post_time)
1276        {
1277            // We do not need to mark read, this happened before. Therefore setting this to true
1278            $row = true;
1279        }
1280        else
1281        {
1282            $sql = 'SELECT t.forum_id
1283                FROM ' . TOPICS_TABLE . ' t
1284                LEFT JOIN ' . TOPICS_TRACK_TABLE . ' tt
1285                    ON (tt.topic_id = t.topic_id
1286                        AND tt.user_id = ' . $user->data['user_id'] . ')
1287                WHERE t.forum_id = ' . $forum_id . '
1288                    AND t.topic_last_post_time > ' . $mark_time_forum . '
1289                    AND t.topic_moved_id = 0
1290                    AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.') . '
1291                    AND (tt.topic_id IS NULL
1292                        OR tt.mark_time < t.topic_last_post_time)';
1293            $result = $db->sql_query_limit($sql, 1);
1294            $row = $db->sql_fetchrow($result);
1295            $db->sql_freeresult($result);
1296        }
1297    }
1298    else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1299    {
1300        // Get information from cookie
1301        if (!isset($tracking_topics['tf'][$forum_id]))
1302        {
1303            // We do not need to mark read, this happened before. Therefore setting this to true
1304            $row = true;
1305        }
1306        else
1307        {
1308            $sql = 'SELECT t.topic_id
1309                FROM ' . TOPICS_TABLE . ' t
1310                WHERE t.forum_id = ' . $forum_id . '
1311                    AND t.topic_last_post_time > ' . $mark_time_forum . '
1312                    AND t.topic_moved_id = 0
1313                    AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.');
1314            $result = $db->sql_query($sql);
1315
1316            $check_forum = $tracking_topics['tf'][$forum_id];
1317            $unread = false;
1318
1319            while ($row = $db->sql_fetchrow($result))
1320            {
1321                if (!isset($check_forum[base_convert($row['topic_id'], 10, 36)]))
1322                {
1323                    $unread = true;
1324                    break;
1325                }
1326            }
1327            $db->sql_freeresult($result);
1328
1329            $row = $unread;
1330        }
1331    }
1332    else
1333    {
1334        $row = true;
1335    }
1336
1337    if (!$row)
1338    {
1339        markread('topics', $forum_id);
1340        return true;
1341    }
1342
1343    return false;
1344}
1345
1346/**
1347* Transform an array into a serialized format
1348*/
1349function tracking_serialize($input)
1350{
1351    $out = '';
1352    foreach ($input as $key => $value)
1353    {
1354        if (is_array($value))
1355        {
1356            $out .= $key . ':(' . tracking_serialize($value) . ');';
1357        }
1358        else
1359        {
1360            $out .= $key . ':' . $value . ';';
1361        }
1362    }
1363    return $out;
1364}
1365
1366/**
1367* Transform a serialized array into an actual array
1368*/
1369function tracking_unserialize($string, $max_depth = 3)
1370{
1371    $n = strlen($string);
1372    if ($n > 10010)
1373    {
1374        die('Invalid data supplied');
1375    }
1376    $data = $stack = array();
1377    $key = '';
1378    $mode = 0;
1379    $level = &$data;
1380    for ($i = 0; $i < $n; ++$i)
1381    {
1382        switch ($mode)
1383        {
1384            case 0:
1385                switch ($string[$i])
1386                {
1387                    case ':':
1388                        $level[$key] = 0;
1389                        $mode = 1;
1390                    break;
1391                    case ')':
1392                        unset($level);
1393                        $level = array_pop($stack);
1394                        $mode = 3;
1395                    break;
1396                    default:
1397                        $key .= $string[$i];
1398                }
1399            break;
1400
1401            case 1:
1402                switch ($string[$i])
1403                {
1404                    case '(':
1405                        if (count($stack) >= $max_depth)
1406                        {
1407                            die('Invalid data supplied');
1408                        }
1409                        $stack[] = &$level;
1410                        $level[$key] = array();
1411                        $level = &$level[$key];
1412                        $key = '';
1413                        $mode = 0;
1414                    break;
1415                    default:
1416                        $level[$key] = $string[$i];
1417                        $mode = 2;
1418                    break;
1419                }
1420            break;
1421
1422            case 2:
1423                switch ($string[$i])
1424                {
1425                    case ')':
1426                        unset($level);
1427                        $level = array_pop($stack);
1428                        $mode = 3;
1429                    break;
1430                    case ';':
1431                        $key = '';
1432                        $mode = 0;
1433                    break;
1434                    default:
1435                        $level[$key] .= $string[$i];
1436                    break;
1437                }
1438            break;
1439
1440            case 3:
1441                switch ($string[$i])
1442                {
1443                    case ')':
1444                        unset($level);
1445                        $level = array_pop($stack);
1446                    break;
1447                    case ';':
1448                        $key = '';
1449                        $mode = 0;
1450                    break;
1451                    default:
1452                        die('Invalid data supplied');
1453                    break;
1454                }
1455            break;
1456        }
1457    }
1458
1459    if (count($stack) != 0 || ($mode != 0 && $mode != 3))
1460    {
1461        die('Invalid data supplied');
1462    }
1463
1464    return $level;
1465}
1466
1467// Server functions (building urls, redirecting...)
1468
1469/**
1470* Append session id to url.
1471*
1472* @param string $url The url the session id needs to be appended to (can have params)
1473* @param mixed $params String or array of additional url parameters
1474* @param bool $is_amp Is url using &amp; (true) or & (false)
1475* @param string $session_id Possibility to use a custom session id instead of the global one; deprecated as of 4.0.0-a1
1476* @param bool $is_route Is url generated by a route.
1477*
1478* @return string The corrected url.
1479*
1480* Examples:
1481* <code> append_sid("{$phpbb_root_path}viewtopic.$phpEx?t=1");
1482* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1');
1483* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1', false);
1484* append_sid("{$phpbb_root_path}viewtopic.$phpEx", array('t' => 1, 'f' => 2));
1485* </code>
1486*
1487*/
1488function append_sid($url, $params = false, $is_amp = true, $session_id = false, $is_route = false)
1489{
1490    global $_SID, $_EXTRA_URL, $phpbb_path_helper;
1491    global $phpbb_dispatcher;
1492
1493    if ($params === '' || (is_array($params) && empty($params)))
1494    {
1495        // Do not append the ? if the param-list is empty anyway.
1496        $params = false;
1497    }
1498
1499    // Update the root path with the correct relative web path
1500    if (!$is_route && $phpbb_path_helper instanceof \phpbb\path_helper)
1501    {
1502        $url = $phpbb_path_helper->update_web_root_path($url);
1503    }
1504
1505    $append_sid_overwrite = false;
1506
1507    /**
1508    * This event can either supplement or override the append_sid() function
1509    *
1510    * To override this function, the event must set $append_sid_overwrite to
1511    * the new URL value, which will be returned following the event
1512    *
1513    * @event core.append_sid
1514    * @var    string        url                        The url the session id needs
1515    *                                            to be appended to (can have
1516    *                                            params)
1517    * @var    mixed        params                    String or array of additional
1518    *                                            url parameters
1519    * @var    bool        is_amp                    Is url using &amp; (true) or
1520    *                                            & (false)
1521    * @var    bool|string    session_id                Possibility to use a custom
1522    *                                            session id (string) instead of
1523    *                                            the global one (false)
1524    * @var    bool|string    append_sid_overwrite    Overwrite function (string
1525    *                                            URL) or not (false)
1526    * @var    bool    is_route                    Is url generated by a route.
1527    * @since 3.1.0-a1
1528    */
1529    $vars = array('url', 'params', 'is_amp', 'session_id', 'append_sid_overwrite', 'is_route');
1530    extract($phpbb_dispatcher->trigger_event('core.append_sid', compact($vars)));
1531
1532    if ($append_sid_overwrite)
1533    {
1534        return $append_sid_overwrite;
1535    }
1536
1537    $params_is_array = is_array($params);
1538
1539    // Get anchor
1540    $anchor = '';
1541    if (strpos($url, '#') !== false)
1542    {
1543        list($url, $anchor) = explode('#', $url, 2);
1544        $anchor = '#' . $anchor;
1545    }
1546    else if (!$params_is_array && strpos($params, '#') !== false)
1547    {
1548        list($params, $anchor) = explode('#', $params, 2);
1549        $anchor = '#' . $anchor;
1550    }
1551
1552    // Handle really simple cases quickly
1553    if ($_SID == '' && $session_id === false && empty($_EXTRA_URL) && !$params_is_array && !$anchor)
1554    {
1555        if ($params === false)
1556        {
1557            return $url;
1558        }
1559
1560        $url_delim = (strpos($url, '?') === false) ? '?' : (($is_amp) ? '&amp;' : '&');
1561        return $url . ($params !== false ? $url_delim. $params : '');
1562    }
1563
1564    // Assign sid if session id is not specified
1565    if ($session_id === false)
1566    {
1567        $session_id = $_SID;
1568    }
1569
1570    $amp_delim = ($is_amp) ? '&amp;' : '&';
1571    $url_delim = (strpos($url, '?') === false) ? '?' : $amp_delim;
1572
1573    // Appending custom url parameter?
1574    $append_url = (!empty($_EXTRA_URL)) ? implode($amp_delim, $_EXTRA_URL) : '';
1575
1576    // Use the short variant if possible ;)
1577    if ($params === false)
1578    {
1579        // Append session id
1580        if (!$session_id)
1581        {
1582            return $url . (($append_url) ? $url_delim . $append_url : '') . $anchor;
1583        }
1584        else
1585        {
1586            return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . 'sid=' . $session_id . $anchor;
1587        }
1588    }
1589
1590    // Build string if parameters are specified as array
1591    if (is_array($params))
1592    {
1593        $output = array();
1594
1595        foreach ($params as $key => $item)
1596        {
1597            if ($item === NULL)
1598            {
1599                continue;
1600            }
1601
1602            if ($key == '#')
1603            {
1604                $anchor = '#' . $item;
1605                continue;
1606            }
1607
1608            $output[] = $key . '=' . $item;
1609        }
1610
1611        $params = implode($amp_delim, $output);
1612    }
1613
1614    // Append session id and parameters (even if they are empty)
1615    // If parameters are empty, the developer can still append his/her parameters without caring about the delimiter
1616    return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . $params . ((!$session_id) ? '' : $amp_delim . 'sid=' . $session_id) . $anchor;
1617}
1618
1619/**
1620* Generate board url (example: http://www.example.com/phpBB)
1621*
1622* @param bool $without_script_path if set to true the script path gets not appended (example: http://www.example.com)
1623*
1624* @return string the generated board url
1625*/
1626function generate_board_url($without_script_path = false)
1627{
1628    global $config, $user, $request, $symfony_request;
1629
1630    $server_name = $user->host;
1631
1632    // Forcing server vars is the only way to specify/override the protocol
1633    if ($config['force_server_vars'] || !$server_name)
1634    {
1635        $server_protocol = ($config['server_protocol']) ? $config['server_protocol'] : (($config['cookie_secure']) ? 'https://' : 'http://');
1636        $server_name = $config['server_name'];
1637        $server_port = (int) $config['server_port'];
1638        $script_path = $config['script_path'];
1639
1640        $url = $server_protocol . $server_name;
1641        $cookie_secure = $config['cookie_secure'];
1642    }
1643    else
1644    {
1645        $server_port = (int) $symfony_request->getPort();
1646
1647        $forwarded_proto = $request->server('HTTP_X_FORWARDED_PROTO');
1648
1649        if (!empty($forwarded_proto) && $forwarded_proto === 'https')
1650        {
1651            $server_port = 443;
1652        }
1653        // Do not rely on cookie_secure, users seem to think that it means a secured cookie instead of an encrypted connection
1654        $cookie_secure = $request->is_secure() ? 1 : 0;
1655        $url = (($cookie_secure) ? 'https://' : 'http://') . $server_name;
1656
1657        $script_path = $user->page['root_script_path'];
1658    }
1659
1660    if ($server_port && (($cookie_secure && $server_port <> 443) || (!$cookie_secure && $server_port <> 80)))
1661    {
1662        // HTTP HOST can carry a port number (we fetch $user->host, but for old versions this may be true)
1663        if (strpos($server_name, ':') === false)
1664        {
1665            $url .= ':' . $server_port;
1666        }
1667    }
1668
1669    if (!$without_script_path)
1670    {
1671        $url .= $script_path;
1672    }
1673
1674    // Strip / from the end
1675    if (substr($url, -1, 1) == '/')
1676    {
1677        $url = substr($url, 0, -1);
1678    }
1679
1680    return $url;
1681}
1682
1683/**
1684* Redirects the user to another page then exits the script nicely
1685* This function is intended for urls within the board. It's not meant to redirect to cross-domains.
1686*
1687* @param string $url The url to redirect to
1688* @param bool $return If true, do not redirect but return the sanitized URL. Default is no return.
1689* @param bool $disable_cd_check If true, redirect() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain. Default is false.
1690* @return string|never
1691*/
1692function redirect($url, $return = false, $disable_cd_check = false)
1693{
1694    global $user, $phpbb_path_helper, $phpbb_dispatcher;
1695
1696    if (!$user->is_setup())
1697    {
1698        $user->add_lang('common');
1699    }
1700
1701    // Make sure no &amp;'s are in, this will break the redirect
1702    $url = str_replace('&amp;', '&', $url);
1703
1704    // Determine which type of redirect we need to handle...
1705    $url_parts = @parse_url($url);
1706
1707    if ($url_parts === false)
1708    {
1709        // Malformed url
1710        trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
1711    }
1712    else if (!empty($url_parts['scheme']) && !empty($url_parts['host']))
1713    {
1714        // Attention: only able to redirect within the same domain if $disable_cd_check is false (yourdomain.com -> www.yourdomain.com will not work)
1715        if (!$disable_cd_check && $url_parts['host'] !== $user->host)
1716        {
1717            trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
1718        }
1719    }
1720    else if ($url[0] == '/')
1721    {
1722        // Absolute uri, prepend direct url...
1723        $url = generate_board_url(true) . $url;
1724    }
1725    else
1726    {
1727        // Relative uri
1728        $pathinfo = pathinfo($url);
1729
1730        // Is the uri pointing to the current directory?
1731        if ($pathinfo['dirname'] == '.')
1732        {
1733            $url = str_replace('./', '', $url);
1734
1735            // Strip / from the beginning
1736            if ($url && substr($url, 0, 1) == '/')
1737            {
1738                $url = substr($url, 1);
1739            }
1740        }
1741
1742        $url = $phpbb_path_helper->remove_web_root_path($url);
1743
1744        if ($user->page['page_dir'])
1745        {
1746            $url = $user->page['page_dir'] . '/' . $url;
1747        }
1748
1749        $url = generate_board_url() . '/' . $url;
1750    }
1751
1752    // Clean URL and check if we go outside the forum directory
1753    $url = $phpbb_path_helper->clean_url($url);
1754
1755    if (!$disable_cd_check && strpos($url, generate_board_url(true) . '/') !== 0)
1756    {
1757        trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
1758    }
1759
1760    // Make sure no linebreaks are there... to prevent http response splitting for PHP < 4.4.2
1761    if (strpos(urldecode($url), "\n") !== false || strpos(urldecode($url), "\r") !== false || strpos($url, ';') !== false)
1762    {
1763        trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
1764    }
1765
1766    // Now, also check the protocol and for a valid url the last time...
1767    $allowed_protocols = array('http', 'https', 'ftp', 'ftps');
1768    $url_parts = parse_url($url);
1769
1770    if ($url_parts === false || empty($url_parts['scheme']) || !in_array($url_parts['scheme'], $allowed_protocols))
1771    {
1772        trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
1773    }
1774
1775    /**
1776    * Execute code and/or overwrite redirect()
1777    *
1778    * @event core.functions.redirect
1779    * @var    string    url                    The url
1780    * @var    bool    return                If true, do not redirect but return the sanitized URL.
1781    * @var    bool    disable_cd_check    If true, redirect() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain.
1782    * @since 3.1.0-RC3
1783    */
1784    $vars = array('url', 'return', 'disable_cd_check');
1785    extract($phpbb_dispatcher->trigger_event('core.functions.redirect', compact($vars)));
1786
1787    if ($return)
1788    {
1789        return $url;
1790    }
1791    else
1792    {
1793        garbage_collection();
1794    }
1795
1796    // Behave as per HTTP/1.1 spec for others
1797    header('Location: ' . $url);
1798    exit;
1799}
1800
1801/**
1802 * Returns the install redirect path for phpBB.
1803 *
1804 * @param string $phpbb_root_path The root path of the phpBB installation.
1805 * @param string $phpEx The file extension of php files, e.g., "php".
1806 * @return string The install redirect path.
1807 */
1808function phpbb_get_install_redirect(string $phpbb_root_path, string $phpEx): string
1809{
1810    $script_name = (!empty($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : getenv('REQUEST_URI');
1811    if (!$script_name)
1812    {
1813        $script_name = (!empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : getenv('PHP_SELF');
1814    }
1815
1816    // Add trailing dot to prevent dirname() from returning parent directory if $script_name is a directory
1817    $script_name = substr($script_name, -1) === '/' ? $script_name . '.' : $script_name;
1818
1819    // $phpbb_root_path accounts for redirects from e.g. /adm
1820    $script_path = trim(dirname($script_name)) . '/' . $phpbb_root_path . 'install/app.' . $phpEx;
1821    // Replace any number of consecutive backslashes and/or slashes with a single slash
1822    // (could happen on some proxy setups and/or Windows servers)
1823    return preg_replace('#[\\\\/]{2,}#', '/', $script_path);
1824}
1825
1826/**
1827* Re-Apply session id after page reloads
1828*/
1829function reapply_sid($url, $is_route = false)
1830{
1831    global $phpEx, $phpbb_root_path;
1832
1833    if ($url === "index.$phpEx")
1834    {
1835        return append_sid("index.$phpEx");
1836    }
1837    else if ($url === "{$phpbb_root_path}index.$phpEx")
1838    {
1839        return append_sid("{$phpbb_root_path}index.$phpEx");
1840    }
1841
1842    // Remove previously added sid
1843    if (strpos($url, 'sid=') !== false)
1844    {
1845        // All kind of links
1846        $url = preg_replace('/(\?)?(&amp;|&)?sid=[a-z0-9]+/', '', $url);
1847        // if the sid was the first param, make the old second as first ones
1848        $url = preg_replace("/$phpEx(&amp;|&)+?/", "$phpEx?", $url);
1849    }
1850
1851    return append_sid($url, false, true, false, $is_route);
1852}
1853
1854/**
1855* Returns url from the session/current page with an re-appended SID with optionally stripping vars from the url
1856*/
1857function build_url($strip_vars = false)
1858{
1859    global $config, $user, $phpbb_path_helper;
1860
1861    $page = $phpbb_path_helper->get_valid_page($user->page['page'], $config['enable_mod_rewrite']);
1862
1863    // Append SID
1864    $redirect = append_sid($page, false, false);
1865
1866    if ($strip_vars !== false)
1867    {
1868        $redirect = $phpbb_path_helper->strip_url_params($redirect, $strip_vars, false);
1869    }
1870    else
1871    {
1872        $redirect = str_replace('&', '&amp;', $redirect);
1873    }
1874
1875    return $redirect;
1876}
1877
1878/**
1879* Meta refresh assignment
1880* Adds META template variable with meta http tag.
1881*
1882* @param int $time Time in seconds for meta refresh tag
1883* @param string $url URL to redirect to. The url will go through redirect() first before the template variable is assigned
1884* @param bool $disable_cd_check If true, meta_refresh() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain. Default is false.
1885*/
1886function meta_refresh($time, $url, $disable_cd_check = false)
1887{
1888    global $template, $refresh_data, $request;
1889
1890    $url = redirect($url, true, $disable_cd_check);
1891    if ($request->is_ajax())
1892    {
1893        $refresh_data = array(
1894            'time'    => $time,
1895            'url'    => $url,
1896        );
1897    }
1898    else
1899    {
1900        // For XHTML compatibility we change back & to &amp;
1901        $url = str_replace('&', '&amp;', $url);
1902
1903        $template->assign_vars(array(
1904            'META' => '<meta http-equiv="refresh" content="' . $time . '; url=' . $url . '" />')
1905        );
1906    }
1907
1908    return $url;
1909}
1910
1911/**
1912* Outputs correct status line header.
1913*
1914* Depending on php sapi one of the two following forms is used:
1915*
1916* Status: 404 Not Found
1917*
1918* HTTP/1.x 404 Not Found
1919*
1920* HTTP version is taken from HTTP_VERSION environment variable,
1921* and defaults to 1.0.
1922*
1923* Sample usage:
1924*
1925* send_status_line(404, 'Not Found');
1926*
1927* @param int $code HTTP status code
1928* @param string $message Message for the status code
1929* @return void
1930*/
1931function send_status_line($code, $message)
1932{
1933    if (substr(strtolower(@php_sapi_name()), 0, 3) === 'cgi')
1934    {
1935        // in theory, we shouldn't need that due to php doing it. Reality offers a differing opinion, though
1936        header("Status: $code $message", true, $code);
1937    }
1938    else
1939    {
1940        $version = phpbb_request_http_version();
1941        header("$version $code $message", true, $code);
1942    }
1943}
1944
1945/**
1946* Returns the HTTP version used in the current request.
1947*
1948* Handles the case of being called before $request is present,
1949* in which case it falls back to the $_SERVER superglobal.
1950*
1951* @return string HTTP version
1952*/
1953function phpbb_request_http_version()
1954{
1955    global $request;
1956
1957    $version = '';
1958    if ($request && $request->server('SERVER_PROTOCOL'))
1959    {
1960        $version = $request->server('SERVER_PROTOCOL');
1961    }
1962    else if (isset($_SERVER['SERVER_PROTOCOL']))
1963    {
1964        $version = $_SERVER['SERVER_PROTOCOL'];
1965    }
1966
1967    if (!empty($version) && is_string($version) && preg_match('#^HTTP/[0-9]\.[0-9]$#', $version))
1968    {
1969        return $version;
1970    }
1971
1972    return 'HTTP/1.0';
1973}
1974
1975//Form validation
1976
1977
1978/**
1979* Add a secret hash   for use in links/GET requests
1980* @param string  $link_name The name of the link; has to match the name used in check_link_hash, otherwise no restrictions apply
1981* @return string the hash
1982
1983*/
1984function generate_link_hash($link_name)
1985{
1986    global $user;
1987
1988    if (!isset($user->data["hash_$link_name"]))
1989    {
1990        $user->data["hash_$link_name"] = substr(sha1($user->data['user_form_salt'] . $link_name), 0, 8);
1991    }
1992
1993    return $user->data["hash_$link_name"];
1994}
1995
1996
1997/**
1998* checks a link hash - for GET requests
1999* @param string $token the submitted token
2000* @param string $link_name The name of the link
2001* @return boolean true if all is fine
2002*/
2003function check_link_hash($token, $link_name)
2004{
2005    return $token === generate_link_hash($link_name);
2006}
2007
2008/**
2009* Add a secret token to the form (requires the S_FORM_TOKEN template variable)
2010* @param string  $form_name The name of the form; has to match the name used in check_form_key, otherwise no restrictions apply
2011* @param string  $template_variable_suffix A string that is appended to the name of the template variable to which the form elements are assigned
2012*/
2013function add_form_key($form_name, $template_variable_suffix = '')
2014{
2015    global $phpbb_container, $phpbb_dispatcher, $template;
2016
2017    /** @var \phpbb\form\form_helper $form_helper */
2018    $form_helper = $phpbb_container->get('form_helper');
2019
2020    $form_tokens = $form_helper->get_form_tokens($form_name, $now, $token_sid, $token);
2021
2022    $s_fields = build_hidden_fields($form_tokens);
2023
2024    /**
2025    * Perform additional actions on creation of the form token
2026    *
2027    * @event core.add_form_key
2028    * @var    string    form_name                    The form name
2029    * @var    int        now                            Current time timestamp
2030    * @var    string    s_fields                    Generated hidden fields
2031    * @var    string    token                        Form token
2032    * @var    string    token_sid                    User session ID
2033    * @var    string    template_variable_suffix    The string that is appended to template variable name
2034    *
2035    * @since 3.1.0-RC3
2036    * @changed 3.1.11-RC1 Added template_variable_suffix
2037    */
2038    $vars = array(
2039        'form_name',
2040        'now',
2041        's_fields',
2042        'token',
2043        'token_sid',
2044        'template_variable_suffix',
2045    );
2046    extract($phpbb_dispatcher->trigger_event('core.add_form_key', compact($vars)));
2047
2048    $template->assign_var('S_FORM_TOKEN' . $template_variable_suffix, $s_fields);
2049}
2050
2051/**
2052 * Check the form key. Required for all altering actions not secured by confirm_box
2053 *
2054 * @param    string    $form_name    The name of the form; has to match the name used
2055 *                                in add_form_key, otherwise no restrictions apply
2056 * @param    int        $timespan    The maximum acceptable age for a submitted form
2057 *                                in seconds. Defaults to the config setting.
2058 * @return    bool    True, if the form key was valid, false otherwise
2059 */
2060function check_form_key($form_name, $timespan = false)
2061{
2062    global $phpbb_container;
2063
2064    /** @var \phpbb\form\form_helper $form_helper */
2065    $form_helper = $phpbb_container->get('form_helper');
2066
2067    return $form_helper->check_form_tokens($form_name, $timespan !== false ? $timespan : null);
2068}
2069
2070// Message/Login boxes
2071
2072/**
2073* Build Confirm box
2074* @param boolean $check True for checking if confirmed (without any additional parameters) and false for displaying the confirm box
2075* @param string|array $title Title/Message used for confirm box.
2076*        message text is _CONFIRM appended to title.
2077*        If title cannot be found in user->lang a default one is displayed
2078*        If title_CONFIRM cannot be found in user->lang the text given is used.
2079*       If title is an array, the first array value is used as explained per above,
2080*       all other array values are sent as parameters to the language function.
2081* @param string $hidden Hidden variables
2082* @param string $html_body Template used for confirm box
2083* @param string $u_action Custom form action
2084*
2085* @return bool True if confirmation was successful, false if not
2086*/
2087function confirm_box($check, $title = '', $hidden = '', $html_body = 'confirm_body.html', $u_action = '')
2088{
2089    global $user, $template, $db, $request;
2090    global $config, $language, $phpbb_path_helper, $phpbb_dispatcher;
2091
2092    if (isset($_POST['cancel']))
2093    {
2094        return false;
2095    }
2096
2097    $confirm = ($language->lang('YES') === $request->variable('confirm', '', true, \phpbb\request\request_interface::POST));
2098
2099    if ($check && $confirm)
2100    {
2101        $user_id = $request->variable('confirm_uid', 0);
2102        $session_id = $request->variable('sess', '');
2103        $confirm_key = $request->variable('confirm_key', '');
2104
2105        if ($user_id != $user->data['user_id'] || $session_id != $user->session_id || !$confirm_key || !$user->data['user_last_confirm_key'] || $confirm_key != $user->data['user_last_confirm_key'])
2106        {
2107            return false;
2108        }
2109
2110        // Reset user_last_confirm_key
2111        $sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = ''
2112            WHERE user_id = " . $user->data['user_id'];
2113        $db->sql_query($sql);
2114
2115        return true;
2116    }
2117    else if ($check)
2118    {
2119        return false;
2120    }
2121
2122    $s_hidden_fields = build_hidden_fields(array(
2123        'confirm_uid'    => $user->data['user_id'],
2124        'sess'            => $user->session_id,
2125        'sid'            => $user->session_id,
2126    ));
2127
2128    // generate activation key
2129    $confirm_key = gen_rand_string(10);
2130
2131    // generate language strings
2132    if (is_array($title))
2133    {
2134        $key = array_shift($title);
2135        $count = array_shift($title);
2136        $confirm_title =  $language->is_set($key) ? $language->lang($key, $count, $title) : $language->lang('CONFIRM');
2137        $confirm_text = $language->is_set($key . '_CONFIRM') ? $language->lang($key . '_CONFIRM', $count, $title) : $key;
2138    }
2139    else
2140    {
2141        $confirm_title = $language->is_set($title) ? $language->lang($title) : $language->lang('CONFIRM');
2142        $confirm_text = $language->is_set($title . '_CONFIRM') ? $language->lang($title . '_CONFIRM') : $title;
2143    }
2144
2145    if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
2146    {
2147        adm_page_header($confirm_title);
2148    }
2149    else
2150    {
2151        page_header($confirm_title);
2152    }
2153
2154    $template->set_filenames(array(
2155        'body' => $html_body)
2156    );
2157
2158    // If activation key already exist, we better do not re-use the key (something very strange is going on...)
2159    if ($request->variable('confirm_key', ''))
2160    {
2161        // This should not occur, therefore we cancel the operation to safe the user
2162        return false;
2163    }
2164
2165    // re-add sid / transform & to &amp; for user->page (user->page is always using &)
2166    $use_page = ($u_action) ? $u_action : str_replace('&', '&amp;', $user->page['page']);
2167    $u_action = reapply_sid($phpbb_path_helper->get_valid_page($use_page, $config['enable_mod_rewrite']));
2168    $u_action .= ((strpos($u_action, '?') === false) ? '?' : '&amp;') . 'confirm_key=' . $confirm_key;
2169
2170    $template->assign_vars(array(
2171        'MESSAGE_TITLE'        => $confirm_title,
2172        'MESSAGE_TEXT'        => $confirm_text,
2173
2174        'YES_VALUE'            => $language->lang('YES'),
2175        'S_CONFIRM_ACTION'    => $u_action,
2176        'S_HIDDEN_FIELDS'    => $hidden . $s_hidden_fields,
2177        'S_AJAX_REQUEST'    => $request->is_ajax(),
2178    ));
2179
2180    $sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = '" . $db->sql_escape($confirm_key) . "'
2181        WHERE user_id = " . $user->data['user_id'];
2182    $db->sql_query($sql);
2183
2184    if ($request->is_ajax())
2185    {
2186        $u_action .= '&confirm_uid=' . $user->data['user_id'] . '&sess=' . $user->session_id . '&sid=' . $user->session_id;
2187        $data = array(
2188            'MESSAGE_BODY'        => $template->assign_display('body'),
2189            'MESSAGE_TITLE'        => $confirm_title,
2190            'MESSAGE_TEXT'        => $confirm_text,
2191
2192            'YES_VALUE'            => $language->lang('YES'),
2193            'S_CONFIRM_ACTION'    => str_replace('&amp;', '&', $u_action), //inefficient, rewrite whole function
2194            'S_HIDDEN_FIELDS'    => $hidden . $s_hidden_fields
2195        );
2196
2197        /**
2198         * This event allows an extension to modify the ajax output of confirm box.
2199         *
2200         * @event core.confirm_box_ajax_before
2201         * @var string    u_action        Action of the form
2202         * @var array    data            Data to be sent
2203         * @var string    hidden            Hidden fields generated by caller
2204         * @var string    s_hidden_fields    Hidden fields generated by this function
2205         * @since 3.2.8-RC1
2206         */
2207        $vars = array(
2208            'u_action',
2209            'data',
2210            'hidden',
2211            's_hidden_fields',
2212        );
2213        extract($phpbb_dispatcher->trigger_event('core.confirm_box_ajax_before', compact($vars)));
2214
2215        $json_response = new \phpbb\json_response;
2216        $json_response->send($data);
2217    }
2218
2219    if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
2220    {
2221        adm_page_footer();
2222    }
2223    else
2224    {
2225        page_footer();
2226    }
2227
2228    exit; // unreachable, page_footer() above will call exit()
2229}
2230
2231/**
2232* Generate login box or verify password
2233*/
2234function login_box($redirect = '', $l_explain = '', $l_success = '', $admin = false, $s_display = true)
2235{
2236    global $user, $template, $auth, $phpEx, $phpbb_root_path, $config;
2237    global $request, $phpbb_container, $phpbb_dispatcher, $phpbb_log;
2238
2239    $err = '';
2240    $form_name = 'login';
2241    $username = $autologin = false;
2242
2243    // Make sure user->setup() has been called
2244    if (!$user->is_setup())
2245    {
2246        $user->setup();
2247    }
2248
2249    /**
2250     * This event allows an extension to modify the login process
2251     *
2252     * @event core.login_box_before
2253     * @var string    redirect    Redirect string
2254     * @var string    l_explain    Explain language string
2255     * @var string    l_success    Success language string
2256     * @var    bool    admin        Is admin?
2257     * @var bool    s_display    Display full login form?
2258     * @var string    err            Error string
2259     * @since 3.1.9-RC1
2260     */
2261    $vars = array('redirect', 'l_explain', 'l_success', 'admin', 's_display', 'err');
2262    extract($phpbb_dispatcher->trigger_event('core.login_box_before', compact($vars)));
2263
2264    // Print out error if user tries to authenticate as an administrator without having the privileges...
2265    if ($admin && !$auth->acl_get('a_'))
2266    {
2267        // Not authd
2268        // anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
2269        if ($user->data['is_registered'])
2270        {
2271            $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
2272        }
2273        send_status_line(403, 'Forbidden');
2274        trigger_error('NO_AUTH_ADMIN');
2275    }
2276
2277    if (empty($err) && ($request->is_set_post('login') || ($request->is_set('login') && $request->variable('login', '') == 'external')))
2278    {
2279        // Get credential
2280        if ($admin)
2281        {
2282            $credential = $request->variable('credential', '');
2283
2284            if (strspn($credential, 'abcdef0123456789') !== strlen($credential) || strlen($credential) != 32)
2285            {
2286                if ($user->data['is_registered'])
2287                {
2288                    $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
2289                }
2290                send_status_line(403, 'Forbidden');
2291                trigger_error('NO_AUTH_ADMIN');
2292            }
2293
2294            $password    = $request->untrimmed_variable('password_' . $credential, '', true);
2295        }
2296        else
2297        {
2298            $password    = $request->untrimmed_variable('password', '', true);
2299        }
2300
2301        $username    = $request->variable('username', '', true);
2302        $autologin    = $request->is_set_post('autologin');
2303        $viewonline = (int) !$request->is_set_post('viewonline');
2304        $admin         = ($admin) ? 1 : 0;
2305        $viewonline = ($admin) ? $user->data['session_viewonline'] : $viewonline;
2306
2307        // Check if the supplied username is equal to the one stored within the database if re-authenticating
2308        if ($admin && utf8_clean_string($username) != utf8_clean_string($user->data['username']))
2309        {
2310            // We log the attempt to use a different username...
2311            $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
2312
2313            send_status_line(403, 'Forbidden');
2314            trigger_error('NO_AUTH_ADMIN_USER_DIFFER');
2315        }
2316
2317        // Check form key
2318        if ($password && !defined('IN_CHECK_BAN') && !check_form_key($form_name))
2319        {
2320            $result = array(
2321                'status' => false,
2322                'error_msg' => 'FORM_INVALID',
2323            );
2324        }
2325        else
2326        {
2327            // If authentication is successful we redirect user to previous page
2328            $result = $auth->login($username, $password, $autologin, $viewonline, $admin);
2329        }
2330
2331        // If admin authentication and login, we will log if it was a success or not...
2332        // We also break the operation on the first non-success login - it could be argued that the user already knows
2333        if ($admin)
2334        {
2335            if ($result['status'] == LOGIN_SUCCESS)
2336            {
2337                $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_SUCCESS');
2338            }
2339            else
2340            {
2341                // Only log the failed attempt if a real user tried to.
2342                // anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
2343                if ($user->data['is_registered'])
2344                {
2345                    $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
2346                }
2347            }
2348        }
2349
2350        // The result parameter is always an array, holding the relevant information...
2351        if ($result['status'] == LOGIN_SUCCESS)
2352        {
2353            $redirect = $request->variable('redirect', "{$phpbb_root_path}index.$phpEx");
2354
2355            /**
2356            * This event allows an extension to modify the redirection when a user successfully logs in
2357            *
2358            * @event core.login_box_redirect
2359            * @var  string    redirect    Redirect string
2360            * @var    bool    admin        Is admin?
2361            * @var    array    result        Result from auth provider
2362            * @since 3.1.0-RC5
2363            * @changed 3.1.9-RC1 Removed undefined return variable
2364            * @changed 3.2.4-RC1 Added result
2365            */
2366            $vars = array('redirect', 'admin', 'result');
2367            extract($phpbb_dispatcher->trigger_event('core.login_box_redirect', compact($vars)));
2368
2369            // append/replace SID (may change during the session for AOL users)
2370            $redirect = reapply_sid($redirect);
2371
2372            // Special case... the user is effectively banned, but we allow founders to login
2373            if (defined('IN_CHECK_BAN') && $result['user_row']['user_type'] != USER_FOUNDER)
2374            {
2375                return;
2376            }
2377
2378            redirect($redirect);
2379        }
2380
2381        // Something failed, determine what...
2382        if ($result['status'] == LOGIN_BREAK)
2383        {
2384            trigger_error($result['error_msg']);
2385        }
2386
2387        // Special cases... determine
2388        switch ($result['status'])
2389        {
2390            case LOGIN_ERROR_PASSWORD_CONVERT:
2391                $err = sprintf(
2392                    $user->lang[$result['error_msg']],
2393                    ($config['email_enable']) ? '<a href="' . append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=sendpassword') . '">' : '',
2394                    ($config['email_enable']) ? '</a>' : '',
2395                    '<a href="' . phpbb_get_board_contact_link($config, $phpbb_root_path, $phpEx) . '">',
2396                    '</a>'
2397                );
2398            break;
2399
2400            case LOGIN_ERROR_ATTEMPTS:
2401
2402                $captcha = $phpbb_container->get('captcha.factory')->get_instance($config['captcha_plugin']);
2403                $captcha->init(CONFIRM_LOGIN);
2404                // $captcha->reset();
2405
2406                $template->assign_vars(array(
2407                    'CAPTCHA_TEMPLATE'            => $captcha->get_template(),
2408                ));
2409            // no break;
2410
2411            // Username, password, etc...
2412            default:
2413                $err = $user->lang[$result['error_msg']];
2414
2415                // Assign admin contact to some error messages
2416                if ($result['error_msg'] == 'LOGIN_ERROR_USERNAME' || $result['error_msg'] == 'LOGIN_ERROR_PASSWORD')
2417                {
2418                    $err = sprintf($user->lang[$result['error_msg']], '<a href="' . append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=contactadmin') . '">', '</a>');
2419                }
2420
2421            break;
2422        }
2423
2424        /**
2425         * This event allows an extension to process when a user fails a login attempt
2426         *
2427         * @event core.login_box_failed
2428         * @var array   result      Login result data
2429         * @var string  username    User name used to login
2430         * @var string  password    Password used to login
2431         * @var string  err         Error message
2432         * @since 3.1.3-RC1
2433         */
2434        $vars = array('result', 'username', 'password', 'err');
2435        extract($phpbb_dispatcher->trigger_event('core.login_box_failed', compact($vars)));
2436    }
2437
2438    // Assign credential for username/password pair
2439    $credential = ($admin) ? md5(unique_id()) : false;
2440
2441    $s_hidden_fields = array(
2442        'sid'        => $user->session_id,
2443    );
2444
2445    if ($redirect)
2446    {
2447        $s_hidden_fields['redirect'] = $redirect;
2448    }
2449
2450    if ($admin)
2451    {
2452        $s_hidden_fields['credential'] = $credential;
2453    }
2454
2455    /* @var $provider_collection \phpbb\auth\provider_collection */
2456    $provider_collection = $phpbb_container->get('auth.provider_collection');
2457    $auth_provider = $provider_collection->get_provider();
2458
2459    $auth_provider_data = $auth_provider->get_login_data();
2460    if ($auth_provider_data)
2461    {
2462        if (isset($auth_provider_data['VARS']))
2463        {
2464            $template->assign_vars($auth_provider_data['VARS']);
2465        }
2466
2467        if (isset($auth_provider_data['BLOCK_VAR_NAME']))
2468        {
2469            foreach ($auth_provider_data['BLOCK_VARS'] as $block_vars)
2470            {
2471                $template->assign_block_vars($auth_provider_data['BLOCK_VAR_NAME'], $block_vars);
2472            }
2473        }
2474
2475        $template->assign_vars(array(
2476            'PROVIDER_TEMPLATE_FILE' => $auth_provider_data['TEMPLATE_FILE'],
2477        ));
2478    }
2479
2480    $s_hidden_fields = build_hidden_fields($s_hidden_fields);
2481
2482    /** @var \phpbb\controller\helper $controller_helper */
2483    $controller_helper = $phpbb_container->get('controller.helper');
2484
2485    $login_box_template_data = array(
2486        'LOGIN_ERROR'        => $err,
2487        'LOGIN_EXPLAIN'        => $l_explain,
2488
2489        'U_SEND_PASSWORD'         => ($config['email_enable'] && $config['allow_password_reset']) ? $controller_helper->route('phpbb_ucp_forgot_password_controller') : '',
2490        'U_RESEND_ACTIVATION'    => ($config['require_activation'] == USER_ACTIVATION_SELF && $config['email_enable']) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=resend_act') : '',
2491        'U_TERMS_USE'            => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=terms'),
2492        'U_PRIVACY'                => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy'),
2493        'UA_PRIVACY'            => addslashes(append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy')),
2494
2495        'S_DISPLAY_FULL_LOGIN'    => ($s_display) ? true : false,
2496        'S_HIDDEN_FIELDS'         => $s_hidden_fields,
2497
2498        'S_ADMIN_AUTH'            => $admin,
2499        'USERNAME'                => ($admin) ? $user->data['username'] : '',
2500
2501        'USERNAME_CREDENTIAL'    => 'username',
2502        'PASSWORD_CREDENTIAL'    => ($admin) ? 'password_' . $credential : 'password',
2503    );
2504
2505    /**
2506     * Event to add/modify login box template data
2507     *
2508     * @event core.login_box_modify_template_data
2509     * @var    int        admin                            Flag whether user is admin
2510     * @var    string    username                        User name
2511     * @var    int        autologin                        Flag whether autologin is enabled
2512     * @var string    redirect                        Redirect URL
2513     * @var    array    login_box_template_data            Array with the login box template data
2514     * @since 3.2.3-RC2
2515     */
2516    $vars = array(
2517        'admin',
2518        'username',
2519        'autologin',
2520        'redirect',
2521        'login_box_template_data',
2522    );
2523    extract($phpbb_dispatcher->trigger_event('core.login_box_modify_template_data', compact($vars)));
2524
2525    $template->assign_vars($login_box_template_data);
2526
2527    page_header($user->lang['LOGIN']);
2528
2529    $template->set_filenames(array(
2530        'body' => 'login_body.html')
2531    );
2532    make_jumpbox(append_sid("{$phpbb_root_path}viewforum.$phpEx"));
2533
2534    page_footer();
2535}
2536
2537/**
2538* Generate forum login box
2539*/
2540function login_forum_box($forum_data)
2541{
2542    global $db, $phpbb_container, $request, $template, $user, $phpbb_dispatcher, $phpbb_root_path, $phpEx;
2543
2544    $password = $request->variable('password', '', true);
2545
2546    $sql = 'SELECT forum_id
2547        FROM ' . FORUMS_ACCESS_TABLE . '
2548        WHERE forum_id = ' . $forum_data['forum_id'] . '
2549            AND user_id = ' . $user->data['user_id'] . "
2550            AND session_id = '" . $db->sql_escape($user->session_id) . "'";
2551    $result = $db->sql_query($sql);
2552    $row = $db->sql_fetchrow($result);
2553    $db->sql_freeresult($result);
2554
2555    if ($row)
2556    {
2557        return true;
2558    }
2559
2560    if ($password)
2561    {
2562        // Remove expired authorised sessions
2563        $sql = 'SELECT f.session_id
2564            FROM ' . FORUMS_ACCESS_TABLE . ' f
2565            LEFT JOIN ' . SESSIONS_TABLE . ' s ON (f.session_id = s.session_id)
2566            WHERE s.session_id IS NULL';
2567        $result = $db->sql_query($sql);
2568
2569        if ($row = $db->sql_fetchrow($result))
2570        {
2571            $sql_in = array();
2572            do
2573            {
2574                $sql_in[] = (string) $row['session_id'];
2575            }
2576            while ($row = $db->sql_fetchrow($result));
2577
2578            // Remove expired sessions
2579            $sql = 'DELETE FROM ' . FORUMS_ACCESS_TABLE . '
2580                WHERE ' . $db->sql_in_set('session_id', $sql_in);
2581            $db->sql_query($sql);
2582        }
2583        $db->sql_freeresult($result);
2584
2585        /* @var $passwords_manager \phpbb\passwords\manager */
2586        $passwords_manager = $phpbb_container->get('passwords.manager');
2587
2588        if ($passwords_manager->check($password, $forum_data['forum_password']))
2589        {
2590            $sql_ary = array(
2591                'forum_id'        => (int) $forum_data['forum_id'],
2592                'user_id'        => (int) $user->data['user_id'],
2593                'session_id'    => (string) $user->session_id,
2594            );
2595
2596            $db->sql_query('INSERT INTO ' . FORUMS_ACCESS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
2597
2598            return true;
2599        }
2600
2601        $template->assign_var('LOGIN_ERROR', $user->lang['WRONG_PASSWORD']);
2602    }
2603
2604    /**
2605    * Performing additional actions, load additional data on forum login
2606    *
2607    * @event core.login_forum_box
2608    * @var    array    forum_data        Array with forum data
2609    * @var    string    password        Password entered
2610    * @since 3.1.0-RC3
2611    */
2612    $vars = array('forum_data', 'password');
2613    extract($phpbb_dispatcher->trigger_event('core.login_forum_box', compact($vars)));
2614
2615    page_header($user->lang['LOGIN']);
2616
2617    $template->assign_vars(array(
2618        'FORUM_NAME'            => isset($forum_data['forum_name']) ? $forum_data['forum_name'] : '',
2619        'S_LOGIN_ACTION'        => build_url(array('f')),
2620        'S_HIDDEN_FIELDS'        => build_hidden_fields(array('f' => $forum_data['forum_id'])))
2621    );
2622
2623    $template->set_filenames(array(
2624        'body' => 'login_forum.html')
2625    );
2626
2627    make_jumpbox(append_sid("{$phpbb_root_path}viewforum.$phpEx"), $forum_data['forum_id']);
2628
2629    page_footer();
2630}
2631
2632// Little helpers
2633
2634/**
2635* Little helper for the build_hidden_fields function
2636*/
2637function _build_hidden_fields($key, $value, $specialchar, $stripslashes)
2638{
2639    $hidden_fields = '';
2640
2641    if (!is_array($value))
2642    {
2643        $value = ($stripslashes) ? stripslashes($value) : $value;
2644        $value = ($specialchar) ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value;
2645
2646        $hidden_fields .= '<input type="hidden" name="' . $key . '" value="' . $value . '" />' . "\n";
2647    }
2648    else
2649    {
2650        foreach ($value as $_key => $_value)
2651        {
2652            $_key = ($stripslashes) ? stripslashes($_key) : $_key;
2653            $_key = ($specialchar) ? htmlspecialchars($_key, ENT_COMPAT, 'UTF-8') : $_key;
2654
2655            $hidden_fields .= _build_hidden_fields($key . '[' . $_key . ']', $_value, $specialchar, $stripslashes);
2656        }
2657    }
2658
2659    return $hidden_fields;
2660}
2661
2662/**
2663* Build simple hidden fields from array
2664*
2665* @param array $field_ary an array of values to build the hidden field from
2666* @param bool $specialchar if true, keys and values get specialchared
2667* @param bool $stripslashes if true, keys and values get stripslashed
2668*
2669* @return string the hidden fields
2670*/
2671function build_hidden_fields($field_ary, $specialchar = false, $stripslashes = false)
2672{
2673    $s_hidden_fields = '';
2674
2675    foreach ($field_ary as $name => $vars)
2676    {
2677        $name = ($stripslashes) ? stripslashes($name) : $name;
2678        $name = ($specialchar) ? htmlspecialchars($name, ENT_COMPAT, 'UTF-8') : $name;
2679
2680        $s_hidden_fields .= _build_hidden_fields($name, $vars, $specialchar, $stripslashes);
2681    }
2682
2683    return $s_hidden_fields;
2684}
2685
2686/**
2687* Return a nicely formatted backtrace.
2688*
2689* Turns the array returned by debug_backtrace() into HTML markup.
2690* Also filters out absolute paths to phpBB root.
2691*
2692* @return string    HTML markup
2693*/
2694function get_backtrace()
2695{
2696    $output = '<div style="font-family: monospace;">';
2697    $backtrace = debug_backtrace();
2698
2699    // We skip the first one, because it only shows this file/function
2700    unset($backtrace[0]);
2701
2702    foreach ($backtrace as $trace)
2703    {
2704        // Strip the current directory from path
2705        $trace['file'] = (empty($trace['file'])) ? '(not given by php)' : htmlspecialchars(phpbb_filter_root_path($trace['file']), ENT_COMPAT);
2706        $trace['line'] = (empty($trace['line'])) ? '(not given by php)' : $trace['line'];
2707
2708        // Only show function arguments for include etc.
2709        // Other parameters may contain sensible information
2710        $argument = '';
2711        if (!empty($trace['args'][0]) && in_array($trace['function'], array('include', 'require', 'include_once', 'require_once')))
2712        {
2713            $argument = htmlspecialchars(phpbb_filter_root_path($trace['args'][0]), ENT_COMPAT);
2714        }
2715
2716        $trace['class'] = (!isset($trace['class'])) ? '' : $trace['class'];
2717        $trace['type'] = (!isset($trace['type'])) ? '' : $trace['type'];
2718
2719        $output .= '<br />';
2720        $output .= '<b>FILE:</b> ' . $trace['file'] . '<br />';
2721        $output .= '<b>LINE:</b> ' . ((!empty($trace['line'])) ? $trace['line'] : '') . '<br />';
2722
2723        $output .= '<b>CALL:</b> ' . htmlspecialchars($trace['class'] . $trace['type'] . $trace['function'], ENT_COMPAT);
2724        $output .= '(' . (($argument !== '') ? "'$argument'" : '') . ')<br />';
2725    }
2726    $output .= '</div>';
2727    return $output;
2728}
2729
2730/**
2731* This function returns a regular expression pattern for commonly used expressions
2732* Use with / as delimiter for email mode and # for url modes
2733* mode can be: email|bbcode_htm|url|url_inline|www_url|www_url_inline|relative_url|relative_url_inline|ipv4|ipv6
2734*/
2735function get_preg_expression($mode)
2736{
2737    switch ($mode)
2738    {
2739        case 'email':
2740            // Regex written by James Watts and Francisco Jose Martin Moreno
2741            // http://fightingforalostcause.net/misc/2006/compare-email-regex.php
2742            return '((?:[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*(?:[\w\!\#$\%\'\*\+\-\/\=\?\^\`{\|\}\~]|&amp;)+)@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,63})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)';
2743        break;
2744
2745        case 'bbcode_htm':
2746            return array(
2747                '#<!\-\- e \-\-><a href="mailto:(.*?)">.*?</a><!\-\- e \-\->#',
2748                '#<!\-\- l \-\-><a (?:class="[\w-]+" )?href="(.*?)(?:(&amp;|\?)sid=[0-9a-f]{32})?">.*?</a><!\-\- l \-\->#',
2749                '#<!\-\- ([mw]) \-\-><a (?:class="[\w-]+" )?href="http://(.*?)">\2</a><!\-\- \1 \-\->#',
2750                '#<!\-\- ([mw]) \-\-><a (?:class="[\w-]+" )?href="(.*?)">.*?</a><!\-\- \1 \-\->#',
2751                '#<!\-\- s(.*?) \-\-><img src="\{SMILIES_PATH\}\/.*? \/><!\-\- s\1 \-\->#',
2752                '#<!\-\- .*? \-\->#s',
2753                '#<.*?>#s',
2754            );
2755        break;
2756
2757        // Whoa these look impressive!
2758        // The code to generate the following two regular expressions which match valid IPv4/IPv6 addresses
2759        // can be found in the develop directory
2760
2761        // @deprecated
2762        case 'ipv4':
2763            return '#^(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$#';
2764        break;
2765
2766        // @deprecated
2767        case 'ipv6':
2768            return '#^(?:(?:(?:[\dA-F]{1,4}:){6}(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:::(?:[\dA-F]{1,4}:){0,5}(?:[\dA-F]{1,4}(?::[\dA-F]{1,4})?|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:):(?:[\dA-F]{1,4}:){4}(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:){1,2}:(?:[\dA-F]{1,4}:){3}(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:){1,3}:(?:[\dA-F]{1,4}:){2}(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:){1,4}:(?:[\dA-F]{1,4}:)(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:){1,5}:(?:[\dA-F]{1,4}:[\dA-F]{1,4}|(?:(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])))|(?:(?:[\dA-F]{1,4}:){1,6}:[\dA-F]{1,4})|(?:(?:[\dA-F]{1,4}:){1,7}:)|(?:::))$#i';
2769        break;
2770
2771        case 'url':
2772            // generated with regex_idn.php file in the develop folder
2773            return "[a-z][a-z\d+\-.]*(?<!javascript):/{2}(?:(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})+|[0-9.]+|\[[a-z0-9.]+:[a-z0-9.]+:[a-z0-9.:]+\])(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2774        break;
2775
2776        case 'url_http':
2777            // generated with regex_idn.php file in the develop folder
2778            return "http[s]?:/{2}(?:(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})+|[0-9.]+|\[[a-z0-9.]+:[a-z0-9.]+:[a-z0-9.:]+\])(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2779        break;
2780
2781        case 'url_inline':
2782            // generated with regex_idn.php file in the develop folder
2783            return "[a-z][a-z\d+]*(?<!javascript):/{2}(?:(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})+|[0-9.]+|\[[a-z0-9.]+:[a-z0-9.]+:[a-z0-9.:]+\])(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2784        break;
2785
2786        case 'www_url':
2787            // generated with regex_idn.php file in the develop folder
2788            return "www\.(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})+(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2789        break;
2790
2791        case 'www_url_inline':
2792            // generated with regex_idn.php file in the develop folder
2793            return "www\.(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})+(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2794        break;
2795
2796        case 'relative_url':
2797            // generated with regex_idn.php file in the develop folder
2798            return "(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})*(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'()*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2799        break;
2800
2801        case 'relative_url_inline':
2802            // generated with regex_idn.php file in the develop folder
2803            return "(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?";
2804        break;
2805
2806        case 'table_prefix':
2807            return '#^[a-zA-Z][a-zA-Z0-9_]*$#';
2808        break;
2809
2810        // Matches the predecing dot
2811        case 'path_remove_dot_trailing_slash':
2812            return '#^(?:(\.)?)+(?:(.+)?)+(?:([\\/\\\])$)#';
2813        break;
2814
2815        case 'semantic_version':
2816            // Regular expression to match semantic versions by http://rgxdb.com/
2817            return '/(?<=^[Vv]|^)(?:(?<major>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<minor>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<patch>(?:0|[1-9](?:(?:0|[1-9])+)*))(?:-(?<prerelease>(?:(?:(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?|(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?)|(?:0|[1-9](?:(?:0|[1-9])+)*))(?:[.](?:(?:(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?|(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?)|(?:0|[1-9](?:(?:0|[1-9])+)*)))*))?(?:[+](?<build>(?:(?:(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?|(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?)|(?:(?:0|[1-9])+))(?:[.](?:(?:(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?|(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)(?:[A-Za-z]|-)(?:(?:(?:0|[1-9])|(?:[A-Za-z]|-))+)?)|(?:(?:0|[1-9])+)))*))?)$/';
2818        break;
2819    }
2820
2821    return '';
2822}
2823
2824/**
2825* Generate regexp for naughty words censoring
2826* Depends on whether installed PHP version supports unicode properties
2827*
2828* @param string    $word            word template to be replaced
2829*
2830* @return string $preg_expr        regex to use with word censor
2831*/
2832function get_censor_preg_expression($word)
2833{
2834    // Unescape the asterisk to simplify further conversions
2835    $word = str_replace('\*', '*', preg_quote($word, '#'));
2836
2837    // Replace asterisk(s) inside the pattern, at the start and at the end of it with regexes
2838    $word = preg_replace(array('#(?<=[\p{Nd}\p{L}_])\*+(?=[\p{Nd}\p{L}_])#iu', '#^\*+#', '#\*+$#'), array('([\x20]*?|[\p{Nd}\p{L}_-]*?)', '[\p{Nd}\p{L}_-]*?', '[\p{Nd}\p{L}_-]*?'), $word);
2839
2840    // Generate the final substitution
2841    $preg_expr = '#(?<![\p{Nd}\p{L}_-])(' . $word . ')(?![\p{Nd}\p{L}_-])#iu';
2842
2843    return $preg_expr;
2844}
2845
2846/**
2847* Returns the first block of the specified IPv6 address and as many additional
2848* ones as specified in the length parameter.
2849* If length is zero, then an empty string is returned.
2850* If length is greater than 3 the complete IP will be returned
2851*/
2852function short_ipv6($ip, $length)
2853{
2854    if ($length < 1)
2855    {
2856        return '';
2857    }
2858
2859    // Handle IPv4 embedded IPv6 addresses
2860    if (preg_match('/(?:\d{1,3}\.){3}\d{1,3}$/i', $ip))
2861    {
2862        $binary_ip = inet_pton($ip);
2863        $ip_v6 = $binary_ip ? inet_ntop($binary_ip) : $ip;
2864        $ip = $ip_v6 ?: $ip;
2865    }
2866
2867    // extend IPv6 addresses
2868    $blocks = substr_count($ip, ':') + 1;
2869    if ($blocks < 9)
2870    {
2871        $ip = str_replace('::', ':' . str_repeat('0000:', 9 - $blocks), $ip);
2872    }
2873    if ($ip[0] == ':')
2874    {
2875        $ip = '0000' . $ip;
2876    }
2877    if ($length < 4)
2878    {
2879        $ip = implode(':', array_slice(explode(':', $ip), 0, 1 + $length));
2880    }
2881
2882    return $ip;
2883}
2884
2885/**
2886* Normalises an internet protocol address,
2887* also checks whether the specified address is valid.
2888*
2889* IPv4 addresses are returned 'as is'.
2890*
2891* IPv6 addresses are normalised according to
2892*    A Recommendation for IPv6 Address Text Representation
2893*    http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-07
2894*
2895* @param string $address    IP address
2896*
2897* @return string|false    false if specified address is not valid,
2898*                        string otherwise
2899*/
2900function phpbb_ip_normalise(string $address)
2901{
2902    $ip_normalised = false;
2903
2904    if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
2905    {
2906        $ip_normalised = $address;
2907    }
2908    else if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
2909    {
2910        $ip_normalised = inet_ntop(inet_pton($address));
2911
2912        // If is ipv4
2913        if (stripos($ip_normalised, '::ffff:') === 0)
2914        {
2915            $ip_normalised = substr($ip_normalised, 7);
2916        }
2917    }
2918
2919    return $ip_normalised;
2920}
2921
2922// Handler, header and footer
2923
2924/**
2925* Error and message handler, call with trigger_error if read
2926* @return bool true to bypass internal error handler, false otherwise
2927*/
2928function msg_handler($errno, $msg_text, $errfile, $errline): bool
2929{
2930    global $cache, $db, $auth, $template, $config, $user, $request;
2931    global $phpbb_root_path, $msg_title, $msg_long_text, $phpbb_log;
2932    global $phpbb_container;
2933
2934    // https://www.php.net/manual/en/language.operators.errorcontrol.php
2935    // error_reporting() return a different error code inside the error handler after php 8.0
2936    $suppresed = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE;
2937    if (PHP_VERSION_ID < 80000)
2938    {
2939        $suppresed = 0;
2940    }
2941
2942    // Do not display notices if we suppress them via @
2943    if (error_reporting() == $suppresed && $errno != E_USER_ERROR && $errno != E_USER_WARNING && $errno != E_USER_NOTICE)
2944    {
2945        return true;
2946    }
2947
2948    // Message handler is stripping text. In case we need it, we are possible to define long text...
2949    if (isset($msg_long_text) && $msg_long_text && !$msg_text)
2950    {
2951        $msg_text = $msg_long_text;
2952    }
2953
2954    switch ($errno)
2955    {
2956        case E_NOTICE:
2957        case E_WARNING:
2958
2959            // Check the error reporting level and return if the error level does not match
2960            // If DEBUG is defined the default level is E_ALL
2961            if (($errno & ($phpbb_container != null && $phpbb_container->getParameter('debug.show_errors') ? E_ALL : error_reporting())) == 0)
2962            {
2963                return true;
2964            }
2965
2966            if (strpos($errfile, 'cache') === false && strpos($errfile, 'template.') === false)
2967            {
2968                $errfile = phpbb_filter_root_path($errfile);
2969                $msg_text = phpbb_filter_root_path($msg_text);
2970                $error_name = ($errno === E_WARNING) ? 'PHP Warning' : 'PHP Notice';
2971                echo '<b>[phpBB Debug] ' . $error_name . '</b>: in file <b>' . $errfile . '</b> on line <b>' . $errline . '</b>: <b>' . $msg_text . '</b><br />' . "\n";
2972
2973                // we are writing an image - the user won't see the debug, so let's place it in the log
2974                if (defined('IMAGE_OUTPUT') || defined('IN_CRON'))
2975                {
2976                    $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_IMAGE_GENERATION_ERROR', false, array($errfile, $errline, $msg_text));
2977                }
2978                // echo '<br /><br />BACKTRACE<br />' . get_backtrace() . '<br />' . "\n";
2979            }
2980
2981            return true;
2982
2983        break;
2984
2985        case E_USER_ERROR:
2986
2987            if (!empty($user) && $user->is_setup())
2988            {
2989                $msg_text = (!empty($user->lang[$msg_text])) ? $user->lang[$msg_text] : $msg_text;
2990                $msg_title = (!isset($msg_title)) ? $user->lang['GENERAL_ERROR'] : ((!empty($user->lang[$msg_title])) ? $user->lang[$msg_title] : $msg_title);
2991
2992                $l_return_index = sprintf($user->lang['RETURN_INDEX'], '<a href="' . $phpbb_root_path . '">', '</a>');
2993                $l_notify = '';
2994
2995                if (!empty($config['board_contact']))
2996                {
2997                    $l_notify = '<p>' . sprintf($user->lang['NOTIFY_ADMIN_EMAIL'], $config['board_contact']) . '</p>';
2998                }
2999            }
3000            else
3001            {
3002                $msg_title = 'General Error';
3003                $l_return_index = '<a href="' . $phpbb_root_path . '">Return to index page</a>';
3004                $l_notify = '';
3005
3006                if (!empty($config['board_contact']))
3007                {
3008                    $l_notify = '<p>Please notify the board administrator or webmaster: <a href="mailto:' . $config['board_contact'] . '">' . $config['board_contact'] . '</a></p>';
3009                }
3010            }
3011
3012            $log_text = $msg_text;
3013            $backtrace = get_backtrace();
3014            if ($backtrace)
3015            {
3016                $log_text .= '<br /><br />BACKTRACE<br />' . $backtrace;
3017            }
3018
3019            if (defined('IN_INSTALL') || ($phpbb_container != null && $phpbb_container->getParameter('debug.show_errors')) || isset($auth) && $auth->acl_get('a_'))
3020            {
3021                $msg_text = $log_text;
3022
3023                // If this is defined there already was some output
3024                // So let's not break it
3025                if (defined('IN_DB_UPDATE'))
3026                {
3027                    echo '<div class="errorbox">' . $msg_text . '</div>';
3028
3029                    $db->sql_return_on_error(true);
3030                    phpbb_end_update($cache, $config);
3031                }
3032            }
3033
3034            if ((defined('IN_CRON') || defined('IMAGE_OUTPUT')) && isset($db))
3035            {
3036                // let's avoid loops
3037                $db->sql_return_on_error(true);
3038                $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_GENERAL_ERROR', false, array($msg_title, $log_text));
3039                $db->sql_return_on_error(false);
3040            }
3041
3042            // Do not send 200 OK, but service unavailable on errors
3043            send_status_line(503, 'Service Unavailable');
3044
3045            garbage_collection();
3046
3047            // Try to not call the adm page data...
3048
3049            echo '<!DOCTYPE html>';
3050            echo '<html dir="ltr">';
3051            echo '<head>';
3052            echo '<meta charset="utf-8">';
3053            echo '<meta http-equiv="X-UA-Compatible" content="IE=edge">';
3054            echo '<title>' . $msg_title . '</title>';
3055            echo '<style type="text/css">' . "\n" . '/* <![CDATA[ */' . "\n";
3056            echo '* { margin: 0; padding: 0; } html { font-size: 100%; height: 100%; margin-bottom: 1px; background-color: #E4EDF0; } body { font-family: "Lucida Grande", Verdana, Helvetica, Arial, sans-serif; color: #536482; background: #E4EDF0; font-size: 62.5%; margin: 0; } ';
3057            echo 'a:link, a:active, a:visited { color: #006699; text-decoration: none; } a:hover { color: #DD6900; text-decoration: underline; } ';
3058            echo '#wrap { padding: 0 20px 15px 20px; min-width: 615px; } #page-header { text-align: right; height: 40px; } #page-footer { clear: both; font-size: 1em; text-align: center; } ';
3059            echo '.panel { margin: 4px 0; background-color: #FFFFFF; border: solid 1px  #A9B8C2; } ';
3060            echo '#errorpage #page-header a { font-weight: bold; line-height: 6em; } #errorpage #content { padding: 10px; } #errorpage #content h1 { line-height: 1.2em; margin-bottom: 0; color: #DF075C; } ';
3061            echo '#errorpage #content div { margin-top: 20px; margin-bottom: 5px; border-bottom: 1px solid #CCCCCC; padding-bottom: 5px; color: #333333; font: bold 1.2em "Lucida Grande", Arial, Helvetica, sans-serif; text-decoration: none; line-height: 120%; text-align: left; } ';
3062            echo "\n" . '/* ]]> */' . "\n";
3063            echo '</style>';
3064            echo '</head>';
3065            echo '<body id="errorpage">';
3066            echo '<div id="wrap">';
3067            echo '    <div id="page-header">';
3068            echo '        ' . $l_return_index;
3069            echo '    </div>';
3070            echo '    <div id="acp">';
3071            echo '    <div class="panel">';
3072            echo '        <div id="content">';
3073            echo '            <h1>' . $msg_title . '</h1>';
3074
3075            echo '            <div>' . $msg_text . '</div>';
3076
3077            echo $l_notify;
3078
3079            echo '        </div>';
3080            echo '    </div>';
3081            echo '    </div>';
3082            echo '    <div id="page-footer">';
3083            echo '        Powered by <a href="https://www.phpbb.com/">phpBB</a>&reg; Forum Software &copy; phpBB Limited';
3084            echo '    </div>';
3085            echo '</div>';
3086            echo '</body>';
3087            echo '</html>';
3088
3089            exit_handler();
3090
3091            // On a fatal error (and E_USER_ERROR *is* fatal) we never want other scripts to continue and force an exit here.
3092            exit;
3093        break;
3094
3095        case E_USER_WARNING:
3096        case E_USER_NOTICE:
3097
3098            define('IN_ERROR_HANDLER', true);
3099
3100            if (empty($user->data))
3101            {
3102                $user->session_begin();
3103            }
3104
3105            // We re-init the auth array to get correct results on login/logout
3106            $auth->acl($user->data);
3107
3108            if (!$user->is_setup())
3109            {
3110                $user->setup();
3111            }
3112
3113            if ($msg_text == 'ERROR_NO_ATTACHMENT' || $msg_text == 'NO_FORUM' || $msg_text == 'NO_TOPIC' || $msg_text == 'NO_USER')
3114            {
3115                send_status_line(404, 'Not Found');
3116            }
3117
3118            $msg_text = (!empty($user->lang[$msg_text])) ? $user->lang[$msg_text] : $msg_text;
3119            $msg_title = (!isset($msg_title)) ? $user->lang['INFORMATION'] : ((!empty($user->lang[$msg_title])) ? $user->lang[$msg_title] : $msg_title);
3120
3121            if (!defined('HEADER_INC'))
3122            {
3123                if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
3124                {
3125                    adm_page_header($msg_title);
3126                }
3127                else
3128                {
3129                    page_header($msg_title);
3130                }
3131            }
3132
3133            $template->set_filenames(array(
3134                'body' => 'message_body.html')
3135            );
3136
3137            $template->assign_vars(array(
3138                'MESSAGE_TITLE'        => $msg_title,
3139                'MESSAGE_TEXT'        => $msg_text,
3140                'S_USER_WARNING'    => ($errno == E_USER_WARNING) ? true : false,
3141                'S_USER_NOTICE'        => ($errno == E_USER_NOTICE) ? true : false)
3142            );
3143
3144            if ($request->is_ajax())
3145            {
3146                global $refresh_data;
3147
3148                $json_response = new \phpbb\json_response;
3149                $json_response->send(array(
3150                    'MESSAGE_TITLE'        => $msg_title,
3151                    'MESSAGE_TEXT'        => $msg_text,
3152                    'S_USER_WARNING'    => ($errno == E_USER_WARNING) ? true : false,
3153                    'S_USER_NOTICE'        => ($errno == E_USER_NOTICE) ? true : false,
3154                    'REFRESH_DATA'        => (!empty($refresh_data)) ? $refresh_data : null
3155                ));
3156            }
3157
3158            // We do not want the cron script to be called on error messages
3159            define('IN_CRON', true);
3160
3161            if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
3162            {
3163                adm_page_footer();
3164            }
3165            else
3166            {
3167                page_footer();
3168            }
3169
3170            exit_handler();
3171        break;
3172
3173        // PHP4 compatibility
3174        case E_DEPRECATED:
3175            return true;
3176        break;
3177    }
3178
3179    // If we notice an error not handled here we pass this back to PHP by returning false
3180    // This may not work for all php versions
3181    return false;
3182}
3183
3184/**
3185* Removes absolute path to phpBB root directory from error messages
3186* and converts backslashes to forward slashes.
3187*
3188* @param string $errfile    Absolute file path
3189*                            (e.g. /var/www/phpbb3/phpBB/includes/functions.php)
3190*                            Please note that if $errfile is outside of the phpBB root,
3191*                            the root path will not be found and can not be filtered.
3192* @return string            Relative file path
3193*                            (e.g. /includes/functions.php)
3194*/
3195function phpbb_filter_root_path($errfile)
3196{
3197    static $root_path;
3198
3199    if (empty($root_path))
3200    {
3201        $root_path = \phpbb\filesystem\helper::realpath(__DIR__ . '/../');
3202    }
3203
3204    return str_replace(array($root_path, '\\'), array('[ROOT]', '/'), $errfile);
3205}
3206
3207/**
3208* Queries the session table to get information about online guests
3209* @param int $item_id Limits the search to the item with this id
3210* @param string $item The name of the item which is stored in the session table as session_{$item}_id
3211* @return int The number of active distinct guest sessions
3212*/
3213function obtain_guest_count($item_id = 0, $item = 'forum')
3214{
3215    global $db, $config;
3216
3217    if ($item_id)
3218    {
3219        $reading_sql = ' AND s.session_' . $item . '_id = ' . (int) $item_id;
3220    }
3221    else
3222    {
3223        $reading_sql = '';
3224    }
3225    $time = (time() - (intval($config['load_online_time']) * 60));
3226
3227    // Get number of online guests
3228
3229    if ($db->get_sql_layer() === 'sqlite3')
3230    {
3231        $sql = 'SELECT COUNT(session_ip) as num_guests
3232            FROM (
3233                SELECT DISTINCT s.session_ip
3234                FROM ' . SESSIONS_TABLE . ' s
3235                WHERE s.session_user_id = ' . ANONYMOUS . '
3236                    AND s.session_time >= ' . ($time - ((int) ($time % 60))) .
3237                $reading_sql .
3238            ')';
3239    }
3240    else
3241    {
3242        $sql = 'SELECT COUNT(DISTINCT s.session_ip) as num_guests
3243            FROM ' . SESSIONS_TABLE . ' s
3244            WHERE s.session_user_id = ' . ANONYMOUS . '
3245                AND s.session_time >= ' . ($time - ((int) ($time % 60))) .
3246            $reading_sql;
3247    }
3248    $result = $db->sql_query($sql);
3249    $guests_online = (int) $db->sql_fetchfield('num_guests');
3250    $db->sql_freeresult($result);
3251
3252    return $guests_online;
3253}
3254
3255/**
3256* Queries the session table to get information about online users
3257* @param int $item_id Limits the search to the item with this id
3258* @param string $item The name of the item which is stored in the session table as session_{$item}_id
3259* @return array An array containing the ids of online, hidden and visible users, as well as statistical info
3260*/
3261function obtain_users_online($item_id = 0, $item = 'forum')
3262{
3263    global $db, $config;
3264
3265    $reading_sql = '';
3266    if ($item_id !== 0)
3267    {
3268        $reading_sql = ' AND s.session_' . $item . '_id = ' . (int) $item_id;
3269    }
3270
3271    $online_users = array(
3272        'online_users'            => array(),
3273        'hidden_users'            => array(),
3274        'total_online'            => 0,
3275        'visible_online'        => 0,
3276        'hidden_online'            => 0,
3277        'guests_online'            => 0,
3278    );
3279
3280    if ($config['load_online_guests'])
3281    {
3282        $online_users['guests_online'] = obtain_guest_count($item_id, $item);
3283    }
3284
3285    // a little discrete magic to cache this for 30 seconds
3286    $time = (time() - (intval($config['load_online_time']) * 60));
3287
3288    $sql = 'SELECT s.session_user_id, s.session_ip, s.session_viewonline
3289        FROM ' . SESSIONS_TABLE . ' s
3290        WHERE s.session_time >= ' . ($time - ((int) ($time % 30))) .
3291            $reading_sql .
3292        ' AND s.session_user_id <> ' . ANONYMOUS;
3293    $result = $db->sql_query($sql);
3294
3295    while ($row = $db->sql_fetchrow($result))
3296    {
3297        // Skip multiple sessions for one user
3298        if (!isset($online_users['online_users'][$row['session_user_id']]))
3299        {
3300            $online_users['online_users'][$row['session_user_id']] = (int) $row['session_user_id'];
3301            if ($row['session_viewonline'])
3302            {
3303                $online_users['visible_online']++;
3304            }
3305            else
3306            {
3307                $online_users['hidden_users'][$row['session_user_id']] = (int) $row['session_user_id'];
3308                $online_users['hidden_online']++;
3309            }
3310        }
3311    }
3312    $online_users['total_online'] = $online_users['guests_online'] + $online_users['visible_online'] + $online_users['hidden_online'];
3313    $db->sql_freeresult($result);
3314
3315    return $online_users;
3316}
3317
3318/**
3319* Uses the result of obtain_users_online to generate a localized, readable representation.
3320* @param mixed $online_users result of obtain_users_online - array with user_id lists for total, hidden and visible users, and statistics
3321* @param int $item_id Indicate that the data is limited to one item and not global
3322* @param string $item The name of the item which is stored in the session table as session_{$item}_id
3323* @return array An array containing the string for output to the template
3324*/
3325function obtain_users_online_string($online_users, $item_id = 0, $item = 'forum')
3326{
3327    global $config, $db, $user, $auth, $phpbb_dispatcher;
3328
3329    $user_online_link = $rowset = array();
3330    // Need caps version of $item for language-strings
3331    $item_caps = strtoupper($item);
3332
3333    if (count($online_users['online_users']))
3334    {
3335        $sql_ary = array(
3336            'SELECT'    => 'u.username, u.username_clean, u.user_id, u.user_type, u.user_allow_viewonline, u.user_colour',
3337            'FROM'        => array(
3338                USERS_TABLE    => 'u',
3339            ),
3340            'WHERE'        => $db->sql_in_set('u.user_id', $online_users['online_users']),
3341            'ORDER_BY'    => 'u.username_clean ASC',
3342        );
3343
3344        /**
3345        * Modify SQL query to obtain online users data
3346        *
3347        * @event core.obtain_users_online_string_sql
3348        * @var    array    online_users    Array with online users data
3349        *                                from obtain_users_online()
3350        * @var    int        item_id            Restrict online users to item id
3351        * @var    string    item            Restrict online users to a certain
3352        *                                session item, e.g. forum for
3353        *                                session_forum_id
3354        * @var    array    sql_ary            SQL query array to obtain users online data
3355        * @since 3.1.4-RC1
3356        * @changed 3.1.7-RC1            Change sql query into array and adjust var accordingly. Allows extension authors the ability to adjust the sql_ary.
3357        */
3358        $vars = array('online_users', 'item_id', 'item', 'sql_ary');
3359        extract($phpbb_dispatcher->trigger_event('core.obtain_users_online_string_sql', compact($vars)));
3360
3361        $result = $db->sql_query($db->sql_build_query('SELECT', $sql_ary));
3362        $rowset = $db->sql_fetchrowset($result);
3363        $db->sql_freeresult($result);
3364
3365        foreach ($rowset as $row)
3366        {
3367            // User is logged in and therefore not a guest
3368            if ($row['user_id'] != ANONYMOUS)
3369            {
3370                if (isset($online_users['hidden_users'][$row['user_id']]))
3371                {
3372                    $row['username'] = '<em>' . $row['username'] . '</em>';
3373                }
3374
3375                if (!isset($online_users['hidden_users'][$row['user_id']]) || $auth->acl_get('u_viewonline') || $row['user_id'] === $user->data['user_id'])
3376                {
3377                    $user_online_link[$row['user_id']] = get_username_string(($row['user_type'] <> USER_IGNORE) ? 'full' : 'no_profile', $row['user_id'], $row['username'], $row['user_colour']);
3378                }
3379            }
3380        }
3381    }
3382
3383    /**
3384    * Modify online userlist data
3385    *
3386    * @event core.obtain_users_online_string_before_modify
3387    * @var    array    online_users        Array with online users data
3388    *                                    from obtain_users_online()
3389    * @var    int        item_id                Restrict online users to item id
3390    * @var    string    item                Restrict online users to a certain
3391    *                                    session item, e.g. forum for
3392    *                                    session_forum_id
3393    * @var    array    rowset                Array with online users data
3394    * @var    array    user_online_link    Array with online users items (usernames)
3395    * @since 3.1.10-RC1
3396    */
3397    $vars = array(
3398        'online_users',
3399        'item_id',
3400        'item',
3401        'rowset',
3402        'user_online_link',
3403    );
3404    extract($phpbb_dispatcher->trigger_event('core.obtain_users_online_string_before_modify', compact($vars)));
3405
3406    $online_userlist = implode(', ', $user_online_link);
3407
3408    if (!$online_userlist)
3409    {
3410        $online_userlist = $user->lang['NO_ONLINE_USERS'];
3411    }
3412
3413    if ($item_id === 0)
3414    {
3415        $online_userlist = $user->lang['REGISTERED_USERS'] . ' ' . $online_userlist;
3416    }
3417    else if ($config['load_online_guests'])
3418    {
3419        $online_userlist = $user->lang('BROWSING_' . $item_caps . '_GUESTS', $online_users['guests_online'], $online_userlist);
3420    }
3421    else
3422    {
3423        $online_userlist = sprintf($user->lang['BROWSING_' . $item_caps], $online_userlist);
3424    }
3425    // Build online listing
3426    $visible_online = $user->lang('REG_USERS_TOTAL', (int) $online_users['visible_online']);
3427    $hidden_online = $user->lang('HIDDEN_USERS_TOTAL', (int) $online_users['hidden_online']);
3428
3429    if ($config['load_online_guests'])
3430    {
3431        $guests_online = $user->lang('GUEST_USERS_TOTAL', (int) $online_users['guests_online']);
3432        $l_online_users = $user->lang('ONLINE_USERS_TOTAL_GUESTS', (int) $online_users['total_online'], $visible_online, $hidden_online, $guests_online);
3433    }
3434    else
3435    {
3436        $l_online_users = $user->lang('ONLINE_USERS_TOTAL', (int) $online_users['total_online'], $visible_online, $hidden_online);
3437    }
3438
3439    /**
3440    * Modify online userlist data
3441    *
3442    * @event core.obtain_users_online_string_modify
3443    * @var    array    online_users        Array with online users data
3444    *                                    from obtain_users_online()
3445    * @var    int        item_id                Restrict online users to item id
3446    * @var    string    item                Restrict online users to a certain
3447    *                                    session item, e.g. forum for
3448    *                                    session_forum_id
3449    * @var    array    rowset                Array with online users data
3450    * @var    array    user_online_link    Array with online users items (usernames)
3451    * @var    string    online_userlist        String containing users online list
3452    * @var    string    l_online_users        String with total online users count info
3453    * @since 3.1.4-RC1
3454    */
3455    $vars = array(
3456        'online_users',
3457        'item_id',
3458        'item',
3459        'rowset',
3460        'user_online_link',
3461        'online_userlist',
3462        'l_online_users',
3463    );
3464    extract($phpbb_dispatcher->trigger_event('core.obtain_users_online_string_modify', compact($vars)));
3465
3466    return array(
3467        'online_userlist'    => $online_userlist,
3468        'l_online_users'    => $l_online_users,
3469    );
3470}
3471
3472/**
3473* Get option bitfield from custom data
3474*
3475* @param int    $bit        The bit/value to get
3476* @param int    $data        Current bitfield to check
3477* @return bool    Returns true if value of constant is set in bitfield, else false
3478*/
3479function phpbb_optionget($bit, $data)
3480{
3481    return ($data & 1 << (int) $bit) ? true : false;
3482}
3483
3484/**
3485* Set option bitfield
3486*
3487* @param int    $bit        The bit/value to set/unset
3488* @param bool    $set        True if option should be set, false if option should be unset.
3489* @param int    $data        Current bitfield to change
3490*
3491* @return int    The new bitfield
3492*/
3493function phpbb_optionset($bit, $set, $data)
3494{
3495    if ($set && !($data & 1 << $bit))
3496    {
3497        $data += 1 << $bit;
3498    }
3499    else if (!$set && ($data & 1 << $bit))
3500    {
3501        $data -= 1 << $bit;
3502    }
3503
3504    return $data;
3505}
3506
3507
3508/**
3509* Escapes and quotes a string for use as an HTML/XML attribute value.
3510*
3511* This is a port of Python xml.sax.saxutils quoteattr.
3512*
3513* The function will attempt to choose a quote character in such a way as to
3514* avoid escaping quotes in the string. If this is not possible the string will
3515* be wrapped in double quotes and double quotes will be escaped.
3516*
3517* @param string $data The string to be escaped
3518* @param array $entities Associative array of additional entities to be escaped
3519* @return string Escaped and quoted string
3520*/
3521function phpbb_quoteattr($data, $entities = null)
3522{
3523    $data = str_replace('&', '&amp;', $data);
3524    $data = str_replace('>', '&gt;', $data);
3525    $data = str_replace('<', '&lt;', $data);
3526
3527    $data = str_replace("\n", '&#10;', $data);
3528    $data = str_replace("\r", '&#13;', $data);
3529    $data = str_replace("\t", '&#9;', $data);
3530
3531    if (!empty($entities))
3532    {
3533        $data = str_replace(array_keys($entities), array_values($entities), $data);
3534    }
3535
3536    if (strpos($data, '"') !== false)
3537    {
3538        if (strpos($data, "'") !== false)
3539        {
3540            $data = '"' . str_replace('"', '&quot;', $data) . '"';
3541        }
3542        else
3543        {
3544            $data = "'" . $data . "'";
3545        }
3546    }
3547    else
3548    {
3549        $data = '"' . $data . '"';
3550    }
3551
3552    return $data;
3553}
3554
3555/**
3556* Generate page header
3557*/
3558function page_header($page_title = '', $display_online_list = false, $item_id = 0, $item = 'forum', $send_headers = true)
3559{
3560    global $db, $config, $template, $SID, $_SID, $_EXTRA_URL, $user, $auth, $phpEx, $phpbb_root_path;
3561    global $phpbb_dispatcher, $request, $phpbb_container, $phpbb_admin_path;
3562
3563    if (defined('HEADER_INC'))
3564    {
3565        return;
3566    }
3567
3568    define('HEADER_INC', true);
3569
3570    // A listener can set this variable to `true` when it overrides this function
3571    $page_header_override = false;
3572
3573    /**
3574    * Execute code and/or overwrite page_header()
3575    *
3576    * @event core.page_header
3577    * @var    string    page_title            Page title
3578    * @var    bool    display_online_list        Do we display online users list
3579    * @var    string    item                Restrict online users to a certain
3580    *                                    session item, e.g. forum for
3581    *                                    session_forum_id
3582    * @var    int        item_id                Restrict online users to item id
3583    * @var    bool    page_header_override    Shall we return instead of running
3584    *                                        the rest of page_header()
3585    * @since 3.1.0-a1
3586    */
3587    $vars = array('page_title', 'display_online_list', 'item_id', 'item', 'page_header_override');
3588    extract($phpbb_dispatcher->trigger_event('core.page_header', compact($vars)));
3589
3590    if ($page_header_override)
3591    {
3592        return;
3593    }
3594
3595    // gzip_compression
3596    if ($config['gzip_compress'])
3597    {
3598        // to avoid partially compressed output resulting in blank pages in
3599        // the browser or error messages, compression is disabled in a few cases:
3600        //
3601        // 1) if headers have already been sent, this indicates plaintext output
3602        //    has been started so further content must not be compressed
3603        // 2) the length of the current output buffer is non-zero. This means
3604        //    there is already some uncompressed content in this output buffer
3605        //    so further output must not be compressed
3606        // 3) if more than one level of output buffering is used because we
3607        //    cannot test all output buffer level content lengths. One level
3608        //    could be caused by php.ini output_buffering. Anything
3609        //    beyond that is manual, so the code wrapping phpBB in output buffering
3610        //    can easily compress the output itself.
3611        //
3612        if (@extension_loaded('zlib') && !headers_sent() && ob_get_level() <= 1 && ob_get_length() == 0)
3613        {
3614            ob_start('ob_gzhandler');
3615        }
3616    }
3617
3618    $user->update_session_infos();
3619
3620    // Generate logged in/logged out status
3621    if ($user->data['user_id'] != ANONYMOUS)
3622    {
3623        $u_login_logout = append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=logout&amp;hash=' . generate_link_hash('ucp_logout'));
3624        $l_login_logout = $user->lang['LOGOUT'];
3625    }
3626    else
3627    {
3628        $redirect = $request->variable('redirect', rawurlencode($user->page['page']));
3629        $u_login_logout = append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=login&amp;redirect=' . $redirect);
3630        $l_login_logout = $user->lang['LOGIN'];
3631    }
3632
3633    // Last visit date/time
3634    $s_last_visit = ($user->data['user_id'] != ANONYMOUS) ? $user->format_date($user->data['session_last_visit']) : '';
3635
3636    // Get users online list ... if required
3637    $l_online_users = $online_userlist = $l_online_record = $l_online_time = '';
3638
3639    if ($config['load_online'] && $config['load_online_time'] && $display_online_list)
3640    {
3641        /**
3642        * Load online data:
3643        * For obtaining another session column use $item and $item_id in the function-parameter, whereby the column is session_{$item}_id.
3644        */
3645        $item_id = max($item_id, 0);
3646
3647        $online_users = obtain_users_online($item_id, $item);
3648        $user_online_strings = obtain_users_online_string($online_users, $item_id, $item);
3649
3650        $l_online_users = $user_online_strings['l_online_users'];
3651        $online_userlist = $user_online_strings['online_userlist'];
3652        $total_online_users = $online_users['total_online'];
3653
3654        if ($total_online_users > $config['record_online_users'])
3655        {
3656            $config->set('record_online_users', $total_online_users, false);
3657            $config->set('record_online_date', time(), false);
3658        }
3659
3660        $l_online_record = $user->lang('RECORD_ONLINE_USERS', (int) $config['record_online_users'], $user->format_date($config['record_online_date'], false, true));
3661
3662        $l_online_time = $user->lang('VIEW_ONLINE_TIMES', (int) $config['load_online_time']);
3663    }
3664
3665    $s_privmsg_new = false;
3666
3667    // Check for new private messages if user is logged in
3668    if (!empty($user->data['is_registered']))
3669    {
3670        if ($user->data['user_new_privmsg'])
3671        {
3672            if (!$user->data['user_last_privmsg'] || $user->data['user_last_privmsg'] > $user->data['session_last_visit'])
3673            {
3674                $sql = 'UPDATE ' . USERS_TABLE . '
3675                    SET user_last_privmsg = ' . $user->data['session_last_visit'] . '
3676                    WHERE user_id = ' . $user->data['user_id'];
3677                $db->sql_query($sql);
3678
3679                $s_privmsg_new = true;
3680            }
3681            else
3682            {
3683                $s_privmsg_new = false;
3684            }
3685        }
3686        else
3687        {
3688            $s_privmsg_new = false;
3689        }
3690    }
3691
3692    // Negative forum and topic IDs are not allowed
3693    $forum_id = max(0, $request->variable('f', 0));
3694    $topic_id = max(0, $request->variable('t', 0));
3695
3696    $s_feed_news = false;
3697
3698    // Get option for news
3699    if ($config['feed_enable'])
3700    {
3701        $sql = 'SELECT forum_id
3702            FROM ' . FORUMS_TABLE . '
3703            WHERE ' . $db->sql_bit_and('forum_options', FORUM_OPTION_FEED_NEWS, '<> 0');
3704        $result = $db->sql_query_limit($sql, 1, 0, 600);
3705        $s_feed_news = (int) $db->sql_fetchfield('forum_id');
3706        $db->sql_freeresult($result);
3707    }
3708
3709    // This path is sent with the base template paths in the assign_vars()
3710    // call below. We need to correct it in case we are accessing from a
3711    // controller because the web paths will be incorrect otherwise.
3712    /* @var $phpbb_path_helper \phpbb\path_helper */
3713    $phpbb_path_helper = $phpbb_container->get('path_helper');
3714    $web_path = $phpbb_path_helper->get_web_root_path();
3715
3716    // Send a proper content-language to the output
3717    $user_lang = $user->lang('USER_LANG');
3718    if (strpos($user_lang, '-x-') !== false)
3719    {
3720        $user_lang = substr($user_lang, 0, strpos($user_lang, '-x-'));
3721    }
3722
3723    $s_search_hidden_fields = array();
3724    if ($_SID)
3725    {
3726        $s_search_hidden_fields['sid'] = $_SID;
3727    }
3728
3729    if (!empty($_EXTRA_URL))
3730    {
3731        foreach ($_EXTRA_URL as $url_param)
3732        {
3733            $url_param = explode('=', $url_param, 2);
3734            $s_search_hidden_fields[$url_param[0]] = $url_param[1];
3735        }
3736    }
3737
3738    $dt = $user->create_datetime();
3739    $timezone_offset = $user->lang(array('timezones', 'UTC_OFFSET'), phpbb_format_timezone_offset($dt->getOffset()));
3740    $timezone_name = $user->timezone->getName();
3741    if (isset($user->lang['timezones'][$timezone_name]))
3742    {
3743        $timezone_name = $user->lang['timezones'][$timezone_name];
3744    }
3745
3746    /** @var \phpbb\controller\helper $controller_helper */
3747    $controller_helper = $phpbb_container->get('controller.helper');
3748
3749    // Output the notifications
3750    $notifications = false;
3751    if ($config['load_notifications'] && $config['allow_board_notifications'] && $user->data['user_id'] != ANONYMOUS && $user->data['user_type'] != USER_IGNORE)
3752    {
3753        /* @var $phpbb_notifications \phpbb\notification\manager */
3754        $phpbb_notifications = $phpbb_container->get('notification_manager');
3755
3756        $notifications = $phpbb_notifications->load_notifications('notification.method.board', array(
3757            'all_unread'    => true,
3758            'limit'            => 5,
3759        ));
3760
3761        foreach ($notifications['notifications'] as $notification)
3762        {
3763            $template->assign_block_vars('notifications', $notification->prepare_for_display());
3764        }
3765
3766        // Assign web push template vars globally (if not done already by ucp_notifications) for the dropdown subscribe button
3767        if ($config['webpush_enable'] && $config['webpush_dropdown_subscribe']
3768            && $template->retrieve_var('NOTIFICATIONS_WEBPUSH_ENABLE') === null)
3769        {
3770            $methods = $phpbb_notifications->get_subscription_methods();
3771            $webpush = $methods['notification.method.webpush'] ?? null;
3772
3773            if ($webpush)
3774            {
3775                $form_helper = $phpbb_container->get('form_helper');
3776                $template->assign_vars($webpush['method']->get_ucp_template_data($controller_helper, $form_helper));
3777            }
3778        }
3779    }
3780
3781    $notification_mark_hash = generate_link_hash('mark_all_notifications_read');
3782
3783    $phpbb_version_parts = explode('.', PHPBB_VERSION, 3);
3784    $phpbb_major = $phpbb_version_parts[0] . '.' . $phpbb_version_parts[1];
3785
3786    $s_login_redirect = build_hidden_fields(array('redirect' => $phpbb_path_helper->remove_web_root_path(build_url())));
3787
3788    // Add form token for login box, in case page is presenting a login form.
3789    add_form_key('login', '_LOGIN');
3790
3791    /** @var \phpbb\avatar\helper $avatar_helper */
3792    $avatar_helper = $phpbb_container->get('avatar.helper');
3793
3794    $avatar = $avatar_helper->get_user_avatar($user->data);
3795    $template->assign_vars($avatar_helper->get_template_vars($avatar, 'CURRENT_USER_'));
3796
3797    // The following assigns all _common_ variables that may be used at any point in a template.
3798    $template->assign_vars(array(
3799        'SITENAME'                        => $config['sitename'],
3800        'SITE_DESCRIPTION'                => $config['site_desc'],
3801        'PAGE_TITLE'                    => $page_title,
3802        'SCRIPT_NAME'                    => str_replace('.' . $phpEx, '', $user->page['page_name']),
3803        'LAST_VISIT_DATE'                => sprintf($user->lang['YOU_LAST_VISIT'], $s_last_visit),
3804        'LAST_VISIT_YOU'                => $s_last_visit,
3805        'CURRENT_TIME'                    => sprintf($user->lang['CURRENT_TIME'], $user->format_date(time(), false, true)),
3806        'TOTAL_USERS_ONLINE'            => $l_online_users,
3807        'LOGGED_IN_USER_LIST'            => $online_userlist,
3808        'RECORD_USERS'                    => $l_online_record,
3809
3810        'NO_AVATAR_SOURCE'                => $avatar_helper->get_no_avatar_source(),
3811        'PRIVATE_MESSAGE_COUNT'            => (!empty($user->data['user_unread_privmsg'])) ? $user->data['user_unread_privmsg'] : 0,
3812        'CURRENT_USERNAME_SIMPLE'        => get_username_string('no_profile', $user->data['user_id'], $user->data['username'], $user->data['user_colour']),
3813        'CURRENT_USERNAME_FULL'            => get_username_string('full', $user->data['user_id'], $user->data['username'], $user->data['user_colour']),
3814        'CURRENT_USER_GROUP_COLOR'        => $user->data['user_colour'],
3815        'UNREAD_NOTIFICATIONS_COUNT'    => ($notifications !== false) ? $notifications['unread_count'] : '',
3816        'NOTIFICATIONS_COUNT'            => ($notifications !== false) ? $notifications['unread_count'] : '',
3817        'U_VIEW_ALL_NOTIFICATIONS'        => append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=ucp_notifications'),
3818        'U_MARK_ALL_NOTIFICATIONS'        => append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=ucp_notifications&amp;mode=notification_list&amp;mark=all&amp;token=' . $notification_mark_hash),
3819        'U_NOTIFICATION_SETTINGS'        => append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=ucp_notifications&amp;mode=notification_options'),
3820        'S_NOTIFICATIONS_DISPLAY'        => $config['load_notifications'] && $config['allow_board_notifications'],
3821
3822        'S_USER_NEW_PRIVMSG'            => $user->data['user_new_privmsg'],
3823        'S_USER_UNREAD_PRIVMSG'            => $user->data['user_unread_privmsg'],
3824        'S_USER_NEW'                    => $user->data['user_new'],
3825
3826        'SID'                => $SID,
3827        '_SID'                => $_SID,
3828        'SESSION_ID'        => $user->session_id,
3829        'ROOT_PATH'            => $web_path,
3830        'BOARD_URL'            => generate_board_url() . '/',
3831        'PHPBB_VERSION'        => PHPBB_VERSION,
3832        'PHPBB_MAJOR'        => $phpbb_major,
3833
3834        'L_LOGIN_LOGOUT'    => $l_login_logout,
3835        'L_INDEX'            => ($config['board_index_text'] !== '') ? $config['board_index_text'] : $user->lang['FORUM_INDEX'],
3836        'L_SITE_HOME'        => ($config['site_home_text'] !== '') ? $config['site_home_text'] : $user->lang['HOME'],
3837        'L_ONLINE_EXPLAIN'    => $l_online_time,
3838        'L_RECAPTCHA_LANG'    => $user->lang('RECAPTCHA_LANG'),
3839
3840        'U_PRIVATEMSGS'            => append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=pm&amp;folder=inbox'),
3841        'U_RETURN_INBOX'        => append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=pm&amp;folder=inbox'),
3842        'U_MEMBERLIST'            => append_sid("{$phpbb_root_path}memberlist.$phpEx"),
3843        'U_VIEWONLINE'            => ($auth->acl_gets('u_viewprofile', 'a_user', 'a_useradd', 'a_userdel')) ? append_sid("{$phpbb_root_path}viewonline.$phpEx") : '',
3844        'U_LOGIN_LOGOUT'        => $u_login_logout,
3845        'U_INDEX'                => append_sid("{$phpbb_root_path}index.$phpEx"),
3846        'U_SEARCH'                => append_sid("{$phpbb_root_path}search.$phpEx"),
3847        'U_SITE_HOME'            => $config['site_home_url'],
3848        'U_REGISTER'            => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=register'),
3849        'U_PROFILE'                => append_sid("{$phpbb_root_path}ucp.$phpEx"),
3850        'U_USER_PROFILE'        => get_username_string('profile', $user->data['user_id'], $user->data['username'], $user->data['user_colour']),
3851        'U_MODCP'                => append_sid("{$phpbb_root_path}mcp.$phpEx"),
3852        'U_FAQ'                    => $controller_helper->route('phpbb_help_faq_controller'),
3853        'U_SEARCH_SELF'            => append_sid("{$phpbb_root_path}search.$phpEx", 'search_id=egosearch'),
3854        'U_SEARCH_NEW'            => append_sid("{$phpbb_root_path}search.$phpEx", 'search_id=newposts'),
3855        'U_SEARCH_UNANSWERED'    => append_sid("{$phpbb_root_path}search.$phpEx", 'search_id=unanswered'),
3856        'U_SEARCH_UNREAD'        => append_sid("{$phpbb_root_path}search.$phpEx", 'search_id=unreadposts'),
3857        'U_SEARCH_ACTIVE_TOPICS'=> append_sid("{$phpbb_root_path}search.$phpEx", 'search_id=active_topics'),
3858        'U_DELETE_COOKIES'        => $controller_helper->route('phpbb_ucp_delete_cookies_controller'),
3859        'U_CONTACT_US'            => ($config['contact_admin_form_enable'] && $config['email_enable']) ? append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=contactadmin') : '',
3860        'U_TEAM'                => (!$auth->acl_get('u_viewprofile')) ? '' : append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=team'),
3861        'U_TERMS_USE'            => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=terms'),
3862        'U_PRIVACY'                => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy'),
3863        'UA_PRIVACY'            => addslashes(append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy')),
3864        'U_RESTORE_PERMISSIONS'    => ($user->data['user_perm_from'] && $auth->acl_get('a_switchperm')) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=restore_perm') : '',
3865        'U_FEED'                => $controller_helper->route('phpbb_feed_index'),
3866        'U_MANIFEST'            => $controller_helper->route('phpbb_manifest_controller'),
3867
3868        'S_ALLOW_MENTIONS'        => ($config['allow_mentions'] && $auth->acl_get('u_mention') && (empty($forum_id) || $auth->acl_get('f_mention', $forum_id))) ? true : false,
3869        'S_MENTION_NAMES_LIMIT'    => $config['mention_names_limit'],
3870        'U_MENTION_URL'            => $controller_helper->route('phpbb_mention_controller'),
3871
3872        'S_USER_LOGGED_IN'        => ($user->data['user_id'] != ANONYMOUS) ? true : false,
3873        'S_AUTOLOGIN_ENABLED'    => ($config['allow_autologin']) ? true : false,
3874        'S_BOARD_DISABLED'        => ($config['board_disable']) ? true : false,
3875        'S_REGISTERED_USER'        => (!empty($user->data['is_registered'])) ? true : false,
3876        'S_IS_BOT'                => (!empty($user->data['is_bot'])) ? true : false,
3877        'S_USER_LANG'            => $user_lang,
3878        'S_USER_BROWSER'        => (isset($user->data['session_browser'])) ? $user->data['session_browser'] : $user->lang['UNKNOWN_BROWSER'],
3879        'S_USERNAME'            => $user->data['username'],
3880        'S_CONTENT_DIRECTION'    => $user->lang('DIRECTION'),
3881        'S_CONTENT_FLOW_BEGIN'    => ($user->lang('DIRECTION')  == 'ltr') ? 'left' : 'right',
3882        'S_CONTENT_FLOW_END'    => ($user->lang('DIRECTION')  == 'ltr') ? 'right' : 'left',
3883        'S_CONTENT_ENCODING'    => 'UTF-8',
3884        'S_TIMEZONE'            => sprintf($user->lang['ALL_TIMES'], $timezone_offset, $timezone_name),
3885        'S_DISPLAY_ONLINE_LIST'    => ($l_online_time) ? 1 : 0,
3886        'S_DISPLAY_SEARCH'        => (!$config['load_search']) ? 0 : (isset($auth) ? ($auth->acl_get('u_search') && $auth->acl_getf_global('f_search')) : 1),
3887        'S_DISPLAY_PM'            => ($config['allow_privmsg'] && !empty($user->data['is_registered']) && ($auth->acl_get('u_readpm') || $auth->acl_get('u_sendpm'))) ? true : false,
3888        'S_DISPLAY_MEMBERLIST'    => (isset($auth)) ? $auth->acl_get('u_viewprofile') : 0,
3889        'S_NEW_PM'                => ($s_privmsg_new) ? 1 : 0,
3890        'S_REGISTER_ENABLED'    => ($config['require_activation'] != USER_ACTIVATION_DISABLE) ? true : false,
3891        'S_FORUM_ID'            => $forum_id,
3892        'S_TOPIC_ID'            => $topic_id,
3893        'S_USER_ID'                => $user->data['user_id'],
3894
3895        'S_LOGIN_ACTION'        => ((!defined('ADMIN_START')) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=login') : append_sid("{$phpbb_admin_path}index.$phpEx", false, true, $user->session_id)),
3896        'S_LOGIN_REDIRECT'        => $s_login_redirect,
3897
3898        'S_ENABLE_FEEDS'            => ($config['feed_enable']) ? true : false,
3899        'S_ENABLE_FEEDS_OVERALL'    => ($config['feed_overall']) ? true : false,
3900        'S_ENABLE_FEEDS_FORUMS'        => ($config['feed_overall_forums']) ? true : false,
3901        'S_ENABLE_FEEDS_TOPICS'        => ($config['feed_topics_new']) ? true : false,
3902        'S_ENABLE_FEEDS_TOPICS_ACTIVE'    => ($config['feed_topics_active']) ? true : false,
3903        'S_ENABLE_FEEDS_NEWS'        => ($s_feed_news) ? true : false,
3904
3905        'S_LOAD_UNREADS'            => (bool) $config['load_unreads_search'] && ($config['load_anon_lastread'] || !empty($user->data['is_registered'])),
3906
3907        'S_SEARCH_HIDDEN_FIELDS'    => build_hidden_fields($s_search_hidden_fields),
3908
3909        'T_ASSETS_VERSION'        => $config['assets_version'],
3910        'T_ASSETS_PATH'            => "{$web_path}assets",
3911        'T_THEME_PATH'            => "{$web_path}styles/" . rawurlencode($user->style['style_path']) . '/theme',
3912        'T_TEMPLATE_PATH'        => "{$web_path}styles/" . rawurlencode($user->style['style_path']) . '/template',
3913        'T_SUPER_TEMPLATE_PATH'    => "{$web_path}styles/" . rawurlencode($user->style['style_path']) . '/template',
3914        'T_IMAGES_PATH'            => "{$web_path}images/",
3915        'T_SMILIES_PATH'        => "{$web_path}{$config['smilies_path']}/",
3916        'T_AVATAR_GALLERY_PATH'    => "{$web_path}{$config['avatar_gallery_path']}/",
3917        'T_ICONS_PATH'            => "{$web_path}{$config['icons_path']}/",
3918        'T_RANKS_PATH'            => "{$web_path}{$config['ranks_path']}/",
3919        'T_STYLESHEET_LINK'        => "{$web_path}styles/" . rawurlencode($user->style['style_path']) . '/theme/stylesheet.css?assets_version=' . $config['assets_version'],
3920        'T_STYLESHEET_LANG_LINK'=> "{$web_path}styles/" . rawurlencode($user->style['style_path']) . '/theme/' . $user->lang_name . '/stylesheet.css?assets_version=' . $config['assets_version'],
3921
3922        'T_FONT_AWESOME_LINK'    => !empty($config['allow_cdn']) && !empty($config['load_font_awesome_url']) ? $config['load_font_awesome_url'] : "{$web_path}assets/css/font-awesome.min.css?assets_version=" . $config['assets_version'],
3923
3924        'T_JQUERY_LINK'            => !empty($config['allow_cdn']) && !empty($config['load_jquery_url']) ? $config['load_jquery_url'] : "{$web_path}assets/javascript/jquery-3.7.1.min.js?assets_version=" . $config['assets_version'],
3925        'S_ALLOW_CDN'            => !empty($config['allow_cdn']),
3926        'S_COOKIE_NOTICE'        => !empty($config['cookie_notice']),
3927
3928        'T_THEME_NAME'            => rawurlencode($user->style['style_path']),
3929        'T_THEME_LANG_NAME'        => $user->lang_name,
3930        'T_TEMPLATE_NAME'        => $user->style['style_path'],
3931        'T_SUPER_TEMPLATE_NAME'    => rawurlencode((isset($user->style['style_parent_tree']) && $user->style['style_parent_tree']) ? $user->style['style_parent_tree'] : $user->style['style_path']),
3932        'T_IMAGES'                => 'images',
3933        'T_SMILIES'                => $config['smilies_path'],
3934        'T_AVATAR_GALLERY'        => $config['avatar_gallery_path'],
3935        'T_ICONS'                => $config['icons_path'],
3936        'T_RANKS'                => $config['ranks_path'],
3937
3938        'SITE_LOGO_IMG'            => $user->img('site_logo'),
3939    ));
3940
3941    $http_headers = array();
3942
3943    if ($send_headers)
3944    {
3945        // An array of http headers that phpBB will set. The following event may override these.
3946        $http_headers += array(
3947            // application/xhtml+xml not used because of IE
3948            'Content-type' => 'text/html; charset=UTF-8',
3949            'Cache-Control' => 'private, no-cache="set-cookie"',
3950            'Expires' => gmdate('D, d M Y H:i:s', time()) . ' GMT',
3951            'Referrer-Policy' => 'strict-origin-when-cross-origin',
3952        );
3953        if (!empty($user->data['is_bot']))
3954        {
3955            // Let reverse proxies know we detected a bot.
3956            $http_headers['X-PHPBB-IS-BOT'] = 'yes';
3957        }
3958    }
3959
3960    /**
3961    * Execute code and/or overwrite _common_ template variables after they have been assigned.
3962    *
3963    * @event core.page_header_after
3964    * @var    string    page_title            Page title
3965    * @var    bool    display_online_list        Do we display online users list
3966    * @var    string    item                Restrict online users to a certain
3967    *                                    session item, e.g. forum for
3968    *                                    session_forum_id
3969    * @var    int        item_id                Restrict online users to item id
3970    * @var    array        http_headers            HTTP headers that should be set by phpbb
3971    *
3972    * @since 3.1.0-b3
3973    */
3974    $vars = array('page_title', 'display_online_list', 'item_id', 'item', 'http_headers');
3975    extract($phpbb_dispatcher->trigger_event('core.page_header_after', compact($vars)));
3976
3977    foreach ($http_headers as $hname => $hval)
3978    {
3979        header((string) $hname . ': ' . (string) $hval);
3980    }
3981
3982    return;
3983}
3984
3985/**
3986* Generate the debug output string
3987*
3988* @param \phpbb\db\driver\driver_interface    $db            Database connection
3989* @param \phpbb\config\config                $config        Config object
3990* @param \phpbb\auth\auth                    $auth        Auth object
3991* @param \phpbb\user                        $user        User object
3992* @param \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher
3993* @return string
3994*/
3995function phpbb_generate_debug_output(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\auth\auth $auth, \phpbb\user $user, \phpbb\event\dispatcher_interface $phpbb_dispatcher)
3996{
3997    global $phpbb_container;
3998
3999    $debug_info = array();
4000
4001    // Output page creation time
4002    if ($phpbb_container->getParameter('debug.load_time'))
4003    {
4004        if (isset($GLOBALS['starttime']))
4005        {
4006            $totaltime = microtime(true) - $GLOBALS['starttime'];
4007            $debug_info[] = sprintf('<span title="SQL time: %.3fs / PHP time: %.3fs">Time: %.3fs</span>', $db->get_sql_time(), ($totaltime - $db->get_sql_time()), $totaltime);
4008        }
4009    }
4010
4011    if ($phpbb_container->getParameter('debug.memory'))
4012    {
4013        $memory_usage = memory_get_peak_usage();
4014        if ($memory_usage)
4015        {
4016            $memory_usage = get_formatted_filesize($memory_usage);
4017
4018            $debug_info[] = 'Peak Memory Usage: ' . $memory_usage;
4019        }
4020
4021        $debug_info[] = 'GZIP: ' . (($config['gzip_compress'] && @extension_loaded('zlib')) ? 'On' : 'Off');
4022
4023        if ($user->load)
4024        {
4025            $debug_info[] = 'Load: ' . $user->load;
4026        }
4027    }
4028
4029    if ($phpbb_container->getParameter('debug.sql_explain'))
4030    {
4031        $debug_info[] = sprintf('<span title="Cached: %d">Queries: %d</span>', $db->sql_num_queries(true), $db->sql_num_queries());
4032
4033        if ($auth->acl_get('a_'))
4034        {
4035            $page_url = build_url();
4036            $page_url .= ((!str_contains($page_url, '?')) ? '?' : '&amp;') . 'explain=1';
4037            $debug_info[] = '<a href="' . $page_url . '">SQL Explain</a>';
4038        }
4039    }
4040
4041    /**
4042    * Modify debug output information
4043    *
4044    * @event core.phpbb_generate_debug_output
4045    * @var    array    debug_info        Array of strings with debug information
4046    *
4047    * @since 3.1.0-RC3
4048    */
4049    $vars = array('debug_info');
4050    extract($phpbb_dispatcher->trigger_event('core.phpbb_generate_debug_output', compact($vars)));
4051
4052    return implode(' | ', $debug_info);
4053}
4054
4055/**
4056* Generate page footer
4057*
4058* @param bool $run_cron Whether or not to run the cron
4059* @param bool $display_template Whether or not to display the template
4060* @param bool $exit_handler Whether or not to run the exit_handler()
4061*/
4062function page_footer($run_cron = true, $display_template = true, $exit_handler = true)
4063{
4064    global $phpbb_dispatcher, $phpbb_container, $template;
4065
4066    // A listener can set this variable to `true` when it overrides this function
4067    $page_footer_override = false;
4068
4069    /**
4070    * Execute code and/or overwrite page_footer()
4071    *
4072    * @event core.page_footer
4073    * @var    bool    run_cron            Shall we run cron tasks
4074    * @var    bool    page_footer_override    Shall we return instead of running
4075    *                                        the rest of page_footer()
4076    * @since 3.1.0-a1
4077    */
4078    $vars = array('run_cron', 'page_footer_override');
4079    extract($phpbb_dispatcher->trigger_event('core.page_footer', compact($vars)));
4080
4081    if ($page_footer_override)
4082    {
4083        return;
4084    }
4085
4086    /** @var \phpbb\controller\helper $controller_helper */
4087    $controller_helper = $phpbb_container->get('controller.helper');
4088
4089    $controller_helper->display_footer($run_cron);
4090
4091    /**
4092    * Execute code and/or modify output before displaying the template.
4093    *
4094    * @event core.page_footer_after
4095    * @var    bool display_template    Whether or not to display the template
4096    * @var    bool exit_handler        Whether or not to run the exit_handler()
4097    *
4098    * @since 3.1.0-RC5
4099    */
4100    $vars = array('display_template', 'exit_handler');
4101    extract($phpbb_dispatcher->trigger_event('core.page_footer_after', compact($vars)));
4102
4103    if ($display_template)
4104    {
4105        $template->display('body');
4106    }
4107
4108    garbage_collection();
4109
4110    if ($exit_handler)
4111    {
4112        exit_handler();
4113    }
4114}
4115
4116/**
4117* Closing the cache object and the database
4118* Cool function name, eh? We might want to add operations to it later
4119*/
4120function garbage_collection()
4121{
4122    global $cache, $db;
4123    global $phpbb_dispatcher;
4124
4125    if (!empty($phpbb_dispatcher))
4126    {
4127        /**
4128        * Unload some objects, to free some memory, before we finish our task
4129        *
4130        * @event core.garbage_collection
4131        * @since 3.1.0-a1
4132        */
4133        $phpbb_dispatcher->trigger_event('core.garbage_collection');
4134    }
4135
4136    // Unload cache, must be done before the DB connection if closed
4137    if (!empty($cache))
4138    {
4139        $cache->unload();
4140    }
4141
4142    // Close our DB connection.
4143    if (!empty($db))
4144    {
4145        $db->sql_close();
4146    }
4147}
4148
4149/**
4150* Handler for exit calls in phpBB.
4151*
4152* Note: This function is called after the template has been outputted.
4153 *
4154 * @return void
4155*/
4156function exit_handler()
4157{
4158    global $phpbb_dispatcher;
4159
4160    $exit_handler_override = false;
4161
4162    /**
4163     * This event can either supplement or override the exit_handler() function
4164     *
4165     * To override this function, the event must set $exit_handler_override to
4166     * true to force it to exit directly after calling this event.
4167     *
4168     * @event core.exit_handler
4169     * @var    bool    exit_handler_override        Flag to signify exit is handled elsewhere
4170     * @since 4.0.0-a1
4171     */
4172    $vars = ['exit_handler_override'];
4173    extract($phpbb_dispatcher->trigger_event('core.exit_handler', compact($vars)));
4174
4175    if ($exit_handler_override)
4176    {
4177        return;
4178    }
4179
4180    // As a pre-caution... some setups display a blank page if the flush() is not there.
4181    (ob_get_level() > 0) ? @ob_flush() : @flush();
4182
4183    exit;
4184}
4185
4186/**
4187* Get the board contact details (e.g. for emails)
4188*
4189* @param \phpbb\config\config    $config
4190* @param string                    $phpEx
4191* @return string
4192*/
4193function phpbb_get_board_contact(\phpbb\config\config $config, $phpEx)
4194{
4195    if ($config['contact_admin_form_enable'])
4196    {
4197        return generate_board_url() . '/memberlist.' . $phpEx . '?mode=contactadmin';
4198    }
4199    else
4200    {
4201        return $config['board_contact'];
4202    }
4203}
4204
4205/**
4206* Get a clickable board contact details link
4207*
4208* @param \phpbb\config\config    $config
4209* @param string                    $phpbb_root_path
4210* @param string                    $phpEx
4211* @return string
4212*/
4213function phpbb_get_board_contact_link(\phpbb\config\config $config, $phpbb_root_path, $phpEx)
4214{
4215    if ($config['contact_admin_form_enable'] && $config['email_enable'])
4216    {
4217        return append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=contactadmin');
4218    }
4219    else
4220    {
4221        return 'mailto:' . htmlspecialchars($config['board_contact'], ENT_COMPAT);
4222    }
4223}