Add alerts, fix dark mode

main
Elbert Alias 5 years ago
parent d9c504a19c
commit cb1668fb04

@ -15,6 +15,7 @@
"termsAccept": { "message": "Acceptar" }, "termsAccept": { "message": "Acceptar" },
"termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Això pot desactivar-se a Opcions." }, "termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Això pot desactivar-se a Opcions." },
"privacyPolicy": { "message": "Política de privadesa" }, "privacyPolicy": { "message": "Política de privadesa" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taulers de missatgeria" }, "categoryName2": { "message": "Taulers de missatgeria" },
"categoryName3": { "message": "Gestor de bases de dades" }, "categoryName3": { "message": "Gestor de bases de dades" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Immer Icon anzeigen" }, "categoryPin": { "message": "Immer Icon anzeigen" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Nachrichten Board" }, "categoryName2": { "message": "Nachrichten Board" },
"categoryName3": { "message": "Datenbankverwaltung" }, "categoryName3": { "message": "Datenbankverwaltung" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" }, "categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" }, "categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" }, "privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message boards" }, "categoryName2": { "message": "Message boards" },
"categoryName3": { "message": "Database managers" }, "categoryName3": { "message": "Database managers" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Gestor de Contenido" }, "categoryName1": { "message": "Gestor de Contenido" },
"categoryName2": { "message": "Foro" }, "categoryName2": { "message": "Foro" },
"categoryName3": { "message": "Gestor de Bases de Datos" }, "categoryName3": { "message": "Gestor de Bases de Datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "همیشه نماد را نشان بده" }, "categoryPin": { "message": "همیشه نماد را نشان بده" },
"termsAccept": { "message": "قبول" }, "termsAccept": { "message": "قبول" },
"termsContent": { "message": "این افزونه اطلاعات وب‌سایت‌های بازدید شده توسط شما را به صورت ناشناس ارسال می‌کند، مانند آدرس سایت و تکنولوژی‌های استفاده شده در آن سایت را ارسال می‌کند. اطلاعات بیشتر در <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. شما می‌توانید این افزونه را غیرفعال کنید." }, "termsContent": { "message": "این افزونه اطلاعات وب‌سایت‌های بازدید شده توسط شما را به صورت ناشناس ارسال می‌کند، مانند آدرس سایت و تکنولوژی‌های استفاده شده در آن سایت را ارسال می‌کند. اطلاعات بیشتر در <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. شما می‌توانید این افزونه را غیرفعال کنید." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "سیستم مدیریت محتوا" }, "categoryName1": { "message": "سیستم مدیریت محتوا" },
"categoryName2": { "message": "انجمن پیام" }, "categoryName2": { "message": "انجمن پیام" },
"categoryName3": { "message": "مدیریت پایگاه داده" }, "categoryName3": { "message": "مدیریت پایگاه داده" },

@ -14,6 +14,8 @@
"categoryPin": { "message": " Toujours afficher l'icône" }, "categoryPin": { "message": " Toujours afficher l'icône" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestionnaire de base de données" }, "categoryName3": { "message": "Gestionnaire de base de données" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceptar" }, "termsAccept": { "message": "Aceptar" },
"termsContent": { "message": "Esta extensión envía anonimamente información acerca das webs que visitas, incluindo dominio e aplicativos identificados, a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isto pode ser desactivado nas preferencias." }, "termsContent": { "message": "Esta extensión envía anonimamente información acerca das webs que visitas, incluindo dominio e aplicativos identificados, a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isto pode ser desactivado nas preferencias." },
"privacyPolicy": { "message": "Política de privacidade" }, "privacyPolicy": { "message": "Política de privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taboleiro de mensaxes" }, "categoryName2": { "message": "Taboleiro de mensaxes" },
"categoryName3": { "message": "Xestor de base de datos" }, "categoryName3": { "message": "Xestor de base de datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" }, "categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" }, "categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Sistem Pengelola Konten" }, "categoryName1": { "message": "Sistem Pengelola Konten" },
"categoryName2": { "message": "Papan Pesan" }, "categoryName2": { "message": "Papan Pesan" },
"categoryName3": { "message": "Pengelola Basis Data" }, "categoryName3": { "message": "Pengelola Basis Data" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestore di Database" }, "categoryName3": { "message": "Gestore di Database" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "受諾する" }, "termsAccept": { "message": "受諾する" },
"termsContent": { "message": "この拡張機能は、ドメイン名や特定された技術など、アクセスしたWebサイトに関する匿名情報を<a href='https://www.wappalyzer.com'>wappalyzer.com</a>に送信します。これは設定で無効にできます。" }, "termsContent": { "message": "この拡張機能は、ドメイン名や特定された技術など、アクセスしたWebサイトに関する匿名情報を<a href='https://www.wappalyzer.com'>wappalyzer.com</a>に送信します。これは設定で無効にできます。" },
"privacyPolicy": { "message": "プライバシーポリシー" }, "privacyPolicy": { "message": "プライバシーポリシー" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "メッセージボード" }, "categoryName2": { "message": "メッセージボード" },
"categoryName3": { "message": "データベースマネージャー" }, "categoryName3": { "message": "データベースマネージャー" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Zawsze pokazuj tą ikonę" }, "categoryPin": { "message": "Zawsze pokazuj tą ikonę" },
"termsAccept": { "message": "Akceptuj" }, "termsAccept": { "message": "Akceptuj" },
"termsContent": { "message": "To rozszerzenie wysyła anonimowe informacje o stronach, które odwiedzasz, uwzględniając nazwy domen i zidentyfikowane technologie do <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Opcja może zostać wyłączona w ustawieniach." }, "termsContent": { "message": "To rozszerzenie wysyła anonimowe informacje o stronach, które odwiedzasz, uwzględniając nazwy domen i zidentyfikowane technologie do <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Opcja może zostać wyłączona w ustawieniach." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "System zarządzania treścią" }, "categoryName1": { "message": "System zarządzania treścią" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Menedżer baz danych" }, "categoryName3": { "message": "Menedżer baz danych" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceitar" }, "termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anónimas sobre os sites que visitas, incluindo o nome de domínio e as tecnologias identificadas, para o <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isso pode ser desativado nas configurações." }, "termsContent": { "message": "Esta extensão envia informações anónimas sobre os sites que visitas, incluindo o nome de domínio e as tecnologias identificadas, para o <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isso pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Políticas de Privacidade" }, "privacyPolicy": { "message": "Políticas de Privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" }, "categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestor de Base de Dados" }, "categoryName3": { "message": "Gestor de Base de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Sempre mostrar ícone" }, "categoryPin": { "message": "Sempre mostrar ícone" },
"termsAccept": { "message": "Aceitar" }, "termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anônimas sobre os sites que você visita, incluindo domínio e tecnologias identificadas para <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Este comportamento pode ser desativado nas configurações." }, "termsContent": { "message": "Esta extensão envia informações anônimas sobre os sites que você visita, incluindo domínio e tecnologias identificadas para <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Este comportamento pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" }, "categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestão de Banco de Dados" }, "categoryName3": { "message": "Gestão de Banco de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Afișează icon tot timpul" }, "categoryPin": { "message": "Afișează icon tot timpul" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum de discuții" }, "categoryName2": { "message": "Forum de discuții" },
"categoryName3": { "message": "Manager baze de date" }, "categoryName3": { "message": "Manager baze de date" },

@ -67,6 +67,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName54": { "message": "SEO" }, "categoryName54": { "message": "SEO" },
"categoryName55": { "message": "Бухгалтерский учёт" }, "categoryName55": { "message": "Бухгалтерский учёт" },
"categoryName56": { "message": "Криптомайнер" }, "categoryName56": { "message": "Криптомайнер" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message Board" }, "categoryName2": { "message": "Message Board" },
"categoryName3": { "message": "Správca databáz" }, "categoryName3": { "message": "Správca databáz" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Her zaman bu kategorinin ikonunu kullan" }, "categoryPin": { "message": "Her zaman bu kategorinin ikonunu kullan" },
"termsAccept": { "message": "Kabul Ediyorum" }, "termsAccept": { "message": "Kabul Ediyorum" },
"termsContent": { "message": "Bu eklenti, ziyaret ettiğiniz web site bilgilerini, alan adları ve tespit edilen teknolojiler ile beraber anonim olarak <a href='https://www.wappalyzer.com'>wappalyzer.com</a>'a gönderir. Bunu, eklenti ayarlarından değiştirebilirsiniz." }, "termsContent": { "message": "Bu eklenti, ziyaret ettiğiniz web site bilgilerini, alan adları ve tespit edilen teknolojiler ile beraber anonim olarak <a href='https://www.wappalyzer.com'>wappalyzer.com</a>'a gönderir. Bunu, eklenti ayarlarından değiştirebilirsiniz." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "İçerik Yönetim Sistemi" }, "categoryName1": { "message": "İçerik Yönetim Sistemi" },
"categoryName2": { "message": "Mesaj Tahtası" }, "categoryName2": { "message": "Mesaj Tahtası" },
"categoryName3": { "message": "Veritabanı Yöneticisi" }, "categoryName3": { "message": "Veritabanı Yöneticisi" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Форум" }, "categoryName2": { "message": "Форум" },
"categoryName3": { "message": "Менеджер БД" }, "categoryName3": { "message": "Менеджер БД" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS (KBT)" }, "categoryName1": { "message": "CMS (KBT)" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "MB boshqaruvi" }, "categoryName3": { "message": "MB boshqaruvi" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "接受" }, "termsAccept": { "message": "接受" },
"termsContent": { "message": "此扩展程序发送关于您访问的网站的匿名信息至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>,包含域名和检测到的技术。这可以在设置中禁用。" }, "termsContent": { "message": "此扩展程序发送关于您访问的网站的匿名信息至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>,包含域名和检测到的技术。这可以在设置中禁用。" },
"privacyPolicy": { "message": "隐私政策" }, "privacyPolicy": { "message": "隐私政策" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "内容管理系统CMS" }, "categoryName1": { "message": "内容管理系统CMS" },
"categoryName2": { "message": "消息板" }, "categoryName2": { "message": "消息板" },
"categoryName3": { "message": "数据库管理器" }, "categoryName3": { "message": "数据库管理器" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "永遠顯示圖示" }, "categoryPin": { "message": "永遠顯示圖示" },
"termsAccept": { "message": "接受" }, "termsAccept": { "message": "接受" },
"termsContent": { "message": "這個擴充功能將你所造訪網站的網域名稱和識別到的技術等資訊,匿名傳送至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>。你可以在選項中停用。" }, "termsContent": { "message": "這個擴充功能將你所造訪網站的網域名稱和識別到的技術等資訊,匿名傳送至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>。你可以在選項中停用。" },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "內容管理系統CMS" }, "categoryName1": { "message": "內容管理系統CMS" },
"categoryName2": { "message": "留言板/討論區" }, "categoryName2": { "message": "留言板/討論區" },
"categoryName3": { "message": "資料庫管理" }, "categoryName3": { "message": "資料庫管理" },

@ -31,6 +31,24 @@ body {
display: none; display: none;
} }
.footer {
align-items: center;
border-top: 1px solid #dbdbdb;
height: 3rem;
display: flex;
padding: 0 1.5rem;
}
.footer__link {
color: #4608ad;
text-decoration: none;
}
.footer__link:hover, .footer__link:active {
color: #4608ad;
text-decoration: underline;
}
.container { .container {
min-height: 5rem; min-height: 5rem;
padding: 1rem 1.5rem 0rem 1.5rem; padding: 1rem 1.5rem 0rem 1.5rem;
@ -202,50 +220,68 @@ body {
margin-top: 1rem; margin-top: 1rem;
} }
/* Add alternative color palette for Dark mode theme. */ @media (prefers-color-scheme: dark) {
body.theme-mode-sync { /* Add alternative color palette for Dark mode theme. */
background: linear-gradient(160deg, #32067c, #150233); body.theme-mode-sync {
} background: linear-gradient(160deg, #32067c, #150233);
}
.theme-mode-sync .header { .theme-mode-sync .header {
border-bottom: 1px solid #000; border-bottom: 1px solid rgba(255, 255, 255, .2);
} }
.theme-mode-sync .header__logo--dark { .theme-mode-sync .header__logo--dark {
display: inline-block; display: inline-block;
} }
.theme-mode-sync .header__logo--light { .theme-mode-sync .header__logo--light {
display: none; display: none;
} }
.theme-mode-sync .container { .theme-mode-sync .footer {
color: white; border-top: 1px solid rgba(255, 255, 255, .2);
} }
.theme-mode-sync .detected__category-link, .theme-mode-sync .detected__app { .theme-mode-sync .footer__link {
color: white; color: rgba(255, 255, 255, .8);
} }
.theme-mode-sync .detected__category-link:hover { .theme-mode-sync .footer__link:hover, .theme-mode-sync .footer__link:active {
color: white; color: rgba(255, 255, 255, .8);
border-bottom: 1px solid white; }
}
.theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app-confidence { .theme-mode-sync .container {
background-color: #4608ad; color: white;
} }
.theme-mode-sync .detected__app:hover .detected__app-name { .theme-mode-sync .detected__category-link {
border-bottom: 1px solid white; color: #fff;
} }
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app {
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-confidence { color: rgba(255, 255, 255, .8);
border-bottom: none; }
}
.theme-mode-sync .terms__accept, .theme-mode-sync .detected__category-link:hover {
.theme-mode-sync .terms__privacy { color: white;
color: white; border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app-confidence {
background-color: #4608ad;
}
.theme-mode-sync .detected__app:hover .detected__app-name {
border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-version,
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-confidence {
border-bottom: none;
}
.theme-mode-sync .terms__accept,
.theme-mode-sync .terms__privacy {
color: white;
}
} }

@ -14,8 +14,8 @@
<body> <body>
<div class="header"> <div class="header">
<a href="https://www.wappalyzer.com/" class="header__link" target="_blank"> <a href="https://www.wappalyzer.com/" class="header__link" target="_blank">
<img class="header__logo header__logo--light" src="../images/logo-purple.svg"> <img alt="" class="header__logo header__logo--light" src="../images/logo-purple.svg">
<img class="header__logo header__logo--dark" src="../images/logo-white.svg"> <img alt="" class="header__logo header__logo--dark" src="../images/logo-white.svg">
</a> </a>
</div> </div>
@ -24,11 +24,15 @@
<div class="terms"> <div class="terms">
<div class="terms__content" data-i18n="termsContent"></div> <div class="terms__content" data-i18n="termsContent"></div>
<button class="terms__accept" data-i18n="termsAccept"></button> <button class="terms__accept" data-i18n="termsAccept" />
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a> <a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
</div> </div>
</div> </div>
</div> </div>
<div class="footer">
<a class="footer__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
</div>
</body> </body>
</html> </html>

@ -10,29 +10,29 @@
/** global: fetch */ /** global: fetch */
/** global: Wappalyzer */ /** global: Wappalyzer */
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
const tabCache = {}; const tabCache = {}
const robotsTxtQueue = {}; const robotsTxtQueue = {}
let categoryOrder = []; let categoryOrder = []
browser.tabs.onRemoved.addListener((tabId) => { browser.tabs.onRemoved.addListener((tabId) => {
tabCache[tabId] = null; tabCache[tabId] = null
}); })
function userAgent() { function userAgent() {
const url = chrome.extension.getURL('/'); const url = chrome.extension.getURL('/')
if (url.match(/^moz-/)) { if (url.match(/^moz-/)) {
return 'firefox'; return 'firefox'
} }
if (url.match(/^ms-browser-/)) { if (url.match(/^ms-browser-/)) {
return 'edge'; return 'edge'
} }
return 'chrome'; return 'chrome'
} }
/** /**
@ -40,22 +40,22 @@ function userAgent() {
*/ */
function getOption(name, defaultValue = null) { function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let value = defaultValue; let value = defaultValue
try { try {
const option = await browser.storage.local.get(name); const option = await browser.storage.local.get(name)
if (option[name] !== undefined) { if (option[name] !== undefined) {
value = option[name]; value = option[name]
} }
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(value); return resolve(value)
}); })
} }
/** /**
@ -64,15 +64,15 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) { function setOption(name, value) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
await browser.storage.local.set({ [name]: value }); await browser.storage.local.set({ [name]: value })
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(); return resolve()
}); })
} }
/** /**
@ -81,8 +81,8 @@ function setOption(name, value) {
function openTab(args) { function openTab(args) {
browser.tabs.create({ browser.tabs.create({
url: args.url, url: args.url,
active: args.background === undefined || !args.background, active: args.background === undefined || !args.background
}); })
} }
/** /**
@ -92,321 +92,346 @@ async function post(url, body) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body)
}); })
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver'); wappalyzer.log(`POST ${url}: ${response.status}`, 'driver')
} catch (error) { } catch (error) {
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error'); wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error')
} }
} }
// Capture response headers // Capture response headers
browser.webRequest.onCompleted.addListener(async (request) => { browser.webRequest.onCompleted.addListener(
const headers = {}; async (request) => {
const headers = {}
if (request.responseHeaders) { if (request.responseHeaders) {
const url = wappalyzer.parseUrl(request.url); const url = wappalyzer.parseUrl(request.url)
let tab; let tab
try { try {
[tab] = await browser.tabs.query({ url: [url.href] }); ;[tab] = await browser.tabs.query({ url: [url.href] })
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
} }
if (tab) { if (tab) {
request.responseHeaders.forEach((header) => { request.responseHeaders.forEach((header) => {
const name = header.name.toLowerCase(); const name = header.name.toLowerCase()
headers[name] = headers[name] || []; headers[name] = headers[name] || []
headers[name].push((header.value || header.binaryValue || '').toString()); headers[name].push(
}); (header.value || header.binaryValue || '').toString()
)
})
if (headers['content-type'] && /\/x?html/.test(headers['content-type'][0])) { if (
wappalyzer.analyze(url, { headers }, { tab }); headers['content-type'] &&
/\/x?html/.test(headers['content-type'][0])
) {
wappalyzer.analyze(url, { headers }, { tab })
}
} }
} }
} },
}, { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, ['responseHeaders']); { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
['responseHeaders']
)
browser.runtime.onConnect.addListener((port) => { browser.runtime.onConnect.addListener((port) => {
port.onMessage.addListener(async (message) => { port.onMessage.addListener(async (message) => {
if (message.id === undefined) { if (message.id === undefined) {
return; return
} }
if (message.id !== 'log') { if (message.id !== 'log') {
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver'); wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver')
} }
const pinnedCategory = await getOption('pinnedCategory'); const pinnedCategory = await getOption('pinnedCategory')
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : ''); const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '')
const cookies = await browser.cookies.getAll({ domain: `.${url.hostname}`, firstPartyDomain: null }); const cookies = await browser.cookies.getAll({
domain: `.${url.hostname}`,
firstPartyDomain: null
})
let response; let response
switch (message.id) { switch (message.id) {
case 'log': case 'log':
wappalyzer.log(message.subject, message.source); wappalyzer.log(message.subject, message.source)
break; break
case 'init': case 'init':
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab }); wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab })
break; break
case 'analyze': case 'analyze':
if (message.subject.html) { if (message.subject.html) {
browser.i18n.detectLanguage(message.subject.html) browser.i18n
.detectLanguage(message.subject.html)
.then(({ languages }) => { .then(({ languages }) => {
const language = languages const language = languages
.filter(({ percentage }) => percentage >= 75) .filter(({ percentage }) => percentage >= 75)
.map(({ language: lang }) => lang)[0]; .map(({ language: lang }) => lang)[0]
message.subject.language = language; message.subject.language = language
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }); wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
}); })
} else { } else {
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }); wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
} }
await setOption('hostnameCache', wappalyzer.hostnameCache); await setOption('hostnameCache', wappalyzer.hostnameCache)
break; break
case 'ad_log': case 'ad_log':
wappalyzer.cacheDetectedAds(message.subject); wappalyzer.cacheDetectedAds(message.subject)
break; break
case 'get_apps': case 'get_apps':
response = { response = {
tabCache: tabCache[message.tab.id], tabCache: tabCache[message.tab.id],
apps: wappalyzer.apps, apps: wappalyzer.apps,
categories: wappalyzer.categories, categories: wappalyzer.categories,
pinnedCategory, pinnedCategory,
termsAccepted: userAgent() === 'chrome' || await getOption('termsAccepted', false), termsAccepted:
}; userAgent() === 'chrome' ||
(await getOption('termsAccepted', false))
}
break; break
case 'set_option': case 'set_option':
await setOption(message.key, message.value); await setOption(message.key, message.value)
break; break
case 'get_js_patterns': case 'get_js_patterns':
response = { response = {
patterns: wappalyzer.jsPatterns, patterns: wappalyzer.jsPatterns
}; }
break; break
case 'update_theme_mode': case 'update_theme_mode':
// Sync theme mode to popup. // Sync theme mode to popup.
response = { response = {
themeMode: await getOption('themeMode', false), themeMode: await getOption('themeMode', false)
}; }
break; break
default: default:
// Do nothing // Do nothing
} }
if (response) { if (response) {
port.postMessage({ port.postMessage({
id: message.id, id: message.id,
response, response
}); })
} }
}); })
}); })
wappalyzer.driver.document = document; wappalyzer.driver.document = document
/** /**
* Log messages to console * Log messages to console
*/ */
wappalyzer.driver.log = (message, source, type) => { wappalyzer.driver.log = (message, source, type) => {
const log = ['warn', 'error'].indexOf(type) !== -1 ? type : 'log'; const log = ['warn', 'error'].includes(type) ? type : 'log'
console[log](`[wappalyzer ${type}]`, `[${source}]`, message); // eslint-disable-line no-console console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console
}; }
/** /**
* Display apps * Display apps
*/ */
wappalyzer.driver.displayApps = async (detected, meta, context) => { wappalyzer.driver.displayApps = async (detected, meta, context) => {
const { tab } = context; const { tab } = context
if (tab === undefined) { if (tab === undefined) {
return; return
} }
tabCache[tab.id] = tabCache[tab.id] || { tabCache[tab.id] = tabCache[tab.id] || {
detected: [], detected: []
}; }
tabCache[tab.id].detected = detected; tabCache[tab.id].detected = detected
const pinnedCategory = await getOption('pinnedCategory'); const pinnedCategory = await getOption('pinnedCategory')
const dynamicIcon = await getOption('dynamicIcon', true); const dynamicIcon = await getOption('dynamicIcon', true)
let found = false; let found = false
// Find the main application to display // Find the main application to display
[pinnedCategory].concat(categoryOrder).forEach((match) => { ;[pinnedCategory].concat(categoryOrder).forEach((match) => {
Object.keys(detected).forEach((appName) => { Object.keys(detected).forEach((appName) => {
const app = detected[appName]; const app = detected[appName]
app.props.cats.forEach((category) => { app.props.cats.forEach((category) => {
if (category === match && !found) { if (category === match && !found) {
let icon = app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'; let icon =
app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'
if (/\.svg$/i.test(icon)) { if (/\.svg$/i.test(icon)) {
icon = `converted/${icon.replace(/\.svg$/, '.png')}`; icon = `converted/${icon.replace(/\.svg$/, '.png')}`
} }
try { try {
browser.pageAction.setIcon({ browser.pageAction.setIcon({
tabId: tab.id, tabId: tab.id,
path: `../images/icons/${icon}`, path: `../images/icons/${icon}`
}); })
} catch (e) { } catch (e) {
// Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746 // Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746
} }
found = true; found = true
} }
}); })
}); })
}); })
browser.pageAction.show(tab.id); browser.pageAction.show(tab.id)
}; }
/** /**
* Fetch and cache robots.txt for host * Fetch and cache robots.txt for host
*/ */
wappalyzer.driver.getRobotsTxt = async (host, secure = false) => { wappalyzer.driver.getRobotsTxt = async (host, secure = false) => {
if (robotsTxtQueue[host]) { if (robotsTxtQueue[host]) {
return robotsTxtQueue[host]; return robotsTxtQueue[host]
} }
const tracking = await getOption('tracking', true); const tracking = await getOption('tracking', true)
const robotsTxtCache = await getOption('robotsTxtCache', {}); const robotsTxtCache = await getOption('robotsTxtCache', {})
robotsTxtQueue[host] = new Promise(async (resolve) => { robotsTxtQueue[host] = new Promise(async (resolve) => {
if (!tracking) { if (!tracking) {
return resolve([]); return resolve([])
} }
if (host in robotsTxtCache) { if (host in robotsTxtCache) {
return resolve(robotsTxtCache[host]); return resolve(robotsTxtCache[host])
} }
const timeout = setTimeout(() => resolve([]), 3000); const timeout = setTimeout(() => resolve([]), 3000)
let response; let response
try { try {
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { redirect: 'follow', mode: 'no-cors' }); response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, {
redirect: 'follow',
mode: 'no-cors'
})
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
return resolve([]); return resolve([])
} }
clearTimeout(timeout); clearTimeout(timeout)
const robotsTxt = response.ok ? await response.text() : ''; const robotsTxt = response.ok ? await response.text() : ''
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt); robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt)
await setOption('robotsTxtCache', robotsTxtCache); await setOption('robotsTxtCache', robotsTxtCache)
delete robotsTxtQueue[host]; delete robotsTxtQueue[host]
return resolve(robotsTxtCache[host]); return resolve(robotsTxtCache[host])
}); })
return robotsTxtQueue[host]; return robotsTxtQueue[host]
}; }
/** /**
* Anonymously track detected applications for research purposes * Anonymously track detected applications for research purposes
*/ */
wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => { wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => {
const tracking = await getOption('tracking', true); const tracking = await getOption('tracking', true)
const termsAccepted = userAgent() === 'chrome' || await getOption('termsAccepted', false); const termsAccepted =
userAgent() === 'chrome' || (await getOption('termsAccepted', false))
if (tracking && termsAccepted) { if (tracking && termsAccepted) {
if (Object.keys(hostnameCache).length) { if (Object.keys(hostnameCache).length) {
post('https://api.wappalyzer.com/ping/v1/', hostnameCache); post('https://api.wappalyzer.com/ping/v1/', hostnameCache)
} }
if (adCache.length) { if (adCache.length) {
post('https://ad.wappalyzer.com/log/wp/', adCache); post('https://ad.wappalyzer.com/log/wp/', adCache)
} }
await setOption('robotsTxtCache', {}); await setOption('robotsTxtCache', {})
} }
}; }
// Init // Init
(async () => { ;(async () => {
// Technologies // Technologies
try { try {
const response = await fetch('../apps.json'); const response = await fetch('../apps.json')
const json = await response.json(); const json = await response.json()
wappalyzer.apps = json.apps; wappalyzer.apps = json.apps
wappalyzer.categories = json.categories; wappalyzer.categories = json.categories
} catch (error) { } catch (error) {
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error'); wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error')
} }
wappalyzer.parseJsPatterns(); wappalyzer.parseJsPatterns()
categoryOrder = Object.keys(wappalyzer.categories) categoryOrder = Object.keys(wappalyzer.categories)
.map(categoryId => parseInt(categoryId, 10)) .map((categoryId) => parseInt(categoryId, 10))
.sort((a, b) => wappalyzer.categories[a].priority - wappalyzer.categories[b].priority); .sort(
(a, b) =>
wappalyzer.categories[a].priority - wappalyzer.categories[b].priority
)
// Version check // Version check
const { version } = browser.runtime.getManifest(); const { version } = browser.runtime.getManifest()
const previousVersion = await getOption('version'); const previousVersion = await getOption('version')
const upgradeMessage = await getOption('upgradeMessage', true); const upgradeMessage = await getOption('upgradeMessage', true)
if (previousVersion === null) { if (previousVersion === null) {
openTab({ openTab({
url: `${wappalyzer.config.websiteURL}installed`, url: `${wappalyzer.config.websiteURL}installed`
}); })
} else if (version !== previousVersion && upgradeMessage) { } else if (version !== previousVersion && upgradeMessage) {
openTab({ openTab({
url: `${wappalyzer.config.websiteURL}upgraded?v${version}`, url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
background: true, background: true
}); })
} }
await setOption('version', version); await setOption('version', version)
// Hostname cache // Hostname cache
wappalyzer.hostnameCache = await getOption('hostnameCache', {}); wappalyzer.hostnameCache = await getOption('hostnameCache', {})
// Run content script on all tabs // Run content script on all tabs
try { try {
const tabs = await browser.tabs.query({ url: ['http://*/*', 'https://*/*'] }); const tabs = await browser.tabs.query({
url: ['http://*/*', 'https://*/*']
})
tabs.forEach(async (tab) => { tabs.forEach(async (tab) => {
try { try {
await browser.tabs.executeScript(tab.id, { await browser.tabs.executeScript(tab.id, {
file: '../js/content.js', file: '../js/content.js'
}); })
} catch (error) { } catch (error) {
// //
} }
}); })
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
} }
})(); })()

