Passed
Push — release-2.1 ( 0e5bfc...908430 )
by Mathias
07:53 queued 12s
created

Mentions::getQuotedMembers()   C

Complexity

Conditions 13
Paths 73

Size

Total Lines 66
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 35
nc 73
nop 2
dl 0
loc 66
rs 6.6166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 2021 Simple Machines and individual contributors
10
 * @license https://www.simplemachines.org/about/smf/license.php BSD
11
 *
12
 * @version 2.1 RC4
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 set 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
	 * Updates list of mentioned members.
100
	 *
101
	 * Intended for use when a post is modified.
102
	 *
103
	 * @static
104
	 * @access public
105
	 * @param string $content_type The content type
106
	 * @param int $content_id The ID of the specified content
107
	 * @param array $members An array of members who have been mentioned
108
	 * @param int $id_member The ID of the member who mentioned them
109
	 * @return array An array of unchanged, removed, and added member IDs.
110
	 */
111
	public static function modifyMentions($content_type, $content_id, array $members, $id_member)
112
	{
113
		global $smcFunc;
114
115
		$existing_members = self::getMentionsByContent($content_type, $content_id);
116
117
		$members_to_remove = array_diff_key($existing_members, $members);
118
		$members_to_insert = array_diff_key($members, $existing_members);
119
		$members_unchanged = array_diff_key($existing_members, $members_to_remove, $members_to_insert);
120
121
		// Delete mentions from the table that have been deleted in the content.
122
		if (!empty($members_to_remove))
123
			$smcFunc['db_query']('', '
124
				DELETE FROM {db_prefix}mentions
125
				WHERE content_type = {string:type}
126
					AND content_id = {int:id}
127
					AND id_mentioned IN ({array_int:members})',
128
				array(
129
					'type' => $content_type,
130
					'id' => $content_id,
131
					'members' => array_keys($members_to_remove),
132
				)
133
			);
134
135
		// Insert any new mentions.
136
		if (!empty($members_to_insert))
137
			self::insertMentions($content_type, $content_id, $members_to_insert, $id_member);
138
139
		return array(
140
			'unchanged' => $members_unchanged,
141
			'removed' => $members_to_remove,
142
			'added' => $members_to_insert,
143
		);
144
	}
145
146
	/**
147
	 * Gets appropriate mentions replaced in the body
148
	 *
149
	 * @static
150
	 * @access public
151
	 * @param string $body The text to look for mentions in
152
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member')
153
	 * @return string The body with mentions replaced
154
	 */
155
	public static function getBody($body, array $members)
156
	{
157
		if (empty($body))
158
			return $body;
159
160
		foreach ($members as $member)
161
			$body = str_ireplace(static::$char . $member['real_name'], '[member=' . $member['id'] . ']' . $member['real_name'] . '[/member]', $body);
162
163
		return $body;
164
	}
165
166
	/**
167
	 * Takes a piece of text and finds all the mentioned members in it
168
	 *
169
	 * @static
170
	 * @access public
171
	 * @param string $body The body to get mentions from
172
	 * @return array An array of arrays containing members who were mentioned (each has 'id_member' and 'real_name')
173
	 */
174
	public static function getMentionedMembers($body)
175
	{
176
		global $smcFunc;
177
178
		if (empty($body))
179
			return array();
180
181
		$possible_names = self::getPossibleMentions($body);
182
		$existing_mentions = self::getExistingMentions($body);
183
184
		if ((empty($possible_names) && empty($existing_mentions)) || !allowedTo('mention'))
185
			return array();
186
187
		// Make sure we don't pass empty arrays to the query.
188
		if (empty($existing_mentions))
189
			$existing_mentions = array(0 => '');
190
		if (empty($possible_names))
191
			$possible_names = $existing_mentions;
192
193
		$request = $smcFunc['db_query']('', '
194
			SELECT id_member, real_name
195
			FROM {db_prefix}members
196
			WHERE id_member IN ({array_int:ids})
197
				OR real_name IN ({array_string:names})
198
			ORDER BY LENGTH(real_name) DESC
199
			LIMIT {int:count}',
200
			array(
201
				'ids' => array_keys($existing_mentions),
202
				'names' => $possible_names,
203
				'count' => count($possible_names),
204
			)
205
		);
206
		$members = array();
207
		while ($row = $smcFunc['db_fetch_assoc']($request))
208
		{
209
			if (!isset($existing_mentions[$row['id_member']]) && stripos($body, static::$char . $row['real_name']) === false)
210
				continue;
211
212
			$members[$row['id_member']] = array(
213
				'id' => $row['id_member'],
214
				'real_name' => $row['real_name'],
215
			);
216
		}
217
		$smcFunc['db_free_result']($request);
218
219
		return $members;
220
	}
