PageSourceSearch

https://sadako.ai/Widget/widget.js

js sadako.ai collected 2026-10-05 12:33:01 UTC 40,015 bytes, 997 lines download raw bytes

1/**
2 * Age Verification Widget - JavaScript universel
3 * Compatible avec tous types de sites web
4 */
5(function(window, document) {
6    'use strict';
7    
8    // Éviter les conflits si le widget est déjà chargé
9    if (window.AgeVerificationWidget) {
10        return;
11    }
12    
13    var AgeVerificationWidget = {
14        // Configuration par défaut
15        config: {
16            apiUrl: '',
17            debug: false,
18            onVerified: null,
19            onDenied: null,
20            onError: null
21        },
22        
23        // État du widget
24        state: {
25            initialized: false,
26            popupVisible: false,
27            cameraStream: null,
28            verificationConfig: null
29        },
30        
31
32
33        /**
34         * Initialise le widget
35         */
36        init: function(options) {
37            if (this.state.initialized) {
38                this.log('Widget déjà initialisé');
39                return;
40            }
41            
42            // Fusion des options
43            this.config = this.extend(this.config, options || {});
44            
45            if (!this.config.apiUrl) {
46                this.error('URL API requise pour initialiser le widget');
47                return;
48            }
49            
50            this.log('Initialisation du widget...');
51            
52            // Vérifier si on est en mode administration
53            if (window.AgeVerificationWidget_AdminMode === true) {
54                this.log('Mode administration détecté - pas de vérification automatique');
55                this.state.initialized = true;
56                return;
57            }
58            
59            // Vérification de la configuration d'abord
60            this.checkConfiguration();
61            
62            this.state.initialized = true;
63        },
64        
65        /**
66         * Vérifie la configuration côté serveur
67         */
68        checkConfiguration: function() {
69            var self = this;
70            
71            // 🍪 PREMIÈRE VÉRIFICATION: Cookie existant (avant même l'API)
72            var verifiedCookie = this.getCookie('avs_verified');
73            this.log('🍪 Vérification préalable du cookie avs_verified: "' + verifiedCookie + '"');
74            
75            if (verifiedCookie === '1') {
76                this.log('✅ Utilisateur DÉJÀ vérifié via cookie - AUCUNE protection ni popup');
77                return; // Sortir complètement - pas besoin d'API
78            }
79            
80            this.request('GET', '/api/config', null, function(response) {
81                if (response.show_popup) {
82                    // Fusionner la configuration avec les textes multilingues
83                    self.state.verificationConfig = self.mergeConfigWithTexts(response);
84                    
85                    self.log('Configuration multilingue chargée. Langue détectée: ' + response.language);
86                    if (response.detected_country) {
87                        self.log('Pays détecté: ' + response.detected_country);
88                    }
89                    
90                    // 🔒 Initialiser la protection du contenu seulement si popup requis
91                    self.initContentProtection();
92                    
93                    self.showPopup();
94                } else {
95                    self.log('✅ Popup non requis pour cet utilisateur - AUCUNE PROTECTION');
96                    // Aucune protection du contenu nécessaire
97                }
98            }, function(error) {
99                self.error('Erreur lors de la vérification de configuration:', error);
100                // En cas d'erreur, ne pas activer la protection pour éviter de bloquer le site
101                self.log('⚠️ Erreur de configuration - protection désactivée par sécurité');
102            });
103        },
104        
105        /**
106         * Charge la configuration spécifiquement pour affichage manuel du popup
107         */
108        loadConfigurationForPopup: function() {
109            var self = this;
110            
111            this.request('GET', '/api/config', null, function(response) {
112                // Toujours charger la configuration, même si show_popup est false
113                if (response.config) {
114                    // Fusionner la configuration avec les textes multilingues
115                    self.state.verificationConfig = self.mergeConfigWithTexts(response);
116                    
117                    self.log('Configuration multilingue chargée pour affichage manuel. Langue: ' + response.language);
118                    // Maintenant afficher le popup
119                    self.showPopup();
120                } else {
121                    self.error('Configuration non disponible depuis l\'API');
122                }
123            }, function(error) {
124                self.error('Erreur lors du chargement de la configuration:', error);
125            });
126        },
127        
128        /**
129         * Fusionne la configuration avec les textes de la langue appropriée
130         */
131        mergeConfigWithTexts: function(response) {
132            var config = response.config || {};
133            var popupTexts = response.popup_texts || {};
134            
135            // Fusionner les textes multilingues dans la configuration principale
136            for (var key in popupTexts) {
137                if (popupTexts.hasOwnProperty(key)) {
138                    config[key] = popupTexts[key];
139                }
140            }
141            
142            // Ajouter des informations sur la langue
143            config.detected_language = response.language || 'fr';
144            config.detected_country = response.detected_country || null;
145            config.auto_language_detection = response.auto_language_detection || false;
146            
147            this.log('🌍 Textes fusionnés pour la langue: ' + config.detected_language);
148            
149            return config;
150        },
151        
152        /**
153         * Affiche le popup de vérification
154         */
155        showPopup: function() {
156            if (this.state.popupVisible) {
157                return;
158            }
159            
160            // Si pas de configuration, la récupérer d'abord
161            if (!this.state.verificationConfig) {
162                this.log('Récupération de la configuration pour affichage manuel du popup...');
163                this.loadConfigurationForPopup();
164                return;
165            }
166            
167            this.log('Affichage du popup de vérification');
168            
169            this.createPopupHTML();
170            this.setupEventListeners();
171            this.state.popupVisible = true;
172            
173            // Empêcher le scroll de la page
174            document.body.style.overflow = 'hidden';
175        },
176        
177        /**
178         * Crée le HTML du popup
179         */
180        createPopupHTML: function() {
181            var config = this.state.verificationConfig;
182            
183            // Création du container principal
184            var modal = document.createElement('div');
185            modal.id = 'avs-modal';
186            
187            // Ajouter la classe de thème si spécifiée
188            if (config.theme && config.theme !== 'default') {
189                modal.className = 'avs-theme-' + config.theme;
190            }
191            
192            modal.innerHTML = this.getPopupHTML(config);
193            
194            // Ajouter le CSS personnalisé si défini
195            if (config.custom_css && config.custom_css.trim()) {
196                this.injectCustomCSS(config.custom_css);
197            }
198            
199            document.body.appendChild(modal);
200        },
201        
202        /**
203         * Génère le HTML du popup
204         */
205        getPopupHTML: function(config) {
206            var borderRadius = config.popup_shape === 'square' ? '0px' : '12px';
207            var buttonBorderRadius = config.rounded_buttons ? '999px' : '4px';
208            
209            return `
210                <div id="avs-overlay" style="
211                    position: fixed;
212                    top: 0; left: 0; right: 0; bottom: 0;
213                    background: rgba(0,0,0,0.6);
214                    backdrop-filter: blur(4px);
215                    display: flex;
216                    justify-content: center;
217                    align-items: center;
218                    z-index: 999999;
219                    font-family: '${config.font_family}', Arial, sans-serif;
220                ">
221                    <div id="avs-content" style="
222                        max-width: 420px;
223                        width: 90vw;
224                        background: ${config.popup_bg_color};
225                        color: ${config.popup_text_color};
226                        padding: 1.5rem;
227                        border-radius: ${borderRadius};
228                        box-sizing: border-box;
229                        max-height: 90vh;
230                        overflow-y: auto;
231                        font-size: ${config.font_size_desktop}px;
232                    ">
233                        <!-- Onglets -->
234                        <div id="avs-tabs" style="
235                            display: flex;
236                            border-bottom: 2px solid ${config.popup_button_color};
237                            margin-bottom: 1rem;
238                        ">
239                            <button type="button" id="avs-tab-verify" class="avs-tab active" style="
240                                flex: 1;
241                                padding: 0.7rem 0;
242                                font-size: 1.1rem;
243                                border: none;
244                                background: none;
245                                cursor: pointer;
246                                border-bottom: 3px solid ${config.popup_button_color};
247                                color: ${config.popup_button_color};
248                                font-weight: bold;
249                            ">${config.tab_verify_title}</button>
250                        </div>
251                        
252                        <!-- Contenu vérification -->
253                        <div id="avs-verify-content">
254                            <h2 id="avs-title">${config.tab_verify_title}</h2>
255                            <p id="avs-intro">${config.intro_text}</p>
256                            
257                            <button id="avs-start-btn" style="
258                                width: 100%;
259                                margin-top: 1rem;
260                                padding: 0.6rem 1.2rem;
261                                font-size: 1.1rem;
262                                background-color: ${config.popup_button_color};
263                                color: white;
264                                border: none;
265                                border-radius: ${buttonBorderRadius};
266                                cursor: pointer;
267                                margin-bottom: 1.5rem;
268                            ">${config.start_button_text}</button>
269                            
270                            <div id="avs-camera-container" style="display: none;">
271                                <p id="avs-camera-instructions">${config.camera_instructions}</p>
272                                
273                                <div style="position: relative; width: 100%; margin-bottom: 1rem;">
274                                    <video id="avs-video" autoplay playsinline style="
275                                        width: 100%;
276                                        border-radius: 12px;
277                                    "></video>
278                                    <div id="avs-overlay-guide" style="
279                                        position: absolute;
280                                        border: 3px solid rgba(0, 123, 255, 0.8);
281                                        border-radius: 50% / 60%;
282                                        width: 70%;
283                                        height: 80%;
284                                        top: 50%;
285                                        left: 50%;
286                                        transform: translate(-50%, -50%);
287                                        pointer-events: none;
288                                        box-sizing: border-box;
289                                    "></div>
290                                </div>
291                                
292                                <button id="avs-capture-btn" style="
293                                    width: 100%;
294                                    padding: 0.6rem 1.2rem;
295                                    font-size: 1.1rem;
296                                    background-color: ${config.popup_button_color};
297                                    color: white;
298                                    border: none;
299                                    border-radius: ${buttonBorderRadius};
300                                    cursor: pointer;
301                                ">${config.capture_button_text}</button>
302                            </div>
303                            
304                            <div id="avs-result" style="
305                                text-align: center;
306                                font-weight: bold;
307                                margin-top: 1rem;
308                                min-height: 1.5em;
309                            "></div>
310                            
311                            <div id="avs-access-denied" style="
312                                display: none;
313                                color: red;
314                                text-align: center;
315                                font-weight: bold;
316                                margin-top: 1rem;
317                            ">
317${config.access_denied_text}</div>
318                            
319                            <p style="margin-top: 1rem; text-align: center; font-size: 0.85rem;">
320                                <a href="#" id="avs-privacy-link" style="color: ${config.popup_button_color};">
321                                    ${config.privacy_link_text}
322                                </a>
323                            </p>
324                            
325                            ${config.login_link_enabled ? `
326                            <div style="text-align: center; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(128,128,128,0.3);">
327                                <a href="${config.login_link_url}" target="${config.login_link_target}" 
328                                   style="
329                                       display: inline-block;
330                                       padding: 0.5rem 1rem;
331                                       background-color: transparent;
332                                       color: ${config.popup_button_color};
333                                       text-decoration: none;
334                                       border: 2px solid ${config.popup_button_color};
335                                       border-radius: ${buttonBorderRadius};
336                                       font-weight: bold;
337                                       transition: all 0.3s ease;
338                                   "
339                                   onmouseover="this.style.backgroundColor='${config.popup_button_color}'; this.style.color='white';"
340                                   onmouseout="this.style.backgroundColor='transparent'; this.style.color='${config.popup_button_color}';"
341                                >${config.login_link_text}</a>
342                            </div>
343                            ` : ''}
344                        </div>
345                    </div>
346                </div>
347                
348                <!-- Modal de confidentialité -->
349                <div id="avs-privacy-modal" style="
350                    display: none;
351                    position: fixed;
352                    z-index: 1000000;
353                    top: 0; left: 0;
354                    width: 100%; height: 100%;
355                    background: rgba(0,0,0,0.6);
356                    backdrop-filter: blur(4px);
357                ">
358                    <div style="
359                        background: white;
360                        max-width: 500px;
361                        margin: 5% auto;
362                        padding: 1.5rem;
363                        border-radius: 10px;
364                        font-size: 0.9rem;
365                        overflow-y: auto;
366                        max-height: 90%;
367                    ">
368                        <h3 style="margin-top: 0;">${config.privacy_modal_title}</h3>
369                        <div>${config.privacy_text}</div>
370                        <button id="avs-close-privacy" style="
371                            margin-top: 1rem;
372                            padding: 0.5rem 1rem;
373                            border: none;
374                            background: ${config.popup_button_color};
375                            color: white;
376                            border-radius: 5px;
377                            cursor: pointer;
378                        ">Fermer</button>
379                    </div>
380                </div>
381                
382                <canvas id="avs-canvas" style="display: none;"></canvas>
383            `;
384        },
385        
386        /**
387         * Configure les événements
388         */
389        setupEventListeners: function() {
390            var self = this;
391            
392            // Bouton démarrer
393            document.getElementById('avs-start-btn').addEventListener('click', function() {
394                self.startCamera();
395            });
396            
397            // Bouton capturer
398            document.getElementById('avs-capture-btn').addEventListener('click', function() {
399                self.capturePhoto();
400            });
401            
402            // Lien confidentialité
403            document.getElementById('avs-privacy-link').addEventListener('click', function(e) {
404                e.preventDefault();
405                document.getElementById('avs-privacy-modal').style.display = 'block';
406            });
407            
408            // Fermer modal confidentialité
409            document.getElementById('avs-close-privacy').addEventListener('click', function() {
410                document.getElementById('avs-privacy-modal').style.display = 'none';
411            });
412        },
413        
414        /**
415         * Démarre la caméra
416         */
417        startCamera: function() {
418            var self = this;
419            var startBtn = document.getElementById('avs-start-btn');
420            var cameraContainer = document.getElementById('avs-camera-container');
421            var video = document.getElementById('avs-video');
422            
423            startBtn.disabled = true;
424            startBtn.textContent = this.state.verificationConfig.loading_text || 'Démarrage...';
425            
426            navigator.mediaDevices.getUserMedia({ 
427                video: { facingMode: 'user' } 
428            }).then(function(stream) {
429                self.state.cameraStream = stream;
430                video.srcObject = stream;
431                
432                // Afficher la caméra et masquer le bouton
433                cameraContainer.style.display = 'block';
434                startBtn.style.display = 'none';
435                
436                // Masquer titre et intro si configuré
437                if (self.state.verificationConfig.hide_intro_on_start) {
438                    document.getElementById('avs-title').style.display = 'none';
439                    document.getElementById('avs-intro').style.display = 'none';
440                }
441                
442            }).catch(function(error) {
443                self.error('Erreur accès caméra:', error);
444                startBtn.disabled = false;
445                startBtn.textContent = self.state.verificationConfig.start_button_text;
446                
447                var errorMsg = self.state.verificationConfig.camera_error_text || 
448                               'Erreur lors de l\'accès à la caméra';
449                alert(errorMsg);
450            });
451        },
452        
453        /**
454         * Capture une photo
455         */
456        capturePhoto: function() {
457            var self = this;
458            var video = document.getElementById('avs-video');
459            var canvas = document.getElementById('avs-canvas');
460            var captureBtn = document.getElementById('avs-capture-btn');
461            var resultDiv = document.getElementById('avs-result');
462            var accessDeniedDiv = document.getElementById('avs-access-denied');
463            
464            captureBtn.disabled = true;
465            resultDiv.textContent = 'Analyse en cours...';
466            accessDeniedDiv.style.display = 'none';
467            
468            // Capture de l'image
469            canvas.width = video.videoWidth;
470            canvas.height = video.videoHeight;
471            var ctx = canvas.getContext('2d');
472            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
473            
474            var base64 = canvas.toDataURL('image/jpeg');
475            
476            // Envoi pour analyse
477            this.request('POST', '/api/analyze', {
478                photo_base64: base64
479            }, function(response) {
480                if (response.success && response.verified) {
481                    // Succès
482                    resultDiv.style.color = 'green';
483                    resultDiv.textContent = self.state.verificationConfig.success_message;
484                    
485                    // Cookie de vérification
486                    // Succès - marquer comme vérifié avec la durée configurée
487                    var duration = self.state.verificationConfig.verification_duration_days || 30;
488                    self.setCookie('avs_verified', '1', duration);
489                    
490                    // 🔒 Révéler le contenu et arrêter la surveillance
491                    self.showContent();
492                    self.stopDOMSurveillance();
493                    
494                    // Callback
495                    if (self.config.onVerified) {
496                        self.config.onVerified(response.age);
497                    }
498                    
499                    // Fermeture automatique
500                    setTimeout(function() {
501                        self.hidePopup();
502                    }, (self.state.verificationConfig.success_delay || 2) * 1000);
503                    
504                } else if (response.success && !response.verified) {
505                    // Âge insuffisant
506                    accessDeniedDiv.style.display = 'block';
507                    resultDiv.textContent = '';
508                    
509                    if (self.config.onDenied) {
510                        self.config.onDenied(response.age);
511                    }
512                    
513                } else {
514                    // Erreur
515                    resultDiv.textContent = response.error || 'Erreur lors de l\'analyse';
516                }
517                
518                captureBtn.disabled = false;
519                
520            }, function(error) {
521                resultDiv.textContent = 'Erreur: ' + error;
522                captureBtn.disabled = false;
523                
524                if (self.config.onError) {
525                    self.config.onError(error);
526                }
527            });
528        },
529        
530        /**
531         * Masque le popup
532         */
533        hidePopup: function() {
534            if (!this.state.popupVisible) {
535                return;
536            }
537            
538            // Arrêt de la caméra
539            if (this.state.cameraStream) {
540                this.state.cameraStream.getTracks().forEach(function(track) {
541                    track.stop();
542                });
543                this.state.cameraStream = null;
544            }
545            
546            // Suppression du DOM
547            var modal = document.getElementById('avs-modal');
548            if (modal) {
549                modal.remove();
550            }
551            
552            // Suppression du CSS personnalisé
553            var customStyleTag = document.getElementById('avs-custom-styles');
554            if (customStyleTag) {
555                customStyleTag.remove();
556            }
557            
558            // Restauration du scroll
559            document.body.style.overflow = '';
560            
561            this.state.popupVisible = false;
562            
563            this.log('Popup masqué');
564        },
565        
566        /**
567         * Effectue une requête HTTP
568         */
569        request: function(method, endpoint, data, success, error) {
570            var xhr = new XMLHttpRequest();
571            var url = this.config.apiUrl + endpoint;
572            
573            xhr.open(method, url, true);
574            xhr.setRequestHeader('Content-Type', 'application/json');
575            
576            xhr.onreadystatechange = function() {
577                if (xhr.readyState === 4) {
578                    try {
579                        var response = JSON.parse(xhr.responseText);
580                        
581                        if (xhr.status >= 200 && xhr.status < 300) {
582                            if (success) success(response);
583                        } else {
584                            if (error) error(response.error || 'Erreur HTTP ' + xhr.status);
585                        }
586                    } catch (e) {
587                        if (error) error('Erreur de parsing JSON');
588                    }
589                }
590            };
591            
592            xhr.onerror = function() {
593                if (error) error('Erreur de réseau');
594            };
595            
596            if (data) {
597                xhr.send(JSON.stringify(data));
598            } else {
599                xhr.send();
600            }
601        },
602        
603        /**
604         * Définit un cookie
605         */
606        setCookie: function(name, value, days) {
607            var expires = '';
608            if (days) {
609                var date = new Date();
610                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
611                expires = '; expires=' + date.toUTCString();
612            }
613            document.cookie = name + '=' + (value || '') + expires + '; path=/';
614        },
615        
616        /**
617         * Fusion d'objets
618         */
619        extend: function(target, source) {
620            var result = {};
621            for (var key in target) {
622                if (target.hasOwnProperty(key)) {
623                    result[key] = target[key];
624                }
625            }
626            for (var key in source) {
627                if (source.hasOwnProperty(key)) {
628                    result[key] = source[key];
629                }
630            }
631            return result;
632        },
633        
634        /**
635         * Log de débogage
636         */
637        log: function() {
638            if (this.config.debug && console && console.log) {
639                console.log.apply(console, ['[AgeVerificationWidget]'].concat(Array.prototype.slice.call(arguments)));
640            }
641        },
642        
643        /**
644         * 🔒 Initialise la protection du contenu
645         */
646        initContentProtection: function() {
647            var self = this;
648            
649            // Vérifier que le DOM est prêt avant toute manipulation
650            if (!document.body) {
651                this.log('⚠️ DOM non prêt - report de l\'initialisation de la protection');
652                setTimeout(function() {
653                    self.initContentProtection();
654                }, 100);
655                return;
656            }
657            
658            // 🔒 Protection du contenu (cookie déjà vérifié en amont)
659            this.log('🔒 Activation de la protection du contenu - utilisateur non vérifié');
660            
661            // Masquer le contenu immédiatement
662            this.hideContent();
663            
664            // Démarrer la surveillance DOM
665            this.startDOMSurveillance();
666            
667            // Protection contre l'inspection (optionnel)
668            this.addInspectionProtection();
669            
670            this.log('Protection du contenu initialisée');
671        },
672        
673        /**
674         * 🔒 Masque le contenu de la page
675         */
676        hideContent: function() {
677            // Vérifier que le DOM est prêt
678            if (!document.body) {
679                this.log('⚠️ DOM non prêt - report du masquage du contenu');
680                var self = this;
681                setTimeout(function() {
682                    self.hideContent();
683                }, 100);
684                return;
685            }
686            
687            document.body.classList.add('avs-content-hidden');
688            
689            // Note: Le contenu est maintenant flouté (pas caché), pas besoin de message de protection systématique
690            
691            this.log('Contenu flouté - vérification d\'âge requise');
692        },
693        
694        /**
695         * 🔒 Révèle le contenu après vérification
696         */
697        showContent: function() {
698            // Vérifier que le DOM est prêt
699            if (!document.body) {
700                this.log('⚠️ DOM non prêt - report de la révélation du contenu');
701                var self = this;
702                setTimeout(function() {
703                    self.showContent();
704                }, 100);
705                return;
706            }
707            
708            document.body.classList.remove('avs-content-hidden');
709            
710            // Supprimer le message de protection
711            this.hideProtectionMessage();
712            
713            this.log('Contenu révélé - vérification réussie');
714        },
715        
716        /**
717         * 🔒 Affiche un message de protection
718         */
719        showProtectionMessage: function() {
720            // Vérifier que le DOM est prêt
721            if (!document.body) {
722                this.log('⚠️ DOM non prêt - report de l\'affichage du message de protection');
723                var self = this;
724                setTimeout(function() {
725                    self.showProtectionMessage();
726                }, 100);
727                return;
728            }
729            
730            // Éviter de créer plusieurs messages
731            if (document.getElementById('avs-protection-overlay')) {
732                return;
733            }
734            
735            var overlay = document.createElement('div');
736            overlay.id = 'avs-protection-overlay';
737            overlay.className = 'avs-protection-overlay';
738            
739            overlay.innerHTML = 
740                '<div class="avs-protection-message">' +
741                '<h2 style="margin-bottom: 15px; font-size: 24px;">🔒 Contenu Protégé</h2>' +
742                '<p style="margin-bottom: 15px; opacity: 0.9;">Ce site contient du contenu réservé aux adultes.</p>' +
743                '<p style="margin-bottom: 20px; opacity: 0.8;">Veuillez compléter la vérification d\'âge pour accéder au contenu.</p>' +
744                '<div style="animation: pulse 2s infinite;">⏳ Chargement de la vérification...</div>' +
745                '</div>';
746            
747            document.body.appendChild(overlay);
748        },
749        
750        /**
751         * 🔒 Masque le message de protection
752         */
753        hideProtectionMessage: function() {
754            var overlay = document.getElementById('avs-protection-overlay');
755            if (overlay) {
756                overlay.remove();
757            }
758        },
759        
760        /**
761         * 🔒 Surveillance DOM pour détecter la suppression du modal
762         */
763        startDOMSurveillance: function() {
764            var self = this;
765            
766            // Observer pour mutations DOM
767            if (window.MutationObserver) {
768                this.domObserver = new MutationObserver(function(mutations) {
769                    // Vérifier si le modal a été supprimé
770                    if (!document.getElementById('avs-modal') && 
771                        self.state.popupVisible && 
772                        self.getCookie('avs_verified') !== '1') {
773                        
774                        self.log('⚠️ Tentative de suppression du modal détectée !');
775                        self.handleSecurityBreach();
776                    }
777                });
778                
779                this.domObserver.observe(document.body, {
780                    childList: true,
781                    subtree: true
782                });
783            }
784            
785            // Surveillance par intervalle (fallback)
786            this.surveillanceInterval = setInterval(function() {
787                if (self.state.popupVisible && 
788                    !document.getElementById('avs-modal') && 
789                    self.getCookie('avs_verified') !== '1') {
790                    
791                    self.log('⚠️ Modal manquant détecté par surveillance !');
792                    self.handleSecurityBreach();
793                }
794            }, 1000);
795        },
796        
797        /**
798         * 🔒 Arrête la surveillance DOM
799         */
800        stopDOMSurveillance: function() {
801            if (this.domObserver) {
802                this.domObserver.disconnect();
803                this.domObserver = null;
804            }
805            
806            if (this.surveillanceInterval) {
807                clearInterval(this.surveillanceInterval);
808                this.surveillanceInterval = null;
809            }
810        },
811        
812        /**
813         * 🔒 Gestion des tentatives de contournement
814         */
815        handleSecurityBreach: function() {
816            // Masquer le contenu immédiatement
817            this.hideContent();
818            
819            // Afficher le message de protection seulement en cas de violation
820            this.showProtectionMessage();
821            
822            // Recréer le popup
823            this.showPopup();
824            
825            // Message d'avertissement (optionnel)
826            this.log('🚨 Breach de sécurité - rechargement de la vérification');
827            
828            // Optionnel: Rediriger ou recharger la page
829            // setTimeout(function() { location.reload(); }, 2000);
830        },
831        
832        /**
833         * 🔒 Protection contre l'inspection (optionnel)
834         */
835        addInspectionProtection: function() {
836            // Désactiver le clic droit
837            document.addEventListener('contextmenu', function(e) {
838                e.preventDefault();
839                return false;
840            });
841            
842            // Désactiver les raccourcis clavier de développement
843            document.addEventListener('keydown', function(e) {
844                // F12, Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+U
845                if (e.key === 'F12' || 
846                    (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'J')) ||
847                    (e.ctrlKey && e.key === 'U')) {
848                    e.preventDefault();
849                    return false;
850                }
851            });
852            
853            // Détecter l'ouverture des outils de développement
854            var devtools = {open: false, orientation: null};
855            var threshold = 160;
856            
857            setInterval(function() {
858                if (window.outerHeight - window.innerHeight > threshold || 
859                    window.outerWidth - window.innerWidth > threshold) {
860                    if (!devtools.open) {
861                        devtools.open = true;
862                        // Optionnel: Action lors de l'ouverture des devtools
863                        console.clear();
864                    }
865                } else {
866                    devtools.open = false;
867                }
868            }, 500);
869        },
870        
871        /**
872         * Injecte du CSS personnalisé dans la page
873         */
874        injectCustomCSS: function(customCSS) {
875            // Vérifier si un style tag pour le widget existe déjà
876            var existingStyleTag = document.getElementById('avs-custom-styles');
877            
878            if (existingStyleTag) {
879                // Mettre à jour le CSS existant
880                existingStyleTag.textContent = customCSS;
881            } else {
882                // Créer un nouveau style tag
883                var styleTag = document.createElement('style');
884                styleTag.id = 'avs-custom-styles';
885                styleTag.type = 'text/css';
886                styleTag.textContent = customCSS;
887                
888                // Ajouter au head du document
889                document.head.appendChild(styleTag);
890            }
891            
892            this.log('CSS personnalisé injecté:', customCSS.length + ' caractères');
893        },
894        
895        /**
896         * 🍪 Définit un cookie
897         */
898        setCookie: function(name, value, days) {
899            var expires = "";
900            if (days) {
901                var date = new Date();
902                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
903                expires = "; expires=" + date.toUTCString();
904            }
905            
906            // Déterminer le chemin optimal pour le cookie
907            var path = this.getCookiePath();
908            
909            // Créer le cookie avec tous les paramètres nécessaires
910            var cookieString = name + "=" + (value || "") + expires + 
911                              "; path=" + path + 
912                              "; SameSite=Lax";
913            
914            // Ajouter le domaine si on n'est pas sur localhost
915            if (window.location.hostname !== 'localhost' && 
916                window.location.hostname !== '127.0.0.1' && 
917                !window.location.hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) {
918                cookieString += "; domain=" + window.location.hostname;
919            }
920            
921            document.cookie = cookieString;
922            
923            this.log('🍪 Cookie défini: ' + name + '=' + value + ' [path=' + path + ']');
924        },
925        
926        /**
927         * 🍪 Récupère un cookie
928         */
929        getCookie: function(name) {
930            var nameEQ = name + "=";
931            var ca = document.cookie.split(';');
932            for (var i = 0; i < ca.length; i++) {
933                var c = ca[i];
934                while (c.charAt(0) === ' ') {
935                    c = c.substring(1, c.length);
936                }
937                if (c.indexOf(nameEQ) === 0) {
938                    return c.substring(nameEQ.length, c.length);
939                }
940            }
941            return null;
942        },
943        
944        /**
945         * 🍪 Détermine le chemin optimal pour les cookies
946         */
947        getCookiePath: function() {
948            // Si on est à la racine, utiliser /
949            if (window.location.pathname === '/' || window.location.pathname === '') {
950                return '/';
951            }
952            
953            // Si on est dans un sous-dossier, utiliser le chemin du dossier parent
954            var pathParts = window.location.pathname.split('/');
955            
956            // Enlever la dernière partie (nom du fichier)
957            pathParts.pop();
958            
959            // Si il reste des parties, joindre avec / et ajouter / à la fin
960            if (pathParts.length > 1) {
961                return pathParts.join('/') + '/';
962            }
963            
964            // Par défaut, utiliser /
965            return '/';
966        },
967        
968        /**
969         * 🍪 Supprime un cookie
970         */
971        eraseCookie: function(name) {
972            var path = this.getCookiePath();
973            
974            // Supprimer avec différents chemins pour être sûr
975            var pathsToTry = ['/', path];
976            
977            pathsToTry.forEach(function(p) {
978                document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=' + p + ';';
979            });
980            
981            this.log('🗑️ Cookie supprimé: ' + name);
982        },
983        
984        /**
985         * Log d'erreur
986         */
987        error: function() {
988            if (console && console.error) {
989                console.error.apply(console, ['[AgeVerificationWidget]'].concat(Array.prototype.slice.call(arguments)));
990            }
991        }
992    };
993    
994    // Exposition globale
995    window.AgeVerificationWidget = AgeVerificationWidget;
996    
997})(window, document);

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