Failed Conditions
Push — release-2.1 ( 8d1977...8da17b )
by Rick
06:19
created

AddMailQueue()   B

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
nop 8
dl 0
loc 87
rs 7.3166
c 0
b 0
f 0
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 2020 Simple Machines and individual contributors
13
 * @license https://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 RC2
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;
32
33
	static $tags_regex, $disallowed_tags_regex;
34
35
	// This line makes all languages *theoretically* work even with the wrong charset ;).
36
	if (empty($context['utf8']))
37
		$message = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message);
38
39
	// Clean up after nobbc ;).
40
	$message = preg_replace_callback('~\[nobbc\](.+?)\[/nobbc\]~is', function($a)
41
	{
42
		return '[nobbc]' . strtr($a[1], array('[' => '&#91;', ']' => '&#93;', ':' => '&#58;', '@' => '&#64;')) . '[/nobbc]';
43
	}, $message);
44
45
	// Remove \r's... they're evil!
46
	$message = strtr($message, array("\r" => ''));
47
48
	// You won't believe this - but too many periods upsets apache it seems!
49
	$message = preg_replace('~\.{100,}~', '...', $message);
50
51
	// Trim off trailing quotes - these often happen by accident.
52
	while (substr($message, -7) == '[quote]')
53
		$message = substr($message, 0, -7);
54
	while (substr($message, 0, 8) == '[/quote]')
55
		$message = substr($message, 8);
56
57
	if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay'))
58
		$message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message);
59
60
	// Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed.
61
	$in_tag = false;
62
	$had_tag = false;
63
	$codeopen = 0;
64
	if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches))
65
		foreach ($matches[0] as $index => $dummy)
66
		{
67
			// Closing?
68
			if (!empty($matches[2][$index]))
69
			{
70
				// If it's closing and we're not in a tag we need to open it...
71
				if (!$in_tag)
72
					$codeopen = true;
73
				// Either way we ain't in one any more.
74
				$in_tag = false;
75
			}
76
			// Opening tag...
77
			else
78
			{
79
				$had_tag = true;
80
				// If we're in a tag don't do nought!
81
				if (!$in_tag)
82
					$in_tag = true;
83
			}
84
		}
85
86
	// If we have an open tag, close it.
87
	if ($in_tag)
88
		$message .= '[/code]';
89
	// Open any ones that need to be open, only if we've never had a tag.
90
	if ($codeopen && !$had_tag)
91
		$message = '[code]' . $message;
92
93
	// Replace code BBC with placeholders. We'll restore them at the end.
94
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
95
	for ($i = 0, $n = count($parts); $i < $n; $i++)
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

95
	for ($i = 0, $n = count(/** @scrutinizer ignore-type */ $parts); $i < $n; $i++)
Loading history...
96
	{
97
		// It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
98
		if ($i % 4 == 2)
99
		{
100
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
101
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
102
			$code_tags[$substitute] = $code_tag;
103
			$parts[$i] = $i;
104
		}
105
	}
106
107
	$message = implode('', $parts);
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

107
	$message = implode('', /** @scrutinizer ignore-type */ $parts);
Loading history...
108
109
	// The regular expression non breaking space has many versions.
110
	$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
111
112
	// Now that we've fixed all the code tags, let's fix the img and url tags...
113
	fixTags($message);
114
115
	// Replace /me.+?\n with [me=name]dsf[/me]\n.
116
	if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false)
117
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=&quot;' . $user_info['name'] . '&quot;]$2[/me]', $message);
118
	else
119
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $message);
120
121
	if (!$previewing && strpos($message, '[html]') !== false)
122
	{
123
		if (allowedTo('bbc_html'))
124
			$message = preg_replace_callback('~\[html\](.+?)\[/html\]~is', function($m)
125
			{
126
				return '[html]' . strtr(un_htmlspecialchars($m[1]), array("\n" => '&#13;', '  ' => ' &#32;', '[' => '&#91;', ']' => '&#93;')) . '[/html]';
127
			}, $message);
128
129
		// We should edit them out, or else if an admin edits the message they will get shown...
130
		else
131
		{
132
			while (strpos($message, '[html]') !== false)
133
				$message = preg_replace('~\[[/]?html\]~i', '', $message);
134
		}
135
	}
136
137
	// Let's look at the time tags...
