Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Subs-Post.php ➔ server_parse()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 14
nop 4
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
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 2017 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
	$message = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message);
35
36
	// Clean up after nobbc ;).
37
	$message = preg_replace_callback('~\[nobbc\](.+?)\[/nobbc\]~is', function($a)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
38
	{
39
		return '[nobbc]' . strtr($a[1], array('[' => '&#91;', ']' => '&#93;', ':' => '&#58;', '@' => '&#64;')) . '[/nobbc]';
40
	}, $message);
41
42
	// Remove \r's... they're evil!
43
	$message = strtr($message, array("\r" => ''));
44
45
	// You won't believe this - but too many periods upsets apache it seems!
46
	$message = preg_replace('~\.{100,}~', '...', $message);
47
48
	// Trim off trailing quotes - these often happen by accident.
49
	while (substr($message, -7) == '[quote]')
50
		$message = substr($message, 0, -7);
51
	while (substr($message, 0, 8) == '[/quote]')
52
		$message = substr($message, 8);
53
54
	// Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed.
55
	$in_tag = false;
56
	$had_tag = false;
57
	$codeopen = 0;
58
	if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches))
59
		foreach ($matches[0] as $index => $dummy)
60
		{
61
			// Closing?
62
			if (!empty($matches[2][$index]))
63
			{
64
				// If it's closing and we're not in a tag we need to open it...
65
				if (!$in_tag)
66
					$codeopen = true;
67
				// Either way we ain't in one any more.
68
				$in_tag = false;
69
			}
70
			// Opening tag...
71
			else
72
			{
73
				$had_tag = true;
74
				// If we're in a tag don't do nought!
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
75
				if (!$in_tag)
76
					$in_tag = true;
77
			}
78
		}
79
80
	// If we have an open tag, close it.
81
	if ($in_tag)
82
		$message .= '[/code]';
83
	// Open any ones that need to be open, only if we've never had a tag.
84
	if ($codeopen && !$had_tag)
85
		$message = '[code]' . $message;
86
87
	// Now that we've fixed all the code tags, let's fix the img and url tags...
88
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
89
90
	// Replace code BBC with placeholders. We'll restore them at the end.
91 View Code Duplication
	for ($i = 0, $n = count($parts); $i < $n; $i++)
92
	{
93
		// It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
94
		if ($i % 4 == 2)
95
		{
96
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
97
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
98
			$code_tags[$substitute] = $code_tag;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$code_tags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $code_tags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
99
			$parts[$i] = $i;
100
		}
101
	}
102
103
	$message = implode('', $parts);
104
105
	// The regular expression non breaking space has many versions.
106
	$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
107
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) {
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
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]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [time] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal absolute does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal time_offset does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal [/time] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
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]]";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
145
	}, $message);
146
147
	$list_open = substr_count($message, '[list]') + substr_count($message, '[list ');
148
	$list_close = substr_count($message, '[/list]');
149 View Code Duplication
	if ($list_close - $list_open > 0)
150
		$message = str_repeat('[list]', $list_close - $list_open) . $message;
151 View Code Duplication
	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].
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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
	foreach (($codes = parse_bbc(false)) as $code)
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
The expression $codes = parse_bbc(false) of type string is not traversable.
Loading history...
213
		if (!in_array($code['tag'], $allowedEmpty))
214
			$alltags[] = $code['tag'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$alltags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $alltags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
215
216
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
0 ignored issues
show
Bug introduced by
The variable $alltags does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
217
218
	while (preg_match('~\[(' . $alltags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', $message))
219
		$message = preg_replace('~\[(' . $alltags_regex . ')[^\]]*\]\s*\[/\1\]\s?~i', '', $message);
220
221
	// Restore code blocks
222
	if (!empty($code_tags))
223
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
224
225
	// Restore white space entities
226
	if (!$previewing)
227
		$message = strtr($message, array('  ' => '&nbsp; ', "\n" => '<br>', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
228
	else
229
		$message = strtr($message, array('  ' => '&nbsp; ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
230
231
	// Now let's quickly clean up things that will slow our parser (which are common in posted code.)
232
	$message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
233
}
234
235
/**
236
 * This is very simple, and just removes things done by preparsecode.
237
 *
238
 * @param string $message The message
239
 */
240
function un_preparsecode($message)
241
{
242
	global $smcFunc;
243
244
	$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
245
246
	// We're going to unparse only the stuff outside [code]...
247 View Code Duplication
	for ($i = 0, $n = count($parts); $i < $n; $i++)
248
	{
249
		// If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
250
		if ($i % 4 == 2)
251
		{
252
			$code_tag = $parts[$i - 1] . $parts[$i] . $parts[$i + 1];
253
			$substitute = $parts[$i - 1] . $i . $parts[$i + 1];
254
			$code_tags[$substitute] = $code_tag;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$code_tags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $code_tags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
255
			$parts[$i] = $i;
256
		}
257
	}
258
259
	$message = implode('', $parts);
260
261
	$message = preg_replace_callback('~\[html\](.+?)\[/html\]~i', function($m) use ($smcFunc)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
262
	{
263
		return "[html]" . strtr($smcFunc['htmlspecialchars']("$m[1]", ENT_QUOTES), array("\\&quot;" => "&quot;", "&amp;#13;" => "<br>", "&amp;#32;" => " ", "&amp;#91;" => "[", "&amp;#93;" => "]")) . "[/html]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [html] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal \\" does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal " does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal &#13; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal <br> does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal &#32; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal &#91; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal [ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal &#93; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal ] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal [/html] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
264
	}, $message);
265
266
	// Attempt to un-parse the time to something less awful.
267
	$message = preg_replace_callback('~\[time\](\d{0,10})\[/time\]~i', function($m)
268
	{
269
		return "[time]" . timeformat("$m[1]", false) . "[/time]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [time] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal [/time] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
270
	}, $message);
271
272
	if (!empty($code_tags))
273
		$message = str_replace(array_keys($code_tags), array_values($code_tags), $message);
274
275
	// Change breaks back to \n's and &nsbp; back to spaces.
276
	return preg_replace('~<br( /)?' . '>~', "\n", str_replace('&nbsp;', ' ', $message));
277
}
278
279
/**
280
 * Fix any URLs posted - ie. remove 'javascript:'.
281
 * Used by preparsecode, fixes links in message and returns nothing.
282
 *
283
 * @param string $message The message
284
 */
285
function fixTags(&$message)
286
{
287
	global $modSettings;
288
289
	// WARNING: Editing the below can cause large security holes in your forum.
290
	// Edit only if you are sure you know what you are doing.
291
292
	$fixArray = array(
293
		// [img]http://...[/img] or [img width=1]http://...[/img]
294
		array(
295
			'tag' => 'img',
296
			'protocols' => array('http', 'https'),
297
			'embeddedUrl' => false,
298
			'hasEqualSign' => false,
299
			'hasExtra' => true,
300
		),
301
		// [url]http://...[/url]
302
		array(
303
			'tag' => 'url',
304
			'protocols' => array('http', 'https'),
305
			'embeddedUrl' => false,
306
			'hasEqualSign' => false,
307
		),
308
		// [url=http://...]name[/url]
309
		array(
310
			'tag' => 'url',
311
			'protocols' => array('http', 'https'),
312
			'embeddedUrl' => true,
313
			'hasEqualSign' => true,
314
		),
315
		// [iurl]http://...[/iurl]
316
		array(
317
			'tag' => 'iurl',
318
			'protocols' => array('http', 'https'),
319
			'embeddedUrl' => false,
320
			'hasEqualSign' => false,
321
		),
322
		// [iurl=http://...]name[/iurl]
323
		array(
324
			'tag' => 'iurl',
325
			'protocols' => array('http', 'https'),
326
			'embeddedUrl' => true,
327
			'hasEqualSign' => true,
328
		),
329
		// [ftp]ftp://...[/ftp]
330
		array(
331
			'tag' => 'ftp',
332
			'protocols' => array('ftp', 'ftps'),
333
			'embeddedUrl' => false,
334
			'hasEqualSign' => false,
335
		),
336
		// [ftp=ftp://...]name[/ftp]
337
		array(
338
			'tag' => 'ftp',
339
			'protocols' => array('ftp', 'ftps'),
340
			'embeddedUrl' => true,
341
			'hasEqualSign' => true,
342
		),
343
		// [flash]http://...[/flash]
344
		array(
345
			'tag' => 'flash',
346
			'protocols' => array('http', 'https'),
347
			'embeddedUrl' => false,
348
			'hasEqualSign' => false,
349
			'hasExtra' => true,
350
		),
351
	);
352
353
	// Fix each type of tag.
354
	foreach ($fixArray as $param)
355
		fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra']));
0 ignored issues
show
Documentation introduced by
$param['protocols'] is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
356
357
	// Now fix possible security problems with images loading links automatically...
358
	$message = preg_replace_callback('~(\[img.*?\])(.+?)\[/img\]~is', function($m)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
359
	{
360
		return "$m[1]" . preg_replace("~action(=|%3d)(?!dlattach)~i", "action-", "$m[2]") . "[/img]";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal ~action(=|%3d)(?!dlattach)~i does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal action- does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal [/img] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
361
	}, $message);
362
363
	// Limit the size of images posted?
364
	if (!empty($modSettings['max_image_width']) || !empty($modSettings['max_image_height']))
365
	{
366
		// Find all the img tags - with or without width and height.
367
		preg_match_all('~\[img(\s+width=\d+)?(\s+height=\d+)?(\s+width=\d+)?\](.+?)\[/img\]~is', $message, $matches, PREG_PATTERN_ORDER);
368
369
		$replaces = array();
370
		foreach ($matches[0] as $match => $dummy)
371
		{
372
			// If the width was after the height, handle it.
373
			$matches[1][$match] = !empty($matches[3][$match]) ? $matches[3][$match] : $matches[1][$match];
374
375
			// Now figure out if they had a desired height or width...
376
			$desired_width = !empty($matches[1][$match]) ? (int) substr(trim($matches[1][$match]), 6) : 0;
377
			$desired_height = !empty($matches[2][$match]) ? (int) substr(trim($matches[2][$match]), 7) : 0;
378
379
			// One was omitted, or both.  We'll have to find its real size...
380
			if (empty($desired_width) || empty($desired_height))
381
			{
382
				list ($width, $height) = url_image_size(un_htmlspecialchars($matches[4][$match]));
383
384
				// They don't have any desired width or height!
385
				if (empty($desired_width) && empty($desired_height))
386
				{
387
					$desired_width = $width;
388
					$desired_height = $height;
389
				}
390
				// Scale it to the width...
391
				elseif (empty($desired_width) && !empty($height))
392
					$desired_width = (int) (($desired_height * $width) / $height);
393
				// Scale if to the height.
394
				elseif (!empty($width))
395
					$desired_height = (int) (($desired_width * $height) / $width);
396
			}
397
398
			// If the width and height are fine, just continue along...
399
			if ($desired_width <= $modSettings['max_image_width'] && $desired_height <= $modSettings['max_image_height'])
400
				continue;
401
402
			// Too bad, it's too wide.  Make it as wide as the maximum.
403
			if ($desired_width > $modSettings['max_image_width'] && !empty($modSettings['max_image_width']))
404
			{
405
				$desired_height = (int) (($modSettings['max_image_width'] * $desired_height) / $desired_width);
406
				$desired_width = $modSettings['max_image_width'];
407
			}
408
409
			// Now check the height, as well.  Might have to scale twice, even...
410 View Code Duplication
			if ($desired_height > $modSettings['max_image_height'] && !empty($modSettings['max_image_height']))
411
			{
412
				$desired_width = (int) (($modSettings['max_image_height'] * $desired_width) / $desired_height);
413
				$desired_height = $modSettings['max_image_height'];
414
			}
415
416
			$replaces[$matches[0][$match]] = '[img' . (!empty($desired_width) ? ' width=' . $desired_width : '') . (!empty($desired_height) ? ' height=' . $desired_height : '') . ']' . $matches[4][$match] . '[/img]';
417
		}
418
419
		// If any img tags were actually changed...
420
		if (!empty($replaces))
421
			$message = strtr($message, $replaces);
422
	}
423
}
424
425
/**
426
 * Fix a specific class of tag - ie. url with =.
427
 * Used by fixTags, fixes a specific tag's links.
428
 *
429
 * @param string $message The message
430
 * @param string $myTag The tag
431
 * @param string $protocols The protocols
432
 * @param bool $embeddedUrl Whether it *can* be set to something
433
 * @param bool $hasEqualSign Whether it *is* set to something
434
 * @param bool $hasExtra Whether it can have extra cruft after the begin tag.
435
 */
436
function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false)
437
{
438
	global $boardurl, $scripturl;
439
440
	if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0)
441
		$domain_url = $match[1];
442
	else
443
		$domain_url = $boardurl . '/';
444
445
	$replaces = array();
446
447
	if ($hasEqualSign && $embeddedUrl)
448
	{
449
		$quoted = preg_match('~\[(' . $myTag . ')=&quot;~', $message);
450
		preg_match_all('~\[(' . $myTag . ')=' . ($quoted ? '&quot;(.*?)&quot;' : '([^\]]*?)') . '\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
451
	}
452
	elseif ($hasEqualSign)
453
		preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
454
	else
455
		preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches);
456
457
	foreach ($matches[0] as $k => $dummy)
458
	{
459
		// Remove all leading and trailing whitespace.
460
		$replace = trim($matches[2][$k]);
461
		$this_tag = $matches[1][$k];
462
		$this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k];
463
464
		$found = false;
465
		foreach ($protocols as $protocol)
0 ignored issues
show
Bug introduced by
The expression $protocols of type string is not traversable.
Loading history...
466
		{
467
			$found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0;
468
			if ($found)
469
				break;
470
		}
471
472
		if (!$found && $protocols[0] == 'http')
473
		{
474
			if (substr($replace, 0, 1) == '/' && substr($replace, 0, 2) != '//')
475
				$replace = $domain_url . $replace;
476
			elseif (substr($replace, 0, 1) == '?')
477
				$replace = $scripturl . $replace;
478
			elseif (substr($replace, 0, 1) == '#' && $embeddedUrl)
479
			{
480
				$replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1));
481
				$this_tag = 'iurl';
482
				$this_close = 'iurl';
483
			}
484
			elseif (substr($replace, 0, 2) != '//')
485
				$replace = $protocols[0] . '://' . $replace;
486
		}
487
		elseif (!$found && $protocols[0] == 'ftp')
488
			$replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace);
489
		elseif (!$found)
490
			$replace = $protocols[0] . '://' . $replace;
491
492
		if ($hasEqualSign && $embeddedUrl)
493
			$replaces[$matches[0][$k]] = '[' . $this_tag . '=&quot;' . $replace . '&quot;]' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']');
494
		elseif ($hasEqualSign)
495
			$replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']';
496
		elseif ($embeddedUrl)
497
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']';
498
		else
499
			$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']';
500
	}
