Issues (1065)

Sources/Load.php (2 issues)

1
<?php
2
3
/**
4
 * This file has the hefty job of loading information for the forum.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2025 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1.5
14
 */
15
16
use SMF\Cache\CacheApi;
17
use SMF\Cache\CacheApiInterface;
18
19
if (!defined('SMF'))
20
	die('No direct access...');
21
22
/**
23
 * Load the $modSettings array.
24
 */
25
function reloadSettings()
26
{
27
	global $modSettings, $boarddir, $smcFunc, $txt, $db_character_set;
28
	global $cache_enable, $sourcedir, $context, $forum_version, $boardurl;
29
	global $image_proxy_enabled;
30
31
	// Most database systems have not set UTF-8 as their default input charset.
32
	if (empty($db_character_set))
33
		$db_character_set = 'utf8';
34
	$smcFunc['db_query']('', '
35
		SET NAMES {string:db_character_set}',
36
		array(
37
			'db_character_set' => $db_character_set,
38
		)
39
	);
40
41
	// We need some caching support, maybe.
42
	loadCacheAccelerator();
43
44
	// Try to load it from the cache first; it'll never get cached if the setting is off.
45
	if (($modSettings = cache_get_data('modSettings', 90)) == null)
46
	{
47
		$request = $smcFunc['db_query']('', '
48
			SELECT variable, value
49
			FROM {db_prefix}settings',
50
			array(
51
			)
52
		);
53
		$modSettings = array();
54
		if (!$request)
55
			display_db_error();
56
		foreach ($smcFunc['db_fetch_all']($request) as $row)
57
			$modSettings[$row['variable']] = $row['value'];
58
		$smcFunc['db_free_result']($request);
59
60
		// Do a few things to protect against missing settings or settings with invalid values...
61
		if (empty($modSettings['defaultMaxTopics']) || $modSettings['defaultMaxTopics'] <= 0 || $modSettings['defaultMaxTopics'] > 999)
62
			$modSettings['defaultMaxTopics'] = 20;
63
		if (empty($modSettings['defaultMaxMessages']) || $modSettings['defaultMaxMessages'] <= 0 || $modSettings['defaultMaxMessages'] > 999)
64
			$modSettings['defaultMaxMessages'] = 15;
65
		if (empty($modSettings['defaultMaxMembers']) || $modSettings['defaultMaxMembers'] <= 0 || $modSettings['defaultMaxMembers'] > 999)
66
			$modSettings['defaultMaxMembers'] = 30;
67
		if (empty($modSettings['defaultMaxListItems']) || $modSettings['defaultMaxListItems'] <= 0 || $modSettings['defaultMaxListItems'] > 999)
68
			$modSettings['defaultMaxListItems'] = 15;
69
70
		// We explicitly do not use $smcFunc['json_decode'] here yet, as $smcFunc is not fully loaded.
71
		if (!is_array($modSettings['attachmentUploadDir']))
72
		{
73
			$attachmentUploadDir = smf_json_decode($modSettings['attachmentUploadDir'], true, false);
74
			$modSettings['attachmentUploadDir'] = !empty($attachmentUploadDir) ? $attachmentUploadDir : $modSettings['attachmentUploadDir'];
75
		}
76
77
		if (!empty($cache_enable))
78
			cache_put_data('modSettings', $modSettings, 90);
79
	}
80
81
	// Going anything further when the files don't match the database can make nasty messes (unless we're actively installing or upgrading)
82
	if (!defined('SMF_INSTALLING') && (!isset($_REQUEST['action']) || $_REQUEST['action'] !== 'admin' || !isset($_REQUEST['area']) || $_REQUEST['area'] !== 'packages') && !empty($modSettings['smfVersion']) && version_compare(strtolower(strtr($modSettings['smfVersion'], array(' ' => '.'))), strtolower(strtr(SMF_VERSION, array(' ' => '.'))), '!='))
83
	{
84
		// Wipe the cached $modSettings values so they don't interfere with anything later
85
		cache_put_data('modSettings', null);
86
87
		// Redirect to the upgrader if we can
88
		if (file_exists($boarddir . '/upgrade.php'))
89
			header('location: ' . $boardurl . '/upgrade.php');
90
91
		die('SMF file version (' . SMF_VERSION . ') does not match SMF database version (' . $modSettings['smfVersion'] . ').<br>Run the SMF upgrader to fix this.<br><a href="https://wiki.simplemachines.org/smf/Upgrading">More information</a>.');
92
	}
93
94
	$modSettings['cache_enable'] = $cache_enable;
95
96
	// Used to force browsers to download fresh CSS and JavaScript when necessary
97
	$modSettings['browser_cache'] = !empty($modSettings['browser_cache']) ? (int) $modSettings['browser_cache'] : 0;
98
	$context['browser_cache'] = '?' . preg_replace('~\W~', '', strtolower(SMF_FULL_VERSION)) . '_' . $modSettings['browser_cache'];
99
100
	// Disable image proxy if we don't have SSL enabled
101
	if (empty($modSettings['force_ssl']))
102
		$image_proxy_enabled = false;
103
104
	// UTF-8 ?
105
	$utf8 = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
106
	$context['utf8'] = $utf8;
107
108
	// Set a list of common functions.
109
	$ent_list = '&(?:#' . (empty($modSettings['disableEntityCheck']) ? '\d{1,7}' : '021') . '|quot|amp|lt|gt|nbsp);';
110
	$ent_check = empty($modSettings['disableEntityCheck']) ? function($string)
111
		{
112
			$string = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', (string) $string);
113
			return $string;
114
		} : function($string)
115
		{
116
			return (string) $string;
117
		};
118
	$smcFunc['fix_utf8mb4'] = function($string) use ($utf8, $smcFunc)
119
	{
120
		if (!$utf8 || $smcFunc['db_mb4'])
121
			return $string;
122
123
		$string = (string) $string;
124
125
		$i = 0;
126
		$len = strlen($string);
127
		$new_string = '';
128
		while ($i < $len)
129
		{
130
			$ord = ord($string[$i]);
131
			if ($ord < 128)
132
			{
133
				$new_string .= $string[$i];
134
				$i++;
135
			}
136
			elseif ($ord < 224)
137
			{
138
				$new_string .= $string[$i] . $string[$i + 1];
139
				$i += 2;
140
			}
141
			elseif ($ord < 240)
142
			{
143
				$new_string .= $string[$i] . $string[$i + 1] . $string[$i + 2];
144
				$i += 3;
145
			}
146
			elseif ($ord < 248)
147
			{
148
				// Magic happens.
149
				$val = (ord($string[$i]) & 0x07) << 18;
150
				$val += (ord($string[$i + 1]) & 0x3F) << 12;
151
				$val += (ord($string[$i + 2]) & 0x3F) << 6;
152
				$val += (ord($string[$i + 3]) & 0x3F);
153
				$new_string .= '&#' . $val . ';';
154
				$i += 4;
155
			}
156
		}
157
		return $new_string;
158
	};
159
160
	// global array of anonymous helper functions, used mostly to properly handle multi byte strings
161
	$smcFunc += array(
162
		'entity_fix' => function($string)
163
		{
164
			$num = $string[0] === 'x' ? hexdec(substr($string, 1)) : (int) $string;
165
			return $num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202E || $num === 0x202D ? '' : '&#' . $num . ';';
166
		},
167
		'htmlspecialchars' => function($string, $quote_style = ENT_COMPAT, $charset = 'ISO-8859-1') use ($ent_check, $utf8, &$smcFunc)
168
		{
169
			$string = $smcFunc['normalize']($string);
170
171
			return $smcFunc['fix_utf8mb4']($ent_check(htmlspecialchars($string, $quote_style, $utf8 ? 'UTF-8' : $charset)));
172
		},
173
		'htmltrim' => function($string) use ($utf8, $ent_check)
174
		{
175
			// Preg_replace space characters depend on the character set in use
176
			$space_chars = $utf8 ? '\p{Z}\p{C}' : '\x00-\x20\x80-\xA0';
177
178
			return preg_replace('~^(?:[' . $space_chars . ']|&nbsp;)+|(?:[' . $space_chars . ']|&nbsp;)+$~' . ($utf8 ? 'u' : ''), '', $ent_check($string));
179
		},
180
		'strlen' => function($string) use ($ent_list, $utf8, $ent_check)
181
		{
182
			return strlen(preg_replace('~' . $ent_list . ($utf8 ? '|.~u' : '~'), '_', $ent_check($string)));
183
		},
184
		'strpos' => function($haystack, $needle, $offset = 0) use ($utf8, $ent_check, $ent_list, $modSettings)
185
		{
186
			$haystack_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : ''), $ent_check($haystack), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
187
188
			if (strlen($needle) === 1)
189
			{
190
				$result = array_search($needle, array_slice($haystack_arr, $offset));
191
				return is_int($result) ? $result + $offset : false;
192
			}
193
			else
194
			{
195
				$needle_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : '') . '', $ent_check($needle), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
196
				$needle_size = count($needle_arr);
197
198
				$result = array_search($needle_arr[0], array_slice($haystack_arr, $offset));
199
				while ((int) $result === $result)
200
				{
201
					$offset += $result;
202
					if (array_slice($haystack_arr, $offset, $needle_size) === $needle_arr)
203
						return $offset;
204
					$result = array_search($needle_arr[0], array_slice($haystack_arr, ++$offset));
205
				}
206
				return false;
207
			}
208
		},
209
		'substr' => function($string, $start, $length = null) use ($utf8, $ent_check, $ent_list, $modSettings)
210
		{
211
			$ent_arr = preg_split('~(' . $ent_list . '|.)~' . ($utf8 ? 'u' : '') . '', $ent_check($string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
212
			return $length === null ? implode('', array_slice($ent_arr, $start)) : implode('', array_slice($ent_arr, $start, $length));
213
		},
214
		'strtolower' => function($string) use (&$smcFunc)
215
		{
216
			return $smcFunc['convert_case']($string, 'lower');
217
		},
218
		'strtoupper' => function($string) use (&$smcFunc)
219
		{
220
			return $smcFunc['convert_case']($string, 'upper');
221
		},
222
		'truncate' => function($string, $length) use ($utf8, $ent_check, $ent_list, &$smcFunc)
223
		{
224
			$string = $ent_check($string);
225
			preg_match('~^(' . $ent_list . '|.){' . $smcFunc['strlen'](substr($string, 0, $length)) . '}~' . ($utf8 ? 'u' : ''), $string, $matches);
226
			$string = $matches[0];
227
			while (strlen($string) > $length)
228
				$string = preg_replace('~(?:' . $ent_list . '|.)$~' . ($utf8 ? 'u' : ''), '', $string);
229
			return $string;
230
		},
231
		'ucfirst' => function($string) use (&$smcFunc)
232
		{
233
			return $smcFunc['convert_case']($string, 'ucfirst');
234
		},
235
		'ucwords' => function($string) use (&$smcFunc)
236
		{
237
			return $smcFunc['convert_case']($string, 'ucwords');
238
		},
239
		'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $sourcedir)
240
		{
241
			if (!$utf8)
242
			{
243
				switch ($case)
244
				{
245
					case 'upper':
246
						$string = strtoupper($string);
247
						break;
248
249
					case 'lower':
250
					case 'fold';
251
						$string = strtolower($string);
252
						break;
253
254
					case 'title':
255
						$string = ucwords(strtolower($string));
256
						break;
257
258
					case 'ucwords':
259
						$string = ucwords($string);
260
						break;
261
262
					case 'ucfirst':
263
						$string = ucfirst($string);
264
						break;
265
266
					default:
267
						break;
268
				}
269
			}
270
			else
271
			{
272
				// Convert numeric entities to characters, except special ones.
273
				if (function_exists('mb_decode_numericentity') && strpos($string, '&#') !== false)
274
				{
275
					$string = strtr($ent_check($string), array(
276
						'&#34;' => '&quot;',
277
						'&#38;' => '&amp;',
278
						'&#39;' => '&apos;',
279
						'&#60;' => '&lt;',
280
						'&#62;' => '&gt;',
281
						'&#160;' => '&nbsp;',
282
					));
283
284
					$string = mb_decode_numericentity($string, array(0, 0x10FFFF, 0, 0xFFFFFF), 'UTF-8');
285
				}
286
287
				// Use optmized function for compatibility casefolding.
288
				if ($form === 'kc_casefold' || ($case === 'fold' && $form === 'kc'))
289
				{
290
					$string = $smcFunc['normalize']($string, 'kc_casefold');
291
				}
292
				// Everything else.
293
				else
294
				{
295
					require_once($sourcedir . '/Subs-Charset.php');
296
					$string = $smcFunc['normalize'](utf8_convert_case($string, $case, $simple), $form);
297
				}
298
			}
299
300
			return $smcFunc['fix_utf8mb4']($string);
301
		},
302
		'json_decode' => 'smf_json_decode',
303
		'json_encode' => 'json_encode',
304
		'random_int' => function($min = 0, $max = PHP_INT_MAX)
305
		{
306
			global $sourcedir;
307
308
			// Oh, wouldn't it be great if I *was* crazy? Then the world would be okay.
309
			if (!is_callable('random_int'))
310
				require_once($sourcedir . '/random_compat/random.php');
311
312
			return random_int($min, $max);
313
		},
314
		'random_bytes' => function($length = 64)
315
		{
316
			global $sourcedir;
317
318
			if (!is_callable('random_bytes'))
319
				require_once($sourcedir . '/random_compat/random.php');
320
321
			// Make sure length is valid
322
			$length = max(1, (int) $length);
323
324
			return random_bytes($length);
325
		},
326
		'normalize' => function($string, $form = 'c') use ($utf8)
327
		{
328
			global $sourcedir;
329
330
			$string = (string) $string;
331
332
			if (!$utf8)
333
				return $string;
334
335
			require_once($sourcedir . '/Subs-Charset.php');
336
337
			$normalize_func = 'utf8_normalize_' . strtolower((string) $form);
338
339
			if (!function_exists($normalize_func))
340
				return false;
341
342
			return $normalize_func($string);
343
		},
344
	);
345
346
	// Setting the timezone is a requirement for some functions.
347
	if (isset($modSettings['default_timezone']) && in_array($modSettings['default_timezone'], timezone_identifiers_list()))
348
		date_default_timezone_set($modSettings['default_timezone']);
349
	else
350
	{
351
		// Get PHP's default timezone, if set
352
		$ini_tz = ini_get('date.timezone');
353
		if (!empty($ini_tz))
354
			$modSettings['default_timezone'] = $ini_tz;
355
		else
356
			$modSettings['default_timezone'] = '';
357
358
		// If date.timezone is unset, invalid, or just plain weird, make a best guess
359
		if (!in_array($modSettings['default_timezone'], timezone_identifiers_list()))
360
		{
361
			$server_offset = @mktime(0, 0, 0, 1, 1, 1970) * -1;
362
			$modSettings['default_timezone'] = timezone_name_from_abbr('', $server_offset, 0);
363
364
			if (empty($modSettings['default_timezone']))
365
				$modSettings['default_timezone'] = 'UTC';
366
		}
367
368
		date_default_timezone_set($modSettings['default_timezone']);
369
	}
370
371
	// Check the load averages?
372
	if (!empty($modSettings['loadavg_enable']))
373
	{
374
		if (($modSettings['load_average'] = cache_get_data('loadavg', 90)) == null)
375
		{
376
			$modSettings['load_average'] = @file_get_contents('/proc/loadavg');
377
			if (!empty($modSettings['load_average']) && preg_match('~^([^ ]+?) ([^ ]+?) ([^ ]+)~', $modSettings['load_average'], $matches) != 0)
378
				$modSettings['load_average'] = (float) $matches[1];
379
			elseif (($modSettings['load_average'] = @`uptime`) != null && preg_match('~load average[s]?: (\d+\.\d+), (\d+\.\d+), (\d+\.\d+)~i', $modSettings['load_average'], $matches) != 0)
380
				$modSettings['load_average'] = (float) $matches[1];
381
			else
382
				unset($modSettings['load_average']);
383
384
			if (!empty($modSettings['load_average']) || $modSettings['load_average'] === 0.0)
385
				cache_put_data('loadavg', $modSettings['load_average'], 90);
386
		}
387
388
		if (!empty($modSettings['load_average']) || $modSettings['load_average'] === 0.0)
389
			call_integration_hook('integrate_load_average', array($modSettings['load_average']));
390
391
		if (!empty($modSettings['loadavg_forum']) && !empty($modSettings['load_average']) && $modSettings['load_average'] >= $modSettings['loadavg_forum'])
392
			display_loadavg_error();
393
	}
394
395
	// Ensure we know who can manage boards.
396
	if (!isset($modSettings['board_manager_groups']))
397
	{
398
		require_once($sourcedir . '/Subs-Members.php');
399
		$board_managers = groupsAllowedTo('manage_boards', null);
400
		$board_managers = implode(',', $board_managers['allowed']);
401
		updateSettings(array('board_manager_groups' => $board_managers));
402
	}
403
404
	// Is post moderation alive and well? Everywhere else assumes this has been defined, so let's make sure it is.
405
	$modSettings['postmod_active'] = !empty($modSettings['postmod_active']);
406
407
	// Here to justify the name of this function. :P
408
	// It should be added to the install and upgrade scripts.
409
	// But since the converters need to be updated also. This is easier.
410
	if (empty($modSettings['currentAttachmentUploadDir']))
411
	{
412
		updateSettings(array(
413
			'attachmentUploadDir' => $smcFunc['json_encode'](array(1 => $modSettings['attachmentUploadDir'])),
414
			'currentAttachmentUploadDir' => 1,
415
		));
416
	}
417
418
	// Respect PHP's limits.
419
	$post_max_kb = floor(memoryReturnBytes(ini_get('post_max_size')) / 1024);
420
	$file_max_kb = floor(memoryReturnBytes(ini_get('upload_max_filesize')) / 1024);
421
	$modSettings['attachmentPostLimit'] = empty($modSettings['attachmentPostLimit']) ? $post_max_kb : min($modSettings['attachmentPostLimit'], $post_max_kb);
422
	$modSettings['attachmentSizeLimit'] = empty($modSettings['attachmentSizeLimit']) ? $file_max_kb : min($modSettings['attachmentSizeLimit'], $file_max_kb);
423
	$modSettings['attachmentNumPerPostLimit'] = !isset($modSettings['attachmentNumPerPostLimit']) ? 4 : $modSettings['attachmentNumPerPostLimit'];
424
425
	// Integration is cool.
426
	if (defined('SMF_INTEGRATION_SETTINGS'))
427
	{
428
		$integration_settings = $smcFunc['json_decode'](SMF_INTEGRATION_SETTINGS, true);
429
		foreach ($integration_settings as $hook => $function)
430
			add_integration_function($hook, $function, false);
431
	}
432
433
	// Any files to pre include?
434
	if (!empty($modSettings['integrate_pre_include']))
435
	{
436
		$pre_includes = explode(',', $modSettings['integrate_pre_include']);
437
		foreach ($pre_includes as $include)
438
		{
439
			$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
440
			if (file_exists($include))
441
				require_once($include);
442
		}
443
	}
444
445
	// This determines the server... not used in many places, except for login fixing.
446
	$context['server'] = array(
447
		'is_iis' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false,
448
		'is_apache' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false,
449
		'is_litespeed' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') !== false,
450
		'is_lighttpd' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false,
451
		'is_nginx' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false,
452
		'is_cgi' => isset($_SERVER['SERVER_SOFTWARE']) && strpos(php_sapi_name(), 'cgi') !== false,
453
		'is_windows' => DIRECTORY_SEPARATOR === '\\',
454
		'iso_case_folding' => ord(strtolower(chr(138))) === 154,
455
	);
456
	// A bug in some versions of IIS under CGI (older ones) makes cookie setting not work with Location: headers.
457
	$context['server']['needs_login_fix'] = $context['server']['is_cgi'] && $context['server']['is_iis'];
458
459
	// Define a list of icons used across multiple places.
460
	$context['stable_icons'] = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'poll', 'moved', 'recycled', 'clip');
461
462
	// Define an array for custom profile fields placements.
463
	$context['cust_profile_fields_placement'] = array(
464
		'standard',
465
		'icons',
466
		'above_signature',
467
		'below_signature',
468
		'below_avatar',
469
		'above_member',
470
		'bottom_poster',
471
		'before_member',
472
		'after_member',
473
	);
474
475
	// Define an array for content-related <meta> elements (e.g. description, keywords, Open Graph) for the HTML head.
476
	$context['meta_tags'] = array();
477
478
	// Define an array of allowed HTML tags.
479
	$context['allowed_html_tags'] = array(
480
		'<img>',
481
		'<div>',
482
	);
483
484
	// These are the only valid image types for SMF attachments, by default anyway.
485
	// Note: The values are for image mime types, not file extensions.
486
	$context['valid_image_types'] = array(
487
		IMAGETYPE_GIF => 'gif',
488
		IMAGETYPE_JPEG => 'jpeg',
489
		IMAGETYPE_PNG => 'png',
490
		IMAGETYPE_PSD => 'psd',
491
		IMAGETYPE_BMP => 'bmp',
492
		IMAGETYPE_TIFF_II => 'tiff',
493
		IMAGETYPE_TIFF_MM => 'tiff',
494
		IMAGETYPE_IFF => 'iff',
495
		IMAGETYPE_WEBP => 'webp'
496
	);
497
498
	// Define a list of allowed tags for descriptions.
499
	$context['description_allowed_tags'] = array(
500
		'abbr', 'anchor', 'b', 'br', 'center', 'color', 'font', 'hr', 'i', 'img',
501
		'iurl', 'left', 'li', 'list', 'ltr', 'pre', 'right', 's', 'sub',
502
		'sup', 'table', 'td', 'tr', 'u', 'url',
503
	);
504
505
	// Define a list of deprecated BBC tags
506
	// Even when enabled, they'll only work in old posts and not new ones
507
	$context['legacy_bbc'] = array(
508
		'acronym', 'bdo', 'black', 'blue', 'flash', 'ftp', 'glow',
509
		'green', 'move', 'red', 'shadow', 'tt', 'white',
510
	);
511
512
	// Define a list of BBC tags that require permissions to use
513
	$context['restricted_bbc'] = array(
514
		'html',
515
	);
516
517
	// Login Cookie times. Format: time => txt
518
	$context['login_cookie_times'] = array(
519
		3153600 => 'always_logged_in',
520
		60 => 'one_hour',
521
		1440 => 'one_day',
522
		10080 => 'one_week',
523
		43200 => 'one_month',
524
	);
525
526
	$context['show_spellchecking'] = false;
527
528
	// Call pre load integration functions.
529
	call_integration_hook('integrate_pre_load');
530
}
531
532
/**
533
 * Load all the important user information.
534
 * What it does:
535
 * 	- sets up the $user_info array
536
 * 	- assigns $user_info['query_wanna_see_board'] for what boards the user can see.
537
 * 	- first checks for cookie or integration validation.
538
 * 	- uses the current session if no integration function or cookie is found.
539
 * 	- checks password length, if member is activated and the login span isn't over.
540
 * 		- if validation fails for the user, $id_member is set to 0.
541
 * 		- updates the last visit time when needed.
542
 */
