'.esc_html__('Settings', 'admin-maintenance-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-maintenance-msg'),
__('Wartungshinweis', 'admin-maintenance-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-maintenance-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-maintenance-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-maintenance-msg') .
'
';
},
self::OPTION,
'ammsg_section_main'
);
// Frontend: Template
add_settings_field('fe_template', __('Template-Datei (assets)', 'admin-maintenance-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; ?>
codekeks.de . Andreas Grzybowski · GitHub', 'admin-maintenance-msg'),
esc_url('https://codekeks.de'),
esc_url('https://github.com/antman313')
);
?>
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-maintenance-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-maintenance-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-maintenance-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-maintenance-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
$ctx = [
/* translators: 1: (site_name) */
'title' => sprintf(__('Wartungsmodus – %s', 'admin-maintenance-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') : '',
];
// Werte für Text-Kontext escapen und als Platzhalter-Replacements vorbereiten
$repl = $this->build_replacements([
'title' => esc_html($ctx['title']),
'message' => esc_html($ctx['message']),
'site_name' => esc_html($ctx['site_name']),
'window_text' => esc_html($ctx['window_text']),
'from' => esc_html($ctx['from']),
'to' => esc_html($ctx['to']),
'date' => esc_html($ctx['date']),
'date_to' => esc_html($ctx['date_to']),
'tz' => esc_html($ctx['tz']),
]);
$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}}' => $ctx['title'] ?? '',
'{{MESSAGE}}' => $ctx['message'] ?? '',
'{{SITE_NAME}}' => $ctx['site_name'] ?? get_bloginfo('name'),
'{{WINDOW}}' => $ctx['window_text'] ?? '',
'{{FROM}}' => $ctx['from'] ?? '',
'{{TO}}' => $ctx['to'] ?? '',
'{{DATE}}' => $ctx['date'] ?? '',
'{{DATE_TO}}' => $ctx['date_to'] ?? '',
'{{TZ}}' => $ctx['tz'] ?? '',
];
}
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 'MaintenanceMaintenance
';
return;
}
$out = strtr($html, $repl);
echo $out; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
new AMMSG_Admin_Maintenance();