501
502
	foreach ($replaces as $k => $v)
503
	{
504
		if ($k == $v)
505
			unset($replaces[$k]);
506
	}
507
508
	if (!empty($replaces))
509
		$message = strtr($message, $replaces);
510
}
511
512
/**
513
 * This function sends an email to the specified recipient(s).
514
 * It uses the mail_type settings and webmaster_email variable.
515
 *
516
 * @param array $to The email(s) to send to
517
 * @param string $subject Email subject, expected to have entities, and slashes, but not be parsed
518
 * @param string $message Email body, expected to have slashes, no htmlentities
519
 * @param string $from The address to use for replies
0 ignored issues
show
Documentation introduced by
Should the type for parameter $from not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
520
 * @param string $message_id If specified, it will be used as local part of the Message-ID header.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $message_id not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
521
 * @param bool $send_html Whether or not the message is HTML vs. plain text
522
 * @param int $priority The priority of the message
523
 * @param bool $hotmail_fix Whether to apply the "hotmail fix"
0 ignored issues
show
Documentation introduced by
Should the type for parameter $hotmail_fix not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
524
 * @param bool $is_private Whether this is private
525
 * @return boolean Whether ot not the email was sent properly.
526
 */
527
function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $to. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
528
{
529
	global $webmaster_email, $context, $modSettings, $txt, $scripturl;
530
531
	// Use sendmail if it's set or if no SMTP server is set.
532
	$use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
533
534
	// Line breaks need to be \r\n only in windows or for SMTP.
535
	$line_break = $context['server']['is_windows'] || !$use_sendmail ? "\r\n" : "\n";
536
537
	// So far so good.
538
	$mail_result = true;
539
540
	// If the recipient list isn't an array, make it one.
541
	$to_array = is_array($to) ? $to : array($to);
542
543
	// Make sure we actually have email addresses to send this to
544
	foreach ($to_array as $k => $v)
545
	{
546
		// This should never happen, but better safe than sorry
547
		if (trim($v) == '')
548
		{
549
			unset($to_array[$k]);
550
		}
551
	}
552
553
	// Nothing left? Nothing else to do
554
	if (empty($to_array))
555
		return true;
556
557
	// Once upon a time, Hotmail could not interpret non-ASCII mails.
558
	// In honour of those days, it's still called the 'hotmail fix'.
559
	if ($hotmail_fix === null)
560
	{
561
		$hotmail_to = array();
562
		foreach ($to_array as $i => $to_address)
563
		{
564
			if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
565
			{
566
				$hotmail_to[] = $to_address;
567
				$to_array = array_diff($to_array, array($to_address));
568
			}
569
		}
570
571
		// Call this function recursively for the hotmail addresses.
572
		if (!empty($hotmail_to))
573
			$mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true, $is_private);
574
575
		// The remaining addresses no longer need the fix.
576
		$hotmail_fix = false;
577
578
		// No other addresses left? Return instantly.
579
		if (empty($to_array))
580
			return $mail_result;
581
	}
582
583
	// Get rid of entities.
584
	$subject = un_htmlspecialchars($subject);
585
	// Make the message use the proper line breaks.
586
	$message = str_replace(array("\r", "\n"), array('', $line_break), $message);
587
588
	// Make sure hotmail mails are sent as HTML so that HTML entities work.
589
	if ($hotmail_fix && !$send_html)
590
	{
591
		$send_html = true;
592
		$message = strtr($message, array($line_break => '<br>' . $line_break));
593
		$message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
594
	}
595
596
	list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
597
	list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
598
599
	// Construct the mail headers...
600
	$headers = 'From: ' . $from_name . ' <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
601
	$headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
602
	$headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
603
	$headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
604
605
	if ($message_id !== null && empty($modSettings['mail_no_message_id']))
606
		$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
607
	$headers .= 'X-Mailer: SMF' . $line_break;
608
609
	// Pass this to the integration before we start modifying the output -- it'll make it easier later.
610
	if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers, &$to_array)), true))
611
		return false;
612
613
	// Save the original message...
614
	$orig_message = $message;
615
616
	// The mime boundary separates the different alternative versions.
617
	$mime_boundary = 'SMF-' . md5($message . time());
618
619
	// Using mime, as it allows to send a plain unencoded alternative.
620
	$headers .= 'Mime-Version: 1.0' . $line_break;
621
	$headers .= 'Content-Type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
622
	$headers .= 'Content-Transfer-Encoding: 7bit' . $line_break;
623
624
	// Sending HTML?  Let's plop in some basic stuff, then.
625
	if ($send_html)
626
	{
627
		$no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
628
629
		// But, then, dump it and use a plain one for dinosaur clients.
630
		list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
631
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
632
633
		// This is the plain text version.  Even if no one sees it, we need it for spam checkers.
634
		list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
635
		$message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
636
		$message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
637
		$message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
638
639
		// This is the actual HTML message, prim and proper.  If we wanted images, they could be inlined here (with multipart/related, etc.)
640
		list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
641
		$message .= 'Content-Type: text/html; charset=' . $charset . $line_break;
642
		$message .= 'Content-Transfer-Encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
643
		$message .= $html_message . $line_break . '--' . $mime_boundary . '--';
644
	}
645
	// Text is good too.
646
	else
647
	{
648
		// Send a plain message first, for the older web clients.
649
		list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
650
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
651
652
		// Now add an encoded message using the forum's character set.
653
		list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
654
		$message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
655
		$message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
656
		$message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
657
	}
658
659
	// Are we using the mail queue, if so this is where we butt in...
660
	if ($priority != 0)
661
		return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
662
663
	// If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
664
	elseif (!empty($modSettings['mail_limit']))
665
	{
666
		list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
667
		if (empty($mails_this_minute) || time() > $last_mail_time + 60)
668
			$new_queue_stat = time() . '|' . 1;
669
		else
670
			$new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
671
672
		updateSettings(array('mail_recent' => $new_queue_stat));
673
	}
674
675
	// SMTP or sendmail?
676
	if ($use_sendmail)
677
	{
678
		$subject = strtr($subject, array("\r" => '', "\n" => ''));
679
		if (!empty($modSettings['mail_strip_carriage']))
680
		{
681
			$message = strtr($message, array("\r" => ''));
682
			$headers = strtr($headers, array("\r" => ''));
683
		}
684
685
		foreach ($to_array as $to)
686
		{
687
			if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
688
			{
689
				log_error(sprintf($txt['mail_send_unable'], $to));
690
				$mail_result = false;
691
			}
692
693
			// Wait, wait, I'm still sending here!
694
			@set_time_limit(300);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
695
			if (function_exists('apache_reset_timeout'))
696
				@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
697
		}
698
	}
699
	else
700
		$mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
701
702
	// Everything go smoothly?
703
	return $mail_result;
704
}
705
706
/**
707
 * Add an email to the mail queue.
708
 *
709
 * @param bool $flush Whether to flush the queue
710
 * @param array $to_array An array of recipients
711
 * @param string $subject The subject of the message
712
 * @param string $message The message
713
 * @param string $headers The headers
714
 * @param bool $send_html Whether to send in HTML format
715
 * @param int $priority The priority
716
 * @param bool $is_private Whether this is private
717
 * @return boolean Whether the message was added
718
 */
719
function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
720
{
721
	global $context, $smcFunc;
722
723
	static $cur_insert = array();
724
	static $cur_insert_len = 0;
725
726
	if ($cur_insert_len == 0)
727
		$cur_insert = array();
728
729
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
730
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
731
	{
732
		// Only do these once.
733
		$cur_insert_len = 0;
734
735
		// Dump the data...
736
		$smcFunc['db_insert']('',
737
			'{db_prefix}mail_queue',
738
			array(
739
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
740
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
741
			),
742
			$cur_insert,
743
			array('id_mail')
744
		);
745
746
		$cur_insert = array();
747
		$context['flush_mail'] = false;
748
	}
749
750
	// If we're flushing we're done.
751
	if ($flush)
752
	{
753
		$nextSendTime = time() + 10;
754
755
		$smcFunc['db_query']('', '
756
			UPDATE {db_prefix}settings
757
			SET value = {string:nextSendTime}
758
			WHERE variable = {literal:mail_next_send}
759
				AND value = {string:no_outstanding}',
760
			array(
761
				'nextSendTime' => $nextSendTime,
762
				'no_outstanding' => '0',
763
			)
764
		);
765
766
		return true;
767
	}
768
769
	// Ensure we tell obExit to flush.
770
	$context['flush_mail'] = true;
771
772
	foreach ($to_array as $to)
773
	{
774
		// Will this insert go over MySQL's limit?
775
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
776
777
		// Insert limit of 1M (just under the safety) is reached?
778
		if ($this_insert_len + $cur_insert_len > 1000000)
779
		{
780
			// Flush out what we have so far.
781
			$smcFunc['db_insert']('',
782
				'{db_prefix}mail_queue',
783
				array(
784
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
785
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
786
				),
787
				$cur_insert,
788
				array('id_mail')
789
			);
790
791
			// Clear this out.
792
			$cur_insert = array();
793
			$cur_insert_len = 0;
794
		}
795
796
		// Now add the current insert to the array...
797
		$cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
798
		$cur_insert_len += $this_insert_len;
799
	}
800
801
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
802
	if (SMF === 'SSI' || SMF === 'BACKGROUND')
803
		return AddMailQueue(true);
804
805
	return true;
806
}
807
808
/**
809
 * Sends an personal message from the specified person to the specified people
810
 * ($from defaults to the user)
811
 *
812
 * @param array $recipients An array containing the arrays 'to' and 'bcc', both containing id_member's.
813
 * @param string $subject Should have no slashes and no html entities
814
 * @param string $message Should have no slashes and no html entities
815
 * @param bool $store_outbox Whether to store it in the sender's outbox
816
 * @param array $from An array with the id, name, and username of the member.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $from not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
817
 * @param int $pm_head The ID of the chain being replied to - if any.
818
 * @return array An array with log entries telling how many recipients were successful and which recipients it failed to send to.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
819
 */
820
function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
821
{
822
	global $scripturl, $txt, $user_info, $language, $sourcedir;
823
	global $modSettings, $smcFunc;
824
825
	// Make sure the PM language file is loaded, we might need something out of it.
826
	loadLanguage('PersonalMessage');
827
828
	// Initialize log array.
829
	$log = array(
830
		'failed' => array(),
831
		'sent' => array()
832
	);
833
834
	if ($from === null)
835
		$from = array(
836
			'id' => $user_info['id'],
837
			'name' => $user_info['name'],
838
			'username' => $user_info['username']
839
		);
840
841
	// This is the one that will go in their inbox.
842
	$htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
843
	preparsecode($htmlmessage);
844
	$htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => ''));
845
	if ($smcFunc['strlen']($htmlsubject) > 100)
846
		$htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100);
847
848
	// Make sure is an array
849
	if (!is_array($recipients))
850
		$recipients = array($recipients);
851
852
	// Integrated PMs
853
	call_integration_hook('integrate_personal_message', array(&$recipients, &$from, &$subject, &$message));
854
855
	// Get a list of usernames and convert them to IDs.
856
	$usernames = array();
857
	foreach ($recipients as $rec_type => $rec)
