@@ -11,6 +11,7 @@ import {
1111 isNumber ,
1212 isStr ,
1313 logError ,
14+ logWarn ,
1415 parseQueryStringParameters ,
1516 parseUrl
1617} from '../src/utils.js' ;
@@ -57,7 +58,7 @@ export const spec = {
5758 */
5859 isBidRequestValid : function ( bid ) {
5960 return ! ! ( bid && bid . adUnitCode && bid . bidId && ( hasBannerMediaType ( bid ) || hasVideoMediaType ( bid ) ) &&
60- validateVideoParams ( bid ) ) ;
61+ validateVideoParams ( bid ) && validateBlocklistParams ( bid ) ) ;
6162 } ,
6263
6364 /**
@@ -145,6 +146,18 @@ export const spec = {
145146 if ( eids . length ) {
146147 serverRequest . eids = JSON . stringify ( eids ) ;
147148 } ;
149+
150+ // Blocklists (request-level): merge ortb2 + params, send as comma-delimited
151+ // params per the ad server's prebid-js endpoint (AS-5349). Omitted when empty.
152+ const bcat = getBlocklist ( bidderRequest , bannerBidRequests [ 0 ] , 'bcat' ) ;
153+ if ( bcat . length ) {
154+ serverRequest . bcat = bcat . join ( ',' ) ;
155+ }
156+ const badv = getBlocklist ( bidderRequest , bannerBidRequests [ 0 ] , 'badv' ) ;
157+ if ( badv . length ) {
158+ serverRequest . badv = badv . join ( ',' ) ;
159+ }
160+
148161 // check if url exceeded max length
149162 const fullUrl = `${ bannerUrl } ?${ parseQueryStringParameters ( serverRequest ) } ` ;
150163 let extraCharacters = fullUrl . length - MAX_BANNER_REQUEST_URL_LENGTH ;
@@ -392,6 +405,40 @@ function getId(request, idType) {
392405 return ( typeof deepAccess ( request , 'userId' ) === 'object' ) ? request . userId [ idType ] : undefined ;
393406}
394407
408+ /**
409+ * Resolve a request-level blocklist field (bcat/badv) from its two sources — the
410+ * standardized ORTB global (`bidderRequest.ortb2.<field>`) and the Yieldmo-specific
411+ * param (`bid.params.<field>`) — into a single deduped array of strings. Neither
412+ * source is allowed to silently win (union, not precedence). Invalid values are
413+ * ignored and logged rather than dropping the bid: a source that isn't an array, and
414+ * any non-string/empty element, are filtered out (with a warning) so a
415+ * misconfiguration is surfaced without losing the impression.
416+ * @param {BidderRequest } bidderRequest bidder request (source of ortb2.<field>)
417+ * @param {BidRequest } bid bid request (source of params.<field>)
418+ * @param {string } field blocklist field name — 'bcat' or 'badv'
419+ * @return {string[] } deduped, trimmed, non-empty string entries (possibly empty)
420+ */
421+ function getBlocklist ( bidderRequest , bid , field ) {
422+ const normalize = ( value , source ) => {
423+ if ( value === undefined || value === null ) {
424+ return [ ] ;
425+ }
426+ if ( ! isArray ( value ) ) {
427+ logWarn ( `yieldmo: ignoring ${ source } blocklist value; expected an array of strings, got ${ JSON . stringify ( value ) } ` ) ;
428+ return [ ] ;
429+ }
430+ const dropped = value . filter ( item => ! isStr ( item ) || ! item . trim ( ) ) ;
431+ if ( dropped . length ) {
432+ logWarn ( `yieldmo: ignoring invalid ${ source } blocklist entries (expected non-empty strings): ${ JSON . stringify ( dropped ) } ` ) ;
433+ }
434+ return value . filter ( item => isStr ( item ) && item . trim ( ) ) . map ( item => item . trim ( ) ) ;
435+ } ;
436+ return [ ...new Set ( [
437+ ...normalize ( deepAccess ( bidderRequest , `ortb2.${ field } ` ) , 'ortb2' ) ,
438+ ...normalize ( deepAccess ( bid , `params.${ field } ` ) , 'params' ) ,
439+ ] ) ] ;
440+ }
441+
395442/**
396443 * @param {BidRequest[] } bidRequests bid request object
397444 * @param {BidderRequest } bidderRequest bidder request object
@@ -406,8 +453,8 @@ function openRtbRequest(bidRequests, bidderRequest) {
406453 imp : bidRequests . map ( bidRequest => openRtbImpression ( bidRequest ) ) ,
407454 site : openRtbSite ( bidRequests [ 0 ] , bidderRequest ) ,
408455 device : deepAccess ( bidderRequest , 'ortb2.device' ) ,
409- badv : bidRequests [ 0 ] . params . badv || [ ] ,
410- bcat : deepAccess ( bidderRequest , 'bcat' ) || bidRequests [ 0 ] . params . bcat || [ ] ,
456+ badv : getBlocklist ( bidderRequest , bidRequests [ 0 ] , 'badv' ) ,
457+ bcat : getBlocklist ( bidderRequest , bidRequests [ 0 ] , 'bcat' ) ,
411458 ext : {
412459 prebid : '$prebid.version$' ,
413460 } ,
@@ -587,6 +634,54 @@ function populateOpenRtbGdpr(openRtbRequest, bidderRequest) {
587634 }
588635}
589636
637+ const isDefined = val => typeof val !== 'undefined' ;
638+
639+ const paramRequired = ( paramStr , value , conditionStr ) => {
640+ let error = `"${ paramStr } " is required` ;
641+ if ( conditionStr ) {
642+ error += ' when ' + conditionStr ;
643+ }
644+ throw new Error ( error ) ;
645+ } ;
646+
647+ const paramInvalid = ( paramStr , value , expectedStr ) => {
648+ expectedStr = expectedStr ? ', expected: ' + expectedStr : '' ;
649+ value = JSON . stringify ( value ) ;
650+ throw new Error ( `"${ paramStr } "=${ value } is invalid${ expectedStr } ` ) ;
651+ } ;
652+
653+ /**
654+ * Build a field validator bound to a bid. `video.*` paths are checked against both
655+ * `params.video.*` and `mediaTypes.video.*`; any other path is read directly.
656+ * The error callback (paramRequired/paramInvalid) throws, so callers wrap in try/catch.
657+ * @param {BidRequest } bid bid request
658+ * @return {(fieldPath: string, validateCb: Function, errorCb: Function, errorCbParam?: string) => * }
659+ */
660+ const createParamValidator = ( bid ) => ( fieldPath , validateCb , errorCb , errorCbParam ) => {
661+ if ( fieldPath . indexOf ( 'video' ) === 0 ) {
662+ const valueFieldPath = 'params.' + fieldPath ;
663+ const mediaFieldPath = 'mediaTypes.' + fieldPath ;
664+ const valueParams = deepAccess ( bid , valueFieldPath ) ;
665+ const mediaTypesParams = deepAccess ( bid , mediaFieldPath ) ;
666+ const hasValidValueParams = validateCb ( valueParams ) ;
667+ const hasValidMediaTypesParams = validateCb ( mediaTypesParams ) ;
668+
669+ if ( hasValidValueParams ) return valueParams ;
670+ else if ( hasValidMediaTypesParams ) return mediaTypesParams ;
671+ else {
672+ if ( ! hasValidValueParams ) errorCb ( valueFieldPath , valueParams , errorCbParam ) ;
673+ else if ( ! hasValidMediaTypesParams ) errorCb ( mediaFieldPath , mediaTypesParams , errorCbParam ) ;
674+ }
675+ return valueParams || mediaTypesParams ;
676+ } else {
677+ const value = deepAccess ( bid , fieldPath ) ;
678+ if ( ! validateCb ( value ) ) {
679+ errorCb ( fieldPath , value , errorCbParam ) ;
680+ }
681+ return value ;
682+ }
683+ } ;
684+
590685/**
591686 * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true.
592687 * @param {object } bid bid to validate
@@ -596,55 +691,16 @@ function validateVideoParams(bid) {
596691 if ( ! hasVideoMediaType ( bid ) ) {
597692 return true ;
598693 }
599-
600- const paramRequired = ( paramStr , value , conditionStr ) => {
601- let error = `"${ paramStr } " is required` ;
602- if ( conditionStr ) {
603- error += ' when ' + conditionStr ;
604- }
605- throw new Error ( error ) ;
606- } ;
607-
608- const paramInvalid = ( paramStr , value , expectedStr ) => {
609- expectedStr = expectedStr ? ', expected: ' + expectedStr : '' ;
610- value = JSON . stringify ( value ) ;
611- throw new Error ( `"${ paramStr } "=${ value } is invalid${ expectedStr } ` ) ;
612- } ;
613-
614- const isDefined = val => typeof val !== 'undefined' ;
615- const validate = ( fieldPath , validateCb , errorCb , errorCbParam ) => {
616- if ( fieldPath . indexOf ( 'video' ) === 0 ) {
617- const valueFieldPath = 'params.' + fieldPath ;
618- const mediaFieldPath = 'mediaTypes.' + fieldPath ;
619- const valueParams = deepAccess ( bid , valueFieldPath ) ;
620- const mediaTypesParams = deepAccess ( bid , mediaFieldPath ) ;
621- const hasValidValueParams = validateCb ( valueParams ) ;
622- const hasValidMediaTypesParams = validateCb ( mediaTypesParams ) ;
623-
624- if ( hasValidValueParams ) return valueParams ;
625- else if ( hasValidMediaTypesParams ) return hasValidMediaTypesParams ;
626- else {
627- if ( ! hasValidValueParams ) errorCb ( valueFieldPath , valueParams , errorCbParam ) ;
628- else if ( ! hasValidMediaTypesParams ) errorCb ( mediaFieldPath , mediaTypesParams , errorCbParam ) ;
629- }
630- return valueParams || mediaTypesParams ;
631- } else {
632- const value = deepAccess ( bid , fieldPath ) ;
633- if ( ! validateCb ( value ) ) {
634- errorCb ( fieldPath , value , errorCbParam ) ;
635- }
636- return value ;
637- }
638- } ;
694+ const validate = createParamValidator ( bid ) ;
639695
640696 try {
641697 validate ( 'video.context' , val => ! isEmpty ( val ) , paramRequired ) ;
642698
643699 validate ( 'params.placementId' , val => ! isEmpty ( val ) , paramRequired ) ;
644700
645- validate ( 'video.playerSize' , val => isArrayOfNums ( val , 2 ) ||
646- ( isArray ( val ) && val . every ( v => isArrayOfNums ( v , 2 ) ) ) ,
647- paramInvalid , 'array of 2 integers, ex: [640,480] or [[640,480]]' ) ;
701+ validate ( 'video.playerSize' ,
702+ val => isArrayOfNums ( val , 2 ) || ( isArray ( val ) && val . every ( v => isArrayOfNums ( v , 2 ) ) ) ,
703+ paramInvalid , 'array of 2 integers, ex: [640,480] or [[640,480]]' ) ;
648704
649705 validate ( 'video.mimes' , val => isDefined ( val ) , paramRequired ) ;
650706 validate ( 'video.mimes' , val => isArray ( val ) && val . every ( v => isStr ( v ) ) , paramInvalid ,
@@ -665,10 +721,28 @@ function validateVideoParams(bid) {
665721 validate ( 'video.skippable' , val => ! isDefined ( val ) || isBoolean ( val ) , paramInvalid ) ;
666722 validate ( 'video.skipafter' , val => ! isDefined ( val ) || isNumber ( val ) , paramInvalid ) ;
667723 validate ( 'video.pos' , val => ! isDefined ( val ) || isNumber ( val ) , paramInvalid ) ;
668- validate ( 'params.badv' , val => ! isDefined ( val ) || isArray ( val ) , paramInvalid ,
669- 'array of strings, ex: ["ford.com","pepsi.com"]' ) ;
724+ return true ;
725+ } catch ( e ) {
726+ logError ( e . message ) ;
727+ return false ;
728+ }
729+ }
730+
731+ /**
732+ * Validate the publisher-set blocklist params (`bcat`/`badv`) for all media types
733+ * (banner and video). A missing value is allowed; a value that is present but is
734+ * not an array is rejected (drops the bid) — the agreed middle ground between
735+ * dropping nothing and dropping over an absent optional field. See FS-12403.
736+ * @param {BidRequest } bid bid request
737+ * @return {boolean } true if valid (or absent), false if present but malformed
738+ */
739+ function validateBlocklistParams ( bid ) {
740+ const validate = createParamValidator ( bid ) ;
741+ try {
670742 validate ( 'params.bcat' , val => ! isDefined ( val ) || isArray ( val ) , paramInvalid ,
671743 'array of strings, ex: ["IAB1-5","IAB1-6"]' ) ;
744+ validate ( 'params.badv' , val => ! isDefined ( val ) || isArray ( val ) , paramInvalid ,
745+ 'array of strings, ex: ["ford.com","pepsi.com"]' ) ;
672746 return true ;
673747 } catch ( e ) {
674748 logError ( e . message ) ;
0 commit comments