sendmail()   F
last analyzed

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 197
Code Lines 97

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 37
eloc 97
nc 184464
nop 9
dl 0
loc 197
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * This file contains those functions pertaining to posting, and other such
5
 * operations, including sending emails, ims, blocking spam, preparsing posts,
6
 * spell checking, and the post box.
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines https://www.simplemachines.org
12
 * @copyright 2022 Simple Machines and individual contributors
13
 * @license https://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1.0
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * Takes a message and parses it, returning nothing.
23
 * Cleans up links (javascript, etc.) and code/quote sections.
24
 * Won't convert \n's and a few other things if previewing is true.
25
 *
26
 * @param string $message The mesasge
27
 * @param bool $previewing Whether we're previewing
28
 */
29
function preparsecode(&$message, $previewing = false)
30
{
31
	global $user_info, $modSettings, $context, $sourcedir, $smcFunc;
32
33
	static $tags_regex, $disallowed_tags_regex;
34
35
	// Convert control characters (except \t, \r, and \n) to harmless Unicode symbols
36
	$control_replacements = array(
37
		"\x00" => '&#x2400;', "\x01" => '&#x2401;', "\x02" => '&#x2402;', "\x03" => '&#x2403;',
38
		"\x04" => '&#x2404;', "\x05" => '&#x2405;', "\x06" => '&#x2406;', "\x07" => '&#x2407;',
39
		"\x08" => '&#x2408;', "\x0b" => '&#x240b;', "\x0c" => '&#x240c;', "\x0e" => '&#x240e;',
40
		"\x0f" => '&#x240f;', "\x10" => '&#x2410;', "\x11" => '&#x2411;', "\x12" => '&#x2412;',
41
		"\x13" => '&#x2413;', "\x14" => '&#x2414;', "\x15" => '&#x2415;', "\x16" => '&#x2416;',
42
		"\x17" => '&#x2417;', "\x18" => '&#x2418;', "\x19" => '&#x2419;', "\x1a" => '&#x241a;',
43
		"\x1b" => '&#x241b;', "\x1c" => '&#x241c;', "\x1d" => '&#x241d;', "\x1e" => '&#x241e;',
44
		"\x1f" => '&#x241f;',
45
	);
46
	$message = strtr($message, $control_replacements);
47
48
	// This line makes all languages *theoretically* work even with the wrong charset ;).
49
	if (empty($context['utf8']))
50
		$message = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message);
51
52
	// Normalize Unicode characters for storage efficiency, better searching, etc.
53
	else
54
		$message = $smcFunc['normalize']($message);
55
56
	// Clean out any other funky stuff.
57
	$message = sanitize_chars($message, 0);
58
59
	// Clean up after nobbc ;).
60
	$message = preg_replace_callback(
61
		'~\[nobbc\](.+?)\[/nobbc\]~is',
62
		function($a)
63
		{
64
			return '[nobbc]' . strtr($a[1], array('[' => '&#91;', ']' => '&#93;', ':' => '&#58;', '@' => '&#64;')) . '[/nobbc]';
65
		},
66
		$message
67
	);
68
69
	// Remove \r's... they're evil!
70
	$message = strtr($message, array("\r" => ''));
71
72
	// You won't believe this - but too many periods upsets apache it seems!
73
	$message = preg_replace('~\.{100,}~', '...', $message);
74
75
	// Trim off trailing quotes - these often happen by accident.
76
	while (substr($message, -7) == '[quote]')
77
		$message = substr($message, 0, -7);
78
	while (substr($message, 0, 8) == '[/quote]')
79
		$message = substr($message, 8);
80
81
	if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay'))
82
		$message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message);
83
84
	// Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed.
85
	$in_tag = false;
86
	$had_tag = false;
87
	$codeopen = 0;
88
	if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches))
89
		foreach ($matches[0] as $index => $dummy)
90
		{
91
			// Closing?
92
			if (!empty($matches[2][$index]))
93
			{
94
				// If it's closing and we're not in a tag we need to open it...
95
				if (!$in_tag)
96
					$codeopen = true;
97
				// Either way we ain't in one any more.
98
				$in_tag = false;
99
			}
100
			// Opening tag...
101
			else
102
			{
103
				$had_tag = true;
104
				// If we're in a tag don't do nought!
105
				if (!$in_tag)
106
					$in_tag = true;
107
			}
108
		}
109
110
	// If we have an open tag, close it.
111
	if ($in_tag)
112
		$message .= '[/code]';
113
	// Open any ones that need to be open, only if we've never had a tag.
114
	if ($codeopen && !$had_tag)
115
		$message = '[code]' . $message;
116
117
	// Replace code BBC with placeholders. We'll restore them at the end.
118
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
119
	for ($i = 0, $n = count($parts); $i < $n; $i++)
120
	{
121
		// It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
122
		if ($i % 4 == 2)
123
		{
124
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
125
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
126
			$code_tags[$substitute] = $code_tag;
127
			$parts[$i] = $i;
128
		}
129
	}
130
131
	$message = implode('', $parts);
132
133
	// The regular expression non breaking space has many versions.
134
	$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
135
136
	// Now that we've fixed all the code tags, let's fix the img and url tags...
137
	fixTags($message);
138
139
	// Replace /me.+?\n with [me=name]dsf[/me]\n.
140
	if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false)
141
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=&quot;' . $user_info['name'] . '&quot;]$2[/me]', $message);
142
	else
143
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $message);
144
145
	if (!$previewing && strpos($message, '[html]') !== false)
146
	{
147
		if (allowedTo('bbc_html'))
148
			$message = preg_replace_callback(
149
				'~\[html\](.+?)\[/html\]~is',
150
				function($m)
151
				{
152
					return '[html]' . strtr(un_htmlspecialchars($m[1]), array("\n" => '&#13;', '  ' => ' &#32;', '[' => '&#91;', ']' => '&#93;')) . '[/html]';
153
				},
154
				$message
155
			);
156
157
		// We should edit them out, or else if an admin edits the message they will get shown...
158
		else
159
		{
160
			while (strpos($message, '[html]') !== false)
161
				$message = preg_replace('~\[[/]?html\]~i', '', $message);
162
		}
163
	}
164
165
	// Let's look at the time tags...
166
	$message = preg_replace_callback(
167
		'~\[time(?:=(absolute))*\](.+?)\[/time\]~i',
168
		function($m) use ($modSettings, $user_info)
169
		{
170
			return "[time]" . (is_numeric("$m[2]") || @strtotime("$m[2]") == 0 ? "$m[2]" : strtotime("$m[2]") - ("$m[1]" == "absolute" ? 0 : (($modSettings["time_offset"] + $user_info["time_offset"]) * 3600))) . "[/time]";
171
		},
172
		$message
173
	);
174
175
	// Change the color specific tags to [color=the color].
176
	$message = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $message); // First do the opening tags.
177
	$message = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $message); // And now do the closing tags
178
179
	// Neutralize any BBC tags this member isn't permitted to use.
180
	if (empty($disallowed_tags_regex))
181
	{
182
		// Legacy BBC are only retained for historical reasons. They're not for use in new posts.
183
		$disallowed_bbc = $context['legacy_bbc'];
184
185
		// Some BBC require permissions.
186
		foreach ($context['restricted_bbc'] as $bbc)
187
		{
188
			// Skip html, since we handled it separately above.
189
			if ($bbc === 'html')
190
				continue;
191
			if (!allowedTo('bbc_' . $bbc))
192
				$disallowed_bbc[] = $bbc;
193
		}
194
195
		$disallowed_tags_regex = build_regex(array_unique($disallowed_bbc), '~');
196
	}
197
	if (!empty($disallowed_tags_regex))
198
		$message = preg_replace('~\[(?=/?' . $disallowed_tags_regex . '\b)~i', '&#91;', $message);
199
200
	// Make sure all tags are lowercase.
201
	$message = preg_replace_callback(
202
		'~\[(/?)(list|li|table|tr|td)\b([^\]]*)\]~i',
203
		function($m)
204
		{
205
			return "[$m[1]" . strtolower("$m[2]") . "$m[3]]";
206
		},
207
		$message
208
	);
209
210
	$list_open = substr_count($message, '[list]') + substr_count($message, '[list ');
211
	$list_close = substr_count($message, '[/list]');
212
	if ($list_close - $list_open > 0)
213
		$message = str_repeat('[list]', $list_close - $list_open) . $message;
214
	if ($list_open - $list_close > 0)
215
		$message = $message . str_repeat('[/list]', $list_open - $list_close);
216
217
	$mistake_fixes = array(
218
		// Find [table]s not followed by [tr].
219
		'~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]',
220
		// Find [tr]s not followed by [td].
221
		'~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]',
222
		// Find [/td]s not followed by something valid.
223
		'~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]',
224
		// Find [/tr]s not followed by something valid.
225
		'~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]',
226
		// Find [/td]s incorrectly followed by [/table].
227
		'~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]',
228
		// Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td].
229
		'~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]',
230
		// Now, any [td]s left should have a [tr] before them.
231
		'~\[td\]~s' => '[tr][td]',
232
		// Look for [tr]s which are correctly placed.
233
		'~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]',
234
		// Any remaining [tr]s should have a [table] before them.
235
		'~\[tr\]~s' => '[table][tr]',
236
		// Look for [/td]s followed by [/tr].
237
		'~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]',
238
		// Any remaining [/tr]s should have a [/td].
239
		'~\[/tr\]~s' => '[/td][/tr]',
240
		// Look for properly opened [li]s which aren't closed.
241
		'~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]',
242
		'~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]',
243
		'~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]',
244
		// Lists - find correctly closed items/lists.
245
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]',
246
		// Find list items closed and then opened.
247
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]',
248
		// Now, find any [list]s or [/li]s followed by [li].
249
		'~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]',
250
		// Allow for sub lists.
251
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[list]',
252
		'~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' . ($context['utf8'] ? 'u' : '') => '[/list]$1[_li_]',
253
		// Any remaining [li]s weren't inside a [list].
254
		'~\[li\]~' => '[list][li]',
255
		// Any remaining [/li]s weren't before a [/list].
256
		'~\[/li\]~' => '[/li][/list]',
257
		// Put the correct ones back how we found them.
258
		'~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]',
259
		// Images with no real url.
260
		'~\[img\]https?://.{0,7}\[/img\]~' => '',
261
	);
262
263
	// Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.)
264
	for ($j = 0; $j < 3; $j++)
265
		$message = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $message);
266
267
	// Remove empty bbc from the sections outside the code tags
268
	if (empty($tags_regex))
269
	{
270
		require_once($sourcedir . '/Subs.php');
271
272
		$allowed_empty = array('anchor', 'td',);
273
274
		$tags = array();
275
		foreach (($codes = parse_bbc(false)) as $code)
0 ignored issues
show
Bug introduced by
The expression $codes = parse_bbc(false) of type string is not traversable.
Loading history...
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
276
			if (!in_array($code['tag'], $allowed_empty))
277
				$tags[] = $code['tag'];
278
279
		$tags_regex = build_regex($tags, '~');
280
	}
281
	while (preg_match('~\[(' . $tags_regex . ')\b[^\]]*\]\s*\[/\1\]\s?~i', $message))
