PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.6.4
Jetpack – WP Security, Backup, Speed, & Growth v12.6.4
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / vendor / nojimage / twitter-text-php / lib / Twitter / Text / Extractor.php
jetpack / vendor / nojimage / twitter-text-php / lib / Twitter / Text Last commit date
Autolink.php 5 years ago Configuration.php 5 years ago EmojiRegex.php 5 years ago Extractor.php 5 years ago HitHighlighter.php 5 years ago ParseResults.php 5 years ago Parser.php 5 years ago Regex.php 5 years ago StringUtils.php 5 years ago TldLists.php 5 years ago Validator.php 5 years ago
Extractor.php
573 lines
1 <?php
2
3 /**
4 * @author Mike Cochrane <mikec@mikenz.geek.nz>
5 * @author Nick Pope <nick@nickpope.me.uk>
6 * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
7 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
8 * @package Twitter.Text
9 */
10
11 namespace Twitter\Text;
12
13 /**
14 * Twitter Extractor Class
15 *
16 * Parses tweets and extracts URLs, usernames, username/list pairs and
17 * hashtags.
18 *
19 * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
20 * is based on code by {@link http://github.com/mzsanford Matt Sanford} and
21 * heavily modified by {@link http://github.com/ngnpope Nick Pope}.
22 *
23 * @author Mike Cochrane <mikec@mikenz.geek.nz>
24 * @author Nick Pope <nick@nickpope.me.uk>
25 * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
26 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
27 * @package Twitter.Text
28 */
29 class Extractor
30 {
31
32 /**
33 * The maximum url length that the Twitter backend supports.
34 */
35 const MAX_URL_LENGTH = 4096;
36
37 /**
38 * The backend adds http:// for normal links and https to *.twitter.com URLs (it also rewrites http to https for
39 * URLs matching *.twitter.com). We're better off adding https:// all the time.
40 * By making the assumption that URL_GROUP_PROTOCOL_LENGTH is https, the trade off is we'll disallow a http URL
41 * that is 4096 characters.
42 */
43 const URL_GROUP_PROTOCOL_LENGTH = 4104; // https:// + MAX_URL_LENGTH
44
45 /**
46 * The maximum t.co path length that the Twitter backend supports.
47 */
48 const MAX_TCO_SLUG_LENGTH = 40;
49
50 /**
51 * The maximum hostname length that the ASCII domain.
52 */
53 const MAX_ASCII_HOSTNAME_LENGTH = 63;
54
55 /**
56 * @var boolean
57 */
58 protected $extractURLWithoutProtocol = true;
59
60 /**
61 * Provides fluent method chaining.
62 *
63 * @see __construct()
64 *
65 * @return Extractor
66 */
67 public static function create()
68 {
69 return new self();
70 }
71
72 /**
73 * Reads in a tweet to be parsed and extracts elements from it.
74 *
75 * Extracts various parts of a tweet including URLs, usernames, hashtags...
76 */
77 public function __construct()
78 {
79 }
80
81 /**
82 * Extracts all parts of a tweet and returns an associative array containing
83 * the extracted elements.
84 *
85 * @param string $tweet The tweet to extract.
86 * @return array The elements in the tweet.
87 */
88 public function extract($tweet)
89 {
90 return array(
91 'hashtags' => $this->extractHashtags($tweet),
92 'cashtags' => $this->extractCashtags($tweet),
93 'urls' => $this->extractURLs($tweet),
94 'mentions' => $this->extractMentionedScreennames($tweet),
95 'replyto' => $this->extractReplyScreenname($tweet),
96 'hashtags_with_indices' => $this->extractHashtagsWithIndices($tweet),
97 'urls_with_indices' => $this->extractURLsWithIndices($tweet),
98 'mentions_with_indices' => $this->extractMentionedScreennamesWithIndices($tweet),
99 );
100 }
101
102 /**
103 * Extract URLs, @mentions, lists and #hashtag from a given text/tweet.
104 *
105 * @param string $tweet The tweet to extract.
106 * @return array list of extracted entities
107 */
108 public function extractEntitiesWithIndices($tweet)
109 {
110 $entities = array();
111 $entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
112 $entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
113 $entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
114 $entities = array_merge($entities, $this->extractCashtagsWithIndices($tweet));
115 $entities = $this->removeOverlappingEntities($entities);
116 return $entities;
117 }
118
119 /**
120 * Extracts all the hashtags from the tweet.
121 *
122 * @param string $tweet The tweet to extract.
123 * @return array The hashtag elements in the tweet.
124 */
125 public function extractHashtags($tweet)
126 {
127 $hashtagsOnly = array();
128 $hashtagsWithIndices = $this->extractHashtagsWithIndices($tweet);
129
130 foreach ($hashtagsWithIndices as $hashtagWithIndex) {
131 $hashtagsOnly[] = $hashtagWithIndex['hashtag'];
132 }
133 return $hashtagsOnly;
134 }
135
136 /**
137 * Extracts all the cashtags from the tweet.
138 *
139 * @param string $tweet The tweet to extract.
140 * @return array The cashtag elements in the tweet.
141 */
142 public function extractCashtags($tweet)
143 {
144 $cashtagsOnly = array();
145 $cashtagsWithIndices = $this->extractCashtagsWithIndices($tweet);
146
147 foreach ($cashtagsWithIndices as $cashtagWithIndex) {
148 $cashtagsOnly[] = $cashtagWithIndex['cashtag'];
149 }
150 return $cashtagsOnly;
151 }
152
153 /**
154 * Extracts all the URLs from the tweet.
155 *
156 * @param string $tweet The tweet to extract.
157 * @return array The URL elements in the tweet.
158 */
159 public function extractURLs($tweet)
160 {
161 $urlsOnly = array();
162 $urlsWithIndices = $this->extractURLsWithIndices($tweet);
163
164 foreach ($urlsWithIndices as $urlWithIndex) {
165 $urlsOnly[] = $urlWithIndex['url'];
166 }
167 return $urlsOnly;
168 }
169
170 /**
171 * Extract all the usernames from the tweet.
172 *
173 * A mention is an occurrence of a username anywhere in a tweet.
174 *
175 * @param string $tweet The tweet to extract.
176 * @return array The usernames elements in the tweet.
177 */
178 public function extractMentionedScreennames($tweet)
179 {
180 $usernamesOnly = array();
181 $mentionsWithIndices = $this->extractMentionsOrListsWithIndices($tweet);
182
183 foreach ($mentionsWithIndices as $mentionWithIndex) {
184 if (empty($mentionWithIndex['screen_name'])) {
185 continue;
186 }
187 $usernamesOnly[] = $mentionWithIndex['screen_name'];
188 }
189 return $usernamesOnly;
190 }
191
192 /**
193 * Extract all the usernames replied to from the tweet.
194 *
195 * A reply is an occurrence of a username at the beginning of a tweet.
196 *
197 * @param string $tweet The tweet to extract.
198 * @return array The usernames replied to in a tweet.
199 */
200 public function extractReplyScreenname($tweet)
201 {
202 $matched = preg_match(Regex::getValidReplyMatcher(), $tweet, $matches);
203 # Check username ending in
204 if ($matched && preg_match(Regex::getEndMentionMatcher(), $matches[2])) {
205 $matched = false;
206 }
207 return $matched ? $matches[1] : null;
208 }
209
210 /**
211 * Extracts all the emoji and the indices they occur at from the tweet.
212 *
213 * @param string $tweet The tweet to extract.
214 * @return array The emoji chars in the tweet.
215 */
216 public function extractEmojiWithIndices($tweet)
217 {
218 preg_match_all(EmojiRegex::VALID_EMOJI_PATTERN, $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
219 $entities = array();
220
221 foreach ($matches as $match) {
222 list($emoji) = $match;
223 list($emojiChar, $offset) = $emoji;
224 $startPosition = StringUtils::strlen(substr($tweet, 0, $offset));
225 $endPosition = $startPosition + StringUtils::strlen($emojiChar) - 1;
226
227 $entities[] = array(
228 'emoji' => $emoji[0],
229 'indices' => array($startPosition, $endPosition)
230 );
231 }
232
233 return $entities;
234 }
235
236 /**
237 * Extracts all the hashtags and the indices they occur at from the tweet.
238 *
239 * @param string $tweet The tweet to extract.
240 * @param boolean $checkUrlOverlap if true, check if extracted hashtags overlap URLs and remove overlapping ones
241 * @return array The hashtag elements in the tweet.
242 */
243 public function extractHashtagsWithIndices($tweet, $checkUrlOverlap = true)
244 {
245 if (!preg_match('/[##]/u', $tweet)) {
246 return array();
247 }
248
249 preg_match_all(Regex::getValidHashtagMatcher(), $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
250 $tags = array();
251
252 foreach ($matches as $match) {
253 list($all, $before, $hash, $hashtag, $outer) = array_pad($match, 3, array('', 0));
254 $start_position = $hash[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $hash[1])) : $hash[1];
255 $end_position = $start_position + StringUtils::strlen($hash[0] . $hashtag[0]);
256
257 if (preg_match(Regex::getEndHashtagMatcher(), $outer[0])) {
258 continue;
259 }
260
261 $tags[] = array(
262 'hashtag' => $hashtag[0],
263 'indices' => array($start_position, $end_position)
264 );
265 }
266
267 if (!$checkUrlOverlap) {
268 return $tags;
269 }
270
271 # check url overlap
272 $urls = $this->extractURLsWithIndices($tweet);
273 $entities = $this->removeOverlappingEntities(array_merge($tags, $urls));
274
275 $validTags = array();
276 foreach ($entities as $entity) {
277 if (empty($entity['hashtag'])) {
278 continue;
279 }
280 $validTags[] = $entity;
281 }
282
283 return $validTags;
284 }
285
286 /**
287 * Extracts all the cashtags and the indices they occur at from the tweet.
288 *
289 * @param string $tweet The tweet to extract.
290 * @return array The cashtag elements in the tweet.
291 */
292 public function extractCashtagsWithIndices($tweet)
293 {
294 if (!preg_match('/\$/u', $tweet)) {
295 return array();
296 }
297
298 preg_match_all(Regex::getValidCashtagMatcher(), $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
299 $tags = array();
300
301 foreach ($matches as $match) {
302 list($all, $before, $dollar, $cash_text, $outer) = array_pad($match, 3, array('', 0));
303 $start_position = $dollar[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $dollar[1])) : $dollar[1];
304 $end_position = $start_position + StringUtils::strlen($dollar[0] . $cash_text[0]);
305
306 if (preg_match(Regex::getEndHashtagMatcher(), $outer[0])) {
307 continue;
308 }
309
310 $tags[] = array(
311 'cashtag' => $cash_text[0],
312 'indices' => array($start_position, $end_position)
313 );
314 }
315
316 return $tags;
317 }
318
319 /**
320 * Extracts all the URLs and the indices they occur at from the tweet.
321 *
322 * @param string $tweet The tweet to extract.
323 * @return array The URLs elements in the tweet.
324 */
325 public function extractURLsWithIndices($tweet)
326 {
327 $needle = $this->extractURLWithoutProtocol() ? '.' : ':';
328 if (strpos($tweet, $needle) === false) {
329 return array();
330 }
331
332 $urls = array();
333 preg_match_all(Regex::getValidUrlMatcher(), $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
334
335 foreach ($matches as $match) {
336 list($all, $before, $url, $protocol, $domain, $port, $path, $query) = array_pad($match, 8, array(''));
337 $start_position = $url[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $url[1])) : $url[1];
338 $end_position = $start_position + StringUtils::strlen($url[0]);
339
340 $all = $all[0];
341 $before = $before[0];
342 $url = $url[0];
343 $protocol = $protocol[0];
344 $domain = $domain[0];
345 $port = $port[0];
346 $path = $path[0];
347 $query = $query[0];
348
349 // If protocol is missing and domain contains non-ASCII characters,
350 // extract ASCII-only domains.
351 if (empty($protocol)) {
352 if (
353 !$this->extractURLWithoutProtocol
354 || preg_match(Regex::getInvalidUrlWithoutProtocolPrecedingCharsMatcher(), $before)
355 ) {
356 continue;
357 }
358
359 $last_url = null;
360 $ascii_end_position = 0;
361
362 if (preg_match(Regex::getValidAsciiDomainMatcher(), $domain, $asciiDomain)) {
363 // check hostname length
364 if (
365 isset($asciiDomain[1])
366 && strlen(rtrim($asciiDomain[1], '.')) > static::MAX_ASCII_HOSTNAME_LENGTH
367 ) {
368 continue;
369 }
370
371 $asciiDomain[0] = preg_replace('/' . preg_quote($domain, '/') . '/u', $asciiDomain[0], $url);
372 $ascii_start_position = StringUtils::strpos($domain, $asciiDomain[0], $ascii_end_position);
373 $ascii_end_position = $ascii_start_position + StringUtils::strlen($asciiDomain[0]);
374 $last_url = array(
375 'url' => $asciiDomain[0],
376 'indices' => array(
377 $start_position + $ascii_start_position,
378 $start_position + $ascii_end_position
379 ),
380 );
381 if (
382 !empty($path)
383 || preg_match(Regex::getValidSpecialShortDomainMatcher(), $asciiDomain[0])
384 || !preg_match(Regex::getInvalidCharactersMatcher(), $asciiDomain[0])
385 ) {
386 $urls[] = $last_url;
387 }
388 }
389
390 // no ASCII-only domain found. Skip the entire URL
391 if (empty($last_url)) {
392 continue;
393 }
394
395 // $last_url only contains domain. Need to add path and query if they exist.
396 if (!empty($path)) {
397 // last_url was not added. Add it to urls here.
398 $last_url['url'] = preg_replace('/' . preg_quote($domain, '/') . '/u', $last_url['url'], $url);
399 $last_url['indices'][1] = $end_position;
400 }
401 } else {
402 // In the case of t.co URLs, don't allow additional path characters
403 if (preg_match(Regex::getValidTcoUrlMatcher(), $url, $tcoUrlMatches)) {
404 list($url, $tcoUrlSlug) = $tcoUrlMatches;
405 $end_position = $start_position + StringUtils::strlen($url);
406
407 // In the case of t.co URLs, don't allow additional path characters and
408 // ensure that the slug is under 40 chars.
409 if (strlen($tcoUrlSlug) > static::MAX_TCO_SLUG_LENGTH) {
410 continue;
411 }
412 }
413 if ($this->isValidHostAndLength(StringUtils::strlen($url), $protocol, $domain)) {
414 $urls[] = array(
415 'url' => $url,
416 'indices' => array($start_position, $end_position),
417 );
418 }
419 }
420 }
421
422 return $urls;
423 }
424
425 /**
426 * Verifies that the host name adheres to RFC 3490 and 1035
427 * Also, verifies that the entire url (including protocol) doesn't exceed MAX_URL_LENGTH
428 *
429 * @param int $originalUrlLength The length of the entire URL, including protocol if any
430 * @param string $protocol The protocol used
431 * @param string $host The hostname to check validity of
432 * @return bool true if the host is valid
433 */
434 public function isValidHostAndLength($originalUrlLength, $protocol, $host)
435 {
436 if (empty($host)) {
437 return false;
438 }
439
440 $originalHostLength = StringUtils::strlen($host);
441
442 // Use IDN for all host names, if the host is all ASCII, it returns unchanged.
443 // It comes with an added benefit of checking the host length to be between 1 to 63 characters.
444 $encodedHost = StringUtils::idnToAscii($host);
445 if ($encodedHost === false || empty($encodedHost)) {
446 return false;
447 }
448
449 $punycodeEncodedHostLength = StringUtils::strlen($encodedHost);
450 if ($punycodeEncodedHostLength === 0) {
451 return false;
452 }
453
454 // The punycodeEncoded host length might be different now, offset that length from the URL.
455 $encodedUrlLength = $originalUrlLength + $punycodeEncodedHostLength - $originalHostLength;
456 // Add the protocol to our length check, if there isn't one, to ensure it doesn't go over the limit.
457 $urlLengthWithProtocol = $encodedUrlLength + (empty($protocol) ? self::URL_GROUP_PROTOCOL_LENGTH : 0);
458
459 return $urlLengthWithProtocol <= self::MAX_URL_LENGTH;
460 }
461
462 /**
463 * Extracts all the usernames and the indices they occur at from the tweet.
464 *
465 * @param string $tweet The tweet to extract.
466 * @return array The username elements in the tweet.
467 */
468 public function extractMentionedScreennamesWithIndices($tweet)
469 {
470 $usernamesOnly = array();
471 $mentions = $this->extractMentionsOrListsWithIndices($tweet);
472 foreach ($mentions as $mention) {
473 if (isset($mention['list_slug'])) {
474 unset($mention['list_slug']);
475 }
476 $usernamesOnly[] = $mention;
477 }
478 return $usernamesOnly;
479 }
480
481 /**
482 * Extracts all the usernames and the indices they occur at from the tweet.
483 *
484 * @param string $tweet The tweet to extract.
485 * @return array The username elements in the tweet.
486 */
487 public function extractMentionsOrListsWithIndices($tweet)
488 {
489 if (!preg_match('/[@@]/u', $tweet)) {
490 return array();
491 }
492
493 preg_match_all(Regex::getValidMentionsOrListsMatcher(), $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
494 $results = array();
495
496 foreach ($matches as $match) {
497 list($all, $before, $at, $username, $list_slug, $outer) = array_pad($match, 6, array('', 0));
498 $start_position = $at[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $at[1])) : $at[1];
499 $end_position = $start_position + StringUtils::strlen($at[0]) + StringUtils::strlen($username[0]);
500 $entity = array(
501 'screen_name' => $username[0],
502 'list_slug' => $list_slug[0],
503 'indices' => array($start_position, $end_position),
504 );
505
506 if (preg_match(Regex::getEndMentionMatcher(), $outer[0])) {
507 continue;
508 }
509
510 if (!empty($list_slug[0])) {
511 $entity['indices'][1] = $end_position + StringUtils::strlen($list_slug[0]);
512 }
513
514 $results[] = $entity;
515 }
516
517 return $results;
518 }
519
520 /**
521 * setter/getter for extractURLWithoutProtocol
522 *
523 * @param boolean $flag
524 * @return bool|Extractor
525 */
526 public function extractURLWithoutProtocol($flag = null)
527 {
528 if ($flag === null) {
529 return $this->extractURLWithoutProtocol;
530 }
531 $this->extractURLWithoutProtocol = (bool) $flag;
532 return $this;
533 }
534
535 /**
536 * Remove overlapping entities.
537 * This returns a new array with no overlapping entities.
538 *
539 * @param array $entities
540 * @return array
541 */
542 public function removeOverlappingEntities($entities)
543 {
544 $result = array();
545 usort($entities, array($this, 'sortEntities'));
546
547 $prev = null;
548 foreach ($entities as $entity) {
549 if ($prev !== null && $entity['indices'][0] < $prev['indices'][1]) {
550 continue;
551 }
552 $prev = $entity;
553 $result[] = $entity;
554 }
555 return $result;
556 }
557
558 /**
559 * sort by entity start index
560 *
561 * @param array $a
562 * @param array $b
563 * @return int
564 */
565 protected function sortEntities($a, $b)
566 {
567 if ($a['indices'][0] === $b['indices'][0]) {
568 return 0;
569 }
570 return ($a['indices'][0] < $b['indices'][0]) ? -1 : 1;
571 }
572 }
573