858
	{
859
		foreach ($rec as $id => $member)
860
		{
861
			if (!is_numeric($recipients[$rec_type][$id]))
862
			{
863
				$recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('~[<>&"\'=\\\]~', '', $recipients[$rec_type][$id])));
864
				$usernames[$recipients[$rec_type][$id]] = 0;
865
			}
866
		}
867
	}
868
	if (!empty($usernames))
869
	{
870
		$request = $smcFunc['db_query']('pm_find_username', '
871
			SELECT id_member, member_name
872
			FROM {db_prefix}members
873
			WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
874
			array(
875
				'usernames' => array_keys($usernames),
876
			)
877
		);
878
		while ($row = $smcFunc['db_fetch_assoc']($request))
879
			if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
880
				$usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
881
		$smcFunc['db_free_result']($request);
882
883
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
884
		foreach ($recipients as $rec_type => $rec)
885
			foreach ($rec as $id => $member)
886
			{
887
				if (is_numeric($recipients[$rec_type][$id]))
888
					continue;
889
890
				if (!empty($usernames[$member]))
891
					$recipients[$rec_type][$id] = $usernames[$member];
892
				else
893
				{
894
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
895
					unset($recipients[$rec_type][$id]);
896
				}
897
			}
898
	}
899
900
	// Make sure there are no duplicate 'to' members.
901
	$recipients['to'] = array_unique($recipients['to']);
902
903
	// Only 'bcc' members that aren't already in 'to'.
904
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
905
906
	// Combine 'to' and 'bcc' recipients.
907
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
908
909
	// Check no-one will want it deleted right away!
910
	$request = $smcFunc['db_query']('', '
911
		SELECT
912
			id_member, criteria, is_or
913
		FROM {db_prefix}pm_rules
914
		WHERE id_member IN ({array_int:to_members})
915
			AND delete_pm = {int:delete_pm}',
916
		array(
917
			'to_members' => $all_to,
918
			'delete_pm' => 1,
919
		)
920
	);
921
	$deletes = array();
922
	// Check whether we have to apply anything...
923
	while ($row = $smcFunc['db_fetch_assoc']($request))
924
	{
925
		$criteria = $smcFunc['json_decode']($row['criteria'], true);
926
		// Note we don't check the buddy status, cause deletion from buddy = madness!
927
		$delete = false;
928
		foreach ($criteria as $criterium)
929
		{
930
			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))
931
				$delete = true;
932
			// If we're adding and one criteria don't match then we stop!
933
			elseif (!$row['is_or'])
934
			{
935
				$delete = false;
936
				break;
937
			}
938
		}
939
		if ($delete)
940
			$deletes[$row['id_member']] = 1;
941
	}
942
	$smcFunc['db_free_result']($request);
943
944
	// Load the membergrounp message limits.
945
	// @todo Consider caching this?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
946
	static $message_limit_cache = array();
947 View Code Duplication
	if (!allowedTo('moderate_forum') && empty($message_limit_cache))
948
	{
949
		$request = $smcFunc['db_query']('', '
950
			SELECT id_group, max_messages
951
			FROM {db_prefix}membergroups',
952
			array(
953
			)
954
		);
955
		while ($row = $smcFunc['db_fetch_assoc']($request))
956
			$message_limit_cache[$row['id_group']] = $row['max_messages'];
957
		$smcFunc['db_free_result']($request);
958
	}
959
960
	// Load the groups that are allowed to read PMs.
961
	require_once($sourcedir . '/Subs-Members.php');
962
	$pmReadGroups = groupsAllowedTo('pm_read');
963
964
	if (empty($modSettings['permission_enable_deny']))
965
		$pmReadGroups['denied'] = array();
966
967
	// Load their alert preferences
968
	require_once($sourcedir . '/Subs-Notify.php');
969
	$notifyPrefs = getNotifyPrefs($all_to, array('pm_new', 'pm_reply', 'pm_notify'), true);
970
971
	$request = $smcFunc['db_query']('', '
972
		SELECT
973
			member_name, real_name, id_member, email_address, lngfile
974
			instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
975
			(pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
976
			(pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
977
			(pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
978
			FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
979
			additional_groups, id_group, id_post_group
980
		FROM {db_prefix}members
981
		WHERE id_member IN ({array_int:recipients})
982
		ORDER BY lngfile
983
		LIMIT {int:count_recipients}',
984
		array(
985
			'not_on_ignore_list' => 1,
986
			'buddies_only' => 2,
987
			'admins_only' => 3,
988
			'recipients' => $all_to,
989
			'count_recipients' => count($all_to),
990
			'from_id' => $from['id'],
991
		)
992
	);
993
	$notifications = array();
994
	while ($row = $smcFunc['db_fetch_assoc']($request))
995
	{
996
		// Don't do anything for members to be deleted!
997
		if (isset($deletes[$row['id_member']]))
998
			continue;
999
1000
		// Load the preferences for this member (if any)
1001
		$prefs = !empty($notifyPrefs[$row['id_member']]) ? $notifyPrefs[$row['id_member']] : array();
1002
		$prefs = array_merge(array(
1003
			'pm_new' => 0,
1004
			'pm_reply' => 0,
1005
			'pm_notify' => 0,
1006
		), $prefs);
1007
1008
		// We need to know this members groups.
1009
		$groups = explode(',', $row['additional_groups']);
1010
		$groups[] = $row['id_group'];
1011
		$groups[] = $row['id_post_group'];
1012
1013
		$message_limit = -1;
1014
		// For each group see whether they've gone over their limit - assuming they're not an admin.
1015
		if (!in_array(1, $groups))
1016
		{
1017
			foreach ($groups as $id)
1018
			{
1019
				if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
1020
					$message_limit = $message_limit_cache[$id];
1021
			}
1022
1023
			if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
1024
			{
1025
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
1026
				unset($all_to[array_search($row['id_member'], $all_to)]);
1027
				continue;
1028
			}
1029
1030
			// Do they have any of the allowed groups?
1031
			if (count(array_intersect($pmReadGroups['allowed'], $groups)) == 0 || count(array_intersect($pmReadGroups['denied'], $groups)) != 0)
1032
			{
1033
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1034
				unset($all_to[array_search($row['id_member'], $all_to)]);
1035
				continue;
1036
			}
1037
		}
1038
1039
		// Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
1040
		if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id'])
1041
		{
1042
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
1043
			unset($all_to[array_search($row['id_member'], $all_to)]);
1044
			continue;
1045
		}
1046
1047
		// If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
1048
		if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin']))
1049
		{
1050
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1051
			unset($all_to[array_search($row['id_member'], $all_to)]);
1052
			continue;
1053
		}
1054
1055
		// Send a notification, if enabled - taking the buddy list into account.
1056
		if (!empty($row['email_address'])
1057
			&& ((empty($pm_head) && $prefs['pm_new'] & 0x02) || (!empty($pm_head) && $prefs['pm_reply'] & 0x02))
1058
			&& ($prefs['pm_notify'] <= 1 || ($prefs['pm_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
1059
		{
1060
			$notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
1061
		}
1062
1063
		$log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
1064
	}
1065
	$smcFunc['db_free_result']($request);
1066
1067
	// Only 'send' the message if there are any recipients left.
1068
	if (empty($all_to))
1069
		return $log;
1070
1071
	// Insert the message itself and then grab the last insert id.
1072
	$id_pm = $smcFunc['db_insert']('',
1073
		'{db_prefix}personal_messages',
1074
		array(
1075
			'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
1076
			'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
1077
		),
1078
		array(
1079
			$pm_head, $from['id'], ($store_outbox ? 0 : 1),
1080
			$from['username'], time(), $htmlsubject, $htmlmessage,
1081
		),
1082
		array('id_pm'),
1083
		1
1084
	);
1085
1086
	// Add the recipients.
1087
	if (!empty($id_pm))
1088
	{
1089
		// If this is new we need to set it part of it's own conversation.
1090
		if (empty($pm_head))
1091
			$smcFunc['db_query']('', '
1092
				UPDATE {db_prefix}personal_messages
1093
				SET id_pm_head = {int:id_pm_head}
1094
				WHERE id_pm = {int:id_pm_head}',
1095
				array(
1096
					'id_pm_head' => $id_pm,
1097
				)
1098
			);
1099
1100
		// Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
1101
		$smcFunc['db_query']('', '
1102
			DELETE FROM {db_prefix}pm_recipients
1103
			WHERE id_pm = {int:id_pm}',
1104
			array(
1105
				'id_pm' => $id_pm,
1106
			)
1107
		);
1108
1109
		$insertRows = array();
1110
		$to_list = array();
1111
		foreach ($all_to as $to)
1112
		{
1113
			$insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
1114
			if (!in_array($to, $recipients['bcc']))
1115
				$to_list[] = $to;
1116
		}
1117
1118
		$smcFunc['db_insert']('insert',
1119
			'{db_prefix}pm_recipients',
1120
			array(
1121
				'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
1122
			),
1123
			$insertRows,
1124
			array('id_pm', 'id_member')
1125
		);
1126
	}
1127
1128
	censorText($subject);
1129
	if (empty($modSettings['disallow_sendBody']))
1130
	{
1131
		censorText($message);
1132
		$message = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($smcFunc['htmlspecialchars']($message), false), array('<br>' => "\n", '</div>' => "\n", '</li>' => "\n", '&#91;' => '[', '&#93;' => ']')))));
1133
	}
1134
	else
1135
		$message = '';
1136
1137
	$to_names = array();
1138 View Code Duplication
	if (count($to_list) > 1)
1139
	{
1140
		$request = $smcFunc['db_query']('', '
1141
			SELECT real_name
1142
			FROM {db_prefix}members
1143
			WHERE id_member IN ({array_int:to_members})',
1144
			array(
1145
				'to_members' => $to_list,
0 ignored issues
show
Bug introduced by
The variable $to_list does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1146
			)
1147
		);
1148
		while ($row = $smcFunc['db_fetch_assoc']($request))
1149
			$to_names[] = un_htmlspecialchars($row['real_name']);
1150
		$smcFunc['db_free_result']($request);
1151
	}
1152
	$replacements = array(
1153
		'SUBJECT' => $subject,
1154
		'MESSAGE' => $message,
1155
		'SENDER' => un_htmlspecialchars($from['name']),
1156
		'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm,
1157
		'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'],
1158
		'TOLIST' => implode(', ', $to_names),
1159
	);
1160
	$email_template = 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : '');
1161
1162
	foreach ($notifications as $lang => $notification_list)
1163
	{
1164
		$emaildata = loadEmailTemplate($email_template, $replacements, $lang);
1165
1166
		// Off the notification email goes!
1167
		sendmail($notification_list, $emaildata['subject'], $emaildata['body'], null, 'p' . $id_pm, $emaildata['is_html'], 2, null, true);
1168
	}
1169
1170
	// Integrated After PMs
1171
	call_integration_hook('integrate_personal_message_after', array(&$id_pm, &$log, &$recipients, &$from, &$subject, &$message));
1172
1173
	// Back to what we were on before!
1174
	loadLanguage('index+PersonalMessage');
1175
1176
	// Add one to their unread and read message counts.
1177
	foreach ($all_to as $k => $id)
1178
		if (isset($deletes[$id]))
1179
			unset($all_to[$k]);
1180
	if (!empty($all_to))
1181
		updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
1182
1183
	return $log;
1184
}
1185
1186
/**
1187
 * Prepare text strings for sending as email body or header.
1188
 * In case there are higher ASCII characters in the given string, this
1189
 * function will attempt the transport method 'quoted-printable'.
1190
 * Otherwise the transport method '7bit' is used.
1191
 *
1192
 * @param string $string The string
1193
 * @param bool $with_charset Whether we're specifying a charset ($custom_charset must be set here)
1194
 * @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)
1195
 * @param string $line_break The linebreak
1196
 * @param string $custom_charset If set, it uses this character set
0 ignored issues
show
Documentation introduced by
Should the type for parameter $custom_charset not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1197
 * @return array An array containing the character set, the converted string and the transport method.
1198
 */
1199
function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
1200
{
1201
	global $context;
1202
1203
	$charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
1204
1205
	// This is the fun part....
1206
	if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
1207
	{
1208
		// Let's, for now, assume there are only &#021;'ish characters.
1209
		$simple = true;
1210
1211
		foreach ($matches[1] as $entity)
1212
			if ($entity > 128)
1213
				$simple = false;
1214
		unset($matches);
1215
1216
		if ($simple)
1217
			$string = preg_replace_callback('~&#(\d{3,8});~', function($m)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1218
			{
1219
				return chr("$m[1]");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
1220
			}, $string);
1221
		else
1222
		{
1223
			// Try to convert the string to UTF-8.
1224 View Code Duplication
			if (!$context['utf8'] && function_exists('iconv'))
1225
			{
1226
				$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1227
				if ($newstring)
1228
					$string = $newstring;
1229
			}
1230
1231
			$string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string);
1232
1233
			// Unicode, baby.
1234
			$charset = 'UTF-8';
1235
		}
1236
	}
1237
1238
	// Convert all special characters to HTML entities...just for Hotmail :-\
1239
	if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