282
		$message = preg_replace('~\[(' . $tags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', '', $message);
283
284
	// Restore code blocks
285
	if (!empty($code_tags))
286
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
287
288
	// Restore white space entities
289
	if (!$previewing)
290
		$message = strtr($message, array('  ' => '&nbsp; ', "\n" => '<br>', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
291
	else
292
		$message = strtr($message, array('  ' => '&nbsp; ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
293
294
	// Now let's quickly clean up things that will slow our parser (which are common in posted code.)
295
	$message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
296
297
	// Any hooks want to work here?
298
	call_integration_hook('integrate_preparsecode', array(&$message, $previewing));
299
}
300
301
/**
302
 * This is very simple, and just removes things done by preparsecode.
303
 *
304
 * @param string $message The message
305
 */
306
function un_preparsecode($message)
307
{
308
	global $smcFunc;
309
310
	// Any hooks want to work here?
311
	call_integration_hook('integrate_unpreparsecode', array(&$message));
312
313
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
314
315
	// We're going to unparse only the stuff outside [code]...
316
	for ($i = 0, $n = count($parts); $i < $n; $i++)
317
	{
318
		// If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
319
		if ($i % 4 == 2)
320
		{
321
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
322
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
323
			$code_tags[$substitute] = $code_tag;
324
			$parts[$i] = $i;
325
		}
326
	}
327
328
	$message = implode('', $parts);
329
330
	$message = preg_replace_callback(
331
		'~\[html\](.+?)\[/html\]~i',
332
		function($m) use ($smcFunc)
333
		{
334
			return "[html]" . strtr($smcFunc['htmlspecialchars']("$m[1]", ENT_QUOTES), array("\\&quot;" => "&quot;", "&amp;#13;" => "<br>", "&amp;#32;" => " ", "&amp;#91;" => "[", "&amp;#93;" => "]")) . "[/html]";
335
		},
336
		$message
337
	);
338
339
	if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay'))
340
		$message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message);
341
342
	// Attempt to un-parse the time to something less awful.
343
	$message = preg_replace_callback(
344
		'~\[time\](\d{0,10})\[/time\]~i',
345
		function($m)
346
		{
347
			return "[time]" . timeformat("$m[1]", false) . "[/time]";
0 ignored issues
show
Bug introduced by
$m['1'] of type string is incompatible with the type integer expected by parameter $log_time of timeformat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

347
			return "[time]" . timeformat(/** @scrutinizer ignore-type */ "$m[1]", false) . "[/time]";
Loading history...
348
		},
349
		$message
350
	);
351
352
	if (!empty($code_tags))
353
		$message = strtr($message, $code_tags);
354
355
	// Change breaks back to \n's and &nsbp; back to spaces.
356
	return preg_replace('~<br\s*/?' . '>~', "\n", str_replace('&nbsp;', ' ', $message));
357
}
358
359
/**
360
 * Fix any URLs posted - ie. remove 'javascript:'.
361
 * Used by preparsecode, fixes links in message and returns nothing.
362
 *
363
 * @param string $message The message
364
 */
365
function fixTags(&$message)
366
{
367
	global $modSettings;
368
369
	// WARNING: Editing the below can cause large security holes in your forum.
370
	// Edit only if you are sure you know what you are doing.
371
372
	$fixArray = array(
373
		// [img]http://...[/img] or [img width=1]http://...[/img]
374
		array(
375
			'tag' => 'img',
376
			'protocols' => array('http', 'https'),
377
			'embeddedUrl' => false,
378
			'hasEqualSign' => false,
379
			'hasExtra' => true,
380
		),
381
		// [url]http://...[/url]
382
		array(
383
			'tag' => 'url',
384
			'protocols' => array('http', 'https'),
385
			'embeddedUrl' => false,
386
			'hasEqualSign' => false,
387
		),
388
		// [url=http://...]name[/url]
389
		array(
390
			'tag' => 'url',
391
			'protocols' => array('http', 'https'),
392
			'embeddedUrl' => true,
393
			'hasEqualSign' => true,
394
		),
395
		// [iurl]http://...[/iurl]
396
		array(
397
			'tag' => 'iurl',
398
			'protocols' => array('http', 'https'),
399
			'embeddedUrl' => false,
400
			'hasEqualSign' => false,
401
		),
402
		// [iurl=http://...]name[/iurl]
403
		array(
404
			'tag' => 'iurl',
405
			'protocols' => array('http', 'https'),
406
			'embeddedUrl' => true,
407
			'hasEqualSign' => true,
408
		),
409
		// The rest of these are deprecated.
410
		// [ftp]ftp://...[/ftp]
411
		array(
412
			'tag' => 'ftp',
413
			'protocols' => array('ftp', 'ftps', 'sftp'),
414
			'embeddedUrl' => false,
415
			'hasEqualSign' => false,
416
		),
417
		// [ftp=ftp://...]name[/ftp]
418
		array(
419
			'tag' => 'ftp',
420
			'protocols' => array('ftp', 'ftps', 'sftp'),
421
			'embeddedUrl' => true,
422
			'hasEqualSign' => true,
423
		),
424
		// [flash]http://...[/flash]
425
		array(
426
			'tag' => 'flash',
427
			'protocols' => array('http', 'https'),
428
			'embeddedUrl' => false,
429
			'hasEqualSign' => false,
430
			'hasExtra' => true,
431
		),
432
	);
433
434
	// Fix each type of tag.
435
	foreach ($fixArray as $param)
436
		fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra']));
437
438
	// Now fix possible security problems with images loading links automatically...
439
	$message = preg_replace_callback(
440
		'~(\[img.*?\])(.+?)\[/img\]~is',
441
		function($m)
442
		{
443
			return "$m[1]" . preg_replace("~action(=|%3d)(?!dlattach)~i", "action-", "$m[2]") . "[/img]";
444
		},
445
		$message
446
	);
447
448
}
449
450
/**
451
 * Fix a specific class of tag - ie. url with =.
452
 * Used by fixTags, fixes a specific tag's links.
453
 *
454
 * @param string $message The message
455
 * @param string $myTag The tag
456
 * @param string $protocols The protocols
457
 * @param bool $embeddedUrl Whether it *can* be set to something
458
 * @param bool $hasEqualSign Whether it *is* set to something
459
 * @param bool $hasExtra Whether it can have extra cruft after the begin tag.
460
 */
461
function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false)
462
{
463
	global $boardurl, $scripturl;
464
465
	$forbidden_protocols = array(
466
		// Poses security risks.
467
		'javascript',
468
		// Allows file data to be embedded, bypassing our attachment system.
469
		'data',
470
	);
471
472
	if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0)
473
		$domain_url = $match[1];
474
	else
475
		$domain_url = $boardurl . '/';
476
477
	$replaces = array();
478
479
	if ($hasEqualSign && $embeddedUrl)
480
	{
481
		$quoted = preg_match('~\[(' . $myTag . ')=&quot;~', $message);
482
		preg_match_all('~\[(' . $myTag . ')=' . ($quoted ? '&quot;(.*?)&quot;' : '([^\]]*?)') . '\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
483
	}
484
	elseif ($hasEqualSign)
485
		preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
486
	else
487
		preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches);
488
489
	foreach ($matches[0] as $k => $dummy)
490
	{
491
		// Remove all leading and trailing whitespace.
492
		$replace = trim($matches[2][$k]);
493
		$this_tag = $matches[1][$k];
494
		$this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k];
495
496
		$found = false;
497
		foreach ($protocols as $protocol)
0 ignored issues
show
Bug introduced by
The expression $protocols of type string is not traversable.
Loading history...
498
		{
499
			$found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0;
500
			if ($found)
501
				break;
502
		}
503
504
		$current_protocol = strtolower(parse_iri($replace, PHP_URL_SCHEME) ?? "");
505
506
		if (in_array($current_protocol, $forbidden_protocols))
507
		{
508
			$replace = 'about:invalid';
509
		}
510
		elseif (!$found && $protocols[0] == 'http')
511
		{
512
			// A path
513
			if (substr($replace, 0, 1) == '/' && substr($replace, 0, 2) != '//')
514
				$replace = $domain_url . $replace;
515
			// A query
516
			elseif (substr($replace, 0, 1) == '?')
517
				$replace = $scripturl . $replace;
518
			// A fragment
519
			elseif (substr($replace, 0, 1) == '#' && $embeddedUrl)
520
			{
521
				$replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1));
522
				$this_tag = 'iurl';
523
				$this_close = 'iurl';
524
			}
525
			elseif (substr($replace, 0, 2) != '//' && empty($current_protocol))
526
				$replace = $protocols[0] . '://' . $replace;
527
		}
528
		elseif (!$found && $protocols[0] == 'ftp')
529
			$replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace);
530
		elseif (!$found && empty($current_protocol))
531
			$replace = $protocols[0] . '://' . $replace;
532
533
		if ($hasEqualSign && $embeddedUrl)
534
			$replaces[$matches[0][$k]] = '[' . $this_tag . '=&quot;' . $replace . '&quot;]' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']');
535
		elseif ($hasEqualSign)
536
			$replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']';
537
		elseif ($embeddedUrl)
538
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']';
539
		else
540
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']';
541
	}
542
543
	foreach ($replaces as $k => $v)
544
	{
545
		if ($k == $v)
546
			unset($replaces[$k]);
547
	}
548
549
	if (!empty($replaces))
550
		$message = strtr($message, $replaces);
551
}
552
553
/**
554
 * This function sends an email to the specified recipient(s).
555
 * It uses the mail_type settings and webmaster_email variable.
556
 *
557
 * @param array $to The email(s) to send to
558
 * @param string $subject Email subject, expected to have entities, and slashes, but not be parsed
559
 * @param string $message Email body, expected to have slashes, no htmlentities
560
 * @param string $from The address to use for replies
561
 * @param string $message_id If specified, it will be used as local part of the Message-ID header.
562
 * @param bool $send_html Whether or not the message is HTML vs. plain text
563
 * @param int $priority The priority of the message
564
 * @param bool $hotmail_fix Whether to apply the "hotmail fix"
565
 * @param bool $is_private Whether this is private
566
 * @return boolean Whether ot not the email was sent properly.
567
 */
568
function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false)
569
{
570
	global $webmaster_email, $context, $modSettings, $txt, $scripturl;
571
572
	// Use sendmail if it's set or if no SMTP server is set.
573
	$use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
574
575
	// Line breaks need to be \r\n only in windows or for SMTP.
576
	$line_break = $context['server']['is_windows'] || !$use_sendmail ? "\r\n" : "\n";
577
578
	// So far so good.
579
	$mail_result = true;
580
581
	// If the recipient list isn't an array, make it one.
582
	$to_array = is_array($to) ? $to : array($to);
0 ignored issues
show
introduced by
The condition is_array($to) is always true.
Loading history...
583
584
	// Make sure we actually have email addresses to send this to
585
	foreach ($to_array as $k => $v)
586
	{
587
		// This should never happen, but better safe than sorry
588
		if (trim($v) == '')
589
		{
590
			unset($to_array[$k]);
591
		}
592
	}
593
594
	// Nothing left? Nothing else to do
595
	if (empty($to_array))
596
		return true;
597
598
	// Once upon a time, Hotmail could not interpret non-ASCII mails.
599
	// In honour of those days, it's still called the 'hotmail fix'.
600
	if ($hotmail_fix === null)
601
	{
602
		$hotmail_to = array();
603
		foreach ($to_array as $i => $to_address)
604
		{
605
			if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
606
			{
607
				$hotmail_to[] = $to_address;
608
				$to_array = array_diff($to_array, array($to_address));
609
			}
610
		}
611
612
		// Call this function recursively for the hotmail addresses.
613
		if (!empty($hotmail_to))
614
			$mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true, $is_private);
615
616
		// The remaining addresses no longer need the fix.
617
		$hotmail_fix = false;
618
619
		// No other addresses left? Return instantly.
620
		if (empty($to_array))
621
			return $mail_result;
622
	}
623
624
	// Get rid of entities.
625
	$subject = un_htmlspecialchars($subject);
626
	// Make the message use the proper line breaks.
627
	$message = str_replace(array("\r", "\n"), array('', $line_break), $message);
628
629
	// Make sure hotmail mails are sent as HTML so that HTML entities work.
630
	if ($hotmail_fix && !$send_html)
631
	{
632
		$send_html = true;
633
		$message = strtr($message, array($line_break => '<br>' . $line_break));
634
		$message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
635
	}
636
637
	list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
638
	list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
639
640
	// Construct the mail headers...
641
	$headers = 'From: "' . $from_name . '" <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
642
	$headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
643
	$headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
644
	$headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
645
646
	if ($message_id !== null && empty($modSettings['mail_no_message_id']))
647
		$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
648
	$headers .= 'X-Mailer: SMF' . $line_break;
649
650
	// Pass this to the integration before we start modifying the output -- it'll make it easier later.
651
	if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers, &$to_array)), true))
652
		return false;
653
654
	// Save the original message...
655
	$orig_message = $message;
656
657
	// The mime boundary separates the different alternative versions.
658
	$mime_boundary = 'SMF-' . md5($message . time());
659
660
	// Using mime, as it allows to send a plain unencoded alternative.
661
	$headers .= 'Mime-Version: 1.0' . $line_break;
662
	$headers .= 'content-type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
663
	$headers .= 'content-transfer-encoding: 7bit' . $line_break;
664
665
	// Sending HTML?  Let's plop in some basic stuff, then.
666
	if ($send_html)
667
	{
668
		$no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
669
670
		// But, then, dump it and use a plain one for dinosaur clients.
671
		list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
672
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
673
674
		// This is the plain text version.  Even if no one sees it, we need it for spam checkers.
675
		list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
676
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
677
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
678
		$message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
679
680
		// This is the actual HTML message, prim and proper.  If we wanted images, they could be inlined here (with multipart/related, etc.)
681
		list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
682
		$message .= 'content-type: text/html; charset=' . $charset . $line_break;
683
		$message .= 'content-transfer-encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
684
		$message .= $html_message . $line_break . '--' . $mime_boundary . '--';
685
	}
686
	// Text is good too.
687
	else
688
	{
689
		// Send a plain message first, for the older web clients.
690
		list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
691
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
692
693
		// Now add an encoded message using the forum's character set.
694
		list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
695
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
696
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
697
		$message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
698
	}
699
700
	// Are we using the mail queue, if so this is where we butt in...
701
	if ($priority != 0)
702
		return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
703
704
	// If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
705
	elseif (!empty($modSettings['mail_limit']))
706
	{
707
		list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
708
		if (empty($mails_this_minute) || time() > $last_mail_time + 60)
709
			$new_queue_stat = time() . '|' . 1;
710
		else
711
			$new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
712
713
		updateSettings(array('mail_recent' => $new_queue_stat));
714
	}
715
716
	// SMTP or sendmail?
717
	if ($use_sendmail)
718
	{
719
		$subject = strtr($subject, array("\r" => '', "\n" => ''));
720
		if (!empty($modSettings['mail_strip_carriage']))
721
		{
722
			$message = strtr($message, array("\r" => ''));
723
			$headers = strtr($headers, array("\r" => ''));
724
		}
725
726
		foreach ($to_array as $to)
0 ignored issues
show
introduced by
$to is overwriting one of the parameters of this function.
Loading history...
727
		{
728
			set_error_handler(
729
				function($errno, $errstr, $errfile, $errline)
730
				{
731
					// error was suppressed with the @-operator
732
					if (0 === error_reporting())
733
						return false;
734
735
					throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
736
				}
737
			);
738
			try
739
			{
740
				if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
741
				{
742
					log_error(sprintf($txt['mail_send_unable'], $to));
743
					$mail_result = false;
744
				}
745
			}
746
			catch (ErrorException $e)
747
			{
748
				log_error($e->getMessage(), 'general', $e->getFile(), $e->getLine());
749
				log_error(sprintf($txt['mail_send_unable'], $to));
750
				$mail_result = false;
751
			}
752
			restore_error_handler();
753
754
			// Wait, wait, I'm still sending here!
755
			@set_time_limit(300);
756
			if (function_exists('apache_reset_timeout'))
757
				@apache_reset_timeout();
758
		}
759
	}
760
	else
761
		$mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
762
763
	// Everything go smoothly?
764
	return $mail_result;
765
}
766
767
/**
768
 * Add an email to the mail queue.
769
 *
770
 * @param bool $flush Whether to flush the queue
771
 * @param array $to_array An array of recipients
772
 * @param string $subject The subject of the message
773
 * @param string $message The message
774
 * @param string $headers The headers
775
 * @param bool $send_html Whether to send in HTML format
776
 * @param int $priority The priority
777
 * @param bool $is_private Whether this is private
778
 * @return boolean Whether the message was added
779
 */
