diff --git a/adapters/bidder.go b/adapters/bidder.go index 15e16ce00b1..f758b2f8d83 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -110,8 +110,14 @@ type ResponseData struct { Headers http.Header } +type BidRequestParams struct { + ImpIndex int + VASTTagIndex int +} + // RequestData packages together the fields needed to make an http.Request. type RequestData struct { + Params *BidRequestParams Method string Uri string Body []byte @@ -129,7 +135,7 @@ type ExtImpBidder struct { // // Bidder implementations may safely assume that this JSON has been validated by their // static/bidder-params/{bidder}.json file. - Bidder json.RawMessage `json:"bidder"` + Bidder json.RawMessage `json:"bidder"` } func (r *RequestData) SetBasicAuth(username string, password string) { diff --git a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json index dd656237680..f3cb4713c9d 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json +++ b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json @@ -81,7 +81,7 @@ "w": 300 }, "ext": { - "pmZoneID": "Zone1,Zone2", + "pmZoneId": "Zone1,Zone2", "preference": "sports,movies", "dfp_ad_unit_code": "/2222/home" } diff --git a/adapters/vastbidder/bidder_macro.go b/adapters/vastbidder/bidder_macro.go new file mode 100644 index 00000000000..1df6fe2f444 --- /dev/null +++ b/adapters/vastbidder/bidder_macro.go @@ -0,0 +1,1233 @@ +package vastbidder + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//BidderMacro default implementation +type BidderMacro struct { + IBidderMacro + + //Configuration Parameters + Conf *config.Adapter + + //OpenRTB Specific Parameters + Request *openrtb2.BidRequest + IsApp bool + HasGeo bool + Imp *openrtb2.Imp + Publisher *openrtb2.Publisher + Content *openrtb2.Content + + //Extensions + ImpBidderExt openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag + UserExt *openrtb_ext.ExtUser + RegsExt *openrtb_ext.ExtRegs + + //Impression level Request Headers + ImpReqHeaders http.Header +} + +//NewBidderMacro contains definition for all openrtb macro's +func NewBidderMacro() IBidderMacro { + obj := &BidderMacro{} + obj.IBidderMacro = obj + return obj +} + +func (tag *BidderMacro) init() { + if nil != tag.Request.App { + tag.IsApp = true + tag.Publisher = tag.Request.App.Publisher + tag.Content = tag.Request.App.Content + } else { + tag.Publisher = tag.Request.Site.Publisher + tag.Content = tag.Request.Site.Content + } + tag.HasGeo = nil != tag.Request.Device && nil != tag.Request.Device.Geo + + //Read User Extensions + if nil != tag.Request.User && nil != tag.Request.User.Ext { + var ext openrtb_ext.ExtUser + err := json.Unmarshal(tag.Request.User.Ext, &ext) + if nil == err { + tag.UserExt = &ext + } + } + + //Read Regs Extensions + if nil != tag.Request.Regs && nil != tag.Request.Regs.Ext { + var ext openrtb_ext.ExtRegs + err := json.Unmarshal(tag.Request.Regs.Ext, &ext) + if nil == err { + tag.RegsExt = &ext + } + } +} + +//InitBidRequest will initialise BidRequest +func (tag *BidderMacro) InitBidRequest(request *openrtb2.BidRequest) { + tag.Request = request + tag.init() +} + +//LoadImpression will set current imp +func (tag *BidderMacro) LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) { + tag.Imp = imp + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err + } + + tag.ImpBidderExt = openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &tag.ImpBidderExt); err != nil { + return nil, err + } + return &tag.ImpBidderExt, nil +} + +//LoadVASTTag will set current VAST Tag details in bidder keys +func (tag *BidderMacro) LoadVASTTag(vastTag *openrtb_ext.ExtImpVASTBidderTag) { + tag.VASTTag = vastTag +} + +//GetBidderKeys will set bidder level keys +func (tag *BidderMacro) GetBidderKeys() map[string]string { + var keys map[string]string + //Adding VAST Tag Bidder Parameters + keys = NormalizeJSON(tag.VASTTag.Params) + + //Adding VAST Tag Standard Params + keys["dur"] = strconv.Itoa(tag.VASTTag.Duration) + + //Adding Headers as Custom Macros + + //Adding Cookies as Custom Macros + + //Adding Default Empty for standard keys + for i := range ParamKeys { + if _, ok := keys[ParamKeys[i]]; !ok { + keys[ParamKeys[i]] = "" + } + } + return keys +} + +//SetAdapterConfig will set Adapter config +func (tag *BidderMacro) SetAdapterConfig(conf *config.Adapter) { + tag.Conf = conf +} + +//GetURI get URL +func (tag *BidderMacro) GetURI() string { + + //check for URI at impression level + if nil != tag.VASTTag { + return tag.VASTTag.URL + } + + //check for URI at config level + return tag.Conf.Endpoint +} + +//GetHeaders returns list of custom request headers +//Override this method if your Vast bidder needs custom request headers +func (tag *BidderMacro) GetHeaders() http.Header { + return http.Header{} +} + +/********************* Request *********************/ + +//MacroTest contains definition for Test Parameter +func (tag *BidderMacro) MacroTest(key string) string { + if tag.Request.Test > 0 { + return strconv.Itoa(int(tag.Request.Test)) + } + return "" +} + +//MacroTimeout contains definition for Timeout Parameter +func (tag *BidderMacro) MacroTimeout(key string) string { + if tag.Request.TMax > 0 { + return strconv.FormatInt(tag.Request.TMax, intBase) + } + return "" +} + +//MacroWhitelistSeat contains definition for WhitelistSeat Parameter +func (tag *BidderMacro) MacroWhitelistSeat(key string) string { + return strings.Join(tag.Request.WSeat, comma) +} + +//MacroWhitelistLang contains definition for WhitelistLang Parameter +func (tag *BidderMacro) MacroWhitelistLang(key string) string { + return strings.Join(tag.Request.WLang, comma) +} + +//MacroBlockedSeat contains definition for Blockedseat Parameter +func (tag *BidderMacro) MacroBlockedSeat(key string) string { + return strings.Join(tag.Request.BSeat, comma) +} + +//MacroCurrency contains definition for Currency Parameter +func (tag *BidderMacro) MacroCurrency(key string) string { + return strings.Join(tag.Request.Cur, comma) +} + +//MacroBlockedCategory contains definition for BlockedCategory Parameter +func (tag *BidderMacro) MacroBlockedCategory(key string) string { + return strings.Join(tag.Request.BCat, comma) +} + +//MacroBlockedAdvertiser contains definition for BlockedAdvertiser Parameter +func (tag *BidderMacro) MacroBlockedAdvertiser(key string) string { + return strings.Join(tag.Request.BAdv, comma) +} + +//MacroBlockedApp contains definition for BlockedApp Parameter +func (tag *BidderMacro) MacroBlockedApp(key string) string { + return strings.Join(tag.Request.BApp, comma) +} + +/********************* Source *********************/ + +//MacroFD contains definition for FD Parameter +func (tag *BidderMacro) MacroFD(key string) string { + if nil != tag.Request.Source { + return strconv.Itoa(int(tag.Request.Source.FD)) + } + return "" +} + +//MacroTransactionID contains definition for TransactionID Parameter +func (tag *BidderMacro) MacroTransactionID(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.TID + } + return "" +} + +//MacroPaymentIDChain contains definition for PaymentIDChain Parameter +func (tag *BidderMacro) MacroPaymentIDChain(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.PChain + } + return "" +} + +/********************* Regs *********************/ + +//MacroCoppa contains definition for Coppa Parameter +func (tag *BidderMacro) MacroCoppa(key string) string { + if nil != tag.Request.Regs { + return strconv.Itoa(int(tag.Request.Regs.COPPA)) + } + return "" +} + +/********************* Impression *********************/ + +//MacroDisplayManager contains definition for DisplayManager Parameter +func (tag *BidderMacro) MacroDisplayManager(key string) string { + return tag.Imp.DisplayManager +} + +//MacroDisplayManagerVersion contains definition for DisplayManagerVersion Parameter +func (tag *BidderMacro) MacroDisplayManagerVersion(key string) string { + return tag.Imp.DisplayManagerVer +} + +//MacroInterstitial contains definition for Interstitial Parameter +func (tag *BidderMacro) MacroInterstitial(key string) string { + if tag.Imp.Instl > 0 { + return strconv.Itoa(int(tag.Imp.Instl)) + } + return "" +} + +//MacroTagID contains definition for TagID Parameter +func (tag *BidderMacro) MacroTagID(key string) string { + return tag.Imp.TagID +} + +//MacroBidFloor contains definition for BidFloor Parameter +func (tag *BidderMacro) MacroBidFloor(key string) string { + if tag.Imp.BidFloor > 0 { + return fmt.Sprintf("%g", tag.Imp.BidFloor) + } + return "" +} + +//MacroBidFloorCurrency contains definition for BidFloorCurrency Parameter +func (tag *BidderMacro) MacroBidFloorCurrency(key string) string { + return tag.Imp.BidFloorCur +} + +//MacroSecure contains definition for Secure Parameter +func (tag *BidderMacro) MacroSecure(key string) string { + if nil != tag.Imp.Secure { + return strconv.Itoa(int(*tag.Imp.Secure)) + } + return "" +} + +//MacroPMP contains definition for PMP Parameter +func (tag *BidderMacro) MacroPMP(key string) string { + if nil != tag.Imp.PMP { + data, _ := json.Marshal(tag.Imp.PMP) + return string(data) + } + return "" +} + +/********************* Video *********************/ + +//MacroVideoMIMES contains definition for VideoMIMES Parameter +func (tag *BidderMacro) MacroVideoMIMES(key string) string { + if nil != tag.Imp.Video { + return strings.Join(tag.Imp.Video.MIMEs, comma) + } + return "" +} + +//MacroVideoMinimumDuration contains definition for VideoMinimumDuration Parameter +func (tag *BidderMacro) MacroVideoMinimumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MinDuration, intBase) + } + return "" +} + +//MacroVideoMaximumDuration contains definition for VideoMaximumDuration Parameter +func (tag *BidderMacro) MacroVideoMaximumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxDuration, intBase) + } + return "" +} + +//MacroVideoProtocols contains definition for VideoProtocols Parameter +func (tag *BidderMacro) MacroVideoProtocols(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Protocols + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPlayerWidth contains definition for VideoPlayerWidth Parameter +func (tag *BidderMacro) MacroVideoPlayerWidth(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.W > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.W), intBase) + } + return "" +} + +//MacroVideoPlayerHeight contains definition for VideoPlayerHeight Parameter +func (tag *BidderMacro) MacroVideoPlayerHeight(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.H > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.H), intBase) + } + return "" +} + +//MacroVideoStartDelay contains definition for VideoStartDelay Parameter +func (tag *BidderMacro) MacroVideoStartDelay(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.StartDelay { + return strconv.FormatInt(int64(*tag.Imp.Video.StartDelay), intBase) + } + return "" +} + +//MacroVideoPlacement contains definition for VideoPlacement Parameter +func (tag *BidderMacro) MacroVideoPlacement(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Placement > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Placement), intBase) + } + return "" +} + +//MacroVideoLinearity contains definition for VideoLinearity Parameter +func (tag *BidderMacro) MacroVideoLinearity(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Linearity > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Linearity), intBase) + } + return "" +} + +//MacroVideoSkip contains definition for VideoSkip Parameter +func (tag *BidderMacro) MacroVideoSkip(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Skip { + return strconv.FormatInt(int64(*tag.Imp.Video.Skip), intBase) + } + return "" +} + +//MacroVideoSkipMinimum contains definition for VideoSkipMinimum Parameter +func (tag *BidderMacro) MacroVideoSkipMinimum(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipMin > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipMin, intBase) + } + return "" +} + +//MacroVideoSkipAfter contains definition for VideoSkipAfter Parameter +func (tag *BidderMacro) MacroVideoSkipAfter(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipAfter > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipAfter, intBase) + } + return "" +} + +//MacroVideoSequence contains definition for VideoSequence Parameter +func (tag *BidderMacro) MacroVideoSequence(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Sequence > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Sequence), intBase) + } + return "" +} + +//MacroVideoBlockedAttribute contains definition for VideoBlockedAttribute Parameter +func (tag *BidderMacro) MacroVideoBlockedAttribute(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.BAttr + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoMaximumExtended contains definition for VideoMaximumExtended Parameter +func (tag *BidderMacro) MacroVideoMaximumExtended(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxExtended > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxExtended, intBase) + } + return "" +} + +//MacroVideoMinimumBitRate contains definition for VideoMinimumBitRate Parameter +func (tag *BidderMacro) MacroVideoMinimumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MinBitRate), intBase) + } + return "" +} + +//MacroVideoMaximumBitRate contains definition for VideoMaximumBitRate Parameter +func (tag *BidderMacro) MacroVideoMaximumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MaxBitRate), intBase) + } + return "" +} + +//MacroVideoBoxing contains definition for VideoBoxing Parameter +func (tag *BidderMacro) MacroVideoBoxing(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.BoxingAllowed > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.BoxingAllowed), intBase) + } + return "" +} + +//MacroVideoPlaybackMethod contains definition for VideoPlaybackMethod Parameter +func (tag *BidderMacro) MacroVideoPlaybackMethod(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.PlaybackMethod + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoDelivery contains definition for VideoDelivery Parameter +func (tag *BidderMacro) MacroVideoDelivery(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Delivery + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPosition contains definition for VideoPosition Parameter +func (tag *BidderMacro) MacroVideoPosition(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Pos { + return strconv.FormatInt(int64(*tag.Imp.Video.Pos), intBase) + } + return "" +} + +//MacroVideoAPI contains definition for VideoAPI Parameter +func (tag *BidderMacro) MacroVideoAPI(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.API + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +/********************* Site *********************/ + +//MacroSiteID contains definition for SiteID Parameter +func (tag *BidderMacro) MacroSiteID(key string) string { + if !tag.IsApp { + return tag.Request.Site.ID + } + return "" +} + +//MacroSiteName contains definition for SiteName Parameter +func (tag *BidderMacro) MacroSiteName(key string) string { + if !tag.IsApp { + return tag.Request.Site.Name + } + return "" +} + +//MacroSitePage contains definition for SitePage Parameter +func (tag *BidderMacro) MacroSitePage(key string) string { + if !tag.IsApp && nil != tag.Request && nil != tag.Request.Site { + return tag.Request.Site.Page + } + return "" +} + +//MacroSiteReferrer contains definition for SiteReferrer Parameter +func (tag *BidderMacro) MacroSiteReferrer(key string) string { + if !tag.IsApp { + return tag.Request.Site.Ref + } + return "" +} + +//MacroSiteSearch contains definition for SiteSearch Parameter +func (tag *BidderMacro) MacroSiteSearch(key string) string { + if !tag.IsApp { + return tag.Request.Site.Search + } + return "" +} + +//MacroSiteMobile contains definition for SiteMobile Parameter +func (tag *BidderMacro) MacroSiteMobile(key string) string { + if !tag.IsApp && tag.Request.Site.Mobile > 0 { + return strconv.FormatInt(int64(tag.Request.Site.Mobile), intBase) + } + return "" +} + +/********************* App *********************/ + +//MacroAppID contains definition for AppID Parameter +func (tag *BidderMacro) MacroAppID(key string) string { + if tag.IsApp { + return tag.Request.App.ID + } + return "" +} + +//MacroAppName contains definition for AppName Parameter +func (tag *BidderMacro) MacroAppName(key string) string { + if tag.IsApp { + return tag.Request.App.Name + } + return "" +} + +//MacroAppBundle contains definition for AppBundle Parameter +func (tag *BidderMacro) MacroAppBundle(key string) string { + if tag.IsApp { + return tag.Request.App.Bundle + } + return "" +} + +//MacroAppStoreURL contains definition for AppStoreURL Parameter +func (tag *BidderMacro) MacroAppStoreURL(key string) string { + if tag.IsApp { + return tag.Request.App.StoreURL + } + return "" +} + +//MacroAppVersion contains definition for AppVersion Parameter +func (tag *BidderMacro) MacroAppVersion(key string) string { + if tag.IsApp { + return tag.Request.App.Ver + } + return "" +} + +//MacroAppPaid contains definition for AppPaid Parameter +func (tag *BidderMacro) MacroAppPaid(key string) string { + if tag.IsApp && tag.Request.App.Paid != 0 { + return strconv.FormatInt(int64(tag.Request.App.Paid), intBase) + } + return "" +} + +/********************* Site/App Common *********************/ + +//MacroCategory contains definition for Category Parameter +func (tag *BidderMacro) MacroCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.Cat, comma) + } + return strings.Join(tag.Request.Site.Cat, comma) +} + +//MacroDomain contains definition for Domain Parameter +func (tag *BidderMacro) MacroDomain(key string) string { + if tag.IsApp { + return tag.Request.App.Domain + } + return tag.Request.Site.Domain +} + +//MacroSectionCategory contains definition for SectionCategory Parameter +func (tag *BidderMacro) MacroSectionCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.SectionCat, comma) + } + return strings.Join(tag.Request.Site.SectionCat, comma) +} + +//MacroPageCategory contains definition for PageCategory Parameter +func (tag *BidderMacro) MacroPageCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.PageCat, comma) + } + return strings.Join(tag.Request.Site.PageCat, comma) +} + +//MacroPrivacyPolicy contains definition for PrivacyPolicy Parameter +func (tag *BidderMacro) MacroPrivacyPolicy(key string) string { + var value int8 = 0 + if tag.IsApp { + value = tag.Request.App.PrivacyPolicy + } else { + value = tag.Request.Site.PrivacyPolicy + } + if value > 0 { + return strconv.FormatInt(int64(value), intBase) + } + return "" +} + +//MacroKeywords contains definition for Keywords Parameter +func (tag *BidderMacro) MacroKeywords(key string) string { + if tag.IsApp { + return tag.Request.App.Keywords + } + return tag.Request.Site.Keywords +} + +/********************* Publisher *********************/ + +//MacroPubID contains definition for PubID Parameter +func (tag *BidderMacro) MacroPubID(key string) string { + if nil != tag.Publisher { + return tag.Publisher.ID + } + return "" +} + +//MacroPubName contains definition for PubName Parameter +func (tag *BidderMacro) MacroPubName(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Name + } + return "" +} + +//MacroPubDomain contains definition for PubDomain Parameter +func (tag *BidderMacro) MacroPubDomain(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Domain + } + return "" +} + +/********************* Content *********************/ + +//MacroContentID contains definition for ContentID Parameter +func (tag *BidderMacro) MacroContentID(key string) string { + if nil != tag.Content { + return tag.Content.ID + } + return "" +} + +//MacroContentEpisode contains definition for ContentEpisode Parameter +func (tag *BidderMacro) MacroContentEpisode(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Episode), intBase) + } + return "" +} + +//MacroContentTitle contains definition for ContentTitle Parameter +func (tag *BidderMacro) MacroContentTitle(key string) string { + if nil != tag.Content { + return tag.Content.Title + } + return "" +} + +//MacroContentSeries contains definition for ContentSeries Parameter +func (tag *BidderMacro) MacroContentSeries(key string) string { + if nil != tag.Content { + return tag.Content.Series + } + return "" +} + +//MacroContentSeason contains definition for ContentSeason Parameter +func (tag *BidderMacro) MacroContentSeason(key string) string { + if nil != tag.Content { + return tag.Content.Season + } + return "" +} + +//MacroContentArtist contains definition for ContentArtist Parameter +func (tag *BidderMacro) MacroContentArtist(key string) string { + if nil != tag.Content { + return tag.Content.Artist + } + return "" +} + +//MacroContentGenre contains definition for ContentGenre Parameter +func (tag *BidderMacro) MacroContentGenre(key string) string { + if nil != tag.Content { + return tag.Content.Genre + } + return "" +} + +//MacroContentAlbum contains definition for ContentAlbum Parameter +func (tag *BidderMacro) MacroContentAlbum(key string) string { + if nil != tag.Content { + return tag.Content.Album + } + return "" +} + +//MacroContentISrc contains definition for ContentISrc Parameter +func (tag *BidderMacro) MacroContentISrc(key string) string { + if nil != tag.Content { + return tag.Content.ISRC + } + return "" +} + +//MacroContentURL contains definition for ContentURL Parameter +func (tag *BidderMacro) MacroContentURL(key string) string { + if nil != tag.Content { + return tag.Content.URL + } + return "" +} + +//MacroContentCategory contains definition for ContentCategory Parameter +func (tag *BidderMacro) MacroContentCategory(key string) string { + if nil != tag.Content { + return strings.Join(tag.Content.Cat, comma) + } + return "" +} + +//MacroContentProductionQuality contains definition for ContentProductionQuality Parameter +func (tag *BidderMacro) MacroContentProductionQuality(key string) string { + if nil != tag.Content && nil != tag.Content.ProdQ { + return strconv.FormatInt(int64(*tag.Content.ProdQ), intBase) + } + return "" +} + +//MacroContentVideoQuality contains definition for ContentVideoQuality Parameter +func (tag *BidderMacro) MacroContentVideoQuality(key string) string { + if nil != tag.Content && nil != tag.Content.VideoQuality { + return strconv.FormatInt(int64(*tag.Content.VideoQuality), intBase) + } + return "" +} + +//MacroContentContext contains definition for ContentContext Parameter +func (tag *BidderMacro) MacroContentContext(key string) string { + if nil != tag.Content && tag.Content.Context > 0 { + return strconv.FormatInt(int64(tag.Content.Context), intBase) + } + return "" +} + +//MacroContentContentRating contains definition for ContentContentRating Parameter +func (tag *BidderMacro) MacroContentContentRating(key string) string { + if nil != tag.Content { + return tag.Content.ContentRating + } + return "" +} + +//MacroContentUserRating contains definition for ContentUserRating Parameter +func (tag *BidderMacro) MacroContentUserRating(key string) string { + if nil != tag.Content { + return tag.Content.UserRating + } + return "" +} + +//MacroContentQAGMediaRating contains definition for ContentQAGMediaRating Parameter +func (tag *BidderMacro) MacroContentQAGMediaRating(key string) string { + if nil != tag.Content && tag.Content.QAGMediaRating > 0 { + return strconv.FormatInt(int64(tag.Content.QAGMediaRating), intBase) + } + return "" +} + +//MacroContentKeywords contains definition for ContentKeywords Parameter +func (tag *BidderMacro) MacroContentKeywords(key string) string { + if nil != tag.Content { + return tag.Content.Keywords + } + return "" +} + +//MacroContentLiveStream contains definition for ContentLiveStream Parameter +func (tag *BidderMacro) MacroContentLiveStream(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.LiveStream), intBase) + } + return "" +} + +//MacroContentSourceRelationship contains definition for ContentSourceRelationship Parameter +func (tag *BidderMacro) MacroContentSourceRelationship(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.SourceRelationship), intBase) + } + return "" +} + +//MacroContentLength contains definition for ContentLength Parameter +func (tag *BidderMacro) MacroContentLength(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Len), intBase) + } + return "" +} + +//MacroContentLanguage contains definition for ContentLanguage Parameter +func (tag *BidderMacro) MacroContentLanguage(key string) string { + if nil != tag.Content { + return tag.Content.Language + } + return "" +} + +//MacroContentEmbeddable contains definition for ContentEmbeddable Parameter +func (tag *BidderMacro) MacroContentEmbeddable(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Embeddable), intBase) + } + return "" +} + +/********************* Producer *********************/ + +//MacroProducerID contains definition for ProducerID Parameter +func (tag *BidderMacro) MacroProducerID(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.ID + } + return "" +} + +//MacroProducerName contains definition for ProducerName Parameter +func (tag *BidderMacro) MacroProducerName(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.Name + } + return "" +} + +/********************* Device *********************/ + +//MacroUserAgent contains definition for UserAgent Parameter +func (tag *BidderMacro) MacroUserAgent(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.UA + } + return "" +} + +//MacroDNT contains definition for DNT Parameter +func (tag *BidderMacro) MacroDNT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.DNT { + return strconv.FormatInt(int64(*tag.Request.Device.DNT), intBase) + } + return "" +} + +//MacroLMT contains definition for LMT Parameter +func (tag *BidderMacro) MacroLMT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.Lmt { + return strconv.FormatInt(int64(*tag.Request.Device.Lmt), intBase) + } + return "" +} + +//MacroIP contains definition for IP Parameter +func (tag *BidderMacro) MacroIP(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + if len(tag.Request.Device.IP) > 0 { + return tag.Request.Device.IP + } else if len(tag.Request.Device.IPv6) > 0 { + return tag.Request.Device.IPv6 + } + } + return "" +} + +//MacroDeviceType contains definition for DeviceType Parameter +func (tag *BidderMacro) MacroDeviceType(key string) string { + if nil != tag.Request.Device && tag.Request.Device.DeviceType > 0 { + return strconv.FormatInt(int64(tag.Request.Device.DeviceType), intBase) + } + return "" +} + +//MacroMake contains definition for Make Parameter +func (tag *BidderMacro) MacroMake(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Make + } + return "" +} + +//MacroModel contains definition for Model Parameter +func (tag *BidderMacro) MacroModel(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Model + } + return "" +} + +//MacroDeviceOS contains definition for DeviceOS Parameter +func (tag *BidderMacro) MacroDeviceOS(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OS + } + return "" +} + +//MacroDeviceOSVersion contains definition for DeviceOSVersion Parameter +func (tag *BidderMacro) MacroDeviceOSVersion(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OSV + } + return "" +} + +//MacroDeviceWidth contains definition for DeviceWidth Parameter +func (tag *BidderMacro) MacroDeviceWidth(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.W), intBase) + } + return "" +} + +//MacroDeviceHeight contains definition for DeviceHeight Parameter +func (tag *BidderMacro) MacroDeviceHeight(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.H), intBase) + } + return "" +} + +//MacroDeviceJS contains definition for DeviceJS Parameter +func (tag *BidderMacro) MacroDeviceJS(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.JS), intBase) + } + return "" +} + +//MacroDeviceLanguage contains definition for DeviceLanguage Parameter +func (tag *BidderMacro) MacroDeviceLanguage(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.Language + } + return "" +} + +//MacroDeviceIFA contains definition for DeviceIFA Parameter +func (tag *BidderMacro) MacroDeviceIFA(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.IFA + } + return "" +} + +//MacroDeviceDIDSHA1 contains definition for DeviceDIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDSHA1 + } + return "" +} + +//MacroDeviceDIDMD5 contains definition for DeviceDIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDMD5 + } + return "" +} + +//MacroDeviceDPIDSHA1 contains definition for DeviceDPIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDPIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDSHA1 + } + return "" +} + +//MacroDeviceDPIDMD5 contains definition for DeviceDPIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDPIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDMD5 + } + return "" +} + +//MacroDeviceMACSHA1 contains definition for DeviceMACSHA1 Parameter +func (tag *BidderMacro) MacroDeviceMACSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACSHA1 + } + return "" +} + +//MacroDeviceMACMD5 contains definition for DeviceMACMD5 Parameter +func (tag *BidderMacro) MacroDeviceMACMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACMD5 + } + return "" +} + +/********************* Geo *********************/ + +//MacroLatitude contains definition for Latitude Parameter +func (tag *BidderMacro) MacroLatitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lat) + } + return "" +} + +//MacroLongitude contains definition for Longitude Parameter +func (tag *BidderMacro) MacroLongitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lon) + } + return "" +} + +//MacroCountry contains definition for Country Parameter +func (tag *BidderMacro) MacroCountry(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Country + } + return "" +} + +//MacroRegion contains definition for Region Parameter +func (tag *BidderMacro) MacroRegion(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Region + } + return "" +} + +//MacroCity contains definition for City Parameter +func (tag *BidderMacro) MacroCity(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.City + } + return "" +} + +//MacroZip contains definition for Zip Parameter +func (tag *BidderMacro) MacroZip(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.ZIP + } + return "" +} + +//MacroUTCOffset contains definition for UTCOffset Parameter +func (tag *BidderMacro) MacroUTCOffset(key string) string { + if tag.HasGeo { + return strconv.FormatInt(tag.Request.Device.Geo.UTCOffset, intBase) + } + return "" +} + +/********************* User *********************/ + +//MacroUserID contains definition for UserID Parameter +func (tag *BidderMacro) MacroUserID(key string) string { + if nil != tag.Request.User { + return tag.Request.User.ID + } + return "" +} + +//MacroYearOfBirth contains definition for YearOfBirth Parameter +func (tag *BidderMacro) MacroYearOfBirth(key string) string { + if nil != tag.Request.User && tag.Request.User.Yob > 0 { + return strconv.FormatInt(tag.Request.User.Yob, intBase) + } + return "" +} + +//MacroGender contains definition for Gender Parameter +func (tag *BidderMacro) MacroGender(key string) string { + if nil != tag.Request.User { + return tag.Request.User.Gender + } + return "" +} + +/********************* Extension *********************/ + +//MacroGDPRConsent contains definition for GDPRConsent Parameter +func (tag *BidderMacro) MacroGDPRConsent(key string) string { + if nil != tag.UserExt { + return tag.UserExt.Consent + } + return "" +} + +//MacroGDPR contains definition for GDPR Parameter +func (tag *BidderMacro) MacroGDPR(key string) string { + if nil != tag.RegsExt && nil != tag.RegsExt.GDPR { + return strconv.FormatInt(int64(*tag.RegsExt.GDPR), intBase) + } + return "" +} + +//MacroUSPrivacy contains definition for USPrivacy Parameter +func (tag *BidderMacro) MacroUSPrivacy(key string) string { + if nil != tag.RegsExt { + return tag.RegsExt.USPrivacy + } + return "" +} + +/********************* Additional *********************/ + +//MacroCacheBuster contains definition for CacheBuster Parameter +func (tag *BidderMacro) MacroCacheBuster(key string) string { + //change implementation + return strconv.FormatInt(time.Now().UnixNano(), intBase) +} + +/********************* Request Headers *********************/ + +// setDefaultHeaders sets following default headers based on VAST protocol version +// X-device-IP; end users IP address, per VAST 4.x +// X-Forwarded-For; end users IP address, prior VAST versions +// X-Device-User-Agent; End users user agent, per VAST 4.x +// User-Agent; End users user agent, prior VAST versions +// X-Device-Referer; Referer value from the original request, per VAST 4.x +// X-device-Accept-Language, Accept-language value from the original request, per VAST 4.x +func setDefaultHeaders(tag *BidderMacro) { + // openrtb2. auction.go setDeviceImplicitly + // already populates OpenRTB bid request based on http request headers + // reusing the same information to set these headers via Macro* methods + headers := http.Header{} + ip := tag.IBidderMacro.MacroIP("") + userAgent := tag.IBidderMacro.MacroUserAgent("") + referer := tag.IBidderMacro.MacroSitePage("") + language := tag.IBidderMacro.MacroDeviceLanguage("") + + // 1 - vast 1 - 3 expected, 2 - vast 4 expected + expectedVastTags := 0 + if nil != tag.Imp && nil != tag.Imp.Video && nil != tag.Imp.Video.Protocols && len(tag.Imp.Video.Protocols) > 0 { + for _, protocol := range tag.Imp.Video.Protocols { + if protocol == openrtb2.ProtocolVAST40 || protocol == openrtb2.ProtocolVAST40Wrapper { + expectedVastTags |= 1 << 1 + } + if protocol <= openrtb2.ProtocolVAST30Wrapper { + expectedVastTags |= 1 << 0 + } + } + } else { + // not able to detect protocols. set all headers + expectedVastTags = 3 + } + + if expectedVastTags == 1 || expectedVastTags == 3 { + // vast prior to version 3 headers + setHeaders(headers, "X-Forwarded-For", ip) + setHeaders(headers, "User-Agent", userAgent) + } + + if expectedVastTags == 2 || expectedVastTags == 3 { + // vast 4 specific headers + setHeaders(headers, "X-device-Ip", ip) + setHeaders(headers, "X-Device-User-Agent", userAgent) + setHeaders(headers, "X-Device-Referer", referer) + setHeaders(headers, "X-Device-Accept-Language", language) + } + tag.ImpReqHeaders = headers +} + +func setHeaders(headers http.Header, key, value string) { + if "" != value { + headers.Set(key, value) + } +} + +//getAllHeaders combines default and custom headers and returns common list +//It internally calls GetHeaders() method for obtaining list of custom headers +func (tag *BidderMacro) getAllHeaders() http.Header { + setDefaultHeaders(tag) + customHeaders := tag.IBidderMacro.GetHeaders() + if nil != customHeaders { + for k, v := range customHeaders { + // custom header may contains default header key with value + // in such case custom value will be prefered + if nil != v && len(v) > 0 { + tag.ImpReqHeaders.Set(k, v[0]) + for i := 1; i < len(v); i++ { + tag.ImpReqHeaders.Add(k, v[i]) + } + } + } + } + return tag.ImpReqHeaders +} diff --git a/adapters/vastbidder/bidder_macro_test.go b/adapters/vastbidder/bidder_macro_test.go new file mode 100644 index 00000000000..0e6afa74cf4 --- /dev/null +++ b/adapters/vastbidder/bidder_macro_test.go @@ -0,0 +1,1258 @@ +package vastbidder + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" +) + +//TestSetDefaultHeaders verifies SetDefaultHeaders +func TestSetDefaultHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + } + type want struct { + headers http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "check all default headers", + args: args{req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }}, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "nil bid request", + args: args{req: nil}, + want: want{ + headers: http.Header{}, + }, + }, + { + name: "no headers set", + args: args{req: &openrtb2.BidRequest{}}, + want: want{ + headers: http.Header{}, + }, + }, { + name: "vast 4 protocol", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, { + name: "< vast 4", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST20, + openrtb2.ProtocolDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + }, + }, + }, { + name: "vast 4.0 and 4.0 wrapper", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolVAST40Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "vast 2.0 and 4.0", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + openrtb2.ProtocolVAST20Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := new(BidderMacro) + tag.IBidderMacro = tag + tag.IsApp = false + tag.Request = tt.args.req + if nil != tt.args.req && nil != tt.args.req.Imp && len(tt.args.req.Imp) > 0 { + tag.Imp = &tt.args.req.Imp[0] + } + setDefaultHeaders(tag) + assert.Equal(t, tt.want.headers, tag.ImpReqHeaders) + }) + } +} + +//TestGetAllHeaders verifies default and custom headers are returned +func TestGetAllHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + myBidder IBidderMacro + } + type want struct { + headers http.Header + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "Default and custom headers check", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "my-custom-header": "some-value", + }), + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"some-value"}, + }, + }, + }, + { + name: "override default header value", + args: args{ + req: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Page: "http://test.com/", // default header value + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "X-Device-Referer": "my-custom-value", + }), + }, + want: want{ + headers: http.Header{ + // http://test.com/ is not expected here as value + "X-Device-Referer": []string{"my-custom-value"}, + }, + }, + }, + { + name: "no custom headers", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(nil), // nil - no custom headers + }, + want: want{ + headers: http.Header{ // expect default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := tt.args.myBidder + tag.(*myVastBidderMacro).Request = tt.args.req + allHeaders := tag.getAllHeaders() + assert.Equal(t, tt.want.headers, allHeaders) + }) + } +} + +type myVastBidderMacro struct { + *BidderMacro + customHeaders map[string]string +} + +func newMyVastBidderMacro(customHeaders map[string]string) IBidderMacro { + obj := &myVastBidderMacro{ + BidderMacro: &BidderMacro{}, + customHeaders: customHeaders, + } + obj.IBidderMacro = obj + return obj +} + +func (tag *myVastBidderMacro) GetHeaders() http.Header { + if nil == tag.customHeaders { + return nil + } + h := http.Header{} + for k, v := range tag.customHeaders { + h.Set(k, v) + } + return h +} + +type testBidderMacro struct { + *BidderMacro +} + +func (tag *testBidderMacro) MacroCacheBuster(key string) string { + return `cachebuster` +} + +func newTestBidderMacro() IBidderMacro { + obj := &testBidderMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +func TestBidderMacro_MacroTest(t *testing.T) { + type args struct { + tag IBidderMacro + conf *config.Adapter + bidRequest *openrtb2.BidRequest + } + tests := []struct { + name string + args args + macros map[string]string + }{ + { + name: `App:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []openrtb2.Protocol{openrtb2.ProtocolVAST30, openrtb2.ProtocolVAST40Wrapper}, + Protocol: openrtb2.ProtocolVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(openrtb2.StartDelay), + Placement: openrtb2.VideoPlacementTypeInStream, + Linearity: openrtb2.VideoLinearityLinearInStream, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []openrtb2.CreativeAttribute{openrtb2.CreativeAttributeAudioAdAutoPlay, openrtb2.CreativeAttributeAudioAdUserInitiated}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []openrtb2.PlaybackMethod{openrtb2.PlaybackMethodPageLoadSoundOn, openrtb2.PlaybackMethodClickSoundOn}, + PlaybackEnd: openrtb2.PlaybackCessationModeVideoCompletionOrTerminatedByUser, + Delivery: []openrtb2.ContentDeliveryMethod{openrtb2.ContentDeliveryMethodStreaming, openrtb2.ContentDeliveryMethodDownload}, + Pos: new(openrtb2.AdPosition), + API: []openrtb2.APIFramework{openrtb2.APIFrameworkVPAID10, openrtb2.APIFrameworkVPAID20}, + }, + }, + }, + Site: &openrtb2.Site{ + ID: `site-id`, + Name: `site-name`, + Domain: `site-domain`, + Cat: []string{`site-cat1`, `site-cat2`}, + SectionCat: []string{`site-sec-cat1`, `site-sec-cat2`}, + PageCat: []string{`site-page-cat1`, `site-page-cat2`}, + Page: `site-page-url`, + Ref: `site-referer-url`, + Search: `site-search-keywords`, + Mobile: 1, + PrivacyPolicy: 2, + Keywords: `site-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `site-pub-id`, + Name: `site-pub-name`, + Domain: `site-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `site-cnt-id`, + Episode: 2, + Title: `site-cnt-title`, + Series: `site-cnt-series`, + Season: `site-cnt-season`, + Artist: `site-cnt-artist`, + Genre: `site-cnt-genre`, + Album: `site-cnt-album`, + ISRC: `site-cnt-isrc`, + URL: `site-cnt-url`, + Cat: []string{`site-cnt-cat1`, `site-cnt-cat2`}, + ProdQ: new(openrtb2.ProductionQuality), + VideoQuality: new(openrtb2.ProductionQuality), + Context: openrtb2.ContentContextVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: openrtb2.IQGMediaRatingAll, + Keywords: `site-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `site-cnt-prod-id`, + Name: `site-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IP: `ipv4`, + IPv6: `ipv6`, + DeviceType: openrtb2.DeviceTypeConnectedTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(openrtb2.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: `site-id`, + MacroSiteName: `site-name`, + MacroSitePage: `site-page-url`, + MacroSiteReferrer: `site-referer-url`, + MacroSiteSearch: `site-search-keywords`, + MacroSiteMobile: `1`, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: `site-cat1,site-cat2`, + MacroDomain: `site-domain`, + MacroSectionCategory: `site-sec-cat1,site-sec-cat2`, + MacroPageCategory: `site-page-cat1,site-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `site-keywords`, + MacroPubID: `site-pub-id`, + MacroPubName: `site-pub-name`, + MacroPubDomain: `site-pub-domain`, + MacroContentID: `site-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `site-cnt-title`, + MacroContentSeries: `site-cnt-series`, + MacroContentSeason: `site-cnt-season`, + MacroContentArtist: `site-cnt-artist`, + MacroContentGenre: `site-cnt-genre`, + MacroContentAlbum: `site-cnt-album`, + MacroContentISrc: `site-cnt-isrc`, + MacroContentURL: `site-cnt-url`, + MacroContentCategory: `site-cnt-cat1,site-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `site-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `site-cnt-prod-id`, + MacroProducerName: `site-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv4`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `App:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []openrtb2.Protocol{openrtb2.ProtocolVAST30, openrtb2.ProtocolVAST40Wrapper}, + Protocol: openrtb2.ProtocolVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(openrtb2.StartDelay), + Placement: openrtb2.VideoPlacementTypeInStream, + Linearity: openrtb2.VideoLinearityLinearInStream, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []openrtb2.CreativeAttribute{openrtb2.CreativeAttributeAudioAdAutoPlay, openrtb2.CreativeAttributeAudioAdUserInitiated}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []openrtb2.PlaybackMethod{openrtb2.PlaybackMethodPageLoadSoundOn, openrtb2.PlaybackMethodClickSoundOn}, + PlaybackEnd: openrtb2.PlaybackCessationModeVideoCompletionOrTerminatedByUser, + Delivery: []openrtb2.ContentDeliveryMethod{openrtb2.ContentDeliveryMethodStreaming, openrtb2.ContentDeliveryMethodDownload}, + Pos: new(openrtb2.AdPosition), + API: []openrtb2.APIFramework{openrtb2.APIFrameworkVPAID10, openrtb2.APIFrameworkVPAID20}, + }, + }, + }, + App: &openrtb2.App{ + ID: `app-id`, + Bundle: `app-bundle`, + StoreURL: `app-store-url`, + Ver: `app-version`, + Paid: 1, + Name: `app-name`, + Domain: `app-domain`, + Cat: []string{`app-cat1`, `app-cat2`}, + SectionCat: []string{`app-sec-cat1`, `app-sec-cat2`}, + PageCat: []string{`app-page-cat1`, `app-page-cat2`}, + PrivacyPolicy: 2, + Keywords: `app-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `app-pub-id`, + Name: `app-pub-name`, + Domain: `app-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `app-cnt-id`, + Episode: 2, + Title: `app-cnt-title`, + Series: `app-cnt-series`, + Season: `app-cnt-season`, + Artist: `app-cnt-artist`, + Genre: `app-cnt-genre`, + Album: `app-cnt-album`, + ISRC: `app-cnt-isrc`, + URL: `app-cnt-url`, + Cat: []string{`app-cnt-cat1`, `app-cnt-cat2`}, + ProdQ: new(openrtb2.ProductionQuality), + VideoQuality: new(openrtb2.ProductionQuality), + Context: openrtb2.ContentContextVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: openrtb2.IQGMediaRatingAll, + Keywords: `app-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `app-cnt-prod-id`, + Name: `app-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IPv6: `ipv6`, + DeviceType: openrtb2.DeviceTypeConnectedTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(openrtb2.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: `app-id`, + MacroAppName: `app-name`, + MacroAppBundle: `app-bundle`, + MacroAppStoreURL: `app-store-url`, + MacroAppVersion: `app-version`, + MacroAppPaid: `1`, + MacroCategory: `app-cat1,app-cat2`, + MacroDomain: `app-domain`, + MacroSectionCategory: `app-sec-cat1,app-sec-cat2`, + MacroPageCategory: `app-page-cat1,app-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `app-keywords`, + MacroPubID: `app-pub-id`, + MacroPubName: `app-pub-name`, + MacroPubDomain: `app-pub-domain`, + MacroContentID: `app-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `app-cnt-title`, + MacroContentSeries: `app-cnt-series`, + MacroContentSeason: `app-cnt-season`, + MacroContentArtist: `app-cnt-artist`, + MacroContentGenre: `app-cnt-genre`, + MacroContentAlbum: `app-cnt-album`, + MacroContentISrc: `app-cnt-isrc`, + MacroContentURL: `app-cnt-url`, + MacroContentCategory: `app-cnt-cat1,app-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `app-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `app-cnt-prod-id`, + MacroProducerName: `app-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv6`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + macroMappings := GetDefaultMapper() + + tag := tt.args.tag + tag.InitBidRequest(tt.args.bidRequest) + tag.SetAdapterConfig(tt.args.conf) + tag.LoadImpression(&tt.args.bidRequest.Imp[0]) + + for key, result := range tt.macros { + cb, ok := macroMappings[key] + if !ok { + assert.NotEmpty(t, result) + } else { + actual := cb.callback(tag, key) + assert.Equal(t, result, actual, fmt.Sprintf("MacroFunction: %v", key)) + } + } + }) + } +} diff --git a/adapters/vastbidder/constant.go b/adapters/vastbidder/constant.go new file mode 100644 index 00000000000..08b524d2bca --- /dev/null +++ b/adapters/vastbidder/constant.go @@ -0,0 +1,166 @@ +package vastbidder + +const ( + intBase = 10 + comma = `,` +) + +//List of Tag Bidder Macros +const ( + //Request + MacroTest = `test` + MacroTimeout = `timeout` + MacroWhitelistSeat = `wseat` + MacroWhitelistLang = `wlang` + MacroBlockedSeat = `bseat` + MacroCurrency = `cur` + MacroBlockedCategory = `bcat` + MacroBlockedAdvertiser = `badv` + MacroBlockedApp = `bapp` + + //Source + MacroFD = `fd` + MacroTransactionID = `tid` + MacroPaymentIDChain = `pchain` + + //Regs + MacroCoppa = `coppa` + + //Impression + MacroDisplayManager = `displaymanager` + MacroDisplayManagerVersion = `displaymanagerver` + MacroInterstitial = `instl` + MacroTagID = `tagid` + MacroBidFloor = `bidfloor` + MacroBidFloorCurrency = `bidfloorcur` + MacroSecure = `secure` + MacroPMP = `pmp` + + //Video + MacroVideoMIMES = `mimes` + MacroVideoMinimumDuration = `minduration` + MacroVideoMaximumDuration = `maxduration` + MacroVideoProtocols = `protocols` + MacroVideoPlayerWidth = `playerwidth` + MacroVideoPlayerHeight = `playerheight` + MacroVideoStartDelay = `startdelay` + MacroVideoPlacement = `placement` + MacroVideoLinearity = `linearity` + MacroVideoSkip = `skip` + MacroVideoSkipMinimum = `skipmin` + MacroVideoSkipAfter = `skipafter` + MacroVideoSequence = `sequence` + MacroVideoBlockedAttribute = `battr` + MacroVideoMaximumExtended = `maxextended` + MacroVideoMinimumBitRate = `minbitrate` + MacroVideoMaximumBitRate = `maxbitrate` + MacroVideoBoxing = `boxingallowed` + MacroVideoPlaybackMethod = `playbackmethod` + MacroVideoDelivery = `delivery` + MacroVideoPosition = `position` + MacroVideoAPI = `api` + + //Site + MacroSiteID = `siteid` + MacroSiteName = `sitename` + MacroSitePage = `page` + MacroSiteReferrer = `ref` + MacroSiteSearch = `search` + MacroSiteMobile = `mobile` + + //App + MacroAppID = `appid` + MacroAppName = `appname` + MacroAppBundle = `bundle` + MacroAppStoreURL = `storeurl` + MacroAppVersion = `appver` + MacroAppPaid = `paid` + + //SiteAppCommon + MacroCategory = `cat` + MacroDomain = `domain` + MacroSectionCategory = `sectioncat` + MacroPageCategory = `pagecat` + MacroPrivacyPolicy = `privacypolicy` + MacroKeywords = `keywords` + + //Publisher + MacroPubID = `pubid` + MacroPubName = `pubname` + MacroPubDomain = `pubdomain` + + //Content + MacroContentID = `contentid` + MacroContentEpisode = `episode` + MacroContentTitle = `title` + MacroContentSeries = `series` + MacroContentSeason = `season` + MacroContentArtist = `artist` + MacroContentGenre = `genre` + MacroContentAlbum = `album` + MacroContentISrc = `isrc` + MacroContentURL = `contenturl` + MacroContentCategory = `contentcat` + MacroContentProductionQuality = `contentprodq` + MacroContentVideoQuality = `contentvideoquality` + MacroContentContext = `context` + MacroContentContentRating = `contentrating` + MacroContentUserRating = `userrating` + MacroContentQAGMediaRating = `qagmediarating` + MacroContentKeywords = `contentkeywords` + MacroContentLiveStream = `livestream` + MacroContentSourceRelationship = `sourcerelationship` + MacroContentLength = `contentlen` + MacroContentLanguage = `contentlanguage` + MacroContentEmbeddable = `contentembeddable` + + //Producer + MacroProducerID = `prodid` + MacroProducerName = `prodname` + + //Device + MacroUserAgent = `useragent` + MacroDNT = `dnt` + MacroLMT = `lmt` + MacroIP = `ip` + MacroDeviceType = `devicetype` + MacroMake = `make` + MacroModel = `model` + MacroDeviceOS = `os` + MacroDeviceOSVersion = `osv` + MacroDeviceWidth = `devicewidth` + MacroDeviceHeight = `deviceheight` + MacroDeviceJS = `js` + MacroDeviceLanguage = `lang` + MacroDeviceIFA = `ifa` + MacroDeviceDIDSHA1 = `didsha1` + MacroDeviceDIDMD5 = `didmd5` + MacroDeviceDPIDSHA1 = `dpidsha1` + MacroDeviceDPIDMD5 = `dpidmd5` + MacroDeviceMACSHA1 = `macsha1` + MacroDeviceMACMD5 = `macmd5` + + //Geo + MacroLatitude = `lat` + MacroLongitude = `lon` + MacroCountry = `country` + MacroRegion = `region` + MacroCity = `city` + MacroZip = `zip` + MacroUTCOffset = `utcoffset` + + //User + MacroUserID = `uid` + MacroYearOfBirth = `yob` + MacroGender = `gender` + + //Extension + MacroGDPRConsent = `consent` + MacroGDPR = `gdpr` + MacroUSPrivacy = `usprivacy` + + //Additional + MacroCacheBuster = `cachebuster` +) + +var ParamKeys = []string{"param1", "param2", "param3", "param4", "param5"} diff --git a/adapters/vastbidder/ibidder_macro.go b/adapters/vastbidder/ibidder_macro.go new file mode 100644 index 00000000000..d5531b70413 --- /dev/null +++ b/adapters/vastbidder/ibidder_macro.go @@ -0,0 +1,199 @@ +package vastbidder + +import ( + "net/http" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//Flags of each tag bidder +type Flags struct { + RemoveEmptyParam bool `json:"remove_empty,omitempty"` +} + +//IBidderMacro interface will capture all macro definition +type IBidderMacro interface { + //Helper Function + InitBidRequest(request *openrtb2.BidRequest) + LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) + LoadVASTTag(tag *openrtb_ext.ExtImpVASTBidderTag) + GetBidderKeys() map[string]string + SetAdapterConfig(*config.Adapter) + GetURI() string + GetHeaders() http.Header + //getAllHeaders returns default and custom heades + getAllHeaders() http.Header + + //Request + MacroTest(string) string + MacroTimeout(string) string + MacroWhitelistSeat(string) string + MacroWhitelistLang(string) string + MacroBlockedSeat(string) string + MacroCurrency(string) string + MacroBlockedCategory(string) string + MacroBlockedAdvertiser(string) string + MacroBlockedApp(string) string + + //Source + MacroFD(string) string + MacroTransactionID(string) string + MacroPaymentIDChain(string) string + + //Regs + MacroCoppa(string) string + + //Impression + MacroDisplayManager(string) string + MacroDisplayManagerVersion(string) string + MacroInterstitial(string) string + MacroTagID(string) string + MacroBidFloor(string) string + MacroBidFloorCurrency(string) string + MacroSecure(string) string + MacroPMP(string) string + + //Video + MacroVideoMIMES(string) string + MacroVideoMinimumDuration(string) string + MacroVideoMaximumDuration(string) string + MacroVideoProtocols(string) string + MacroVideoPlayerWidth(string) string + MacroVideoPlayerHeight(string) string + MacroVideoStartDelay(string) string + MacroVideoPlacement(string) string + MacroVideoLinearity(string) string + MacroVideoSkip(string) string + MacroVideoSkipMinimum(string) string + MacroVideoSkipAfter(string) string + MacroVideoSequence(string) string + MacroVideoBlockedAttribute(string) string + MacroVideoMaximumExtended(string) string + MacroVideoMinimumBitRate(string) string + MacroVideoMaximumBitRate(string) string + MacroVideoBoxing(string) string + MacroVideoPlaybackMethod(string) string + MacroVideoDelivery(string) string + MacroVideoPosition(string) string + MacroVideoAPI(string) string + + //Site + MacroSiteID(string) string + MacroSiteName(string) string + MacroSitePage(string) string + MacroSiteReferrer(string) string + MacroSiteSearch(string) string + MacroSiteMobile(string) string + + //App + MacroAppID(string) string + MacroAppName(string) string + MacroAppBundle(string) string + MacroAppStoreURL(string) string + MacroAppVersion(string) string + MacroAppPaid(string) string + + //SiteAppCommon + MacroCategory(string) string + MacroDomain(string) string + MacroSectionCategory(string) string + MacroPageCategory(string) string + MacroPrivacyPolicy(string) string + MacroKeywords(string) string + + //Publisher + MacroPubID(string) string + MacroPubName(string) string + MacroPubDomain(string) string + + //Content + MacroContentID(string) string + MacroContentEpisode(string) string + MacroContentTitle(string) string + MacroContentSeries(string) string + MacroContentSeason(string) string + MacroContentArtist(string) string + MacroContentGenre(string) string + MacroContentAlbum(string) string + MacroContentISrc(string) string + MacroContentURL(string) string + MacroContentCategory(string) string + MacroContentProductionQuality(string) string + MacroContentVideoQuality(string) string + MacroContentContext(string) string + MacroContentContentRating(string) string + MacroContentUserRating(string) string + MacroContentQAGMediaRating(string) string + MacroContentKeywords(string) string + MacroContentLiveStream(string) string + MacroContentSourceRelationship(string) string + MacroContentLength(string) string + MacroContentLanguage(string) string + MacroContentEmbeddable(string) string + + //Producer + MacroProducerID(string) string + MacroProducerName(string) string + + //Device + MacroUserAgent(string) string + MacroDNT(string) string + MacroLMT(string) string + MacroIP(string) string + MacroDeviceType(string) string + MacroMake(string) string + MacroModel(string) string + MacroDeviceOS(string) string + MacroDeviceOSVersion(string) string + MacroDeviceWidth(string) string + MacroDeviceHeight(string) string + MacroDeviceJS(string) string + MacroDeviceLanguage(string) string + MacroDeviceIFA(string) string + MacroDeviceDIDSHA1(string) string + MacroDeviceDIDMD5(string) string + MacroDeviceDPIDSHA1(string) string + MacroDeviceDPIDMD5(string) string + MacroDeviceMACSHA1(string) string + MacroDeviceMACMD5(string) string + + //Geo + MacroLatitude(string) string + MacroLongitude(string) string + MacroCountry(string) string + MacroRegion(string) string + MacroCity(string) string + MacroZip(string) string + MacroUTCOffset(string) string + + //User + MacroUserID(string) string + MacroYearOfBirth(string) string + MacroGender(string) string + + //Extension + MacroGDPRConsent(string) string + MacroGDPR(string) string + MacroUSPrivacy(string) string + + //Additional + MacroCacheBuster(string) string +} + +var bidderMacroMap = map[openrtb_ext.BidderName]func() IBidderMacro{} + +//RegisterNewBidderMacro will be used by each bidder to set its respective macro IBidderMacro +func RegisterNewBidderMacro(bidder openrtb_ext.BidderName, macro func() IBidderMacro) { + bidderMacroMap[bidder] = macro +} + +//GetNewBidderMacro will return IBidderMacro of specific bidder +func GetNewBidderMacro(bidder openrtb_ext.BidderName) IBidderMacro { + callback, ok := bidderMacroMap[bidder] + if ok { + return callback() + } + return NewBidderMacro() +} diff --git a/adapters/vastbidder/itag_response_handler.go b/adapters/vastbidder/itag_response_handler.go new file mode 100644 index 00000000000..fd6d8b3357a --- /dev/null +++ b/adapters/vastbidder/itag_response_handler.go @@ -0,0 +1,43 @@ +package vastbidder + +import ( + "errors" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" +) + +//ITagRequestHandler parse bidder request +type ITagRequestHandler interface { + MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) +} + +//ITagResponseHandler parse bidder response +type ITagResponseHandler interface { + Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error + MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) +} + +//HandlerType list of tag based response handlers +type HandlerType string + +const ( + VASTTagHandlerType HandlerType = `vasttag` +) + +//GetResponseHandler returns response handler +func GetResponseHandler(responseType HandlerType) (ITagResponseHandler, error) { + switch responseType { + case VASTTagHandlerType: + return NewVASTTagResponseHandler(), nil + } + return nil, errors.New(`Unkown Response Handler`) +} + +func GetRequestHandler(responseType HandlerType) (ITagRequestHandler, error) { + switch responseType { + case VASTTagHandlerType: + return nil, nil + } + return nil, errors.New(`Unkown Response Handler`) +} diff --git a/adapters/vastbidder/macro_processor.go b/adapters/vastbidder/macro_processor.go new file mode 100644 index 00000000000..47e15d5178a --- /dev/null +++ b/adapters/vastbidder/macro_processor.go @@ -0,0 +1,216 @@ +package vastbidder + +import ( + "bytes" + "net/url" + "strings" + + "github.com/golang/glog" +) + +const ( + macroPrefix string = `{` //macro prefix can not be empty + macroSuffix string = `}` //macro suffix can not be empty + macroEscapeSuffix string = `_ESC` + macroPrefixLen int = len(macroPrefix) + macroSuffixLen int = len(macroSuffix) + macroEscapeSuffixLen int = len(macroEscapeSuffix) +) + +//Flags to customize macro processing wrappers + +//MacroProcessor struct to hold openrtb request and cache values +type MacroProcessor struct { + bidderMacro IBidderMacro + mapper Mapper + macroCache map[string]string + bidderKeys map[string]string +} + +//NewMacroProcessor will process macro's of openrtb bid request +func NewMacroProcessor(bidderMacro IBidderMacro, mapper Mapper) *MacroProcessor { + return &MacroProcessor{ + bidderMacro: bidderMacro, + mapper: mapper, + macroCache: make(map[string]string), + } +} + +//SetMacro Adding Custom Macro Manually +func (mp *MacroProcessor) SetMacro(key, value string) { + mp.macroCache[key] = value +} + +//SetBidderKeys will flush and set bidder specific keys +func (mp *MacroProcessor) SetBidderKeys(keys map[string]string) { + mp.bidderKeys = keys +} + +//processKey : returns value of key macro and status found or not +func (mp *MacroProcessor) processKey(key string) (string, bool) { + var valueCallback *macroCallBack + var value string + nEscaping := 0 + tmpKey := key + found := false + + for { + //Search in macro cache + if value, found = mp.macroCache[tmpKey]; found { + break + } + + //Search for bidder keys + if nil != mp.bidderKeys { + if value, found = mp.bidderKeys[tmpKey]; found { + break + } + } + + valueCallback, found = mp.mapper[tmpKey] + if found { + //found callback function + value = valueCallback.callback(mp.bidderMacro, tmpKey) + break + } else if strings.HasSuffix(tmpKey, macroEscapeSuffix) { + //escaping macro found + tmpKey = tmpKey[0 : len(tmpKey)-macroEscapeSuffixLen] + nEscaping++ + continue + } + break + } + + if found { + if len(value) > 0 { + if nEscaping > 0 { + //escaping string nEscaping times + value = escape(value, nEscaping) + } + if nil != valueCallback && valueCallback.cached { + //cached value if its cached flag is true + mp.macroCache[key] = value + } + } + } + + return value, found +} + +//ProcessString : Substitute macros in input string +func (mp *MacroProcessor) ProcessString(in string) (response string) { + var out bytes.Buffer + pos, start, end, size := 0, 0, 0, len(in) + + for pos < size { + //find macro prefix index + if start = strings.Index(in[pos:], macroPrefix); -1 == start { + //[prefix_not_found] append remaining string to response + out.WriteString(in[pos:]) + + //macro prefix not found + break + } + + //prefix index w.r.t original string + start = start + pos + + //append non macro prefix content + out.WriteString(in[pos:start]) + + if (end - macroSuffixLen) <= (start + macroPrefixLen) { + //string contains {{TEXT_{{MACRO}} -> it should replace it with{{TEXT_MACROVALUE + //find macro suffix index + if end = strings.Index(in[start+macroPrefixLen:], macroSuffix); -1 == end { + //[suffix_not_found] append remaining string to response + out.WriteString(in[start:]) + + // We Found First %% and Not Found Second %% But We are in between of string + break + } + + end = start + macroPrefixLen + end + macroSuffixLen + } + + //get actual macro key by removing macroPrefix and macroSuffix from key itself + key := in[start+macroPrefixLen : end-macroSuffixLen] + + //process macro + value, found := mp.processKey(key) + if found { + out.WriteString(value) + pos = end + } else { + out.WriteByte(macroPrefix[0]) + pos = start + 1 + } + //glog.Infof("\nSearch[%d] : [%d,%d,%s]", count, start, end, key) + } + response = out.String() + glog.V(3).Infof("[MACRO]:in:[%s] replaced:[%s]", in, response) + return +} + +//ProcessURL : Substitute macros in input string +func (mp *MacroProcessor) ProcessURL(uri string, flags Flags) (response string) { + if !flags.RemoveEmptyParam { + return mp.ProcessString(uri) + } + + murl, _ := url.Parse(uri) + + murl.Path = mp.ProcessString(murl.Path) + murl.RawQuery = mp.processURLValues(murl.Query(), flags) + murl.Fragment = mp.ProcessString(murl.Fragment) + + response = murl.String() + + glog.V(3).Infof("[MACRO]:in:[%s] replaced:[%s]", uri, response) + return +} + +//processURLValues : returns replaced macro values of url.values +func (mp *MacroProcessor) processURLValues(values url.Values, flags Flags) (response string) { + var out bytes.Buffer + for k, v := range values { + macroKey := v[0] + found := false + value := "" + + if len(macroKey) > (macroPrefixLen+macroSuffixLen) && + strings.HasPrefix(macroKey, macroPrefix) && + strings.HasSuffix(macroKey, macroSuffix) { + //Check macro key directly if present + newKey := macroKey[macroPrefixLen : len(macroKey)-macroSuffixLen] + value, found = mp.processKey(newKey) + } + + if !found { + //if key is not present then process it as normal string + value = mp.ProcessString(macroKey) + } + + if flags.RemoveEmptyParam == false || len(value) > 0 { + //append + if out.Len() > 0 { + out.WriteByte('&') + } + out.WriteString(k) + out.WriteByte('=') + out.WriteString(url.QueryEscape(value)) + } + } + return out.String() +} + +//GetMacroKey will return macro formatted key +func GetMacroKey(key string) string { + return macroPrefix + key + macroSuffix +} + +func escape(str string, n int) string { + for ; n > 0; n-- { + str = url.QueryEscape(str) + } + return str[:] +} diff --git a/adapters/vastbidder/macro_processor_test.go b/adapters/vastbidder/macro_processor_test.go new file mode 100644 index 00000000000..2b2b513df2c --- /dev/null +++ b/adapters/vastbidder/macro_processor_test.go @@ -0,0 +1,587 @@ +package vastbidder + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/stretchr/testify/assert" +) + +func getBidRequest(requestJSON string) *openrtb2.BidRequest { + bidRequest := &openrtb2.BidRequest{} + json.Unmarshal([]byte(requestJSON), bidRequest) + return bidRequest +} +func TestMacroProcessor_ProcessString(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pubID`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + + type fields struct { + bidRequest *openrtb2.BidRequest + } + tests := []struct { + name string + in string + expected string + }{ + { + name: "Empty Input", + in: "", + expected: "", + }, + { + name: "No Macro Replacement", + in: "Hello Test No Macro", + expected: "Hello Test No Macro", + }, + { + name: "Start Macro", + in: GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "End Macro", + in: "HELLO" + GetMacroKey(MacroTagID), + expected: "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "Start-End Macro", + in: GetMacroKey(MacroTagID) + "HELLO" + GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID] + "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "Half Start Macro", + in: macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Half End Macro", + in: "HELLO" + GetMacroKey(MacroTagID) + macroSuffix, + expected: "HELLO" + testMacroValues[MacroTagID] + macroSuffix, + }, + { + name: "Concatenated Macro", + in: GetMacroKey(MacroTagID) + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Incomplete Concatenation Macro", + in: GetMacroKey(MacroTagID) + macroSuffix + "LINKHELLO", + expected: testMacroValues[MacroTagID] + macroSuffix + "LINKHELLO", + }, + { + name: "Concatenation with Suffix Macro", + in: GetMacroKey(MacroTagID) + macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "Unknown Macro", + in: GetMacroKey(`UNKNOWN`) + `ABC`, + expected: GetMacroKey(`UNKNOWN`) + `ABC`, + }, + { + name: "Incomplete macro suffix", + in: "START" + macroSuffix, + expected: "START" + macroSuffix, + }, + { + name: "Incomplete Start and End", + in: string(macroPrefix[0]) + GetMacroKey(MacroTagID) + " Value " + GetMacroKey(MacroTagID) + string(macroSuffix[0]), + expected: string(macroPrefix[0]) + testMacroValues[MacroTagID] + " Value " + testMacroValues[MacroTagID] + string(macroSuffix[0]), + }, + { + name: "Special Character", + in: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + GetMacroKey(MacroTagID) + "\" Data", + expected: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + testMacroValues[MacroTagID] + "\" Data", + }, + { + name: "Empty Value", + in: GetMacroKey(MacroTimeout) + "Hello", + expected: "Hello", + }, + { + name: "EscapingMacræo", + in: GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID], + }, + { + name: "SingleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix], + }, + { + name: "DoubleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + }, + + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //Init Bidder Macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + gotResponse := mp.ProcessString(tt.in) + assert.Equal(t, tt.expected, gotResponse) + }) + } +} + +func TestMacroProcessor_processKey(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pub id`, + MacroPubID + macroEscapeSuffix: `pub+id`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + type args struct { + cache map[string]string + key string + } + type want struct { + expected string + ok bool + cache map[string]string + } + tests := []struct { + name string + args args + want want + }{ + { + name: `emptyKey`, + args: args{}, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKeyFound`, + args: args{ + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `valueFound`, + args: args{ + key: MacroTagID, + }, + want: want{ + expected: testMacroValues[MacroTagID], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `2TimesEscaping`, + args: args{ + key: MacroTagID + macroEscapeSuffix + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresent`, + args: args{ + key: `Unknown`, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresentInEscaping`, + args: args{ + key: `Unknown` + macroEscapeSuffix, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKey`, + args: args{ + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `cachedEscapingKey`, + args: args{ + key: MacroPubID + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroPubID+macroEscapeSuffix], + ok: true, + cache: map[string]string{ + MacroPubID + macroEscapeSuffix: testMacroValues[MacroPubID+macroEscapeSuffix], + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + //init cache of macro processor + if nil != tt.args.cache { + mp.macroCache = tt.args.cache + } + + actual, ok := mp.processKey(tt.args.key) + assert.Equal(t, tt.want.expected, actual) + assert.Equal(t, tt.want.ok, ok) + assert.Equal(t, tt.want.cache, mp.macroCache) + }) + } +} + +func TestMacroProcessor_processURLValues(t *testing.T) { + testMacroValues := map[string]string{ + MacroPubID: `pub id`, + MacroPubID + macroEscapeSuffix: `pub+id`, + MacroTagID: `tagid value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroValues[MacroPubID], + }, + }, + } + type args struct { + values url.Values + flags Flags + } + tests := []struct { + name string + args args + want url.Values + }{ + { + name: `AllEmptyParamsRemovedEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubName)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroPubName)}, + }, + flags: Flags{ + RemoveEmptyParam: true, + }, + }, + want: url.Values{}, + }, + { + name: `AllEmptyParamsKeepEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubName)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroPubName)}, + }, + flags: Flags{ + RemoveEmptyParam: false, + }, + }, + want: url.Values{ + `k1`: []string{""}, + `k2`: []string{""}, + `k3`: []string{""}, + }, + }, + { + name: `MixedParamsRemoveEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubID)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroTagID)}, + }, + flags: Flags{ + RemoveEmptyParam: true, + }, + }, + want: url.Values{ + `k1`: []string{testMacroValues[MacroPubID]}, + `k3`: []string{testMacroValues[MacroTagID]}, + }, + }, + { + name: `MixedParamsKeepEmptyParams`, + args: args{ + values: url.Values{ + `k1`: []string{GetMacroKey(MacroPubID)}, + `k2`: []string{GetMacroKey(MacroPubName)}, + `k3`: []string{GetMacroKey(MacroTagID)}, + `k4`: []string{`UNKNOWN`}, + `k5`: []string{GetMacroKey(`UNKNOWN`)}, + }, + flags: Flags{ + RemoveEmptyParam: false, + }, + }, + want: url.Values{ + `k1`: []string{testMacroValues[MacroPubID]}, + `k2`: []string{""}, + `k3`: []string{testMacroValues[MacroTagID]}, + `k4`: []string{`UNKNOWN`}, + `k5`: []string{GetMacroKey(`UNKNOWN`)}, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + actual := mp.processURLValues(tt.args.values, tt.args.flags) + + actualValues, _ := url.ParseQuery(actual) + assert.Equal(t, tt.want, actualValues) + }) + } +} + +func TestMacroProcessor_processURLValuesEscapingKeys(t *testing.T) { + testMacroImpValues := map[string]string{ + MacroPubID: `pub id`, + MacroTagID: `tagid value`, + } + + testMacroValues := map[string]string{ + MacroPubID: `pub+id`, + MacroTagID: `tagid+value`, + MacroTagID + macroEscapeSuffix: `tagid%2Bvalue`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%252Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroImpValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: testMacroImpValues[MacroPubID], + }, + }, + } + type args struct { + key string + value string + } + tests := []struct { + name string + args args + want string + }{ + { + name: `EmptyKeyValue`, + args: args{}, + want: ``, + }, + { + name: `WithoutEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID)}, + want: `k1=` + testMacroValues[MacroTagID], + }, + { + name: `WithEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix)}, + want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix], + }, + { + name: `With2LevelEscaping`, + args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix)}, + want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + values := url.Values{} + if len(tt.args.key) > 0 { + values.Add(tt.args.key, tt.args.value) + } + + actual := mp.processURLValues(values, Flags{}) + assert.Equal(t, tt.want, actual) + }) + } +} + +func TestMacroProcessor_ProcessURL(t *testing.T) { + testMacroImpValues := map[string]string{ + MacroPubID: `123`, + MacroSiteID: `567`, + MacroTagID: `tagid value`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: testMacroImpValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + ID: testMacroImpValues[MacroSiteID], + Publisher: &openrtb2.Publisher{ + ID: testMacroImpValues[MacroPubID], + }, + }, + } + + type args struct { + uri string + flags Flags + } + tests := []struct { + name string + args args + wantResponse string + }{ + { + name: "EmptyURI", + args: args{ + uri: ``, + flags: Flags{RemoveEmptyParam: true}, + }, + wantResponse: ``, + }, + { + name: "RemovedEmptyParams1", + args: args{ + uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, + flags: Flags{RemoveEmptyParam: true}, + }, + wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value&k1=v1&k2=v2`, + }, + { + name: "RemovedEmptyParams2", + args: args{ + uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID+macroEscapeSuffix) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, + flags: Flags{RemoveEmptyParam: false}, + }, + wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value¬found=&k1=v1&k2=v2`, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + gotResponse := mp.ProcessURL(tt.args.uri, tt.args.flags) + assertURL(t, tt.wantResponse, gotResponse) + }) + } +} + +func assertURL(t *testing.T, expected, actual string) { + actualURL, _ := url.Parse(actual) + expectedURL, _ := url.Parse(expected) + + if nil == actualURL || nil == expectedURL { + assert.True(t, (nil == actualURL) == (nil == expectedURL), `actual or expected url parsing failed`) + } else { + assert.Equal(t, expectedURL.Scheme, actualURL.Scheme) + assert.Equal(t, expectedURL.Opaque, actualURL.Opaque) + assert.Equal(t, expectedURL.User, actualURL.User) + assert.Equal(t, expectedURL.Host, actualURL.Host) + assert.Equal(t, expectedURL.Path, actualURL.Path) + assert.Equal(t, expectedURL.RawPath, actualURL.RawPath) + assert.Equal(t, expectedURL.ForceQuery, actualURL.ForceQuery) + assert.Equal(t, expectedURL.Query(), actualURL.Query()) + assert.Equal(t, expectedURL.Fragment, actualURL.Fragment) + } +} diff --git a/adapters/vastbidder/mapper.go b/adapters/vastbidder/mapper.go new file mode 100644 index 00000000000..cbbfe34c119 --- /dev/null +++ b/adapters/vastbidder/mapper.go @@ -0,0 +1,180 @@ +package vastbidder + +type macroCallBack struct { + cached bool + callback func(IBidderMacro, string) string +} + +//Mapper will map macro with its respective call back function +type Mapper map[string]*macroCallBack + +func (obj Mapper) clone() Mapper { + cloned := make(Mapper, len(obj)) + for k, v := range obj { + newCallback := *v + cloned[k] = &newCallback + } + return cloned +} + +var _defaultMapper = Mapper{ + //Request + MacroTest: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTest}, + MacroTimeout: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTimeout}, + MacroWhitelistSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistSeat}, + MacroWhitelistLang: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistLang}, + MacroBlockedSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedSeat}, + MacroCurrency: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCurrency}, + MacroBlockedCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedCategory}, + MacroBlockedAdvertiser: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedAdvertiser}, + MacroBlockedApp: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedApp}, + + //Source + MacroFD: ¯oCallBack{cached: true, callback: IBidderMacro.MacroFD}, + MacroTransactionID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTransactionID}, + MacroPaymentIDChain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPaymentIDChain}, + + //Regs + MacroCoppa: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCoppa}, + + //Impression + MacroDisplayManager: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManager}, + MacroDisplayManagerVersion: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManagerVersion}, + MacroInterstitial: ¯oCallBack{cached: false, callback: IBidderMacro.MacroInterstitial}, + MacroTagID: ¯oCallBack{cached: false, callback: IBidderMacro.MacroTagID}, + MacroBidFloor: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloor}, + MacroBidFloorCurrency: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloorCurrency}, + MacroSecure: ¯oCallBack{cached: false, callback: IBidderMacro.MacroSecure}, + MacroPMP: ¯oCallBack{cached: false, callback: IBidderMacro.MacroPMP}, + + //Video + MacroVideoMIMES: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMIMES}, + MacroVideoMinimumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumDuration}, + MacroVideoMaximumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumDuration}, + MacroVideoProtocols: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoProtocols}, + MacroVideoPlayerWidth: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerWidth}, + MacroVideoPlayerHeight: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerHeight}, + MacroVideoStartDelay: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoStartDelay}, + MacroVideoPlacement: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlacement}, + MacroVideoLinearity: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoLinearity}, + MacroVideoSkip: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkip}, + MacroVideoSkipMinimum: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipMinimum}, + MacroVideoSkipAfter: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipAfter}, + MacroVideoSequence: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSequence}, + MacroVideoBlockedAttribute: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBlockedAttribute}, + MacroVideoMaximumExtended: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumExtended}, + MacroVideoMinimumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumBitRate}, + MacroVideoMaximumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumBitRate}, + MacroVideoBoxing: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBoxing}, + MacroVideoPlaybackMethod: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlaybackMethod}, + MacroVideoDelivery: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoDelivery}, + MacroVideoPosition: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPosition}, + MacroVideoAPI: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoAPI}, + + //Site + MacroSiteID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteID}, + MacroSiteName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteName}, + MacroSitePage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSitePage}, + MacroSiteReferrer: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteReferrer}, + MacroSiteSearch: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteSearch}, + MacroSiteMobile: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteMobile}, + + //App + MacroAppID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppID}, + MacroAppName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppName}, + MacroAppBundle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppBundle}, + MacroAppStoreURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppStoreURL}, + MacroAppVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppVersion}, + MacroAppPaid: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppPaid}, + + //SiteAppCommon + MacroCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCategory}, + MacroDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDomain}, + MacroSectionCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSectionCategory}, + MacroPageCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPageCategory}, + MacroPrivacyPolicy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPrivacyPolicy}, + MacroKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroKeywords}, + + //Publisher + MacroPubID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubID}, + MacroPubName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubName}, + MacroPubDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubDomain}, + + //Content + MacroContentID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentID}, + MacroContentEpisode: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEpisode}, + MacroContentTitle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentTitle}, + MacroContentSeries: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeries}, + MacroContentSeason: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeason}, + MacroContentArtist: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentArtist}, + MacroContentGenre: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentGenre}, + MacroContentAlbum: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentAlbum}, + MacroContentISrc: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentISrc}, + MacroContentURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentURL}, + MacroContentCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentCategory}, + MacroContentProductionQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentProductionQuality}, + MacroContentVideoQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentVideoQuality}, + MacroContentContext: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContext}, + MacroContentContentRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContentRating}, + MacroContentUserRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentUserRating}, + MacroContentQAGMediaRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentQAGMediaRating}, + MacroContentKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentKeywords}, + MacroContentLiveStream: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLiveStream}, + MacroContentSourceRelationship: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSourceRelationship}, + MacroContentLength: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLength}, + MacroContentLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLanguage}, + MacroContentEmbeddable: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEmbeddable}, + + //Producer + MacroProducerID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerID}, + MacroProducerName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerName}, + + //Device + MacroUserAgent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserAgent}, + MacroDNT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDNT}, + MacroLMT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLMT}, + MacroIP: ¯oCallBack{cached: true, callback: IBidderMacro.MacroIP}, + MacroDeviceType: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceType}, + MacroMake: ¯oCallBack{cached: true, callback: IBidderMacro.MacroMake}, + MacroModel: ¯oCallBack{cached: true, callback: IBidderMacro.MacroModel}, + MacroDeviceOS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOS}, + MacroDeviceOSVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOSVersion}, + MacroDeviceWidth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceWidth}, + MacroDeviceHeight: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceHeight}, + MacroDeviceJS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceJS}, + MacroDeviceLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceLanguage}, + MacroDeviceIFA: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceIFA}, + MacroDeviceDIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDSHA1}, + MacroDeviceDIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDMD5}, + MacroDeviceDPIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDSHA1}, + MacroDeviceDPIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDMD5}, + MacroDeviceMACSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACSHA1}, + MacroDeviceMACMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACMD5}, + + //Geo + MacroLatitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLatitude}, + MacroLongitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLongitude}, + MacroCountry: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCountry}, + MacroRegion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroRegion}, + MacroCity: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCity}, + MacroZip: ¯oCallBack{cached: true, callback: IBidderMacro.MacroZip}, + MacroUTCOffset: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUTCOffset}, + + //User + MacroUserID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserID}, + MacroYearOfBirth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroYearOfBirth}, + MacroGender: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGender}, + + //Extension + MacroGDPRConsent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPRConsent}, + MacroGDPR: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPR}, + MacroUSPrivacy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUSPrivacy}, + + //Additional + MacroCacheBuster: ¯oCallBack{cached: false, callback: IBidderMacro.MacroCacheBuster}, +} + +//GetDefaultMapper will return clone of default Mapper function +func GetDefaultMapper() Mapper { + return _defaultMapper.clone() +} diff --git a/adapters/vastbidder/sample_spotx_macro.go.bak b/adapters/vastbidder/sample_spotx_macro.go.bak new file mode 100644 index 00000000000..8f3aafbdcc7 --- /dev/null +++ b/adapters/vastbidder/sample_spotx_macro.go.bak @@ -0,0 +1,28 @@ +package vastbidder + +import ( + "github.com/prebid/prebid-server/openrtb_ext" +) + +//SpotxMacro default implementation +type SpotxMacro struct { + *BidderMacro +} + +//NewSpotxMacro contains definition for all openrtb macro's +func NewSpotxMacro() IBidderMacro { + obj := &SpotxMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +//GetBidderKeys will set bidder level keys +func (tag *SpotxMacro) GetBidderKeys() map[string]string { + return NormalizeJSON(tag.ImpBidderExt) +} + +func init() { + RegisterNewBidderMacro(openrtb_ext.BidderSpotX, NewSpotxMacro) +} diff --git a/adapters/vastbidder/tagbidder.go b/adapters/vastbidder/tagbidder.go new file mode 100644 index 00000000000..4b7e5efc82f --- /dev/null +++ b/adapters/vastbidder/tagbidder.go @@ -0,0 +1,87 @@ +package vastbidder + +import ( + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//TagBidder is default implementation of ITagBidder +type TagBidder struct { + adapters.Bidder + bidderName openrtb_ext.BidderName + adapterConfig *config.Adapter +} + +//MakeRequests will contains default definition for processing queries +func (a *TagBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + bidderMacro := GetNewBidderMacro(a.bidderName) + bidderMapper := GetDefaultMapper() + macroProcessor := NewMacroProcessor(bidderMacro, bidderMapper) + + //Setting config parameters + //bidderMacro.SetBidderConfig(a.bidderConfig) + bidderMacro.SetAdapterConfig(a.adapterConfig) + bidderMacro.InitBidRequest(request) + + requestData := []*adapters.RequestData{} + for impIndex := range request.Imp { + bidderExt, err := bidderMacro.LoadImpression(&request.Imp[impIndex]) + if nil != err { + continue + } + + //iterate each vast tags, and load vast tag + for vastTagIndex, tag := range bidderExt.Tags { + //load vasttag + bidderMacro.LoadVASTTag(tag) + + //Setting Bidder Level Keys + bidderKeys := bidderMacro.GetBidderKeys() + macroProcessor.SetBidderKeys(bidderKeys) + + uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), Flags{RemoveEmptyParam: true}) + + // append custom headers if any + headers := bidderMacro.getAllHeaders() + + requestData = append(requestData, &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: impIndex, + VASTTagIndex: vastTagIndex, + }, + Method: `GET`, + Uri: uri, + Headers: headers, + }) + } + } + + return requestData, nil +} + +//MakeBids makes bids +func (a *TagBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + //response validation can be done here independently + //handler, err := GetResponseHandler(a.bidderConfig.ResponseType) + handler, err := GetResponseHandler(VASTTagHandlerType) + if nil != err { + return nil, []error{err} + } + return handler.MakeBids(internalRequest, externalRequest, response) +} + +//NewTagBidder is an constructor for TagBidder +func NewTagBidder(bidderName openrtb_ext.BidderName, config config.Adapter) *TagBidder { + obj := &TagBidder{ + bidderName: bidderName, + adapterConfig: &config, + } + return obj +} + +// Builder builds a new instance of the 33Across adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + return NewTagBidder(bidderName, config), nil +} diff --git a/adapters/vastbidder/tagbidder_test.go b/adapters/vastbidder/tagbidder_test.go new file mode 100644 index 00000000000..086e3a1ad3a --- /dev/null +++ b/adapters/vastbidder/tagbidder_test.go @@ -0,0 +1,149 @@ +package vastbidder + +import ( + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +//TestMakeRequests verifies +// 1. default and custom headers are set +func TestMakeRequests(t *testing.T) { + + type args struct { + customHeaders map[string]string + req *openrtb2.BidRequest + } + type want struct { + impIDReqHeaderMap map[string]http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "multi_impression_req", + args: args{ + customHeaders: map[string]string{ + "my-custom-header": "custom-value", + }, + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { // vast 2.0 + ID: "vast_2_0_imp_req", + Video: &openrtb2.Video{ + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_4_0_imp_req", + Video: &openrtb2.Video{ // vast 4.0 + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_2_0_4_0_wrapper_imp_req", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolVAST40Wrapper, + openrtb2.ProtocolVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "other_non_vast_protocol", + Video: &openrtb2.Video{ // DAAST 1.0 + Protocols: []openrtb2.Protocol{ + openrtb2.ProtocolDAAST10, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + + ID: "no_protocol_field_set", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []openrtb2.Protocol{}, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + }, + }, + }, + want: want{ + impIDReqHeaderMap: map[string]http.Header{ + "vast_2_0_imp_req": { + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_4_0_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_2_0_4_0_wrapper_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "other_non_vast_protocol": { + "My-Custom-Header": []string{"custom-value"}, + }, // no default headers expected + "no_protocol_field_set": { // set all default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderName := openrtb_ext.BidderName("myVastBidderMacro") + RegisterNewBidderMacro(bidderName, func() IBidderMacro { + return newMyVastBidderMacro(tt.args.customHeaders) + }) + bidder := NewTagBidder(bidderName, config.Adapter{}) + reqData, err := bidder.MakeRequests(tt.args.req, nil) + assert.Nil(t, err) + for _, req := range reqData { + impID := tt.args.req.Imp[req.Params.ImpIndex].ID + expectedHeaders := tt.want.impIDReqHeaderMap[impID] + assert.Equal(t, expectedHeaders, req.Headers, "test for - "+impID) + } + }) + } +} diff --git a/adapters/vastbidder/util.go b/adapters/vastbidder/util.go new file mode 100644 index 00000000000..8ad02535ec6 --- /dev/null +++ b/adapters/vastbidder/util.go @@ -0,0 +1,70 @@ +package vastbidder + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "strconv" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func ObjectArrayToString(len int, separator string, cb func(i int) string) string { + if 0 == len { + return "" + } + + var out bytes.Buffer + for i := 0; i < len; i++ { + if out.Len() > 0 { + out.WriteString(separator) + } + out.WriteString(cb(i)) + } + return out.String() +} + +func readImpExt(impExt json.RawMessage) (*openrtb_ext.ExtImpVASTBidder, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(impExt, &bidderExt); err != nil { + return nil, err + } + + vastBidderExt := openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &vastBidderExt); err != nil { + return nil, err + } + return &vastBidderExt, nil +} + +func normalizeObject(prefix string, out map[string]string, obj map[string]interface{}) { + for k, value := range obj { + key := k + if len(prefix) > 0 { + key = prefix + "." + k + } + + switch val := value.(type) { + case string: + out[key] = val + case []interface{}: //array + continue + case map[string]interface{}: //object + normalizeObject(key, out, val) + default: //all int, float + out[key] = fmt.Sprint(value) + } + } +} + +func NormalizeJSON(obj map[string]interface{}) map[string]string { + out := map[string]string{} + normalizeObject("", out, obj) + return out +} + +var GetRandomID = func() string { + return strconv.FormatInt(rand.Int63(), intBase) +} diff --git a/adapters/vastbidder/vast_tag_response_handler.go b/adapters/vastbidder/vast_tag_response_handler.go new file mode 100644 index 00000000000..f3436370854 --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler.go @@ -0,0 +1,334 @@ +package vastbidder + +import ( + "encoding/json" + "errors" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/beevik/etree" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +var durationRegExp = regexp.MustCompile(`^([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)(\.(\d{1,3}))?$`) + +//IVASTTagResponseHandler to parse VAST Tag +type IVASTTagResponseHandler interface { + ITagResponseHandler + ParseExtension(version string, tag *etree.Element, bid *adapters.TypedBid) []error + GetStaticPrice(ext json.RawMessage) float64 +} + +//VASTTagResponseHandler to parse VAST Tag +type VASTTagResponseHandler struct { + IVASTTagResponseHandler + ImpBidderExt *openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag +} + +//NewVASTTagResponseHandler returns new object +func NewVASTTagResponseHandler() *VASTTagResponseHandler { + obj := &VASTTagResponseHandler{} + obj.IVASTTagResponseHandler = obj + return obj +} + +//Validate will return bids +func (handler *VASTTagResponseHandler) Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error { + if response.StatusCode != http.StatusOK { + return []error{errors.New(`validation failed`)} + } + + if len(internalRequest.Imp) < externalRequest.Params.ImpIndex { + return []error{errors.New(`validation failed invalid impression index`)} + } + + impExt, err := readImpExt(internalRequest.Imp[externalRequest.Params.ImpIndex].Ext) + if nil != err { + return []error{err} + } + + if len(impExt.Tags) < externalRequest.Params.VASTTagIndex { + return []error{errors.New(`validation failed invalid vast tag index`)} + } + + //Initialise Extensions + handler.ImpBidderExt = impExt + handler.VASTTag = impExt.Tags[externalRequest.Params.VASTTagIndex] + return nil +} + +//MakeBids will return bids +func (handler *VASTTagResponseHandler) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if err := handler.IVASTTagResponseHandler.Validate(internalRequest, externalRequest, response); len(err) > 0 { + return nil, err[:] + } + + bidResponses, err := handler.vastTagToBidderResponse(internalRequest, externalRequest, response) + return bidResponses, err +} + +//ParseExtension will parse VAST XML extension object +func (handler *VASTTagResponseHandler) ParseExtension(version string, ad *etree.Element, bid *adapters.TypedBid) []error { + return nil +} + +func (handler *VASTTagResponseHandler) vastTagToBidderResponse(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + doc := etree.NewDocument() + + //Read Document + if err := doc.ReadFromBytes(response.Body); err != nil { + errs = append(errs, err) + return nil, errs[:] + } + + //Check VAST Tag + vast := doc.Element.FindElement(`./VAST`) + if vast == nil { + errs = append(errs, errors.New("VAST Tag Not Found")) + return nil, errs[:] + } + + //Check VAST/Ad Tag + adElement := getAdElement(vast) + if nil == adElement { + errs = append(errs, errors.New("VAST/Ad Tag Not Found")) + return nil, errs[:] + } + + typedBid := &adapters.TypedBid{ + Bid: &openrtb2.Bid{}, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: handler.VASTTag.TagID, + }, + } + + creatives := adElement.FindElements("Creatives/Creative") + if nil != creatives { + for _, creative := range creatives { + // get creative id + typedBid.Bid.CrID = getCreativeID(creative) + + // get duration from vast creative + dur, err := getDuration(creative) + if nil != err { + // get duration from input bidder vast tag + dur = getStaticDuration(handler.VASTTag) + } + if dur > 0 { + typedBid.BidVideo.Duration = int(dur) // prebid expects int value + } + } + } + + bidResponse := &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{typedBid}, + Currency: `USD`, //TODO: Need to check how to get currency value + } + + //GetVersion + version := vast.SelectAttrValue(`version`, `2.0`) + + if err := handler.IVASTTagResponseHandler.ParseExtension(version, adElement, typedBid); len(err) > 0 { + errs = append(errs, err...) + return nil, errs[:] + } + + //if bid.price is not set in ParseExtension + if typedBid.Bid.Price <= 0 { + price, currency := getPricingDetails(version, adElement) + if price <= 0 { + price, currency = getStaticPricingDetails(handler.VASTTag) + if price <= 0 { + errs = append(errs, &errortypes.NoBidPrice{Message: "Bid Price Not Present"}) + return nil, errs[:] + } + } + typedBid.Bid.Price = price + if len(currency) > 0 { + bidResponse.Currency = currency + } + } + + typedBid.Bid.ADomain = getAdvertisers(version, adElement) + + //if bid.id is not set in ParseExtension + if len(typedBid.Bid.ID) == 0 { + typedBid.Bid.ID = GetRandomID() + } + + //if bid.impid is not set in ParseExtension + if len(typedBid.Bid.ImpID) == 0 { + typedBid.Bid.ImpID = internalRequest.Imp[externalRequest.Params.ImpIndex].ID + } + + //if bid.adm is not set in ParseExtension + if len(typedBid.Bid.AdM) == 0 { + typedBid.Bid.AdM = string(response.Body) + } + + //if bid.CrID is not set in ParseExtension + if len(typedBid.Bid.CrID) == 0 { + typedBid.Bid.CrID = "cr_" + GetRandomID() + } + + return bidResponse, nil +} + +func getAdElement(vast *etree.Element) *etree.Element { + if ad := vast.FindElement(`./Ad/Wrapper`); nil != ad { + return ad + } + if ad := vast.FindElement(`./Ad/InLine`); nil != ad { + return ad + } + return nil +} + +func getAdvertisers(vastVer string, ad *etree.Element) []string { + version, err := strconv.ParseFloat(vastVer, 64) + if err != nil { + version = 2.0 + } + + advertisers := make([]string, 0) + + switch int(version) { + case 2, 3: + for _, ext := range ad.FindElements(`./Extensions/Extension/`) { + for _, attr := range ext.Attr { + if attr.Key == "type" && attr.Value == "advertiser" { + for _, ele := range ext.ChildElements() { + if ele.Tag == "Advertiser" { + if strings.TrimSpace(ele.Text()) != "" { + advertisers = append(advertisers, ele.Text()) + } + } + } + } + } + } + case 4: + if ad.FindElement("./Advertiser") != nil { + adv := strings.TrimSpace(ad.FindElement("./Advertiser").Text()) + if adv != "" { + advertisers = append(advertisers, adv) + } + } + default: + glog.V(3).Infof("Handle getAdvertisers for VAST version %d", int(version)) + } + + if len(advertisers) == 0 { + return nil + } + return advertisers +} + +func getStaticPricingDetails(vastTag *openrtb_ext.ExtImpVASTBidderTag) (float64, string) { + if nil == vastTag { + return 0.0, "" + } + return vastTag.Price, "USD" +} + +func getPricingDetails(version string, ad *etree.Element) (float64, string) { + var currency string + var node *etree.Element + + if `2.0` == version { + node = ad.FindElement(`./Extensions/Extension/Price`) + } else { + node = ad.FindElement(`./Pricing`) + } + + if nil == node { + return 0.0, currency + } + + priceValue, err := strconv.ParseFloat(node.Text(), 64) + if nil != err { + return 0.0, currency + } + + currencyNode := node.SelectAttr(`currency`) + if nil != currencyNode { + currency = currencyNode.Value + } + + return priceValue, currency +} + +// getDuration extracts the duration of the bid from input creative of Linear type. +// The lookup may vary from vast version provided in the input +// returns duration in seconds or error if failed to obtained the duration. +// If multple Linear tags are present, onlyfirst one will be used +// +// It will lookup for duration only in case of creative type is Linear. +// If creative type other than Linear then this function will return error +// For Linear Creative it will lookup for Duration attribute.Duration value will be in hh:mm:ss.mmm format as per VAST specifications +// If Duration attribute not present this will return error +// +// After extracing the duration it will convert it into seconds +// +// The ad server uses the element to denote +// the intended playback duration for the video or audio component of the ad. +// Time value may be in the format HH:MM:SS.mmm where .mmm indicates milliseconds. +// Providing milliseconds is optional. +// +// Reference +// 1.https://iabtechlab.com/wp-content/uploads/2019/06/VAST_4.2_final_june26.pdf +// 2.https://iabtechlab.com/wp-content/uploads/2018/11/VAST4.1-final-Nov-8-2018.pdf +// 3.https://iabtechlab.com/wp-content/uploads/2016/05/VAST4.0_Updated_April_2016.pdf +// 4.https://iabtechlab.com/wp-content/uploads/2016/04/VASTv3_0.pdf +func getDuration(creative *etree.Element) (int, error) { + if nil == creative { + return 0, errors.New("Invalid Creative") + } + node := creative.FindElement("./Linear/Duration") + if nil == node { + return 0, errors.New("Invalid Duration") + } + duration := node.Text() + // check if milliseconds is provided + match := durationRegExp.FindStringSubmatch(duration) + if nil == match { + return 0, errors.New("Invalid Duration") + } + repl := "${1}h${2}m${3}s" + ms := match[5] + if "" != ms { + repl += "${5}ms" + } + duration = durationRegExp.ReplaceAllString(duration, repl) + dur, err := time.ParseDuration(duration) + if err != nil { + return 0, err + } + return int(dur.Seconds()), nil +} + +func getStaticDuration(vastTag *openrtb_ext.ExtImpVASTBidderTag) int { + if nil == vastTag { + return 0 + } + return vastTag.Duration +} + +//getCreativeID looks for ID inside input creative tag +func getCreativeID(creative *etree.Element) string { + if nil == creative { + return "" + } + return creative.SelectAttrValue("id", "") +} diff --git a/adapters/vastbidder/vast_tag_response_handler_test.go b/adapters/vastbidder/vast_tag_response_handler_test.go new file mode 100644 index 00000000000..28c29ef6776 --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler_test.go @@ -0,0 +1,385 @@ +package vastbidder + +import ( + "errors" + "fmt" + "sort" + "testing" + + "github.com/beevik/etree" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { + type args struct { + internalRequest *openrtb2.BidRequest + externalRequest *adapters.RequestData + response *adapters.ResponseData + vastTag *openrtb_ext.ExtImpVASTBidderTag + } + type want struct { + bidderResponse *adapters.BidderResponse + err []error + } + tests := []struct { + name string + args args + want want + }{ + { + name: `InlinePricingNode`, + args: args{ + internalRequest: &openrtb2.BidRequest{ + ID: `request_id_1`, + Imp: []openrtb2.Imp{ + { + ID: `imp_id_1`, + }, + }, + }, + externalRequest: &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: 0, + }, + }, + response: &adapters.ResponseData{ + Body: []byte(` `), + }, + vastTag: &openrtb_ext.ExtImpVASTBidderTag{ + TagID: "101", + Duration: 15, + }, + }, + want: want{ + bidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: `1234`, + ImpID: `imp_id_1`, + Price: 0.05, + AdM: ` `, + CrID: "cr_1234", + }, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: "101", + Duration: 15, + }, + }, + }, + Currency: `USD`, + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := NewVASTTagResponseHandler() + GetRandomID = func() string { + return `1234` + } + handler.VASTTag = tt.args.vastTag + + bidderResponse, err := handler.vastTagToBidderResponse(tt.args.internalRequest, tt.args.externalRequest, tt.args.response) + assert.Equal(t, tt.want.bidderResponse, bidderResponse) + assert.Equal(t, tt.want.err, err) + }) + } +} + +//TestGetDurationInSeconds ... +// hh:mm:ss.mmm => 3:40:43.5 => 3 hours, 40 minutes, 43 seconds and 5 milliseconds +// => 3*60*60 + 40*60 + 43 + 5*0.001 => 10800 + 2400 + 43 + 0.005 => 13243.005 +func TestGetDurationInSeconds(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + duration int // seconds (will converted from string with format as HH:MM:SS.mmm) + err error + } + tests := []struct { + name string + args args + want want + }{ + // duration validation tests + {name: "duration 00:00:25 (= 25 seconds)", want: want{duration: 25}, args: args{creativeTag: ` 00:00:25 `}}, + {name: "duration 00:00:-25 (= -25 seconds)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:-25 `}}, + {name: "duration 00:00:30.999 (= 30.990 seconds (int -> 30 seconds))", want: want{duration: 30}, args: args{creativeTag: ` 00:00:30.999 `}}, + {name: "duration 00:01:08 (1 min 8 seconds = 68 seconds)", want: want{duration: 68}, args: args{creativeTag: ` 00:01:08 `}}, + {name: "duration 02:13:12 (2 hrs 13 min 12 seconds) = 7992 seconds)", want: want{duration: 7992}, args: args{creativeTag: ` 02:13:12 `}}, + {name: "duration 3:40:43.5 (3 hrs 40 min 43 seconds 5 ms) = 6043.005 seconds (int -> 6043 seconds))", want: want{duration: 13243}, args: args{creativeTag: ` 3:40:43.5 `}}, + {name: "duration 00:00:25.0005458 (0 hrs 0 min 25 seconds 0005458 ms) - invalid max ms is 999", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:25.0005458 `}}, + {name: "invalid duration 3:13:900 (3 hrs 13 min 900 seconds) = Invalid seconds )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:900 `}}, + {name: "invalid duration 3:13:34:44 (3 hrs 13 min 34 seconds :44=invalid) = ?? )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:34:44 `}}, + {name: "duration = 0:0:45.038 , with milliseconds duration (0 hrs 0 min 45 seconds and 038 millseconds) = 45.038 seconds (int -> 45 seconds) )", want: want{duration: 45}, args: args{creativeTag: ` 0:0:45.038 `}}, + {name: "duration = 0:0:48.50 = 48.050 seconds (int -> 48 seconds))", want: want{duration: 48}, args: args{creativeTag: ` 0:0:48.50 `}}, + {name: "duration = 0:0:28.59 = 28.059 seconds (int -> 28 seconds))", want: want{duration: 28}, args: args{creativeTag: ` 0:0:28.59 `}}, + {name: "duration = 56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56 `}}, + {name: "duration = :56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56 `}}, + {name: "duration = :56: (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56: `}}, + {name: "duration = ::56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` ::56 `}}, + {name: "duration = 56.445 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56.445 `}}, + {name: "duration = a:b:c.d (no numbers)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` a:b:c.d `}}, + + // tag validations tests + {name: "Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Companion Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Non-Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Invalid Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ``}}, + {name: "Nil Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ""}}, + + // multiple linear tags in creative + {name: "Multiple Linear Ads within Creative", want: want{duration: 25}, args: args{creativeTag: `0:0:250:0:30`}}, + // Case sensitivity check - passing DURATION (vast is case-sensitive as per https://vastvalidator.iabtechlab.com/dash) + {name: " all caps", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `0:0:10`}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + dur, err := getDuration(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.duration, dur) + assert.Equal(t, tt.want.err, err) + // if error expects 0 value for duration + if nil != err { + assert.Equal(t, 0, dur) + } + }) + } +} + +func BenchmarkGetDuration(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` 0:0:56.3 `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getDuration(creative) + } +} + +func TestGetCreativeId(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + id string + } + tests := []struct { + name string + args args + want want + }{ + {name: "creative tag with id", want: want{id: "233ff44"}, args: args{creativeTag: ``}}, + {name: "creative tag without id", want: want{id: ""}, args: args{creativeTag: ``}}, + {name: "no creative tag", want: want{id: ""}, args: args{creativeTag: ""}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + id := getCreativeID(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.id, id) + }) + } +} + +func BenchmarkGetCreativeID(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getCreativeID(creative) + } +} + +func TestGetAdvertisers(t *testing.T) { + tt := []struct { + name string + vastStr string + expected []string + }{ + { + name: "vast_4_with_advertiser", + vastStr: ` + + + www.iabtechlab.com + + + `, + expected: []string{"www.iabtechlab.com"}, + }, + { + name: "vast_4_without_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_4_with_empty_advertiser", + vastStr: ` + + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_2_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_2_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_3_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_3_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + doc := etree.NewDocument() + if err := doc.ReadFromString(tc.vastStr); err != nil { + t.Errorf("Failed to create etree doc from string %+v", err) + } + + vastDoc := doc.FindElement("./VAST") + vastVer := vastDoc.SelectAttrValue(`version`, `2.0`) + + ad := getAdElement(vastDoc) + + result := getAdvertisers(vastVer, ad) + + sort.Strings(result) + sort.Strings(tc.expected) + + if !assert.Equal(t, len(tc.expected), len(result), fmt.Sprintf("Expected slice length - %+v \nResult slice length - %+v", len(tc.expected), len(result))) { + return + } + + for i, expected := range tc.expected { + assert.Equal(t, expected, result[i], fmt.Sprintf("Element mismatch at position %d.\nExpected - %s\nActual - %s", i, expected, result[i])) + } + }) + } +} diff --git a/config/config.go b/config/config.go index 33855cb1f43..83001b10f3c 100755 --- a/config/config.go +++ b/config/config.go @@ -650,6 +650,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUcfunnel, "https://sync.aralego.com/idsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&usprivacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId") // openrtb_ext.BidderUnicorn doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://sync.1rx.io/usersync2/rmpssp?sub=openwrap&gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dunruly%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + // openrtb_ext.BidderVASTBidder doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderValueImpression, "https://rtb.valueimpression.com/usersync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvalueimpression%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") // openrtb_ext.BidderVrtcal doesn't have a good default. @@ -920,6 +921,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.unicorn.endpoint", "https://ds.uncn.jp/pb/0/bid.json") v.SetDefault("adapters.unruly.endpoint", "http://targeting.unrulymedia.com/openrtb/2.2") v.SetDefault("adapters.valueimpression.endpoint", "https://rtb.valueimpression.com/endpoint") + v.SetDefault("adapters.vastbidder.endpoint", "https://test.com") v.SetDefault("adapters.verizonmedia.disabled", true) v.SetDefault("adapters.visx.endpoint", "https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard") v.SetDefault("adapters.vrtcal.endpoint", "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1812") diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go index b58d758d877..d547158b6c3 100644 --- a/endpoints/events/vtrack.go +++ b/endpoints/events/vtrack.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" @@ -563,6 +563,5 @@ func getDomain(site *openrtb2.Site) string { hostname = pageURL.Host } } - return hostname } diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go index 0980843e650..6f290b22499 100644 --- a/endpoints/events/vtrack_test.go +++ b/endpoints/events/vtrack_test.go @@ -11,9 +11,8 @@ import ( "strings" "testing" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" - "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/stored_requests" diff --git a/endpoints/openrtb2/ctv/types/adpod_types.go b/endpoints/openrtb2/ctv/types/adpod_types.go index 6e46929547d..275a01c2fbc 100644 --- a/endpoints/openrtb2/ctv/types/adpod_types.go +++ b/endpoints/openrtb2/ctv/types/adpod_types.go @@ -53,8 +53,10 @@ type ImpAdPodConfig struct { //ImpData example type ImpData struct { //AdPodGenerator - VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"` - Config []*ImpAdPodConfig `json:"imp,omitempty"` - ErrorCode *int `json:"ec,omitempty"` - Bid *AdPodBid `json:"-"` + ImpID string `json:"-"` + VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"` + Config []*ImpAdPodConfig `json:"imp,omitempty"` + ErrorCode *int `json:"ec,omitempty"` + BlockedVASTTags map[string][]string `json:"blockedtags,omitempty"` + Bid *AdPodBid `json:"-"` } diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index c8f5a32e307..a2299517695 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/prebid/prebid-server/endpoints/events" "math" "net/http" "net/url" @@ -14,7 +13,7 @@ import ( "strings" "time" - "github.com/PubMatic-OpenWrap/etree" + "github.com/beevik/etree" "github.com/buger/jsonparser" uuid "github.com/gofrs/uuid" "github.com/golang/glog" @@ -23,6 +22,7 @@ import ( accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/endpoints/events" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/combination" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/impressions" @@ -41,12 +41,14 @@ import ( //CTV Specific Endpoint type ctvEndpointDeps struct { endpointDeps - request *openrtb2.BidRequest - reqExt *openrtb_ext.ExtRequestAdPod - impData []*types.ImpData - videoSeats []*openrtb2.SeatBid //stores pure video impression bids - impIndices map[string]int - isAdPodRequest bool + request *openrtb2.BidRequest + reqExt *openrtb_ext.ExtRequestAdPod + impData []*types.ImpData + videoSeats []*openrtb2.SeatBid //stores pure video impression bids + impIndices map[string]int + isAdPodRequest bool + impsExt map[string]map[string]map[string]interface{} + impPartnerBlockedTagIDMap map[string]map[string][]string //Prebid Specific ctx context.Context @@ -382,6 +384,13 @@ func (deps *ctvEndpointDeps) setDefaultValues() { //set request is adpod request or normal request deps.setIsAdPodRequest() + + //TODO: OTT-217, OTT-161 commenting code of filtering vast tags + /* + if deps.isAdPodRequest { + deps.readImpExtensionsAndTags() + } + */ } //validateBidRequest will validate AdPod specific mandatory Parameters and returns error @@ -409,6 +418,42 @@ func (deps *ctvEndpointDeps) validateBidRequest() (err []error) { return } +//readImpExtensionsAndTags will read the impression extensions +func (deps *ctvEndpointDeps) readImpExtensionsAndTags() (errs []error) { + deps.impsExt = make(map[string]map[string]map[string]interface{}) + deps.impPartnerBlockedTagIDMap = make(map[string]map[string][]string) //Initially this will have all tags, eligible tags will be filtered in filterImpsVastTagsByDuration + + for _, imp := range deps.request.Imp { + var impExt map[string]map[string]interface{} + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + errs = append(errs, err) + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID] = make(map[string][]string) + + for partnerName, partnerExt := range impExt { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID][partnerName] = append(deps.impPartnerBlockedTagIDMap[imp.ID][partnerName], vastTag["tagid"].(string)) + } + } + + deps.impsExt[imp.ID] = impExt + } + + return errs +} + /********************* Creating CTV BidRequest *********************/ //createBidRequest will return new bid request with all things copy from bid request except impression objects @@ -421,16 +466,106 @@ func (deps *ctvEndpointDeps) createBidRequest(req *openrtb2.BidRequest) *openrtb //createImpressions ctvRequest.Imp = deps.createImpressions() + //TODO: OTT-217, OTT-161 commenting code of filtering vast tags + //deps.filterImpsVastTagsByDuration(&ctvRequest) + //TODO: remove adpod extension if not required to send further return &ctvRequest } +//filterImpsVastTagsByDuration checks if a Vast tag should be called for a generated impression based on the duration of tag and impression +func (deps *ctvEndpointDeps) filterImpsVastTagsByDuration(bidReq *openrtb2.BidRequest) { + + for impCount, imp := range bidReq.Imp { + index := strings.LastIndex(imp.ID, "_") + if index == -1 { + continue + } + + originalImpID := imp.ID[:index] + + impExtMap := deps.impsExt[originalImpID] + newImpExtMap := make(map[string]map[string]interface{}) + for k, v := range impExtMap { + newImpExtMap[k] = v + } + + for partnerName, partnerExt := range newImpExtMap { + if partnerExt["tags"] != nil { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + var compatibleVasts []interface{} + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + tagDuration := int(vastTag["dur"].(float64)) + if int(imp.Video.MinDuration) <= tagDuration && tagDuration <= int(imp.Video.MaxDuration) { + compatibleVasts = append(compatibleVasts, tag) + + deps.impPartnerBlockedTagIDMap[originalImpID][partnerName] = remove(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName], vastTag["tagid"].(string)) + if len(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName]) == 0 { + delete(deps.impPartnerBlockedTagIDMap[originalImpID], partnerName) + } + } + } + + if len(compatibleVasts) < 1 { + delete(newImpExtMap, partnerName) + } else { + newImpExtMap[partnerName] = map[string]interface{}{ + "tags": compatibleVasts, + } + } + + bExt, err := json.Marshal(newImpExtMap) + if err != nil { + continue + } + imp.Ext = bExt + } + } + bidReq.Imp[impCount] = imp + } + + for impID, blockedTags := range deps.impPartnerBlockedTagIDMap { + for _, datum := range deps.impData { + if datum.ImpID == impID { + datum.BlockedVASTTags = blockedTags + break + } + } + } +} + +func remove(slice []string, item string) []string { + index := -1 + for i := range slice { + if slice[i] == item { + index = i + break + } + } + + if index == -1 { + return slice + } + + return append(slice[:index], slice[index+1:]...) +} + //getAllAdPodImpsConfigs will return all impression adpod configurations func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { for index, imp := range deps.request.Imp { if nil == imp.Video || nil == deps.impData[index].VideoExt || nil == deps.impData[index].VideoExt.AdPod { continue } + deps.impData[index].ImpID = imp.ID deps.impData[index].Config = deps.getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod) if 0 == len(deps.impData[index].Config) { errorCode := new(int) diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go index f03312ead9c..51fa4b499f7 100644 --- a/endpoints/openrtb2/ctv_auction_test.go +++ b/endpoints/openrtb2/ctv_auction_test.go @@ -3,11 +3,12 @@ package openrtb2 import ( "encoding/json" "fmt" - "github.com/PubMatic-OpenWrap/etree" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" "net/url" "strings" "testing" + "github.com/beevik/etree" "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" @@ -149,3 +150,230 @@ func TestAdjustBidIDInVideoEventTrackers(t *testing.T) { } } } + +func TestFilterImpsVastTagsByDuration(t *testing.T) { + type inputParams struct { + request *openrtb2.BidRequest + generatedRequest *openrtb2.BidRequest + impData []*types.ImpData + } + + type output struct { + reqs openrtb2.BidRequest + blockedTags []map[string][]string + } + + tt := []struct { + testName string + input inputParams + expectedOutput output + }{ + { + testName: "test_single_impression_single_vast_partner_with_no_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_single_vast_partner_with_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + }, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]},"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":35,"tagid":"spotx_35"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":40,"tagid":"openx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35", "openx_40"}, "spotx_vast_bidder": []string{"spotx_35"}}, + }, + }, + }, + { + testName: "test_multi_impression_multi_partner_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: nil, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: nil, + }, + }, + { + testName: "test_multi_impression_multi_partner_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + {ImpID: "imp2"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + {"spotx_vast_bidder": []string{"spotx_40"}}, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + deps := ctvEndpointDeps{request: tc.input.request, impData: tc.input.impData} + deps.readImpExtensionsAndTags() + + outputBids := tc.input.generatedRequest + deps.filterImpsVastTagsByDuration(outputBids) + + assert.Equal(t, tc.expectedOutput.reqs, *outputBids, "Expected length of impressions array was %d but actual was %d", tc.expectedOutput.reqs, outputBids) + + for i, datum := range deps.impData { + assert.Equal(t, tc.expectedOutput.blockedTags[i], datum.BlockedVASTTags, "Expected and actual impData was different") + } + }) + } +} diff --git a/errortypes/code.go b/errortypes/code.go index 2749b978006..f8206525b27 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -11,6 +11,7 @@ const ( BidderTemporarilyDisabledErrorCode BlacklistedAcctErrorCode AcctRequiredErrorCode + NoBidPriceErrorCode ) // Defines numeric codes for well-known warnings. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index 1fed2d7da6e..6bac21fcb0d 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -182,3 +182,19 @@ func (err *Warning) Code() int { func (err *Warning) Severity() Severity { return SeverityWarning } + +type NoBidPrice struct { + Message string +} + +func (err *NoBidPrice) Error() string { + return err.Message +} + +func (err *NoBidPrice) Code() int { + return NoBidPriceErrorCode +} + +func (err *NoBidPrice) Severity() Severity { + return SeverityWarning +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index e3924e5b8cc..6ac494448ca 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -99,6 +99,7 @@ import ( "github.com/prebid/prebid-server/adapters/unicorn" "github.com/prebid/prebid-server/adapters/unruly" "github.com/prebid/prebid-server/adapters/valueimpression" + "github.com/prebid/prebid-server/adapters/vastbidder" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" @@ -214,6 +215,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, openrtb_ext.BidderValueImpression: valueimpression.Builder, + openrtb_ext.BidderVASTBidder: vastbidder.Builder, openrtb_ext.BidderVerizonMedia: verizonmedia.Builder, openrtb_ext.BidderVisx: visx.Builder, openrtb_ext.BidderVrtcal: vrtcal.Builder, diff --git a/exchange/bidder.go b/exchange/bidder.go index 262d2d8d3f3..07d222c9602 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -357,6 +357,12 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { ext.ResponseBody = string(httpInfo.response.Body) ext.Status = httpInfo.response.StatusCode } + + if nil != httpInfo.request.Params { + ext.Params = make(map[string]int) + ext.Params["ImpIndex"] = httpInfo.request.Params.ImpIndex + ext.Params["VASTTagIndex"] = httpInfo.request.Params.VASTTagIndex + } } return ext diff --git a/exchange/events.go b/exchange/events.go index 35929d8e604..9742e50e424 100644 --- a/exchange/events.go +++ b/exchange/events.go @@ -4,9 +4,8 @@ import ( "encoding/json" "time" - "github.com/mxmCherry/openrtb/v15/openrtb2" - jsonpatch "github.com/evanphx/json-patch" + "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/endpoints/events" @@ -63,7 +62,6 @@ func (ev *eventTracking) modifyBidVAST(pbsBid *pbsOrtbBid, bidderName openrtb_ex if pbsBid.bidType != openrtb_ext.BidTypeVideo || len(bid.AdM) == 0 && len(bid.NURL) == 0 { return } - vastXML := makeVAST(bid) bidID := bid.ID if len(pbsBid.generatedBidID) > 0 { diff --git a/exchange/events_test.go b/exchange/events_test.go index a4fe03601b7..217cca5351d 100644 --- a/exchange/events_test.go +++ b/exchange/events_test.go @@ -1,10 +1,11 @@ package exchange import ( - "github.com/prebid/prebid-server/config" "strings" "testing" + "github.com/prebid/prebid-server/config" + "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" diff --git a/exchange/exchange.go b/exchange/exchange.go index 04af7729273..b28c60e4fbe 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -27,6 +27,7 @@ import ( "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/prebid_cache_client" + "golang.org/x/net/publicsuffix" ) type ContextKey string @@ -202,6 +203,12 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * var bidResponseExt *openrtb_ext.ExtBidResponse if anyBidsReturned { + adapterBids, rejections := applyAdvertiserBlocking(r.BidRequest, adapterBids) + // add advertiser blocking specific errors + for _, message := range rejections { + errs = append(errs, errors.New(message)) + } + var bidCategory map[string]string //If includebrandcategory is present in ext then CE feature is on. if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil { @@ -444,7 +451,11 @@ func (e *exchange) getAllBids( // Structure to record extra tracking data generated during bidding ae := new(seatResponseExtra) ae.ResponseTimeMillis = int(elapsed / time.Millisecond) + if bids != nil { + // Setting bidderCoreName in SeatBid + bids.bidderCoreName = bidderRequest.BidderCoreName + ae.HttpCalls = bids.httpCalls // Setting bidderCoreName in SeatBid @@ -1045,3 +1056,78 @@ func recordAdaptorDuplicateBidIDs(metricsEngine metrics.MetricsEngine, adapterBi } return bidIDCollisionFound } + +//normalizeDomain validates, normalizes and returns valid domain or error if failed to validate +//checks if domain starts with http by lowercasing entire domain +//if not it prepends it before domain. This is required for obtaining the url +//using url.parse method. on successfull url parsing, it will replace first occurance of www. +//from the domain +func normalizeDomain(domain string) (string, error) { + domain = strings.Trim(strings.ToLower(domain), " ") + // not checking if it belongs to icann + suffix, _ := publicsuffix.PublicSuffix(domain) + if domain != "" && suffix == domain { // input is publicsuffix + return "", errors.New("domain [" + domain + "] is public suffix") + } + if !strings.HasPrefix(domain, "http") { + domain = fmt.Sprintf("http://%s", domain) + } + url, err := url.Parse(domain) + if nil == err && url.Host != "" { + return strings.Replace(url.Host, "www.", "", 1), nil + } + return "", err +} + +//applyAdvertiserBlocking rejects the bids of blocked advertisers mentioned in req.badv +//the rejection is currently only applicable to vast tag bidders. i.e. not for ortb bidders +//it returns seatbids containing valid bids and rejections containing rejected bid.id with reason +func applyAdvertiserBlocking(bidRequest *openrtb2.BidRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string) { + rejections := []string{} + nBadvs := []string{} + if nil != bidRequest.BAdv { + for _, domain := range bidRequest.BAdv { + nDomain, err := normalizeDomain(domain) + if nil == err && nDomain != "" { // skip empty and domains with errors + nBadvs = append(nBadvs, nDomain) + } + } + } + + for bidderName, seatBid := range seatBids { + if seatBid.bidderCoreName == openrtb_ext.BidderVASTBidder && len(nBadvs) > 0 { + for bidIndex := len(seatBid.bids) - 1; bidIndex >= 0; bidIndex-- { + bid := seatBid.bids[bidIndex] + for _, bAdv := range nBadvs { + aDomains := bid.bid.ADomain + rejectBid := false + if nil == aDomains { + // provision to enable rejecting of bids when req.badv is set + rejectBid = true + } else { + for _, d := range aDomains { + if aDomain, err := normalizeDomain(d); nil == err { + // compare and reject bid if + // 1. aDomain == bAdv + // 2. .bAdv is suffix of aDomain + // 3. aDomain not present but request has list of block advertisers + if aDomain == bAdv || strings.HasSuffix(aDomain, "."+bAdv) || (len(aDomain) == 0 && len(bAdv) > 0) { + // aDomain must be subdomain of bAdv + rejectBid = true + break + } + } + } + } + if rejectBid { + // reject the bid. bid belongs to blocked advertisers list + seatBid.bids = append(seatBid.bids[:bidIndex], seatBid.bids[bidIndex+1:]...) + rejections = updateRejections(rejections, bid.bid.ID, fmt.Sprintf("Bid (From '%s') belongs to blocked advertiser '%s'", bidderName, bAdv)) + break // bid is rejected due to advertiser blocked. No need to check further domains + } + } + } + } + } + return seatBids, rejections +} diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 718fb98c8f1..fbce03f6810 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -17,6 +17,7 @@ import ( "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/vastbidder" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/errortypes" @@ -3093,6 +3094,562 @@ func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer, return "", nil } +//TestApplyAdvertiserBlocking verifies advertiser blocking +//Currently it is expected to work only with TagBidders and not woth +// normal bidders +func TestApplyAdvertiserBlocking(t *testing.T) { + type args struct { + advBlockReq *openrtb2.BidRequest + adaptorSeatBids map[*bidderAdapter]*pbsOrtbSeatBid // bidder adaptor and its dummy seat bids map + } + type want struct { + rejectedBidIds []string + validBidCountPerSeat map[string]int + } + tests := []struct { + name string + args args + want want + }{ + { + name: "reject_bid_of_blocked_adv_from_tag_bidder", + args: args{ + advBlockReq: &openrtb2.BidRequest{ + BAdv: []string{"a.com"}, // block bids returned by a.com + }, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("vast_tag_bidder"): { // tag bidder returning 1 bid from blocked advertiser + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "a.com_bid", + ADomain: []string{"a.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "b.com_bid", + ADomain: []string{"b.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"ba.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"b.a.com.shri.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "reject_b.a.com.a.com.b.c.d.a.com", + ADomain: []string{"b.a.com.a.com.b.c.d.a.com"}, + }, + }, + }, + bidderCoreName: openrtb_ext.BidderVASTBidder, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"a.com_bid", "reject_b.a.com.a.com.b.c.d.a.com"}, + validBidCountPerSeat: map[string]int{ + "vast_tag_bidder": 3, + }, + }, + }, + { + name: "Badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tab_bidder_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no bid rejection expected + validBidCountPerSeat: map[string]int{ + "tab_bidder_1": 2, + }, + }, + }, + { + name: "adomain_is_not_present_but_Badv_is_set", // reject bids without adomain as badv is set + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_adapter_1_without_adomain", "bid_2_adapter_1_with_empty_adomain"}, + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 0, // expect 0 bids. i.e. all bids are rejected + "rtb_bidder_1": 2, // no bid must be rejected + }, + }, + }, + { + name: "adomain_and_badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adaptor_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_without_adomain"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejection expected as badv not present + validBidCountPerSeat: map[string]int{ + "tag_adaptor_1": 1, + }, + }, + }, + { + name: "empty_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "nil_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "ad_domains_normalized_and_checked", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"a.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("my_adapter"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_of_blocked_adv", ADomain: []string{"www.a.com"}}}, + // expect a.com is extracted from page url + {bid: &openrtb2.Bid{ID: "bid_2_of_blocked_adv", ADomain: []string{"http://a.com/my/page?k1=v1&k2=v2"}}}, + // invalid adomain - will be skipped and the bid will be not be rejected + {bid: &openrtb2.Bid{ID: "bid_3_with_domain_abcd1234", ADomain: []string{"abcd1234"}}}, + }, + }}, + }, + want: want{ + rejectedBidIds: []string{"bid_1_of_blocked_adv", "bid_2_of_blocked_adv"}, + validBidCountPerSeat: map[string]int{"my_adapter": 1}, + }, + }, { + name: "multiple_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com", "advertiser_2.com", "www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"advertiser_2.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "multiple_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"a.com", "b.com", "advertiser_3.com", "d.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"a.com", "https://advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"a.com", "b.com", "www.advertiser_3.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"a.com", "b.com", "advertiser_3.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "case_insensitive_badv", // case of domain not matters + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"ADVERTISER_1.COM"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "case_insensitive_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.COM"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"wWw.ADVERTISER_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "various_tld_combinations", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"http://blockme.shri"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "reject_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "rejecthttp://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "reject_https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "reject_https://www.blockme.shri"}}, + }, + }, + newTestRtbAdapter("rtb_non_block_bidder"): { + bids: []*pbsOrtbBid{ // all below bids are eligible and should not be rejected + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "accept_bid_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "accept_bid__http://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "accept_bid__https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "accept_bid__https://www.blockme.shri"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_www.blockme.shri", "reject_http://www.blockme.shri", "reject_https://blockme.shri", "reject_https://www.blockme.shri"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 0, + "rtb_non_block_bidder": 4, + }, + }, + }, + { + name: "subdomain_tests", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"10th.college.puneunv.edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"shri.10th.college.puneunv.edu"}, ID: "reject_shri.10th.college.puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"puneunv.edu"}, ID: "allow_puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://WWW.123.456.10th.college.PUNEUNV.edu"}, ID: "reject_123.456.10th.college.puneunv.edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_shri.10th.college.puneunv.edu", "reject_123.456.10th.college.puneunv.edu"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 1, + }, + }, + }, { + name: "only_domain_test", // do not expect bid rejection. edu is valid domain + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"school.edu"}, ID: "keep_bid_school.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"edu"}, ID: "keep_bid_edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"..edu"}, ID: "keep_bid_..edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 3, + }, + }, + }, + { + name: "public_suffix_in_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"co.in"}}, // co.in is valid public suffix + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"a.co.in"}, ID: "allow_a.co.in"}}, + {bid: &openrtb2.Bid{ADomain: []string{"b.com"}, ID: "allow_b.com"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name != "reject_bid_of_blocked_adv_from_tag_bidder" { + return + } + seatBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + tagBidders := make(map[openrtb_ext.BidderName]adapters.Bidder) + adapterMap := make(map[openrtb_ext.BidderName]adaptedBidder, 0) + for adaptor, sbids := range tt.args.adaptorSeatBids { + adapterMap[adaptor.BidderName] = adaptor + if tagBidder, ok := adaptor.Bidder.(*vastbidder.TagBidder); ok { + tagBidders[adaptor.BidderName] = tagBidder + } + seatBids[adaptor.BidderName] = sbids + } + + // applyAdvertiserBlocking internally uses tagBidders from (adapter_map.go) + // not testing alias here + seatBids, rejections := applyAdvertiserBlocking(tt.args.advBlockReq, seatBids) + + re := regexp.MustCompile("bid rejected \\[bid ID:(.*?)\\] reason") + for bidder, sBid := range seatBids { + // verify only eligible bids are returned + assert.Equal(t, tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids), "Expected eligible bids are %d, but found [%d] ", tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids)) + // verify rejections + assert.Equal(t, len(tt.want.rejectedBidIds), len(rejections), "Expected bid rejections are %d, but found [%d]", len(tt.want.rejectedBidIds), len(rejections)) + // verify rejected bid ids + present := false + for _, expectRejectedBidID := range tt.want.rejectedBidIds { + for _, rejection := range rejections { + match := re.FindStringSubmatch(rejection) + rejectedBidID := strings.Trim(match[1], " ") + if expectRejectedBidID == rejectedBidID { + present = true + break + } + } + if present { + break + } + } + if len(tt.want.rejectedBidIds) > 0 && !present { + assert.Fail(t, "Expected Bid ID [%s] as rejected. But bid is not rejected", re) + } + + if sBid.bidderCoreName != openrtb_ext.BidderVASTBidder { + continue // advertiser blocking is currently enabled only for tag bidders + } + // verify eligible bids not belongs to blocked advertisers + for _, bid := range sBid.bids { + if nil != bid.bid.ADomain { + for _, adomain := range bid.bid.ADomain { + for _, blockDomain := range tt.args.advBlockReq.BAdv { + nDomain, _ := normalizeDomain(adomain) + if nDomain == blockDomain { + assert.Fail(t, "bid %s with ad domain %s is not blocked", bid.bid.ID, adomain) + } + } + } + } + + // verify this bid not belongs to rejected list + for _, rejectedBidID := range tt.want.rejectedBidIds { + if rejectedBidID == bid.bid.ID { + assert.Fail(t, "Bid ID [%s] is not expected in list of rejected bids", bid.bid.ID) + } + } + } + } + }) + } +} + +func TestNormalizeDomain(t *testing.T) { + type args struct { + domain string + } + type want struct { + domain string + err error + } + tests := []struct { + name string + args args + want want + }{ + {name: "a.com", args: args{domain: "a.com"}, want: want{domain: "a.com"}}, + {name: "http://a.com", args: args{domain: "http://a.com"}, want: want{domain: "a.com"}}, + {name: "https://a.com", args: args{domain: "https://a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com", args: args{domain: "https://www.a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com/my/page?k=1", args: args{domain: "https://www.a.com/my/page?k=1"}, want: want{domain: "a.com"}}, + {name: "empty_domain", args: args{domain: ""}, want: want{domain: ""}}, + {name: "trim_domain", args: args{domain: " trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "trim_domain_with_http_in_it", args: args{domain: " http://trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "https://www.something.a.com/my/page?k=1", args: args{domain: "https://www.something.a.com/my/page?k=1"}, want: want{domain: "something.a.com"}}, + {name: "wWW.something.a.com", args: args{domain: "wWW.something.a.com"}, want: want{domain: "something.a.com"}}, + {name: "2_times_www", args: args{domain: "www.something.www.a.com"}, want: want{domain: "something.www.a.com"}}, + {name: "consecutive_www", args: args{domain: "www.www.something.a.com"}, want: want{domain: "www.something.a.com"}}, + {name: "abchttp.com", args: args{domain: "abchttp.com"}, want: want{domain: "abchttp.com"}}, + {name: "HTTP://CAPS.com", args: args{domain: "HTTP://CAPS.com"}, want: want{domain: "caps.com"}}, + + // publicsuffix + {name: "co.in", args: args{domain: "co.in"}, want: want{domain: "", err: fmt.Errorf("domain [co.in] is public suffix")}}, + {name: ".co.in", args: args{domain: ".co.in"}, want: want{domain: ".co.in"}}, + {name: "amazon.co.in", args: args{domain: "amazon.co.in"}, want: want{domain: "amazon.co.in"}}, + // we wont check if shriprasad belongs to icann + {name: "shriprasad", args: args{domain: "shriprasad"}, want: want{domain: "", err: fmt.Errorf("domain [shriprasad] is public suffix")}}, + {name: ".shriprasad", args: args{domain: ".shriprasad"}, want: want{domain: ".shriprasad"}}, + {name: "abc.shriprasad", args: args{domain: "abc.shriprasad"}, want: want{domain: "abc.shriprasad"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adjustedDomain, err := normalizeDomain(tt.args.domain) + actualErr := "nil" + expectedErr := "nil" + if nil != err { + actualErr = err.Error() + } + if nil != tt.want.err { + actualErr = tt.want.err.Error() + } + assert.Equal(t, tt.want.err, err, "Expected error is %s, but found [%s]", expectedErr, actualErr) + assert.Equal(t, tt.want.domain, adjustedDomain, "Expected domain is %s, but found [%s]", tt.want.domain, adjustedDomain) + }) + } +} + +func newTestTagAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: vastbidder.NewTagBidder(openrtb_ext.BidderName(name), config.Adapter{}), + BidderName: openrtb_ext.BidderName(name), + } +} + +func newTestRtbAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: &goodSingleBidder{}, + BidderName: openrtb_ext.BidderName(name), + } +} + func TestRecordAdaptorDuplicateBidIDs(t *testing.T) { type bidderCollisions = map[string]int testCases := []struct { diff --git a/go.mod b/go.mod index 13cd3748779..dee3615b79b 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/DATA-DOG/go-sqlmock v1.3.0 github.com/NYTimes/gziphandler v1.1.1 github.com/OneOfOne/xxhash v1.2.5 // indirect - github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf + github.com/beevik/etree v1.0.2 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/blang/semver v3.5.1+incompatible github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 @@ -60,3 +60,5 @@ require ( ) replace github.com/mxmCherry/openrtb/v15 v15.0.0 => github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425063110-b01110089669 + +replace github.com/beevik/etree v1.0.2 => github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 diff --git a/go.sum b/go.sum index ce383174fb8..0ccf122d248 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425134848-adbedb6f42c6 h1: github.com/PubMatic-OpenWrap/openrtb/v15 v15.0.0-20210425134848-adbedb6f42c6/go.mod h1:XMFHmDvfVyIhz5JGzvNXVt0afDLN2mrdYviUOKIYqAo= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index a568392beba..f3cfb3df6a4 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -58,6 +58,7 @@ type ExtBidPrebidMeta struct { type ExtBidPrebidVideo struct { Duration int `json:"duration"` PrimaryCategory string `json:"primary_category"` + VASTTagID string `json:"vasttagid"` } // ExtBidPrebidEvents defines the contract for bidresponse.seatbid.bid[i].ext.prebid.events diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index ef114914cd6..7d5684600bb 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -173,6 +173,7 @@ const ( BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" BidderValueImpression BidderName = "valueimpression" + BidderVASTBidder BidderName = "vastbidder" BidderVerizonMedia BidderName = "verizonmedia" BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" @@ -286,6 +287,7 @@ func CoreBidderNames() []BidderName { BidderUnicorn, BidderUnruly, BidderValueImpression, + BidderVASTBidder, BidderVerizonMedia, BidderVisx, BidderVrtcal, diff --git a/openrtb_ext/imp_vastbidder.go b/openrtb_ext/imp_vastbidder.go new file mode 100644 index 00000000000..2923c2dd8d7 --- /dev/null +++ b/openrtb_ext/imp_vastbidder.go @@ -0,0 +1,18 @@ +package openrtb_ext + +// ExtImpVASTBidder defines the contract for bidrequest.imp[i].ext.vastbidder +type ExtImpVASTBidder struct { + Tags []*ExtImpVASTBidderTag `json:"tags,omitempty"` + Parser string `json:"parser,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Cookies map[string]string `json:"cookies,omitempty"` +} + +// ExtImpVASTBidderTag defines the contract for bidrequest.imp[i].ext.pubmatic.tags[i] +type ExtImpVASTBidderTag struct { + TagID string `json:"tagid"` + URL string `json:"url"` + Duration int `json:"dur"` + Price float64 `json:"price"` + Params map[string]interface{} `json:"params,omitempty"` +} diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index 1c7177daf49..a1d1e18d380 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -59,6 +59,7 @@ type ExtHttpCall struct { RequestHeaders map[string][]string `json:"requestheaders"` ResponseBody string `json:"responsebody"` Status int `json:"status"` + Params map[string]int `json:"params,omitempty"` } // CookieStatus describes the allowed values for bidresponse.ext.usersync.{bidder}.status diff --git a/static/bidder-info/vastbidder.yaml b/static/bidder-info/vastbidder.yaml new file mode 100644 index 00000000000..b8eb41d4e49 --- /dev/null +++ b/static/bidder-info/vastbidder.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "UOEDev@pubmatic.com" +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video diff --git a/static/bidder-params/vastbidder.json b/static/bidder-params/vastbidder.json new file mode 100644 index 00000000000..0bef9b5fd5e --- /dev/null +++ b/static/bidder-params/vastbidder.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Tag Bidder Base Adapter", + "description": "A schema which validates params accepted by the VAST tag bidders", + + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tagid": { "type": "string" }, + "url": { "type": "string" }, + "dur": { "type": "integer" }, + "price": { "type": "number" }, + "params": { "type": "object" } + }, + "required": [ "tagid", "url", "dur" ] + } + }, + "parser": { "type": "string" }, + "headers": { "type": "object" }, + "cookies": { "type": "object" } + }, + "required": ["tags"] +} \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 7e10c41cd76..10a95fb4b67 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -124,6 +124,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderSilverMob: true, openrtb_ext.BidderSmaato: true, openrtb_ext.BidderSpotX: true, + openrtb_ext.BidderVASTBidder: true, openrtb_ext.BidderUnicorn: true, openrtb_ext.BidderYeahmobi: true, }