543
function loadUserSettings()
544
{
545
	global $modSettings, $user_settings, $sourcedir, $smcFunc;
546
	global $cookiename, $user_info, $language, $context, $cache_enable;
547
548
	require_once($sourcedir . '/Subs-Auth.php');
549
550
	// Check first the integration, then the cookie, and last the session.
551
	if (count($integration_ids = call_integration_hook('integrate_verify_user')) > 0)
552
	{
553
		$id_member = 0;
554
		foreach ($integration_ids as $integration_id)
555
		{
556
			$integration_id = (int) $integration_id;
557
			if ($integration_id > 0)
558
			{
559
				$id_member = $integration_id;
560
				$already_verified = true;
561
				break;
562
			}
563
		}
564
	}
565
	else
566
		$id_member = 0;
567
568
	if (empty($id_member) && isset($_COOKIE[$cookiename]))
569
	{
570
		// First try 2.1 json-format cookie
571
		$cookie_data = $smcFunc['json_decode']($_COOKIE[$cookiename], true, false);
572
573
		// Legacy format (for recent 2.0 --> 2.1 upgrades)
574
		if (empty($cookie_data))
575
			$cookie_data = safe_unserialize($_COOKIE[$cookiename]);
576
577
		list($id_member, $password, $login_span, $cookie_domain, $cookie_path) = array_pad((array) $cookie_data, 5, '');
578
579
		$id_member = !empty($id_member) && strlen($password) > 0 ? (int) $id_member : 0;
580
581
		// Make sure the cookie is set to the correct domain and path
582
		if (array($cookie_domain, $cookie_path) !== url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies'])))
583
			setLoginCookie((int) $login_span - time(), $id_member);
584
	}
585
	elseif (empty($id_member) && isset($_SESSION['login_' . $cookiename]) && ($_SESSION['USER_AGENT'] == $_SERVER['HTTP_USER_AGENT'] || !empty($modSettings['disableCheckUA'])))
586
	{
587
		// @todo Perhaps we can do some more checking on this, such as on the first octet of the IP?
588
		$cookie_data = $smcFunc['json_decode']($_SESSION['login_' . $cookiename], true);
589
590
		if (empty($cookie_data))
591
			$cookie_data = safe_unserialize($_SESSION['login_' . $cookiename]);
592
593
		list($id_member, $password, $login_span) = array_pad((array) $cookie_data, 3, '');
594
		$id_member = !empty($id_member) && strlen($password) == 40 && (int) $login_span > time() ? (int) $id_member : 0;
595
	}
596
597
	// Only load this stuff if the user isn't a guest.
598
	if ($id_member != 0)
599
	{
600
		// Is the member data cached?
601
		if (empty($cache_enable) || $cache_enable < 2 || ($user_settings = cache_get_data('user_settings-' . $id_member, 60)) == null)
602
		{
603
			$request = $smcFunc['db_query']('', '
604
				SELECT mem.*, COALESCE(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type, a.width AS "attachment_width", a.height AS "attachment_height"
605
				FROM {db_prefix}members AS mem
606
					LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = {int:id_member})
607
				WHERE mem.id_member = {int:id_member}
608
				LIMIT 1',
609
				array(
610
					'id_member' => $id_member,
611
				)
612
			);
613
			$user_settings = $smcFunc['db_fetch_assoc']($request);
614
			$smcFunc['db_free_result']($request);
615
616
			if (!empty($user_settings['avatar']))
617
				$user_settings['avatar'] = get_proxied_url($user_settings['avatar']);
618
619
			if (!empty($cache_enable) && $cache_enable >= 2)
620
				cache_put_data('user_settings-' . $id_member, $user_settings, 60);
621
		}
622
623
		// Did we find 'im?  If not, junk it.
624
		if (!empty($user_settings))
625
		{
626
			// As much as the password should be right, we can assume the integration set things up.
627
			if (!empty($already_verified) && $already_verified === true)
628
				$check = true;
629
			// SHA-512 hash should be 128 characters long.
630
			elseif (strlen($password) == 128)
631
				$check = hash_equals(hash_salt($user_settings['passwd'], $user_settings['password_salt']), $password);
632
			else
633
				$check = false;
634
635
			// Wrong password or not activated - either way, you're going nowhere.
636
			$id_member = $check && ($user_settings['is_activated'] == 1 || $user_settings['is_activated'] == 11) ? (int) $user_settings['id_member'] : 0;
637
		}
638
		else
639
			$id_member = 0;
640
641
		// Check if we are forcing TFA
642
		$force_tfasetup = !empty($modSettings['tfa_mode']) && $modSettings['tfa_mode'] >= 2 && $id_member && empty($user_settings['tfa_secret']) && SMF != 'SSI' && !isset($_REQUEST['xml']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != '.xml');
643
644
		// Don't force TFA on popups
645
		if ($force_tfasetup)
646
		{
647
			if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'profile' && isset($_REQUEST['area']) && in_array($_REQUEST['area'], array('popup', 'alerts_popup')))
648
				$force_tfasetup = false;
649
			elseif (isset($_REQUEST['action']) && $_REQUEST['action'] == 'pm' && (isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'popup'))
650
				$force_tfasetup = false;
651
652
			call_integration_hook('integrate_force_tfasetup', array(&$force_tfasetup));
653
		}
654
655
		// If we no longer have the member maybe they're being all hackey, stop brute force!
656
		if (!$id_member)
657
		{
658
			require_once($sourcedir . '/LogInOut.php');
659
			validatePasswordFlood(
660
				!empty($user_settings['id_member']) ? $user_settings['id_member'] : $id_member,
661
				!empty($user_settings['member_name']) ? $user_settings['member_name'] : '',
662
				!empty($user_settings['passwd_flood']) ? $user_settings['passwd_flood'] : false,
663
				$id_member != 0
664
			);
665
		}
666
		// Validate for Two Factor Authentication
667
		elseif (!empty($modSettings['tfa_mode']) && $id_member && !empty($user_settings['tfa_secret']) && (empty($_REQUEST['action']) || !in_array($_REQUEST['action'], array('login2', 'logintfa'))))
668
		{
669
			$tfacookie = $cookiename . '_tfa';
670
			$tfasecret = null;
671
672
			$verified = call_integration_hook('integrate_verify_tfa', array($id_member, $user_settings));
673
674
			if (empty($verified) || !in_array(true, $verified))
675
			{
676
				if (!empty($_COOKIE[$tfacookie]))
677
				{
678
					$tfa_data = $smcFunc['json_decode']($_COOKIE[$tfacookie], true);
679
680
					list ($tfamember, $tfasecret) = array_pad((array) $tfa_data, 2, '');
681
682
					if (!isset($tfamember, $tfasecret) || (int) $tfamember != $id_member)
683
						$tfasecret = null;
684
				}
685
686
				// They didn't finish logging in before coming here? Then they're no one to us.
687
				if (empty($tfasecret) || !hash_equals(hash_salt($user_settings['tfa_backup'], $user_settings['password_salt']), $tfasecret))
688
				{
689
					setLoginCookie(-3600, $id_member);
690
					$id_member = 0;
691
					$user_settings = array();
692
				}
693
			}
694
		}
695
		// When authenticating their two factor code, make sure to reset their ID for security
696
		elseif (!empty($modSettings['tfa_mode']) && $id_member && !empty($user_settings['tfa_secret']) && $_REQUEST['action'] == 'logintfa')
697
		{
698
			$id_member = 0;
699
			$context['tfa_member'] = $user_settings;
700
			$user_settings = array();
701
		}
702
		// Are we forcing 2FA? Need to check if the user groups actually require 2FA
703
		elseif ($force_tfasetup)
704
		{
705
			if ($modSettings['tfa_mode'] == 2) //only do this if we are just forcing SOME membergroups
706
			{
707
				//Build an array of ALL user membergroups.
708
				$full_groups = array($user_settings['id_group']);
709
				if (!empty($user_settings['additional_groups']))
710
				{
711
					$full_groups = array_merge($full_groups, explode(',', $user_settings['additional_groups']));
712
					$full_groups = array_unique($full_groups); //duplicates, maybe?
713
				}
714
715
				//Find out if any group requires 2FA
716
				$request = $smcFunc['db_query']('', '
717
					SELECT COUNT(id_group) AS total
718
					FROM {db_prefix}membergroups
719
					WHERE tfa_required = {int:tfa_required}
720
						AND id_group IN ({array_int:full_groups})',
721
					array(
722
						'tfa_required' => 1,
723
						'full_groups' => $full_groups,
724
					)
725
				);
726
				$row = $smcFunc['db_fetch_assoc']($request);
727
				$smcFunc['db_free_result']($request);
728
			}
729
			else
730
				$row['total'] = 1; //simplifies logics in the next "if"
731
732
			$area = !empty($_REQUEST['area']) ? $_REQUEST['area'] : '';
733
			$action = !empty($_REQUEST['action']) ? $_REQUEST['action'] : '';
734
735
			if ($row['total'] > 0 && !in_array($action, array('profile', 'logout')) || ($action == 'profile' && $area != 'tfasetup'))
736
				redirectexit('action=profile;area=tfasetup;forced');
737
		}
738
	}
739
740
	// Found 'im, let's set up the variables.
741
	if ($id_member != 0)
742
	{
743
		// Let's not update the last visit time in these cases...
744
		// 1. SSI doesn't count as visiting the forum.
745
		// 2. RSS feeds and XMLHTTP requests don't count either.
746
		// 3. If it was set within this session, no need to set it again.
747
		// 4. New session, yet updated < five hours ago? Maybe cache can help.
748
		// 5. We're still logging in or authenticating
749
		if (SMF != 'SSI' && !isset($_REQUEST['xml']) && (!isset($_REQUEST['action']) || !in_array($_REQUEST['action'], array('.xml', 'login2', 'logintfa'))) && empty($_SESSION['id_msg_last_visit']) && (empty($cache_enable) || ($_SESSION['id_msg_last_visit'] = cache_get_data('user_last_visit-' . $id_member, 5 * 3600)) === null))
0 ignored issues
show
The condition SMF != 'SSI' is always false.
Loading history...
750
		{
751
			// @todo can this be cached?
752
			// Do a quick query to make sure this isn't a mistake.
753
			$result = $smcFunc['db_query']('', '
754
				SELECT poster_time
755
				FROM {db_prefix}messages
756
				WHERE id_msg = {int:id_msg}
757
				LIMIT 1',
758
				array(
759
					'id_msg' => $user_settings['id_msg_last_visit'],
760
				)
761
			);
762
			list ($visitTime) = $smcFunc['db_fetch_row']($result);
763
			$smcFunc['db_free_result']($result);
764
765
			$_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit'];
766
767
			// If it was *at least* five hours ago...
768
			if ($visitTime < time() - 5 * 3600)
769
			{
770
				updateMemberData($id_member, array('id_msg_last_visit' => (int) $modSettings['maxMsgID'], 'last_login' => time(), 'member_ip' => $_SERVER['REMOTE_ADDR'], 'member_ip2' => $_SERVER['BAN_CHECK_IP']));
771
				$user_settings['last_login'] = time();
772
773
				if (!empty($cache_enable) && $cache_enable >= 2)
774
					cache_put_data('user_settings-' . $id_member, $user_settings, 60);
775
776
				if (!empty($cache_enable))
777
					cache_put_data('user_last_visit-' . $id_member, $_SESSION['id_msg_last_visit'], 5 * 3600);
778
			}
779
		}
780
		elseif (empty($_SESSION['id_msg_last_visit']))
781
			$_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit'];
782
783
		$username = $user_settings['member_name'];
784
785
		if (empty($user_settings['additional_groups']))
786
			$user_info = array(
787
				'groups' => array($user_settings['id_group'], $user_settings['id_post_group'])
788
			);
789
790
		else
791
			$user_info = array(
792
				'groups' => array_merge(
793
					array($user_settings['id_group'], $user_settings['id_post_group']),
794
					explode(',', $user_settings['additional_groups'])
795
				)
796
			);
797
798
		// Because history has proven that it is possible for groups to go bad - clean up in case.
799
		$user_info['groups'] = array_map('intval', $user_info['groups']);
800
801
		// This is a logged in user, so definitely not a spider.
802
		$user_info['possibly_robot'] = false;
803
804
		// Figure out the new time offset.
805
		if (!empty($user_settings['timezone']))
806
		{
807
			// Get the offsets from UTC for the server, then for the user.
808
			$tz_system = new DateTimeZone($modSettings['default_timezone']);
809
			$tz_user = new DateTimeZone($user_settings['timezone']);
810
			$time_system = new DateTime('now', $tz_system);
811
			$time_user = new DateTime('now', $tz_user);
812
			$user_settings['time_offset'] = ($tz_user->getOffset($time_user) - $tz_system->getOffset($time_system)) / 3600;
813
		}
814
		// We need a time zone.
815
		else
816
		{
817
			if (!empty($user_settings['time_offset']))
818
			{
819
				$tz_system = new DateTimeZone($modSettings['default_timezone']);
820
				$time_system = new DateTime('now', $tz_system);
821
822
				$user_settings['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $user_settings['time_offset'] * 3600, (int) $time_system->format('I'));
823
			}
824
825
			if (empty($user_settings['timezone']))
826
			{
827
				$user_settings['timezone'] = $modSettings['default_timezone'];
828
				$user_settings['time_offset'] = 0;
829
			}
830
		}
831
	}
832
	// If the user is a guest, initialize all the critical user settings.
833
	else
834
	{
835
		// This is what a guest's variables should be.
836
		$username = '';
837
		$user_info = array('groups' => array(-1));
838
		$user_settings = array();
839
840
		if (isset($_COOKIE[$cookiename]) && empty($context['tfa_member']))
841
			$_COOKIE[$cookiename] = '';
842
843
		// Expire the 2FA cookie
844
		if (isset($_COOKIE[$cookiename . '_tfa']) && empty($context['tfa_member']))
845
		{
846
			$tfa_data = $smcFunc['json_decode']($_COOKIE[$cookiename . '_tfa'], true);
847
848
			list (,, $exp) = array_pad((array) $tfa_data, 3, 0);
849
850
			if (time() > $exp)
851
			{
852
				$_COOKIE[$cookiename . '_tfa'] = '';
853
				setTFACookie(-3600, 0, '');
854
			}
855
		}
856
857
		// Create a login token if it doesn't exist yet.
858
		if (!isset($_SESSION['token']['post-login']))
859
			createToken('login');
860
		else
861
			list ($context['login_token_var'],,, $context['login_token']) = $_SESSION['token']['post-login'];
862
863
		// Do we perhaps think this is a search robot? Check every five minutes just in case...
864
		if ((!empty($modSettings['spider_mode']) || !empty($modSettings['spider_group'])) && (!isset($_SESSION['robot_check']) || $_SESSION['robot_check'] < time() - 300))
865
		{
866
			require_once($sourcedir . '/ManageSearchEngines.php');
867
			$user_info['possibly_robot'] = SpiderCheck();
868
		}
869
		elseif (!empty($modSettings['spider_mode']))
870
			$user_info['possibly_robot'] = isset($_SESSION['id_robot']) ? $_SESSION['id_robot'] : 0;
871
		// If we haven't turned on proper spider hunts then have a guess!
872
		else
873
		{
874
			$ci_user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
875
			$user_info['possibly_robot'] = (strpos($_SERVER['HTTP_USER_AGENT'], 'Mozilla') === false && strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') === false) || strpos($ci_user_agent, 'googlebot') !== false || strpos($ci_user_agent, 'slurp') !== false || strpos($ci_user_agent, 'crawl') !== false || strpos($ci_user_agent, 'bingbot') !== false || strpos($ci_user_agent, 'bingpreview') !== false || strpos($ci_user_agent, 'adidxbot') !== false || strpos($ci_user_agent, 'msnbot') !== false;
876
		}
877
878
		$user_settings['timezone'] = $modSettings['default_timezone'];
879
		$user_settings['time_offset'] = 0;
880
	}
881
882
	// Set up the $user_info array.
883
	$user_info += array(
884
		'id' => $id_member,
885
		'username' => $username,
886
		'name' => isset($user_settings['real_name']) ? $user_settings['real_name'] : '',
887
		'email' => isset($user_settings['email_address']) ? $user_settings['email_address'] : '',
888
		'passwd' => isset($user_settings['passwd']) ? $user_settings['passwd'] : '',
889
		'language' => empty($user_settings['lngfile']) || empty($modSettings['userLanguage']) ? $language : $user_settings['lngfile'],
890
		'is_guest' => $id_member == 0,
891
		'is_admin' => in_array(1, $user_info['groups']),
892
		'theme' => empty($user_settings['id_theme']) ? 0 : $user_settings['id_theme'],
893
		'last_login' => empty($user_settings['last_login']) ? 0 : $user_settings['last_login'],
894
		'ip' => $_SERVER['REMOTE_ADDR'],
895
		'ip2' => $_SERVER['BAN_CHECK_IP'],
896
		'posts' => empty($user_settings['posts']) ? 0 : $user_settings['posts'],
897
		'time_format' => empty($user_settings['time_format']) ? $modSettings['time_format'] : $user_settings['time_format'],
898
		'timezone' => $user_settings['timezone'],
899
		'time_offset' => $user_settings['time_offset'],
900
		'avatar' => array(
901
			'url' => isset($user_settings['avatar']) ? $user_settings['avatar'] : '',
902
			'filename' => empty($user_settings['filename']) ? '' : $user_settings['filename'],
903
			'custom_dir' => !empty($user_settings['attachment_type']) && $user_settings['attachment_type'] == 1,
904
			'id_attach' => isset($user_settings['id_attach']) ? $user_settings['id_attach'] : 0,
905
			'width' => isset($user_settings['attachment_width']) > 0 ? $user_settings['attachment_width']: 0,
906
			'height' => isset($user_settings['attachment_height']) > 0 ? $user_settings['attachment_height'] : 0,
907
		),
908
		'smiley_set' => isset($user_settings['smiley_set']) ? $user_settings['smiley_set'] : '',
909
		'messages' => empty($user_settings['instant_messages']) ? 0 : $user_settings['instant_messages'],
910
		'unread_messages' => empty($user_settings['unread_messages']) ? 0 : $user_settings['unread_messages'],
911
		'alerts' => empty($user_settings['alerts']) ? 0 : $user_settings['alerts'],
912
		'total_time_logged_in' => empty($user_settings['total_time_logged_in']) ? 0 : $user_settings['total_time_logged_in'],
913
		'buddies' => !empty($modSettings['enable_buddylist']) && !empty($user_settings['buddy_list']) ? explode(',', $user_settings['buddy_list']) : array(),
914
		'ignoreboards' => !empty($user_settings['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $user_settings['ignore_boards']) : array(),
915
		'ignoreusers' => !empty($user_settings['pm_ignore_list']) ? explode(',', $user_settings['pm_ignore_list']) : array(),
916
		'warning' => isset($user_settings['warning']) ? $user_settings['warning'] : 0,
917
		'permissions' => array(),
918
	);
919
	$user_info['groups'] = array_unique($user_info['groups']);
920
	$user_info['can_manage_boards'] = !empty($user_info['is_admin']) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($user_info['groups'], explode(',', $modSettings['board_manager_groups']))) > 0);
921
922
	// Make sure that the last item in the ignore boards array is valid. If the list was too long it could have an ending comma that could cause problems.
923
	if (!empty($user_info['ignoreboards']) && empty($user_info['ignoreboards'][$tmp = count($user_info['ignoreboards']) - 1]))
924
		unset($user_info['ignoreboards'][$tmp]);
925
926
	// Allow the user to change their language.
927
	if (!empty($modSettings['userLanguage']))
928
	{
929
		$languages = getLanguages();
930
931
		// Is it valid?
932
		if (!empty($_GET['language']) && isset($languages[strtr($_GET['language'], './\\:', '____')]))
933
		{
934
			$user_info['language'] = strtr($_GET['language'], './\\:', '____');
935
936
			// Make it permanent for members.
937
			if (!empty($user_info['id']))
938
				updateMemberData($user_info['id'], array('lngfile' => $user_info['language']));
939
			else
940
				$_SESSION['language'] = $user_info['language'];
941
			// Reload same url with new language, if it exist
942
			if (isset($_SESSION['old_url']))
943
				redirectexit($_SESSION['old_url']);
944
		}
945
		elseif (!empty($_SESSION['language']) && isset($languages[strtr($_SESSION['language'], './\\:', '____')]))
946
			$user_info['language'] = strtr($_SESSION['language'], './\\:', '____');
947
	}
948
949
	$temp = build_query_board($user_info['id']);
950
	$user_info['query_see_board'] = $temp['query_see_board'];
951
	$user_info['query_see_message_board'] = $temp['query_see_message_board'];
952
	$user_info['query_see_topic_board'] = $temp['query_see_topic_board'];
953
	$user_info['query_wanna_see_board'] = $temp['query_wanna_see_board'];
954
	$user_info['query_wanna_see_message_board'] = $temp['query_wanna_see_message_board'];
955
	$user_info['query_wanna_see_topic_board'] = $temp['query_wanna_see_topic_board'];
956
957
	call_integration_hook('integrate_user_info');
958
}
959
960
/**
961
 * Load minimal user info from members table.
962
 * Intended for use by background tasks that need to populate $user_info.
963
 *
964
 * @param int|array $user_ids The users IDs to get the data for.
965
 * @return array
966
 * @throws Exception
967
 */
968
function loadMinUserInfo($user_ids = array())
969
{
970
	global $smcFunc, $modSettings, $language, $modSettings;
971
	static $user_info_min = array();
972
973
	$user_ids = (array) $user_ids;
974
975
	// Already loaded?
976
	if (!empty($user_ids))
977
		$user_ids = array_diff($user_ids, array_keys($user_info_min));
978
979
	if (empty($user_ids))
980
		return $user_info_min;
981
982
	$columns_to_load = array(
983
		'id_member',
984
		'member_name',
985
		'real_name',
986
		'time_offset',
987
		'additional_groups',
988
		'id_group',
989
		'id_post_group',
990
		'lngfile',
991
		'smiley_set',
992
		'timezone',
993
	);
994
995
	call_integration_hook('integrate_load_min_user_settings_columns', array(&$columns_to_load));
996
997
	$request = $smcFunc['db_query']('', '
998
		SELECT {raw:columns}
999
		FROM {db_prefix}members
1000
		WHERE id_member IN ({array_int:user_ids})',
1001
		array(
1002
			'user_ids' => array_map('intval', array_unique($user_ids)),
1003
			'columns' => implode(', ', $columns_to_load)
1004
		)
1005
	);
1006
1007
	while ($row = $smcFunc['db_fetch_assoc']($request))
1008
	{
1009
		$user_info_min[$row['id_member']] = array(
1010
			'id' => $row['id_member'],
1011
			'username' => $row['member_name'],
1012
			'name' => isset($row['real_name']) ? $row['real_name'] : '',
1013
			'language' => (empty($row['lngfile']) || empty($modSettings['userLanguage'])) ? $language : $row['lngfile'],
1014
			'is_guest' => false,
1015
			'time_format' => empty($row['time_format']) ? $modSettings['time_format'] : $row['time_format'],
1016
			'smiley_set' => empty($row['smiley_set']) ? $modSettings['smiley_sets_default'] : $row['smiley_set'],
1017
		);
1018
1019
		if (empty($row['additional_groups']))
1020
			$user_info_min[$row['id_member']]['groups'] = array($row['id_group'], $row['id_post_group']);
1021
1022
		else
1023
			$user_info_min[$row['id_member']]['groups'] = array_merge(
1024
				array($row['id_group'], $row['id_post_group']),
1025
				explode(',', $row['additional_groups'])
1026
			);
1027
1028
		$user_info_min[$row['id_member']]['is_admin'] = in_array(1, $user_info_min[$row['id_member']]['groups']);
1029
1030
		if (!empty($row['timezone']))
1031
		{
1032
			$tz_system = new \DateTimeZone($modSettings['default_timezone']);
1033
			$tz_user = new \DateTimeZone($row['timezone']);
1034
			$time_system = new \DateTime('now', $tz_system);
1035
			$time_user = new \DateTime('now', $tz_user);
1036
			$row['time_offset'] = ($tz_user->getOffset($time_user) -
1037
					$tz_system->getOffset($time_system)) / 3600;
1038
		}
1039
		else
1040
		{
1041
			if (!empty($row['time_offset']))
1042
			{
1043
				$tz_system = new \DateTimeZone($modSettings['default_timezone']);
1044
				$time_system = new \DateTime('now', $tz_system);
1045
1046
				$row['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $row['time_offset'] * 3600, (int) $time_system->format('I'));
1047
			}
1048
1049
			if (empty($row['timezone']))
1050
			{
1051
				$row['timezone'] = $modSettings['default_timezone'];
1052
				$row['time_offset'] = 0;
1053
			}
1054
		}
1055
1056
		$user_info_min[$row['id_member']]['timezone'] = $row['timezone'];
1057
		$user_info_min[$row['id_member']]['time_offset'] = $row['time_offset'];
1058
	}
1059
1060
	$smcFunc['db_free_result']($request);
1061
1062
	call_integration_hook('integrate_load_min_user_settings', array(&$user_info_min));
1063
1064
	return $user_info_min;
1065
}
1066
1067
/**
1068
 * Check for moderators and see if they have access to the board.
1069
 * What it does:
1070
 * - sets up the $board_info array for current board information.
1071
 * - if cache is enabled, the $board_info array is stored in cache.
1072
 * - redirects to appropriate post if only message id is requested.
1073
 * - is only used when inside a topic or board.
1074
 * - determines the local moderators for the board.
1075
 * - adds group id 3 if the user is a local moderator for the board they are in.
1076
 * - prevents access if user is not in proper group nor a local moderator of the board.
1077
 */
1078
function loadBoard()
1079
{
1080
	global $txt, $scripturl, $context, $modSettings;
1081
	global $board_info, $board, $topic, $user_info, $smcFunc, $cache_enable;
1082
1083
	// Assume they are not a moderator.
1084
	$user_info['is_mod'] = false;
1085
	$context['user']['is_mod'] = &$user_info['is_mod'];
1086
1087
	// Start the linktree off empty..
1088
	$context['linktree'] = array();
1089
1090
	// Have they by chance specified a message id but nothing else?
1091
	if (empty($_REQUEST['action']) && empty($topic) && empty($board) && !empty($_REQUEST['msg']))
1092
	{
1093
		// Make sure the message id is really an int.
1094
		$_REQUEST['msg'] = (int) $_REQUEST['msg'];
1095
1096
		// Looking through the message table can be slow, so try using the cache first.
1097
		if (($topic = cache_get_data('msg_topic-' . $_REQUEST['msg'], 120)) === null)
1098
		{
1099
			$request = $smcFunc['db_query']('', '
1100
				SELECT id_topic
1101
				FROM {db_prefix}messages
1102
				WHERE id_msg = {int:id_msg}
1103
				LIMIT 1',
1104
				array(
1105
					'id_msg' => $_REQUEST['msg'],
1106
				)
1107
			);
1108
1109
			// So did it find anything?
1110
			if ($smcFunc['db_num_rows']($request))
1111
			{
1112
				list ($topic) = $smcFunc['db_fetch_row']($request);
1113
				$smcFunc['db_free_result']($request);
1114
				// Save save save.
1115
				cache_put_data('msg_topic-' . $_REQUEST['msg'], $topic, 120);
1116
			}
1117
		}
1118
1119
		// Remember redirection is the key to avoiding fallout from your bosses.
1120
		if (!empty($topic))
1121
			redirectexit('topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg']);
1122
		else
1123
		{
1124
			loadPermissions();
1125
			loadTheme();
1126
			fatal_lang_error('topic_gone', false);
1127
		}
1128
	}
1129
1130
	// Load this board only if it is specified.
1131
	if (empty($board) && empty($topic))
1132
	{
1133
		$board_info = array('moderators' => array(), 'moderator_groups' => array());
1134
		return;
1135
	}
1136
1137
	if (!empty($cache_enable) && (empty($topic) || $cache_enable >= 3))
1138
	{
1139
		// @todo SLOW?
1140
		if (!empty($topic))
1141
			$temp = cache_get_data('topic_board-' . $topic, 120);
1142
		else
1143
			$temp = cache_get_data('board-' . $board, 120);
1144
1145
		if (!empty($temp))
1146
		{
1147
			$board_info = $temp;
1148
			$board = $board_info['id'];
1149
		}
1150
	}
1151
1152
	if (empty($temp))
1153
	{
1154
		$custom_column_selects = array();
1155
		$custom_column_parameters = [
1156
			'current_topic' => $topic,
1157
			'board_link' => empty($topic) ? $smcFunc['db_quote']('{int:current_board}', array('current_board' => $board)) : 't.id_board',
1158
		];
1159
1160
		call_integration_hook('integrate_load_board', array(&$custom_column_selects, &$custom_column_parameters));
1161
1162
		$request = $smcFunc['db_query']('load_board_info', '
1163
			SELECT
1164
				c.id_cat, b.name AS bname, b.description, b.num_topics, b.member_groups, b.deny_member_groups,
1165
				b.id_parent, c.name AS cname, COALESCE(mg.id_group, 0) AS id_moderator_group, mg.group_name,
1166
				COALESCE(mem.id_member, 0) AS id_moderator,
1167
				mem.real_name' . (!empty($topic) ? ', b.id_board' : '') . ', b.child_level,
1168
				b.id_theme, b.override_theme, b.count_posts, b.id_profile, b.redirect,
1169
				b.unapproved_topics, b.unapproved_posts' . (!empty($topic) ? ', t.approved, t.id_member_started' : '') . '
1170
				' . (!empty($custom_column_selects) ? (', ' . implode(', ', $custom_column_selects)) : '') . '
1171
			FROM {db_prefix}boards AS b' . (!empty($topic) ? '
1172
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})' : '') . '
1173
				LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
1174
				LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = {raw:board_link})
1175
				LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = modgs.id_group)
1176
				LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = {raw:board_link})