780
function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
781
{
782
	global $context, $smcFunc;
783
784
	static $cur_insert = array();
785
	static $cur_insert_len = 0;
786
787
	if ($cur_insert_len == 0)
788
		$cur_insert = array();
789
790
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
791
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
792
	{
793
		// Only do these once.
794
		$cur_insert_len = 0;
795
796
		// Dump the data...
797
		$smcFunc['db_insert']('',
798
			'{db_prefix}mail_queue',
799
			array(
800
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
801
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
802
			),
803
			$cur_insert,
804
			array('id_mail')
805
		);
806
807
		$cur_insert = array();
808
		$context['flush_mail'] = false;
809
	}
810
811
	// If we're flushing we're done.
812
	if ($flush)
813
	{
814
		$nextSendTime = time() + 10;
815
816
		$smcFunc['db_query']('', '
817
			UPDATE {db_prefix}settings
818
			SET value = {string:nextSendTime}
819
			WHERE variable = {literal:mail_next_send}
820
				AND value = {string:no_outstanding}',
821
			array(
822
				'nextSendTime' => $nextSendTime,
823
				'no_outstanding' => '0',
824
			)
825
		);
826
827
		return true;
828
	}
829
830
	// Ensure we tell obExit to flush.
831
	$context['flush_mail'] = true;
832
833
	foreach ($to_array as $to)
834
	{
835
		// Will this insert go over MySQL's limit?
836
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
837
838
		// Insert limit of 1M (just under the safety) is reached?
839
		if ($this_insert_len + $cur_insert_len > 1000000)
840
		{
841
			// Flush out what we have so far.
842
			$smcFunc['db_insert']('',
843
				'{db_prefix}mail_queue',
844
				array(
845
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
846
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
847
				),
848
				$cur_insert,
849
				array('id_mail')
850
			);
851
852
			// Clear this out.
853
			$cur_insert = array();
854
			$cur_insert_len = 0;
855
		}
856
857
		// Now add the current insert to the array...
858
		$cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
859
		$cur_insert_len += $this_insert_len;
860
	}
861
862
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
863
	if (SMF === 'SSI' || SMF === 'BACKGROUND')
0 ignored issues
show
introduced by
The condition SMF === 'SSI' is always true.
Loading history...
864
		return AddMailQueue(true);
865
866
	return true;
867
}
868
869
/**
870
 * Sends an personal message from the specified person to the specified people
871
 * ($from defaults to the user)
872
 *
873
 * @param array $recipients An array containing the arrays 'to' and 'bcc', both containing id_member's.
874
 * @param string $subject Should have no slashes and no html entities
875
 * @param string $message Should have no slashes and no html entities
876
 * @param bool $store_outbox Whether to store it in the sender's outbox
877
 * @param array $from An array with the id, name, and username of the member.
878
 * @param int $pm_head The ID of the chain being replied to - if any.
879
 * @return array An array with log entries telling how many recipients were successful and which recipients it failed to send to.
880
 */
881
function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
882
{
883
	global $scripturl, $txt, $user_info, $language, $sourcedir;
884
	global $modSettings, $smcFunc;
885
886
	// Make sure the PM language file is loaded, we might need something out of it.
887
	loadLanguage('PersonalMessage');
888
889
	// Initialize log array.
890
	$log = array(
891
		'failed' => array(),
892
		'sent' => array()
893
	);
894
895
	if ($from === null)
896
		$from = array(
897
			'id' => $user_info['id'],
898
			'name' => $user_info['name'],
899
			'username' => $user_info['username']
900
		);
901
902
	// This is the one that will go in their inbox.
903
	$htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
904
	preparsecode($htmlmessage);
905
	$htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => ''));
906
	if ($smcFunc['strlen']($htmlsubject) > 100)
907
		$htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100);
908
909
	// Make sure is an array
910
	if (!is_array($recipients))
0 ignored issues
show
introduced by
The condition is_array($recipients) is always true.
Loading history...
911
		$recipients = array($recipients);
912
913
	// Integrated PMs
914
	call_integration_hook('integrate_personal_message', array(&$recipients, &$from, &$subject, &$message));
915
916
	// Get a list of usernames and convert them to IDs.
917
	$usernames = array();
918
	foreach ($recipients as $rec_type => $rec)
919
	{
920
		foreach ($rec as $id => $member)
921
		{
922
			if (!is_numeric($recipients[$rec_type][$id]))
923
			{
924
				$recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('~[<>&"\'=\\\]~', '', $recipients[$rec_type][$id])));
925
				$usernames[$recipients[$rec_type][$id]] = 0;
926
			}
927
		}
928
	}
929
	if (!empty($usernames))
930
	{
931
		$request = $smcFunc['db_query']('pm_find_username', '
932
			SELECT id_member, member_name
933
			FROM {db_prefix}members
934
			WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
935
			array(
936
				'usernames' => array_keys($usernames),
937
			)
938
		);
939
		while ($row = $smcFunc['db_fetch_assoc']($request))
940
			if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
941
				$usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
942
		$smcFunc['db_free_result']($request);
943
944
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
945
		foreach ($recipients as $rec_type => $rec)
946
			foreach ($rec as $id => $member)
947
			{
948
				if (is_numeric($recipients[$rec_type][$id]))
949
					continue;
950
951
				if (!empty($usernames[$member]))
952
					$recipients[$rec_type][$id] = $usernames[$member];
953
				else
954
				{
955
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
956
					unset($recipients[$rec_type][$id]);
957
				}
958
			}
959
	}
960
961
	// Make sure there are no duplicate 'to' members.
962
	$recipients['to'] = array_unique($recipients['to']);
963
964
	// Only 'bcc' members that aren't already in 'to'.
965
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
966
967
	// Combine 'to' and 'bcc' recipients.
968
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
969
970
	// Check no-one will want it deleted right away!
971
	$request = $smcFunc['db_query']('', '
972
		SELECT
973
			id_member, criteria, is_or
974
		FROM {db_prefix}pm_rules
975
		WHERE id_member IN ({array_int:to_members})
976
			AND delete_pm = {int:delete_pm}',
977
		array(
978
			'to_members' => $all_to,
979
			'delete_pm' => 1,
980
		)
981
	);
982
	$deletes = array();
983
	// Check whether we have to apply anything...
984
	while ($row = $smcFunc['db_fetch_assoc']($request))
985
	{
986
		$criteria = $smcFunc['json_decode']($row['criteria'], true);
987
		// Note we don't check the buddy status, cause deletion from buddy = madness!
988
		$delete = false;
989
		foreach ($criteria as $criterium)
990
		{
991
			if (($criterium['t'] == 'mid' && $criterium['v'] == $from['id']) || ($criterium['t'] == 'gid' && in_array($criterium['v'], $user_info['groups'])) || ($criterium['t'] == 'sub' && strpos($subject, $criterium['v']) !== false) || ($criterium['t'] == 'msg' && strpos($message, $criterium['v']) !== false))
992
				$delete = true;
993
			// If we're adding and one criteria don't match then we stop!
994
			elseif (!$row['is_or'])
995
			{
996
				$delete = false;
997
				break;
998
			}
999
		}
1000
		if ($delete)
1001
			$deletes[$row['id_member']] = 1;
1002
	}
1003
	$smcFunc['db_free_result']($request);
1004
1005
	// Load the membergrounp message limits.
1006
	// @todo Consider caching this?
1007
	static $message_limit_cache = array();
1008
	if (!allowedTo('moderate_forum') && empty($message_limit_cache))
1009
	{
1010
		$request = $smcFunc['db_query']('', '
1011
			SELECT id_group, max_messages
1012
			FROM {db_prefix}membergroups',
1013
			array(
1014
			)
1015
		);
1016
		while ($row = $smcFunc['db_fetch_assoc']($request))
1017
			$message_limit_cache[$row['id_group']] = $row['max_messages'];
1018
		$smcFunc['db_free_result']($request);
1019
	}
1020
1021
	// Load the groups that are allowed to read PMs.
1022
	require_once($sourcedir . '/Subs-Members.php');
1023
	$pmReadGroups = groupsAllowedTo('pm_read');
1024
1025
	if (empty($modSettings['permission_enable_deny']))
1026
		$pmReadGroups['denied'] = array();
1027
1028
	// Load their alert preferences
1029
	require_once($sourcedir . '/Subs-Notify.php');
1030
	$notifyPrefs = getNotifyPrefs($all_to, array('pm_new', 'pm_reply', 'pm_notify'), true);
