diff --git a/README.md b/README.md index d3c1179d2..024c403fd 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,7 @@ Plus any of:
  • onetime One-time payments accepted
  • recurring Subscriptions available
  • poa Price on asking
  • +
  • payg Pay as you go (e.g. commissions or usage-based fees)
  • ["low", "freemium"] diff --git a/schema.json b/schema.json index 41d58aff5..82f938070 100644 --- a/schema.json +++ b/schema.json @@ -51,7 +51,7 @@ "type": "array", "items": { "type": "string", - "pattern": "^(low|mid|high|freemium|poa|onetime|recurring)$" + "pattern": "^(low|mid|high|freemium|poa|payg|onetime|recurring)$" } }, "cats": { diff --git a/src/drivers/npm/package.json b/src/drivers/npm/package.json index 6f53e8b89..1f33bde96 100644 --- a/src/drivers/npm/package.json +++ b/src/drivers/npm/package.json @@ -13,7 +13,7 @@ "software" ], "homepage": "https://www.wappalyzer.com/", - "version": "6.5.18", + "version": "6.5.21", "author": "Wappalyzer", "license": "MIT", "repository": { @@ -25,7 +25,7 @@ "url": "https://github.com/sponsors/aliasio" }, { - "url": "https://paypal.me/aliasio" + "url": "https://paypal.me/elbertalias" } ], "main": "driver.js", diff --git a/src/drivers/webextension/_locales/ca/messages.json b/src/drivers/webextension/_locales/ca/messages.json index be47a27e1..78c1ee342 100644 --- a/src/drivers/webextension/_locales/ca/messages.json +++ b/src/drivers/webextension/_locales/ca/messages.json @@ -1,26 +1,26 @@ { - "github": { "message": "Fork Wappalyzer a GitHub!" }, + "github": { "message": "Bifurcar Wappalyzer a GitHub" }, "twitter": { "message": "Seguir Wappalyzer a Twitter" }, "website": { "message": "Anar a wappalyzer.com" }, "options": { "message": "Opcions" }, "optionsSave": { "message": "Desar opcions" }, "optionsSaved": { "message": "Desat" }, - "optionUpgradeMessage": { "message": "Avisar-me quan hi hagi una actualització disponible" }, + "optionUpgradeMessage": { "message": "Notificar les actualitzacions disponibles" }, "optionDynamicIcon": { "message": "Utilitzar la icona de la tecnologia enlloc del logotip de Wappalyzer" }, "optionTracking": { "message": "Enviar les tecnologies identificades de forma anònima a wappalyzer.com" }, - "optionThemeMode": { "message": "Habilitar la compatibilitat de la manera fosc." }, - "optionBadge": { "message": "Show the number of identified technologies on the icon" }, - "disableOnDomain": { "message": "Disable on this website" }, - "clearCache": { "message": "Clear cached detections" }, + "optionThemeMode": { "message": "Habilitar la compatibilitat de l'aspecte fosc" }, + "optionBadge": { "message": "Mostrar el nombre de tecnologies identificades en la icona" }, + "disableOnDomain": { "message": "Desactivar en aquest web" }, + "clearCache": { "message": "Esborrar la memòria cau de les deteccions" }, "nothingToDo": { "message": "Res a fer aquí." }, "noAppsDetected": { "message": "No s'ha detectat cap tecnologia." }, "categoryPin": { "message": "Mostrar sempre la icona" }, - "termsAccept": { "message": "I'm ok with that" }, - "termsDecline": { "message": "Disable" }, + "termsAccept": { "message": "M'està bé" }, + "termsDecline": { "message": "Desactivar" }, "termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a wappalyzer.com. Això pot desactivar-se a Opcions." }, "privacyPolicy": { "message": "Política de privadesa" }, - "createAlert": { "message": "Create an alert for this website" }, - "leadLists": { "message": "Website & contact lists" }, + "createAlert": { "message": "Crear una alerta per aquest web" }, + "leadLists": { "message": "Llistes de webs i contactes" }, "categoryName1": { "message": "CMS" }, "categoryName2": { "message": "Taulers de missatgeria" }, "categoryName3": { "message": "Gestor de bases de dades" }, @@ -37,7 +37,7 @@ "categoryName14": { "message": "Reproductors de vídeo" }, "categoryName15": { "message": "Sistemes de comentaris" }, "categoryName16": { "message": "Security" }, - "categoryName17": { "message": "Font Script" }, + "categoryName17": { "message": "Tipografies" }, "categoryName18": { "message": "Marcs web" }, "categoryName19": { "message": "Miscel·lània" }, "categoryName20": { "message": "Editors" }, @@ -52,7 +52,7 @@ "categoryName29": { "message": "Motors de cerca" }, "categoryName30": { "message": "Correu web" }, "categoryName31": { "message": "CDN" }, - "categoryName32": { "message": "Marketing Automation" }, + "categoryName32": { "message": "Automatitzacions de màrqueting" }, "categoryName33": { "message": "Extensions del servidor web" }, "categoryName34": { "message": "Bases de dades" }, "categoryName35": { "message": "Mapes" }, @@ -66,7 +66,7 @@ "categoryName43": { "message": "Paywall" }, "categoryName44": { "message": "Sistemes Build/CI" }, "categoryName45": { "message": "Sistemes SCADA" }, - "categoryName46": { "message": "Accés remot" }, + "categoryName46": { "message": "Accessos remots" }, "categoryName47": { "message": "Eines de desenvolupament" }, "categoryName48": { "message": "Emmagatzematge de xarxa" }, "categoryName49": { "message": "Lectors de canals" }, @@ -78,7 +78,7 @@ "categoryName55": { "message": "Comptabilitat" }, "categoryName56": { "message": "Cryptominer" }, "categoryName57": { "message": "Generadors de llocs estàtics" }, - "categoryName58": { "message": "User Onboarding" }, + "categoryName58": { "message": "Incorporacions d'usuaris" }, "categoryName59": { "message": "Llibreries JavaScript" }, "categoryName60": { "message": "Contenidors" }, "categoryName61": { "message": "SaaS" }, @@ -86,14 +86,14 @@ "categoryName63": { "message": "IaaS" }, "categoryName64": { "message": "Proxys invers" }, "categoryName65": { "message": "Balanceigs de càrrega" }, - "categoryName66": { "message": "UI Frameworks" }, + "categoryName66": { "message": "Marcs UI" }, "categoryName67": { "message": "Cookie compliance" }, - "categoryName68": { "message": "Accessibility" }, - "categoryName69": { "message": "Social login" }, - "categoryName70": { "message": "SSL/TLS certificate authorities" }, - "categoryName71": { "message": "Affiliate program" }, - "categoryName72": { "message": "Appointment scheduling" }, - "categoryName73": { "message": "Surveys" }, - "categoryName74": { "message": "A/B testing" }, - "categoryName75": { "message": "Email" } + "categoryName68": { "message": "Accesibilitat" }, + "categoryName69": { "message": "Inicis de sessió socials" }, + "categoryName70": { "message": "Autoritats de certificació SSL/TLS" }, + "categoryName71": { "message": "Programes d'afiliació" }, + "categoryName72": { "message": "Programacions de cites" }, + "categoryName73": { "message": "Enquestes" }, + "categoryName74": { "message": "Testeigs A/B" }, + "categoryName75": { "message": "Correus electrònics" } } diff --git a/src/drivers/webextension/images/icons/ATSHOP.png b/src/drivers/webextension/images/icons/ATSHOP.png new file mode 100644 index 000000000..f117fb93a Binary files /dev/null and b/src/drivers/webextension/images/icons/ATSHOP.png differ diff --git a/src/drivers/webextension/images/icons/Aimtell.png b/src/drivers/webextension/images/icons/Aimtell.png new file mode 100644 index 000000000..2d94a6da2 Binary files /dev/null and b/src/drivers/webextension/images/icons/Aimtell.png differ diff --git a/src/drivers/webextension/images/icons/Airship.svg b/src/drivers/webextension/images/icons/Airship.svg new file mode 100644 index 000000000..a88a8cca6 --- /dev/null +++ b/src/drivers/webextension/images/icons/Airship.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Aplazame.svg b/src/drivers/webextension/images/icons/Aplazame.svg new file mode 100644 index 000000000..7ef2f1547 --- /dev/null +++ b/src/drivers/webextension/images/icons/Aplazame.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/ApostropheCMS.svg b/src/drivers/webextension/images/icons/ApostropheCMS.svg index 2b67074f8..775946684 100644 --- a/src/drivers/webextension/images/icons/ApostropheCMS.svg +++ b/src/drivers/webextension/images/icons/ApostropheCMS.svg @@ -1,38 +1,3 @@ -<<<<<<< HEAD - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -======= @@ -58,4 +23,3 @@ ->>>>>>> dffac961239cb3ed09863b0431d3b758236ee62a diff --git a/src/drivers/webextension/images/icons/Autopilot.png b/src/drivers/webextension/images/icons/Autopilot.png new file mode 100644 index 000000000..832059bab Binary files /dev/null and b/src/drivers/webextension/images/icons/Autopilot.png differ diff --git a/src/drivers/webextension/images/icons/Birdeye.svg b/src/drivers/webextension/images/icons/Birdeye.svg new file mode 100644 index 000000000..92bc67c17 --- /dev/null +++ b/src/drivers/webextension/images/icons/Birdeye.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/Borlabs Cookie.svg b/src/drivers/webextension/images/icons/Borlabs Cookie.svg new file mode 100644 index 000000000..83f8501c9 --- /dev/null +++ b/src/drivers/webextension/images/icons/Borlabs Cookie.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/drivers/webextension/images/icons/Braze.svg b/src/drivers/webextension/images/icons/Braze.svg new file mode 100644 index 000000000..646e312e7 --- /dev/null +++ b/src/drivers/webextension/images/icons/Braze.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Buildertrend.svg b/src/drivers/webextension/images/icons/Buildertrend.svg new file mode 100644 index 000000000..8dcbb901f --- /dev/null +++ b/src/drivers/webextension/images/icons/Buildertrend.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/drivers/webextension/images/icons/CartStack.svg b/src/drivers/webextension/images/icons/CartStack.svg new file mode 100644 index 000000000..40022347b --- /dev/null +++ b/src/drivers/webextension/images/icons/CartStack.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Clerk.io.svg b/src/drivers/webextension/images/icons/Clerk.io.svg new file mode 100644 index 000000000..660d4a9aa --- /dev/null +++ b/src/drivers/webextension/images/icons/Clerk.io.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/drivers/webextension/images/icons/CleverTap.png b/src/drivers/webextension/images/icons/CleverTap.png new file mode 100644 index 000000000..203ae7570 Binary files /dev/null and b/src/drivers/webextension/images/icons/CleverTap.png differ diff --git a/src/drivers/webextension/images/icons/ClustrMaps.svg b/src/drivers/webextension/images/icons/ClustrMaps.svg new file mode 100644 index 000000000..194175f99 --- /dev/null +++ b/src/drivers/webextension/images/icons/ClustrMaps.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/drivers/webextension/images/icons/CoConstruct.png b/src/drivers/webextension/images/icons/CoConstruct.png new file mode 100644 index 000000000..e52e32363 Binary files /dev/null and b/src/drivers/webextension/images/icons/CoConstruct.png differ diff --git a/src/drivers/webextension/images/icons/Conekta.svg b/src/drivers/webextension/images/icons/Conekta.svg new file mode 100644 index 000000000..713e808ca --- /dev/null +++ b/src/drivers/webextension/images/icons/Conekta.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/drivers/webextension/images/icons/DocFX.svg b/src/drivers/webextension/images/icons/DocFX.svg new file mode 100644 index 000000000..08e5828f7 --- /dev/null +++ b/src/drivers/webextension/images/icons/DocFX.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Dotdigital.svg b/src/drivers/webextension/images/icons/Dotdigital.svg new file mode 100644 index 000000000..cd4319f1a --- /dev/null +++ b/src/drivers/webextension/images/icons/Dotdigital.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/drivers/webextension/images/icons/Firepush.svg b/src/drivers/webextension/images/icons/Firepush.svg new file mode 100644 index 000000000..4409cb3d3 --- /dev/null +++ b/src/drivers/webextension/images/icons/Firepush.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Forte.svg b/src/drivers/webextension/images/icons/Forte.svg new file mode 100644 index 000000000..a7336babd --- /dev/null +++ b/src/drivers/webextension/images/icons/Forte.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Go.svg b/src/drivers/webextension/images/icons/Go.svg index 8b0b1af64..56c36c41c 100644 --- a/src/drivers/webextension/images/icons/Go.svg +++ b/src/drivers/webextension/images/icons/Go.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/drivers/webextension/images/icons/Heartland Payment Systems.svg b/src/drivers/webextension/images/icons/Heartland Payment Systems.svg new file mode 100644 index 000000000..2feaec4a1 --- /dev/null +++ b/src/drivers/webextension/images/icons/Heartland Payment Systems.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Insider.svg b/src/drivers/webextension/images/icons/Insider.svg new file mode 100644 index 000000000..f7a9d39fa --- /dev/null +++ b/src/drivers/webextension/images/icons/Insider.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/drivers/webextension/images/icons/Isotope.svg b/src/drivers/webextension/images/icons/Isotope.svg new file mode 100644 index 000000000..13a8f86c5 --- /dev/null +++ b/src/drivers/webextension/images/icons/Isotope.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Izooto.png b/src/drivers/webextension/images/icons/Izooto.png new file mode 100644 index 000000000..009551a4c Binary files /dev/null and b/src/drivers/webextension/images/icons/Izooto.png differ diff --git a/src/drivers/webextension/images/icons/JShop.svg b/src/drivers/webextension/images/icons/JShop.svg new file mode 100644 index 000000000..8d3302361 --- /dev/null +++ b/src/drivers/webextension/images/icons/JShop.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/KaTeX.svg b/src/drivers/webextension/images/icons/KaTeX.svg new file mode 100644 index 000000000..5d907c57d --- /dev/null +++ b/src/drivers/webextension/images/icons/KaTeX.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/Leanplum.svg b/src/drivers/webextension/images/icons/Leanplum.svg new file mode 100644 index 000000000..df8e851f7 --- /dev/null +++ b/src/drivers/webextension/images/icons/Leanplum.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Loja2.svg b/src/drivers/webextension/images/icons/Loja2.svg new file mode 100644 index 000000000..ea4200a3b --- /dev/null +++ b/src/drivers/webextension/images/icons/Loja2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Loox.svg b/src/drivers/webextension/images/icons/Loox.svg new file mode 100644 index 000000000..b001e7f9a --- /dev/null +++ b/src/drivers/webextension/images/icons/Loox.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/Mapbogljs.png b/src/drivers/webextension/images/icons/Mapbogljs.png new file mode 100644 index 000000000..f21e1e0df Binary files /dev/null and b/src/drivers/webextension/images/icons/Mapbogljs.png differ diff --git a/src/drivers/webextension/images/icons/Marketo.png b/src/drivers/webextension/images/icons/Marketo.png index e6da17327..d9c0b636f 100644 Binary files a/src/drivers/webextension/images/icons/Marketo.png and b/src/drivers/webextension/images/icons/Marketo.png differ diff --git a/src/drivers/webextension/images/icons/Mercado Shops.svg b/src/drivers/webextension/images/icons/Mercado Shops.svg new file mode 100644 index 000000000..addc35c9d --- /dev/null +++ b/src/drivers/webextension/images/icons/Mercado Shops.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Nosto.svg b/src/drivers/webextension/images/icons/Nosto.svg new file mode 100644 index 000000000..28b58a954 --- /dev/null +++ b/src/drivers/webextension/images/icons/Nosto.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/Omise.svg b/src/drivers/webextension/images/icons/Omise.svg new file mode 100644 index 000000000..73b91d6d9 --- /dev/null +++ b/src/drivers/webextension/images/icons/Omise.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Omnisend.svg b/src/drivers/webextension/images/icons/Omnisend.svg new file mode 100644 index 000000000..1580f1da1 --- /dev/null +++ b/src/drivers/webextension/images/icons/Omnisend.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/drivers/webextension/images/icons/OneSignal.svg b/src/drivers/webextension/images/icons/OneSignal.svg new file mode 100644 index 000000000..adea16dad --- /dev/null +++ b/src/drivers/webextension/images/icons/OneSignal.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/OpenStreetMap.svg b/src/drivers/webextension/images/icons/OpenStreetMap.svg new file mode 100644 index 000000000..c08f3c493 --- /dev/null +++ b/src/drivers/webextension/images/icons/OpenStreetMap.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Paddle.svg b/src/drivers/webextension/images/icons/Paddle.svg new file mode 100644 index 000000000..157048db2 --- /dev/null +++ b/src/drivers/webextension/images/icons/Paddle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/Plataforma NEO.svg b/src/drivers/webextension/images/icons/Plataforma NEO.svg index 559ac6e48..2a9527aa2 100644 --- a/src/drivers/webextension/images/icons/Plataforma NEO.svg +++ b/src/drivers/webextension/images/icons/Plataforma NEO.svg @@ -1,16 +1,4 @@ -<<<<<<< HEAD -======= - - - - - - - - - ->>>>>>> dffac961239cb3ed09863b0431d3b758236ee62a diff --git a/src/drivers/webextension/images/icons/PushEngage.png b/src/drivers/webextension/images/icons/PushEngage.png new file mode 100644 index 000000000..0f31ff56a Binary files /dev/null and b/src/drivers/webextension/images/icons/PushEngage.png differ diff --git a/src/drivers/webextension/images/icons/PushOwl.svg b/src/drivers/webextension/images/icons/PushOwl.svg new file mode 100644 index 000000000..c9a97812b --- /dev/null +++ b/src/drivers/webextension/images/icons/PushOwl.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/drivers/webextension/images/icons/RevolverMaps.svg b/src/drivers/webextension/images/icons/RevolverMaps.svg new file mode 100644 index 000000000..16ba3b073 --- /dev/null +++ b/src/drivers/webextension/images/icons/RevolverMaps.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/Skedify.svg b/src/drivers/webextension/images/icons/Skedify.svg new file mode 100644 index 000000000..16fec8b83 --- /dev/null +++ b/src/drivers/webextension/images/icons/Skedify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/drivers/webextension/images/icons/SkyVerge.svg b/src/drivers/webextension/images/icons/SkyVerge.svg new file mode 100644 index 000000000..29286e6e9 --- /dev/null +++ b/src/drivers/webextension/images/icons/SkyVerge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/drivers/webextension/images/icons/TrustYou.svg b/src/drivers/webextension/images/icons/TrustYou.svg new file mode 100644 index 000000000..4d3b276fd --- /dev/null +++ b/src/drivers/webextension/images/icons/TrustYou.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/VWO.svg b/src/drivers/webextension/images/icons/VWO.svg new file mode 100644 index 000000000..5d89c9556 --- /dev/null +++ b/src/drivers/webextension/images/icons/VWO.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/drivers/webextension/images/icons/Voracio.svg b/src/drivers/webextension/images/icons/Voracio.svg new file mode 100644 index 000000000..2d0ccf2f1 --- /dev/null +++ b/src/drivers/webextension/images/icons/Voracio.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/drivers/webextension/images/icons/WebAssembly.svg b/src/drivers/webextension/images/icons/WebAssembly.svg new file mode 100644 index 000000000..5eba1e63a --- /dev/null +++ b/src/drivers/webextension/images/icons/WebAssembly.svg @@ -0,0 +1,18 @@ + + + + + web-assembly-icon + + + + + + + + + diff --git a/src/drivers/webextension/images/icons/upvoty.png b/src/drivers/webextension/images/icons/upvoty.png new file mode 100644 index 000000000..f8c96b1be Binary files /dev/null and b/src/drivers/webextension/images/icons/upvoty.png differ diff --git a/src/drivers/webextension/images/icons/vwo.svg b/src/drivers/webextension/images/icons/vwo.svg deleted file mode 100644 index ac70798e9..000000000 --- a/src/drivers/webextension/images/icons/vwo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/drivers/webextension/images/safari.svg b/src/drivers/webextension/images/safari.svg new file mode 100644 index 000000000..6edbdff4e Binary files /dev/null and b/src/drivers/webextension/images/safari.svg differ diff --git a/src/drivers/webextension/js/driver.js b/src/drivers/webextension/js/driver.js index ae19199a1..62144f4ad 100644 --- a/src/drivers/webextension/js/driver.js +++ b/src/drivers/webextension/js/driver.js @@ -551,7 +551,7 @@ const Driver = { !(await getOption('tracking', true)) || hostnameIgnoreList.test(hostname) ) { - return + return [] } if (typeof Driver.cache.robots[hostname] !== 'undefined') { @@ -693,7 +693,7 @@ const Driver = { const count = Object.keys(hostnames).length - if (count && (count >= 50 || Driver.lastPing < Date.now() - expiry)) { + if (count && (count >= 25 || Driver.lastPing < Date.now() - expiry)) { await Driver.post('https://api.wappalyzer.com/ping/v2/', hostnames) await setOption('hostnames', (Driver.cache.hostnames = {})) @@ -701,8 +701,10 @@ const Driver = { Driver.lastPing = Date.now() } - if (Driver.cache.ads.length > 1) { + if (Driver.cache.ads.length > 25) { await Driver.post('https://ad.wappalyzer.com/log/wp/', Driver.cache.ads) + + Driver.cache.ads = [] } } }, diff --git a/src/drivers/webextension/js/lib/iframe.js b/src/drivers/webextension/js/lib/iframe.js index d36e22baa..eb910cc93 100644 --- a/src/drivers/webextension/js/lib/iframe.js +++ b/src/drivers/webextension/js/lib/iframe.js @@ -1,168 +1,168 @@ 'use strict' -;(function(win) { - var exports = {} - - ;(function(exports) { - var utils = { - /** - * Normalize URL - * @param {String} url - */ - normalizeUrl: function(url) { - return this.hashUrl(url) || null - }, - - /** - * Get referrer. - */ - getReferrer: function() { - return this.normalizeUrl(document.referrer) - }, - - /** - * Get current page URL. - */ - getPageUrl: function() { - return this.normalizeUrl(window.location.href) - }, - - /** - * Generated hashed URL. - * @param {String} url - */ - hashUrl: function(url) { - var a, result - - if (!url || url.indexOf('http') !== 0) { - return null - } - - a = document.createElement('a') - a.href = url - - result = a.protocol + '//' + a.hostname + '/' - - if (a.pathname && a.pathname !== '/') { - result += this.hashCode(a.pathname) - } - - if (a.search) { - result += '?' + this.hashCode(a.search) - } - - if (a.hash) { - result += '#' + this.hashCode(a.hash) - } - - return result - }, - - /** - * Generate random hash. - * @param {String} str - */ - hashCode: function(str) { - var hash = 0, - kar, - i - - if (str.length === 0) { - return hash - } - - for (i = 0; i < str.length; i++) { - kar = str.charCodeAt(i) - hash = (hash << 5) - hash + kar - hash = hash & hash - } - - return hash + Math.pow(2, 32) - }, - - /** - * Apply array function to non-array. - * @param {Object} a - */ - realArray: function(a) { - return Array.prototype.slice.apply(a) - }, - - /** - * Listener callback for onDocLoaded. - * @param {Object} doc - * @param {Function} callback - */ - onDocLoaded: function(doc, callback) { - if (doc.readyState === 'loading') { - doc.addEventListener('DOMContentLoaded', callback) - } else { - callback() - } - }, - - SCRIPT_IN_WINDOW_TOP: window === window.top, - - /** - * Check for href Window object. - * @param {Object} win - */ - isFriendlyWindow: function(win) { - var href - try { - href = win.location.href - } catch (e) { - return false - } - return true - }, - - /** - * Get default view from element. - * @param {Object} el - */ - elementWindow: function(el) { - return el.ownerDocument.defaultView - }, - - /** - * Get viewport size. - * @param {Object} win - */ - viewport: function(win) { - return { width: win.innerWidth, height: win.innerHeight } - }, - - /** - * Parse query string parameters. - * @param {String} qs - */ - parseQS: function(qs) { - if (qs.indexOf('http') === 0) { - qs = qs.split('?')[1] - } - var i, kvs, key, val - var dict = {} - qs = qs.split('&') - for (i = 0; i < qs.length; i++) { - kvs = qs[i].split('=') - key = kvs[0] - val = kvs.slice(1).join('=') - try { - dict[key] = window.decodeURIComponent(val) - } catch (e) { - continue - } - } - return dict - }, - - /** - * Send PostMessage response. - * @param {Object} message - * @param {String} event - * @param {String} responseMessage - */ - sendToBackground: function(message, event, responseMessage) { +;(function (win) { + const exports = {} + + ;(function (exports) { + const utils = { + /** + * Normalize URL + * @param {String} url + */ + normalizeUrl(url) { + return this.hashUrl(url) || null + }, + + /** + * Get referrer. + */ + getReferrer() { + return this.normalizeUrl(document.referrer) + }, + + /** + * Get current page URL. + */ + getPageUrl() { + return this.normalizeUrl(window.location.href) + }, + + /** + * Generated hashed URL. + * @param {String} url + */ + hashUrl(url) { + let a, result + + if (!url || url.indexOf('http') !== 0) { + return null + } + + a = document.createElement('a') + a.href = url + + result = a.protocol + '//' + a.hostname + '/' + + if (a.pathname && a.pathname !== '/') { + result += this.hashCode(a.pathname) + } + + if (a.search) { + result += '?' + this.hashCode(a.search) + } + + if (a.hash) { + result += '#' + this.hashCode(a.hash) + } + + return result + }, + + /** + * Generate random hash. + * @param {String} str + */ + hashCode(str) { + let hash = 0 + let kar + let i + + if (str.length === 0) { + return hash + } + + for (i = 0; i < str.length; i++) { + kar = str.charCodeAt(i) + hash = (hash << 5) - hash + kar + hash = hash & hash + } + + return hash + Math.pow(2, 32) + }, + + /** + * Apply array function to non-array. + * @param {Object} a + */ + realArray(a) { + return Array.prototype.slice.apply(a) + }, + + /** + * Listener callback for onDocLoaded. + * @param {Object} doc + * @param {Function} callback + */ + onDocLoaded(doc, callback) { + if (doc.readyState === 'loading') { + doc.addEventListener('DOMContentLoaded', callback) + } else { + callback() + } + }, + + SCRIPT_IN_WINDOW_TOP: window === window.top, + + /** + * Check for href Window object. + * @param {Object} win + */ + isFriendlyWindow(win) { + let href + try { + href = win.location.href + } catch (e) { + return false + } + return true + }, + + /** + * Get default view from element. + * @param {Object} el + */ + elementWindow(el) { + return el.ownerDocument.defaultView + }, + + /** + * Get viewport size. + * @param {Object} win + */ + viewport(win) { + return { width: win.innerWidth, height: win.innerHeight } + }, + + /** + * Parse query string parameters. + * @param {String} qs + */ + parseQS(qs) { + if (qs.indexOf('http') === 0) { + qs = qs.split('?')[1] + } + let i, kvs, key, val + const dict = {} + qs = qs.split('&') + for (i = 0; i < qs.length; i++) { + kvs = qs[i].split('=') + key = kvs[0] + val = kvs.slice(1).join('=') + try { + dict[key] = window.decodeURIComponent(val) + } catch (e) { + continue + } + } + return dict + }, + + /** + * Send PostMessage response. + * @param {Object} message + * @param {String} event + * @param {String} responseMessage + */ + sendToBackground(message, event, responseMessage) { chrome.runtime.sendMessage(message, (message) => { if (message && typeof message.tracking_enabled !== 'undefined') { if (message.tracking_enabled) { @@ -172,1373 +172,1369 @@ } } }) - }, - - /** - * Check if anonymous tracking is enabled. - * @param {Function} callback - * @param {Function} elseCallback - * @todo validate if utilCallback or utilElseCallback are being used. - */ - askIfTrackingEnabled: function(callback, elseCallback) { - utilCallback = callback - utilElseCallback = elseCallback - - this.sendToBackground( - 'is_tracking_enabled', - '', - 'tracking_enabled_response' - ) - } - } - - utils.SCRIPT_IN_FRIENDLY_IFRAME = - !utils.SCRIPT_IN_WINDOW_TOP && utils.isFriendlyWindow(window.parent) - utils.SCRIPT_IN_HOSTILE_IFRAME = - !utils.SCRIPT_IN_WINDOW_TOP && !utils.SCRIPT_IN_FRIENDLY_IFRAME - - /** - * Generate new Logging object. - */ - function LogGenerator() { - this.msgNum = 0 - this.pageMeta = { - url: utils.getPageUrl(), - isHP: window.location.pathname === '/', - referrer: utils.getReferrer(), - rand: Math.floor(Math.random() * 10e12), - startTime: new Date().getTime() - } - } - - LogGenerator.prototype = { - /** - * Log data. - * @param {String} event - * @param {Array} opt_assets - * @param {Array} opt_pageTags - */ - log: function(event, opt_assets, opt_pageTags) { - var opt_video_assets - if (event === 'video' || event === 'invalid-video') { - opt_video_assets = opt_assets || [] - opt_assets = [] - } else { - opt_video_assets = [] - opt_assets = opt_assets || [] - } - var result = { - doc: this.pageMeta, - event: event, - video_assets: opt_video_assets, - assets: opt_assets, - version: '3', - mrev: '15a9f21-d', - msgNum: this.msgNum, - timestamp: new Date().getTime(), - pageVis: document.visibilityState, - pageFoc: document.hasFocus(), - pageTags: opt_pageTags || [] - } - this.msgNum++ - return result - } - } - - utils.LogGenerator = LogGenerator - - let utilCallback, utilElseCallback - - exports.utils = utils - })(exports) - ;(function(exports) { - var SizeMatcher = { - VALID_AD_SIZES: [ - [300, 50], - [320, 50], - [160, 600], - [300, 250], - [300, 600], - [300, 1050], - [336, 280], - [336, 850], - [468, 60], - [728, 90], - [728, 250], - [728, 270], - [970, 66], - [970, 90], - [970, 125], - [970, 250], - [970, 400], - [970, 415], - [1280, 100] - ], - - PX_SIZE_TOL: 10, - - /** - * Get ad size. - * @param {Int} width - * @param {Int} height - */ - getMatchedAdSize: function(width, height) { - if (!this.set) { - this.set = this._makeSizeSet() - } - - return this.set[Math.round(width) + 'x' + Math.round(height)] - }, - - /** - * Check element size. - * @param {HTMLElement} el - */ - elementIsAdShaped: function(el) { - return !!this.getMatchedAdSizeForElement(el) - }, - - /** - * Get ad size. - * @param {HTMLElement} el - * @todo better description - */ - getMatchedAdSizeForElement: function(el) { - var rect = el.getBoundingClientRect() - return this.getMatchedAdSize(rect.width, rect.height) - }, - - /** - * Generate ad sizes. - */ - _makeSizeSet: function() { - var set = {} - var i - var xfuz - var yfuz - var size - var width - var height - - for (i = 0; i < this.VALID_AD_SIZES.length; i++) { - for (xfuz = -this.PX_SIZE_TOL; xfuz <= this.PX_SIZE_TOL; xfuz++) { - for (yfuz = -this.PX_SIZE_TOL; yfuz <= this.PX_SIZE_TOL; yfuz++) { - size = this.VALID_AD_SIZES[i] - width = size[0] + xfuz - height = size[1] + yfuz - set[width + 'x' + height] = size - } - } - } - return set - } - } - - var Throttler = { - MAX_SEARCHES_PER_WINDOW: 10, - MAX_SEARCHES_PER_ELEMENT: 2, - - /** - * Count number of elements. - * @param {HTMLElement} el - */ - countSearch: (el) => { - if (typeof el.searches !== 'number') { - el.searches = 0 - } - - el.searches += 1 - }, - - /** - * - * @param {*} el - * @param {*} max - * - * @todo add description - */ - throttle: function(el, max) { - if (typeof el.searches === 'number' && el.searches >= max) { - return true - } - return false - }, - - /** - * - * @param {*} el - * - * @todo add description - */ - throttleElement: function(el) { - return this.throttle(el, this.MAX_SEARCHES_PER_ELEMENT) - }, - - /** - * - * @param {*} win - * - * @todo add description - */ - throttleWin: function(win) { - return this.throttle(win, this.MAX_SEARCHES_PER_WINDOW) - }, - - /** - * - * @param {*} el - * - * @todo add description - */ - getCount: function(el) { - return el.searches || 0 - } - } - - /** - * Initialize window and document elements. - * @param {*} win - */ - function TopSearcher(win) { - this.win = win - this.doc = win.document - } - - /** - * Add search function. - */ - TopSearcher.prototype.search = function() { - var candidates = exports.utils.realArray( - this.doc.querySelectorAll('img, object, embed') - ), - html5Ad, - ads = [] - - ads = ads.concat( - candidates.filter(function(el) { - if (!el.mpAdFound && !Throttler.throttleElement(el)) { - Throttler.countSearch(el) - if ( - (el.tagName !== 'IMG' || isStandardImage(el)) && - SizeMatcher.elementIsAdShaped(el) - ) { - el.mpAdFound = true - return true - } - } - return false - }) - ) - - html5Ad = this._mainGetHTMLAd() - if (html5Ad) { - html5Ad.html5 = true - html5Ad.mpAdFound = true - ads.push(html5Ad) - } - - return ads - } - - /** - * @todo add description - */ - TopSearcher.prototype._mainGetHTMLAd = function() { - var styles = this.doc.querySelectorAll( - 'div > style, div > link[rel="stylesheet"]' - ), - i, - div - for (i = 0; i < styles.length; i++) { - div = styles[i].parentNode - if ( - !div.mpAdFound && - SizeMatcher.elementIsAdShaped(div) && - this._jumpedOut(div) - ) { - return div - } - } - } - - /** - * @todo add description - */ - TopSearcher.prototype._jumpedOut = function(el) { - var siblings, ifrs - siblings = exports.utils.realArray(el.parentNode.children) - ifrs = siblings.filter(function(el) { - return ( - el.tagName === 'IFRAME' && - el.offsetWidth === 0 && - el.offsetHeight === 0 - ) - }) - return ifrs.length > 0 - } - - /** - * - * @param {*} win - * - * @todo add description - */ - function IframeSearcher(win) { - this.MIN_AD_AREA = 14000 - this.MIN_WINDOW_PX = 10 - - this.win = win - this.doc = win.document - this.body = win.document.body - this.winClickTag = win.clickTag - this.adSizeMeta = this._getAdSizeMeta() - this.numElementsInBody = - (this.body && this.body.querySelectorAll('*').length) || 0 - - this.shouldSearchWindow = false - if ( - !this.win.mpAdFound && - this.body && - !Throttler.throttleWin(this.win) - ) { - this.winWidth = this.win.innerWidth - this.winHeight = this.win.innerHeight - if ( - this._meetsMinAdSize(this.winWidth, this.winHeight) && - !this._containsLargeIframes() - ) { - this.shouldSearchWindow = true - } - } - } - - /** - * @todo add description - */ - IframeSearcher.prototype.search = function() { - var ad - - if (this.shouldSearchWindow) { - ad = this._search() - if (ad) { - ad.mpAdFound = true - win.mpAdFound = true - return ad - } - Throttler.countSearch(this.win) - } - - return null - } - - /** - * @todo add description - */ - IframeSearcher.prototype._search = function() { - var _this = this, - stdCandidates, - html5Candidates, - stdEl, - html5El - - stdCandidates = this.body.querySelectorAll('img, object, embed') - - stdEl = getFirst(stdCandidates, function(el) { - if ( - !el.mpAdFound && - !Throttler.throttleElement(el) && - (el.tagName !== 'IMG' || isStandardImage(el)) && - _this._elementIsAtLeastAsBigAsWindow(el) - ) { - return true - } - Throttler.countSearch(el) - return false - }) - - if (stdEl) { - return stdEl - } - - if (this._isHTML5Iframe()) { - html5Candidates = this.doc.querySelectorAll( - 'body, canvas, button, video, svg, div' - ) - html5El = getFirst(html5Candidates, function(el) { - if (_this._elementIsAtLeastAsBigAsWindow(el)) { - return true - } - Throttler.countSearch(el) - return false - }) - } - - if (html5El) { - html5El.html5 = true - html5El.winClickTag = this.winClickTag - html5El.adSizeMeta = this.adSizeMeta - return html5El - } - - return null - } - - /** - * @todo add description - */ - IframeSearcher.prototype._isHTML5Iframe = function() { - if (this.winClickTag || this.adSizeMeta) { - return true - } - - if ( - this.doc.querySelectorAll('canvas', 'button', 'video', 'svg').length > 0 - ) { - return true - } - - if ( - this.numElementsInBody >= 5 && - Throttler.getCount(this.win) > 0 && - this.doc.querySelectorAll('div').length > 0 - ) { - return true - } - - return false - } - - /** - * @todo add description - */ - IframeSearcher.prototype._elementIsAtLeastAsBigAsWindow = function(el) { - var rect = el.getBoundingClientRect(), - tol = 0.95 - - return ( - rect.width >= tol * this.winWidth && rect.height >= tol * this.winHeight - ) - } - - /** - * @todo add description - */ - IframeSearcher.prototype._meetsMinAdSize = function(width, height) { - return width * height >= this.MIN_AD_AREA - } - - /** - * @todo add description - */ - IframeSearcher.prototype._containsLargeIframes = function() { - var iframes = this.doc.querySelectorAll('iframe') - var rect - var i - for (i = 0; i < iframes.length; i++) { - rect = iframes[i].getBoundingClientRect() - if ( - rect.width > this.MIN_WINDOW_PX || - rect.height > this.MIN_WINDOW_PX - ) { - return true - } - } - return false - } - - /** - * @todo add description - */ - IframeSearcher.prototype._getAdSizeMeta = function() { - var adSizeMeta = this.doc.querySelectorAll('meta[name="ad.size"]') - if (adSizeMeta.length > 0) { - return adSizeMeta[0].content - } else { - return null - } - } - - /** - * - * @param {*} arr - * @param {*} testFn - * - * @todo add description - */ - function getFirst(arr, testFn) { - var i, el - for (i = 0; i < arr.length; i++) { - el = arr[i] - if (testFn(el)) { - return el - } - } - return null - } - - /** - * Check for image attributes. - * @param {HTMLElement} img - */ - function isStandardImage(img) { - return ( - img.src && - (img.parentNode.tagName === 'A' || img.getAttribute('onclick')) - ) - } - - /** - * Extract iFrames from page. - * @param {Object} win - */ - function getFriendlyIframes(win) { - var iframes = win.document.querySelectorAll('iframe') - iframes = exports.utils.realArray(iframes) - var friendlyIframes = iframes.filter(function(ifr) { - return exports.utils.isFriendlyWindow(ifr.contentWindow) - }) - return friendlyIframes - } - - /** - * - * @param {*} win - */ - function findAds(win) { - var i, - iframes, - searcher, - ad, - ads = [] - - if (win === win.top) { - searcher = new TopSearcher(win) - ads = ads.concat(searcher.search()) - } else { - searcher = new IframeSearcher(win) - ad = searcher.search() - if (ad) { - ads.push(ad) - } - } - - iframes = getFriendlyIframes(win) - for (i = 0; i < iframes.length; i++) { - ads = ads.concat(findAds(iframes[i].contentWindow)) - } - - return ads - } - - exports.adfinder = { - getMatchedAdSize: SizeMatcher.getMatchedAdSize.bind(SizeMatcher), - findAds: findAds - } - })(exports) - ;(function(exports) { - var parser = { - TAGS_WITH_SRC_ATTR: { - IMG: true, - SCRIPT: true, - IFRAME: true, - EMBED: true - }, - - MAX_ATTR_LEN: 100, - - /** - * - * @param {*} el - * @param {*} params - * - * @todo add description - */ - getUrl: function(el, params) { - var url - - if (this.TAGS_WITH_SRC_ATTR.hasOwnProperty(el.tagName)) { - url = el.src - } else if (el.tagName === 'OBJECT') { - url = el.data || (params && params.movie) || null - } else if (el.tagName === 'A') { - url = el.href - } - - if (url && url.indexOf('http') === 0) { - return url - } else { - return null - } - }, - - /** - * - * @param {*} el - * - * @todo add description - */ - getParams: function(el) { - if (el.tagName !== 'OBJECT') { - return null - } - - var i, child - var params = {} - var children = el.children - for (i = 0; i < children.length; i++) { - child = children[i] - if (child.tagName === 'PARAM' && child.name) { - params[child.name.toLowerCase()] = child.value - } - } - return params - }, - - /** - * Get element position. - * @param {HTMLElement} el - */ - getPosition: function(el) { - var rect = el.getBoundingClientRect() - var win = exports.utils.elementWindow(el) - - return { - width: Math.round(rect.width), - height: Math.round(rect.height), - left: Math.round(rect.left + win.pageXOffset), - top: Math.round(rect.top + win.pageYOffset) - } - }, - - /** - * - * @param {*} el - * @param {*} params - * @param {*} url - * - * @todo add description - */ - getFlashvars: function(el, params, url) { - var flashvars - var urlQS = url && url.split('?')[1] - - if (el.tagName === 'EMBED') { - flashvars = el.getAttribute('flashvars') || urlQS - } else if (el.tagName === 'OBJECT') { - flashvars = params.flashvars || el.getAttribute('flashvars') || urlQS - } - - return (flashvars && exports.utils.parseQS(flashvars)) || null - }, - - /** - * - * @param {*} el - * @param {*} flashvars - * - * @todo add description - */ - findClickThru: function(el, flashvars) { - var key - if (el.tagName === 'IMG' && el.parentElement.tagName === 'A') { - return el.parentElement.href - } else if (flashvars) { - for (key in flashvars) { - if (flashvars.hasOwnProperty(key)) { - if (key.toLowerCase().indexOf('clicktag') === 0) { - return flashvars[key] - } - } - } - } - return null - }, - - /** - * Get element attribute. - * @param {HTMLElement} el - * @param {String} name - */ - getAttr: function(el, name) { - var val = el.getAttribute(name) - - if (val && val.slice && val.toString) { - return val.slice(0, this.MAX_ATTR_LEN).toString() - } else { - return null - } - }, - - /** - * - * @param {*} obj - * @param {*} name - * @param {*} val - * - * @todo add description - */ - putPropIfExists: function(obj, name, val) { - if (val) { - obj[name] = val - } - }, - - /** - * - * @param {*} obj - * @param {*} el - * @param {*} name - * - * @todo add description - */ - putAttrIfExists: function(obj, el, name) { - var val = this.getAttr(el, name) - this.putPropIfExists(obj, name, val) - }, - - /** - * Convert Element to JSON - * @param {HTMLElement} el - * @param {Boolean} opt_findClickThru - */ - elementToJSON: function(el, opt_findClickThru) { - var pos = this.getPosition(el) - var params = this.getParams(el) - var url = this.getUrl(el, params) - var flashvars = this.getFlashvars(el, params, url) - var clickThru = opt_findClickThru && this.findClickThru(el, flashvars) - var json = { - tagName: el.tagName, - width: pos.width, - height: pos.height, - left: pos.left, - top: pos.top, - children: [] - } - - if (params) { - delete params.flashvars - } - - this.putAttrIfExists(json, el, 'id') - this.putAttrIfExists(json, el, 'class') - this.putAttrIfExists(json, el, 'name') - - this.putPropIfExists(json, 'flashvars', flashvars) - this.putPropIfExists(json, 'url', url) - this.putPropIfExists(json, 'params', params) - this.putPropIfExists(json, 'clickThru', clickThru) - - return json - } - } - - exports.parser = { elementToJSON: parser.elementToJSON.bind(parser) } - })(exports) - - // Anonymous invocation. - ;(function(exports) { - /** - * Setter for ad data. - * @param {*} adData - */ - var ContextManager = function(adData) { - this.adData = adData - } - - ContextManager.prototype = { - CONTAINER_SIZE_TOL: 0.4, - ASPECT_RATIO_FOR_LEADERBOARDS: 2, - - /** - * Check if iframe is valid. - * @param {HTMLElement} el - * @param {HTMLElement} opt_curWin - */ - isValidContainer: function(el, opt_curWin) { - var cWidth = el.clientWidth - var cHeight = el.clientHeight - - var adWidth = this.adData.width - var adHeight = this.adData.height - - var winWidth = opt_curWin && opt_curWin.innerWidth - var winHeight = opt_curWin && opt_curWin.innerHeight - var similarWin = - opt_curWin && - this.withinTol(adWidth, winWidth) && - this.withinTol(adHeight, winHeight) - - var similarSizeX = this.withinTol(adWidth, cWidth) - var similarSizeY = this.withinTol(adHeight, cHeight) - var adAspect = adWidth / adHeight - - return ( - similarWin || - el.tagName === 'A' || - (adAspect >= this.ASPECT_RATIO_FOR_LEADERBOARDS && similarSizeY) || - (similarSizeX && similarSizeY) - ) - }, - - /** - * Check tolerance. - * @param {Int} adlen - * @param {Int} conlen - */ - withinTol: function(adlen, conlen) { - var pct = (conlen - adlen) / adlen - - return pct <= this.CONTAINER_SIZE_TOL - }, - - /** - * Serialize elements. - * @param {*} el - * @todo define parameter type. - */ - serializeElements: function(el) { - if (!el) { - return - } - var i - var ifrWin - var adId = this.adData.adId - var elIsAd = false - - if (adId && el[adId] && el[adId].isAd === true) { - elIsAd = true - } - - var json = exports.parser.elementToJSON(el, elIsAd) - var childJSON - - if (elIsAd) { - json.adId = adId - this.adData.element = {} - - var keys = Object.keys(json) - for (i = 0; i < keys.length; i++) { - var key = keys[i] - if (key !== 'children' && key !== 'contents') { - this.adData.element[key] = json[key] - } - } - } - - var children = exports.utils - .realArray(el.children) - .filter(function(el) { - var param = el.tagName === 'PARAM' - var inlineScript = - el.tagName === 'SCRIPT' && - !(el.src && el.src.indexOf('http') >= 0) - var noScript = el.tagName === 'NOSCRIPT' - return !(param || inlineScript || noScript) - }) - - for (i = 0; i < children.length; i++) { - childJSON = this.serializeElements(children[i]) - if (childJSON) { - json.children.push(childJSON) - } - } - - if (el.tagName === 'IFRAME') { - ifrWin = el.contentWindow - - if (adId && el[adId] && el[adId].needsWindow) { - json.contents = this.adData.serializedIframeContents - el[adId].needsWindow = false - delete this.adData.serializedIframeContents - } else if (exports.utils.isFriendlyWindow(ifrWin)) { - childJSON = this.serializeElements(ifrWin.document.documentElement) - if (childJSON) { - json.contents = childJSON - } - } - } - - if ( - json.children.length > 0 || - json.adId || - json.tagName === 'IFRAME' || - json.url - ) { - return json - } else { - return null - } - }, - - /** - * Get element containers. - * @param {*} containerEl - */ - captureHTML: function(containerEl) { - this.adData.context = this.serializeElements(containerEl) - }, - - /** - * Get number of Nodes. - * @param {HTMLElement} el - */ - nodeCount: function(el) { - return el.getElementsByTagName('*').length + 1 - }, - - /** - * - * @param {*} curWin - * @param {*} referenceElement - * - * @todo add description - */ - highestContainer: function(curWin, referenceElement) { - var curContainer = referenceElement - var docEl = curWin.document.documentElement - var parentContainer - - if (curWin !== curWin.top && this.isValidContainer(docEl, curWin)) { - return docEl - } - - while (true) { - parentContainer = curContainer.parentElement - if (parentContainer && this.isValidContainer(parentContainer)) { - curContainer = parentContainer - } else { - return curContainer - } - } - } - } - - var tagfinder = { - /** - * - * @param {*} adData - * @param {*} opt_el - * @param {*} opt_winPos - * - * @todo add description - */ - setPositions: function(adData, opt_el, opt_winPos) { - var el = opt_el || adData.context - var winPos = opt_winPos || { left: 0, top: 0 } - var ifrPos - - el.left += winPos.left - el.top += winPos.top - - if (el.children) { - el.children.forEach(function(child) { - this.setPositions(adData, child, winPos) - }, this) - } - - if (el.contents) { - ifrPos = { left: el.left, top: el.top } - this.setPositions(adData, el.contents, ifrPos) - } - - if (el.adId === adData.adId) { - adData.element.left = el.left - adData.element.top = el.top - } - }, - - /** - * - * @param {*} adData - * @param {*} referenceElement - * - * @todo add description - */ - appendTags: (adData, referenceElement) => { - var mgr = new ContextManager(adData) - var curWin = exports.utils.elementWindow(referenceElement) - var highestContainer - - while (true) { - highestContainer = mgr.highestContainer(curWin, referenceElement) - mgr.captureHTML(highestContainer) - if (curWin === curWin.top) { - break - } else { - curWin.mpAdFound = true - - mgr.adData.serializedIframeContents = mgr.adData.context - - if (exports.utils.isFriendlyWindow(curWin.parent)) { - referenceElement = curWin.frameElement - referenceElement[mgr.adData.adId] = { needsWindow: true } - curWin = curWin.parent - } else { - break - } - } - } - return { - referenceElement: referenceElement, - highestContainer: highestContainer - } - } - } - - exports.tagfinder = tagfinder - })(exports) - ;(function(exports) { - var _onAdFound - var _logGen = new exports.utils.LogGenerator() - var _pageTags - var INIT_MS_BW_SEARCHES = 2000 - var PAGE_TAG_RE = new RegExp('gpt|oascentral') - var POST_MSG_ID = '1554456894-8541-12665-19466-15909' - var AD_SERVER_RE = new RegExp('^(google_ads_iframe|oas_frame|atwAdFrame)') - - /** - * Get script tags from document. - * @param {Object} doc - */ - function getPageTags(doc) { - var scripts = doc.getElementsByTagName('script') - var pageTags = [] - scripts = exports.utils.realArray(scripts) - scripts.forEach(function(script) { - if (PAGE_TAG_RE.exec(script.src)) { - pageTags.push({ tagName: 'SCRIPT', url: script.src }) - } - }) - return pageTags - } - - /** - * Send message to parent iFrames. - * @param {String} adData - */ - function messageAllParentFrames(adData) { - adData.postMessageId = POST_MSG_ID - - adData = JSON.stringify(adData) - - var win = window - while (win !== win.top) { - win = win.parent - win.postMessage(adData, '*') - } - } - - /** - * - * @param {String} adData - * @param {HTMLElement} referenceElement - * - * @todo update description - */ - function appendTagsAndSendToParent(adData, referenceElement) { - var results = exports.tagfinder.appendTags(adData, referenceElement) - if (exports.utils.SCRIPT_IN_HOSTILE_IFRAME) { - messageAllParentFrames(adData) - } else if (exports.utils.SCRIPT_IN_WINDOW_TOP) { - exports.tagfinder.setPositions(adData) - - adData.matchedSize = exports.adfinder.getMatchedAdSize( - adData.width, - adData.height - ) - if (!adData.matchedSize) { - if (AD_SERVER_RE.exec(results.referenceElement.id)) { - adData.matchedSize = [adData.width, adData.height] - adData.oddSize = true - } else { - return - } - } - delete adData.width - delete adData.height - adData.curPageUrl = exports.utils.getPageUrl() - _pageTags = _pageTags || getPageTags(document) - var log = _logGen.log('ad', [adData], _pageTags) - - if (_onAdFound) { - _onAdFound(log, results.referenceElement) - } - } - } - - /** - * SetTimeout wrapper for extracting ads. - */ - function extractAdsWrapper() { - if ( - exports.utils.SCRIPT_IN_WINDOW_TOP || - document.readyState === 'complete' - ) { - extractAds() - } - setTimeout(function() { - extractAdsWrapper() - }, INIT_MS_BW_SEARCHES) - } - - /** - * Main function for extracting ads after loaded. - */ - function extractAds() { - var ads = exports.adfinder.findAds(window) - ads.forEach(function(ad) { - var startTime = new Date().getTime() - var adId = startTime + '-' + Math.floor(Math.random() * 10e12) - - var adData = { - width: Math.round(ad.offsetWidth), - height: Math.round(ad.offsetHeight), - startTime: startTime, - adId: adId, - html5: ad.html5 || false - } - - if (ad.html5) { - adData.adSizeMeta = ad.adSizeMeta || null - adData.winClickTag = ad.winClickTag || null - } - - ad[adId] = { isAd: true } - - appendTagsAndSendToParent(adData, ad) - }) - } - - /** - * Check if window is child of parent. - * @param {Object} myWin - * @param {Object} otherWin - */ - function isChildWin(myWin, otherWin) { - var parentWin = otherWin.parent - while (parentWin !== otherWin) { - if (parentWin === myWin) { - return true - } - otherWin = parentWin - parentWin = parentWin.parent - } - return false - } - - /** - * - * @param {*} win - * @param {*} winToMatch - * - * @todo update description - */ - function iframeFromWindow(win, winToMatch) { - var i, - ifr, - ifrWin, - iframes = win.document.querySelectorAll('iframe') - - for (i = 0; i < iframes.length; i++) { - ifr = iframes[i] - if (ifr.contentWindow === winToMatch) { - return ifr - } - } - - for (i = 0; i < iframes.length; i++) { - ifrWin = iframes[i].contentWindow - if (exports.utils.isFriendlyWindow(ifrWin)) { - ifr = iframeFromWindow(ifrWin, winToMatch) - if (ifr) { - return ifr - } - } - } - } - - /** - * - * @param {*} event - * - * @todo update description - */ - function onPostMessage(event) { - var adData, - ifrWin = event.source, - myWin = window.document.defaultView, - ifrTag - - if ( - typeof event.data === 'string' && - event.data.indexOf(POST_MSG_ID) != -1 - ) { - try { - adData = JSON.parse(event.data) - } catch (e) { - return - } - } else return - - if (adData.postMessageId === POST_MSG_ID) { - delete adData.postMessageId - - event.stopImmediatePropagation() - - if (isChildWin(myWin, ifrWin)) { - if (exports.utils.isFriendlyWindow(ifrWin)) { - ifrTag = ifrWin.frameElement - } else { - ifrTag = iframeFromWindow(myWin, ifrWin) - } - - if (ifrTag) { - ifrTag[adData.adId] = { needsWindow: true } - appendTagsAndSendToParent(adData, ifrTag) - } - } - } - } - - /** - * - * @param {*} msg - * @param {*} sender - * @param {*} callback - * - * @todo update description - */ - function onVideoMessage(msg, sender, callback) { - var log - if (msg.event === 'new-video-ad') { - msg.assets.forEach(function(asset) {}) - log = _logGen.log('video', msg.assets) - } else { - log = _logGen.log('invalid-video', msg.assets) - } - - msg.assets.forEach(function(a) { - delete a.isVideo - }) - log.displayAdFound = msg.displayAdFound - log.requests = msg.requests - log.data = msg.event_data - - log.doc.finalPageUrl = log.doc.url - log.doc.url = exports.utils.normalizeUrl(msg.origUrl) - - _onAdFound(log) - } - - /** - * Add background listener. - * @param {String} event - * @param {Function} callback - */ - function addBackgroundListener(event, callback) { - chrome.runtime.onMessage.addListener(function(msg) { + }, + + /** + * Check if anonymous tracking is enabled. + * @param {Function} callback + * @param {Function} elseCallback + * @todo validate if utilCallback or utilElseCallback are being used. + */ + askIfTrackingEnabled(callback, elseCallback) { + utilCallback = callback + utilElseCallback = elseCallback + + this.sendToBackground( + 'is_tracking_enabled', + '', + 'tracking_enabled_response' + ) + }, + } + + utils.SCRIPT_IN_FRIENDLY_IFRAME = + !utils.SCRIPT_IN_WINDOW_TOP && utils.isFriendlyWindow(window.parent) + utils.SCRIPT_IN_HOSTILE_IFRAME = + !utils.SCRIPT_IN_WINDOW_TOP && !utils.SCRIPT_IN_FRIENDLY_IFRAME + + /** + * Generate new Logging object. + */ + function LogGenerator() { + this.msgNum = 0 + this.pageMeta = { + url: utils.getPageUrl(), + isHP: window.location.pathname === '/', + referrer: utils.getReferrer(), + rand: Math.floor(Math.random() * 10e12), + startTime: new Date().getTime(), + } + } + + LogGenerator.prototype = { + /** + * Log data. + * @param {String} event + * @param {Array} opt_assets + * @param {Array} opt_pageTags + */ + log(event, opt_assets, opt_pageTags) { + let opt_video_assets + if (event === 'video' || event === 'invalid-video') { + opt_video_assets = opt_assets || [] + opt_assets = [] + } else { + opt_video_assets = [] + opt_assets = opt_assets || [] + } + const result = { + doc: this.pageMeta, + event, + video_assets: opt_video_assets, + assets: opt_assets, + version: '3', + mrev: '15a9f21-d', + msgNum: this.msgNum, + timestamp: new Date().getTime(), + pageVis: document.visibilityState, + pageFoc: document.hasFocus(), + pageTags: opt_pageTags || [], + } + this.msgNum++ + return result + }, + } + + utils.LogGenerator = LogGenerator + + let utilCallback, utilElseCallback + + exports.utils = utils + })(exports) + ;(function (exports) { + const SizeMatcher = { + VALID_AD_SIZES: [ + [300, 50], + [320, 50], + [160, 600], + [300, 250], + [300, 600], + [300, 1050], + [336, 280], + [336, 850], + [468, 60], + [728, 90], + [728, 250], + [728, 270], + [970, 66], + [970, 90], + [970, 125], + [970, 250], + [970, 400], + [970, 415], + [1280, 100], + ], + + PX_SIZE_TOL: 10, + + /** + * Get ad size. + * @param {Int} width + * @param {Int} height + */ + getMatchedAdSize(width, height) { + if (!this.set) { + this.set = this._makeSizeSet() + } + + return this.set[Math.round(width) + 'x' + Math.round(height)] + }, + + /** + * Check element size. + * @param {HTMLElement} el + */ + elementIsAdShaped(el) { + return !!this.getMatchedAdSizeForElement(el) + }, + + /** + * Get ad size. + * @param {HTMLElement} el + * @todo better description + */ + getMatchedAdSizeForElement(el) { + const rect = el.getBoundingClientRect() + return this.getMatchedAdSize(rect.width, rect.height) + }, + + /** + * Generate ad sizes. + */ + _makeSizeSet() { + const set = {} + let i + let xfuz + let yfuz + let size + let width + let height + + for (i = 0; i < this.VALID_AD_SIZES.length; i++) { + for (xfuz = -this.PX_SIZE_TOL; xfuz <= this.PX_SIZE_TOL; xfuz++) { + for (yfuz = -this.PX_SIZE_TOL; yfuz <= this.PX_SIZE_TOL; yfuz++) { + size = this.VALID_AD_SIZES[i] + width = size[0] + xfuz + height = size[1] + yfuz + set[width + 'x' + height] = size + } + } + } + return set + }, + } + + const Throttler = { + MAX_SEARCHES_PER_WINDOW: 10, + MAX_SEARCHES_PER_ELEMENT: 2, + + /** + * Count number of elements. + * @param {HTMLElement} el + */ + countSearch: (el) => { + if (typeof el.searches !== 'number') { + el.searches = 0 + } + + el.searches += 1 + }, + + /** + * + * @param {*} el + * @param {*} max + * + * @todo add description + */ + throttle(el, max) { + if (typeof el.searches === 'number' && el.searches >= max) { + return true + } + return false + }, + + /** + * + * @param {*} el + * + * @todo add description + */ + throttleElement(el) { + return this.throttle(el, this.MAX_SEARCHES_PER_ELEMENT) + }, + + /** + * + * @param {*} win + * + * @todo add description + */ + throttleWin(win) { + return this.throttle(win, this.MAX_SEARCHES_PER_WINDOW) + }, + + /** + * + * @param {*} el + * + * @todo add description + */ + getCount(el) { + return el.searches || 0 + }, + } + + /** + * Initialize window and document elements. + * @param {*} win + */ + function TopSearcher(win) { + this.win = win + this.doc = win.document + } + + /** + * Add search function. + */ + TopSearcher.prototype.search = function () { + const candidates = exports.utils.realArray( + this.doc.querySelectorAll('img, object, embed') + ) + let html5Ad + let ads = [] + + ads = ads.concat( + candidates.filter(function (el) { + if (!el.mpAdFound && !Throttler.throttleElement(el)) { + Throttler.countSearch(el) + if ( + (el.tagName !== 'IMG' || isStandardImage(el)) && + SizeMatcher.elementIsAdShaped(el) + ) { + el.mpAdFound = true + return true + } + } + return false + }) + ) + + html5Ad = this._mainGetHTMLAd() + if (html5Ad) { + html5Ad.html5 = true + html5Ad.mpAdFound = true + ads.push(html5Ad) + } + + return ads + } + + /** + * @todo add description + */ + TopSearcher.prototype._mainGetHTMLAd = function () { + const styles = this.doc.querySelectorAll( + 'div > style, div > link[rel="stylesheet"]' + ) + let i + let div + for (i = 0; i < styles.length; i++) { + div = styles[i].parentNode + if ( + !div.mpAdFound && + SizeMatcher.elementIsAdShaped(div) && + this._jumpedOut(div) + ) { + return div + } + } + } + + /** + * @todo add description + */ + TopSearcher.prototype._jumpedOut = function (el) { + let siblings, ifrs + siblings = exports.utils.realArray(el.parentNode.children) + ifrs = siblings.filter(function (el) { + return ( + el.tagName === 'IFRAME' && + el.offsetWidth === 0 && + el.offsetHeight === 0 + ) + }) + return ifrs.length > 0 + } + + /** + * + * @param {*} win + * + * @todo add description + */ + function IframeSearcher(win) { + this.MIN_AD_AREA = 14000 + this.MIN_WINDOW_PX = 10 + + this.win = win + this.doc = win.document + this.body = win.document.body + this.winClickTag = win.clickTag + this.adSizeMeta = this._getAdSizeMeta() + this.numElementsInBody = + (this.body && this.body.querySelectorAll('*').length) || 0 + + this.shouldSearchWindow = false + if ( + !this.win.mpAdFound && + this.body && + !Throttler.throttleWin(this.win) + ) { + this.winWidth = this.win.innerWidth + this.winHeight = this.win.innerHeight + if ( + this._meetsMinAdSize(this.winWidth, this.winHeight) && + !this._containsLargeIframes() + ) { + this.shouldSearchWindow = true + } + } + } + + /** + * @todo add description + */ + IframeSearcher.prototype.search = function () { + let ad + + if (this.shouldSearchWindow) { + ad = this._search() + if (ad) { + ad.mpAdFound = true + win.mpAdFound = true + return ad + } + Throttler.countSearch(this.win) + } + + return null + } + + /** + * @todo add description + */ + IframeSearcher.prototype._search = function () { + const _this = this + let stdCandidates + let html5Candidates + let stdEl + let html5El + + stdCandidates = this.body.querySelectorAll('img, object, embed') + + stdEl = getFirst(stdCandidates, function (el) { + if ( + !el.mpAdFound && + !Throttler.throttleElement(el) && + (el.tagName !== 'IMG' || isStandardImage(el)) && + _this._elementIsAtLeastAsBigAsWindow(el) + ) { + return true + } + Throttler.countSearch(el) + return false + }) + + if (stdEl) { + return stdEl + } + + if (this._isHTML5Iframe()) { + html5Candidates = this.doc.querySelectorAll( + 'body, canvas, button, video, svg, div' + ) + html5El = getFirst(html5Candidates, function (el) { + if (_this._elementIsAtLeastAsBigAsWindow(el)) { + return true + } + Throttler.countSearch(el) + return false + }) + } + + if (html5El) { + html5El.html5 = true + html5El.winClickTag = this.winClickTag + html5El.adSizeMeta = this.adSizeMeta + return html5El + } + + return null + } + + /** + * @todo add description + */ + IframeSearcher.prototype._isHTML5Iframe = function () { + if (this.winClickTag || this.adSizeMeta) { + return true + } + + if ( + this.doc.querySelectorAll('canvas', 'button', 'video', 'svg').length > 0 + ) { + return true + } + + if ( + this.numElementsInBody >= 5 && + Throttler.getCount(this.win) > 0 && + this.doc.querySelectorAll('div').length > 0 + ) { + return true + } + + return false + } + + /** + * @todo add description + */ + IframeSearcher.prototype._elementIsAtLeastAsBigAsWindow = function (el) { + const rect = el.getBoundingClientRect() + const tol = 0.95 + + return ( + rect.width >= tol * this.winWidth && rect.height >= tol * this.winHeight + ) + } + + /** + * @todo add description + */ + IframeSearcher.prototype._meetsMinAdSize = function (width, height) { + return width * height >= this.MIN_AD_AREA + } + + /** + * @todo add description + */ + IframeSearcher.prototype._containsLargeIframes = function () { + const iframes = this.doc.querySelectorAll('iframe') + let rect + let i + for (i = 0; i < iframes.length; i++) { + rect = iframes[i].getBoundingClientRect() + if ( + rect.width > this.MIN_WINDOW_PX || + rect.height > this.MIN_WINDOW_PX + ) { + return true + } + } + return false + } + + /** + * @todo add description + */ + IframeSearcher.prototype._getAdSizeMeta = function () { + const adSizeMeta = this.doc.querySelectorAll('meta[name="ad.size"]') + if (adSizeMeta.length > 0) { + return adSizeMeta[0].content + } else { + return null + } + } + + /** + * + * @param {*} arr + * @param {*} testFn + * + * @todo add description + */ + function getFirst(arr, testFn) { + let i, el + for (i = 0; i < arr.length; i++) { + el = arr[i] + if (testFn(el)) { + return el + } + } + return null + } + + /** + * Check for image attributes. + * @param {HTMLElement} img + */ + function isStandardImage(img) { + return ( + img.src && + (img.parentNode.tagName === 'A' || img.getAttribute('onclick')) + ) + } + + /** + * Extract iFrames from page. + * @param {Object} win + */ + function getFriendlyIframes(win) { + let iframes = win.document.querySelectorAll('iframe') + iframes = exports.utils.realArray(iframes) + const friendlyIframes = iframes.filter(function (ifr) { + return exports.utils.isFriendlyWindow(ifr.contentWindow) + }) + return friendlyIframes + } + + /** + * + * @param {*} win + */ + function findAds(win) { + let i + let iframes + let searcher + let ad + let ads = [] + + if (win === win.top) { + searcher = new TopSearcher(win) + ads = ads.concat(searcher.search()) + } else { + searcher = new IframeSearcher(win) + ad = searcher.search() + if (ad) { + ads.push(ad) + } + } + + iframes = getFriendlyIframes(win) + for (i = 0; i < iframes.length; i++) { + ads = ads.concat(findAds(iframes[i].contentWindow)) + } + + return ads + } + + exports.adfinder = { + getMatchedAdSize: SizeMatcher.getMatchedAdSize.bind(SizeMatcher), + findAds, + } + })(exports) + ;(function (exports) { + const parser = { + TAGS_WITH_SRC_ATTR: { + IMG: true, + SCRIPT: true, + IFRAME: true, + EMBED: true, + }, + + MAX_ATTR_LEN: 100, + + /** + * + * @param {*} el + * @param {*} params + * + * @todo add description + */ + getUrl(el, params) { + let url + + if (this.TAGS_WITH_SRC_ATTR.hasOwnProperty(el.tagName)) { + url = el.src + } else if (el.tagName === 'OBJECT') { + url = el.data || (params && params.movie) || null + } else if (el.tagName === 'A') { + url = el.href + } + + if (url && url.indexOf('http') === 0) { + return url + } else { + return null + } + }, + + /** + * + * @param {*} el + * + * @todo add description + */ + getParams(el) { + if (el.tagName !== 'OBJECT') { + return null + } + + let i, child + const params = {} + const children = el.children + for (i = 0; i < children.length; i++) { + child = children[i] + if (child.tagName === 'PARAM' && child.name) { + params[child.name.toLowerCase()] = child.value + } + } + return params + }, + + /** + * Get element position. + * @param {HTMLElement} el + */ + getPosition(el) { + const rect = el.getBoundingClientRect() + const win = exports.utils.elementWindow(el) + + return { + width: Math.round(rect.width), + height: Math.round(rect.height), + left: Math.round(rect.left + win.pageXOffset), + top: Math.round(rect.top + win.pageYOffset), + } + }, + + /** + * + * @param {*} el + * @param {*} params + * @param {*} url + * + * @todo add description + */ + getFlashvars(el, params, url) { + let flashvars + const urlQS = url && url.split('?')[1] + + if (el.tagName === 'EMBED') { + flashvars = el.getAttribute('flashvars') || urlQS + } else if (el.tagName === 'OBJECT') { + flashvars = params.flashvars || el.getAttribute('flashvars') || urlQS + } + + return (flashvars && exports.utils.parseQS(flashvars)) || null + }, + + /** + * + * @param {*} el + * @param {*} flashvars + * + * @todo add description + */ + findClickThru(el, flashvars) { + let key + if (el.tagName === 'IMG' && el.parentElement.tagName === 'A') { + return el.parentElement.href + } else if (flashvars) { + for (key in flashvars) { + if (flashvars.hasOwnProperty(key)) { + if (key.toLowerCase().indexOf('clicktag') === 0) { + return flashvars[key] + } + } + } + } + return null + }, + + /** + * Get element attribute. + * @param {HTMLElement} el + * @param {String} name + */ + getAttr(el, name) { + const val = el.getAttribute(name) + + if (val && val.slice && val.toString) { + return val.slice(0, this.MAX_ATTR_LEN).toString() + } else { + return null + } + }, + + /** + * + * @param {*} obj + * @param {*} name + * @param {*} val + * + * @todo add description + */ + putPropIfExists(obj, name, val) { + if (val) { + obj[name] = val + } + }, + + /** + * + * @param {*} obj + * @param {*} el + * @param {*} name + * + * @todo add description + */ + putAttrIfExists(obj, el, name) { + const val = this.getAttr(el, name) + this.putPropIfExists(obj, name, val) + }, + + /** + * Convert Element to JSON + * @param {HTMLElement} el + * @param {Boolean} opt_findClickThru + */ + elementToJSON(el, opt_findClickThru) { + const pos = this.getPosition(el) + const params = this.getParams(el) + const url = this.getUrl(el, params) + const flashvars = this.getFlashvars(el, params, url) + const clickThru = opt_findClickThru && this.findClickThru(el, flashvars) + const json = { + tagName: el.tagName, + width: pos.width, + height: pos.height, + left: pos.left, + top: pos.top, + children: [], + } + + if (params) { + delete params.flashvars + } + + this.putAttrIfExists(json, el, 'id') + this.putAttrIfExists(json, el, 'class') + this.putAttrIfExists(json, el, 'name') + + this.putPropIfExists(json, 'flashvars', flashvars) + this.putPropIfExists(json, 'url', url) + this.putPropIfExists(json, 'params', params) + this.putPropIfExists(json, 'clickThru', clickThru) + + return json + }, + } + + exports.parser = { elementToJSON: parser.elementToJSON.bind(parser) } + })(exports) + + // Anonymous invocation. + ;(function (exports) { + /** + * Setter for ad data. + * @param {*} adData + */ + const ContextManager = function (adData) { + this.adData = adData + } + + ContextManager.prototype = { + CONTAINER_SIZE_TOL: 0.4, + ASPECT_RATIO_FOR_LEADERBOARDS: 2, + + /** + * Check if iframe is valid. + * @param {HTMLElement} el + * @param {HTMLElement} opt_curWin + */ + isValidContainer(el, opt_curWin) { + const cWidth = el.clientWidth + const cHeight = el.clientHeight + + const adWidth = this.adData.width + const adHeight = this.adData.height + + const winWidth = opt_curWin && opt_curWin.innerWidth + const winHeight = opt_curWin && opt_curWin.innerHeight + const similarWin = + opt_curWin && + this.withinTol(adWidth, winWidth) && + this.withinTol(adHeight, winHeight) + + const similarSizeX = this.withinTol(adWidth, cWidth) + const similarSizeY = this.withinTol(adHeight, cHeight) + const adAspect = adWidth / adHeight + + return ( + similarWin || + el.tagName === 'A' || + (adAspect >= this.ASPECT_RATIO_FOR_LEADERBOARDS && similarSizeY) || + (similarSizeX && similarSizeY) + ) + }, + + /** + * Check tolerance. + * @param {Int} adlen + * @param {Int} conlen + */ + withinTol(adlen, conlen) { + const pct = (conlen - adlen) / adlen + + return pct <= this.CONTAINER_SIZE_TOL + }, + + /** + * Serialize elements. + * @param {*} el + * @todo define parameter type. + */ + serializeElements(el) { + if (!el) { + return + } + let i + let ifrWin + const adId = this.adData.adId + let elIsAd = false + + if (adId && el[adId] && el[adId].isAd === true) { + elIsAd = true + } + + const json = exports.parser.elementToJSON(el, elIsAd) + let childJSON + + if (elIsAd) { + json.adId = adId + this.adData.element = {} + + const keys = Object.keys(json) + for (i = 0; i < keys.length; i++) { + const key = keys[i] + if (key !== 'children' && key !== 'contents') { + this.adData.element[key] = json[key] + } + } + } + + const children = exports.utils + .realArray(el.children) + .filter(function (el) { + const param = el.tagName === 'PARAM' + const inlineScript = + el.tagName === 'SCRIPT' && !(el.src && el.src.includes('http')) + const noScript = el.tagName === 'NOSCRIPT' + return !(param || inlineScript || noScript) + }) + + for (i = 0; i < children.length; i++) { + childJSON = this.serializeElements(children[i]) + if (childJSON) { + json.children.push(childJSON) + } + } + + if (el.tagName === 'IFRAME') { + ifrWin = el.contentWindow + + if (adId && el[adId] && el[adId].needsWindow) { + json.contents = this.adData.serializedIframeContents + el[adId].needsWindow = false + delete this.adData.serializedIframeContents + } else if (exports.utils.isFriendlyWindow(ifrWin)) { + childJSON = this.serializeElements(ifrWin.document.documentElement) + if (childJSON) { + json.contents = childJSON + } + } + } + + if ( + json.children.length > 0 || + json.adId || + json.tagName === 'IFRAME' || + json.url + ) { + return json + } else { + return null + } + }, + + /** + * Get element containers. + * @param {*} containerEl + */ + captureHTML(containerEl) { + this.adData.context = this.serializeElements(containerEl) + }, + + /** + * Get number of Nodes. + * @param {HTMLElement} el + */ + nodeCount(el) { + return el.getElementsByTagName('*').length + 1 + }, + + /** + * + * @param {*} curWin + * @param {*} referenceElement + * + * @todo add description + */ + highestContainer(curWin, referenceElement) { + let curContainer = referenceElement + const docEl = curWin.document.documentElement + let parentContainer + + if (curWin !== curWin.top && this.isValidContainer(docEl, curWin)) { + return docEl + } + + while (true) { + parentContainer = curContainer.parentElement + if (parentContainer && this.isValidContainer(parentContainer)) { + curContainer = parentContainer + } else { + return curContainer + } + } + }, + } + + const tagfinder = { + /** + * + * @param {*} adData + * @param {*} opt_el + * @param {*} opt_winPos + * + * @todo add description + */ + setPositions(adData, opt_el, opt_winPos) { + const el = opt_el || adData.context + const winPos = opt_winPos || { left: 0, top: 0 } + let ifrPos + + el.left += winPos.left + el.top += winPos.top + + if (el.children) { + el.children.forEach(function (child) { + this.setPositions(adData, child, winPos) + }, this) + } + + if (el.contents) { + ifrPos = { left: el.left, top: el.top } + this.setPositions(adData, el.contents, ifrPos) + } + + if (el.adId === adData.adId) { + adData.element.left = el.left + adData.element.top = el.top + } + }, + + /** + * + * @param {*} adData + * @param {*} referenceElement + * + * @todo add description + */ + appendTags: (adData, referenceElement) => { + const mgr = new ContextManager(adData) + let curWin = exports.utils.elementWindow(referenceElement) + let highestContainer + + while (true) { + highestContainer = mgr.highestContainer(curWin, referenceElement) + mgr.captureHTML(highestContainer) + if (curWin === curWin.top) { + break + } else { + curWin.mpAdFound = true + + mgr.adData.serializedIframeContents = mgr.adData.context + + if (exports.utils.isFriendlyWindow(curWin.parent)) { + referenceElement = curWin.frameElement + referenceElement[mgr.adData.adId] = { needsWindow: true } + curWin = curWin.parent + } else { + break + } + } + } + return { + referenceElement, + highestContainer, + } + }, + } + + exports.tagfinder = tagfinder + })(exports) + ;(function (exports) { + let _onAdFound + const _logGen = new exports.utils.LogGenerator() + let _pageTags + const INIT_MS_BW_SEARCHES = 2000 + const PAGE_TAG_RE = new RegExp('gpt|oascentral') + const POST_MSG_ID = '1554456894-8541-12665-19466-15909' + const AD_SERVER_RE = new RegExp('^(google_ads_iframe|oas_frame|atwAdFrame)') + + /** + * Get script tags from document. + * @param {Object} doc + */ + function getPageTags(doc) { + let scripts = doc.getElementsByTagName('script') + const pageTags = [] + scripts = exports.utils.realArray(scripts) + scripts.forEach(function (script) { + if (PAGE_TAG_RE.exec(script.src)) { + pageTags.push({ tagName: 'SCRIPT', url: script.src }) + } + }) + return pageTags + } + + /** + * Send message to parent iFrames. + * @param {String} adData + */ + function messageAllParentFrames(adData) { + adData.postMessageId = POST_MSG_ID + + adData = JSON.stringify(adData) + + let win = window + while (win !== win.top) { + win = win.parent + win.postMessage(adData, '*') + } + } + + /** + * + * @param {String} adData + * @param {HTMLElement} referenceElement + * + * @todo update description + */ + function appendTagsAndSendToParent(adData, referenceElement) { + const results = exports.tagfinder.appendTags(adData, referenceElement) + if (exports.utils.SCRIPT_IN_HOSTILE_IFRAME) { + messageAllParentFrames(adData) + } else if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + exports.tagfinder.setPositions(adData) + + adData.matchedSize = exports.adfinder.getMatchedAdSize( + adData.width, + adData.height + ) + if (!adData.matchedSize) { + if (AD_SERVER_RE.exec(results.referenceElement.id)) { + adData.matchedSize = [adData.width, adData.height] + adData.oddSize = true + } else { + return + } + } + delete adData.width + delete adData.height + adData.curPageUrl = exports.utils.getPageUrl() + _pageTags = _pageTags || getPageTags(document) + const log = _logGen.log('ad', [adData], _pageTags) + + if (_onAdFound) { + _onAdFound(log, results.referenceElement) + } + } + } + + /** + * SetTimeout wrapper for extracting ads. + */ + function extractAdsWrapper() { + if ( + exports.utils.SCRIPT_IN_WINDOW_TOP || + document.readyState === 'complete' + ) { + extractAds() + } + setTimeout(function () { + extractAdsWrapper() + }, INIT_MS_BW_SEARCHES) + } + + /** + * Main function for extracting ads after loaded. + */ + function extractAds() { + const ads = exports.adfinder.findAds(window) + ads.forEach(function (ad) { + const startTime = new Date().getTime() + const adId = startTime + '-' + Math.floor(Math.random() * 10e12) + + const adData = { + width: Math.round(ad.offsetWidth), + height: Math.round(ad.offsetHeight), + startTime, + adId, + html5: ad.html5 || false, + } + + if (ad.html5) { + adData.adSizeMeta = ad.adSizeMeta || null + adData.winClickTag = ad.winClickTag || null + } + + ad[adId] = { isAd: true } + + appendTagsAndSendToParent(adData, ad) + }) + } + + /** + * Check if window is child of parent. + * @param {Object} myWin + * @param {Object} otherWin + */ + function isChildWin(myWin, otherWin) { + let parentWin = otherWin.parent + while (parentWin !== otherWin) { + if (parentWin === myWin) { + return true + } + otherWin = parentWin + parentWin = parentWin.parent + } + return false + } + + /** + * + * @param {*} win + * @param {*} winToMatch + * + * @todo update description + */ + function iframeFromWindow(win, winToMatch) { + let i + let ifr + let ifrWin + const iframes = win.document.querySelectorAll('iframe') + + for (i = 0; i < iframes.length; i++) { + ifr = iframes[i] + if (ifr.contentWindow === winToMatch) { + return ifr + } + } + + for (i = 0; i < iframes.length; i++) { + ifrWin = iframes[i].contentWindow + if (exports.utils.isFriendlyWindow(ifrWin)) { + ifr = iframeFromWindow(ifrWin, winToMatch) + if (ifr) { + return ifr + } + } + } + } + + /** + * + * @param {*} event + * + * @todo update description + */ + function onPostMessage(event) { + let adData + const ifrWin = event.source + const myWin = window.document.defaultView + let ifrTag + + if (typeof event.data === 'string' && event.data.includes(POST_MSG_ID)) { + try { + adData = JSON.parse(event.data) + } catch (e) { + return + } + } else return + + if (adData.postMessageId === POST_MSG_ID) { + delete adData.postMessageId + + event.stopImmediatePropagation() + + if (isChildWin(myWin, ifrWin)) { + if (exports.utils.isFriendlyWindow(ifrWin)) { + ifrTag = ifrWin.frameElement + } else { + ifrTag = iframeFromWindow(myWin, ifrWin) + } + + if (ifrTag) { + ifrTag[adData.adId] = { needsWindow: true } + appendTagsAndSendToParent(adData, ifrTag) + } + } + } + } + + /** + * + * @param {*} msg + * @param {*} sender + * @param {*} callback + * + * @todo update description + */ + function onVideoMessage(msg, sender, callback) { + let log + if (msg.event === 'new-video-ad') { + msg.assets.forEach(function (asset) {}) + log = _logGen.log('video', msg.assets) + } else { + log = _logGen.log('invalid-video', msg.assets) + } + + msg.assets.forEach(function (a) { + delete a.isVideo + }) + log.displayAdFound = msg.displayAdFound + log.requests = msg.requests + log.data = msg.event_data + + log.doc.finalPageUrl = log.doc.url + log.doc.url = exports.utils.normalizeUrl(msg.origUrl) + + _onAdFound(log) + } + + /** + * Add background listener. + * @param {String} event + * @param {Function} callback + */ + function addBackgroundListener(event, callback) { + chrome.runtime.onMessage.addListener(function (msg) { if (msg.event === event) { callback(msg) } }) - } - - exports.coordinator = { - /** - * @todo update description - */ - addPostMessageListener: function() { - if (!exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { - window.addEventListener('message', onPostMessage, false) - } - }, - - /** - * - * @param {*} sendFcn - * @param {*} origUrl - * - * @todo update description - */ - blockedRobotsMsgGen: function(sendFcn, origUrl) { - if (origUrl.indexOf('google.com/_/chrome/newtab') === -1) { - var onBlockedRobotsMessage = function() { - var log - log = _logGen.log('invalid-robotstxt', []) - log.doc.finalPageUrl = log.doc.url - log.doc.url = exports.utils.normalizeUrl(origUrl) - - sendFcn(log) - } - return onBlockedRobotsMessage - } else { - return function() {} - } - }, - - /** - * - * @param {*} onAdFound - */ - init: function(onAdFound) { - if (exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { - return false - } - - _onAdFound = onAdFound - if (exports.utils.SCRIPT_IN_WINDOW_TOP) { - var log = _logGen.log('page') - onAdFound(log) - - window.addEventListener('beforeunload', function(event) { - var log = _logGen.log('unload') - log.timing = window.performance.timing - onAdFound(log) - }) - - addBackgroundListener('new-video-ad', onVideoMessage) - addBackgroundListener('new-invalid-video-ad', onVideoMessage) - } - - exports.utils.onDocLoaded(document, extractAdsWrapper) - } - } - })(exports) - - if (exports.utils.SCRIPT_IN_WINDOW_TOP) { - window.adparser = { - init: exports.coordinator.init, - addPostMessageListener: exports.coordinator.addPostMessageListener, - askIfTrackingEnabled: exports.utils.askIfTrackingEnabled, - blockedRobotsMsgGen: exports.coordinator.blockedRobotsMsgGen, - inWindowTop: exports.utils.SCRIPT_IN_WINDOW_TOP, - sendToBackground: exports.utils.sendToBackground - } - } else { - exports.coordinator.addPostMessageListener() - exports.utils.askIfTrackingEnabled( - function() { - exports.coordinator.init(function() {}) - }, - function() {} - ) - } + } + + exports.coordinator = { + /** + * @todo update description + */ + addPostMessageListener() { + if (!exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { + window.addEventListener('message', onPostMessage, false) + } + }, + + /** + * + * @param {*} sendFcn + * @param {*} origUrl + * + * @todo update description + */ + blockedRobotsMsgGen(sendFcn, origUrl) { + if (!origUrl.includes('google.com/_/chrome/newtab')) { + const onBlockedRobotsMessage = function () { + let log + log = _logGen.log('invalid-robotstxt', []) + log.doc.finalPageUrl = log.doc.url + log.doc.url = exports.utils.normalizeUrl(origUrl) + + sendFcn(log) + } + return onBlockedRobotsMessage + } else { + return function () {} + } + }, + + /** + * + * @param {*} onAdFound + */ + init(onAdFound) { + if (exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { + return false + } + + _onAdFound = onAdFound + if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + const log = _logGen.log('page') + onAdFound(log) + + window.addEventListener('beforeunload', function (event) { + const log = _logGen.log('unload') + log.timing = window.performance.timing + onAdFound(log) + }) + + addBackgroundListener('new-video-ad', onVideoMessage) + addBackgroundListener('new-invalid-video-ad', onVideoMessage) + } + + exports.utils.onDocLoaded(document, extractAdsWrapper) + }, + } + })(exports) + + if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + window.adparser = { + init: exports.coordinator.init, + addPostMessageListener: exports.coordinator.addPostMessageListener, + askIfTrackingEnabled: exports.utils.askIfTrackingEnabled, + blockedRobotsMsgGen: exports.coordinator.blockedRobotsMsgGen, + inWindowTop: exports.utils.SCRIPT_IN_WINDOW_TOP, + sendToBackground: exports.utils.sendToBackground, + } + } else { + exports.coordinator.addPostMessageListener() + exports.utils.askIfTrackingEnabled( + function () { + exports.coordinator.init(function () {}) + }, + function () {} + ) + } })(window) -;(function(adparser, pageUrl) { - function onAdFound(log) { - adparser.sendToBackground( - { source: 'iframe.js', func: 'onAd', args: [log] }, - 'onAd', - '', - function() {} - ) - } - - if (adparser && adparser.inWindowTop) { - adparser.addPostMessageListener() - adparser.askIfTrackingEnabled(function() { - adparser.init(onAdFound) - }, adparser.blockedRobotsMsgGen(onAdFound, pageUrl)) - } +;(function (adparser, pageUrl) { + function onAdFound(log) { + adparser.sendToBackground( + { source: 'iframe.js', func: 'onAd', args: [log] }, + 'onAd', + '', + function () {} + ) + } + + if (adparser && adparser.inWindowTop) { + adparser.addPostMessageListener() + adparser.askIfTrackingEnabled(function () { + adparser.init(onAdFound) + }, adparser.blockedRobotsMsgGen(onAdFound, pageUrl)) + } })(window.adparser, window.location.href) diff --git a/src/drivers/webextension/js/lib/network.js b/src/drivers/webextension/js/lib/network.js index 3e0c8d46a..7b0573205 100644 --- a/src/drivers/webextension/js/lib/network.js +++ b/src/drivers/webextension/js/lib/network.js @@ -1,747 +1,860 @@ -'use strict'; - -(function() { - var MIN_FF_MAJOR_VERSION = 51; - - var areListenersRegistered = false; - var secBefore = 2000; - var secAfter = 5000; - var secBetweenDupAssets = 10e3; - var minVidSize = 500e3; - var maxVidSize = 25e6; - var maxContentRange = 25e6; - var videoExtensions = [ - 'af', '3gp', 'asf', 'avchd', 'avi', 'cam', 'dsh', 'flv', 'm1v', 'm2v', - 'fla', 'flr', 'sol', 'm4v', 'mkv', 'wrap', 'mng', 'mov', 'mpeg', 'mpg', - 'mpe', 'mp4', 'mxf', 'nsv', 'ogg', 'rm', 'svi', 'smi', 'wmv', 'webm' - ]; - var extensionsReg = new RegExp('\\.' + videoExtensions.join('$|\\.') + '$'); - var videoContentTypesPrefixes = ['binary/octet-stream', 'video/', 'flv-application/', 'media']; - - var bannedContentTypes = ['video/mp2t','video/f4m','video/f4f']; - var bannedFiletypes = ['ts']; - var bannedFiletypesReg = new RegExp('\\.' + bannedFiletypes.join('$|\\.') + '$'); - var whitelistReqTypes = ['object', 'xmlhttprequest', 'other']; - - var topVideoAssetDomains = [ - '2mdn.net', - 'adap.tv', - 'adnxs.com', - 'adsrvr.org', - 'btrll.com', - 'celtra.com', - 'flashtalking.com', - 'flite.com', - 'innovid.com', - 'jivox.com', - 'mixpo.com', - 'nytimes.com', - 'playwire.com', - 'selectmedia.asia', - 'serving-sys.com', - 'solvemedia.com', - 'spotible.com', - 'teads.tv', - 'tribalfusion.com', - 'tubemogul.com', - 'videologygroup.com', - 'washingtonpost.com' - ]; - - var robotsTxtAllows = Driver.checkRobots; - if ( !String.prototype.endsWith ) { - String.prototype.endsWith = function(searchString, position) { - var subjectString = this.toString(); - if ( typeof position !== 'number' || !isFinite(position) || - Math.floor(position) !== position || position > subjectString.length) { - position = subjectString.length; - } - position -= searchString.length; - var lastIndex = subjectString.indexOf(searchString, position); - return lastIndex !== -1 && lastIndex === position; - }; - } - - function getFrame(getFrameDetails, callback) { - chrome.webNavigation.getFrame(getFrameDetails, callback); - } - - function ifTrackingEnabled(details, ifCallback, elseCallback) { - - var fullIfCallback = function() { - allowedByRobotsTxt(details, ifCallback, elseCallback); - }; - - Utils.getOption('tracking', true).then(function(tracking) { - if ( tracking ) { - fullIfCallback(); +'use strict' +;(function () { + const MIN_FF_MAJOR_VERSION = 51 + + let areListenersRegistered = false + const secBefore = 2000 + const secAfter = 5000 + const secBetweenDupAssets = 10e3 + const minVidSize = 500e3 + const maxVidSize = 25e6 + const maxContentRange = 25e6 + const videoExtensions = [ + 'af', + '3gp', + 'asf', + 'avchd', + 'avi', + 'cam', + 'dsh', + 'flv', + 'm1v', + 'm2v', + 'fla', + 'flr', + 'sol', + 'm4v', + 'mkv', + 'wrap', + 'mng', + 'mov', + 'mpeg', + 'mpg', + 'mpe', + 'mp4', + 'mxf', + 'nsv', + 'ogg', + 'rm', + 'svi', + 'smi', + 'wmv', + 'webm', + ] + const extensionsReg = new RegExp('\\.' + videoExtensions.join('$|\\.') + '$') + const videoContentTypesPrefixes = [ + 'binary/octet-stream', + 'video/', + 'flv-application/', + 'media', + ] + + const bannedContentTypes = ['video/mp2t', 'video/f4m', 'video/f4f'] + const bannedFiletypes = ['ts'] + const bannedFiletypesReg = new RegExp( + '\\.' + bannedFiletypes.join('$|\\.') + '$' + ) + const whitelistReqTypes = ['object', 'xmlhttprequest', 'other'] + + const topVideoAssetDomains = [ + '2mdn.net', + 'adap.tv', + 'adnxs.com', + 'adsrvr.org', + 'btrll.com', + 'celtra.com', + 'flashtalking.com', + 'flite.com', + 'innovid.com', + 'jivox.com', + 'mixpo.com', + 'nytimes.com', + 'playwire.com', + 'selectmedia.asia', + 'serving-sys.com', + 'solvemedia.com', + 'spotible.com', + 'teads.tv', + 'tribalfusion.com', + 'tubemogul.com', + 'videologygroup.com', + 'washingtonpost.com', + ] + + const robotsTxtAllows = Driver.checkRobots + if (!String.prototype.endsWith) { + String.prototype.endsWith = function (searchString, position) { + const subjectString = this.toString() + if ( + typeof position !== 'number' || + !isFinite(position) || + Math.floor(position) !== position || + position > subjectString.length + ) { + position = subjectString.length + } + position -= searchString.length + const lastIndex = subjectString.indexOf(searchString, position) + return lastIndex !== -1 && lastIndex === position + } + } + + function getFrame(getFrameDetails, callback) { + chrome.webNavigation.getFrame(getFrameDetails, callback) + } + + function ifTrackingEnabled(details, ifCallback, elseCallback) { + const fullIfCallback = function () { + allowedByRobotsTxt(details, ifCallback, elseCallback) + } + + Utils.getOption('tracking', true).then(function (tracking) { + if (tracking) { + fullIfCallback() + } else { + elseCallback() + } + }) + } + + function allowedByRobotsTxt(details, ifCallback, elseCallback) { + if (details.url && !details.url.startsWith('chrome://')) { + Driver.checkRobots(details.url, details.url.startsWith('https:')) + .then(ifCallback) + .catch(elseCallback) + } else { + elseCallback() + } + } + + function isPixelRequest(request) { + return ( + (request.type === 'image' || request.responseStatus === 204) && + request.size <= 1000 + ) + } + + function isVpaidOrVastRequest(request) { + const lowerCaseUrl = request.url.toLowerCase() + return lowerCaseUrl.includes('vpaid') || lowerCaseUrl.includes('vast') + } + + function hasValidRequestType(request) { + return whitelistReqTypes.includes(request.type) + } + + function stripQueryParams(url) { + return url.split('?', 1)[0] + } + + function parseHostnameFromUrl(url) { + const parser = document.createElement('a') + parser.href = url + return parser.hostname + } + + function hasDomain(url, domain) { + return parseHostnameFromUrl(url).endsWith(domain) + } + + function findHeader(headers, key) { + let header + for (let i = 0; i < headers.length; i += 1) { + header = headers[i] + if (header.name.toLowerCase() === key) { + return header + } + } + return null + } + + function validVideoType(vtype) { + const goodType = videoContentTypesPrefixes.some(function (prefix) { + return vtype.indexOf(prefix) === 0 + }) + return goodType + } + + function assetMsgKey(assetReq) { + const url = stripQueryParams(assetReq.url) + const key = assetReq.frameId + '-' + url + return key + } + + const PageNetworkTrafficCollector = function (tabId) { + this.tabId = tabId + this.displayAdFound = false + this.requests = {} + this.msgsBeingSent = {} + this.assetsSeen = {} + this.allRedirects = {} + } + + var globalPageContainer = { + collectors: {}, + dyingCollectors: {}, + + cleanupCollector(tabId) { + if (tabId in this.collectors) { + delete globalPageContainer.collectors[tabId] + } + }, + + onNewNavigation(details) { + const tabId = details.tabId + this.cleanupCollector(tabId) + + ifTrackingEnabled( + details, + function () { + if (!areListenersRegistered) { + registerListeners() + } + this.collectors[tabId] = new PageNetworkTrafficCollector(tabId) + }.bind(this), + function () { + if (areListenersRegistered) { + unregisterListeners() + } + } + ) + }, + + onNavigationCommitted(details) {}, + + onNavigationCompleted(details) {}, + + onTabClose(tabId, closeInfo) { + this.cleanupCollector(tabId) + delete this.collectors[tabId] + }, + + onDisplayAdFound(tabId) { + this.collectors[tabId].displayAdFound = true + }, + + getRandId() { + return String(Math.floor(Math.random() * 1e9)) + }, + + getCollector(tabId) { + if (this.collectors.hasOwnProperty(tabId)) { + return this.collectors[tabId] + } + return null + }, + + forwardCall(details, collectorMemberFunction) { + const collector = this.getCollector(details.tabId) + if (collector !== null) { + collectorMemberFunction.apply(collector, [details]) + } + }, + } + + PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function () { + const logMessage = Array.from(arguments).join(' ') + const message = { message: logMessage, event: 'console-log-message' } + chrome.tabs.sendMessage(this.tabId, message) + } + + PageNetworkTrafficCollector.prototype.sendToTab = function ( + assetReq, + reqs, + curPageUrl, + adTrackingEvent + ) { + const msg = {} + msg.assets = [] + msg.requests = [] + msg.event_data = {} + msg.event = adTrackingEvent + if (adTrackingEvent === 'new-video-ad') { + msg.requests = reqs + msg.requests.sort(function (reqA, reqB) { + return reqA.requestTimestamp - reqB.requestTimestamp + }) + if (assetReq) { + msg.assets = [assetReq] + } + } else if (adTrackingEvent === 'new-invalid-video-ad') { + msg.requests = reqs.map(function (request) { + return parseHostnameFromUrl(request.url) + }) + msg.assets = [ + { + url: parseHostnameFromUrl(assetReq.url), + + contentType: assetReq.contentType, + size: assetReq.size, + }, + ] + } + msg.origUrl = curPageUrl + msg.displayAdFound = this.displayAdFound + + chrome.tabs.sendMessage(this.tabId, msg) + } + + PageNetworkTrafficCollector.prototype.getRedirKey = function (url, frameId) { + return url + ':' + frameId + } + + PageNetworkTrafficCollector.prototype.seenBefore = function (request) { + const oldTime = this.assetsSeen[assetMsgKey(request)] + if (oldTime && request.requestTimestamp - oldTime < secBetweenDupAssets) { + return true + } + return false + } + + PageNetworkTrafficCollector.prototype.recordSeenAsset = function (request) { + this.assetsSeen[assetMsgKey(request)] = request.requestTimestamp + } + + PageNetworkTrafficCollector.prototype.onBeforeRequest = function (details) { + const req = { + url: details.url, + type: details.type, + httpMethod: details.method, + frameId: details.frameId, + parentFrameId: details.parentFrameId, + requestTimestamp: details.timeStamp, + } + this.requests[details.requestId] = req + } + + PageNetworkTrafficCollector.prototype.onSendHeaders = function (details) { + let request, header + request = this.requests[details.requestId] + header = request && findHeader(details.requestHeaders, 'x-requested-with') + if (header && header.value.toLowerCase().includes('flash')) { + request.from_flash = true + } + } + + PageNetworkTrafficCollector.prototype.onHeadersReceived = function (details) { + const getFrameDetails = { + tabId: details.tabId, + processId: null, + frameId: details.frameId, + } + const pageNetworkTrafficController = this + getFrame(getFrameDetails, function (frameDetails) { + if (frameDetails && frameDetails.url) { + pageNetworkTrafficController._onHeadersReceived(details, frameDetails) + } + }) + } + + PageNetworkTrafficCollector.prototype._onHeadersReceived = function ( + details, + frameDetails + ) { + let contentSize, contentRange + + const request = this.requests[details.requestId] + if (request) { + const redirParent = this.allRedirects[ + this.getRedirKey(details.url, details.frameId) + ] + let header = + request && findHeader(details.responseHeaders, 'content-type') + const contentType = header && header.value.toLowerCase() + + if (contentType) { + request.contentType = contentType + } + header = request && findHeader(details.responseHeaders, 'content-length') + contentSize = header && header.value + if (contentSize) { + request.size = request.size || 0 + request.size += parseInt(contentSize) + } + header = request && findHeader(details.responseHeaders, 'content-range') + contentRange = header && header.value + if (contentRange) { + request.contentRange = parseInt(contentRange.split('/')[1]) + } + + let frameUrl = null + if (frameDetails && frameDetails.url) { + frameUrl = frameDetails.url + } + if ( + !this.bannedRequest(request) && + (this.isVideoReq(frameUrl, request) || + (redirParent && redirParent.isVideo)) + ) { + request.isVideo = true + } + } + } + + PageNetworkTrafficCollector.prototype.onBeforeRedirect = function (details) { + const request = this.requests[details.requestId] + if (request) { + if (request.redirects) { + request.redirects.push(details.redirectUrl) + } else { + request.redirects = [details.redirectUrl] + } + this.allRedirects[ + this.getRedirKey(details.redirectUrl, details.frameId) + ] = request + } + } + + PageNetworkTrafficCollector.prototype.isYoutubeMastheadRequest = function ( + url + ) { + const re = /video_masthead/ + return this.hasYoutubeDomain(url) && re.test(url) + } + PageNetworkTrafficCollector.prototype.isYoutubeVideoRequest = function ( + srcUrl, + destUrl + ) { + if (!this.hasYoutubeDomain(srcUrl)) { + return false + } + + const re = /https?:\/\/r.*?\.googlevideo\.com\/videoplayback\?/ + return re.test(destUrl) + } + PageNetworkTrafficCollector.prototype.processResponse = function ( + requestDetails, + frameDetails + ) { + let request + if (requestDetails) { + request = this.requests[requestDetails.requestId] + if (request) { + request.responseStatus = requestDetails.statusCode + request.responseTimestamp = requestDetails.timeStamp + + let frameUrl = null + if (frameDetails && frameDetails.url) { + frameUrl = frameDetails.url + } + + let requestUrl = null + if (request.url) { + requestUrl = request.url + } + + if (this.isYoutubeAdReq(frameUrl, requestUrl)) { + const destVideoId = this.parseYoutubeVideoIdFromUrl(requestUrl) + const srcVideoId = this.parseYoutubeVideoIdFromUrl(frameUrl) + if (srcVideoId && destVideoId) { + request.isYoutubeAd = true + request.isVideo = true + request.rawSrcUrl = frameUrl + request.rawDestUrl = requestUrl + request.url = + 'https://www.youtube.com/watch?v=' + + this.parseYoutubeVideoIdFromUrl(requestUrl) + } + } else if ( + !this.bannedRequest(request) && + (this.isVideo || this.isVideoReq(frameUrl, request)) + ) { + request.isVideo = true + } + + if (request.isVideo) { + const msgKey = assetMsgKey(request) + this.msgsBeingSent[msgKey] = request + if (!this.seenBefore(request)) { + this.sendMsgWhenQuiet(msgKey) + } + this.recordSeenAsset(request) + } + } + } + } + + PageNetworkTrafficCollector.prototype.onResponseStarted = function ( + responseDetails + ) { + if (responseDetails.frameId < 0) { + responseDetails.frameId = 99999 + } + const getFrameDetails = { + tabId: responseDetails.tabId, + processId: null, + frameId: responseDetails.frameId, + } + const pageNetworkTrafficController = this + getFrame(getFrameDetails, function (frameDetails) { + if (frameDetails && frameDetails.url) { + pageNetworkTrafficController.processResponse( + responseDetails, + frameDetails + ) + } + }) + } + + PageNetworkTrafficCollector.prototype.hasBannedFiletype = function (request) { + const url = stripQueryParams(request.url) + if (bannedFiletypesReg.exec(url)) { + return true + } else { + return false + } + } + + PageNetworkTrafficCollector.prototype.checkContentHeaders = function ( + request + ) { + if (request.contentType && validVideoType(request.contentType)) { + return true + } + return false + } + + PageNetworkTrafficCollector.prototype.checkUrlExtension = function (request) { + const url = stripQueryParams(request.url) + if (extensionsReg.exec(url)) { + return true + } else { + return false + } + } + + PageNetworkTrafficCollector.prototype.isVideoReq = function ( + srcUrl, + request + ) { + if (this.isYoutubeVideoRequest(srcUrl, request.url)) { + return false + } + return this.checkUrlExtension(request) || this.checkContentHeaders(request) + } + PageNetworkTrafficCollector.prototype.hasYoutubeDomain = function (url) { + const hostname = parseHostnameFromUrl(url) + if (hostname === 'www.youtube.com') { + return true + } + return false + } + PageNetworkTrafficCollector.prototype.parseYoutubeVideoIdFromUrl = function ( + url + ) { + let re = /^https?:\/\/www\.youtube\.com\/get_video_info.*(?:\?|&)video_id=(.*?)(?:$|&)/ + let match = re.exec(url) + if (match && match.length > 1) { + return match[1] + } + + re = /^https?:\/\/www\.youtube\.com\/embed\/(.*?)(?:$|\?)/ + match = re.exec(url) + if (match && match.length > 1) { + return match[1] + } + + re = /^https?:\/\/www\.youtube\.com\/watch.*(\?|&)v=([^&]*)/ + match = re.exec(url) + if (match && match.length > 1) { + return match[1] + } + return null + } + + PageNetworkTrafficCollector.prototype.isYoutubeGetVideoInfoReq = function ( + url + ) { + const re = /^https?:\/\/www\.youtube\.com\/get_video_info\?/ + return re.test(url) + } + PageNetworkTrafficCollector.prototype.isYoutubeAdReq = function ( + srcUrl, + destUrl + ) { + if ( + !this.hasYoutubeDomain(srcUrl) || + !this.isYoutubeGetVideoInfoReq(destUrl) + ) { + return false + } + if ( + this.parseYoutubeVideoIdFromUrl(srcUrl) === + this.parseYoutubeVideoIdFromUrl(destUrl) && + !this.isYoutubeMastheadRequest(destUrl) + ) { + return false + } + return true + } + + PageNetworkTrafficCollector.prototype.bannedRequest = function (request) { + return ( + this.bannedVideoType(request) || + this.hasBannedFiletype(request) || + this.bannedVideoSize(request) + ) + } + + PageNetworkTrafficCollector.prototype.bannedVideoType = function (request) { + let badType = false + if (request.contentType) { + badType = bannedContentTypes.some(function (prefix) { + return request.contentType.includes(prefix) + }) + } + return badType + } + + PageNetworkTrafficCollector.prototype.bannedVideoSize = function (request) { + if (request.size !== null) { + if ( + request.size < minVidSize || + request.size > maxVidSize || + request.contentRange > maxContentRange + ) { + return true + } + } + return false + } + + PageNetworkTrafficCollector.prototype.grabTagReqs = function ( + tabRequests, + assetRequest + ) { + let minTimestamp, maxTimestamp + minTimestamp = assetRequest.requestTimestamp - secBefore + maxTimestamp = assetRequest.requestTimestamp + secAfter + + const filteredRequests = tabRequests.filter(function (request) { + return ( + request.requestTimestamp > minTimestamp && + request.requestTimestamp < maxTimestamp && + request.frameId === assetRequest.frameId && + request.url !== assetRequest.url && + (hasValidRequestType(request) || isPixelRequest(request)) + ) + }) + + return filteredRequests + } + + PageNetworkTrafficCollector.prototype.isValidVideoAd = function ( + assetRequest, + tagRequests + ) { + const hasVpaidOrVastRequest = tagRequests.some(function (tagRequest) { + return isVpaidOrVastRequest(tagRequest) + }) + + if (assetRequest.isYoutubeAd) { + return true + } + if (hasVpaidOrVastRequest) { + return true + } + const hasTopVideoAssetDomain = topVideoAssetDomains.some(function ( + assetDomain + ) { + return hasDomain(assetRequest.url, assetDomain) + }) + + return hasTopVideoAssetDomain + } + + PageNetworkTrafficCollector.prototype.sendMsgWhenQuiet = function (msgKey) { + const _this = this + let origPageUrl + let msgAssetReq + msgAssetReq = this.msgsBeingSent[msgKey] + chrome.tabs.get(this.tabId, function (tab) { + origPageUrl = tab.url + }) + + setTimeout(function () { + const rawRequests = [] + if (globalPageContainer.collectors[_this.tabId] === _this) { + for (const reqId in _this.requests) { + rawRequests.push(_this.requests[reqId]) + } + const tagReqs = _this.grabTagReqs(rawRequests, msgAssetReq) + + if (_this.isValidVideoAd(msgAssetReq, tagReqs)) { + _this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-video-ad') + } else { + _this.sendToTab( + msgAssetReq, + tagReqs, + origPageUrl, + 'new-invalid-video-ad' + ) + } } else { - elseCallback(); } - }); - - } - - function allowedByRobotsTxt(details, ifCallback, elseCallback) { - if ( details.url && !details.url.startsWith('chrome://') ) { - Driver.checkRobots(details.url, details.url.startsWith('https:')).then(ifCallback).catch(elseCallback); - } else { - elseCallback(); - } - } - - function isPixelRequest(request) { - return (request.type === 'image' || request.responseStatus === 204) && - request.size <= 1000; - } - - function isVpaidOrVastRequest(request) { - var lowerCaseUrl = request.url.toLowerCase(); - return lowerCaseUrl.indexOf('vpaid') !== -1 || lowerCaseUrl.indexOf('vast') !== -1; - } - - function hasValidRequestType(request) { - return whitelistReqTypes.indexOf(request.type) >= 0; - } - - function stripQueryParams(url) { - return url.split('?', 1)[0]; - } - - function parseHostnameFromUrl(url) { - var parser = document.createElement('a'); - parser.href = url; - return parser.hostname; - } - - function hasDomain(url, domain) { - return parseHostnameFromUrl(url).endsWith(domain); - } - - function findHeader(headers, key) { - var header; - for ( var i = 0; i < headers.length; i += 1 ) { - header = headers[i]; - if ( header.name.toLowerCase() === key ) { - return header; - } - } - return null; - } - - function validVideoType(vtype) { - var goodType = videoContentTypesPrefixes.some(function(prefix) { - return vtype.indexOf(prefix) === 0; - }); - return goodType; - } - - function assetMsgKey(assetReq) { - var url = stripQueryParams(assetReq.url); - var key = assetReq.frameId + '-' + url; - return key; - } - - var PageNetworkTrafficCollector = function(tabId) { - this.tabId = tabId; - this.displayAdFound = false; - this.requests = {}; - this.msgsBeingSent = {}; - this.assetsSeen = {}; - this.allRedirects = {}; - }; - - var globalPageContainer = { - collectors: {}, - dyingCollectors: {}, - - cleanupCollector: function(tabId) { - if ( tabId in this.collectors ) { - delete globalPageContainer.collectors[tabId]; - } - }, - - onNewNavigation: function(details) { - var tabId = details.tabId; - this.cleanupCollector(tabId); - - ifTrackingEnabled( - details, - function() { - if ( !areListenersRegistered ) { - - registerListeners(); - } - this.collectors[tabId] = new PageNetworkTrafficCollector(tabId); - }.bind(this), - function() { - if ( areListenersRegistered ) { - - unregisterListeners(); - } - } - ); - }, - - onNavigationCommitted: function(details) { - - }, - - onNavigationCompleted: function(details) { - - }, - - onTabClose: function(tabId, closeInfo) { - - this.cleanupCollector(tabId); - delete this.collectors[tabId]; - }, - - onDisplayAdFound: function(tabId) { - this.collectors[tabId].displayAdFound = true; - }, - - getRandId: function() { - return String(Math.floor(Math.random() * 1e9)); - }, - - getCollector: function(tabId) { - if ( this.collectors.hasOwnProperty(tabId) ) { - return this.collectors[tabId]; - } - return null; - }, - - forwardCall: function(details, collectorMemberFunction) { - var collector = this.getCollector(details.tabId); - if ( collector !== null ) { - collectorMemberFunction.apply(collector, [details]); - } - } - }; - - PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function() { - var logMessage = Array.from(arguments).join(' '); - var message = {message: logMessage, event: 'console-log-message'}; - chrome.tabs.sendMessage(this.tabId, message); - }; - - PageNetworkTrafficCollector.prototype.sendToTab = function(assetReq, reqs, curPageUrl, adTrackingEvent) { - var msg = {}; - msg.assets = []; - msg.requests = []; - msg.event_data = {}; - msg.event = adTrackingEvent; - if ( adTrackingEvent === 'new-video-ad' ) { - msg.requests = reqs; - msg.requests.sort(function(reqA, reqB) {return reqA.requestTimestamp - reqB.requestTimestamp;}); - if ( assetReq ) { - msg.assets = [assetReq]; - } - } else if ( adTrackingEvent === 'new-invalid-video-ad' ) { - msg.requests = reqs.map(function(request) { - return parseHostnameFromUrl(request.url); - }); - msg.assets = [{ - - url: parseHostnameFromUrl(assetReq.url), - - contentType: assetReq.contentType, - size: assetReq.size - }]; - } - msg.origUrl = curPageUrl; - msg.displayAdFound = this.displayAdFound; - - chrome.tabs.sendMessage(this.tabId, msg); - }; - - PageNetworkTrafficCollector.prototype.getRedirKey = function(url, frameId) { - return url + ':' + frameId; - }; - - PageNetworkTrafficCollector.prototype.seenBefore = function(request) { - var oldTime = this.assetsSeen[assetMsgKey(request)]; - if ( oldTime && (request.requestTimestamp-oldTime < secBetweenDupAssets)){ - - return true; - } - return false; - }; - - PageNetworkTrafficCollector.prototype.recordSeenAsset = function(request) { - this.assetsSeen[assetMsgKey(request)] = request.requestTimestamp; - }; - - PageNetworkTrafficCollector.prototype.onBeforeRequest = function(details) { - var req = { - url: details.url, - type: details.type, - httpMethod: details.method, - frameId: details.frameId, - parentFrameId: details.parentFrameId, - requestTimestamp: details.timeStamp, - }; - this.requests[details.requestId] = req; - }; - - PageNetworkTrafficCollector.prototype.onSendHeaders = function(details) { - var request, header; - request = this.requests[details.requestId]; - header = request && findHeader(details.requestHeaders, 'x-requested-with'); - if ( header && header.value.toLowerCase().indexOf('flash') > -1 ) { - request.from_flash = true; - } - }; - - PageNetworkTrafficCollector.prototype.onHeadersReceived = function(details) { - var getFrameDetails = { - tabId: details.tabId, - processId: null, - frameId: details.frameId - }; - var pageNetworkTrafficController = this; - getFrame(getFrameDetails, function(frameDetails) { - if ( frameDetails && frameDetails.url ) { - pageNetworkTrafficController._onHeadersReceived(details, frameDetails); - } - }); - }; - - PageNetworkTrafficCollector.prototype._onHeadersReceived = function(details, frameDetails) { - var contentSize, contentRange; - - var request = this.requests[details.requestId]; - if ( request ) { - var redirParent = this.allRedirects[this.getRedirKey(details.url, details.frameId)]; - var header = request && findHeader(details.responseHeaders, 'content-type'); - var contentType = header && header.value.toLowerCase(); - - if ( contentType){ - request.contentType = contentType; - } - header = request && findHeader(details.responseHeaders, 'content-length'); - contentSize = header && header.value; - if ( contentSize ) { - request.size = request.size || 0; - request.size += parseInt(contentSize); - } - header = request && findHeader(details.responseHeaders, 'content-range'); - contentRange = header && header.value; - if ( contentRange ) { - request.contentRange = parseInt(contentRange.split('/')[1]); - } - - var frameUrl = null; - if ( frameDetails && frameDetails.url ) { - frameUrl = frameDetails.url; - } - if ( !this.bannedRequest(request) && - (this.isVideoReq(frameUrl, request) || (redirParent && redirParent.isVideo))) { - request.isVideo = true; - } - } - }; - - PageNetworkTrafficCollector.prototype.onBeforeRedirect = function(details) { - var request = this.requests[details.requestId]; - if ( request ) { - if ( request.redirects ) { - request.redirects.push(details.redirectUrl); - } else { - request.redirects = [details.redirectUrl]; - } - this.allRedirects[this.getRedirKey(details.redirectUrl, details.frameId)] = request; - } - }; - - PageNetworkTrafficCollector.prototype.isYoutubeMastheadRequest = function(url) { - var re = /video_masthead/; - return this.hasYoutubeDomain(url) && re.test(url); - }; - PageNetworkTrafficCollector.prototype.isYoutubeVideoRequest = function(srcUrl, destUrl) { - if ( !this.hasYoutubeDomain(srcUrl) ) { - return false; - } - - var re = /https?:\/\/r.*?\.googlevideo\.com\/videoplayback\?/; - return re.test(destUrl); - }; - PageNetworkTrafficCollector.prototype.processResponse = function(requestDetails, frameDetails) { - var request; - if ( requestDetails ) { - request = this.requests[requestDetails.requestId]; - if ( request ) { - request.responseStatus = requestDetails.statusCode; - request.responseTimestamp = requestDetails.timeStamp; - - var frameUrl = null; - if ( frameDetails && frameDetails.url ) { - frameUrl = frameDetails.url; - } - - var requestUrl = null; - if ( request.url ) { - requestUrl = request.url; - } - - if ( this.isYoutubeAdReq(frameUrl, requestUrl) ) { - var destVideoId = this.parseYoutubeVideoIdFromUrl(requestUrl); - var srcVideoId = this.parseYoutubeVideoIdFromUrl(frameUrl); - if ( srcVideoId && destVideoId ) { - request.isYoutubeAd = true; - request.isVideo = true; - request.rawSrcUrl = frameUrl; - request.rawDestUrl = requestUrl; - request.url = 'https://www.youtube.com/watch?v=' + this.parseYoutubeVideoIdFromUrl(requestUrl); - } - } else if ( !this.bannedRequest(request) && - (this.isVideo || this.isVideoReq(frameUrl, request))) { - request.isVideo = true; - } - - if ( request.isVideo ) { - - var msgKey = assetMsgKey(request); - this.msgsBeingSent[msgKey] = request; - if ( !this.seenBefore(request) ) { - this.sendMsgWhenQuiet(msgKey); - } - this.recordSeenAsset(request); - } - } - } - }; - - PageNetworkTrafficCollector.prototype.onResponseStarted = function(responseDetails) { - if ( responseDetails.frameId < 0 ) { - responseDetails.frameId = 99999; - - } - var getFrameDetails = { - tabId: responseDetails.tabId, - processId: null, - frameId: responseDetails.frameId - }; - var pageNetworkTrafficController = this; - getFrame(getFrameDetails, function(frameDetails) { - if ( frameDetails && frameDetails.url ) { - pageNetworkTrafficController.processResponse(responseDetails, frameDetails); - } - }); - }; - - PageNetworkTrafficCollector.prototype.hasBannedFiletype = function(request) { - var url = stripQueryParams(request.url); - if ( bannedFiletypesReg.exec(url) ) { - return true; - } else { - return false; - } - }; - - PageNetworkTrafficCollector.prototype.checkContentHeaders = function(request) { - if ( request.contentType && validVideoType(request.contentType) ) { - return true; - } - return false; - }; - - PageNetworkTrafficCollector.prototype.checkUrlExtension = function(request) { - var url = stripQueryParams(request.url); - if ( extensionsReg.exec(url) ) { - return true; - } else { - return false; - } - }; - - PageNetworkTrafficCollector.prototype.isVideoReq = function(srcUrl, request) { - if ( this.isYoutubeVideoRequest(srcUrl, request.url) ) { - return false; - } - return this.checkUrlExtension(request) || this.checkContentHeaders(request); - }; - PageNetworkTrafficCollector.prototype.hasYoutubeDomain = function(url) { - var hostname = parseHostnameFromUrl(url) ; - if ( hostname === 'www.youtube.com' ) { - return true; - } - return false; - }; - PageNetworkTrafficCollector.prototype.parseYoutubeVideoIdFromUrl = function(url) { - var re = /^https?:\/\/www\.youtube\.com\/get_video_info.*(?:\?|&)video_id=(.*?)(?:$|&)/; - var match = re.exec(url); - if ( match && match.length > 1 ) { - return match[1]; - } - - re = /^https?:\/\/www\.youtube\.com\/embed\/(.*?)(?:$|\?)/; - match = re.exec(url); - if ( match && match.length > 1 ) { - return match[1]; - } - - re = /^https?:\/\/www\.youtube\.com\/watch.*(\?|&)v=([^&]*)/; - match = re.exec(url); - if ( match && match.length > 1 ) { - return match[1]; - } - return null; - }; - - PageNetworkTrafficCollector.prototype.isYoutubeGetVideoInfoReq = function(url) { - var re = /^https?:\/\/www\.youtube\.com\/get_video_info\?/; - return re.test(url); - }; - PageNetworkTrafficCollector.prototype.isYoutubeAdReq = function(srcUrl, destUrl) { - - if ( !this.hasYoutubeDomain(srcUrl) || - !this.isYoutubeGetVideoInfoReq(destUrl)) { - return false; - } - if ( this.parseYoutubeVideoIdFromUrl(srcUrl) === - this.parseYoutubeVideoIdFromUrl(destUrl) && - !this.isYoutubeMastheadRequest(destUrl)) { - return false; - } - return true; - }; - - PageNetworkTrafficCollector.prototype.bannedRequest = function(request) { - return this.bannedVideoType(request) || this.hasBannedFiletype(request) || this.bannedVideoSize(request); - }; - - PageNetworkTrafficCollector.prototype.bannedVideoType = function(request) { - var badType = false; - if ( request.contentType ) { - badType = bannedContentTypes.some(function(prefix) { - return request.contentType.indexOf(prefix) >= 0; - }); - } - return badType; - }; - - PageNetworkTrafficCollector.prototype.bannedVideoSize = function(request) { - if ( request.size !== null ) { - if ( request.size < minVidSize || request.size > maxVidSize || request.contentRange > maxContentRange ) { - return true; - } - } - return false; - }; - - PageNetworkTrafficCollector.prototype.grabTagReqs = function(tabRequests, assetRequest) { - var minTimestamp, maxTimestamp; - minTimestamp = assetRequest.requestTimestamp - secBefore; - maxTimestamp = assetRequest.requestTimestamp + secAfter; - - var filteredRequests = tabRequests.filter(function(request) { - return (request.requestTimestamp > minTimestamp && - request.requestTimestamp < maxTimestamp && - request.frameId === assetRequest.frameId && - request.url !== assetRequest.url && - (hasValidRequestType(request) || - isPixelRequest(request))); - }); - - return filteredRequests; - }; - - PageNetworkTrafficCollector.prototype.isValidVideoAd = function(assetRequest, tagRequests) { - var hasVpaidOrVastRequest = tagRequests.some(function(tagRequest) { - return isVpaidOrVastRequest(tagRequest); - }); - - if ( assetRequest.isYoutubeAd ) { - return true; - } - if ( hasVpaidOrVastRequest ) { - return true; - } - var hasTopVideoAssetDomain = topVideoAssetDomains.some(function(assetDomain) { - return hasDomain(assetRequest.url, assetDomain); - }); - - return hasTopVideoAssetDomain; - }; - - PageNetworkTrafficCollector.prototype.sendMsgWhenQuiet = function(msgKey) { - var _this = this, - origPageUrl, msgAssetReq; - msgAssetReq = this.msgsBeingSent[msgKey]; - chrome.tabs.get(this.tabId, function(tab) { - origPageUrl = tab.url; - }); - - setTimeout(function() { - var rawRequests = []; - if ( globalPageContainer.collectors[_this.tabId] === _this ) { - for ( var reqId in _this.requests ) { - rawRequests.push(_this.requests[reqId]); - } - var tagReqs = _this.grabTagReqs(rawRequests, msgAssetReq); - - if ( _this.isValidVideoAd(msgAssetReq, tagReqs) ) { - _this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-video-ad'); - } else { - - _this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-invalid-video-ad'); - } - - } else { - - } - delete _this.msgsBeingSent[msgKey]; - }, secAfter+secBefore); - }; - - PageNetworkTrafficCollector.prototype.existingMessage = function(candidateRequest) { - var frameMsg = this.msgsBeingSent[candidateRequest.frameId]; - if ( frameMsg ) { - return frameMsg; - } else { - return null; - } - }; - - function onBeforeRequestListener(details) { - globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRequest); - } - - function onSendHeadersListener(details) { - globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onSendHeaders); - } - - function onHeadersReceivedListener(details) { - globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onHeadersReceived); - } - - function onBeforeRedirectListener(details) { - globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRedirect); - } - - function onResponseStartedListener(details) { - globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onResponseStarted); - } - - function onCommittedListener(details) { - if ( details.frameId === 0 ) { - globalPageContainer.onNavigationCommitted(details); - } - } - - function onCompletedListener(details) { - if ( details.frameId === 0 ) { - globalPageContainer.onNavigationCompleted(details); - } - } - - function onRemovedListener(tabId, closeInfo) { - globalPageContainer.onTabClose(tabId, closeInfo); - } - - function onMessageListener(message, sender, sendResponse) { - if ( message.event === 'new-ad' && message.data.event === 'ad' ) { - var tabId = sender.tab.id; - if ( tabId ) { - globalPageContainer.onDisplayAdFound(tabId); - } - } - } - - function registerListeners() { - - chrome.webRequest.onBeforeRequest.addListener( - onBeforeRequestListener, - {urls: ['http://*/*', 'https://*/*']}, - [] - ); - - chrome.webRequest.onSendHeaders.addListener( - onSendHeadersListener, - {urls: ['http://*/*', 'https://*/*']}, - ['requestHeaders'] - ); - - chrome.webRequest.onHeadersReceived.addListener( - onHeadersReceivedListener, - {urls: ['http://*/*', 'https://*/*']}, - ['responseHeaders'] - ); - - chrome.webRequest.onBeforeRedirect.addListener( - onBeforeRedirectListener, - {urls: ['http://*/*', 'https://*/*']}, - [] - ); - - chrome.webRequest.onResponseStarted.addListener( - onResponseStartedListener, - {urls: ['http://*/*', 'https://*/*']}, - ['responseHeaders'] - ); - - chrome.webNavigation.onCommitted.addListener(onCommittedListener); - chrome.webNavigation.onCompleted.addListener(onCompletedListener); - chrome.tabs.onRemoved.addListener(onRemovedListener); - chrome.runtime.onMessage.addListener(onMessageListener); - - areListenersRegistered = true; - } - - function unregisterListeners() { - - chrome.webRequest.onBeforeRequest.removeListener( - onBeforeRequestListener - ); - - chrome.webRequest.onSendHeaders.removeListener( - onSendHeadersListener - ); - - chrome.webRequest.onHeadersReceived.removeListener( - onHeadersReceivedListener - ); - - chrome.webRequest.onBeforeRedirect.removeListener( - onBeforeRedirectListener - ); - - chrome.webRequest.onResponseStarted.removeListener( - onResponseStartedListener - ); - - chrome.webNavigation.onCommitted.removeListener(onCommittedListener); - chrome.webNavigation.onCompleted.removeListener(onCompletedListener); - chrome.tabs.onRemoved.removeListener(onRemovedListener); - chrome.runtime.onMessage.removeListener(onMessageListener); - areListenersRegistered = false; - } + delete _this.msgsBeingSent[msgKey] + }, secAfter + secBefore) + } + + PageNetworkTrafficCollector.prototype.existingMessage = function ( + candidateRequest + ) { + const frameMsg = this.msgsBeingSent[candidateRequest.frameId] + if (frameMsg) { + return frameMsg + } else { + return null + } + } + + function onBeforeRequestListener(details) { + globalPageContainer.forwardCall( + details, + PageNetworkTrafficCollector.prototype.onBeforeRequest + ) + } + + function onSendHeadersListener(details) { + globalPageContainer.forwardCall( + details, + PageNetworkTrafficCollector.prototype.onSendHeaders + ) + } + + function onHeadersReceivedListener(details) { + globalPageContainer.forwardCall( + details, + PageNetworkTrafficCollector.prototype.onHeadersReceived + ) + } + + function onBeforeRedirectListener(details) { + globalPageContainer.forwardCall( + details, + PageNetworkTrafficCollector.prototype.onBeforeRedirect + ) + } + + function onResponseStartedListener(details) { + globalPageContainer.forwardCall( + details, + PageNetworkTrafficCollector.prototype.onResponseStarted + ) + } + + function onCommittedListener(details) { + if (details.frameId === 0) { + globalPageContainer.onNavigationCommitted(details) + } + } + + function onCompletedListener(details) { + if (details.frameId === 0) { + globalPageContainer.onNavigationCompleted(details) + } + } + + function onRemovedListener(tabId, closeInfo) { + globalPageContainer.onTabClose(tabId, closeInfo) + } + + function onMessageListener(message, sender, sendResponse) { + if (message.event === 'new-ad' && message.data.event === 'ad') { + const tabId = sender.tab.id + if (tabId) { + globalPageContainer.onDisplayAdFound(tabId) + } + } + } + + function registerListeners() { + chrome.webRequest.onBeforeRequest.addListener( + onBeforeRequestListener, + { urls: ['http://*/*', 'https://*/*'] }, + [] + ) + + chrome.webRequest.onSendHeaders.addListener( + onSendHeadersListener, + { urls: ['http://*/*', 'https://*/*'] }, + ['requestHeaders'] + ) + + chrome.webRequest.onHeadersReceived.addListener( + onHeadersReceivedListener, + { urls: ['http://*/*', 'https://*/*'] }, + ['responseHeaders'] + ) + + chrome.webRequest.onBeforeRedirect.addListener( + onBeforeRedirectListener, + { urls: ['http://*/*', 'https://*/*'] }, + [] + ) + + chrome.webRequest.onResponseStarted.addListener( + onResponseStartedListener, + { urls: ['http://*/*', 'https://*/*'] }, + ['responseHeaders'] + ) + + chrome.webNavigation.onCommitted.addListener(onCommittedListener) + chrome.webNavigation.onCompleted.addListener(onCompletedListener) + chrome.tabs.onRemoved.addListener(onRemovedListener) + chrome.runtime.onMessage.addListener(onMessageListener) + + areListenersRegistered = true + } + + function unregisterListeners() { + chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestListener) + + chrome.webRequest.onSendHeaders.removeListener(onSendHeadersListener) + + chrome.webRequest.onHeadersReceived.removeListener( + onHeadersReceivedListener + ) + + chrome.webRequest.onBeforeRedirect.removeListener(onBeforeRedirectListener) + + chrome.webRequest.onResponseStarted.removeListener( + onResponseStartedListener + ) + + chrome.webNavigation.onCommitted.removeListener(onCommittedListener) + chrome.webNavigation.onCompleted.removeListener(onCompletedListener) + chrome.tabs.onRemoved.removeListener(onRemovedListener) + chrome.runtime.onMessage.removeListener(onMessageListener) + areListenersRegistered = false + } chrome.webNavigation.onBeforeNavigate.addListener( - function(details) { - if ( details.frameId === 0 ) { - globalPageContainer.onNewNavigation(details); + function (details) { + if (details.frameId === 0) { + globalPageContainer.onNewNavigation(details) } }, { - url: [{urlMatches: 'http://*/*'}, {urlMatches: 'https://*/*'}] + url: [{ urlMatches: 'http://*/*' }, { urlMatches: 'https://*/*' }], } - ); + ) chrome.runtime.onMessage.addListener((message, sender, callback) => { - if ( message === 'is_tracking_enabled' ) { + if (message === 'is_tracking_enabled') { ifTrackingEnabled( sender.tab, - function() { - try {callback({'tracking_enabled': true});} - catch(err) {} }, - function() { - try {callback({'tracking_enabled': false});} - catch(err) {} } - ); - } - return true; - }); -})(); + function () { + try { + callback({ tracking_enabled: true }) + } catch (err) {} + }, + function () { + try { + callback({ tracking_enabled: false }) + } catch (err) {} + } + ) + } + return true + }) +})() diff --git a/src/drivers/webextension/manifest-safari.json b/src/drivers/webextension/manifest-safari.json new file mode 100644 index 000000000..b21a490d9 --- /dev/null +++ b/src/drivers/webextension/manifest-safari.json @@ -0,0 +1,79 @@ +{ + "name": "Wappalyzer", + "short_name": "Wappalyzer", + "author": "Wappalyzer", + "homepage_url": "https://www.wappalyzer.com/", + "description": "Identify web technologies", + "version": "6.5.20", + "default_locale": "en", + "manifest_version": 2, + "icons": { + "16": "images/icon_16.png", + "19": "images/icon_19.png", + "32": "images/icon_32.png", + "38": "images/icon_38.png", + "64": "images/icon_64.png", + "128": "images/icon_128.png", + "256": "images/icon_256.png", + "512": "images/icon_512.png", + "1024": "images/icon_1024.png" + }, + "browser_action": { + "default_icon": { + "16": "images/safari.svg", + "19": "images/safari.svg", + "32": "images/safari.svg", + "38": "images/safari.svg", + "64": "images/safari.svg", + "128": "images/safari.svg", + "256": "images/safari.svg", + "512": "images/safari.svg", + "1024": "images/safari.svg" + }, + "default_title": "Wappalyzer", + "default_popup": "html/popup.html" + }, + "background": { + "page": "html/background.html" + }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "js/content.js" + ], + "run_at": "document_idle" + }, + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "js/lib/iframe.js" + ], + "run_at": "document_start", + "all_frames": true + } + ], + "web_accessible_resources": [ + "js/inject.js" + ], + "options_ui": { + "page": "html/options.html", + "open_in_tab": false + }, + "permissions": [ + "cookies", + "storage", + "tabs", + "webRequest", + "webNavigation", + "http://*/*", + "https://*/*" + ], + "content_security_policy": "script-src 'self'; object-src 'self'" +} diff --git a/src/drivers/webextension/manifest.json b/src/drivers/webextension/manifest.json index 011111088..4a3df7bc1 100644 --- a/src/drivers/webextension/manifest.json +++ b/src/drivers/webextension/manifest.json @@ -4,7 +4,7 @@ "author": "Wappalyzer", "homepage_url": "https://www.wappalyzer.com/", "description": "Identify web technologies", - "version": "6.5.18", + "version": "6.5.21", "default_locale": "en", "manifest_version": 2, "icons": { diff --git a/src/package.json b/src/package.json index 1a887d892..1ef7ec4ae 100644 --- a/src/package.json +++ b/src/package.json @@ -13,7 +13,7 @@ "software" ], "homepage": "https://www.wappalyzer.com/", - "version": "6.5.18", + "version": "6.5.21", "author": "Wappalyzer", "license": "MIT", "repository": { @@ -25,7 +25,7 @@ "url": "https://github.com/sponsors/aliasio" }, { - "url": "https://paypal.me/aliasio" + "url": "https://paypal.me/elbertalias" } ], "main": "wappalyzer.js", diff --git a/src/technologies.json b/src/technologies.json index a242d0a59..3689c27f5 100644 --- a/src/technologies.json +++ b/src/technologies.json @@ -448,6 +448,27 @@ "scripts": "xiti\\.com/hit\\.xiti", "website": "http://atinternet.com/en" }, + "ATSHOP": { + "cats": [ + 6 + ], + "description": "ATSHOP is an all-in-one ecommerce platform.", + "dom": { + "link[href*='cdn\\.atshop\\.io']": { + "attributes": { + "href": "" + } + } + }, + "icon": "ATSHOP.png", + "pricing": [ + "low", + "recurring" + ], + "saas": true, + "scripts": "\\.atshop\\.io", + "website": "https://atshop.io" + }, "AWIN": { "cats": [ 71 @@ -461,9 +482,11 @@ "js": { "AWIN.Tracking": "" }, - "scripts": "dwin1\\.com", + "pricing": [ + "payg" + ], "saas": true, - "pricing": ["payg"], + "scripts": "dwin1\\.com", "website": "https://www.awin.com" }, "AWS Certificate Manager": { @@ -568,9 +591,12 @@ "adroll_adv_id": "", "adroll_pix_id": "" }, - "scripts": "(?:a|s)\\.adroll\\.com", + "pricing": [ + "low", + "recurring" + ], "saas": true, - "pricing": ["low", "recurring"], + "scripts": "(?:a|s)\\.adroll\\.com", "website": "http://adroll.com" }, "Adally": { @@ -929,14 +955,30 @@ }, "website": "https://github.com/ankane/ahoy" }, + "Aimtell": { + "cats": [ + 32 + ], + "description": "Aimtell is a cloud-hosted marketing platform that allows digital marketers and businesses to deliver web-based push notifications.", + "icon": "Aimtell.png", + "js": { + "_aimtellPushToken": "", + "_aimtellLoad": "", + "_aimtellWebhook": "" + }, + "scripts": "cdn\\.aimtell\\.\\w+/", + "saas": true, + "pricing": ["low", "recurring"], + "website": "https://aimtell.com" + }, "Aircall": { "cats": [ 52 ], "description": "Aircall is a cloud-based phone system for customer support and sales teams.", "icon": "aircall.png", - "scripts": "^https?://cdn\\.aircall\\.io/", "saas": true, + "scripts": "^https?://cdn\\.aircall\\.io/", "website": "http://aircall.io" }, "Airee": { @@ -959,6 +1001,18 @@ "icon": "Airform.svg", "website": "https://airform.io" }, + "Airship": { + "cats": [ + 32, + 10 + ], + "description": "Airship is an American company that provides marketing and branding services. Airship allows companies to generate custom messages to consumers via push notifications, SMS messaging, and similar, and provides customer analytics services.", + "icon": "Airship.svg", + "scripts": "urbanairship\\.\\w+/notify/v([\\d.]+)\\;version:\\1", + "saas": true, + "pricing": ["poa"], + "website": "https://www.airship.com" + }, "Akamai": { "cats": [ 31 @@ -1021,8 +1075,14 @@ "icon": "Algolia.svg", "js": { "AlgoliaSearch": "", + "__algolia": "", "algoliasearch.version": "^(.+)$\\;version:\\1" }, + "pricing": [ + "freemium", + "payg" + ], + "saas": true, "website": "http://www.algolia.com" }, "All in One SEO Pack": { @@ -1493,6 +1553,25 @@ "scripts": "/profiles/apigee", "website": "https://cloud.google.com/apigee/" }, + "Aplazame": { + "cats": [ + 41 + ], + "description": "Aplazame is a consumer credit company that provides instant financing service for online purchases. It combines an overtime payment method integrated at the ecommerce checkout with marketing tools to enable ecommerce to use financing as a promotional lever to boost sales.", + "icon": "Aplazame.svg", + "js": { + "aplazame": "" + }, + "pricing": [ + "payg" + ], + "saas": true, + "scripts": [ + "cdn\\.aplazame\\.com/aplazame\\.js", + "aplazame\\.com/static/aplazame\\.js" + ], + "website": "https://aplazame.com" + }, "Apollo": { "cats": [ 59 @@ -1531,9 +1610,11 @@ "description": "AppNexus is a cloud-based software platform that enables and optimizes programmatic online advertising.", "html": "<(?:iframe|img)[^>]+adnxs\\.(?:net|com)", "icon": "AppNexus.svg", - "scripts": "adnxs\\.(?:net|com)", + "pricing": [ + "poa" + ], "saas": true, - "pricing": ["poa"], + "scripts": "adnxs\\.(?:net|com)", "website": "http://appnexus.com" }, "Apple Pay": { @@ -1553,12 +1634,22 @@ 69 ], "description": "Apple Sign-in is based on OAuth 2.0 and OpenID Connect, and provides a privacy-friendly way for users to sign in to websites and apps.", - "html": [ - "]*appleid-signin-client-id", - "]*appleid\\.apple\\.com/auth/authorize" - ], + "dom": { + "a[href*='appleid.apple.com/auth/authorize']": { + "attributes": { + "href": "" + } + }, + "button": { + "text": "(Sign (in|up)|Log in|Continue) with Apple" + } + }, + "html": "]*appleid-signin-client-id", + "js": { + "AppleID": "" + }, + "scripts": "appleid\\.auth\\.js", "icon": "Apple.svg", - "scripts": "appleid.auth.js", "website": "https://developer.apple.com/sign-in-with-apple/" }, "Appointy": { @@ -1568,6 +1659,12 @@ "description": "Appointy’s online scheduling software.", "html": "]+src=\"?https://[\\w\\d\\-]+\\.appointy\\.com", "icon": "Appointy.png", + "pricing": [ + "low", + "freemium", + "recurring" + ], + "saas": true, "website": "https://www.appointy.com/" }, "Arastta": { @@ -1704,10 +1801,14 @@ ], "cpe": "cpe:/a:atlassian:confluence", "description": "Atlassian Confluence is a web-based collaboration wiki tool.", + "dom": { + "li.print-only": { + "text": "Atlassian Confluence ([\\d.]+)\\;version:\\1" + } + }, "headers": { "X-Confluence-Request-Time": "" }, - "html": "Powered by ]+atlassian\\.com/software/confluence(?:[^>]+>Atlassian Confluence ([\\d.]+))?\\;version:\\1", "icon": "Atlassian Confluence.svg", "implies": "Java", "meta": { @@ -1732,15 +1833,19 @@ 13 ], "cpe": "cpe:/a:atlassian:jira", - "html": "Powered by\\s+]+atlassian\\.com/(?:software/jira|jira-bug-tracking/)[^>]+>Atlassian\\s+JIRA(?:[^v]*v(?:ersion: )?(\\d+\\.\\d+(?:\\.\\d+)?))?\\;version:\\1", + "dom": { + "#jira": { + "text": "" + } + }, "icon": "Atlassian Jira.svg", "implies": "Java", "js": { - "jira": "" + "jira.id": "" }, "meta": { - "ajs-version-number": "^(.+)$\\;version:\\1", - "application-name": "JIRA" + "application-name": "JIRA", + "data-version": "([\\d.]+)\\;version:\\1\\;confidence:0" }, "website": "http://www.atlassian.com/software/jira/overview/" }, @@ -1829,6 +1934,22 @@ "implies": "WordPress", "website": "https://automattic.com/" }, + "Autopilot": { + "cats": [ + 32, + 74, + 75 + ], + "description": "Autopilot is a visual marketing software that enables users to create marketing campaigns and manage lead conversions. ", + "icon": "Autopilot.png", + "js": { + "AutopilotAnywhere": "", + "Autopilot": "\\;confidence:50" + }, + "saas": true, + "pricing": ["mid", "recurring"], + "website": "https://www.autopilothq.com" + }, "Avangate": { "cats": [ 6 @@ -2067,10 +2188,13 @@ "description": "BigCommerce is a hosted ecommerce platform that allows business owners to set up an online store and sell their products online.", "html": "]+cdn\\d+\\.bigcommerce\\.com/", "icon": "BigCommerce.svg", + "pricing": [ + "low", + "recurring" + ], + "saas": true, "scripts": "cdn\\d+\\.bigcommerce\\.com/", "url": "mybigcommerce\\.com", - "saas": true, - "pricing": ["low", "recurring"], "website": "http://www.bigcommerce.com" }, "BigDump": { @@ -2099,6 +2223,24 @@ "url": "(?:\\?|&)bigWAdminID=", "website": "http://bigware.de" }, + "Birdeye": { + "cats": [ + 32, + 5 + ], + "description": "Birdeye is an all-in-one customer experience platform.", + "icon": "Birdeye.svg", + "scripts": [ + "birdeye\\.com/embed", + "birdeye\\.com" + ], + "js": { + "bfiframe": "" + }, + "saas": true, + "pricing": ["mid", "recurring"], + "website": "https://birdeye.com" + }, "BittAds": { "cats": [ 36 @@ -2331,6 +2473,11 @@ "js": { "BookingKitApp": "" }, + "pricing": [ + "low", + "recurring" + ], + "saas": true, "website": "https://bookingkit.net/" }, "Booksy": { @@ -2343,6 +2490,10 @@ "js": { "booksy": "" }, + "pricing": [ + "low", + "recurring" + ], "scripts": "booksy\\.com/widget/code\\.js", "website": "https://booksy.com/" }, @@ -2382,6 +2533,27 @@ "scripts": "bootstrap-table(?:\\.min)?\\.js", "website": "http://bootstrap-table.wenzhixin.net.cn/" }, + "Borlabs Cookie": { + "cats": [ + 67 + ], + "description": "Borlabs Cookie is a GDPR cookie consent plugin for WordPress.", + "dom": { + "#BorlabsCookieBox": { + "text": "" + } + }, + "icon": "Borlabs Cookie.svg", + "implies": "WordPress", + "js": { + "borlabsCookieConfig": "" + }, + "pricing": [ + "low", + "onetime" + ], + "website": "https://borlabs.io/borlabs-cookie/" + }, "Botble CMS": { "cats": [ 1, @@ -2422,6 +2594,22 @@ "scripts": "js\\.braintreegateway\\.com", "website": "https://www.braintreepayments.com" }, + "Braze": { + "cats": [ + 32, + 10 + ], + "description": "Braze is a customer engagement platform that delivers messaging experiences across push, email, in-product, and more.", + "icon": "Braze.svg", + "js": { + "appboy": "", + "appboyQueue": "" + }, + "scripts": "js\\.appboycdn\\.com/web-sdk/([\\d.]+)\\;version:\\1", + "saas": true, + "pricing": ["poa"], + "website": "https://www.braze.com" + }, "Brightspot": { "cats": [ 1 @@ -2446,9 +2634,10 @@ }, "Bubble": { "cats": [ - 1, + 51, 18 ], + "description": "Bubble is a no-code platform that lets anyone build web apps without writing any code.", "headers": { "x-bubble-capacity-limit": "", "x-bubble-capacity-used": "", @@ -2462,7 +2651,13 @@ "bubble_hostname_modifier": "", "bubble_version": "" }, - "website": "http://bubble.is" + "pricing": [ + "low", + "freemium", + "recurring" + ], + "saas": true, + "website": "http://bubble.io" }, "BugSnag": { "cats": [ @@ -2515,6 +2710,25 @@ }, "website": "http://www.bugzilla.org" }, + "Buildertrend": { + "cats": [ + 19 + ], + "dom": { + "iframe[src*='buildertrend.net'], script[src*='buildertrend.net']": { + "attributes": { + "src": "" + } + } + }, + "icon": "Buildertrend.svg", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, + "website": "https://buildertrend.com" + }, "Bulma": { "cats": [ 66 @@ -2674,7 +2888,10 @@ "js": { "fn_compare_strings": "" }, - "pricing": ["mid", "onetime"], + "pricing": [ + "mid", + "onetime" + ], "website": "http://www.cs-cart.com" }, "CacheFly": { @@ -2711,8 +2928,10 @@ "EC_GLOBAL_INFO": "", "EC_ROOT_DOMAIN": "" }, + "pricing": [ + "low" + ], "saas": true, - "pricing": ["low"], "website": "https://ec.cafe24.com/" }, "CakePHP": { @@ -2797,6 +3016,24 @@ "icon": "Cart-generic.svg", "website": "https://www.wappalyzer.com/technologies/ecommerce/cart-functionality" }, + "CartStack": { + "cats": [ + 6, + 10 + ], + "description": "CartStack is a SaaS solution that allows any company with an ecommerce site or reservation system to increase revenue through reminding/encouraging consumers to return to their abandoned cart and complete their purchase.", + "icon": "CartStack.svg", + "js": { + "_cartstack": "" + }, + "pricing": [ + "low", + "recurring" + ], + "saas": true, + "scripts": "api\\.cartstack\\.\\w+", + "website": "https://www.cartstack.com" + }, "Catberry.js": { "cats": [ 12, @@ -2918,6 +3155,12 @@ ], "description": "Checkfront is a cloud-based booking management application and ecommerce platform.", "icon": "Checkfront.svg", + "pricing": [ + "low", + "recurring", + "poa" + ], + "saas": true, "scripts": "\\.checkfront\\.com/", "website": "https://www.checkfront.com" }, @@ -3063,6 +3306,24 @@ ], "website": "https://clearbit.com/reveal" }, + "Clerk.io": { + "cats": [ + 10, + 6 + ], + "description": "Clerk.io is an all-in-one ecommerce personalization platform.", + "icon": "Clerk.io.svg", + "js": { + "__clerk_cb_0": "", + "__clerk_q": "" + }, + "scripts": [ + "\\.clerk\\.io/" + ], + "saas": true, + "pricing": ["mid", "recurring"], + "website": "https://clerk.io" + }, "Cleverbridge": { "cats": [ 6 @@ -3077,6 +3338,20 @@ ], "website": "https://www.cleverbridge.com" }, + "CleverTap": { + "cats": [ + 32, + 10 + ], + "description": "CleverTap is a SaaS based customer lifecycle management and mobile marketing company headquartered in Mountain View, California.", + "icon": "CleverTap.png", + "js": { + "clevertap": "" + }, + "saas": true, + "pricing": ["mid", "recurring"], + "website": "https://clevertap.com" + }, "ClickFunnels": { "cats": [ 32 @@ -3222,6 +3497,18 @@ }, "website": "http://www.cloudflare.com" }, + "Cloudflare Browser Insights": { + "cats": [ + 10 + ], + "description": "Cloudflare Browser Insights is a tool tool that measures the performance of websites from the perspective of users.", + "icon": "CloudFlare.svg", + "js": { + "__cfBeaconCustomTag": "" + }, + "scripts": "static\\.cloudflareinsights\\.com/beacon(?:\\.min)?\\.js", + "website": "http://www.cloudflare.com" + }, "Cloudinary": { "cats": [ 31 @@ -3232,6 +3519,46 @@ "icon": "Cloudinary.svg", "website": "https://cloudinary.com" }, + "ClustrMaps Widget": { + "cats": [ + 35 + ], + "description": "ClustrMaps widget is a visitor tracker, designed for general web and blog use.", + "dom": { + "img[src*='clustrmaps.com']": { + "attributes": { + "src": "" + } + } + }, + "icon": "ClustrMaps.svg", + "scripts": "clustrmaps\\.com", + "website": "https://clustrmaps.com/" + }, + "CoConstruct": { + "cats": [ + 19 + ], + "dom": { + "a[href*='co-construct.com/skins']": { + "attributes": { + "href": "" + } + }, + "iframe[src*='co-construct.com']": { + "attributes": { + "src": "" + } + } + }, + "icon": "CoConstruct.png", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, + "website": "https://www.coconstruct.com" + }, "Coaster CMS": { "cats": [ 1 @@ -3434,6 +3761,22 @@ "scripts": "/concrete/js/", "website": "https://concrete5.org" }, + "Conekta": { + "cats": [ + 41 + ], + "description": "Conekta is a Mexican payment platform.", + "icon": "Conekta.svg", + "pricing": [ + "payg" + ], + "saas": true, + "scripts": [ + "conektaapi/v([\\d.]+)\\;version:\\1", + "cdn\\.conekta\\.\\w+/js/(?:v([\\d.]+)|)\\;version:\\1" + ], + "website": "https://conekta.com" + }, "Contao": { "cats": [ 1 @@ -3644,6 +3987,12 @@ "implies": "Yii", "oss": true, "pricing": ["low", "freemium", "recurring", "onetime"], + "pricing": [ + "low", + "freemium", + "recurring", + "onetime" + ], "website": "https://craftcms.com" }, "Craft Commerce": { @@ -3691,12 +4040,14 @@ "criteo_pubtag": "", "criteo_q": "" }, + "pricing": [ + "poa" + ], + "saas": true, "scripts": [ "//(?:cas\\.criteo\\.com|(?:[^/]\\.)?criteo\\.net)/", "//static\\.criteo\\.net/js/ld/ld\\.js" ], - "saas": true, - "pricing": ["poa"], "website": "http://criteo.com" }, "Cross Pixel": { @@ -4069,12 +4420,12 @@ "cats": [ 6 ], - "description": "Digital River provides global ecommerce, payments and marketing services.", - "icon": "DigitalRiver.svg", "cookies": { "X-DR-SHOPPER-ets": "", "X-DR-THEME": "^\\d+$" }, + "description": "Digital River provides global ecommerce, payments and marketing services.", + "icon": "DigitalRiver.svg", "scripts": "/drh\\.img\\.digitalriver\\.\\w+/DRHM/", "website": "https://www.digitalriver.com" }, @@ -4158,6 +4509,20 @@ }, "website": "https://djangoproject.com" }, + "DocFX": { + "cats": [ + 4 + ], + "description": "DocFX is a tool for building and publishing API documentation for .NET projects.", + "icon": "DocFX.svg", + "meta": { + "docfx:navrel": "toc.html", + "docfx:tocrel": "toc.html", + "generator": "docfx\\s([\\d\\.]+)\\;version:\\1" + }, + "oss": true, + "website": "https://github.com/dotnet/docfx" + }, "Docker": { "cats": [ 60 @@ -4170,7 +4535,8 @@ }, "Docusaurus": { "cats": [ - 4 + 4, + 57 ], "description": "Docusaurus is a tool for teams to publish documentation websites.", "icon": "docusaurus.svg", @@ -4182,7 +4548,7 @@ "search.indexName": "" }, "meta": { - "generator": "^Docusaurus$" + "generator": "^Docusaurus(?: v(.+))?$\\;version:\\1" }, "website": "https://docusaurus.io/" }, @@ -4240,6 +4606,23 @@ }, "website": "https://www.dokuwiki.org" }, + "Dotdigital": { + "cats": [ + 32, + 10 + ], + "description": "Dotdigital is an all-in-one cloud-based customer engagement multichannel marketing platform.", + "icon": "Dotdigital.svg", + "js": { + "dmPt": "\\;confidence:25", + "dmtrackingobjectname": "\\;confidence:75", + "dm_insight_id": "" + }, + "scripts": "js/_dmptv([\\d.]+)\\.js\\;version:\\1", + "saas": true, + "pricing": ["poa"], + "website": "https://dotdigital.com" + }, "Dotclear": { "cats": [ 1 @@ -4258,12 +4641,12 @@ ], "description": "DoubleClick Ad Exchange is a real-time marketplace to buy and sell display advertising space.", "icon": "DoubleClick.svg", + "saas": true, "scripts": [ "googlesyndication\\.com/pagead/show_ads\\.js", "tpc\\.googlesyndication\\.com/safeframe", "googlesyndication\\.com.*abg\\.js" ], - "saas": true, "website": "http://www.doubleclickbygoogle.com/solutions/digital-marketing/ad-exchange/" }, "DoubleClick Campaign Manager (DCM)": { @@ -4288,9 +4671,11 @@ ], "description": "DoubleClick for Publishers (DFP) is a hosted ad serving platform that streamlines your ad management.", "icon": "DoubleClick.svg", - "scripts": "googletagservices\\.com/tag/js/gpt(?:_mobile)?\\.js", + "pricing": [ + "freemium" + ], "saas": true, - "pricing": ["freemium"], + "scripts": "googletagservices\\.com/tag/js/gpt(?:_mobile)?\\.js", "website": "http://www.google.com/dfp" }, "DovetailWRP": { @@ -4456,11 +4841,11 @@ "cpe": "cpe:/a:lockon:ec-cube", "icon": "ec-cube.png", "implies": "PHP", + "oss": true, "scripts": [ "eccube\\.js", "win_op\\.js" ], - "oss": true, "website": "http://www.ec-cube.net" }, "EKM": { @@ -4506,8 +4891,11 @@ "js": { "epages": "" }, + "pricing": [ + "low", + "recurring" + ], "saas": true, - "pricing": ["low", "recurring"], "website": "http://www.epages.com/" }, "EPiServer": { @@ -4550,6 +4938,11 @@ "meta": { "generator": "^Easy Digital Downloads v(.*)$\\;version:\\1" }, + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "website": "https://easydigitaldownloads.com" }, "EasyEngine": { @@ -4574,12 +4967,14 @@ "Ecwid": "", "EcwidCart": "" }, + "pricing": [ + "freemium" + ], + "saas": true, "scripts": [ "https://app\\.multiscreenstore\\.com/script\\.js", "https://app\\.ecwid\\.com/script\\.js" ], - "saas": true, - "pricing": ["freemium"], "website": "https://www.ecwid.com/" }, "EdgeCast": { @@ -4832,6 +5227,11 @@ ], "description": "Estore Shopserve is an all-in-one payment processing and ecommerce solution.", "icon": "EstoreShopserve.svg", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "scripts": "cart\\d+\\.shopserve\\.jp/", "website": "https://estore.co.jp/shopserve" }, @@ -4864,6 +5264,11 @@ "html": "]*[\\w]+\\.eveve\\.com", "icon": "Eveve.svg", "implies": "PHP", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "website": "https://www.eveve.com" }, "Exhibit": { @@ -5044,6 +5449,10 @@ "description": "FareHarbor is an booking and schedule management solution intended for tour and activity companies.", "html": "]+fareharbor", "icon": "FareHarbor.svg", + "pricing": [ + "payg" + ], + "saas": true, "scripts": "fareharbor\\.com/embeds/api/", "saas": true, "pricing": ["poa"], @@ -5181,6 +5590,24 @@ "icon": "Fireblade.png", "website": "http://fireblade.com" }, + "Firepush": { + "cats": [ + 32 + ], + "description": "Firepush is an omnichannel marketing app that helps Shopify stores to drive sales with automated web push, email and SMS campaigns.", + "icon": "Firepush.svg", + "dom": { + "link[href*='cdn.firepush.net']": { + "attributes": { + "href": "" + } + } + }, + "scripts": "cdn\\.firepush\\.\\w+", + "saas": true, + "pricing": ["low", "freemium", "recurring"], + "website": "https://getfirepush.com" + }, "Flarum": { "cats": [ 2 @@ -5305,6 +5732,9 @@ "]* href=[^>]*kit\\-pro\\.fontawesome\\.com/releases/v([0-9.]+)/\\;version:\\1" ], "icon": "font-awesome.svg", + "js": { + "___FONT_AWESOME___": "" + }, "scripts": [ "(?:F|f)o(?:n|r)t-?(?:A|a)wesome(?:.*?([0-9a-fA-F]{7,40}|[\\d]+(?:.[\\d]+(?:.[\\d]+)?)?)|)", "kit\\.fontawesome\\.com/([0-9a-z]+).js" @@ -5331,6 +5761,11 @@ ], "description": "Formitable is an reservation management system for restaurants.", "icon": "Formitable.svg", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "scripts": [ "formitable\\.js(?:\\?ver=([\\d.]+))?\\;version:\\1", "cdn\\.formitable\\.com" @@ -5351,6 +5786,19 @@ }, "website": "https://www.foroshgostar.com" }, + "Forte": { + "cats": [ + 41 + ], + "description": "Forte, a CSG Company offers merchants and partners a broad range of payment solutions.", + "icon": "Forte.svg", + "pricing": [ + "payg" + ], + "saas": true, + "scripts": "checkout\\.forte\\.net", + "website": "https://www.forte.net" + }, "Forter": { "cats": [ 10 @@ -5992,13 +6440,13 @@ "__google_ad_urls": "", "google_ad_": "" }, + "saas": true, "scripts": [ "googlesyndication\\.com/", "ad\\.ca\\.doubleclick\\.net", "2mdn\\.net", "ad\\.ca\\.doubleclick\\.net" ], - "saas": true, "website": "https://www.google.fr/adsense/start/" }, "Google Analytics": { @@ -6153,6 +6601,11 @@ 69 ], "description": "Google Sign-In is a secure authentication system that reduces the burden of login for users, by enabling them to sign in with their Google account.", + "dom": { + "button": { + "text": "(Sign|Log) in with Google" + } + }, "html": [ "]*google-signin-client_id", "]*google-signin-scope", @@ -6370,6 +6823,11 @@ } }, "icon": "Guestonline.svg", + "pricing": [ + "low", + "recurring" + ], + "saas": true, "scripts": "ib\\.guestonline\\.\\w+", "website": "https://www.guestonline.io" }, @@ -6541,6 +6999,21 @@ "scripts": "heap-\\d+\\.js", "website": "http://heapanalytics.com" }, + "Heartland Payment Systems": { + "cats": [ + 41 + ], + "description": "Heartland Payment Systems is a US-based payment processing and technology provider.", + "icon": "Heartland Payment Systems.svg", + "pricing": [ + "payg", + "low", + "recurring" + ], + "saas": true, + "scripts": "\\.heartlandportico\\.com", + "website": "https://www.heartlandpaymentsystems.com" + }, "Hello Bar": { "cats": [ 5 @@ -6684,6 +7157,11 @@ ], "description": "Hostmeapp is an restaurant software. Includes reservation, waitlist, guestbook and marketing tools.", "icon": "Hostmeapp.svg", + "pricing": [ + "low", + "recurring" + ], + "saas": true, "scripts": "tables\\.hostmeapp\\.com", "website": "https://www.hostmeapp.com" }, @@ -6797,9 +7275,11 @@ "html": "<(?:a|link|script)[^>]*(?:href|src)=\".*(?:/wcsstore/|webapp\\/wcs)", "icon": "IBM.svg", "implies": "Java", - "url": "/wcs/", + "pricing": [ + "poa" + ], "saas": true, - "pricing": ["poa"], + "url": "/wcs/", "website": "http://ibm.com/software/genservers/commerceproductline" }, "IIS": { @@ -6862,11 +7342,17 @@ "cats": [ 6 ], + "description": "Ideasoft is a Turkish software company providing web-based software solutions, software design, ecommerce solutions, and other services.", "icon": "Ideasoft.png", + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "scripts": [ - "\\.myideasoft\\.com/" + "\\.myideasoft\\.com/([\\d.]+)\\;version:\\1" ], - "website": "https://www.ideasoft.com" + "website": "https://www.ideasoft.com.tr" }, "Identrust": { "cats": [ @@ -7032,6 +7518,20 @@ "icon": "infusionsoft.svg", "website": "http://infusionsoft.com" }, + "Insider": { + "cats": [ + 32 + ], + "description": "Insider is the first integrated Growth Management Platform helping digital marketers drive growth across the funnel, from Acquisition to Activation, Retention, and Revenue from a unified platform powered by Artificial Intelligence and Machine Learning.", + "icon": "Insider.svg", + "js": { + "Insider": "\\;confidence:20" + }, + "scripts": "api\\.useinsider\\.\\w+/", + "saas": true, + "pricing": ["poa"], + "website": "https://useinsider.com" + }, "Inspectlet": { "cats": [ 10 @@ -7172,6 +7672,24 @@ "icon": "irroba.svg", "website": "https://www.irroba.com.br/" }, + "Isotope": { + "cats": [ + 59 + ], + "description": "Isotope.js is a JavaScript library that makes it easy to sort, filter, and add Masonry layouts to items on a webpage.", + "icon": "Isotope.svg", + "js": { + "Isotope": "", + "init_isotope": "" + }, + "oss": true, + "pricing": [ + "low", + "freemium", + "onetime" + ], + "website": "https://isotope.metafizzy.co" + }, "Iubenda": { "cats": [ 67 @@ -7182,6 +7700,22 @@ ], "website": "https://www.iubenda.com/" }, + "Izooto": { + "cats": [ + 32, + 5 + ], + "description": "iZooto is a user engagement and retention tool that leverages web push notifications to help business to drive repeat traffic, leads and sales.", + "icon": "Izooto.png", + "js": { + "_izooto": "", + "Izooto": "" + }, + "scripts": "cdn\\.izooto\\.\\w+", + "saas": true, + "pricing": ["mid", "recurring"], + "website": "https://www.izooto.com" + }, "J2Store": { "cats": [ 6 @@ -7260,6 +7794,18 @@ "scripts": "^(?:https):?//load\\.jsecoin\\.com/load/", "website": "https://jsecoin.com/" }, + "JShop": { + "cats": [ + 6 + ], + "description": "JShop is the ecommerce database solution marketed by Whorl Ltd. worldwide.", + "icon": "JShop.svg", + "js": { + "jss_1stepDeliveryType": "", + "jss_1stepFillShipping": "" + }, + "website": "http://www.whorl.co.uk" + }, "JTL Shop": { "cats": [ 6 @@ -7529,6 +8075,11 @@ "js": { "Jumpseller": "" }, + "pricing": [ + "low", + "recurring" + ], + "saas": true, "scripts": [ "assets\\.jumpseller\\.\\w+/", "jumpseller-apps\\.herokuapp\\.\\w+/" @@ -7557,6 +8108,27 @@ }, "website": "https://www.kissmetrics.com" }, + "KaTeX": { + "cats": [ + 25 + ], + "description": "KaTeX is a cross-browser JavaScript library that displays mathematical notation in web browsers.", + "dom": { + "link[href*=katex]": { + "attributes": { + "href": "katex(?:\\.min)?\\.css" + } + } + }, + "icon": "KaTeX.svg", + "js": { + "katex": "", + "katex.version": "^(.+)$\\;version:\\1" + }, + "oss": true, + "scripts": "katex(?:\\.min)?\\.js", + "website": "https://katex.org/" + }, "Kajabi": { "cats": [ 6 @@ -7568,8 +8140,10 @@ "js": { "Kajabi": "" }, + "pricing": [ + "mid" + ], "saas": true, - "pricing": ["mid"], "website": "https://newkajabi.com" }, "Kampyle": { @@ -7993,6 +8567,21 @@ "scripts": "leaflet.{0,32}\\.js", "website": "http://leafletjs.com" }, + "Leanplum": { + "cats": [ + 32, + 74 + ], + "description": "Leanplum is a multi-channel messaging and campaign orchestration platform.", + "icon": "Leanplum.svg", + "js": { + "Leanplum": "" + }, + "scripts": "npm/leanplum-sdk\\@([\\d.]+)\\;version:\\1", + "saas": true, + "pricing": ["poa"], + "website": "https://www.leanplum.com" + }, "Less": { "cats": [ 19 @@ -8070,10 +8659,12 @@ ], "html": "", "icon": "Lightspeed.svg", + "pricing": [ + "low" + ], + "saas": true, "scripts": "http://assets\\.webshopapp\\.com", "url": "seoshop.webshopapp.com", - "saas": true, - "pricing": ["low"], "website": "http://www.lightspeedhq.com/products/ecommerce/" }, "LinkSmart": { @@ -8385,6 +8976,23 @@ }, "website": "https://lojaintegrada.com.br/" }, + "Loja Mestre": { + "cats": [ + 6 + ], + "description": "Loja Mestre is an all-in-one ecommerce platform from Brazil.", + "icon": "Loja Mestre.svg", + "meta": { + "webmaster": "www\\.lojamestre\\.\\w+\\.br" + }, + "pricing": [ + "low", + "recurring" + ], + "saas": true, + "scripts": "lojamestre\\.\\w+\\.br", + "website": "https://www.lojamestre.com.br/" + }, "Loja Virtual": { "cats": [ 6 @@ -8396,23 +9004,49 @@ "link_loja_virtual": "", "loja_sem_dominio": "" }, + "pricing": [ + "low", + "recurring" + ], + "saas": true, "scripts": [ "/js/ljvt_v(\\d+)/\\;version:\\1\\;confidence:20", "cdn1\\.solojavirtual\\.com" ], "website": "https://www.lojavirtual.com.br" }, - "Loja Mestre": { + "Loja2": { "cats": [ 6 ], - "description": "Loja Mestre is an all-in-one ecommerce platform from Brazil.", - "icon": "Loja Mestre.svg", - "meta": { - "webmaster": "www\\.lojamestre\\.\\w+\\.br" + "description": "Loja2 is an all-in-one ecommerce platform from Brazil.", + "icon": "Loja2.svg", + "pricing": [ + "low", + "freemium", + "recurring" + ], + "saas": true, + "scripts": "loja2\\.com\\.br", + "website": "https://www.loja2.com.br" + }, + "Loox": { + "cats": [ + 5, + 6 + ], + "description": "Loox is a reviews app for Shopify that helps you gather reviews and user-generated photos from your customers.", + "icon": "Loox.svg", + "js": { + "loox_global_hash": "" }, - "scripts": "lojamestre\\.\\w+\\.br", - "website": "https://www.lojamestre.com.br/" + "pricing": [ + "low", + "recurring" + ], + "saas": true, + "scripts": "loox\\.io/widget", + "website": "https://loox.app" }, "Lotus Domino": { "cats": [ @@ -8523,12 +9157,12 @@ "Mage": "", "VarienForm": "" }, + "oss": true, "scripts": [ "js/mage", "skin/frontend/(?:default|(enterprise))\\;version:\\1?Enterprise:Community", "static/_requirejs\\;confidence:50\\;version:2" ], - "oss": true, "website": "https://magento.com" }, "MailChimp": { @@ -8538,6 +9172,7 @@ ], "cpe": "cpe:/a:thinkshout:mailchimp", "description": "Mailchimp is a marketing automation platform and email marketing service.", + "icon": "mailchimp.svg", "dns": { "TXT": [ "spf\\.mandrillapp\\.com" @@ -8549,12 +9184,19 @@ "
    ]*name=\"mc-embedded-subscribe-form\"", "]*id=\"mc-email\"\\;confidence:20", "" - ], - "icon": "mailchimp.svg", + ], + "js": { + "mc4wp": "\\;confidence:50" + }, "scripts": [ "s3\\.amazonaws\\.com/downloads\\.mailchimp\\.com/js/mc-validate\\.js", - "cdn-images\\.mailchimp\\.com/[^>]*\\.css" + "cdn-images\\.mailchimp\\.com/[^>]*\\.css", + "mailchimp-woocommerce-public\\.min\\.js(?:\\?ver=([\\d.]+))?\\;version:\\1", + "mailchimp-for-wp/assets/js/forms\\.min\\.js(?:\\?ver=([\\d.]+))?\\;version:\\1", + "chimpstatic\\.com/mcjs-connected" ], + "saas": true, + "pricing": ["low", "freemium", "recurring"], "website": "http://mailchimp.com" }, "Mailgun": { @@ -8657,6 +9299,25 @@ "scripts": "\\/assets\\/js\\/manycontacts\\.min\\.js", "website": "http://www.manycontacts.com" }, + "Mapbox GL JS": { + "cats": [ + 35 + ], + "description": "Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps from vector tiles and Mapbox styles.", + "dom": { + "link[href*='mapbox-gl.css']": { + "attributes": { + "href": "" + } + } + }, + "icon": "Mapbogljs.png", + "js": { + "mapboxgl.version": "^(.+)$\\;version:\\1\\;confidence:0" + }, + "scripts": "mapbox-gl.js", + "website": "https://github.com/mapbox/mapbox-gl-js" + }, "MariaDB": { "cats": [ 34 @@ -8704,7 +9365,26 @@ "js": { "Munchkin": "" }, - "scripts": "munchkin\\.marketo\\.net/munchkin\\.js", + "scripts": "munchkin\\.marketo\\.\\w+/(?:([\\d.]+)/)?munchkin\\.js\\;version:\\1", + "saas": true, + "pricing": ["poa"], + "website": "https://www.marketo.com" + }, + "Marketo Forms": { + "cats": [ + 5 + ], + "description": "Marketo Forms help create web forms without programming knowledge. Forms can reside on Marketo landing pages and also be embedded on any page of website.", + "icon": "Marketo.png", + "js": { + "formatMarketoForm": "" + }, + "scripts": [ + "marketo\\.\\w+/js/forms(?:[\\d.]+)/js/forms([\\d.]+)\\.min\\.js\\;version:\\1", + "v([\\d.]+)/js/marketo-alt-form\\.min\\.js\\;version:\\1" + ], + "saas": true, + "pricing": ["poa"], "website": "https://www.marketo.com" }, "Mastercard": { @@ -8982,6 +9662,22 @@ "url": "^https?//.+\\.memberstack\\.io", "website": "https://www.memberstack.io" }, + "Mercado Shops": { + "cats": [ + 6 + ], + "cookies": { + "_mshops_ga_gid": "" + }, + "description": "Mercado Shops is an all-in-one ecommerce platform.", + "icon": "Mercado Shops.svg", + "pricing": [ + "payg" + ], + "saas": true, + "scripts": "frontend-assets/mshops-web-home/vendor", + "website": "https://www.mercadoshops.com" + }, "Mermaid": { "cats": [ 25 @@ -9179,6 +9875,11 @@ "js": { "HealcodeWidget": "" }, + "pricing": [ + "mid", + "recurring" + ], + "saas": true, "scripts": "\\w+\\.healcode\\.com", "saas": true, "pricing": ["mid"], @@ -9962,6 +10663,25 @@ "scripts": "^/nodebb\\.min\\.js\\?", "website": "https://nodebb.org" }, + "Nosto": { + "cats": [ + 32, + 74 + ], + "description": "Nosto is an ecommerce platform providing product recommendations based on individual behavioral data.", + "icon": "Nosto.svg", + "js": { + "nostojs": "\\;confidence:50", + "nosto": "\\;confidence:50" + }, + "meta": { + "nosto-version": "([\\d.]+)\\;version:\\1" + }, + "scripts": "connect\\.nosto\\.\\w+/", + "saas": true, + "pricing": ["poa"], + "website": "https://www.nosto.com" + }, "Nuvemshop": { "cats": [ 6 @@ -9973,8 +10693,12 @@ }, "Nuxt.js": { "cats": [ - 12 + 12, + 18, + 22, + 57 ], + "description": "Nuxt is a Vue framework for developing modern web applications.", "html": [ "
    ]*id=\"__nuxt\"", "