1177
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
1178
			WHERE b.id_board = {raw:board_link}',
1179
			$custom_column_parameters
1180
		);
1181
1182
		// If there aren't any, skip.
1183
		if ($smcFunc['db_num_rows']($request) > 0)
1184
		{
1185
			$row = $smcFunc['db_fetch_assoc']($request);
1186
1187
			// Set the current board.
1188
			if (!empty($row['id_board']))
1189
				$board = $row['id_board'];
1190
1191
			// Basic operating information. (globals... :/)
1192
			$board_info = array(
1193
				'id' => $board,
1194
				'moderators' => array(),
1195
				'moderator_groups' => array(),
1196
				'cat' => array(
1197
					'id' => $row['id_cat'],
1198
					'name' => $row['cname']
1199
				),
1200
				'name' => $row['bname'],
1201
				'description' => $row['description'],
1202
				'num_topics' => $row['num_topics'],
1203
				'unapproved_topics' => $row['unapproved_topics'],
1204
				'unapproved_posts' => $row['unapproved_posts'],
1205
				'unapproved_user_topics' => 0,
1206
				'parent_boards' => getBoardParents($row['id_parent']),
1207
				'parent' => $row['id_parent'],
1208
				'child_level' => $row['child_level'],
1209
				'theme' => $row['id_theme'],
1210
				'override_theme' => !empty($row['override_theme']),
1211
				'profile' => $row['id_profile'],
1212
				'redirect' => $row['redirect'],
1213
				'recycle' => !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board,
1214
				'posts_count' => empty($row['count_posts']),
1215
				'cur_topic_approved' => empty($topic) || $row['approved'],
1216
				'cur_topic_starter' => empty($topic) ? 0 : $row['id_member_started'],
1217
			);
1218
1219
			// Load the membergroups allowed, and check permissions.
1220
			$board_info['groups'] = $row['member_groups'] == '' ? array() : explode(',', $row['member_groups']);
1221
			$board_info['deny_groups'] = $row['deny_member_groups'] == '' ? array() : explode(',', $row['deny_member_groups']);
1222
1223
			call_integration_hook('integrate_board_info', array(&$board_info, $row));
1224
1225
			if (!empty($modSettings['board_manager_groups']))
1226
			{
1227
				$board_info['groups'] = array_unique(array_merge($board_info['groups'], explode(',', $modSettings['board_manager_groups'])));
1228
				$board_info['deny_groups'] = array_diff($board_info['deny_groups'], explode(',', $modSettings['board_manager_groups']));
1229
			}
1230
1231
			do
1232
			{
1233
				if (!empty($row['id_moderator']))
1234
					$board_info['moderators'][$row['id_moderator']] = array(
1235
						'id' => $row['id_moderator'],
1236
						'name' => $row['real_name'],
1237
						'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'],
1238
						'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
1239
					);
1240
1241
				if (!empty($row['id_moderator_group']))
1242
					$board_info['moderator_groups'][$row['id_moderator_group']] = array(
1243
						'id' => $row['id_moderator_group'],
1244
						'name' => $row['group_name'],
1245
						'href' => $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'],
1246
						'link' => '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'] . '">' . $row['group_name'] . '</a>'
1247
					);
1248
			}
1249
			while ($row = $smcFunc['db_fetch_assoc']($request));
1250
1251
			// If the board only contains unapproved posts and the user isn't an approver then they can't see any topics.
1252
			// If that is the case do an additional check to see if they have any topics waiting to be approved.
1253
			if ($board_info['num_topics'] == 0 && $modSettings['postmod_active'] && !allowedTo('approve_posts'))
1254
			{
1255
				// Free the previous result
1256
				$smcFunc['db_free_result']($request);
1257
1258
				// @todo why is this using id_topic?
1259
				// @todo Can this get cached?
1260
				$request = $smcFunc['db_query']('', '
1261
					SELECT COUNT(id_topic)
1262
					FROM {db_prefix}topics
1263
					WHERE id_member_started={int:id_member}
1264
						AND approved = {int:unapproved}
1265
						AND id_board = {int:board}',
1266
					array(
1267
						'id_member' => $user_info['id'],
1268
						'unapproved' => 0,
1269
						'board' => $board,
1270
					)
1271
				);
1272
1273
				list ($board_info['unapproved_user_topics']) = $smcFunc['db_fetch_row']($request);
1274
			}
1275
1276
			if (!empty($cache_enable) && (empty($topic) || $cache_enable >= 3))
1277
			{
1278
				// @todo SLOW?
1279
				if (!empty($topic))
1280
					cache_put_data('topic_board-' . $topic, $board_info, 120);
1281
				cache_put_data('board-' . $board, $board_info, 120);
1282
			}
1283
		}
1284
		else
1285
		{
1286
			// Otherwise the topic is invalid, there are no moderators, etc.
1287
			$board_info = array(
1288
				'moderators' => array(),
1289
				'moderator_groups' => array(),
1290
				'error' => 'exist'
1291
			);
1292
			$topic = null;
1293
			$board = 0;
1294
		}
1295
		$smcFunc['db_free_result']($request);
1296
	}
1297
1298
	if (!empty($topic))
1299
		$_GET['board'] = (int) $board;
1300
1301
	if (!empty($board))
1302
	{
1303
		// Get this into an array of keys for array_intersect
1304
		$moderator_groups = array_keys($board_info['moderator_groups']);
1305
1306
		// Now check if the user is a moderator.
1307
		$user_info['is_mod'] = isset($board_info['moderators'][$user_info['id']]) || count(array_intersect($user_info['groups'], $moderator_groups)) != 0;
1308
1309
		if (count(array_intersect($user_info['groups'], $board_info['groups'])) == 0 && !$user_info['is_admin'])
1310
			$board_info['error'] = 'access';
1311
		if (!empty($modSettings['deny_boards_access']) && count(array_intersect($user_info['groups'], $board_info['deny_groups'])) != 0 && !$user_info['is_admin'])
1312
			$board_info['error'] = 'access';
1313
1314
		// Build up the linktree.
1315
		$context['linktree'] = array_merge(
1316
			$context['linktree'],
1317
			array(array(
1318
				'url' => $scripturl . '#c' . $board_info['cat']['id'],
1319
				'name' => $board_info['cat']['name']
1320
			)),
1321
			array_reverse($board_info['parent_boards']),
1322
			array(array(
1323
				'url' => $scripturl . '?board=' . $board . '.0',
1324
				'name' => $board_info['name']
1325
			))
1326
		);
1327
	}
1328
1329
	// Set the template contextual information.
1330
	$context['user']['is_mod'] = &$user_info['is_mod'];
1331
	$context['current_topic'] = $topic;
1332
	$context['current_board'] = $board;
1333
1334
	// No posting in redirection boards!
1335
	if (!empty($_REQUEST['action']) && $_REQUEST['action'] == 'post' && !empty($board_info['redirect']))
1336
		$board_info['error'] = 'post_in_redirect';
1337
1338
	// Hacker... you can't see this topic, I'll tell you that. (but moderators can!)
1339
	if (!empty($board_info['error']) && (!empty($modSettings['deny_boards_access']) || $board_info['error'] != 'access' || !$user_info['is_mod']))
1340
	{
1341
		// The permissions and theme need loading, just to make sure everything goes smoothly.
1342
		loadPermissions();
1343
		loadTheme();
1344
1345
		$_GET['board'] = '';
1346
		$_GET['topic'] = '';
1347
1348
		// The linktree should not give the game away mate!
1349
		$context['linktree'] = array(
1350
			array(
1351
				'url' => $scripturl,
1352
				'name' => $context['forum_name_html_safe']
1353
			)
1354
		);
1355
1356
		// If it's a prefetching agent or we're requesting an attachment.
1357
		if ((isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') || (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'dlattach'))
1358
		{
1359
			ob_end_clean();
1360
			send_http_status(403);
1361
			die;
1362
		}
1363
		elseif ($board_info['error'] == 'post_in_redirect')
1364
		{
1365
			// Slightly different error message here...
1366
			fatal_lang_error('cannot_post_redirect', false);
1367
		}
1368
		elseif ($user_info['is_guest'])
1369
		{
1370
			loadLanguage('Errors');
1371
			is_not_guest($txt['topic_gone']);
1372
		}
1373
		else
1374
			fatal_lang_error('topic_gone', false);
1375
	}
1376
1377
	if ($user_info['is_mod'])
1378
		$user_info['groups'][] = 3;
1379
}
1380
1381
/**
1382
 * Load this user's permissions.
1383
 */
1384
function loadPermissions()
1385
{
1386
	global $user_info, $board, $board_info, $modSettings, $smcFunc, $sourcedir, $cache_enable;
1387
1388
	if ($user_info['is_admin'])
1389
	{
1390
		banPermissions();
1391
		return;
1392
	}
1393
1394
	if (!empty($cache_enable))
1395
	{
1396
		$cache_groups = $user_info['groups'];
1397
		asort($cache_groups);
1398
		$cache_groups = implode(',', $cache_groups);
1399
		// If it's a spider then cache it different.
1400
		if ($user_info['possibly_robot'])
1401
			$cache_groups .= '-spider';
1402
1403
		if ($cache_enable >= 2 && !empty($board) && ($temp = cache_get_data('permissions:' . $cache_groups . ':' . $board, 240)) != null && time() - 240 > $modSettings['settings_updated'])
1404
		{
1405
			list ($user_info['permissions']) = $temp;
1406
			banPermissions();
1407
1408
			return;
1409
		}
1410
		elseif (($temp = cache_get_data('permissions:' . $cache_groups, 240)) != null && time() - 240 > $modSettings['settings_updated'])
1411
			list ($user_info['permissions'], $removals) = $temp;
1412
	}
1413
1414
	// If it is detected as a robot, and we are restricting permissions as a special group - then implement this.
1415
	$spider_restrict = $user_info['possibly_robot'] && !empty($modSettings['spider_group']) ? ' OR (id_group = {int:spider_group} AND add_deny = 0)' : '';
1416
1417
	if (empty($user_info['permissions']))
1418
	{
1419
		// Get the general permissions.
1420
		$request = $smcFunc['db_query']('', '
1421
			SELECT permission, add_deny
1422
			FROM {db_prefix}permissions
1423
			WHERE id_group IN ({array_int:member_groups})
1424
				' . $spider_restrict,
1425
			array(
1426
				'member_groups' => $user_info['groups'],
1427
				'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0,
1428
			)
1429
		);
1430
		$removals = array();
1431
		while ($row = $smcFunc['db_fetch_assoc']($request))
1432
		{
1433
			if (empty($row['add_deny']))
1434
				$removals[] = $row['permission'];
1435
			else
1436
				$user_info['permissions'][] = $row['permission'];
1437
		}
1438
		$smcFunc['db_free_result']($request);
1439
1440
		if (isset($cache_groups))
1441
			cache_put_data('permissions:' . $cache_groups, array($user_info['permissions'], $removals), 240);
1442
	}
1443
1444
	// Get the board permissions.
1445
	if (!empty($board))
1446
	{
1447
		// Make sure the board (if any) has been loaded by loadBoard().
1448
		if (!isset($board_info['profile']))
1449
			fatal_lang_error('no_board');
1450
1451
		$request = $smcFunc['db_query']('', '
1452
			SELECT permission, add_deny
1453
			FROM {db_prefix}board_permissions
1454
			WHERE (id_group IN ({array_int:member_groups})
1455
				' . $spider_restrict . ')
1456
				AND id_profile = {int:id_profile}',
1457
			array(
1458
				'member_groups' => $user_info['groups'],
1459
				'id_profile' => $board_info['profile'],
1460
				'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0,
1461
			)
1462
		);
1463
		while ($row = $smcFunc['db_fetch_assoc']($request))
1464
		{
1465
			if (empty($row['add_deny']))
1466
				$removals[] = $row['permission'];
1467
			else
1468
				$user_info['permissions'][] = $row['permission'];
1469
		}
1470
		$smcFunc['db_free_result']($request);
1471
	}
1472
1473
	// Remove all the permissions they shouldn't have ;).
1474
	if (!empty($modSettings['permission_enable_deny']))
1475
		$user_info['permissions'] = array_diff($user_info['permissions'], $removals);
1476
1477
	if (isset($cache_groups) && !empty($board) && $cache_enable >= 2)
1478
		cache_put_data('permissions:' . $cache_groups . ':' . $board, array($user_info['permissions'], null), 240);
1479
1480
	// Banned?  Watch, don't touch..
1481
	banPermissions();
1482
1483
	// Load the mod cache so we can know what additional boards they should see, but no sense in doing it for guests
1484
	if (!$user_info['is_guest'])
1485
	{
1486
		if (!isset($_SESSION['mc']) || $_SESSION['mc']['time'] <= $modSettings['settings_updated'])
1487
		{
1488
			require_once($sourcedir . '/Subs-Auth.php');
1489
			rebuildModCache();
1490
		}
1491
		else
1492
			$user_info['mod_cache'] = $_SESSION['mc'];
1493
1494
		// This is a useful phantom permission added to the current user, and only the current user while they are logged in.
1495
		// For example this drastically simplifies certain changes to the profile area.
1496
		$user_info['permissions'][] = 'is_not_guest';
1497
		// And now some backwards compatibility stuff for mods and whatnot that aren't expecting the new permissions.
1498
		$user_info['permissions'][] = 'profile_view_own';
1499
		if (in_array('profile_view', $user_info['permissions']))
1500
			$user_info['permissions'][] = 'profile_view_any';
1501
	}
1502
}
1503
1504
/**
1505
 * Loads an array of users' data by ID or member_name.
1506
 *
1507
 * @param array|string $users An array of users by id or name or a single username/id
1508
 * @param bool $is_name Whether $users contains names
1509
 * @param string $set What kind of data to load (normal, profile, minimal)
1510
 * @return array The ids of the members loaded
1511
 */
1512
function loadMemberData($users, $is_name = false, $set = 'normal')
1513
{
1514
	global $user_profile, $modSettings, $board_info, $smcFunc, $context;
1515
	global $user_info, $cache_enable, $txt;
1516
1517
	// Can't just look for no users :P.
1518
	if (empty($users))
1519
		return array();
1520
1521
	// Pass the set value
1522
	$context['loadMemberContext_set'] = $set;
1523
1524
	// Make sure it's an array.
1525
	$users = !is_array($users) ? array($users) : array_unique($users);
1526
	$loaded_ids = array();
1527
1528
	if (!$is_name && !empty($cache_enable) && $cache_enable >= 3)
1529
	{
1530
		$users = array_values($users);
1531
		for ($i = 0, $n = count($users); $i < $n; $i++)
1532
		{
1533
			$data = cache_get_data('member_data-' . $set . '-' . $users[$i], 240);
1534
			if ($data == null)
1535
				continue;
1536
1537
			$loaded_ids[] = $data['id_member'];
1538
			$user_profile[$data['id_member']] = $data;
1539
			unset($users[$i]);
1540
		}
1541
	}
1542
1543
	// Used by default
1544
	$select_columns = '
1545
			COALESCE(lo.log_time, 0) AS is_online, COALESCE(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type, a.width "attachment_width", a.height "attachment_height",
1546
			mem.signature, mem.personal_text, mem.avatar, mem.id_member, mem.member_name,
1547
			mem.real_name, mem.email_address, mem.date_registered, mem.website_title, mem.website_url,
1548
			mem.birthdate, mem.member_ip, mem.member_ip2, mem.posts, mem.last_login, mem.id_post_group, mem.lngfile, mem.id_group, mem.time_offset, mem.timezone, mem.show_online,
1549
			mg.online_color AS member_group_color, COALESCE(mg.group_name, {string:blank_string}) AS member_group,
1550
			pg.online_color AS post_group_color, COALESCE(pg.group_name, {string:blank_string}) AS post_group,
1551
			mem.is_activated, mem.warning, ' . (!empty($modSettings['titlesEnable']) ? 'mem.usertitle, ' : '') . '
1552
			CASE WHEN mem.id_group = 0 OR mg.icons = {string:blank_string} THEN pg.icons ELSE mg.icons END AS icons';
1553
	$select_tables = '
1554
			LEFT JOIN {db_prefix}log_online AS lo ON (lo.id_member = mem.id_member)
1555
			LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = mem.id_member)
1556
			LEFT JOIN {db_prefix}membergroups AS pg ON (pg.id_group = mem.id_post_group)
1557
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)';
1558
1559
	// We add or replace according the the set
1560
	switch ($set)
1561
	{
1562
		case 'normal':
1563
			$select_columns .= ', mem.buddy_list,  mem.additional_groups';
1564
			break;
1565
		case 'profile':
1566
			$select_columns .= ', mem.additional_groups, mem.id_theme, mem.pm_ignore_list, mem.pm_receive_from,
1567
			mem.time_format, mem.timezone, mem.secret_question, mem.smiley_set, mem.tfa_secret,
1568
			mem.total_time_logged_in, lo.url, mem.ignore_boards, mem.password_salt, mem.pm_prefs, mem.buddy_list, mem.alerts';
1569
			break;
1570
		case 'minimal':
1571
			$select_columns = '
1572
			mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.date_registered,
1573
			mem.posts, mem.last_login, mem.member_ip, mem.member_ip2, mem.lngfile, mem.id_group';
1574
			$select_tables = '';
1575
			break;
1576
		default:
1577
		{
1578
			loadLanguage('Errors');
1579
			trigger_error(sprintf($txt['invalid_member_data_set'], $set), E_USER_WARNING);
1580
		}
1581
	}
1582
1583
	// Allow mods to easily add to the selected member data
1584
	call_integration_hook('integrate_load_member_data', array(&$select_columns, &$select_tables, &$set));
1585
1586
	if (!empty($users))
1587
	{
1588
		// Load the member's data.
1589
		$request = $smcFunc['db_query']('', '
1590
			SELECT' . $select_columns . '
1591
			FROM {db_prefix}members AS mem' . $select_tables . '
1592
			WHERE mem.' . ($is_name ? 'member_name' : 'id_member') . ' IN ({' . ($is_name ? 'array_string' : 'array_int') . ':users})',
1593
			array(
1594
				'blank_string' => '',
1595
				'users' => $users,
1596
			)
1597
		);
1598
		$new_loaded_ids = array();
1599
		while ($row = $smcFunc['db_fetch_assoc']($request))
1600
		{
1601
			// If the image proxy is enabled, we still want the original URL when they're editing the profile...
1602
			$row['avatar_original'] = !empty($row['avatar']) ? $row['avatar'] : '';
1603
1604
			// Take care of proxying avatar if required, do this here for maximum reach
1605
			if (!empty($row['avatar']))
1606
				$row['avatar'] = get_proxied_url($row['avatar']);
1607
1608
			// Keep track of the member's normal member group
1609
			$row['primary_group'] = !empty($row['member_group']) ? $row['member_group'] : '';
1610
1611
			if (isset($row['member_ip']))
1612
				$row['member_ip'] = inet_dtop($row['member_ip']);
1613
			if (isset($row['member_ip2']))
1614
				$row['member_ip2'] = inet_dtop($row['member_ip2']);
1615
			$row['id_member'] = (int) $row['id_member'];
1616
			$new_loaded_ids[] = $row['id_member'];
1617
			$loaded_ids[] = $row['id_member'];
1618
			$row['options'] = array();
1619
			$user_profile[$row['id_member']] = $row;
1620
		}
1621
		$smcFunc['db_free_result']($request);
1622
	}
1623
1624
	if (!empty($new_loaded_ids) && $set !== 'minimal')
1625
	{
1626
		$request = $smcFunc['db_query']('', '
1627
			SELECT id_member, variable, value
1628
			FROM {db_prefix}themes
1629
			WHERE id_member IN ({array_int:loaded_ids})',
1630
			array(
1631
				'loaded_ids' => $new_loaded_ids,
1632
			)
1633
		);
1634
		while ($row = $smcFunc['db_fetch_assoc']($request))
1635
			$user_profile[$row['id_member']]['options'][$row['variable']] = $row['value'];
1636
		$smcFunc['db_free_result']($request);
1637
	}
1638
1639
	$additional_mods = array();
1640
1641
	// Are any of these users in groups assigned to moderate this board?
1642
	if (!empty($loaded_ids) && !empty($board_info['moderator_groups']) && $set === 'normal')
1643
	{
1644
		foreach ($loaded_ids as $a_member)
1645
		{
1646
			if (!empty($user_profile[$a_member]['additional_groups']))
1647
				$groups = array_merge(array($user_profile[$a_member]['id_group']), explode(',', $user_profile[$a_member]['additional_groups']));
1648
			else
1649
				$groups = array($user_profile[$a_member]['id_group']);
1650
1651
			$temp = array_intersect($groups, array_keys($board_info['moderator_groups']));
1652
1653
			if (!empty($temp))
1654
			{
1655
				$additional_mods[] = $a_member;
1656
			}
1657
		}
1658
	}
1659
1660
	if (!empty($new_loaded_ids) && !empty($cache_enable) && $cache_enable >= 3)
1661
	{
1662
		for ($i = 0, $n = count($new_loaded_ids); $i < $n; $i++)
1663
			cache_put_data('member_data-' . $set . '-' . $new_loaded_ids[$i], $user_profile[$new_loaded_ids[$i]], 240);
1664
	}
1665
1666
	// Are we loading any moderators?  If so, fix their group data...
1667
	if (!empty($loaded_ids) && (!empty($board_info['moderators']) || !empty($board_info['moderator_groups'])) && $set === 'normal' && count($temp_mods = array_merge(array_intersect($loaded_ids, array_keys($board_info['moderators'])), $additional_mods)) !== 0)
1668
	{
1669
		if (($row = cache_get_data('moderator_group_info', 480)) == null)
1670
		{
1671
			$request = $smcFunc['db_query']('', '
1672
				SELECT group_name AS member_group, online_color AS member_group_color, icons
1673
				FROM {db_prefix}membergroups
1674
				WHERE id_group = {int:moderator_group}
1675
				LIMIT 1',
1676
				array(
1677
					'moderator_group' => 3,
1678
				)
1679
			);
1680
			$row = $smcFunc['db_fetch_assoc']($request);
1681
			$smcFunc['db_free_result']($request);
1682
1683
			cache_put_data('moderator_group_info', $row, 480);
1684
		}
1685
1686
		foreach ($temp_mods as $id)
1687
		{
1688
			// By popular demand, don't show admins or global moderators as moderators.
1689
			if ($user_profile[$id]['id_group'] != 1 && $user_profile[$id]['id_group'] != 2)
1690
				$user_profile[$id]['member_group'] = $row['member_group'];
1691
1692
			// If the Moderator group has no color or icons, but their group does... don't overwrite.
1693
			if (!empty($row['icons']))
1694
				$user_profile[$id]['icons'] = $row['icons'];
1695
			if (!empty($row['member_group_color']))
1696
				$user_profile[$id]['member_group_color'] = $row['member_group_color'];
1697
		}
1698
	}
1699
1700
	return $loaded_ids;
1701
}
1702
1703
/**
1704
 * Loads the user's basic values... meant for template/theme usage.
1705
 *
1706
 * @param int $user The ID of a user previously loaded by {@link loadMemberData()}
1707
 * @param bool $display_custom_fields Whether or not to display custom profile fields
1708
 * @return boolean|array  False if the data wasn't loaded or the loaded data.
1709
 * @throws Exception
1710
 */