1031
1032
	$request = $smcFunc['db_query']('', '
1033
		SELECT
1034
			member_name, real_name, id_member, email_address, lngfile,
1035
			instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
1036
			(pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
1037
			(pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
1038
			(pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
1039
			FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
1040
			additional_groups, id_group, id_post_group
1041
		FROM {db_prefix}members
1042
		WHERE id_member IN ({array_int:recipients})
1043
		ORDER BY lngfile
1044
		LIMIT {int:count_recipients}',
1045
		array(
1046
			'not_on_ignore_list' => 1,
1047
			'buddies_only' => 2,
1048
			'admins_only' => 3,
1049
			'recipients' => $all_to,
1050
			'count_recipients' => count($all_to),
1051
			'from_id' => $from['id'],
1052
		)
1053
	);
1054
	$notifications = array();
1055
	while ($row = $smcFunc['db_fetch_assoc']($request))
1056
	{
1057
		// Don't do anything for members to be deleted!
1058
		if (isset($deletes[$row['id_member']]))
1059
			continue;
1060
1061
		// Load the preferences for this member (if any)
1062
		$prefs = !empty($notifyPrefs[$row['id_member']]) ? $notifyPrefs[$row['id_member']] : array();
1063
		$prefs = array_merge(array(
1064
			'pm_new' => 0,
1065
			'pm_reply' => 0,
1066
			'pm_notify' => 0,
1067
		), $prefs);
1068
1069
		// We need to know this members groups.
1070
		$groups = explode(',', $row['additional_groups']);
1071
		$groups[] = $row['id_group'];
1072
		$groups[] = $row['id_post_group'];
1073
1074
		$message_limit = -1;
1075
		// For each group see whether they've gone over their limit - assuming they're not an admin.
1076
		if (!in_array(1, $groups))
1077
		{
1078
			foreach ($groups as $id)
1079
			{
1080
				if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
1081
					$message_limit = $message_limit_cache[$id];
1082
			}
1083
1084
			if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
1085
			{
1086
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
1087
				unset($all_to[array_search($row['id_member'], $all_to)]);
1088
				continue;
1089
			}
1090
1091
			// Do they have any of the allowed groups?
1092
			if (count(array_intersect($pmReadGroups['allowed'], $groups)) == 0 || count(array_intersect($pmReadGroups['denied'], $groups)) != 0)
1093
			{
1094
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1095
				unset($all_to[array_search($row['id_member'], $all_to)]);
1096
				continue;
1097
			}
1098
		}
1099
1100
		// Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
1101
		if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id'])
1102
		{
1103
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
1104
			unset($all_to[array_search($row['id_member'], $all_to)]);
1105
			continue;
1106
		}
1107
1108
		// If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
1109
		if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin']))
1110
		{
1111
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1112
			unset($all_to[array_search($row['id_member'], $all_to)]);
1113
			continue;
1114
		}
1115
1116
		// Send a notification, if enabled - taking the buddy list into account.
1117
		if (!empty($row['email_address'])
1118
			&& ((empty($pm_head) && $prefs['pm_new'] & 0x02) || (!empty($pm_head) && $prefs['pm_reply'] & 0x02))
1119
			&& ($prefs['pm_notify'] <= 1 || ($prefs['pm_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
1120
		{
1121
			$notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
1122
		}
1123
1124
		$log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
1125
	}
1126
	$smcFunc['db_free_result']($request);
1127
1128
	// Only 'send' the message if there are any recipients left.
1129
	if (empty($all_to))
1130
		return $log;
1131
1132
	// Insert the message itself and then grab the last insert id.
1133
	$id_pm = $smcFunc['db_insert']('',
1134
		'{db_prefix}personal_messages',
1135
		array(
1136
			'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
1137
			'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
1138
		),
1139
		array(
1140
			$pm_head, $from['id'], ($store_outbox ? 0 : 1),
1141
			$from['username'], time(), $htmlsubject, $htmlmessage,
1142
		),
1143
		array('id_pm'),
1144
		1
1145
	);
1146
1147
	// Add the recipients.
1148
	if (!empty($id_pm))
1149
	{
1150
		// If this is new we need to set it part of it's own conversation.
1151
		if (empty($pm_head))
1152
			$smcFunc['db_query']('', '
1153
				UPDATE {db_prefix}personal_messages
1154
				SET id_pm_head = {int:id_pm_head}
1155
				WHERE id_pm = {int:id_pm_head}',
1156
				array(
1157
					'id_pm_head' => $id_pm,
1158
				)
1159
			);
1160
1161
		// Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
1162
		$smcFunc['db_query']('', '
1163
			DELETE FROM {db_prefix}pm_recipients
1164
			WHERE id_pm = {int:id_pm}',
1165
			array(
1166
				'id_pm' => $id_pm,
1167
			)
1168
		);
1169
1170
		$insertRows = array();
1171
		$to_list = array();
1172
		foreach ($all_to as $to)
1173
		{
1174
			$insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
1175
			if (!in_array($to, $recipients['bcc']))
1176
				$to_list[] = $to;
1177
		}
1178
1179
		$smcFunc['db_insert']('insert',
1180
			'{db_prefix}pm_recipients',
1181
			array(
1182
				'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
1183
			),
1184
			$insertRows,
1185
			array('id_pm', 'id_member')
1186
		);
1187
	}
1188
1189
	$to_names = array();
1190
	if (count($to_list) > 1)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $to_list does not seem to be defined for all execution paths leading up to this point.
Loading history...
1191
	{
1192
		$request = $smcFunc['db_query']('', '
1193
			SELECT real_name
1194
			FROM {db_prefix}members
1195
			WHERE id_member IN ({array_int:to_members})
1196
				AND id_member != {int:from}',
1197
			array(
1198
				'to_members' => $to_list,
1199
				'from' => $from['id'],
1200
			)
1201
		);
1202
		while ($row = $smcFunc['db_fetch_assoc']($request))
1203
			$to_names[] = un_htmlspecialchars($row['real_name']);
1204
		$smcFunc['db_free_result']($request);
1205
	}
1206
	$replacements = array(
1207
		'SUBJECT' => $subject,
1208
		'MESSAGE' => $message,
1209
		'SENDER' => un_htmlspecialchars($from['name']),
1210
		'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm,
1211
		'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'],
1212
		'TOLIST' => implode(', ', $to_names),
1213
	);
1214
	$email_template = 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : '');
1215
1216
	$notification_texts = array();
1217
1218
	foreach ($notifications as $lang => $notification_list)
1219
	{
1220
		// Censor and parse BBC in the receiver's language. Only do each language once.
1221
		if (empty($notification_texts[$lang]))
1222
		{
1223
			if ($lang != $user_info['language'])
1224
				loadLanguage('index+Modifications', $lang, false);
1225
1226
			$notification_texts[$lang]['subject'] = $subject;
1227
			censorText($notification_texts[$lang]['subject']);
1228
1229
			if (empty($modSettings['disallow_sendBody']))
1230
			{
1231
				$notification_texts[$lang]['body'] = $message;
1232
1233
				censorText($notification_texts[$lang]['body']);
1234
1235
				$notification_texts[$lang]['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($smcFunc['htmlspecialchars']($notification_texts[$lang]['body']), false), array('<br>' => "\n", '</div>' => "\n", '</li>' => "\n", '&#91;' => '[', '&#93;' => ']')))));
1236
			}
1237
			else
1238
				$notification_texts[$lang]['body'] = '';
1239
1240
1241
			if ($lang != $user_info['language'])
1242
				loadLanguage('index+Modifications', $user_info['language'], false);
1243
		}
1244
1245
		$replacements['SUBJECT'] = $notification_texts[$lang]['subject'];
1246
		$replacements['MESSAGE'] = $notification_texts[$lang]['body'];
1247
1248
		$emaildata = loadEmailTemplate($email_template, $replacements, $lang);
1249
1250
		// Off the notification email goes!
1251
		sendmail($notification_list, $emaildata['subject'], $emaildata['body'], null, 'p' . $id_pm, $emaildata['is_html'], 2, null, true);
1252
	}
1253
1254
	// Integrated After PMs
1255
	call_integration_hook('integrate_personal_message_after', array(&$id_pm, &$log, &$recipients, &$from, &$subject, &$message));
1256
1257
	// Back to what we were on before!
1258
	loadLanguage('index+PersonalMessage');
1259
1260
	// Add one to their unread and read message counts.
1261
	foreach ($all_to as $k => $id)
1262
		if (isset($deletes[$id]))
1263
			unset($all_to[$k]);
1264
	if (!empty($all_to))
1265
		updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
1266
1267
	return $log;
1268
}
1269
1270
/**
1271
 * Prepare text strings for sending as email body or header.
1272
 * In case there are higher ASCII characters in the given string, this
1273
 * function will attempt the transport method 'quoted-printable'.
1274
 * Otherwise the transport method '7bit' is used.
1275
 *
1276
 * @param string $string The string
1277
 * @param bool $with_charset Whether we're specifying a charset ($custom_charset must be set here)
1278
 * @param bool $hotmail_fix Whether to apply the hotmail fix  (all higher ASCII characters are converted to HTML entities to assure proper display of the mail)
1279
 * @param string $line_break The linebreak
1280
 * @param string $custom_charset If set, it uses this character set
1281
 * @return array An array containing the character set, the converted string and the transport method.
1282
 */
1283
function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
1284
{
1285
	global $context;
1286
1287
	$charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
1288
1289
	// This is the fun part....
1290
	if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
1291
	{
1292
		// Let's, for now, assume there are only &#021;'ish characters.
1293
		$simple = true;
1294
1295
		foreach ($matches[1] as $entity)
1296
			if ($entity > 128)
1297
				$simple = false;
1298
		unset($matches);
1299
1300
		if ($simple)
1301
			$string = preg_replace_callback(
1302
				'~&#(\d{3,8});~',
1303
				function($m)
1304
				{
1305
					return chr("$m[1]");
0 ignored issues
show
Bug introduced by
$m['1'] of type string is incompatible with the type integer expected by parameter $codepoint of chr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1305
					return chr(/** @scrutinizer ignore-type */ "$m[1]");
Loading history...
1306
				},
1307
				$string
1308
			);
1309
		else
1310
		{
1311
			// Try to convert the string to UTF-8.
1312
			if (!$context['utf8'] && function_exists('iconv'))
1313
			{
1314
				$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1315
				if ($newstring)
1316
					$string = $newstring;
1317
			}
1318
1319
			$string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string);
1320
1321
			// Unicode, baby.
1322
			$charset = 'UTF-8';
1323
		}
1324
	}
1325
1326
	// Convert all special characters to HTML entities...just for Hotmail :-\
1327
	if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
1328
	{
1329
		if (!$context['utf8'] && function_exists('iconv'))
1330
		{
1331
			$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1332
			if ($newstring)
1333
				$string = $newstring;
1334
		}
1335
1336
		$entityConvert = function($m)
1337
		{
1338
			$c = $m[1];
1339
			if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
1340
				return $c;
1341
			elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
1342
				return "&#" . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ";";
1343
			elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
1344
				return "&#" . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ";";
1345
			elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
1346
				return "&#" . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ";";
1347
			else
1348
				return "";
1349
		};
1350
1351
		// Convert all 'special' characters to HTML entities.
1352
		return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', $entityConvert, $string), '7bit');
1353
	}
1354
1355
	// We don't need to mess with the subject line if no special characters were in it..
1356
	elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
1357
	{
1358
		// Base64 encode.
1359
		$string = base64_encode($string);
1360
1361
		// Show the characterset and the transfer-encoding for header strings.
1362
		if ($with_charset)
1363
			$string = '=?' . $charset . '?B?' . $string . '?=';
1364
1365
		// Break it up in lines (mail body).
1366
		else
1367
			$string = chunk_split($string, 76, $line_break);
1368
1369
		return array($charset, $string, 'base64');
1370
	}
1371
1372
	else
1373
		return array($charset, $string, '7bit');
1374
}
1375
1376
/**
1377
 * Sends mail, like mail() but over SMTP.
1378
 * It expects no slashes or entities.
1379
 *
1380
 * @internal
1381
 *
1382
 * @param array $mail_to_array Array of strings (email addresses)
1383
 * @param string $subject Email subject
1384
 * @param string $message Email message
1385
 * @param string $headers Email headers
1386
 * @return boolean Whether it sent or not.
1387
 */
1388
function smtp_mail($mail_to_array, $subject, $message, $headers)
1389
{
1390
	global $modSettings, $webmaster_email, $txt, $boardurl, $sourcedir;
1391
1392
	static $helo;
1393
1394
	$modSettings['smtp_host'] = trim($modSettings['smtp_host']);
1395
1396
	// Try POP3 before SMTP?
1397
	// @todo There's no interface for this yet.
1398
	if ($modSettings['mail_type'] == 3 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1399
	{
1400
		$socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
1401
		if (!$socket && (substr($modSettings['smtp_host'], 0, 5) == 'smtp.' || substr($modSettings['smtp_host'], 0, 11) == 'ssl://smtp.'))
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1402
			$socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
1403
1404
		if ($socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1405
		{
1406
			fgets($socket, 256);
1407
			fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
1408
			fgets($socket, 256);
1409
			fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
1410
			fgets($socket, 256);
1411
			fputs($socket, 'QUIT' . "\r\n");
1412
1413
			fclose($socket);
1414
		}
1415
	}
1416
1417
	// Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
1418
	if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
1419
	{
1420
		// Maybe we can still save this?  The port might be wrong.
1421
		if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
1422
		{
1423
			// ssl:hostname can cause fsocketopen to fail with a lookup failure, ensure it exists for this test.
1424
			if (substr($modSettings['smtp_host'], 0, 6) != 'ssl://')
1425
				$modSettings['smtp_host'] = str_replace('ssl:', 'ss://', $modSettings['smtp_host']);
1426
1427
			if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3))
1428
				log_error($txt['smtp_port_ssl']);
1429
		}
1430
1431
		// Unable to connect!  Don't show any error message, but just log one and try to continue anyway.
1432
		if (!$socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1433
		{
1434
			log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
1435
			return false;
1436
		}
1437
	}
1438
1439
	// Wait for a response of 220, without "-" continuer.
1440
	if (!server_parse(null, $socket, '220'))
1441
		return false;
1442
1443
	// Try to determine the server's fully qualified domain name
1444
	// Can't rely on $_SERVER['SERVER_NAME'] because it can be spoofed on Apache
1445
	if (empty($helo))
1446
	{
1447
		// See if we can get the domain name from the host itself
1448
		if (function_exists('gethostname'))
1449
			$helo = gethostname();
1450
		elseif (function_exists('php_uname'))
1451
			$helo = php_uname('n');
1452
1453
		// If the hostname isn't a fully qualified domain name, we can use the host name from $boardurl instead
1454
		if (empty($helo) || strpos($helo, '.') === false || substr_compare($helo, '.local', -6) === 0 || (!empty($modSettings['tld_regex']) && !preg_match('/\.' . $modSettings['tld_regex'] . '$/u', $helo)))
1455
			$helo = parse_iri($boardurl, PHP_URL_HOST);
1456
1457
		// This is one of those situations where 'www.' is undesirable
1458
		if (strpos($helo, 'www.') === 0)
1459
			$helo = substr($helo, 4);
1460
1461
		if (!function_exists('idn_to_ascii'))
1462
			require_once($sourcedir . '/Subs-Compat.php');
1463
1464
		$helo = idn_to_ascii($helo, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
1465
	}
1466
1467
	// SMTP = 1, SMTP - STARTTLS = 2
1468
	if (in_array($modSettings['mail_type'], array(1, 2)) && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1469
	{
1470
		// EHLO could be understood to mean encrypted hello...
1471
		if (server_parse('EHLO ' . $helo, $socket, null, $response) == '250')
1472
		{
1473
			// Are we using STARTTLS and does the server support STARTTLS?
1474
			if ($modSettings['mail_type'] == 2 && preg_match("~250( |-)STARTTLS~mi", $response))
1475
			{
1476
				// Send STARTTLS to enable encryption
1477
				if (!server_parse('STARTTLS', $socket, '220'))
1478
					return false;
1479
				// Enable the encryption
1480
				// php 5.6+ fix
1481
				$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
1482
1483
				if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT'))
1484
				{
1485
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
1486
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
1487
				}
1488
1489
				if (!@stream_socket_enable_crypto($socket, true, $crypto_method))
1490
					return false;
1491
				// Send the EHLO command again
1492
				if (!server_parse('EHLO ' . $helo, $socket, null) == '250')
1493
					return false;
1494
			}
1495
1496
			if (!server_parse('AUTH LOGIN', $socket, '334'))
1497
				return false;
1498
			// Send the username and password, encoded.
1499
			if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1500
				return false;
1501
			// The password is already encoded ;)
1502
			if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1503
				return false;
1504
		}
1505
		elseif (!server_parse('HELO ' . $helo, $socket, '250'))
1506
			return false;
1507
	}
1508
	else
1509
	{
1510
		// Just say "helo".
1511
		if (!server_parse('HELO ' . $helo, $socket, '250'))
1512
			return false;
1513
	}
1514
1515
	// Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1516
	$message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1517
1518
	// !! Theoretically, we should be able to just loop the RCPT TO.
1519
	$mail_to_array = array_values($mail_to_array);
1520
	foreach ($mail_to_array as $i => $mail_to)
1521
	{
1522
		// Reset the connection to send another email.
1523
		if ($i != 0)
1524
		{
1525
			if (!server_parse('RSET', $socket, '250'))
1526
				return false;
1527
		}
1528
1529
		// From, to, and then start the data...
1530
		if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1531
			return false;
1532
		if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1533
			return false;
1534
		if (!server_parse('DATA', $socket, '354'))
1535
			return false;
1536
		fputs($socket, 'Subject: ' . $subject . "\r\n");
1537
		if (strlen($mail_to) > 0)
1538
			fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1539
		fputs($socket, $headers . "\r\n\r\n");
1540
		fputs($socket, $message . "\r\n");
1541
1542
		// Send a ., or in other words "end of data".
1543
		if (!server_parse('.', $socket, '250'))
1544
			return false;
1545
1546
		// Almost done, almost done... don't stop me just yet!
1547
		@set_time_limit(300);
1548
		if (function_exists('apache_reset_timeout'))
1549
			@apache_reset_timeout();
1550
	}
1551
	fputs($socket, 'QUIT' . "\r\n");
1552
	fclose($socket);
1553
1554
	return true;
1555
}
1556
1557
/**
1558
 * Parse a message to the SMTP server.
1559
 * Sends the specified message to the server, and checks for the
1560
 * expected response.
1561
 *
1562
 * @internal
1563
 *
1564
 * @param string $message The message to send
1565
 * @param resource $socket Socket to send on
1566
 * @param string $code The expected response code
1567
 * @param string $response The response from the SMTP server
1568
 * @return bool Whether it responded as such.
1569
 */
1570
function server_parse($message, $socket, $code, &$response = null)
1571
{
1572
	global $txt;
1573
1574
	if ($message !== null)
0 ignored issues
show
introduced by
The condition $message !== null is always true.
Loading history...
1575
		fputs($socket, $message . "\r\n");
1576
1577
	// No response yet.
1578
	$server_response = '';
1579
1580
	while (substr($server_response, 3, 1) != ' ')
1581
	{
1582
		if (!($server_response = fgets($socket, 256)))
1583
		{
1584
			// @todo Change this message to reflect that it may mean bad user/password/server issues/etc.
1585
			log_error($txt['smtp_bad_response']);
1586
			return false;
1587
		}
1588
		$response .= $server_response;
1589
	}
1590
1591
	if ($code === null)
0 ignored issues
show
introduced by
The condition $code === null is always false.
Loading history...
1592
		return substr($server_response, 0, 3);
1593
1594
	if (substr($server_response, 0, 3) != $code)
1595
	{
1596
		log_error($txt['smtp_error'] . $server_response);
1597
		return false;
1598
	}
1599
1600
	return true;
1601
}
1602
1603
/**
1604
 * Spell checks the post for typos ;).
1605
 * It uses the pspell or enchant library, one of which MUST be installed.
1606
 * It has problems with internationalization.
1607
 * It is accessed via ?action=spellcheck.
1608
 */
1609
function SpellCheck()
1610
{
1611
	global $txt, $context, $smcFunc;
1612
1613
	// A list of "words" we know about but pspell doesn't.
1614
	$known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1615
1616
	loadLanguage('Post');
1617
	loadTemplate('Post');
1618
1619
	// Create a pspell or enchant dictionary resource
1620
	$dict = spell_init();
1621
1622
	if (!isset($_POST['spellstring']) || !$dict)
1623
		die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1624
1625
	// Construct a bit of Javascript code.
1626
	$context['spell_js'] = '
1627
		var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1628
		var mispstr = window.opener.spellCheckGetText(spell_fieldname);
1629
		var misps = Array(';
1630
1631
	// Get all the words (Javascript already separated them).
1632
	$alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1633
1634
	$found_words = false;
1635
	for ($i = 0, $n = count($alphas); $i < $n; $i++)
1636
	{
1637
		// Words are sent like 'word|offset_begin|offset_end'.
1638
		$check_word = explode('|', $alphas[$i]);
1639
1640
		// If the word is a known word, or spelled right...
1641
		if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || spell_check($dict, $check_word[0]) || !isset($check_word[2]))
1642
			continue;
1643
1644
		// Find the word, and move up the "last occurrence" to here.
1645
		$found_words = true;
1646
1647
		// Add on the javascript for this misspelling.
1648
		$context['spell_js'] .= '
1649
			new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '&gt;' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1650
1651
		// If there are suggestions, add them in...
1652
		$suggestions = spell_suggest($dict, $check_word[0]);
1653
		if (!empty($suggestions))
1654
		{
1655
			// But first check they aren't going to be censored - no naughty words!
1656
			foreach ($suggestions as $k => $word)
1657
				if ($suggestions[$k] != censorText($word))
1658
					unset($suggestions[$k]);
1659
1660
			if (!empty($suggestions))
1661
				$context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1662
		}
1663
1664
		$context['spell_js'] .= ']),';
1665
	}
1666
1667
	// If words were found, take off the last comma.
1668
	if ($found_words)
1669
		$context['spell_js'] = substr($context['spell_js'], 0, -1);
1670
1671
	$context['spell_js'] .= '
1672
		);';
1673
1674
	// And instruct the template system to just show the spellcheck sub template.
1675
	$context['template_layers'] = array();
1676
	$context['sub_template'] = 'spellcheck';
1677
1678
	// Free resources for enchant...
1679
	if (isset($context['enchant_broker']))
1680
	{
1681
		enchant_broker_free_dict($dict);
1682
		enchant_broker_free($context['enchant_broker']);
1683
	}
1684
}
1685
1686
/**
1687
 * Sends a notification to members who have elected to receive emails
1688
 * when things happen to a topic, such as replies are posted.
1689
 * The function automatically finds the subject and its board, and
1690
 * checks permissions for each member who is "signed up" for notifications.
1691
 * It will not send 'reply' notifications more than once in a row.
1692
 * Uses Post language file
1693
 *
1694
 * @param array $topics Represents the topics the action is happening to.
1695
 * @param string $type Can be any of reply, sticky, lock, unlock, remove, move, merge, and split.  An appropriate message will be sent for each.
1696
 * @param array $exclude Members in the exclude array will not be processed for the topic with the same key.
1697
 * @param array $members_only Are the only ones that will be sent the notification if they have it on.
1698
 */
1699
function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
1700
{
1701
	global $user_info, $smcFunc;
1702
1703
	// Can't do it if there's no topics.
1704
	if (empty($topics))
1705
		return;
1706
	// It must be an array - it must!
1707
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
1708
		$topics = array($topics);
1709
1710
	// Get the subject and body...
1711
	$result = $smcFunc['db_query']('', '
1712
		SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board,
1713
			COALESCE(mem.real_name, ml.poster_name) AS poster_name, mf.id_msg
1714
		FROM {db_prefix}topics AS t
1715
			INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1716
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1717
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1718
		WHERE t.id_topic IN ({array_int:topic_list})
1719
		LIMIT 1',
1720
		array(
1721
			'topic_list' => $topics,
1722
		)
1723
	);
1724
	$task_rows = array();
1725
	while ($row = $smcFunc['db_fetch_assoc']($result))
1726
	{
1727
		$task_rows[] = array(
1728
			'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
1729
				'msgOptions' => array(
1730
					'id' => $row['id_msg'],
1731
					'subject' => $row['subject'],
1732
					'body' => $row['body'],
1733
				),
1734
				'topicOptions' => array(
1735
					'id' => $row['id_topic'],
1736
					'board' => $row['id_board'],
1737
				),
1738
				// Kinda cheeky, but for any action the originator is usually the current user
1739
				'posterOptions' => array(
1740
					'id' => $user_info['id'],
1741
					'name' => $user_info['name'],
1742
				),
1743
				'type' => $type,
1744
				'members_only' => $members_only,
1745
			)), 0
1746
		);
1747
	}
1748
	$smcFunc['db_free_result']($result);
1749
1750
	if (!empty($task_rows))
1751
		$smcFunc['db_insert']('',
1752
			'{db_prefix}background_tasks',
1753
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1754
			$task_rows,
1755
			array('id_task')
1756
		);
1757
}
1758
1759
/**
1760
 * Create a post, either as new topic (id_topic = 0) or in an existing one.
1761
 * The input parameters of this function assume:
1762
 * - Strings have been escaped.
1763
 * - Integers have been cast to integer.
1764
 * - Mandatory parameters are set.
1765
 *
1766
 * @param array $msgOptions An array of information/options for the post
1767
 * @param array $topicOptions An array of information/options for the topic
1768
 * @param array $posterOptions An array of information/options for the poster
1769
 * @return bool Whether the operation was a success
1770
 */
1771
function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1772
{
1773
	global $user_info, $txt, $modSettings, $smcFunc, $sourcedir;
1774
1775
	require_once($sourcedir . '/Mentions.php');
1776
1777
	// Set optional parameters to the default value.
1778
	$msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1779
	$msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1780
	$msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1781
	$msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1782
	$msgOptions['poster_time'] = isset($msgOptions['poster_time']) ? (int) $msgOptions['poster_time'] : time();
1783
	$topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1784
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1785
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1786
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1787
	$topicOptions['redirect_expires'] = isset($topicOptions['redirect_expires']) ? $topicOptions['redirect_expires'] : null;
1788
	$topicOptions['redirect_topic'] = isset($topicOptions['redirect_topic']) ? $topicOptions['redirect_topic'] : null;
1789
	$posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1790
	$posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1791
1792
	// Not exactly a post option but it allows hooks and/or other sources to skip sending notifications if they don't want to
1793
	$msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true;
1794
1795
	// We need to know if the topic is approved. If we're told that's great - if not find out.
1796
	if (!$modSettings['postmod_active'])
1797
		$topicOptions['is_approved'] = true;
1798
	elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1799
	{
1800
		$request = $smcFunc['db_query']('', '
1801
			SELECT approved
1802
			FROM {db_prefix}topics
1803
			WHERE id_topic = {int:id_topic}
1804
			LIMIT 1',
1805
			array(
1806
				'id_topic' => $topicOptions['id'],
1807
			)
1808
		);
1809
		list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1810
		$smcFunc['db_free_result']($request);
1811
	}
1812
1813
	// If nothing was filled in as name/e-mail address, try the member table.
1814
	if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1815
	{
1816
		if (empty($posterOptions['id']))
1817
		{
1818
			$posterOptions['id'] = 0;
1819
			$posterOptions['name'] = $txt['guest_title'];
1820
			$posterOptions['email'] = '';
1821
		}
1822
		elseif ($posterOptions['id'] != $user_info['id'])
1823
		{
1824
			$request = $smcFunc['db_query']('', '
1825
				SELECT member_name, email_address
1826
				FROM {db_prefix}members
1827
				WHERE id_member = {int:id_member}
1828
				LIMIT 1',
1829
				array(
1830
					'id_member' => $posterOptions['id'],
1831
				)
1832
			);
1833
			// Couldn't find the current poster?
1834
			if ($smcFunc['db_num_rows']($request) == 0)
1835
			{
1836
				loadLanguage('Errors');
1837
				trigger_error(sprintf($txt['create_post_invalid_member_id'], $posterOptions['id']), E_USER_NOTICE);
1838
				$posterOptions['id'] = 0;
1839
				$posterOptions['name'] = $txt['guest_title'];
1840
				$posterOptions['email'] = '';
1841
			}
1842
			else
1843
				list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1844
			$smcFunc['db_free_result']($request);
1845
		}
1846
		else
1847
		{
1848
			$posterOptions['name'] = $user_info['name'];
1849
			$posterOptions['email'] = $user_info['email'];
1850
		}
1851
	}
1852
1853
	// Get any members who were quoted in this post.
1854
	$msgOptions['quoted_members'] = Mentions::getQuotedMembers($msgOptions['body'], $posterOptions['id']);
1855
1856
	if (!empty($modSettings['enable_mentions']))
1857
	{
1858
		// Get any members who were possibly mentioned
1859
		$msgOptions['mentioned_members'] = Mentions::getMentionedMembers($msgOptions['body']);
1860
		if (!empty($msgOptions['mentioned_members']))
1861
		{
1862
			// Replace @name with [member=id]name[/member]
1863
			$msgOptions['body'] = Mentions::getBody($msgOptions['body'], $msgOptions['mentioned_members']);
1864
1865
			// Remove any members who weren't actually mentioned, to prevent bogus notifications
1866
			$msgOptions['mentioned_members'] = Mentions::verifyMentionedMembers($msgOptions['body'], $msgOptions['mentioned_members']);
1867
		}
1868
	}
1869
1870
	// It's do or die time: forget any user aborts!
1871
	$previous_ignore_user_abort = ignore_user_abort(true);
1872
1873
	$new_topic = empty($topicOptions['id']);
1874
1875
	$message_columns = array(
1876
		'id_board' => 'int', 'id_topic' => 'int', 'id_member' => 'int', 'subject' => 'string-255', 'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : (empty($modSettings['max_messageLength']) ? 'string' : 'string-65534')),
1877
		'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'inet',
1878
		'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1879
	);
1880
1881
	$message_parameters = array(
1882
		$topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1883
		$posterOptions['name'], $posterOptions['email'], $msgOptions['poster_time'], $posterOptions['ip'],
1884
		$msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1885
	);
1886
1887
	// What if we want to do anything with posts?
1888
	call_integration_hook('integrate_create_post', array(&$msgOptions, &$topicOptions, &$posterOptions, &$message_columns, &$message_parameters));
1889
1890
	// Insert the post.
1891
	$msgOptions['id'] = $smcFunc['db_insert']('',
1892
		'{db_prefix}messages',
1893
		$message_columns,
1894
		$message_parameters,
1895
		array('id_msg'),
1896
		1
1897
	);
1898
1899
	// Something went wrong creating the message...
1900
	if (empty($msgOptions['id']))
1901
		return false;
1902
1903
	// Fix the attachments.
1904
	if (!empty($msgOptions['attachments']))
1905
		$smcFunc['db_query']('', '
1906
			UPDATE {db_prefix}attachments
1907
			SET id_msg = {int:id_msg}
1908
			WHERE id_attach IN ({array_int:attachment_list})',
1909
			array(
1910
				'attachment_list' => $msgOptions['attachments'],
1911
				'id_msg' => $msgOptions['id'],
1912
			)
1913
		);
1914
1915
	// What if we want to export new posts out to a CMS?
1916
	call_integration_hook('integrate_after_create_post', array($msgOptions, $topicOptions, $posterOptions, $message_columns, $message_parameters));
1917
1918
	// Insert a new topic (if the topicID was left empty.)
1919
	if ($new_topic)
1920
	{
1921
		$topic_columns = array(
1922
			'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1923
			'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1924
			'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1925
			'redirect_expires' => 'int', 'id_redirect_topic' => 'int',
1926
		);
1927
		$topic_parameters = array(
1928
			$topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1929
			$msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1930
			$topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1931
			$topicOptions['redirect_expires'] === null ? 0 : $topicOptions['redirect_expires'], $topicOptions['redirect_topic'] === null ? 0 : $topicOptions['redirect_topic'],
1932
		);
1933
1934
		call_integration_hook('integrate_before_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions, &$topic_columns, &$topic_parameters));
1935
1936
		$topicOptions['id'] = $smcFunc['db_insert']('',
1937
			'{db_prefix}topics',
1938
			$topic_columns,
1939
			$topic_parameters,
1940
			array('id_topic'),
1941
			1
1942
		);
1943
1944
		// The topic couldn't be created for some reason.
1945
		if (empty($topicOptions['id']))
1946
		{
1947
			// We should delete the post that did work, though...
1948
			$smcFunc['db_query']('', '
1949
				DELETE FROM {db_prefix}messages
1950
				WHERE id_msg = {int:id_msg}',
1951
				array(
1952
					'id_msg' => $msgOptions['id'],
1953
				)
1954
			);
1955
1956
			return false;
1957
		}
1958
1959
		// Fix the message with the topic.
1960
		$smcFunc['db_query']('', '
1961
			UPDATE {db_prefix}messages
1962
			SET id_topic = {int:id_topic}
1963
			WHERE id_msg = {int:id_msg}',
1964
			array(
1965
				'id_topic' => $topicOptions['id'],
1966
				'id_msg' => $msgOptions['id'],
1967
			)
1968
		);
1969
1970
		// There's been a new topic AND a new post today.
1971
		trackStats(array('topics' => '+', 'posts' => '+'));
1972
1973
		updateStats('topic', true);
1974
		updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1975
1976
		// What if we want to export new topics out to a CMS?
1977
		call_integration_hook('integrate_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions));
1978
	}
1979
	// The topic already exists, it only needs a little updating.
1980
	else
1981
	{
1982
		$update_parameters = array(
1983
			'poster_id' => $posterOptions['id'],
1984
			'id_msg' => $msgOptions['id'],
1985
			'locked' => $topicOptions['lock_mode'],
1986
			'is_sticky' => $topicOptions['sticky_mode'],
1987
			'id_topic' => $topicOptions['id'],
1988
			'counter_increment' => 1,
1989
		);
1990
		if ($msgOptions['approved'])
1991
			$topics_columns = array(
1992
				'id_member_updated = {int:poster_id}',
1993
				'id_last_msg = {int:id_msg}',
1994
				'num_replies = num_replies + {int:counter_increment}',
1995
			);
1996
		else
1997
			$topics_columns = array(
1998
				'unapproved_posts = unapproved_posts + {int:counter_increment}',
1999
			);
2000
		if ($topicOptions['lock_mode'] !== null)
2001
			$topics_columns[] = 'locked = {int:locked}';
2002
		if ($topicOptions['sticky_mode'] !== null)
2003
			$topics_columns[] = 'is_sticky = {int:is_sticky}';
2004
2005
		call_integration_hook('integrate_modify_topic', array(&$topics_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions));
2006
2007
		// Update the number of replies and the lock/sticky status.
2008
		$smcFunc['db_query']('', '
2009
			UPDATE {db_prefix}topics
2010
			SET
2011
				' . implode(', ', $topics_columns) . '
2012
			WHERE id_topic = {int:id_topic}',
2013
			$update_parameters
2014
		);
2015
2016
		// One new post has been added today.
2017
		trackStats(array('posts' => '+'));
2018
	}
2019
2020
	// Creating is modifying...in a way.
2021
	// @todo Why not set id_msg_modified on the insert?
2022
	$smcFunc['db_query']('', '
2023
		UPDATE {db_prefix}messages
2024
		SET id_msg_modified = {int:id_msg}
2025
		WHERE id_msg = {int:id_msg}',
2026
		array(
2027
			'id_msg' => $msgOptions['id'],
2028
		)
2029
	);
2030
2031
	// Increase the number of posts and topics on the board.
2032
	if ($msgOptions['approved'])
2033
		$smcFunc['db_query']('', '
2034
			UPDATE {db_prefix}boards
2035
			SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
2036
			WHERE id_board = {int:id_board}',
2037
			array(
2038
				'id_board' => $topicOptions['board'],
2039
			)
2040
		);
2041
	else
2042
	{
2043
		$smcFunc['db_query']('', '
2044
			UPDATE {db_prefix}boards
2045
			SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
2046
			WHERE id_board = {int:id_board}',
2047
			array(
2048
				'id_board' => $topicOptions['board'],
2049
			)
2050
		);
2051
2052
		// Add to the approval queue too.
2053
		$smcFunc['db_insert']('',
2054
			'{db_prefix}approval_queue',
2055
			array(
2056
				'id_msg' => 'int',
2057
			),
2058
			array(
2059
				$msgOptions['id'],
2060
			),
2061
			array()
2062
		);
2063
2064
		$smcFunc['db_insert']('',
2065
			'{db_prefix}background_tasks',
2066
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2067
			array(
2068
				'$sourcedir/tasks/ApprovePost-Notify.php', 'ApprovePost_Notify_Background', $smcFunc['json_encode'](array(
2069
					'msgOptions' => $msgOptions,
2070
					'topicOptions' => $topicOptions,
2071
					'posterOptions' => $posterOptions,
2072
					'type' => $new_topic ? 'topic' : 'post',
2073
				)), 0
2074
			),
2075
			array('id_task')
2076
		);
2077
	}
2078
2079
	// Mark inserted topic as read (only for the user calling this function).
2080
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2081
	{
2082
		// Since it's likely they *read* it before replying, let's try an UPDATE first.
2083
		if (!$new_topic)
2084
		{
2085
			$smcFunc['db_query']('', '
2086
				UPDATE {db_prefix}log_topics
2087
				SET id_msg = {int:id_msg}
2088
				WHERE id_member = {int:current_member}
2089
					AND id_topic = {int:id_topic}',
2090
				array(
2091
					'current_member' => $posterOptions['id'],
2092
					'id_msg' => $msgOptions['id'],
2093
					'id_topic' => $topicOptions['id'],
2094
				)
2095
			);
2096
2097
			$flag = $smcFunc['db_affected_rows']() != 0;
2098
		}
2099
2100
		if (empty($flag))
2101
		{
2102
			$smcFunc['db_insert']('ignore',
2103
				'{db_prefix}log_topics',
2104
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2105
				array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
2106
				array('id_topic', 'id_member')
2107
			);
2108
		}
2109
	}
2110
2111
	if ($msgOptions['approved'] && empty($topicOptions['is_approved']) && $posterOptions['id'] != $user_info['id'])
2112
		$smcFunc['db_insert']('',
2113
			'{db_prefix}background_tasks',
2114
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2115
			array(
2116
				'$sourcedir/tasks/ApproveReply-Notify.php', 'ApproveReply_Notify_Background', $smcFunc['json_encode'](array(
2117
					'msgOptions' => $msgOptions,
2118
					'topicOptions' => $topicOptions,
2119
					'posterOptions' => $posterOptions,
2120
				)), 0
2121
			),
2122
			array('id_task')
2123
		);
2124
2125
	// If there's a custom search index, it may need updating...
2126
	require_once($sourcedir . '/Search.php');
2127
	$searchAPI = findSearchAPI();
2128
	if (is_callable(array($searchAPI, 'postCreated')))
2129
		$searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
2130
2131
	// Increase the post counter for the user that created the post.
2132
	if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
2133
	{
2134
		// Are you the one that happened to create this post?
2135
		if ($user_info['id'] == $posterOptions['id'])
2136
			$user_info['posts']++;
2137
		updateMemberData($posterOptions['id'], array('posts' => '+'));
2138
	}
2139
2140
	// They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2141
	$_SESSION['last_read_topic'] = 0;
2142
2143
	// Better safe than sorry.
2144
	if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2145
		$_SESSION['topicseen_cache'][$topicOptions['board']]--;
2146
2147
	// Keep track of quotes and mentions.
2148
	if (!empty($msgOptions['quoted_members']))
2149
		Mentions::insertMentions('quote', $msgOptions['id'], $msgOptions['quoted_members'], $posterOptions['id']);
2150
	if (!empty($msgOptions['mentioned_members']))
2151
		Mentions::insertMentions('msg', $msgOptions['id'], $msgOptions['mentioned_members'], $posterOptions['id']);
2152
2153
	// Update all the stats so everyone knows about this new topic and message.
2154
	updateStats('message', true, $msgOptions['id']);
2155
2156
	// Update the last message on the board assuming it's approved AND the topic is.
2157
	if ($msgOptions['approved'])
2158
		updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2159
2160
	// Queue createPost background notification
2161
	if ($msgOptions['send_notifications'] && $msgOptions['approved'])
2162
		$smcFunc['db_insert']('',
2163
			'{db_prefix}background_tasks',
2164
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2165
			array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2166
				'msgOptions' => $msgOptions,
2167
				'topicOptions' => $topicOptions,
2168
				'posterOptions' => $posterOptions,
2169
				'type' => $new_topic ? 'topic' : 'reply',
2170
			)), 0),
2171
			array('id_task')
2172
		);
2173
2174
	// Alright, done now... we can abort now, I guess... at least this much is done.
2175
	ignore_user_abort($previous_ignore_user_abort);
0 ignored issues
show
Bug introduced by
$previous_ignore_user_abort of type integer is incompatible with the type boolean|null expected by parameter $enable of ignore_user_abort(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2175
	ignore_user_abort(/** @scrutinizer ignore-type */ $previous_ignore_user_abort);
Loading history...
2176
2177
	// Success.
2178
	return true;
2179
}
2180
2181
/**
2182
 * Modifying a post...
2183
 *
2184
 * @param array &$msgOptions An array of information/options for the post
2185
 * @param array &$topicOptions An array of information/options for the topic
2186
 * @param array &$posterOptions An array of information/options for the poster
2187
 * @return bool Whether the post was modified successfully
2188
 */
2189
function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions)
2190
{
2191
	global $user_info, $modSettings, $smcFunc, $sourcedir;
2192
2193
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
2194
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
2195
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
2196
2197
	// This is longer than it has to be, but makes it so we only set/change what we have to.
2198
	$messages_columns = array();
2199
	if (isset($posterOptions['name']))
2200
		$messages_columns['poster_name'] = $posterOptions['name'];
2201
	if (isset($posterOptions['email']))
2202
		$messages_columns['poster_email'] = $posterOptions['email'];
2203
	if (isset($msgOptions['icon']))
2204
		$messages_columns['icon'] = $msgOptions['icon'];
2205
	if (isset($msgOptions['subject']))
2206
		$messages_columns['subject'] = $msgOptions['subject'];
2207
	if (isset($msgOptions['body']))
2208
	{
2209
		$messages_columns['body'] = $msgOptions['body'];
2210
2211
		// using a custom search index, then lets get the old message so we can update our index as needed
2212
		if (!empty($modSettings['search_custom_index_config']))
2213
		{
2214
			$request = $smcFunc['db_query']('', '
2215
				SELECT body
2216
				FROM {db_prefix}messages
2217
				WHERE id_msg = {int:id_msg}',
2218
				array(
2219
					'id_msg' => $msgOptions['id'],
2220
				)
2221
			);
2222
			list ($msgOptions['old_body']) = $smcFunc['db_fetch_row']($request);
2223
			$smcFunc['db_free_result']($request);
2224
		}
2225
	}
2226
	if (!empty($msgOptions['modify_time']))
2227
	{
2228
		$messages_columns['modified_time'] = $msgOptions['modify_time'];
2229
		$messages_columns['modified_name'] = $msgOptions['modify_name'];
2230
		$messages_columns['modified_reason'] = $msgOptions['modify_reason'];
2231
		$messages_columns['id_msg_modified'] = $modSettings['maxMsgID'];
2232
	}
2233
	if (isset($msgOptions['smileys_enabled']))
2234
		$messages_columns['smileys_enabled'] = empty($msgOptions['smileys_enabled']) ? 0 : 1;
2235
2236
	// Which columns need to be ints?
2237
	$messageInts = array('modified_time', 'id_msg_modified', 'smileys_enabled');
2238
	$update_parameters = array(
2239
		'id_msg' => $msgOptions['id'],
2240
	);
2241
2242
	// Update search api
2243
	require_once($sourcedir . '/Search.php');
2244
	$searchAPI = findSearchAPI();
2245
	if ($searchAPI->supportsMethod('postRemoved'))
2246
		$searchAPI->postRemoved($msgOptions['id']);
2247
2248
	// Anyone quoted or mentioned?
2249
	require_once($sourcedir . '/Mentions.php');
2250
2251
	$quoted_members = Mentions::getQuotedMembers($msgOptions['body'], $posterOptions['id']);
2252
	$quoted_modifications = Mentions::modifyMentions('quote', $msgOptions['id'], $quoted_members, $posterOptions['id']);
2253
2254
	if (!empty($quoted_modifications['added']))
2255
	{
2256
		$msgOptions['quoted_members'] = array_intersect_key($quoted_members, array_flip(array_keys($quoted_modifications['added'])));
2257
2258
		// You don't need a notification about quoting yourself.
2259
		unset($msgOptions['quoted_members'][$user_info['id']]);
2260
	}
2261
2262
	if (!empty($modSettings['enable_mentions']) && isset($msgOptions['body']))
2263
	{
2264
		$mentions = Mentions::getMentionedMembers($msgOptions['body']);
2265
		$messages_columns['body'] = $msgOptions['body'] = Mentions::getBody($msgOptions['body'], $mentions);
2266
		$mentions = Mentions::verifyMentionedMembers($msgOptions['body'], $mentions);
2267
2268
		// Update our records in the database.
2269
		$mention_modifications = Mentions::modifyMentions('msg', $msgOptions['id'], $mentions, $posterOptions['id']);
2270
2271
		if (!empty($mention_modifications['added']))
2272
		{
2273
			// Queue this for notification.
2274
			$msgOptions['mentioned_members'] = array_intersect_key($mentions, array_flip(array_keys($mention_modifications['added'])));
2275
2276
			// Mentioning yourself is silly, and we aren't going to notify you about it.
2277
			unset($msgOptions['mentioned_members'][$user_info['id']]);
2278
		}
2279
	}
2280
2281
	// This allows mods to skip sending notifications if they don't want to.
2282
	$msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true;
2283
2284
	// Maybe a mod wants to make some changes?
2285
	call_integration_hook('integrate_modify_post', array(&$messages_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions, &$messageInts));
2286
2287
	foreach ($messages_columns as $var => $val)
2288
	{
2289
		$messages_columns[$var] = $var . ' = {' . (in_array($var, $messageInts) ? 'int' : 'string') . ':var_' . $var . '}';
2290
		$update_parameters['var_' . $var] = $val;
2291
	}
2292
2293
	// Nothing to do?
2294
	if (empty($messages_columns))
2295
		return true;
2296
2297
	// Change the post.
2298
	$smcFunc['db_query']('', '
2299
		UPDATE {db_prefix}messages
2300
		SET ' . implode(', ', $messages_columns) . '
2301
		WHERE id_msg = {int:id_msg}',
2302
		$update_parameters
2303
	);
2304
2305
	// Lock and or sticky the post.
2306
	if ($topicOptions['sticky_mode'] !== null || $topicOptions['lock_mode'] !== null || $topicOptions['poll'] !== null)
2307
	{
2308
		$smcFunc['db_query']('', '
2309
			UPDATE {db_prefix}topics
2310
			SET
2311
				is_sticky = {raw:is_sticky},
2312
				locked = {raw:locked},
2313
				id_poll = {raw:id_poll}
2314
			WHERE id_topic = {int:id_topic}',
2315
			array(
2316
				'is_sticky' => $topicOptions['sticky_mode'] === null ? 'is_sticky' : (int) $topicOptions['sticky_mode'],
2317
				'locked' => $topicOptions['lock_mode'] === null ? 'locked' : (int) $topicOptions['lock_mode'],
2318
				'id_poll' => $topicOptions['poll'] === null ? 'id_poll' : (int) $topicOptions['poll'],
2319
				'id_topic' => $topicOptions['id'],
2320
			)
2321
		);
2322
	}
2323
2324
	// Mark the edited post as read.
2325
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2326
	{
2327
		// Since it's likely they *read* it before editing, let's try an UPDATE first.
2328
		$smcFunc['db_query']('', '
2329
			UPDATE {db_prefix}log_topics
2330
			SET id_msg = {int:id_msg}
2331
			WHERE id_member = {int:current_member}
2332
				AND id_topic = {int:id_topic}',
2333
			array(
2334
				'current_member' => $user_info['id'],
2335
				'id_msg' => $modSettings['maxMsgID'],
2336
				'id_topic' => $topicOptions['id'],
2337
			)
2338
		);
2339
2340
		$flag = $smcFunc['db_affected_rows']() != 0;
2341
2342
		if (empty($flag))
2343
		{
2344
			$smcFunc['db_insert']('ignore',
2345
				'{db_prefix}log_topics',
2346
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2347
				array($topicOptions['id'], $user_info['id'], $modSettings['maxMsgID']),
2348
				array('id_topic', 'id_member')
2349
			);
2350
		}
2351
	}
2352
2353
	// If there's a custom search index, it needs to be modified...
2354
	require_once($sourcedir . '/Search.php');
2355
	$searchAPI = findSearchAPI();
2356
	if (is_callable(array($searchAPI, 'postModified')))
2357
		$searchAPI->postModified($msgOptions, $topicOptions, $posterOptions);
2358
2359
	// Send notifications about any new quotes or mentions.
2360
	if ($msgOptions['send_notifications'] && !empty($msgOptions['approved']) && (!empty($msgOptions['quoted_members']) || !empty($msgOptions['mentioned_members'])))
2361
	{
2362
		$smcFunc['db_insert']('',
2363
			'{db_prefix}background_tasks',
2364
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2365
			array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2366
				'msgOptions' => $msgOptions,
2367
				'topicOptions' => $topicOptions,
2368
				'posterOptions' => $posterOptions,
2369
				'type' => 'edit',
2370
			)), 0),
2371
			array('id_task')
2372
		);
2373
	}
2374
2375
	if (isset($msgOptions['subject']))
2376
	{
2377
		// Only update the subject if this was the first message in the topic.
2378
		$request = $smcFunc['db_query']('', '
2379
			SELECT id_topic
2380
			FROM {db_prefix}topics
2381
			WHERE id_first_msg = {int:id_first_msg}
2382
			LIMIT 1',
2383
			array(
2384
				'id_first_msg' => $msgOptions['id'],
2385
			)
2386
		);
2387
		if ($smcFunc['db_num_rows']($request) == 1)
2388
			updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
2389
		$smcFunc['db_free_result']($request);
2390
	}
2391
2392
	// Finally, if we are setting the approved state we need to do much more work :(
2393
	if ($modSettings['postmod_active'] && isset($msgOptions['approved']))
2394
		approvePosts($msgOptions['id'], $msgOptions['approved']);
2395
2396
	return true;
2397
}
2398
2399
/**
2400
 * Approve (or not) some posts... without permission checks...
2401
 *
2402
 * @param array $msgs Array of message ids
2403
 * @param bool $approve Whether to approve the posts (if false, posts are unapproved)
2404
 * @param bool $notify Whether to notify users
2405
 * @return bool Whether the operation was successful
2406
 */