1240
	{
1241 View Code Duplication
		if (!$context['utf8'] && function_exists('iconv'))
1242
		{
1243
			$newstring = @iconv($context['character_set'], 'UTF-8', $string);
1244
			if ($newstring)
1245
				$string = $newstring;
1246
		}
1247
1248
		$entityConvert = function($m)
1249
		{
1250
			$c = $m[1];
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $c. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1251
			if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
1252
				return $c;
1253
			elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
1254
				return "&#" . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ";";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal &# does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal ; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1255
			elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
1256
				return "&#" . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ";";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal &# does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal ; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1257
			elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
1258
				return "&#" . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ";";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal &# does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal ; does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1259
			else
1260
				return "";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1261
		};
1262
1263
		// Convert all 'special' characters to HTML entities.
1264
		return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', $entityConvert, $string), '7bit');
1265
	}
1266
1267
	// We don't need to mess with the subject line if no special characters were in it..
1268
	elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
1269
	{
1270
		// Base64 encode.
1271
		$string = base64_encode($string);
1272
1273
		// Show the characterset and the transfer-encoding for header strings.
1274
		if ($with_charset)
1275
			$string = '=?' . $charset . '?B?' . $string . '?=';
1276
1277
		// Break it up in lines (mail body).
1278
		else
1279
			$string = chunk_split($string, 76, $line_break);
1280
1281
		return array($charset, $string, 'base64');
1282
	}
1283
1284
	else
1285
		return array($charset, $string, '7bit');
1286
}
1287
1288
/**
1289
 * Sends mail, like mail() but over SMTP.
1290
 * It expects no slashes or entities.
1291
 * @internal
1292
 *
1293
 * @param array $mail_to_array Array of strings (email addresses)
1294
 * @param string $subject Email subject
1295
 * @param string $message Email message
1296
 * @param string $headers Email headers
1297
 * @return boolean Whether it sent or not.
1298
 */
1299
function smtp_mail($mail_to_array, $subject, $message, $headers)
1300
{
1301
	global $modSettings, $webmaster_email, $txt;
1302
1303
	$modSettings['smtp_host'] = trim($modSettings['smtp_host']);
1304
1305
	// Try POP3 before SMTP?
1306
	// @todo There's no interface for this yet.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1307
	if ($modSettings['mail_type'] == 3 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1308
	{
1309
		$socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
1310
		if (!$socket && (substr($modSettings['smtp_host'], 0, 5) == 'smtp.' || substr($modSettings['smtp_host'], 0, 11) == 'ssl://smtp.'))
1311
			$socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
1312
1313
		if ($socket)
1314
		{
1315
			fgets($socket, 256);
1316
			fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
1317
			fgets($socket, 256);
1318
			fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
1319
			fgets($socket, 256);
1320
			fputs($socket, 'QUIT' . "\r\n");
1321
1322
			fclose($socket);
1323
		}
1324
	}
1325
1326
	// Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
1327
	if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
1328
	{
1329
		// Maybe we can still save this?  The port might be wrong.
1330
		if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
1331
		{
1332
			if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3))
1333
				log_error($txt['smtp_port_ssl']);
1334
		}
1335
1336
		// Unable to connect!  Don't show any error message, but just log one and try to continue anyway.
1337
		if (!$socket)
1338
		{
1339
			log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
1340
			return false;
1341
		}
1342
	}
1343
1344
	// Wait for a response of 220, without "-" continuer.
1345
	if (!server_parse(null, $socket, '220'))
1346
		return false;
1347
1348
	// Try and determine the servers name, fall back to the mail servers if not found
1349
	$helo = false;
1350
	if (function_exists('gethostname') && gethostname() !== false)
1351
		$helo = gethostname();
1352
	elseif (function_exists('php_uname'))
1353
		$helo = php_uname('n');
1354
	elseif (array_key_exists('SERVER_NAME', $_SERVER) && !empty($_SERVER['SERVER_NAME']))
1355
		$helo = $_SERVER['SERVER_NAME'];
1356
1357
	if (empty($helo))
1358
		$helo = $modSettings['smtp_host'];
1359
1360
	// SMTP = 1, SMTP - STARTTLS = 2
1361
	if (in_array($modSettings['mail_type'], array(1, 2)) && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1362
	{
1363
		// EHLO could be understood to mean encrypted hello...
1364
		if (server_parse('EHLO ' . $helo, $socket, null, $response) == '250')
1365
		{
1366
			// Are we using STARTTLS and does the server support STARTTLS?
1367
			if ($modSettings['mail_type'] == 2 && preg_match("~250( |-)STARTTLS~mi", $response))
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal ~250( |-)STARTTLS~mi does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1368
			{
1369
				// Send STARTTLS to enable encryption
1370
				if (!server_parse('STARTTLS', $socket, '220'))
1371
					return false;
1372
				// Enable the encryption
1373
				if (!@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT))
1374
					return false;
1375
				// Send the EHLO command again
1376
				if (!server_parse('EHLO ' . $helo, $socket, null) == '250')
1377
					return false;
1378
			}
1379
1380
			if (!server_parse('AUTH LOGIN', $socket, '334'))
1381
				return false;
1382
			// Send the username and password, encoded.
1383
			if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1384
				return false;
1385
			// The password is already encoded ;)
1386
			if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1387
				return false;
1388
		}
1389
		elseif (!server_parse('HELO ' . $helo, $socket, '250'))
1390
			return false;
1391
	}
1392
	else
1393
	{
1394
		// Just say "helo".
1395
		if (!server_parse('HELO ' . $helo, $socket, '250'))
1396
			return false;
1397
	}
1398
1399
	// Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1400
	$message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1401
1402
	// !! Theoretically, we should be able to just loop the RCPT TO.
1403
	$mail_to_array = array_values($mail_to_array);
1404
	foreach ($mail_to_array as $i => $mail_to)
1405
	{
1406
		// Reset the connection to send another email.
1407
		if ($i != 0)
1408
		{
1409
			if (!server_parse('RSET', $socket, '250'))
1410
				return false;
1411
		}
1412
1413
		// From, to, and then start the data...
1414
		if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1415
			return false;
1416
		if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1417
			return false;
1418
		if (!server_parse('DATA', $socket, '354'))
1419
			return false;
1420
		fputs($socket, 'Subject: ' . $subject . "\r\n");
1421
		if (strlen($mail_to) > 0)
1422
			fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1423
		fputs($socket, $headers . "\r\n\r\n");
1424
		fputs($socket, $message . "\r\n");
1425
1426
		// Send a ., or in other words "end of data".
1427
		if (!server_parse('.', $socket, '250'))
1428
			return false;
1429
1430
		// Almost done, almost done... don't stop me just yet!
1431
		@set_time_limit(300);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
1432
		if (function_exists('apache_reset_timeout'))
1433
			@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
1434
	}
1435
	fputs($socket, 'QUIT' . "\r\n");
1436
	fclose($socket);
1437
1438
	return true;
1439
}
1440
1441
/**
1442
 * Parse a message to the SMTP server.
1443
 * Sends the specified message to the server, and checks for the
1444
 * expected response.
1445
 * @internal
1446
 *
1447
 * @param string $message The message to send
1448
 * @param resource $socket Socket to send on
1449
 * @param string $code The expected response code
1450
 * @param string $response The response from the SMTP server
0 ignored issues
show
Documentation introduced by
Should the type for parameter $response not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1451
 * @return bool Whether it responded as such.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1452
 */
1453
function server_parse($message, $socket, $code, &$response = null)
1454
{
1455
	global $txt;
1456
1457
	if ($message !== null)
1458
		fputs($socket, $message . "\r\n");
1459
1460
	// No response yet.
1461
	$server_response = '';
1462
1463
	while (substr($server_response, 3, 1) != ' ')
1464
	{
1465
		if (!($server_response = fgets($socket, 256)))
1466
		{
1467
			// @todo Change this message to reflect that it may mean bad user/password/server issues/etc.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1468
			log_error($txt['smtp_bad_response']);
1469
			return false;
1470
		}
1471
		$response .= $server_response;
1472
	}
1473
1474
	if ($code === null)
1475
		return substr($server_response, 0, 3);
1476
1477
	if (substr($server_response, 0, 3) != $code)
1478
	{
1479
		log_error($txt['smtp_error'] . $server_response);
1480
		return false;
1481
	}
1482
1483
	return true;
1484
}
1485
1486
/**
1487
 * Spell checks the post for typos ;).
1488
 * It uses the pspell or enchant library, one of which MUST be installed.
1489
 * It has problems with internationalization.
1490
 * It is accessed via ?action=spellcheck.
1491
 */
1492
function SpellCheck()
1493
{
1494
	global $txt, $context, $smcFunc;
1495
1496
	// A list of "words" we know about but pspell doesn't.
1497
	$known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1498
1499
	loadLanguage('Post');
1500
	loadTemplate('Post');
1501
1502
	// Create a pspell or enchant dictionary resource
1503
	$dict = spell_init();
1504
1505
	if (!isset($_POST['spellstring']) || !$dict)
0 ignored issues
show
Bug Best Practice introduced by
The expression $dict of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1506
		die;
1507
1508
	// Construct a bit of Javascript code.
1509
	$context['spell_js'] = '
1510
		var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1511
		var mispstr = window.opener.spellCheckGetText(spell_fieldname);
1512
		var misps = Array(';
1513
1514
	// Get all the words (Javascript already separated them).
1515
	$alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1516
1517
	$found_words = false;
1518
	for ($i = 0, $n = count($alphas); $i < $n; $i++)
1519
	{
1520
		// Words are sent like 'word|offset_begin|offset_end'.
1521
		$check_word = explode('|', $alphas[$i]);
1522
1523
		// If the word is a known word, or spelled right...
1524
		if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || spell_check($dict, $check_word[0]) || !isset($check_word[2]))
0 ignored issues
show
Documentation introduced by
$dict is of type integer, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1525
			continue;
1526
1527
		// Find the word, and move up the "last occurrence" to here.
1528
		$found_words = true;
1529
1530
		// Add on the javascript for this misspelling.
1531
		$context['spell_js'] .= '
1532
			new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '&gt;' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1533
1534
		// If there are suggestions, add them in...
1535
		$suggestions = spell_suggest($dict, $check_word[0]);
0 ignored issues
show
Documentation introduced by
$dict is of type integer, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1536
		if (!empty($suggestions))
1537
		{
1538
			// But first check they aren't going to be censored - no naughty words!
1539
			foreach ($suggestions as $k => $word)
1540
				if ($suggestions[$k] != censorText($word))
1541
					unset($suggestions[$k]);
1542
1543
			if (!empty($suggestions))
1544
				$context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1545
		}
1546
1547
		$context['spell_js'] .= ']),';
1548
	}
1549
1550
	// If words were found, take off the last comma.
1551
	if ($found_words)
1552
		$context['spell_js'] = substr($context['spell_js'], 0, -1);
1553
1554
	$context['spell_js'] .= '
1555
		);';
1556
1557
	// And instruct the template system to just show the spellcheck sub template.
1558
	$context['template_layers'] = array();
1559
	$context['sub_template'] = 'spellcheck';
1560
1561
	// Free resources for enchant...
1562
	if (isset($context['enchant_broker']))
1563
	{
1564
		enchant_broker_free_dict($dict);
1565
		enchant_broker_free($context['enchant_broker']);
1566
	}
1567
}
1568
1569
/**
1570
 * Sends a notification to members who have elected to receive emails
1571
 * when things happen to a topic, such as replies are posted.
1572
 * The function automatically finds the subject and its board, and
1573
 * checks permissions for each member who is "signed up" for notifications.
1574
 * It will not send 'reply' notifications more than once in a row.
1575
 *
1576
 * @param array $topics Represents the topics the action is happening to.
1577
 * @param string $type Can be any of reply, sticky, lock, unlock, remove, move, merge, and split.  An appropriate message will be sent for each.
1578
 * @param array $exclude Members in the exclude array will not be processed for the topic with the same key.
1579
 * @param array $members_only Are the only ones that will be sent the notification if they have it on.
1580
 * @uses Post language file
1581
 */
1582
function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
0 ignored issues
show
Unused Code introduced by
The parameter $exclude is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1583
{
1584
	global $user_info, $smcFunc;
1585
1586
	// Can't do it if there's no topics.
1587
	if (empty($topics))
1588
		return;
1589
	// It must be an array - it must!
1590
	if (!is_array($topics))
1591
		$topics = array($topics);
1592
1593
	// Get the subject and body...
1594
	$result = $smcFunc['db_query']('', '
1595
		SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board,
1596
			COALESCE(mem.real_name, ml.poster_name) AS poster_name, mf.id_msg
1597
		FROM {db_prefix}topics AS t
1598
			INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1599
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1600
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1601
		WHERE t.id_topic IN ({array_int:topic_list})
1602
		LIMIT 1',
1603
		array(
1604
			'topic_list' => $topics,
1605
		)
1606
	);
1607
	$task_rows = array();
1608
	while ($row = $smcFunc['db_fetch_assoc']($result))
1609
	{
1610
		// Clean it up.
1611
		censorText($row['subject']);
1612
		censorText($row['body']);
1613
		$row['subject'] = un_htmlspecialchars($row['subject']);
1614
		$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;' => ']')))));
1615
1616
		$task_rows[] = array(
1617
			'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
1618
				'msgOptions' => array(
1619
					'id' => $row['id_msg'],
1620
					'subject' => $row['subject'],
1621
					'body' => $row['body'],
1622
				),
1623
				'topicOptions' => array(
1624
					'id' => $row['id_topic'],
1625
					'board' => $row['id_board'],
1626
				),
1627
				// Kinda cheeky, but for any action the originator is usually the current user
1628
				'posterOptions' => array(
1629
					'id' => $user_info['id'],
1630
					'name' => $user_info['name'],
1631
				),
1632
				'type' => $type,
1633
				'members_only' => $members_only,
1634
			)), 0
