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  }