138
	$message = preg_replace_callback('~\[time(?:=(absolute))*\](.+?)\[/time\]~i', function($m) use ($modSettings, $user_info)
139
	{
140
		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]";
141
	}, $message);
142
143
	// Change the color specific tags to [color=the color].
144
	$message = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $message); // First do the opening tags.
145
	$message = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $message); // And now do the closing tags
146
147
	// Neutralize any BBC tags this member isn't permitted to use.
148
	if (empty($disallowed_tags_regex))
149
	{
150
		// Legacy BBC are only retained for historical reasons. They're not for use in new posts.
151
		$disallowed_bbc = $context['legacy_bbc'];
152
153
		// Some BBC require permissions.
154
		foreach ($context['restricted_bbc'] as $bbc)
155
		{
156
			// Skip html, since we handled it separately above.
157
			if ($bbc === 'html')
158
				continue;
159
			if (!allowedTo('bbc_' . $bbc))
160
				$disallowed_bbc[] = $bbc;
161
		}
162
163
		$disallowed_tags_regex = build_regex(array_unique($disallowed_bbc), '~');
164
	}
165
	if (!empty($disallowed_tags_regex))
166
		$message = preg_replace('~\[(?=/?' . $disallowed_tags_regex . '\b)~i', '&#91;', $message);
167
168
	// Make sure all tags are lowercase.
169
	$message = preg_replace_callback('~\[(/?)(list|li|table|tr|td)\b([^\]]*)\]~i', function($m)
170
	{
171
		return "[$m[1]" . strtolower("$m[2]") . "$m[3]]";
172
	}, $message);
173
174
	$list_open = substr_count($message, '[list]') + substr_count($message, '[list ');
175
	$list_close = substr_count($message, '[/list]');
176
	if ($list_close - $list_open > 0)
177
		$message = str_repeat('[list]', $list_close - $list_open) . $message;
178
	if ($list_open - $list_close > 0)
179
		$message = $message . str_repeat('[/list]', $list_open - $list_close);
180
181
	$mistake_fixes = array(
182
		// Find [table]s not followed by [tr].
183
		'~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]',
184
		// Find [tr]s not followed by [td].
185
		'~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]',
186
		// Find [/td]s not followed by something valid.
187
		'~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]',
188
		// Find [/tr]s not followed by something valid.
189
		'~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]',
190
		// Find [/td]s incorrectly followed by [/table].
191
		'~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]',
192
		// Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td].
193
		'~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]',
194
		// Now, any [td]s left should have a [tr] before them.
195
		'~\[td\]~s' => '[tr][td]',
196
		// Look for [tr]s which are correctly placed.
197
		'~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]',
198
		// Any remaining [tr]s should have a [table] before them.
199
		'~\[tr\]~s' => '[table][tr]',
200
		// Look for [/td]s followed by [/tr].
201
		'~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]',
202
		// Any remaining [/tr]s should have a [/td].
203
		'~\[/tr\]~s' => '[/td][/tr]',
204
		// Look for properly opened [li]s which aren't closed.
205
		'~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]',
206
		'~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]',
207
		'~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]',
208
		// Lists - find correctly closed items/lists.
209
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]',
210
		// Find list items closed and then opened.
211
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]',
212
		// Now, find any [list]s or [/li]s followed by [li].
213
		'~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]',
214
		// Allow for sub lists.
215
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[list]',
216
		'~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' . ($context['utf8'] ? 'u' : '') => '[/list]$1[_li_]',
217
		// Any remaining [li]s weren't inside a [list].
218
		'~\[li\]~' => '[list][li]',
219
		// Any remaining [/li]s weren't before a [/list].
220
		'~\[/li\]~' => '[/li][/list]',
221
		// Put the correct ones back how we found them.
222
		'~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]',
223
		// Images with no real url.
224
		'~\[img\]https?://.{0,7}\[/img\]~' => '',
225
	);
226
227
	// Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.)
228
	for ($j = 0; $j < 3; $j++)
229
		$message = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $message);
230
231
	// Remove empty bbc from the sections outside the code tags
232
	if (empty($tags_regex))
233
	{
234
		require_once($sourcedir . '/Subs.php');
235
236
		$allowed_empty = array('anchor', 'td',);
237
238
		$tags = array();
239
		foreach (($codes = parse_bbc(false)) as $code)
0 ignored issues
show
Bug introduced by
The expression $codes = parse_bbc(false) of type string is not traversable.
Loading history...
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
240
			if (!in_array($code['tag'], $allowed_empty))
241
				$tags[] = $code['tag'];
242
243
		$tags_regex = build_regex($tags, '~');
244
	}