1711
function loadMemberContext($user, $display_custom_fields = false)
1712
{
1713
	global $memberContext, $user_profile, $txt, $scripturl, $user_info;
1714
	global $context, $modSettings, $settings, $smcFunc;
1715
	static $already_loaded_custom_fields = array();
1716
	static $loadedLanguages = array();
1717
1718
	// If this person's data is already loaded, skip it.
1719
	if (!empty($memberContext[$user]) && !empty($already_loaded_custom_fields[$user]) >= $display_custom_fields)
1720
		return $memberContext[$user];
1721
1722
	// We can't load guests or members not loaded by loadMemberData()!
1723
	if ($user == 0)
1724
		return false;
1725
	if (!isset($user_profile[$user]))
1726
	{
1727
		loadLanguage('Errors');
1728
		trigger_error(sprintf($txt['user_not_loaded'], $user), E_USER_WARNING);
1729
		return false;
1730
	}
1731
1732
	// Well, it's loaded now anyhow.
1733
	$profile = $user_profile[$user];
1734
1735
	// These minimal values are always loaded
1736
	$memberContext[$user] = array(
1737
		'username' => $profile['member_name'],
1738
		'name' => $profile['real_name'],
1739
		'id' => $profile['id_member'],
1740
		'href' => $scripturl . '?action=profile;u=' . $profile['id_member'],
1741
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $profile['id_member'] . '" title="' . sprintf($txt['view_profile_of_username'], $profile['real_name']) . '">' . $profile['real_name'] . '</a>',
1742
		'email' => $profile['email_address'],
1743
		'show_email' => !$user_info['is_guest'] && ($user_info['id'] == $profile['id_member'] || allowedTo('moderate_forum')),
1744
		'registered' => empty($profile['date_registered']) ? $txt['not_applicable'] : timeformat($profile['date_registered']),
1745
		'registered_timestamp' => empty($profile['date_registered']) ? 0 : $profile['date_registered'],
1746
	);
1747
1748
	// If the set isn't minimal then load the monstrous array.
1749
	if ($context['loadMemberContext_set'] != 'minimal')
1750
	{
1751
		// Censor everything.
1752
		censorText($profile['signature']);
1753
		censorText($profile['personal_text']);
1754
1755
		// Set things up to be used before hand.
1756
		$profile['signature'] = str_replace(array("\n", "\r"), array('<br>', ''), $profile['signature']);
1757
		$profile['signature'] = parse_bbc($profile['signature'], true, 'sig' . $profile['id_member'], get_signature_allowed_bbc_tags());
1758
1759
		$profile['is_online'] = (!empty($profile['show_online']) || allowedTo('moderate_forum')) && $profile['is_online'] > 0;
1760
		$profile['icons'] = empty($profile['icons']) ? array('', '') : explode('#', $profile['icons']);
1761
		// Setup the buddy status here (One whole in_array call saved :P)
1762
		$profile['buddy'] = in_array($profile['id_member'], $user_info['buddies']);
1763
		$buddy_list = !empty($profile['buddy_list']) ? explode(',', $profile['buddy_list']) : array();
1764
1765
		//We need a little fallback for the membergroup icons. If it doesn't exist in the current theme, fallback to default theme
1766
		if (isset($profile['icons'][1]) && file_exists($settings['actual_theme_dir'] . '/images/membericons/' . $profile['icons'][1])) //icon is set and exists
1767
			$group_icon_url = $settings['images_url'] . '/membericons/' . $profile['icons'][1];
1768
		elseif (isset($profile['icons'][1])) //icon is set and doesn't exist, fallback to default
1769
			$group_icon_url = $settings['default_images_url'] . '/membericons/' . $profile['icons'][1];
1770
		else //not set, bye bye
1771
			$group_icon_url = '';
1772
1773
		// Go the extra mile and load the user's native language name.
1774
		if (empty($loadedLanguages))
1775
			$loadedLanguages = getLanguages();
1776
1777
		// Figure out the new time offset.
1778
		if (!empty($profile['timezone']))
1779
		{
1780
			// Get the offsets from UTC for the server, then for the user.
1781
			$tz_system = new DateTimeZone($modSettings['default_timezone']);
1782
			$tz_user = new DateTimeZone($profile['timezone']);
1783
			$time_system = new DateTime('now', $tz_system);
1784
			$time_user = new DateTime('now', $tz_user);
1785
			$profile['time_offset'] = ($tz_user->getOffset($time_user) - $tz_system->getOffset($time_system)) / 3600;
1786
		}
1787
		// We need a time zone.
1788
		else
1789
		{
1790
			if (!empty($profile['time_offset']))
1791
			{
1792
				$tz_system = new DateTimeZone($modSettings['default_timezone']);
1793
				$time_system = new DateTime('now', $tz_system);
1794
1795
				$profile['timezone'] = @timezone_name_from_abbr('', $tz_system->getOffset($time_system) + $profile['time_offset'] * 3600, (int) $time_system->format('I'));
1796
			}
1797
1798
			if (empty($profile['timezone']))
1799
			{
1800
				$profile['timezone'] = $modSettings['default_timezone'];
1801
				$profile['time_offset'] = 0;
1802
			}
1803
		}
1804
1805
		$memberContext[$user] += array(
1806
			'username_color' => '<span ' . (!empty($profile['member_group_color']) ? 'style="color:' . $profile['member_group_color'] . ';"' : '') . '>' . $profile['member_name'] . '</span>',
1807
			'name_color' => '<span ' . (!empty($profile['member_group_color']) ? 'style="color:' . $profile['member_group_color'] . ';"' : '') . '>' . $profile['real_name'] . '</span>',
1808
			'link_color' => '<a href="' . $scripturl . '?action=profile;u=' . $profile['id_member'] . '" title="' . sprintf($txt['view_profile_of_username'], $profile['real_name']) . '" ' . (!empty($profile['member_group_color']) ? 'style="color:' . $profile['member_group_color'] . ';"' : '') . '>' . $profile['real_name'] . '</a>',
1809
			'is_buddy' => $profile['buddy'],
1810
			'is_reverse_buddy' => in_array($user_info['id'], $buddy_list),
1811
			'buddies' => $buddy_list,
1812
			'title' => !empty($modSettings['titlesEnable']) ? $profile['usertitle'] : '',
1813
			'blurb' => $profile['personal_text'],
1814
			'website' => array(
1815
				'title' => $profile['website_title'],
1816
				'url' => $profile['website_url'],
1817
			),
1818
			'birth_date' => empty($profile['birthdate']) ? '1004-01-01' : (substr($profile['birthdate'], 0, 4) === '0004' ? '1004' . substr($profile['birthdate'], 4) : $profile['birthdate']),
1819
			'signature' => $profile['signature'],
1820
			'real_posts' => $profile['posts'],
1821
			'posts' => $profile['posts'] > 500000 ? $txt['geek'] : comma_format($profile['posts']),
1822
			'last_login' => empty($profile['last_login']) ? $txt['never'] : timeformat($profile['last_login']),
1823
			'last_login_timestamp' => empty($profile['last_login']) ? 0 : $profile['last_login'],
1824
			'ip' => $smcFunc['htmlspecialchars']($profile['member_ip']),
1825
			'ip2' => $smcFunc['htmlspecialchars']($profile['member_ip2']),
1826
			'online' => array(
1827
				'is_online' => $profile['is_online'],
1828
				'text' => $smcFunc['htmlspecialchars']($txt[$profile['is_online'] ? 'online' : 'offline']),
1829
				'member_online_text' => sprintf($txt[$profile['is_online'] ? 'member_is_online' : 'member_is_offline'], $smcFunc['htmlspecialchars']($profile['real_name'])),
1830
				'href' => $scripturl . '?action=pm;sa=send;u=' . $profile['id_member'],
1831
				'link' => '<a href="' . $scripturl . '?action=pm;sa=send;u=' . $profile['id_member'] . '">' . $txt[$profile['is_online'] ? 'online' : 'offline'] . '</a>',
1832
				'label' => $txt[$profile['is_online'] ? 'online' : 'offline']
1833
			),
1834
			'language' => !empty($loadedLanguages[$profile['lngfile']]) && !empty($loadedLanguages[$profile['lngfile']]['name']) ? $loadedLanguages[$profile['lngfile']]['name'] : $smcFunc['ucwords'](strtr($profile['lngfile'], array('_' => ' ', '-utf8' => ''))),
1835
			'is_activated' => isset($profile['is_activated']) ? $profile['is_activated'] : 1,
1836
			'is_banned' => isset($profile['is_activated']) ? $profile['is_activated'] >= 10 : 0,
1837
			'options' => $profile['options'],
1838
			'is_guest' => false,
1839
			'primary_group' => $profile['primary_group'],
1840
			'group' => $profile['member_group'],
1841
			'group_color' => $profile['member_group_color'],
1842
			'group_id' => $profile['id_group'],
1843
			'post_group' => $profile['post_group'],
1844
			'post_group_color' => $profile['post_group_color'],
1845
			'group_icons' => str_repeat('<img src="' . str_replace('$language', $context['user']['language'], isset($profile['icons'][1]) ? $group_icon_url : '') . '" alt="*">', empty($profile['icons'][0]) || empty($profile['icons'][1]) ? 0 : $profile['icons'][0]),
1846
			'warning' => $profile['warning'],
1847
			'warning_status' => !empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $profile['warning'] ? 'mute' : (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $profile['warning'] ? 'moderate' : (!empty($modSettings['warning_watch']) && $modSettings['warning_watch'] <= $profile['warning'] ? 'watch' : (''))),
1848
			'local_time' => timeformat(time(), false, $profile['timezone']),
1849
			'custom_fields' => array(),
1850
		);
1851
	}
1852
1853
	// If the set isn't minimal then load their avatar as well.
1854
	if ($context['loadMemberContext_set'] != 'minimal')
1855
	{
1856
		$avatarData = set_avatar_data(array(
1857
			'filename' => $profile['filename'],
1858
			'avatar' => $profile['avatar'],
1859
			'email' => $profile['email_address'],
1860
		));
1861
1862
		if (!empty($avatarData['image']))
1863
			$memberContext[$user]['avatar'] = $avatarData;
1864
	}
1865
1866
	// Are we also loading the members custom fields into context?
1867
	if ($display_custom_fields && !empty($modSettings['displayFields']))
1868
	{
1869
		$memberContext[$user]['custom_fields'] = array();
1870
1871
		if (!isset($context['display_fields']))
1872
			$context['display_fields'] = $smcFunc['json_decode']($modSettings['displayFields'], true);
1873
1874
		foreach ($context['display_fields'] as $custom)
1875
		{
1876
			if (!isset($custom['col_name']) || trim($custom['col_name']) == '' || empty($profile['options'][$custom['col_name']]))
1877
				continue;
1878
1879
			$value = $profile['options'][$custom['col_name']];
1880
1881
			$fieldOptions = array();
1882
			$currentKey = 0;
1883
1884
			// Create a key => value array for multiple options fields
1885
			if (!empty($custom['options']))
1886
				foreach ($custom['options'] as $k => $v)
1887
				{
1888
					$fieldOptions[] = $v;
1889
					if (empty($currentKey))
1890
						$currentKey = $v == $value ? $k : 0;
1891
				}
1892
1893
			// BBC?
1894
			if ($custom['bbc'])
1895
				$value = parse_bbc($value);
1896
1897
			// ... or checkbox?
1898
			elseif (isset($custom['type']) && $custom['type'] == 'check')
1899
				$value = $value ? $txt['yes'] : $txt['no'];
1900
1901
			// Enclosing the user input within some other text?
1902
			$simple_value = $value;
1903
			if (!empty($custom['enclose']))
1904
				$value = strtr($custom['enclose'], array(
1905
					'{SCRIPTURL}' => $scripturl,
1906
					'{IMAGES_URL}' => $settings['images_url'],
1907
					'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1908
					'{INPUT}' => tokenTxtReplace($value),
1909
					'{KEY}' => $currentKey,
1910
				));
1911
1912
			$memberContext[$user]['custom_fields'][] = array(
1913
				'title' => tokenTxtReplace(!empty($custom['title']) ? $custom['title'] : $custom['col_name']),
1914
				'col_name' => tokenTxtReplace($custom['col_name']),
1915
				'value' => un_htmlspecialchars(tokenTxtReplace($value)),
1916
				'simple' => tokenTxtReplace($simple_value),
1917
				'raw' => $profile['options'][$custom['col_name']],
1918
				'placement' => !empty($custom['placement']) ? $custom['placement'] : 0,
1919
			);
1920
		}
1921
	}
1922
1923
	call_integration_hook('integrate_member_context', array(&$memberContext[$user], $user, $display_custom_fields));
1924
1925
	$already_loaded_custom_fields[$user] = !empty($already_loaded_custom_fields[$user]) | $display_custom_fields;
1926
1927
	return $memberContext[$user];
1928
}
1929
1930
/**
1931
 * Loads the user's custom profile fields
1932
 *
1933
 * @param integer|array $users A single user ID or an array of user IDs
1934
 * @param string|array $params Either a string or an array of strings with profile field names
1935
 * @return array|boolean An array of data about the fields and their values or false if nothing was loaded
1936
 */
1937
function loadMemberCustomFields($users, $params)
1938
{
1939
	global $smcFunc, $txt, $scripturl, $settings;
1940
1941
	// Do not waste my time...
1942
	if (empty($users) || empty($params))
1943
		return false;
1944
1945
	// Make sure it's an array.
1946
	$users = (array) array_unique($users);
1947
	$params = (array) array_unique($params);
1948
	$return = array();
1949
1950
	$request = $smcFunc['db_query']('', '
1951
		SELECT c.id_field, c.col_name, c.field_name, c.field_desc, c.field_type, c.field_order, c.field_length, c.field_options, c.mask, show_reg,
1952
		c.show_display, c.show_profile, c.private, c.active, c.bbc, c.can_search, c.default_value, c.enclose, c.placement, t.variable, t.value, t.id_member
1953
		FROM {db_prefix}themes AS t
1954
			LEFT JOIN {db_prefix}custom_fields AS c ON (c.col_name = t.variable)
1955
		WHERE id_member IN ({array_int:loaded_ids})
1956
			AND variable IN ({array_string:params})
1957
		ORDER BY field_order',
1958
		array(
1959
			'loaded_ids' => $users,
1960
			'params' => $params,
1961
		)
1962
	);
1963
1964
	while ($row = $smcFunc['db_fetch_assoc']($request))
1965
	{
1966
		$fieldOptions = array();
1967
		$currentKey = 0;
1968
		$row['field_name'] = tokenTxtReplace($row['field_name']);
1969
		$row['field_desc'] = tokenTxtReplace($row['field_desc']);
1970
1971
		// Create a key => value array for multiple options fields
1972
		if (!empty($row['field_options']))
1973
			foreach (explode(',', $row['field_options']) as $k => $v)
1974
			{
1975
				$fieldOptions[] = $v;
1976
				if (empty($currentKey))
1977
					$currentKey = $v == $row['value'] ? $k : 0;
1978
			}
1979
1980
		// BBC?
1981
		if (!empty($row['bbc']))
1982
			$row['value'] = parse_bbc($row['value']);
1983
1984
		// ... or checkbox?
1985
		elseif (isset($row['type']) && $row['type'] == 'check')
1986
			$row['value'] = !empty($row['value']) ? $txt['yes'] : $txt['no'];
1987
1988
		// Enclosing the user input within some other text?
1989
		if (!empty($row['enclose']))
1990
			$row['value'] = strtr($row['enclose'], array(
1991
				'{SCRIPTURL}' => $scripturl,
1992
				'{IMAGES_URL}' => $settings['images_url'],
1993
				'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1994
				'{INPUT}' => un_htmlspecialchars($row['value']),
1995
				'{KEY}' => $currentKey,
1996
			));
1997
1998
		// Send a simple array if there is just 1 param
1999
		if (count($params) == 1)
2000
			$return[$row['id_member']] = $row;
2001
2002
		// More than 1? knock yourself out...
2003
		else
2004
		{
2005
			if (!isset($return[$row['id_member']]))
2006
				$return[$row['id_member']] = array();
2007
2008
			$return[$row['id_member']][$row['variable']] = $row;
2009
		}
2010
	}
2011
2012
	$smcFunc['db_free_result']($request);
2013
2014
	return !empty($return) ? $return : false;
2015
}
2016
2017
/**
2018
 * Loads information about what browser the user is viewing with and places it in $context
2019
 *  - uses the class from {@link Class-BrowserDetect.php}
2020
 */
2021
function detectBrowser()
2022
{
2023
	// Load the current user's browser of choice
2024
	$detector = new browser_detector;
2025
	$detector->detectBrowser();
2026
}
2027
2028
/**
2029
 * Are we using this browser?
2030
 *
2031
 * Wrapper function for detectBrowser
2032
 *
2033
 * @param string $browser The browser we are checking for.
2034
 * @return bool Whether or not the current browser is what we're looking for
2035
 */
