Passed
Pull Request — release-2.1 (#7039)
by Jon
04:23
created

Mentions   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 161
c 3
b 0
f 0
dl 0
loc 428
rs 5.5199
wmc 56

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getMentionsByContent() 0 35 3
A insertMentions() 0 12 2
B getMentionedMembers() 0 46 10
A getBody() 0 9 3
A modifyMentions() 0 32 3
C getPossibleMentions() 0 71 16
A getExistingMentions() 0 13 2
C getQuotedMembers() 0 66 13
A verifyMentionedMembers() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like Mentions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mentions, and based on these observations, apply Extract Interface, too.

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
		// Exclude the content of various BBCodes.
257
		if (empty($excluded_bbc_regex))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $excluded_bbc_regex does not exist. Did you maybe mean $excluded_bbc?
Loading history...
258
		{
259
			// Remove quotes. We don't want to get double mentions.
260
			$excluded_bbc = array('quote');
261
262
			// Remove everything with unparsed content.
263
			foreach (parse_bbc(false) as $code)
0 ignored issues
show
Bug introduced by
The expression parse_bbc(false) of type string is not traversable.
Loading history...
264
			{
265
				if (!empty($code['type']) && in_array($code['type'], array('unparsed_content', 'unparsed_commas_content', 'unparsed_equals_content')))
266
					$excluded_bbc[] = $code['tag'];
267
			}
268
269
			$excluded_bbc_regex = build_regex($excluded_bbc, '~');
270
		}
271
		$body = preg_replace('~\[(' . $excluded_bbc_regex . ')[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?\1[^\]]*\]))|(?0))*\[/\1\]~', '', $body);
272
273
		$matches = array();
274
		// Split before every Unicode character.
275
		$string = preg_split('/(?=\X)/u', $body, -1, PREG_SPLIT_NO_EMPTY);
276
		$depth = 0;
277
		foreach ($string as $k => $char)
278
		{
279
			if ($char == static::$char && ($k == 0 || trim($string[$k - 1]) == ''))
280
			{
281
				$depth++;
282
				$matches[] = array();
283
			}
284
			elseif ($char == "\n")
285
				$depth = 0;
286
287
			for ($i = $depth; $i > 0; $i--)
288
			{
289
				if (count($matches[count($matches) - $i]) > 60)
290
				{
291
					$depth--;
292
					continue;
293
				}
294
				$matches[count($matches) - $i][] = $char;
295
			}
296
		}
297
298
		foreach ($matches as $k => $match)
299
			$matches[$k] = substr(implode('', $match), 1);
300
301
		// Names can have spaces, other breaks, or they can't...we try to match every possible
302
		// combination.
303
		$names = array();
304
		foreach ($matches as $match)
305
		{
306
			// '[^\p{L}\p{M}\p{N}_]' is the Unicode equivalent of '[^\w]'
307
			$match = preg_split('/([^\p{L}\p{M}\p{N}_])/u', $match, -1, PREG_SPLIT_DELIM_CAPTURE);
308
			$count = count($match);
309
310
			for ($i = 1; $i <= $count; $i++)
311
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice($match, 0, $i))));
312
		}
313
314
		$names = array_unique($names);
315
316
		return $names;
317
	}
318
319
	/**
320
	 * Like getPossibleMentions(), but for `[member=1]name[/member]` format.
321
	 *
322
	 * @static
323
	 * @access public
324
	 * @param string $body The text to look for mentions in.
325
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member').
326
	 * @return array An array of arrays containing info about members that are in fact mentioned in the body.
327
	 */
328
	public static function getExistingMentions($body)
329
	{
330
		// Don't include mentions inside quotations.
331
		$body = preg_replace('~\[quote[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?quote[^\]]*\]))|(?0))*\[/quote\]~', '', $body);
332
333
		$existing_mentions = array();
334
335
		preg_match_all('~\[member=([0-9]+)\]([^\[]*)\[/member\]~', $body, $matches, PREG_SET_ORDER);
336
337
		foreach ($matches as $match_set)
338
			$existing_mentions[$match_set[1]] = trim($match_set[2]);
339
340
		return $existing_mentions;
341
	}
