Passed
Push — release-2.1 ( 0c2197...207d2d )
by Jeremy
05:47
created

sendmail()   F

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 197
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

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

90
	for ($i = 0, $n = count(/** @scrutinizer ignore-type */ $parts); $i < $n; $i++)
Loading history...
91
	{
92
		// It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
93
		if ($i % 4 == 2)
94
		{
95
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
96
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
97
			$code_tags[$substitute] = $code_tag;
98
			$parts[$i] = $i;
99
		}
100
	}
101
102
	$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

102
	$message = implode('', /** @scrutinizer ignore-type */ $parts);
Loading history...
103
104
	// The regular expression non breaking space has many versions.
105
	$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
106
107
	// Now that we've fixed all the code tags, let's fix the img and url tags...
108
	fixTags($message);
109
110
	// Replace /me.+?\n with [me=name]dsf[/me]\n.
111
	if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false)
112
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=&quot;' . $user_info['name'] . '&quot;]$2[/me]', $message);
113
	else
114
		$message = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $message);
115
116
	if (!$previewing && strpos($message, '[html]') !== false)
117
	{
118
		if (allowedTo('admin_forum'))
119
			$message = preg_replace_callback('~\[html\](.+?)\[/html\]~is', function($m) {
120
				return '[html]' . strtr(un_htmlspecialchars($m[1]), array("\n" => '&#13;', '  ' => ' &#32;', '[' => '&#91;', ']' => '&#93;')) . '[/html]';
121
			}, $message);
122
123
		// We should edit them out, or else if an admin edits the message they will get shown...
124
		else
125
		{
126
			while (strpos($message, '[html]') !== false)
127
				$message = preg_replace('~\[[/]?html\]~i', '', $message);
128
		}
129
	}
130
131
	// Let's look at the time tags...
132
	$message = preg_replace_callback('~\[time(?:=(absolute))*\](.+?)\[/time\]~i', function($m) use ($modSettings, $user_info)
133
	{
134
		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]";
135
	}, $message);
136
137
	// Change the color specific tags to [color=the color].
138
	$message = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $message); // First do the opening tags.
139
	$message = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $message); // And now do the closing tags
140
141
	// Make sure all tags are lowercase.
142
	$message = preg_replace_callback('~\[([/]?)(list|li|table|tr|td)((\s[^\]]+)*)\]~i', function($m)
143
	{
144
		return "[$m[1]" . strtolower("$m[2]") . "$m[3]]";
145
	}, $message);
146
147
	$list_open = substr_count($message, '[list]') + substr_count($message, '[list ');
148
	$list_close = substr_count($message, '[/list]');
149
	if ($list_close - $list_open > 0)
150
		$message = str_repeat('[list]', $list_close - $list_open) . $message;
151
	if ($list_open - $list_close > 0)
152
		$message = $message . str_repeat('[/list]', $list_open - $list_close);
153
154
	$mistake_fixes = array(
155
		// Find [table]s not followed by [tr].
156
		'~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]',
157
		// Find [tr]s not followed by [td].
158
		'~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]',
159
		// Find [/td]s not followed by something valid.
160
		'~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]',
161
		// Find [/tr]s not followed by something valid.
162
		'~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]',
163
		// Find [/td]s incorrectly followed by [/table].
164
		'~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]',
165
		// Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td].
166
		'~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]',
167
		// Now, any [td]s left should have a [tr] before them.
168
		'~\[td\]~s' => '[tr][td]',
169
		// Look for [tr]s which are correctly placed.
170
		'~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]',
171
		// Any remaining [tr]s should have a [table] before them.
172
		'~\[tr\]~s' => '[table][tr]',
173
		// Look for [/td]s followed by [/tr].
174
		'~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]',
175
		// Any remaining [/tr]s should have a [/td].
176
		'~\[/tr\]~s' => '[/td][/tr]',
177
		// Look for properly opened [li]s which aren't closed.
178
		'~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]',
179
		'~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]',
180
		'~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]',
181
		// Lists - find correctly closed items/lists.
182
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]',
183
		// Find list items closed and then opened.
184
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]',
185
		// Now, find any [list]s or [/li]s followed by [li].
186
		'~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]',
187
		// Allow for sub lists.
188
		'~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[list]',
189
		'~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' . ($context['utf8'] ? 'u' : '') => '[/list]$1[_li_]',
190
		// Any remaining [li]s weren't inside a [list].
191
		'~\[li\]~' => '[list][li]',
192
		// Any remaining [/li]s weren't before a [/list].
193
		'~\[/li\]~' => '[/li][/list]',
194
		// Put the correct ones back how we found them.
195
		'~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]',
196
		// Images with no real url.
197
		'~\[img\]https?://.{0,7}\[/img\]~' => '',
198
	);
199
200
	// Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.)
201
	for ($j = 0; $j < 3; $j++)
202
		$message = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $message);
203
204
	// Remove empty bbc from the sections outside the code tags
205
	$allowedEmpty = array(
206
		'anchor',
207
		'td',
208
	);
209
210
	require_once($sourcedir . '/Subs.php');
211
212
	$alltags = array();
213
	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...
Bug introduced by
false of type false is incompatible with the type string expected by parameter $message of parse_bbc(). ( Ignorable by Annotation )

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

213
	foreach (($codes = parse_bbc(/** @scrutinizer ignore-type */ false)) as $code)
Loading history...
214
		if (!in_array($code['tag'], $allowedEmpty))
215
			$alltags[] = $code['tag'];
