PageRenderTime 11ms CodeModel.GetById 4ms app.highlight 84ms RepoModel.GetById 1ms app.codeStats 1ms

/phpBB/includes/functions.php

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

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