1635
		);
1636
	}
1637
	$smcFunc['db_free_result']($result);
1638
1639 View Code Duplication
	if (!empty($task_rows))
1640
		$smcFunc['db_insert']('',
1641
			'{db_prefix}background_tasks',
1642
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1643
			$task_rows,
1644
			array('id_task')
1645
		);
1646
}
1647
1648
/**
1649
 * Create a post, either as new topic (id_topic = 0) or in an existing one.
1650
 * The input parameters of this function assume:
1651
 * - Strings have been escaped.
1652
 * - Integers have been cast to integer.
1653
 * - Mandatory parameters are set.
1654
 *
1655
 * @param array $msgOptions An array of information/options for the post
1656
 * @param array $topicOptions An array of information/options for the topic
1657
 * @param array $posterOptions An array of information/options for the poster
1658
 * @return bool Whether the operation was a success
1659
 */
1660
function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1661
{
1662
	global $user_info, $txt, $modSettings, $smcFunc, $sourcedir;
1663
1664
	require_once($sourcedir . '/Mentions.php');
1665
1666
	// Set optional parameters to the default value.
1667
	$msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1668
	$msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1669
	$msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1670
	$msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1671
	$topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1672
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1673
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1674
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1675
	$topicOptions['redirect_expires'] = isset($topicOptions['redirect_expires']) ? $topicOptions['redirect_expires'] : null;
1676
	$topicOptions['redirect_topic'] = isset($topicOptions['redirect_topic']) ? $topicOptions['redirect_topic'] : null;
1677
	$posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1678
	$posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1679
1680
	// Not exactly a post option but it allows hooks and/or other sources to skip sending notifications if they don't want to
1681
	$msgOptions['send_notifications'] = isset($msgOptions['send_notifications']) ? (bool) $msgOptions['send_notifications'] : true;
1682
1683
	// We need to know if the topic is approved. If we're told that's great - if not find out.
1684 View Code Duplication
	if (!$modSettings['postmod_active'])
1685
		$topicOptions['is_approved'] = true;
1686
	elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1687
	{
1688
		$request = $smcFunc['db_query']('', '
1689
			SELECT approved
1690
			FROM {db_prefix}topics
1691
			WHERE id_topic = {int:id_topic}
1692
			LIMIT 1',
1693
			array(
1694
				'id_topic' => $topicOptions['id'],
1695
			)
1696
		);
1697
		list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1698
		$smcFunc['db_free_result']($request);
1699
	}
1700
1701
	// If nothing was filled in as name/e-mail address, try the member table.
1702
	if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1703
	{
1704
		if (empty($posterOptions['id']))
1705
		{
1706
			$posterOptions['id'] = 0;
1707
			$posterOptions['name'] = $txt['guest_title'];
1708
			$posterOptions['email'] = '';
1709
		}
1710
		elseif ($posterOptions['id'] != $user_info['id'])
1711
		{
1712
			$request = $smcFunc['db_query']('', '
1713
				SELECT member_name, email_address
1714
				FROM {db_prefix}members
1715
				WHERE id_member = {int:id_member}
1716
				LIMIT 1',
1717
				array(
1718
					'id_member' => $posterOptions['id'],
1719
				)
1720
			);
1721
			// Couldn't find the current poster?
1722
			if ($smcFunc['db_num_rows']($request) == 0)
1723
			{
1724
				trigger_error('createPost(): Invalid member id ' . $posterOptions['id'], E_USER_NOTICE);
1725
				$posterOptions['id'] = 0;
1726
				$posterOptions['name'] = $txt['guest_title'];
1727
				$posterOptions['email'] = '';
1728
			}
1729
			else
1730
				list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1731
			$smcFunc['db_free_result']($request);
1732
		}
1733
		else
1734
		{
1735
			$posterOptions['name'] = $user_info['name'];
1736
			$posterOptions['email'] = $user_info['email'];
1737
		}
1738
	}
1739
1740
	if (!empty($modSettings['enable_mentions']))
1741
	{
1742
		$msgOptions['mentioned_members'] = Mentions::getMentionedMembers($msgOptions['body']);
1743
		if (!empty($msgOptions['mentioned_members']))
1744
			$msgOptions['body'] = Mentions::getBody($msgOptions['body'], $msgOptions['mentioned_members']);
1745
	}
1746
1747
	// It's do or die time: forget any user aborts!
1748
	$previous_ignore_user_abort = ignore_user_abort(true);
1749
1750
	$new_topic = empty($topicOptions['id']);
1751
1752
	$message_columns = array(
1753
		'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')),
1754
		'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'inet',
1755
		'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1756
	);
1757
1758
	$message_parameters = array(
1759
		$topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1760
		$posterOptions['name'], $posterOptions['email'], time(), $posterOptions['ip'],
1761
		$msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1762
	);
1763
1764
	// What if we want to do anything with posts?
1765
	call_integration_hook('integrate_create_post', array(&$msgOptions, &$topicOptions, &$posterOptions, &$message_columns, &$message_parameters));
1766
1767
	// Insert the post.
1768
	$msgOptions['id'] = $smcFunc['db_insert']('',
1769
		'{db_prefix}messages',
1770
		$message_columns,
1771
		$message_parameters,
1772
		array('id_msg'),
1773
		1
1774
	);
1775
1776
	// Something went wrong creating the message...
1777
	if (empty($msgOptions['id']))
1778
		return false;
1779
1780
	// Fix the attachments.
1781
	if (!empty($msgOptions['attachments']))
1782
		$smcFunc['db_query']('', '
1783
			UPDATE {db_prefix}attachments
1784
			SET id_msg = {int:id_msg}
1785
			WHERE id_attach IN ({array_int:attachment_list})',
1786
			array(
1787
				'attachment_list' => $msgOptions['attachments'],
1788
				'id_msg' => $msgOptions['id'],
1789
			)
1790
		);
1791
1792
	// What if we want to export new posts out to a CMS?
1793
	call_integration_hook('integrate_after_create_post', array($msgOptions, $topicOptions, $posterOptions, $message_columns, $message_parameters));
1794
1795
	// Insert a new topic (if the topicID was left empty.)
1796
	if ($new_topic)
1797
	{
1798
		$topic_columns = array(
1799
			'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1800
			'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1801
			'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1802
			'redirect_expires' => 'int', 'id_redirect_topic' => 'int',
1803
		);
1804
		$topic_parameters = array(
1805
			$topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1806
			$msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1807
			$topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1808
			$topicOptions['redirect_expires'] === null ? 0 : $topicOptions['redirect_expires'], $topicOptions['redirect_topic'] === null ? 0 : $topicOptions['redirect_topic'],
1809
		);
1810
1811
		call_integration_hook('integrate_before_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions, &$topic_columns, &$topic_parameters));
1812
1813
		$topicOptions['id'] = $smcFunc['db_insert']('',
1814
			'{db_prefix}topics',
1815
			$topic_columns,
1816
			$topic_parameters,
1817
			array('id_topic'),
1818
			1
1819
		);
1820
1821
		// The topic couldn't be created for some reason.
1822
		if (empty($topicOptions['id']))
1823
		{
1824
			// We should delete the post that did work, though...
1825
			$smcFunc['db_query']('', '
1826
				DELETE FROM {db_prefix}messages
1827
				WHERE id_msg = {int:id_msg}',
1828
				array(
1829
					'id_msg' => $msgOptions['id'],
1830
				)
1831
			);
1832
1833
			return false;
1834
		}
1835
1836
		// Fix the message with the topic.
1837
		$smcFunc['db_query']('', '
1838
			UPDATE {db_prefix}messages
1839
			SET id_topic = {int:id_topic}
1840
			WHERE id_msg = {int:id_msg}',
1841
			array(
1842
				'id_topic' => $topicOptions['id'],
1843
				'id_msg' => $msgOptions['id'],
1844
			)
1845
		);
1846
1847
		// There's been a new topic AND a new post today.
1848
		trackStats(array('topics' => '+', 'posts' => '+'));
1849
1850
		updateStats('topic', true);
1851
		updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1852
1853
		// What if we want to export new topics out to a CMS?
1854
		call_integration_hook('integrate_create_topic', array(&$msgOptions, &$topicOptions, &$posterOptions));
1855
	}
1856
	// The topic already exists, it only needs a little updating.
1857
	else
1858
	{
1859
		$update_parameters = array(
1860
			'poster_id' => $posterOptions['id'],
1861
			'id_msg' => $msgOptions['id'],
1862
			'locked' => $topicOptions['lock_mode'],
1863
			'is_sticky' => $topicOptions['sticky_mode'],
1864
			'id_topic' => $topicOptions['id'],
1865
			'counter_increment' => 1,
1866
		);
1867
		if ($msgOptions['approved'])
1868
			$topics_columns = array(
1869
				'id_member_updated = {int:poster_id}',
1870
				'id_last_msg = {int:id_msg}',
1871
				'num_replies = num_replies + {int:counter_increment}',
1872
			);
1873
		else
1874
			$topics_columns = array(
1875
				'unapproved_posts = unapproved_posts + {int:counter_increment}',
1876
			);
1877
		if ($topicOptions['lock_mode'] !== null)
1878
			$topics_columns[] = 'locked = {int:locked}';
1879
		if ($topicOptions['sticky_mode'] !== null)
1880
			$topics_columns[] = 'is_sticky = {int:is_sticky}';
1881
1882
		call_integration_hook('integrate_modify_topic', array(&$topics_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions));
1883
1884
		// Update the number of replies and the lock/sticky status.
1885
		$smcFunc['db_query']('', '
1886
			UPDATE {db_prefix}topics
1887
			SET
1888
				' . implode(', ', $topics_columns) . '
1889
			WHERE id_topic = {int:id_topic}',
1890
			$update_parameters
1891
		);
1892
1893
		// One new post has been added today.
1894
		trackStats(array('posts' => '+'));
1895
	}
1896
1897
	// Creating is modifying...in a way.
1898
	// @todo Why not set id_msg_modified on the insert?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1899
	$smcFunc['db_query']('', '
1900
		UPDATE {db_prefix}messages
1901
		SET id_msg_modified = {int:id_msg}
1902
		WHERE id_msg = {int:id_msg}',
1903
		array(
1904
			'id_msg' => $msgOptions['id'],
1905
		)
1906
	);
1907
1908
	// Increase the number of posts and topics on the board.
1909
	if ($msgOptions['approved'])
1910
		$smcFunc['db_query']('', '
1911
			UPDATE {db_prefix}boards
1912
			SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
1913
			WHERE id_board = {int:id_board}',
1914
			array(
1915
				'id_board' => $topicOptions['board'],
1916
			)
1917
		);
1918
	else
1919
	{
1920
		$smcFunc['db_query']('', '
1921
			UPDATE {db_prefix}boards
1922
			SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
1923
			WHERE id_board = {int:id_board}',
1924
			array(
1925
				'id_board' => $topicOptions['board'],
1926
			)
1927
		);
1928
1929
		// Add to the approval queue too.
1930
		$smcFunc['db_insert']('',
1931
			'{db_prefix}approval_queue',
1932
			array(
1933
				'id_msg' => 'int',
1934
			),
1935
			array(
1936
				$msgOptions['id'],
1937
			),
1938
			array()
1939
		);
1940
1941
		$smcFunc['db_insert']('',
1942
			'{db_prefix}background_tasks',
1943
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1944
			array(
1945
				'$sourcedir/tasks/ApprovePost-Notify.php', 'ApprovePost_Notify_Background', $smcFunc['json_encode'](array(
1946
					'msgOptions' => $msgOptions,
1947
					'topicOptions' => $topicOptions,
1948
					'posterOptions' => $posterOptions,
1949
					'type' => $new_topic ? 'topic' : 'post',
1950
				)), 0
1951
			),
1952
			array('id_task')
1953
		);
1954
	}
1955
1956
	// Mark inserted topic as read (only for the user calling this function).
1957 View Code Duplication
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
1958
	{
1959
		// Since it's likely they *read* it before replying, let's try an UPDATE first.
1960
		if (!$new_topic)
1961
		{
1962
			$smcFunc['db_query']('', '
1963
				UPDATE {db_prefix}log_topics
1964
				SET id_msg = {int:id_msg}
1965
				WHERE id_member = {int:current_member}
1966
					AND id_topic = {int:id_topic}',
1967
				array(
1968
					'current_member' => $posterOptions['id'],
1969
					'id_msg' => $msgOptions['id'],
1970
					'id_topic' => $topicOptions['id'],
1971
				)
1972
			);
1973
1974
			$flag = $smcFunc['db_affected_rows']() != 0;
1975
		}
1976
1977
		if (empty($flag))
1978
		{
1979
			$smcFunc['db_insert']('ignore',
1980
				'{db_prefix}log_topics',
1981
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
1982
				array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
1983
				array('id_topic', 'id_member')
1984
			);
1985
		}
1986
	}
1987
1988
	if ($msgOptions['approved'] && empty($topicOptions['is_approved']))
1989
		$smcFunc['db_insert']('',
1990
			'{db_prefix}background_tasks',
1991
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
1992
			array(
1993
				'$sourcedir/tasks/ApproveReply-Notify.php', 'ApproveReply_Notify_Background', $smcFunc['json_encode'](array(
1994
					'msgOptions' => $msgOptions,
1995
					'topicOptions' => $topicOptions,
1996
					'posterOptions' => $posterOptions,
1997
				)), 0
1998
			),
1999
			array('id_task')
2000
		);
2001
2002
	// If there's a custom search index, it may need updating...
2003
	require_once($sourcedir . '/Search.php');
2004
	$searchAPI = findSearchAPI();
2005
	if (is_callable(array($searchAPI, 'postCreated')))
2006
		$searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
2007
2008
	// Increase the post counter for the user that created the post.
2009
	if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
2010
	{
2011
		// Are you the one that happened to create this post?
2012
		if ($user_info['id'] == $posterOptions['id'])
2013
			$user_info['posts']++;
2014
		updateMemberData($posterOptions['id'], array('posts' => '+'));
2015
	}
2016
2017
	// They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2018
	$_SESSION['last_read_topic'] = 0;
2019
2020
	// Better safe than sorry.
2021
	if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2022
		$_SESSION['topicseen_cache'][$topicOptions['board']]--;
2023
2024
	// Update all the stats so everyone knows about this new topic and message.
2025
	updateStats('message', true, $msgOptions['id']);
2026
2027
	// Update the last message on the board assuming it's approved AND the topic is.
2028
	if ($msgOptions['approved'])
2029
		updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2030
2031
	// Queue createPost background notification
2032 View Code Duplication
	if ($msgOptions['send_notifications'] && $msgOptions['approved'])
2033
		$smcFunc['db_insert']('',
2034
			'{db_prefix}background_tasks',
2035
			array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2036
			array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2037
				'msgOptions' => $msgOptions,
2038
				'topicOptions' => $topicOptions,
2039
				'posterOptions' => $posterOptions,
2040
				'type' => $new_topic ? 'topic' : 'reply',
2041
			)), 0),
