[ Index ]

PHP Cross Reference of phpBB-3.3.10-deutsch

title

Body

[close]

/includes/ -> functions.php (source)

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