2407
function approvePosts($msgs, $approve = true, $notify = true)
2408
{
2409
	global $smcFunc;
2410
2411
	if (!is_array($msgs))
0 ignored issues
show
introduced by
The condition is_array($msgs) is always true.
Loading history...
2412
		$msgs = array($msgs);
2413
2414
	if (empty($msgs))
2415
		return false;
2416
2417
	// May as well start at the beginning, working out *what* we need to change.
2418
	$request = $smcFunc['db_query']('', '
2419
		SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg,
2420
			m.body, m.subject, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.id_member,
2421
			t.approved AS topic_approved, b.count_posts
2422
		FROM {db_prefix}messages AS m
2423
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
2424
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
2425
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2426
		WHERE m.id_msg IN ({array_int:message_list})
2427
			AND m.approved = {int:approved_state}',
2428
		array(
2429
			'message_list' => $msgs,
2430
			'approved_state' => $approve ? 0 : 1,
2431
		)
2432
	);
2433
	$msgs = array();
2434
	$topics = array();
2435
	$topic_changes = array();
2436
	$board_changes = array();
2437
	$notification_topics = array();
2438
	$notification_posts = array();
2439
	$member_post_changes = array();
2440
	while ($row = $smcFunc['db_fetch_assoc']($request))
2441
	{
2442
		// Easy...
2443
		$msgs[] = $row['id_msg'];
2444
		$topics[] = $row['id_topic'];
2445
2446
		// Ensure our change array exists already.
2447
		if (!isset($topic_changes[$row['id_topic']]))
2448
			$topic_changes[$row['id_topic']] = array(
2449
				'id_last_msg' => $row['id_last_msg'],
2450
				'approved' => $row['topic_approved'],
2451
				'replies' => 0,
2452
				'unapproved_posts' => 0,
2453
			);
2454
		if (!isset($board_changes[$row['id_board']]))
2455
			$board_changes[$row['id_board']] = array(
2456
				'posts' => 0,
2457
				'topics' => 0,
2458
				'unapproved_posts' => 0,
2459
				'unapproved_topics' => 0,
2460
			);
2461
2462
		// If it's the first message then the topic state changes!
2463
		if ($row['id_msg'] == $row['id_first_msg'])
2464
		{
2465
			$topic_changes[$row['id_topic']]['approved'] = $approve ? 1 : 0;
2466
2467
			$board_changes[$row['id_board']]['unapproved_topics'] += $approve ? -1 : 1;
2468
			$board_changes[$row['id_board']]['topics'] += $approve ? 1 : -1;
2469
2470
			// Note we need to ensure we announce this topic!
2471
			$notification_topics[] = array(
2472
				'body' => $row['body'],
2473
				'subject' => $row['subject'],
2474
				'name' => $row['poster_name'],
2475
				'board' => $row['id_board'],
2476
				'topic' => $row['id_topic'],
2477
				'msg' => $row['id_first_msg'],
2478
				'poster' => $row['id_member'],
2479
				'new_topic' => true,
2480
			);
2481
		}
2482
		else
2483
		{
2484
			$topic_changes[$row['id_topic']]['replies'] += $approve ? 1 : -1;
2485
2486
			// This will be a post... but don't notify unless it's not followed by approved ones.
2487
			if ($row['id_msg'] > $row['id_last_msg'])
2488
				$notification_posts[$row['id_topic']] = array(
2489
					'id' => $row['id_msg'],
2490
					'body' => $row['body'],
2491
					'subject' => $row['subject'],
2492
					'name' => $row['poster_name'],
2493
					'topic' => $row['id_topic'],
2494
					'board' => $row['id_board'],
2495
					'poster' => $row['id_member'],
2496
					'new_topic' => false,
2497
					'msg' => $row['id_msg'],
2498
				);
2499
		}
2500
2501
		// If this is being approved and id_msg is higher than the current id_last_msg then it changes.
2502
		if ($approve && $row['id_msg'] > $topic_changes[$row['id_topic']]['id_last_msg'])
2503
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_msg'];
2504
		// If this is being unapproved, and it's equal to the id_last_msg we need to find a new one!
2505
		elseif (!$approve)
2506
			// Default to the first message and then we'll override in a bit ;)
2507
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_first_msg'];
2508
2509
		$topic_changes[$row['id_topic']]['unapproved_posts'] += $approve ? -1 : 1;
2510
		$board_changes[$row['id_board']]['unapproved_posts'] += $approve ? -1 : 1;
2511
		$board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1;
2512
2513
		// Post count for the user?
2514
		if ($row['id_member'] && empty($row['count_posts']))
2515
			$member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1;
2516
	}
2517
	$smcFunc['db_free_result']($request);
2518
2519
	if (empty($msgs))
2520
		return;
2521
2522
	// Now we have the differences make the changes, first the easy one.
2523
	$smcFunc['db_query']('', '
2524
		UPDATE {db_prefix}messages
2525
		SET approved = {int:approved_state}
2526
		WHERE id_msg IN ({array_int:message_list})',
2527
		array(
2528
			'message_list' => $msgs,
2529
			'approved_state' => $approve ? 1 : 0,
2530
		)
2531
	);
2532
2533
	// If we were unapproving find the last msg in the topics...
2534
	if (!$approve)
2535
	{
2536
		$request = $smcFunc['db_query']('', '
2537
			SELECT id_topic, MAX(id_msg) AS id_last_msg
2538
			FROM {db_prefix}messages
2539
			WHERE id_topic IN ({array_int:topic_list})
2540
				AND approved = {int:approved}
2541
			GROUP BY id_topic',
2542
			array(
2543
				'topic_list' => $topics,
2544
				'approved' => 1,
2545
			)
2546
		);
2547
		while ($row = $smcFunc['db_fetch_assoc']($request))
2548
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_last_msg'];
2549
		$smcFunc['db_free_result']($request);
2550
	}
2551
2552
	// ... next the topics...
2553
	foreach ($topic_changes as $id => $changes)
2554
		$smcFunc['db_query']('', '
2555
			UPDATE {db_prefix}topics
2556
			SET approved = {int:approved}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2557
				num_replies = num_replies + {int:num_replies}, id_last_msg = {int:id_last_msg}
2558
			WHERE id_topic = {int:id_topic}',
2559
			array(
2560
				'approved' => $changes['approved'],
2561
				'unapproved_posts' => $changes['unapproved_posts'],
2562
				'num_replies' => $changes['replies'],
2563
				'id_last_msg' => $changes['id_last_msg'],
2564
				'id_topic' => $id,
2565
			)
2566
		);
2567
2568
	// ... finally the boards...
2569
	foreach ($board_changes as $id => $changes)
2570
		$smcFunc['db_query']('', '
2571
			UPDATE {db_prefix}boards
2572
			SET num_posts = num_posts + {int:num_posts}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2573
				num_topics = num_topics + {int:num_topics}, unapproved_topics = unapproved_topics + {int:unapproved_topics}
2574
			WHERE id_board = {int:id_board}',
2575
			array(
2576
				'num_posts' => $changes['posts'],
2577
				'unapproved_posts' => $changes['unapproved_posts'],
2578
				'num_topics' => $changes['topics'],
2579
				'unapproved_topics' => $changes['unapproved_topics'],
2580
				'id_board' => $id,
2581
			)
2582
		);
2583
2584
	// Finally, least importantly, notifications!
2585
	if ($approve)
2586
	{
2587
		$task_rows = array();
2588
		foreach (array_merge($notification_topics, $notification_posts) as $topic)
2589
			$task_rows[] = array(
2590
				'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2591
					'msgOptions' => array(
2592
						'id' => $topic['msg'],
2593
						'body' => $topic['body'],
2594
						'subject' => $topic['subject'],
2595
					),
2596
					'topicOptions' => array(
2597
						'id' => $topic['topic'],
2598
						'board' => $topic['board'],
2599
					),
2600
					'posterOptions' => array(
2601
						'id' => $topic['poster'],
2602
						'name' => $topic['name'],
2603
					),
2604
					'type' => $topic['new_topic'] ? 'topic' : 'reply',
2605
				)), 0
2606
			);