2036
function isBrowser($browser)
2037
{
2038
	global $context;
2039
2040
	// Don't know any browser!
2041
	if (empty($context['browser']))
2042
		detectBrowser();
2043
2044
	return !empty($context['browser'][$browser]) || !empty($context['browser']['is_' . $browser]) ? true : false;
2045
}
2046
2047
/**
2048
 * Load a theme, by ID.
2049
 *
2050
 * @param int $id_theme The ID of the theme to load
2051
 * @param bool $initialize Whether or not to initialize a bunch of theme-related variables/settings
2052
 */
2053
function loadTheme($id_theme = 0, $initialize = true)
2054
{
2055
	global $user_info, $user_settings, $board_info, $boarddir, $maintenance;
2056
	global $txt, $boardurl, $scripturl, $mbname, $modSettings;
2057
	global $context, $settings, $options, $sourcedir, $smcFunc, $language, $board, $cache_enable;
2058
2059
	if (empty($id_theme))
2060
	{
2061
		// The theme was specified by the board.
2062
		if (!empty($board_info['theme']))
2063
			$id_theme = $board_info['theme'];
2064
		// The theme is the forum's default.
2065
		else
2066
			$id_theme = $modSettings['theme_guests'];
2067
2068
		// Sometimes the user can choose their own theme.
2069
		if (!empty($modSettings['theme_allow']) || allowedTo('admin_forum'))
2070
		{
2071
			// The theme was specified by REQUEST.
2072
			if (!empty($_REQUEST['theme']) && (allowedTo('admin_forum') || in_array($_REQUEST['theme'], explode(',', $modSettings['knownThemes']))))
2073
			{
2074
				$id_theme = (int) $_REQUEST['theme'];
2075
				$_SESSION['id_theme'] = $id_theme;
2076
			}
2077
			// The theme was specified by REQUEST... previously.
2078
			elseif (!empty($_SESSION['id_theme']))
2079
				$id_theme = (int) $_SESSION['id_theme'];
2080
			// The theme is just the user's choice. (might use ?board=1;theme=0 to force board theme.)
2081
			elseif (!empty($user_info['theme']))
2082
				$id_theme = $user_info['theme'];
2083
		}
2084
2085
		// Verify the id_theme... no foul play.
2086
		// Always allow the board specific theme, if they are overriding.
2087
		if (!empty($board_info['theme']) && $board_info['override_theme'])
2088
			$id_theme = $board_info['theme'];
2089
		elseif (!empty($modSettings['enableThemes']))
2090
		{
2091
			$themes = explode(',', $modSettings['enableThemes']);
2092
			if (!in_array($id_theme, $themes))
2093
				$id_theme = $modSettings['theme_guests'];
2094
			else
2095
				$id_theme = (int) $id_theme;
2096
		}
2097
	}
2098
2099
	// Allow mod authors the option to override the theme id for custom page themes
2100
	call_integration_hook('integrate_pre_load_theme', array(&$id_theme));
2101
2102
	// We already load the basic stuff?
2103
	if (empty($settings['theme_id']) || $settings['theme_id'] != $id_theme)
2104
	{
2105
		$member = empty($user_info['id']) ? -1 : $user_info['id'];
2106
2107
		if (!empty($cache_enable) && $cache_enable >= 2 && ($temp = cache_get_data('theme_settings-' . $id_theme . ':' . $member, 60)) != null && time() - 60 > $modSettings['settings_updated'])
2108
		{
2109
			$themeData = $temp;
2110
			$flag = true;
2111
		}
2112
		elseif (($temp = cache_get_data('theme_settings-' . $id_theme, 90)) != null && time() - 60 > $modSettings['settings_updated'])
2113
			$themeData = $temp + array($member => array());
2114
		else
2115
			$themeData = array(-1 => array(), 0 => array(), $member => array());
2116
2117
		if (empty($flag))
2118
		{
2119
			// Load variables from the current or default theme, global or this user's.
2120
			$result = $smcFunc['db_query']('', '
2121
				SELECT variable, value, id_member, id_theme
2122
				FROM {db_prefix}themes
2123
				WHERE id_member' . (empty($themeData[0]) ? ' IN (-1, 0, {int:id_member})' : ' = {int:id_member}') . '
2124
					AND id_theme' . ($id_theme == 1 ? ' = {int:id_theme}' : ' IN ({int:id_theme}, 1)') . '
2125
				ORDER BY id_theme asc',
2126
				array(
2127
					'id_theme' => $id_theme,
2128
					'id_member' => $member,
2129
				)
2130
			);
2131
			// Pick between $settings and $options depending on whose data it is.
2132
			foreach ($smcFunc['db_fetch_all']($result) as $row)
2133
			{
2134
				// There are just things we shouldn't be able to change as members.
2135
				if ($row['id_member'] != 0 && in_array($row['variable'], array('actual_theme_url', 'actual_images_url', 'base_theme_dir', 'base_theme_url', 'default_images_url', 'default_theme_dir', 'default_theme_url', 'default_template', 'images_url', 'number_recent_posts', 'smiley_sets_default', 'theme_dir', 'theme_id', 'theme_layers', 'theme_templates', 'theme_url')))
2136
					continue;
2137
2138
				// If this is the theme_dir of the default theme, store it.
2139
				if (in_array($row['variable'], array('theme_dir', 'theme_url', 'images_url')) && $row['id_theme'] == '1' && empty($row['id_member']))
2140
					$themeData[0]['default_' . $row['variable']] = $row['value'];
2141
2142
				// If this isn't set yet, is a theme option, or is not the default theme..
2143
				if (!isset($themeData[$row['id_member']][$row['variable']]) || $row['id_theme'] != '1')
2144
					$themeData[$row['id_member']][$row['variable']] = substr($row['variable'], 0, 5) == 'show_' ? $row['value'] == '1' : $row['value'];
2145
			}
2146
			$smcFunc['db_free_result']($result);
2147
2148
			if (!empty($themeData[-1]))
2149
				foreach ($themeData[-1] as $k => $v)
2150
				{
2151
					if (!isset($themeData[$member][$k]))
2152
						$themeData[$member][$k] = $v;
2153
				}
2154
2155
			if (!empty($cache_enable) && $cache_enable >= 2)
2156
				cache_put_data('theme_settings-' . $id_theme . ':' . $member, $themeData, 60);
2157
			// Only if we didn't already load that part of the cache...
2158
			elseif (!isset($temp))
2159
				cache_put_data('theme_settings-' . $id_theme, array(-1 => $themeData[-1], 0 => $themeData[0]), 90);
2160
		}
2161
2162
		$settings = $themeData[0];
2163
		$options = $themeData[$member];
2164
2165
		$settings['theme_id'] = $id_theme;
2166
2167
		$settings['actual_theme_url'] = $settings['theme_url'];
2168
		$settings['actual_images_url'] = $settings['images_url'];
2169
		$settings['actual_theme_dir'] = $settings['theme_dir'];
2170
2171
		$settings['template_dirs'] = array();
2172
		// This theme first.
2173
		$settings['template_dirs'][] = $settings['theme_dir'];
2174
2175
		// Based on theme (if there is one).
2176
		if (!empty($settings['base_theme_dir']))
2177
			$settings['template_dirs'][] = $settings['base_theme_dir'];
2178
2179
		// Lastly the default theme.
2180
		if ($settings['theme_dir'] != $settings['default_theme_dir'])
2181
			$settings['template_dirs'][] = $settings['default_theme_dir'];
2182
	}
2183
2184
	if (!$initialize)
2185
		return;
2186
2187
	// Perhaps we've changed the agreement or privacy policy? Only redirect if:
2188
	// 1. They're not a guest or admin
2189
	// 2. This isn't called from SSI
2190
	// 3. This isn't an XML request
2191
	// 4. They're not trying to do any of the following actions:
2192
	// 4a. View or accept the agreement and/or policy
2193
	// 4b. Login or logout
2194
	// 4c. Get a feed (RSS, ATOM, etc.)
2195
	$agreement_actions = array(
2196
		'agreement' => true,
2197
		'acceptagreement' => true,
2198
		'login2' => true,
2199
		'logintfa' => true,
2200
		'logout' => true,
2201
		'pm' => array('sa' => array('popup')),
2202
		'profile' => array('area' => array('popup', 'alerts_popup')),
2203
		'xmlhttp' => true,
2204
		'.xml' => true,
2205
	);
2206
	if (empty($user_info['is_guest']) && empty($user_info['is_admin']) && SMF != 'SSI' && !isset($_REQUEST['xml']) && !is_filtered_request($agreement_actions, 'action'))
2207
	{
2208
		require_once($sourcedir . '/Agreement.php');
2209
		$can_accept_agreement = !empty($modSettings['requireAgreement']) && canRequireAgreement();
2210
		$can_accept_privacy_policy = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy();
2211
2212
		if ($can_accept_agreement || $can_accept_privacy_policy)
2213
			redirectexit('action=agreement');
2214
	}
2215
2216
	// Check to see if we're forcing SSL
2217
	if (!empty($modSettings['force_ssl']) && empty($maintenance) &&
2218
		!httpsOn() && SMF != 'SSI')
2219
	{
2220
		if (isset($_GET['sslRedirect']))
2221
		{
2222
			loadLanguage('Errors');
2223
			fatal_lang_error('login_ssl_required', false);
2224
		}
2225
2226
		redirectexit(strtr($_SERVER['REQUEST_URL'], array('http://' => 'https://')) . (strpos($_SERVER['REQUEST_URL'], '?') > 0 ? ';' : '?') . 'sslRedirect');
2227
	}
2228
2229
	// Check to see if they're accessing it from the wrong place.
2230
	if (isset($_SERVER['HTTP_HOST']) || isset($_SERVER['SERVER_NAME']))
2231
	{
2232
		$detected_url = httpsOn() ? 'https://' : 'http://';
2233
		$detected_url .= empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST'];
2234
		$temp = preg_replace('~/' . basename($scripturl) . '(/.+)?$~', '', strtr(dirname($_SERVER['PHP_SELF']), '\\', '/'));
2235
		if ($temp != '/')
2236
			$detected_url .= $temp;
2237
	}
2238
	if (isset($detected_url) && $detected_url != $boardurl)
2239
	{
2240
		// Try #1 - check if it's in a list of alias addresses.
2241
		if (!empty($modSettings['forum_alias_urls']))
2242
		{
2243
			$aliases = explode(',', $modSettings['forum_alias_urls']);
2244
2245
			foreach ($aliases as $alias)
2246
			{
2247
				// Rip off all the boring parts, spaces, etc.
2248
				if ($detected_url == trim($alias) || strtr($detected_url, array('http://' => '', 'https://' => '')) == trim($alias))
2249
					$do_fix = true;
2250
			}
2251
		}
2252
2253
		// Hmm... check #2 - is it just different by a www?  Send them to the correct place!!
2254
		if (empty($do_fix) && strtr($detected_url, array('://' => '://www.')) == $boardurl && (empty($_GET) || count($_GET) == 1) && SMF != 'SSI')
2255
		{
2256
			// Okay, this seems weird, but we don't want an endless loop - this will make $_GET not empty ;).
2257
			if (empty($_GET))
2258
				redirectexit('wwwRedirect');
2259
			else
2260
			{
2261
				$k = key($_GET);
2262
				$v = current($_GET);
2263
2264
				if ($k != 'wwwRedirect')
2265
					redirectexit('wwwRedirect;' . $k . '=' . $v);
2266
			}
2267
		}
2268
2269
		// #3 is just a check for SSL...
2270
		if (strtr($detected_url, array('https://' => 'http://')) == $boardurl)
2271
			$do_fix = true;
2272
2273
		// Okay, #4 - perhaps it's an IP address?  We're gonna want to use that one, then. (assuming it's the IP or something...)
2274
		if (!empty($do_fix) || preg_match('~^http[s]?://(?:[\d\.:]+|\[[\d:]+\](?::\d+)?)(?:$|/)~', $detected_url) == 1)
2275
		{
2276
			// Caching is good ;).
2277
			$oldurl = $boardurl;
2278
2279
			// Fix $boardurl and $scripturl.
2280
			$boardurl = $detected_url;
2281
			$scripturl = strtr($scripturl, array($oldurl => $boardurl));
2282
			$_SERVER['REQUEST_URL'] = strtr($_SERVER['REQUEST_URL'], array($oldurl => $boardurl));
2283
2284
			// Fix the theme urls...
2285
			$settings['theme_url'] = strtr($settings['theme_url'], array($oldurl => $boardurl));
2286
			$settings['default_theme_url'] = strtr($settings['default_theme_url'], array($oldurl => $boardurl));
2287
			$settings['actual_theme_url'] = strtr($settings['actual_theme_url'], array($oldurl => $boardurl));
2288
			$settings['images_url'] = strtr($settings['images_url'], array($oldurl => $boardurl));
2289
			$settings['default_images_url'] = strtr($settings['default_images_url'], array($oldurl => $boardurl));
2290
			$settings['actual_images_url'] = strtr($settings['actual_images_url'], array($oldurl => $boardurl));
2291
2292
			// And just a few mod settings :).
2293
			$modSettings['smileys_url'] = strtr($modSettings['smileys_url'], array($oldurl => $boardurl));
2294
			$modSettings['avatar_url'] = strtr($modSettings['avatar_url'], array($oldurl => $boardurl));
2295
			$modSettings['custom_avatar_url'] = strtr($modSettings['custom_avatar_url'], array($oldurl => $boardurl));
2296
2297
			// Clean up after loadBoard().
2298
			if (isset($board_info['moderators']))
2299
			{
2300
				foreach ($board_info['moderators'] as $k => $dummy)
2301
				{
2302
					$board_info['moderators'][$k]['href'] = strtr($dummy['href'], array($oldurl => $boardurl));
2303
					$board_info['moderators'][$k]['link'] = strtr($dummy['link'], array('"' . $oldurl => '"' . $boardurl));
2304
				}
2305
			}
2306
			foreach ($context['linktree'] as $k => $dummy)
2307
				$context['linktree'][$k]['url'] = strtr($dummy['url'], array($oldurl => $boardurl));
2308
		}
2309
	}
2310
	// Set up the contextual user array.
2311
	if (!empty($user_info))
2312
	{
2313
		$context['user'] = array(
2314
			'id' => $user_info['id'],
2315
			'is_logged' => !$user_info['is_guest'],
2316
			'is_guest' => &$user_info['is_guest'],
2317
			'is_admin' => &$user_info['is_admin'],
2318
			'is_mod' => &$user_info['is_mod'],
2319
			// A user can mod if they have permission to see the mod center, or they are a board/group/approval moderator.
2320
			'can_mod' => allowedTo('access_mod_center') || (!$user_info['is_guest'] && ($user_info['mod_cache']['gq'] != '0=1' || $user_info['mod_cache']['bq'] != '0=1' || ($modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap'])))),
2321
			'name' => $user_info['username'],
2322
			'language' => $user_info['language'],
2323
			'email' => $user_info['email'],
2324
			'ignoreusers' => $user_info['ignoreusers'],
2325
		);
2326
		if (!$context['user']['is_guest'])
2327
			$context['user']['name'] = $user_info['name'];
2328
		elseif ($context['user']['is_guest'] && !empty($txt['guest_title']))
2329
			$context['user']['name'] = $txt['guest_title'];
2330
2331
		// Determine the current smiley set.
2332
		$smiley_sets_known = explode(',', $modSettings['smiley_sets_known']);
2333
		$user_info['smiley_set'] = (!in_array($user_info['smiley_set'], $smiley_sets_known) && $user_info['smiley_set'] != 'none') || empty($modSettings['smiley_sets_enable']) ? (!empty($settings['smiley_sets_default']) ? $settings['smiley_sets_default'] : $modSettings['smiley_sets_default']) : $user_info['smiley_set'];
2334
		$context['user']['smiley_set'] = $user_info['smiley_set'];
2335
	}
2336
	else
2337
	{
2338
		// What to do when there is no $user_info (e.g., an error very early in the login process)
2339
		$context['user'] = array(
2340
			'id' => -1,
2341
			'is_logged' => false,
2342
			'is_guest' => true,
2343
			'is_mod' => false,
2344
			'can_mod' => false,
2345
			'name' => $txt['guest_title'],
2346
			'language' => $language,
2347
			'email' => '',
2348
			'ignoreusers' => array(),
2349
		);
2350
		// Note we should stuff $user_info with some guest values also...
2351
		$user_info = array(
2352
			'id' => 0,
2353
			'is_guest' => true,
2354
			'is_admin' => false,
2355
			'is_mod' => false,
2356
			'username' => $txt['guest_title'],
2357
			'language' => $language,
2358
			'email' => '',
2359
			'smiley_set' => '',
2360
			'permissions' => array(),
2361
			'groups' => array(),
2362
			'ignoreusers' => array(),
2363
			'possibly_robot' => true,
2364
			'time_offset' => 0,
2365
			'timezone' => $modSettings['default_timezone'],
2366
			'time_format' => $modSettings['time_format'],
2367
		);
2368
	}
2369
2370
	// Some basic information...
2371
	if (!isset($context['html_headers']))
2372
		$context['html_headers'] = '';
2373
	if (!isset($context['javascript_files']))
2374
		$context['javascript_files'] = array();
2375
	if (!isset($context['css_files']))
2376
		$context['css_files'] = array();
2377
	if (!isset($context['css_header']))
2378
		$context['css_header'] = array();
2379
	if (!isset($context['javascript_inline']))
2380
		$context['javascript_inline'] = array('standard' => array(), 'defer' => array());
2381
	if (!isset($context['javascript_vars']))
2382
		$context['javascript_vars'] = array();
2383
2384
	$context['login_url'] = $scripturl . '?action=login2';
2385
	$context['menu_separator'] = !empty($settings['use_image_buttons']) ? ' ' : ' | ';
2386
	$context['session_var'] = $_SESSION['session_var'];
2387
	$context['session_id'] = $_SESSION['session_value'];
2388
	$context['forum_name'] = $mbname;
2389
	$context['forum_name_html_safe'] = $smcFunc['htmlspecialchars']($context['forum_name']);
2390
	$context['header_logo_url_html_safe'] = empty($settings['header_logo_url']) ? '' : $smcFunc['htmlspecialchars']($settings['header_logo_url']);
2391
	$context['current_action'] = isset($_REQUEST['action']) ? $smcFunc['htmlspecialchars']($_REQUEST['action']) : null;
2392
	$context['current_subaction'] = isset($_REQUEST['sa']) ? $_REQUEST['sa'] : null;
2393
	$context['can_register'] = empty($modSettings['registration_method']) || $modSettings['registration_method'] != 3;
2394
	if (isset($modSettings['load_average']))
2395
		$context['load_average'] = $modSettings['load_average'];
2396
2397
	// Detect the browser. This is separated out because it's also used in attachment downloads
2398
	detectBrowser();
2399
2400
	// Set the top level linktree up.
2401
	// Note that if we're dealing with certain very early errors (e.g., login) the linktree might not be set yet...
2402
	if (empty($context['linktree']))
2403
		$context['linktree'] = array();
2404
	array_unshift($context['linktree'], array(
2405
		'url' => $scripturl,
2406
		'name' => $context['forum_name_html_safe']
2407
	));
2408
2409
	// This allows sticking some HTML on the page output - useful for controls.
2410
	$context['insert_after_template'] = '';
2411
2412
	if (!isset($txt))
2413
		$txt = array();
2414
2415
	$simpleActions = array(
2416
		'findmember',
2417
		'helpadmin',
2418
		'printpage',
2419
	);
2420
2421
	// Parent action => array of areas
2422
	$simpleAreas = array(
2423
		'profile' => array('popup', 'alerts_popup',),
2424
	);
2425
2426
	// Parent action => array of subactions
2427
	$simpleSubActions = array(
2428
		'pm' => array('popup',),
2429
		'signup' => array('usernamecheck'),
2430
	);
2431
2432
	// Extra params like ;preview ;js, etc.
2433
	$extraParams = array(
2434
		'preview',
2435
		'splitjs',
2436
	);
2437
2438
	// Actions that specifically uses XML output.
2439
	$xmlActions = array(
2440
		'quotefast',
2441
		'jsmodify',
2442
		'xmlhttp',
2443
		'post2',
2444
		'suggest',
2445
		'stats',
2446
		'notifytopic',
2447
		'notifyboard',
2448
	);
2449
2450
	call_integration_hook('integrate_simple_actions', array(&$simpleActions, &$simpleAreas, &$simpleSubActions, &$extraParams, &$xmlActions));
2451
2452
	$context['simple_action'] = in_array($context['current_action'], $simpleActions) ||
2453
		(isset($simpleAreas[$context['current_action']]) && isset($_REQUEST['area']) && in_array($_REQUEST['area'], $simpleAreas[$context['current_action']])) ||
2454
		(isset($simpleSubActions[$context['current_action']]) && in_array($context['current_subaction'], $simpleSubActions[$context['current_action']]));
2455
2456
	// See if theres any extra param to check.
2457
	$requiresXML = false;
2458
	foreach ($extraParams as $key => $extra)
2459
		if (isset($_REQUEST[$extra]))
2460
			$requiresXML = true;
2461
2462
	// Output is fully XML, so no need for the index template.
2463
	if (isset($_REQUEST['xml']) && (in_array($context['current_action'], $xmlActions) || $requiresXML))
2464
	{
2465
		loadLanguage('index+Modifications');
2466
		loadTemplate('Xml');
2467
		$context['template_layers'] = array();
2468
	}
2469
2470
	// These actions don't require the index template at all.
2471
	elseif (!empty($context['simple_action']))
2472
	{
2473
		loadLanguage('index+Modifications');
2474
		$context['template_layers'] = array();
2475
	}
2476
2477
	else
2478
	{
2479
		// Custom templates to load, or just default?
2480
		if (isset($settings['theme_templates']))
2481
			$templates = explode(',', $settings['theme_templates']);
2482
		else
2483
			$templates = array('index');
2484
2485
		// Load each template...
2486
		foreach ($templates as $template)
2487
			loadTemplate($template);
2488
2489
		// ...and attempt to load their associated language files.
2490
		$required_files = implode('+', array_merge($templates, array('Modifications')));
2491
		loadLanguage($required_files, '', false);
2492
2493
		// Custom template layers?
2494
		if (isset($settings['theme_layers']))
2495
			$context['template_layers'] = explode(',', $settings['theme_layers']);
2496
		else
2497
			$context['template_layers'] = array('html', 'body');
2498
	}
2499
2500
	// Initialize the theme.
2501
	loadSubTemplate('init', 'ignore');
2502
2503
	// Allow overriding the board wide time/number formats.
2504
	if (empty($user_settings['time_format']) && !empty($txt['time_format']))
2505
		$user_info['time_format'] = $txt['time_format'];
2506
2507
	// Set the character set from the template.
2508
	$context['character_set'] = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set'];
2509
	$context['right_to_left'] = !empty($txt['lang_rtl']);
2510
2511
	// Guests may still need a name.
2512
	if ($context['user']['is_guest'] && empty($context['user']['name']))
2513
		$context['user']['name'] = $txt['guest_title'];
2514
2515
	// Any theme-related strings that need to be loaded?
2516
	if (!empty($settings['require_theme_strings']))
2517
		loadLanguage('ThemeStrings', '', false);
2518
2519
	// Make a special URL for the language.
2520
	$settings['lang_images_url'] = $settings['images_url'] . '/' . (!empty($txt['image_lang']) ? $txt['image_lang'] : $user_info['language']);
2521
2522
	// And of course, let's load the default CSS file.
2523
	loadCSSFile('index.css', array('minimize' => true, 'order_pos' => 1), 'smf_index');
2524
2525
	// Here is my luvly Responsive CSS
2526
	loadCSSFile('responsive.css', array('force_current' => false, 'validate' => true, 'minimize' => true, 'order_pos' => 9000), 'smf_responsive');
2527
2528
	if ($context['right_to_left'])
2529
		loadCSSFile('rtl.css', array('order_pos' => 4000), 'smf_rtl');
2530
2531
	// We allow theme variants, because we're cool.
2532
	$context['theme_variant'] = '';
2533
	$context['theme_variant_url'] = '';
2534
	if (!empty($settings['theme_variants']))
2535
	{
2536
		// Overriding - for previews and that ilk.
2537
		if (!empty($_REQUEST['variant']))
2538
			$_SESSION['id_variant'] = $_REQUEST['variant'];
2539
		// User selection?
2540
		if (empty($settings['disable_user_variant']) || allowedTo('admin_forum'))
2541
			$context['theme_variant'] = !empty($_SESSION['id_variant']) && in_array($_SESSION['id_variant'], $settings['theme_variants']) ? $_SESSION['id_variant'] : (!empty($options['theme_variant']) && in_array($options['theme_variant'], $settings['theme_variants']) ? $options['theme_variant'] : '');
2542
		// If not a user variant, select the default.
2543
		if ($context['theme_variant'] == '' || !in_array($context['theme_variant'], $settings['theme_variants']))
2544
			$context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0];
2545
2546
		// Do this to keep things easier in the templates.
2547
		$context['theme_variant'] = '_' . $context['theme_variant'];
2548
		$context['theme_variant_url'] = $context['theme_variant'] . '/';
2549
2550
		if (!empty($context['theme_variant']))
2551
		{
2552
			loadCSSFile('index' . $context['theme_variant'] . '.css', array('order_pos' => 300), 'smf_index' . $context['theme_variant']);
2553
			if ($context['right_to_left'])
2554
				loadCSSFile('rtl' . $context['theme_variant'] . '.css', array('order_pos' => 4200), 'smf_rtl' . $context['theme_variant']);
2555
		}
2556
	}
2557
2558
	// Let's be compatible with old themes!
2559
	if (!function_exists('template_html_above') && in_array('html', $context['template_layers']))
2560
		$context['template_layers'] = array('main');
2561
2562
	$context['tabindex'] = 1;
2563
2564
	// Compatibility.
2565
	if (!isset($settings['theme_version']))
2566
		$modSettings['memberCount'] = $modSettings['totalMembers'];
2567
2568
	// Default JS variables for use in every theme
2569
	$context['javascript_vars'] = array(
2570
		'smf_theme_url' => '"' . $settings['theme_url'] . '"',
2571
		'smf_default_theme_url' => '"' . $settings['default_theme_url'] . '"',
2572
		'smf_images_url' => '"' . $settings['images_url'] . '"',
2573
		'smf_smileys_url' => '"' . $modSettings['smileys_url'] . '"',
2574
		'smf_smiley_sets' => '"' . $modSettings['smiley_sets_known'] . '"',
2575
		'smf_smiley_sets_default' => '"' . $modSettings['smiley_sets_default'] . '"',
2576
		'smf_avatars_url' => '"' . $modSettings['avatar_url'] . '"',
2577
		'smf_scripturl' => '"' . $scripturl . '"',
2578
		'smf_iso_case_folding' => $context['server']['iso_case_folding'] ? 'true' : 'false',
2579
		'smf_charset' => '"' . $context['character_set'] . '"',
2580
		'smf_session_id' => '"' . $context['session_id'] . '"',
2581
		'smf_session_var' => '"' . $context['session_var'] . '"',
2582
		'smf_member_id' => $context['user']['id'],
2583
		'ajax_notification_text' => JavaScriptEscape($txt['ajax_in_progress']),
2584
		'help_popup_heading_text' => JavaScriptEscape($txt['help_popup']),
2585
		'banned_text' => JavaScriptEscape(sprintf($txt['your_ban'], $context['user']['name'])),
2586
		'smf_txt_expand' => JavaScriptEscape($txt['code_expand']),
2587
		'smf_txt_shrink' => JavaScriptEscape($txt['code_shrink']),
2588
		'smf_collapseAlt' => JavaScriptEscape($txt['hide']),
2589
		'smf_expandAlt' => JavaScriptEscape($txt['show']),
2590
		'smf_quote_expand' => !empty($modSettings['quote_expand']) ? $modSettings['quote_expand'] : 'false',
2591
		'allow_xhjr_credentials' => !empty($modSettings['allow_cors_credentials']) ? 'true' : 'false',
2592
	);
2593
2594
	// Add the JQuery library to the list of files to load.
2595
	$jQueryUrls = array ('cdn' => 'https://ajax.googleapis.com/ajax/libs/jquery/'. JQUERY_VERSION . '/jquery.min.js', 'jquery_cdn' => 'https://code.jquery.com/jquery-'. JQUERY_VERSION . '.min.js', 'microsoft_cdn' => 'https://ajax.aspnetcdn.com/ajax/jQuery/jquery-'. JQUERY_VERSION . '.min.js');
2596
2597
	if (isset($modSettings['jquery_source']) && array_key_exists($modSettings['jquery_source'], $jQueryUrls))
2598
		loadJavaScriptFile($jQueryUrls[$modSettings['jquery_source']], array('external' => true, 'seed' => false), 'smf_jquery');
2599
2600
	elseif (isset($modSettings['jquery_source']) && $modSettings['jquery_source'] == 'local')
2601
		loadJavaScriptFile('jquery-' . JQUERY_VERSION . '.min.js', array('seed' => false), 'smf_jquery');
2602
2603
	elseif (isset($modSettings['jquery_source'], $modSettings['jquery_custom']) && $modSettings['jquery_source'] == 'custom')
2604
		loadJavaScriptFile($modSettings['jquery_custom'], array('external' => true, 'seed' => false), 'smf_jquery');
2605
2606
	// Fall back to the forum default
2607
	else
2608
		loadJavaScriptFile('https://ajax.googleapis.com/ajax/libs/jquery/' . JQUERY_VERSION . '/jquery.min.js', array('external' => true, 'seed' => false), 'smf_jquery');
2609
2610
	// Queue our JQuery plugins!
2611
	loadJavaScriptFile('smf_jquery_plugins.js', array('minimize' => true), 'smf_jquery_plugins');
2612
	if (!$user_info['is_guest'])
2613
	{
2614
		loadJavaScriptFile('jquery.custom-scrollbar.js', array('minimize' => true), 'smf_jquery_scrollbar');
2615
		loadCSSFile('jquery.custom-scrollbar.css', array('force_current' => false, 'validate' => true), 'smf_scrollbar');
2616
	}
2617
2618
	// script.js and theme.js, always required, so always add them! Makes index.template.php cleaner and all.
2619
	loadJavaScriptFile('script.js', array('defer' => false, 'minimize' => true), 'smf_script');
2620
	loadJavaScriptFile('theme.js', array('minimize' => true), 'smf_theme');
2621
2622
	// And we should probably trigger the cron too.
2623
	if (empty($modSettings['cron_is_real_cron']))
2624
	{
2625
		$ts = time();
2626
		$ts -= $ts % 15;
2627
		addInlineJavaScript('
2628
	function triggerCron()
2629
	{
2630
		$.get(' . JavaScriptEscape($boardurl) . ' + "/cron.php?ts=' . $ts . '");
2631
	}
2632
	window.setTimeout(triggerCron, 1);', true);
2633
2634
		// Robots won't normally trigger cron.php, so for them run the scheduled tasks directly.
2635
		if (isBrowser('possibly_robot') && (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time() || (!empty($modSettings['mail_next_send']) && $modSettings['mail_next_send'] < time() && empty($modSettings['mail_queue_use_cron']))))
2636
		{
2637
			require_once($sourcedir . '/ScheduledTasks.php');
2638
2639
			// What to do, what to do?!
2640
			if (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
2641
				AutoTask();
2642
			else
2643
				ReduceMailQueue();
2644
		}
2645
	}
2646
2647
	// Filter out the restricted boards from the linktree
2648
	if (!$user_info['is_admin'] && !empty($board))
2649
	{
2650
		foreach ($context['linktree'] as $k => $element)
2651
		{
2652
			if (!empty($element['groups']) &&
2653
				(count(array_intersect($user_info['groups'], $element['groups'])) == 0 ||
2654
					(!empty($modSettings['deny_boards_access']) && count(array_intersect($user_info['groups'], $element['deny_groups'])) != 0)))
2655
			{
2656
				$context['linktree'][$k]['name'] = $txt['restricted_board'];
2657
				$context['linktree'][$k]['extra_before'] = '<i>';
2658
				$context['linktree'][$k]['extra_after'] = '</i>';
2659
				unset($context['linktree'][$k]['url']);
2660
			}
2661
		}
2662
	}
2663
2664
	// Any files to include at this point?
2665
	if (!empty($modSettings['integrate_theme_include']))
2666
	{
2667
		$theme_includes = explode(',', $modSettings['integrate_theme_include']);
2668
		foreach ($theme_includes as $include)
2669
		{
2670
			$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
2671
			if (file_exists($include))
2672
				require_once($include);
2673
		}
2674
	}
2675
2676
	// Call load theme integration functions.
2677
	call_integration_hook('integrate_load_theme');
2678
2679
	// We are ready to go.
2680
	$context['theme_loaded'] = true;
2681
}
2682
2683
/**
2684
 * Load a template - if the theme doesn't include it, use the default.
2685
 * What this function does:
2686
 *  - loads a template file with the name template_name from the current, default, or base theme.
2687
 *  - detects a wrong default theme directory and tries to work around it.
2688
 *
2689
 * @uses template_include() to include the file.
2690
 * @param string $template_name The name of the template to load
2691
 * @param array|string $style_sheets The name of a single stylesheet or an array of names of stylesheets to load
2692
 * @param bool $fatal If true, dies with an error message if the template cannot be found
2693
 * @return boolean Whether or not the template was loaded
2694
 */
2695
function loadTemplate($template_name, $style_sheets = array(), $fatal = true)
2696
{
2697
	global $context, $settings, $txt, $scripturl, $boarddir, $db_show_debug;
2698
2699
	// Do any style sheets first, cause we're easy with those.
2700
	if (!empty($style_sheets))
2701
	{
2702
		if (!is_array($style_sheets))
2703
			$style_sheets = array($style_sheets);
2704
2705
		foreach ($style_sheets as $sheet)
2706
			loadCSSFile($sheet . '.css', array(), $sheet);
2707
	}
2708
2709
	// No template to load?
2710
	if ($template_name === false)
2711
		return true;
2712
2713
	$loaded = false;
2714
	foreach ($settings['template_dirs'] as $template_dir)
2715
	{
2716
		if (file_exists($template_dir . '/' . $template_name . '.template.php'))
2717
		{
2718
			$loaded = true;
2719
			template_include($template_dir . '/' . $template_name . '.template.php', true);
2720
			break;
2721
		}
2722
	}
2723
2724
	if ($loaded)
2725
	{
2726
		if ($db_show_debug === true)
2727
			$context['debug']['templates'][] = $template_name . ' (' . basename($template_dir) . ')';
2728
2729
		// If they have specified an initialization function for this template, go ahead and call it now.
2730
		if (function_exists('template_' . $template_name . '_init'))
2731
			call_user_func('template_' . $template_name . '_init');
2732
	}
2733
	// Hmmm... doesn't exist?!  I don't suppose the directory is wrong, is it?
2734
	elseif (!file_exists($settings['default_theme_dir']) && file_exists($boarddir . '/Themes/default'))
2735
	{
2736
		$settings['default_theme_dir'] = $boarddir . '/Themes/default';
2737
		$settings['template_dirs'][] = $settings['default_theme_dir'];
2738
2739
		if (!empty($context['user']['is_admin']) && !isset($_GET['th']))
2740
		{
2741
			loadLanguage('Errors');
2742
			echo '
2743
<div class="alert errorbox">
2744
	<a href="', $scripturl . '?action=admin;area=theme;sa=list;' . $context['session_var'] . '=' . $context['session_id'], '" class="alert">', $txt['theme_dir_wrong'], '</a>
2745
</div>';
2746
		}
2747
2748
		loadTemplate($template_name);
2749
	}
2750
	// Cause an error otherwise.
2751
	elseif ($template_name != 'Errors' && $template_name != 'index' && $fatal)
2752
		fatal_lang_error('theme_template_error', 'template', array((string) $template_name));
2753
	elseif ($fatal)
2754
		die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load Themes/default/%s.template.php!', (string) $template_name), 'template'));
2755
	else
2756
		return false;
2757
}
2758
2759
/**
2760
 * Load a sub-template.
2761
 * What it does:
2762
 * 	- loads the sub template specified by sub_template_name, which must be in an already-loaded template.
2763
 *  - if ?debug is in the query string, shows administrators a marker after every sub template
2764
 *	for debugging purposes.
2765
 *
2766
 * @todo get rid of reading $_REQUEST directly
2767
 *
2768
 * @param string $sub_template_name The name of the sub-template to load
2769
 * @param bool $fatal Whether to die with an error if the sub-template can't be loaded
2770
 */
2771
function loadSubTemplate($sub_template_name, $fatal = false)
2772
{
2773
	global $context, $txt, $db_show_debug;
2774
2775
	if ($db_show_debug === true)
2776
		$context['debug']['sub_templates'][] = $sub_template_name;
2777
2778
	// Figure out what the template function is named.
2779
	$theme_function = 'template_' . $sub_template_name;
2780
	if (function_exists($theme_function))
2781
		$theme_function();
2782
	elseif ($fatal === false)
2783
		fatal_lang_error('theme_template_error', 'template', array((string) $sub_template_name));
2784
	elseif ($fatal !== 'ignore')
2785
		die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load the %s sub template!', (string) $sub_template_name), 'template'));
2786
2787
	// Are we showing debugging for templates?  Just make sure not to do it before the doctype...
2788
	if (allowedTo('admin_forum') && isset($_REQUEST['debug']) && !in_array($sub_template_name, array('init', 'main_below')) && ob_get_length() > 0 && !isset($_REQUEST['xml']))
2789
	{
2790
		echo '
2791
<div class="noticebox">---- ', $sub_template_name, ' ends ----</div>';
2792
	}
2793
}
2794
2795
/**
2796
 * Add a CSS file for output later
2797
 *
2798
 * @param string $fileName The name of the file to load
2799
 * @param array $params An array of parameters
2800
 * Keys are the following:
2801
 * 	- ['external'] (true/false): define if the file is a externally located file. Needs to be set to true if you are loading an external file
2802
 * 	- ['default_theme'] (true/false): force use of default theme url
2803
 * 	- ['force_current'] (true/false): if this is false, we will attempt to load the file from the default theme if not found in the current theme
2804
 *  - ['validate'] (true/false): if true script will validate the local file exists
2805
 *  - ['rtl'] (string): additional file to load in RTL mode
2806
 *  - ['seed'] (true/false/string): if true or null, use cache stale, false do not, or used a supplied string
2807
 *  - ['minimize'] boolean to add your file to the main minimized file. Useful when you have a file thats loaded everywhere and for everyone.
2808
 *  - ['order_pos'] int define the load order, when not define it's loaded in the middle, before index.css = -500, after index.css = 500, middle = 3000, end (i.e. after responsive.css) = 10000
2809
 *  - ['attributes'] array extra attributes to add to the element
2810
 * @param string $id An ID to stick on the end of the filename for caching purposes
2811
 */
2812
function loadCSSFile($fileName, $params = array(), $id = '')
2813
{
2814
	global $settings, $context, $modSettings;
2815
2816
	if (empty($context['css_files_order']))
2817
		$context['css_files_order'] = array();
2818
2819
	$params['seed'] = (!array_key_exists('seed', $params) || (array_key_exists('seed', $params) && $params['seed'] === true)) ?
2820
		(array_key_exists('browser_cache', $context) ? $context['browser_cache'] : '') :
2821
		(is_string($params['seed']) ? '?' . ltrim($params['seed'], '?') : '');
2822
	$params['force_current'] = isset($params['force_current']) ? $params['force_current'] : false;
2823
	$themeRef = !empty($params['default_theme']) ? 'default_theme' : 'theme';
2824
	$params['minimize'] = isset($params['minimize']) ? $params['minimize'] : true;
2825
	$params['external'] = isset($params['external']) ? $params['external'] : false;
2826
	$params['validate'] = isset($params['validate']) ? $params['validate'] : true;
2827
	$params['order_pos'] = isset($params['order_pos']) ? (int) $params['order_pos'] : 3000;
2828
	$params['attributes'] = isset($params['attributes']) ? $params['attributes'] : array();
2829
2830
	// Account for shorthand like admin.css?alp21 filenames
2831
	$id = (empty($id) ? strtr(str_replace('.css', '', basename($fileName)), '?', '_') : $id) . '_css';
2832
2833
	$fileName = str_replace(pathinfo($fileName, PATHINFO_EXTENSION), strtok(pathinfo($fileName, PATHINFO_EXTENSION), '?'), $fileName);
2834
2835
	// Is this a local file?
2836
	if (empty($params['external']))
2837
	{
2838
		// Are we validating the the file exists?
2839
		if (!empty($params['validate']) && ($mtime = @filemtime($settings[$themeRef . '_dir'] . '/css/' . $fileName)) === false)
2840
		{
2841
			// Maybe the default theme has it?
2842
			if ($themeRef === 'theme' && !$params['force_current'] && ($mtime = @filemtime($settings['default_theme_dir'] . '/css/' . $fileName) !== false))
2843
			{
2844
				$fileUrl = $settings['default_theme_url'] . '/css/' . $fileName;
2845
				$filePath = $settings['default_theme_dir'] . '/css/' . $fileName;
2846
			}
2847
			else
2848
			{
2849
				$fileUrl = false;
2850
				$filePath = false;
2851
			}
2852
		}
2853
		else
2854
		{
2855
			$fileUrl = $settings[$themeRef . '_url'] . '/css/' . $fileName;
2856
			$filePath = $settings[$themeRef . '_dir'] . '/css/' . $fileName;
2857
			$mtime = @filemtime($filePath);
2858
		}
2859
	}
2860
	// An external file doesn't have a filepath. Mock one for simplicity.
2861
	else
2862
	{
2863
		$fileUrl = $fileName;
2864
		$filePath = $fileName;
2865
2866
		// Always turn these off for external files.
2867
		$params['minimize'] = false;
2868
		$params['seed'] = false;
2869
	}
2870
2871
	$mtime = empty($mtime) ? 0 : $mtime;
2872
2873
	// Add it to the array for use in the template
2874
	if (!empty($fileName) && !empty($fileUrl))
2875
	{
2876
		// find a free number/position
2877
		while (isset($context['css_files_order'][$params['order_pos']]))
2878
			$params['order_pos']++;
2879
		$context['css_files_order'][$params['order_pos']] = $id;
2880
2881
		$context['css_files'][$id] = array('fileUrl' => $fileUrl, 'filePath' => $filePath, 'fileName' => $fileName, 'options' => $params, 'mtime' => $mtime);
2882
	}
2883
2884
	if (!empty($context['right_to_left']) && !empty($params['rtl']))
2885
		loadCSSFile($params['rtl'], array_diff_key($params, array('rtl' => 0)));
2886
2887
	if ($mtime > $modSettings['browser_cache'])
2888
		updateSettings(array('browser_cache' => $mtime));
2889
}
2890
2891
/**
2892
 * Add a block of inline css code to be executed later
2893
 *
2894
 * - only use this if you have to, generally external css files are better, but for very small changes
2895
 *   or for scripts that require help from PHP/whatever, this can be useful.
2896
 * - all code added with this function is added to the same <style> tag so do make sure your css is valid!
2897
 *
2898
 * @param string $css Some css code
2899
 * @return void|bool Adds the CSS to the $context['css_header'] array or returns if no CSS is specified
2900
 */
2901
function addInlineCss($css)
2902
{
2903
	global $context;
2904
2905
	// Gotta add something...
2906
	if (empty($css))
2907
		return false;
2908
2909
	$context['css_header'][] = $css;
2910
}
2911
2912
/**
2913
 * Add a Javascript file for output later
2914
 *
2915
 * @param string $fileName The name of the file to load
2916
 * @param array $params An array of parameter info
2917
 * Keys are the following:
2918
 * 	- ['external'] (true/false): define if the file is a externally located file. Needs to be set to true if you are loading an external file
2919
 * 	- ['default_theme'] (true/false): force use of default theme url
2920
 * 	- ['defer'] (true/false): define if the file should load in <head> or before the closing <html> tag
2921
 * 	- ['force_current'] (true/false): if this is false, we will attempt to load the file from the
2922
 *	default theme if not found in the current theme
2923
 *	- ['async'] (true/false): if the script should be loaded asynchronously (HTML5)
2924
 *  - ['validate'] (true/false): if true script will validate the local file exists
2925
 *  - ['seed'] (true/false/string): if true or null, use cache stale, false do not, or used a supplied string
2926
 *  - ['minimize'] boolean to add your file to the main minimized file. Useful when you have a file thats loaded everywhere and for everyone.
2927
 *  - ['attributes'] array extra attributes to add to the element
2928
 *
2929
 * @param string $id An ID to stick on the end of the filename
2930
 */
2931
function loadJavaScriptFile($fileName, $params = array(), $id = '')
2932
{
2933
	global $settings, $context, $modSettings;
2934
2935
	$params['seed'] = (!array_key_exists('seed', $params) || (array_key_exists('seed', $params) && $params['seed'] === true)) ?
2936
		(array_key_exists('browser_cache', $context) ? $context['browser_cache'] : '') :
2937
		(is_string($params['seed']) ? '?' . ltrim($params['seed'], '?') : '');
2938
	$params['force_current'] = isset($params['force_current']) ? $params['force_current'] : false;
2939
	$themeRef = !empty($params['default_theme']) ? 'default_theme' : 'theme';
2940
	$params['async'] = isset($params['async']) ? $params['async'] : false;
2941
	$params['defer'] = isset($params['defer']) ? $params['defer'] : false;
2942
	$params['minimize'] = isset($params['minimize']) ? $params['minimize'] : false;
2943
	$params['external'] = isset($params['external']) ? $params['external'] : false;
2944
	$params['validate'] = isset($params['validate']) ? $params['validate'] : true;
2945
	$params['attributes'] = isset($params['attributes']) ? $params['attributes'] : array();
2946
2947
	// Account for shorthand like admin.js?alp21 filenames
2948
	$id = (empty($id) ? strtr(str_replace('.js', '', basename($fileName)), '?', '_') : $id) . '_js';
2949
	$fileName = str_replace(pathinfo($fileName, PATHINFO_EXTENSION), strtok(pathinfo($fileName, PATHINFO_EXTENSION), '?'), $fileName);
2950
2951
	// Is this a local file?
2952
	if (empty($params['external']))
2953
	{
2954
		// Are we validating it exists on disk?
2955
		if (!empty($params['validate']) && ($mtime = @filemtime($settings[$themeRef . '_dir'] . '/scripts/' . $fileName)) === false)
2956
		{
2957
			// Can't find it in this theme, how about the default?
2958
			if ($themeRef === 'theme' && !$params['force_current'] && ($mtime = @filemtime($settings['default_theme_dir'] . '/scripts/' . $fileName)) !== false)
2959
			{
2960
				$fileUrl = $settings['default_theme_url'] . '/scripts/' . $fileName;
2961
				$filePath = $settings['default_theme_dir'] . '/scripts/' . $fileName;
2962
			}
2963
			else
2964
			{
2965
				$fileUrl = false;
2966
				$filePath = false;
2967
			}
2968
		}
2969
		else
2970
		{
2971
			$fileUrl = $settings[$themeRef . '_url'] . '/scripts/' . $fileName;
2972
			$filePath = $settings[$themeRef . '_dir'] . '/scripts/' . $fileName;
2973
			$mtime = @filemtime($filePath);
2974
		}
2975
	}
2976
	// An external file doesn't have a filepath. Mock one for simplicity.
2977
	else
2978
	{
2979
		$fileUrl = $fileName;
2980
		$filePath = $fileName;
2981
2982
		// Always turn these off for external files.
2983
		$params['minimize'] = false;
2984
		$params['seed'] = false;
2985
	}
2986
2987
	$mtime = empty($mtime) ? 0 : $mtime;
2988
2989
	// Add it to the array for use in the template
2990
	if (!empty($fileName) && !empty($fileUrl))
2991
		$context['javascript_files'][$id] = array('fileUrl' => $fileUrl, 'filePath' => $filePath, 'fileName' => $fileName, 'options' => $params, 'mtime' => $mtime);
2992
2993
	if ($mtime > $modSettings['browser_cache'])
2994
		updateSettings(array('browser_cache' => $mtime));
2995
}
2996
2997
/**
2998
 * Add a Javascript variable for output later (for feeding text strings and similar to JS)
2999
 * Cleaner and easier (for modders) than to use the function below.
3000
 *
3001
 * @param string $key The key for this variable
3002
 * @param string $value The value
3003
 * @param bool $escape Whether or not to escape the value
3004
 */
3005
function addJavaScriptVar($key, $value, $escape = false)
3006
{
3007
	global $context;
3008
3009
	// Variable name must be a valid string.
3010
	if (!is_string($key) || $key === '' || is_numeric($key))
3011
		return;
3012
3013
	// Take care of escaping the value for JavaScript?
3014
	if (!empty($escape))
3015
	{
3016
		switch (gettype($value)) {
3017
			// Illegal.
3018
			case 'resource':
3019
				break;
3020
3021
			// Convert PHP objects to arrays before processing.
3022
			case 'object':
3023
				$value = (array) $value;
3024
				// no break
3025
3026
			// Apply JavaScriptEscape() to any strings in the array.
3027
			case 'array':
3028
				$replacements = array();
3029
				array_walk_recursive(
3030
					$value,
3031
					function($v, $k) use (&$replacements)
3032
					{
3033
						if (is_string($v))
3034
							$replacements[json_encode($v)] = JavaScriptEscape($v, true);
3035
					}
3036
				);
3037
				$value = strtr(json_encode($value), $replacements);
3038
				break;
3039
3040
			case 'string':
3041
				$value = JavaScriptEscape($value);
3042
				break;
3043
3044
			default:
3045
				$value = json_encode($value);
3046
				break;
3047
		}
3048
	}
3049
3050
	// At this point, value should contain suitably escaped JavaScript code.
3051
	// If it obviously doesn't, declare the var with an undefined value.
3052
	if (!is_string($value) && !is_numeric($value))
3053
		$value = null;
3054
3055
	$context['javascript_vars'][$key] = $value;
3056
}
3057
3058
/**
3059
 * Add a block of inline Javascript code to be executed later
3060
 *
3061
 * - only use this if you have to, generally external JS files are better, but for very small scripts
3062
 *   or for scripts that require help from PHP/whatever, this can be useful.
3063
 * - all code added with this function is added to the same <script> tag so do make sure your JS is clean!
3064
 *
3065
 * @param string $javascript Some JS code
3066
 * @param bool $defer Whether the script should load in <head> or before the closing <html> tag
3067
 * @return void|bool Adds the code to one of the $context['javascript_inline'] arrays or returns if no JS was specified
3068
 */
3069
function addInlineJavaScript($javascript, $defer = false)
3070
{
3071
	global $context;
3072
3073
	if (empty($javascript))
3074
		return false;
3075
3076
	$context['javascript_inline'][($defer === true ? 'defer' : 'standard')][] = $javascript;
3077
}
3078
3079
/**
3080
 * Load a language file.  Tries the current and default themes as well as the user and global languages.
3081
 *
3082
 * @param string $template_name The name of a template file
3083
 * @param string $lang A specific language to load this file from
3084
 * @param bool $fatal Whether to die with an error if it can't be loaded
3085
 * @param bool $force_reload Whether to load the file again if it's already loaded
3086
 * @return string The language actually loaded.
3087
 */
3088
function loadLanguage($template_name, $lang = '', $fatal = true, $force_reload = false)
3089
{
3090
	global $user_info, $language, $settings, $context, $modSettings;
3091
	global $db_show_debug, $sourcedir, $txt, $birthdayEmails, $txtBirthdayEmails;
3092
	static $already_loaded = array();
3093
3094
	// Default to the user's language.
3095
	if ($lang == '')
3096
		$lang = isset($user_info['language']) ? $user_info['language'] : $language;
3097
3098
	// Do we want the English version of language file as fallback?
3099
	if (empty($modSettings['disable_language_fallback']) && $lang != 'english')
3100
		loadLanguage($template_name, 'english', false);
3101
3102
	if (!$force_reload && isset($already_loaded[$template_name]) && $already_loaded[$template_name] == $lang)
3103
		return $lang;
3104
3105
	// Make sure we have $settings - if not we're in trouble and need to find it!
3106
	if (empty($settings['default_theme_dir']))
3107
	{
3108
		require_once($sourcedir . '/ScheduledTasks.php');
3109
		loadEssentialThemeData();
3110
	}
3111
3112
	// What theme are we in?
3113
	$theme_name = basename($settings['theme_url']);
3114
	if (empty($theme_name))
3115
		$theme_name = 'unknown';
3116
3117
	// For each file open it up and write it out!
3118
	foreach (explode('+', $template_name) as $template)
3119
	{
3120
		// Obviously, the current theme is most important to check.
3121
		$attempts = array(
3122
			array($settings['theme_dir'], $template, $lang, $settings['theme_url']),
3123
			array($settings['theme_dir'], $template, $language, $settings['theme_url']),
3124
		);
3125
3126
		// Do we have a base theme to worry about?
3127
		if (isset($settings['base_theme_dir']))
3128
		{
3129
			$attempts[] = array($settings['base_theme_dir'], $template, $lang, $settings['base_theme_url']);
3130
			$attempts[] = array($settings['base_theme_dir'], $template, $language, $settings['base_theme_url']);
3131
		}
3132
3133
		// Fall back on the default theme if necessary.
3134
		$attempts[] = array($settings['default_theme_dir'], $template, $lang, $settings['default_theme_url']);
3135
		$attempts[] = array($settings['default_theme_dir'], $template, $language, $settings['default_theme_url']);
3136
3137
		// Fall back on the English language if none of the preferred languages can be found.
3138
		if (!in_array('english', array($lang, $language)))
3139
		{
3140
			$attempts[] = array($settings['theme_dir'], $template, 'english', $settings['theme_url']);
3141
			$attempts[] = array($settings['default_theme_dir'], $template, 'english', $settings['default_theme_url']);
3142
		}
3143
3144
		// Try to find the language file.
3145
		$found = false;
3146
		foreach ($attempts as $k => $file)
3147
		{
3148
			if (file_exists($file[0] . '/languages/' . $file[1] . '.' . $file[2] . '.php'))
3149
			{
3150
				// Include it!
3151
				template_include($file[0] . '/languages/' . $file[1] . '.' . $file[2] . '.php');
3152
3153
				// Note that we found it.
3154
				$found = true;
3155
3156
				// setlocale is required for basename() & pathinfo() to work properly on the selected language
3157
				if (!empty($txt['lang_locale']))
3158
				{
3159
					if (strpos($txt['lang_locale'], '.') !== false)
3160
						$locale_variants = $txt['lang_locale'];
3161
					else
3162
						$locale_variants = array_unique(array_merge(
3163
							!empty($modSettings['global_character_set']) ? array($txt['lang_locale'] . '.' . $modSettings['global_character_set']) : array(),
3164
							!empty($context['utf8']) ? array($txt['lang_locale'] . '.UTF-8', $txt['lang_locale'] . '.UTF8', $txt['lang_locale'] . '.utf-8', $txt['lang_locale'] . '.utf8') : array(),
3165
							array($txt['lang_locale'])
3166
						));
3167
3168
					setlocale(LC_CTYPE, $locale_variants);
3169
				}
3170
3171
				break;
3172
			}
3173
		}
3174
3175
		// That couldn't be found!  Log the error, but *try* to continue normally.
3176
		if (!$found && $fatal)
3177
		{
3178
			log_error(sprintf($txt['theme_language_error'], $template_name . '.' . $lang, 'template'));
3179
			break;
3180
		}
3181
3182
		// For the sake of backward compatibility
3183
		if (!empty($txt['emails']))
3184
		{
3185
			foreach ($txt['emails'] as $key => $value)
3186
			{
3187
				$txt[$key . '_subject'] = $value['subject'];
3188
				$txt[$key . '_body'] = $value['body'];
3189
			}
3190
			$txt['emails'] = array();
3191
		}
3192
		// For sake of backward compatibility: $birthdayEmails is supposed to be
3193
		// empty in a normal install. If it isn't it means the forum is using
3194
		// something "old" (it may be the translation, it may be a mod) and this
3195
		// code (like the piece above) takes care of converting it to the new format
3196
		if (!empty($birthdayEmails))
3197
		{
3198
			foreach ($birthdayEmails as $key => $value)
3199
			{
3200
				$txtBirthdayEmails[$key . '_subject'] = $value['subject'];
3201
				$txtBirthdayEmails[$key . '_body'] = $value['body'];
3202
				$txtBirthdayEmails[$key . '_author'] = $value['author'];
3203
			}
3204
			$birthdayEmails = array();
3205
		}
3206
	}
3207
3208
	// Keep track of what we're up to soldier.
3209
	if ($db_show_debug === true)
3210
		$context['debug']['language_files'][] = $template_name . '.' . $lang . ' (' . $theme_name . ')';
3211
3212
	// Remember what we have loaded, and in which language.
3213
	$already_loaded[$template_name] = $lang;
3214
3215
	// Return the language actually loaded.
3216
	return $lang;
3217
}
3218
3219
/**
3220
 * Get all parent boards (requires first parent as parameter)
3221
 * It finds all the parents of id_parent, and that board itself.
3222
 * Additionally, it detects the moderators of said boards.
3223
 *
3224
 * @param int $id_parent The ID of the parent board
3225
 * @return array An array of information about the boards found.
3226
 */
3227
function getBoardParents($id_parent)
3228
{
3229
	global $scripturl, $smcFunc;
3230
3231
	// First check if we have this cached already.
3232
	if (($boards = cache_get_data('board_parents-' . $id_parent, 480)) === null)
3233
	{
3234
		$boards = array();
3235
		$original_parent = $id_parent;
3236
3237
		// Loop while the parent is non-zero.
3238
		while ($id_parent != 0)
3239
		{
3240
			$result = $smcFunc['db_query']('', '
3241
				SELECT
3242
					b.id_parent, b.name, {int:board_parent} AS id_board, b.member_groups, b.deny_member_groups,
3243
					b.child_level, COALESCE(mem.id_member, 0) AS id_moderator, mem.real_name,
3244
					COALESCE(mg.id_group, 0) AS id_moderator_group, mg.group_name
3245
				FROM {db_prefix}boards AS b
3246
					LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board)
3247
					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
3248
					LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board)
3249
					LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = modgs.id_group)
3250
				WHERE b.id_board = {int:board_parent}',
3251
				array(
3252
					'board_parent' => $id_parent,
3253
				)
3254
			);
3255
			// In the EXTREMELY unlikely event this happens, give an error message.
3256
			if ($smcFunc['db_num_rows']($result) == 0)
3257
				fatal_lang_error('parent_not_found', 'critical');
3258
			while ($row = $smcFunc['db_fetch_assoc']($result))
3259
			{
3260
				if (!isset($boards[$row['id_board']]))
3261
				{
3262
					$id_parent = $row['id_parent'];
3263
					$boards[$row['id_board']] = array(
3264
						'url' => $scripturl . '?board=' . $row['id_board'] . '.0',
3265
						'name' => $row['name'],
3266
						'level' => $row['child_level'],
3267
						'groups' => explode(',', $row['member_groups']),
3268
						'deny_groups' => explode(',', $row['deny_member_groups']),
3269
						'moderators' => array(),
3270
						'moderator_groups' => array()
3271
					);
3272
				}
3273
				// If a moderator exists for this board, add that moderator for all children too.
3274
				if (!empty($row['id_moderator']))
3275
					foreach ($boards as $id => $dummy)
3276
					{
3277
						$boards[$id]['moderators'][$row['id_moderator']] = array(
3278
							'id' => $row['id_moderator'],
3279
							'name' => $row['real_name'],
3280
							'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'],
3281
							'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
3282
						);
3283
					}
3284
3285
				// If a moderator group exists for this board, add that moderator group for all children too
3286
				if (!empty($row['id_moderator_group']))
3287
					foreach ($boards as $id => $dummy)
3288
					{
3289
						$boards[$id]['moderator_groups'][$row['id_moderator_group']] = array(
3290
							'id' => $row['id_moderator_group'],
3291
							'name' => $row['group_name'],
3292
							'href' => $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'],
3293
							'link' => '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'] . '">' . $row['group_name'] . '</a>'
3294
						);
3295
					}
3296
			}
3297
			$smcFunc['db_free_result']($result);
3298
		}
3299
3300
		cache_put_data('board_parents-' . $original_parent, $boards, 480);
3301
	}