216
217
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
218
219
	while (preg_match('~\[(' . $alltags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', $message))
220
		$message = preg_replace('~\[(' . $alltags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', '', $message);
221
222
	// Restore code blocks
223
	if (!empty($code_tags))
224
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
225
226
	// Restore white space entities
227
	if (!$previewing)
228
		$message = strtr($message, array('  ' => '&nbsp; ', "\n" => '<br>', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
229
	else
230
		$message = strtr($message, array('  ' => '&nbsp; ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
231
232
	// Now let's quickly clean up things that will slow our parser (which are common in posted code.)
233
	$message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
234
}
235
236
/**
237
 * This is very simple, and just removes things done by preparsecode.
238
 *
239
 * @param string $message The message
240
 */
241
function un_preparsecode($message)
242
{
243
	global $smcFunc;
244
245
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
246
247
	// We're going to unparse only the stuff outside [code]...
248
	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

248
	for ($i = 0, $n = count(/** @scrutinizer ignore-type */ $parts); $i < $n; $i++)
Loading history...
249
	{
250
		// If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
251
		if ($i % 4 == 2)
252
		{
253
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
254
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
255
			$code_tags[$substitute] = $code_tag;
256
			$parts[$i] = $i;
257
		}
258
	}
259
260
	$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

260
	$message = implode('', /** @scrutinizer ignore-type */ $parts);
Loading history...
261
262
	$message = preg_replace_callback('~\[html\](.+?)\[/html\]~i', function($m) use ($smcFunc)
263
	{
264
		return "[html]" . strtr($smcFunc['htmlspecialchars']("$m[1]", ENT_QUOTES), array("\\&quot;" => "&quot;", "&amp;#13;" => "<br>", "&amp;#32;" => " ", "&amp;#91;" => "[", "&amp;#93;" => "]")) . "[/html]";
265
	}, $message);
266
267
	// Attempt to un-parse the time to something less awful.
268
	$message = preg_replace_callback('~\[time\](\d{0,10})\[/time\]~i', function($m)
269
	{
270
		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

270
		return "[time]" . timeformat(/** @scrutinizer ignore-type */ "$m[1]", false) . "[/time]";
Loading history...
271
	}, $message);
272
273
	if (!empty($code_tags))
274
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
275
276
	// Change breaks back to \n's and &nsbp; back to spaces.
277
	return preg_replace('~<br( /)?' . '>~', "\n", str_replace('&nbsp;', ' ', $message));
278
}
279
280
/**
281
 * Fix any URLs posted - ie. remove 'javascript:'.
282
 * Used by preparsecode, fixes links in message and returns nothing.
283
 *
284
 * @param string $message The message
285
 */
286
function fixTags(&$message)
287
{
288
	global $modSettings;
289
290
	// WARNING: Editing the below can cause large security holes in your forum.
291
	// Edit only if you are sure you know what you are doing.
292
293
	$fixArray = array(
294
		// [img]http://...[/img] or [img width=1]http://...[/img]
295
		array(
296
			'tag' => 'img',
297
			'protocols' => array('http', 'https'),
298
			'embeddedUrl' => false,
299
			'hasEqualSign' => false,
300
			'hasExtra' => true,
301
		),
302
		// [url]http://...[/url]
303
		array(
304
			'tag' => 'url',
305
			'protocols' => array('http', 'https'),
306
			'embeddedUrl' => false,
307
			'hasEqualSign' => false,
308
		),
309
		// [url=http://...]name[/url]
310
		array(
311
			'tag' => 'url',
312
			'protocols' => array('http', 'https'),
313
			'embeddedUrl' => true,
314
			'hasEqualSign' => true,
315
		),
316
		// [iurl]http://...[/iurl]
317
		array(
318
			'tag' => 'iurl',
319
			'protocols' => array('http', 'https'),
320
			'embeddedUrl' => false,
321
			'hasEqualSign' => false,
322
		),
323
		// [iurl=http://...]name[/iurl]
324
		array(
325
			'tag' => 'iurl',
326
			'protocols' => array('http', 'https'),
327
			'embeddedUrl' => true,
328
			'hasEqualSign' => true,
329
		),
330
		// [ftp]ftp://...[/ftp]
331
		array(
332
			'tag' => 'ftp',
333
			'protocols' => array('ftp', 'ftps'),
334
			'embeddedUrl' => false,
335
			'hasEqualSign' => false,
336
		),
337
		// [ftp=ftp://...]name[/ftp]
338
		array(
339
			'tag' => 'ftp',
340
			'protocols' => array('ftp', 'ftps'),
341
			'embeddedUrl' => true,
342
			'hasEqualSign' => true,
343
		),
344
		// [flash]http://...[/flash]
345
		array(
346
			'tag' => 'flash',
347
			'protocols' => array('http', 'https'),
348
			'embeddedUrl' => false,
349
			'hasEqualSign' => false,
350
			'hasExtra' => true,
351
		),
352
	);
353
354
	// Fix each type of tag.
355
	foreach ($fixArray as $param)
356
		fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra']));
357
358
	// Now fix possible security problems with images loading links automatically...
359
	$message = preg_replace_callback('~(\[img.*?\])(.+?)\[/img\]~is', function($m)
360
	{
361
		return "$m[1]" . preg_replace("~action(=|%3d)(?!dlattach)~i", "action-", "$m[2]") . "[/img]";
362
	}, $message);
363
364
}
365
366
/**
367
 * Fix a specific class of tag - ie. url with =.
368
 * Used by fixTags, fixes a specific tag's links.
369
 *
370
 * @param string $message The message
371
 * @param string $myTag The tag
372
 * @param string $protocols The protocols
373
 * @param bool $embeddedUrl Whether it *can* be set to something
374
 * @param bool $hasEqualSign Whether it *is* set to something
375
 * @param bool $hasExtra Whether it can have extra cruft after the begin tag.
376
 */
377
function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false)
378
{
379
	global $boardurl, $scripturl;
380
381
	if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0)
382
		$domain_url = $match[1];
383
	else
384
		$domain_url = $boardurl . '/';
385
386
	$replaces = array();
387
388
	if ($hasEqualSign && $embeddedUrl)
389
	{
390
		$quoted = preg_match('~\[(' . $myTag . ')=&quot;~', $message);
391
		preg_match_all('~\[(' . $myTag . ')=' . ($quoted ? '&quot;(.*?)&quot;' : '([^\]]*?)') . '\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
392
	}
393
	elseif ($hasEqualSign)
394
		preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
395
	else
396
		preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches);
397
398
	foreach ($matches[0] as $k => $dummy)
399
	{
400
		// Remove all leading and trailing whitespace.
401
		$replace = trim($matches[2][$k]);
402
		$this_tag = $matches[1][$k];
403
		$this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k];
404
405
		$found = false;
406
		foreach ($protocols as $protocol)
0 ignored issues
show
Bug introduced by
The expression $protocols of type string is not traversable.
Loading history...
407
		{
408
			$found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0;
409
			if ($found)
410
				break;
411
		}
412
413
		if (!$found && $protocols[0] == 'http')
414
		{
415
			if (substr($replace, 0, 1) == '/' && substr($replace, 0, 2) != '//')
416
				$replace = $domain_url . $replace;
417
			elseif (substr($replace, 0, 1) == '?')
418
				$replace = $scripturl . $replace;
419
			elseif (substr($replace, 0, 1) == '#' && $embeddedUrl)
420
			{
421
				$replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1));
422
				$this_tag = 'iurl';
423
				$this_close = 'iurl';
424
			}
425
			elseif (substr($replace, 0, 2) != '//')
426
				$replace = $protocols[0] . '://' . $replace;
427
		}
428
		elseif (!$found && $protocols[0] == 'ftp')
429
			$replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace);
430
		elseif (!$found)
431
			$replace = $protocols[0] . '://' . $replace;
432
433
		if ($hasEqualSign && $embeddedUrl)
434
			$replaces[$matches[0][$k]] = '[' . $this_tag . '=&quot;' . $replace . '&quot;]' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']');
435
		elseif ($hasEqualSign)
436
			$replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']';
437
		elseif ($embeddedUrl)
438
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']';
439
		else
440
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']';
441
	}
442
443
	foreach ($replaces as $k => $v)
444
	{
445
		if ($k == $v)
446
			unset($replaces[$k]);
447
	}
448
449
	if (!empty($replaces))
450
		$message = strtr($message, $replaces);
451
}
452
453
/**
454
 * This function sends an email to the specified recipient(s).
455
 * It uses the mail_type settings and webmaster_email variable.
456
 *
457
 * @param array $to The email(s) to send to
458
 * @param string $subject Email subject, expected to have entities, and slashes, but not be parsed
459
 * @param string $message Email body, expected to have slashes, no htmlentities
460
 * @param string $from The address to use for replies
461
 * @param string $message_id If specified, it will be used as local part of the Message-ID header.
462
 * @param bool $send_html Whether or not the message is HTML vs. plain text
463
 * @param int $priority The priority of the message
464
 * @param bool $hotmail_fix Whether to apply the "hotmail fix"
465
 * @param bool $is_private Whether this is private
466
 * @return boolean Whether ot not the email was sent properly.
467
 */
468
function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false)
469
{
470
	global $webmaster_email, $context, $modSettings, $txt, $scripturl;
471
472
	// Use sendmail if it's set or if no SMTP server is set.
473
	$use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
474
475
	// Line breaks need to be \r\n only in windows or for SMTP.
476
	$line_break = $context['server']['is_windows'] || !$use_sendmail ? "\r\n" : "\n";
477
478
	// So far so good.
479
	$mail_result = true;
480
481
	// If the recipient list isn't an array, make it one.
482
	$to_array = is_array($to) ? $to : array($to);
0 ignored issues
show
introduced by
The condition is_array($to) is always true.
Loading history...
483
484
	// Make sure we actually have email addresses to send this to
485
	foreach ($to_array as $k => $v)
486
	{
487
		// This should never happen, but better safe than sorry
488
		if (trim($v) == '')
489
		{
490
			unset($to_array[$k]);
491
		}
492
	}
493
494
	// Nothing left? Nothing else to do
495
	if (empty($to_array))
496
		return true;
497
498
	// Once upon a time, Hotmail could not interpret non-ASCII mails.
499
	// In honour of those days, it's still called the 'hotmail fix'.
500
	if ($hotmail_fix === null)
501
	{
502
		$hotmail_to = array();
503
		foreach ($to_array as $i => $to_address)
504
		{
505
			if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
506
			{
507
				$hotmail_to[] = $to_address;
508
				$to_array = array_diff($to_array, array($to_address));
509
			}
510
		}
511
512
		// Call this function recursively for the hotmail addresses.
513
		if (!empty($hotmail_to))
514
			$mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true, $is_private);
515
516
		// The remaining addresses no longer need the fix.
517
		$hotmail_fix = false;
518
519
		// No other addresses left? Return instantly.
520
		if (empty($to_array))
521
			return $mail_result;
522
	}
523
524
	// Get rid of entities.
525
	$subject = un_htmlspecialchars($subject);
526
	// Make the message use the proper line breaks.
527
	$message = str_replace(array("\r", "\n"), array('', $line_break), $message);
528
529
	// Make sure hotmail mails are sent as HTML so that HTML entities work.
530
	if ($hotmail_fix && !$send_html)
531
	{
532
		$send_html = true;
533
		$message = strtr($message, array($line_break => '<br>' . $line_break));
534
		$message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
535
	}
536
537
	list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
538
	list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
539
540
	// Construct the mail headers...
541
	$headers = 'From: ' . $from_name . ' <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
542
	$headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
543
	$headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
544
	$headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
545
546
	if ($message_id !== null && empty($modSettings['mail_no_message_id']))
547
		$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
548
	$headers .= 'X-Mailer: SMF' . $line_break;
549
550
	// Pass this to the integration before we start modifying the output -- it'll make it easier later.
551
	if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers, &$to_array)), true))
552
		return false;
553
554
	// Save the original message...
555
	$orig_message = $message;
556
557
	// The mime boundary separates the different alternative versions.
558
	$mime_boundary = 'SMF-' . md5($message . time());
559
560
	// Using mime, as it allows to send a plain unencoded alternative.
561
	$headers .= 'Mime-Version: 1.0' . $line_break;
562
	$headers .= 'content-type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
563
	$headers .= 'content-transfer-encoding: 7bit' . $line_break;
564
565
	// Sending HTML?  Let's plop in some basic stuff, then.
566
	if ($send_html)
