AddMailQueue()   B
last analyzed

Complexity

Conditions 11
Paths 44

Size

Total Lines 87
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 41
c 0
b 0
f 0
nop 8
dl 0
loc 87
rs 7.3166
nc 44

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 2025 Simple Machines and individual contributors
13
 * @license https://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1.5
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
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
Bug introduced by
The expression $codes = parse_bbc(false) of type string is not traversable.
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
	// Starting with php 8x, line breaks need to be \r\n even for linux.
577
	$line_break = ($context['server']['is_windows'] || !$use_sendmail || version_compare(PHP_VERSION, '8.0.0', '>=')) ? "\r\n" : "\n";
578
579
	// So far so good.
580
	$mail_result = true;
581
582
	// If the recipient list isn't an array, make it one.
583
	$to_array = is_array($to) ? $to : array($to);
0 ignored issues
show
introduced by
The condition is_array($to) is always true.
Loading history...
584
585
	// Make sure we actually have email addresses to send this to
586
	foreach ($to_array as $k => $v)
587
	{
588
		// This should never happen, but better safe than sorry
589
		if (trim($v) == '')
590
		{
591
			unset($to_array[$k]);
592
		}
593
	}
594
595
	// Nothing left? Nothing else to do
596
	if (empty($to_array))
597
		return true;
598
599
	// Once upon a time, Hotmail could not interpret non-ASCII mails.
600
	// In honour of those days, it's still called the 'hotmail fix'.
601
	if ($hotmail_fix === null)
602
	{
603
		$hotmail_to = array();
604
		foreach ($to_array as $i => $to_address)
605
		{
606
			if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
607
			{
608
				$hotmail_to[] = $to_address;
609
				$to_array = array_diff($to_array, array($to_address));
610
			}
611
		}
612
613
		// Call this function recursively for the hotmail addresses.
614
		if (!empty($hotmail_to))
615
			$mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true, $is_private);
616
617
		// The remaining addresses no longer need the fix.
618
		$hotmail_fix = false;
619
620
		// No other addresses left? Return instantly.
621
		if (empty($to_array))
622
			return $mail_result;
623
	}
624
625
	// Get rid of entities.
626
	$subject = un_htmlspecialchars($subject);
627
	// Make the message use the proper line breaks.
628
	$message = str_replace(array("\r", "\n"), array('', $line_break), $message);
629
630
	// Make sure hotmail mails are sent as HTML so that HTML entities work.
631
	if ($hotmail_fix && !$send_html)
632
	{
633
		$send_html = true;
634
		$message = strtr($message, array($line_break => '<br>' . $line_break));
635
		$message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
636
	}
637
638
	list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
639
	list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
640
641
	// Construct the mail headers...
642
	$headers = 'From: "' . $from_name . '" <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
643
	$headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
644
	$headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
645
	$headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
646
	$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . ($message_id ?? 0) . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
647
	$headers .= 'X-Mailer: SMF' . $line_break;
648
649
	// Pass this to the integration before we start modifying the output -- it'll make it easier later.
650
	if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers, &$to_array)), true))
651
		return false;
652
653
	// Save the original message...
654
	$orig_message = $message;
655
656
	// The mime boundary separates the different alternative versions.
657
	$mime_boundary = 'SMF-' . md5($message . time());
658
659
	// Using mime, as it allows to send a plain unencoded alternative.
660
	$headers .= 'Mime-Version: 1.0' . $line_break;
661
	$headers .= 'content-type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
662
	$headers .= 'content-transfer-encoding: 7bit' . $line_break;
663
664
	// Sending HTML?  Let's plop in some basic stuff, then.
665
	if ($send_html)
666
	{
667
		$no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
668
669
		// But, then, dump it and use a plain one for dinosaur clients.
670
		list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
671
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
672
673
		// This is the plain text version.  Even if no one sees it, we need it for spam checkers.
674
		list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
675
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
676
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
677
		$message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
678
679
		// This is the actual HTML message, prim and proper.  If we wanted images, they could be inlined here (with multipart/related, etc.)
680
		list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
681
		$message .= 'content-type: text/html; charset=' . $charset . $line_break;
682
		$message .= 'content-transfer-encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
683
		$message .= $html_message . $line_break . '--' . $mime_boundary . '--';
684
	}
685
	// Text is good too.
686
	else
687
	{
688
		// Send a plain message first, for the older web clients.
689
		list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
690
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
691
692
		// Now add an encoded message using the forum's character set.
693
		list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
694
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
695
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
696
		$message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
697
	}
698
699
	// Are we using the mail queue, if so this is where we butt in...
700
	if ($priority != 0)
701
		return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
702
703
	// If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
704
	elseif (!empty($modSettings['mail_limit']))
705
	{
706
		list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
707
		if (empty($mails_this_minute) || time() > $last_mail_time + 60)
708
			$new_queue_stat = time() . '|' . 1;
709
		else
710
			$new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
711
712
		updateSettings(array('mail_recent' => $new_queue_stat));
713
	}
714
715
	// SMTP or sendmail?
716
	if ($use_sendmail)
717
	{
718
		$subject = strtr($subject, array("\r" => '', "\n" => ''));
719
		if (!empty($modSettings['mail_strip_carriage']))
720
		{
721
			$message = strtr($message, array("\r" => ''));
722
			$headers = strtr($headers, array("\r" => ''));
723
		}
724
725
		foreach ($to_array as $to)
0 ignored issues
show
introduced by
$to is overwriting one of the parameters of this function.
Loading history...
726
		{
727
			set_error_handler(
728
				function($errno, $errstr, $errfile, $errline)
729
				{
730
					// error was suppressed with the @-operator
731
					if (0 === error_reporting())
732
						return false;
733
734
					throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
735
				}
736
			);
737
			try
738
			{
739
				if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
740
				{
741
					log_error(sprintf($txt['mail_send_unable'], $to));
742
					$mail_result = false;
743
				}
744
			}
745
			catch (ErrorException $e)
746
			{
747
				log_error($e->getMessage(), 'general', $e->getFile(), $e->getLine());
748
				log_error(sprintf($txt['mail_send_unable'], $to));
749
				$mail_result = false;
750
			}
751
			restore_error_handler();
752
753
			// Wait, wait, I'm still sending here!
754
			@set_time_limit(300);
755
			if (function_exists('apache_reset_timeout'))
756
				@apache_reset_timeout();
757
		}
758
	}
759
	else
760
		$mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
761
762
	// Everything go smoothly?
763
	return $mail_result;
764
}
765
766
/**
767
 * Add an email to the mail queue.
768
 *
769
 * @param bool $flush Whether to flush the queue
770
 * @param array $to_array An array of recipients
771
 * @param string $subject The subject of the message
772
 * @param string $message The message
773
 * @param string $headers The headers
774
 * @param bool $send_html Whether to send in HTML format
775
 * @param int $priority The priority
776
 * @param bool $is_private Whether this is private
777
 * @return boolean Whether the message was added
778
 */
