'.esc_html__('Settings', 'admin-maintance-msg').''; } return $links; }); } /* ==================== Defaults: Single Source of Truth ==================== */ private static function defaults(): array { return [ // Admin Notice (bestehend) 'enabled' => 0, 'from' => '', 'to' => '', 'message' => '', // NEU: Rollensteuerung für Notice 'roles' => [], // Frontend Wartungsmodus // fe_mode: 'page' | 'custom' | 'template' 'fe_enabled' => 0, 'fe_bypass_logged' => 1, 'fe_mode' => 'page', 'fe_page_id' => 0, 'fe_html' => '', 'fe_template' => '', // z. B. 'default.html' ]; } /* ==================== Lifecycle ==================== */ public function on_activate(): void { if (!get_option(self::OPTION)) { $defaults = self::defaults(); $old = get_option(self::OPTION_OLD); if (is_array($old)) { add_option(self::OPTION, wp_parse_args($old, $defaults)); delete_option(self::OPTION_OLD); } else { add_option(self::OPTION, $defaults); } } } /* ==================== Helpers (Options, Dates, Weekday, Window) ==================== */ private function get_opts(): array { $opts = get_option(self::OPTION, []); return wp_parse_args($opts, self::defaults()); } private function parse_local_dt($dt): ?DateTime { if (!$dt) return null; $tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC'); $norm = str_replace(' ', 'T', $dt); $d = DateTime::createFromFormat('Y-m-d\TH:i', $norm, $tz); if ($d instanceof DateTime) return $d; try { return new DateTime($dt, $tz); } catch (Exception $e) { return null; } } private function weekday_de_short(DateTimeInterface $dt): string { $map = ['So','Mo','Di','Mi','Do','Fr','Sa']; return $map[(int)$dt->format('w')]; } private function weekday_de_long(DateTimeInterface $dt): string { $map = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag']; return $map[(int)$dt->format('w')]; } private function format_window_text(?DateTime $from, ?DateTime $to): string { if (!$from) return ''; $tzabbr = $from->format('T'); if ($to && $from->format('Y-m-d') !== $to->format('Y-m-d')) { return sprintf( '%s, %s, %s – %s, %s, %s (%s)', $this->weekday_de_short($from), $from->format('d.m.Y'), $from->format('H:i'), $this->weekday_de_short($to), $to->format('d.m.Y'), $to->format('H:i'), $tzabbr ); } return sprintf( '%s, %s, %s–%s (%s)', $this->weekday_de_long($from), $from->format('d.m.Y'), $from->format('H:i'), $to ? $to->format('H:i') : $from->format('H:i'), $tzabbr ); } /* ==================== Admin Assets ==================== */ public function enqueue_admin_assets(): void { $css_url = plugin_dir_url(__FILE__) . 'assets/admin.css'; wp_register_style(self::HANDLE_CSS, $css_url, [], self::VERSION, 'all'); wp_enqueue_style(self::HANDLE_CSS); } /* ==================== Settings Page ==================== */ public function add_settings_page(): void { add_options_page( __('Wartungshinweis', 'admin-maintance-msg'), __('Wartungshinweis', 'admin-maintance-msg'), 'manage_options', self::PAGE_SLUG, [$this, 'render_settings_page'] ); } public function register_settings(): void { register_setting(self::OPTION, self::OPTION, [ 'type' => 'array', 'sanitize_callback' => [$this, 'sanitize_opts'], 'default' => self::defaults(), ]); add_settings_section('ammsg_section_main', '', '__return_null', self::OPTION); // Toggle Notice add_settings_field('enabled', __('Wartung aktiv (Admin-Notice)', 'admin-maintance-msg'), function() { $o = $this->get_opts(); ?> get_opts(); ?>

get_opts(); ?> get_opts(); ?>

get_opts(); global $wp_roles; $roles = is_object($wp_roles) ? $wp_roles->roles : []; ?>

get_opts(); ?>

get_opts(); ?> get_opts(); $mode = $o['fe_mode'] ?: 'page'; ?>

get_opts(); // Core-Funktion soll HTML zurückgeben, nicht direkt echo'n $select = wp_dropdown_pages([ 'name' => self::OPTION . '[fe_page_id]', 'echo' => 0, // <— wichtig: kein direktes Echo 'show_option_none' => __('— Keine Seite ausgewählt —', 'admin-maintance-msg'), 'option_none_value' => 0, 'selected' => (int) $o['fe_page_id'], ]); // Sicher ausgeben: Core-HTML darf nicht esc_html()’ed werden // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe Core HTML from wp_dropdown_pages() echo $select; echo '

' . esc_html__('Wird genutzt, wenn „WordPress-Seite“ gewählt ist.', 'admin-maintance-msg') . '

'; }, self::OPTION, 'ammsg_section_main' ); // Frontend: Template add_settings_field('fe_template', __('Template-Datei (assets)', 'admin-maintance-msg'), function () { $o = $this->get_opts(); $templates = $this->available_templates(); ?>

get_opts(); ?>

get_opts(); // Notice $out['enabled'] = !empty($input['enabled']) ? 1 : 0; $out['from'] = isset($input['from']) ? sanitize_text_field($input['from']) : ''; $out['to'] = isset($input['to']) ? sanitize_text_field($input['to']) : ''; $out['message'] = isset($input['message']) ? wp_kses_post($input['message']) : ''; foreach (['from','to'] as $k) { if ($out[$k] && !$this->parse_local_dt($out[$k])) { $out[$k] = ''; } } // Rollen $out['roles'] = isset($input['roles']) && is_array($input['roles']) ? array_values(array_map('sanitize_text_field', $input['roles'])) : []; // Frontend-Maintenance $out['fe_enabled'] = !empty($input['fe_enabled']) ? 1 : 0; $out['fe_bypass_logged'] = !empty($input['fe_bypass_logged']) ? 1 : 0; $mode = isset($input['fe_mode']) ? sanitize_text_field($input['fe_mode']) : 'page'; $out['fe_mode'] = in_array($mode, ['page','custom','template'], true) ? $mode : 'page'; $out['fe_page_id'] = isset($input['fe_page_id']) ? (int)$input['fe_page_id'] : 0; // Custom HTML – erlaubte Tags (erweiterbar) $allowed = [ 'a' => ['href'=>[], 'title'=>[], 'target'=>[], 'rel'=>[]], 'p' => [], 'div'=>['class'=>[], 'id'=>[]], 'span'=>['class'=>[], 'id'=>[]], 'h1'=>[], 'h2'=>[], 'h3'=>[], 'h4'=>[], 'h5'=>[], 'h6'=>[], 'ul'=>[], 'ol'=>[], 'li'=>[], 'strong'=>[], 'em'=>[], 'br'=>[], 'hr'=>[], 'img'=>['src'=>[], 'alt'=>[], 'width'=>[], 'height'=>[], 'loading'=>[]], ]; $out['fe_html'] = isset($input['fe_html']) ? wp_kses($input['fe_html'], $allowed) : ''; // Template-Datei $templates = array_keys($this->available_templates()); $tpl = isset($input['fe_template']) ? sanitize_text_field($input['fe_template']) : ''; $out['fe_template'] = in_array($tpl, $templates, true) ? $tpl : ''; return $out; } private function available_templates(): array { $dir = plugin_dir_path(__FILE__) . 'assets/maintenance/templates/'; if (!is_dir($dir)) return []; $files = glob($dir . '*.html'); $out = []; foreach ($files as $f) { $base = basename($f); $out[$base] = $base; } return $out; } public function render_settings_page(): void { if (!current_user_can('manage_options')) return; // Erfolgs-Notice sauber vor Hero ausgeben // Pfade zu (optionalen) Assets $hero_url = plugins_url('assets/hero.png', __FILE__); $promo_url = plugins_url('assets/codekeks.png', __FILE__); ?>

'; settings_errors('ammsg_notices', false, true); echo '
'; ?>

codekeks.de · Andreas Grzybowski', 'admin-maintance-msg'), esc_html( gmdate('Y') ), esc_url('https://codekeks.de') ); ?>

get_opts(); // Rollenfilter if (!empty($o['roles'])) { $user = wp_get_current_user(); $allow = false; foreach ((array)$o['roles'] as $r) { if (in_array($r, (array)$user->roles, true)) { $allow = true; break; } } if (!$allow) return; } $enabled = !empty($o['enabled']); $from = $this->parse_local_dt($o['from']); $to = $this->parse_local_dt($o['to']); $within = false; if ($from && $to) { $now = new DateTime('now', $from->getTimezone()); $within = ($now >= $from && $now <= $to); } if (!$enabled && !$within) return; $window_text = $this->format_window_text($from, $to); $repl = []; if ($from) { $repl['{WEEKDAY}'] = $this->weekday_de_long($from); $repl['{DATE}'] = $from->format('d.m.Y'); $repl['{FROM}'] = $from->format('H:i'); $repl['{DATETIME_FROM}'] = $from->format('d.m.Y H:i'); $repl['{TZ}'] = $from->format('T'); } if ($to) { $repl['{WEEKDAY_TO}'] = $this->weekday_de_long($to); $repl['{DATE_TO}'] = $to->format('d.m.Y'); $repl['{TO}'] = $to->format('H:i'); $repl['{DATETIME_TO}'] = $to->format('d.m.Y H:i'); } $raw = isset($o['message']) ? trim($o['message']) : ''; if ($raw === '' && $from && $to) { $message = sprintf( /* translators: 1: date (dd.mm.yyyy), 2: start time (HH:MM), 3: end time (HH:MM) */ __('Wir führen am %1$s zwischen %2$s und %3$s planmäßige Wartungsarbeiten durch. Innerhalb dieses Zeitraums steht die Website nicht zur Verfügung.', 'admin-maintance-msg'), $from->format('d.m.Y'), $from->format('H:i'), $to->format('H:i') ); if ($window_text) { $message .= ' – ' . $window_text; } } else { $message = $raw !== '' ? $raw : __('Geplante Wartungsarbeiten.', 'admin-maintance-msg'); if ($repl) { $message = strtr($message, $repl); } if ($window_text && !preg_match('/\{(WEEKDAY|DATE|FROM|TO|TZ|DATETIME_FROM|DATETIME_TO|WEEKDAY_TO|DATE_TO)\}/', $raw)) { $message .= ' – ' . $window_text; } } ?> get_opts(); // Aktiv, wenn manuell enabled ODER wenn jetzt im Zeitfenster $active = !empty($o['fe_enabled']); $from = $this->parse_local_dt($o['from'] ?? ''); $to = $this->parse_local_dt($o['to'] ?? ''); if (!$active && $from && $to) { $now = new DateTime('now', $from->getTimezone()); $active = ($now >= $from && $now <= $to); } if (!$active) return; // Bypässe if (defined('REST_REQUEST') && REST_REQUEST) return; if (wp_doing_ajax()) return; if (wp_doing_cron()) return; if (!empty($o['fe_bypass_logged']) && is_user_logged_in()) return; // SEO / Header status_header(503); header('Retry-After: 3600'); add_action('wp_head', function(){ echo '' . "\n"; }); $site_name = get_bloginfo('name'); $window_text = $this->format_window_text($from, $to); // Admin-Text als Basis nutzen (klingt konsistent) $message = ''; $raw = isset($o['message']) ? trim($o['message']) : ''; if ($raw === '' && $from && $to) { $message = sprintf( /* translators: 1: date (dd.mm.yyyy), 2: start time (HH:MM), 3: end time (HH:MM) */ __('Wir führen am %1$s zwischen %2$s und %3$s planmäßige Wartungsarbeiten durch. Innerhalb dieses Zeitraums steht die Website nicht zur Verfügung.', 'admin-maintance-msg'), $from->format('d.m.Y'), $from->format('H:i'), $to->format('H:i') ); if ($window_text) { $message .= ' – ' . $window_text; } } else { $message = $raw !== '' ? $raw : __('Wir verbessern gerade die Website. In Kürze sind wir wieder für Sie da.', 'admin-maintance-msg'); if ($from) { $message = strtr($message, [ '{WEEKDAY}' => $this->weekday_de_long($from), '{DATE}' => $from->format('d.m.Y'), '{FROM}' => $from->format('H:i'), '{TZ}' => $from->format('T'), '{DATETIME_FROM}' => $from->format('d.m.Y H:i'), ]); } if ($to) { $message = strtr($message, [ '{WEEKDAY_TO}' => $this->weekday_de_long($to), '{DATE_TO}' => $to->format('d.m.Y'), '{TO}' => $to->format('H:i'), '{DATETIME_TO}'=> $to->format('d.m.Y H:i'), ]); } if ($window_text && !preg_match('/\{(WEEKDAY|DATE|FROM|TO|TZ|DATETIME_FROM|DATETIME_TO|WEEKDAY_TO|DATE_TO)\}/', $raw)) { $message .= ' – ' . $window_text; } } // Kontext zusammenbauen // Kontext zusammenbauen (UNESCAPED Rohwerte) $ctx = [ /* translators: maintance at saved date format, see http://php.net/date */ 'title' => sprintf(__('Wartungsmodus – %s', 'admin-maintance-msg'), $site_name), 'message' => wp_strip_all_tags($message, true), 'site_name' => $site_name, 'window_text' => $window_text, 'from' => $from ? $from->format('H:i') : '', 'to' => $to ? $to->format('H:i') : '', 'date' => $from ? $from->format('d.m.Y') : '', 'date_to' => $to ? $to->format('d.m.Y') : '', 'tz' => $from ? $from->format('T') : '', 'datetime_from' => $from ? $from->format('d.m.Y H:i') : '', 'datetime_to' => $to ? $to->format('d.m.Y H:i') : '', 'asset_url' => plugin_dir_url(__FILE__) . 'assets/maintenance/', ]; // Werte für Text-Kontext escapen und als Platzhalter-Replacements vorbereiten $repl = $this->build_replacements($ctx); $mode = $o['fe_mode'] ?: 'page'; // 1) Template-Datei aus assets if ($mode === 'template' && !empty($o['fe_template'])) { $this->render_template_file($o['fe_template'], $repl); exit; } // 2) WordPress-Seite if ($mode === 'page' && (int)$o['fe_page_id'] > 0) { $post = get_post((int)$o['fe_page_id']); if ($post && $post->post_status === 'publish') { $title = apply_filters('the_title', $post->post_title, $post->ID); $content = apply_filters('the_content', $post->post_content); echo ''.esc_html($title ?: $site_name).''; echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; exit; } } // 3) Custom HTML (bereits via wp_kses gefiltert) if ($mode === 'custom' && trim((string)$o['fe_html']) !== '') { echo ''.esc_html($ctx['title']).''; echo $o['fe_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; exit; } // 4) Fallback: Standard-Template $this->render_template_file('default.html', $repl); exit; } private function build_replacements(array $ctx): array { return [ '{{TITLE}}' => isset($ctx['title']) ? esc_html($ctx['title']) : '', '{{MESSAGE}}' => isset($ctx['message']) ? esc_html($ctx['message']) : '', '{{SITE_NAME}}' => isset($ctx['site_name']) ? esc_html($ctx['site_name']) : esc_html(get_bloginfo('name')), '{{WINDOW}}' => isset($ctx['window_text']) ? esc_html($ctx['window_text']) : '', '{{FROM}}' => isset($ctx['from']) ? esc_html($ctx['from']) : '', '{{TO}}' => isset($ctx['to']) ? esc_html($ctx['to']) : '', '{{DATE}}' => isset($ctx['date']) ? esc_html($ctx['date']) : '', '{{DATE_TO}}' => isset($ctx['date_to']) ? esc_html($ctx['date_to']) : '', '{{TZ}}' => isset($ctx['tz']) ? esc_html($ctx['tz']) : '', '{{DATETIME_FROM}}' => isset($ctx['datetime_from']) ? esc_html($ctx['datetime_from']) : '', '{{DATETIME_TO}}' => isset($ctx['datetime_to']) ? esc_html($ctx['datetime_to']) : '', '{{ASSET_URL}}' => isset($ctx['asset_url']) ? esc_url($ctx['asset_url']) : esc_url(plugin_dir_url(__FILE__) . 'assets/maintenance/'), ]; } private function render_template_file(string $template_file, array $repl): void { $base_dir = plugin_dir_path(__FILE__) . 'assets/maintenance/templates/'; $base_real = realpath($base_dir); $real = $base_dir . $template_file; // Sicherheit: nur im Templates-Verzeichnis erlauben if (!$base_real || !is_string($template_file) || strpos(realpath($real) ?: '', $base_real) !== 0 || !is_readable($real)) { // einfacher Fallback $title = $repl['{{TITLE}}'] ?? 'Maintenance'; $msg = $repl['{{MESSAGE}}'] ?? ''; $win = $repl['{{WINDOW}}'] ?? ''; $charset = esc_attr(get_bloginfo('charset')); echo ''.esc_html($title).''; echo '

'.esc_html($title).'

'; if ($msg) echo '

'.esc_html($msg).'

'; if ($win) echo '

'.esc_html($win).'

'; echo ''; return; } $html = file_get_contents($real); if ($html === false) { $charset = esc_attr(get_bloginfo('charset')); echo 'Maintenance

Maintenance

'; return; } $out = strtr($html, $repl); echo $out; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } new AMMSG_Admin_Maintenance();