PageRenderTime 126ms CodeModel.GetById 64ms app.highlight 45ms RepoModel.GetById 1ms app.codeStats 1ms

/forum/includes/functions.php

https://github.com/GreyTeardrop/socionicasys-forum
PHP | 4877 lines | 3407 code | 668 blank | 802 comment | 714 complexity | 77a1a3a415cf8219e9f455cc2f33e3b1 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/**
   3*
   4* @package phpBB3
   5* @version $Id$
   6* @copyright (c) 2005 phpBB Group
   7* @license http://opensource.org/licenses/gpl-license.php GNU Public License
   8*
   9*/
  10
  11/**
  12* @ignore
  13*/
  14if (!defined('IN_PHPBB'))
  15{
  16	exit;
  17}
  18
  19// Common global functions
  20
  21/**
  22* set_var
  23*
  24* Set variable, used by {@link request_var the request_var function}
  25*
  26* @access private
  27*/
  28function set_var(&$result, $var, $type, $multibyte = false)
  29{
  30	settype($var, $type);
  31	$result = $var;
  32
  33	if ($type == 'string')
  34	{
  35		$result = trim(htmlspecialchars(str_replace(array("\r\n", "\r", "\0"), array("\n", "\n", ''), $result), ENT_COMPAT, 'UTF-8'));
  36
  37		if (!empty($result))
  38		{
  39			// Make sure multibyte characters are wellformed
  40			if ($multibyte)
  41			{
  42				if (!preg_match('/^./u', $result))
  43				{
  44					$result = '';
  45				}
  46			}
  47			else
  48			{
  49				// no multibyte, allow only ASCII (0-127)
  50				$result = preg_replace('/[\x80-\xFF]/', '?', $result);
  51			}
  52		}
  53
  54		$result = (STRIP) ? stripslashes($result) : $result;
  55	}
  56}
  57
  58/**
  59* request_var
  60*
  61* Used to get passed variable
  62*/
  63function request_var($var_name, $default, $multibyte = false, $cookie = false)
  64{
  65	if (!$cookie && isset($_COOKIE[$var_name]))
  66	{
  67		if (!isset($_GET[$var_name]) && !isset($_POST[$var_name]))
  68		{
  69			return (is_array($default)) ? array() : $default;
  70		}
  71		$_REQUEST[$var_name] = isset($_POST[$var_name]) ? $_POST[$var_name] : $_GET[$var_name];
  72	}
  73
  74	$super_global = ($cookie) ? '_COOKIE' : '_REQUEST';
  75	if (!isset($GLOBALS[$super_global][$var_name]) || is_array($GLOBALS[$super_global][$var_name]) != is_array($default))
  76	{
  77		return (is_array($default)) ? array() : $default;
  78	}
  79
  80	$var = $GLOBALS[$super_global][$var_name];
  81	if (!is_array($default))
  82	{
  83		$type = gettype($default);
  84	}
  85	else
  86	{
  87		list($key_type, $type) = each($default);
  88		$type = gettype($type);
  89		$key_type = gettype($key_type);
  90		if ($type == 'array')
  91		{
  92			reset($default);
  93			$default = current($default);
  94			list($sub_key_type, $sub_type) = each($default);
  95			$sub_type = gettype($sub_type);
  96			$sub_type = ($sub_type == 'array') ? 'NULL' : $sub_type;
  97			$sub_key_type = gettype($sub_key_type);
  98		}
  99	}
 100
 101	if (is_array($var))
 102	{
 103		$_var = $var;
 104		$var = array();
 105
 106		foreach ($_var as $k => $v)
 107		{
 108			set_var($k, $k, $key_type);
 109			if ($type == 'array' && is_array($v))
 110			{
 111				foreach ($v as $_k => $_v)
 112				{
 113					if (is_array($_v))
 114					{
 115						$_v = null;
 116					}
 117					set_var($_k, $_k, $sub_key_type, $multibyte);
 118					set_var($var[$k][$_k], $_v, $sub_type, $multibyte);
 119				}
 120			}
 121			else
 122			{
 123				if ($type == 'array' || is_array($v))
 124				{
 125					$v = null;
 126				}
 127				set_var($var[$k], $v, $type, $multibyte);
 128			}
 129		}
 130	}
 131	else
 132	{
 133		set_var($var, $var, $type, $multibyte);
 134	}
 135
 136	return $var;
 137}
 138
 139/**
 140* Set config value. Creates missing config entry.
 141*/
 142function set_config($config_name, $config_value, $is_dynamic = false)
 143{
 144	global $db, $cache, $config;
 145
 146	$sql = 'UPDATE ' . CONFIG_TABLE . "
 147		SET config_value = '" . $db->sql_escape($config_value) . "'
 148		WHERE config_name = '" . $db->sql_escape($config_name) . "'";
 149	$db->sql_query($sql);
 150
 151	if (!$db->sql_affectedrows() && !isset($config[$config_name]))
 152	{
 153		$sql = 'INSERT INTO ' . CONFIG_TABLE . ' ' . $db->sql_build_array('INSERT', array(
 154			'config_name'	=> $config_name,
 155			'config_value'	=> $config_value,
 156			'is_dynamic'	=> ($is_dynamic) ? 1 : 0));
 157		$db->sql_query($sql);
 158	}
 159
 160	$config[$config_name] = $config_value;
 161
 162	if (!$is_dynamic)
 163	{
 164		$cache->destroy('config');
 165	}
 166}
 167
 168/**
 169* Set dynamic config value with arithmetic operation.
 170*/
 171function set_config_count($config_name, $increment, $is_dynamic = false)
 172{
 173	global $db, $cache;
 174
 175	switch ($db->sql_layer)
 176	{
 177		case 'firebird':
 178			// Precision must be from 1 to 18
 179			$sql_update = 'CAST(CAST(config_value as DECIMAL(18, 0)) + ' . (int) $increment . ' as VARCHAR(255))';
 180		break;
 181
 182		case 'postgres':
 183			// Need to cast to text first for PostgreSQL 7.x
 184			$sql_update = 'CAST(CAST(config_value::text as DECIMAL(255, 0)) + ' . (int) $increment . ' as VARCHAR(255))';
 185		break;
 186
 187		// MySQL, SQlite, mssql, mssql_odbc, oracle
 188		default:
 189			$sql_update = 'config_value + ' . (int) $increment;
 190		break;
 191	}
 192
 193	$db->sql_query('UPDATE ' . CONFIG_TABLE . ' SET config_value = ' . $sql_update . " WHERE config_name = '" . $db->sql_escape($config_name) . "'");
 194
 195	if (!$is_dynamic)
 196	{
 197		$cache->destroy('config');
 198	}
 199}
 200
 201/**
 202* Generates an alphanumeric random string of given length
 203*
 204* @return string
 205*/
 206function gen_rand_string($num_chars = 8)
 207{
 208	// [a, z] + [0, 9] = 36
 209	return substr(strtoupper(base_convert(unique_id(), 16, 36)), 0, $num_chars);
 210}
 211
 212/**
 213* Generates a user-friendly alphanumeric random string of given length
 214* We remove 0 and O so users cannot confuse those in passwords etc.
 215*
 216* @return string
 217*/
 218function gen_rand_string_friendly($num_chars = 8)
 219{
 220	$rand_str = unique_id();
 221
 222	// Remove Z and Y from the base_convert(), replace 0 with Z and O with Y
 223	// [a, z] + [0, 9] - {z, y} = [a, z] + [0, 9] - {0, o} = 34
 224	$rand_str = str_replace(array('0', 'O'), array('Z', 'Y'), strtoupper(base_convert($rand_str, 16, 34)));
 225
 226	return substr($rand_str, 0, $num_chars);
 227}
 228
 229/**
 230* Return unique id
 231* @param string $extra additional entropy
 232*/
 233function unique_id($extra = 'c')
 234{
 235	static $dss_seeded = false;
 236	global $config;
 237
 238	$val = $config['rand_seed'] . microtime();
 239	$val = md5($val);
 240	$config['rand_seed'] = md5($config['rand_seed'] . $val . $extra);
 241
 242	if ($dss_seeded !== true && ($config['rand_seed_last_update'] < time() - rand(1,10)))
 243	{
 244		set_config('rand_seed_last_update', time(), true);
 245		set_config('rand_seed', $config['rand_seed'], true);
 246		$dss_seeded = true;
 247	}
 248
 249	return substr($val, 4, 16);
 250}
 251
 252/**
 253* Wrapper for mt_rand() which allows swapping $min and $max parameters.
 254*
 255* PHP does not allow us to swap the order of the arguments for mt_rand() anymore.
 256* (since PHP 5.3.4, see http://bugs.php.net/46587)
 257*
 258* @param int $min		Lowest value to be returned
 259* @param int $max		Highest value to be returned
 260*
 261* @return int			Random integer between $min and $max (or $max and $min)
 262*/
 263function phpbb_mt_rand($min, $max)
 264{
 265	return ($min > $max) ? mt_rand($max, $min) : mt_rand($min, $max);
 266}
 267
 268/**
 269* Return formatted string for filesizes
 270*
 271* @param int	$value			filesize in bytes
 272* @param bool	$string_only	true if language string should be returned
 273* @param array	$allowed_units	only allow these units (data array indexes)
 274*
 275* @return mixed					data array if $string_only is false
 276* @author bantu
 277*/
 278function get_formatted_filesize($value, $string_only = true, $allowed_units = false)
 279{
 280	global $user;
 281
 282	$available_units = array(
 283		'gb' => array(
 284			'min' 		=> 1073741824, // pow(2, 30)
 285			'index'		=> 3,
 286			'si_unit'	=> 'GB',
 287			'iec_unit'	=> 'GIB',
 288		),
 289		'mb' => array(
 290			'min'		=> 1048576, // pow(2, 20)
 291			'index'		=> 2,
 292			'si_unit'	=> 'MB',
 293			'iec_unit'	=> 'MIB',
 294		),
 295		'kb' => array(
 296			'min'		=> 1024, // pow(2, 10)
 297			'index'		=> 1,
 298			'si_unit'	=> 'KB',
 299			'iec_unit'	=> 'KIB',
 300		),
 301		'b' => array(
 302			'min'		=> 0,
 303			'index'		=> 0,
 304			'si_unit'	=> 'BYTES', // Language index
 305			'iec_unit'	=> 'BYTES',  // Language index
 306		),
 307	);
 308
 309	foreach ($available_units as $si_identifier => $unit_info)
 310	{
 311		if (!empty($allowed_units) && $si_identifier != 'b' && !in_array($si_identifier, $allowed_units))
 312		{
 313			continue;
 314		}
 315
 316		if ($value >= $unit_info['min'])
 317		{
 318			$unit_info['si_identifier'] = $si_identifier;
 319
 320			break;
 321		}
 322	}
 323	unset($available_units);
 324
 325	for ($i = 0; $i < $unit_info['index']; $i++)
 326	{
 327		$value /= 1024;
 328	}
 329	$value = round($value, 2);
 330
 331	// Lookup units in language dictionary
 332	$unit_info['si_unit'] = (isset($user->lang[$unit_info['si_unit']])) ? $user->lang[$unit_info['si_unit']] : $unit_info['si_unit'];
 333	$unit_info['iec_unit'] = (isset($user->lang[$unit_info['iec_unit']])) ? $user->lang[$unit_info['iec_unit']] : $unit_info['iec_unit'];
 334
 335	// Default to IEC
 336	$unit_info['unit'] = $unit_info['iec_unit'];
 337
 338	if (!$string_only)
 339	{
 340		$unit_info['value'] = $value;
 341
 342		return $unit_info;
 343	}
 344
 345	return $value  . ' ' . $unit_info['unit'];
 346}
 347
 348/**
 349* Determine whether we are approaching the maximum execution time. Should be called once
 350* at the beginning of the script in which it's used.
 351* @return	bool	Either true if the maximum execution time is nearly reached, or false
 352*					if some time is still left.
 353*/
 354function still_on_time($extra_time = 15)
 355{
 356	static $max_execution_time, $start_time;
 357
 358	$time = explode(' ', microtime());
 359	$current_time = $time[0] + $time[1];
 360
 361	if (empty($max_execution_time))
 362	{
 363		$max_execution_time = (function_exists('ini_get')) ? (int) @ini_get('max_execution_time') : (int) @get_cfg_var('max_execution_time');
 364
 365		// If zero, then set to something higher to not let the user catch the ten seconds barrier.
 366		if ($max_execution_time === 0)
 367		{
 368			$max_execution_time = 50 + $extra_time;
 369		}
 370
 371		$max_execution_time = min(max(10, ($max_execution_time - $extra_time)), 50);
 372
 373		// For debugging purposes
 374		// $max_execution_time = 10;
 375
 376		global $starttime;
 377		$start_time = (empty($starttime)) ? $current_time : $starttime;
 378	}
 379
 380	return (ceil($current_time - $start_time) < $max_execution_time) ? true : false;
 381}
 382
 383/**
 384*
 385* @version Version 0.1 / slightly modified for phpBB 3.0.x (using $H$ as hash type identifier)
 386*
 387* Portable PHP password hashing framework.
 388*
 389* Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
 390* the public domain.
 391*
 392* There's absolutely no warranty.
 393*
 394* The homepage URL for this framework is:
 395*
 396*	http://www.openwall.com/phpass/
 397*
 398* Please be sure to update the Version line if you edit this file in any way.
 399* It is suggested that you leave the main version number intact, but indicate
 400* your project name (after the slash) and add your own revision information.
 401*
 402* Please do not change the "private" password hashing method implemented in
 403* here, thereby making your hashes incompatible.  However, if you must, please
 404* change the hash type identifier (the "$P$") to something different.
 405*
 406* Obviously, since this code is in the public domain, the above are not
 407* requirements (there can be none), but merely suggestions.
 408*
 409*
 410* Hash the password
 411*/
 412function phpbb_hash($password)
 413{
 414	$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 415
 416	$random_state = unique_id();
 417	$random = '';
 418	$count = 6;
 419
 420	if (($fh = @fopen('/dev/urandom', 'rb')))
 421	{
 422		$random = fread($fh, $count);
 423		fclose($fh);
 424	}
 425
 426	if (strlen($random) < $count)
 427	{
 428		$random = '';
 429
 430		for ($i = 0; $i < $count; $i += 16)
 431		{
 432			$random_state = md5(unique_id() . $random_state);
 433			$random .= pack('H*', md5($random_state));
 434		}
 435		$random = substr($random, 0, $count);
 436	}
 437
 438	$hash = _hash_crypt_private($password, _hash_gensalt_private($random, $itoa64), $itoa64);
 439
 440	if (strlen($hash) == 34)
 441	{
 442		return $hash;
 443	}
 444
 445	return md5($password);
 446}
 447
 448/**
 449* Check for correct password
 450*
 451* @param string $password The password in plain text
 452* @param string $hash The stored password hash
 453*
 454* @return bool Returns true if the password is correct, false if not.
 455*/
 456function phpbb_check_hash($password, $hash)
 457{
 458	$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 459	if (strlen($hash) == 34)
 460	{
 461		return (_hash_crypt_private($password, $hash, $itoa64) === $hash) ? true : false;
 462	}
 463
 464	return (md5($password) === $hash) ? true : false;
 465}
 466
 467/**
 468* Generate salt for hash generation
 469*/
 470function _hash_gensalt_private($input, &$itoa64, $iteration_count_log2 = 6)
 471{
 472	if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
 473	{
 474		$iteration_count_log2 = 8;
 475	}
 476
 477	$output = '$H$';
 478	$output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= 5) ? 5 : 3), 30)];
 479	$output .= _hash_encode64($input, 6, $itoa64);
 480
 481	return $output;
 482}
 483
 484/**
 485* Encode hash
 486*/
 487function _hash_encode64($input, $count, &$itoa64)
 488{
 489	$output = '';
 490	$i = 0;
 491
 492	do
 493	{
 494		$value = ord($input[$i++]);
 495		$output .= $itoa64[$value & 0x3f];
 496
 497		if ($i < $count)
 498		{
 499			$value |= ord($input[$i]) << 8;
 500		}
 501
 502		$output .= $itoa64[($value >> 6) & 0x3f];
 503
 504		if ($i++ >= $count)
 505		{
 506			break;
 507		}
 508
 509		if ($i < $count)
 510		{
 511			$value |= ord($input[$i]) << 16;
 512		}
 513
 514		$output .= $itoa64[($value >> 12) & 0x3f];
 515
 516		if ($i++ >= $count)
 517		{
 518			break;
 519		}
 520
 521		$output .= $itoa64[($value >> 18) & 0x3f];
 522	}
 523	while ($i < $count);
 524
 525	return $output;
 526}
 527
 528/**
 529* The crypt function/replacement
 530*/
 531function _hash_crypt_private($password, $setting, &$itoa64)
 532{
 533	$output = '*';
 534
 535	// Check for correct hash
 536	if (substr($setting, 0, 3) != '$H$' && substr($setting, 0, 3) != '$P$')
 537	{
 538		return $output;
 539	}
 540
 541	$count_log2 = strpos($itoa64, $setting[3]);
 542
 543	if ($count_log2 < 7 || $count_log2 > 30)
 544	{
 545		return $output;
 546	}
 547
 548	$count = 1 << $count_log2;
 549	$salt = substr($setting, 4, 8);
 550
 551	if (strlen($salt) != 8)
 552	{
 553		return $output;
 554	}
 555
 556	/**
 557	* We're kind of forced to use MD5 here since it's the only
 558	* cryptographic primitive available in all versions of PHP
 559	* currently in use.  To implement our own low-level crypto
 560	* in PHP would result in much worse performance and
 561	* consequently in lower iteration counts and hashes that are
 562	* quicker to crack (by non-PHP code).
 563	*/
 564	if (PHP_VERSION >= 5)
 565	{
 566		$hash = md5($salt . $password, true);
 567		do
 568		{
 569			$hash = md5($hash . $password, true);
 570		}
 571		while (--$count);
 572	}
 573	else
 574	{
 575		$hash = pack('H*', md5($salt . $password));
 576		do
 577		{
 578			$hash = pack('H*', md5($hash . $password));
 579		}
 580		while (--$count);
 581	}
 582
 583	$output = substr($setting, 0, 12);
 584	$output .= _hash_encode64($hash, 16, $itoa64);
 585
 586	return $output;
 587}
 588
 589/**
 590* Hashes an email address to a big integer
 591*
 592* @param string $email		Email address
 593*
 594* @return string			Unsigned Big Integer
 595*/
 596function phpbb_email_hash($email)
 597{
 598	return sprintf('%u', crc32(strtolower($email))) . strlen($email);
 599}
 600
 601/**
 602* Global function for chmodding directories and files for internal use
 603*
 604* This function determines owner and group whom the file belongs to and user and group of PHP and then set safest possible file permissions.
 605* The function determines owner and group from common.php file and sets the same to the provided file.
 606* The function uses bit fields to build the permissions.
 607* The function sets the appropiate execute bit on directories.
 608*
 609* Supported constants representing bit fields are:
 610*
 611* CHMOD_ALL - all permissions (7)
 612* CHMOD_READ - read permission (4)
 613* CHMOD_WRITE - write permission (2)
 614* CHMOD_EXECUTE - execute permission (1)
 615*
 616* 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.
 617*
 618* @param string	$filename	The file/directory to be chmodded
 619* @param int	$perms		Permissions to set
 620*
 621* @return bool	true on success, otherwise false
 622* @author faw, phpBB Group
 623*/
 624function phpbb_chmod($filename, $perms = CHMOD_READ)
 625{
 626	static $_chmod_info;
 627
 628	// Return if the file no longer exists.
 629	if (!file_exists($filename))
 630	{
 631		return false;
 632	}
 633
 634	// Determine some common vars
 635	if (empty($_chmod_info))
 636	{
 637		if (!function_exists('fileowner') || !function_exists('filegroup'))
 638		{
 639			// No need to further determine owner/group - it is unknown
 640			$_chmod_info['process'] = false;
 641		}
 642		else
 643		{
 644			global $phpbb_root_path, $phpEx;
 645
 646			// Determine owner/group of common.php file and the filename we want to change here
 647			$common_php_owner = @fileowner($phpbb_root_path . 'common.' . $phpEx);
 648			$common_php_group = @filegroup($phpbb_root_path . 'common.' . $phpEx);
 649
 650			// And the owner and the groups PHP is running under.
 651			$php_uid = (function_exists('posix_getuid')) ? @posix_getuid() : false;
 652			$php_gids = (function_exists('posix_getgroups')) ? @posix_getgroups() : false;
 653
 654			// If we are unable to get owner/group, then do not try to set them by guessing
 655			if (!$php_uid || empty($php_gids) || !$common_php_owner || !$common_php_group)
 656			{
 657				$_chmod_info['process'] = false;
 658			}
 659			else
 660			{
 661				$_chmod_info = array(
 662					'process'		=> true,
 663					'common_owner'	=> $common_php_owner,
 664					'common_group'	=> $common_php_group,
 665					'php_uid'		=> $php_uid,
 666					'php_gids'		=> $php_gids,
 667				);
 668			}
 669		}
 670	}
 671
 672	if ($_chmod_info['process'])
 673	{
 674		$file_uid = @fileowner($filename);
 675		$file_gid = @filegroup($filename);
 676
 677		// Change owner
 678		if (@chown($filename, $_chmod_info['common_owner']))
 679		{
 680			clearstatcache();
 681			$file_uid = @fileowner($filename);
 682		}
 683
 684		// Change group
 685		if (@chgrp($filename, $_chmod_info['common_group']))
 686		{
 687			clearstatcache();
 688			$file_gid = @filegroup($filename);
 689		}
 690
 691		// If the file_uid/gid now match the one from common.php we can process further, else we are not able to change something
 692		if ($file_uid != $_chmod_info['common_owner'] || $file_gid != $_chmod_info['common_group'])
 693		{
 694			$_chmod_info['process'] = false;
 695		}
 696	}
 697
 698	// Still able to process?
 699	if ($_chmod_info['process'])
 700	{
 701		if ($file_uid == $_chmod_info['php_uid'])
 702		{
 703			$php = 'owner';
 704		}
 705		else if (in_array($file_gid, $_chmod_info['php_gids']))
 706		{
 707			$php = 'group';
 708		}
 709		else
 710		{
 711			// Since we are setting the everyone bit anyway, no need to do expensive operations
 712			$_chmod_info['process'] = false;
 713		}
 714	}
 715
 716	// We are not able to determine or change something
 717	if (!$_chmod_info['process'])
 718	{
 719		$php = 'other';
 720	}
 721
 722	// Owner always has read/write permission
 723	$owner = CHMOD_READ | CHMOD_WRITE;
 724	if (is_dir($filename))
 725	{
 726		$owner |= CHMOD_EXECUTE;
 727
 728		// Only add execute bit to the permission if the dir needs to be readable
 729		if ($perms & CHMOD_READ)
 730		{
 731			$perms |= CHMOD_EXECUTE;
 732		}
 733	}
 734
 735	switch ($php)
 736	{
 737		case 'owner':
 738			$result = @chmod($filename, ($owner << 6) + (0 << 3) + (0 << 0));
 739
 740			clearstatcache();
 741
 742			if (is_readable($filename) && phpbb_is_writable($filename))
 743			{
 744				break;
 745			}
 746
 747		case 'group':
 748			$result = @chmod($filename, ($owner << 6) + ($perms << 3) + (0 << 0));
 749
 750			clearstatcache();
 751
 752			if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
 753			{
 754				break;
 755			}
 756
 757		case 'other':
 758			$result = @chmod($filename, ($owner << 6) + ($perms << 3) + ($perms << 0));
 759
 760			clearstatcache();
 761
 762			if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
 763			{
 764				break;
 765			}
 766
 767		default:
 768			return false;
 769		break;
 770	}
 771
 772	return $result;
 773}
 774
 775/**
 776* Test if a file/directory is writable
 777*
 778* This function calls the native is_writable() when not running under
 779* Windows and it is not disabled.
 780*
 781* @param string $file Path to perform write test on
 782* @return bool True when the path is writable, otherwise false.
 783*/
 784function phpbb_is_writable($file)
 785{
 786	if (strtolower(substr(PHP_OS, 0, 3)) === 'win' || !function_exists('is_writable'))
 787	{
 788		if (file_exists($file))
 789		{
 790			// Canonicalise path to absolute path
 791			$file = phpbb_realpath($file);
 792
 793			if (is_dir($file))
 794			{
 795				// Test directory by creating a file inside the directory
 796				$result = @tempnam($file, 'i_w');
 797
 798				if (is_string($result) && file_exists($result))
 799				{
 800					unlink($result);
 801
 802					// Ensure the file is actually in the directory (returned realpathed)
 803					return (strpos($result, $file) === 0) ? true : false;
 804				}
 805			}
 806			else
 807			{
 808				$handle = @fopen($file, 'r+');
 809
 810				if (is_resource($handle))
 811				{
 812					fclose($handle);
 813					return true;
 814				}
 815			}
 816		}
 817		else
 818		{
 819			// file does not exist test if we can write to the directory
 820			$dir = dirname($file);
 821
 822			if (file_exists($dir) && is_dir($dir) && phpbb_is_writable($dir))
 823			{
 824				return true;
 825			}
 826		}
 827
 828		return false;
 829	}
 830	else
 831	{
 832		return is_writable($file);
 833	}
 834}
 835
 836// Compatibility functions
 837
 838if (!function_exists('array_combine'))
 839{
 840	/**
 841	* A wrapper for the PHP5 function array_combine()
 842	* @param array $keys contains keys for the resulting array
 843	* @param array $values contains values for the resulting array
 844	*
 845	* @return Returns an array by using the values from the keys array as keys and the
 846	* 	values from the values array as the corresponding values. Returns false if the
 847	* 	number of elements for each array isn't equal or if the arrays are empty.
 848	*/
 849	function array_combine($keys, $values)
 850	{
 851		$keys = array_values($keys);
 852		$values = array_values($values);
 853
 854		$n = sizeof($keys);
 855		$m = sizeof($values);
 856		if (!$n || !$m || ($n != $m))
 857		{
 858			return false;
 859		}
 860
 861		$combined = array();
 862		for ($i = 0; $i < $n; $i++)
 863		{
 864			$combined[$keys[$i]] = $values[$i];
 865		}
 866		return $combined;
 867	}
 868}
 869
 870if (!function_exists('str_split'))
 871{
 872	/**
 873	* A wrapper for the PHP5 function str_split()
 874	* @param array $string contains the string to be converted
 875	* @param array $split_length contains the length of each chunk
 876	*
 877	* @return  Converts a string to an array. If the optional split_length parameter is specified,
 878	*  	the returned array will be broken down into chunks with each being split_length in length,
 879	*  	otherwise each chunk will be one character in length. FALSE is returned if split_length is
 880	*  	less than 1. If the split_length length exceeds the length of string, the entire string is
 881	*  	returned as the first (and only) array element.
 882	*/
 883	function str_split($string, $split_length = 1)
 884	{
 885		if ($split_length < 1)
 886		{
 887			return false;
 888		}
 889		else if ($split_length >= strlen($string))
 890		{
 891			return array($string);
 892		}
 893		else
 894		{
 895			preg_match_all('#.{1,' . $split_length . '}#s', $string, $matches);
 896			return $matches[0];
 897		}
 898	}
 899}
 900
 901if (!function_exists('stripos'))
 902{
 903	/**
 904	* A wrapper for the PHP5 function stripos
 905	* Find position of first occurrence of a case-insensitive string
 906	*
 907	* @param string $haystack is the string to search in
 908	* @param string $needle is the string to search for
 909	*
 910	* @return mixed Returns the numeric position of the first occurrence of needle in the haystack string. Unlike strpos(), stripos() is case-insensitive.
 911	* Note that the needle may be a string of one or more characters.
 912	* If needle is not found, stripos() will return boolean FALSE.
 913	*/
 914	function stripos($haystack, $needle)
 915	{
 916		if (preg_match('#' . preg_quote($needle, '#') . '#i', $haystack, $m))
 917		{
 918			return strpos($haystack, $m[0]);
 919		}
 920
 921		return false;
 922	}
 923}
 924
 925/**
 926* Checks if a path ($path) is absolute or relative
 927*
 928* @param string $path Path to check absoluteness of
 929* @return boolean
 930*/
 931function is_absolute($path)
 932{
 933	return ($path[0] == '/' || (DIRECTORY_SEPARATOR == '\\' && preg_match('#^[a-z]:[/\\\]#i', $path))) ? true : false;
 934}
 935
 936/**
 937* @author Chris Smith <chris@project-minerva.org>
 938* @copyright 2006 Project Minerva Team
 939* @param string $path The path which we should attempt to resolve.
 940* @return mixed
 941*/
 942function phpbb_own_realpath($path)
 943{
 944	// Now to perform funky shizzle
 945
 946	// Switch to use UNIX slashes
 947	$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
 948	$path_prefix = '';
 949
 950	// Determine what sort of path we have
 951	if (is_absolute($path))
 952	{
 953		$absolute = true;
 954
 955		if ($path[0] == '/')
 956		{
 957			// Absolute path, *NIX style
 958			$path_prefix = '';
 959		}
 960		else
 961		{
 962			// Absolute path, Windows style
 963			// Remove the drive letter and colon
 964			$path_prefix = $path[0] . ':';
 965			$path = substr($path, 2);
 966		}
 967	}
 968	else
 969	{
 970		// Relative Path
 971		// Prepend the current working directory
 972		if (function_exists('getcwd'))
 973		{
 974			// This is the best method, hopefully it is enabled!
 975			$path = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . '/' . $path;
 976			$absolute = true;
 977			if (preg_match('#^[a-z]:#i', $path))
 978			{
 979				$path_prefix = $path[0] . ':';
 980				$path = substr($path, 2);
 981			}
 982			else
 983			{
 984				$path_prefix = '';
 985			}
 986		}
 987		else if (isset($_SERVER['SCRIPT_FILENAME']) && !empty($_SERVER['SCRIPT_FILENAME']))
 988		{
 989			// Warning: If chdir() has been used this will lie!
 990			// Warning: This has some problems sometime (CLI can create them easily)
 991			$path = str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['SCRIPT_FILENAME'])) . '/' . $path;
 992			$absolute = true;
 993			$path_prefix = '';
 994		}
 995		else
 996		{
 997			// We have no way of getting the absolute path, just run on using relative ones.
 998			$absolute = false;
 999			$path_prefix = '.';
1000		}
1001	}
1002
1003	// Remove any repeated slashes
1004	$path = preg_replace('#/{2,}#', '/', $path);
1005
1006	// Remove the slashes from the start and end of the path
1007	$path = trim($path, '/');
1008
1009	// Break the string into little bits for us to nibble on
1010	$bits = explode('/', $path);
1011
1012	// Remove any . in the path, renumber array for the loop below
1013	$bits = array_values(array_diff($bits, array('.')));
1014
1015	// Lets get looping, run over and resolve any .. (up directory)
1016	for ($i = 0, $max = sizeof($bits); $i < $max; $i++)
1017	{
1018		// @todo Optimise
1019		if ($bits[$i] == '..' )
1020		{
1021			if (isset($bits[$i - 1]))
1022			{
1023				if ($bits[$i - 1] != '..')
1024				{
1025					// We found a .. and we are able to traverse upwards, lets do it!
1026					unset($bits[$i]);
1027					unset($bits[$i - 1]);
1028					$i -= 2;
1029					$max -= 2;
1030					$bits = array_values($bits);
1031				}
1032			}
1033			else if ($absolute) // ie. !isset($bits[$i - 1]) && $absolute
1034			{
1035				// We have an absolute path trying to descend above the root of the filesystem
1036				// ... Error!
1037				return false;
1038			}
1039		}
1040	}
1041
1042	// Prepend the path prefix
1043	array_unshift($bits, $path_prefix);
1044
1045	$resolved = '';
1046
1047	$max = sizeof($bits) - 1;
1048
1049	// Check if we are able to resolve symlinks, Windows cannot.
1050	$symlink_resolve = (function_exists('readlink')) ? true : false;
1051
1052	foreach ($bits as $i => $bit)
1053	{
1054		if (@is_dir("$resolved/$bit") || ($i == $max && @is_file("$resolved/$bit")))
1055		{
1056			// Path Exists
1057			if ($symlink_resolve && is_link("$resolved/$bit") && ($link = readlink("$resolved/$bit")))
1058			{
1059				// Resolved a symlink.
1060				$resolved = $link . (($i == $max) ? '' : '/');
1061				continue;
1062			}
1063		}
1064		else
1065		{
1066			// Something doesn't exist here!
1067			// This is correct realpath() behaviour but sadly open_basedir and safe_mode make this problematic
1068			// return false;
1069		}
1070		$resolved .= $bit . (($i == $max) ? '' : '/');
1071	}
1072
1073	// @todo If the file exists fine and open_basedir only has one path we should be able to prepend it
1074	// because we must be inside that basedir, the question is where...
1075	// @internal The slash in is_dir() gets around an open_basedir restriction
1076	if (!@file_exists($resolved) || (!@is_dir($resolved . '/') && !is_file($resolved)))
1077	{
1078		return false;
1079	}
1080
1081	// Put the slashes back to the native operating systems slashes
1082	$resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved);
1083
1084	// Check for DIRECTORY_SEPARATOR at the end (and remove it!)
1085	if (substr($resolved, -1) == DIRECTORY_SEPARATOR)
1086	{
1087		return substr($resolved, 0, -1);
1088	}
1089
1090	return $resolved; // We got here, in the end!
1091}
1092
1093if (!function_exists('realpath'))
1094{
1095	/**
1096	* A wrapper for realpath
1097	* @ignore
1098	*/
1099	function phpbb_realpath($path)
1100	{
1101		return phpbb_own_realpath($path);
1102	}
1103}
1104else
1105{
1106	/**
1107	* A wrapper for realpath
1108	*/
1109	function phpbb_realpath($path)
1110	{
1111		$realpath = realpath($path);
1112
1113		// Strangely there are provider not disabling realpath but returning strange values. :o
1114		// We at least try to cope with them.
1115		if ($realpath === $path || $realpath === false)
1116		{
1117			return phpbb_own_realpath($path);
1118		}
1119
1120		// Check for DIRECTORY_SEPARATOR at the end (and remove it!)
1121		if (substr($realpath, -1) == DIRECTORY_SEPARATOR)
1122		{
1123			$realpath = substr($realpath, 0, -1);
1124		}
1125
1126		return $realpath;
1127	}
1128}
1129
1130if (!function_exists('htmlspecialchars_decode'))
1131{
1132	/**
1133	* A wrapper for htmlspecialchars_decode
1134	* @ignore
1135	*/
1136	function htmlspecialchars_decode($string, $quote_style = ENT_COMPAT)
1137	{
1138		return strtr($string, array_flip(get_html_translation_table(HTML_SPECIALCHARS, $quote_style)));
1139	}
1140}
1141
1142// functions used for building option fields
1143
1144/**
1145* Pick a language, any language ...
1146*/
1147function language_select($default = '')
1148{
1149	global $db;
1150
1151	$sql = 'SELECT lang_iso, lang_local_name
1152		FROM ' . LANG_TABLE . '
1153		ORDER BY lang_english_name';
1154	$result = $db->sql_query($sql);
1155
1156	$lang_options = '';
1157	while ($row = $db->sql_fetchrow($result))
1158	{
1159		$selected = ($row['lang_iso'] == $default) ? ' selected="selected"' : '';
1160		$lang_options .= '<option value="' . $row['lang_iso'] . '"' . $selected . '>' . $row['lang_local_name'] . '</option>';
1161	}
1162	$db->sql_freeresult($result);
1163
1164	return $lang_options;
1165}
1166
1167/**
1168* Pick a template/theme combo,
1169*/
1170function style_select($default = '', $all = false)
1171{
1172	global $db;
1173
1174	$sql_where = (!$all) ? 'WHERE style_active = 1 ' : '';
1175	$sql = 'SELECT style_id, style_name
1176		FROM ' . STYLES_TABLE . "
1177		$sql_where
1178		ORDER BY style_name";
1179	$result = $db->sql_query($sql);
1180
1181	$style_options = '';
1182	while ($row = $db->sql_fetchrow($result))
1183	{
1184		$selected = ($row['style_id'] == $default) ? ' selected="selected"' : '';
1185		$style_options .= '<option value="' . $row['style_id'] . '"' . $selected . '>' . $row['style_name'] . '</option>';
1186	}
1187	$db->sql_freeresult($result);
1188
1189	return $style_options;
1190}
1191
1192/**
1193* Pick a timezone
1194*/
1195function tz_select($default = '', $truncate = false)
1196{
1197	global $user;
1198
1199	$tz_select = '';
1200
1201	$timezones = automatic_dst_get_timezones();
1202
1203	foreach ($timezones as $id => $zone)
1204	{
1205		if ($truncate)
1206		{
1207			$zone_trunc = truncate_string($zone, 50, 255, false, '...');
1208		}
1209		else
1210		{
1211			$zone_trunc = $zone;
1212		}
1213
1214		$selected = ($id == $default) ? ' selected="selected"' : '';
1215		$tz_select .= '<option title="' . $zone . '" value="' . $id . '"' . $selected . '>' . $zone_trunc . '</option>';
1216	}
1217
1218	return $tz_select;
1219}
1220
1221// Functions handling topic/post tracking/marking
1222
1223/**
1224* Marks a topic/forum as read
1225* Marks a topic as posted to
1226*
1227* @param int $user_id can only be used with $mode == 'post'
1228*/
1229function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $user_id = 0)
1230{
1231	global $db, $user, $config;
1232
1233	if ($mode == 'all')
1234	{
1235		if ($forum_id === false || !sizeof($forum_id))
1236		{
1237			if ($config['load_db_lastread'] && $user->data['is_registered'])
1238			{
1239				// Mark all forums read (index page)
1240				$db->sql_query('DELETE FROM ' . TOPICS_TRACK_TABLE . " WHERE user_id = {$user->data['user_id']}");
1241				$db->sql_query('DELETE FROM ' . FORUMS_TRACK_TABLE . " WHERE user_id = {$user->data['user_id']}");
1242				$db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . time() . " WHERE user_id = {$user->data['user_id']}");
1243			}
1244			else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1245			{
1246				$tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
1247				$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1248
1249				unset($tracking_topics['tf']);
1250				unset($tracking_topics['t']);
1251				unset($tracking_topics['f']);
1252				$tracking_topics['l'] = base_convert(time() - $config['board_startdate'], 10, 36);
1253
1254				$user->set_cookie('track', tracking_serialize($tracking_topics), time() + 31536000);
1255				$_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking_topics)) : tracking_serialize($tracking_topics);
1256
1257				unset($tracking_topics);
1258
1259				if ($user->data['is_registered'])
1260				{
1261					$db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . time() . " WHERE user_id = {$user->data['user_id']}");
1262				}
1263			}
1264		}
1265
1266		return;
1267	}
1268	else if ($mode == 'topics')
1269	{
1270		// Mark all topics in forums read
1271		if (!is_array($forum_id))
1272		{
1273			$forum_id = array($forum_id);
1274		}
1275
1276		// Add 0 to forums array to mark global announcements correctly
1277		// $forum_id[] = 0;
1278
1279		if ($config['load_db_lastread'] && $user->data['is_registered'])
1280		{
1281			$sql = 'DELETE FROM ' . TOPICS_TRACK_TABLE . "
1282				WHERE user_id = {$user->data['user_id']}
1283					AND " . $db->sql_in_set('forum_id', $forum_id);
1284			$db->sql_query($sql);
1285
1286			$sql = 'SELECT forum_id
1287				FROM ' . FORUMS_TRACK_TABLE . "
1288				WHERE user_id = {$user->data['user_id']}
1289					AND " . $db->sql_in_set('forum_id', $forum_id);
1290			$result = $db->sql_query($sql);
1291
1292			$sql_update = array();
1293			while ($row = $db->sql_fetchrow($result))
1294			{
1295				$sql_update[] = (int) $row['forum_id'];
1296			}
1297			$db->sql_freeresult($result);
1298
1299			if (sizeof($sql_update))
1300			{
1301				$sql = 'UPDATE ' . FORUMS_TRACK_TABLE . '
1302					SET mark_time = ' . time() . "
1303					WHERE user_id = {$user->data['user_id']}
1304						AND " . $db->sql_in_set('forum_id', $sql_update);
1305				$db->sql_query($sql);
1306			}
1307
1308			if ($sql_insert = array_diff($forum_id, $sql_update))
1309			{
1310				$sql_ary = array();
1311				foreach ($sql_insert as $f_id)
1312				{
1313					$sql_ary[] = array(
1314						'user_id'	=> (int) $user->data['user_id'],
1315						'forum_id'	=> (int) $f_id,
1316						'mark_time'	=> time()
1317					);
1318				}
1319
1320				$db->sql_multi_insert(FORUMS_TRACK_TABLE, $sql_ary);
1321			}
1322		}
1323		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1324		{
1325			$tracking = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
1326			$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
1327
1328			foreach ($forum_id as $f_id)
1329			{
1330				$topic_ids36 = (isset($tracking['tf'][$f_id])) ? $tracking['tf'][$f_id] : array();
1331
1332				if (isset($tracking['tf'][$f_id]))
1333				{
1334					unset($tracking['tf'][$f_id]);
1335				}
1336
1337				foreach ($topic_ids36 as $topic_id36)
1338				{
1339					unset($tracking['t'][$topic_id36]);
1340				}
1341
1342				if (isset($tracking['f'][$f_id]))
1343				{
1344					unset($tracking['f'][$f_id]);
1345				}
1346
1347				$tracking['f'][$f_id] = base_convert(time() - $config['board_startdate'], 10, 36);
1348			}
1349
1350			if (isset($tracking['tf']) && empty($tracking['tf']))
1351			{
1352				unset($tracking['tf']);
1353			}
1354
1355			$user->set_cookie('track', tracking_serialize($tracking), time() + 31536000);
1356			$_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking)) : tracking_serialize($tracking);
1357
1358			unset($tracking);
1359		}
1360
1361		return;
1362	}
1363	else if ($mode == 'topic')
1364	{
1365		if ($topic_id === false || $forum_id === false)
1366		{
1367			return;
1368		}
1369
1370		if ($config['load_db_lastread'] && $user->data['is_registered'])
1371		{
1372			$sql = 'UPDATE ' . TOPICS_TRACK_TABLE . '
1373				SET mark_time = ' . (($post_time) ? $post_time : time()) . "
1374				WHERE user_id = {$user->data['user_id']}
1375					AND topic_id = $topic_id";
1376			$db->sql_query($sql);
1377
1378			// insert row
1379			if (!$db->sql_affectedrows())
1380			{
1381				$db->sql_return_on_error(true);
1382
1383				$sql_ary = array(
1384					'user_id'		=> (int) $user->data['user_id'],
1385					'topic_id'		=> (int) $topic_id,
1386					'forum_id'		=> (int) $forum_id,
1387					'mark_time'		=> ($post_time) ? (int) $post_time : time(),
1388				);
1389
1390				$db->sql_query('INSERT INTO ' . TOPICS_TRACK_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
1391
1392				$db->sql_return_on_error(false);
1393			}
1394		}
1395		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1396		{
1397			$tracking = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
1398			$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
1399
1400			$topic_id36 = base_convert($topic_id, 10, 36);
1401
1402			if (!isset($tracking['t'][$topic_id36]))
1403			{
1404				$tracking['tf'][$forum_id][$topic_id36] = true;
1405			}
1406
1407			$post_time = ($post_time) ? $post_time : time();
1408			$tracking['t'][$topic_id36] = base_convert($post_time - $config['board_startdate'], 10, 36);
1409
1410			// If the cookie grows larger than 10000 characters we will remove the smallest value
1411			// This can result in old topics being unread - but most of the time it should be accurate...
1412			if (isset($_COOKIE[$config['cookie_name'] . '_track']) && strlen($_COOKIE[$config['cookie_name'] . '_track']) > 10000)
1413			{
1414				//echo 'Cookie grown too large' . print_r($tracking, true);
1415
1416				// We get the ten most minimum stored time offsets and its associated topic ids
1417				$time_keys = array();
1418				for ($i = 0; $i < 10 && sizeof($tracking['t']); $i++)
1419				{
1420					$min_value = min($tracking['t']);
1421					$m_tkey = array_search($min_value, $tracking['t']);
1422					unset($tracking['t'][$m_tkey]);
1423
1424					$time_keys[$m_tkey] = $min_value;
1425				}
1426
1427				// Now remove the topic ids from the array...
1428				foreach ($tracking['tf'] as $f_id => $topic_id_ary)
1429				{
1430					foreach ($time_keys as $m_tkey => $min_value)
1431					{
1432						if (isset($topic_id_ary[$m_tkey]))
1433						{
1434							$tracking['f'][$f_id] = $min_value;
1435							unset($tracking['tf'][$f_id][$m_tkey]);
1436						}
1437					}
1438				}
1439
1440				if ($user->data['is_registered'])
1441				{
1442					$user->data['user_lastmark'] = intval(base_convert(max($time_keys) + $config['board_startdate'], 36, 10));
1443					$db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . $user->data['user_lastmark'] . " WHERE user_id = {$user->data['user_id']}");
1444				}
1445				else
1446				{
1447					$tracking['l'] = max($time_keys);
1448				}
1449			}
1450
1451			$user->set_cookie('track', tracking_serialize($tracking), time() + 31536000);
1452			$_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking)) : tracking_serialize($tracking);
1453		}
1454
1455		return;
1456	}
1457	else if ($mode == 'post')
1458	{
1459		if ($topic_id === false)
1460		{
1461			return;
1462		}
1463
1464		$use_user_id = (!$user_id) ? $user->data['user_id'] : $user_id;
1465
1466		if ($config['load_db_track'] && $use_user_id != ANONYMOUS)
1467		{
1468			$db->sql_return_on_error(true);
1469
1470			$sql_ary = array(
1471				'user_id'		=> (int) $use_user_id,
1472				'topic_id'		=> (int) $topic_id,
1473				'topic_posted'	=> 1
1474			);
1475
1476			$db->sql_query('INSERT INTO ' . TOPICS_POSTED_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
1477
1478			$db->sql_return_on_error(false);
1479		}
1480
1481		return;
1482	}
1483}
1484
1485/**
1486* Get topic tracking info by using already fetched info
1487*/
1488function get_topic_tracking($forum_id, $topic_ids, &$rowset, $forum_mark_time, $global_announce_list = false)
1489{
1490	global $config, $user;
1491
1492	$last_read = array();
1493
1494	if (!is_array($topic_ids))
1495	{
1496		$topic_ids = array($topic_ids);
1497	}
1498
1499	foreach ($topic_ids as $topic_id)
1500	{
1501		if (!empty($rowset[$topic_id]['mark_time']))
1502		{
1503			$last_read[$topic_id] = $rowset[$topic_id]['mark_time'];
1504		}
1505	}
1506
1507	$topic_ids = array_diff($topic_ids, array_keys($last_read));
1508
1509	if (sizeof($topic_ids))
1510	{
1511		$mark_time = array();
1512
1513		// Get global announcement info
1514		if ($global_announce_list && sizeof($global_announce_list))
1515		{
1516			if (!isset($forum_mark_time[0]))
1517			{
1518				global $db;
1519
1520				$sql = 'SELECT mark_time
1521					FROM ' . FORUMS_TRACK_TABLE . "
1522					WHERE user_id = {$user->data['user_id']}
1523						AND forum_id = 0";
1524				$result = $db->sql_query($sql);
1525				$row = $db->sql_fetchrow($result);
1526				$db->sql_freeresult($result);
1527
1528				if ($row)
1529				{
1530					$mark_time[0] = $row['mark_time'];
1531				}
1532			}
1533			else
1534			{
1535				if ($forum_mark_time[0] !== false)
1536				{
1537					$mark_time[0] = $forum_mark_time[0];
1538				}
1539			}
1540		}
1541
1542		if (!empty($forum_mark_time[$forum_id]) && $forum_mark_time[$forum_id] !== false)
1543		{
1544			$mark_time[$forum_id] = $forum_mark_time[$forum_id];
1545		}
1546
1547		$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
1548
1549		foreach ($topic_ids as $topic_id)
1550		{
1551			if ($global_announce_list && isset($global_announce_list[$topic_id]))
1552			{
1553				$last_read[$topic_id] = (isset($mark_time[0])) ? $mark_time[0] : $user_lastmark;
1554			}
1555			else
1556			{
1557				$last_read[$topic_id] = $user_lastmark;
1558			}
1559		}
1560	}
1561
1562	return $last_read;
1563}
1564
1565/**
1566* Get topic tracking info from db (for cookie based tracking only this function is used)
1567*/
1568function get_complete_topic_tracking($forum_id, $topic_ids, $global_announce_list = false)
1569{
1570	global $config, $user;
1571
1572	$last_read = array();
1573
1574	if (!is_array($topic_ids))
1575	{
1576		$topic_ids = array($topic_ids);
1577	}
1578
1579	if ($config['load_db_lastread'] && $user->data['is_registered'])
1580	{
1581		global $db;
1582
1583		$sql = 'SELECT topic_id, mark_time
1584			FROM ' . TOPICS_TRACK_TABLE . "
1585			WHERE user_id = {$user->data['user_id']}
1586				AND " . $db->sql_in_set('topic_id', $topic_ids);
1587		$result = $db->sql_query($sql);
1588
1589		while ($row = $db->sql_fetchrow($result))
1590		{
1591			$last_read[$row['topic_id']] = $row['mark_time'];
1592		}
1593		$db->sql_freeresult($result);
1594
1595		$topic_ids = array_diff($topic_ids, array_keys($last_read));
1596
1597		if (sizeof($topic_ids))
1598		{
1599			$sql = 'SELECT forum_id, mark_time
1600				FROM ' . FORUMS_TRACK_TABLE . "
1601				WHERE user_id = {$user->data['user_id']}
1602					AND forum_id " .
1603					(($global_announce_list && sizeof($global_announce_list)) ? "IN (0, $forum_id)" : "= $forum_id");
1604			$result = $db->sql_query($sql);
1605
1606			$mark_time = array();
1607			while ($row = $db->sql_fetchrow($result))
1608			{
1609				$mark_time[$row['forum_id']] = $row['mark_time'];
1610			}
1611			$db->sql_freeresult($result);
1612
1613			$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
1614
1615			foreach ($topic_ids as $topic_id)
1616			{
1617				if ($global_announce_list && isset($global_announce_list[$topic_id]))
1618				{
1619					$last_read[$topic_id] = (isset($mark_time[0])) ? $mark_time[0] : $user_lastmark;
1620				}
1621				else
1622				{
1623					$last_read[$topic_id] = $user_lastmark;
1624				}
1625			}
1626		}
1627	}
1628	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1629	{
1630		global $tracking_topics;
1631
1632		if (!isset($tracking_topics) || !sizeof($tracking_topics))
1633		{
1634			$tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
1635			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1636		}
1637
1638		if (!$user->data['is_registered'])
1639		{
1640			$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
1641		}
1642		else
1643		{
1644			$user_lastmark = $user->data['user_lastmark'];
1645		}
1646
1647		foreach ($topic_ids as $topic_id)
1648		{
1649			$topic_id36 = base_convert($topic_id, 10, 36);
1650
1651			if (isset($tracking_topics['t'][$topic_id36]))
1652			{
1653				$last_read[$topic_id] = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
1654			}
1655		}
1656
1657		$topic_ids = array_diff($topic_ids, array_keys($last_read));
1658
1659		if (sizeof($topic_ids))
1660		{
1661			$mark_time = array();
1662			if ($global_announce_list && sizeof($global_announce_list))
1663			{
1664				if (isset($tracking_topics['f'][0]))
1665				{
1666					$mark_time[0] = base_convert($tracking_topics['f'][0], 36, 10) + $config['board_startdate'];
1667				}
1668			}
1669
1670			if (isset($tracking_topics['f'][$forum_id]))
1671			{
1672				$mark_time[$forum_id] = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
1673			}
1674
1675			$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user_lastmark;
1676
1677			foreach ($topic_ids as $topic_id)
1678			{
1679				if ($global_announce_list && isset($global_announce_list[$topic_id]))
1680				{
1681					$last_read[$topic_id] = (isset($mark_time[0])) ? $mark_time[0] : $user_lastmark;
1682				}
1683				else
1684				{
1685					$last_read[$topic_id] = $user_lastmark;
1686				}
1687			}
1688		}
1689	}
1690
1691	return $last_read;
1692}
1693
1694/**
1695* Get list of unread topics
1696*
1697* @param int $user_id			User ID (or false for current user)
1698* @param string $sql_extra		Extra WHERE SQL statement
1699* @param string $sql_sort		ORDER BY SQL sorting statement
1700* @param string $sql_limit		Limits the size of unread topics list, 0 for unlimited query
1701* @param string $sql_limit_offset  Sets the offset of the first row to search, 0 to search from the start
1702*
1703* @return array[int][int]		Topic ids as keys, mark_time of topic as value
1704*/
1705function get_unread_topics($user_id = false, $sql_extra = '', $sql_sort = '', $sql_limit = 1001, $sql_limit_offset = 0)
1706{
1707	global $config, $db, $user;
1708
1709	$user_id = ($user_id === false) ? (int) $user->data['user_id'] : (int) $user_id;
1710
1711	// Data array we're going to return
1712	$unread_topics = array();
1713
1714	if (empty($sql_sort))
1715	{
1716		$sql_sort = 'ORDER BY t.topic_last_post_time DESC';
1717	}
1718
1719	if ($config['load_db_lastread'] && $user->data['is_registered'])
1720	{
1721		// Get list of the unread topics
1722		$last_mark = (int) $user->data['user_lastmark'];
1723
1724		$sql_array = array(
1725			'SELECT'		=> 't.topic_id, t.topic_last_post_time, tt.mark_time as topic_mark_time, ft.mark_time as forum_mark_time',
1726
1727			'FROM'			=> array(TOPICS_TABLE => 't'),
1728
1729			'LEFT_JOIN'		=> array(
1730				array(
1731					'FROM'	=> array(TOPICS_TRACK_TABLE => 'tt'),
1732					'ON'	=> "tt.user_id = $user_id AND t.topic_id = tt.topic_id",
1733				),
1734				array(
1735					'FROM'	=> array(FORUMS_TRACK_TABLE => 'ft'),
1736					'ON'	=> "ft.user_id = $user_id AND t.forum_id = ft.forum_id",
1737				),
1738			),
1739
1740			'WHERE'			=> "
1741				 t.topic_last_post_time > $last_mark AND
1742				(
1743				(tt.mark_time IS NOT NULL AND t.topic_last_post_time > tt.mark_time) OR
1744				(tt.mark_time IS NULL AND ft.mark_time IS NOT NULL AND t.topic_last_post_time > ft.mark_time) OR
1745				(tt.mark_time IS NULL AND ft.mark_time IS NULL)
1746				)
1747				$sql_extra
1748				$sql_sort",
1749		);
1750
1751		$sql = $db->sql_build_query('SELECT', $sql_array);
1752		$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
1753
1754		while ($row = $db->sql_fetchrow($result))
1755		{
1756			$topic_id = (int) $row['topic_id'];
1757			$unread_topics[$topic_id] = ($row['topic_mark_time']) ? (int) $row['topic_mark_time'] : (($row['forum_mark_time']) ? (int) $row['forum_mark_time'] : $last_mark);
1758		}
1759		$db->sql_freeresult($result);
1760	}
1761	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1762	{
1763		global $tracking_topics;
1764
1765		if (empty($tracking_topics))
1766		{
1767			$tracking_topics = request_var($config['cookie_name'] . '_track', '', false, true);
1768			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1769		}
1770
1771		if (!$user->data['is_registered'])
1772		{
1773			$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
1774		}
1775		else
1776		{
1777			$user_lastmark = (int) $user->data['user_lastmark'];
1778		}
1779
1780		$sql = 'SELECT t.topic_id, t.forum_id, t.topic_last_post_time
1781			FROM ' . TOPICS_TABLE . ' t
1782			WHERE t.topic_last_post_time > ' . $user_lastmark . "
1783			$sql_extra
1784			$sql_sort";
1785		$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
1786
1787		while ($row = $db->sql_fetchrow($result))
1788		{
1789			$forum_id = (int) $row['forum_id'];
1790			$topic_id = (int) $row['topic_id'];
1791			$topic_id36 = base_convert($topic_id, 10, 36);
1792
1793			if (isset($tracking_topics['t'][$topic_id36]))
1794			{
1795				$last_read = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
1796
1797				if ($row['topic_last_post_time'] > $last_read)
1798				{
1799					$unread_topics[$topic_id] = $last_read;
1800				}
1801			}
1802			else if (isset($tracking_topics['f'][$forum_id]))
1803			{
1804				$mark_time = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
1805
1806				if ($row['topic_last_post_time'] > $mark_time)
1807				{
1808					$unread_topics[$topic_id] = $mark_time;
1809				}
1810			}
1811			else
1812			{
1813				$unread_topics[$topic_id] = $user_lastmark;
1814			}
1815		}
1816		$db->sql_freeresult($result);
1817	}
1818
1819	return $unread_topics;
1820}
1821
1822/**
1823* Check for read forums and update topic tracking info accordingly
1824*
1825* @param int $forum_id the forum id to check
1826* @param int $forum_last_post_time the forums last post time
1827* @param int $f_mark_time the forums last mark time if user is registered and load_db_lastread enabled
1828* @param int $mark_time_forum false if the mark time needs to be obtained, else the last users forum mark time
1829*
1830* @return true if complete forum got marked read, else false.
1831*/
1832function update_forum_tracking_info($forum_id, $forum_last_post_time, $f_mark_time = false, $mark_time_forum = false)
1833{
1834	global $db, $tracking_topics, $user, $config;
1835
1836	// Determine the users last forum mark time if not given.
1837	if ($mark_time_forum === false)
1838	{
1839		if ($config['load_db_lastread'] && $user->data['is_registered'])
1840		{
1841			$mark_time_forum = (!empty($f_mark_time)) ? $f_mark_time : $user->data['user_lastmark'];
1842		}
1843		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1844		{
1845			$tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
1846			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
1847
1848			if (!$user->data['is_registered'])
1849			{
1850				$user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate']) : 0;
1851			}
1852
1853			$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'];
1854		}
1855	}
1856
1857	// Check the forum for any left unread topics.
1858	// If there are none, we mark the forum as read.
1859	if ($config['load_db_lastread'] && $user->data['is_registered'])
1860	{
1861		if ($mark_time_forum >= $forum_last_post_time)
1862		{
1863			// We do not need to mark read, this happened before. Therefore setting this to true
1864			$row = true;
1865		}
1866		else
1867		{
1868			$sql = 'SELECT t.forum_id FROM ' . TOPICS_TABLE . ' t
1869				LEFT JOIN ' . TOPICS_TRACK_TABLE . ' tt ON (tt.topic_id = t.topic_id AND tt.user_id = ' . $user->data['user_id'] . ')
1870				WHERE t.forum_id = ' . $forum_id . '
1871					AND t.topic_last_post_time > ' . $mark_time_forum . '
1872					AND t.topic_moved_id = 0
1873					AND (tt.topic_id IS NULL OR tt.mark_time < t.topic_last_post_time)
1874				GROUP BY t.forum_id';
1875			$result = $db->sql_query_limit($sql, 1);
1876			$row = $db->sql_fetchrow($result);
1877			$db->sql_freeresult($result);
1878		}
1879	}
1880	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
1881	{
1882		// Get information from cookie
1883		$row = false;
1884
1885		if (!isset($tracking_topics['tf'][$forum_id]))
1886		{
1887			// We do not need to mark read, this happened before. Therefore setting this to true
1888			$row = true;
1889		}
1890		else
1891		{
1892			$sql = 'SELECT topic_id
1893				FROM ' . TOPICS_TABLE . '
1894				WHERE forum_id = ' . $forum_id . '
1895					AND topic_last_post_time > ' . $mark_time_forum . '
1896					AND topic_moved_id = 0';
1897			$result = $db->sql_query($sql);
1898
1899			$check_forum = $tracking_topics['tf'][$forum_id];
1900			$unread = false;
1901
1902			while ($row = $db->sql_fetchrow($result))
1903			{
1904				if (!isset($check_forum[base_convert($row['topic_id'], 10, 36)]))
1905				{
1906					$unread = true;
1907					break;
1908				}
1909			}
1910			$db->sql_freeresult($result);
1911
1912			$row = $unread;
1913		}
1914	}
1915	else
1916	{
1917		$row = true;
1918	}
1919
1920	if (!$row)
1921	{
1922		markread('topics', $forum_id);
1923		return true;
1924	}
1925
1926	r…

Large files files are truncated, but you can click here to view the full file