2607
2608
		if ($notify)
2609
			$smcFunc['db_insert']('',
2610
				'{db_prefix}background_tasks',
2611
				array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2612
				$task_rows,
2613
				array('id_task')
2614
			);
2615
2616
		$smcFunc['db_query']('', '
2617
			DELETE FROM {db_prefix}approval_queue
2618
			WHERE id_msg IN ({array_int:message_list})
2619
				AND id_attach = {int:id_attach}',
2620
			array(
2621
				'message_list' => $msgs,
2622
				'id_attach' => 0,
2623
			)
2624
		);
2625
	}
2626
	// If unapproving add to the approval queue!
2627
	else
2628
	{
2629
		$msgInserts = array();
2630
		foreach ($msgs as $msg)
2631
			$msgInserts[] = array($msg);
2632
2633
		$smcFunc['db_insert']('ignore',
2634
			'{db_prefix}approval_queue',
2635
			array('id_msg' => 'int'),
2636
			$msgInserts,
2637
			array('id_msg')
2638
		);
2639
	}
2640
2641
	// Update the last messages on the boards...
2642
	updateLastMessages(array_keys($board_changes));
2643
2644
	// Post count for the members?
2645
	if (!empty($member_post_changes))
2646
		foreach ($member_post_changes as $id_member => $count_change)
