Mentions::getMentionsByContent()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

224
			$count = count(/** @scrutinizer ignore-type */ $match);
Loading history...
225
226
			for ($i = 1; $i <= $count; $i++)
227
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice($match, 0, $i))));
0 ignored issues
show
Bug introduced by
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

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