Repository navigation
Upd. ContactsEncoder. Content search improved. #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
cc1b8f3
Upd. ContactsEncoder. Matches search enhanced. Drop attributes logic …
alexandergull aad8094
Fix. ContactsEncoder. Attribute containment updated. Title duplicatio…
alexandergull 855a156
Fix. ContactsEncoder. Tests PHP 7.4 compatibility.
alexandergull f9672c5
Fix. ContactsEncoder. Protect raw text blocks in attribute stripping.
alexandergull File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -270,7 +270,7 @@ public function modifyContent($content, $skip_exclusions = false) | |
| // will use this in regexp callback | ||
| $this->temp_content = $content; | ||
|
|
||
| $content = self::dropAttributesContainEmail($content, self::$attributes_to_drop); | ||
| $content = $this->dropAttributesContainEmail($content, self::$attributes_to_drop); | ||
|
|
||
| // Main logic | ||
|
|
||
|
|
@@ -299,14 +299,17 @@ public function modifyGlobalEmails($content) | |
| } | ||
|
|
||
| $this->temp_content = $content; | ||
| $this->helper->indexMarkup($this->temp_content); | ||
|
|
||
| $match_cursor = 0; | ||
| $replacing_result = preg_replace_callback($this->global_email_pattern, function ($matches) use (&$match_cursor) { | ||
| $match_offsets = $this->collectMatchOffsets($this->global_email_pattern, $content); | ||
| $match_offset_index = 0; | ||
| $replacing_result = preg_replace_callback($this->global_email_pattern, function ($matches) use ($match_offsets, &$match_offset_index, &$match_cursor) { | ||
| if ( ! isset($matches[0]) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| $position = $this->advanceMatchCursor($matches[0], $match_cursor); | ||
| $position = $this->resolveNextMatchOffset($matches[0], $match_offsets, $match_offset_index, $match_cursor); | ||
|
|
||
| if ( $this->exclusions->isContactExcluded($matches[0]) ) { | ||
| return $matches[0]; | ||
|
|
@@ -316,26 +319,30 @@ public function modifyGlobalEmails($content) | |
| return $matches[0]; | ||
| } | ||
|
|
||
| //chek if email is placed in excluded attributes and return unchanged if so | ||
| if ( $this->helper->hasAttributeExclusions($matches[0], $this->temp_content, $position) ) { | ||
| // Raw text of script/style/template blocks is never markup we may rewrite. | ||
| if ( $this->helper->isInsideRawTextTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| // skip encoding if the content in script tag | ||
| if ( $this->helper->isInsideScriptTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| // Drop the cc=/bcc= copies before the markup guards take them out of the flow. | ||
| if ( $this->helper->isMailtoAdditionalCopy($matches[0], $this->temp_content, $position) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| if ( $this->helper->isInsideOptionTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| // mailto: links are encoded in place inside the href attribute, so they bypass the markup guards. | ||
| if ( $this->helper->isMailto($matches[0]) ) { | ||
| $position = $position === false ? 0 : $position; | ||
|
|
||
| return $this->encodeMailtoLink($matches[0], $position); | ||
| } | ||
|
|
||
| if ( $this->helper->isMailtoAdditionalCopy($matches[0], $this->temp_content, $position) ) { | ||
| return ''; | ||
| //chek if email is placed in excluded attributes and return unchanged if so | ||
| if ( $this->helper->hasAttributeExclusions($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| if ( $this->helper->isMailto($matches[0]) ) { | ||
| return $this->encodeMailtoLink($matches[0]); | ||
| if ( $this->helper->isInsideOptionTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| return $this->encodePlainEmail($matches[0]); | ||
|
|
@@ -364,24 +371,35 @@ public function modifyGlobalPhoneNumbers($content) | |
| } | ||
|
|
||
| $this->temp_content = $content; | ||
| $this->helper->indexMarkup($this->temp_content); | ||
|
|
||
| $phones_pattern = $this->global_phones_pattern; | ||
| $match_cursor = 0; | ||
| $match_offsets = $this->collectMatchOffsets($phones_pattern, $content); | ||
| $match_offset_index = 0; | ||
| $replacing_result = preg_replace_callback( | ||
| $phones_pattern, | ||
| function ($matches) use (&$match_cursor) { | ||
| function ($matches) use ($match_offsets, &$match_offset_index, &$match_cursor) { | ||
| if ( ! isset($matches[0]) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| $position = $this->advanceMatchCursor($matches[0], $match_cursor); | ||
| $position = $this->resolveNextMatchOffset($matches[0], $match_offsets, $match_offset_index, $match_cursor); | ||
|
|
||
| if ( $this->exclusions->isContactExcluded($matches[0]) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| // Raw text of script/style/template blocks is never markup we may rewrite. | ||
| if ( $this->helper->isInsideRawTextTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| // tel: links are encoded in place inside the href attribute, so they bypass the markup guards. | ||
| if ( $this->helper->isTelTag($matches[0]) ) { | ||
| return $this->encodeTelLink($matches[0]); | ||
| $position = $position === false ? 0 : $position; | ||
|
|
||
| return $this->encodeTelLink($matches[0], $position); | ||
| } | ||
|
|
||
| // symbols clearance | ||
|
|
@@ -397,11 +415,6 @@ function ($matches) use (&$match_cursor) { | |
| return $matches[0]; | ||
| } | ||
|
|
||
| // check if in script | ||
| if ( $this->helper->isInsideScriptTag($matches[0], $this->temp_content, $position) ) { | ||
| return $matches[0]; | ||
| } | ||
|
|
||
| return $this->encodeAny( | ||
| $matches[0], | ||
| $this->global_obfuscation_mode, | ||
|
|
@@ -421,22 +434,66 @@ function ($matches) use (&$match_cursor) { | |
| } | ||
|
|
||
| /** | ||
| * Advance the left-to-right cursor so repeated contacts use their own offset. | ||
| * Resolve the real offset of the current match. | ||
| * | ||
| * The offsets are collected up front with PREG_OFFSET_CAPTURE and consumed in order, because | ||
| * preg_match_all() and preg_replace_callback() walk the same pattern over the same subject and | ||
| * therefore produce the same sequence of matches. Looking the match up with strpos() instead | ||
| * would land on an earlier literal occurrence that the pattern did not match, e.g. the address | ||
| * inside `<script>var t="[email protected]";</script>` shadowing a real one further down. | ||
| * | ||
| * @param string $match | ||
| * @param int[] $offsets | ||
| * @param int $offset_index | ||
| * @param int $cursor | ||
| * @return int|false | ||
| */ | ||
| private function advanceMatchCursor($match, &$cursor) | ||
| private function resolveNextMatchOffset($match, $offsets, &$offset_index, &$cursor) | ||
| { | ||
| $length = strlen($match); | ||
|
|
||
| if ( isset($offsets[$offset_index]) ) { | ||
| $position = $offsets[$offset_index]; | ||
| $offset_index++; | ||
|
|
||
| if ( substr($this->temp_content, $position, $length) === $match ) { | ||
| $cursor = $position + $length; | ||
|
|
||
| return $position; | ||
| } | ||
| } | ||
|
|
||
| // The pre-collected offsets are unusable, fall back to a left-to-right scan. | ||
| $position = strpos($this->temp_content, $match, $cursor); | ||
| if ( $position !== false ) { | ||
| $cursor = $position + strlen($match); | ||
| $cursor = $position + $length; | ||
| } | ||
|
|
||
| return $position; | ||
| } | ||
|
|
||
| /** | ||
| * Collect the real offset of every match of the pattern in the content. | ||
| * | ||
| * @param string $pattern | ||
| * @param string $content | ||
| * @return int[] | ||
| */ | ||
| private function collectMatchOffsets($pattern, $content) | ||
| { | ||
| $offsets = array(); | ||
|
|
||
| if ( preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE) && isset($matches[0]) ) { | ||
| foreach ( $matches[0] as $match ) { | ||
| if ( isset($match[1]) ) { | ||
| $offsets[] = $match[1]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return $offsets; | ||
| } | ||
|
|
||
| /* | ||
| * =============== ENCODE ENTITIES =============== | ||
| */ | ||
|
|
@@ -504,10 +561,11 @@ protected function encodeAny($string, $mode = Params::OBFUSCATION_MODE_BLUR, $re | |
| * Method to process mailto: links. | ||
| * | ||
| * @param string $mailto_link_str | ||
| * @param int $position Offset of the match inside the content being processed. | ||
| * | ||
| * @return string | ||
| */ | ||
| private function encodeMailtoLink($mailto_link_str) | ||
| private function encodeMailtoLink($mailto_link_str, $position = 0) | ||
| { | ||
| // Get inner tag text and place it in $matches[1] | ||
| preg_match($this->global_mailto_pattern, $mailto_link_str, $matches); | ||
|
|
@@ -523,17 +581,18 @@ private function encodeMailtoLink($mailto_link_str) | |
|
|
||
| $text = isset($mailto_inner_text) ? $mailto_inner_text : $mailto_link_str; | ||
|
|
||
| return 'mailto:' . $text . '" data-original-string="' . $encoded . '" title="' . htmlspecialchars($this->getTooltip(), ENT_QUOTES, 'UTF-8'); | ||
| return 'mailto:' . $text . $this->buildSchemeLinkAttributes($encoded, $position); | ||
| } | ||
|
|
||
| /** | ||
| * Method to process tel: links. | ||
| * | ||
| * @param string $tel_link_str | ||
| * @param int $position Offset of the match inside the content being processed. | ||
| * | ||
| * @return string | ||
| */ | ||
| private function encodeTelLink($tel_link_str) | ||
| private function encodeTelLink($tel_link_str, $position = 0) | ||
| { | ||
| // Get inner tag text and place it in $matches[1] | ||
| preg_match($this->global_tel_pattern, $tel_link_str, $matches); | ||
|
|
@@ -550,7 +609,32 @@ private function encodeTelLink($tel_link_str) | |
|
|
||
| $text = isset($tel_inner_text) ? $tel_inner_text : $tel_link_str; | ||
|
|
||
| return 'tel:' . $text . '" data-original-string="' . $encoded . '" title="' . htmlspecialchars($this->getTooltip(), ENT_QUOTES, 'UTF-8'); | ||
| return 'tel:' . $text . $this->buildSchemeLinkAttributes($encoded, $position); | ||
| } | ||
|
|
||
| /** | ||
| * Builds the attribute tail appended to an opening tag when a scheme link is encoded in place. | ||
| * | ||
| * The tail intentionally leaves the last attribute value unclosed: the quote that used to close | ||
| * the original href value closes it instead. The tooltip is skipped when the tag already carries | ||
| * a title attribute, otherwise the markup would end up with a duplicated attribute and the | ||
| * author's own title would be wiped by the decoder script. | ||
| * | ||
| * @param string $encoded Encoded original string. | ||
| * @param int $position Offset of the match inside the content being processed. | ||
| * | ||
| * @return string | ||
| */ | ||
| private function buildSchemeLinkAttributes($encoded, $position) | ||
| { | ||
| $has_own_title = $this->helper->enclosingTagHasAttribute($this->temp_content, (int)$position, 'title'); | ||
|
|
||
| if ( $has_own_title ) { | ||
| return '" data-original-string="' . $encoded; | ||
| } | ||
|
|
||
| return '" title="' . htmlspecialchars($this->getTooltip(), ENT_QUOTES, 'UTF-8') | ||
| . '" data-original-string="' . $encoded; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -823,24 +907,75 @@ protected static function getDefaultReplacingText() | |
| * Example: <code><a title="[email protected]" href="mailto:[email protected]">Email</a></code> | ||
| * Will be turned to <code><a href="mailto:[email protected]">Email</a></code> | ||
| * | ||
| * The attribute is dropped wherever the address sits inside its value, so | ||
| * <code>title="Write to [email protected]"</code> is handled as well. Matching is scoped to a | ||
| * single tag and a single quoted value, otherwise a greedy match would swallow the rest of the | ||
| * markup along with it. | ||
| * | ||
| * @param string $content The content to process. | ||
| * @return string The content with attributes removed. | ||
| */ | ||
| private static function dropAttributesContainEmail($content, $tags) | ||
| private function dropAttributesContainEmail($content, $tags) | ||
| { | ||
| $attribute_content_chunk = '[\s]{0,}=[\s]{0,}[\"\']\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\..*\b[\"\']'; | ||
| $email_pattern = '/' . self::EMAIL_PATTERN . '/'; | ||
| $this->helper->indexMarkup($content); | ||
|
|
||
| foreach ($tags as $tag => $attribute) { | ||
| // Regular expression to match the attribute without the tag | ||
| $regexp_chunk_without_tag = "/{$attribute}{$attribute_content_chunk}/"; | ||
| // Regular expression to match the attribute with the tag | ||
| $regexp_chunk_with_tag = "/<{$tag}.*{$attribute}{$attribute_content_chunk}/"; | ||
| // Find all matches of the attribute with the tag in the content | ||
| preg_match_all($regexp_chunk_with_tag, $content, $matches); | ||
| if (!empty($matches[0])) { | ||
| // Remove the attribute without the tag from the content | ||
| $content = preg_replace($regexp_chunk_without_tag, '', $content, count($matches[0])); | ||
| if ( ! is_string($tag) || $tag === '' || ! is_string($attribute) || $attribute === '' ) { | ||
| continue; | ||
| } | ||
|
|
||
| // A whole tag, with quoted attribute values consumed as a unit so `>` inside them is kept. | ||
| $tag_pattern = '/<' . preg_quote($tag, '/') . '\b(?:[^>"\']|"[^"]*"|\'[^\']*\')*>/i'; | ||
| // The target attribute inside that tag, value limited to its own quotes. | ||
| $attribute_pattern = '/\s' . preg_quote($attribute, '/') . '\s*=\s*(["\'])(.*?)\1/is'; | ||
|
|
||
| $replaced = preg_replace_callback( | ||
| $tag_pattern, | ||
| function ($tag_match) use ($attribute_pattern, $email_pattern, $content) { | ||
| if ( ! isset($tag_match[0]) || ! is_array($tag_match[0]) || ! isset($tag_match[0][0], $tag_match[0][1]) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| $opening_tag = $tag_match[0][0]; | ||
| $tag_offset = $tag_match[0][1]; | ||
|
|
||
| if ( ! is_string($opening_tag) || ! is_int($tag_offset) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| if ( $this->helper->isInsideRawTextTag($opening_tag, $content, $tag_offset) ) { | ||
| return $opening_tag; | ||
| } | ||
|
|
||
| $stripped = preg_replace_callback( | ||
| $attribute_pattern, | ||
| static function ($attribute_match) use ($email_pattern) { | ||
| if ( ! isset($attribute_match[0], $attribute_match[2]) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| return preg_match($email_pattern, $attribute_match[2]) | ||
| ? '' | ||
| : $attribute_match[0]; | ||
| }, | ||
| $opening_tag | ||
| ); | ||
|
|
||
| return $stripped === null ? $opening_tag : $stripped; | ||
| }, | ||
| $content, | ||
| -1, | ||
| $count, | ||
| PREG_OFFSET_CAPTURE | ||
| ); | ||
|
|
||
| if ( $replaced !== null && $count > 0 ) { | ||
| $content = $replaced; | ||
| $this->helper->indexMarkup($content); | ||
| } | ||
| } | ||
|
|
||
| return $content; | ||
| } | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.