342
343
	/**
344
	 * Verifies that members really are mentioned in the text.
345
	 *
346
	 * This function assumes the incoming text has already been processed by
347
	 * the Mentions::getBody() function.
348
	 *
349
	 * @static
350
	 * @access public
351
	 * @param string $body The text to look for mentions in.
352
	 * @param array $members An array of arrays containing info about members (each should have 'id' and 'member').
353
	 * @return array An array of arrays containing info about members that are in fact mentioned in the body.
354
	 */
355
	public static function verifyMentionedMembers($body, array $members)
356
	{
357
		if (empty($body))
358
			return array();
359
360
		// Don't include mentions inside quotations.
361
		$body = preg_replace('~\[quote[^\]]*\](?' . '>(?' . '>[^\[]|\[(?!/?quote[^\]]*\]))|(?0))*\[/quote\]~', '', $body);
362
363
		foreach ($members as $member)
364
		{
365
			if (strpos($body, '[member=' . $member['id'] . ']' . $member['real_name'] . '[/member]') === false)
366
				unset($members[$member['id']]);
367
		}
368
369
		return $members;
370
	}
371
372
	/**
373
	 * Retrieves info about the authors of posts quoted in a block of text.
374
	 *
375
	 * @static
376
	 * @access public
377
	 * @param string $body A block of text, such as the body of a post.
378
	 * @param int $poster_id The member ID of the author of the text.
379
	 * @return array Info about any members who were quoted.
380
	 */
381
	public static function getQuotedMembers($body, $poster_id)
382
	{
383
		global $smcFunc;
384
385
		if (empty($body))
386
			return array();
387
388
		$blocks = preg_split('/(\[quote.*?\]|\[\/quote\])/i', $body, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
389
390
		$quote_level = 0;
391
		$message = '';
392
393
		foreach ($blocks as $block)
394
		{
395
			if (preg_match('/\[quote(.*)?\]/i', $block, $matches))
396
			{
397
				if ($quote_level == 0)
398
					$message .= '[quote' . $matches[1] . ']';
399
				$quote_level++;
400
			}
401
			elseif (preg_match('/\[\/quote\]/i', $block))
402
			{
403
				if ($quote_level <= 1)
404
					$message .= '[/quote]';
405
				if ($quote_level >= 1)
406
				{
407
					$quote_level--;
408
					$message .= "\n";
409
				}
410
			}
411
			elseif ($quote_level <= 1)
412
				$message .= $block;
413
		}
414
415
		preg_match_all('/\[quote.*?link=msg=([0-9]+).*?\]/i', $message, $matches);
416
417
		$id_msgs = $matches[1];
418
		foreach ($id_msgs as $k => $id_msg)
419
			$id_msgs[$k] = (int) $id_msg;
420
421
		if (empty($id_msgs))
422
			return array();
423
424
		// Get the messages
425
		$request = $smcFunc['db_query']('', '
426
			SELECT m.id_member AS id, mem.email_address, mem.lngfile, mem.real_name
427
			FROM {db_prefix}messages AS m
428
				INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
429
			WHERE id_msg IN ({array_int:msgs})
430
			LIMIT {int:count}',
431
			array(
432
				'msgs' => array_unique($id_msgs),
433
				'count' => count(array_unique($id_msgs)),
434
			)
435
		);
436
437
		$members = array();
438
		while ($row = $smcFunc['db_fetch_assoc']($request))
439
		{
440
			if ($poster_id == $row['id'])
441
				continue;
442
443
			$members[$row['id']] = $row;
444
		}
445
446
		return $members;
447
	}
448
449
}
450
451
?>