2647
			updateMemberData($id_member, array('posts' => 'posts ' . ($approve ? '+' : '-') . ' ' . $count_change));
2648
2649
	// In case an external CMS needs to know about this approval/unapproval.
2650
	call_integration_hook('integrate_after_approve_posts', array($approve, $msgs, $topic_changes, $member_post_changes));
2651
2652
	return true;
2653
}
2654
2655
/**
2656
 * Approve topics?
2657
 *
2658
 * @todo shouldn't this be in topic
2659
 *
2660
 * @param array $topics Array of topic ids
2661
 * @param bool $approve Whether to approve the topics. If false, unapproves them instead
2662
 * @return bool Whether the operation was successful
2663
 */
2664
function approveTopics($topics, $approve = true)
2665
{
2666
	global $smcFunc;
2667
2668
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
2669
		$topics = array($topics);
2670
2671
	if (empty($topics))
2672
		return false;
2673
2674
	$approve_type = $approve ? 0 : 1;
2675
2676
	// Just get the messages to be approved and pass through...
2677
	$request = $smcFunc['db_query']('', '
2678
		SELECT id_first_msg
2679
		FROM {db_prefix}topics
2680
		WHERE id_topic IN ({array_int:topic_list})
2681
			AND approved = {int:approve_type}',
2682
		array(
2683
			'topic_list' => $topics,
2684
			'approve_type' => $approve_type,
2685
		)
2686
	);
2687
	$msgs = array();
2688
	while ($row = $smcFunc['db_fetch_assoc']($request))
2689
		$msgs[] = $row['id_first_msg'];
2690
	$smcFunc['db_free_result']($request);
2691
2692
	return approvePosts($msgs, $approve);
2693
}
2694
2695
/**
2696
 * Takes an array of board IDs and updates their last messages.
2697
 * If the board has a parent, that parent board is also automatically
2698
 * updated.
2699
 * The columns updated are id_last_msg and last_updated.
2700
 * Note that id_last_msg should always be updated using this function,
2701
 * and is not automatically updated upon other changes.
2702
 *
2703
 * @param array $setboards An array of board IDs
2704
 * @param int $id_msg The ID of the message
2705
 * @return void|false Returns false if $setboards is empty for some reason
2706
 */