2042
			array('id_task')
2043
		);
2044
2045
	// Alright, done now... we can abort now, I guess... at least this much is done.
2046
	ignore_user_abort($previous_ignore_user_abort);
2047
2048
	// Success.
2049
	return true;
2050
}
2051
2052
/**
2053
 * Modifying a post...
2054
 *
2055
 * @param array &$msgOptions An array of information/options for the post
2056
 * @param array &$topicOptions An array of information/options for the topic
2057
 * @param array &$posterOptions An array of information/options for the poster
2058
 * @return bool Whether the post was modified successfully
2059
 */
2060
function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions)
2061
{
2062
	global $user_info, $modSettings, $smcFunc, $sourcedir;
2063
2064
	$topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
2065
	$topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
2066
	$topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
2067
2068
	// This is longer than it has to be, but makes it so we only set/change what we have to.
2069
	$messages_columns = array();
2070
	if (isset($posterOptions['name']))
2071
		$messages_columns['poster_name'] = $posterOptions['name'];
2072
	if (isset($posterOptions['email']))
2073
		$messages_columns['poster_email'] = $posterOptions['email'];
2074
	if (isset($msgOptions['icon']))
2075
		$messages_columns['icon'] = $msgOptions['icon'];
2076
	if (isset($msgOptions['subject']))
2077
		$messages_columns['subject'] = $msgOptions['subject'];
2078 View Code Duplication
	if (isset($msgOptions['body']))
2079
	{
2080
		$messages_columns['body'] = $msgOptions['body'];
2081
2082
		// using a custom search index, then lets get the old message so we can update our index as needed
2083
		if (!empty($modSettings['search_custom_index_config']))
2084
		{
2085
			$request = $smcFunc['db_query']('', '
2086
				SELECT body
2087
				FROM {db_prefix}messages
2088
				WHERE id_msg = {int:id_msg}',
2089
				array(
2090
					'id_msg' => $msgOptions['id'],
2091
				)
2092
			);
2093
			list ($msgOptions['old_body']) = $smcFunc['db_fetch_row']($request);
2094
			$smcFunc['db_free_result']($request);
2095
		}
2096
	}
2097 View Code Duplication
	if (!empty($msgOptions['modify_time']))
2098
	{
2099
		$messages_columns['modified_time'] = $msgOptions['modify_time'];
2100
		$messages_columns['modified_name'] = $msgOptions['modify_name'];
2101
		$messages_columns['modified_reason'] = $msgOptions['modify_reason'];
2102
		$messages_columns['id_msg_modified'] = $modSettings['maxMsgID'];
2103
	}
2104
	if (isset($msgOptions['smileys_enabled']))
2105
		$messages_columns['smileys_enabled'] = empty($msgOptions['smileys_enabled']) ? 0 : 1;
2106
2107
	// Which columns need to be ints?
2108
	$messageInts = array('modified_time', 'id_msg_modified', 'smileys_enabled');
2109
	$update_parameters = array(
2110
		'id_msg' => $msgOptions['id'],
2111
	);
2112
2113
	if (!empty($modSettings['enable_mentions']) && isset($msgOptions['body']))
2114
	{
2115
		require_once($sourcedir . '/Mentions.php');
2116
2117
		$oldmentions = array();
2118
2119
		if (!empty($msgOptions['old_body']))
2120
		{
2121
			preg_match_all('/\[member\=([0-9]+)\]([^\[]*)\[\/member\]/U', $msgOptions['old_body'], $match);
2122
2123
			if (isset($match[1]) && isset($match[2]) && is_array($match[1]) && is_array($match[2]))
2124
				foreach ($match[1] as $i => $oldID)
2125
					$oldmentions[$oldID] = array('id' => $oldID, 'real_name' => $match[2][$i]);
2126
2127
			if (empty($modSettings['search_custom_index_config']))
2128
				unset($msgOptions['old_body']);
2129
		}
2130
2131
		$mentions = Mentions::getMentionedMembers($msgOptions['body']);
2132
		$messages_columns['body'] = $msgOptions['body'] = Mentions::getBody($msgOptions['body'], $mentions);
2133
2134
		// Remove the poster.
2135
		if (isset($mentions[$user_info['id']]))
2136
			unset($mentions[$user_info['id']]);
2137
2138
		if (isset($oldmentions[$user_info['id']]))
2139
			unset($oldmentions[$user_info['id']]);
2140
2141
		if (is_array($mentions) && is_array($oldmentions) && count(array_diff_key($mentions, $oldmentions)) > 0 && count($mentions) > count($oldmentions))
2142
		{
2143
			// Queue this for notification.
2144
			$msgOptions['mentioned_members'] = array_diff_key($mentions, $oldmentions);
2145
2146
			$smcFunc['db_insert']('',
2147
				'{db_prefix}background_tasks',
2148
				array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2149
				array('$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2150
					'msgOptions' => $msgOptions,
2151
					'topicOptions' => $topicOptions,
2152
					'posterOptions' => $posterOptions,
2153
					'type' => 'edit',
2154
				)), 0),
2155
				array('id_task')
2156
			);
2157
		}
2158
	}
2159
2160
	call_integration_hook('integrate_modify_post', array(&$messages_columns, &$update_parameters, &$msgOptions, &$topicOptions, &$posterOptions, &$messageInts));
2161
2162
	foreach ($messages_columns as $var => $val)
2163
	{
2164
		$messages_columns[$var] = $var . ' = {' . (in_array($var, $messageInts) ? 'int' : 'string') . ':var_' . $var . '}';
2165
		$update_parameters['var_' . $var] = $val;
2166
	}
2167
2168
	// Nothing to do?
2169
	if (empty($messages_columns))
2170
		return true;
2171
2172
	// Change the post.
2173
	$smcFunc['db_query']('', '
2174
		UPDATE {db_prefix}messages
2175
		SET ' . implode(', ', $messages_columns) . '
2176
		WHERE id_msg = {int:id_msg}',
2177
		$update_parameters
2178
	);
2179
2180
	// Lock and or sticky the post.
2181
	if ($topicOptions['sticky_mode'] !== null || $topicOptions['lock_mode'] !== null || $topicOptions['poll'] !== null)
2182
	{
2183
		$smcFunc['db_query']('', '
2184
			UPDATE {db_prefix}topics
2185
			SET
2186
				is_sticky = {raw:is_sticky},
2187
				locked = {raw:locked},
2188
				id_poll = {raw:id_poll}
2189
			WHERE id_topic = {int:id_topic}',
2190
			array(
2191
				'is_sticky' => $topicOptions['sticky_mode'] === null ? 'is_sticky' : (int) $topicOptions['sticky_mode'],
2192
				'locked' => $topicOptions['lock_mode'] === null ? 'locked' : (int) $topicOptions['lock_mode'],
2193
				'id_poll' => $topicOptions['poll'] === null ? 'id_poll' : (int) $topicOptions['poll'],
2194
				'id_topic' => $topicOptions['id'],
2195
			)
2196
		);
2197
	}
2198
2199
	// Mark the edited post as read.
2200 View Code Duplication
	if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2201
	{
2202
		// Since it's likely they *read* it before editing, let's try an UPDATE first.
2203
		$smcFunc['db_query']('', '
2204
			UPDATE {db_prefix}log_topics
2205
			SET id_msg = {int:id_msg}
2206
			WHERE id_member = {int:current_member}
2207
				AND id_topic = {int:id_topic}',
2208
			array(
2209
				'current_member' => $user_info['id'],
2210
				'id_msg' => $modSettings['maxMsgID'],
2211
				'id_topic' => $topicOptions['id'],
2212
			)
2213
		);
2214
2215
		$flag = $smcFunc['db_affected_rows']() != 0;
2216
2217
		if (empty($flag))
2218
		{
2219
			$smcFunc['db_insert']('ignore',
2220
				'{db_prefix}log_topics',
2221
				array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2222
				array($topicOptions['id'], $user_info['id'], $modSettings['maxMsgID']),
2223
				array('id_topic', 'id_member')
2224
			);
2225
		}
2226
	}
2227
2228
	// If there's a custom search index, it needs to be modified...
2229
	require_once($sourcedir . '/Search.php');
2230
	$searchAPI = findSearchAPI();
2231
	if (is_callable(array($searchAPI, 'postModified')))
2232
		$searchAPI->postModified($msgOptions, $topicOptions, $posterOptions);
2233
2234
	if (isset($msgOptions['subject']))
2235
	{
2236
		// Only update the subject if this was the first message in the topic.
2237
		$request = $smcFunc['db_query']('', '
2238
			SELECT id_topic
2239
			FROM {db_prefix}topics
2240
			WHERE id_first_msg = {int:id_first_msg}
2241
			LIMIT 1',
2242
			array(
2243
				'id_first_msg' => $msgOptions['id'],
2244
			)
2245
		);
2246
		if ($smcFunc['db_num_rows']($request) == 1)
2247
			updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
2248
		$smcFunc['db_free_result']($request);
2249
	}
2250
2251
	// Finally, if we are setting the approved state we need to do much more work :(
2252
	if ($modSettings['postmod_active'] && isset($msgOptions['approved']))
2253
		approvePosts($msgOptions['id'], $msgOptions['approved']);
2254
2255
	return true;
2256
}
2257
2258
/**
2259
 * Approve (or not) some posts... without permission checks...
2260
 *
2261
 * @param array $msgs Array of message ids
2262
 * @param bool $approve Whether to approve the posts (if false, posts are unapproved)
2263
 * @param bool $notify Whether to notify users
2264
 * @return bool Whether the operation was successful
0 ignored issues
show
Documentation introduced by
Should the return type not be null|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2265
 */
