Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: develop
Fetching contributors…

Cannot retrieve contributors at this time

5320 lines (4580 sloc) 166.857 kb
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
/**
* @ignore
*/
if (!defined('IN_PHPBB'))
{
exit;
}
// Common global functions
/**
* Load the autoloaders added by the extensions.
*
* @param string $phpbb_root_path Path to the phpbb root directory.
*/
function phpbb_load_extensions_autoloaders($phpbb_root_path)
{
$iterator = new \RecursiveIteratorIterator(
new \phpbb\recursive_dot_prefix_filter_iterator(
new \RecursiveDirectoryIterator(
$phpbb_root_path . 'ext/',
\FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS
)
),
\RecursiveIteratorIterator::SELF_FIRST
);
$iterator->setMaxDepth(2);
foreach ($iterator as $file_info)
{
if ($file_info->getFilename() === 'vendor' && $iterator->getDepth() === 2)
{
$filename = $file_info->getRealPath() . '/autoload.php';
if (file_exists($filename))
{
require $filename;
}
}
}
}
/**
* Casts a variable to the given type.
*
* @deprecated
*/
function set_var(&$result, $var, $type, $multibyte = false)
{
// no need for dependency injection here, if you have the object, call the method yourself!
$type_cast_helper = new \phpbb\request\type_cast_helper();
$type_cast_helper->set_var($result, $var, $type, $multibyte);
}
/**
* Generates an alphanumeric random string of given length
*
* @return string
*/
function gen_rand_string($num_chars = 8)
{
// [a, z] + [0, 9] = 36
return substr(strtoupper(base_convert(unique_id(), 16, 36)), 0, $num_chars);
}
/**
* Generates a user-friendly alphanumeric random string of given length
* We remove 0 and O so users cannot confuse those in passwords etc.
*
* @return string
*/
function gen_rand_string_friendly($num_chars = 8)
{
$rand_str = unique_id();
// Remove Z and Y from the base_convert(), replace 0 with Z and O with Y
// [a, z] + [0, 9] - {z, y} = [a, z] + [0, 9] - {0, o} = 34
$rand_str = str_replace(array('0', 'O'), array('Z', 'Y'), strtoupper(base_convert($rand_str, 16, 34)));
return substr($rand_str, 0, $num_chars);
}
/**
* Return unique id
* @param string $extra additional entropy
*/
function unique_id($extra = 'c')
{
static $dss_seeded = false;
global $config;
$val = $config['rand_seed'] . microtime();
$val = md5($val);
$config['rand_seed'] = md5($config['rand_seed'] . $val . $extra);
if ($dss_seeded !== true && ($config['rand_seed_last_update'] < time() - rand(1,10)))
{
$config->set('rand_seed_last_update', time(), false);
$config->set('rand_seed', $config['rand_seed'], false);
$dss_seeded = true;
}
return substr($val, 4, 16);
}
/**
* Wrapper for mt_rand() which allows swapping $min and $max parameters.
*
* PHP does not allow us to swap the order of the arguments for mt_rand() anymore.
* (since PHP 5.3.4, see http://bugs.php.net/46587)
*
* @param int $min Lowest value to be returned
* @param int $max Highest value to be returned
*
* @return int Random integer between $min and $max (or $max and $min)
*/
function phpbb_mt_rand($min, $max)
{
return ($min > $max) ? mt_rand($max, $min) : mt_rand($min, $max);
}
/**
* Wrapper for getdate() which returns the equivalent array for UTC timestamps.
*
* @param int $time Unix timestamp (optional)
*
* @return array Returns an associative array of information related to the timestamp.
* See http://www.php.net/manual/en/function.getdate.php
*/
function phpbb_gmgetdate($time = false)
{
if ($time === false)
{
$time = time();
}
// getdate() interprets timestamps in local time.
// What follows uses the fact that getdate() and
// date('Z') balance each other out.
return getdate($time - date('Z'));
}
/**
* Return formatted string for filesizes
*
* @param mixed $value filesize in bytes
* (non-negative number; int, float or string)
* @param bool $string_only true if language string should be returned
* @param array $allowed_units only allow these units (data array indexes)
*
* @return mixed data array if $string_only is false
*/
function get_formatted_filesize($value, $string_only = true, $allowed_units = false)
{
global $user;
$available_units = array(
'tb' => array(
'min' => 1099511627776, // pow(2, 40)
'index' => 4,
'si_unit' => 'TB',
'iec_unit' => 'TIB',
),
'gb' => array(
'min' => 1073741824, // pow(2, 30)
'index' => 3,
'si_unit' => 'GB',
'iec_unit' => 'GIB',
),
'mb' => array(
'min' => 1048576, // pow(2, 20)
'index' => 2,
'si_unit' => 'MB',
'iec_unit' => 'MIB',
),
'kb' => array(
'min' => 1024, // pow(2, 10)
'index' => 1,
'si_unit' => 'KB',
'iec_unit' => 'KIB',
),
'b' => array(
'min' => 0,
'index' => 0,
'si_unit' => 'BYTES', // Language index
'iec_unit' => 'BYTES', // Language index
),
);
foreach ($available_units as $si_identifier => $unit_info)
{
if (!empty($allowed_units) && $si_identifier != 'b' && !in_array($si_identifier, $allowed_units))
{
continue;
}
if ($value >= $unit_info['min'])
{
$unit_info['si_identifier'] = $si_identifier;
break;
}
}
unset($available_units);
for ($i = 0; $i < $unit_info['index']; $i++)
{
$value /= 1024;
}
$value = round($value, 2);
// Lookup units in language dictionary
$unit_info['si_unit'] = (isset($user->lang[$unit_info['si_unit']])) ? $user->lang[$unit_info['si_unit']] : $unit_info['si_unit'];
$unit_info['iec_unit'] = (isset($user->lang[$unit_info['iec_unit']])) ? $user->lang[$unit_info['iec_unit']] : $unit_info['iec_unit'];
// Default to IEC
$unit_info['unit'] = $unit_info['iec_unit'];
if (!$string_only)
{
$unit_info['value'] = $value;
return $unit_info;
}
return $value . ' ' . $unit_info['unit'];
}
/**
* Determine whether we are approaching the maximum execution time. Should be called once
* at the beginning of the script in which it's used.
* @return bool Either true if the maximum execution time is nearly reached, or false
* if some time is still left.
*/
function still_on_time($extra_time = 15)
{
static $max_execution_time, $start_time;
$current_time = microtime(true);
if (empty($max_execution_time))
{
$max_execution_time = (function_exists('ini_get')) ? (int) @ini_get('max_execution_time') : (int) @get_cfg_var('max_execution_time');
// If zero, then set to something higher to not let the user catch the ten seconds barrier.
if ($max_execution_time === 0)
{
$max_execution_time = 50 + $extra_time;
}
$max_execution_time = min(max(10, ($max_execution_time - $extra_time)), 50);
// For debugging purposes
// $max_execution_time = 10;
global $starttime;
$start_time = (empty($starttime)) ? $current_time : $starttime;
}
return (ceil($current_time - $start_time) < $max_execution_time) ? true : false;
}
/**
* Hashes an email address to a big integer
*
* @param string $email Email address
*
* @return string Unsigned Big Integer
*/
function phpbb_email_hash($email)
{
return sprintf('%u', crc32(strtolower($email))) . strlen($email);
}
/**
* Wrapper for version_compare() that allows using uppercase A and B
* for alpha and beta releases.
*
* See http://www.php.net/manual/en/function.version-compare.php
*
* @param string $version1 First version number
* @param string $version2 Second version number
* @param string $operator Comparison operator (optional)
*
* @return mixed Boolean (true, false) if comparison operator is specified.
* Integer (-1, 0, 1) otherwise.
*/
function phpbb_version_compare($version1, $version2, $operator = null)
{
$version1 = strtolower($version1);
$version2 = strtolower($version2);
if (is_null($operator))
{
return version_compare($version1, $version2);
}
else
{
return version_compare($version1, $version2, $operator);
}
}
/**
* Global function for chmodding directories and files for internal use
*
* This function determines owner and group whom the file belongs to and user and group of PHP and then set safest possible file permissions.
* The function determines owner and group from common.php file and sets the same to the provided file.
* The function uses bit fields to build the permissions.
* The function sets the appropiate execute bit on directories.
*
* Supported constants representing bit fields are:
*
* CHMOD_ALL - all permissions (7)
* CHMOD_READ - read permission (4)
* CHMOD_WRITE - write permission (2)
* CHMOD_EXECUTE - execute permission (1)
*
* NOTE: The function uses POSIX extension and fileowner()/filegroup() functions. If any of them is disabled, this function tries to build proper permissions, by calling is_readable() and is_writable() functions.
*
* @param string $filename The file/directory to be chmodded
* @param int $perms Permissions to set
*
* @return bool true on success, otherwise false
*/
function phpbb_chmod($filename, $perms = CHMOD_READ)
{
static $_chmod_info;
// Return if the file no longer exists.
if (!file_exists($filename))
{
return false;
}
// Determine some common vars
if (empty($_chmod_info))
{
if (!function_exists('fileowner') || !function_exists('filegroup'))
{
// No need to further determine owner/group - it is unknown
$_chmod_info['process'] = false;
}
else
{
global $phpbb_root_path, $phpEx;
// Determine owner/group of common.php file and the filename we want to change here
$common_php_owner = @fileowner($phpbb_root_path . 'common.' . $phpEx);
$common_php_group = @filegroup($phpbb_root_path . 'common.' . $phpEx);
// And the owner and the groups PHP is running under.
$php_uid = (function_exists('posix_getuid')) ? @posix_getuid() : false;
$php_gids = (function_exists('posix_getgroups')) ? @posix_getgroups() : false;
// If we are unable to get owner/group, then do not try to set them by guessing
if (!$php_uid || empty($php_gids) || !$common_php_owner || !$common_php_group)
{
$_chmod_info['process'] = false;
}
else
{
$_chmod_info = array(
'process' => true,
'common_owner' => $common_php_owner,
'common_group' => $common_php_group,
'php_uid' => $php_uid,
'php_gids' => $php_gids,
);
}
}
}
if ($_chmod_info['process'])
{
$file_uid = @fileowner($filename);
$file_gid = @filegroup($filename);
// Change owner
if (@chown($filename, $_chmod_info['common_owner']))
{
clearstatcache();
$file_uid = @fileowner($filename);
}
// Change group
if (@chgrp($filename, $_chmod_info['common_group']))
{
clearstatcache();
$file_gid = @filegroup($filename);
}
// If the file_uid/gid now match the one from common.php we can process further, else we are not able to change something
if ($file_uid != $_chmod_info['common_owner'] || $file_gid != $_chmod_info['common_group'])
{
$_chmod_info['process'] = false;
}
}
// Still able to process?
if ($_chmod_info['process'])
{
if ($file_uid == $_chmod_info['php_uid'])
{
$php = 'owner';
}
else if (in_array($file_gid, $_chmod_info['php_gids']))
{
$php = 'group';
}
else
{
// Since we are setting the everyone bit anyway, no need to do expensive operations
$_chmod_info['process'] = false;
}
}
// We are not able to determine or change something
if (!$_chmod_info['process'])
{
$php = 'other';
}
// Owner always has read/write permission
$owner = CHMOD_READ | CHMOD_WRITE;
if (is_dir($filename))
{
$owner |= CHMOD_EXECUTE;
// Only add execute bit to the permission if the dir needs to be readable
if ($perms & CHMOD_READ)
{
$perms |= CHMOD_EXECUTE;
}
}
switch ($php)
{
case 'owner':
$result = @chmod($filename, ($owner << 6) + (0 << 3) + (0 << 0));
clearstatcache();
if (is_readable($filename) && phpbb_is_writable($filename))
{
break;
}
case 'group':
$result = @chmod($filename, ($owner << 6) + ($perms << 3) + (0 << 0));
clearstatcache();
if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
{
break;
}
case 'other':
$result = @chmod($filename, ($owner << 6) + ($perms << 3) + ($perms << 0));
clearstatcache();
if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
{
break;
}
default:
return false;
break;
}
return $result;
}
/**
* Test if a file/directory is writable
*
* This function calls the native is_writable() when not running under
* Windows and it is not disabled.
*
* @param string $file Path to perform write test on
* @return bool True when the path is writable, otherwise false.
*/
function phpbb_is_writable($file)
{
if (strtolower(substr(PHP_OS, 0, 3)) === 'win' || !function_exists('is_writable'))
{
if (file_exists($file))
{
// Canonicalise path to absolute path
$file = phpbb_realpath($file);
if (is_dir($file))
{
// Test directory by creating a file inside the directory
$result = @tempnam($file, 'i_w');
if (is_string($result) && file_exists($result))
{
unlink($result);
// Ensure the file is actually in the directory (returned realpathed)
return (strpos($result, $file) === 0) ? true : false;
}
}
else
{
$handle = @fopen($file, 'r+');
if (is_resource($handle))
{
fclose($handle);
return true;
}
}
}
else
{
// file does not exist test if we can write to the directory
$dir = dirname($file);
if (file_exists($dir) && is_dir($dir) && phpbb_is_writable($dir))
{
return true;
}
}
return false;
}
else
{
return is_writable($file);
}
}
/**
* Checks if a path ($path) is absolute or relative
*
* @param string $path Path to check absoluteness of
* @return boolean
*/
function phpbb_is_absolute($path)
{
return (isset($path[0]) && $path[0] == '/' || preg_match('#^[a-z]:[/\\\]#i', $path)) ? true : false;
}
/**
* @author Chris Smith <chris@project-minerva.org>
* @copyright 2006 Project Minerva Team
* @param string $path The path which we should attempt to resolve.
* @return mixed
*/
function phpbb_own_realpath($path)
{
global $request;
// Now to perform funky shizzle
// Switch to use UNIX slashes
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$path_prefix = '';
// Determine what sort of path we have
if (phpbb_is_absolute($path))
{
$absolute = true;
if ($path[0] == '/')
{
// Absolute path, *NIX style
$path_prefix = '';
}
else
{
// Absolute path, Windows style
// Remove the drive letter and colon
$path_prefix = $path[0] . ':';
$path = substr($path, 2);
}
}
else
{
// Relative Path
// Prepend the current working directory
if (function_exists('getcwd'))
{
// This is the best method, hopefully it is enabled!
$path = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . '/' . $path;
$absolute = true;
if (preg_match('#^[a-z]:#i', $path))
{
$path_prefix = $path[0] . ':';
$path = substr($path, 2);
}
else
{
$path_prefix = '';
}
}
else if ($request->server('SCRIPT_FILENAME'))
{
// Warning: If chdir() has been used this will lie!
// Warning: This has some problems sometime (CLI can create them easily)
$filename = htmlspecialchars_decode($request->server('SCRIPT_FILENAME'));
$path = str_replace(DIRECTORY_SEPARATOR, '/', dirname($filename)) . '/' . $path;
$absolute = true;
$path_prefix = '';
}
else
{
// We have no way of getting the absolute path, just run on using relative ones.
$absolute = false;
$path_prefix = '.';
}
}
// Remove any repeated slashes
$path = preg_replace('#/{2,}#', '/', $path);
// Remove the slashes from the start and end of the path
$path = trim($path, '/');
// Break the string into little bits for us to nibble on
$bits = explode('/', $path);
// Remove any . in the path, renumber array for the loop below
$bits = array_values(array_diff($bits, array('.')));
// Lets get looping, run over and resolve any .. (up directory)
for ($i = 0, $max = sizeof($bits); $i < $max; $i++)
{
// @todo Optimise
if ($bits[$i] == '..' )
{
if (isset($bits[$i - 1]))
{
if ($bits[$i - 1] != '..')
{
// We found a .. and we are able to traverse upwards, lets do it!
unset($bits[$i]);
unset($bits[$i - 1]);
$i -= 2;
$max -= 2;
$bits = array_values($bits);
}
}
else if ($absolute) // ie. !isset($bits[$i - 1]) && $absolute
{
// We have an absolute path trying to descend above the root of the filesystem
// ... Error!
return false;
}
}
}
// Prepend the path prefix
array_unshift($bits, $path_prefix);
$resolved = '';
$max = sizeof($bits) - 1;
// Check if we are able to resolve symlinks, Windows (prior to Vista and Server 2008) cannot.
$symlink_resolve = (function_exists('readlink')) ? true : false;
foreach ($bits as $i => $bit)
{
if (@is_dir("$resolved/$bit") || ($i == $max && @is_file("$resolved/$bit")))
{
// Path Exists
if ($symlink_resolve && is_link("$resolved/$bit") && ($link = readlink("$resolved/$bit")))
{
// Resolved a symlink.
$resolved = $link . (($i == $max) ? '' : '/');
continue;
}
}
else
{
// Something doesn't exist here!
// This is correct realpath() behaviour but sadly open_basedir and safe_mode make this problematic
// return false;
}
$resolved .= $bit . (($i == $max) ? '' : '/');
}
// @todo If the file exists fine and open_basedir only has one path we should be able to prepend it
// because we must be inside that basedir, the question is where...
// @internal The slash in is_dir() gets around an open_basedir restriction
if (!@file_exists($resolved) || (!@is_dir($resolved . '/') && !is_file($resolved)))
{
return false;
}
// Put the slashes back to the native operating systems slashes
$resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved);
// Check for DIRECTORY_SEPARATOR at the end (and remove it!)
if (substr($resolved, -1) == DIRECTORY_SEPARATOR)
{
return substr($resolved, 0, -1);
}
return $resolved; // We got here, in the end!
}
if (!function_exists('realpath'))
{
/**
* A wrapper for realpath
* @ignore
*/
function phpbb_realpath($path)
{
return phpbb_own_realpath($path);
}
}
else
{
/**
* A wrapper for realpath
*/
function phpbb_realpath($path)
{
$realpath = realpath($path);
// Strangely there are provider not disabling realpath but returning strange values. :o
// We at least try to cope with them.
if ($realpath === $path || $realpath === false)
{
return phpbb_own_realpath($path);
}
// Check for DIRECTORY_SEPARATOR at the end (and remove it!)
if (substr($realpath, -1) == DIRECTORY_SEPARATOR)
{
$realpath = substr($realpath, 0, -1);
}
return $realpath;
}
}
// functions used for building option fields
/**
* Pick a language, any language ...
*/
function language_select($default = '')
{
global $db;
$sql = 'SELECT lang_iso, lang_local_name
FROM ' . LANG_TABLE . '
ORDER BY lang_english_name';
$result = $db->sql_query($sql);
$lang_options = '';
while ($row = $db->sql_fetchrow($result))
{
$selected = ($row['lang_iso'] == $default) ? ' selected="selected"' : '';
$lang_options .= '<option value="' . $row['lang_iso'] . '"' . $selected . '>' . $row['lang_local_name'] . '</option>';
}
$db->sql_freeresult($result);
return $lang_options;
}
/**
* Pick a template/theme combo,
*/
function style_select($default = '', $all = false)
{
global $db;
$sql_where = (!$all) ? 'WHERE style_active = 1 ' : '';
$sql = 'SELECT style_id, style_name
FROM ' . STYLES_TABLE . "
$sql_where
ORDER BY style_name";
$result = $db->sql_query($sql);
$style_options = '';
while ($row = $db->sql_fetchrow($result))
{
$selected = ($row['style_id'] == $default) ? ' selected="selected"' : '';
$style_options .= '<option value="' . $row['style_id'] . '"' . $selected . '>' . $row['style_name'] . '</option>';
}
$db->sql_freeresult($result);
return $style_options;
}
/**
* Format the timezone offset with hours and minutes
*
* @param int $tz_offset Timezone offset in seconds
* @param bool $show_null Whether null offsets should be shown
* @return string Normalized offset string: -7200 => -02:00
* 16200 => +04:30
*/
function phpbb_format_timezone_offset($tz_offset, $show_null = false)
{
$sign = ($tz_offset < 0) ? '-' : '+';
$time_offset = abs($tz_offset);
if ($time_offset == 0 && $show_null == false)
{
return '';
}
$offset_seconds = $time_offset % 3600;
$offset_minutes = $offset_seconds / 60;
$offset_hours = ($time_offset - $offset_seconds) / 3600;
$offset_string = sprintf("%s%02d:%02d", $sign, $offset_hours, $offset_minutes);
return $offset_string;
}
/**
* Compares two time zone labels.
* Arranges them in increasing order by timezone offset.
* Places UTC before other timezones in the same offset.
*/
function phpbb_tz_select_compare($a, $b)
{
$a_sign = $a[3];
$b_sign = $b[3];
if ($a_sign != $b_sign)
{
return $a_sign == '-' ? -1 : 1;
}
$a_offset = substr($a, 4, 5);
$b_offset = substr($b, 4, 5);
if ($a_offset == $b_offset)
{
$a_name = substr($a, 12);
$b_name = substr($b, 12);
if ($a_name == $b_name)
{
return 0;
}
else if ($a_name == 'UTC')
{
return -1;
}
else if ($b_name == 'UTC')
{
return 1;
}
else
{
return $a_name < $b_name ? -1 : 1;
}
}
else
{
if ($a_sign == '-')
{
return $a_offset > $b_offset ? -1 : 1;
}
else
{
return $a_offset < $b_offset ? -1 : 1;
}
}
}
/**
* Return list of timezone identifiers
* We also add the selected timezone if we can create an object with it.
* DateTimeZone::listIdentifiers seems to not add all identifiers to the list,
* because some are only kept for backward compatible reasons. If the user has
* a deprecated value, we add it here, so it can still be kept. Once the user
* changed his value, there is no way back to deprecated values.
*
* @param string $selected_timezone Additional timezone that shall
* be added to the list of identiers
* @return array DateTimeZone::listIdentifiers and additional
* selected_timezone if it is a valid timezone.
*/
function phpbb_get_timezone_identifiers($selected_timezone)
{
$timezones = DateTimeZone::listIdentifiers();
if (!in_array($selected_timezone, $timezones))
{
try
{
// Add valid timezones that are currently selected but not returned
// by DateTimeZone::listIdentifiers
$validate_timezone = new DateTimeZone($selected_timezone);
$timezones[] = $selected_timezone;
}
catch (\Exception $e)
{
}
}
return $timezones;
}
/**
* Options to pick a timezone and date/time
*
* @param \phpbb\template\template $template phpBB template object
* @param \phpbb\user $user Object of the current user
* @param string $default A timezone to select
* @param boolean $truncate Shall we truncate the options text
*
* @return array Returns an array containing the options for the time selector.
*/
function phpbb_timezone_select($template, $user, $default = '', $truncate = false)
{
static $timezones;
$default_offset = '';
if (!isset($timezones))
{
$unsorted_timezones = phpbb_get_timezone_identifiers($default);
$timezones = array();
foreach ($unsorted_timezones as $timezone)
{
$tz = new DateTimeZone($timezone);
$dt = $user->create_datetime('now', $tz);
$offset = $dt->getOffset();
$current_time = $dt->format($user->lang['DATETIME_FORMAT'], true);
$offset_string = phpbb_format_timezone_offset($offset, true);
$timezones['UTC' . $offset_string . ' - ' . $timezone] = array(
'tz' => $timezone,
'offset' => $offset_string,
'current' => $current_time,
);
if ($timezone === $default)
{
$default_offset = 'UTC' . $offset_string;
}
}
unset($unsorted_timezones);
uksort($timezones, 'phpbb_tz_select_compare');
}
$tz_select = $opt_group = '';
foreach ($timezones as $key => $timezone)
{
if ($opt_group != $timezone['offset'])
{
// Generate tz_select for backwards compatibility
$tz_select .= ($opt_group) ? '</optgroup>' : '';
$tz_select .= '<optgroup label="' . $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $timezone['current']) . '">';
$opt_group = $timezone['offset'];
$template->assign_block_vars('timezone_select', array(
'LABEL' => $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $timezone['current']),
'VALUE' => $key . ' - ' . $timezone['current'],
));
$selected = (!empty($default_offset) && strpos($key, $default_offset) !== false) ? ' selected="selected"' : '';
$template->assign_block_vars('timezone_date', array(
'VALUE' => $key . ' - ' . $timezone['current'],
'SELECTED' => !empty($selected),
'TITLE' => $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $timezone['current']),
));
}
$label = $timezone['tz'];
if (isset($user->lang['timezones'][$label]))
{
$label = $user->lang['timezones'][$label];
}
$title = $user->lang(array('timezones', 'UTC_OFFSET_CURRENT'), $timezone['offset'], $label);
if ($truncate)
{
$label = truncate_string($label, 50, 255, false, '...');
}
// Also generate timezone_select for backwards compatibility
$selected = ($timezone['tz'] === $default) ? ' selected="selected"' : '';
$tz_select .= '<option title="' . $title . '" value="' . $timezone['tz'] . '"' . $selected . '>' . $label . '</option>';
$template->assign_block_vars('timezone_select.timezone_options', array(
'TITLE' => $title,
'VALUE' => $timezone['tz'],
'SELECTED' => !empty($selected),
'LABEL' => $label,
));
}
$tz_select .= '</optgroup>';
return $tz_select;
}
// Functions handling topic/post tracking/marking
/**
* Marks a topic/forum as read
* Marks a topic as posted to
*
* @param string $mode (all, topics, topic, post)
* @param int|bool $forum_id Used in all, topics, and topic mode
* @param int|bool $topic_id Used in topic and post mode
* @param int $post_time 0 means current time(), otherwise to set a specific mark time
* @param int $user_id can only be used with $mode == 'post'
*/
function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $user_id = 0)
{
global $db, $user, $config;
global $request, $phpbb_container, $phpbb_dispatcher;
$post_time = ($post_time === 0 || $post_time > time()) ? time() : (int) $post_time;
$should_markread = true;
/**
* This event is used for performing actions directly before marking forums,
* topics or posts as read.
*
* It is also possible to prevent the marking. For that, the $should_markread parameter
* should be set to FALSE.
*
* @event core.markread_before
* @var string mode Variable containing marking mode value
* @var mixed forum_id Variable containing forum id, or false
* @var mixed topic_id Variable containing topic id, or false
* @var int post_time Variable containing post time
* @var int user_id Variable containing the user id
* @var bool should_markread Flag indicating if the markread should be done or not.
* @since 3.1.4-RC1
*/
$vars = array(
'mode',
'forum_id',
'topic_id',
'post_time',
'user_id',
'should_markread',
);
extract($phpbb_dispatcher->trigger_event('core.markread_before', compact($vars)));
if (!$should_markread)
{
return;
}
if ($mode == 'all')
{
if ($forum_id === false || !sizeof($forum_id))
{
// Mark all forums read (index page)
/* @var $phpbb_notifications \phpbb\notification\manager */
$phpbb_notifications = $phpbb_container->get('notification_manager');
// Mark all topic notifications read for this user
$phpbb_notifications->mark_notifications_read(array(
'notification.type.topic',
'notification.type.quote',
'notification.type.bookmark',
'notification.type.post',
'notification.type.approve_topic',
'notification.type.approve_post',
), false, $user->data['user_id'], $post_time);
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
// Mark all forums read (index page)
$tables = array(TOPICS_TRACK_TABLE, FORUMS_TRACK_TABLE);
foreach ($tables as $table)
{
$sql = 'DELETE FROM ' . $table . "
WHERE user_id = {$user->data['user_id']}
AND mark_time < $post_time";
$db->sql_query($sql);
}
$sql = 'UPDATE ' . USERS_TABLE . "
SET user_lastmark = $post_time
WHERE user_id = {$user->data['user_id']}
AND user_lastmark < $post_time";
$db->sql_query($sql);
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
unset($tracking_topics['tf']);
unset($tracking_topics['t']);
unset($tracking_topics['f']);
$tracking_topics['l'] = base_convert($post_time - $config['board_startdate'], 10, 36);
$user->set_cookie('track', tracking_serialize($tracking_topics), $post_time + 31536000);
$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking_topics), \phpbb\request\request_interface::COOKIE);
unset($tracking_topics);
if ($user->data['is_registered'])
{
$sql = 'UPDATE ' . USERS_TABLE . "
SET user_lastmark = $post_time
WHERE user_id = {$user->data['user_id']}
AND user_lastmark < $post_time";
$db->sql_query($sql);
}
}
}
return;
}
else if ($mode == 'topics')
{
// Mark all topics in forums read
if (!is_array($forum_id))
{
$forum_id = array($forum_id);
}
/* @var $phpbb_notifications \phpbb\notification\manager */
$phpbb_notifications = $phpbb_container->get('notification_manager');
$phpbb_notifications->mark_notifications_read_by_parent(array(
'notification.type.topic',
'notification.type.approve_topic',
), $forum_id, $user->data['user_id'], $post_time);
// Mark all post/quote notifications read for this user in this forum
$topic_ids = array();
$sql = 'SELECT topic_id
FROM ' . TOPICS_TABLE . '
WHERE ' . $db->sql_in_set('forum_id', $forum_id);
$result = $db->sql_query($sql);
while ($row = $db->sql_fetchrow($result))
{
$topic_ids[] = $row['topic_id'];
}
$db->sql_freeresult($result);
$phpbb_notifications->mark_notifications_read_by_parent(array(
'notification.type.quote',
'notification.type.bookmark',
'notification.type.post',
'notification.type.approve_post',
), $topic_ids, $user->data['user_id'], $post_time);
// Add 0 to forums array to mark global announcements correctly
// $forum_id[] = 0;
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
$sql = 'DELETE FROM ' . TOPICS_TRACK_TABLE . "
WHERE user_id = {$user->data['user_id']}
AND mark_time < $post_time
AND " . $db->sql_in_set('forum_id', $forum_id);
$db->sql_query($sql);
$sql = 'SELECT forum_id
FROM ' . FORUMS_TRACK_TABLE . "
WHERE user_id = {$user->data['user_id']}
AND " . $db->sql_in_set('forum_id', $forum_id);
$result = $db->sql_query($sql);
$sql_update = array();
while ($row = $db->sql_fetchrow($result))
{
$sql_update[] = (int) $row['forum_id'];
}
$db->sql_freeresult($result);
if (sizeof($sql_update))
{
$sql = 'UPDATE ' . FORUMS_TRACK_TABLE . "
SET mark_time = $post_time
WHERE user_id = {$user->data['user_id']}
AND mark_time < $post_time
AND " . $db->sql_in_set('forum_id', $sql_update);
$db->sql_query($sql);
}
if ($sql_insert = array_diff($forum_id, $sql_update))
{
$sql_ary = array();
foreach ($sql_insert as $f_id)
{
$sql_ary[] = array(
'user_id' => (int) $user->data['user_id'],
'forum_id' => (int) $f_id,
'mark_time' => $post_time,
);
}
$db->sql_multi_insert(FORUMS_TRACK_TABLE, $sql_ary);
}
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
$tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
foreach ($forum_id as $f_id)
{
$topic_ids36 = (isset($tracking['tf'][$f_id])) ? $tracking['tf'][$f_id] : array();
if (isset($tracking['tf'][$f_id]))
{
unset($tracking['tf'][$f_id]);
}
foreach ($topic_ids36 as $topic_id36)
{
unset($tracking['t'][$topic_id36]);
}
if (isset($tracking['f'][$f_id]))
{
unset($tracking['f'][$f_id]);
}
$tracking['f'][$f_id] = base_convert($post_time - $config['board_startdate'], 10, 36);
}
if (isset($tracking['tf']) && empty($tracking['tf']))
{
unset($tracking['tf']);
}
$user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
unset($tracking);
}
return;
}
else if ($mode == 'topic')
{
if ($topic_id === false || $forum_id === false)
{
return;
}
/* @var $phpbb_notifications \phpbb\notification\manager */
$phpbb_notifications = $phpbb_container->get('notification_manager');
// Mark post notifications read for this user in this topic
$phpbb_notifications->mark_notifications_read(array(
'notification.type.topic',
'notification.type.approve_topic',
), $topic_id, $user->data['user_id'], $post_time);
$phpbb_notifications->mark_notifications_read_by_parent(array(
'notification.type.quote',
'notification.type.bookmark',
'notification.type.post',
'notification.type.approve_post',
), $topic_id, $user->data['user_id'], $post_time);
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
$sql = 'UPDATE ' . TOPICS_TRACK_TABLE . "
SET mark_time = $post_time
WHERE user_id = {$user->data['user_id']}
AND mark_time < $post_time
AND topic_id = $topic_id";
$db->sql_query($sql);
// insert row
if (!$db->sql_affectedrows())
{
$db->sql_return_on_error(true);
$sql_ary = array(
'user_id' => (int) $user->data['user_id'],
'topic_id' => (int) $topic_id,
'forum_id' => (int) $forum_id,
'mark_time' => $post_time,
);
$db->sql_query('INSERT INTO ' . TOPICS_TRACK_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
$db->sql_return_on_error(false);
}
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
$tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
$topic_id36 = base_convert($topic_id, 10, 36);
if (!isset($tracking['t'][$topic_id36]))
{
$tracking['tf'][$forum_id][$topic_id36] = true;
}
$tracking['t'][$topic_id36] = base_convert($post_time - $config['board_startdate'], 10, 36);
// If the cookie grows larger than 10000 characters we will remove the smallest value
// This can result in old topics being unread - but most of the time it should be accurate...
if (strlen($request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE)) > 10000)
{
//echo 'Cookie grown too large' . print_r($tracking, true);
// We get the ten most minimum stored time offsets and its associated topic ids
$time_keys = array();
for ($i = 0; $i < 10 && sizeof($tracking['t']); $i++)
{
$min_value = min($tracking['t']);
$m_tkey = array_search($min_value, $tracking['t']);
unset($tracking['t'][$m_tkey]);
$time_keys[$m_tkey] = $min_value;
}
// Now remove the topic ids from the array...
foreach ($tracking['tf'] as $f_id => $topic_id_ary)
{
foreach ($time_keys as $m_tkey => $min_value)
{
if (isset($topic_id_ary[$m_tkey]))
{
$tracking['f'][$f_id] = $min_value;
unset($tracking['tf'][$f_id][$m_tkey]);
}
}
}
if ($user->data['is_registered'])
{
$user->data['user_lastmark'] = intval(base_convert(max($time_keys) + $config['board_startdate'], 36, 10));
$sql = 'UPDATE ' . USERS_TABLE . "
SET user_lastmark = $post_time
WHERE user_id = {$user->data['user_id']}
AND mark_time < $post_time";
$db->sql_query($sql);
}
else
{
$tracking['l'] = max($time_keys);
}
}
$user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
}
return;
}
else if ($mode == 'post')
{
if ($topic_id === false)
{
return;
}
$use_user_id = (!$user_id) ? $user->data['user_id'] : $user_id;
if ($config['load_db_track'] && $use_user_id != ANONYMOUS)
{
$db->sql_return_on_error(true);
$sql_ary = array(
'user_id' => (int) $use_user_id,
'topic_id' => (int) $topic_id,
'topic_posted' => 1,
);
$db->sql_query('INSERT INTO ' . TOPICS_POSTED_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
$db->sql_return_on_error(false);
}
return;
}
}
/**
* Get topic tracking info by using already fetched info
*/
function get_topic_tracking($forum_id, $topic_ids, &$rowset, $forum_mark_time, $global_announce_list = false)
{
global $config, $user;
$last_read = array();
if (!is_array($topic_ids))
{
$topic_ids = array($topic_ids);
}
foreach ($topic_ids as $topic_id)
{
if (!empty($rowset[$topic_id]['mark_time']))
{
$last_read[$topic_id] = $rowset[$topic_id]['mark_time'];
}
}
$topic_ids = array_diff($topic_ids, array_keys($last_read));
if (sizeof($topic_ids))
{
$mark_time = array();
if (!empty($forum_mark_time[$forum_id]) && $forum_mark_time[$forum_id] !== false)
{
$mark_time[$forum_id] = $forum_mark_time[$forum_id];
}
$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
foreach ($topic_ids as $topic_id)
{
$last_read[$topic_id] = $user_lastmark;
}
}
return $last_read;
}
/**
* Get topic tracking info from db (for cookie based tracking only this function is used)
*/
function get_complete_topic_tracking($forum_id, $topic_ids, $global_announce_list = false)
{
global $config, $user, $request;
$last_read = array();
if (!is_array($topic_ids))
{
$topic_ids = array($topic_ids);
}
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
global $db;
$sql = 'SELECT topic_id, mark_time
FROM ' . TOPICS_TRACK_TABLE . "
WHERE user_id = {$user->data['user_id']}
AND " . $db->sql_in_set('topic_id', $topic_ids);
$result = $db->sql_query($sql);
while ($row = $db->sql_fetchrow($result))
{
$last_read[$row['topic_id']] = $row['mark_time'];
}
$db->sql_freeresult($result);
$topic_ids = array_diff($topic_ids, array_keys($last_read));
if (sizeof($topic_ids))
{
$sql = 'SELECT forum_id, mark_time
FROM ' . FORUMS_TRACK_TABLE . "
WHERE user_id = {$user->data['user_id']}
AND forum_id = $forum_id";
$result = $db->sql_query($sql);
$mark_time = array();
while ($row = $db->sql_fetchrow($result))
{
$mark_time[$row['forum_id']] = $row['mark_time'];
}
$db->sql_freeresult($result);
$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
foreach ($topic_ids as $topic_id)
{
$last_read[$topic_id] = $user_lastmark;
}
}
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
global $tracking_topics;
if (!isset($tracking_topics) || !sizeof($tracking_topics))
{
$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
}
if (!$user->data['is_registered'])
{
$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
}
else
{
$user_lastmark = $user->data['user_lastmark'];
}
foreach ($topic_ids as $topic_id)
{
$topic_id36 = base_convert($topic_id, 10, 36);
if (isset($tracking_topics['t'][$topic_id36]))
{
$last_read[$topic_id] = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
}
}
$topic_ids = array_diff($topic_ids, array_keys($last_read));
if (sizeof($topic_ids))
{
$mark_time = array();
if (isset($tracking_topics['f'][$forum_id]))
{
$mark_time[$forum_id] = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
}
$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user_lastmark;
foreach ($topic_ids as $topic_id)
{
$last_read[$topic_id] = $user_lastmark;
}
}
}
return $last_read;
}
/**
* Get list of unread topics
*
* @param int $user_id User ID (or false for current user)
* @param string $sql_extra Extra WHERE SQL statement
* @param string $sql_sort ORDER BY SQL sorting statement
* @param string $sql_limit Limits the size of unread topics list, 0 for unlimited query
* @param string $sql_limit_offset Sets the offset of the first row to search, 0 to search from the start
*
* @return array[int][int] Topic ids as keys, mark_time of topic as value
*/
function get_unread_topics($user_id = false, $sql_extra = '', $sql_sort = '', $sql_limit = 1001, $sql_limit_offset = 0)
{
global $config, $db, $user, $request;
global $phpbb_dispatcher;
$user_id = ($user_id === false) ? (int) $user->data['user_id'] : (int) $user_id;
// Data array we're going to return
$unread_topics = array();
if (empty($sql_sort))
{
$sql_sort = 'ORDER BY t.topic_last_post_time DESC, t.topic_last_post_id DESC';
}
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
// Get list of the unread topics
$last_mark = (int) $user->data['user_lastmark'];
$sql_array = array(
'SELECT' => 't.topic_id, t.topic_last_post_time, tt.mark_time as topic_mark_time, ft.mark_time as forum_mark_time',
'FROM' => array(TOPICS_TABLE => 't'),
'LEFT_JOIN' => array(
array(
'FROM' => array(TOPICS_TRACK_TABLE => 'tt'),
'ON' => "tt.user_id = $user_id AND t.topic_id = tt.topic_id",
),
array(
'FROM' => array(FORUMS_TRACK_TABLE => 'ft'),
'ON' => "ft.user_id = $user_id AND t.forum_id = ft.forum_id",
),
),
'WHERE' => "
t.topic_last_post_time > $last_mark AND
(
(tt.mark_time IS NOT NULL AND t.topic_last_post_time > tt.mark_time) OR
(tt.mark_time IS NULL AND ft.mark_time IS NOT NULL AND t.topic_last_post_time > ft.mark_time) OR
(tt.mark_time IS NULL AND ft.mark_time IS NULL)
)
$sql_extra
$sql_sort",
);
/**
* Change SQL query for fetching unread topics data
*
* @event core.get_unread_topics_modify_sql
* @var array sql_array Fully assembled SQL query with keys SELECT, FROM, LEFT_JOIN, WHERE
* @var int last_mark User's last_mark time
* @var string sql_extra Extra WHERE SQL statement
* @var string sql_sort ORDER BY SQL sorting statement
* @since 3.1.4-RC1
*/
$vars = array(
'sql_array',
'last_mark',
'sql_extra',
'sql_sort',
);
extract($phpbb_dispatcher->trigger_event('core.get_unread_topics_modify_sql', compact($vars)));
$sql = $db->sql_build_query('SELECT', $sql_array);
$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
while ($row = $db->sql_fetchrow($result))
{
$topic_id = (int) $row['topic_id'];
$unread_topics[$topic_id] = ($row['topic_mark_time']) ? (int) $row['topic_mark_time'] : (($row['forum_mark_time']) ? (int) $row['forum_mark_time'] : $last_mark);
}
$db->sql_freeresult($result);
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
global $tracking_topics;
if (empty($tracking_topics))
{
$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', false, \phpbb\request\request_interface::COOKIE);
$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
}
if (!$user->data['is_registered'])
{
$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
}
else
{
$user_lastmark = (int) $user->data['user_lastmark'];
}
$sql = 'SELECT t.topic_id, t.forum_id, t.topic_last_post_time
FROM ' . TOPICS_TABLE . ' t
WHERE t.topic_last_post_time > ' . $user_lastmark . "
$sql_extra
$sql_sort";
$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
while ($row = $db->sql_fetchrow($result))
{
$forum_id = (int) $row['forum_id'];
$topic_id = (int) $row['topic_id'];
$topic_id36 = base_convert($topic_id, 10, 36);
if (isset($tracking_topics['t'][$topic_id36]))
{
$last_read = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
if ($row['topic_last_post_time'] > $last_read)
{
$unread_topics[$topic_id] = $last_read;
}
}
else if (isset($tracking_topics['f'][$forum_id]))
{
$mark_time = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
if ($row['topic_last_post_time'] > $mark_time)
{
$unread_topics[$topic_id] = $mark_time;
}
}
else
{
$unread_topics[$topic_id] = $user_lastmark;
}
}
$db->sql_freeresult($result);
}
return $unread_topics;
}
/**
* Check for read forums and update topic tracking info accordingly
*
* @param int $forum_id the forum id to check
* @param int $forum_last_post_time the forums last post time
* @param int $f_mark_time the forums last mark time if user is registered and load_db_lastread enabled
* @param int $mark_time_forum false if the mark time needs to be obtained, else the last users forum mark time
*
* @return true if complete forum got marked read, else false.
*/
function update_forum_tracking_info($forum_id, $forum_last_post_time, $f_mark_time = false, $mark_time_forum = false)
{
global $db, $tracking_topics, $user, $config, $auth, $request, $phpbb_container;
// Determine the users last forum mark time if not given.
if ($mark_time_forum === false)
{
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
$mark_time_forum = (!empty($f_mark_time)) ? $f_mark_time : $user->data['user_lastmark'];
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
if (!$user->data['is_registered'])
{
$user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate']) : 0;
}
$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'];
}
}
// Handle update of unapproved topics info.
// Only update for moderators having m_approve permission for the forum.
/* @var $phpbb_content_visibility \phpbb\content_visibility */
$phpbb_content_visibility = $phpbb_container->get('content.visibility');
// Check the forum for any left unread topics.
// If there are none, we mark the forum as read.
if ($config['load_db_lastread'] && $user->data['is_registered'])
{
if ($mark_time_forum >= $forum_last_post_time)
{
// We do not need to mark read, this happened before. Therefore setting this to true
$row = true;
}
else
{
$sql = 'SELECT t.forum_id
FROM ' . TOPICS_TABLE . ' t
LEFT JOIN ' . TOPICS_TRACK_TABLE . ' tt
ON (tt.topic_id = t.topic_id
AND tt.user_id = ' . $user->data['user_id'] . ')
WHERE t.forum_id = ' . $forum_id . '
AND t.topic_last_post_time > ' . $mark_time_forum . '
AND t.topic_moved_id = 0
AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.') . '
AND (tt.topic_id IS NULL
OR tt.mark_time < t.topic_last_post_time)';
$result = $db->sql_query_limit($sql, 1);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);
}
}
else if ($config['load_anon_lastread'] || $user->data['is_registered'])
{
// Get information from cookie
$row = false;
if (!isset($tracking_topics['tf'][$forum_id]))
{
// We do not need to mark read, this happened before. Therefore setting this to true
$row = true;
}
else
{
$sql = 'SELECT t.topic_id
FROM ' . TOPICS_TABLE . ' t
WHERE t.forum_id = ' . $forum_id . '
AND t.topic_last_post_time > ' . $mark_time_forum . '
AND t.topic_moved_id = 0
AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.');
$result = $db->sql_query($sql);
$check_forum = $tracking_topics['tf'][$forum_id];
$unread = false;
while ($row = $db->sql_fetchrow($result))
{
if (!isset($check_forum[base_convert($row['topic_id'], 10, 36)]))
{
$unread = true;
break;
}
}
$db->sql_freeresult($result);
$row = $unread;
}
}
else
{
$row = true;
}
if (!$row)
{
markread('topics', $forum_id);
return true;
}
return false;
}
/**
* Transform an array into a serialized format
*/
function tracking_serialize($input)
{
$out = '';
foreach ($input as $key => $value)
{
if (is_array($value))
{
$out .= $key . ':(' . tracking_serialize($value) . ');';
}
else
{
$out .= $key . ':' . $value . ';';
}
}
return $out;
}
/**
* Transform a serialized array into an actual array
*/
function tracking_unserialize($string, $max_depth = 3)
{
$n = strlen($string);
if ($n > 10010)
{
die('Invalid data supplied');
}
$data = $stack = array();
$key = '';
$mode = 0;
$level = &$data;
for ($i = 0; $i < $n; ++$i)
{
switch ($mode)
{
case 0:
switch ($string[$i])
{
case ':':
$level[$key] = 0;
$mode = 1;
break;
case ')':
unset($level);
$level = array_pop($stack);
$mode = 3;
break;
default:
$key .= $string[$i];
}
break;
case 1:
switch ($string[$i])
{
case '(':
if (sizeof($stack) >= $max_depth)
{
die('Invalid data supplied');
}
$stack[] = &$level;
$level[$key] = array();
$level = &$level[$key];
$key = '';
$mode = 0;
break;
default:
$level[$key] = $string[$i];
$mode = 2;
break;
}
break;
case 2:
switch ($string[$i])
{
case ')':
unset($level);
$level = array_pop($stack);
$mode = 3;
break;
case ';':
$key = '';
$mode = 0;
break;
default:
$level[$key] .= $string[$i];
break;
}
break;
case 3:
switch ($string[$i])
{
case ')':
unset($level);
$level = array_pop($stack);
break;
case ';':
$key = '';
$mode = 0;
break;
default:
die('Invalid data supplied');
break;
}
break;
}
}
if (sizeof($stack) != 0 || ($mode != 0 && $mode != 3))
{
die('Invalid data supplied');
}
return $level;
}
// Server functions (building urls, redirecting...)
/**
* Append session id to url.
* This function supports hooks.
*
* @param string $url The url the session id needs to be appended to (can have params)
* @param mixed $params String or array of additional url parameters
* @param bool $is_amp Is url using &amp; (true) or & (false)
* @param string $session_id Possibility to use a custom session id instead of the global one
* @param bool $is_route Is url generated by a route.
*
* @return string The corrected url.
*
* Examples:
* <code>
* append_sid("{$phpbb_root_path}viewtopic.$phpEx?t=1&amp;f=2");
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&amp;f=2');
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&f=2', false);
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", array('t' => 1, 'f' => 2));
* </code>
*
*/
function append_sid($url, $params = false, $is_amp = true, $session_id = false, $is_route = false)
{
global $_SID, $_EXTRA_URL, $phpbb_hook, $phpbb_path_helper;
global $phpbb_dispatcher;
if ($params === '' || (is_array($params) && empty($params)))
{
// Do not append the ? if the param-list is empty anyway.
$params = false;
}
// Update the root path with the correct relative web path
if (!$is_route && $phpbb_path_helper instanceof \phpbb\path_helper)
{
$url = $phpbb_path_helper->update_web_root_path($url);
}
$append_sid_overwrite = false;
/**
* This event can either supplement or override the append_sid() function
*
* To override this function, the event must set $append_sid_overwrite to
* the new URL value, which will be returned following the event
*
* @event core.append_sid
* @var string url The url the session id needs
* to be appended to (can have
* params)
* @var mixed params String or array of additional
* url parameters
* @var bool is_amp Is url using &amp; (true) or
* & (false)
* @var bool|string session_id Possibility to use a custom
* session id (string) instead of
* the global one (false)
* @var bool|string append_sid_overwrite Overwrite function (string
* URL) or not (false)
* @var bool is_route Is url generated by a route.
* @since 3.1.0-a1
*/
$vars = array('url', 'params', 'is_amp', 'session_id', 'append_sid_overwrite', 'is_route');
extract($phpbb_dispatcher->trigger_event('core.append_sid', compact($vars)));
if ($append_sid_overwrite)
{
return $append_sid_overwrite;
}
// The following hook remains for backwards compatibility, though use of
// the event above is preferred.
// Developers using the hook function need to globalise the $_SID and $_EXTRA_URL on their own and also handle it appropriately.
// They could mimic most of what is within this function
if (!empty($phpbb_hook) && $phpbb_hook->call_hook(__FUNCTION__, $url, $params, $is_amp, $session_id))
{
if ($phpbb_hook->hook_return(__FUNCTION__))
{
return $phpbb_hook->hook_return_result(__FUNCTION__);
}
}
$params_is_array = is_array($params);
// Get anchor
$anchor = '';
if (strpos($url, '#') !== false)
{
list($url, $anchor) = explode('#', $url, 2);
$anchor = '#' . $anchor;
}
else if (!$params_is_array && strpos($params, '#') !== false)
{
list($params, $anchor) = explode('#', $params, 2);
$anchor = '#' . $anchor;
}
// Handle really simple cases quickly
if ($_SID == '' && $session_id === false && empty($_EXTRA_URL) && !$params_is_array && !$anchor)
{
if ($params === false)
{
return $url;
}
$url_delim = (strpos($url, '?') === false) ? '?' : (($is_amp) ? '&amp;' : '&');
return $url . ($params !== false ? $url_delim. $params : '');
}
// Assign sid if session id is not specified
if ($session_id === false)
{
$session_id = $_SID;
}
$amp_delim = ($is_amp) ? '&amp;' : '&';
$url_delim = (strpos($url, '?') === false) ? '?' : $amp_delim;
// Appending custom url parameter?
$append_url = (!empty($_EXTRA_URL)) ? implode($amp_delim, $_EXTRA_URL) : '';
// Use the short variant if possible ;)
if ($params === false)
{
// Append session id
if (!$session_id)
{
return $url . (($append_url) ? $url_delim . $append_url : '') . $anchor;
}
else
{
return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . 'sid=' . $session_id . $anchor;
}
}
// Build string if parameters are specified as array
if (is_array($params))
{
$output = array();
foreach ($params as $key => $item)
{
if ($item === NULL)
{
continue;
}
if ($key == '#')
{
$anchor = '#' . $item;
continue;
}
$output[] = $key . '=' . $item;
}
$params = implode($amp_delim, $output);
}
// Append session id and parameters (even if they are empty)
// If parameters are empty, the developer can still append his/her parameters without caring about the delimiter
return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . $params . ((!$session_id) ? '' : $amp_delim . 'sid=' . $session_id) . $anchor;
}
/**
* Generate board url (example: http://www.example.com/phpBB)
*
* @param bool $without_script_path if set to true the script path gets not appended (example: http://www.example.com)
*
* @return string the generated board url
*/
function generate_board_url($without_script_path = false)
{
global $config, $user, $request;
$server_name = $user->host;
$server_port = $request->server('SERVER_PORT', 0);
// Forcing server vars is the only way to specify/override the protocol
if ($config['force_server_vars'] || !$server_name)
{
$server_protocol = ($config['server_protocol']) ? $config['server_protocol'] : (($config['cookie_secure']) ? 'https://' : 'http://');
$server_name = $config['server_name'];
$server_port = (int) $config['server_port'];
$script_path = $config['script_path'];
$url = $server_protocol . $server_name;
$cookie_secure = $config['cookie_secure'];
}
else
{
// Do not rely on cookie_secure, users seem to think that it means a secured cookie instead of an encrypted connection
$cookie_secure = $request->is_secure() ? 1 : 0;
$url = (($cookie_secure) ? 'https://' : 'http://') . $server_name;
$script_path = $user->page['root_script_path'];
}
if ($server_port && (($cookie_secure && $server_port <> 443) || (!$cookie_secure && $server_port <> 80)))
{
// HTTP HOST can carry a port number (we fetch $user->host, but for old versions this may be true)
if (strpos($server_name, ':') === false)
{
$url .= ':' . $server_port;
}
}
if (!$without_script_path)
{
$url .= $script_path;
}
// Strip / from the end
if (substr($url, -1, 1) == '/')
{
$url = substr($url, 0, -1);
}
return $url;
}
/**
* Redirects the user to another page then exits the script nicely
* This function is intended for urls within the board. It's not meant to redirect to cross-domains.
*
* @param string $url The url to redirect to
* @param bool $return If true, do not redirect but return the sanitized URL. Default is no return.
* @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.
*/
function redirect($url, $return = false, $disable_cd_check = false)
{
global $db, $cache, $config, $user, $phpbb_root_path, $phpbb_filesystem, $phpbb_path_helper, $phpEx, $phpbb_dispatcher;
$failover_flag = false;
if (empty($user->lang))
{
$user->add_lang('common');
}
// Make sure no &amp;'s are in, this will break the redirect
$url = str_replace('&amp;', '&', $url);
// Determine which type of redirect we need to handle...
$url_parts = @parse_url($url);
if ($url_parts === false)
{
// Malformed url, redirect to current page...
$url = generate_board_url() . '/' . $user->page['page'];
}
else if (!empty($url_parts['scheme']) && !empty($url_parts['host']))
{
// Attention: only able to redirect within the same domain if $disable_cd_check is false (yourdomain.com -> www.yourdomain.com will not work)
if (!$disable_cd_check && $url_parts['host'] !== $user->host)
{
$url = generate_board_url();
}
}
else if ($url[0] == '/')
{
// Absolute uri, prepend direct url...
$url = generate_board_url(true) . $url;
}
else
{
// Relative uri
$pathinfo = pathinfo($url);
// Is the uri pointing to the current directory?
if ($pathinfo['dirname'] == '.')
{
$url = str_replace('./', '', $url);
// Strip / from the beginning
if ($url && substr($url, 0, 1) == '/')
{
$url = substr($url, 1);
}
}
$url = $phpbb_path_helper->remove_web_root_path($url);
if ($user->page['page_dir'])
{
$url = $user->page['page_dir'] . '/' . $url;
}
$url = generate_board_url() . '/' . $url;
}
// Clean URL and check if we go outside the forum directory
$url = $phpbb_path_helper->clean_url($url);
if (!$disable_cd_check && strpos($url, generate_board_url(true)) === false)
{
trigger_error('INSECURE_REDIRECT', E_USER_ERROR);
}
// Make sure no linebreaks are there... to prevent http response splitting for PHP < 4.4.2
if (strpos(urldecode($url), "\n") !== false || strpos(urldecode($url), "\r") !== false || strpos($url, ';') !== false)
{
trigger_error('INSECURE_REDIRECT', E_USER_ERROR);
}
// Now, also check the protocol and for a valid url the last time...
$allowed_protocols = array('http', 'https', 'ftp', 'ftps');
$url_parts = parse_url($url);
if ($url_parts === false || empty($url_parts['scheme']) || !in_array($url_parts['scheme'], $allowed_protocols))
{
trigger_error('INSECURE_REDIRECT', E_USER_ERROR);
}
/**
* Execute code and/or overwrite redirect()
*
* @event core.functions.redirect
* @var string url The url
* @var bool return If true, do not redirect but return the sanitized URL.
* @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.
* @since 3.1.0-RC3
*/
$vars = array('url', 'return', 'disable_cd_check');
extract($phpbb_dispatcher->trigger_event('core.functions.redirect', compact($vars)));
if ($return)
{
return $url;
}
else
{
garbage_collection();
}
// Redirect via an HTML form for PITA webservers
if (@preg_match('#Microsoft|WebSTAR|Xitami#', getenv('SERVER_SOFTWARE')))
{
header('Refresh: 0; URL=' . $url);
echo '<!DOCTYPE html>';
echo '<html dir="' . $user->lang['DIRECTION'] . '" lang="' . $user->lang['USER_LANG'] . '">';
echo '<head>';
echo '<meta charset="utf-8">';
echo '<meta http-equiv="refresh" content="0; url=' . str_replace('&', '&amp;', $url) . '" />';
echo '<title>' . $user->lang['REDIRECT'] . '</title>';
echo '</head>';
echo '<body>';
echo '<div style="text-align: center;">' . sprintf($user->lang['URL_REDIRECT'], '<a href="' . str_replace('&', '&amp;', $url) . '">', '</a>') . '</div>';
echo '</body>';
echo '</html>';
exit;
}
// Behave as per HTTP/1.1 spec for others
header('Location: ' . $url);
exit;
}
/**
* Re-Apply session id after page reloads
*/
function reapply_sid($url)
{
global $phpEx, $phpbb_root_path;
if ($url === "index.$phpEx")
{
return append_sid("index.$phpEx");
}
else if ($url === "{$phpbb_root_path}index.$phpEx")
{
return append_sid("{$phpbb_root_path}index.$phpEx");
}
// Remove previously added sid
if (strpos($url, 'sid=') !== false)
{
// All kind of links
$url = preg_replace('/(\?)?(&amp;|&)?sid=[a-z0-9]+/', '', $url);
// if the sid was the first param, make the old second as first ones
$url = preg_replace("/$phpEx(&amp;|&)+?/", "$phpEx?", $url);
}
return append_sid($url);
}
/**
* Returns url from the session/current page with an re-appended SID with optionally stripping vars from the url
*/
function build_url($strip_vars = false)
{
global $config, $user, $phpbb_path_helper;
$page = $phpbb_path_helper->get_valid_page($user->page['page'], $config['enable_mod_rewrite']);
// Append SID
$redirect = append_sid($page, false, false);
if ($strip_vars !== false)
{
$redirect = $phpbb_path_helper->strip_url_params($redirect, $strip_vars, false);
}
else
{
$redirect = str_replace('&', '&amp;', $redirect);
}
return $redirect . ((strpos($redirect, '?') === false) ? '?' : '');
}
/**
* Meta refresh assignment
* Adds META template variable with meta http tag.
*
* @param int $time Time in seconds for meta refresh tag
* @param string $url URL to redirect to. The url will go through redirect() first before the template variable is assigned
* @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.
*/
function meta_refresh($time, $url, $disable_cd_check = false)
{
global $template, $refresh_data, $request;
$url = redirect($url, true, $disable_cd_check);
if ($request->is_ajax())
{
$refresh_data = array(
'time' => $time,
'url' => $url,
);
}
else
{
// For XHTML compatibility we change back & to &amp;
$url = str_replace('&', '&amp;', $url);
$template->assign_vars(array(
'META' => '<meta http-equiv="refresh" content="' . $time . '; url=' . $url . '" />')
);
}
return $url;
}
/**
* Outputs correct status line header.
*
* Depending on php sapi one of the two following forms is used:
*
* Status: 404 Not Found
*
* HTTP/1.x 404 Not Found
*
* HTTP version is taken from HTTP_VERSION environment variable,
* and defaults to 1.0.
*
* Sample usage:
*
* send_status_line(404, 'Not Found');
*
* @param int $code HTTP status code
* @param string $message Message for the status code
* @return null
*/
function send_status_line($code, $message)
{
if (substr(strtolower(@php_sapi_name()), 0, 3) === 'cgi')
{
// in theory, we shouldn't need that due to php doing it. Reality offers a differing opinion, though
header("Status: $code $message", true, $code);
}
else
{
$version = phpbb_request_http_version();
header("$version $code $message", true, $code);
}
}
/**
* Returns the HTTP version used in the current request.
*
* Handles the case of being called before $request is present,
* in which case it falls back to the $_SERVER superglobal.
*
* @return string HTTP version
*/
function phpbb_request_http_version()
{
global $request;
if ($request && $request->server('SERVER_PROTOCOL'))
{
return $request->server('SERVER_PROTOCOL');
}
else if (isset($_SERVER['SERVER_PROTOCOL']))
{
return $_SERVER['SERVER_PROTOCOL'];
}
return 'HTTP/1.0';
}
//Form validation
/**
* Add a secret hash for use in links/GET requests
* @param string $link_name The name of the link; has to match the name used in check_link_hash, otherwise no restrictions apply
* @return string the hash
*/
function generate_link_hash($link_name)
{
global $user;
if (!isset($user->data["hash_$link_name"]))
{
$user->data["hash_$link_name"] = substr(sha1($user->data['user_form_salt'] . $link_name), 0, 8);
}
return $user->data["hash_$link_name"];
}
/**
* checks a link hash - for GET requests
* @param string $token the submitted token
* @param string $link_name The name of the link
* @return boolean true if all is fine
*/
function check_link_hash($token, $link_name)
{
return $token === generate_link_hash($link_name);
}
/**
* Add a secret token to the form (requires the S_FORM_TOKEN template variable)
* @param string $form_name The name of the form; has to match the name used in check_form_key, otherwise no restrictions apply
*/
function add_form_key($form_name)
{
global $config, $template, $user, $phpbb_dispatcher;
$now = time();
$token_sid = ($user->data['user_id'] == ANONYMOUS && !empty($config['form_token_sid_guests'])) ? $user->session_id : '';
$token = sha1($now . $user->data['user_form_salt'] . $form_name . $token_sid);
$s_fields = build_hidden_fields(array(
'creation_time' => $now,
'form_token' => $token,
));
/**
* Perform additional actions on creation of the form token
*
* @event core.add_form_key
* @var string form_name The form name
* @var int now Current time timestamp
* @var string s_fields Generated hidden fields
* @var string token Form token
* @var string token_sid User session ID
*
* @since 3.1.0-RC3
*/
$vars = array(
'form_name',
'now',
's_fields',
'token',
'token_sid',
);
extract($phpbb_dispatcher->trigger_event('core.add_form_key', compact($vars)));
$template->assign_vars(array(
'S_FORM_TOKEN' => $s_fields,
));
}
/**
* Check the form key. Required for all altering actions not secured by confirm_box
*
* @param string $form_name The name of the form; has to match the name used
* in add_form_key, otherwise no restrictions apply
* @param int $timespan The maximum acceptable age for a submitted form
* in seconds. Defaults to the config setting.
* @return bool True, if the form key was valid, false otherwise
*/
function check_form_key($form_name, $timespan = false)
{
global $config, $request, $user;
if ($timespan === false)
{
// we enforce a minimum value of half a minute here.
$timespan = ($config['form_token_lifetime'] == -1) ? -1 : max(30, $config['form_token_lifetime']);
}
if ($request->is_set_post('creation_time') && $request->is_set_post('form_token'))
{
$creation_time = abs($request->variable('creation_time', 0));
$token = $request->variable('form_token', '');
$diff = time() - $creation_time;
// If creation_time and the time() now is zero we can assume it was not a human doing this (the check for if ($diff)...
if (defined('DEBUG_TEST') || $diff && ($diff <= $timespan || $timespan === -1))
{
$token_sid = ($user->data['user_id'] == ANONYMOUS && !empty($config['form_token_sid_guests'])) ? $user->session_id : '';
$key = sha1($creation_time . $user->data['user_form_salt'] . $form_name . $token_sid);
if ($key === $token)
{
return true;
}
}
}
return false;
}
// Message/Login boxes
/**
* Build Confirm box
* @param boolean $check True for checking if confirmed (without any additional parameters) and false for displaying the confirm box
* @param string $title Title/Message used for confirm box.
* message text is _CONFIRM appended to title.
* If title cannot be found in user->lang a default one is displayed
* If title_CONFIRM cannot be found in user->lang the text given is used.
* @param string $hidden Hidden variables
* @param string $html_body Template used for confirm box
* @param string $u_action Custom form action
*/
function confirm_box($check, $title = '', $hidden = '', $html_body = 'confirm_body.html', $u_action = '')
{
global $user, $template, $db, $request;
global $config, $phpbb_path_helper;
if (isset($_POST['cancel']))
{
return false;
}
$confirm = ($user->lang['YES'] === $request->variable('confirm', '', true, \phpbb\request\request_interface::POST));
if ($check && $confirm)
{
$user_id = $request->variable('confirm_uid', 0);
$session_id = $request->variable('sess', '');
$confirm_key = $request->variable('confirm_key', '');
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'])
{
return false;
}
// Reset user_last_confirm_key
$sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = ''
WHERE user_id = " . $user->data['user_id'];
$db->sql_query($sql);
return true;
}
else if ($check)
{
return false;
}
$s_hidden_fields = build_hidden_fields(array(
'confirm_uid' => $user->data['user_id'],
'sess' => $user->session_id,
'sid' => $user->session_id,
));
// generate activation key
$confirm_key = gen_rand_string(10);
if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
{
adm_page_header((!isset($user->lang[$title])) ? $user->lang['CONFIRM'] : $user->lang[$title]);
}
else
{
page_header((!isset($user->lang[$title])) ? $user->lang['CONFIRM'] : $user->lang[$title]);
}
$template->set_filenames(array(
'body' => $html_body)
);
// If activation key already exist, we better do not re-use the key (something very strange is going on...)
if ($request->variable('confirm_key', ''))
{
// This should not occur, therefore we cancel the operation to safe the user
return false;
}
// re-add sid / transform & to &amp; for user->page (user->page is always using &)
$use_page = ($u_action) ? $u_action : str_replace('&', '&amp;', $user->page['page']);
$u_action = reapply_sid($phpbb_path_helper->get_valid_page($use_page, $config['enable_mod_rewrite']));
$u_action .= ((strpos($u_action, '?') === false) ? '?' : '&amp;') . 'confirm_key=' . $confirm_key;
$template->assign_vars(array(
'MESSAGE_TITLE' => (!isset($user->lang[$title])) ? $user->lang['CONFIRM'] : $user->lang[$title],
'MESSAGE_TEXT' => (!isset($user->lang[$title . '_CONFIRM'])) ? $title : $user->lang[$title . '_CONFIRM'],
'YES_VALUE' => $user->lang['YES'],
'S_CONFIRM_ACTION' => $u_action,
'S_HIDDEN_FIELDS' => $hidden . $s_hidden_fields,
'S_AJAX_REQUEST' => $request->is_ajax(),
));
$sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = '" . $db->sql_escape($confirm_key) . "'
WHERE user_id = " . $user->data['user_id'];
$db->sql_query($sql);
if ($request->is_ajax())
{
$u_action .= '&confirm_uid=' . $user->data['user_id'] . '&sess=' . $user->session_id . '&sid=' . $user->session_id;
$json_response = new \phpbb\json_response;
$json_response->send(array(
'MESSAGE_BODY' => $template->assign_display('body'),
'MESSAGE_TITLE' => (!isset($user->lang[$title])) ? $user->lang['CONFIRM'] : $user->lang[$title],
'MESSAGE_TEXT' => (!isset($user->lang[$title . '_CONFIRM'])) ? $title : $user->lang[$title . '_CONFIRM'],
'YES_VALUE' => $user->lang['YES'],
'S_CONFIRM_ACTION' => str_replace('&amp;', '&', $u_action), //inefficient, rewrite whole function
'S_HIDDEN_FIELDS' => $hidden . $s_hidden_fields
));
}
if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
{
adm_page_footer();
}
else
{
page_footer();
}
}
/**
* Generate login box or verify password
*/
function login_box($redirect = '', $l_explain = '', $l_success = '', $admin = false, $s_display = true)
{
global $db, $user, $template, $auth, $phpEx, $phpbb_root_path, $config;
global $request, $phpbb_container, $phpbb_dispatcher, $phpbb_log;
$err = '';
// Make sure user->setup() has been called
if (empty($user->lang))
{
$user->setup();
}
// Print out error if user tries to authenticate as an administrator without having the privileges...
if ($admin && !$auth->acl_get('a_'))
{
// Not authd
// anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
if ($user->data['is_registered'])
{
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
}
trigger_error('NO_AUTH_ADMIN');
}
if ($request->is_set_post('login') || ($request->is_set('login') && $request->variable('login', '') == 'external'))
{
// Get credential
if ($admin)
{
$credential = $request->variable('credential', '');
if (strspn($credential, 'abcdef0123456789') !== strlen($credential) || strlen($credential) != 32)
{
if ($user->data['is_registered'])
{
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
}
trigger_error('NO_AUTH_ADMIN');
}
$password = $request->untrimmed_variable('password_' . $credential, '', true);
}
else
{
$password = $request->untrimmed_variable('password', '', true);
}
$username = $request->variable('username', '', true);
$autologin = $request->is_set_post('autologin');
$viewonline = (int) !$request->is_set_post('viewonline');
$admin = ($admin) ? 1 : 0;
$viewonline = ($admin) ? $user->data['session_viewonline'] : $viewonline;
// Check if the supplied username is equal to the one stored within the database if re-authenticating
if ($admin && utf8_clean_string($username) != utf8_clean_string($user->data['username']))
{
// We log the attempt to use a different username...
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
trigger_error('NO_AUTH_ADMIN_USER_DIFFER');
}
// If authentication is successful we redirect user to previous page
$result = $auth->login($username, $password, $autologin, $viewonline, $admin);
// If admin authentication and login, we will log if it was a success or not...
// We also break the operation on the first non-success login - it could be argued that the user already knows
if ($admin)
{
if ($result['status'] == LOGIN_SUCCESS)
{
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_SUCCESS');
}
else
{
// Only log the failed attempt if a real user tried to.
// anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
if ($user->data['is_registered'])
{
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
}
}
}
// The result parameter is always an array, holding the relevant information...
if ($result['status'] == LOGIN_SUCCESS)
{
$redirect = $request->variable('redirect', "{$phpbb_root_path}index.$phpEx");
/**
* This event allows an extension to modify the redirection when a user successfully logs in
*
* @event core.login_box_redirect
* @var string redirect Redirect string
* @var boolean admin Is admin?
* @var bool return If true, do not redirect but return the sanitized URL.
* @since 3.1.0-RC5
*/
$vars = array('redirect', 'admin', 'return');
extract($phpbb_dispatcher->trigger_event('core.login_box_redirect', compact($vars)));
// append/replace SID (may change during the session for AOL users)
$redirect = reapply_sid($redirect);
// Special case... the user is effectively banned, but we allow founders to login
if (defined('IN_CHECK_BAN') && $result['user_row']['user_type'] != USER_FOUNDER)
{
return;
}
redirect($redirect);
}
// Something failed, determine what...
if ($result['status'] == LOGIN_BREAK)
{
trigger_error($result['error_msg']);
}
// Special cases... determine
switch ($result['status'])
{
case LOGIN_ERROR_ATTEMPTS:
$captcha = $phpbb_container->get('captcha.factory')->get_instance($config['captcha_plugin']);
$captcha->init(CONFIRM_LOGIN);
// $captcha->reset();
$template->assign_vars(array(
'CAPTCHA_TEMPLATE' => $captcha->get_template(),
));
$err = $user->lang[$result['error_msg']];
break;
case LOGIN_ERROR_PASSWORD_CONVERT:
$err = sprintf(
$user->lang[$result['error_msg']],
($config['email_enable']) ? '<a href="' . append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=sendpassword') . '">' : '',
($config['email_enable']) ? '</a>' : '',
'<a href="' . phpbb_get_board_contact_link($config, $phpbb_root_path, $phpEx) . '">',
'</a>'
);
break;
// Username, password, etc...
default:
$err = $user->lang[$result['error_msg']];
// Assign admin contact to some error messages
if ($result['error_msg'] == 'LOGIN_ERROR_USERNAME' || $result['error_msg'] == 'LOGIN_ERROR_PASSWORD')
{
$err = sprintf($user->lang[$result['error_msg']], '<a href="' . append_sid("{$phpbb_root_path}memberlist.$phpEx", 'mode=contactadmin') . '">', '</a>');
}
break;
}
/**
* This event allows an extension to process when a user fails a login attempt
*
* @event core.login_box_failed
* @var array result Login result data
* @var string username User name used to login
* @var string password Password used to login
* @var string err Error message
* @since 3.1.3-RC1
*/
$vars = array('result', 'username', 'password', 'err');
extract($phpbb_dispatcher->trigger_event('core.login_box_failed', compact($vars)));
}
// Assign credential for username/password pair
$credential = ($admin) ? md5(unique_id()) : false;
$s_hidden_fields = array(
'sid' => $user->session_id,
);
if ($redirect)
{
$s_hidden_fields['redirect'] = $redirect;
}
if ($admin)
{
$s_hidden_fields['credential'] = $credential;
}
/* @var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$auth_provider = $provider_collection->get_provider();
$auth_provider_data = $auth_provider->get_login_data();
if ($auth_provider_data)
{
if (isset($auth_provider_data['VARS']))
{
$template->assign_vars($auth_provider_data['VARS']);
}
if (isset($auth_provider_data['BLOCK_VAR_NAME']))
{
foreach ($auth_provider_data['BLOCK_VARS'] as $block_vars)
{
$template->assign_block_vars($auth_provider_data['BLOCK_VAR_NAME'], $block_vars);
}
}
$template->assign_vars(array(
'PROVIDER_TEMPLATE_FILE' => $auth_provider_data['TEMPLATE_FILE'],
));
}
$s_hidden_fields = build_hidden_fields($s_hidden_fields);
$template->assign_vars(array(
'LOGIN_ERROR' => $err,
'LOGIN_EXPLAIN' => $l_explain,
'U_SEND_PASSWORD' => ($config['email_enable']) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=sendpassword') : '',
'U_RESEND_ACTIVATION' => ($config['require_activation'] == USER_ACTIVATION_SELF && $config['email_enable']) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=resend_act') : '',
'U_TERMS_USE' => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=terms'),
'U_PRIVACY' => append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy'),
'S_DISPLAY_FULL_LOGIN' => ($s_display) ? true : false,
'S_HIDDEN_FIELDS' => $s_hidden_fields,
'S_ADMIN_AUTH' => $admin,
'USERNAME' => ($admin) ? $user->data['username'] : '',
'USERNAME_CREDENTIAL' => 'username',
'PASSWORD_CREDENTIAL' => ($admin) ? 'password_' . $credential : 'password',
));
page_header($user->lang['LOGIN']);
$template->set_filenames(array(
'body' => 'login_body.html')
);
make_jumpbox(append_sid("{$phpbb_root_path}viewforum.$phpEx"));
page_footer();
}
/**
* Generate forum login box
*/
function login_forum_box($forum_data)
{
global $db, $phpbb_container, $request, $template, $user, $phpbb_dispatcher;
$password = $request->variable('password', '', true);
$sql = 'SELECT forum_id
FROM ' . FORUMS_ACCESS_TABLE . '
WHERE forum_id = ' . $forum_data['forum_id'] . '
AND user_id = ' . $user->data['user_id'] . "
AND session_id = '" . $db->sql_escape($user->session_id) . "'";
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);
if ($row)
{
return true;
}
if ($password)
{
// Remove expired authorised sessions
$sql = 'SELECT f.session_id
FROM ' . FORUMS_ACCESS_TABLE . ' f
LEFT JOIN ' . SESSIONS_TABLE . ' s ON (f.session_id = s.session_id)
WHERE s.session_id IS NULL';
$result = $db->sql_query($sql);
if ($row = $db->sql_fetchrow($result))
{
$sql_in = array();
do
{
$sql_in[] = (string) $row['session_id'];
}
while ($row = $db->sql_fetchrow($result));
// Remove expired sessions
$sql = 'DELETE FROM ' . FORUMS_ACCESS_TABLE . '
WHERE ' . $db->sql_in_set('session_id', $sql_in);
$db->sql_query($sql);
}
$db->sql_freeresult($result);
/* @var $passwords_manager \phpbb\passwords\manager */
$passwords_manager = $phpbb_container->get('passwords.manager');
if ($passwords_manager->check($password, $forum_data['forum_password']))
{
$sql_ary = array(
'forum_id' => (int) $forum_data['forum_id'],
'user_id' => (int) $user->data['user_id'],
'session_id' => (string) $user->session_id,
);
$db->sql_query('INSERT INTO ' . FORUMS_ACCESS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
return true;
}
$template->assign_var('LOGIN_ERROR', $user->lang['WRONG_PASSWORD']);
}
/**
* Performing additional actions, load additional data on forum login
*
* @event core.login_forum_box
* @var array forum_data Array with forum data
* @var string password Password entered
* @since 3.1.0-RC3
*/
$vars = array('forum_data', 'password');
extract($phpbb_dispatcher->trigger_event('core.login_forum_box', compact($vars)));
page_header($user->lang['LOGIN']);
$template->assign_vars(array(
'FORUM_NAME' => isset($forum_data['forum_name']) ? $forum_data['forum_name'] : '',
'S_LOGIN_ACTION' => build_url(array('f')),
'S_HIDDEN_FIELDS' => build_hidden_fields(array('f' => $forum_data['forum_id'])))
);
$template->set_filenames(array(
'body' => 'login_forum.html')
);
page_footer();
}
// Little helpers
/**
* Little helper for the build_hidden_fields function
*/
function _build_hidden_fields($key, $value, $specialchar, $stripslashes)
{
$hidden_fields = '';
if (!is_array($value))
{
$value = ($stripslashes) ? stripslashes($value) : $value;
$value = ($specialchar) ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value;
$hidden_fields .= '<input type="hidden" name="' . $key . '" value="' . $value . '" />' . "\n";
}
else
{
foreach ($value as $_key => $_value)
{
$_key = ($stripslashes) ? stripslashes($_key) : $_key;
$_key = ($specialchar) ? htmlspecialchars($_key, ENT_COMPAT, 'UTF-8') : $_key;
$hidden_fields .= _build_hidden_fields($key . '[' . $_key . ']', $_value, $specialchar, $stripslashes);
}
}
return $hidden_fields;
}
/**
* Build simple hidden fields from array
*
* @param array $field_ary an array of values to build the hidden field from
* @param bool $specialchar if true, keys and values get specialchared
* @param bool $stripslashes if true, keys and values get stripslashed
*
* @return string the hidden fields
*/
function build_hidden_fields($field_ary, $specialchar = false, $stripslashes = false)
{
$s_hidden_fields = '';
foreach ($field_ary as $name => $vars)
{
$name = ($stripslashes) ? stripslashes($name) : $name;
$name = ($specialchar) ? htmlspecialchars($name, ENT_COMPAT, 'UTF-8') : $name;
$s_hidden_fields .= _build_hidden_fields($name, $vars, $specialchar, $stripslashes);
}
return $s_hidden_fields;
}
/**
* Parse cfg file
*/
function parse_cfg_file($filename, $lines = false)
{
$parsed_items = array();
if ($lines === false)
{
$lines = file($filename);
}
foreach ($lines as $line)
{
$line = trim($line);
if (!$line || $line[0] == '#' || ($delim_pos = strpos($line, '=')) === false)
{
continue;
}
// Determine first occurrence, since in values the equal sign is allowed
$key = htmlspecialchars(strtolower(trim(substr($line, 0, $delim_pos))));
$value = trim(substr($line, $delim_pos + 1));
if (in_array($value, array('off', 'false', '0')))
{
$value = false;
}
else if (in_array($value, array('on', 'true', '1')))
{
$value = true;
}
else if (!trim($value))
{
$value = '';
}
else if (($value[0] == "'" && $value[sizeof($value) - 1] == "'") || ($value[0] == '"' && $value[sizeof($value) - 1] == '"'))
{
$value = htmlspecialchars(substr($value, 1, sizeof($value)-2));
}
else
{
$value = htmlspecialchars($value);
}
$parsed_items[$key] = $value;
}
if (isset($parsed_items['parent']) && isset($parsed_items['name']) && $parsed_items['parent'] == $parsed_items['name'])
{
unset($parsed_items['parent']);
}
return $parsed_items;
}
/**
* Return a nicely formatted backtrace.
*
* Turns the array returned by debug_backtrace() into HTML markup.
* Also filters out absolute paths to phpBB root.
*
* @return string HTML markup
*/
function get_backtrace()
{
$output = '<div style="font-family: monospace;">';
$backtrace = debug_backtrace();
// We skip the first one, because it only shows this file/function
unset($backtrace[0]);
foreach ($backtrace as $trace)
{
// Strip the current directory from path
$trace['file'] = (empty($trace['file'])) ? '(not given by php)' : htmlspecialchars(phpbb_filter_root_path($trace['file']));
$trace['line'] = (empty($trace['line'])) ? '(not given by php)' : $trace['line'];
// Only show function arguments for include etc.
// Other parameters may contain sensible information
$argument = '';
if (!empty($trace['args'][0]) && in_array($trace['function'], array('include', 'require', 'include_once', 'require_once')))
{
$argument = htmlspecialchars(phpbb_filter_root_path($trace['args'][0]));
}