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