diff --git a/content.js b/content.js index 2ecd90b..f56c3e0 100644 --- a/content.js +++ b/content.js @@ -102,24 +102,49 @@ class DIMAAnalyzer { // ===== INITIALISATION ET STYLES ===== -// CSS pour les animations +// CSS pour les animations (tous les @keyframes utilisés par l'extension +// sont définis ici une seule fois pour qu'ils soient disponibles avant +// l'apparition du bouton, de l'alerte de site suspect ou du modal). +// Tous les noms sont préfixés `dima*` car @keyframes partage un namespace +// global avec la page hôte; des noms génériques (fadeIn, slideIn) auraient +// pu écraser ou être écrasés par les animations existantes du site visité. const style = document.createElement("style"); style.textContent = ` - @keyframes dimaFadeIn { + @keyframes dimaFadeInScale { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } - + @keyframes dimaFadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes dimaSlideIn { + from { transform: translateY(30px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes dimaSlideInRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + #dima-btn { - animation: dimaFadeIn 0.5s ease-out; + animation: dimaFadeInScale 0.5s ease-out; } `; document.head.appendChild(style); -// Initialisation sécurisée avec gestion d'erreurs améliorée -console.log("DIMA: Script chargé - Version complète avec mots-clés améliorés"); +// Initialisation sécurisée. Le log d'init est derrière le flag debug pour +// rester silencieux sur les sites hôtes (l'extension tourne sur ). +const _dimaDebug = () => !!window.DIMA_DEBUG; +if (_dimaDebug()) { + console.log("DIMA: Script chargé - Version complète avec mots-clés améliorés"); +} -// Vérifier que toutes les dépendances sont chargées +// Vérifier que toutes les dépendances sont chargées. Ces dépendances sont +// censées être présentes dès l'exécution de content.js (dernier script +// déclaré dans le manifest, document_end, ordre garanti). Le retry sert +// uniquement de filet de sécurité pour les déploiements où l'ordre des +// scripts viendrait à changer. function checkDependencies() { return ( window.DIMA_TECHNIQUES && @@ -127,7 +152,8 @@ function checkDependencies() { window.CONTEXT_PATTERNS && window.ContentExtractor && window.TechniqueAnalyzer && - window.UIManager + window.UIManager && + typeof window.checkSuspiciousSite === "function" ); } @@ -140,16 +166,23 @@ function initializeDIMA(retryCount = 0) { console.error("DIMA: Échec du chargement des dépendances après 3 secondes."); return; } - console.log(`DIMA: Attente du chargement des dépendances... (${retryCount + 1}/${MAX_RETRIES})`); + // Log de retry derrière le flag debug. En fonctionnement normal le + // premier check passe et on n'imprime rien — pas de bruit sur la + // console de la page hôte. + if (_dimaDebug()) { + console.log(`DIMA: Attente du chargement des dépendances... (${retryCount + 1}/${MAX_RETRIES})`); + } setTimeout(() => initializeDIMA(retryCount + 1), 100); return; } try { const analyzer = new DIMAAnalyzer(); - console.log( - `DIMA: Analyseur initialisé pour page de type: ${analyzer.pageType}` - ); + if (_dimaDebug()) { + console.log( + `DIMA: Analyseur initialisé pour page de type: ${analyzer.pageType}` + ); + } } catch (error) { console.error("DIMA: Erreur d'initialisation critique:", error); } diff --git a/data/keywords.js b/data/keywords.js index cacfc4a..bc08842 100644 --- a/data/keywords.js +++ b/data/keywords.js @@ -53,7 +53,7 @@ const DIMA_ENHANCED_KEYWORDS = { patterns: [ /\d+\s+(?:choses|façons|méthodes|secrets|things|ways|methods)\s+(?:que|pour|de|to|that)/i, /(?:voici|découvrez|here's|discover)\s+(?:comment|pourquoi|ce que|how|why|what)/i, - /(?:cette|cette|this)\s+\w+\s+va\s+vous\s+(?:\w+|will)/i, + /(?:cette|this)\s+\w+\s+va\s+vous\s+(?:\w+|will)/i, /(?:shocking|amazing|incredible)\s+(?:secret|truth|fact)/i, ], }, diff --git a/modules/Suspicioussitesmanager.js b/modules/Suspicioussitesmanager.js index 7dd5176..440a9b2 100644 --- a/modules/Suspicioussitesmanager.js +++ b/modules/Suspicioussitesmanager.js @@ -19,28 +19,35 @@ class SuspiciousSitesManager { byTags: {}, bySocialPlatform: {} }; - + // Logs verbeux désactivés par défaut: l'extension s'exécute sur + // et inondait la console de chaque page hôte. + this.debug = !!(typeof window !== 'undefined' && window.DIMA_DEBUG); + this.init(); } + log(...args) { + if (this.debug) console.log(...args); + } + /** * Initialise le gestionnaire en chargeant toutes les sources disponibles */ init() { - console.log('🛡️ DIMA: Initialisation du gestionnaire de sites suspects...'); - + this.log('🛡️ DIMA: Initialisation du gestionnaire de sites suspects...'); + // Détecter et charger les sources disponibles this.detectAndLoadSources(); - + // Agréger tous les sites this.aggregateAllSites(); - + // Calculer les statistiques this.calculateStats(); - - console.log(`✅ DIMA: ${this.allSites.length} entrées chargées depuis ${this.sources.size} source(s)`); - console.log(` - ${this.stats.totalDomains} domaines`); - console.log(` - ${this.stats.totalSocialAccounts} comptes de réseaux sociaux`); + + this.log(`✅ DIMA: ${this.allSites.length} entrées chargées depuis ${this.sources.size} source(s)`); + this.log(` - ${this.stats.totalDomains} domaines`); + this.log(` - ${this.stats.totalSocialAccounts} comptes de réseaux sociaux`); this.logStats(); } @@ -57,7 +64,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.recordedfuture.com/research/cta-ru-2025-0917', reportDate: '2025-09-17' }); - console.log(` ✓ Source CopyCop chargée: ${copycopDomains.length} domaines`); + this.log(` ✓ Source CopyCop chargée: ${copycopDomains.length} domaines`); } // Source 2: RRN (VIGINUM) @@ -69,7 +76,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.sgdsn.gouv.fr/files/files/20230619_NP_VIGINUM_RAPPORT-CAMPAGNE-RRN_VF_0.pdf', reportDate: '2023-06-19' }); - console.log(` ✓ Source RRN chargée: ${rrnDomains.length} domaines`); + this.log(` ✓ Source RRN chargée: ${rrnDomains.length} domaines`); } // Source 3: Portal Kombat (VIGINUM) @@ -81,7 +88,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.sgdsn.gouv.fr/files/files/20240212_NP_SGDSN_VIGINUM_RAPPORT-RESEAU-PORTAL-KOMBAT_VF.pdf', reportDate: '2024-02-01' }); - console.log(` ✓ Source Portal Kombat chargée: ${portalKombatDomains.length} domaines`); + this.log(` ✓ Source Portal Kombat chargée: ${portalKombatDomains.length} domaines`); } // Source 4: Baybridge (IRSEM) @@ -93,7 +100,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.irsem.fr/focus', reportDate: '2025-10-17' }); - console.log(` ✓ Source Baybridge chargée: ${baybridgeDomains.length} domaines`); + this.log(` ✓ Source Baybridge chargée: ${baybridgeDomains.length} domaines`); } // Source 5: Storm 1516 - Domaines (VIGINUM) @@ -105,7 +112,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.defense.gouv.fr/sites/default/files/desinformation/Rapport%20Storm%201516%20-%20SGDSN.pdf', reportDate: '2025-05-02' }); - console.log(` ✓ Source Storm 1516 (domaines) chargée: ${storm1516Domains.length} domaines`); + this.log(` ✓ Source Storm 1516 (domaines) chargée: ${storm1516Domains.length} domaines`); } // Source 6: Storm 1516 - Comptes sociaux (VIGINUM) - FORMAT NATIF @@ -117,7 +124,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.defense.gouv.fr/sites/default/files/desinformation/Rapport%20Storm%201516%20-%20SGDSN.pdf', reportDate: '2025-05-02' }); - console.log(` ✓ Source Storm 1516 (comptes sociaux) chargée: ${storm1516SocialAccounts.length} comptes`); + this.log(` ✓ Source Storm 1516 (comptes sociaux) chargée: ${storm1516SocialAccounts.length} comptes`); } // Source 7: Pravda @@ -129,7 +136,7 @@ class SuspiciousSitesManager { reportUrl: 'https://www.sgdsn.gouv.fr/files/files/20240212_NP_SGDSN_VIGINUM_PORTAL-KOMBAT-NETWORK_ENG_VF.pdf', reportDate: '2024-12-02' }); - console.log(` ✓ Source Pravda chargée: ${pravdaDomains.length} domaines`); + this.log(` ✓ Source Pravda chargée: ${pravdaDomains.length} domaines`); } // Source 8: Doppelganger - noms de domaines @@ -141,7 +148,7 @@ class SuspiciousSitesManager { reportUrl: 'https://en.wikipedia.org/wiki/List_of_political_disinformation_website_campaigns_in_Russia', reportDate: '2023-11-23' }); - console.log(` ✓ Source Doppelganger chargée: ${doppelgangerDomains.length} domaines`); + this.log(` ✓ Source Doppelganger chargée: ${doppelgangerDomains.length} domaines`); } // Source 9: InfoRos - noms de domaines @@ -153,7 +160,7 @@ class SuspiciousSitesManager { reportUrl: 'https://openfacto.fr/2022/01/27/the-grus-galaxy-of-russian-speaking-websites/', reportDate: '2022-01-27' }); - console.log(` ✓ Source InfoRos chargée: ${infoRosDomains.length} domaines`); + this.log(` ✓ Source InfoRos chargée: ${infoRosDomains.length} domaines`); } // Source 10: Laundromat - noms de domaines @@ -165,7 +172,7 @@ class SuspiciousSitesManager { reportUrl: 'https://securingdemocracy.gmfus.org/wp-content/uploads/2024/05/Laundromat-Paper.pdf', reportDate: '2024-05-01' }); - console.log(` ✓ Source laundromat chargée: ${laundromatDomains.length} domaines`); + this.log(` ✓ Source laundromat chargée: ${laundromatDomains.length} domaines`); } // Avertissement si aucune source n'est chargée @@ -247,20 +254,20 @@ class SuspiciousSitesManager { * Affiche les statistiques dans la console */ logStats() { - console.log('📊 Statistiques:'); - console.log(` Total: ${this.stats.totalSites} entrées`); - console.log(` - Domaines: ${this.stats.totalDomains}`); - console.log(` - Comptes sociaux: ${this.stats.totalSocialAccounts}`); + this.log('📊 Statistiques:'); + this.log(` Total: ${this.stats.totalSites} entrées`); + this.log(` - Domaines: ${this.stats.totalDomains}`); + this.log(` - Comptes sociaux: ${this.stats.totalSocialAccounts}`); if (this.stats.totalSocialAccounts > 0) { - console.log(' Répartition par plateforme:'); + this.log(' Répartition par plateforme:'); for (const [platform, count] of Object.entries(this.stats.bySocialPlatform)) { - console.log(` • ${platform}: ${count}`); + this.log(` • ${platform}: ${count}`); } } - console.log(` Risque élevé: ${this.stats.byRiskLevel.high || 0}`); - console.log(` Risque moyen: ${this.stats.byRiskLevel.medium || 0}`); - console.log(` Risque faible: ${this.stats.byRiskLevel.low || 0}`); - console.log(` Sources: ${Object.keys(this.stats.bySources).length}`); + this.log(` Risque élevé: ${this.stats.byRiskLevel.high || 0}`); + this.log(` Risque moyen: ${this.stats.byRiskLevel.medium || 0}`); + this.log(` Risque faible: ${this.stats.byRiskLevel.low || 0}`); + this.log(` Sources: ${Object.keys(this.stats.bySources).length}`); } /** @@ -317,7 +324,7 @@ class SuspiciousSitesManager { } if (isMatch) { - console.log(`🎯 DIMA: Match trouvé!`, { + this.log(`🎯 DIMA: Match trouvé!`, { type: matchType, site: site.handle || site.domain, url: url @@ -399,7 +406,7 @@ class SuspiciousSitesManager { } if (extractedHandle) { - console.log(`🔍 DIMA: Comparaison - URL: "${extractedHandle}" vs DB: "${handle}"`); + this.log(`🔍 DIMA: Comparaison - URL: "${extractedHandle}" vs DB: "${handle}"`); return extractedHandle === handle; } @@ -464,7 +471,7 @@ class SuspiciousSitesManager { const match = pathname.match(pattern.regex); if (match) { const handle = match[1]; - console.log(`DIMA: Handle extrait de ${accountType}: ${handle}`); + this.log(`DIMA: Handle extrait de ${accountType}: ${handle}`); return handle; } @@ -560,25 +567,27 @@ class SuspiciousSitesManager { } } -// Initialisation automatique du gestionnaire -let suspiciousSitesManager; - -// Initialiser après le chargement de toutes les bases de données +// Initialisation automatique du gestionnaire. +// +// Historique: un setTimeout(100ms) entourait cette init pour "laisser les +// autres fichiers se charger". Mais en MV3 avec `run_at: document_end`, les +// scripts du content_scripts sont chargés et exécutés dans l'ordre déclaré +// dans manifest.json — à ce point toutes les bases (copycopDomains, ...) +// sont déjà définies. Le délai était donc inutile et créait une fenêtre +// pendant laquelle `window.checkSuspiciousSite` n'existait pas, forçant +// content.js à boucler sur ses retries pendant >=100ms à chaque page. if (typeof window !== 'undefined') { - // Dans le navigateur, initialiser après un court délai pour laisser les autres fichiers se charger - setTimeout(() => { - suspiciousSitesManager = new SuspiciousSitesManager(); - - // Rendre disponible globalement - window.suspiciousSitesManager = suspiciousSitesManager; - - // Pour compatibilité avec l'ancien code, exposer aussi checkSuspiciousSite - window.checkSuspiciousSite = (url) => suspiciousSitesManager.checkSite(url); - - // Exposer aussi les statistiques et infos - window.getSuspiciousSitesStats = () => suspiciousSitesManager.getStats(); - window.getSuspiciousSitesSourcesInfo = () => suspiciousSitesManager.getSourcesInfo(); - }, 100); + const suspiciousSitesManager = new SuspiciousSitesManager(); + + // Rendre disponible globalement + window.suspiciousSitesManager = suspiciousSitesManager; + + // Pour compatibilité avec l'ancien code, exposer aussi checkSuspiciousSite + window.checkSuspiciousSite = (url) => suspiciousSitesManager.checkSite(url); + + // Exposer aussi les statistiques et infos + window.getSuspiciousSitesStats = () => suspiciousSitesManager.getStats(); + window.getSuspiciousSitesSourcesInfo = () => suspiciousSitesManager.getSourcesInfo(); } // Export pour Node.js si nécessaire diff --git a/modules/contentExtractor.js b/modules/contentExtractor.js index 15a7806..b43e2a6 100644 --- a/modules/contentExtractor.js +++ b/modules/contentExtractor.js @@ -219,35 +219,67 @@ class ContentExtractor { cleanText(text) { if (!text) return ""; + // On préserve toutes les écritures (cyrillique, chinois, arabe, etc.): + // \p{L} = lettres tous scripts, \p{N} = chiffres, \p{M} = signes diacritiques. + // L'ancienne regex basée sur \w (ASCII) stripait le contenu non latin — + // bloquant l'analyse précisément sur les médias que le plugin cible. return text - .replace(/\s+/g, " ") .replace(/[\r\n\t]/g, " ") - .replace(/[^\w\s\.,!?;:()\-'"%àâäéèêëïîôöùûüÿç]/gi, "") + .replace(/[^\p{L}\p{N}\p{M}\s\.,!?;:()\-'"%]/gu, "") + .replace(/\s+/g, " ") .trim(); } detectPageType() { - const url = window.location.href.toLowerCase(); - if ( - url.includes("news") || - url.includes("article") || - url.includes("actualit") - ) - return "news"; - if (url.includes("blog")) return "blog"; + // On distingue le hostname du reste de l'URL pour éviter que des + // sous-chaînes comme "news" matchent dans une querystring quelconque, + // et on utilise des frontières non alphanumériques pour distinguer + // "news.example.com" de "businessnews.example.com". + let hostname = ""; + let pathAndQuery = ""; + try { + const u = new URL(window.location.href); + hostname = u.hostname.toLowerCase(); + pathAndQuery = (u.pathname + u.search).toLowerCase(); + } catch (_) { + const url = window.location.href.toLowerCase(); + hostname = url; + pathAndQuery = url; + } + + // matchWord exige une frontière complète (ex: `news` ne matche pas + // `businessnews`); matchStem n'exige qu'une frontière de début pour + // accepter les pluriels / dérivés (`product` → `products`, `shop` → `shopping`). + const matchWord = (haystack, word) => + new RegExp(`(^|[^a-z0-9])${word}([^a-z0-9]|$)`).test(haystack); + const matchStem = (haystack, stem) => + new RegExp(`(^|[^a-z0-9])${stem}`).test(haystack); + + // Réseaux sociaux : on cible le hostname pour éviter qu'un article + // mentionnant "facebook" dans l'URL d'un autre média soit classé "social". + const socialHosts = ["facebook.com", "twitter.com", "x.com", "instagram.com", "t.me", "telegram.me", "tiktok.com", "vk.com"]; + if (socialHosts.some((h) => hostname === h || hostname.endsWith("." + h))) return "social"; + if ( - url.includes("facebook") || - url.includes("twitter") || - url.includes("instagram") - ) - return "social"; + matchWord(hostname, "news") || + matchWord(pathAndQuery, "news") || + // matchStem accepte les pluriels et dérivés: + // `/articles/foo` et `/blogs/foo` sont des segments très courants + // qu'un matchWord strict aurait classés en "general". + matchStem(pathAndQuery, "article") || + matchStem(pathAndQuery, "actualit") + ) return "news"; + + if (matchStem(hostname, "blog") || matchStem(pathAndQuery, "blog")) return "blog"; + if ( - url.includes("shop") || - url.includes("buy") || - url.includes("product") || - url.includes("commerce") - ) - return "commerce"; + matchStem(hostname, "shop") || + matchStem(pathAndQuery, "shop") || + matchStem(pathAndQuery, "product") || + matchWord(pathAndQuery, "commerce") || + matchWord(pathAndQuery, "buy") + ) return "commerce"; + return "general"; } } diff --git a/modules/uiManager.js b/modules/uiManager.js index f0ac365..8c837f0 100644 --- a/modules/uiManager.js +++ b/modules/uiManager.js @@ -28,42 +28,72 @@ class UIManager { if (analysisResults) { this.analysisResults = analysisResults; } - + if (!this.analysisResults) { console.error('DIMA: Aucun résultat d\'analyse disponible pour créer le bouton'); return; } // Vérifier si le site est suspect - this.suspiciousSiteCheck = window.checkSuspiciousSite ? - window.checkSuspiciousSite(window.location.href) : + this.suspiciousSiteCheck = window.checkSuspiciousSite ? + window.checkSuspiciousSite(window.location.href) : { isSuspicious: false }; - + try { // Supprimer bouton existant document.getElementById('dima-btn')?.remove(); document.getElementById('dima-suspicious-alert')?.remove(); - if (this.buttonCreated) return; - - // Créer le bouton principal + // Couleur du bouton: blindée pour éviter qu'une valeur invalide + // ne produise un gradient cassé via adjustColor. + const safeRiskColor = this.sanitizeHexColor(this.analysisResults.riskColor); + + // Créer le bouton principal (construction DOM-safe: jamais d'innerHTML + // sur des valeurs dérivées des résultats d'analyse). + // + // Accessibilité: c'est un
(pas un - @@ -879,18 +963,6 @@ ${techniques.map(t => `• ${t.nom}`).join('\n')}`;