Skip to content

Commit

Permalink
Core: Relax VAST tag matching for vtrack handling (#3059)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Mar 19, 2024
1 parent 183b3b1 commit f580dcc
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 37 deletions.
78 changes: 42 additions & 36 deletions src/main/java/org/prebid/server/vast/VastModifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class VastModifier {

private static final String IN_LINE_TAG = "<InLine>";
private static final String IN_LINE_CLOSE_TAG = "</InLine>";
private static final String WRAPPER_TAG = "<Wrapper>";
private static final String WRAPPER_CLOSE_TAG = "</Wrapper>";
private static final String IMPRESSION_CLOSE_TAG = "</Impression>";
private static final Pattern WRAPPER_OPEN_TAG_PATTERN =
Pattern.compile("<\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE);
private static final Pattern WRAPPER_CLOSE_TAG_PATTERN =
Pattern.compile("<\\s*/\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE);
private static final Pattern INLINE_OPEN_TAG_PATTERN =
Pattern.compile("<\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE);
private static final Pattern INLINE_CLOSE_TAG_PATTERN =
Pattern.compile("<\\s*/\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE);
private static final Pattern IMPRESSION_CLOSE_TAG_PATTERN =
Pattern.compile("<\\s*/\\s*impression(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE);

private final BidderCatalog bidderCatalog;
private final EventsService eventsService;
private final Metrics metrics;
Expand Down Expand Up @@ -102,45 +111,42 @@ private static String resolveVastXmlFrom(String bidAdm, String bidNurl) {
: bidAdm;
}

private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking, String bidder) {
final int inLineTagIndex = StringUtils.indexOfIgnoreCase(vastXml, IN_LINE_TAG);
final int wrapperTagIndex = StringUtils.indexOfIgnoreCase(vastXml, WRAPPER_TAG);
private static String appendTrackingUrlToVastXml(String xml, String urlTracking, String bidder) {
return appendTrackingUrl(xml, urlTracking, INLINE_OPEN_TAG_PATTERN, INLINE_CLOSE_TAG_PATTERN)
.or(() -> appendTrackingUrl(xml, urlTracking, WRAPPER_OPEN_TAG_PATTERN, WRAPPER_CLOSE_TAG_PATTERN))
.orElseThrow(() -> new PreBidException(
"VastXml does not contain neither InLine nor Wrapper for %s response".formatted(bidder)));
}

private static Optional<String> appendTrackingUrl(String vastXml,
String vastUrlTracking,
Pattern openTagPattern,
Pattern closeTagPattern) {

if (inLineTagIndex != -1) {
return appendTrackingUrl(vastXml, vastUrlTracking, IN_LINE_CLOSE_TAG);
} else if (wrapperTagIndex != -1) {
return appendTrackingUrl(vastXml, vastUrlTracking, WRAPPER_CLOSE_TAG);
final Matcher openTagMatcher = openTagPattern.matcher(vastXml);
if (!openTagMatcher.find()) {
return Optional.empty();
}
throw new PreBidException("VastXml does not contain neither InLine nor Wrapper for %s response"
.formatted(bidder));
}

private static String appendTrackingUrl(String vastXml, String vastUrlTracking, String elementCloseTag) {
if (vastXml.contains(IMPRESSION_CLOSE_TAG)) {
return insertAfterExistingImpressionTag(vastXml, vastUrlTracking);
final Matcher impressionCloseTagMatcher = IMPRESSION_CLOSE_TAG_PATTERN.matcher(vastXml);
if (impressionCloseTagMatcher.find(openTagMatcher.end())) {
int replacementEnd = impressionCloseTagMatcher.end();
while (impressionCloseTagMatcher.find(replacementEnd)) {
replacementEnd = impressionCloseTagMatcher.end();
}
return Optional.of(insertUrlTracking(vastXml, replacementEnd, vastUrlTracking));
}
return insertBeforeElementCloseTag(vastXml, vastUrlTracking, elementCloseTag);
}

private static String insertAfterExistingImpressionTag(String vastXml, String vastUrlTracking) {
final String impressionTag = "<Impression><![CDATA[" + vastUrlTracking + "]]></Impression>";
final int replacementStart = vastXml.lastIndexOf(IMPRESSION_CLOSE_TAG);
final Matcher closeTagMatcher = closeTagPattern.matcher(vastXml);
if (!closeTagMatcher.find(openTagMatcher.end())) {
return Optional.of(vastXml);
}

return vastXml.substring(0, replacementStart) + IMPRESSION_CLOSE_TAG + impressionTag
+ vastXml.substring(replacementStart + IMPRESSION_CLOSE_TAG.length());
return Optional.of(insertUrlTracking(vastXml, closeTagMatcher.start(), vastUrlTracking));
}

private static String insertBeforeElementCloseTag(String vastXml, String vastUrlTracking, String elementCloseTag) {
final int indexOfCloseTag = StringUtils.indexOfIgnoreCase(vastXml, elementCloseTag);

if (indexOfCloseTag == -1) {
return vastXml;
}

final String caseSpecificCloseTag =
vastXml.substring(indexOfCloseTag, indexOfCloseTag + elementCloseTag.length());
private static String insertUrlTracking(String vastXml, int index, String vastUrlTracking) {
final String impressionTag = "<Impression><![CDATA[" + vastUrlTracking + "]]></Impression>";

return vastXml.replace(caseSpecificCloseTag, impressionTag + caseSpecificCloseTag);
return vastXml.substring(0, index) + impressionTag + vastXml.substring(index);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.prebid.server.functional.tests

import org.prebid.server.functional.model.config.AccountAuctionConfig
import org.prebid.server.functional.model.config.AccountConfig
import org.prebid.server.functional.model.config.AccountEventsConfig
import org.prebid.server.functional.model.db.Account
import org.prebid.server.functional.model.request.auction.Asset
import org.prebid.server.functional.model.request.auction.BidRequest
import org.prebid.server.functional.model.request.auction.Imp
Expand Down Expand Up @@ -192,4 +196,50 @@ class CacheSpec extends BaseSpec {
false | BANNER
true | VIDEO
}

def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() {
given: "Current value of metric prebid_cache.requests.ok"
def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok")

and: "Create and save enabled events config in account"
def accountId = PBSUtils.randomNumber.toString()
def account = new Account().tap {
uuid = accountId
config = new AccountConfig().tap {
auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true))
}
}
accountDao.save(account)

and: "Vtrack request with custom tags"
def payload = PBSUtils.randomString
def creative = "<VAST version=\"3.0\"><Ad><${wrapper}><AdSystem>prebid.org wrapper</AdSystem>" +
"<VASTAdTagURI>&lt;![CDATA[//${payload}]]&gt;</VASTAdTagURI>" +
"<${impression}> &lt;![CDATA[ ]]&gt; </${impression}><Creatives></Creatives></${wrapper}></Ad></VAST>"
def request = VtrackRequest.getDefaultVtrackRequest(creative)

when: "PBS processes vtrack request"
defaultPbsService.sendVtrackRequest(request, accountId)

then: "Vast xml is modified"
def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload)
assert prebidCacheRequest.size() == 1
assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}")

and: "prebid_cache.creative_size.xml metric should be updated"
def metrics = defaultPbsService.sendCollectedMetricsRequest()
assert metrics["prebid_cache.requests.ok"] == initialValue + 1

and: "account.<account-id>.prebid_cache.creative_size.xml should be updated"
assert metrics["account.${accountId}.prebid_cache.requests.ok" as String] == 1

where:
wrapper | impression
" wrapper " | " impression "
PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ")
" wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}"
" inLine " | " ImpreSSion $PBSUtils.randomNumber"
PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber "
" inline ${PBSUtils.getRandomString()} " | " ImpreSSion "
}
}
127 changes: 126 additions & 1 deletion src/test/java/org/prebid/server/vast/VastModifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ public void createBidVastXmlShouldBeModifiedWithNewImpressionVastUrlWhenEventsEn
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></Wrapper>");
}

@Test
public void createBidVastXmlShouldBeModifiedWithNewImpressionVastUrlWhenEventsEnabledAndNoEmptyTag2() {
// when
final String bidAdm = "<Wrapper>< impreSSion garbage >http:/test.com< /ImPression garbage ></Wrapper>";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result).isEqualTo("<Wrapper>< impreSSion garbage >http:/test.com< /ImPression garbage >"
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></Wrapper>");
}

@Test
public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpressionTags() {
// when
Expand All @@ -196,6 +211,23 @@ public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpres
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression><Creatives></Creatives></InLine>");
}

@Test
public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpressionTags2() {
// when
final String bidAdm = "<InLine>< Impression >http:/test.com< /Impression >"
+ "<ImprEssion garbage>http:/test2.com< /ImPRession garbage><Creatives></Creatives></InLine>";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result).isEqualTo("<InLine>< Impression >http:/test.com< /Impression >"
+ "<ImprEssion garbage>http:/test2.com< /ImPRession garbage>"
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression><Creatives></Creatives></InLine>");
}

@Test
public void createBidVastXmlShouldInsertImpressionTagForEmptyInLine() {
// when
Expand Down Expand Up @@ -239,11 +271,27 @@ public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode() {
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></wrapper>");
}

@Test
public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode2() {
// when
final String bidAdm = "< wraPPer garbage><Impression>http:/test.com</Impression>< / wraPPer garbage>";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result).isEqualTo("< wraPPer garbage><Impression>http:/test.com</Impression>"
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression>< / wraPPer garbage>");
}

@Test
public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper() {
// when
final String bidAdm = "<wrapper></wrapper>";
final String result = target
.createBidVastXml(BIDDER, "<wrapper></wrapper>", BID_NURL,
.createBidVastXml(BIDDER, bidAdm, BID_NURL,
BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

Expand All @@ -254,6 +302,23 @@ BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
.isEqualTo("<wrapper><Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></wrapper>");
}

@Test
public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper2() {
// when
final String bidAdm = "< wraPPer garbage>< / wrapPer garbage>";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL,
BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result)
.isEqualTo("< wraPPer garbage><Impression><![CDATA["
+ VAST_URL_TRACKING + "]]></Impression>< / wrapPer garbage>");
}

@Test
public void createBidVastXmlShouldNotInsertImpressionTagForNoWrapperCloseTag() {
// when
Expand Down Expand Up @@ -283,6 +348,21 @@ public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode() {
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></Inline>");
}

@Test
public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode2() {
// when
final String bidAdm = "< InLIne garbage ><Impression>http:/test.com</Impression></ Inline garbage >";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result).isEqualTo("< InLIne garbage ><Impression>http:/test.com</Impression>"
+ "<Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></ Inline garbage >");
}

@Test
public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() {
// when
Expand All @@ -297,6 +377,21 @@ public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() {
assertThat(result).isEqualTo("<InLine><Impression><![CDATA[" + VAST_URL_TRACKING + "]]></Impression></InLine>");
}

@Test
public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags2() {
// when
final String bidAdm = "< InLIne garbage >< / InLIne garbage >";
final String result = target
.createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(),
LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());

assertThat(result).isEqualTo("< InLIne garbage ><Impression><![CDATA["
+ VAST_URL_TRACKING + "]]></Impression>< / InLIne garbage >");
}

@Test
public void createBidVastXmlShouldNotBeModifiedIfNoParentTagsPresent() {
// when
Expand All @@ -312,6 +407,36 @@ public void createBidVastXmlShouldNotBeModifiedIfNoParentTagsPresent() {
verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse);
}

@Test
public void createBidVastXmlShouldNotBeModifiedIfWrapperTagIsInvalid() {
// when
final String adm = "<wrappergarbage></wrapper>";
final List<String> warnings = new ArrayList<>();
final String result = target
.createBidVastXml(BIDDER, adm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), warnings, LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());
assertThat(result).isEqualTo(adm);
assertThat(warnings).containsExactly("VastXml does not contain neither InLine nor Wrapper for bidder response");
verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse);
}

@Test
public void createBidVastXmlShouldNotBeModifiedIfInlineTagIsInvalid() {
// when
final String adm = "<inlinegarbage></inline>";
final List<String> warnings = new ArrayList<>();
final String result = target
.createBidVastXml(BIDDER, adm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), warnings, LINEITEM_ID);

// then
verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext());
assertThat(result).isEqualTo(adm);
assertThat(warnings).containsExactly("VastXml does not contain neither InLine nor Wrapper for bidder response");
verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse);
}

@Test
public void createBidVastXmlShouldNotModifyWhenEventsEnabledAndAdmHaveNoImpression() {
// when
Expand Down

0 comments on commit f580dcc

Please sign in to comment.