3302
3303
	return $boards;
3304
}
3305
3306
/**
3307
 * Attempt to reload our known languages.
3308
 * It will try to choose only utf8 or non-utf8 languages.
3309
 *
3310
 * @param bool $use_cache Whether or not to use the cache
3311
 * @return array An array of information about available languages
3312
 */
3313
function getLanguages($use_cache = true)
3314
{
3315
	global $context, $smcFunc, $settings, $modSettings, $cache_enable;
3316
3317
	// Either we don't use the cache, or its expired.
3318
	if (!$use_cache || ($context['languages'] = cache_get_data('known_languages', !empty($cache_enable) && $cache_enable < 1 ? 86400 : 3600)) == null)
3319
	{
3320
		// If we don't have our ucwords function defined yet, let's load the settings data.
3321
		if (empty($smcFunc['ucwords']))
3322
			reloadSettings();
3323
3324
		// If we don't have our theme information yet, let's get it.
3325
		if (empty($settings['default_theme_dir']))
3326
			loadTheme(0, false);
3327
3328
		// Default language directories to try.
3329
		$language_directories = array(
3330
			$settings['default_theme_dir'] . '/languages',
3331
		);
3332
		if (!empty($settings['actual_theme_dir']) && $settings['actual_theme_dir'] != $settings['default_theme_dir'])
3333
			$language_directories[] = $settings['actual_theme_dir'] . '/languages';
3334
3335
		// We possibly have a base theme directory.
3336
		if (!empty($settings['base_theme_dir']))
3337
			$language_directories[] = $settings['base_theme_dir'] . '/languages';
3338
3339
		// Remove any duplicates.
3340
		$language_directories = array_unique($language_directories);
3341
3342
		foreach ($language_directories as $language_dir)
3343
		{
3344
			// Can't look in here... doesn't exist!
3345
			if (!file_exists($language_dir))
3346
				continue;
3347
3348
			$dir = dir($language_dir);
3349
			while ($entry = $dir->read())
3350
			{
3351
				// Look for the index language file... For good measure skip any "index.language-utf8.php" files
3352
				if (!preg_match('~^index\.((?:.(?!-utf8))+)\.php$~', $entry, $matches))
3353
					continue;
3354
3355
				$langName = $smcFunc['ucwords'](strtr($matches[1], array('_' => ' ')));
3356
3357
				if (($spos = strpos($langName, ' ')) !== false)
3358
					$langName = substr($langName, 0, ++$spos) . '(' . substr($langName, $spos) . ')';
3359
3360
				// Get the line we need.
3361
				$fp = @fopen($language_dir . '/' . $entry, 'r');
3362
3363
				// Yay!
3364
				if ($fp)
3365
				{
3366
					while (($line = fgets($fp)) !== false)
3367
					{
3368
						if (strpos($line, '$txt[\'native_name\']') === false)
3369
							continue;
3370
3371
						preg_match('~\$txt\[\'native_name\'\]\s*=\s*\'([^\']+)\';~', $line, $matchNative);
3372
3373
						// Set the language's name.
3374
						if (!empty($matchNative) && !empty($matchNative[1]))
3375
						{
3376
							// Don't mislabel the language if the translator missed this one.
3377
							if ($langName !== 'English' && $matchNative[1] === 'English')
3378
								break;
3379
3380
							$langName = un_htmlspecialchars($matchNative[1]);
3381
							break;
3382
						}
3383
					}
3384
3385
					fclose($fp);
3386
				}
3387
3388
				// Build this language entry.
3389
				$context['languages'][$matches[1]] = array(
3390
					'name' => $langName,
3391
					'selected' => false,
3392
					'filename' => $matches[1],
3393
					'location' => $language_dir . '/index.' . $matches[1] . '.php',
3394
				);
3395
			}
3396
			$dir->close();
3397
		}
3398
3399
		// Avoid confusion when we have more than one English variant installed.
3400
		// Honestly, our default English version should always have been called "English (US)"
3401
		if (substr_count(implode(' ', array_keys($context['languages'])), 'english') > 1 && $context['languages']['english']['name'] === 'English')
3402
			$context['languages']['english']['name'] = 'English (US)';
3403
3404
		// Let's cash in on this deal.
3405
		if (!empty($cache_enable))
3406
			cache_put_data('known_languages', $context['languages'], !empty($cache_enable) && $cache_enable < 1 ? 86400 : 3600);
3407
	}
3408
3409
	return $context['languages'];
3410
}
3411
3412
/**
3413
 * Replace all vulgar words with respective proper words. (substring or whole words..)
3414
 * What this function does:
3415
 *  - it censors the passed string.
3416
 *  - if the theme setting allow_no_censored is on, and the theme option
3417
 *	show_no_censored is enabled, does not censor, unless force is also set.
3418
 *  - it caches the list of censored words to reduce parsing.
3419
 *
3420
 * @param string &$text The text to censor
3421
 * @param bool $force Whether to censor the text regardless of settings
3422
 * @return string The censored text
3423
 */
