Completed
Push — release-2.1 ( 121660...f19596 )
by Mathias
09:09
created

Sources/Mentions.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 2017 Simple Machines and individual contributors
10
 * @license http://www.simplemachines.org/about/smf/license.php BSD
11
 *
12
 * @version 2.1 Beta 4
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);
225
226
			for ($i = 1; $i <= $count; $i++)
227
				$names[] = $smcFunc['htmlspecialchars']($smcFunc['htmltrim'](implode('', array_slice($match, 0, $i))));
228
		}
229
230
		$names = array_unique($names);
231
232
		return $names;
233
	}
234
}
235
236
?>
0 ignored issues
show
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...