Issues (1061)

Sources/Mentions.php (2 issues)

1
<?php
2
/**
3
 * This file contains core of the code for Mentions
4
 *
5
 * Simple Machines Forum (SMF)
6
 *
7
 * @package SMF
8
 * @author Simple Machines https://www.simplemachines.org
9
 * @copyright 2020 Simple Machines and individual contributors
10
 * @license https://www.simplemachines.org/about/smf/license.php BSD
11
 *
12
 * @version 2.1 RC2
13
 */
14
15
/**
16
 * This really is a pseudo class, I couldn't justify having instance of it
17
 * while mentioning so I just made every method static
18
 */
19
class Mentions
20
{
21
	/**
22
	 * @var string The character used for mentioning users
23
	 */
24
	protected static $char = '@';
25
26
	/**
27
	 * Returns mentions for a specific content
28
	 *
29
	 * @static
30
	 * @access public
31
	 * @param string $content_type The content type
32
	 * @param int $content_id The ID of the desired content
33
	 * @param array $members Whether to limit to a specific sect of members
34
	 * @return array An array of arrays containing info about each member mentioned
35
	 */
36
	public static function getMentionsByContent($content_type, $content_id, array $members = array())
37
	{
38
		global $smcFunc;
39
40
		$request = $smcFunc['db_query']('', '
41
			SELECT mem.id_member, mem.real_name, mem.email_address, mem.id_group, mem.id_post_group, mem.additional_groups,
42
				mem.lngfile, ment.id_member AS id_mentioned_by, ment.real_name AS mentioned_by_name
43
			FROM {db_prefix}mentions AS m
44
				INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_mentioned)
45
				INNER JOIN {db_prefix}members AS ment ON (ment.id_member = m.id_member)
46
			WHERE content_type = {string:type}
47
				AND content_id = {int:id}' . (!empty($members) ? '
48
				AND mem.id_member IN ({array_int:members})' : ''),
49
			array(
50
				'type' => $content_type,
51
				'id' => $content_id,
52
				'members' => (array) $members,
53
			)
54
		);
55
		$members = array();
56
		while ($row = $smcFunc['db_fetch_assoc']($request))
57
			$members[$row['id_member']] = array(
58
				'id' => $row['id_member'],
59
				'real_name' => $row['real_name'],
60
				'email_address' => $row['email_address'],
61
				'groups' => array_unique(array_merge(array($row['id_group'], $row['id_post_group']), explode(',', $row['additional_groups']))),
62
				'mentioned_by' => array(
63
					'id' => $row['id_mentioned_by'],
64
					'name' => $row['mentioned_by_name'],
65
				),
66
				'lngfile' => $row['lngfile'],
67
			);
68
		$smcFunc['db_free_result']($request);
69
70
		return $members;
71
	}
72
73
	/**
74
	 * Inserts mentioned members
75
	 *
76
	 * @static
77
	 * @access public
78
	 * @param string $content_type The content type
79
	 * @param int $content_id The ID of the specified content
80
	 * @param array $members An array of members who have been mentioned
81
	 * @param int $id_member The ID of the member who mentioned them
82
	 */
83
	public static function insertMentions($content_type, $content_id, array $members, $id_member)
84
	{
85
		global $smcFunc;
86
87
		call_integration_hook('mention_insert_' . $content_type, array($content_id, &$members));
88
89
		foreach ($members as $member)
90
			$smcFunc['db_insert']('ignore',
91
				'{db_prefix}mentions',
92
				array('content_id' => 'int', 'content_type' => 'string', 'id_member' => 'int', 'id_mentioned' => 'int', 'time' => 'int'),
93
				array((int) $content_id, $content_type, $id_member, $member['id'], time()),
94
				array('content_id', 'content_type', 'id_mentioned')
95
			);
96
	}
97
98
	/**
99
	 * Gets appropriate mentions replaced in the body
100
	 *
101
	 * @static
102
	 * @access public
103
	 * @param string $body The text to look for mentions in
104
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member')
105
	 * @return string The body with mentions replaced
106
	 */
107
	public static function getBody($body, array $members)
108
	{
109
		foreach ($members as $member)
110
			$body = str_ireplace(static::$char . $member['real_name'], '[member=' . $member['id'] . ']' . $member['real_name'] . '[/member]', $body);
111
112
		return $body;
113
	}
114
115
	/**
116
	 * Takes a piece of text and finds all the mentioned members in it
117
	 *
118
	 * @static
119
	 * @access public
120
	 * @param string $body The body to get mentions from
121
	 * @return array An array of arrays containing members who were mentioned (each has 'id_member' and 'real_name')
122
	 */
