sendmail()   F
last analyzed

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 198
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

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

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 http://www.simplemachines.org
12
 * @copyright 2019 Simple Machines and individual contributors
13
 * @license http://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
262
/**
263
 * This is very simple, and just removes things done by preparsecode.
264
 *
265
 * @param string $message The message
266
 */
267
function un_preparsecode($message)
268
{
269
	global $smcFunc;
270
271
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
272
273
	// We're going to unparse only the stuff outside [code]...
274
	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

274
	for ($i = 0, $n = count(/** @scrutinizer ignore-type */ $parts); $i < $n; $i++)
Loading history...
275
	{
276
		// If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
277
		if ($i % 4 == 2)
278
		{
279
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
280
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
281
			$code_tags[$substitute] = $code_tag;
282
			$parts[$i] = $i;
283
		}
284
	}
285
286
	$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

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

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

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

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

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

3010
		return pspell_suggest(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
3011
	}
3012
}
3013
3014
?>