@@ -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