123
	public static function getMentionedMembers($body)
124
	{
125
		global $smcFunc;
126
127
		$possible_names = self::getPossibleMentions($body);
128
129
		if (empty($possible_names) || !allowedTo('mention'))
130
			return array();
131
132
		$request = $smcFunc['db_query']('', '
133
			SELECT id_member, real_name
134
			FROM {db_prefix}members
135
			WHERE real_name IN ({array_string:names})
136
			ORDER BY LENGTH(real_name) DESC
137
			LIMIT {int:count}',
138
			array(
139
				'names' => $possible_names,
140
				'count' => count($possible_names),
141
			)
142
		);
143
		$members = array();
144
		while ($row = $smcFunc['db_fetch_assoc']($request))
145
		{
146
			if (stripos($body, static::$char . $row['real_name']) === false)
147
				continue;
148
149
			$members[$row['id_member']] = array(
150
				'id' => $row['id_member'],
151
				'real_name' => $row['real_name'],
152
			);
153
		}
154
		$smcFunc['db_free_result']($request);
155
156
		return $members;
157
	}
158
159
	/**
160
	 * Parses a body in order to see if there are any mentions, returns possible mention names
161
	 *
162
	 * Names are tagged by "@<username>" format in post, but they can contain
163
	 * any type of character up to 60 characters length. So we extract, starting from @
164
	 * up to 60 characters in length (or if we encounter a line break) and make
165
	 * several combination of strings after splitting it by anything that's not a word and join
166
	 * by having the first word, first and second word, first, second and third word and so on and
167
	 * search every name.
168
	 *
169
	 * One potential problem with this is something like "@Admin Space" can match
170
	 * "Admin Space" as well as "Admin", so we sort by length in descending order.
171
	 * One disadvantage of this is that we can only match by one column, hence I've chosen
172
	 * real_name since it's the most obvious.
173
	 *
174
	 * If there's an @ symbol within the name, it is counted in the ongoing string and a new
175
	 * combination string is started from it as well in order to account for all the possibilities.
176
	 * This makes the @ symbol to not be required to be escaped
177
	 *
178
	 * @static
179
	 * @access protected
180
	 * @param string $body The text to look for mentions in
181
	 * @return array An array of names of members who have been mentioned
182
	 */
183
	protected static function getPossibleMentions($body)
184
	{
185
		global $smcFunc;
186
187
		// preparse code does a few things which might mess with our parsing
188
		$body = htmlspecialchars_decode(preg_replace('~<br\s*/?\>~', "\n", str_replace('&nbsp;', ' ', $body)), ENT_QUOTES);
189
190
		// Remove quotes, we don't want to get double mentions.
191
		while (preg_match('~\[quote[^\]]*\](.+?)\[\/quote\]~s', $body))
192
			$body = preg_replace('~\[quote[^\]]*\](.+?)\[\/quote\]~s', '', $body);
193
194
		$matches = array();
195
		$string = str_split($body);
196
		$depth = 0;
197
		foreach ($string as $k => $char)
198
		{
199
			if ($char == static::$char && ($k == 0 || trim($string[$k - 1]) == ''))
200
			{
201
				$depth++;
202
				$matches[] = array();
203
			}
204
			elseif ($char == "\n")
205
				$depth = 0;
206
207
			for ($i = $depth; $i > 0; $i--)
208
			{
209
				if (count($matches[count($matches) - $i]) > 60)
210
				{
211
					$depth--;
212
					continue;
213
				}
214
				$matches[count($matches) - $i][] = $char;
215
			}
216
		}
217
218
		foreach ($matches as $k => $match)
219
			$matches[$k] = substr(implode('', $match), 1);
220
221
		// Names can have spaces, other breaks, or they can't...we try to match every possible
222
		// combination.
223
		$names = array();
224
		foreach ($matches as $match)
225
		{
226
			$match = preg_split('/([^\w])/', $match, -1, PREG_SPLIT_DELIM_CAPTURE);
227
			$count = count($match);
0 ignored issues
show
It seems like $match can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

227
			$count = count(/** @scrutinizer ignore-type */ $match);
Loading history...
228
229
			for ($i = 1; $i <= $count; $i++)
230
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice($match, 0, $i))));
0 ignored issues
show
It seems like $match can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

230
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice(/** @scrutinizer ignore-type */ $match, 0, $i))));
Loading history...
231
		}
232
233
		$names = array_unique($names);
234
235
		return $names;
236
	}
237
}
238
239
?>