github.com/prebid/prebid-server/v2@v2.18.0/injector/injector.go (about) 1 package injector 2 3 import ( 4 "encoding/xml" 5 "fmt" 6 "io" 7 "strings" 8 9 "github.com/prebid/prebid-server/v2/macros" 10 "github.com/prebid/prebid-server/v2/metrics" 11 ) 12 13 const ( 14 emptyAdmResponse = `<VAST version="3.0"><Ad><Wrapper><AdSystem>prebid.org wrapper</AdSystem><VASTAdTagURI><![CDATA[%s]]></VASTAdTagURI><Creatives></Creatives></Wrapper></Ad></VAST>` 15 ) 16 17 const ( 18 companionStartTag = "<Companion>" 19 companionEndTag = "</Companion>" 20 nonLinearStartTag = "<NonLinear>" 21 nonLinearEndTag = "</NonLinear>" 22 videoClickStartTag = "<VideoClicks>" 23 videoClickEndTag = "</VideoClicks>" 24 trackingEventStartTag = "<TrackingEvents>" 25 trackingEventEndTag = "</TrackingEvents>" 26 clickTrackingStartTag = "<ClickTracking><![CDATA[" 27 clickTrackingEndTag = "]]></ClickTracking>" 28 impressionStartTag = "<Impression><![CDATA[" 29 impressionEndTag = "]]></Impression>" 30 errorStartTag = "<Error><![CDATA[" 31 errorEndTag = "]]></Error>" 32 nonLinearClickTrackingStartTag = "<NonLinearClickTracking><![CDATA[" 33 nonLinearClickTrackingEndTag = "]]></NonLinearClickTracking>" 34 companionClickThroughStartTag = "<CompanionClickThrough><![CDATA[" 35 companionClickThroughEndTag = "]]></CompanionClickThrough>" 36 tracking = "tracking" 37 companionclickthrough = "companionclickthrough" 38 nonlinearclicktracking = "nonlinearclicktracking" 39 impression = "impression" 40 err = "error" 41 clicktracking = "clicktracking" 42 adId = "adid" 43 trackingStartTag = `<Tracking event="%s"><![CDATA[` 44 trackingEndTag = "]]></Tracking>" 45 ) 46 47 const ( 48 inlineCase = "InLine" 49 wrapperCase = "Wrapper" 50 creativeCase = "Creative" 51 linearCase = "Linear" 52 nonLinearCase = "NonLinear" 53 videoClicksCase = "VideoClicks" 54 nonLinearAdsCase = "NonLinearAds" 55 trackingEventsCase = "TrackingEvents" 56 impressionCase = "Impression" 57 errorCase = "Error" 58 companionCase = "Companion" 59 companionAdsCase = "CompanionAds" 60 ) 61 62 type Injector interface { 63 InjectTracker(vastXML string, NURL string) string 64 } 65 66 type VASTEvents struct { 67 Errors []string 68 Impressions []string 69 VideoClicks []string 70 NonLinearClickTracking []string 71 CompanionClickThrough []string 72 TrackingEvents map[string][]string 73 } 74 75 type InjectionState struct { 76 injectTracker bool 77 injectVideoClicks bool 78 inlineWrapperTagFound bool 79 wrapperTagFound bool 80 impressionTagFound bool 81 errorTagFound bool 82 creativeId string 83 isCreative bool 84 companionTagFound bool 85 nonLinearTagFound bool 86 } 87 88 type TrackerInjector struct { 89 replacer macros.Replacer 90 events VASTEvents 91 me metrics.MetricsEngine 92 provider *macros.MacroProvider 93 } 94 95 var trimRunes = "\t\r\b\n " 96 97 func NewTrackerInjector(replacer macros.Replacer, provider *macros.MacroProvider, events VASTEvents) *TrackerInjector { 98 return &TrackerInjector{ 99 replacer: replacer, 100 provider: provider, 101 events: events, 102 } 103 } 104 105 func (trackerinjector *TrackerInjector) InjectTracker(vastXML string, NURL string) (string, error) { 106 if vastXML == "" && NURL == "" { 107 // TODO Log a adapter.<bidder-name>.requests.badserverresponse 108 return vastXML, fmt.Errorf("invalid Vast XML") 109 } 110 111 if vastXML == "" { 112 return fmt.Sprintf(emptyAdmResponse, NURL), nil 113 } 114 115 var outputXML strings.Builder 116 encoder := xml.NewEncoder(&outputXML) 117 state := &InjectionState{ 118 injectTracker: false, 119 injectVideoClicks: false, 120 inlineWrapperTagFound: false, 121 wrapperTagFound: false, 122 impressionTagFound: false, 123 errorTagFound: false, 124 creativeId: "", 125 isCreative: false, 126 companionTagFound: false, 127 nonLinearTagFound: false, 128 } 129 130 reader := strings.NewReader(vastXML) 131 decoder := xml.NewDecoder(reader) 132 133 for { 134 rawToken, err := decoder.RawToken() 135 if err != nil { 136 if err == io.EOF { 137 break 138 } else { 139 return "", fmt.Errorf("XML processing error: %w", err) 140 } 141 } 142 143 switch token := rawToken.(type) { 144 case xml.StartElement: 145 err = trackerinjector.handleStartElement(token, state, &outputXML, encoder) 146 case xml.EndElement: 147 err = trackerinjector.handleEndElement(token, state, &outputXML, encoder) 148 case xml.CharData: 149 charData := strings.Trim(string(token), trimRunes) 150 if len(charData) != 0 { 151 err = encoder.Flush() 152 outputXML.WriteString("<![CDATA[" + charData + "]]>") 153 } 154 default: 155 err = encoder.EncodeToken(rawToken) 156 } 157 158 if err != nil { 159 return "", fmt.Errorf("XML processing error: %w", err) 160 } 161 } 162 163 if err := encoder.Flush(); err != nil { 164 return "", fmt.Errorf("XML processing error: %w", err) 165 } 166 167 if !state.inlineWrapperTagFound { 168 // Todo log adapter.<bidder-name>.requests.badserverresponse metrics 169 return vastXML, fmt.Errorf("invalid VastXML, inline/wrapper tag not found") 170 } 171 return outputXML.String(), nil 172 } 173 174 func (trackerinjector *TrackerInjector) handleStartElement(token xml.StartElement, state *InjectionState, outputXML *strings.Builder, encoder *xml.Encoder) error { 175 var err error 176 switch token.Name.Local { 177 case wrapperCase: 178 state.wrapperTagFound = true 179 if err = encoder.EncodeToken(token); err != nil { 180 return err 181 } 182 case creativeCase: 183 state.isCreative = true 184 for _, attr := range token.Attr { 185 if strings.ToLower(attr.Name.Local) == adId { 186 state.creativeId = attr.Value 187 } 188 } 189 if err = encoder.EncodeToken(token); err != nil { 190 return err 191 } 192 case linearCase: 193 state.injectVideoClicks = true 194 state.injectTracker = true 195 if err = encoder.EncodeToken(token); err != nil { 196 return err 197 } 198 case videoClicksCase: 199 state.injectVideoClicks = false 200 if err = encoder.EncodeToken(token); err != nil { 201 return err 202 } 203 if err = encoder.Flush(); err != nil { 204 return err 205 } 206 trackerinjector.addClickTrackingEvent(outputXML, state.creativeId, false) 207 case nonLinearAdsCase: 208 state.injectTracker = true 209 if err = encoder.EncodeToken(token); err != nil { 210 return err 211 } 212 case trackingEventsCase: 213 if state.isCreative { 214 state.injectTracker = false 215 if err = encoder.EncodeToken(token); err != nil { 216 return err 217 } 218 if err = encoder.Flush(); err != nil { 219 return err 220 } 221 trackerinjector.addTrackingEvent(outputXML, state.creativeId, false) 222 } 223 default: 224 if err = encoder.EncodeToken(token); err != nil { 225 return err 226 } 227 } 228 return nil 229 } 230 231 func (trackerinjector *TrackerInjector) handleEndElement(token xml.EndElement, state *InjectionState, outputXML *strings.Builder, encoder *xml.Encoder) error { 232 var err error 233 switch token.Name.Local { 234 case impressionCase: 235 if err = encoder.EncodeToken(token); err != nil { 236 return err 237 } 238 if err = encoder.Flush(); err != nil { 239 return err 240 } 241 if !state.impressionTagFound { 242 trackerinjector.addImpressionTrackingEvent(outputXML) 243 state.impressionTagFound = true 244 } 245 case errorCase: 246 if err = encoder.EncodeToken(token); err != nil { 247 return err 248 } 249 if err = encoder.Flush(); err != nil { 250 return err 251 } 252 if !state.errorTagFound { 253 trackerinjector.addErrorTrackingEvent(outputXML) 254 state.errorTagFound = true 255 } 256 case nonLinearAdsCase: 257 if state.injectTracker { 258 state.injectTracker = false 259 if err = encoder.Flush(); err != nil { 260 return err 261 } 262 trackerinjector.addTrackingEvent(outputXML, state.creativeId, true) 263 if !state.nonLinearTagFound && state.wrapperTagFound { 264 trackerinjector.addNonLinearClickTrackingEvent(outputXML, state.creativeId, true) 265 } 266 if err = encoder.EncodeToken(token); err != nil { 267 return err 268 } 269 } 270 case linearCase: 271 if state.injectVideoClicks { 272 state.injectVideoClicks = false 273 if err = encoder.Flush(); err != nil { 274 return err 275 } 276 trackerinjector.addClickTrackingEvent(outputXML, state.creativeId, true) 277 } 278 if state.injectTracker { 279 state.injectTracker = false 280 if err = encoder.Flush(); err != nil { 281 return err 282 } 283 trackerinjector.addTrackingEvent(outputXML, state.creativeId, true) 284 } 285 encoder.EncodeToken(token) 286 case inlineCase, wrapperCase: 287 state.wrapperTagFound = false 288 state.inlineWrapperTagFound = true 289 if err = encoder.Flush(); err != nil { 290 return err 291 } 292 if !state.impressionTagFound { 293 trackerinjector.addImpressionTrackingEvent(outputXML) 294 } 295 state.impressionTagFound = false 296 if !state.errorTagFound { 297 trackerinjector.addErrorTrackingEvent(outputXML) 298 } 299 state.errorTagFound = false 300 if err = encoder.EncodeToken(token); err != nil { 301 return err 302 } 303 case nonLinearCase: 304 if err = encoder.Flush(); err != nil { 305 return err 306 } 307 trackerinjector.addNonLinearClickTrackingEvent(outputXML, state.creativeId, false) 308 state.nonLinearTagFound = true 309 if err = encoder.EncodeToken(token); err != nil { 310 return err 311 } 312 case companionCase: 313 state.companionTagFound = true 314 if err = encoder.Flush(); err != nil { 315 return err 316 } 317 trackerinjector.addCompanionClickThroughEvent(outputXML, state.creativeId, false) 318 if err = encoder.EncodeToken(token); err != nil { 319 return err 320 } 321 case creativeCase: 322 state.isCreative = false 323 if err = encoder.EncodeToken(token); err != nil { 324 return err 325 } 326 case companionAdsCase: 327 if !state.companionTagFound && state.wrapperTagFound { 328 if err = encoder.Flush(); err != nil { 329 return err 330 } 331 trackerinjector.addCompanionClickThroughEvent(outputXML, state.creativeId, true) 332 } 333 if err = encoder.EncodeToken(token); err != nil { 334 return err 335 } 336 default: 337 if err = encoder.EncodeToken(token); err != nil { 338 return err 339 } 340 } 341 return nil 342 } 343 344 func (trackerinjector *TrackerInjector) addTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { 345 if addParentTag { 346 outputXML.WriteString(trackingEventStartTag) 347 } 348 for typ, urls := range trackerinjector.events.TrackingEvents { 349 trackerinjector.writeTrackingEvent(urls, outputXML, fmt.Sprintf(trackingStartTag, typ), trackingEndTag, creativeId, typ, tracking) 350 } 351 if addParentTag { 352 outputXML.WriteString(trackingEventEndTag) 353 } 354 } 355 356 func (trackerinjector *TrackerInjector) addClickTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { 357 if addParentTag { 358 outputXML.WriteString(videoClickStartTag) 359 } 360 trackerinjector.writeTrackingEvent(trackerinjector.events.VideoClicks, outputXML, clickTrackingStartTag, clickTrackingEndTag, creativeId, "", clicktracking) 361 if addParentTag { 362 outputXML.WriteString(videoClickEndTag) 363 } 364 } 365 366 func (trackerinjector *TrackerInjector) addImpressionTrackingEvent(outputXML *strings.Builder) { 367 trackerinjector.writeTrackingEvent(trackerinjector.events.Impressions, outputXML, impressionStartTag, impressionEndTag, "", "", impression) 368 } 369 370 func (trackerinjector *TrackerInjector) addErrorTrackingEvent(outputXML *strings.Builder) { 371 trackerinjector.writeTrackingEvent(trackerinjector.events.Errors, outputXML, errorStartTag, errorEndTag, "", "", err) 372 } 373 374 func (trackerinjector *TrackerInjector) addNonLinearClickTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { 375 if addParentTag { 376 outputXML.WriteString(nonLinearStartTag) 377 } 378 trackerinjector.writeTrackingEvent(trackerinjector.events.NonLinearClickTracking, outputXML, nonLinearClickTrackingStartTag, nonLinearClickTrackingEndTag, creativeId, "", nonlinearclicktracking) 379 if addParentTag { 380 outputXML.WriteString(nonLinearEndTag) 381 } 382 } 383 384 func (trackerinjector *TrackerInjector) addCompanionClickThroughEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { 385 if addParentTag { 386 outputXML.WriteString(companionStartTag) 387 } 388 trackerinjector.writeTrackingEvent(trackerinjector.events.CompanionClickThrough, outputXML, companionClickThroughStartTag, companionClickThroughEndTag, creativeId, "", companionclickthrough) 389 if addParentTag { 390 outputXML.WriteString(companionEndTag) 391 } 392 } 393 394 func (trackerinjector *TrackerInjector) writeTrackingEvent(urls []string, outputXML *strings.Builder, startTag, endTag, creativeId, eventType, vastEvent string) { 395 trackerinjector.provider.PopulateEventMacros(creativeId, eventType, vastEvent) 396 for _, url := range urls { 397 outputXML.WriteString(startTag) 398 trackerinjector.replacer.Replace(outputXML, url, trackerinjector.provider) 399 outputXML.WriteString(endTag) 400 } 401 }