2266
function approvePosts($msgs, $approve = true, $notify = true)
2267
{
2268
	global $smcFunc;
2269
2270
	if (!is_array($msgs))
2271
		$msgs = array($msgs);
2272
2273
	if (empty($msgs))
2274
		return false;
2275
2276
	// May as well start at the beginning, working out *what* we need to change.
2277
	$request = $smcFunc['db_query']('', '
2278
		SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg,
2279
			m.body, m.subject, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.id_member,
2280
			t.approved AS topic_approved, b.count_posts
2281
		FROM {db_prefix}messages AS m
2282
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
2283
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
2284
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2285
		WHERE m.id_msg IN ({array_int:message_list})
2286
			AND m.approved = {int:approved_state}',
2287
		array(
2288
			'message_list' => $msgs,
2289
			'approved_state' => $approve ? 0 : 1,
2290
		)
2291
	);
2292
	$msgs = array();
2293
	$topics = array();
2294
	$topic_changes = array();
2295
	$board_changes = array();
2296
	$notification_topics = array();
2297
	$notification_posts = array();
2298
	$member_post_changes = array();
2299
	while ($row = $smcFunc['db_fetch_assoc']($request))
2300
	{
2301
		// Easy...
2302
		$msgs[] = $row['id_msg'];
2303
		$topics[] = $row['id_topic'];
2304
2305
		// Ensure our change array exists already.
2306
		if (!isset($topic_changes[$row['id_topic']]))
2307
			$topic_changes[$row['id_topic']] = array(
2308
				'id_last_msg' => $row['id_last_msg'],
2309
				'approved' => $row['topic_approved'],
2310
				'replies' => 0,
2311
				'unapproved_posts' => 0,
2312
			);
2313 View Code Duplication
		if (!isset($board_changes[$row['id_board']]))
2314
			$board_changes[$row['id_board']] = array(
2315
				'posts' => 0,
2316
				'topics' => 0,
2317
				'unapproved_posts' => 0,
2318
				'unapproved_topics' => 0,
2319
			);
2320
2321
		// If it's the first message then the topic state changes!
2322
		if ($row['id_msg'] == $row['id_first_msg'])
2323
		{
2324
			$topic_changes[$row['id_topic']]['approved'] = $approve ? 1 : 0;
2325
2326
			$board_changes[$row['id_board']]['unapproved_topics'] += $approve ? -1 : 1;
2327
			$board_changes[$row['id_board']]['topics'] += $approve ? 1 : -1;
2328
2329
			// Note we need to ensure we announce this topic!
2330
			$notification_topics[] = array(
2331
				'body' => $row['body'],
2332
				'subject' => $row['subject'],
2333
				'name' => $row['poster_name'],
2334
				'board' => $row['id_board'],
2335
				'topic' => $row['id_topic'],
2336
				'msg' => $row['id_first_msg'],
2337
				'poster' => $row['id_member'],
2338
				'new_topic' => true,
2339
			);
2340
		}
2341
		else
2342
		{
2343
			$topic_changes[$row['id_topic']]['replies'] += $approve ? 1 : -1;
2344
2345
			// This will be a post... but don't notify unless it's not followed by approved ones.
2346
			if ($row['id_msg'] > $row['id_last_msg'])
2347
				$notification_posts[$row['id_topic']] = array(
2348
					'id' => $row['id_msg'],
2349
					'body' => $row['body'],
2350
					'subject' => $row['subject'],
2351
					'name' => $row['poster_name'],
2352
					'topic' => $row['id_topic'],
2353
					'board' => $row['id_board'],
2354
					'poster' => $row['id_member'],
2355
					'new_topic' => false,
2356
					'msg' => $row['id_msg'],
2357
				);
2358
		}
2359
2360
		// If this is being approved and id_msg is higher than the current id_last_msg then it changes.
2361
		if ($approve && $row['id_msg'] > $topic_changes[$row['id_topic']]['id_last_msg'])
2362
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_msg'];
2363
		// If this is being unapproved, and it's equal to the id_last_msg we need to find a new one!
2364
		elseif (!$approve)
2365
			// Default to the first message and then we'll override in a bit ;)
2366
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_first_msg'];
2367
2368
		$topic_changes[$row['id_topic']]['unapproved_posts'] += $approve ? -1 : 1;
2369
		$board_changes[$row['id_board']]['unapproved_posts'] += $approve ? -1 : 1;
2370
		$board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1;
2371
2372
		// Post count for the user?
2373
		if ($row['id_member'] && empty($row['count_posts']))
2374
			$member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1;
2375
	}
2376
	$smcFunc['db_free_result']($request);
2377
2378
	if (empty($msgs))
2379
		return;
2380
2381
	// Now we have the differences make the changes, first the easy one.
2382
	$smcFunc['db_query']('', '
2383
		UPDATE {db_prefix}messages
2384
		SET approved = {int:approved_state}
2385
		WHERE id_msg IN ({array_int:message_list})',
2386
		array(
2387
			'message_list' => $msgs,
2388
			'approved_state' => $approve ? 1 : 0,
2389
		)
2390
	);
2391
2392
	// If we were unapproving find the last msg in the topics...
2393 View Code Duplication
	if (!$approve)
2394
	{
2395
		$request = $smcFunc['db_query']('', '
2396
			SELECT id_topic, MAX(id_msg) AS id_last_msg
2397
			FROM {db_prefix}messages
2398
			WHERE id_topic IN ({array_int:topic_list})
2399
				AND approved = {int:approved}
2400
			GROUP BY id_topic',
2401
			array(
2402
				'topic_list' => $topics,
2403
				'approved' => 1,
2404
			)
2405
		);
2406
		while ($row = $smcFunc['db_fetch_assoc']($request))
2407
			$topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_last_msg'];
2408
		$smcFunc['db_free_result']($request);
2409
	}
2410
2411
	// ... next the topics...
2412
	foreach ($topic_changes as $id => $changes)
2413
		$smcFunc['db_query']('', '
2414
			UPDATE {db_prefix}topics
2415
			SET approved = {int:approved}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2416
				num_replies = num_replies + {int:num_replies}, id_last_msg = {int:id_last_msg}
2417
			WHERE id_topic = {int:id_topic}',
2418
			array(
2419
				'approved' => $changes['approved'],
2420
				'unapproved_posts' => $changes['unapproved_posts'],
2421
				'num_replies' => $changes['replies'],
2422
				'id_last_msg' => $changes['id_last_msg'],
2423
				'id_topic' => $id,
2424
			)
2425
		);
2426
2427
	// ... finally the boards...
2428 View Code Duplication
	foreach ($board_changes as $id => $changes)
2429
		$smcFunc['db_query']('', '
2430
			UPDATE {db_prefix}boards
2431
			SET num_posts = num_posts + {int:num_posts}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2432
				num_topics = num_topics + {int:num_topics}, unapproved_topics = unapproved_topics + {int:unapproved_topics}
2433
			WHERE id_board = {int:id_board}',
2434
			array(
2435
				'num_posts' => $changes['posts'],
2436
				'unapproved_posts' => $changes['unapproved_posts'],
2437
				'num_topics' => $changes['topics'],
2438
				'unapproved_topics' => $changes['unapproved_topics'],
2439
				'id_board' => $id,
2440
			)
2441
		);
2442
2443
	// Finally, least importantly, notifications!
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2444
	if ($approve)
2445
	{
2446
		$task_rows = array();
2447
		foreach (array_merge($notification_topics, $notification_posts) as $topic)
2448
			$task_rows[] = array(
2449
				'$sourcedir/tasks/CreatePost-Notify.php', 'CreatePost_Notify_Background', $smcFunc['json_encode'](array(
2450
					'msgOptions' => array(
2451
						'id' => $topic['msg'],
2452
						'body' => $topic['body'],
2453
						'subject' => $topic['subject'],
2454
					),
2455
					'topicOptions' => array(
2456
						'id' => $topic['topic'],
2457
						'board' => $topic['board'],
2458
					),
2459
					'posterOptions' => array(
2460
						'id' => $topic['poster'],
2461
						'name' => $topic['name'],
2462
					),
2463
					'type' => $topic['new_topic'] ? 'topic' : 'reply',
2464
				)), 0
2465
			);
2466
2467 View Code Duplication
		if ($notify)
2468
			$smcFunc['db_insert']('',
2469
				'{db_prefix}background_tasks',
2470
				array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2471
				$task_rows,
2472
				array('id_task')
2473
			);
2474
2475
		$smcFunc['db_query']('', '
2476
			DELETE FROM {db_prefix}approval_queue
2477
			WHERE id_msg IN ({array_int:message_list})
2478
				AND id_attach = {int:id_attach}',
2479
			array(
2480
				'message_list' => $msgs,
2481
				'id_attach' => 0,
2482
			)
2483
		);
2484
	}
2485
	// If unapproving add to the approval queue!
2486
	else
2487
	{
2488
		$msgInserts = array();
2489
		foreach ($msgs as $msg)
2490
			$msgInserts[] = array($msg);
2491
2492
		$smcFunc['db_insert']('ignore',
2493
			'{db_prefix}approval_queue',
2494
			array('id_msg' => 'int'),
2495
			$msgInserts,
2496
			array('id_msg')
2497
		);
2498
	}
2499
2500
	// Update the last messages on the boards...
2501
	updateLastMessages(array_keys($board_changes));
2502
2503
	// Post count for the members?
2504
	if (!empty($member_post_changes))
2505
		foreach ($member_post_changes as $id_member => $count_change)
2506
			updateMemberData($id_member, array('posts' => 'posts ' . ($approve ? '+' : '-') . ' ' . $count_change));
2507
2508
	return true;
2509
}
2510
2511
/**
2512
 * Approve topics?
2513
 * @todo shouldn't this be in topic
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
2514
 *
2515
 * @param array $topics Array of topic ids
2516
 * @param bool $approve Whether to approve the topics. If false, unapproves them instead
2517
 * @return bool Whether the operation was successful
0 ignored issues
show
Documentation introduced by
Should the return type not be null|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2518
 */
2519
function approveTopics($topics, $approve = true)
2520
{
2521
	global $smcFunc;
2522
2523
	if (!is_array($topics))
2524
		$topics = array($topics);
2525
2526
	if (empty($topics))
2527
		return false;
2528
2529
	$approve_type = $approve ? 0 : 1;
2530
2531
	// Just get the messages to be approved and pass through...
2532
	$request = $smcFunc['db_query']('', '
2533
		SELECT id_msg
2534
		FROM {db_prefix}messages
2535
		WHERE id_topic IN ({array_int:topic_list})
2536
			AND approved = {int:approve_type}',
2537
		array(
2538
			'topic_list' => $topics,
2539
			'approve_type' => $approve_type,
2540
		)
2541
	);
2542
	$msgs = array();
2543
	while ($row = $smcFunc['db_fetch_assoc']($request))
2544
		$msgs[] = $row['id_msg'];
2545
	$smcFunc['db_free_result']($request);
2546
2547
	return approvePosts($msgs, $approve);
2548
}
2549
2550
/**
2551
 * Takes an array of board IDs and updates their last messages.
2552
 * If the board has a parent, that parent board is also automatically
2553
 * updated.
2554
 * The columns updated are id_last_msg and last_updated.
2555
 * Note that id_last_msg should always be updated using this function,
2556
 * and is not automatically updated upon other changes.
2557
 *
2558
 * @param array $setboards An array of board IDs
2559
 * @param int $id_msg The ID of the message
2560
 * @return void|false Returns false if $setboards is empty for some reason
2561
 */
