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.