$secret_key, 'response' => $token ); $mgenius_ts_ip = (string) MGENIUS_Main::client_ip(); if ( '0.0.0.0' !== $mgenius_ts_ip ) { $mgenius_ts_body['remoteip'] = $mgenius_ts_ip; } // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Turnstile server-side verification: the token can only be validated by Cloudflare. Declared under External Services in readme.txt. $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'timeout' => 10, 'body' => $mgenius_ts_body, ]); if (is_wp_error($response)) { self::debug_log('Turnstile verification failed - ' . $response->get_error_message()); return $response; } $result = json_decode(wp_remote_retrieve_body($response), true); if (200 !== (int) wp_remote_retrieve_response_code($response) || !is_array($result) || !array_key_exists('success', $result)) { self::debug_log('Turnstile verification failed - HTTP ' . (int) wp_remote_retrieve_response_code($response)); return new WP_Error('mgenius_turnstile_unavailable', 'Turnstile verification unavailable'); } return true === $result['success']; } /** * Turnstile check of a public form: stops the request with a message * unless the token is valid. * * @param string $form_type 'notification' or 'visitor'. */ private static function require_turnstile($form_type) { $turnstile = self::is_turnstile_required($form_type); if (!$turnstile['required']) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by the calling AJAX handler. $token = isset($_POST['turnstile_token']) ? sanitize_text_field( wp_unslash( $_POST['turnstile_token'] ) ) : ''; if (empty($token)) { wp_send_json_error(['message' => __('Please complete the security verification', '1001web-maintenance-launch-pages')]); } $check = self::verify_turnstile($token, $turnstile['secret_key']); if (is_wp_error($check)) { wp_send_json_error(['message' => __('The security verification service could not be reached. Nothing was sent: please try again in a few minutes.', '1001web-maintenance-launch-pages')]); } if (true !== $check) { wp_send_json_error(['message' => __('Security verification failed. Please try again.', '1001web-maintenance-launch-pages')]); } } /** * Vérifier si Turnstile est requis pour un formulaire spécifique * @param string $form_type 'notification' ou 'visitor' * @return array ['required' => bool, 'secret_key' => string] */ private static function is_turnstile_required($form_type) { $settings = MGENIUS_Settings::load_fresh(); if (empty($settings['turnstile_enabled']) || empty($settings['turnstile_secret_key'])) { return ['required' => false, 'secret_key' => '']; } $required = false; if ($form_type === 'notification' && !empty($settings['turnstile_on_notification'])) { $required = true; } elseif ($form_type === 'visitor' && !empty($settings['turnstile_on_visitor'])) { $required = true; } return [ 'required' => $required, 'secret_key' => $settings['turnstile_secret_key'] ?? '' ]; } /** * AJAX: Toggle maintenance ON/OFF */ // TOGGLE — Active/désactive le mode maintenance // AJAX: wp_ajax_mgenius_toggle /** * État courant de la maintenance et compteurs de visites. * * Sert à resynchroniser l'interface sans rechargement : le compte à rebours * peut désactiver la maintenance côté serveur (countdown_auto_disable), et * l'écran d'administration affichait alors un état périmé jusqu'au prochain * rafraîchissement manuel. * * @return void */ public static function get_state() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } // Un compte a rebours echu se termine ici aussi, sans // attendre une visite publique : l'ecran affiche alors le vrai etat. MGENIUS_Main::maybe_end_countdown(); $settings = MGENIUS_Settings::load_fresh(); // Vraie source des compteurs (le nombre de visites vit // dans sa propre option) et vraie cle du mode de page. $stats = MGENIUS_Settings::get_stats(); wp_send_json_success([ 'enabled' => !empty($settings['maintenance_enabled']), 'countdown_enabled' => !empty($settings['countdown_enabled']), 'mode' => (isset($settings['page_mode']) && 'coming_soon' === $settings['page_mode']) ? 'coming_soon' : 'maintenance', 'stats' => [ 'enabled' => !empty($settings['stats_enabled']), 'visits' => (int) $stats['visits'], 'unique' => (int) $stats['unique'], ], ]); } /** * Verrou de transition occupe : une autre bascule est en * cours (le verrou d'une requete interrompue expire apres 5 minutes). * * @return string */ private static function busy_message() { return __('Another maintenance operation is in progress. Please try again in a few minutes.', '1001web-maintenance-launch-pages'); } /** * Refus de la maintenance sans acces direct aux fichiers : * WordPress utilise FTP ou SSH, qu'il demande des identifiants ou qu'ils * soient definis dans wp-config.php. * * @return string */ private static function no_direct_access_message() { return __('Maintenance mode was not switched on: WordPress does not write files directly on this server (it uses FTP or SSH). The plugin needs direct file access to suspend cache plugins safely. Your host can enable it, or you can define FS_METHOD as \'direct\' in wp-config.php if PHP is allowed to write to your WordPress files.', '1001web-maintenance-launch-pages'); } /** * Enregistre les reglages, ou arrete la requete en disant * pourquoi : un « enregistre » n'est jamais affiche pour rien. * * @param array $settings Reglages complets. */ private static function save_settings_or_fail($settings) { if (MGENIUS_Settings::save($settings)) { return; } if ('busy' === MGENIUS_Settings::last_save_error()) { wp_send_json_error(['message' => __('Your changes were not saved: another maintenance operation is in progress. Please try again in a few minutes.', '1001web-maintenance-launch-pages')]); } wp_send_json_error(['message' => __('Your changes could not be saved to the database. Please try again.', '1001web-maintenance-launch-pages')]); } public static function toggle() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } // Une seule entree ou sortie de maintenance a la fois : le // verrou est pris avant de lire et d'enregistrer l'etat. if (!MGENIUS_Main::begin_transition()) { wp_send_json_error(['message' => self::busy_message()]); } $settings = MGENIUS_Settings::load_fresh(); $was_enabled = !empty($settings['maintenance_enabled']); $will_enable = self::post_bool('enabled'); // Deja hors maintenance (deux onglets, barre d'outils // perimee) : aucune purge des caches du site en ligne, mais ce qu'une // maintenance precedente a laisse (reglages de cache coupes, regles // .htaccess retirees) est remis. if (!$was_enabled && !$will_enable) { $repaired = MGENIUS_Main::repair_offline_state(); MGENIUS_Main::end_transition(); $notices = MGENIUS_Main::cache_warning_messages(); if ($repaired && empty($notices)) { $notices[] = __('Maintenance mode was already off. The cache settings and .htaccess rules that an earlier maintenance had suspended were put back.', '1001web-maintenance-launch-pages'); } wp_send_json_success([ 'enabled' => false, 'health' => null, 'visitor' => null, 'baseline' => null, 'notice' => implode(' ', $notices), 'message' => __('Maintenance mode disabled', '1001web-maintenance-launch-pages'), ]); } // Sans acces direct aux fichiers, les plugins de cache ne // peuvent pas etre suspendus sans risque : la maintenance n'est pas // activee, et on le dit. if ($will_enable && !$was_enabled && !MGENIUS_Main::filesystem_ready()) { MGENIUS_Main::end_transition(); wp_send_json_error(['message' => self::no_direct_access_message()]); } // Relevé de référence AVANT de couper : le site est encore servi // normalement, c'est le seul moment où la mesure a du sens. if (!$was_enabled && $will_enable) { // « Put the site back in maintenance » après une casse : la // référence prise quand le site était sain est gardée. if (!self::post_bool('keep_baseline') || !MGENIUS_Healthcheck::keep_baseline()) { MGENIUS_Healthcheck::capture_baseline(); } } $settings['maintenance_enabled'] = $will_enable; // Geste manuel : la planification ne pilote plus cet etat. $settings['schedule_auto_on'] = false; // Activer la maintenance avec un compte a rebours deja // echu la faisait couper aussitot par la premiere visite publique. // Ce compte a rebours est termine : on le desactive et on le dit. $countdown_notice = ''; if ($will_enable && MGENIUS_Main::countdown_end_is_past($settings)) { $settings['countdown_enabled'] = false; $countdown_notice = __('Your countdown had already ended, so it was turned off. Maintenance stays on until you disable it or set a new countdown.', '1001web-maintenance-launch-pages'); } // Ecriture refusee par la base : on le dit, rien n'est // bascule (le verrou est libere a l'arret de la requete). self::save_settings_or_fail($settings); MGENIUS_Main::flush_all_caches(); MGENIUS_Main::end_transition(); $health = null; $visitor = null; // Juste après l'activation : ce que reçoit réellement un // visiteur anonyme (page, HTTP 503, Retry-After, non mise en cache), // vérifié depuis le serveur et affiché tout de suite. if (!$was_enabled && $settings['maintenance_enabled']) { $visitor = MGENIUS_Healthcheck::check_visitor_view(); } if ($was_enabled && !$settings['maintenance_enabled']) { self::send_notifications(); // Le site vient de redevenir public : on rejoue le relevé et on // compare. Une régression est signalée tout de suite, pas par le // client trois jours plus tard. $health = MGENIUS_Healthcheck::verify(); } // Problemes d'ecriture (.htaccess, fichiers de cache) affiches // tout de suite, en plus de l'avis dans l'administration. $notices = MGENIUS_Main::cache_warning_messages(); if ('' !== $countdown_notice) { array_unshift($notices, $countdown_notice); } wp_send_json_success([ 'enabled' => $settings['maintenance_enabled'], 'health' => $health, 'visitor' => $visitor, 'baseline' => $settings['maintenance_enabled'] ? MGENIUS_Healthcheck::baseline_summary() : null, 'notice' => implode(' ', $notices), 'message' => $settings['maintenance_enabled'] ? __('Maintenance mode enabled', '1001web-maintenance-launch-pages') : __('Maintenance mode disabled', '1001web-maintenance-launch-pages') ]); } /** * AJAX: Sauvegarde des paramètres */ // SAVE — Sauvegarde TOUS les settings (60+ champs) // AJAX: wp_ajax_mgenius_save // Chaque $_POST est sanitizé individuellement selon son type public static function save() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $mgenius_countdown_was_enabled = !empty($settings['countdown_enabled']); if ( isset( $_POST['page_mode'] ) ) { $raw_mode = sanitize_key( wp_unslash( $_POST['page_mode'] ) ); $settings['page_mode'] = in_array( $raw_mode, ['maintenance', 'coming_soon'], true ) ? $raw_mode : 'maintenance'; } if (isset($_POST['title'])) { $settings['title'] = sanitize_text_field( wp_unslash( $_POST['title'] ) ); } if (isset($_POST['subtitle'])) { $settings['subtitle'] = sanitize_text_field( wp_unslash( $_POST['subtitle'] ) ); } if (isset($_POST['message'])) { $settings['message'] = wp_kses_post( wp_unslash( $_POST['message'] ) ); } if (isset($_POST['logo_url'])) { $settings['logo_url'] = esc_url_raw( wp_unslash( $_POST['logo_url'] ) ); } if (isset($_POST['show_admin_button'])) { $settings['show_admin_button'] = self::post_bool('show_admin_button'); } if (isset($_POST['visitor_access_enabled'])) { $settings['visitor_access_enabled'] = self::post_bool('visitor_access_enabled'); } // A new (or removed) visitor password revokes the access already // granted with the old one: the cookies are signed with the visitor // token, which is renewed after the save. $mgenius_password_changed = false; if (isset($_POST['visitor_password'])) { $new_password = sanitize_text_field( wp_unslash( $_POST['visitor_password'] ) ); if (!empty($new_password) && !wp_check_password($new_password, $settings['visitor_password'])) { $settings['visitor_password'] = wp_hash_password($new_password); $mgenius_password_changed = true; } elseif (empty($new_password) && self::post_bool('clear_password')) { $mgenius_password_changed = !empty($settings['visitor_password']); $settings['visitor_password'] = ''; } } // Accès visiteur coché sans mot de passe : seul cet accès est désactivé, au lieu de faire // échouer tout l'enregistrement (ce qui bloquait aussi les autres réglages). // Le bouton dédié « Enregistrer l'accès » valide déjà côté client avec un message clair. if (!empty($settings['visitor_access_enabled']) && empty($settings['visitor_password'])) { $settings['visitor_access_enabled'] = false; } if (isset($_POST['visitor_access_duration'])) { $dur_raw = sanitize_text_field( wp_unslash( $_POST['visitor_access_duration'] ) ); if ($dur_raw === 'custom') { $days = isset($_POST['visitor_access_custom_days']) ? absint( wp_unslash( $_POST['visitor_access_custom_days'] ) ) : 0; $days = max(1, min(365, $days)); $settings['visitor_access_duration'] = $days * DAY_IN_SECONDS; } else { // Clamp 1 heure – 1 an. $settings['visitor_access_duration'] = max(3600, min(31536000, absint($dur_raw))); } } if (isset($_POST['rgpd_mode'])) { $settings['rgpd_mode'] = self::post_bool('rgpd_mode'); } if (isset($_POST['seo_503_enabled'])) { $settings['seo_503_enabled'] = self::post_bool('seo_503_enabled'); } if (isset($_POST['retry_after_hours'])) { // v2.9.180 — plafond cote serveur (7 jours, comme le champ du // formulaire) : un envoi direct ne peut plus fixer une valeur // aberrante. 0 = en-tete Retry-After desactive. $settings['retry_after_hours'] = min( 168, absint( wp_unslash( $_POST['retry_after_hours'] ) ) ); } if (isset($_POST['noindex_enabled'])) { $settings['noindex_enabled'] = self::post_bool('noindex_enabled'); } if (isset($_POST['show_powered_by'])) { $settings['show_powered_by'] = self::post_bool('show_powered_by'); } if (isset($_POST['notification_enabled'])) { $settings['notification_enabled'] = self::post_bool('notification_enabled'); } if (isset($_POST['notification_button_text'])) { $settings['notification_button_text'] = sanitize_text_field( wp_unslash( $_POST['notification_button_text'] ) ); } if (isset($_POST['notification_success_msg'])) { $settings['notification_success_msg'] = sanitize_text_field( wp_unslash( $_POST['notification_success_msg'] ) ); } if (isset($_POST['turnstile_enabled'])) { $settings['turnstile_enabled'] = self::post_bool('turnstile_enabled'); } if (isset($_POST['turnstile_site_key'])) { $settings['turnstile_site_key'] = sanitize_text_field( wp_unslash( $_POST['turnstile_site_key'] ) ); } if (isset($_POST['turnstile_secret_key'])) { $settings['turnstile_secret_key'] = sanitize_text_field( wp_unslash( $_POST['turnstile_secret_key'] ) ); } if (isset($_POST['turnstile_on_notification'])) { $settings['turnstile_on_notification'] = self::post_bool('turnstile_on_notification'); } if (isset($_POST['turnstile_on_visitor'])) { $settings['turnstile_on_visitor'] = self::post_bool('turnstile_on_visitor'); } if (isset($_POST['countdown_enabled'])) { $settings['countdown_enabled'] = self::post_bool('countdown_enabled'); } if (isset($_POST['countdown_date'])) { $settings['countdown_date'] = MGENIUS_Settings::clean_date( sanitize_text_field( wp_unslash( $_POST['countdown_date'] ) ) ); } if (isset($_POST['countdown_time'])) { $settings['countdown_time'] = MGENIUS_Settings::clean_time( sanitize_text_field( wp_unslash( $_POST['countdown_time'] ) ), '00:00' ); } if (isset($_POST['countdown_title'])) { $settings['countdown_title'] = sanitize_text_field( wp_unslash( $_POST['countdown_title'] ) ); } if (isset($_POST['countdown_end_text'])) { $settings['countdown_end_text'] = sanitize_text_field( wp_unslash( $_POST['countdown_end_text'] ) ); } if (isset($_POST['countdown_style'])) { $settings['countdown_style'] = sanitize_text_field( wp_unslash( $_POST['countdown_style'] ) ); } if (isset($_POST['countdown_show_days'])) { $settings['countdown_show_days'] = self::post_bool('countdown_show_days'); } if (isset($_POST['countdown_show_hours'])) { $settings['countdown_show_hours'] = self::post_bool('countdown_show_hours'); } if (isset($_POST['countdown_show_minutes'])) { $settings['countdown_show_minutes'] = self::post_bool('countdown_show_minutes'); } if (isset($_POST['countdown_show_seconds'])) { $settings['countdown_show_seconds'] = self::post_bool('countdown_show_seconds'); } if (isset($_POST['countdown_auto_disable'])) { $settings['countdown_auto_disable'] = self::post_bool('countdown_auto_disable'); } if (isset($_POST['lottie_enabled'])) { $settings['lottie_enabled'] = self::post_bool('lottie_enabled'); } if (isset($_POST['lottie_url'])) { $settings['lottie_url'] = esc_url_raw( wp_unslash( $_POST['lottie_url'] ) ); } if (isset($_POST['lottie_position'])) { $settings['lottie_position'] = sanitize_text_field( wp_unslash( $_POST['lottie_position'] ) ); } if (isset($_POST['lottie_size'])) { $settings['lottie_size'] = absint( wp_unslash( $_POST['lottie_size'] ) ); } // Decalages Lottie : envoyes par l'ecran, jamais enregistres. foreach (['lottie_offset_x', 'lottie_offset_y'] as $mgenius_off_key) { if (isset($_POST[$mgenius_off_key])) { $settings[$mgenius_off_key] = max(-300, min(300, intval(wp_unslash($_POST[$mgenius_off_key])))); } } if (isset($_POST['lottie_speed'])) { $settings['lottie_speed'] = floatval( wp_unslash( $_POST['lottie_speed'] ) ); } if (isset($_POST['lottie_loop'])) { $settings['lottie_loop'] = self::post_bool('lottie_loop'); } if (isset($_POST['lottie_autoplay'])) { $settings['lottie_autoplay'] = self::post_bool('lottie_autoplay'); } if (isset($_POST['video_bg_enabled'])) { $settings['video_bg_enabled'] = self::post_bool('video_bg_enabled'); } if (isset($_POST['video_bg_url'])) { $settings['video_bg_url'] = esc_url_raw( wp_unslash( $_POST['video_bg_url'] ) ); } if (isset($_POST['video_bg_opacity'])) { $settings['video_bg_opacity'] = min(100, max(10, absint( wp_unslash( $_POST['video_bg_opacity'] ) ))); } if (isset($_POST['video_bg_muted'])) { $settings['video_bg_muted'] = self::post_bool('video_bg_muted'); } if (isset($_POST['parallax_enabled'])) { $settings['parallax_enabled'] = self::post_bool('parallax_enabled'); } if (isset($_POST['parallax_intensity'])) { $settings['parallax_intensity'] = min(100, max(10, absint( wp_unslash( $_POST['parallax_intensity'] ) ))); } if (isset($_POST['stats_enabled'])) { $new_stats_enabled = self::post_bool('stats_enabled'); if ($new_stats_enabled && empty($settings['stats_enabled'])) { $settings['stats_start_date'] = current_time('mysql'); } $settings['stats_enabled'] = $new_stats_enabled; } if (isset($_POST['schedule_enabled'])) { $settings['schedule_enabled'] = self::post_bool('schedule_enabled'); } if (isset($_POST['schedule_start_date'])) { $settings['schedule_start_date'] = MGENIUS_Settings::clean_date( sanitize_text_field( wp_unslash( $_POST['schedule_start_date'] ) ) ); } if (isset($_POST['schedule_start_time'])) { $settings['schedule_start_time'] = MGENIUS_Settings::clean_time( sanitize_text_field( wp_unslash( $_POST['schedule_start_time'] ) ) ); } if (isset($_POST['schedule_end_date'])) { $settings['schedule_end_date'] = MGENIUS_Settings::clean_date( sanitize_text_field( wp_unslash( $_POST['schedule_end_date'] ) ) ); } if (isset($_POST['schedule_end_time'])) { $settings['schedule_end_time'] = MGENIUS_Settings::clean_time( sanitize_text_field( wp_unslash( $_POST['schedule_end_time'] ) ) ); } if (isset($_POST['schedule_timezone'])) { $settings['schedule_timezone'] = sanitize_text_field( wp_unslash( $_POST['schedule_timezone'] ) ); } $color_fields = [ 'bg_color_1', 'bg_color_2', 'title_color', 'message_color', 'button_color', 'container_bg_color' ]; foreach ($color_fields as $field) { if (isset($_POST[$field])) { $settings[$field] = sanitize_hex_color( wp_unslash( $_POST[$field] ) ); } } $opacity_fields = [ 'bg_color_1_opacity', 'bg_color_2_opacity', 'title_opacity', 'message_opacity', 'button_opacity', 'bg_overlay_opacity', 'container_bg_opacity', 'container_shadow_intensity' ]; foreach ($opacity_fields as $field) { if (isset($_POST[$field])) { $settings[$field] = min(100, max(0, absint( wp_unslash( $_POST[$field] ) ))); } } $bool_fields = [ 'bg_image_enabled', 'container_shadow_enabled', 'animation_enabled', 'rgpd_mode', 'seo_503_enabled', 'noindex_enabled', 'rest_content_block', 'sidebar_enabled', 'custom_bg_enabled', 'show_powered_by', 'purge_data_on_uninstall' ]; foreach ($bool_fields as $field) { if (isset($_POST[$field])) { $settings[$field] = self::post_bool($field); } } if (isset($_POST['bg_image_url'])) { $settings['bg_image_url'] = esc_url_raw( wp_unslash( $_POST['bg_image_url'] ) ); } if (isset($_POST['custom_bg_url'])) { $settings['custom_bg_url'] = esc_url_raw( wp_unslash( $_POST['custom_bg_url'] ) ); } $dimension_fields = [ 'container_max_width', 'container_padding', 'container_border_radius', 'animation_duration', 'title_size', 'message_size', 'logo_size' ]; foreach ($dimension_fields as $field) { if (isset($_POST[$field])) { $settings[$field] = absint( wp_unslash( $_POST[$field] ) ); } } if (isset($_POST['sidebar_position'])) { $settings['sidebar_position'] = in_array( sanitize_text_field( wp_unslash( $_POST['sidebar_position'] ) ), ['left', 'right'], true) ? sanitize_text_field( wp_unslash( $_POST['sidebar_position'] ) ) : 'right'; } if (isset($_POST['sidebar_tab_color'])) { $settings['sidebar_tab_color'] = sanitize_hex_color( wp_unslash( $_POST['sidebar_tab_color'] ) ); } $social_fields = ['social_facebook', 'social_twitter', 'social_instagram', 'social_linkedin', 'social_youtube', 'social_tiktok']; foreach ($social_fields as $field) { if (isset($_POST[$field])) { $settings[$field] = esc_url_raw( wp_unslash( $_POST[$field] ) ); } } if (isset($_POST['contact_email'])) { $settings['contact_email'] = sanitize_email( wp_unslash( $_POST['contact_email'] ) ); } if (isset($_POST['contact_phone'])) { $settings['contact_phone'] = sanitize_text_field( wp_unslash( $_POST['contact_phone'] ) ); } if (isset($_POST['contact_whatsapp'])) { $settings['contact_whatsapp'] = sanitize_text_field( wp_unslash( $_POST['contact_whatsapp'] ) ); } $allowed_fonts = array_keys(MGENIUS_Settings::get_fonts()); if (isset($_POST['title_font']) && in_array( sanitize_text_field( wp_unslash( $_POST['title_font'] ) ), $allowed_fonts, true)) { $settings['title_font'] = sanitize_text_field( wp_unslash( $_POST['title_font'] ) ); } if (isset($_POST['message_font']) && in_array( sanitize_text_field( wp_unslash( $_POST['message_font'] ) ), $allowed_fonts, true)) { $settings['message_font'] = sanitize_text_field( wp_unslash( $_POST['message_font'] ) ); } $allowed_animations = array_keys(MGENIUS_Settings::get_animations()); $animation_fields = ['logo_animation', 'text_animation', 'button_animation']; foreach ($animation_fields as $field) { if (isset($_POST[$field]) && in_array( sanitize_text_field( wp_unslash( $_POST[$field] ) ), $allowed_animations, true)) { $settings[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) ); } } // v2.9.174 — une seule façon de terminer la maintenance. // 1) La planification ne s'enregistre activée qu'avec un début ET une // fin valides (elle restait « activée » sans dates). // 2) Planification valide + compte à rebours : le compte à rebours // affiche la fin planifiée (une seule heure de fin, jamais deux). $mgenius_schedule_notice = ''; if (!empty($settings['schedule_enabled'])) { $mgenius_window = MGENIUS_Main::schedule_window($settings); if (!$mgenius_window) { $settings['schedule_enabled'] = false; $mgenius_schedule_notice = __('Scheduling is not active yet: choose a start and an end (the end after the start).', '1001web-maintenance-launch-pages'); } elseif (!empty($settings['countdown_enabled'])) { $mgenius_end = (new \DateTime('@' . $mgenius_window['end']))->setTimezone(wp_timezone()); // Date et heure seulement : le choix « désactiver // automatiquement » reste celui de l'utilisateur (une // maintenance allumée à la main n'est pas coupée en douce). $settings['countdown_date'] = $mgenius_end->format('Y-m-d'); $settings['countdown_time'] = $mgenius_end->format('H:i'); } } // A countdown whose end is already past is switched off, whatever its // previous state, and the screen says so. Saving a past date (a typo // in the year) during maintenance used to end the maintenance a few // seconds later; a save never ends it now. $mgenius_countdown_notice = ''; if (MGENIUS_Main::countdown_end_is_past($settings)) { $settings['countdown_enabled'] = false; $mgenius_countdown_notice = $mgenius_countdown_was_enabled ? __('The countdown was turned off: its end date and time are already past. Choose a future date and time.', '1001web-maintenance-launch-pages') : __('The countdown was not enabled: its end date and time are already past. Choose a future date and time.', '1001web-maintenance-launch-pages'); } self::save_settings_or_fail($settings); if ($mgenius_password_changed) { MGENIUS_Settings::rotate_visitor_token(); } wp_send_json_success([ 'message' => __('Settings saved successfully!', '1001web-maintenance-launch-pages'), 'countdown_enabled' => !empty($settings['countdown_enabled']), 'countdown_end' => MGENIUS_Admin::countdown_end_ms(), 'notice' => $mgenius_countdown_notice, 'schedule_notice' => $mgenius_schedule_notice, 'schedule_enabled' => !empty($settings['schedule_enabled']), 'countdown_date' => (string) ($settings['countdown_date'] ?? ''), 'countdown_time' => (string) ($settings['countdown_time'] ?? ''), ]); } /** * AJAX: Révoquer tous les accès visiteurs */ // ACCÈS VISITEUR — Gestion cache, tokens, stats public static function clear_visitor_cache() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } // Rotation du token = révocation de tous les cookies en cours. MGENIUS_Settings::rotate_visitor_token(); wp_send_json_success([ 'message' => __('All visitor access has been revoked!', '1001web-maintenance-launch-pages') ]); } /** * AJAX: Réinitialiser le design */ public static function reset_design() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $settings = MGENIUS_Settings::reset_design($settings); self::save_settings_or_fail($settings); wp_send_json_success([ 'message' => __('Design reset successfully!', '1001web-maintenance-launch-pages') ]); } /** * v2.9.176+ — Applique un style aux réglages. Seule logique partagée par * la grille Styles (apply_preset) et l'assistant (wizard_save) : mêmes * couleurs, même voile, mêmes médias. * * @param array $settings Réglages courants. * @param array $preset Style (MGENIUS_Settings::get_presets()). * @return array array( réglages modifiés, true si l'image perso est gardée ) */ private static function apply_preset_to_settings( $settings, $preset ) { // v2.9.174 — a color preset never removes the user's own background // image, video or animation (a photo preset still replaces them). $own_media = MGENIUS_Settings::own_media_keys($settings); $kept = array_intersect_key($settings, array_flip($own_media)); $preset_has_media = !empty($preset['bg_image_enabled']) || !empty($preset['video_bg_enabled']); $settings['bg_color_1'] = $preset['bg_color_1']; $settings['bg_color_2'] = $preset['bg_color_2']; $settings['title_color'] = $preset['title_color']; $settings['message_color'] = $preset['message_color']; $settings['button_color'] = $preset['button_color']; $settings['container_bg_color'] = $preset['container_bg_color']; $settings['container_bg_opacity'] = $preset['container_bg_opacity'] ?? 85; // Voile sur l'image de fond : valeur du preset, sinon le defaut. $settings['bg_overlay_opacity'] = isset($preset['bg_overlay_opacity']) ? (int) $preset['bg_overlay_opacity'] : 50; if (!empty($preset['bg_image_enabled'])) { $settings['bg_image_enabled'] = true; $settings['bg_image_url'] = $preset['bg_image_url'] ?? ''; $settings['video_bg_enabled'] = false; } else { $settings['bg_image_enabled'] = false; $settings['bg_image_url'] = ''; } if (!empty($preset['video_bg_enabled'])) { $settings['video_bg_enabled'] = true; $settings['video_bg_url'] = $preset['video_bg_url'] ?? ''; $settings['video_bg_opacity'] = $preset['video_bg_opacity'] ?? 30; $settings['bg_image_enabled'] = false; $settings['lottie_enabled'] = false; } else if (empty($preset['bg_image_enabled'])) { $settings['video_bg_enabled'] = false; } $settings['lottie_enabled'] = false; $settings['lottie_url'] = ''; if (!$preset_has_media) { foreach ($kept as $own_key => $own_value) { $settings[$own_key] = $own_value; } } return array( $settings, ( ! $preset_has_media && ! empty( $kept ) ) ); } /** * AJAX: Appliquer un preset de design */ // PRESETS — Application, sauvegarde, import/export public static function apply_preset() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $preset_key = sanitize_text_field( wp_unslash( $_POST['preset'] ?? '' ) ); $presets = MGENIUS_Settings::get_presets(); if (!isset($presets[$preset_key])) { wp_send_json_error(['message' => __('Preset not found', '1001web-maintenance-launch-pages')]); } $preset = $presets[$preset_key]; $settings = MGENIUS_Settings::load_fresh(); $mgenius_applied = self::apply_preset_to_settings($settings, $preset); $settings = $mgenius_applied[0]; self::save_settings_or_fail($settings); $mgenius_kept = $mgenius_applied[1]; wp_send_json_success([ 'message' => $mgenius_kept /* translators: %s: preset name */ ? sprintf(__('Style "%s" applied. Your own background image is kept.', '1001web-maintenance-launch-pages'), $preset['name']) /* translators: %s: name or value */ : sprintf(__('Preset "%s" applied!', '1001web-maintenance-launch-pages'), $preset['name']), 'preset' => $preset, // v2.9.174 — background state actually saved, so that the screen // shows it instead of clearing the user's own image. 'media' => [ 'bg_image_enabled' => !empty($settings['bg_image_enabled']), 'bg_image_url' => (string) ($settings['bg_image_url'] ?? ''), 'video_bg_enabled' => !empty($settings['video_bg_enabled']), 'video_bg_url' => (string) ($settings['video_bg_url'] ?? ''), 'lottie_enabled' => !empty($settings['lottie_enabled']), 'lottie_url' => (string) ($settings['lottie_url'] ?? ''), ], ]); } /** * AJAX: Sauvegarder les paramètres d'accès (whitelist/blacklist) */ // ACCÈS — Rôles, IPs, tokens public static function save_access() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); if (isset($_POST['whitelist_enabled'])) { $settings['whitelist_enabled'] = self::post_bool('whitelist_enabled'); } if (isset($_POST['whitelist_urls'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each line is decoded, then passed through sanitize_text_field() by sanitize_path_list(): sanitizing first would strip the %XX octets of non-ASCII addresses. $settings['whitelist_urls'] = MGENIUS_Settings::sanitize_path_list( wp_unslash( $_POST['whitelist_urls'] ) ); } if (isset($_POST['whitelist_ips'])) { $ips = sanitize_textarea_field( wp_unslash( $_POST['whitelist_ips'] ) ); $settings['whitelist_ips'] = array_filter(array_map('trim', explode("\n", $ips))); } if (isset($_POST['whitelist_roles'])) { $roles = is_array($_POST['whitelist_roles']) ? array_map('sanitize_text_field', wp_unslash($_POST['whitelist_roles'])) : []; $settings['whitelist_roles'] = array_map('sanitize_text_field', $roles); } if (isset($_POST['whitelist_user_agents'])) { $agents = sanitize_textarea_field( wp_unslash( $_POST['whitelist_user_agents'] ) ); $settings['whitelist_user_agents'] = array_filter(array_map('trim', explode("\n", $agents))); } if (isset($_POST['blacklist_enabled'])) { $settings['blacklist_enabled'] = self::post_bool('blacklist_enabled'); } if (isset($_POST['blacklist_ips'])) { $ips = sanitize_textarea_field( wp_unslash( $_POST['blacklist_ips'] ) ); $settings['blacklist_ips'] = array_filter(array_map('trim', explode("\n", $ips))); } if (isset($_POST['blacklist_user_agents'])) { $agents = sanitize_textarea_field( wp_unslash( $_POST['blacklist_user_agents'] ) ); $settings['blacklist_user_agents'] = array_filter(array_map('trim', explode("\n", $agents))); } if (isset($_POST['blacklist_message'])) { $settings['blacklist_message'] = sanitize_text_field( wp_unslash( $_POST['blacklist_message'] ) ); } if (isset($_POST['blacklist_logging'])) { $settings['blacklist_logging'] = self::post_bool('blacklist_logging'); } self::save_settings_or_fail($settings); wp_send_json_success([ 'message' => __('Access settings saved!', '1001web-maintenance-launch-pages') ]); } /** * AJAX: Vider les logs de la blacklist */ public static function clear_blacklist_logs() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } delete_option('mgenius_blacklist_logs'); wp_send_json_success([ 'message' => __('Logs cleared!', '1001web-maintenance-launch-pages') ]); } /** * AJAX: Refresh nonce visiteur (public) * Retourne un nonce frais pour contrer le cache mobile (bfcache, proxy) */ /** * Refresh visitor nonce (nopriv) * Intentionally accessible without nonce verification — this endpoint * serves to renew expired nonces for the frontend login/subscribe forms. */ public static function refresh_nonce() { header('Content-Type: application/json; charset=utf-8'); wp_send_json_success([ 'nonce' => wp_create_nonce('mgenius_visitor_nonce') ]); } /** * AJAX: Connexion visiteur (public) * Sécurisé avec nonce, hashage, cookie sécurisé et rate limiting */ // VISITEUR — Login par mot de passe + Turnstile public static function visitor_login() { header('Content-Type: application/json; charset=utf-8'); $ip = self::get_client_ip(); $ip_hash = md5($ip . 'mgenius_salt'); $transient_key = 'mgenius_login_attempts_' . $ip_hash; $lockout_key = 'mgenius_lockout_' . $ip_hash; $lockout_until = get_transient($lockout_key); if ($lockout_until) { $remaining = $lockout_until - time(); $minutes = (int) ceil($remaining / 60); wp_send_json_error([ /* translators: %d: number of minutes */ 'message' => sprintf(_n('Too many attempts. Try again in %d minute.', 'Too many attempts. Try again in %d minutes.', $minutes, '1001web-maintenance-launch-pages'), $minutes), 'locked' => true ]); } if (!wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mgenius_visitor_nonce')) { wp_send_json_error(['message' => __('Session expired, please refresh the page', '1001web-maintenance-launch-pages')]); } self::require_turnstile('visitor'); $password = sanitize_text_field( wp_unslash( $_POST['password'] ?? '' ) ); $settings = MGENIUS_Settings::load_fresh(); if (empty($password)) { wp_send_json_error(['message' => __('Please enter the password', '1001web-maintenance-launch-pages')]); } // Acces visiteur desactive (ou sans mot de passe) : le mot // de passe ne doit plus rien ouvrir, meme par un appel direct. if (empty($settings['visitor_access_enabled']) || empty($settings['visitor_password'])) { wp_send_json_error(['message' => __('Visitor access is disabled.', '1001web-maintenance-launch-pages')]); } if (wp_check_password($password, $settings['visitor_password'])) { delete_transient($transient_key); delete_transient($lockout_key); $duration = absint($settings['visitor_access_duration'] ?? 86400); if ($duration < 3600) { $duration = 86400; } $expiry = time() + $duration; $token = MGENIUS_Settings::get_visitor_token(); $cookie_hash = hash('sha256', $expiry . $token . SECURE_AUTH_KEY); $cookie_value = $expiry . '.' . $cookie_hash; setcookie('mgenius_visitor_access', $cookie_value, [ 'expires' => $expiry, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax' ]); wp_send_json_success([ 'message' => __('Access granted', '1001web-maintenance-launch-pages'), 'expires_in' => $duration ]); } else { $attempts = (int) get_transient($transient_key); $attempts++; set_transient($transient_key, $attempts, 15 * MINUTE_IN_SECONDS); if ($attempts >= 5) { $lockout_until = time() + (15 * MINUTE_IN_SECONDS); set_transient($lockout_key, $lockout_until, 15 * MINUTE_IN_SECONDS); delete_transient($transient_key); wp_send_json_error([ 'message' => __('Too many attempts. Try again in 15 minutes.', '1001web-maintenance-launch-pages'), 'locked' => true ]); } $remaining = 5 - $attempts; $message = __('Incorrect password', '1001web-maintenance-launch-pages'); if ($remaining <= 2) { $message .= ' ' . sprintf( /* translators: %d: number of login attempts left */ _n('(%d attempt left)', '(%d attempts left)', $remaining, '1001web-maintenance-launch-pages'), $remaining ); } wp_send_json_error(['message' => $message]); } } /** * Obtenir l'IP du client de manière sécurisée */ private static function get_client_ip() { // Source unique, non falsifiable : voir MGENIUS_Main::client_ip(). return MGENIUS_Main::client_ip(); } /** * AJAX: Créer un token d'accès */ public static function create_token() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } // Seuls les champs de l'ecran : nom et duree (l'usage // unique, la restriction IP et l'envoi par courriel n'avaient pas // d'ecran et ne fonctionnaient pas). $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); $expiration = sanitize_text_field( wp_unslash( $_POST['expiration'] ?? '7d' ) ); $custom_date = sanitize_text_field( wp_unslash( $_POST['custom_date'] ?? '' ) ); if (empty($name)) { wp_send_json_error(['message' => __('Name is required', '1001web-maintenance-launch-pages')]); } $expires = self::calculate_expiration($expiration, $custom_date); $token = MGENIUS_Settings::create_access_token([ 'name' => $name, 'expires' => $expires, ]); if ($token === false) { $settings = MGENIUS_Settings::load_fresh(); $limit = intval($settings['custom_access_limit'] ?? 25); if ($limit > 0 && MGENIUS_Settings::count_active_tokens() >= $limit) { wp_send_json_error([ /* translators: %d: count/number */ 'message' => sprintf(__('Active token limit of %d reached. Delete expired tokens or increase the limit.', '1001web-maintenance-launch-pages'), $limit) ]); } wp_send_json_error([ 'message' => __('Token could not be saved to the database. Run the Diagnostic tab to investigate.', '1001web-maintenance-launch-pages') ]); } $access_url = home_url('?mgenius_access=' . $token); wp_send_json_success([ 'message' => __('Access created successfully!', '1001web-maintenance-launch-pages'), 'token' => $token, 'url' => $access_url, ]); } /** * Calculer la date d'expiration */ private static function calculate_expiration($expiration, $custom_date = '') { // time() et non current_time('timestamp') : wp_date() // applique deja le fuseau du site. L'ancien calcul le comptait deux // fois (un jeton « 1 h » expirait avant meme d'etre cree a Montreal). $now = time(); switch ($expiration) { case '1h': return wp_date('Y-m-d H:i:s', $now + HOUR_IN_SECONDS); case '6h': return wp_date('Y-m-d H:i:s', $now + 6 * HOUR_IN_SECONDS); case '24h': return wp_date('Y-m-d H:i:s', $now + DAY_IN_SECONDS); case '48h': return wp_date('Y-m-d H:i:s', $now + 2 * DAY_IN_SECONDS); case '7d': return wp_date('Y-m-d H:i:s', $now + 7 * DAY_IN_SECONDS); case '14d': return wp_date('Y-m-d H:i:s', $now + 14 * DAY_IN_SECONDS); case '30d': return wp_date('Y-m-d H:i:s', $now + 30 * DAY_IN_SECONDS); case '90d': return wp_date('Y-m-d H:i:s', $now + 90 * DAY_IN_SECONDS); case 'custom': if (!empty($custom_date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $custom_date)) { // Date choisie par l'utilisateur, deja dans le fuseau du site. return $custom_date . ' 23:59:59'; } return wp_date('Y-m-d H:i:s', $now + 7 * DAY_IN_SECONDS); default: return wp_date('Y-m-d H:i:s', $now + 7 * DAY_IN_SECONDS); } } /** * AJAX: Supprimer un token */ public static function delete_token() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $token = sanitize_text_field( wp_unslash( $_POST['token'] ?? '' ) ); if (empty($token)) { wp_send_json_error(['message' => __('Invalid token', '1001web-maintenance-launch-pages')]); } if (MGENIUS_Settings::delete_access_token($token)) { wp_send_json_success([ 'message' => __('Access deleted!', '1001web-maintenance-launch-pages'), 'stats' => MGENIUS_Settings::get_access_stats(), ]); } else { wp_send_json_error(['message' => __('Token not found', '1001web-maintenance-launch-pages')]); } } /** * AJAX: Récupérer tous les tokens */ public static function get_tokens() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $tokens = MGENIUS_Settings::get_access_tokens(); // Horodatages Unix reels : les dates des jetons sont en // heure du site, converties par local_to_timestamp(). $now = time(); $formatted = []; foreach ($tokens as $token => $data) { $is_expired = false; $expires_text = __('Never', '1001web-maintenance-launch-pages'); // « Unlimited » est enregistré au 2099-12-31 (admin.js) : affiché sans expiration. if (!empty($data['expires']) && '2099-12-31' !== substr((string) $data['expires'], 0, 10)) { $expires_time = MGENIUS_Settings::local_to_timestamp($data['expires']); $is_expired = $expires_time <= $now; $expires_text = wp_date('j M Y', $expires_time); } $last_visit_text = '-'; if (!empty($data['last_visit'])) { $last_visit_time = MGENIUS_Settings::local_to_timestamp($data['last_visit']); $diff = $now - $last_visit_time; if ($diff < HOUR_IN_SECONDS) { /* translators: %d: number of minutes */ $last_visit_text = sprintf(__('%d min ago', '1001web-maintenance-launch-pages'), floor($diff / 60)); } elseif ($diff < DAY_IN_SECONDS) { /* translators: %d: number of hours */ $last_visit_text = sprintf(__('%d h ago', '1001web-maintenance-launch-pages'), floor($diff / HOUR_IN_SECONDS)); } else { $last_visit_text = wp_date('j M', $last_visit_time); } } $formatted[] = [ 'token' => $token, 'name' => $data['name'], 'url' => home_url('?mgenius_access=' . $token), 'expires' => $expires_text, 'expires_raw' => $data['expires'] ?? '', 'is_expired' => $is_expired, 'visits' => $data['visits'] ?? 0, 'last_visit' => $last_visit_text, 'meta' => MGENIUS_Settings::token_meta_text($data), 'created' => $data['created'] ?? '', ]; } usort($formatted, function($a, $b) { if ($a['is_expired'] !== $b['is_expired']) { return $a['is_expired'] ? 1 : -1; } return strtotime($b['created']) - strtotime($a['created']); }); wp_send_json_success([ 'tokens' => $formatted, 'stats' => MGENIUS_Settings::get_access_stats(), ]); } /** * AJAX: Exporter les emails de notification en CSV */ public static function export_notification_emails() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $emails = MGENIUS_Settings::get_notification_emails(); if (empty($emails)) { wp_send_json_error(['message' => __('No emails to export', '1001web-maintenance-launch-pages')]); } $csv = "Email,Signup date\n"; foreach ($emails as $email_data) { $csv .= '"' . $email_data['email'] . '","' . $email_data['date'] . '"' . "\n"; } wp_send_json_success(['csv' => $csv]); } /** * AJAX: Vider les emails de notification */ public static function clear_notification_emails() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } if ( ! MGENIUS_Settings::set_notification_emails( [] ) ) { wp_send_json_error(['message' => __('Deletion failed. Please try again.', '1001web-maintenance-launch-pages')]); } wp_send_json_success(['message' => __('Emails deleted', '1001web-maintenance-launch-pages')]); } /** * AJAX: Inscription à la notification (publique) */ // NOTIFICATIONS EMAIL — Inscription visiteurs public static function subscribe_notification() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_visitor_nonce', 'nonce'); // The form only exists on the maintenance page: no address is // collected while it is switched off, or while the site is online // (otherwise anyone could queue addresses that the site would email // when maintenance ends). Administrators can still try it in the // preview. $settings = MGENIUS_Settings::load_fresh(); if (empty($settings['notification_enabled']) || (empty($settings['maintenance_enabled']) && !current_user_can('manage_options'))) { wp_send_json_error(['message' => __('Email notifications are not available at the moment.', '1001web-maintenance-launch-pages')]); } $visitor_ip = self::get_validated_visitor_ip(); if ( self::is_rate_limited( 'subscribe', $visitor_ip, 3, HOUR_IN_SECONDS ) ) { wp_send_json_error( [ 'message' => __( 'Too many requests. Please try again later.', '1001web-maintenance-launch-pages' ) ] ); } self::require_turnstile('notification'); $email = isset($_POST['email']) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; if (empty($email) || !is_email($email)) { wp_send_json_error(['message' => __('Invalid email address', '1001web-maintenance-launch-pages')]); } // Bug corrigé : « Merci ! » était envoyé même quand update_option échouait // silencieusement (object cache Redis). S3 : IP hashée, jamais stockée en clair. $mgenius_add_result = MGENIUS_Settings::add_notification_email( $email, $visitor_ip ); if ( 'error' === $mgenius_add_result ) { wp_send_json_error(['message' => __('Registration failed. Please try again.', '1001web-maintenance-launch-pages')]); } // 'added' et 'duplicate' → même réponse (pas de fuite d'info sur les inscrits). wp_send_json_success(['message' => $settings['notification_success_msg'] ?? __('Thank you! You will be notified.', '1001web-maintenance-launch-pages')]); } /** * Envoyer les notifications par email lors de la désactivation de la maintenance * * $emails : liste deja prise (et videe) par l'appelant sous le * verrou de transition (fin de plage planifiee) ; elle est alors envoyee * telle quelle. Sans argument : la liste enregistree, videe apres l'envoi. * * @param array|null $emails Abonnes a prevenir. */ public static function send_notifications($emails = null) { $from_list = !is_array($emails); if ($from_list) { $emails = MGENIUS_Settings::get_notification_emails(); } if (empty($emails)) { return; } $email_count = count($emails); self::debug_log('Manual deactivation - ' . $email_count . ' email(s) to send'); $site_name = get_bloginfo('name'); $site_url = home_url(); /* translators: %s: site name */ $subject = sprintf(__('🎉 %s is now online!', '1001web-maintenance-launch-pages'), $site_name); $message = sprintf( /* translators: 1: site name, 2: site URL */ __("Hello,\n\nGreat news! The site %1\$s is now online.\n\nYou can visit it right now:\n%2\$s\n\nThank you for your patience!\n\n---\nThis email was sent automatically because you requested to be notified.", '1001web-maintenance-launch-pages'), $site_name, $site_url ); $headers = [ 'Content-Type: text/plain; charset=UTF-8', 'From: ' . $site_name . ' <' . get_option('admin_email') . '>' ]; $sent_count = 0; $failed_count = 0; foreach ($emails as $email_data) { $email = $email_data['email'] ?? ''; if (!empty($email) && is_email($email)) { // Aucune adresse dans le journal (donnee personnelle). if (wp_mail($email, $subject, $message, $headers)) { $sent_count++; // Envoi reussi : compte dans le resume ci-dessous. } else { $failed_count++; self::debug_log('An email could not be sent'); } } } if ($from_list) { MGENIUS_Settings::set_notification_emails( [] ); } self::debug_log('Result - ' . $sent_count . ' sent, ' . $failed_count . ' failed'); } /** * Champs à sauvegarder dans un preset */ private static function get_preset_fields() { return MGENIUS_Settings::get_preset_fields(); } /** * AJAX: Sauvegarder un preset personnalisé */ // PRESETS CUSTOM — CRUD + Import/Export public static function save_custom_preset() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $preset_name = sanitize_text_field( wp_unslash( $_POST['preset_name'] ?? '' ) ); if (empty($preset_name)) { wp_send_json_error(['message' => __('Please enter a name for the preset', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $preset_data = []; foreach (self::get_preset_fields() as $field) { if (isset($settings[$field])) { $preset_data[$field] = $settings[$field]; } } $preset_id = 'preset_' . time() . '_' . wp_rand(1000, 9999); $custom_presets = $settings['custom_presets'] ?? []; $custom_presets[$preset_id] = [ 'name' => $preset_name, 'created' => current_time('mysql'), 'data' => $preset_data ]; $settings['custom_presets'] = $custom_presets; self::save_settings_or_fail($settings); wp_send_json_success([ /* translators: %s: name or value */ 'message' => sprintf(__('Preset "%s" saved!', '1001web-maintenance-launch-pages'), $preset_name), 'preset_id' => $preset_id, 'preset' => $custom_presets[$preset_id] ]); } /** * AJAX: Appliquer un preset personnalisé */ public static function apply_custom_preset() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $preset_id = sanitize_text_field( wp_unslash( $_POST['preset_id'] ?? '' ) ); if (empty($preset_id)) { wp_send_json_error(['message' => __('Preset ID missing', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $custom_presets = $settings['custom_presets'] ?? []; if (!isset($custom_presets[$preset_id])) { wp_send_json_error(['message' => __('Preset not found', '1001web-maintenance-launch-pages')]); } // Un preset ne transporte que du design : il repasse par le // filtre d'import (cles connues, types ramenes au defaut, aucun secret). // Design keys only: a preset saved by an older version, or imported, // never switches maintenance on, opens the whitelist or enables // visitor access. $preset_data = array_intersect_key( MGENIUS_Settings::sanitize_import( (array) ( $custom_presets[$preset_id]['data'] ?? [] ) ), array_flip( self::get_preset_fields() ) ); foreach ($preset_data as $key => $value) { $settings[$key] = $value; } self::save_settings_or_fail($settings); wp_send_json_success([ /* translators: %s: name or value */ 'message' => sprintf(__('Preset "%s" applied!', '1001web-maintenance-launch-pages'), $custom_presets[$preset_id]['name']), 'settings' => $preset_data ]); } /** * AJAX: Supprimer un preset personnalisé */ public static function delete_custom_preset() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $preset_id = sanitize_text_field( wp_unslash( $_POST['preset_id'] ?? '' ) ); if (empty($preset_id)) { wp_send_json_error(['message' => __('Preset ID missing', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $custom_presets = $settings['custom_presets'] ?? []; if (!isset($custom_presets[$preset_id])) { wp_send_json_error(['message' => __('Preset not found', '1001web-maintenance-launch-pages')]); } $preset_name = $custom_presets[$preset_id]['name']; unset($custom_presets[$preset_id]); $settings['custom_presets'] = $custom_presets; self::save_settings_or_fail($settings); wp_send_json_success([ /* translators: %s: name or value */ 'message' => sprintf(__('Preset "%s" deleted', '1001web-maintenance-launch-pages'), $preset_name) ]); } /** * AJAX: Exporter un preset personnalisé */ public static function export_custom_preset() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied', '1001web-maintenance-launch-pages')); } $preset_id = sanitize_text_field( wp_unslash( $_GET['preset_id'] ?? '' ) ); if (empty($preset_id)) { wp_die(esc_html__('Preset ID missing', '1001web-maintenance-launch-pages')); } $settings = MGENIUS_Settings::load_fresh(); $custom_presets = $settings['custom_presets'] ?? []; if (!isset($custom_presets[$preset_id])) { wp_die(esc_html__('Preset not found', '1001web-maintenance-launch-pages')); } $preset = $custom_presets[$preset_id]; $export_data = [ 'plugin' => 'maintenance-genius', 'version' => MGENIUS_VERSION, 'export_date' => current_time('mysql'), 'preset' => $preset ]; $filename = sanitize_file_name('mg-preset-' . $preset['name'] . '-' . gmdate('Y-m-d') . '.json'); header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Pragma: no-cache'); header('Expires: 0'); echo wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } /** * AJAX: Exporter tous les presets */ public static function export_all_presets() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied', '1001web-maintenance-launch-pages')); } $settings = MGENIUS_Settings::load_fresh(); $custom_presets = $settings['custom_presets'] ?? []; if (empty($custom_presets)) { wp_die(esc_html__('No presets to export', '1001web-maintenance-launch-pages')); } $export_data = [ 'plugin' => 'maintenance-genius', 'version' => MGENIUS_VERSION, 'export_date' => current_time('mysql'), 'presets_count' => count($custom_presets), 'presets' => $custom_presets ]; $filename = 'mg-all-presets-' . gmdate('Y-m-d') . '.json'; header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Pragma: no-cache'); header('Expires: 0'); echo wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } /** * AJAX: Importer un preset */ public static function import_custom_preset() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $file_content = self::read_uploaded_json('preset_file'); $import_data = json_decode($file_content, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_send_json_error(['message' => __('Invalid JSON file', '1001web-maintenance-launch-pages')]); } if (empty($import_data['plugin']) || $import_data['plugin'] !== 'maintenance-genius') { wp_send_json_error(['message' => __('This file is not a valid export from this plugin', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); $custom_presets = $settings['custom_presets'] ?? []; $imported_count = 0; if (isset($import_data['preset'])) { // Sanitization par champ : le fichier vient de l'exterieur et ses // valeurs finissent dans le CSS de la page publique. $preset = MGENIUS_Settings::sanitize_preset($import_data['preset']); if (empty($preset)) { wp_send_json_error(['message' => __('No usable preset found in the file.', '1001web-maintenance-launch-pages')]); } $preset_id = 'preset_' . time() . '_' . wp_rand(1000, 9999); $preset['name'] = $preset['name'] . ' (' . __('imported', '1001web-maintenance-launch-pages') . ')'; $preset['created'] = current_time('mysql'); $custom_presets[$preset_id] = $preset; $imported_count = 1; } elseif (isset($import_data['presets']) && is_array($import_data['presets'])) { foreach ($import_data['presets'] as $preset) { $preset = MGENIUS_Settings::sanitize_preset($preset); if (empty($preset)) { continue; } $preset_id = 'preset_' . time() . '_' . wp_rand(1000, 9999); $preset['name'] = $preset['name'] . ' (' . __('imported', '1001web-maintenance-launch-pages') . ')'; $preset['created'] = current_time('mysql'); $custom_presets[$preset_id] = $preset; $imported_count++; usleep(1000); } } else { wp_send_json_error(['message' => __('Unrecognized file format', '1001web-maintenance-launch-pages')]); } $settings['custom_presets'] = $custom_presets; self::save_settings_or_fail($settings); wp_send_json_success([ /* translators: %d: number of presets */ 'message' => sprintf(_n('%d preset imported successfully!', '%d presets imported successfully!', (int) $imported_count, '1001web-maintenance-launch-pages'), $imported_count), 'count' => $imported_count ]); } /** * AJAX: Upload fichier Lottie */ // UPLOAD LOTTIE — Fichier .json/.lottie public static function upload_lottie() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } if (!isset($_FILES['lottie_file']) || $_FILES['lottie_file']['error'] !== UPLOAD_ERR_OK) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $error_code = isset($_FILES['lottie_file']['error']) ? (int) $_FILES['lottie_file']['error'] : -1; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated /* translators: %d: PHP upload error code */ wp_send_json_error(['message' => sprintf(__('Upload error: code %d', '1001web-maintenance-launch-pages'), $error_code)]); } $file = $_FILES['lottie_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $filename = sanitize_file_name($file['name']); $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if (!in_array($ext, ['json', 'lottie'], true)) { wp_send_json_error(['message' => __('Unsupported format. Use .json or .lottie', '1001web-maintenance-launch-pages')]); } if (!is_uploaded_file($file['tmp_name'])) { wp_send_json_error(['message' => __('Error saving the file', '1001web-maintenance-launch-pages')]); } // A .lottie file is a ZIP archive: its content is checked, not only // its name (any file renamed .lottie used to be accepted). if ('lottie' === $ext) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Fichier televerse local (chemin temporaire PHP), pas une URL distante. $signature = file_get_contents($file['tmp_name'], false, null, 0, 4); $is_zip = ("PK\x03\x04" === $signature); if ($is_zip && class_exists('ZipArchive')) { $zip = new ZipArchive(); $is_zip = (true === $zip->open($file['tmp_name'])); if ($is_zip) { $zip->close(); } } if (!$is_zip) { wp_send_json_error(['message' => __('This .lottie file is not a valid Lottie animation (it should be a ZIP archive).', '1001web-maintenance-launch-pages')]); } } if ($ext === 'json') { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Fichier televerse local (chemin temporaire PHP), pas une URL distante. $content = file_get_contents($file['tmp_name']); $json = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_send_json_error(['message' => __('Invalid JSON file', '1001web-maintenance-launch-pages')]); } if (!isset($json['v']) || (!isset($json['layers']) && !isset($json['assets']))) { wp_send_json_error(['message' => __('This JSON file is not a valid Lottie animation', '1001web-maintenance-launch-pages')]); } } // wp_handle_upload() plutôt que move_uploaded_file() : on hérite des // contrôles de WordPress (type MIME réel, permissions, filtres, // compatibilité avec les hébergements exotiques). if (!function_exists('wp_handle_upload')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $mgenius_lottie_subdir = '/1001web-maintenance-launch-pages/lottie'; $mgenius_upload_filter = function ($dirs) use ($mgenius_lottie_subdir) { $dirs['subdir'] = $mgenius_lottie_subdir; $dirs['path'] = $dirs['basedir'] . $mgenius_lottie_subdir; $dirs['url'] = $dirs['baseurl'] . $mgenius_lottie_subdir; return $dirs; }; add_filter('upload_dir', $mgenius_upload_filter); add_filter('wp_check_filetype_and_ext', ['MGENIUS_Admin', 'fix_json_mime_type'], 10, 5); $moved = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => [ 'json' => 'application/json', 'lottie' => 'application/zip', ], ]); remove_filter('upload_dir', $mgenius_upload_filter); remove_filter('wp_check_filetype_and_ext', ['MGENIUS_Admin', 'fix_json_mime_type'], 10); if (!is_array($moved) || !empty($moved['error']) || empty($moved['url'])) { $mgenius_err = is_array($moved) && !empty($moved['error']) ? $moved['error'] : __('Error saving the file', '1001web-maintenance-launch-pages'); wp_send_json_error(['message' => $mgenius_err]); } $file_url = $moved['url']; // wp_upload_dir() est necessaire pour retrouver l'ancien fichier a // supprimer : la variable etait definie par l'ancien code base sur // move_uploaded_file(), retire depuis. Sans elle, PHP 8.3 emet un // warning qui pollue la reponse JSON — le navigateur affichait alors // « Connection error » au lieu du vrai message. $upload_dir = wp_upload_dir(); $settings = MGENIUS_Settings::load_fresh(); $old_url = $settings['lottie_url'] ?? ''; // Ancien fichier supprime seulement s'il est reellement // dans le dossier Lottie du plugin (chemin resolu, pas de « ../ »). if (!empty($old_url) && strpos($old_url, '/1001web-maintenance-launch-pages/lottie/') !== false) { $old_path = realpath(str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $old_url)); $lottie_rd = realpath($upload_dir['basedir'] . '/1001web-maintenance-launch-pages/lottie'); if ($old_path && $lottie_rd && 0 === strpos($old_path, $lottie_rd . DIRECTORY_SEPARATOR) && is_file($old_path)) { wp_delete_file( $old_path ); } } $settings['lottie_url'] = $file_url; self::save_settings_or_fail($settings); wp_send_json_success([ 'message' => __('Animation uploaded successfully!', '1001web-maintenance-launch-pages'), 'url' => $file_url, 'filename' => basename($moved['file']) ]); } /** * AJAX: Réinitialiser les statistiques */ // STATISTIQUES — Visites, tracking public static function reset_stats() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); MGENIUS_Settings::reset_stats(); $settings['stats_start_date'] = current_time('mysql'); self::save_settings_or_fail($settings); wp_send_json_success([ 'message' => __('Statistics reset successfully!', '1001web-maintenance-launch-pages') ]); } /** * AJAX: Tracker une visite - appelé par le frontend */ public static function track_visit() { check_ajax_referer('mgenius_visitor_nonce', 'nonce'); $settings = MGENIUS_Settings::load_fresh(); if (empty($settings['stats_enabled'])) { wp_send_json_error(['message' => __('Stats disabled', '1001web-maintenance-launch-pages')]); return; } // Remplace l'ancien code qui acceptait HTTP_CLIENT_IP sans validation (spoofable). $visitor_ip = self::get_validated_visitor_ip(); if ( empty( $visitor_ip ) ) { wp_send_json_error( [ 'message' => __( 'Invalid request', '1001web-maintenance-launch-pages' ) ] ); return; } if ( self::is_rate_limited( 'track', $visitor_ip, 30, MINUTE_IN_SECONDS ) ) { // Silencieux (pas d'erreur visible côté JS pour ne pas alerter le bot) wp_send_json_success( [ 'tracked' => false ] ); return; } // Compteur incrémenté de façon atomique dans son option, au lieu de réécrire les réglages // à chaque visite (élimine write amplification + lost updates sous charge). $ip_hash = md5($visitor_ip . wp_salt()); $stats = MGENIUS_Settings::record_visit($ip_hash); wp_send_json_success([ 'visits' => $stats['visits'], 'unique' => $stats['unique'], ]); } /** * AJAX: Exporter TOUTES les configurations */ // IMPORT/EXPORT CONFIG COMPLÈTE public static function export_all_settings() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); // Secrets et données personnelles jamais exportés : le fichier JSON est téléchargé par // l'utilisateur, joint aux tickets et déposé dans des sauvegardes. visitor_token vit dans une // option dédiée et n'est donc plus présent ici, mais on // le liste par sécurité pour les fichiers issus d'installs non migrées. $exclude_keys = [ 'stats_visitors_ips', 'notification_emails', 'visitor_token', 'visitor_password', 'turnstile_secret_key', 'schedule_auto_on', 'schedule_started', ]; foreach ($exclude_keys as $key) { unset($settings[$key]); } $export_data = [ 'plugin' => 'maintenance-genius', 'version' => MGENIUS_VERSION, 'export_date' => current_time('mysql'), 'site_url' => home_url(), 'settings' => $settings, // access_tokens EXCLU : la clé du tableau est le jeton secret // lui-même et la valeur contient le courriel du destinataire // (Loi 25 / RGPD). Un jeton est lié à un site et à une personne : // le transporter vers une autre install n'a aucun sens. 'wizard_completed' => get_option('mgenius_wizard_completed', false), ]; wp_send_json_success([ 'data' => $export_data, 'filename' => 'maintenance-genius-config-' . gmdate('Y-m-d') . '.json' ]); } /** * Lit un fichier JSON televerse, ou repond par une erreur. * Verifie le code d'erreur, l'origine (is_uploaded_file), l'extension * et la taille (1 Mo au plus) avant toute lecture. * * @param string $field Nom du champ de fichier. * @return string Contenu du fichier. */ private static function read_uploaded_json($field) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing -- Nonce verifie par l'appelant ; chemin temporaire et code d'erreur valides ci-dessous. if (!isset($_FILES[$field]['error'], $_FILES[$field]['tmp_name'], $_FILES[$field]['name'], $_FILES[$field]['size']) || UPLOAD_ERR_OK !== (int) $_FILES[$field]['error']) { wp_send_json_error(['message' => __('Error uploading the file', '1001web-maintenance-launch-pages')]); } $tmp = (string) $_FILES[$field]['tmp_name']; $name = sanitize_file_name(wp_unslash((string) $_FILES[$field]['name'])); $size = (int) $_FILES[$field]['size']; // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing if (!is_uploaded_file($tmp)) { wp_send_json_error(['message' => __('Error uploading the file', '1001web-maintenance-launch-pages')]); } if ('json' !== strtolower(pathinfo($name, PATHINFO_EXTENSION))) { wp_send_json_error(['message' => __('Invalid file format. Please upload a JSON file.', '1001web-maintenance-launch-pages')]); } if ($size <= 0 || $size > MB_IN_BYTES) { wp_send_json_error(['message' => __('The file is empty or larger than 1 MB.', '1001web-maintenance-launch-pages')]); } $content = file_get_contents($tmp); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Fichier local temporaire, pas une URL. return false === $content ? '' : $content; } /** * AJAX: Importer TOUTES les configurations */ public static function import_all_settings() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $content = self::read_uploaded_json('config_file'); $import_data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_send_json_error(['message' => __('Invalid JSON file', '1001web-maintenance-launch-pages')]); } if (empty($import_data['plugin']) || $import_data['plugin'] !== 'maintenance-genius') { wp_send_json_error(['message' => __('This file is not a valid configuration export from this plugin.', '1001web-maintenance-launch-pages')]); } if (empty($import_data['settings']) || !is_array($import_data['settings'])) { wp_send_json_error(['message' => __('No settings found in the export file.', '1001web-maintenance-launch-pages')]); } $current_settings = MGENIUS_Settings::load_fresh(); $preserve_keys = [ 'stats_visits', 'stats_unique_visitors', 'stats_visitors_ips', 'stats_start_date', 'notification_emails', // Secrets jamais lus depuis le fichier : on garde ceux // du site, sinon l'import les effacait (Turnstile coupe en silence). 'visitor_password', 'turnstile_secret_key', // L'import ne bascule jamais la maintenance : ce // passage se fait par l'interrupteur (caches, releve, abonnes). 'maintenance_enabled', 'schedule_auto_on', 'schedule_started', // Who may see the site during maintenance stays as set on this // site: an imported file never opens the whitelist. 'whitelist_enabled', 'whitelist_urls', 'whitelist_ips', 'whitelist_roles', 'whitelist_user_agents', ]; // Sanitization par champ : le JSON vient d'un fichier externe. $new_settings = MGENIUS_Settings::sanitize_import($import_data['settings']); if (empty($new_settings)) { wp_send_json_error(['message' => __('No usable settings found in the export file.', '1001web-maintenance-launch-pages')]); } foreach ($preserve_keys as $key) { if (isset($current_settings[$key])) { $new_settings[$key] = $current_settings[$key]; } } // Visitor access needs this site's password (never imported). if (!empty($new_settings['visitor_access_enabled']) && empty($new_settings['visitor_password'])) { $new_settings['visitor_access_enabled'] = false; } // A countdown already over is switched off: during maintenance it // would end the maintenance at the next visit. $mgenius_import_notice = ''; if (MGENIUS_Main::countdown_end_is_past($new_settings)) { $new_settings['countdown_enabled'] = false; $mgenius_import_notice = ' ' . __('The imported countdown was turned off: its end date and time are already past.', '1001web-maintenance-launch-pages'); } self::save_settings_or_fail($new_settings); // Les blocs ci-dessous entraient bruts en base : seul // 'settings' etait assaini. Un fichier trafique pouvait donc injecter // des valeurs arbitraires dans mg_cache_saved_states, qui servait // ensuite a reecrire les options d'autres plugins. // Chaque champ est desormais valide individuellement. // 'remote_config' (module de controle a distance retire) // n'est plus ni exporte ni importe : un ancien fichier le contenant // est accepte, la cle est simplement ignoree. // access_tokens N'EST PAS importé : la clé du tableau est le jeton // secret lui-même. Importer un fichier trafiqué injecterait des accès // valides créés par un tiers. Ils ne sont pas exportés non plus. // cache_saved_states N'EST PLUS importé : ce tableau contient les // reglages d'origine de plugins tiers (WP Rocket, WP Fastest Cache, // LiteSpeed...) et sert a les reecrire a la sortie de maintenance. // Importer un fichier trafique aurait permis d'ecraser la configuration // d'un autre plugin. Il est reconstruit localement a chaque activation // de la maintenance et n'a aucune raison de voyager entre deux sites. // Restaurer wizard flag if (isset($import_data['wizard_completed'])) { update_option('mgenius_wizard_completed', (bool) $import_data['wizard_completed']); } // Valeurs venant du fichier : assainies avant d'etre renvoyees. $source_site = !empty($import_data['site_url']) && is_string($import_data['site_url']) ? esc_url_raw($import_data['site_url']) : __('Unknown', '1001web-maintenance-launch-pages'); $export_date = !empty($import_data['export_date']) && is_string($import_data['export_date']) ? sanitize_text_field($import_data['export_date']) : __('Unknown', '1001web-maintenance-launch-pages'); $export_version = !empty($import_data['version']) && is_string($import_data['version']) ? sanitize_text_field($import_data['version']) : __('Unknown', '1001web-maintenance-launch-pages'); wp_send_json_success([ 'message' => sprintf( /* translators: 1: source site URL, 2: plugin version, 3: export date */ __('Configuration imported successfully! Source: %1$s (v%2$s, exported on %3$s)', '1001web-maintenance-launch-pages'), $source_site, $export_version, $export_date ) . $mgenius_import_notice ]); } /** * Rejoue la comparaison avant/après (carte « Last update check »). */ public static function health_recheck() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $settings = MGENIUS_Settings::load_fresh(); if (!empty($settings['maintenance_enabled'])) { wp_send_json_error(['message' => __('Switch maintenance off first: the comparison checks the site as visitors see it.', '1001web-maintenance-launch-pages')]); } wp_send_json_success(['health' => MGENIUS_Healthcheck::verify()]); } /** * v2.9.181 — Rouvre l'assistant au prochain chargement de la page. * Ne touche à aucun réglage : seul l'assistant terminé (« Launch » ou * « Save without activating ») enregistre ses choix. */ public static function wizard_reset() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } delete_option('mgenius_wizard_completed'); wp_send_json_success(); } // WIZARD — Configuration initiale public static function wizard_save() { check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } if (!empty($_POST['skip'])) { update_option('mgenius_wizard_completed', true); wp_send_json_success(); return; } // Activation : le verrou de transition est pris AVANT de // lire les reglages (une bascule concurrente n'est plus defaite par // un etat lu avant elle). $mgenius_activate = !empty($_POST['activate']); if ($mgenius_activate && !MGENIUS_Main::begin_transition()) { wp_send_json_error(['message' => self::busy_message()]); } $settings = MGENIUS_Settings::load_fresh(); if (isset($_POST['title'])) { $settings['title'] = sanitize_text_field(wp_unslash($_POST['title'])); } if (isset($_POST['message'])) { $settings['message'] = sanitize_textarea_field(wp_unslash($_POST['message'])); } if (!empty($_POST['preset'])) { $presets = MGENIUS_Settings::get_presets(); $preset_key = sanitize_text_field( wp_unslash( $_POST['preset'] ) ); if (isset($presets[$preset_key])) { // Même logique que la grille Styles (voile, vidéo, animation compris). $mgenius_applied = self::apply_preset_to_settings($settings, $presets[$preset_key]); $settings = $mgenius_applied[0]; } } // Credit facultatif, decoche par defaut : consentement explicite // recueilli a l'etape 4 de l'assistant (Guideline 10). $settings['show_powered_by'] = !empty($_POST['show_powered_by']); // Same rule as the settings: a new password revokes the visitor // access granted with the previous one (token renewed below). $mgenius_password_changed = false; if (!empty($_POST['visitor_password'])) { $mgenius_new_password = sanitize_text_field(wp_unslash($_POST['visitor_password'])); if (!wp_check_password($mgenius_new_password, (string) $settings['visitor_password'])) { $settings['visitor_password'] = wp_hash_password($mgenius_new_password); $mgenius_password_changed = true; } } // Meme regle que les reglages : pas d'acces visiteur sans // mot de passe (le controle du navigateur ne suffit pas). $settings['visitor_access_enabled'] = !empty($_POST['visitor_enabled']) && !empty($settings['visitor_password']); if ($mgenius_activate) { // Meme regles que l'interrupteur : pas de maintenance sans // acces direct aux fichiers, et une seule transition a la fois. if (empty($settings['maintenance_enabled']) && !MGENIUS_Main::filesystem_ready()) { MGENIUS_Main::end_transition(); wp_send_json_error(['message' => self::no_direct_access_message()]); } // Meme parcours que l'interrupteur : releve de // reference avant la coupure, pour que la comparaison avant/apres // fonctionne aussi apres une activation depuis l'assistant. if (empty($settings['maintenance_enabled'])) { MGENIUS_Healthcheck::capture_baseline(); } $settings['maintenance_enabled'] = true; $settings['schedule_auto_on'] = false; if (MGENIUS_Main::countdown_end_is_past($settings)) { $settings['countdown_enabled'] = false; } } // Meme ecriture que les autres reglages : hors activation, // l'etat de la maintenance reste celui de la base. self::save_settings_or_fail($settings); update_option('mgenius_wizard_completed', true); if ($mgenius_password_changed) { MGENIUS_Settings::rotate_visitor_token(); } $visitor = null; $settings = MGENIUS_Settings::load_fresh(); if (!empty($settings['maintenance_enabled'])) { MGENIUS_Main::flush_all_caches(); } MGENIUS_Main::end_transition(); // Meme preuve que l'interrupteur : ce que recoit un // visiteur, affiche apres le rechargement de la page. if ($mgenius_activate && !empty($settings['maintenance_enabled'])) { $visitor = MGENIUS_Healthcheck::check_visitor_view(); } wp_send_json_success([ 'activated' => !empty($_POST['activate']), 'visitor' => $visitor, ]); } // THÈMES COMPLETS — Application + Annulation /** * AJAX: Appliquer un thème complet */ public static function apply_theme_handler() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } $theme_id = sanitize_key(wp_unslash($_POST['theme_id'] ?? '')); $themes = MGENIUS_Settings::get_themes(); if (!isset($themes[$theme_id])) { wp_send_json_error(['message' => __('Theme not found', '1001web-maintenance-launch-pages')]); } if (!MGENIUS_Settings::apply_theme($theme_id)) { if ('busy' === MGENIUS_Settings::last_save_error()) { wp_send_json_error(['message' => __('The theme was not applied: another maintenance operation is in progress. Please try again in a few minutes.', '1001web-maintenance-launch-pages')]); } wp_send_json_error(['message' => __('Theme could not be applied. Run the Diagnostic tab to investigate.', '1001web-maintenance-launch-pages')]); } // Toast spécifique Lancement si le countdown vient d\'être activé (aucune date pré-existante). $settings_after = MGENIUS_Settings::load_fresh(); $lancement_msg = ''; if ('lancement' === $theme_id && !empty($settings_after['countdown_enabled']) && empty($settings_after['countdown_date'])) { $lancement_msg = __('Theme applied — set your launch date in General > Countdown Timer (Expert mode).', '1001web-maintenance-launch-pages'); } /* translators: %s: theme label */ $default_msg = sprintf(__('Theme "%s" applied! Your texts and advanced settings are preserved.', '1001web-maintenance-launch-pages'), esc_html($themes[$theme_id]['label'])); wp_send_json_success([ 'message' => $lancement_msg ?: $default_msg, 'theme_id' => $theme_id, 'has_snapshot' => true, ]); } /** * AJAX: Annuler le dernier thème appliqué (restaure le snapshot) */ public static function revert_theme_handler() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } if (!MGENIUS_Settings::revert_theme()) { if ('busy' === MGENIUS_Settings::last_save_error()) { wp_send_json_error(['message' => __('The theme was not reverted: another maintenance operation is in progress. Please try again in a few minutes.', '1001web-maintenance-launch-pages')]); } wp_send_json_error(['message' => __('No theme snapshot found. Nothing to revert.', '1001web-maintenance-launch-pages')]); } wp_send_json_success([ 'message' => __('Your design from before the themes has been restored.', '1001web-maintenance-launch-pages'), 'has_snapshot' => false, ]); } /** * Récupère l'IP visiteur validée. * * Délègue à MGENIUS_Main::client_ip() : REMOTE_ADDR, et les en-têtes de * proxy seulement si la connexion vient d'un proxy de confiance * (Cloudflare par défaut, filtre mgenius_trusted_proxies). * * @return string IP validée (IPv4 ou IPv6), ou chaîne vide si invalide. */ private static function get_validated_visitor_ip() { // Source unique, non falsifiable : voir MGENIUS_Main::client_ip(). // L'ancienne version lisait X-Forwarded-For en premier et comptait sur // FILTER_VALIDATE_IP pour bloquer l'usurpation. Ce filtre verifie le // format d'une IP, pas son authenticite : une fausse IP bien formee // passait, et chaque tentative pouvait en changer. $ip = MGENIUS_Main::client_ip(); return ( '0.0.0.0' === $ip ) ? '' : $ip; } /** * Vérifie si une IP a dépassé un quota d'actions. * * Utilise les transients WP comme compteur sliding window. Sans dépendance externe. * * @param string $action Identifiant unique de l'action (ex: 'subscribe', 'track_visit'). * @param string $ip IP visiteur (déjà validée par get_validated_visitor_ip). * @param int $max Nombre max d'actions autorisées dans la période. * @param int $period Fenêtre de temps en secondes (ex: 3600 pour 1h). * @return bool true si rate-limited (BLOQUER), false si OK (laisser passer). */ private static function is_rate_limited( $action, $ip, $max, $period ) { if ( empty( $ip ) ) { // IP invalide = bloquer par sécurité return true; } $key = 'mgenius_rl_' . $action . '_' . md5( $ip ); $count = (int) get_transient( $key ); if ( $count >= $max ) { return true; } set_transient( $key, $count + 1, $period ); return false; } }