567
	{
568
		$no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
569
570
		// But, then, dump it and use a plain one for dinosaur clients.
571
		list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
572
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
573
574
		// This is the plain text version.  Even if no one sees it, we need it for spam checkers.
575
		list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
576
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
577
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
578
		$message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
579
580
		// This is the actual HTML message, prim and proper.  If we wanted images, they could be inlined here (with multipart/related, etc.)
581
		list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
582
		$message .= 'content-type: text/html; charset=' . $charset . $line_break;
583
		$message .= 'content-transfer-encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
584
		$message .= $html_message . $line_break . '--' . $mime_boundary . '--';
585
	}
586
	// Text is good too.
587
	else
588
	{
589
		// Send a plain message first, for the older web clients.
590
		list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
591
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
592
593
		// Now add an encoded message using the forum's character set.
594
		list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
595
		$message .= 'content-type: text/plain; charset=' . $charset . $line_break;
596
		$message .= 'content-transfer-encoding: ' . $encoding . $line_break . $line_break;
597
		$message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
598
	}
599
600
	// Are we using the mail queue, if so this is where we butt in...
601
	if ($priority != 0)
602
		return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
603
604
	// If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
605
	elseif (!empty($modSettings['mail_limit']))
606
	{
607
		list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
608
		if (empty($mails_this_minute) || time() > $last_mail_time + 60)
609
			$new_queue_stat = time() . '|' . 1;
610
		else
611
			$new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
612
613
		updateSettings(array('mail_recent' => $new_queue_stat));
614
	}
615
616
	// SMTP or sendmail?
617
	if ($use_sendmail)
618
	{
619
		$subject = strtr($subject, array("\r" => '', "\n" => ''));
620
		if (!empty($modSettings['mail_strip_carriage']))
621
		{
622
			$message = strtr($message, array("\r" => ''));
623
			$headers = strtr($headers, array("\r" => ''));
624
		}
625
626
		foreach ($to_array as $to)
0 ignored issues
show
introduced by
$to is overwriting one of the parameters of this function.
Loading history...
627
		{
628
			set_error_handler(function($errno, $errstr, $errfile, $errline)
629
				{
630
					// error was suppressed with the @-operator
631
					if (0 === error_reporting()) {
632
						return false;
633
					}
634
635
					throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
636
				}
637
			);
638
			try
639
			{
640
				if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
641
				{
642
					log_error(sprintf($txt['mail_send_unable'], $to));
643
					$mail_result = false;
644
				}
645
			}
646
			catch(ErrorException $e)
647
			{
648
				log_error($e->getMessage(), 'general', $e->getFile(), $e->getLine());
649
				log_error(sprintf($txt['mail_send_unable'], $to));
650
				$mail_result = false;
651
			}
652
			restore_error_handler();
653
654
			// Wait, wait, I'm still sending here!
655
			@set_time_limit(300);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

655
			/** @scrutinizer ignore-unhandled */ @set_time_limit(300);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
656
			if (function_exists('apache_reset_timeout'))
657
				@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for apache_reset_timeout(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

657
				/** @scrutinizer ignore-unhandled */ @apache_reset_timeout();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
658
		}
659
	}
660
	else
661
		$mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
662
663
	// Everything go smoothly?
664
	return $mail_result;
665
}
666
667
/**
668
 * Add an email to the mail queue.
669
 *
670
 * @param bool $flush Whether to flush the queue
671
 * @param array $to_array An array of recipients
672
 * @param string $subject The subject of the message
673
 * @param string $message The message
674
 * @param string $headers The headers
675
 * @param bool $send_html Whether to send in HTML format
676
 * @param int $priority The priority
677
 * @param bool $is_private Whether this is private
678
 * @return boolean Whether the message was added
679
 */
680
function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
681
{
682
	global $context, $smcFunc;
683
684
	static $cur_insert = array();
685
	static $cur_insert_len = 0;
686
687
	if ($cur_insert_len == 0)
688
		$cur_insert = array();
689
690
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
691
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
692
	{
693
		// Only do these once.
694
		$cur_insert_len = 0;
695
696
		// Dump the data...
697
		$smcFunc['db_insert']('',
698
			'{db_prefix}mail_queue',
699
			array(
700
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
701
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
702
			),
703
			$cur_insert,
704
			array('id_mail')
705
		);
706
707
		$cur_insert = array();
708
		$context['flush_mail'] = false;
709
	}
710
711
	// If we're flushing we're done.
712
	if ($flush)
713
	{
714
		$nextSendTime = time() + 10;
715
716
		$smcFunc['db_query']('', '
717
			UPDATE {db_prefix}settings
718
			SET value = {string:nextSendTime}
719
			WHERE variable = {literal:mail_next_send}
720
				AND value = {string:no_outstanding}',
721
			array(
722
				'nextSendTime' => $nextSendTime,
723
				'no_outstanding' => '0',
724
			)
725
		);
726
727
		return true;
728
	}
729
730
	// Ensure we tell obExit to flush.
731
	$context['flush_mail'] = true;
732
733
	foreach ($to_array as $to)
734
	{
735
		// Will this insert go over MySQL's limit?
736
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
737
738
		// Insert limit of 1M (just under the safety) is reached?
739
		if ($this_insert_len + $cur_insert_len > 1000000)
740
		{
741
			// Flush out what we have so far.
742
			$smcFunc['db_insert']('',
743
				'{db_prefix}mail_queue',
744
				array(
745
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
746
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
747
				),
748
				$cur_insert,
749
				array('id_mail')
750
			);
751
752
			// Clear this out.
753
			$cur_insert = array();
754
			$cur_insert_len = 0;
755
		}
756
757
		// Now add the current insert to the array...
758
		$cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
759
		$cur_insert_len += $this_insert_len;
760
	}
761
762
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
763
	if (SMF === 'SSI' || SMF === 'BACKGROUND')
0 ignored issues
show
introduced by
The condition SMF === 'SSI' is always true.
Loading history...
764
		return AddMailQueue(true);
765
766
	return true;
767
}
768
769
/**
770
 * Sends an personal message from the specified person to the specified people
771
 * ($from defaults to the user)
772
 *
773
 * @param array $recipients An array containing the arrays 'to' and 'bcc', both containing id_member's.
774
 * @param string $subject Should have no slashes and no html entities
775
 * @param string $message Should have no slashes and no html entities
776
 * @param bool $store_outbox Whether to store it in the sender's outbox
777
 * @param array $from An array with the id, name, and username of the member.
778
 * @param int $pm_head The ID of the chain being replied to - if any.
779
 * @return array An array with log entries telling how many recipients were successful and which recipients it failed to send to.
780
 */
781
function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
782
{
783
	global $scripturl, $txt, $user_info, $language, $sourcedir;
784
	global $modSettings, $smcFunc;
785
786
	// Make sure the PM language file is loaded, we might need something out of it.
787
	loadLanguage('PersonalMessage');
788
789
	// Initialize log array.
790
	$log = array(
791
		'failed' => array(),
792
		'sent' => array()
793
	);
794
795
	if ($from === null)
796
		$from = array(
797
			'id' => $user_info['id'],
798
			'name' => $user_info['name'],
799
			'username' => $user_info['username']
800
		);
801
802
	// This is the one that will go in their inbox.
803
	$htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
804
	preparsecode($htmlmessage);
805
	$htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => ''));
806
	if ($smcFunc['strlen']($htmlsubject) > 100)
807
		$htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100);
808
809
	// Make sure is an array
810
	if (!is_array($recipients))
0 ignored issues
show
introduced by
The condition is_array($recipients) is always true.
Loading history...
811
		$recipients = array($recipients);
812
813
	// Integrated PMs
814
	call_integration_hook('integrate_personal_message', array(&$recipients, &$from, &$subject, &$message));
815
816
	// Get a list of usernames and convert them to IDs.
817
	$usernames = array();
818
	foreach ($recipients as $rec_type => $rec)
819
	{
820
		foreach ($rec as $id => $member)
821
		{
822
			if (!is_numeric($recipients[$rec_type][$id]))
823
			{
824
				$recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('~[<>&"\'=\\\]~', '', $recipients[$rec_type][$id])));
825
				$usernames[$recipients[$rec_type][$id]] = 0;
826
			}
827
		}
828
	}
829
	if (!empty($usernames))
830
	{
831
		$request = $smcFunc['db_query']('pm_find_username', '
832
			SELECT id_member, member_name
833
			FROM {db_prefix}members
834
			WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
835
			array(
836
				'usernames' => array_keys($usernames),
837
			)
838
		);
839
		while ($row = $smcFunc['db_fetch_assoc']($request))
840
			if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
841
				$usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
842
		$smcFunc['db_free_result']($request);
843
844
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
845
		foreach ($recipients as $rec_type => $rec)
846
			foreach ($rec as $id => $member)
847
			{
848
				if (is_numeric($recipients[$rec_type][$id]))
849
					continue;
850
851
				if (!empty($usernames[$member]))
852
					$recipients[$rec_type][$id] = $usernames[$member];
853
				else
854
				{
855
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
856
					unset($recipients[$rec_type][$id]);
857
				}
858
			}
859
	}
860
861
	// Make sure there are no duplicate 'to' members.
862
	$recipients['to'] = array_unique($recipients['to']);