779
function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
780
{
781
	global $context, $smcFunc;
782
783
	static $cur_insert = array();
784
	static $cur_insert_len = 0;
785
786
	if ($cur_insert_len == 0)
787
		$cur_insert = array();
788
789
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
790
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
791
	{
792
		// Only do these once.
793
		$cur_insert_len = 0;
794
795
		// Dump the data...
796
		$smcFunc['db_insert']('',
797
			'{db_prefix}mail_queue',
798
			array(
799
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
800
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
801
			),
802
			$cur_insert,
803
			array('id_mail')
804
		);
805
806
		$cur_insert = array();
807
		$context['flush_mail'] = false;
808
	}
809
810
	// If we're flushing we're done.
811
	if ($flush)
812
	{
813
		$nextSendTime = time() + 10;
814
815
		$smcFunc['db_query']('', '
816
			UPDATE {db_prefix}settings
817
			SET value = {string:nextSendTime}
818
			WHERE variable = {literal:mail_next_send}
819
				AND value = {string:no_outstanding}',
820
			array(
821
				'nextSendTime' => $nextSendTime,
822
				'no_outstanding' => '0',
823
			)
824
		);
825
826
		return true;
827
	}
828
829
	// Ensure we tell obExit to flush.
830
	$context['flush_mail'] = true;
831
832
	foreach ($to_array as $to)
833
	{
834
		// Will this insert go over MySQL's limit?
835
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
836
837
		// Insert limit of 1M (just under the safety) is reached?
838
		if ($this_insert_len + $cur_insert_len > 1000000)
839
		{
840
			// Flush out what we have so far.
841
			$smcFunc['db_insert']('',
842
				'{db_prefix}mail_queue',
843
				array(
844
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
845
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
846
				),
847
				$cur_insert,
848
				array('id_mail')
849
			);
850
851
			// Clear this out.
852
			$cur_insert = array();
853
			$cur_insert_len = 0;
854
		}
855
856
		// Now add the current insert to the array...
857
		$cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
858
		$cur_insert_len += $this_insert_len;
859
	}
860
861
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
862
	if (SMF === 'SSI' || SMF === 'BACKGROUND')
0 ignored issues
show
introduced by
The condition SMF === 'SSI' is always true.
Loading history...
863
		return AddMailQueue(true);
864
865
	return true;
866
}
867
868
/**
869
 * Sends an personal message from the specified person to the specified people
870
 * ($from defaults to the user)
871
 *
872
 * @param array $recipients An array containing the arrays 'to' and 'bcc', both containing id_member's.
873
 * @param string $subject Should have no slashes and no html entities
874
 * @param string $message Should have no slashes and no html entities
875
 * @param bool $store_outbox Whether to store it in the sender's outbox
876
 * @param array $from An array with the id, name, and username of the member.
877
 * @param int $pm_head The ID of the chain being replied to - if any.
878
 * @return array An array with log entries telling how many recipients were successful and which recipients it failed to send to.
879
 */
880
function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
881
{
882
	global $scripturl, $txt, $user_info, $language, $sourcedir;
883
	global $modSettings, $smcFunc;
884
885
	// Make sure the PM language file is loaded, we might need something out of it.
886
	loadLanguage('PersonalMessage');
887
888
	// Initialize log array.
889
	$log = array(
890
		'failed' => array(),
891
		'sent' => array()
892
	);
893
894
	if ($from === null)
895
		$from = array(
896
			'id' => $user_info['id'],
897
			'name' => $user_info['name'],
898
			'username' => $user_info['username']
899
		);
900
901
	// This is the one that will go in their inbox.
902
	$htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
903
	preparsecode($htmlmessage);
904
	$htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => ''));
905
	if ($smcFunc['strlen']($htmlsubject) > 100)
906
		$htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100);
907
908
	// Make sure is an array
909
	if (!is_array($recipients))
0 ignored issues
show
introduced by
The condition is_array($recipients) is always true.
Loading history...
910
		$recipients = array($recipients);
911
912
	// Integrated PMs
913
	call_integration_hook('integrate_personal_message', array(&$recipients, &$from, &$subject, &$message));
914
915
	// Get a list of usernames and convert them to IDs.
916
	$usernames = array();
917
	foreach ($recipients as $rec_type => $rec)
918
	{
919
		foreach ($rec as $id => $member)
920
		{
921
			if (!is_numeric($recipients[$rec_type][$id]))
922
			{
923
				$recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('~[<>&"\'=\\\]~', '', $recipients[$rec_type][$id])));
924
				$usernames[$recipients[$rec_type][$id]] = 0;
925
			}
926
		}
927
	}
928
	if (!empty($usernames))
929
	{
930
		$request = $smcFunc['db_query']('pm_find_username', '
931
			SELECT id_member, member_name
932
			FROM {db_prefix}members
933
			WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
934
			array(
935
				'usernames' => array_keys($usernames),
936
			)
937
		);
938
		while ($row = $smcFunc['db_fetch_assoc']($request))
939
			if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
940
				$usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
941
		$smcFunc['db_free_result']($request);
942
943
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
944
		foreach ($recipients as $rec_type => $rec)
945
			foreach ($rec as $id => $member)
946
			{
947
				if (is_numeric($recipients[$rec_type][$id]))
948
					continue;
949
950
				if (!empty($usernames[$member]))
951
					$recipients[$rec_type][$id] = $usernames[$member];
952
				else
953
				{
954
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
955
					unset($recipients[$rec_type][$id]);
956
				}
957
			}
958
	}
959
960
	// Make sure there are no duplicate 'to' members.
961
	$recipients['to'] = array_unique($recipients['to']);
962
963
	// Only 'bcc' members that aren't already in 'to'.
964
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
965
966
	// Combine 'to' and 'bcc' recipients.
967
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
968
969
	// Check no-one will want it deleted right away!
970
	$request = $smcFunc['db_query']('', '
971
		SELECT
972
			id_member, criteria, is_or
973
		FROM {db_prefix}pm_rules
974
		WHERE id_member IN ({array_int:to_members})
975
			AND delete_pm = {int:delete_pm}',
976
		array(
977
			'to_members' => $all_to,
978
			'delete_pm' => 1,
979
		)
980
	);
981
	$deletes = array();
982
	// Check whether we have to apply anything...
983
	while ($row = $smcFunc['db_fetch_assoc']($request))
984
	{
985
		$criteria = $smcFunc['json_decode']($row['criteria'], true);
986
		// Note we don't check the buddy status, cause deletion from buddy = madness!
987
		$delete = false;
988
		foreach ($criteria as $criterium)
989
		{
990
			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))
991
				$delete = true;
992
			// If we're adding and one criteria don't match then we stop!
993
			elseif (!$row['is_or'])
994
			{
995
				$delete = false;
996
				break;
997
			}
998
		}
999
		if ($delete)
1000
			$deletes[$row['id_member']] = 1;
1001
	}
1002
	$smcFunc['db_free_result']($request);
1003
1004
	// Load the membergrounp message limits.
1005
	// @todo Consider caching this?
1006
	static $message_limit_cache = array();
1007
	if (!allowedTo('moderate_forum') && empty($message_limit_cache))
1008
	{
1009
		$request = $smcFunc['db_query']('', '
1010
			SELECT id_group, max_messages
1011
			FROM {db_prefix}membergroups',
1012
			array(
1013
			)
1014
		);
1015
		while ($row = $smcFunc['db_fetch_assoc']($request))
1016
			$message_limit_cache[$row['id_group']] = $row['max_messages'];
1017
		$smcFunc['db_free_result']($request);
1018
	}
