1- import { logError , logWarn , mergeDeep } from '../src/utils.js' ;
1+ import { logError , logWarn , mergeDeep , isEmpty , safeJSONParse , logInfo , hasDeviceAccess } from '../src/utils.js' ;
22import { getRefererInfo } from '../src/refererDetection.js' ;
33import { submodule } from '../src/hook.js' ;
44import { GreedyPromise } from '../src/utils/promise.js' ;
5+ import { config } from '../src/config.js' ;
6+ import { getCoreStorageManager } from '../src/storageManager.js' ;
7+ import { includes } from '../src/polyfill.js' ;
8+ import { gdprDataHandler } from '../src/adapterManager.js' ;
59
10+ const MODULE_NAME = 'topicsFpd' ;
11+ const DEFAULT_EXPIRATION_DAYS = 21 ;
12+ const TCF_REQUIRED_PURPOSES = [ '1' , '2' , '3' , '4' ] ;
13+ let HAS_GDPR_CONSENT = true ;
14+ let LOAD_TOPICS_INITIALISE = false ;
15+ const HAS_DEVICE_ACCESS = hasDeviceAccess ( ) ;
16+
17+ const bidderIframeList = {
18+ maxTopicCaller : 1 ,
19+ bidders : [ {
20+ bidder : 'pubmatic' ,
21+ iframeURL : 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html'
22+ } ]
23+ }
24+ export const coreStorage = getCoreStorageManager ( MODULE_NAME ) ;
25+ export const topicStorageName = 'prebid:topics' ;
26+ export const lastUpdated = 'lastUpdated' ;
27+
28+ const iframeLoadedURL = [ ] ;
629const TAXONOMIES = {
730 // map from topic taxonomyVersion to IAB segment taxonomy
831 '1' : 600
@@ -17,6 +40,20 @@ function partitionBy(field, items) {
1740 } , { } ) ;
1841}
1942
43+ /**
44+ * function to get list of loaded Iframes calling Topics API
45+ */
46+ function getLoadedIframeURL ( ) {
47+ return iframeLoadedURL ;
48+ }
49+
50+ /**
51+ * function to set/push iframe in the list which is loaded to called topics API.
52+ */
53+ function setLoadedIframeURL ( url ) {
54+ return iframeLoadedURL . push ( url ) ;
55+ }
56+
2057export function getTopicsData ( name , topics , taxonomies = TAXONOMIES ) {
2158 return Object . entries ( partitionBy ( 'taxonomyVersion' , topics ) )
2259 . filter ( ( [ taxonomyVersion ] ) => {
@@ -61,7 +98,12 @@ export function getTopics(doc = document) {
6198const topicsData = getTopics ( ) . then ( ( topics ) => getTopicsData ( getRefererInfo ( ) . domain , topics ) ) ;
6299
63100export function processFpd ( config , { global} , { data = topicsData } = { } ) {
101+ if ( ! LOAD_TOPICS_INITIALISE ) {
102+ loadTopicsForBidders ( ) ;
103+ LOAD_TOPICS_INITIALISE = true ;
104+ }
64105 return data . then ( ( data ) => {
106+ data = [ ] . concat ( data , getCachedTopics ( ) ) ; // Add cached data in FPD data.
65107 if ( data . length ) {
66108 mergeDeep ( global , {
67109 user : {
@@ -73,6 +115,145 @@ export function processFpd(config, {global}, {data = topicsData} = {}) {
73115 } ) ;
74116}
75117
118+ /**
119+ * function to fetch the cached topic data from storage for bidders and return it
120+ */
121+ export function getCachedTopics ( ) {
122+ let cachedTopicData = [ ] ;
123+ if ( ! HAS_GDPR_CONSENT || ! HAS_DEVICE_ACCESS ) {
124+ return cachedTopicData ;
125+ }
126+ const topics = config . getConfig ( 'userSync.topics' ) || bidderIframeList ;
127+ const bidderList = topics . bidders || [ ] ;
128+ let storedSegments = new Map ( safeJSONParse ( coreStorage . getDataFromLocalStorage ( topicStorageName ) ) ) ;
129+ storedSegments && storedSegments . forEach ( ( value , cachedBidder ) => {
130+ // Check bidder exist in config for cached bidder data and then only retrieve the cached data
131+ let bidderConfigObj = bidderList . find ( ( { bidder} ) => cachedBidder == bidder )
132+ if ( bidderConfigObj ) {
133+ if ( ! isCachedDataExpired ( value [ lastUpdated ] , bidderConfigObj ?. expiry || DEFAULT_EXPIRATION_DAYS ) ) {
134+ Object . keys ( value ) . forEach ( ( segData ) => {
135+ segData != lastUpdated && cachedTopicData . push ( value [ segData ] ) ;
136+ } )
137+ } else {
138+ // delete the specific bidder map from the store and store the updated maps
139+ storedSegments . delete ( cachedBidder ) ;
140+ coreStorage . setDataInLocalStorage ( topicStorageName , JSON . stringify ( [ ...storedSegments ] ) ) ;
141+ }
142+ }
143+ } ) ;
144+ return cachedTopicData ;
145+ }
146+
147+ /**
148+ * Recieve messages from iframe loaded for bidders to fetch topic
149+ * @param {MessageEvent } evt
150+ */
151+ export function receiveMessage ( evt ) {
152+ if ( evt && evt . data ) {
153+ try {
154+ let data = safeJSONParse ( evt . data ) ;
155+ if ( includes ( getLoadedIframeURL ( ) , evt . origin ) && data && data . segment && ! isEmpty ( data . segment . topics ) ) {
156+ const { domain, topics, bidder} = data . segment ;
157+ const iframeTopicsData = getTopicsData ( domain , topics ) [ 0 ] ;
158+ iframeTopicsData && storeInLocalStorage ( bidder , iframeTopicsData ) ;
159+ }
160+ } catch ( err ) { }
161+ }
162+ }
163+
164+ /**
165+ Function to store Topics data recieved from iframe in storage(name: "prebid:topics")
166+ * @param {Topics } topics
167+ */
168+ export function storeInLocalStorage ( bidder , topics ) {
169+ const storedSegments = new Map ( safeJSONParse ( coreStorage . getDataFromLocalStorage ( topicStorageName ) ) ) ;
170+ if ( storedSegments . has ( bidder ) ) {
171+ storedSegments . get ( bidder ) [ topics [ 'ext' ] [ 'segclass' ] ] = topics ;
172+ storedSegments . get ( bidder ) [ lastUpdated ] = new Date ( ) . getTime ( ) ;
173+ storedSegments . set ( bidder , storedSegments . get ( bidder ) ) ;
174+ } else {
175+ storedSegments . set ( bidder , { [ topics . ext . segclass ] : topics , [ lastUpdated ] : new Date ( ) . getTime ( ) } )
176+ }
177+ coreStorage . setDataInLocalStorage ( topicStorageName , JSON . stringify ( [ ...storedSegments ] ) ) ;
178+ }
179+
180+ function isCachedDataExpired ( storedTime , cacheTime ) {
181+ const _MS_PER_DAY = 1000 * 60 * 60 * 24 ;
182+ const currentTime = new Date ( ) . getTime ( ) ;
183+ const daysDifference = Math . ceil ( ( currentTime - storedTime ) / _MS_PER_DAY ) ;
184+ return daysDifference > cacheTime ;
185+ }
186+
187+ /**
188+ * Function to get random bidders based on count passed with array of bidders
189+ **/
190+ function getRandomBidders ( arr , count ) {
191+ return ( [ ...arr ] . sort ( ( ) => 0.5 - Math . random ( ) ) ) . slice ( 0 , count )
192+ }
193+
194+ /**
195+ * function to add listener for message receiving from IFRAME
196+ */
197+ function listenMessagesFromTopicIframe ( ) {
198+ window . addEventListener ( 'message' , receiveMessage , false ) ;
199+ }
200+
201+ function checkTCFv2 ( vendorData , requiredPurposes = TCF_REQUIRED_PURPOSES ) {
202+ const { gdprApplies, purpose} = vendorData ;
203+ if ( ! gdprApplies || ! purpose ) {
204+ return true ;
205+ }
206+ return requiredPurposes . map ( ( purposeNo ) => {
207+ const purposeConsent = purpose . consents ? purpose . consents [ purposeNo ] : false ;
208+ if ( purposeConsent ) {
209+ return true ;
210+ }
211+ return false ;
212+ } ) . reduce ( ( a , b ) => a && b , true ) ;
213+ }
214+
215+ export function hasGDPRConsent ( ) {
216+ // Check for GDPR consent for purpose 1,2,3,4 and return false if consent has not been given
217+ const gdprConsent = gdprDataHandler . getConsentData ( ) ;
218+ const hasGdpr = ( gdprConsent && typeof gdprConsent . gdprApplies === 'boolean' && gdprConsent . gdprApplies ) ? 1 : 0 ;
219+ const gdprConsentString = hasGdpr ? gdprConsent . consentString : '' ;
220+ if ( hasGdpr ) {
221+ if ( ( ! gdprConsentString || gdprConsentString === '' ) || ! gdprConsent . vendorData ) {
222+ return false ;
223+ }
224+ return checkTCFv2 ( gdprConsent . vendorData ) ;
225+ }
226+ return true ;
227+ }
228+
229+ /**
230+ * function to load the iframes of the bidder to load the topics data
231+ */
232+ function loadTopicsForBidders ( ) {
233+ HAS_GDPR_CONSENT = hasGDPRConsent ( ) ;
234+ if ( ! HAS_GDPR_CONSENT || ! HAS_DEVICE_ACCESS ) {
235+ logInfo ( 'Topics Module : Consent string is required to fetch the topics from third party domains.' ) ;
236+ return ;
237+ }
238+ const topics = config . getConfig ( 'userSync.topics' ) || bidderIframeList ;
239+ if ( topics ) {
240+ listenMessagesFromTopicIframe ( ) ;
241+ const randomBidders = getRandomBidders ( topics . bidders || [ ] , topics . maxTopicCaller || 1 )
242+ randomBidders && randomBidders . forEach ( ( { bidder, iframeURL } ) => {
243+ if ( bidder && iframeURL ) {
244+ let ifrm = document . createElement ( 'iframe' ) ;
245+ ifrm . name = 'ifrm_' . concat ( bidder ) ;
246+ ifrm . src = '' . concat ( iframeURL , '?bidder=' ) . concat ( bidder ) ;
247+ ifrm . style . display = 'none' ;
248+ setLoadedIframeURL ( new URL ( iframeURL ) . origin ) ;
249+ iframeURL && window . document . documentElement . appendChild ( ifrm ) ;
250+ }
251+ } )
252+ } else {
253+ logWarn ( `Topics config not defined under userSync Object` ) ;
254+ }
255+ }
256+
76257submodule ( 'firstPartyData' , {
77258 name : 'topics' ,
78259 queue : 1 ,
0 commit comments