221
222
	/**
223
	 * Parses a body in order to see if there are any mentions, returns possible mention names
224
	 *
225
	 * Names are tagged by "@<username>" format in post, but they can contain
226
	 * any type of character up to 60 characters length. So we extract, starting from @
227
	 * up to 60 characters in length (or if we encounter a line break) and make
228
	 * several combination of strings after splitting it by anything that's not a word and join
229
	 * by having the first word, first and second word, first, second and third word and so on and
230
	 * search every name.
231
	 *
232
	 * One potential problem with this is something like "@Admin Space" can match
233
	 * "Admin Space" as well as "Admin", so we sort by length in descending order.
234
	 * One disadvantage of this is that we can only match by one column, hence I've chosen
235
	 * real_name since it's the most obvious.
236
	 *
237
	 * If there's an @ symbol within the name, it is counted in the ongoing string and a new
238
	 * combination string is started from it as well in order to account for all the possibilities.
239
	 * This makes the @ symbol to not be required to be escaped
240
	 *
241
	 * @static
242
	 * @access protected
243
	 * @param string $body The text to look for mentions in
244
	 * @return array An array of names of members who have been mentioned
245
	 */
246
	protected static function getPossibleMentions($body)
247
	{
248
		global $smcFunc;
249
250
		if (empty($body))
251
			return array();
252
253
		// preparse code does a few things which might mess with our parsing
254
		$body = htmlspecialchars_decode(preg_replace('~<br\s*/?'.'>~', "\n", str_replace('&nbsp;', ' ', $body)), ENT_QUOTES);
255
256
		// Remove quotes, we don't want to get double mentions.
257
		$body = preg_replace('~\[quote[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?quote[^\]]*\]))|(?0))*\[/quote\]~', '', $body);
258
259
		$matches = array();
260
		// Split before every Unicode character.
261
		$string = preg_split('/(?=\X)/u', $body, -1, PREG_SPLIT_NO_EMPTY);
262
		$depth = 0;
263
		foreach ($string as $k => $char)
264
		{
265
			if ($char == static::$char && ($k == 0 || trim($string[$k - 1]) == ''))
266
			{
267
				$depth++;
268
				$matches[] = array();
269
			}
270
			elseif ($char == "\n")
271
				$depth = 0;
272
273
			for ($i = $depth; $i > 0; $i--)
274
			{
275
				if (count($matches[count($matches) - $i]) > 60)
276
				{
277
					$depth--;
278
					continue;
279
				}
280
				$matches[count($matches) - $i][] = $char;
281
			}
282
		}
283
284
		foreach ($matches as $k => $match)
285
			$matches[$k] = substr(implode('', $match), 1);
286
287
		// Names can have spaces, other breaks, or they can't...we try to match every possible
288
		// combination.
289
		$names = array();
290
		foreach ($matches as $match)
291
		{
292
			// '[^\p{L}\p{M}\p{N}_]' is the Unicode equivalent of '[^\w]'
293
			$match = preg_split('/([^\p{L}\p{M}\p{N}_])/u', $match, -1, PREG_SPLIT_DELIM_CAPTURE);
294
			$count = count($match);
295
296
			for ($i = 1; $i <= $count; $i++)
297
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice($match, 0, $i))));
298
		}
299
300
		$names = array_unique($names);
301
302
		return $names;
303
	}
304
305
	/**
306
	 * Like getPossibleMentions(), but for `[member=1]name[/member]` format.
307
	 *
308
	 * @static
309
	 * @access public
310
	 * @param string $body The text to look for mentions in.
311
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member').
312
	 * @return array An array of arrays containing info about members that are in fact mentioned in the body.
313
	 */