1019
1020
	// Load the groups that are allowed to read PMs.
1021
	require_once($sourcedir . '/Subs-Members.php');
1022
	$pmReadGroups = groupsAllowedTo('pm_read');
1023
1024
	if (empty($modSettings['permission_enable_deny']))
1025
		$pmReadGroups['denied'] = array();
1026
1027
	// Load their alert preferences
1028
	require_once($sourcedir . '/Subs-Notify.php');
1029
	$notifyPrefs = getNotifyPrefs($all_to, array('pm_new', 'pm_reply', 'pm_notify'), true);
1030
1031
	$request = $smcFunc['db_query']('', '
1032
		SELECT
1033
			member_name, real_name, id_member, email_address, lngfile,
1034
			instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
1035
			(pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
1036
			(pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
1037
			(pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
1038
			FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
1039
			additional_groups, id_group, id_post_group
1040
		FROM {db_prefix}members
1041
		WHERE id_member IN ({array_int:recipients})
1042
		ORDER BY lngfile
1043
		LIMIT {int:count_recipients}',
1044
		array(
1045
			'not_on_ignore_list' => 1,
1046
			'buddies_only' => 2,
1047
			'admins_only' => 3,
1048
			'recipients' => $all_to,
1049
			'count_recipients' => count($all_to),
1050
			'from_id' => $from['id'],
1051
		)
1052
	);
1053
	$notifications = array();
1054
	while ($row = $smcFunc['db_fetch_assoc']($request))
1055
	{
1056
		// Don't do anything for members to be deleted!
1057
		if (isset($deletes[$row['id_member']]))
1058
			continue;
1059
1060
		// Load the preferences for this member (if any)
1061
		$prefs = !empty($notifyPrefs[$row['id_member']]) ? $notifyPrefs[$row['id_member']] : array();
1062
		$prefs = array_merge(array(
1063
			'pm_new' => 0,
1064
			'pm_reply' => 0,
1065
			'pm_notify' => 0,
1066
		), $prefs);
1067
1068
		// We need to know this members groups.
1069
		$groups = explode(',', $row['additional_groups']);
1070
		$groups[] = $row['id_group'];
1071
		$groups[] = $row['id_post_group'];
1072
1073
		$message_limit = -1;
1074
		// For each group see whether they've gone over their limit - assuming they're not an admin.
1075
		if (!in_array(1, $groups))
1076
		{
1077
			foreach ($groups as $id)
1078
			{
1079
				if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
1080
					$message_limit = $message_limit_cache[$id];
1081
			}
1082
1083
			if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
1084
			{
1085
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
1086
				unset($all_to[array_search($row['id_member'], $all_to)]);
1087
				continue;
1088
			}
1089
1090
			// Do they have any of the allowed groups?
1091
			if (count(array_intersect($pmReadGroups['allowed'], $groups)) == 0 || count(array_intersect($pmReadGroups['denied'], $groups)) != 0)
1092
			{
1093
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1094
				unset($all_to[array_search($row['id_member'], $all_to)]);
1095
				continue;
1096
			}
1097
		}
1098
1099
		// Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
1100
		if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id'])
1101
		{
1102
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
1103
			unset($all_to[array_search($row['id_member'], $all_to)]);
1104
			continue;
1105
		}
1106
1107
		// If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
1108
		if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin']))
1109
		{
1110
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1111
			unset($all_to[array_search($row['id_member'], $all_to)]);
1112
			continue;
1113
		}
1114
1115
		// Send a notification, if enabled - taking the buddy list into account.
1116
		if (!empty($row['email_address'])
1117
			&& ((empty($pm_head) && $prefs['pm_new'] & 0x02) || (!empty($pm_head) && $prefs['pm_reply'] & 0x02))
1118
			&& ($prefs['pm_notify'] <= 1 || ($prefs['pm_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
1119
		{
1120
			$notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
1121
		}
1122
1123
		$log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
1124
	}
1125
	$smcFunc['db_free_result']($request);
1126
1127
	// Only 'send' the message if there are any recipients left.
1128
	if (empty($all_to))
1129
		return $log;
1130
1131
	// Insert the message itself and then grab the last insert id.
1132
	$id_pm = $smcFunc['db_insert']('',
1133
		'{db_prefix}personal_messages',
1134
		array(
1135
			'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
1136
			'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
1137
		),
1138
		array(
1139
			$pm_head, $from['id'], ($store_outbox ? 0 : 1),
1140
			$from['username'], time(), $htmlsubject, $htmlmessage,
1141
		),
1142
		array('id_pm'),
1143
		1
1144
	);
1145
1146
	// Add the recipients.
1147
	if (!empty($id_pm))
1148
	{
1149
		// If this is new we need to set it part of it's own conversation.
1150
		if (empty($pm_head))
1151
			$smcFunc['db_query']('', '
1152
				UPDATE {db_prefix}personal_messages
1153
				SET id_pm_head = {int:id_pm_head}
1154
				WHERE id_pm = {int:id_pm_head}',
1155
				array(
1156
					'id_pm_head' => $id_pm,
1157
				)
1158
			);
1159
1160
		// Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
1161
		$smcFunc['db_query']('', '
1162
			DELETE FROM {db_prefix}pm_recipients
1163
			WHERE id_pm = {int:id_pm}',
1164
			array(
1165
				'id_pm' => $id_pm,
1166
			)
1167
		);
1168
1169
		$insertRows = array();
1170
		$to_list = array();
1171
		foreach ($all_to as $to)
1172
		{
1173
			$insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
1174
			if (!in_array($to, $recipients['bcc']))
1175
				$to_list[] = $to;
1176
		}
1177
1178
		$smcFunc['db_insert']('insert',
1179
			'{db_prefix}pm_recipients',
1180
			array(
1181
				'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
1182
			),
1183
			$insertRows,
1184
			array('id_pm', 'id_member')
1185
		);
1186
	}
1187
1188
	$to_names = array();
1189
	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...
1190
	{
1191
		$request = $smcFunc['db_query']('', '
1192
			SELECT real_name
1193
			FROM {db_prefix}members
1194
			WHERE id_member IN ({array_int:to_members})
1195
				AND id_member != {int:from}',
1196
			array(
1197
				'to_members' => $to_list,
1198
				'from' => $from['id'],
1199
			)
1200
		);
1201
		while ($row = $smcFunc['db_fetch_assoc']($request))
1202
			$to_names[] = un_htmlspecialchars($row['real_name']);
1203
		$smcFunc['db_free_result']($request);
1204
	}
1205
	$replacements = array(
1206
		'SUBJECT' => $subject,
1207
		'MESSAGE' => $message,
1208
		'SENDER' => un_htmlspecialchars($from['name']),
1209
		'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm,
1210
		'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'],
1211
		'TOLIST' => implode(', ', $to_names),
1212
	);
1213
	$email_template = 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : '');
1214
1215
	$notification_texts = array();
1216
1217
	foreach ($notifications as $lang => $notification_list)
1218
	{
1219
		// Censor and parse BBC in the receiver's language. Only do each language once.
1220
		if (empty($notification_texts[$lang]))
1221
		{
1222
			if ($lang != $user_info['language'])
1223
				loadLanguage('index+Modifications', $lang, false);
1224
1225
			$notification_texts[$lang]['subject'] = $subject;
1226
			censorText($notification_texts[$lang]['subject']);
1227
1228
			if (empty($modSettings['disallow_sendBody']))
1229
			{
1230
				$notification_texts[$lang]['body'] = $message;
1231
1232
				censorText($notification_texts[$lang]['body']);
1233
1234
				$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;' => ']')))));
1235
			}
1236
			else
1237
				$notification_texts[$lang]['body'] = '';
1238
1239
1240
			if ($lang != $user_info['language'])
1241
				loadLanguage('index+Modifications', $user_info['language'], false);
1242
		}
1243
1244
		$replacements['SUBJECT'] = $notification_texts[$lang]['subject'];
1245
		$replacements['MESSAGE'] = $notification_texts[$lang]['body'];
1246
1247
		$emaildata = loadEmailTemplate($email_template, $replacements, $lang);
1248
1249
		// Off the notification email goes!
1250
		sendmail($notification_list, $emaildata['subject'], $emaildata['body'], null, 'p' . $id_pm, $emaildata['is_html'], 2, null, true);
1251
	}
1252
1253
	// Integrated After PMs
1254
	call_integration_hook('integrate_personal_message_after', array(&$id_pm, &$log, &$recipients, &$from, &$subject, &$message));
1255
1256
	// Back to what we were on before!
1257
	loadLanguage('index+PersonalMessage');
1258
1259
	// Add one to their unread and read message counts.
1260
	foreach ($all_to as $k => $id)
1261
		if (isset($deletes[$id]))
1262
			unset($all_to[$k]);
1263
	if (!empty($all_to))
1264
		updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
1265
1266
	return $log;
1267
}
1268
1269
/**
1270
 * Prepare text strings for sending as email body or header.
1271
 * In case there are higher ASCII characters in the given string, this
1272
 * function will attempt the transport method 'quoted-printable'.
1273
 * Otherwise the transport method '7bit' is used.
1274
 *
1275
 * @param string $string The string
1276
 * @param bool $with_charset Whether we're specifying a charset ($custom_charset must be set here)
1277
 * @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)
1278
 * @param string $line_break The linebreak
1279
 * @param string $custom_charset If set, it uses this character set
1280
 * @return array An array containing the character set, the converted string and the transport method.
1281
 */
1282
function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
1283
{
1284
	global $context;
1285
1286
	$charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
1287
1288
	// This is the fun part....
1289
	if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
1290
	{
1291
		// Let's, for now, assume there are only &#021;'ish characters.
1292
		$simple = true;
1293
1294
		foreach ($matches[1] as $entity)
1295
			if ($entity > 128)
1296
				$simple = false;
1297
		unset($matches);
1298
1299
		if ($simple)
1300
			$string = preg_replace_callback(
1301
				'~&#(\d{3,8});~',
1302
				function($m)
1303
				{
1304
					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

1304
					return chr(/** @scrutinizer ignore-type */ "$m[1]");
Loading history...
1305
				},
1306
				$string
1307
			);
1308
		else
1309
		{
1310
			// Try to convert the string to UTF-8.
1311
			if (!$context['utf8'] && function_exists('iconv'))
1312
			{
1313
				$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1314
				if ($newstring)
1315
					$string = $newstring;
1316
			}
1317
1318
			$string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string);
1319
1320
			// Unicode, baby.
1321
			$charset = 'UTF-8';
1322
		}
1323
	}
1324
1325
	// Convert all special characters to HTML entities...just for Hotmail :-\
1326
	if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
1327
	{
1328
		if (!$context['utf8'] && function_exists('iconv'))
1329
		{
1330
			$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1331
			if ($newstring)
1332
				$string = $newstring;
1333
		}
1334
1335
		$entityConvert = function($m)
1336
		{
1337
			$c = $m[1];
1338
			if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
1339
				return $c;
1340
			elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
1341
				return "&#" . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ";";
1342
			elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
1343
				return "&#" . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ";";
1344
			elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
1345
				return "&#" . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ";";
1346
			else
1347
				return "";
1348
		};
1349
1350
		// Convert all 'special' characters to HTML entities.
1351
		return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', $entityConvert, $string), '7bit');
1352
	}
1353
1354
	// We don't need to mess with the subject line if no special characters were in it..
1355
	elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
1356
	{
1357
		// Base64 encode.
1358
		$string = base64_encode($string);
1359
1360
		// Show the characterset and the transfer-encoding for header strings.
1361
		if ($with_charset)
1362
			$string = '=?' . $charset . '?B?' . $string . '?=';
1363
1364
		// Break it up in lines (mail body).
1365
		else
1366
			$string = chunk_split($string, 76, $line_break);
1367
1368
		return array($charset, $string, 'base64');
1369
	}
1370
1371
	else
1372
		return array($charset, $string, '7bit');
1373
}
1374
1375
/**
1376
 * Sends mail, like mail() but over SMTP.
1377
 * It expects no slashes or entities.
1378
 *
1379
 * @internal
1380
 *
1381
 * @param array $mail_to_array Array of strings (email addresses)
1382
 * @param string $subject Email subject
1383
 * @param string $message Email message
1384
 * @param string $headers Email headers
1385
 * @return boolean Whether it sent or not.
1386
 */
1387
function smtp_mail($mail_to_array, $subject, $message, $headers)
1388
{
1389
	global $modSettings, $webmaster_email, $txt, $boardurl, $sourcedir;
1390
1391
	static $helo;
1392
1393
	$modSettings['smtp_host'] = trim($modSettings['smtp_host']);
1394
1395
	// Try POP3 before SMTP?
1396
	// @todo There's no interface for this yet.
1397
	if ($modSettings['mail_type'] == 3 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1398
	{
1399
		$socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
1400
		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...
1401
			$socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
1402
1403
		if ($socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1404
		{
1405
			fgets($socket, 256);
1406
			fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
1407
			fgets($socket, 256);
1408
			fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
1409
			fgets($socket, 256);
1410
			fputs($socket, 'QUIT' . "\r\n");
1411
1412
			fclose($socket);
1413
		}
1414
	}
1415
1416
	// Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
1417
	if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
1418
	{
1419
		// Maybe we can still save this?  The port might be wrong.
1420
		if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
1421
		{
1422
			// ssl:hostname can cause fsocketopen to fail with a lookup failure, ensure it exists for this test.
1423
			if (substr($modSettings['smtp_host'], 0, 6) != 'ssl://')
1424
				$modSettings['smtp_host'] = str_replace('ssl:', 'ss://', $modSettings['smtp_host']);
1425
1426
			if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3))
1427
				log_error($txt['smtp_port_ssl']);
1428
		}
1429
1430
		// Unable to connect!  Don't show any error message, but just log one and try to continue anyway.
1431
		if (!$socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1432
		{
1433
			log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
1434
			return false;
1435
		}
1436
	}
1437
1438
	// Wait for a response of 220, without "-" continuer.
1439
	if (!server_parse(null, $socket, '220'))
1440
		return false;
1441
1442
	// Try to determine the server's fully qualified domain name
1443
	// Can't rely on $_SERVER['SERVER_NAME'] because it can be spoofed on Apache
1444
	if (empty($helo))
1445
	{
1446
		// See if we can get the domain name from the host itself
1447
		if (function_exists('gethostname'))
1448
			$helo = gethostname();
1449
		elseif (function_exists('php_uname'))
1450
			$helo = php_uname('n');
1451
1452
		// If the hostname isn't a fully qualified domain name, we can use the host name from $boardurl instead
1453
		if (empty($helo) || strpos($helo, '.') === false || substr_compare($helo, '.local', -6) === 0 || (!empty($modSettings['tld_regex']) && !preg_match('/\.' . $modSettings['tld_regex'] . '$/u', $helo)))
1454
			$helo = parse_iri($boardurl, PHP_URL_HOST);
1455
1456
		// This is one of those situations where 'www.' is undesirable
1457
		if (strpos($helo, 'www.') === 0)
1458
			$helo = substr($helo, 4);
1459
1460
		if (!function_exists('idn_to_ascii'))
1461
			require_once($sourcedir . '/Subs-Compat.php');
1462
1463
		$helo = idn_to_ascii($helo, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
1464
	}
1465
1466
	// SMTP = 1, SMTP - STARTTLS = 2
1467
	if (in_array($modSettings['mail_type'], array(1, 2)) && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1468
	{
1469
		// EHLO could be understood to mean encrypted hello...
1470
		if (server_parse('EHLO ' . $helo, $socket, null, $response) == '250')
1471
		{
1472
			// Are we using STARTTLS and does the server support STARTTLS?
1473
			if ($modSettings['mail_type'] == 2 && preg_match("~250( |-)STARTTLS~mi", $response))
1474
			{
1475
				// Send STARTTLS to enable encryption
1476
				if (!server_parse('STARTTLS', $socket, '220'))
1477
					return false;
1478
				// Enable the encryption
1479
				// php 5.6+ fix
1480
				$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
1481
1482
				if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT'))
1483
				{
1484
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
1485
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
1486
				}
1487
1488
				if (!@stream_socket_enable_crypto($socket, true, $crypto_method))
1489
					return false;
1490
				// Send the EHLO command again
1491
				if (!server_parse('EHLO ' . $helo, $socket, null) == '250')
1492
					return false;
1493
			}
1494
1495
			if (!server_parse('AUTH LOGIN', $socket, '334'))
1496
				return false;
1497
			// Send the username and password, encoded.
1498
			if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1499
				return false;
1500
			// The password is already encoded ;)
1501
			if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1502
				return false;
1503
		}
1504
		elseif (!server_parse('HELO ' . $helo, $socket, '250'))
1505
			return false;
1506
	}
1507
	else
1508
	{
1509
		// Just say "helo".
1510
		if (!server_parse('HELO ' . $helo, $socket, '250'))
1511
			return false;
1512
	}
1513
1514
	// Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1515
	$message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1516
1517
	// !! Theoretically, we should be able to just loop the RCPT TO.
1518
	$mail_to_array = array_values($mail_to_array);
1519
	foreach ($mail_to_array as $i => $mail_to)
1520
	{
1521
		// Reset the connection to send another email.
1522
		if ($i != 0)
1523
		{
1524
			if (!server_parse('RSET', $socket, '250'))
1525
				return false;
1526
		}
1527
1528
		// From, to, and then start the data...
1529
		if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1530
			return false;
1531
		if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1532
			return false;
1533
		if (!server_parse('DATA', $socket, '354'))
1534
			return false;
1535
		fputs($socket, 'Subject: ' . $subject . "\r\n");
1536
		if (strlen($mail_to) > 0)
1537
			fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1538
		fputs($socket, $headers . "\r\n\r\n");
1539
		fputs($socket, $message . "\r\n");
1540
1541
		// Send a ., or in other words "end of data".
1542
		if (!server_parse('.', $socket, '250'))
1543
			return false;
1544
1545
		// Almost done, almost done... don't stop me just yet!
1546
		@set_time_limit(300);
1547
		if (function_exists('apache_reset_timeout'))
1548
			@apache_reset_timeout();
1549
	}
1550
	fputs($socket, 'QUIT' . "\r\n");
1551
	fclose($socket);
1552
1553
	return true;
1554
}
1555
1556
/**
1557
 * Parse a message to the SMTP server.
1558
 * Sends the specified message to the server, and checks for the
1559
 * expected response.
1560
 *
1561
 * @internal
1562
 *
1563
 * @param string $message The message to send
1564
 * @param resource $socket Socket to send on
1565
 * @param string $code The expected response code
1566
 * @param string $response The response from the SMTP server
1567
 * @return bool Whether it responded as such.
1568
 */
1569
function server_parse($message, $socket, $code, &$response = null)
1570
{
1571
	global $txt;
1572
1573
	if ($message !== null)
0 ignored issues
show
introduced by
The condition $message !== null is always true.
Loading history...
1574
		fputs($socket, $message . "\r\n");
1575
1576
	// No response yet.
1577
	$server_response = '';
1578
1579
	while (substr($server_response, 3, 1) != ' ')
1580
	{
1581
		if (!($server_response = fgets($socket, 256)))
1582
		{
1583
			// @todo Change this message to reflect that it may mean bad user/password/server issues/etc.
1584
			log_error($txt['smtp_bad_response']);
1585
			return false;
1586
		}
1587
		$response .= $server_response;
1588
	}
1589
1590
	if ($code === null)
0 ignored issues
show
introduced by
The condition $code === null is always false.
Loading history...
1591
		return substr($server_response, 0, 3);
1592
1593
	$response_code = (int) substr($server_response, 0, 3);
1594
	if ($response_code != $code)
1595
	{
1596
		// Ignoreable errors that we can't fix should not be logged.
1597
		/*
1598
		 * 550 - cPanel rejected sending due to DNS issues
1599
		 * 450 - DNS Routing issues
1600
		 * 451 - cPanel "Temporary local problem - please try later"
1601
		 */
1602
		if ($response_code < 500 && !in_array($response_code, array(450, 451)))
1603
			log_error($txt['smtp_error'] . $server_response);
1604
1605
		return false;
1606
	}
1607
1608
	return true;
1609
}
1610
1611
/**
1612
 * Spell checks the post for typos ;).
1613
 * It uses the pspell or enchant library, one of which MUST be installed.
1614
 * It has problems with internationalization.
1615
 * It is accessed via ?action=spellcheck.
1616
 */
1617
function SpellCheck()
1618
{
1619
	global $txt, $context, $smcFunc;
1620
1621
	// A list of "words" we know about but pspell doesn't.
1622
	$known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1623
1624
	loadLanguage('Post');
1625
	loadTemplate('Post');
1626
1627
	// Create a pspell or enchant dictionary resource
1628
	$dict = spell_init();
1629
1630
	if (!isset($_POST['spellstring']) || !$dict)
1631
		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...
1632
1633
	// Construct a bit of Javascript code.
1634
	$context['spell_js'] = '
1635
		var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1636
		var mispstr = window.opener.spellCheckGetText(spell_fieldname);
1637
		var misps = Array(';
1638
1639
	// Get all the words (Javascript already separated them).
1640
	$alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1641
1642
	$found_words = false;
1643
	for ($i = 0, $n = count($alphas); $i < $n; $i++)
1644
	{
1645
		// Words are sent like 'word|offset_begin|offset_end'.
1646
		$check_word = explode('|', $alphas[$i]);
1647
1648
		// If the word is a known word, or spelled right...
1649
		if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || spell_check($dict, $check_word[0]) || !isset($check_word[2]))
1650
			continue;
1651
1652
		// Find the word, and move up the "last occurrence" to here.
1653
		$found_words = true;
1654
1655
		// Add on the javascript for this misspelling.
1656
		$context['spell_js'] .= '
1657
			new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '&gt;' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1658
1659
		// If there are suggestions, add them in...
1660
		$suggestions = spell_suggest($dict, $check_word[0]);
1661
		if (!empty($suggestions))
1662
		{
1663
			// But first check they aren't going to be censored - no naughty words!
1664
			foreach ($suggestions as $k => $word)
1665
				if ($suggestions[$k] != censorText($word))
1666
					unset($suggestions[$k]);
1667
1668
			if (!empty($suggestions))
1669
				$context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1670
		}
1671
1672
		$context['spell_js'] .= ']),';
1673
	}
1674
1675
	// If words were found, take off the last comma.
1676
	if ($found_words)
1677
		$context['spell_js'] = substr($context['spell_js'], 0, -1);
1678
1679
	$context['spell_js'] .= '
1680
		);';
1681
1682
	// And instruct the template system to just show the spellcheck sub template.
1683
	$context['template_layers'] = array();
1684
	$context['sub_template'] = 'spellcheck';
1685
1686
	// Free resources for enchant...
1687
	if (isset($context['enchant_broker']))
1688
	{
1689
		enchant_broker_free_dict($dict);
1690
		enchant_broker_free($context['enchant_broker']);
1691
	}
1692
}
1693
1694
/**
1695
 * Sends a notification to members who have elected to receive emails
1696
 * when things happen to a topic, such as replies are posted.
1697
 * The function automatically finds the subject and its board, and
1698
 * checks permissions for each member who is "signed up" for notifications.
1699
 * It will not send 'reply' notifications more than once in a row.
1700
 * Uses Post language file
1701
 *
1702
 * @param array $topics Represents the topics the action is happening to.
1703
 * @param string $type Can be any of reply, sticky, lock, unlock, remove, move, merge, and split.  An appropriate message will be sent for each.
1704
 * @param array $exclude Members in the exclude array will not be processed for the topic with the same key.
1705
 * @param array $members_only Are the only ones that will be sent the notification if they have it on.
1706
 */
1707
function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
1708
{
1709
	global $user_info, $smcFunc;
1710
1711
	// Can't do it if there's no topics.
1712
	if (empty($topics))
1713
		return;
1714
	// It must be an array - it must!
1715
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
1716
		$topics = array($topics);
1717
1718
	// Get the subject and body...
1719
	$result = $smcFunc['db_query']('', '
1720
		SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board,
1721
			COALESCE(mem.real_name, ml.poster_name) AS poster_name, mf.id_msg
1722
		FROM {db_prefix}topics AS t
1723
			INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1724
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1725
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1726
		WHERE t.id_topic IN ({array_int:topic_list})
1727
		LIMIT 1',
1728
		array(
1729
			'topic_list' => $topics,
1730
		)
1731
	);
1732
	$task_rows = array();
1733
	while ($row = $smcFunc['db_fetch_assoc']($result))
1734
	{
1735
		$task_rows[] = array(
1736
			'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
1737
				'msgOptions' => array(
1738
					'id' => $row['id_msg'],
1739
					'subject' => $row['subject'],
1740
					'body' => $row['body'],
1741
				),
1742
				'topicOptions' => array(
1743
					'id' => $row['id_topic'],
1744
					'board' => $row['id_board'],
1745
				),
1746
				// Kinda cheeky, but for any action the originator is usually the current user
1747
				'posterOptions' => array(
1748
					'id' => $user_info['id'],
1749
					'name' => $user_info['name'],
1750
				),
1751
				'type' => $type,
1752
				'members_only' => $members_only,
1753
			)), 0
1754
		);
1755
	}
1756
	$smcFunc['db_free_result']($result);
1757
1758
	if (!empty($task_rows))
1759
		$smcFunc['db_insert']('',
1760
			'{db_prefix}background_tasks',
1761
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1762
			$task_rows,
1763
			array('id_task')
1764
		);
1765
}
1766
1767
/**
1768
 * Create a post, either as new topic (id_topic = 0) or in an existing one.
1769
 * The input parameters of this function assume:
1770
 * - Strings have been escaped.
1771
 * - Integers have been cast to integer.
1772
 * - Mandatory parameters are set.
1773
 *
1774
 * @param array $msgOptions An array of information/options for the post
1775
 * @param array $topicOptions An array of information/options for the topic
1776
 * @param array $posterOptions An array of information/options for the poster
1777
 * @return bool Whether the operation was a success
1778
 */
1779
function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1780
{
1781
	global $user_info, $txt, $modSettings, $smcFunc, $sourcedir;
1782
1783
	require_once($sourcedir . '/Mentions.php');
1784
1785
	// Set optional parameters to the default value.
1786
	$msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1787
	$msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1788
	$msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1789
	$msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1790
	$msgOptions['poster_time'] = isset($msgOptions['poster_time']) ? (int) $msgOptions['poster_time'] : time();
1791
	$topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1792
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1793
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1794
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1795
	$topicOptions['redirect_expires'] = isset($topicOptions['redirect_expires']) ? $topicOptions['redirect_expires'] : null;
1796
	$topicOptions['redirect_topic'] = isset($topicOptions['redirect_topic']) ? $topicOptions['redirect_topic'] : null;
1797
	$posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1798
	$posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1799
1800
	// Not exactly a post option but it allows hooks and/or other sources to skip sending notifications if they don't want to
1801
	$msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true;
1802
1803
	// We need to know if the topic is approved. If we're told that's great - if not find out.
1804
	if (!$modSettings['postmod_active'])
1805
		$topicOptions['is_approved'] = true;
1806
	elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1807
	{
1808
		$request = $smcFunc['db_query']('', '
1809
			SELECT approved
1810
			FROM {db_prefix}topics
1811
			WHERE id_topic = {int:id_topic}
1812
			LIMIT 1',
1813
			array(
1814
				'id_topic' => $topicOptions['id'],
1815
			)
1816
		);
1817
		list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1818
		$smcFunc['db_free_result']($request);
1819
	}
1820
1821
	// If nothing was filled in as name/e-mail address, try the member table.
1822
	if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1823
	{
1824
		if (empty($posterOptions['id']))
1825
		{
1826
			$posterOptions['id'] = 0;
1827
			$posterOptions['name'] = $txt['guest_title'];
1828
			$posterOptions['email'] = '';
1829
		}
1830
		elseif ($posterOptions['id'] != $user_info['id'])
1831
		{
1832
			$request = $smcFunc['db_query']('', '
1833
				SELECT member_name, email_address
1834
				FROM {db_prefix}members
1835
				WHERE id_member = {int:id_member}
1836
				LIMIT 1',
1837
				array(
1838
					'id_member' => $posterOptions['id'],
1839
				)
1840
			);
1841
			// Couldn't find the current poster?
1842
			if ($smcFunc['db_num_rows']($request) == 0)
1843
			{
1844
				loadLanguage('Errors');
1845
				trigger_error(sprintf($txt['create_post_invalid_member_id'], $posterOptions['id']), E_USER_NOTICE);
1846
				$posterOptions['id'] = 0;
1847
				$posterOptions['name'] = $txt['guest_title'];
1848
				$posterOptions['email'] = '';
1849
			}
1850
			else
1851
				list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1852
			$smcFunc['db_free_result']($request);
1853
		}
1854
		else
1855
		{
1856
			$posterOptions['name'] = $user_info['name'];
1857
			$posterOptions['email'] = $user_info['email'];
1858
		}
1859
	}
1860
1861
	// Get any members who were quoted in this post.
1862
	$msgOptions['quoted_members'] = Mentions::getQuotedMembers($msgOptions['body'], $posterOptions['id']);
1863
1864
	if (!empty($modSettings['enable_mentions']))
1865
	{
1866
		// Get any members who were possibly mentioned
1867
		$msgOptions['mentioned_members'] = Mentions::getMentionedMembers($msgOptions['body']);
1868
		if (!empty($msgOptions['mentioned_members']))
1869
		{
1870
			// Replace @name with [member=id]name[/member]
1871
			$msgOptions['body'] = Mentions::getBody($msgOptions['body'], $msgOptions['mentioned_members']);
1872
1873
			// Remove any members who weren't actually mentioned, to prevent bogus notifications
1874
			$msgOptions['mentioned_members'] = Mentions::verifyMentionedMembers($msgOptions['body'], $msgOptions['mentioned_members']);
1875
		}
1876
	}
1877
1878
	// It's do or die time: forget any user aborts!
1879
	$previous_ignore_user_abort = ignore_user_abort(true);
1880
1881
	$new_topic = empty($topicOptions['id']);
1882
1883
	$message_columns = array(
1884
		'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')),
1885
		'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'inet',
1886
		'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1887
	);
1888
1889
	$message_parameters = array(
1890
		$topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1891
		$posterOptions['name'], $posterOptions['email'], $msgOptions['poster_time'], $posterOptions['ip'],
1892
		$msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1893
	);
1894
1895
	// What if we want to do anything with posts?
1896
	call_integration_hook('integrate_create_post', array(&$msgOptions, &$topicOptions, &$posterOptions, &$message_columns, &$message_parameters));
1897
1898
	// Insert the post.
1899
	$msgOptions['id'] = $smcFunc['db_insert']('',
1900
		'{db_prefix}messages',
1901
		$message_columns,
1902
		$message_parameters,
1903
		array('id_msg'),
1904
		1
1905
	);
1906
1907
	// Something went wrong creating the message...
1908
	if (empty($msgOptions['id']))
1909
		return false;
1910
1911
	// Fix the attachments.
1912
	if (!empty($msgOptions['attachments']))
1913
		$smcFunc['db_query']('', '
1914
			UPDATE {db_prefix}attachments
1915
			SET id_msg = {int:id_msg}
1916
			WHERE id_attach IN ({array_int:attachment_list})',
1917
			array(
1918
				'attachment_list' => $msgOptions['attachments'],
1919
				'id_msg' => $msgOptions['id'],
1920
			)
1921
		);
1922
1923
	// What if we want to export new posts out to a CMS?
1924
	call_integration_hook('integrate_after_create_post', array($msgOptions, $topicOptions, $posterOptions, $message_columns, $message_parameters));
1925
1926
	// Insert a new topic (if the topicID was left empty.)
1927
	if ($new_topic)
1928
	{
1929
		$topic_columns = array(
1930
			'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1931
			'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1932
			'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1933
			'redirect_expires' => 'int', 'id_redirect_topic' => 'int',
1934
		);
1935
		$topic_parameters = array(
1936
			$topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1937
			$msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1938
			$topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1939
			$topicOptions['redirect_expires'] === null ? 0 : $topicOptions['redirect_expires'], $topicOptions['redirect_topic'] === null ? 0 : $topicOptions['redirect_topic'],
1940
		);
1941
1942
		call_integration_hook('integrate_before_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions, &$topic_columns, &$topic_parameters));
1943
1944
		$topicOptions['id'] = $smcFunc['db_insert']('',
1945
			'{db_prefix}topics',
1946
			$topic_columns,
1947
			$topic_parameters,
1948
			array('id_topic'),
1949
			1
1950
		);
1951
1952
		// The topic couldn't be created for some reason.
1953
		if (empty($topicOptions['id']))
1954
		{
1955
			// We should delete the post that did work, though...
1956
			$smcFunc['db_query']('', '
1957
				DELETE FROM {db_prefix}messages
1958
				WHERE id_msg = {int:id_msg}',
1959
				array(
1960
					'id_msg' => $msgOptions['id'],
1961
				)
1962
			);
1963
1964
			return false;
1965
		}
1966
1967
		// Fix the message with the topic.
1968
		$smcFunc['db_query']('', '
1969
			UPDATE {db_prefix}messages
1970
			SET id_topic = {int:id_topic}
1971
			WHERE id_msg = {int:id_msg}',
1972
			array(
1973
				'id_topic' => $topicOptions['id'],
1974
				'id_msg' => $msgOptions['id'],
1975
			)
1976
		);
1977
1978
		// There's been a new topic AND a new post today.
1979
		trackStats(array('topics' => '+', 'posts' => '+'));
1980
1981
		updateStats('topic', true);
1982
		updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1983
1984
		// What if we want to export new topics out to a CMS?
1985
		call_integration_hook('integrate_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions));
1986
	}
1987
	// The topic already exists, it only needs a little updating.
1988
	else
1989
	{
1990
		$update_parameters = array(
1991
			'poster_id' => $posterOptions['id'],
1992
			'id_msg' => $msgOptions['id'],
1993
			'locked' => $topicOptions['lock_mode'],
1994
			'is_sticky' => $topicOptions['sticky_mode'],
1995
			'id_topic' => $topicOptions['id'],
1996
			'counter_increment' => 1,
1997
		);
1998
		if ($msgOptions['approved'])
1999
			$topics_columns = array(
2000
				'id_member_updated = {int:poster_id}',
2001
				'id_last_msg = {int:id_msg}',
2002
				'num_replies = num_replies + {int:counter_increment}',
2003
			);
2004
		else
2005
			$topics_columns = array(
2006
				'unapproved_posts = unapproved_posts + {int:counter_increment}',
2007
			);
2008
		if ($topicOptions['lock_mode'] !== null)
2009
			$topics_columns[] = 'locked = {int:locked}';
2010
		if ($topicOptions['sticky_mode'] !== null)
2011
			$topics_columns[] = 'is_sticky = {int:is_sticky}';
2012
2013
		call_integration_hook('integrate_modify_topic', array(&$topics_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions));
2014
2015
		// Update the number of replies and the lock/sticky status.
2016
		$smcFunc['db_query']('', '
2017
			UPDATE {db_prefix}topics
2018
			SET
2019
				' . implode(', ', $topics_columns) . '
2020
			WHERE id_topic = {int:id_topic}',
2021
			$update_parameters
2022
		);
2023
2024
		// One new post has been added today.
2025
		trackStats(array('posts' => '+'));
2026
	}
2027
2028
	// Creating is modifying...in a way.
2029
	// @todo Why not set id_msg_modified on the insert?
2030
	$smcFunc['db_query']('', '
2031
		UPDATE {db_prefix}messages
2032
		SET id_msg_modified = {int:id_msg}
2033
		WHERE id_msg = {int:id_msg}',
2034
		array(
2035
			'id_msg' => $msgOptions['id'],
2036
		)
2037
	);
2038
2039
	// Increase the number of posts and topics on the board.
2040
	if ($msgOptions['approved'])
2041
		$smcFunc['db_query']('', '
2042
			UPDATE {db_prefix}boards
2043
			SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
2044
			WHERE id_board = {int:id_board}',
2045
			array(
2046
				'id_board' => $topicOptions['board'],
2047
			)
2048
		);
2049
	else
2050
	{
2051
		$smcFunc['db_query']('', '
2052
			UPDATE {db_prefix}boards
2053
			SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
2054
			WHERE id_board = {int:id_board}',
2055
			array(
2056
				'id_board' => $topicOptions['board'],
2057
			)
2058
		);
2059
2060
		// Add to the approval queue too.
2061
		$smcFunc['db_insert']('',
2062
			'{db_prefix}approval_queue',
2063
			array(
2064
				'id_msg' => 'int',
2065
			),
2066
			array(
2067
				$msgOptions['id'],
2068
			),
2069
			array()
2070
		);
2071
2072
		$smcFunc['db_insert']('',
2073
			'{db_prefix}background_tasks',
2074
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2075
			array(
2076
				'$sourcedir/tasks/ApprovePost-Notify.php', 'ApprovePost_Notify_Background', $smcFunc['json_encode'](array(
2077
					'msgOptions' => $msgOptions,
2078
					'topicOptions' => $topicOptions,
2079
					'posterOptions' => $posterOptions,
2080
					'type' => $new_topic ? 'topic' : 'post',
2081
				)), 0
2082
			),
2083
			array('id_task')
2084
		);
2085
	}
2086
2087
	// Mark inserted topic as read (only for the user calling this function).
2088
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2089
	{
2090
		// Since it's likely they *read* it before replying, let's try an UPDATE first.
2091
		if (!$new_topic)
2092
		{
2093
			$smcFunc['db_query']('', '
2094
				UPDATE {db_prefix}log_topics
2095
				SET id_msg = {int:id_msg}
2096
				WHERE id_member = {int:current_member}
2097
					AND id_topic = {int:id_topic}',
2098
				array(
2099
					'current_member' => $posterOptions['id'],
2100
					'id_msg' => $msgOptions['id'],
2101
					'id_topic' => $topicOptions['id'],
2102
				)
2103
			);
2104
2105
			$flag = $smcFunc['db_affected_rows']() != 0;
2106
		}
2107
2108
		if (empty($flag))
2109
		{
2110
			$smcFunc['db_insert']('ignore',
2111
				'{db_prefix}log_topics',
2112
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2113
				array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
2114
				array('id_topic', 'id_member')
2115
			);
2116
		}
2117
	}
2118
2119
	if ($msgOptions['approved'] && empty($topicOptions['is_approved']) && $posterOptions['id'] != $user_info['id'])
2120
		$smcFunc['db_insert']('',
2121
			'{db_prefix}background_tasks',
2122
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2123
			array(
2124
				'$sourcedir/tasks/ApproveReply-Notify.php', 'ApproveReply_Notify_Background', $smcFunc['json_encode'](array(
2125
					'msgOptions' => $msgOptions,
2126
					'topicOptions' => $topicOptions,
2127
					'posterOptions' => $posterOptions,
2128
				)), 0
2129
			),
2130
			array('id_task')
2131
		);
2132
2133
	// If there's a custom search index, it may need updating...
2134
	require_once($sourcedir . '/Search.php');
2135
	$searchAPI = findSearchAPI();
2136
	if (is_callable(array($searchAPI, 'postCreated')))
2137
		$searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
2138
2139
	// Increase the post counter for the user that created the post.
2140
	if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
2141
	{
2142
		// Are you the one that happened to create this post?
2143
		if ($user_info['id'] == $posterOptions['id'])
2144
			$user_info['posts']++;
2145
		updateMemberData($posterOptions['id'], array('posts' => '+'));
2146
	}
2147
2148
	// They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2149
	$_SESSION['last_read_topic'] = 0;
2150
2151
	// Better safe than sorry.
2152
	if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2153
		$_SESSION['topicseen_cache'][$topicOptions['board']]--;
2154
2155
	// Keep track of quotes and mentions.
2156
	if (!empty($msgOptions['quoted_members']))
2157
		Mentions::insertMentions('quote', $msgOptions['id'], $msgOptions['quoted_members'], $posterOptions['id']);
2158
	if (!empty($msgOptions['mentioned_members']))
2159
		Mentions::insertMentions('msg', $msgOptions['id'], $msgOptions['mentioned_members'], $posterOptions['id']);
2160
2161
	// Update all the stats so everyone knows about this new topic and message.
2162
	updateStats('message', true, $msgOptions['id']);
2163
2164
	// Update the last message on the board assuming it's approved AND the topic is.
2165
	if ($msgOptions['approved'])
2166
		updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2167
2168
	// Queue createPost background notification
2169
	if ($msgOptions['send_notifications'] && $msgOptions['approved'])
2170
		$smcFunc['db_insert']('',
2171
			'{db_prefix}background_tasks',
2172
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2173
			array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2174
				'msgOptions' => $msgOptions,
2175
				'topicOptions' => $topicOptions,
2176
				'posterOptions' => $posterOptions,
2177
				'type' => $new_topic ? 'topic' : 'reply',
2178
			)), 0),
2179
			array('id_task')
2180
		);