863
864
	// Only 'bcc' members that aren't already in 'to'.
865
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
866
867
	// Combine 'to' and 'bcc' recipients.
868
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
869
870
	// Check no-one will want it deleted right away!
871
	$request = $smcFunc['db_query']('', '
872
		SELECT
873
			id_member, criteria, is_or
874
		FROM {db_prefix}pm_rules
875
		WHERE id_member IN ({array_int:to_members})
876
			AND delete_pm = {int:delete_pm}',
877
		array(
878
			'to_members' => $all_to,
879
			'delete_pm' => 1,
880
		)
881
	);
882
	$deletes = array();
883
	// Check whether we have to apply anything...
884
	while ($row = $smcFunc['db_fetch_assoc']($request))
885
	{
886
		$criteria = $smcFunc['json_decode']($row['criteria'], true);
887
		// Note we don't check the buddy status, cause deletion from buddy = madness!
888
		$delete = false;
889
		foreach ($criteria as $criterium)
890
		{
891
			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))
892
				$delete = true;
893
			// If we're adding and one criteria don't match then we stop!
894
			elseif (!$row['is_or'])
895
			{
896
				$delete = false;
897
				break;
898
			}
899
		}
900
		if ($delete)
901
			$deletes[$row['id_member']] = 1;
902
	}
903
	$smcFunc['db_free_result']($request);
904
905
	// Load the membergrounp message limits.
906
	// @todo Consider caching this?
907
	static $message_limit_cache = array();
908
	if (!allowedTo('moderate_forum') && empty($message_limit_cache))
909
	{
910
		$request = $smcFunc['db_query']('', '
911
			SELECT id_group, max_messages
912
			FROM {db_prefix}membergroups',
913
			array(
914
			)
915
		);
916
		while ($row = $smcFunc['db_fetch_assoc']($request))
917
			$message_limit_cache[$row['id_group']] = $row['max_messages'];
918
		$smcFunc['db_free_result']($request);
919
	}
920
921
	// Load the groups that are allowed to read PMs.
922
	require_once($sourcedir . '/Subs-Members.php');
923
	$pmReadGroups = groupsAllowedTo('pm_read');
924
925
	if (empty($modSettings['permission_enable_deny']))
926
		$pmReadGroups['denied'] = array();
927
928
	// Load their alert preferences
929
	require_once($sourcedir . '/Subs-Notify.php');
930
	$notifyPrefs = getNotifyPrefs($all_to, array('pm_new', 'pm_reply', 'pm_notify'), true);
931
932
	$request = $smcFunc['db_query']('', '
933
		SELECT
934
			member_name, real_name, id_member, email_address, lngfile,
935
			instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
936
			(pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
937
			(pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
938
			(pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
939
			FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
940
			additional_groups, id_group, id_post_group
941
		FROM {db_prefix}members
942
		WHERE id_member IN ({array_int:recipients})
943
		ORDER BY lngfile
944
		LIMIT {int:count_recipients}',
945
		array(
946
			'not_on_ignore_list' => 1,
947
			'buddies_only' => 2,
948
			'admins_only' => 3,
949
			'recipients' => $all_to,
950
			'count_recipients' => count($all_to),
951
			'from_id' => $from['id'],
952
		)
953
	);
954
	$notifications = array();
955
	while ($row = $smcFunc['db_fetch_assoc']($request))
956
	{
957
		// Don't do anything for members to be deleted!
958
		if (isset($deletes[$row['id_member']]))
959
			continue;
960
961
		// Load the preferences for this member (if any)
962
		$prefs = !empty($notifyPrefs[$row['id_member']]) ? $notifyPrefs[$row['id_member']] : array();
963
		$prefs = array_merge(array(
964
			'pm_new' => 0,
965
			'pm_reply' => 0,
966
			'pm_notify' => 0,
967
		), $prefs);
968
969
		// We need to know this members groups.
970
		$groups = explode(',', $row['additional_groups']);
971
		$groups[] = $row['id_group'];
972
		$groups[] = $row['id_post_group'];
973
974
		$message_limit = -1;
975
		// For each group see whether they've gone over their limit - assuming they're not an admin.
976
		if (!in_array(1, $groups))
977
		{
978
			foreach ($groups as $id)
979
			{
980
				if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
981
					$message_limit = $message_limit_cache[$id];
982
			}
983
984
			if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
985
			{
986
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
987
				unset($all_to[array_search($row['id_member'], $all_to)]);
988
				continue;
989
			}
990
991
			// Do they have any of the allowed groups?
992
			if (count(array_intersect($pmReadGroups['allowed'], $groups)) == 0 || count(array_intersect($pmReadGroups['denied'], $groups)) != 0)
993
			{
994
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
995
				unset($all_to[array_search($row['id_member'], $all_to)]);
996
				continue;
997
			}
998
		}
999
1000
		// Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
1001
		if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id'])
1002
		{
1003
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
1004
			unset($all_to[array_search($row['id_member'], $all_to)]);
1005
			continue;
1006
		}
1007
1008
		// If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
1009
		if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin']))
1010
		{
1011
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1012
			unset($all_to[array_search($row['id_member'], $all_to)]);
1013
			continue;
1014
		}
1015
1016
		// Send a notification, if enabled - taking the buddy list into account.
1017
		if (!empty($row['email_address'])
1018
			&& ((empty($pm_head) && $prefs['pm_new'] & 0x02) || (!empty($pm_head) && $prefs['pm_reply'] & 0x02))
1019
			&& ($prefs['pm_notify'] <= 1 || ($prefs['pm_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
1020
		{
1021
			$notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
1022
		}
1023
1024
		$log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
1025
	}
1026
	$smcFunc['db_free_result']($request);
1027
1028
	// Only 'send' the message if there are any recipients left.
1029
	if (empty($all_to))
1030
		return $log;
1031
1032
	// Insert the message itself and then grab the last insert id.
1033
	$id_pm = $smcFunc['db_insert']('',
1034
		'{db_prefix}personal_messages',
1035
		array(
1036
			'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
1037
			'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
1038
		),
1039
		array(
1040
			$pm_head, $from['id'], ($store_outbox ? 0 : 1),
1041
			$from['username'], time(), $htmlsubject, $htmlmessage,
1042
		),
1043
		array('id_pm'),
1044
		1
1045
	);
1046
1047
	// Add the recipients.
1048
	if (!empty($id_pm))
1049
	{
1050
		// If this is new we need to set it part of it's own conversation.
1051
		if (empty($pm_head))
1052
			$smcFunc['db_query']('', '
1053
				UPDATE {db_prefix}personal_messages
1054
				SET id_pm_head = {int:id_pm_head}
1055
				WHERE id_pm = {int:id_pm_head}',
1056
				array(
1057
					'id_pm_head' => $id_pm,
1058
				)
1059
			);
1060
1061
		// Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
1062
		$smcFunc['db_query']('', '
1063
			DELETE FROM {db_prefix}pm_recipients
1064
			WHERE id_pm = {int:id_pm}',
1065
			array(
1066
				'id_pm' => $id_pm,
1067
			)
1068
		);
1069
1070
		$insertRows = array();
1071
		$to_list = array();
1072
		foreach ($all_to as $to)
1073
		{
1074
			$insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
1075
			if (!in_array($to, $recipients['bcc']))
1076
				$to_list[] = $to;
1077
		}
1078
1079
		$smcFunc['db_insert']('insert',
1080
			'{db_prefix}pm_recipients',
1081
			array(
1082
				'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
1083
			),
1084
			$insertRows,
1085
			array('id_pm', 'id_member')
1086
		);
1087
	}
1088
1089
	censorText($subject);
1090
	if (empty($modSettings['disallow_sendBody']))
1091
	{
1092
		censorText($message);
1093
		$message = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($smcFunc['htmlspecialchars']($message), false), array('<br>' => "\n", '</div>' => "\n", '</li>' => "\n", '&#91;' => '[', '&#93;' => ']')))));
1094
	}
1095
	else
1096
		$message = '';
1097
1098
	$to_names = array();
1099
	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...
1100
	{
1101
		$request = $smcFunc['db_query']('', '
1102
			SELECT real_name
1103
			FROM {db_prefix}members
1104
			WHERE id_member IN ({array_int:to_members})',
1105
			array(
1106
				'to_members' => $to_list,
1107
			)
1108
		);
1109
		while ($row = $smcFunc['db_fetch_assoc']($request))
1110
			$to_names[] = un_htmlspecialchars($row['real_name']);
1111
		$smcFunc['db_free_result']($request);
1112
	}
1113
	$replacements = array(
1114
		'SUBJECT' => $subject,
1115
		'MESSAGE' => $message,
1116
		'SENDER' => un_htmlspecialchars($from['name']),
1117
		'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm,
1118
		'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'],
1119
		'TOLIST' => implode(', ', $to_names),
1120
	);
1121
	$email_template = 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : '');
1122
1123
	foreach ($notifications as $lang => $notification_list)
1124
	{
1125
		$emaildata = loadEmailTemplate($email_template, $replacements, $lang);
1126
1127
		// Off the notification email goes!
1128
		sendmail($notification_list, $emaildata['subject'], $emaildata['body'], null, 'p' . $id_pm, $emaildata['is_html'], 2, null, true);
1129
	}
1130
1131
	// Integrated After PMs
1132
	call_integration_hook('integrate_personal_message_after', array(&$id_pm, &$log, &$recipients, &$from, &$subject, &$message));
1133
1134
	// Back to what we were on before!
1135
	loadLanguage('index+PersonalMessage');
1136
1137
	// Add one to their unread and read message counts.
1138
	foreach ($all_to as $k => $id)
1139
		if (isset($deletes[$id]))
1140
			unset($all_to[$k]);
1141
	if (!empty($all_to))
1142
		updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
1143
1144
	return $log;
1145
}
1146
1147
/**
1148
 * Prepare text strings for sending as email body or header.
1149
 * In case there are higher ASCII characters in the given string, this
1150
 * function will attempt the transport method 'quoted-printable'.
1151
 * Otherwise the transport method '7bit' is used.
1152
 *
1153
 * @param string $string The string
1154
 * @param bool $with_charset Whether we're specifying a charset ($custom_charset must be set here)
1155
 * @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)
1156
 * @param string $line_break The linebreak
1157
 * @param string $custom_charset If set, it uses this character set
1158
 * @return array An array containing the character set, the converted string and the transport method.
1159
 */