@ -3,29 +3,29 @@
/* globals browser Wappalyzer */ /* globals browser Wappalyzer */
/* eslint-env browser */ /* eslint-env browser */
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
/** /**
* Get a value from localStorage * Get a value from localStorage
*/ */
function getOption(name, defaultValue = null) { function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let value = defaultValue; let value = defaultValue
try { try {
const option = await browser.storage.local.get(name); const option = await browser.storage.local.get(name)
if (option[name] !== undefined) { if (option[name] !== undefined) {
value = option[name]; value = option[name]
} }
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(value); return resolve(value)
}); })
} }
/** /**
@ -34,72 +34,76 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) { function setOption(name, value) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
await browser.storage.local.set({ [name]: value }); await browser.storage.local.set({ [name]: value })
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(); return resolve()
}); })
} }
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const nodes = document.querySelectorAll('[data-i18n]'); const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { Array.prototype.forEach.call(nodes, (node) => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n); node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n)
}); })
document.querySelector('#github').addEventListener('click', () => { document.querySelector('#github').addEventListener('click', () => {
window.open(wappalyzer.config.githubURL); window.open(wappalyzer.config.githubURL)
}); })
document.querySelector('#twitter').addEventListener('click', () => { document.querySelector('#twitter').addEventListener('click', () => {
window.open(wappalyzer.config.twitterURL); window.open(wappalyzer.config.twitterURL)
}); })
document.querySelector('#wappalyzer').addEventListener('click', () => { document.querySelector('#wappalyzer').addEventListener('click', () => {
window.open(wappalyzer.config.websiteURL); window.open(wappalyzer.config.websiteURL)
}); })
let el; let el
let value; let value
// Upgrade message // Upgrade message
value = await getOption('upgradeMessage', true); value = await getOption('upgradeMessage', true)
el = document.querySelector('#option-upgrade-message'); el = document.querySelector('#option-upgrade-message')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('upgradeMessage', e.target.checked)); el.addEventListener('change', (e) =>
setOption('upgradeMessage', e.target.checked)
)
// Dynamic icon // Dynamic icon
value = await getOption('dynamicIcon', true); value = await getOption('dynamicIcon', true)
el = document.querySelector('#option-dynamic-icon'); el = document.querySelector('#option-dynamic-icon')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('dynamicIcon', e.target.checked)); el.addEventListener('change', (e) =>
setOption('dynamicIcon', e.target.checked)
)
// Tracking // Tracking
value = await getOption('tracking', true); value = await getOption('tracking', true)
el = document.querySelector('#option-tracking'); el = document.querySelector('#option-tracking')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('tracking', e.target.checked)); el.addEventListener('change', (e) => setOption('tracking', e.target.checked))
// Theme Mode // Theme Mode
value = await getOption('themeMode', false); value = await getOption('themeMode', false)
el = document.querySelector('#option-theme-mode'); el = document.querySelector('#option-theme-mode')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('themeMode', e.target.checked)); el.addEventListener('change', (e) => setOption('themeMode', e.target.checked))
}); })

@ -4,217 +4,267 @@
/** global: browser */ /** global: browser */
/** global: jsonToDOM */ /** global: jsonToDOM */
let pinnedCategory = null; let pinnedCategory = null
let termsAccepted = false; let termsAccepted = false
const port = browser.runtime.connect({ const port = browser.runtime.connect({
name: 'popup.js', name: 'popup.js'
}); })
function slugify(string) { function slugify(string) {
return string.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/(?:^-|-$)/, ''); return string
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/--+/g, '-')
.replace(/(?:^-|-$)/, '')
} }
function i18n() { function i18n() {
const nodes = document.querySelectorAll('[data-i18n]'); const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { Array.prototype.forEach.call(nodes, (node) => {
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n); node.innerHTML = browser.i18n.getMessage(node.dataset.i18n)
}); })
} }
function replaceDom(domTemplate) { function replaceDom(domTemplate) {
const container = document.getElementsByClassName('container')[0]; const container = document.getElementsByClassName('container')[0]
while (container.firstChild) { while (container.firstChild) {
container.removeChild(container.firstChild); container.removeChild(container.firstChild)
} }
container.appendChild(jsonToDOM(domTemplate, document, {})); container.appendChild(jsonToDOM(domTemplate, document, {}))
i18n(); i18n()
Array.from(document.querySelectorAll('.detected__category-pin-wrapper')).forEach((pin) => { Array.from(
document.querySelectorAll('.detected__category-pin-wrapper')
).forEach((pin) => {
pin.addEventListener('click', () => { pin.addEventListener('click', () => {
const categoryId = parseInt(pin.dataset.categoryId, 10); const categoryId = parseInt(pin.dataset.categoryId, 10)
if (categoryId === pinnedCategory) { if (categoryId === pinnedCategory) {
pin.className = 'detected__category-pin-wrapper'; pin.className = 'detected__category-pin-wrapper'
pinnedCategory = null; pinnedCategory = null
} else { } else {
const active = document.querySelector('.detected__category-pin-wrapper--active'); const active = document.querySelector(
'.detected__category-pin-wrapper--active'
)
if (active) { if (active) {
active.className = 'detected__category-pin-wrapper'; active.className = 'detected__category-pin-wrapper'
} }
pin.className = 'detected__category-pin-wrapper detected__category-pin-wrapper--active'; pin.className =
'detected__category-pin-wrapper detected__category-pin-wrapper--active'
pinnedCategory = categoryId; pinnedCategory = categoryId
} }
port.postMessage({ port.postMessage({
id: 'set_option', id: 'set_option',
key: 'pinnedCategory', key: 'pinnedCategory',
value: pinnedCategory, value: pinnedCategory
}); })
}); })
}); })
} }
function replaceDomWhenReady(dom) { function replaceDomWhenReady(dom) {
if (/complete|interactive|loaded/.test(document.readyState)) { if (/complete|interactive|loaded/.test(document.readyState)) {
replaceDom(dom); replaceDom(dom)
} else { } else {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
replaceDom(dom); replaceDom(dom)
}); })
} }
} }
function appsToDomTemplate(response) { function appsToDomTemplate(response) {
let template = []; let template = []
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) { if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) {
const categories = {}; const categories = {}
// Group apps by category // Group apps by category
for (const appName in response.tabCache.detected) { for (const appName in response.tabCache.detected) {
response.apps[appName].cats.forEach((cat) => { response.apps[appName].cats.forEach((cat) => {
categories[cat] = categories[cat] || { apps: [] }; categories[cat] = categories[cat] || { apps: [] }
categories[cat].apps[appName] = appName; categories[cat].apps[appName] = appName
}); })
} }
for (const cat in categories) { for (const cat in categories) {
const apps = []; const apps = []
for (const appName in categories[cat].apps) { for (const appName in categories[cat].apps) {
const { confidence, version } = response.tabCache.detected[appName]; const { confidence, version } = response.tabCache.detected[appName]
apps.push( apps.push([
'a',
{
class: 'detected__app',
target: '_blank',
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`
},
[ [
'a', { 'img',
class: 'detected__app', {
target: '_blank', class: 'detected__app-icon',
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`, src: `../images/icons/${response.apps[appName].icon ||
}, [ 'default.svg'}`
'img', { }
class: 'detected__app-icon', ],
src: `../images/icons/${response.apps[appName].icon || 'default.svg'}`, [
}, 'span',
], [ {
'span', { class: 'detected__app-name'
class: 'detected__app-name', },
}, appName
appName,
], version ? [
'span', {
class: 'detected__app-version',
},
version,
] : null, confidence < 100 ? [
'span', {
class: 'detected__app-confidence',
},
`${confidence}% sure`,
] : null,
], ],
); version
? [
'span',
{
class: 'detected__app-version'
},
version
]
: null,
confidence < 100
? [
'span',
{
class: 'detected__app-confidence'
},
`${confidence}% sure`
]
: null
])
} }
template.push( template.push([
'div',
{
class: 'detected__category'
},
[ [
'div', { 'div',
class: 'detected__category', {
}, [ class: 'detected__category-name'
'div', { },
class: 'detected__category-name', [
}, [ 'a',
'a', { {
class: 'detected__category-link', class: 'detected__category-link',
target: '_blank', target: '_blank',
href: `https://www.wappalyzer.com/categories/${slugify(response.categories[cat].name)}`, href: `https://www.wappalyzer.com/categories/${slugify(
}, response.categories[cat].name
browser.i18n.getMessage(`categoryName${cat}`), )}`
], [
'span', {
class: `detected__category-pin-wrapper${pinnedCategory == cat ? ' detected__category-pin-wrapper--active' : ''}`,
'data-category-id': cat,
title: browser.i18n.getMessage('categoryPin'),
}, [
'img', {
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg',
},
], [
'img', {
class: 'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg',
},
],
],
], [
'div', {
class: 'detected__apps',
}, },
apps, browser.i18n.getMessage(`categoryName${cat}`)
], ],
[
'span',
{
class: `detected__category-pin-wrapper${
pinnedCategory == cat
? ' detected__category-pin-wrapper--active'
: ''
}`,
'data-category-id': cat,
title: browser.i18n.getMessage('categoryPin')
},
[
'img',
{
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg'
}
],
[
'img',
{
class:
'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg'
}
]
]
], ],
); [
'div',
{
class: 'detected__apps'
},
apps
]
])
} }
template = [ template = [
'div', { 'div',
class: 'detected', {
class: 'detected'
}, },
template, template
]; ]
} else { } else {
template = [ template = [
'div', { 'div',
class: 'empty', {
class: 'empty'
}, },
[ [
'span', { 'span',
class: 'empty__text', {
class: 'empty__text'
}, },
browser.i18n.getMessage('noAppsDetected'), browser.i18n.getMessage('noAppsDetected')
], ]
]; ]
} }
return template; return template
} }
async function getApps() { async function getApps() {
try { try {
const tabs = await browser.tabs.query({ const tabs = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true
}); })
const url = new URL(tabs[0].url)
document.querySelector(
'.footer__link'
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
`${url.protocol}//${url.hostname}`
)}`
port.postMessage({ port.postMessage({
id: 'get_apps', id: 'get_apps',
tab: tabs[0], tab: tabs[0]
}); })
} catch (error) { } catch (error) {
console.error(error); // eslint-disable-line no-console console.error(error) // eslint-disable-line no-console
} }
} }
/** /**
* Async function to update body class based on option. * Async function to update body class based on option.
*/ */
async function getThemeMode() { function getThemeMode() {
try { try {
port.postMessage({ port.postMessage({
id: 'update_theme_mode', id: 'update_theme_mode'
}); })
} catch (error) { } catch (error) {
console.error(error); // eslint-disable-line no-console console.error(error) // eslint-disable-line no-console
} }
} }
@ -224,50 +274,51 @@ async function getThemeMode() {
*/ */
function updateThemeMode(res) { function updateThemeMode(res) {
if (res.hasOwnProperty('themeMode') && res.themeMode !== false) { if (res.hasOwnProperty('themeMode') && res.themeMode !== false) {
document.body.classList.add('theme-mode-sync'); document.body.classList.add('theme-mode-sync')
} }
} }
function displayApps(response) { function displayApps(response) {
pinnedCategory = response.pinnedCategory; // eslint-disable-line prefer-destructuring pinnedCategory = response.pinnedCategory // eslint-disable-line prefer-destructuring
termsAccepted = response.termsAccepted; // eslint-disable-line prefer-destructuring termsAccepted = response.termsAccepted // eslint-disable-line prefer-destructuring
if (termsAccepted) { if (termsAccepted) {
replaceDomWhenReady(appsToDomTemplate(response)); replaceDomWhenReady(appsToDomTemplate(response))
} else { } else {
i18n(); i18n()
const wrapper = document.querySelector('.terms__wrapper'); const wrapper = document.querySelector('.terms__wrapper')
document.querySelector('.terms__accept').addEventListener('click', () => { document.querySelector('.terms__accept').addEventListener('click', () => {
port.postMessage({ port.postMessage({
id: 'set_option', id: 'set_option',
key: 'termsAccepted', key: 'termsAccepted',
value: true, value: true
}); })
wrapper.classList.remove('terms__wrapper--active'); wrapper.classList.remove('terms__wrapper--active')
getApps(); getApps()
}); })
wrapper.classList.add('terms__wrapper--active'); wrapper.classList.add('terms__wrapper--active')
} }
} }
port.onMessage.addListener((message) => { port.onMessage.addListener((message) => {
switch (message.id) { switch (message.id) {
case 'get_apps': case 'get_apps':
displayApps(message.response); displayApps(message.response)
break; break
case 'update_theme_mode': case 'update_theme_mode':
updateThemeMode(message.response); updateThemeMode(message.response)
break; break
default: default:
// Do nothing // Do nothing
} }
}); })
getThemeMode(); getThemeMode()
getApps(); getApps()

@ -8,130 +8,137 @@
const validation = { const validation = {
hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/, hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/,
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/, hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/
}; }
/** /**
* Enclose string in array * Enclose string in array
*/ */
function asArray(value) { function asArray(value) {
return value instanceof Array ? value : [value]; return Array.isArray(value) ? value : [value]
} }
/** /**
* *
*/ */
function asyncForEach(iterable, iterator) { function asyncForEach(iterable, iterator) {
return Promise.all((iterable || []) return Promise.all(
.map(item => new Promise(resolve => setTimeout(() => resolve(iterator(item)), 1)))); (iterable || []).map(
(item) =>
new Promise((resolve) => setTimeout(() => resolve(iterator(item)), 1))
)
)
} }
/** /**
* Mark application as detected, set confidence and version * Mark application as detected, set confidence and version
*/ */
function addDetected(app, pattern, type, value, key) { function addDetected(app, pattern, type, value, key) {
app.detected = true; app.detected = true
// Set confidence level // Set confidence level
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] = pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10); app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] =
pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10)
// Detect version number // Detect version number
if (pattern.version) { if (pattern.version) {
const versions = []; const versions = []
const matches = pattern.regex.exec(value); const matches = pattern.regex.exec(value)
let { version } = pattern; let { version } = pattern
if (matches) { if (matches) {
matches.forEach((match, i) => { matches.forEach((match, i) => {
// Parse ternary operator // Parse ternary operator
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version); const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version)
if (ternary && ternary.length === 3) { if (ternary && ternary.length === 3) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]); version = version.replace(ternary[0], match ? ternary[1] : ternary[2])
} }
// Replace back references // Replace back references
version = version.trim().replace(new RegExp(`\\\\${i}`, 'g'), match || ''); version = version
}); .trim()
.replace(new RegExp(`\\\\${i}`, 'g'), match || '')
})
if (version && versions.indexOf(version) === -1) { if (version && !versions.includes(version)) {
versions.push(version); versions.push(version)
} }
if (versions.length) { if (versions.length) {
// Use the longest detected version number // Use the longest detected version number
app.version = versions.reduce((a, b) => (a.length > b.length ? a : b)); app.version = versions.reduce((a, b) => (a.length > b.length ? a : b))
} }
} }
} }
} }
function resolveExcludes(apps, detected) { function resolveExcludes(apps, detected) {
const excludes = []; const excludes = []
const detectedApps = Object.assign({}, apps, detected); const detectedApps = Object.assign({}, apps, detected)
// Exclude app in detected apps only // Exclude app in detected apps only
Object.keys(detectedApps).forEach((appName) => { Object.keys(detectedApps).forEach((appName) => {
const app = detectedApps[appName]; const app = detectedApps[appName]
if (app.props.excludes) { if (app.props.excludes) {
asArray(app.props.excludes).forEach((excluded) => { asArray(app.props.excludes).forEach((excluded) => {
excludes.push(excluded); excludes.push(excluded)
}); })
} }
}); })
// Remove excluded applications // Remove excluded applications
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
if (excludes.indexOf(appName) > -1) { if (excludes.includes(appName)) {
delete apps[appName]; delete apps[appName]
} }
}); })
} }
class Application { class Application {
constructor(name, props, detected) { constructor(name, props, detected) {
this.confidence = {}; this.confidence = {}
this.confidenceTotal = 0; this.confidenceTotal = 0
this.detected = Boolean(detected); this.detected = Boolean(detected)
this.excludes = []; this.excludes = []
this.name = name; this.name = name
this.props = props; this.props = props
this.version = ''; this.version = ''
} }
/** /**
* Calculate confidence total * Calculate confidence total
*/ */
getConfidence() { getConfidence() {
let total = 0; let total = 0
Object.keys(this.confidence).forEach((id) => { Object.keys(this.confidence).forEach((id) => {
total += this.confidence[id]; total += this.confidence[id]
}); })
this.confidenceTotal = Math.min(total, 100); this.confidenceTotal = Math.min(total, 100)
return this.confidenceTotal; return this.confidenceTotal
} }
} }
class Wappalyzer { class Wappalyzer {
constructor() { constructor() {
this.apps = {}; this.apps = {}
this.categories = {}; this.categories = {}
this.driver = {}; this.driver = {}
this.jsPatterns = {}; this.jsPatterns = {}
this.detected = {}; this.detected = {}
this.hostnameCache = {}; this.hostnameCache = {}
this.adCache = []; this.adCache = []
this.config = { this.config = {
websiteURL: 'https://www.wappalyzer.com/', websiteURL: 'https://www.wappalyzer.com/',
twitterURL: 'https://twitter.com/Wappalyzer', twitterURL: 'https://twitter.com/Wappalyzer',
githubURL: 'https://github.com/AliasIO/Wappalyzer', githubURL: 'https://github.com/AliasIO/Wappalyzer'
}; }
} }
/** /**
@ -139,124 +146,135 @@ class Wappalyzer {
*/ */
log(message, source, type) { log(message, source, type) {
if (this.driver.log) { if (this.driver.log) {
this.driver.log(message, source || '', type || 'debug'); this.driver.log(message, source || '', type || 'debug')
} }
} }
analyze(url, data, context) { analyze(url, data, context) {
const apps = {}; const apps = {}
const promises = []; const promises = []
const startTime = new Date(); const startTime = new Date()
const { const { scripts, cookies, headers, js } = data
scripts,
cookies, let { html } = data
headers,
js,
} = data;
let { html } = data;
if (this.detected[url.canonical] === undefined) { if (this.detected[url.canonical] === undefined) {
this.detected[url.canonical] = {}; this.detected[url.canonical] = {}
} }
const metaTags = []; const metaTags = []
// Additional information // Additional information
let language = null; let language = null
if (html) { if (html) {
if (typeof html !== 'string') { if (typeof html !== 'string') {
html = ''; html = ''
} }
let matches = data.html.match(new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i')); let matches = data.html.match(
new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i')
)
language = matches && matches.length ? matches[1] : data.language || null; language = matches && matches.length ? matches[1] : data.language || null
// Meta tags // Meta tags
const regex = /<meta[^>]+>/ig; const regex = /<meta[^>]+>/gi
do { do {
matches = regex.exec(html); matches = regex.exec(html)
if (!matches) { if (!matches) {
break; break
} }
metaTags.push(matches[0]); metaTags.push(matches[0])
} while (matches); } while (matches)
} }
Object.keys(this.apps).forEach((appName) => { Object.keys(this.apps).forEach((appName) => {
apps[appName] = this.detected[url.canonical] && this.detected[url.canonical][appName] apps[appName] =
? this.detected[url.canonical][appName] this.detected[url.canonical] && this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName]); ? this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName])
const app = apps[appName]; const app = apps[appName]
promises.push(this.analyzeUrl(app, url)); promises.push(this.analyzeUrl(app, url))
if (html) { if (html) {
promises.push(this.analyzeHtml(app, html)); promises.push(this.analyzeHtml(app, html))
promises.push(this.analyzeMeta(app, metaTags)); promises.push(this.analyzeMeta(app, metaTags))
} }
if (scripts) { if (scripts) {
promises.push(this.analyzeScripts(app, scripts)); promises.push(this.analyzeScripts(app, scripts))
} }
if (cookies) { if (cookies) {
promises.push(this.analyzeCookies(app, cookies)); promises.push(this.analyzeCookies(app, cookies))
} }
if (headers) { if (headers) {
promises.push(this.analyzeHeaders(app, headers)); promises.push(this.analyzeHeaders(app, headers))
} }
}); })
if (js) { if (js) {
Object.keys(js).forEach((appName) => { Object.keys(js).forEach((appName) => {
if (typeof js[appName] !== 'function') { if (typeof js[appName] !== 'function') {
promises.push(this.analyzeJs(apps[appName], js[appName])); promises.push(this.analyzeJs(apps[appName], js[appName]))
} }
}); })
} }
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
await Promise.all(promises); await Promise.all(promises)
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
if (!app.detected || !app.getConfidence()) { if (!app.detected || !app.getConfidence()) {
delete apps[app.name]; delete apps[app.name]
} }
}); })
resolveExcludes(apps, this.detected[url]); resolveExcludes(apps, this.detected[url])
this.resolveImplies(apps, url.canonical); this.resolveImplies(apps, url.canonical)
this.cacheDetectedApps(apps, url.canonical); this.cacheDetectedApps(apps, url.canonical)
this.trackDetectedApps(apps, url, language); this.trackDetectedApps(apps, url, language)
this.log(`Processing ${Object.keys(data).join(', ')} took ${((new Date() - startTime) / 1000).toFixed(2)}s (${url.hostname})`, 'core'); this.log(
`Processing ${Object.keys(data).join(', ')} took ${(
(new Date() - startTime) /
1000
).toFixed(2)}s (${url.hostname})`,
'core'
)
if (Object.keys(apps).length) { if (Object.keys(apps).length) {
this.log(`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`, 'core'); this.log(
`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`,
'core'
)
} }
this.driver.displayApps(this.detected[url.canonical], { language }, context); this.driver.displayApps(
this.detected[url.canonical],
{ language },
context
)
return resolve(); return resolve()
}); })
} }
/** /**
* Cache detected ads * Cache detected ads
*/ */
cacheDetectedAds(ad) { cacheDetectedAds(ad) {
this.adCache.push(ad); this.adCache.push(ad)
} }
/** /**
@ -264,58 +282,65 @@ class Wappalyzer {
*/ */
robotsTxtAllows(url) { robotsTxtAllows(url) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const parsed = this.parseUrl(url); const parsed = this.parseUrl(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return reject(); return reject()
} }
const robotsTxt = await this.driver.getRobotsTxt(parsed.host, parsed.protocol === 'https:'); const robotsTxt = await this.driver.getRobotsTxt(
parsed.host,
if (robotsTxt.some(disallowedPath => parsed.pathname.indexOf(disallowedPath) === 0)) { parsed.protocol === 'https:'
return reject(); )
if (
robotsTxt.some(
(disallowedPath) => parsed.pathname.indexOf(disallowedPath) === 0
)
) {
return reject()
} }
return resolve(); return resolve()
}); })
} }
/** /**
* Parse a URL * Parse a URL
*/ */
parseUrl(url) { parseUrl(url) {
const a = this.driver.document.createElement('a'); const a = this.driver.document.createElement('a')
a.href = url; a.href = url
a.canonical = `${a.protocol}//${a.host}${a.pathname}`; a.canonical = `${a.protocol}//${a.host}${a.pathname}`
return a; return a
} }
/** /**
* *
*/ */
static parseRobotsTxt(robotsTxt) { static parseRobotsTxt(robotsTxt) {
const disallow = []; const disallow = []
let userAgent; let userAgent
robotsTxt.split('\n').forEach((line) => { robotsTxt.split('\n').forEach((line) => {
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim()); let matches = /^User-agent:\s*(.+)$/i.exec(line.trim())
if (matches) { if (matches) {
userAgent = matches[1].toLowerCase(); userAgent = matches[1].toLowerCase()
} else if (userAgent === '*' || userAgent === 'wappalyzer') { } else if (userAgent === '*' || userAgent === 'wappalyzer') {
matches = /^Disallow:\s*(.+)$/i.exec(line.trim()); matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
if (matches) { if (matches) {
disallow.push(matches[1]); disallow.push(matches[1])
} }
} }
}); })
return disallow; return disallow
} }
/** /**
@ -323,15 +348,15 @@ class Wappalyzer {
*/ */
ping() { ping() {
if (Object.keys(this.hostnameCache).length > 50) { if (Object.keys(this.hostnameCache).length > 50) {
this.driver.ping(this.hostnameCache); this.driver.ping(this.hostnameCache)
this.hostnameCache = {}; this.hostnameCache = {}
} }
if (this.adCache.length > 50) { if (this.adCache.length > 50) {
this.driver.ping({}, this.adCache); this.driver.ping({}, this.adCache)
this.adCache = []; this.adCache = []
} }
} }
@ -340,55 +365,55 @@ class Wappalyzer {
*/ */
parsePatterns(patterns) { parsePatterns(patterns) {
if (!patterns) { if (!patterns) {
return []; return []
} }
let parsed = {}; let parsed = {}
// Convert string to object containing array containing string // Convert string to object containing array containing string
if (typeof patterns === 'string' || patterns instanceof Array) { if (typeof patterns === 'string' || Array.isArray(patterns)) {
patterns = { patterns = {
main: asArray(patterns), main: asArray(patterns)
}; }
} }
Object.keys(patterns).forEach((key) => { Object.keys(patterns).forEach((key) => {
parsed[key] = []; parsed[key] = []
asArray(patterns[key]).forEach((pattern) => { asArray(patterns[key]).forEach((pattern) => {
const attrs = {}; const attrs = {}
pattern.split('\\;').forEach((attr, i) => { pattern.split('\\;').forEach((attr, i) => {
if (i) { if (i) {
// Key value pairs // Key value pairs
attr = attr.split(':'); attr = attr.split(':')
if (attr.length > 1) { if (attr.length > 1) {
attrs[attr.shift()] = attr.join(':'); attrs[attr.shift()] = attr.join(':')
} }
} else { } else {
attrs.string = attr; attrs.string = attr
try { try {
attrs.regex = new RegExp(attr.replace('/', '\/'), 'i'); // Escape slashes in regular expression attrs.regex = new RegExp(attr.replace('/', '/'), 'i') // Escape slashes in regular expression
} catch (error) { } catch (error) {
attrs.regex = new RegExp(); attrs.regex = new RegExp()
this.log(`${error.message}: ${attr}`, 'error', 'core'); this.log(`${error.message}: ${attr}`, 'error', 'core')
} }
} }
}); })
parsed[key].push(attrs); parsed[key].push(attrs)
}); })
}); })
// Convert back to array if the original pattern list was an array (or string) // Convert back to array if the original pattern list was an array (or string)
if ('main' in parsed) { if ('main' in parsed) {
parsed = parsed.main; parsed = parsed.main
} }
return parsed; return parsed
} }
/** /**
@ -397,49 +422,60 @@ class Wappalyzer {
parseJsPatterns() { parseJsPatterns() {
Object.keys(this.apps).forEach((appName) => { Object.keys(this.apps).forEach((appName) => {
if (this.apps[appName].js) { if (this.apps[appName].js) {
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js); this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js)
} }
}); })
} }
resolveImplies(apps, url) { resolveImplies(apps, url) {
let checkImplies = true; let checkImplies = true
const resolve = (appName) => { const resolve = (appName) => {
const app = apps[appName]; const app = apps[appName]
if (app && app.props.implies) { if (app && app.props.implies) {
asArray(app.props.implies).forEach((implied) => { asArray(app.props.implies).forEach((implied) => {
[implied] = this.parsePatterns(implied); ;[implied] = this.parsePatterns(implied)
if (!this.apps[implied.string]) { if (!this.apps[implied.string]) {
this.log(`Implied application ${implied.string} does not exist`, 'core', 'warn'); this.log(
`Implied application ${implied.string} does not exist`,
'core',
'warn'
)
return; return
} }
if (!(implied.string in apps)) { if (!(implied.string in apps)) {
apps[implied.string] = this.detected[url] && this.detected[url][implied.string] apps[implied.string] =
? this.detected[url][implied.string] this.detected[url] && this.detected[url][implied.string]
: new Application(implied.string, this.apps[implied.string], true); ? this.detected[url][implied.string]
: new Application(
checkImplies = true; implied.string,
this.apps[implied.string],
true
)
checkImplies = true
} }
// Apply app confidence to implied app // Apply app confidence to implied app
Object.keys(app.confidence).forEach((id) => { Object.keys(app.confidence).forEach((id) => {
apps[implied.string].confidence[`${id} implied by ${appName}`] = app.confidence[id] * (implied.confidence === undefined ? 1 : implied.confidence / 100); apps[implied.string].confidence[`${id} implied by ${appName}`] =
}); app.confidence[id] *
}); (implied.confidence === undefined ? 1 : implied.confidence / 100)
})
})
} }
}; }
// Implied applications // Implied applications
// Run several passes as implied apps may imply other apps // Run several passes as implied apps may imply other apps
while (checkImplies) { while (checkImplies) {
checkImplies = false; checkImplies = false
Object.keys(apps).forEach(resolve); Object.keys(apps).forEach(resolve)
} }
} }
@ -448,19 +484,18 @@ class Wappalyzer {
*/ */
cacheDetectedApps(apps, url) { cacheDetectedApps(apps, url) {
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
// Per URL // Per URL
this.detected[url][appName] = app; this.detected[url][appName] = app
Object.keys(app.confidence) Object.keys(app.confidence).forEach((id) => {
.forEach((id) => { this.detected[url][appName].confidence[id] = app.confidence[id]
this.detected[url][appName].confidence[id] = app.confidence[id]; })
}); })
});
if (this.driver.ping instanceof Function) { if (this.driver.ping instanceof Function) {
this.ping(); this.ping()
} }
} }
@ -469,204 +504,219 @@ class Wappalyzer {
*/ */
trackDetectedApps(apps, url, language) { trackDetectedApps(apps, url, language) {
if (!(this.driver.ping instanceof Function)) { if (!(this.driver.ping instanceof Function)) {
return; return
} }
const hostname = `${url.protocol}//${url.hostname}`; const hostname = `${url.protocol}//${url.hostname}`
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
if (this.detected[url.canonical][appName].getConfidence() >= 100) { if (this.detected[url.canonical][appName].getConfidence() >= 100) {
if ( if (
validation.hostname.test(url.hostname) validation.hostname.test(url.hostname) &&
&& !validation.hostnameBlacklist.test(url.hostname) !validation.hostnameBlacklist.test(url.hostname)
) { ) {
if (!(hostname in this.hostnameCache)) { if (!(hostname in this.hostnameCache)) {
this.hostnameCache[hostname] = { this.hostnameCache[hostname] = {
applications: {}, applications: {},
meta: {}, meta: {}
}; }
} }
if (!(appName in this.hostnameCache[hostname].applications)) { if (!(appName in this.hostnameCache[hostname].applications)) {
this.hostnameCache[hostname].applications[appName] = { this.hostnameCache[hostname].applications[appName] = {
hits: 0, hits: 0
}; }
} }
this.hostnameCache[hostname].applications[appName].hits += 1; this.hostnameCache[hostname].applications[appName].hits += 1
if (apps[appName].version) { if (apps[appName].version) {
this.hostnameCache[hostname].applications[appName].version = app.version; this.hostnameCache[hostname].applications[appName].version =
app.version
} }
} }
} }
}); })
if (hostname in this.hostnameCache) { if (hostname in this.hostnameCache) {
this.hostnameCache[hostname].meta.language = language; this.hostnameCache[hostname].meta.language = language
} }
this.ping(); this.ping()
} }
/** /**
* Analyze URL * Analyze URL
*/ */
analyzeUrl(app, url) { analyzeUrl(app, url) {
const patterns = this.parsePatterns(app.props.url); const patterns = this.parsePatterns(app.props.url)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(url.canonical)) { if (pattern.regex.test(url.canonical)) {
addDetected(app, pattern, 'url', url.canonical); addDetected(app, pattern, 'url', url.canonical)
} }
}); })
} }
/** /**
* Analyze HTML * Analyze HTML
*/ */
analyzeHtml(app, html) { analyzeHtml(app, html) {
const patterns = this.parsePatterns(app.props.html); const patterns = this.parsePatterns(app.props.html)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(html)) { if (pattern.regex.test(html)) {
addDetected(app, pattern, 'html', html); addDetected(app, pattern, 'html', html)
} }
}); })
} }
/** /**
* Analyze script tag * Analyze script tag
*/ */
analyzeScripts(app, scripts) { analyzeScripts(app, scripts) {
const patterns = this.parsePatterns(app.props.script); const patterns = this.parsePatterns(app.props.script)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
scripts.forEach((uri) => { scripts.forEach((uri) => {
if (pattern.regex.test(uri)) { if (pattern.regex.test(uri)) {
addDetected(app, pattern, 'script', uri); addDetected(app, pattern, 'script', uri)
} }
}); })
}); })
} }
/** /**
* Analyze meta tag * Analyze meta tag
*/ */
analyzeMeta(app, metaTags) { analyzeMeta(app, metaTags) {
const patterns = this.parsePatterns(app.props.meta); const patterns = this.parsePatterns(app.props.meta)
const promises = []; const promises = []
if (!app.props.meta) { if (!app.props.meta) {
return Promise.resolve(); return Promise.resolve()
} }
metaTags.forEach((match) => { metaTags.forEach((match) => {
Object.keys(patterns).forEach((meta) => { Object.keys(patterns).forEach((meta) => {
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i'); const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i')
if (r.test(match)) { if (r.test(match)) {
const content = match.match(/content=("|')([^"']+)("|')/i); const content = match.match(/content=("|')([^"']+)("|')/i)
promises.push(asyncForEach(patterns[meta], (pattern) => { promises.push(
if (content && content.length === 4 && pattern.regex.test(content[2])) { asyncForEach(patterns[meta], (pattern) => {
addDetected(app, pattern, 'meta', content[2], meta); if (
} content &&
})); content.length === 4 &&
pattern.regex.test(content[2])
) {
addDetected(app, pattern, 'meta', content[2], meta)
}
})
)
} }
}); })
}); })
return Promise.all(promises); return Promise.all(promises)
} }
/** /**
* Analyze response headers * Analyze response headers
*/ */
analyzeHeaders(app, headers) { analyzeHeaders(app, headers) {
const patterns = this.parsePatterns(app.props.headers); const patterns = this.parsePatterns(app.props.headers)
const promises = []; const promises = []
Object.keys(patterns).forEach((headerName) => { Object.keys(patterns).forEach((headerName) => {
if (typeof patterns[headerName] !== 'function') { if (typeof patterns[headerName] !== 'function') {
promises.push(asyncForEach(patterns[headerName], (pattern) => { promises.push(
headerName = headerName.toLowerCase(); asyncForEach(patterns[headerName], (pattern) => {
headerName = headerName.toLowerCase()
if (headerName in headers) {
headers[headerName].forEach((headerValue) => { if (headerName in headers) {
if (pattern.regex.test(headerValue)) { headers[headerName].forEach((headerValue) => {
addDetected(app, pattern, 'headers', headerValue, headerName); if (pattern.regex.test(headerValue)) {
} addDetected(app, pattern, 'headers', headerValue, headerName)
}); }
} })
})); }
})
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
/** /**
* Analyze cookies * Analyze cookies
*/ */
analyzeCookies(app, cookies) { analyzeCookies(app, cookies) {
const patterns = this.parsePatterns(app.props.cookies); const patterns = this.parsePatterns(app.props.cookies)
const promises = []; const promises = []
Object.keys(patterns).forEach((cookieName) => { Object.keys(patterns).forEach((cookieName) => {
if (typeof patterns[cookieName] !== 'function') { if (typeof patterns[cookieName] !== 'function') {
const cookieNameLower = cookieName.toLowerCase(); const cookieNameLower = cookieName.toLowerCase()
promises.push(asyncForEach(patterns[cookieName], (pattern) => { promises.push(
const cookie = cookies.find(_cookie => _cookie.name.toLowerCase() === cookieNameLower); asyncForEach(patterns[cookieName], (pattern) => {
const cookie = cookies.find(
(_cookie) => _cookie.name.toLowerCase() === cookieNameLower
)
if (cookie && pattern.regex.test(cookie.value)) { if (cookie && pattern.regex.test(cookie.value)) {
addDetected(app, pattern, 'cookies', cookie.value, cookieName); addDetected(app, pattern, 'cookies', cookie.value, cookieName)
} }
})); })
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
/** /**
* Analyze JavaScript variables * Analyze JavaScript variables
*/ */
analyzeJs(app, results) { analyzeJs(app, results) {
const promises = []; const promises = []
Object.keys(results).forEach((string) => { Object.keys(results).forEach((string) => {
if (typeof results[string] !== 'function') { if (typeof results[string] !== 'function') {
promises.push(asyncForEach(Object.keys(results[string]), (index) => { promises.push(
const pattern = this.jsPatterns[app.name][string][index]; asyncForEach(Object.keys(results[string]), (index) => {
const value = results[string][index]; const pattern = this.jsPatterns[app.name][string][index]
const value = results[string][index]
if (pattern && pattern.regex.test(value)) { if (pattern && pattern.regex.test(value)) {
addDetected(app, pattern, 'js', value, string); addDetected(app, pattern, 'js', value, string)
} }
})); })
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
} }
if (typeof module === 'object') { if (typeof module === 'object') {
module.exports = Wappalyzer; module.exports = Wappalyzer
} }

Loading…
Cancel
Save