2181
2182
	// Alright, done now... we can abort now, I guess... at least this much is done.
2183
	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

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

3143
		return pspell_check(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3144
	}
3145
}
3146
3147
/**
3148
 * spell_suggest()
3149
 *
3150
 * Returns an array of suggested replacements for the specified word
3151
 *
3152
 * @param resource $dict An enchant or pspell dictionary resource
3153
 * @param string $word A misspelled word
3154
 * @return array An array of suggested replacements for the misspelled word
3155
 */
3156
function spell_suggest($dict, $word)
3157
{
3158
	global $context, $txt;
3159
3160
	if ($context['provider'] == 'enchant')
3161
	{
3162
		// If we're not using UTF-8, we need iconv to handle some stuff...
3163
		if (!$context['spell_utf8'])
3164
		{
3165
			// Convert the word to UTF-8 before getting suggestions
3166
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
3167
			$suggestions = enchant_dict_suggest($dict, $word);
3168
3169
			// Go through the suggestions and convert them back to the proper character set
3170
			foreach ($suggestions as $index => $suggestion)
3171
			{
3172
				// //TRANSLIT makes it use similar-looking characters for incompatible ones...
3173
				$suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion);
3174
			}
3175
3176
			return $suggestions;
3177
		}
3178
		else
3179
		{
3180
			return enchant_dict_suggest($dict, $word);
3181
		}
3182
	}
3183
	elseif ($context['provider'] == 'pspell')
3184
	{
3185
		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

3185
		return pspell_suggest(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3186
	}
3187
}
3188
3189
?>