1160
function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
1161
{
1162
	global $context;
1163
1164
	$charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
1165
1166
	// This is the fun part....
1167
	if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
1168
	{
1169
		// Let's, for now, assume there are only &#021;'ish characters.
1170
		$simple = true;
1171
1172
		foreach ($matches[1] as $entity)
1173
			if ($entity > 128)
1174
				$simple = false;
1175
		unset($matches);
1176
1177
		if ($simple)
1178
			$string = preg_replace_callback('~&#(\d{3,8});~', function($m)
1179
			{
1180
				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

1180
				return chr(/** @scrutinizer ignore-type */ "$m[1]");
Loading history...
1181
			}, $string);
1182
		else
1183
		{
1184
			// Try to convert the string to UTF-8.
1185
			if (!$context['utf8'] && function_exists('iconv'))
1186
			{
1187
				$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1188
				if ($newstring)
1189
					$string = $newstring;
1190
			}
1191
1192
			$string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string);
1193
1194
			// Unicode, baby.
1195
			$charset = 'UTF-8';
1196
		}
1197
	}
1198
1199
	// Convert all special characters to HTML entities...just for Hotmail :-\
1200
	if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
1201
	{
1202
		if (!$context['utf8'] && function_exists('iconv'))
1203
		{
1204
			$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1205
			if ($newstring)
1206
				$string = $newstring;
1207
		}
1208
1209
		$entityConvert = function($m)
1210
		{
1211
			$c = $m[1];
1212
			if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
1213
				return $c;
1214
			elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
1215
				return "&#" . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ";";
1216
			elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
1217
				return "&#" . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ";";
1218
			elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
1219
				return "&#" . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ";";
1220
			else
1221
				return "";
1222
		};
1223
1224
		// Convert all 'special' characters to HTML entities.
1225
		return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', $entityConvert, $string), '7bit');
1226
	}
1227
1228
	// We don't need to mess with the subject line if no special characters were in it..
1229
	elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
1230
	{
1231
		// Base64 encode.
1232
		$string = base64_encode($string);
1233
1234
		// Show the characterset and the transfer-encoding for header strings.
1235
		if ($with_charset)
1236
			$string = '=?' . $charset . '?B?' . $string . '?=';
1237
1238
		// Break it up in lines (mail body).
1239
		else
1240
			$string = chunk_split($string, 76, $line_break);
1241
1242
		return array($charset, $string, 'base64');
1243
	}
1244
1245
	else
1246
		return array($charset, $string, '7bit');
1247
}
1248
1249
/**
1250
 * Sends mail, like mail() but over SMTP.
1251
 * It expects no slashes or entities.
1252
 * @internal
1253
 *
1254
 * @param array $mail_to_array Array of strings (email addresses)
1255
 * @param string $subject Email subject
1256
 * @param string $message Email message
1257
 * @param string $headers Email headers
1258
 * @return boolean Whether it sent or not.
1259
 */
