Skip to content

Commit 8e4a5ef

Browse files
committed
Fix. Code. Fixed match offset.
1 parent 397d671 commit 8e4a5ef

3 files changed

Lines changed: 295 additions & 57 deletions

File tree

ContactsEncoder.php

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -295,40 +295,41 @@ public function modifyGlobalEmails($content)
295295

296296
$this->temp_content = $content;
297297

298-
$replacing_result = preg_replace_callback($this->global_email_pattern, function ($matches) {
299-
if ( isset($matches[3]) && in_array(strtolower($matches[3]), ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']) && isset($matches[0]) ) {
298+
$match_cursor = 0;
299+
$replacing_result = preg_replace_callback($this->global_email_pattern, function ($matches) use (&$match_cursor) {
300+
if ( ! isset($matches[0]) ) {
301+
return '';
302+
}
303+
304+
$position = $this->advanceMatchCursor($matches[0], $match_cursor);
305+
306+
if ( isset($matches[3]) && in_array(strtolower($matches[3]), ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']) ) {
300307
return $matches[0];
301308
}
302309

303310
//chek if email is placed in excluded attributes and return unchanged if so
304-
if ( isset($matches[0]) && $this->helper->hasAttributeExclusions($matches[0], $this->temp_content) ) {
311+
if ( $this->helper->hasAttributeExclusions($matches[0], $this->temp_content, $position) ) {
305312
return $matches[0];
306313
}
307314

308315
// skip encoding if the content in script tag
309-
if ( isset($matches[0]) && $this->helper->isInsideScriptTag($matches[0], $this->temp_content) ) {
316+
if ( $this->helper->isInsideScriptTag($matches[0], $this->temp_content, $position) ) {
310317
return $matches[0];
311318
}
312319

313-
if ( isset($matches[0]) && $this->helper->isInsideOptionTag($matches[0], $this->temp_content) ) {
320+
if ( $this->helper->isInsideOptionTag($matches[0], $this->temp_content, $position) ) {
314321
return $matches[0];
315322
}
316323

317-
if (
318-
isset($matches[0]) && $this->helper->isMailtoAdditionalCopy($matches[0], $this->temp_content)
319-
) {
324+
if ( $this->helper->isMailtoAdditionalCopy($matches[0], $this->temp_content, $position) ) {
320325
return '';
321326
}
322327

323-
if ( isset($matches[0]) && $this->helper->isMailto($matches[0]) ) {
328+
if ( $this->helper->isMailto($matches[0]) ) {
324329
return $this->encodeMailtoLink($matches[0]);
325330
}
326331

327-
if ( isset($matches[0]) ) {
328-
return $this->encodePlainEmail($matches[0]);
329-
}
330-
331-
return '';
332+
return $this->encodePlainEmail($matches[0]);
332333
}, $content);
333334

334335
if ( $owns_aria_protection ) {
@@ -356,41 +357,44 @@ public function modifyGlobalPhoneNumbers($content)
356357
$this->temp_content = $content;
357358

358359
$phones_pattern = $this->global_phones_pattern;
360+
$match_cursor = 0;
359361
$replacing_result = preg_replace_callback(
360362
$phones_pattern,
361-
function ($matches) {
362-
if ( isset($matches[0]) ) {
363-
if ( $this->helper->isTelTag($matches[0]) ) {
364-
return $this->encodeTelLink($matches[0]);
365-
}
366-
367-
// symbols clearance
368-
$item_length = strlen(str_replace([' ', '(', ')', '-', '+', '.'], '', $matches[0]));
369-
370-
// check length
371-
if ( $item_length > 12 || $item_length < 8 ) {
372-
return $matches[0];
373-
}
374-
375-
// check attribute exclusions
376-
if ( $this->helper->hasAttributeExclusions($matches[0], $this->temp_content) ) {
377-
return $matches[0];
378-
}
379-
380-
// check if in script
381-
if ( $this->helper->isInsideScriptTag($matches[0], $this->temp_content) ) {
382-
return $matches[0];
383-
}
384-
385-
return $this->encodeAny(
386-
$matches[0],
387-
$this->global_obfuscation_mode,
388-
$this->global_replacing_text,
389-
true
390-
);
363+
function ($matches) use (&$match_cursor) {
364+
if ( ! isset($matches[0]) ) {
365+
return '';
391366
}
392367

393-
return '';
368+
$position = $this->advanceMatchCursor($matches[0], $match_cursor);
369+
370+
if ( $this->helper->isTelTag($matches[0]) ) {
371+
return $this->encodeTelLink($matches[0]);
372+
}
373+
374+
// symbols clearance
375+
$item_length = strlen(str_replace([' ', '(', ')', '-', '+', '.'], '', $matches[0]));
376+
377+
// check length
378+
if ( $item_length > 12 || $item_length < 8 ) {
379+
return $matches[0];
380+
}
381+
382+
// check attribute exclusions
383+
if ( $this->helper->hasAttributeExclusions($matches[0], $this->temp_content, $position) ) {
384+
return $matches[0];
385+
}
386+
387+
// check if in script
388+
if ( $this->helper->isInsideScriptTag($matches[0], $this->temp_content, $position) ) {
389+
return $matches[0];
390+
}
391+
392+
return $this->encodeAny(
393+
$matches[0],
394+
$this->global_obfuscation_mode,
395+
$this->global_replacing_text,
396+
true
397+
);
394398
},
395399
$content
396400
);
@@ -403,6 +407,23 @@ function ($matches) {
403407
return $replacing_result;
404408
}
405409

410+
/**
411+
* Advance the left-to-right cursor so repeated contacts use their own offset.
412+
*
413+
* @param string $match
414+
* @param int $cursor
415+
* @return int|false
416+
*/
417+
private function advanceMatchCursor($match, &$cursor)
418+
{
419+
$position = strpos($this->temp_content, $match, $cursor);
420+
if ( $position !== false ) {
421+
$cursor = $position + strlen($match);
422+
}
423+
424+
return $position;
425+
}
426+
406427
/*
407428
* =============== ENCODE ENTITIES ===============
408429
*/

Helper/ContactsEncoderHelper.php

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ public function isTelTag($string)
6060
*
6161
* @param string $email
6262
* @param string $content
63+
* @param int|false|null $position Known match offset; null looks up the first occurrence
6364
*
6465
* @return bool
6566
*/
66-
public function isMailtoAdditionalCopy($email, $content)
67+
public function isMailtoAdditionalCopy($email, $content, $position = null)
6768
{
68-
$position = strpos($content, $email);
69+
$position = $this->resolveMatchPosition($email, $content, $position);
6970

7071
if ($position === false) {
7172
return false;
@@ -89,12 +90,13 @@ public function isMailtoAdditionalCopy($email, $content)
8990
*
9091
* @param string $email
9192
* @param string $content
93+
* @param int|false|null $position Known match offset; null looks up the first occurrence
9294
*
9395
* @return bool
9496
*/
95-
public function isInsideOptionTag($email, $content)
97+
public function isInsideOptionTag($email, $content, $position = null)
9698
{
97-
$pos = strpos($content, $email);
99+
$pos = $this->resolveMatchPosition($email, $content, $position);
98100
if ($pos === false) {
99101
return false;
100102
}
@@ -121,12 +123,12 @@ public function isInsideOptionTag($email, $content)
121123
* Check if the given email is inside a script tag
122124
* @param string $email The email to check
123125
* @param string $content The full content
126+
* @param int|false|null $position Known match offset; null looks up the first occurrence
124127
* @return bool
125128
*/
126-
public function isInsideScriptTag($email, $content)
129+
public function isInsideScriptTag($email, $content, $position = null)
127130
{
128-
// Find position of the email in content
129-
$pos = strpos($content, $email);
131+
$pos = $this->resolveMatchPosition($email, $content, $position);
130132
if ($pos === false) {
131133
return false;
132134
}
@@ -236,14 +238,22 @@ public function addAttributeNames(array $names)
236238
*
237239
* @param string $email_match - email
238240
* @param string $temp_content - email
241+
* @param int|false|null $position Known match offset; null accepts any occurrence
239242
* @return bool
240243
*/
241-
public function hasAttributeExclusions($email_match, $temp_content)
244+
public function hasAttributeExclusions($email_match, $temp_content, $position = null)
242245
{
243246
if ( ! is_string($email_match) || $email_match === '' || ! is_string($temp_content) ) {
244247
return false;
245248
}
246249

250+
if ( $position !== null ) {
251+
$position = $this->resolveMatchPosition($email_match, $temp_content, $position);
252+
if ( $position === false ) {
253+
return false;
254+
}
255+
}
256+
247257
$quoted_match = preg_quote($email_match, '/');
248258
$attribute_signs = $this->getWorkingAttributeExclusionsSigns();
249259

@@ -255,14 +265,14 @@ public function hasAttributeExclusions($email_match, $temp_content)
255265
if ( ! is_string($attribute) || $attribute === '' ) {
256266
continue;
257267
}
258-
if ( $this->isMatchInsideAttribute($quoted_match, $attribute, $temp_content, $tag) ) {
268+
if ( $this->isMatchInsideAttribute($quoted_match, $attribute, $temp_content, $tag, $position) ) {
259269
return true;
260270
}
261271
}
262272
}
263273

264274
foreach ( $this->attribute_exclusions_list as $attribute ) {
265-
if ( $this->isMatchInsideAttribute($quoted_match, $attribute, $temp_content) ) {
275+
if ( $this->isMatchInsideAttribute($quoted_match, $attribute, $temp_content, null, $position) ) {
266276
return true;
267277
}
268278
}
@@ -296,14 +306,43 @@ private function sanitizeAttributeNames(array $names)
296306
return $result;
297307
}
298308

309+
/**
310+
* @param string $needle
311+
* @param string $haystack
312+
* @param int|false|null $position
313+
* @return int|false
314+
*/
315+
private function resolveMatchPosition($needle, $haystack, $position)
316+
{
317+
if ( $position === null ) {
318+
return strpos($haystack, $needle);
319+
}
320+
321+
if ( $position === false || ! is_int($position) || $position < 0 || ! is_string($needle) || $needle === '' ) {
322+
return false;
323+
}
324+
325+
$length = strlen($needle);
326+
if ( $position > strlen($haystack) - $length ) {
327+
return false;
328+
}
329+
330+
if ( substr($haystack, $position, $length) !== $needle ) {
331+
return false;
332+
}
333+
334+
return $position;
335+
}
336+
299337
/**
300338
* @param string $quoted_match
301339
* @param string $attribute
302340
* @param string $content
303341
* @param string|null $tag
342+
* @param int|null $position
304343
* @return bool
305344
*/
306-
private function isMatchInsideAttribute($quoted_match, $attribute, $content, $tag = null)
345+
private function isMatchInsideAttribute($quoted_match, $attribute, $content, $tag = null, $position = null)
307346
{
308347
$quoted_attribute = preg_quote($attribute, '/');
309348
// Always require an HTML tag so plain text like attr="..." is not treated as markup.
@@ -319,6 +358,25 @@ private function isMatchInsideAttribute($quoted_match, $attribute, $content, $ta
319358
. $quoted_match
320359
. '[^"\']*\1/';
321360

322-
return (bool) preg_match($pattern, $content);
361+
if ( $position === null ) {
362+
return (bool) preg_match($pattern, $content);
363+
}
364+
365+
if ( ! preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE) || ! isset($matches[0]) ) {
366+
return false;
367+
}
368+
369+
foreach ( $matches[0] as $match ) {
370+
if ( ! isset($match[0], $match[1]) ) {
371+
continue;
372+
}
373+
$start = $match[1];
374+
$end = $start + strlen($match[0]);
375+
if ( $position >= $start && $position < $end ) {
376+
return true;
377+
}
378+
}
379+
380+
return false;
323381
}
324382
}

0 commit comments

Comments
 (0)