PageSourceSearch

https://worldstar.com/videos/js/utils.js

js worldstar.com collected 2026-10-03 19:48:12 UTC 34,266 bytes, 1,028 lines download raw bytes

1/**
2 * Delays function execution till JQuery is present
3 * @param {function} method
4 * @param {argument} param
5 */
6const defer = function(method, param) {
7  if (window.jQuery) {
8    method(param);
9  } else {
10    setTimeout(function() { defer(method, param) }, 50);
11  }
12}
13
14defer (function () {
15  $('#emailSentModal').on('hide.bs.modal', function () {;
16    window.location.href = '/';
17    clearFormErrMsgs('#reset-form');
18  });
19
20
21})
22
23/**
24 * Removes error messages from form
25 * @param {HTMLElement} form
26 */
27const clearFormErrMsgs = function(form) {
28  $(form).find('.is-invalid').removeClass("is-invalid");
29  $(form).removeClass("was-validated");
30  $(form).find('.is-valid').removeClass("is-valid");
31  let feedbackDiv = $('#invalid-form-feedback');
32  feedbackDiv.text('').hide();
33}
34
35/**
36 * Sets Error message in html
37 * @param {Error} err
38 * @param {HTMLElement} errorDiv
39 */
40const setError = function(err = {}, errorDiv = '') {
41  const error = err.error ? err.error : err;
42  const message = error.message;
43  const code = error.code;
44  const errorMsg = {
45    'auth/wrong-password': 'The password or email that you provided was incorrect. Please try again',
46    'auth/user-not-found': 'The email you entered did not match our records. Please try again or sign up for an account.',
47  };
48  let feedbackDiv = errorDiv || $('#invalid-form-feedback');
49  feedbackDiv.text(errorMsg[code] || message).show(100);
50}
51
52/**
53 * Removes error text from HTMLElement
54 * @param {HTMLElement} errorDiv
55 */
56const clearError = function(errorDiv) {
57  let feedbackDiv = errorDiv || $('#invalid-form-feedback');
58  feedbackDiv.text('').hide();
59}
60
61/**
62 * Toggles password input
63 * @param {string} id
64 * @param {string} toggleImg
65 */
66const togglePwd = function(id, toggleImg) {
67  const pwd = document.getElementById(id);
68  const img = document.getElementById(toggleImg);
69  if (pwd.type === "password") {
70    img.src = 'https://worldstarhiphop.com/videos/images/icon_show.svg';
71    pwd.type = "text";
72  } else {
73    img.src = 'https://worldstarhiphop.com/videos/images/icon_hide.svg';
74    pwd.type = "password";
75  }
76}
77
78/**
79 * Closes reset email modal
80 * Opens login page
81 */
82const closeResetModal =  function() {
83  $('#emailSentModal').modal('hide');
84  window.location = '/login';
85}
86
87/**
88 * Checks if current device is Mobile
89 * @returns {Boolean}
90 */
91const isMobile = function() {
92  return (navigator.userAgent.indexOf('iPhone') != -1) || (navigator.userAgent.indexOf('Windows Phone') != -1) || (navigator.userAgent.indexOf('iPod') != -1) || (navigator.userAgent.indexOf('Kindle') != -1) || (navigator.userAgent.indexOf('Android') != -1) || (navigator.userAgent.indexOf('iPad') != -1);
93}
94
95/**
96 * Checks if current device is Desktop
97 * @returns {Boolean}
98 */
99const isDesktop = function() {
100  return (navigator.userAgent.indexOf('Macintosh') != -1) || (navigator.userAgent.indexOf('Windows NT') != -1) || (navigator.userAgent.indexOf('Windows XP') != -1);
101}
102
103/**
104 * Redirects a page based on device
105 * @param {string} type type of device
106 * @param {string} location url to reroute to
107 */
108const pageRedirect = function(type, location) {
109  if ((type == 'mobile' && isMobile()) || (type == 'desktop' && isDesktop())) {
110    document.location = location;
111  }
112}
113
114/**
115 *
116 * @param {number} retryCount Number of times to retry
117 * @returns A promise that executes in the future
118 */
119const backoff = function(retryCount) {
120  return new Promise(function(resolve) {
121    return setTimeout(resolve, 10 ** retryCount);
122  });
123}
124/**
125 *
126 * @param {string} url URL
127 * @param {object} options query parameters
128 * @param {number} maxTries Max number of retries
129 * @param {number} retryCount current retry count
130 * @param {object} err Error object
131 * @returns Response from request made
132 */
133const fetchRetry = function(url, options, maxTries = 3, retryCount = 0, err) {
134  if (maxTries === 0) {
135    return Promise.reject(err);
136  }
137  return fetch(url, options)
138    .catch(function(error) {
139      // only rejects on network failure or if anything prevented the request from completing
140      return backoff(retryCount).then(function() {
141        return fetchRetry(url, options, maxTries - 1, retryCount + 1, error);
142      })
143    })
144    .then(function(res) {
145      const status = res.status, statusText = res.statusText;
146      // retry on 404 (e.g. user not found in DB) in case we have racing condition (e.g. between saving user info and retreiving it)
147      if ((status >= 500 && status <= 599) || status === 404) {
148        // rejects for all other non-network failures
149        return backoff(retryCount).then(function() {
150          return fetchRetry(url, options, maxTries - 1, retryCount + 1, {
151            'status': status,
152            'message': statusText,
153          });
154        })
155      } else if ((status >= 400) && (status <= 499)) {
156        // https://gist.github.com/odewahn/5a5eeb23279eed6a80d7798fdb47fe91
157        return res.json()
158          .then(function(data) {
159            const error = {
160              'status': status,
161              'statusText': statusText,
162              'message': data.error,
163            };
164            return Promise.reject(error);
165          })
166          .catch(function(err) {
167            return Promise.reject(err);
168          });
169      } else {
170        return res.json();
171      }
172    })
173}
174
175/**
176 *
177 * @returns seconds in Epoch
178 */
179const getSecsInEpoch = function() {
180  // The static Date.now() method returns the number of milliseconds elapsed since January 1, 1970
181  return Math.round(Date.now() / 1000);
182}
183
184/**
185 *
186 * @param {string} cname Cookie name
187 * @returns Cookie Value
188 */
189const getCookie = function(cname) {
190  let name = cname + '=';
191  let ca = document.cookie.split(';');
192  for(let i = 0; i < ca.length; i++) {
193    let c = ca[i];
194    while (c.charAt(0) == ' ') {
195      c = c.substring(1);
196    }
197    if (c.indexOf(name) == 0) {
198      return c.substring(name.length, c.length);
199    }
200  }
201  return undefined;
202}
203
204/**
205 * Deletes cookie
206 * @param {string} cname Cookie name
207 */
208const deleteCookie = function(cname, domain=window.location.hostname) {
209  document.cookie = cname + '=; max-age=-1; path=/; domain=.'  + domain +';';
210}
211
212/**
213 * Sets cookie
214 * @param {string} cname Cookie name
215 * @param {string} cvalue Cookie value
216 * @param {number} exhrs number of hours that cookie should last for
217 * @param {string} domain domain/host name
218 */
219const setCookie = function(cname, cvalue, exhrs, domain=window?.location?.hostname) {
220  const d = new Date();
221  d.setTime(d.getTime() + (exhrs * 60 * 60 * 1000));
222  const expires = 'expires='+d.toUTCString();
223  document.cookie = cname + '=' + cvalue + ';' + expires + '; path=/; domain=' + domain + '; secure;';
224}
225
226/**
227 * Sets cookie
228 * @param {string} cname Cookie name
229 * @param {string} cvalue Cookie value
230 * @param {number} minutes number of minutes that cookie should last for
231 * @param {string} domain domain/host name
232 */
233const setCookieNew = function(cname, cvalue, minutes, domain=window?.location?.hostname) {
234  const d = new Date();
235  d.setTime(d.getTime() + (minutes * 60 * 1000));
236  const expires = 'expires='+d.toUTCString();
237  document.cookie = cname + '=' + cvalue + ';' + expires + '; path=/; domain=' + domain + '; secure;';
238  // to test locally.
239  // document.cookie = cname + '=' + cvalue + ';' + expires + '; path=/; domain=' + domain;
240}
241
242/**
243 * Toggle input editable state
244 *
245 * @param {string} id element ID
246 */
247const toggleEdit = function(id) {
248  if($(id).prop('disabled') == true) {
249    $(id).prop('disabled', false);
250  } else {
251    $(id).prop('disabled', true);
252  }
253}
254
255/**
256 * Build Ana video slot
257 * @param {function} adsCbk
258 * @param {string} videoAdUnit
259 * @returns Ana Video Slot
260 */
261const buildVidAd = function(adsCbk, videoAdUnit) {
262  return ANA.createVideoSlot(ANAWeb.VideoPlayerType.VIDEO_JS, {
263    'adPlacement': ANAWeb.VideoPlacementType.InStream,
264    'adPosition': ANAWeb.AdPosition.AboveTheFold,
265    'adsManagerLoadedCbk': adsCbk,
266    'elementID': 'video_player',
267    'adUnitID': videoAdUnit,
268    'startDelay': ANAWeb.StartDelay.PreRoll,
269    'playbackMethod': ANAWeb.PlaybackMethod.ClickSoundOn,
270    'dimensions': [[640, 480]],
271    'startdelay': 5,
272  })
273}
274
275const initDesktopVideoJS = function(vimage) {
276  return videojs("video_player" ,{
277    html5: {},
278    preload: "auto",
279    controls: true,
280    autoplay: false,
281    inactivityTimeout: 1000,
282    controlBar: {
283        durationDisplay: true,
284        currentTimeDisplay: true,
285    },
286    techOrder: ["html5"],
287    poster: vimage,
288    plugins: {
289      overlayVideo: {},
290      watermark: {
291        image: "https://worldstarhiphop.com/videos/newplayer/images/watermark.png",
292        position: "top-right",
293        fadeTime: null,
294        url: "https://worldstarhiphop.com/videos/"
295      },
296    }
297  });
298}
299
300/**
301 * Checks if username is already taken and display any errors.
302 * @param {string} username to check
303 * @param {htmlElement} element to display errors in
304 */
305const checkUserExists = function(username, element, callback) {
306  try {
307    fetchRetry('https://api-mobile.worldstar.com/users/exists?username='+username, {
308      method: 'GET',
309      headers: {
310        'Content-Type': 'application/json',
311        'Accept': 'application/json',
312      },
313    }).then(function(data) {
314      if (data.exists) {
315        // Using the bootstrap class triggers the behavior to show the existing errors
316        // So going with custom
317        element.classList.remove("is_valid");
318        element.classList.add("is_invalid");
319        setError({ 'message': data.message });
320      } else {
321        element.classList.remove("is_invalid");
322        element.classList.add("is_valid");
323        clearError();
324      }
325      if (callback && typeof callback === "function") callback(data);
326    }).catch(function(err) {
327      console.error("Error exception in checkUserExists.then:", err)
328    });
329  } catch (err) {
330    console.error("Error exception in checkUserExists:", err)
331  }
332}
333
334const randomIntFromInterval = function(min, max) { // min and max included
335  return Math.floor(Math.random() * (max - min + 1) + min);
336}
337
338const _FLAGR = '_flagr';
339const _ROLLOUT = '_rollout';
340const _TEN_YEARS = 24 * 365 * 10;
341const ASSEMBLY_UID = 'assembly_uid';
342
343// Define feature constants here or in any other centralized place
344// const HAS_LIVE_ACCESS = 'has_live_access';
345// const LIVE_FLAG_ID = 1;
346
347// Assembly SDK release
348// const SDK_233 = 'sdk_221';
349// const SDK_RELEASE_FLAG_ID = 14;
350
351const ANA_SDK_VARIANT_VERSION = '5.30.0';
352const ANA_SDK_CONTROL_VERSION = '5.28.0';
353const ANA_RELEASE_FLAG_ID = 79;
354const ANA_SDK_COHORT = 'ana_sdk_cohort';
355
356/**
357 * Evaluates uid with Flagr and returns the assigned variant key
358 *
359 * @param {number} flagID Flagr flag ID to use for freature rollout
360 * @param {string} uid uid to be evaluated
361 * 
362 * @return {Promise<string>} promise returning variant key
363 */
364 const getFlagrVariantEvaluation = function(flagID, uid) {
365  return fetchRetry('https://flagr.worldstarhiphop.com/api/v1/evaluation', {
366    method: 'POST',
367    headers: {
368      'Content-Type': 'application/json',
369      'Accept': 'application/json',
370    },
371    body: JSON.stringify({
372      "entityID": uid,
373      "entityType": "report",
374      "enableDebug": false,
375      "flagID": flagID,
376    })
377  })
378  .then(function(data) {
379    return data?.variantKey;
380  })
381  .catch(function(err) {
382    return err;
383  });
384}
385
386
387/**
388 * Get Assembly UID that is set to cookies, if not get it from local storage and sets it in cookies
389 * This function is intented for internal x-domain syncronization purpose
390 * 
391 * Note: make sure Assembly SDK is initialized before calling this fucntion
392 * 
393 * @return  {string} return Assembly UID
394 */
395const getAssemblyUID = function() {
396  const cookie_uid = getCookie(ASSEMBLY_UID);
397
398  if (window.ANAWeb && !cookie_uid) {
399    const local_uid = ANAWeb.uid;
400    setCookie(ASSEMBLY_UID, local_uid, _TEN_YEARS);
401
402    console.log("---> getAssemblyUID() local_uid: ", cookie_uid);
403    
404    return local_uid;
405  }
406  return cookie_uid;
407}
408
409/**
410 * Check if a given domain is considered "clean" (i.e., it does not contain the substring "hiphop").
411 * 
412 * @param {string} host - the hostname to check
413 * @return {boolean} true if the domain is clean, false otherwise
414 */
415const isCleanDomain = (host) => !host.includes('hiphop');
416
417
418/**
419 * Given a URL, return the full URL if the current domain is clean (i.e., it contains the substring "hiphop").
420 * Otherwise, return the original URL.
421 * 
422 * This function is intended to be used for adding parameters to URLs for cross-domain parameter/cookie sync.
423 * 
424 * @param {string} url - the URL to be modified
425 * @return {string} the modified URL
426 */
427const getUncleanClickURL = function (url) {
428  const isCurrentDomainClean = isCleanDomain(window.location.hostname);
429  return !isCurrentDomainClean ? url : getFullURL(url.replace('worldstar', 'worldstarhiphop'));
430}
431
432/**
433 * Given a URL, return the full URL if the current domain is not clean (i.e., it does not contain the substring "hiphop").
434 * Otherwise, return the original URL.
435 * 
436 * This function is intended to be used for adding parameters to URLs for cross-domain parameter/cookie sync.
437 * 
438 * @param {string} url - the URL to be modified
439 * @return {string} the modified URL
440 */
441const getCleanClickURL = function (url) {
442  const isCurrentDomainClean = isCleanDomain(window.location.hostname);
443  return isCurrentDomainClean ? url : getFullURL(url.replace('worldstarhiphop', 'worldstar'));
444}
445
446/**
447 * Handles a video card click event.
448 * It tracks firebase and mixpanel events.
449 * Then redirects the user to the link's URL, when it's cross-domain it adds extra parameters (full url).
450 *
451 * @param {Event} e The click event
452 * @param {string} vcodeenc The video code
453 * @param {string} videoUrl The URL of the video
454 * @param {string} page The page where the click occurred
455 * @param {string} section The section where the click occurred
456 * @param {string} [platform=''] The platform where the click occurred
457 */
458const onClickVideoCard = function(e, vcodeenc, videoUrl, page, section, platform = '', cohort = '') {
459  e.preventDefault();
460
461  // track firebase and mixpanel event
462  selectVideoEv(vcodeenc, page, section, platform, cohort);
463  // redirect to the video's URL
464  const isCurrentDomainClean = isCleanDomain(window.location.hostname);
465  const isCleanVideo = isCleanDomain(videoUrl);
466  // add extra parameters if cross domain
467  const onClickVideoUrl = isCurrentDomainClean === isCleanVideo ? videoUrl : getFullURL(videoUrl);
468  window.location.href = onClickVideoUrl;
469}
470
471/**
472 * Handles a nav link click event.
473 * It tracks a mixpanel event if eventName is provided.
474 * Then redirects the user to the link's URL, when it's cross-domain it adds extra parameters (full url).
475 *
476 * @param {Event} e The click event
477 * @param {string} page The page where the click occurred
478 * @param {boolean} [isClean=false] Whether the link is to a "clean" domain (i.e., it does not contain the substring "hiphop")
479 * @param {string} [eventName=null] The mixpanel event name to track
480 */
481function onNavLinkClick(e, page, isClean = false, eventName = null) {
482  e.preventDefault();
483  // track mixpanel event
484  if (eventName) {
485    trackMixpanelEvent(eventName);
486  }
487  // redirect (add extra parameters in cross domain)
488  const host = window.location.protocol + "//" + window.location.host;
489  const tagURL = host + "/" + page;
490  window.location.href = isClean ? getCleanClickURL(tagURL) : getUncleanClickURL(tagURL);
491  return false;
492};
493
494/**
495 * This function adds extra parameters to URL on client side.
496 * Setting parameters in URL on client side is meant for cross domain
497 * parameter/cookie sync and it shouldn't be used to permanently add parameters to URL.
498 *
499 * @param  {string} url wshh or ws url containing video parameter
500 * @returns {String} full string url
501 */
502const getFullURL = function (url) {
503  // Assembly UID
504  const _auid = getAssemblyUID();
505  // Internal UID
506  const _wshh_uid = getCookie(WSHH_UID_COOKIE);
507  // Liveramp
508  const _ml_em = getCookie(LIVERAMP_EMAIL_COOKIE)?.trim();
509  return `${url}?auid=${_auid}&wuid=${_wshh_uid}${_ml_em ? `&em=${_ml_em}` : ''}`;
510}
511
512/**
513 * Check if substring is included in str(case insensitive)
514 *
515 * @param {string} str
516 * @param {string} substr
517 * @returns Boolean
518 */
519const substrIncluded = function(str, substr) {
520  return str.toLowerCase().indexOf(substr.toLowerCase()) >= 0
521}
522
523/**
524 * Capitalize first letter of string
525 *
526 * @param {*} string
527 * @returns string
528 */
529const capitalizeFirstLetter = function(string) {
530  return string.charAt(0).toUpperCase() + string.slice(1);
531}
532
533/**
534 * Extracts and returns <image_name>.format from full image URL
535 *
536 * @param  {string} profilePicture profile picture URL
537 * @return {string} image file
538 */
539 function getImageFile(profilePicture) {
540  if (!profilePicture) return '';
541  return profilePicture.split('/').pop();
542}
543
544/**
545 * Replaces all whitespaces with dashes and removes all non alphanumeric characters 
546 *
547 * @param  {string} title video title
548 * @return {string} URL usable title
549 */
550function getURLTitle(title) {
551  return title.replace(/[^\w\s]/gi, '').replace(/\s/g , "-");
552}
553
554/**
555 * Extracts parameter's value from URL, sanitizes it and returns it
556 *
557 * @param {string} param URL parameter value if exists; otherwise null
558 */
559const getURLParam = function(param) {
560  const urlParams = new URLSearchParams(window.location.search);  
561  const paramValue = urlParams.get(param);
562  if (paramValue === null) {
563    return null;
564  }
565  let sanitizedParam = '';
566  try {
567    sanitizedParam = DOMPurify.sanitize(paramValue);
568  } catch (error) {
569    console.error('there was an error while processing parameter: ', error);
570  }
571  console.log("---> sanitsanitizedParam: ", sanitizedParam);
572  return sanitizedParam;
573}
574
575/**
576 * Extracts redirect_url parameter from URL and validates it
577 *
578 * @param {string} param redirect_url parameter value if exists and valid; otherwise undefined
579 */
580const getRedirectURL = function() {
581  const redirectURL = getURLParam('redirect_url');
582  // Only match our domains followd by anything. Sanitizatin is done in getURLParam()
583  if (redirectURL && (redirectURL.match('^https:\\/\\/([a-zA-Z0-9]+\\.)*worldstarhiphop\\.com.*$')
584      || redirectURL.match('^https:\\/\\/([a-zA-Z0-9]+\\.)*worldstar\\.com.*$'))) {
585    return redirectURL;
586  }
587  return undefined;
588}
589
590/**
591 * Loads ANA SDK script based on provided version
592 *
593 * @param {string} sdkVersion SDK version to load
594 */
595const loadAnaSDKScript = function(sdkVersion) {
596  var anaScript = document.createElement('script');
597  anaScript.setAttribute("src", `https://js.assemblyexchange.com/wana.${sdkVersion}.js`);
598  anaScript.setAttribute("id", "wana-script");
599  anaScript.setAttribute("async", true);
600
601  document.head.appendChild(anaScript);
602  return new Promise((resolve) => {
603    // const wanaScript = document.querySelector("wana-script");
604    // console.log(wanaScript);
605    anaScript.addEventListener('load', function() {
606      resolve();
607    });
608  });
609}
610
611/**
612 * Loads ANA SDK Sentry script wrapper with provided version for tracking
613 *
614 * @param {string} sdkVersion SDK version in use
615 */
616const loadAnaSentryScript = function(sdkVersion) {
617  var anaSentryScript = document.createElement('script');
618  anaSentryScript.setAttribute("src", "https://js.assemblyexchange.com/ana-sentry.js");
619  anaSentryScript.setAttribute("data-ana-sentry", "true");
620  anaSentryScript.setAttribute("data-dsn", "https://[email protected]/5394514");
621  anaSentryScript.setAttribute("data-release", `wshh@${sdkVersion}`);
622
623  document.head.appendChild(anaSentryScript);
624}
625
626/**
627 * It get's UID that is synced x-domain from cookies, in case it doesn't exists it creates one and sets cookie
628 *
629 * @returns {string} WSHH UID
630 */
631const getWSHHUID = function() {
632  // try to get x-domain synchronized WSHH UID
633  var wshhUID = getCookie(WSHH_UID_COOKIE);
634  const isValid = uuid.validate(wshhUID);
635
636  if (wshhUID === undefined || !isValid) {
637    if (wshhUID && !isValid) {
638      console.error(
639        'Invalid WSHH UID from cookie - getWshhUID function: ',
640        wshhUID,
641      );
642      // Clean cookie with invalid value
643      deleteCookie(WSHH_UID_COOKIE, domain);
644    }
645
646    wshhUID = uuid.v4();
647    setCookie(WSHH_UID_COOKIE, wshhUID, _TEN_YEARS);
648  }
649  return wshhUID;
650};
651
652/**
653 * Loads ANA SDK and its sentry wrapper
654 */
655 const loadAnaSDK = function() {
656  // clean previos A/B test cookie
657  deleteCookie('sdk_221');
658  deleteCookie('sdk_221_rollout');
659  deleteCookie('sdk_222pv');
660  deleteCookie('sdk_222pv_rollout');
661  
662  // Default ANA SDK version
663  let sdkVersion = ANA_SDK_CONTROL_VERSION;
664
665  // Turn this on if we're nunning A/B test
666  const SHOULD_CHECK_COHORT = true;
667
668  // Override sdkVersion from URL parameter
669  const _urlParams = new URLSearchParams(window.location.search);
670  const sdkVersionParam = _urlParams.get('ana-version');
671  if (sdkVersionParam && sdkVersionParam.match('/[0-9]+\.[0-9]+\.[0-9a-zA-Z]+/')) {
672    sdkVersion = sdkVersionParam;
673  }
674
675  return new Promise(resolve => {
676    if (!SHOULD_CHECK_COHORT) {
677      loadAnaSentryScript(sdkVersion);
678      loadAnaSDKScript(sdkVersion)
679        .then(() => {
680          setCookie(ANA_SDK_COHORT, sdkVersion, _TEN_YEARS);
681          resolve();
682        });
683    } else {
684      const wshhUID = getWSHHUID();
685      getFlagrVariantEvaluation(ANA_RELEASE_FLAG_ID, wshhUID)
686        .then(function(variant) {
687            if (variant?.trim().toLowerCase() === ANA_SDK_CONTROL_VERSION) {
688              setCookie(ANA_SDK_COHORT, ANA_SDK_CONTROL_VERSION, _TEN_YEARS);
689            } else if (variant?.trim().toLowerCase() === ANA_SDK_VARIANT_VERSION) {
690              setCookie(ANA_SDK_COHORT, ANA_SDK_VARIANT_VERSION, _TEN_YEARS);
691              sdkVersion = ANA_SDK_VARIANT_VERSION;
692            } else {
693              console.error("something went wrong obtaining ANA SDK cohort. Received variant: ", variant, " loading default SDK version.");
694            }
695            loadAnaSentryScript(sdkVersion);
696            loadAnaSDKScript(sdkVersion)
697              .then(() => {
698                setCookie(ANA_SDK_COHORT, sdkVersion, _TEN_YEARS);
699                resolve();
700              });
701        })
702        .catch(function(err){
703          console.error("loadAnaSDK getFlagrVariantEvaluation failed. Loading ANA with default SDK version: ", err);
704          loadAnaSentryScript(sdkVersion);
705          loadAnaSDKScript(sdkVersion)
706            .then(() => {
707              setCookie(ANA_SDK_COHORT, sdkVersion, _TEN_YEARS);
708              resolve();
709            });
710        });
711    }
712  });
713};
714
715const getHomePageCohort = function(callback) {
716  const wshhUID = getWSHHUID();
717  const xhr = new XMLHttpRequest();
718  xhr.open('POST', 'https://flagr.worldstarhiphop.com/api/v1/evaluation', false);
719  xhr.setRequestHeader('Content-Type', 'application/json');
720  xhr.setRequestHeader('Accept', 'application/json');
721
722  xhr.onreadystatechange = function() {
723    if (xhr.readyState === 4) {
724      if (xhr.status !== 200) {
725        console.error("Hompage cohort evaluation failed: ", xhr.status, " ", xhr.statusText);
726        setCookieNew("homepage_cohort", "v1", 2);
727      } else {
728        let data;
729        try {
730          data = JSON.parse(xhr.responseText);
731        } catch (e) {
732          console.error("Failed to parse homepage cohort response", e);
733          setCookieNew("homepage_cohort", "v1", 2);
734        }
735        if (data?.variantKey && (data.variantKey === "v1" || data.variantKey === "v2")) {
736          console.log("Received valid home page cohort: ", data.variantKey);
737          setCookieNew("homepage_cohort", data.variantKey, 2);
738          callback(data.variantKey);
739        } else {
740          console.error("Received invalid home page cohort. variant: ", data?.variantKey);
741          setCookieNew("homepage_cohort", "v1", 2);
742        }
743      }
744    }
745  };
746
747  xhr.ontimeout = function() {
748    console.error("Hompage cohort evaluation timed out");
749    setCookieNew("homepage_cohort", "v1", 2);
750  };
751
752  xhr.onerror = function() {
753    console.error("Hompage cohort evaluation network error");
754    setCookieNew("homepage_cohort", "v1", 2);
755  };
756
757  xhr.send(JSON.stringify({
758    "entityID": wshhUID,
759    "entityType": "report",
760    "enableDebug": false,
761    "flagID": 68,
762  }));
763}
764
765const getSubdomain = function() {
766  const hostname = window.location.hostname;
767  const parts = hostname.split('.');
768
769  // The subdomain is everything before the last two parts (domain and TLD).
770  // If there are only two parts, there is no subdomain.
771  let subdomain = "";
772  if (parts.length > 2) {
773      subdomain = parts.slice(0, parts.length - 2).join('.');
774  }
775  return subdomain;
776}
777
778/**
779 * helpfer function to run ANA vs IMA for getting vdieos ads A/B test
780 * this function calls flagr to evalueate cohort and based on cohort
781 * it either calls IMA or ANA
782 * in case of any failure we call ANA
783 */
784const runANAvsIMAtest = function() {
785  const wshhUID = getWSHHUID();
786  const ANA_SDK = 'ana';
787  const IMA_SDK = 'ima';
788
789  getFlagrVariantEvaluation(28, wshhUID)
790    .then(function(variant) {
791        if (variant?.trim().toLowerCase() === ANA_SDK) {
792          window.player = videojs('video_player');
793          window.skipIma3Unskippable = window.player.skipIma3Unskippable({
794            skipAfter: 5,
795          });
796          window.videoPlayerPlacement1 = buildVidAd(setupAdsCallbacks, '/114106652/wshh_vp_mw_video_vmap');
797          console.log('Initialized and calling ANA for video ads');
798        } else if (variant?.trim().toLowerCase() === IMA_SDK) {
799          try {
800            const imaOptions = {
801              // uncomment for debugging
802              // debug: true,
803              id: 'video_player',
804              adTagUrl:
805                'https://pubads.g.doubleclick.net/gampad/live/ads?iu=/114106652/wshh_vp_mw_video&description_url=https%3A%2F%2Fworldstar.com&tfcd=0&npa=0&sz=640x480&min_ad_duration=1000&max_ad_duration=120000&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=',
806              // contribAdsSettings: {
807              //   // https://videojs.github.io/videojs-contrib-ads/integrator/options.html
808              //   // timeout: 5000,
809              //   // prerollTimeout: 1000
810              // },
811            };
812            window.player = videojs('video_player');
813            window.player.ima(imaOptions);
814            console.log('Initialized and calling IMA directly for video ads');
815          } catch(e) {
816            console.error('error initializing IMA directly: ', e);
817          }
818        } else {
819          console.error("something went wrong obtaining ANA SDK cohort. Received variant: ", variant, " loading default SDK ver
819sion.");
820
821          window.player = videojs('video_player');
822          window.skipIma3Unskippable = window.player.skipIma3Unskippable({
823            skipAfter: 5,
824          });
825          window.videoPlayerPlacement1 = buildVidAd(setupAdsCallbacks, '/114106652/wshh_vp_mw_video_vmap');
826          console.log('Initialized and calling ANA for video ads');
827        }
828    })
829    .catch(function(err){
830      console.error("ANA vs IMA getFlagrVariantEvaluation failed. Calling ANA SDK for loading ads. Error: ", err);
831
832      window.player = videojs('video_player');
833      window.skipIma3Unskippable = window.player.skipIma3Unskippable({
834        skipAfter: 5,
835      });
836      window.videoPlayerPlacement1 = buildVidAd(setupAdsCallbacks, '/114106652/wshh_vp_mw_video_vmap');
837      console.log('Initialized and calling ANA for video ads');
838    });
839};
840
841/* 
842  helper function to run the PPID A/B test
843*/
844const runPPIDTest = function(callback) {
845  const wshhUID = getWSHHUID();
846  getFlagrVariantEvaluation(34, wshhUID)
847    .then(callback)
848    .catch((err) => {
849      console.error('PPID AB test evaluation failed, using default value. Error:', err)
850      callback('disabled')
851    })
852};
853
854/**
855 * Checks if image uploaded is a valid image. Validates image file's:
856 * - size
857 * - extension (should already be limited by input's accept attribute)
858 * - name length
859 * - real file signature via the file's magic numbers
860 *   (https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files)
861 *
862 *
863 * Notes:
864 * - file type validation should be done with a specialized library, but current solutions
865 * in JS for this are aimed at Node (eg https://github.com/sindresorhus/file-type, etc) and porting
866 * to client side would require:
867 *   - a) non trivial effort (polyfilling native Node modules, browserifying, etc)
868 *   - b) adding large amount of nested dependencies (node_modules) just for maing library,
869 * - for some advanced XSS attack techniques (@see https://www.onsecurity.io/blog/file-upload-checklist/)
870 * this implementation would need to be revisited.
871 * - logic for file type validation is based off of https://github.com/sindresorhus/file-type,
872 * https://github.com/gchq/CyberChef among others
873 *
874 *
875 * @param   {File} imageFile  The image file to validate
876 * @returns {Promise}         A promise that resolves to true if the image is valid, otherwise returns
877 *                            an Error with the invalidation reason as message
878 *
879 * @see https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
880 * @see https://en.wikipedia.org/wiki/List_of_file_signatures
881 * @see https://www.garykessler.net/library/file_sigs.html
882 * @see https://filesignatures.net/index.php
883 */
884const isValidImageFile = function(imageFile) {
885  return new Promise((resolve, reject) => {
886    console.log({imageFile});
887    const MAX_FILENAME_LENGTH = 255;                // see https://en.wikipedia.org/wiki/Filename
888    const MAX_ALLOWED_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
889    const VALID_TYPES = [
890      'image/png',
891      'image/jpeg',
892    ];
893    const magicNumbers = {
894      'image/png': ['89504e47'],
895      'image/jpeg': ['ffd8ff']
896    };
897
898    if (!VALID_TYPES.includes(imageFile.type)) {
899      reject(new Error(`Please select a jpg, jpeg or png image.`));
900    }
901    if (imageFile.size > MAX_ALLOWED_FILE_SIZE) {
902      reject(new Error(`Image is too large.`));
903    }
904    if (imageFile.name.length > MAX_FILENAME_LENGTH) {
905      reject(new Error(`The image name is too long.`));
906    }
907
908    // validate actual file signature via magic numbers
909    const fileReader = new FileReader();
910    fileReader.onloadend = function(event) {
911      if (event.target.readyState === FileReader.DONE) {
912        // for png we grab first 4 unsigned bytes, for jpeg first 3
913        const byteCount = imageFile.type === 'image/png' ? 4 : 3;
914        const uint = (new Uint8Array(event.target.result)).subarray(0, byteCount);
915
916        // convert to array of hexadecimal values
917        let hexArray = Array.from(uint).map(byte => byte.toString(16));
918
919        // convert to string and normalize
920        const magicNumber = hexArray.join('').toLowerCase();
921
922        // find if magic number is in the list of valid ones
923        const mimeType = Object
924            .keys(magicNumbers)
925            .find(key => magicNumbers[key].includes(magicNumber));
926        console.log('mimeType is: ', mimeType);
927
928        // if the mime type obtained does not match the file type,
929        // image is invalid so we reject, else image is valid
930        if (imageFile.type !== mimeType) {
931          reject(new Error(`Please select a jpg, jpeg or png image.`));
932        } else {
933          resolve(true);
934        }
935      };
936    };
937    fileReader.readAsArrayBuffer(imageFile);
938  });
939};
940
941/**
942 *  Converts a Base64 data uri to a Blob
943 * (see https://stackoverflow.com/a/36183085)
944 * 
945 * @param {string} dataURI the Base64 data uri to convert
946 * @returns {Blob} the converted Blob object
947 */
948const dataURItoBlob = async (dataURI) => {
949  const base64Response = await fetch(dataURI);
950  const blob = await base64Response.blob();
951  return blob; 
952};
953
954/**
955 *  Converts a Blob object to a Base64 string
956 * 
957 * @param {Blob} blob the Blob object to convert
958 * @returns {string} the converted Base64 string
959 */
960const blobToBase64 = blob => {
961  return new Promise((resolve, reject) => {
962    const reader = new FileReader();
963    reader.onerror = reject;
964    reader.onload = () => resolve(reader.result);
965    reader.readAsDataURL(blob);
966  });
967};
968
969/**
970 *  Cleanup geolocation metadata from an image
971 */
972const cleanUpGeoData = async (image, piexif) => {
973  if (!piexif) {
974    console.error('cleanUpGeoData: piexif hasn\'t been loaded');
975  }
976  const base64Image = await blobToBase64(image);
977  const cleanedUpBase64 = piexif.remove(base64Image);
978  const blob = await dataURItoBlob(cleanedUpBase64);
979  return new File([blob], image.name, { type: blob.type });
980};
981
982/************************************************/
983/*************** V2 Release Start ***************/
984/************************************************/
985
986// video V2 A/B test constants
987const VARIANT_V2 = 'v2';
988const VARIANT_V1 = 'v1';
989const VIDEO_PAGE_COHORT_COOKIE = 'video_page_cohort'
990const WSHH_UID_COOKIE = 'wshh_uid';
991const LIVERAMP_EMAIL_COOKIE = 'ml_em'; // liveramp cookie name
992
993/************************************************/
994/*************** V2 Release End *****************/
995/************************************************/
996
997
998
999
1000/**
1001 * Tracks the number of days since the user's last visit by setting a cookie with the current date.
1002 * If the user has visited before, it calculates the days between the last visit and the current date,
1003 * and sends this information to Mixpanel.
1004 *
1005 * @function getAndTrackLastVisitedDay
1006 * @returns {number} The number of days since the user's last visit, or -1 if the user has not visited before.
1007 */
1008const getAndTrackLastVisitedDay = function() {
1009  const LAST_VISIT = 'last_visit';
1010
1011  // get the current date (year, month, day) without the time
1012  const currentDate = new Date();
1013  const currentDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()).getTime();
1014
1015  let lastVisit = getCookie(LAST_VISIT);
1016  // negative value means user has not visited before or lost the cookie
1017  let daysBetweenLastVisit = -1;
1018  if (lastVisit) {
1019    let lastVisitDate = new Date(parseInt(lastVisit));
1020    daysBetweenLastVisit = Math.floor((currentDay - lastVisitDate.getTime()) / (1000 * 60 * 60 * 24));
1021  }
1022
1023  // Set/update the last visit cookie and send an event
1024  setCookie(LAST_VISIT, currentDay, _TEN_YEARS);
1025  trackMixpanelEvent(LAST_VISIT, { days_since_last_visit: daysBetweenLastVisit });
1026
1027  return daysBetweenLastVisit;
1028};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.