1260
function smtp_mail($mail_to_array, $subject, $message, $headers)
1261
{
1262
	global $modSettings, $webmaster_email, $txt, $boardurl;
1263
1264
	static $helo;
1265
1266
	$modSettings['smtp_host'] = trim($modSettings['smtp_host']);
1267
1268
	// Try POP3 before SMTP?
1269
	// @todo There's no interface for this yet.
1270
	if ($modSettings['mail_type'] == 3 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1271
	{
1272
		$socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
1273
		if (!$socket && (substr($modSettings['smtp_host'], 0, 5) == 'smtp.' || substr($modSettings['smtp_host'], 0, 11) == 'ssl://smtp.'))
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1274
			$socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
1275
1276
		if ($socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1277
		{
1278
			fgets($socket, 256);
1279
			fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
1280
			fgets($socket, 256);
1281
			fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
1282
			fgets($socket, 256);
1283
			fputs($socket, 'QUIT' . "\r\n");
1284
1285
			fclose($socket);
1286
		}
1287
	}
1288
1289
	// Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
1290
	if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
1291
	{
1292
		// Maybe we can still save this?  The port might be wrong.
1293
		if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
1294
		{
1295
			// ssl:hostname can cause fsocketopen to fail with a lookup failure, ensure it exists for this test.
1296
			if (substr($modSettings['smtp_host'], 0, 6) != 'ssl://')
1297
				$modSettings['smtp_host'] = str_replace('ssl:', 'ss://', $modSettings['smtp_host']);
1298
1299
			if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3))
1300
				log_error($txt['smtp_port_ssl']);
1301
		}
1302
1303
		// Unable to connect!  Don't show any error message, but just log one and try to continue anyway.
1304
		if (!$socket)
0 ignored issues
show
introduced by
$socket is of type resource, thus it always evaluated to false.
Loading history...
1305
		{
1306
			log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
1307
			return false;
1308
		}
1309
	}
1310
1311
	// Wait for a response of 220, without "-" continuer.
1312
	if (!server_parse(null, $socket, '220'))
1313
		return false;
1314
1315
	// Try to determine the server's fully qualified domain name
1316
	// Can't rely on $_SERVER['SERVER_NAME'] because it can be spoofed on Apache
1317
	if (empty($helo))
1318
	{
1319
		// See if we can get the domain name from the host itself
1320
		if (function_exists('gethostname'))
1321
			$helo = gethostname();
1322
		elseif (function_exists('php_uname'))
1323
			$helo = php_uname('n');
1324
1325
		// If the hostname isn't a fully qualified domain name, we can use the host name from $boardurl instead
1326
		if (empty($helo) || strpos($helo, '.') === false || substr_compare($helo, '.local', -6) === 0 || (!empty($modSettings['tld_regex']) && !preg_match('/\.' . $modSettings['tld_regex'] . '$/u', $helo)))
1327
			$helo = parse_url($boardurl, PHP_URL_HOST);
1328
1329
		// This is one of those situations where 'www.' is undesirable
1330
		if (strpos($helo, 'www.') === 0)
1331
			$helo = substr($helo, 4);
1332
	}
1333
1334
	// SMTP = 1, SMTP - STARTTLS = 2
1335
	if (in_array($modSettings['mail_type'], array(1, 2)) && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1336
	{
1337
		// EHLO could be understood to mean encrypted hello...
1338
		if (server_parse('EHLO ' . $helo, $socket, null, $response) == '250')
1339
		{
1340
			// Are we using STARTTLS and does the server support STARTTLS?
1341
			if ($modSettings['mail_type'] == 2 && preg_match("~250( |-)STARTTLS~mi", $response))
1342
			{
1343
				// Send STARTTLS to enable encryption
1344
				if (!server_parse('STARTTLS', $socket, '220'))
1345
					return false;
1346
				// Enable the encryption
1347
				// php 5.6+ fix
1348
				$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
1349
1350
				if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT'))
1351
				{
1352
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
1353
					$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
1354
				}
1355
1356
				if (!@stream_socket_enable_crypto($socket, true, $crypto_method))
1357
					return false;
1358
				// Send the EHLO command again
1359
				if (!server_parse('EHLO ' . $helo, $socket, null) == '250')
1360
					return false;
1361
			}
1362
1363
			if (!server_parse('AUTH LOGIN', $socket, '334'))
1364
				return false;
1365
			// Send the username and password, encoded.
1366
			if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1367
				return false;
1368
			// The password is already encoded ;)
1369
			if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1370
				return false;
1371
		}
1372
		elseif (!server_parse('HELO ' . $helo, $socket, '250'))
1373
			return false;
1374
	}
1375
	else
1376
	{
1377
		// Just say "helo".
1378
		if (!server_parse('HELO ' . $helo, $socket, '250'))
1379
			return false;
1380
	}
1381
1382
	// Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1383
	$message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1384
1385
	// !! Theoretically, we should be able to just loop the RCPT TO.
1386
	$mail_to_array = array_values($mail_to_array);
1387
	foreach ($mail_to_array as $i => $mail_to)
1388
	{
1389
		// Reset the connection to send another email.
1390
		if ($i != 0)
1391
		{
1392
			if (!server_parse('RSET', $socket, '250'))
1393
				return false;
1394
		}
1395
1396
		// From, to, and then start the data...
1397
		if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1398
			return false;
1399
		if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1400
			return false;
1401
		if (!server_parse('DATA', $socket, '354'))
1402
			return false;
1403
		fputs($socket, 'Subject: ' . $subject . "\r\n");
1404
		if (strlen($mail_to) > 0)
1405
			fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1406
		fputs($socket, $headers . "\r\n\r\n");
1407
		fputs($socket, $message . "\r\n");
1408
1409
		// Send a ., or in other words "end of data".
1410
		if (!server_parse('.', $socket, '250'))
1411
			return false;
1412
1413
		// Almost done, almost done... don't stop me just yet!
1414
		@set_time_limit(300);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1414
		/** @scrutinizer ignore-unhandled */ @set_time_limit(300);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1415
		if (function_exists('apache_reset_timeout'))
1416
			@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for apache_reset_timeout(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1416
			/** @scrutinizer ignore-unhandled */ @apache_reset_timeout();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1417
	}
1418
	fputs($socket, 'QUIT' . "\r\n");
1419
	fclose($socket);
1420
1421
	return true;
1422
}
1423
1424
/**
1425
 * Parse a message to the SMTP server.
1426
 * Sends the specified message to the server, and checks for the
1427
 * expected response.
1428
 * @internal
1429
 *
1430
 * @param string $message The message to send
1431
 * @param resource $socket Socket to send on
1432
 * @param string $code The expected response code
1433
 * @param string $response The response from the SMTP server
1434
 * @return bool Whether it responded as such.
1435
 */
1436
function server_parse($message, $socket, $code, &$response = null)
1437
{
1438
	global $txt;
1439
1440
	if ($message !== null)
0 ignored issues
show
introduced by
The condition $message !== null is always true.
Loading history...
1441
		fputs($socket, $message . "\r\n");
1442
1443
	// No response yet.
1444
	$server_response = '';
1445
1446
	while (substr($server_response, 3, 1) != ' ')
1447
	{
1448
		if (!($server_response = fgets($socket, 256)))
1449
		{
1450
			// @todo Change this message to reflect that it may mean bad user/password/server issues/etc.
1451
			log_error($txt['smtp_bad_response']);
1452
			return false;
1453
		}
1454
		$response .= $server_response;
1455
	}
1456
1457
	if ($code === null)
0 ignored issues
show
introduced by
The condition $code === null is always false.
Loading history...
1458
		return substr($server_response, 0, 3);
1459
1460
	if (substr($server_response, 0, 3) != $code)
1461
	{
1462
		log_error($txt['smtp_error'] . $server_response);
1463
		return false;
1464
	}
1465
1466
	return true;
1467
}
1468
1469
/**
1470
 * Spell checks the post for typos ;).
1471
 * It uses the pspell or enchant library, one of which MUST be installed.
1472
 * It has problems with internationalization.
1473
 * It is accessed via ?action=spellcheck.
1474
 */
1475
function SpellCheck()
1476
{
1477
	global $txt, $context, $smcFunc;
1478
1479
	// A list of "words" we know about but pspell doesn't.
1480
	$known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1481
1482
	loadLanguage('Post');
1483
	loadTemplate('Post');
1484
1485
	// Create a pspell or enchant dictionary resource
1486
	$dict = spell_init();
1487
1488
	if (!isset($_POST['spellstring']) || !$dict)
1489
		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...
1490
1491
	// Construct a bit of Javascript code.
1492
	$context['spell_js'] = '
1493
		var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1494
		var mispstr = window.opener.spellCheckGetText(spell_fieldname);
1495
		var misps = Array(';
1496
1497
	// Get all the words (Javascript already separated them).
1498
	$alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1499
1500
	$found_words = false;
1501
	for ($i = 0, $n = count($alphas); $i < $n; $i++)
1502
	{
1503
		// Words are sent like 'word|offset_begin|offset_end'.
1504
		$check_word = explode('|', $alphas[$i]);
1505
1506
		// If the word is a known word, or spelled right...
1507
		if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || spell_check($dict, $check_word[0]) || !isset($check_word[2]))
1508
			continue;
1509
1510
		// Find the word, and move up the "last occurrence" to here.
1511
		$found_words = true;
1512
1513
		// Add on the javascript for this misspelling.
1514
		$context['spell_js'] .= '
1515
			new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '&gt;' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1516
1517
		// If there are suggestions, add them in...
1518
		$suggestions = spell_suggest($dict, $check_word[0]);
1519
		if (!empty($suggestions))
1520
		{
1521
			// But first check they aren't going to be censored - no naughty words!
1522
			foreach ($suggestions as $k => $word)
1523
				if ($suggestions[$k] != censorText($word))
1524
					unset($suggestions[$k]);
1525
1526
			if (!empty($suggestions))
1527
				$context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1528
		}
1529
1530
		$context['spell_js'] .= ']),';
1531
	}
1532
1533
	// If words were found, take off the last comma.
1534
	if ($found_words)
1535
		$context['spell_js'] = substr($context['spell_js'], 0, -1);
1536
1537
	$context['spell_js'] .= '
1538
		);';
1539
1540
	// And instruct the template system to just show the spellcheck sub template.
1541
	$context['template_layers'] = array();
1542
	$context['sub_template'] = 'spellcheck';
1543
1544
	// Free resources for enchant...
1545
	if (isset($context['enchant_broker']))
1546
	{
1547
		enchant_broker_free_dict($dict);
1548
		enchant_broker_free($context['enchant_broker']);
1549
	}
1550
}
1551
1552
/**
1553
 * Sends a notification to members who have elected to receive emails
1554
 * when things happen to a topic, such as replies are posted.
1555
 * The function automatically finds the subject and its board, and
1556
 * checks permissions for each member who is "signed up" for notifications.
1557
 * It will not send 'reply' notifications more than once in a row.
1558
 *
1559
 * @param array $topics Represents the topics the action is happening to.
1560
 * @param string $type Can be any of reply, sticky, lock, unlock, remove, move, merge, and split.  An appropriate message will be sent for each.
1561
 * @param array $exclude Members in the exclude array will not be processed for the topic with the same key.
1562
 * @param array $members_only Are the only ones that will be sent the notification if they have it on.
1563
 * @uses Post language file
1564
 */
1565
function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
1566
{
1567
	global $user_info, $smcFunc;
1568
1569
	// Can't do it if there's no topics.
1570
	if (empty($topics))
1571
		return;
1572
	// It must be an array - it must!
1573
	if (!is_array($topics))
0 ignored issues
show
introduced by
The condition is_array($topics) is always true.
Loading history...
1574
		$topics = array($topics);
1575
1576
	// Get the subject and body...
1577
	$result = $smcFunc['db_query']('', '
1578
		SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board,
1579
			COALESCE(mem.real_name, ml.poster_name) AS poster_name, mf.id_msg
1580
		FROM {db_prefix}topics AS t
1581
			INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1582
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1583
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1584
		WHERE t.id_topic IN ({array_int:topic_list})
1585
		LIMIT 1',
1586
		array(
1587
			'topic_list' => $topics,
1588
		)
1589
	);
1590
	$task_rows = array();
1591
	while ($row = $smcFunc['db_fetch_assoc']($result))
1592
	{
1593
		// Clean it up.
1594
		censorText($row['subject']);
1595
		censorText($row['body']);
1596
		$row['subject'] = un_htmlspecialchars($row['subject']);
1597
		$row['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($row['body'], false, $row['id_last_msg']), array('<br>' => "\n", '</div>' => "\n", '</li>' => "\n", '&#91;' => '[', '&#93;' => ']')))));
1598
1599
		$task_rows[] = array(
1600
			'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
1601
				'msgOptions' => array(
1602
					'id' => $row['id_msg'],
1603
					'subject' => $row['subject'],
1604
					'body' => $row['body'],
1605
				),
1606
				'topicOptions' => array(
1607
					'id' => $row['id_topic'],
1608
					'board' => $row['id_board'],
1609
				),
1610
				// Kinda cheeky, but for any action the originator is usually the current user
1611
				'posterOptions' => array(
1612
					'id' => $user_info['id'],
1613
					'name' => $user_info['name'],
1614
				),
1615
				'type' => $type,
1616
				'members_only' => $members_only,
1617
			)), 0
1618
		);
1619
	}
1620
	$smcFunc['db_free_result']($result);
1621
1622
	if (!empty($task_rows))
1623
		$smcFunc['db_insert']('',
1624
			'{db_prefix}background_tasks',
1625
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1626
			$task_rows,
1627
			array('id_task')
1628
		);
1629
}
1630
1631
/**
1632
 * Create a post, either as new topic (id_topic = 0) or in an existing one.
1633
 * The input parameters of this function assume:
1634
 * - Strings have been escaped.
1635
 * - Integers have been cast to integer.
1636
 * - Mandatory parameters are set.
1637
 *
1638
 * @param array $msgOptions An array of information/options for the post
1639
 * @param array $topicOptions An array of information/options for the topic
1640
 * @param array $posterOptions An array of information/options for the poster
1641
 * @return bool Whether the operation was a success
1642
 */