2707
function updateLastMessages($setboards, $id_msg = 0)
2708
{
2709
	global $board_info, $board, $smcFunc;
2710
2711
	// Please - let's be sane.
2712
	if (empty($setboards))
2713
		return false;
2714
2715
	if (!is_array($setboards))
0 ignored issues
show
introduced by
The condition is_array($setboards) is always true.
Loading history...
2716
		$setboards = array($setboards);
2717
2718
	// If we don't know the id_msg we need to find it.
2719
	if (!$id_msg)
2720
	{
2721
		// Find the latest message on this board (highest id_msg.)
2722
		$request = $smcFunc['db_query']('', '
2723
			SELECT id_board, MAX(id_last_msg) AS id_msg
2724
			FROM {db_prefix}topics
2725
			WHERE id_board IN ({array_int:board_list})
2726
				AND approved = {int:approved}
2727
			GROUP BY id_board',
2728
			array(
2729
				'board_list' => $setboards,
2730
				'approved' => 1,
2731
			)
2732
		);
2733
		$lastMsg = array();
2734
		while ($row = $smcFunc['db_fetch_assoc']($request))
2735
			$lastMsg[$row['id_board']] = $row['id_msg'];
2736
		$smcFunc['db_free_result']($request);
2737
	}
2738
	else
2739
	{
2740
		// Just to note - there should only be one board passed if we are doing this.
2741
		foreach ($setboards as $id_board)
2742
			$lastMsg[$id_board] = $id_msg;
2743
	}
2744
2745
	$parent_boards = array();
2746
	// Keep track of last modified dates.
2747
	$lastModified = $lastMsg;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lastMsg does not seem to be defined for all execution paths leading up to this point.
Loading history...
2748
	// Get all the child boards for the parents, if they have some...
2749
	foreach ($setboards as $id_board)
2750
	{
2751
		if (!isset($lastMsg[$id_board]))
2752
		{
2753
			$lastMsg[$id_board] = 0;
2754
			$lastModified[$id_board] = 0;
2755
		}
2756
2757
		if (!empty($board) && $id_board == $board)
2758
			$parents = $board_info['parent_boards'];
2759
		else
2760
			$parents = getBoardParents($id_board);
2761
2762
		// Ignore any parents on the top child level.
2763
		// @todo Why?
2764
		foreach ($parents as $id => $parent)
2765
		{
2766
			if ($parent['level'] != 0)
2767
			{
2768
				// If we're already doing this one as a board, is this a higher last modified?
2769
				if (isset($lastModified[$id]) && $lastModified[$id_board] > $lastModified[$id])
2770
					$lastModified[$id] = $lastModified[$id_board];
2771
				elseif (!isset($lastModified[$id]) && (!isset($parent_boards[$id]) || $parent_boards[$id] < $lastModified[$id_board]))
2772
					$parent_boards[$id] = $lastModified[$id_board];
2773
			}
2774
		}
2775
	}
2776
2777
	// Note to help understand what is happening here. For parents we update the timestamp of the last message for determining
2778
	// whether there are child boards which have not been read. For the boards themselves we update both this and id_last_msg.
2779
2780
	$board_updates = array();
2781
	$parent_updates = array();
2782
	// Finally, to save on queries make the changes...
2783
	foreach ($parent_boards as $id => $msg)
2784
	{
2785
		if (!isset($parent_updates[$msg]))
2786
			$parent_updates[$msg] = array($id);
2787
		else
2788
			$parent_updates[$msg][] = $id;
2789
	}
2790
2791
	foreach ($lastMsg as $id => $msg)
2792
	{
2793
		if (!isset($board_updates[$msg . '-' . $lastModified[$id]]))
2794
			$board_updates[$msg . '-' . $lastModified[$id]] = array(
2795
				'id' => $msg,
2796
				'updated' => $lastModified[$id],
2797
				'boards' => array($id)
2798
			);
2799
2800
		else
2801
			$board_updates[$msg . '-' . $lastModified[$id]]['boards'][] = $id;
2802
	}
2803
2804
	// Now commit the changes!
2805
	foreach ($parent_updates as $id_msg => $boards)
2806
	{
2807
		$smcFunc['db_query']('', '
2808
			UPDATE {db_prefix}boards
2809
			SET id_msg_updated = {int:id_msg_updated}
2810
			WHERE id_board IN ({array_int:board_list})
2811
				AND id_msg_updated < {int:id_msg_updated}',
2812
			array(
2813
				'board_list' => $boards,
2814
				'id_msg_updated' => $id_msg,
2815
			)
2816
		);
2817
	}
2818
	foreach ($board_updates as $board_data)
2819
	{
2820
		$smcFunc['db_query']('', '
2821
			UPDATE {db_prefix}boards
2822
			SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
2823
			WHERE id_board IN ({array_int:board_list})',
2824
			array(
2825
				'board_list' => $board_data['boards'],
2826
				'id_last_msg' => $board_data['id'],
2827
				'id_msg_updated' => $board_data['updated'],
2828
			)
2829
		);
2830
	}
2831
}
2832
2833
/**
2834
 * This simple function gets a list of all administrators and sends them an email
2835
 *  to let them know a new member has joined.
2836
 * Called by registerMember() function in Subs-Members.php.
2837
 * Email is sent to all groups that have the moderate_forum permission.
2838
 * The language set by each member is being used (if available).
2839
 * Uses the Login language file
2840
 *
2841
 * @param string $type The type. Types supported are 'approval', 'activation', and 'standard'.
2842
 * @param int $memberID The ID of the member
2843
 * @param string $member_name The name of the member (if null, it is pulled from the database)
2844
 */
2845
function adminNotify($type, $memberID, $member_name = null)
2846
{
2847
	global $smcFunc;
2848
2849
	if ($member_name == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $member_name of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2850
	{
2851
		// Get the new user's name....
2852
		$request = $smcFunc['db_query']('', '
2853
			SELECT real_name
2854
			FROM {db_prefix}members
2855
			WHERE id_member = {int:id_member}
2856
			LIMIT 1',
2857
			array(
2858
				'id_member' => $memberID,
2859
			)
2860
		);
2861
		list ($member_name) = $smcFunc['db_fetch_row']($request);
2862
		$smcFunc['db_free_result']($request);
2863
	}
2864
2865
	// This is really just a wrapper for making a new background task to deal with all the fun.
2866
	$smcFunc['db_insert']('insert',
2867
		'{db_prefix}background_tasks',
2868
		array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2869
		array('$sourcedir/tasks/Register-Notify.php', 'Register_Notify_Background', $smcFunc['json_encode'](array(
2870
			'new_member_id' => $memberID,
2871
			'new_member_name' => $member_name,
2872
			'notify_type' => $type,
2873
			'time' => time(),
2874
		)), 0),
2875
		array('id_task')
2876
	);
2877
}
2878
2879
/**
2880
 * Load a template from EmailTemplates language file.
2881
 *
2882
 * @param string $template The name of the template to load
2883
 * @param array $replacements An array of replacements for the variables in the template
2884
 * @param string $lang The language to use, if different than the user's current language
2885
 * @param bool $loadLang Whether to load the language file first
2886
 * @return array An array containing the subject and body of the email template, with replacements made
2887
 */
2888
function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true)
2889
{
2890
	global $txt, $mbname, $scripturl, $settings, $context;
2891
2892
	// First things first, load up the email templates language file, if we need to.
2893
	if ($loadLang)
2894
		loadLanguage('EmailTemplates', $lang);
2895
2896
	if (!isset($txt[$template . '_subject']) || !isset($txt[$template . '_body']))
2897
		fatal_lang_error('email_no_template', 'template', array($template));
2898
2899
	$ret = array(
2900
		'subject' => $txt[$template . '_subject'],
2901
		'body' => $txt[$template . '_body'],
2902
		'is_html' => !empty($txt[$template . '_html']),
2903
	);
2904
2905
	// Add in the default replacements.
2906
	$replacements += array(
2907
		'FORUMNAME' => $mbname,
2908
		'SCRIPTURL' => $scripturl,
2909
		'THEMEURL' => $settings['theme_url'],
2910
		'IMAGESURL' => $settings['images_url'],
2911
		'DEFAULT_THEMEURL' => $settings['default_theme_url'],
2912
		'REGARDS' => sprintf($txt['regards_team'], $context['forum_name']),
2913
	);
2914
2915
	// Split the replacements up into two arrays, for use with str_replace
2916
	$find = array();
2917
	$replace = array();
2918
2919
	foreach ($replacements as $f => $r)
2920
	{
2921
		$find[] = '{' . $f . '}';
2922
		$replace[] = $r;
2923
	}
2924
2925
	// Do the variable replacements.
2926
	$ret['subject'] = str_replace($find, $replace, $ret['subject']);
2927
	$ret['body'] = str_replace($find, $replace, $ret['body']);
2928
2929
	// Now deal with the {USER.variable} items.
2930
	$ret['subject'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['subject']);
2931
	$ret['body'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['body']);
2932
2933
	// Finally return the email to the caller so they can send it out.
2934
	return $ret;
2935
}
2936
2937
/**
2938
 * Callback function for loademaitemplate on subject and body
2939
 * Uses capture group 1 in array
2940
 *
2941
 * @param array $matches An array of matches
2942
 * @return string The match
2943
 */
2944
function user_info_callback($matches)
2945
{
2946
	global $user_info;
2947
	if (empty($matches[1]))
2948
		return '';
2949
2950
	$use_ref = true;
2951
	$ref = &$user_info;
2952
2953
	foreach (explode('.', $matches[1]) as $index)
2954
	{
2955
		if ($use_ref && isset($ref[$index]))
2956
			$ref = &$ref[$index];
2957
		else
2958
		{
2959
			$use_ref = false;
2960
			break;
2961
		}
2962
	}
2963
2964
	return $use_ref ? $ref : $matches[0];
2965
}
2966
2967
/**
2968
 * spell_init()
2969
 *
2970
 * Sets up a dictionary resource handle. Tries enchant first then falls through to pspell.
2971
 *
2972
 * @return resource|bool An enchant or pspell dictionary resource handle or false if the dictionary couldn't be loaded
2973
 */
2974
function spell_init()
2975
{
2976
	global $context, $txt;
2977
2978
	// Check for UTF-8 and strip ".utf8" off the lang_locale string for enchant
2979
	$context['spell_utf8'] = ($txt['lang_character_set'] == 'UTF-8');
2980
	$lang_locale = str_replace('.utf8', '', $txt['lang_locale']);
2981
2982
	// Try enchant first since PSpell is (supposedly) deprecated as of PHP 5.3
2983
	// enchant only does UTF-8, so we need iconv if you aren't using UTF-8
2984
	if (function_exists('enchant_broker_init') && ($context['spell_utf8'] || function_exists('iconv')))
2985
	{
2986
		// We'll need this to free resources later...
2987
		$context['enchant_broker'] = enchant_broker_init();
2988
2989
		// Try locale first, then general...
2990
		if (!empty($lang_locale) && enchant_broker_dict_exists($context['enchant_broker'], $lang_locale))
2991
		{
2992
			$enchant_link = enchant_broker_request_dict($context['enchant_broker'], $lang_locale);
2993
		}
2994
		elseif (enchant_broker_dict_exists($context['enchant_broker'], $txt['lang_dictionary']))
2995
		{
2996
			$enchant_link = enchant_broker_request_dict($context['enchant_broker'], $txt['lang_dictionary']);
2997
		}
2998
2999
		// Success
3000
		if (!empty($enchant_link))
3001
		{
3002
			$context['provider'] = 'enchant';
3003
			return $enchant_link;
3004
		}
3005
		else
3006
		{
3007
			// Free up any resources used...
3008
			@enchant_broker_free($context['enchant_broker']);
3009
		}
3010
	}
3011
3012
	// Fall through to pspell if enchant didn't work
3013
	if (function_exists('pspell_new'))
3014
	{
3015
		// Okay, this looks funny, but it actually fixes a weird bug.
3016
		ob_start();
3017
		$old = error_reporting(0);
3018
3019
		// See, first, some windows machines don't load pspell properly on the first try.  Dumb, but this is a workaround.
3020
		pspell_new('en');
3021
3022
		// Next, the dictionary in question may not exist. So, we try it... but...
3023
		$pspell_link = pspell_new($txt['lang_dictionary'], '', '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER);
3024
3025
		// Most people don't have anything but English installed... So we use English as a last resort.
3026
		if (!$pspell_link)
3027
			$pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
3028
3029
		error_reporting($old);
3030
		ob_end_clean();
3031
3032
		// If we have pspell, exit now...
3033
		if ($pspell_link)
3034
		{
3035
			$context['provider'] = 'pspell';
3036
			return $pspell_link;
3037
		}
3038
	}
3039
3040
	// If we get this far, we're doomed
3041
	return false;
3042
}
3043
3044
/**
3045
 * spell_check()
3046
 *
3047
 * Determines whether or not the specified word is spelled correctly
3048
 *
3049
 * @param resource $dict An enchant or pspell dictionary resource set up by {@link spell_init()}
3050
 * @param string $word A word to check the spelling of
3051
 * @return bool Whether or not the specified word is spelled properly
3052
 */
3053
function spell_check($dict, $word)
3054
{
3055
	global $context, $txt;
3056
3057
	// Enchant or pspell?
3058
	if ($context['provider'] == 'enchant')
3059
	{
3060
		// This is a bit tricky here...
3061
		if (!$context['spell_utf8'])
3062
		{
3063
			// Convert the word to UTF-8 with iconv
3064
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
3065
		}
3066
		return enchant_dict_check($dict, $word);
3067
	}
3068
	elseif ($context['provider'] == 'pspell')
3069
	{
3070
		return pspell_check($dict, $word);
0 ignored issues
show
Bug introduced by
$dict of type resource is incompatible with the type integer expected by parameter $dictionary_link of pspell_check(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3070
		return pspell_check(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3071
	}
3072
}
3073
3074
/**
3075
 * spell_suggest()
3076
 *
3077
 * Returns an array of suggested replacements for the specified word
3078
 *
3079
 * @param resource $dict An enchant or pspell dictionary resource
3080
 * @param string $word A misspelled word
3081
 * @return array An array of suggested replacements for the misspelled word
3082
 */
3083
function spell_suggest($dict, $word)
3084
{
3085
	global $context, $txt;
3086
3087
	if ($context['provider'] == 'enchant')
3088
	{
3089
		// If we're not using UTF-8, we need iconv to handle some stuff...
3090
		if (!$context['spell_utf8'])
3091
		{
3092
			// Convert the word to UTF-8 before getting suggestions
3093
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
3094
			$suggestions = enchant_dict_suggest($dict, $word);
3095
3096
			// Go through the suggestions and convert them back to the proper character set
3097
			foreach ($suggestions as $index => $suggestion)
3098
			{
3099
				// //TRANSLIT makes it use similar-looking characters for incompatible ones...
3100
				$suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion);
3101
			}
3102
3103
			return $suggestions;
3104
		}
3105
		else
3106
		{
3107
			return enchant_dict_suggest($dict, $word);
3108
		}
3109
	}
3110
	elseif ($context['provider'] == 'pspell')
3111
	{
3112
		return pspell_suggest($dict, $word);
0 ignored issues
show
Bug introduced by
$dict of type resource is incompatible with the type integer expected by parameter $dictionary_link of pspell_suggest(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3112
		return pspell_suggest(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3113
	}
3114
}
3115
3116
?>