3424
function censorText(&$text, $force = false)
3425
{
3426
	global $modSettings, $options, $txt;
3427
	static $censor_vulgar = null, $censor_proper;
3428
3429
	if ((!empty($options['show_no_censored']) && !empty($modSettings['allow_no_censored']) && !$force) || empty($modSettings['censor_vulgar']) || !is_string($text) || trim($text) === '')
3430
		return $text;
3431
3432
	call_integration_hook('integrate_word_censor', array(&$text));
3433
3434
	// If they haven't yet been loaded, load them.
3435
	if ($censor_vulgar == null)
3436
	{
3437
		$censor_vulgar = explode("\n", $modSettings['censor_vulgar']);
3438
		$censor_proper = explode("\n", $modSettings['censor_proper']);
3439
3440
		// Quote them for use in regular expressions.
3441
		if (!empty($modSettings['censorWholeWord']))
3442
		{
3443
			$charset = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set'];
3444
3445
			for ($i = 0, $n = count($censor_vulgar); $i < $n; $i++)
3446
			{
3447
				$censor_vulgar[$i] = str_replace(array('\\\\\\*', '\\*', '&', '\''), array('[*]', '[^\s]*?', '&amp;', '&#039;'), preg_quote($censor_vulgar[$i], '/'));
3448
3449
				// Use the faster \b if we can, or something more complex if we can't
3450
				$boundary_before = preg_match('/^\w/', $censor_vulgar[$i]) ? '\b' : ($charset === 'UTF-8' ? '(?<![\p{L}\p{M}\p{N}_])' : '(?<!\w)');
3451
				$boundary_after = preg_match('/\w$/', $censor_vulgar[$i]) ? '\b' : ($charset === 'UTF-8' ? '(?![\p{L}\p{M}\p{N}_])' : '(?!\w)');
3452
3453
				$censor_vulgar[$i] = '/' . $boundary_before . $censor_vulgar[$i] . $boundary_after . '/' . (empty($modSettings['censorIgnoreCase']) ? '' : 'i') . ($charset === 'UTF-8' ? 'u' : '');
3454
			}
3455
		}
3456
	}
3457
3458
	// Censoring isn't so very complicated :P.
3459
	if (empty($modSettings['censorWholeWord']))
3460
	{
3461
		$func = !empty($modSettings['censorIgnoreCase']) ? 'str_ireplace' : 'str_replace';
3462
		$text = $func($censor_vulgar, $censor_proper, $text);
3463
	}
3464
	else
3465
		$text = preg_replace($censor_vulgar, $censor_proper, $text);
3466
3467
	return $text;
3468
}
3469
3470
/**
3471
 * Load the template/language file using require
3472
 * 	- loads the template or language file specified by filename.
3473
 * 	- uses eval unless disableTemplateEval is enabled.
3474
 * 	- outputs a parse error if the file did not exist or contained errors.
3475
 * 	- attempts to detect the error and line, and show detailed information.
3476
 *
3477
 * @param string $filename The name of the file to include
3478
 * @param bool $once If true only includes the file once (like include_once)
3479
 */