1643
function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1644
{
1645
	global $user_info, $txt, $modSettings, $smcFunc, $sourcedir;
1646
1647
	require_once($sourcedir . '/Mentions.php');
1648
1649
	// Set optional parameters to the default value.
1650
	$msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1651
	$msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1652
	$msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1653
	$msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1654
	$topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1655
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1656
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1657
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1658
	$topicOptions['redirect_expires'] = isset($topicOptions['redirect_expires']) ? $topicOptions['redirect_expires'] : null;
1659
	$topicOptions['redirect_topic'] = isset($topicOptions['redirect_topic']) ? $topicOptions['redirect_topic'] : null;
1660
	$posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1661
	$posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1662
1663
	// Not exactly a post option but it allows hooks and/or other sources to skip sending notifications if they don't want to
1664
	$msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true;
1665
1666
	// We need to know if the topic is approved. If we're told that's great - if not find out.
1667
	if (!$modSettings['postmod_active'])
1668
		$topicOptions['is_approved'] = true;
1669
	elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1670
	{
1671
		$request = $smcFunc['db_query']('', '
1672
			SELECT approved
1673
			FROM {db_prefix}topics
1674
			WHERE id_topic = {int:id_topic}
1675
			LIMIT 1',
1676
			array(
1677
				'id_topic' => $topicOptions['id'],
1678
			)
1679
		);
1680
		list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1681
		$smcFunc['db_free_result']($request);
1682
	}
1683
1684
	// If nothing was filled in as name/e-mail address, try the member table.
1685
	if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1686
	{
1687
		if (empty($posterOptions['id']))
1688
		{
1689
			$posterOptions['id'] = 0;
1690
			$posterOptions['name'] = $txt['guest_title'];
1691
			$posterOptions['email'] = '';
1692
		}
1693
		elseif ($posterOptions['id'] != $user_info['id'])
1694
		{
1695
			$request = $smcFunc['db_query']('', '
1696
				SELECT member_name, email_address
1697
				FROM {db_prefix}members
1698
				WHERE id_member = {int:id_member}
1699
				LIMIT 1',
1700
				array(
1701
					'id_member' => $posterOptions['id'],
1702
				)
1703
			);
1704
			// Couldn't find the current poster?
1705
			if ($smcFunc['db_num_rows']($request) == 0)
1706
			{
1707
				trigger_error('createPost(): Invalid member id ' . $posterOptions['id'], E_USER_NOTICE);
1708
				$posterOptions['id'] = 0;
1709
				$posterOptions['name'] = $txt['guest_title'];
1710
				$posterOptions['email'] = '';
1711
			}
1712
			else
1713
				list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1714
			$smcFunc['db_free_result']($request);
1715
		}
1716
		else
1717
		{
1718
			$posterOptions['name'] = $user_info['name'];
1719
			$posterOptions['email'] = $user_info['email'];
1720
		}
1721
	}
1722
1723
	if (!empty($modSettings['enable_mentions']))
1724
	{
1725
		$msgOptions['mentioned_members'] = Mentions::getMentionedMembers($msgOptions['body']);
1726
		if (!empty($msgOptions['mentioned_members']))
1727
			$msgOptions['body'] = Mentions::getBody($msgOptions['body'], $msgOptions['mentioned_members']);
1728
	}
1729
1730
	// It's do or die time: forget any user aborts!
1731
	$previous_ignore_user_abort = ignore_user_abort(true);
1732
1733
	$new_topic = empty($topicOptions['id']);
1734
1735
	$message_columns = array(
1736
		'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')),
1737
		'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'inet',
1738
		'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1739
	);
1740
1741
	$message_parameters = array(
1742
		$topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1743
		$posterOptions['name'], $posterOptions['email'], time(), $posterOptions['ip'],
1744
		$msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1745
	);
1746
1747
	// What if we want to do anything with posts?
1748
	call_integration_hook('integrate_create_post', array(&$msgOptions, &$topicOptions, &$posterOptions, &$message_columns, &$message_parameters));
1749
1750
	// Insert the post.
1751
	$msgOptions['id'] = $smcFunc['db_insert']('',
1752
		'{db_prefix}messages',
1753
		$message_columns,
1754
		$message_parameters,
1755
		array('id_msg'),
1756
		1
1757
	);
1758
1759
	// Something went wrong creating the message...
1760
	if (empty($msgOptions['id']))
1761
		return false;
1762
1763
	// Fix the attachments.
1764
	if (!empty($msgOptions['attachments']))
1765
		$smcFunc['db_query']('', '
1766
			UPDATE {db_prefix}attachments
1767
			SET id_msg = {int:id_msg}
1768
			WHERE id_attach IN ({array_int:attachment_list})',
1769
			array(
1770
				'attachment_list' => $msgOptions['attachments'],
1771
				'id_msg' => $msgOptions['id'],
1772
			)
1773
		);
1774
1775
	// What if we want to export new posts out to a CMS?
1776
	call_integration_hook('integrate_after_create_post', array($msgOptions, $topicOptions, $posterOptions, $message_columns, $message_parameters));
1777
1778
	// Insert a new topic (if the topicID was left empty.)
1779
	if ($new_topic)