314
	public static function getExistingMentions($body)
315
	{
316
		// Don't include mentions inside quotations.
317
		$body = preg_replace('~\[quote[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?quote[^\]]*\]))|(?0))*\[/quote\]~', '', $body);
318
319
		$existing_mentions = array();
320
321
		preg_match_all('~\[member=([0-9]+)\]([^\[]*)\[/member\]~', $body, $matches, PREG_SET_ORDER);
322
323
		foreach ($matches as $match_set)
324
			$existing_mentions[$match_set[1]] = trim($match_set[2]);
325
326
		return $existing_mentions;
327
	}
328
329
	/**
330
	 * Verifies that members really are mentioned in the text.
331
	 *
332
	 * This function assumes the incoming text has already been processed by
333
	 * the Mentions::getBody() function.
334
	 *
335
	 * @static
336
	 * @access public
337
	 * @param string $body The text to look for mentions in.
338
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member').
339
	 * @return array An array of arrays containing info about members that are in fact mentioned in the body.
340
	 */
341
	public static function verifyMentionedMembers($body, array $members)
342
	{
343
		if (empty($body))
344
			return array();
345
346
		// Don't include mentions inside quotations.
347
		$body = preg_replace('~\[quote[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?quote[^\]]*\]))|(?0))*\[/quote\]~', '', $body);
348
349
		foreach ($members as $member)
350
		{
351
			if (strpos($body, '[member=' . $member['id'] . ']' . $member['real_name'] . '[/member]') === false)
352
				unset($members[$member['id']]);
353
		}
354
355
		return $members;
356
	}
357
358
	/**
359
	 * Retrieves info about the authors of posts quoted in a block of text.
360
	 *
361
	 * @static
362
	 * @access public
363
	 * @param string $body A block of text, such as the body of a post.
364
	 * @param int $poster_id The member ID of the author of the text.
365
	 * @return array Info about any members who were quoted.
366
	 */
367
	public static function getQuotedMembers($body, $poster_id)
368
	{
369
		global $smcFunc;
370
371
		if (empty($body))
372
			return array();
373
374
		$blocks = preg_split('/(\[quote.*?\]|\[\/quote\])/i', $body, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
375
376
		$quote_level = 0;
377
		$message = '';
378
379
		foreach ($blocks as $block)
380
		{
381
			if (preg_match('/\[quote(.*)?\]/i', $block, $matches))
382
			{
383
				if ($quote_level == 0)
384
					$message .= '[quote' . $matches[1] . ']';
385
				$quote_level++;
386
			}
387
			elseif (preg_match('/\[\/quote\]/i', $block))
388
			{
389
				if ($quote_level <= 1)
390
					$message .= '[/quote]';
391
				if ($quote_level >= 1)
392
				{
393
					$quote_level--;
394
					$message .= "\n";
395
				}
396
			}
397
			elseif ($quote_level <= 1)
398
				$message .= $block;
399
		}
400
401
		preg_match_all('/\[quote.*?link=msg=([0-9]+).*?\]/i', $message, $matches);
402
403
		$id_msgs = $matches[1];
404
		foreach ($id_msgs as $k => $id_msg)
405
			$id_msgs[$k] = (int) $id_msg;
406
407
		if (empty($id_msgs))
408
			return array();
409
410
		// Get the messages
411
		$request = $smcFunc['db_query']('', '
412
			SELECT m.id_member AS id, mem.email_address, mem.lngfile, mem.real_name
413
			FROM {db_prefix}messages AS m
414
				INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
415
			WHERE id_msg IN ({array_int:msgs})
416
			LIMIT {int:count}',
417
			array(
418
				'msgs' => array_unique($id_msgs),
419
				'count' => count(array_unique($id_msgs)),
420
			)
421
		);
422
423
		$members = array();
424
		while ($row = $smcFunc['db_fetch_assoc']($request))
425
		{
426
			if ($poster_id == $row['id'])
427
				continue;
428
429
			$members[$row['id']] = $row;
430
		}
431
432
		return $members;
433
	}
434
435
}
436
437
?>