2562
function updateLastMessages($setboards, $id_msg = 0)
2563
{
2564
	global $board_info, $board, $smcFunc;
2565
2566
	// Please - let's be sane.
2567
	if (empty($setboards))
2568
		return false;
2569
2570
	if (!is_array($setboards))
2571
		$setboards = array($setboards);
2572
2573
	// If we don't know the id_msg we need to find it.
2574
	if (!$id_msg)
2575
	{
2576
		// Find the latest message on this board (highest id_msg.)
2577
		$request = $smcFunc['db_query']('', '
2578
			SELECT id_board, MAX(id_last_msg) AS id_msg
2579
			FROM {db_prefix}topics
2580
			WHERE id_board IN ({array_int:board_list})
2581
				AND approved = {int:approved}
2582
			GROUP BY id_board',
2583
			array(
2584
				'board_list' => $setboards,
2585
				'approved' => 1,
2586
			)
2587
		);
2588
		$lastMsg = array();
2589
		while ($row = $smcFunc['db_fetch_assoc']($request))
2590
			$lastMsg[$row['id_board']] = $row['id_msg'];
2591
		$smcFunc['db_free_result']($request);
2592
	}
2593
	else
2594
	{
2595
		// Just to note - there should only be one board passed if we are doing this.
2596
		foreach ($setboards as $id_board)
2597
			$lastMsg[$id_board] = $id_msg;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$lastMsg was never initialized. Although not strictly required by PHP, it is generally a good practice to add $lastMsg = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
2598
	}
2599
2600
	$parent_boards = array();
2601
	// Keep track of last modified dates.
2602
	$lastModified = $lastMsg;
0 ignored issues
show
Bug introduced by
The variable $lastMsg does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2603
	// Get all the child boards for the parents, if they have some...
2604
	foreach ($setboards as $id_board)
2605
	{
2606
		if (!isset($lastMsg[$id_board]))
2607
		{
2608
			$lastMsg[$id_board] = 0;
2609
			$lastModified[$id_board] = 0;
2610
		}
2611
2612
		if (!empty($board) && $id_board == $board)
2613
			$parents = $board_info['parent_boards'];
2614
		else
2615
			$parents = getBoardParents($id_board);
2616
2617
		// Ignore any parents on the top child level.
2618
		// @todo Why?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
2619
		foreach ($parents as $id => $parent)
2620
		{
2621
			if ($parent['level'] != 0)
2622
			{
2623
				// If we're already doing this one as a board, is this a higher last modified?
2624
				if (isset($lastModified[$id]) && $lastModified[$id_board] > $lastModified[$id])
2625
					$lastModified[$id] = $lastModified[$id_board];
2626
				elseif (!isset($lastModified[$id]) && (!isset($parent_boards[$id]) || $parent_boards[$id] < $lastModified[$id_board]))
2627
					$parent_boards[$id] = $lastModified[$id_board];
2628
			}
2629
		}
2630
	}
2631
2632
	// Note to help understand what is happening here. For parents we update the timestamp of the last message for determining
2633
	// whether there are child boards which have not been read. For the boards themselves we update both this and id_last_msg.
2634
2635
	$board_updates = array();
2636
	$parent_updates = array();
2637
	// Finally, to save on queries make the changes...
2638
	foreach ($parent_boards as $id => $msg)
2639
	{
2640
		if (!isset($parent_updates[$msg]))
2641
			$parent_updates[$msg] = array($id);
2642
		else
2643
			$parent_updates[$msg][] = $id;
2644
	}
2645
2646
	foreach ($lastMsg as $id => $msg)
2647
	{
2648
		if (!isset($board_updates[$msg . '-' . $lastModified[$id]]))
2649
			$board_updates[$msg . '-' . $lastModified[$id]] = array(
2650
				'id' => $msg,
2651
				'updated' => $lastModified[$id],
2652
				'boards' => array($id)
2653
			);
2654
2655
		else
2656
			$board_updates[$msg . '-' . $lastModified[$id]]['boards'][] = $id;
2657
	}
2658
2659
	// Now commit the changes!
2660
	foreach ($parent_updates as $id_msg => $boards)
2661
	{
2662
		$smcFunc['db_query']('', '
2663
			UPDATE {db_prefix}boards
2664
			SET id_msg_updated = {int:id_msg_updated}
2665
			WHERE id_board IN ({array_int:board_list})
2666
				AND id_msg_updated < {int:id_msg_updated}',
2667
			array(
2668
				'board_list' => $boards,
2669
				'id_msg_updated' => $id_msg,
2670
			)
2671
		);
2672
	}
2673
	foreach ($board_updates as $board_data)
2674
	{
2675
		$smcFunc['db_query']('', '
2676
			UPDATE {db_prefix}boards
2677
			SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
2678
			WHERE id_board IN ({array_int:board_list})',
2679
			array(
2680
				'board_list' => $board_data['boards'],
2681
				'id_last_msg' => $board_data['id'],
2682
				'id_msg_updated' => $board_data['updated'],
2683
			)
2684
		);
2685
	}
2686
}
2687
2688
/**
2689
 * This simple function gets a list of all administrators and sends them an email
2690
 *  to let them know a new member has joined.
2691
 * Called by registerMember() function in Subs-Members.php.
2692
 * Email is sent to all groups that have the moderate_forum permission.
2693
 * The language set by each member is being used (if available).
2694
 *
2695
 * @param string $type The type. Types supported are 'approval', 'activation', and 'standard'.
2696
 * @param int $memberID The ID of the member
2697
 * @param string $member_name The name of the member (if null, it is pulled from the database)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member_name not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2698
 * @uses the Login language file.
2699
 */
2700
function adminNotify($type, $memberID, $member_name = null)
2701
{
2702
	global $smcFunc;
2703
2704 View Code Duplication
	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...
2705
	{
2706
		// Get the new user's name....
2707
		$request = $smcFunc['db_query']('', '
2708
			SELECT real_name
2709
			FROM {db_prefix}members
2710
			WHERE id_member = {int:id_member}
2711
			LIMIT 1',
2712
			array(
2713
				'id_member' => $memberID,
2714
			)
2715
		);
2716
		list ($member_name) = $smcFunc['db_fetch_row']($request);
2717
		$smcFunc['db_free_result']($request);
2718
	}
2719
2720
	// This is really just a wrapper for making a new background task to deal with all the fun.
2721
	$smcFunc['db_insert']('insert',
2722
		'{db_prefix}background_tasks',
2723
		array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
2724
		array('$sourcedir/tasks/Register-Notify.php', 'Register_Notify_Background', $smcFunc['json_encode'](array(
2725
			'new_member_id' => $memberID,
2726
			'new_member_name' => $member_name,
2727
			'notify_type' => $type,
2728
			'time' => time(),
2729
		)), 0),
2730
		array('id_task')
2731
	);
2732
}
2733
2734
/**
2735
 * Load a template from EmailTemplates language file.
2736
 *
2737
 * @param string $template The name of the template to load
2738
 * @param array $replacements An array of replacements for the variables in the template
2739
 * @param string $lang The language to use, if different than the user's current language
2740
 * @param bool $loadLang Whether to load the language file first
2741
 * @return array An array containing the subject and body of the email template, with replacements made
2742
 */
2743
function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true)
2744
{
2745
	global $txt, $mbname, $scripturl, $settings;
2746
2747
	// First things first, load up the email templates language file, if we need to.
2748
	if ($loadLang)
2749
		loadLanguage('EmailTemplates', $lang);
2750
2751
	if (!isset($txt[$template . '_subject']) || !isset($txt[$template . '_body']))
2752
		fatal_lang_error('email_no_template', 'template', array($template));
2753
2754
	$ret = array(
2755
		'subject' => $txt[$template . '_subject'],
2756
		'body' => $txt[$template . '_body'],
2757
		'is_html' => !empty($txt[$template . '_html']),
2758
	);
2759
2760
	// Add in the default replacements.
2761
	$replacements += array(
2762
		'FORUMNAME' => $mbname,
2763
		'SCRIPTURL' => $scripturl,
2764
		'THEMEURL' => $settings['theme_url'],
2765
		'IMAGESURL' => $settings['images_url'],
2766
		'DEFAULT_THEMEURL' => $settings['default_theme_url'],
2767
		'REGARDS' => $txt['regards_team'],
2768
	);
2769
2770
	// Split the replacements up into two arrays, for use with str_replace
2771
	$find = array();
2772
	$replace = array();
2773
2774
	foreach ($replacements as $f => $r)
2775
	{
2776
		$find[] = '{' . $f . '}';
2777
		$replace[] = $r;
2778
	}
2779
2780
	// Do the variable replacements.
2781
	$ret['subject'] = str_replace($find, $replace, $ret['subject']);
2782
	$ret['body'] = str_replace($find, $replace, $ret['body']);
2783
2784
	// Now deal with the {USER.variable} items.
2785
	$ret['subject'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['subject']);
2786
	$ret['body'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['body']);
2787
2788
	// Finally return the email to the caller so they can send it out.
2789
	return $ret;
2790
}
2791
2792
/**
2793
 * Callback function for loademaitemplate on subject and body
2794
 * Uses capture group 1 in array
2795
 *
2796
 * @param array $matches An array of matches
2797
 * @return string The match
2798
 */
2799
function user_info_callback($matches)
2800
{
2801
	global $user_info;
2802
	if (empty($matches[1]))
2803
		return '';
2804
2805
	$use_ref = true;
2806
	$ref = &$user_info;
2807
2808
	foreach (explode('.', $matches[1]) as $index)
2809
	{
2810
		if ($use_ref && isset($ref[$index]))
2811
			$ref = &$ref[$index];
2812
		else
2813
		{
2814
			$use_ref = false;
2815
			break;
2816
		}
2817
	}
2818
2819
	return $use_ref ? $ref : $matches[0];
2820
}
2821
2822
2823
/**
2824
 * spell_init()
2825
 *
2826
 * Sets up a dictionary resource handle. Tries enchant first then falls through to pspell.
2827
 *
2828
 * @return resource|bool An enchant or pspell dictionary resource handle or false if the dictionary couldn't be loaded
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2829
 */
2830
function spell_init()
2831
{
2832
	global $context, $txt;
2833
2834
	// Check for UTF-8 and strip ".utf8" off the lang_locale string for enchant
2835
	$context['spell_utf8'] = ($txt['lang_character_set'] == 'UTF-8');
2836
	$lang_locale = str_replace('.utf8', '', $txt['lang_locale']);
2837
2838
	// Try enchant first since PSpell is (supposedly) deprecated as of PHP 5.3
2839
	// enchant only does UTF-8, so we need iconv if you aren't using UTF-8
2840
	if (function_exists('enchant_broker_init') && ($context['spell_utf8'] || function_exists('iconv')))
2841
	{
2842
		// We'll need this to free resources later...
2843
		$context['enchant_broker'] = enchant_broker_init();
2844
2845
		// Try locale first, then general...
2846
		if (!empty($lang_locale) && enchant_broker_dict_exists($context['enchant_broker'], $lang_locale))
2847
		{
2848
			$enchant_link = enchant_broker_request_dict($context['enchant_broker'], $lang_locale);
2849
		}
2850
		elseif (enchant_broker_dict_exists($context['enchant_broker'], $txt['lang_dictionary']))
2851
		{
2852
			$enchant_link = enchant_broker_request_dict($context['enchant_broker'], $txt['lang_dictionary']);
2853
		}
2854
2855
		// Success
2856
		if ($enchant_link)
2857
		{
2858
			$context['provider'] = 'enchant';
2859
			return $enchant_link;
0 ignored issues
show
Bug introduced by
The variable $enchant_link does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2860
		}
2861
		else
2862
		{
2863
			// Free up any resources used...
2864
			@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 here. This can introduce security issues, and is generally not recommended.

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...
2865
		}
2866
	}
2867
2868
	// Fall through to pspell if enchant didn't work
2869
	if (function_exists('pspell_new'))
2870
	{
2871
		// Okay, this looks funny, but it actually fixes a weird bug.
2872
		ob_start();
2873
		$old = error_reporting(0);
2874
2875
		// See, first, some windows machines don't load pspell properly on the first try.  Dumb, but this is a workaround.
2876
		pspell_new('en');
2877
2878
		// Next, the dictionary in question may not exist. So, we try it... but...
2879
		$pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER);
0 ignored issues
show
Documentation introduced by
$context['character_set'] is of type boolean|resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2880
2881
		// Most people don't have anything but English installed... So we use English as a last resort.
2882
		if (!$pspell_link)
2883
			$pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
2884
2885
		error_reporting($old);
2886
		ob_end_clean();
2887
2888
		// If we have pspell, exit now...
2889
		if ($pspell_link)
2890
		{
2891
			$context['provider'] = 'pspell';
2892
			return $pspell_link;
2893
		}
2894
	}
2895
2896
	// If we get this far, we're doomed
2897
	return false;
2898
}
2899
2900
/**
2901
 * spell_check()
2902
 *
2903
 * Determines whether or not the specified word is spelled correctly
2904
 *
2905
 * @param resource $dict An enchant or pspell dictionary resource set up by {@link spell_init()}
2906
 * @param string $word A word to check the spelling of
2907
 * @return bool Whether or not the specified word is spelled properly
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2908
 */
2909
function spell_check($dict, $word)
2910
{
2911
	global $context, $txt;
2912
2913
	// Enchant or pspell?
2914
	if ($context['provider'] == 'enchant')
2915
	{
2916
		// This is a bit tricky here...
2917
		if (!$context['spell_utf8'])
2918
		{
2919
			// Convert the word to UTF-8 with iconv
2920
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
2921
		}
2922
		return enchant_dict_check($dict, $word);
2923
	}
2924
	elseif ($context['provider'] == 'pspell')
2925
	{
2926
		return pspell_check($dict, $word);
2927
	}
2928
}
2929
2930
/**
2931
 * spell_suggest()
2932
 *
2933
 * Returns an array of suggested replacements for the specified word
2934
 *
2935
 * @param resource $dict An enchant or pspell dictioary resource
2936
 * @param string $word A misspelled word
2937
 * @return array An array of suggested replacements for the misspelled word
0 ignored issues
show
Documentation introduced by
Should the return type not be array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
2938
 */
2939
function spell_suggest($dict, $word)
2940
{
2941
	global $context, $txt;
2942
2943
	if ($context['provider'] == 'enchant')
2944
	{
2945
		// If we're not using UTF-8, we need iconv to handle some stuff...
2946
		if (!$context['spell_utf8'])
2947
		{
2948
			// Convert the word to UTF-8 before getting suggestions
2949
			$word = iconv($txt['lang_character_set'], 'UTF-8', $word);
2950
			$suggestions = enchant_dict_suggest($dict, $word);
2951
2952
			// Go through the suggestions and convert them back to the proper character set
2953
			foreach ($suggestions as $index => $suggestion)
2954
			{
2955
				// //TRANSLIT makes it use similar-looking characters for incompatible ones...
2956
				$suggestions[$index] = iconv('UTF-8', $txt['lang_character_set'] . '//TRANSLIT', $suggestion);
2957
			}
2958
2959
			return $suggestions;
2960
		}
2961
		else
2962
		{
2963
			return enchant_dict_suggest($dict, $word);
2964
		}
2965
	}
2966
	elseif ($context['provider'] == 'pspell')
2967
	{
2968
		return pspell_suggest($dict, $word);
2969
	}
2970
}
2971
2972
?>