1780
	{
1781
		$topic_columns = array(
1782
			'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1783
			'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1784
			'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1785
			'redirect_expires' => 'int', 'id_redirect_topic' => 'int',
1786
		);
1787
		$topic_parameters = array(
1788
			$topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1789
			$msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1790
			$topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1791
			$topicOptions['redirect_expires'] === null ? 0 : $topicOptions['redirect_expires'], $topicOptions['redirect_topic'] === null ? 0 : $topicOptions['redirect_topic'],
1792
		);
1793
1794
		call_integration_hook('integrate_before_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions, &$topic_columns, &$topic_parameters));
1795
1796
		$topicOptions['id'] = $smcFunc['db_insert']('',
1797
			'{db_prefix}topics',
1798
			$topic_columns,
1799
			$topic_parameters,
1800
			array('id_topic'),
1801
			1
1802
		);
1803
1804
		// The topic couldn't be created for some reason.
1805
		if (empty($topicOptions['id']))
1806
		{
1807
			// We should delete the post that did work, though...
1808
			$smcFunc['db_query']('', '
1809
				DELETE FROM {db_prefix}messages
1810
				WHERE id_msg = {int:id_msg}',
1811
				array(
1812
					'id_msg' => $msgOptions['id'],
1813
				)
1814
			);
1815
1816
			return false;
1817
		}
1818
1819
		// Fix the message with the topic.
1820
		$smcFunc['db_query']('', '
1821
			UPDATE {db_prefix}messages
1822
			SET id_topic = {int:id_topic}
1823
			WHERE id_msg = {int:id_msg}',
1824
			array(
1825
				'id_topic' => $topicOptions['id'],
1826
				'id_msg' => $msgOptions['id'],
1827
			)
1828
		);
1829
1830
		// There's been a new topic AND a new post today.
1831
		trackStats(array('topics' => '+', 'posts' => '+'));
1832
1833
		updateStats('topic', true);
1834
		updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1835
1836
		// What if we want to export new topics out to a CMS?
1837
		call_integration_hook('integrate_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions));
1838
	}
1839
	// The topic already exists, it only needs a little updating.
1840
	else
1841
	{
1842
		$update_parameters = array(
1843
			'poster_id' => $posterOptions['id'],
1844
			'id_msg' => $msgOptions['id'],
1845
			'locked' => $topicOptions['lock_mode'],
1846
			'is_sticky' => $topicOptions['sticky_mode'],
1847
			'id_topic' => $topicOptions['id'],
1848
			'counter_increment' => 1,
1849
		);
1850
		if ($msgOptions['approved'])
1851
			$topics_columns = array(
1852
				'id_member_updated = {int:poster_id}',
1853
				'id_last_msg = {int:id_msg}',
1854
				'num_replies = num_replies + {int:counter_increment}',
1855
			);
1856
		else
1857
			$topics_columns = array(
1858
				'unapproved_posts = unapproved_posts + {int:counter_increment}',
1859
			);
1860
		if ($topicOptions['lock_mode'] !== null)
1861
			$topics_columns[] = 'locked = {int:locked}';
1862
		if ($topicOptions['sticky_mode'] !== null)
1863
			$topics_columns[] = 'is_sticky = {int:is_sticky}';
1864
1865
		call_integration_hook('integrate_modify_topic', array(&$topics_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions));
1866
1867
		// Update the number of replies and the lock/sticky status.
1868
		$smcFunc['db_query']('', '
1869
			UPDATE {db_prefix}topics
1870
			SET
1871
				' . implode(', ', $topics_columns) . '
1872
			WHERE id_topic = {int:id_topic}',
1873
			$update_parameters
1874
		);
1875
1876
		// One new post has been added today.
1877
		trackStats(array('posts' => '+'));
1878
	}
1879
1880
	// Creating is modifying...in a way.
1881
	// @todo Why not set id_msg_modified on the insert?
1882
	$smcFunc['db_query']('', '
1883
		UPDATE {db_prefix}messages
1884
		SET id_msg_modified = {int:id_msg}
1885
		WHERE id_msg = {int:id_msg}',
1886
		array(
1887
			'id_msg' => $msgOptions['id'],
1888
		)
1889
	);
1890
1891
	// Increase the number of posts and topics on the board.
1892
	if ($msgOptions['approved'])
1893
		$smcFunc['db_query']('', '
1894
			UPDATE {db_prefix}boards
1895
			SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
1896
			WHERE id_board = {int:id_board}',
1897
			array(
1898
				'id_board' => $topicOptions['board'],
1899
			)
1900
		);
1901
	else
1902
	{
1903
		$smcFunc['db_query']('', '
1904
			UPDATE {db_prefix}boards
1905
			SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
1906
			WHERE id_board = {int:id_board}',
1907
			array(
1908
				'id_board' => $topicOptions['board'],
1909
			)
1910
		);
1911
1912
		// Add to the approval queue too.
1913
		$smcFunc['db_insert']('',
1914
			'{db_prefix}approval_queue',
1915
			array(
1916
				'id_msg' => 'int',
1917
			),
1918
			array(
1919
				$msgOptions['id'],
1920
			),
1921
			array()
1922
		);
1923
1924
		$smcFunc['db_insert']('',
1925
			'{db_prefix}background_tasks',
1926
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1927
			array(
1928
				'$sourcedir/tasks/ApprovePost-Notify.php', 'ApprovePost_Notify_Background', $smcFunc['json_encode'](array(
1929
					'msgOptions' => $msgOptions,
1930
					'topicOptions' => $topicOptions,
1931
					'posterOptions' => $posterOptions,
1932
					'type' => $new_topic ? 'topic' : 'post',
1933
				)), 0
1934
			),
1935
			array('id_task')
1936
		);
1937
	}
1938
1939
	// Mark inserted topic as read (only for the user calling this function).
1940
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
1941
	{
1942
		// Since it's likely they *read* it before replying, let's try an UPDATE first.
1943
		if (!$new_topic)
1944
		{
1945
			$smcFunc['db_query']('', '
1946
				UPDATE {db_prefix}log_topics
1947
				SET id_msg = {int:id_msg}
1948
				WHERE id_member = {int:current_member}
1949
					AND id_topic = {int:id_topic}',
1950
				array(
1951
					'current_member' => $posterOptions['id'],
1952
					'id_msg' => $msgOptions['id'],
1953
					'id_topic' => $topicOptions['id'],
1954
				)
1955
			);
1956
1957
			$flag = $smcFunc['db_affected_rows']() != 0;
1958
		}
1959
1960
		if (empty($flag))
1961
		{
1962
			$smcFunc['db_insert']('ignore',
1963
				'{db_prefix}log_topics',
1964
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
1965
				array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
1966
				array('id_topic', 'id_member')
1967
			);
1968
		}
1969
	}
1970
1971
	if ($msgOptions['approved'] && empty($topicOptions['is_approved']))
1972
		$smcFunc['db_insert']('',
1973
			'{db_prefix}background_tasks',
1974
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1975
			array(
1976
				'$sourcedir/tasks/ApproveReply-Notify.php', 'ApproveReply_Notify_Background', $smcFunc['json_encode'](array(
1977
					'msgOptions' => $msgOptions,
1978
					'topicOptions' => $topicOptions,
1979
					'posterOptions' => $posterOptions,
1980
				)), 0
1981
			),
1982
			array('id_task')
1983
		);
1984
1985
	// If there's a custom search index, it may need updating...
1986
	require_once($sourcedir . '/Search.php');
1987
	$searchAPI = findSearchAPI();
1988
	if (is_callable(array($searchAPI, 'postCreated')))
1989
		$searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
1990
1991
	// Increase the post counter for the user that created the post.
1992
	if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
1993
	{
1994
		// Are you the one that happened to create this post?
1995
		if ($user_info['id'] == $posterOptions['id'])
1996
			$user_info['posts']++;
1997
		updateMemberData($posterOptions['id'], array('posts' => '+'));
1998
	}
1999
2000
	// They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2001
	$_SESSION['last_read_topic'] = 0;
2002
2003
	// Better safe than sorry.
2004
	if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2005
		$_SESSION['topicseen_cache'][$topicOptions['board']]--;
2006
2007
	// Update all the stats so everyone knows about this new topic and message.
2008
	updateStats('message', true, $msgOptions['id']);
2009
2010
	// Update the last message on the board assuming it's approved AND the topic is.
2011
	if ($msgOptions['approved'])
2012
		updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2013
2014
	// Queue createPost background notification
2015
	if ($msgOptions['send_notifications'] && $msgOptions['approved'])
2016
		$smcFunc['db_insert']('',
2017
			'{db_prefix}background_tasks',
2018
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2019
			array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2020
				'msgOptions' => $msgOptions,
2021
				'topicOptions' => $topicOptions,
2022
				'posterOptions' => $posterOptions,
2023
				'type' => $new_topic ? 'topic' : 'reply',
2024
			)), 0),
2025
			array('id_task')
2026
		);
2027
2028
	// Alright, done now... we can abort now, I guess... at least this much is done.
2029
	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

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

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

2853
			/** @scrutinizer ignore-unhandled */ @enchant_broker_free($context['enchant_broker']);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2854
		}
2855
	}
2856
2857
	// Fall through to pspell if enchant didn't work
2858
	if (function_exists('pspell_new'))
2859
	{
2860
		// Okay, this looks funny, but it actually fixes a weird bug.
2861
		ob_start();
2862
		$old = error_reporting(0);
2863
2864
		// See, first, some windows machines don't load pspell properly on the first try.  Dumb, but this is a workaround.
2865
		pspell_new('en');
2866
2867
		// Next, the dictionary in question may not exist. So, we try it... but...
2868
		$pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER);
2869
2870
		// Most people don't have anything but English installed... So we use English as a last resort.
2871
		if (!$pspell_link)
2872
			$pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
2873
2874
		error_reporting($old);
2875
		ob_end_clean();
2876
2877
		// If we have pspell, exit now...
2878
		if ($pspell_link)
2879
		{
2880
			$context['provider'] = 'pspell';
2881
			return $pspell_link;
2882
		}
2883
	}
2884
2885
	// If we get this far, we're doomed
2886
	return false;
2887
}
2888
2889
/**
2890
 * spell_check()
2891
 *
2892
 * Determines whether or not the specified word is spelled correctly
2893
 *
2894
 * @param resource $dict An enchant or pspell dictionary resource set up by {@link spell_init()}
2895
 * @param string $word A word to check the spelling of
2896
 * @return bool Whether or not the specified word is spelled properly
2897
 */
2898
function spell_check($dict, $word)
2899
{
2900
	global $context, $txt;
2901
2902
	// Enchant or pspell?
2903
	if ($context['provider'] == 'enchant')
2904
	{
2905
		// This is a bit tricky here...
2906
		if (!$context['spell_utf8'])
2907
		{
2908
			// Convert the word to UTF-8 with iconv
2909
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
2910
		}
2911
		return enchant_dict_check($dict, $word);
2912
	}
2913
	elseif ($context['provider'] == 'pspell')
2914
	{
2915
		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

2915
		return pspell_check(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
2916
	}
2917
}
2918
2919
/**
2920
 * spell_suggest()
2921
 *
2922
 * Returns an array of suggested replacements for the specified word
2923
 *
2924
 * @param resource $dict An enchant or pspell dictioary resource
2925
 * @param string $word A misspelled word
2926
 * @return array An array of suggested replacements for the misspelled word
2927
 */
2928
function spell_suggest($dict, $word)
2929
{
2930
	global $context, $txt;
2931
2932
	if ($context['provider'] == 'enchant')
2933
	{
2934
		// If we're not using UTF-8, we need iconv to handle some stuff...
2935
		if (!$context['spell_utf8'])
2936
		{
2937
			// Convert the word to UTF-8 before getting suggestions
2938
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
2939
			$suggestions = enchant_dict_suggest($dict, $word);
2940
2941
			// Go through the suggestions and convert them back to the proper character set
2942
			foreach ($suggestions as $index => $suggestion)
2943
			{
2944
				// //TRANSLIT makes it use similar-looking characters for incompatible ones...
2945
				$suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion);
2946
			}
2947
2948
			return $suggestions;
2949
		}
2950
		else
2951
		{
2952
			return enchant_dict_suggest($dict, $word);
2953
		}
2954
	}
2955
	elseif ($context['provider'] == 'pspell')
2956
	{
2957
		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

2957
		return pspell_suggest(/** @scrutinizer ignore-type */ $dict, $word);
Loading history...
2958
	}
2959
}
2960
2961
?>