245
	while (preg_match('~\[(' . $tags_regex . ')\b[^\]]*\]\s*\[/\1\]\s?~i', $message))
246
		$message = preg_replace('~\[(' . $tags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', '', $message);
247
248
	// Restore code blocks
249
	if (!empty($code_tags))
250
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
251
252
	// Restore white space entities
253
	if (!$previewing)
254
		$message = strtr($message, array('  ' => '&nbsp; ', "\n" => '<br>', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
255
	else
256
		$message = strtr($message, array('  ' => '&nbsp; ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
257
258
	// Now let's quickly clean up things that will slow our parser (which are common in posted code.)
259
	$message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
260
261
	// Any hooks want to work here?
262
	call_integration_hook('integrate_preparsecode', array(&$message, $previewing));
263
}
264
265
/**
266
 * This is very simple, and just removes things done by preparsecode.
267
 *
268
 * @param string $message The message
269
 */
270
function un_preparsecode($message)
271
{
272
	global $smcFunc;
273
274
	// Any hooks want to work here?
275
	call_integration_hook('integrate_unpreparsecode', array(&$message));
276
277
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
278
279
	// We're going to unparse only the stuff outside [code]...
280
	for ($i = 0, $n = count($parts); $i < $n; $i++)
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

280
	for ($i = 0, $n = count(/** @scrutinizer ignore-type */ $parts); $i < $n; $i++)
Loading history...
281
	{
282
		// If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
283
		if ($i % 4 == 2)
284
		{
285
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
286
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
287
			$code_tags[$substitute] = $code_tag;
288
			$parts[$i] = $i;
289
		}
290
	}
291
292
	$message = implode('', $parts);
0 ignored issues
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

292
	$message = implode('', /** @scrutinizer ignore-type */ $parts);
Loading history...
293
294
	$message = preg_replace_callback('~\[html\](.+?)\[/html\]~i', function($m) use ($smcFunc)
295
	{
296
		return "[html]" . strtr($smcFunc['htmlspecialchars']("$m[1]", ENT_QUOTES), array("\\&quot;" => "&quot;", "&amp;#13;" => "<br>", "&amp;#32;" => " ", "&amp;#91;" => "[", "&amp;#93;" => "]")) . "[/html]";
297
	}, $message);
298
299
	if (strpos($message, '[cowsay') !== false && !allowedTo('bbc_cowsay'))
300
		$message = preg_replace('~\[(/?)cowsay[^\]]*\]~iu', '[$1pre]', $message);
301
302
	// Attempt to un-parse the time to something less awful.
303
	$message = preg_replace_callback('~\[time\](\d{0,10})\[/time\]~i', function($m)
304
	{
305
		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

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

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

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

2979
		return pspell_check(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
2980
	}
2981
}
2982
2983
/**
2984
 * spell_suggest()
2985
 *
2986
 * Returns an array of suggested replacements for the specified word
2987
 *
2988
 * @param resource $dict An enchant or pspell dictionary resource
2989
 * @param string $word A misspelled word
2990
 * @return array An array of suggested replacements for the misspelled word
2991
 */
2992
function spell_suggest($dict, $word)
2993
{
2994
	global $context, $txt;
2995
2996
	if ($context['provider'] == 'enchant')
2997
	{
2998
		// If we're not using UTF-8, we need iconv to handle some stuff...
2999
		if (!$context['spell_utf8'])
3000
		{
3001
			// Convert the word to UTF-8 before getting suggestions
3002
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
3003
			$suggestions = enchant_dict_suggest($dict, $word);
3004
3005
			// Go through the suggestions and convert them back to the proper character set
3006
			foreach ($suggestions as $index => $suggestion)
3007
			{
3008
				// //TRANSLIT makes it use similar-looking characters for incompatible ones...
3009
				$suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion);
3010
			}
3011
3012
			return $suggestions;
3013
		}
3014
		else
3015
		{
3016
			return enchant_dict_suggest($dict, $word);
3017
		}
3018
	}
3019
	elseif ($context['provider'] == 'pspell')
3020
	{
3021
		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

3021
		return pspell_suggest(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3022
	}
3023
}
3024
3025
?>