3480
function template_include($filename, $once = false)
3481
{
3482
	global $context, $txt, $scripturl, $modSettings;
3483
	global $boardurl, $boarddir;
3484
	global $maintenance, $mtitle, $mmessage;
3485
	static $templates = array();
3486
3487
	// We want to be able to figure out any errors...
3488
	@ini_set('track_errors', '1');
3489
3490
	// Don't include the file more than once, if $once is true.
3491
	if ($once && in_array($filename, $templates))
3492
		return;
3493
	// Add this file to the include list, whether $once is true or not.
3494
	else
3495
		$templates[] = $filename;
3496
3497
	$file_found = file_exists($filename);
3498
3499
	if ($once && $file_found)
3500
		require_once($filename);
3501
	elseif ($file_found)
3502
		require($filename);
3503
3504
	if ($file_found !== true)
3505
	{
3506
		ob_end_clean();
3507
		if (!empty($modSettings['enableCompressedOutput']))
3508
			@ob_start('ob_gzhandler');
3509
		else
3510
			ob_start();
3511
3512
		if (isset($_GET['debug']))
3513
			header('content-type: application/xhtml+xml; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3514
3515
		// Don't cache error pages!!
3516
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3517
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3518
		header('cache-control: no-cache');
3519
3520
		if (!isset($txt['template_parse_error']))
3521
		{
3522
			$txt['template_parse_error'] = 'Template Parse Error!';
3523
			$txt['template_parse_error_message'] = 'It seems something has gone sour on the forum with the template system.  This problem should only be temporary, so please come back later and try again.  If you continue to see this message, please contact the administrator.<br><br>You can also try <a href="javascript:location.reload();">refreshing this page</a>.';
3524
			$txt['template_parse_error_details'] = 'There was a problem loading the <pre><strong>%1$s</strong></pre> template or language file.  Please check the syntax and try again - remember, single quotes (<pre>\'</pre>) often have to be escaped with a slash (<pre>\\</pre>).  To see more specific error information from PHP, try <a href="%2$s%1$s" class="extern">accessing the file directly</a>.<br><br>You may want to try to <a href="javascript:location.reload();">refresh this page</a> or <a href="%3$s?theme=1">use the default theme</a>.';
3525
			$txt['template_parse_errmsg'] = 'Unfortunately more information is not available at this time as to exactly what is wrong.';
3526
		}
3527
3528
		// First, let's get the doctype and language information out of the way.
3529
		echo '<!DOCTYPE html>
3530
<html', !empty($context['right_to_left']) ? ' dir="rtl"' : '', '>
3531
	<head>';
3532
		if (isset($context['character_set']))
3533
			echo '
3534
		<meta charset="', $context['character_set'], '">';
3535
3536
		if (!empty($maintenance) && !allowedTo('admin_forum'))
3537
			echo '
3538
		<title>', $mtitle, '</title>
3539
	</head>
3540
	<body>
3541
		<h3>', $mtitle, '</h3>
3542
		', $mmessage, '
3543
	</body>
3544
</html>';
3545
		elseif (!allowedTo('admin_forum'))
3546
			echo '
3547
		<title>', $txt['template_parse_error'], '</title>
3548
	</head>
3549
	<body>
3550
		<h3>', $txt['template_parse_error'], '</h3>
3551
		', $txt['template_parse_error_message'], '
3552
	</body>
3553
</html>';
3554
		else
3555
		{
3556
			$error = fetch_web_data($boardurl . strtr($filename, array($boarddir => '', strtr($boarddir, '\\', '/') => '')));
3557
			$error_array = error_get_last();
3558
			if (empty($error) && ini_get('track_errors') && !empty($error_array))
3559
				$error = $error_array['message'];
3560
			if (empty($error))
3561
				$error = $txt['template_parse_errmsg'];
3562
3563
			$error = strtr($error, array('<b>' => '<strong>', '</b>' => '</strong>'));
3564
3565
			echo '
3566
		<title>', $txt['template_parse_error'], '</title>
3567
	</head>
3568
	<body>
3569
		<h3>', $txt['template_parse_error'], '</h3>
3570
		', sprintf($txt['template_parse_error_details'], strtr($filename, array($boarddir => '', strtr($boarddir, '\\', '/') => '')), $boardurl, $scripturl);
3571
3572
			if (!empty($error))
3573
				echo '
3574
		<hr>
3575
3576
		<div style="margin: 0 20px;"><pre>', strtr(strtr($error, array('<strong>' . $boarddir => '<strong>...', '<strong>' . strtr($boarddir, '\\', '/') => '<strong>...')), '\\', '/'), '</pre></div>';
3577
3578
			// I know, I know... this is VERY COMPLICATED.  Still, it's good.
3579
			if (preg_match('~ <strong>(\d+)</strong><br( /)?' . '>$~i', $error, $match) != 0)
3580
			{
3581
				$data = file($filename);
3582
				$data2 = highlight_php_code(implode('', $data));
3583
				$data2 = preg_split('~\<br( /)?\>~', $data2);
3584
3585
				// Fix the PHP code stuff...
3586
				if (!isBrowser('gecko'))
3587
					$data2 = str_replace("\t", '<span style="white-space: pre;">' . "\t" . '</span>', $data2);
3588
				else
3589
					$data2 = str_replace('<pre style="display: inline;">' . "\t" . '</pre>', "\t", $data2);
3590
3591
				// Now we get to work around a bug in PHP where it doesn't escape <br>s!
3592
				$j = -1;
3593
				foreach ($data as $line)
3594
				{
3595
					$j++;
3596
3597
					if (substr_count($line, '<br>') == 0)
3598
						continue;
3599
3600
					$n = substr_count($line, '<br>');
3601
					for ($i = 0; $i < $n; $i++)
3602
					{
3603
						$data2[$j] .= '&lt;br /&gt;' . $data2[$j + $i + 1];
3604
						unset($data2[$j + $i + 1]);
3605
					}
3606
					$j += $n;
3607
				}
3608
				$data2 = array_values($data2);
3609
				array_unshift($data2, '');
3610
3611
				echo '
3612
		<div style="margin: 2ex 20px; width: 96%; overflow: auto;"><pre style="margin: 0;">';
3613
3614
				// Figure out what the color coding was before...
3615
				$line = max($match[1] - 9, 1);
3616
				$last_line = '';
3617
				for ($line2 = $line - 1; $line2 > 1; $line2--)
3618
					if (strpos($data2[$line2], '<') !== false)
3619
					{
3620
						if (preg_match('~(<[^/>]+>)[^<]*$~', $data2[$line2], $color_match) != 0)
3621
							$last_line = $color_match[1];
3622
						break;
3623
					}
3624
3625
				// Show the relevant lines...
3626
				for ($n = min($match[1] + 4, count($data2) + 1); $line <= $n; $line++)
3627
				{
3628
					if ($line == $match[1])
3629
						echo '</pre><div style="background-color: #ffb0b5;"><pre style="margin: 0;">';
3630
3631
					echo '<span style="color: black;">', sprintf('%' . strlen($n) . 's', $line), ':</span> ';
3632
					if (isset($data2[$line]) && $data2[$line] != '')
3633
						echo substr($data2[$line], 0, 2) == '</' ? preg_replace('~^</[^>]+>~', '', $data2[$line]) : $last_line . $data2[$line];
3634
3635
					if (isset($data2[$line]) && preg_match('~(<[^/>]+>)[^<]*$~', $data2[$line], $color_match) != 0)
3636
					{
3637
						$last_line = $color_match[1];
3638
						echo '</', substr($last_line, 1, 4), '>';
3639
					}
3640
					elseif ($last_line != '' && strpos($data2[$line], '<') !== false)
3641
						$last_line = '';
3642
					elseif ($last_line != '' && $data2[$line] != '')
3643
						echo '</', substr($last_line, 1, 4), '>';
3644
3645
					if ($line == $match[1])
3646
						echo '</pre></div><pre style="margin: 0;">';
3647
					else
3648
						echo "\n";
3649
				}
3650
3651
				echo '</pre></div>';
3652
			}
3653
3654
			echo '
3655
	</body>
3656
</html>';
3657
		}
3658
3659
		die;
3660
	}
3661
}
3662
3663
/**
3664
 * Initialize a database connection.
3665
 */
3666
function loadDatabase()
3667
{
3668
	global $db_persist, $db_connection, $db_server, $db_user, $db_passwd;
3669
	global $db_type, $db_name, $ssi_db_user, $ssi_db_passwd, $sourcedir, $db_prefix, $db_port, $db_mb4;
3670
3671
	// Figure out what type of database we are using.
3672
	if (empty($db_type) || !file_exists($sourcedir . '/Subs-Db-' . $db_type . '.php'))
3673
		$db_type = 'mysql';
3674
3675
	// Load the file for the database.
3676
	require_once($sourcedir . '/Subs-Db-' . $db_type . '.php');
3677
3678
	$db_options = array();
3679
3680
	// Add in the port if needed
3681
	if (!empty($db_port))
3682
		$db_options['port'] = $db_port;
3683
3684
	if (!empty($db_mb4))
3685
		$db_options['db_mb4'] = $db_mb4;
3686
3687
	// If we are in SSI try them first, but don't worry if it doesn't work, we have the normal username and password we can use.
3688
	if (SMF == 'SSI' && !empty($ssi_db_user) && !empty($ssi_db_passwd))
3689
	{
3690
		$options = array_merge($db_options, array('persist' => $db_persist, 'non_fatal' => true, 'dont_select_db' => true));
3691
3692
		$db_connection = smf_db_initiate($db_server, $db_name, $ssi_db_user, $ssi_db_passwd, $db_prefix, $options);
3693
	}
3694
3695
	// Either we aren't in SSI mode, or it failed.
3696
	if (empty($db_connection))
3697
	{
3698
		$options = array_merge($db_options, array('persist' => $db_persist, 'dont_select_db' => SMF == 'SSI'));
3699
3700
		$db_connection = smf_db_initiate($db_server, $db_name, $db_user, $db_passwd, $db_prefix, $options);
3701
	}
3702
3703
	// Safe guard here, if there isn't a valid connection lets put a stop to it.
3704
	if (!$db_connection)
3705
		display_db_error();
3706
3707
	// If in SSI mode fix up the prefix.
3708
	if (SMF == 'SSI')
3709
		db_fix_prefix($db_prefix, $db_name);
3710
}
3711
3712
/**
3713
 * Try to load up a supported caching method. This is saved in $cacheAPI if we are not overriding it.
3714
 *
3715
 * @param string $overrideCache Try to use a different cache method other than that defined in $cache_accelerator.
3716
 * @param bool $fallbackSMF Use the default SMF method if the accelerator fails.
3717
 * @return object|false A object of $cacheAPI, or False on failure.
3718
 */
3719
function loadCacheAccelerator($overrideCache = '', $fallbackSMF = true)
3720
{
3721
	global $cacheAPI, $cache_accelerator, $cache_enable;
3722
	global $sourcedir;
3723
3724
	// Is caching enabled?
3725
	if (empty($cache_enable) && empty($overrideCache))
3726
		return false;
3727
3728
	// Not overriding this and we have a cacheAPI, send it back.
3729
	if (empty($overrideCache) && is_object($cacheAPI))
3730
		return $cacheAPI;
3731
3732
	elseif (is_null($cacheAPI))
3733
		$cacheAPI = false;
3734
3735
	require_once($sourcedir . '/Cache/CacheApi.php');
3736
	require_once($sourcedir . '/Cache/CacheApiInterface.php');
3737
3738
	// What accelerator we are going to try.
3739
	$cache_class_name = !empty($cache_accelerator) ? $cache_accelerator : CacheApi::APIS_DEFAULT;
3740
	$fully_qualified_class_name = !empty($overrideCache) ? $overrideCache :
3741
		CacheApi::APIS_NAMESPACE . $cache_class_name;
3742
3743
	// Do some basic tests.
3744
	if (class_exists($fully_qualified_class_name))
3745
	{
3746
		/* @var CacheApiInterface $cache_api */
3747
		$cache_api = new $fully_qualified_class_name();
3748
3749
		// There are rules you know...
3750
		if (!($cache_api instanceof CacheApiInterface) || !($cache_api instanceof CacheApi))
3751
			return false;
3752
3753
		// No Support?  NEXT!
3754
		if (!$cache_api->isSupported())
3755
		{
3756
			// Can we save ourselves?
3757
			if (!empty($fallbackSMF) && $overrideCache == '' &&
3758
				$cache_class_name !== CacheApi::APIS_DEFAULT)
3759
				return loadCacheAccelerator(CacheApi::APIS_NAMESPACE . CacheApi::APIS_DEFAULT, false);
3760
3761
			return false;
3762
		}
3763
3764
		// Connect up to the accelerator.
3765
		if ($cache_api->connect() === false) return false;
3766
3767
		// Don't set this if we are overriding the cache.
3768
		if (empty($overrideCache))
3769
			$cacheAPI = $cache_api;
3770
3771
		return $cache_api;
3772
	}
3773
3774
	return false;
3775
}
3776
3777
/**
3778
 * Try to retrieve a cache entry. On failure, call the appropriate function.
3779
 *
3780
 * @param string $key The key for this entry
3781
 * @param string $file The file associated with this entry
3782
 * @param string $function The function to call
3783
 * @param array $params Parameters to be passed to the specified function
3784
 * @param int $level The cache level
3785
 * @return string The cached data
3786
 */
3787
function cache_quick_get($key, $file, $function, $params, $level = 1)
3788
{
3789
	global $modSettings, $sourcedir, $cache_enable;
3790
3791
	if (function_exists('call_integration_hook'))
3792
		call_integration_hook('pre_cache_quick_get', array(&$key, &$file, &$function, &$params, &$level));
3793
3794
	/* Refresh the cache if either:
3795
		1. Caching is disabled.
3796
		2. The cache level isn't high enough.
3797
		3. The item has not been cached or the cached item expired.
3798
		4. The cached item has a custom expiration condition evaluating to true.
3799
		5. The expire time set in the cache item has passed (needed for Zend).
3800
	*/
3801
	if (empty($cache_enable) || $cache_enable < $level || !is_array($cache_block = cache_get_data($key, 3600)) || (!empty($cache_block['refresh_eval']) && eval($cache_block['refresh_eval'])) || (!empty($cache_block['expires']) && $cache_block['expires'] < time()))
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
3802
	{
3803
		require_once($sourcedir . '/' . $file);
3804
		$cache_block = call_user_func_array($function, $params);
3805
3806
		if (!empty($cache_enable) && $cache_enable >= $level)
3807
			cache_put_data($key, $cache_block, $cache_block['expires'] - time());
3808
	}
3809
3810
	// Some cached data may need a freshening up after retrieval.
3811
	if (!empty($cache_block['post_retri_eval']))
3812
		eval($cache_block['post_retri_eval']);
3813
3814
	if (function_exists('call_integration_hook'))
3815
		call_integration_hook('post_cache_quick_get', array(&$cache_block));
3816
3817
	return $cache_block['data'];
3818
}
3819
3820
/**
3821
 * Puts value in the cache under key for ttl seconds.
3822
 *
3823
 * - It may "miss" so shouldn't be depended on
3824
 * - Uses the cache engine chosen in the ACP and saved in settings.php
3825
 * - It supports:
3826
 *	 memcache: https://php.net/memcache
3827
 *   APCu: https://php.net/book.apcu
3828
 *	 Zend: http://files.zend.com/help/Zend-Platform/output_cache_functions.htm
3829
 *	 Zend: http://files.zend.com/help/Zend-Platform/zend_cache_functions.htm
3830
 *
3831
 * @param string $key A key for this value
3832
 * @param mixed $value The data to cache
3833
 * @param int $ttl How long (in seconds) the data should be cached for
3834
 */
3835
function cache_put_data($key, $value, $ttl = 120)
3836
{
3837
	global $smcFunc, $cache_enable, $cacheAPI;
3838
	global $cache_hits, $cache_count, $db_show_debug;
3839
3840
	if (empty($cache_enable) || empty($cacheAPI))
3841
		return;
3842
3843
	$cache_count = isset($cache_count) ? $cache_count + 1 : 1;
3844
	if (isset($db_show_debug) && $db_show_debug === true)
3845
	{
3846
		$cache_hits[$cache_count] = array('k' => $key, 'd' => 'put', 's' => $value === null ? 0 : strlen(isset($smcFunc['json_encode']) ? $smcFunc['json_encode']($value) : json_encode($value)));
3847
		$st = microtime(true);
3848
	}
3849
3850
	// The API will handle the rest.
3851
	$value = $value === null ? null : (isset($smcFunc['json_encode']) ? $smcFunc['json_encode']($value) : json_encode($value));
3852
	$cacheAPI->putData($key, $value, $ttl);
3853
3854
	if (function_exists('call_integration_hook'))
3855
		call_integration_hook('cache_put_data', array(&$key, &$value, &$ttl));
3856
3857
	if (isset($db_show_debug) && $db_show_debug === true)
3858
		$cache_hits[$cache_count]['t'] = microtime(true) - $st;
3859
}
3860
3861
/**
3862
 * Gets the value from the cache specified by key, so long as it is not older than ttl seconds.
3863
 * - It may often "miss", so shouldn't be depended on.
3864
 * - It supports the same as cache_put_data().
3865
 *
3866
 * @param string $key The key for the value to retrieve
3867
 * @param int $ttl The maximum age of the cached data
3868
 * @return array|null The cached data or null if nothing was loaded
3869
 */
3870
function cache_get_data($key, $ttl = 120)
3871
{
3872
	global $smcFunc, $cache_enable, $cacheAPI;
3873
	global $cache_hits, $cache_count, $cache_misses, $cache_count_misses, $db_show_debug;
3874
3875
	if (empty($cache_enable) || empty($cacheAPI))
3876
		return null;
3877
3878
	$cache_count = isset($cache_count) ? $cache_count + 1 : 1;
3879
	if (isset($db_show_debug) && $db_show_debug === true)
3880
	{
3881
		$cache_hits[$cache_count] = array('k' => $key, 'd' => 'get');
3882
		$st = microtime(true);
3883
		$original_key = $key;
3884
	}
3885
3886
	// Ask the API to get the data.
3887
	$value = $cacheAPI->getData($key, $ttl);
3888
3889
	if (isset($db_show_debug) && $db_show_debug === true)
3890
	{
3891
		$cache_hits[$cache_count]['t'] = microtime(true) - $st;
3892
		$cache_hits[$cache_count]['s'] = isset($value) ? strlen($value) : 0;
3893
3894
		if (empty($value))
3895
		{
3896
			if (!is_array($cache_misses))
3897
				$cache_misses = array();
3898
3899
			$cache_count_misses = isset($cache_count_misses) ? $cache_count_misses + 1 : 1;
3900
			$cache_misses[$cache_count_misses] = array('k' => $original_key, 'd' => 'get');
3901
		}
3902
	}
3903
3904
	if (function_exists('call_integration_hook') && isset($value))
3905
		call_integration_hook('cache_get_data', array(&$key, &$ttl, &$value));
3906
3907
	return empty($value) ? null : (isset($smcFunc['json_decode']) ? $smcFunc['json_decode']($value, true) : smf_json_decode($value, true));
3908
}
3909
3910
/**
3911
 * Empty out the cache in use as best it can
3912
 *
3913
 * It may only remove the files of a certain type (if the $type parameter is given)
3914
 * Type can be user, data or left blank
3915
 * 	- user clears out user data
3916
 *  - data clears out system / opcode data
3917
 *  - If no type is specified will perform a complete cache clearing
3918
 * For cache engines that do not distinguish on types, a full cache flush will be done
3919
 *
3920
 * @param string $type The cache type ('memcached', 'zend' or something else for SMF's file cache)
3921
 */
3922
function clean_cache($type = '')
3923
{
3924
	global $cacheAPI;
3925
3926
	// If we can't get to the API, can't do this.
3927
	if (empty($cacheAPI))
3928
		return;
3929
3930
	// Ask the API to do the heavy lifting. cleanCache also calls invalidateCache to be sure.
3931
	$cacheAPI->cleanCache($type);
3932
3933
	call_integration_hook('integrate_clean_cache');
3934
	clearstatcache();
3935
}
3936
3937
/**
3938
 * Helper function to set an array of data for an user's avatar.
3939
 *
3940
 * Makes assumptions based on the data provided, the following keys are required:
3941
 * - avatar The raw "avatar" column in members table
3942
 * - email The user's email. Used to get the gravatar info
3943
 * - filename The attachment filename
3944
 *
3945
 * @param array $data An array of raw info
3946
 * @return array An array of avatar data
3947
 */
3948
function set_avatar_data($data = array())
3949
{
3950
	global $modSettings, $smcFunc, $user_info;
3951
3952
	// Come on!
3953
	if (empty($data))
3954
		return array();
3955
3956
	// Set a nice default var.
3957
	$image = '';
3958
3959
	// Gravatar has been set as mandatory!
3960
	if (!empty($modSettings['gravatarEnabled']) && !empty($modSettings['gravatarOverride']))
3961
	{
3962
		if (!empty($modSettings['gravatarAllowExtraEmail']) && !empty($data['avatar']) && stristr($data['avatar'], 'gravatar://'))
3963
			$image = get_gravatar_url($smcFunc['substr']($data['avatar'], 11));
3964
3965
		elseif (!empty($data['email']))
3966
			$image = get_gravatar_url($data['email']);
3967
	}
3968
3969
	// Look if the user has a gravatar field or has set an external url as avatar.
3970
	else
3971
	{
3972
		// So it's stored in the member table?
3973
		if (!empty($data['avatar']))
3974
		{
3975
			// Gravatar.
3976
			if (stristr($data['avatar'], 'gravatar://'))
3977
			{
3978
				if ($data['avatar'] == 'gravatar://')
3979
					$image = get_gravatar_url($data['email']);
3980
3981
				elseif (!empty($modSettings['gravatarAllowExtraEmail']))
3982
					$image = get_gravatar_url($smcFunc['substr']($data['avatar'], 11));
3983
			}
3984
3985
			// External url.
3986
			else
3987
				$image = parse_iri($data['avatar'], PHP_URL_SCHEME) !== null ? get_proxied_url($data['avatar']) : $modSettings['avatar_url'] . '/' . $data['avatar'];
3988
		}
3989
3990
		// Perhaps this user has an attachment as avatar...
3991
		elseif (!empty($data['filename']))
3992
			$image = $modSettings['custom_avatar_url'] . '/' . $data['filename'];
3993
3994
		// Right... no avatar... use our default image.
3995
		else
3996
			$image = $modSettings['avatar_url'] . '/default.png';
3997
	}
3998
3999
	call_integration_hook('integrate_set_avatar_data', array(&$image, &$data));
4000
4001
	// At this point in time $image has to be filled unless you chose to force gravatar and the user doesn't have the needed data to retrieve it... thus a check for !empty() is still needed.
4002
	if (!empty($image))
4003
		return array(
4004
			'name' => !empty($data['avatar']) ? $data['avatar'] : '',
4005
			'image' => '<img class="avatar" src="' . $image . '" alt="">',
4006
			'href' => $image,
4007
			'url' => $image,
4008
		);
4009
4010
	// Fallback to make life easier for everyone...
4011
	else
4012
		return array(
4013
			'name' => '',
4014
			'image' => '',
4015
			'href' => '',
4016
			'url' => '',
4017
		);
4018
}
4019
4020
/**
4021
 * Gets, and if necessary creates, the authentication secret to use for cookies, tokens, etc.
4022
 *
4023
 * Note: Never use the $auth_secret variable directly. Always call this function instead.
4024
 *
4025
 * @return string The authentication secret.
4026
 */
4027
function get_auth_secret()
4028
{
4029
	global $context, $auth_secret, $sourcedir, $boarddir, $smcFunc, $db_last_error, $txt;
4030
4031
	if (empty($auth_secret))
4032
	{
4033
		$auth_secret = bin2hex($smcFunc['random_bytes'](32));
4034
4035
		// It is important to store this in Settings.php, not the database.
4036
		require_once($sourcedir . '/Subs-Admin.php');
4037
4038
		// Did this fail?  If so, we should alert, log and set a static value.
4039
		if (!updateSettingsFile(array('auth_secret' => $auth_secret)))
4040
		{
4041
			$context['auth_secret_missing'] = true;
4042
			$auth_secret = hash_file('sha256', $boarddir . '/Settings.php');
4043
4044
			// Set the last error to now, but only every 15 minutes.  Don't need to flood the logs.
4045
			if (empty($db_last_error) || ($db_last_error + 60*15) <= time())
4046
			{
4047
				updateDbLastError(time());
4048
				loadLanguage('Errors');
4049
				log_error($txt['auth_secret_missing'], 'critical');
4050
			}
4051
		}
4052
	}
4053
4054
4055
	return $auth_secret;
4056
}
4057
4058
?>