Upload folder via GUI - inc

This commit is contained in:
Git Manager GUI
2026-04-13 18:52:46 +02:00
parent 9c47501712
commit 09ac38e9fa
31 changed files with 2058 additions and 0 deletions

348
inc/classes/class-admin.php Normal file
View File

@@ -0,0 +1,348 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Admin {
private static $instance=null;
public static function instance() {
if(is_null(self::$instance)){self::$instance=new self();self::$instance->hook();}
return self::$instance;
}
public function hook() {
add_action('admin_menu', array($this,'register_menu'));
add_action('admin_enqueue_scripts', array($this,'enqueue_assets'));
add_action('admin_init', array($this,'handle_actions'));
add_action('admin_notices', array($this,'admin_notices'));
}
public function register_menu() {
add_menu_page('WP Multi Formular','Formulare','manage_options','wp-multi-formular',
array($this,'page_router'),'dashicons-feedback',25);
add_submenu_page('wp-multi-formular','Alle Formulare','Alle Formulare','manage_options','wp-multi-formular',array($this,'page_router'));
add_submenu_page('wp-multi-formular','Neues Formular','+ Neues Formular','manage_options','wmf-new-form',array($this,'page_builder'));
// Neue Einreichungen zaehlen fuer Badge
$new_count = 0;
$forms = get_posts(array('post_type'=>'wmf-form','post_status'=>'publish','numberposts'=>-1,'fields'=>'ids'));
foreach($forms as $fid) $new_count += wmf_count_submissions($fid,'neu');
$badge = $new_count > 0 ? ' <span class="awaiting-mod update-plugins count-'.intval($new_count).'"><span class="pending-count">'.intval($new_count).'</span></span>' : '';
add_submenu_page('wp-multi-formular','Einreichungen','Einreichungen'.$badge,'manage_options','wmf-submissions',array($this,'page_submissions'));
}
public function page_router() {
// Einzelne Einreichung anzeigen
if(!empty($_GET['view_submission']) && !empty($_GET['form_id'])) {
$sid = intval($_GET['view_submission']);
$form_id = intval($_GET['form_id']);
require WMF_TPL.'admin/page-submission-detail.php';
return;
}
// Einreichung löschen
if(!empty($_GET['wmf_action'])&&$_GET['wmf_action']==='delete_submission'&&!empty($_GET['submission_id'])) {
if(wp_verify_nonce($_GET['_wpnonce']??'','wmf_delete_submission')) {
WMF_Submission::delete(intval($_GET['submission_id']));
$this->safe_redirect(add_query_arg('wmf_notice','deleted',admin_url('admin.php?page=wmf-submissions')));
}
}
// CSV Export
if(!empty($_GET['wmf_action'])&&$_GET['wmf_action']==='export_csv'&&!empty($_GET['form_id'])) {
WMF_Submissions_List::maybe_export(intval($_GET['form_id']));
}
// Einreichungen anzeigen
if(!empty($_GET['view_submissions'])) {
$form_id=intval($_GET['view_submissions']);
require WMF_TPL.'admin/page-submissions.php'; return;
}
// Builder (Bearbeiten)
if(!empty($_GET['edit'])) {
require WMF_TPL.'admin/page-builder.php'; return;
}
require WMF_TPL.'admin/page-forms-list.php';
}
public function page_builder() {
require WMF_TPL.'admin/page-builder.php';
}
public function page_submissions() {
// Einzelne Einreichung anzeigen
if(!empty($_GET['view_submission']) && !empty($_GET['form_id'])) {
$sid = intval($_GET['view_submission']);
$form_id = intval($_GET['form_id']);
$form = get_post($form_id);
?>
<div class="wrap wmf-admin-wrap">
<h1 class="wmf-page-title">
Einreichung #<?php echo intval($sid); ?>
<span style="font-size:14px;font-weight:normal;color:#646970;margin-left:8px;">
aus: <?php echo esc_html($form ? $form->post_title : ''); ?>
</span>
</h1>
<?php WMF_Submissions_List::render_single($sid, $form_id); ?>
</div>
<?php
return;
}
// Einreichungen eines Formulars anzeigen
if(!empty($_GET['form_id'])) {
$form_id = intval($_GET['form_id']);
$form = get_post($form_id);
WMF_Submissions_List::maybe_export($form_id);
?>
<div class="wrap wmf-admin-wrap">
<h1 class="wmf-page-title">
Einreichungen:
<span style="color:#646970;"><?php echo esc_html($form ? $form->post_title : ''); ?></span>
<a href="<?php echo esc_url(admin_url('admin.php?page=wmf-submissions')); ?>"
class="page-title-action">&larr; Alle Formulare</a>
</h1>
<?php WMF_Submissions_List::render($form_id); ?>
</div>
<?php
return;
}
// Übersicht aller Formulare mit Einreichungszahlen
?>
<div class="wrap wmf-admin-wrap">
<h1>Einreichungen</h1>
<?php
$forms = get_posts(array(
'post_type' => 'wmf-form',
'post_status' => 'publish',
'numberposts' => -1,
'orderby' => 'date',
'order' => 'DESC',
));
if(empty($forms)): ?>
<div class="wmf-empty-state">
<span class="dashicons dashicons-email-alt wmf-empty-icon"></span>
<h2>Noch keine Formulare vorhanden</h2>
<p>Erstellen Sie zuerst ein Formular und binden Sie es auf einer Seite ein.</p>
<a href="<?php echo esc_url(admin_url('admin.php?page=wmf-new-form')); ?>"
class="button button-primary button-hero">Formular erstellen</a>
</div>
<?php else: ?>
<table class="wp-list-table widefat fixed striped wmf-submissions-overview">
<thead>
<tr>
<th>Formular</th>
<th style="width:100px;text-align:center;">Gesamt</th>
<th style="width:80px;text-align:center;">Neu</th>
<th style="width:100px;text-align:center;">Gelesen</th>
<th style="width:110px;text-align:center;">Archiviert</th>
<th style="width:130px;text-align:center;">Letzte Einreichung</th>
<th style="width:120px;">Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach($forms as $form):
$total = wmf_count_submissions($form->ID);
$count_new = wmf_count_submissions($form->ID, 'neu');
$count_read = wmf_count_submissions($form->ID, 'gelesen');
$count_arch = wmf_count_submissions($form->ID, 'archiviert');
global $wpdb;
$last = $wpdb->get_var($wpdb->prepare(
"SELECT created_at FROM {$wpdb->prefix}wmf_submissions WHERE form_id=%d ORDER BY created_at DESC LIMIT 1",
$form->ID
));
$view_url = add_query_arg(array('page'=>'wmf-submissions','form_id'=>$form->ID), admin_url('admin.php'));
$export_url = WMF_Submissions_List::export_url($form->ID);
?>
<tr>
<td>
<strong>
<a href="<?php echo esc_url($view_url); ?>">
<?php echo esc_html($form->post_title); ?>
</a>
</strong>
<div style="font-size:11px;color:#646970;margin-top:2px;">
<?php echo esc_html(wmf_get_shortcode($form->ID)); ?>
</div>
</td>
<td style="text-align:center;">
<a href="<?php echo esc_url($view_url); ?>" style="font-size:18px;font-weight:700;color:#2271b1;text-decoration:none;">
<?php echo intval($total); ?>
</a>
</td>
<td style="text-align:center;">
<?php if($count_new > 0): ?>
<a href="<?php echo esc_url(add_query_arg('sub_status','neu',$view_url)); ?>"
style="display:inline-block;background:#2271b1;color:#fff;border-radius:10px;padding:2px 9px;font-size:12px;font-weight:700;text-decoration:none;">
<?php echo intval($count_new); ?>
</a>
<?php else: ?>
<span style="color:#a7aaad;">0</span>
<?php endif; ?>
</td>
<td style="text-align:center;color:#646970;"><?php echo intval($count_read); ?></td>
<td style="text-align:center;color:#a7aaad;"><?php echo intval($count_arch); ?></td>
<td style="text-align:center;font-size:12px;color:#646970;">
<?php echo $last ? esc_html(date_i18n('d.m.Y H:i', strtotime($last))) : '—'; ?>
</td>
<td>
<a href="<?php echo esc_url($view_url); ?>" class="button button-small">Anzeigen</a>
<?php if($total > 0): ?>
<a href="<?php echo esc_url($export_url); ?>" class="button button-small">CSV</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
/**
* Sicherer Redirect: funktioniert auch wenn bereits Output gesendet wurde
* (z.B. durch andere Plugins wie wp-multi-ticket)
*/
private function safe_redirect($url) {
$url = esc_url_raw($url);
if(!headers_sent()) {
wp_redirect($url);
exit;
}
// Fallback: JavaScript-Redirect wenn Header bereits gesendet
echo '<script>window.location.href=' . wp_json_encode($url) . ';</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_attr($url) . '"></noscript>';
exit;
}
public function handle_actions() {
// Globale Einstellungen speichern (via admin-post.php)
add_action('admin_post_wmf_save_global_settings', array($this, 'save_global_settings'));
add_action('admin_post_wmf_update_submission_status', array($this, 'update_submission_status'));
if(empty($_POST['wmf_admin_action'])||!current_user_can('manage_options')) return;
if($_POST['wmf_admin_action']==='save_form') {
check_admin_referer('wmf_save_form'); $this->save_form();
}
if($_POST['wmf_admin_action']==='delete_form'&&!empty($_POST['form_id'])) {
check_admin_referer('wmf_delete_form_'.intval($_POST['form_id']));
wp_delete_post(intval($_POST['form_id']),true);
$this->safe_redirect(add_query_arg('wmf_notice','form_deleted',admin_url('admin.php?page=wp-multi-formular')));
}
}
private function save_form() {
$form_id=intval($_POST['form_id']??0);
$title=sanitize_text_field($_POST['form_title']??'Neues Formular');
$fields_raw=stripslashes($_POST['wmf_form_fields']??'[]');
$fields=json_decode($fields_raw,true);
if(!is_array($fields)) $fields=array();
// Felder bereinigen
$clean_fields=array();
foreach($fields as $f) {
if(empty($f['id'])||empty($f['type'])) continue;
$f['name']=sanitize_key($f['name']??$f['id']);
$f['label']=sanitize_text_field($f['label']??'');
$f['placeholder']=sanitize_text_field($f['placeholder']??'');
$f['description']=sanitize_text_field($f['description']??'');
$f['required']=in_array($f['required']??'0',array('0','1'))?$f['required']:'0';
$f['width']=in_array($f['width']??'full',array('full','half','third'))?$f['width']:'full';
$clean_fields[]=$f;
}
$step_labels=array();
if(!empty($_POST['step_labels'])&&is_array($_POST['step_labels'])) {
$step_labels=array_map('sanitize_text_field',$_POST['step_labels']);
}
$data=array(
'fields' => $clean_fields,
'submit_label' => sanitize_text_field($_POST['submit_label']??'Absenden'),
'success_message' => sanitize_textarea_field($_POST['success_message']??''),
'error_message' => sanitize_textarea_field($_POST['error_message']??''),
'notify_admin' => !empty($_POST['notify_admin'])?'1':'0',
'admin_email' => sanitize_email($_POST['admin_email']??''),
'admin_subject' => sanitize_text_field($_POST['admin_subject']??''),
'admin_reply_to' => !empty($_POST['admin_reply_to'])?'1':'0',
'notify_sender' => !empty($_POST['notify_sender'])?'1':'0',
'sender_subject' => sanitize_text_field($_POST['sender_subject']??''),
'sender_message' => sanitize_textarea_field($_POST['sender_message']??''),
'from_name' => sanitize_text_field($_POST['from_name']??''),
'from_email' => sanitize_email($_POST['from_email']??''),
'save_submissions' => !empty($_POST['save_submissions'])?'1':'0',
'recaptcha_enabled' => !empty($_POST['recaptcha_enabled'])?'1':'0',
'honeypot_enabled' => !empty($_POST['honeypot_enabled'])?'1':'0',
'redirect_url' => esc_url_raw($_POST['redirect_url']??''),
'css_class' => sanitize_html_class($_POST['css_class']??''),
'multi_step' => !empty($_POST['multi_step'])?'1':'0',
'step_labels' => $step_labels,
'show_progress' => !empty($_POST['show_progress'])?'1':'0',
);
if($form_id) {
wp_update_post(array('ID'=>$form_id,'post_title'=>$title,'post_status'=>'publish'));
} else {
$form_id=wp_insert_post(array('post_type'=>'wmf-form','post_title'=>$title,'post_status'=>'publish'));
}
wmf_save_form_meta($form_id,$data);
$this->safe_redirect(add_query_arg(array('page'=>'wp-multi-formular','edit'=>$form_id,'wmf_notice'=>'saved'),admin_url('admin.php')));
}
public function admin_notices() {
$n=$_GET['wmf_notice']??'';
$msgs=array('saved'=>array('success','Formular gespeichert.'),'deleted'=>array('success','Einreichung gelöscht.'),'form_deleted'=>array('success','Formular gelöscht.'),'global_saved'=>array('success','Globale E-Mail-Einstellungen gespeichert.'));
if(isset($msgs[$n])) printf('<div class="notice notice-%s is-dismissible"><p>%s</p></div>',esc_attr($msgs[$n][0]),esc_html($msgs[$n][1]));
}
public function enqueue_assets($hook) {
$page=$_GET['page']??'';
if(!in_array($page,array('wp-multi-formular','wmf-new-form','wmf-integrations'))&&strpos($hook,'wmf')=== false) return;
wp_enqueue_style('wmf-admin',WMF_URL.'assets/css/admin.css',array(),WMF_VERSION);
wp_enqueue_script('wmf-admin',WMF_URL.'assets/js/admin.js',array('jquery','jquery-ui-sortable','jquery-ui-draggable'),WMF_VERSION,true);
wp_enqueue_style('dashicons');
wp_localize_script('wmf-admin','WMF',array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wmf_admin'),
'fields' => $this->fields_for_js(),
'i18n' => array(
'confirm_delete' => 'Feld wirklich löschen?',
'confirm_delete_form' => 'Formular und alle Einreichungen wirklich löschen?',
'no_label' => '(kein Titel)',
'loading' => 'Lade …',
'preview' => 'Vorschau',
'close_preview' => 'Vorschau schließen',
),
));
}
public function update_submission_status() {
if(!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'wmf_update_status') || !current_user_can('manage_options')) wp_die('Fehler.');
$sid = intval($_POST['submission_id'] ?? 0);
$status = sanitize_text_field($_POST['status'] ?? 'neu');
$form_id = intval($_POST['form_id'] ?? 0);
$redirect = esc_url_raw($_POST['redirect_url'] ?? admin_url('admin.php?page=wp-multi-formular'));
if($sid && in_array($status, array('neu','gelesen','archiviert'))) {
WMF_Submission::update_status($sid, $status);
}
$this->safe_redirect($redirect);
}
public function save_global_settings() {
if(!wp_verify_nonce($_POST['wmf_global_nonce'] ?? '', 'wmf_global_settings_save')) {
wp_die('Sicherheitsfehler.');
}
if(!current_user_can('manage_options')) wp_die('Keine Berechtigung.');
$settings = array(
'from_name' => sanitize_text_field($_POST['wmf_from_name'] ?? ''),
'from_email' => sanitize_email( $_POST['wmf_from_email'] ?? ''),
'smtp_enabled' => !empty($_POST['wmf_smtp_enabled']) ? '1' : '0',
'smtp_host' => sanitize_text_field($_POST['wmf_smtp_host'] ?? ''),
'smtp_port' => intval( $_POST['wmf_smtp_port'] ?? 587),
'smtp_enc' => sanitize_text_field($_POST['wmf_smtp_enc'] ?? 'tls'),
'smtp_user' => sanitize_text_field($_POST['wmf_smtp_user'] ?? ''),
'smtp_pass' => $_POST['wmf_smtp_pass'] ?? '', // Passwort nicht strippen
);
update_option('wmf_global_settings', $settings);
$this->safe_redirect(add_query_arg(array('page'=>'wmf-integrations','wmf_notice'=>'global_saved'), admin_url('admin.php')));
}
private function fields_for_js() {
$r=array();
foreach(wmf_get_fields() as $type=>$f) {
$r[]=array('type'=>$type,'label'=>$f->label,'icon'=>$f->icon,'category'=>$f->category,'defaults'=>$f->defaults());
}
return $r;
}
}

View File

@@ -0,0 +1,44 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Builder {
private static $instance=null;
public static function instance() {
if(is_null(self::$instance)){self::$instance=new self();self::$instance->hook();}
return self::$instance;
}
public function hook() {
add_action('wp_ajax_wmf_get_field_settings',array($this,'ajax_field_settings'));
add_action('wp_ajax_wmf_preview_form', array($this,'ajax_preview'));
}
public function ajax_field_settings() {
check_ajax_referer('wmf_admin','nonce');
if(!current_user_can('manage_options')) wp_die();
$type=sanitize_text_field($_POST['field_type']??'');
$field=json_decode(stripslashes($_POST['field_data']??'{}'),true);
$obj=wmf_get_field($type);
if(!$obj) wp_send_json_error('Unbekannter Feldtyp: '.$type);
ob_start(); $obj->settings_panel($field); $html=ob_get_clean();
wp_send_json_success(array('html'=>$html));
}
public function ajax_preview() {
check_ajax_referer('wmf_admin','nonce');
if(!current_user_can('manage_options')) wp_die();
$form_id=intval($_POST['form_id']??0);
$fields=json_decode(stripslashes($_POST['fields']??'[]'),true);
$meta=wmf_get_form_meta($form_id);
$meta['fields']=$fields;
ob_start();
echo '<div class="wmf-preview-wrap">';
foreach($fields as $field) {
$obj=wmf_get_field($field['type']??'');
if(!$obj) continue;
echo '<div class="wmf-field-wrap">';
$obj->render($field,'');
echo '</div>';
}
echo '<div class="wmf-submit-wrap"><button type="button" class="wmf-submit-button" disabled>'.esc_html($meta['submit_label']?:'Absenden').'</button></div>';
echo '</div>';
$html=ob_get_clean();
wp_send_json_success(array('html'=>$html));
}
}

View File

@@ -0,0 +1,21 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Registry {
private static $instance=null;
private $fields=array();
public static function instance() {
if(is_null(self::$instance)){self::$instance=new self();self::$instance->register_defaults();}
return self::$instance;
}
private function register_defaults() {
foreach(array('WMF_Field_Text','WMF_Field_Email','WMF_Field_Textarea','WMF_Field_Select',
'WMF_Field_Checkbox','WMF_Field_Radio','WMF_Field_Number','WMF_Field_Phone',
'WMF_Field_URL','WMF_Field_Date','WMF_Field_File','WMF_Field_Rating',
'WMF_Field_Range','WMF_Field_GDPR','WMF_Field_Signature',
'WMF_Field_Hidden','WMF_Field_HTML','WMF_Field_Divider') as $cls) {
$f=new $cls(); $this->fields[$f->type]=$f;
}
}
public function get_fields() { return $this->fields; }
public function get_field($type) { return $this->fields[$type]??null; }
}

View File

@@ -0,0 +1,105 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Form_Processor {
private static $instance=null;
public static function instance() {
if(is_null(self::$instance)){self::$instance=new self();self::$instance->hook();}
return self::$instance;
}
public function hook() {
add_action('init',array($this,'maybe_process'),20);
}
public function maybe_process() {
if(empty($_POST['wmf_action'])||$_POST['wmf_action']!=='submit') return;
if(empty($_POST['wmf_form_id'])) return;
if(!session_id()) session_start();
$form_id=intval($_POST['wmf_form_id']);
if(!wp_verify_nonce($_POST['wmf_nonce']??'','wmf_submit_'.$form_id)) {
$this->fail($form_id,'Sicherheitsüberprüfung fehlgeschlagen.'); return;
}
$form=get_post($form_id);
if(!$form||$form->post_type!=='wmf-form') return;
$meta = wmf_get_form_meta($form_id);
$fields = $meta['fields']??array();
// Honeypot prüfen
if(!empty($meta['honeypot_enabled'])&&$meta['honeypot_enabled']==='1') {
if(!empty($_POST['wmf_hp_'.md5($form_id)])) { $this->ok($form_id,$meta); return; } // stille Ablehnung
}
// Werte sammeln + validieren
$values=array(); $errors=array();
foreach($fields as $field) {
$type=$field['type']??'';
$obj=wmf_get_field($type);
if(!$obj) continue;
if(in_array($type,array('html','divider'))) continue;
$raw=$_POST['wmf_fields'][$field['id']]??'';
$val=$obj->sanitize($raw,$field);
$valid=$obj->validate($val,$field);
$values[$field['id']]=$val;
if($valid!==true) $errors[$field['id']]=$valid;
}
// Datei-Uploads
$file_values=array();
if(!empty($_FILES['wmf_files'])) {
foreach($fields as $field) {
if(($field['type']??'')!=='file') continue;
$uploaded=WMF_Field_File::handle_upload($field,$form_id);
if(!empty($uploaded)) $file_values[$field['id']]=$uploaded;
// Validierung nochmal mit Upload-Ergebnis
$valid=wmf_get_field('file')->validate('',$field);
if($valid!==true&&!isset($errors[$field['id']])) $errors[$field['id']]=$valid;
}
}
if(!empty($errors)) {
$_SESSION['wmf_values_'.$form_id]=$values;
$_SESSION['wmf_errors_'.$form_id]=$errors;
wp_redirect($this->current_url()); exit;
}
// Datei-URLs in Werte mergen
foreach($file_values as $fid=>$uploads) {
$urls=array_map(fn($u)=>$u['url'],$uploads);
$values[$fid]=implode(', ',$urls);
}
// Speichern
$submission_id=null;
if(!empty($meta['save_submissions'])&&$meta['save_submissions']==='1') {
$submission_id=WMF_Submission::save($form_id,$values);
}
// E-Mails
if(!empty($meta['notify_admin'])&&$meta['notify_admin']==='1') {
WMF_Mailer::notify_admin($form_id,$meta,$fields,$values,$file_values);
}
if(!empty($meta['notify_sender'])&&$meta['notify_sender']==='1') {
WMF_Mailer::notify_sender($form_id,$meta,$fields,$values);
}
do_action('wmf_form_submitted',$form_id,$meta,$fields,$values,$submission_id);
$this->ok($form_id,$meta);
}
private function ok($form_id,$meta) {
if(!empty($meta['redirect_url'])) { wp_redirect(esc_url_raw($meta['redirect_url'])); exit; }
if(!session_id()) session_start();
$_SESSION['wmf_success_'.$form_id]=true;
wp_redirect($this->current_url()); exit;
}
private function fail($form_id,$msg) {
if(!session_id()) session_start();
$_SESSION['wmf_errors_'.$form_id]=array('_global'=>$msg);
wp_redirect($this->current_url()); exit;
}
private function current_url() {
return (is_ssl()?'https':'http').'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
}
}

View File

@@ -0,0 +1,145 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Form_Renderer {
public static function render($form_id,$args=array()) {
$form=get_post($form_id);
if(!$form||$form->post_type!=='wmf-form'||$form->post_status!=='publish')
return '<p class="wmf-error">Formular nicht gefunden.</p>';
if(!session_id()) session_start();
$meta = wmf_get_form_meta($form_id);
$fields = $meta['fields']??array();
$saved_values = $_SESSION['wmf_values_'.$form_id]??array();
$errors = $_SESSION['wmf_errors_'.$form_id]??array();
$success = $_SESSION['wmf_success_'.$form_id]??false;
unset($_SESSION['wmf_values_'.$form_id],$_SESSION['wmf_errors_'.$form_id],$_SESSION['wmf_success_'.$form_id]);
ob_start();
if($success) {
echo '<div class="wmf-notice wmf-notice-success" role="alert">'.esc_html($meta['success_message']).'</div>';
if(empty($args['always_show'])) return ob_get_clean();
}
if(!empty($errors['_global']))
echo '<div class="wmf-notice wmf-notice-error" role="alert">'.esc_html($errors['_global']).'</div>';
$multi_step=!empty($meta['multi_step'])&&$meta['multi_step']==='1';
// Schritte ermitteln
$steps=array();
foreach($fields as $f) { $s=intval($f['step']??0); if(!isset($steps[$s])) $steps[$s]=array(); $steps[$s][]=$f; }
ksort($steps);
$total_steps=count($steps);
if($total_steps<2) $multi_step=false;
$form_cls='wmf-form'.(!empty($meta['css_class'])?' '.esc_attr($meta['css_class']):'');
$field_json=wp_json_encode($fields);
?>
<div class="wmf-form-wrap" id="wmf-wrap-<?php echo intval($form_id); ?>">
<?php if($multi_step&&!empty($meta['show_progress'])&&$meta['show_progress']==='1'): ?>
<div class="wmf-progress-bar" data-steps="<?php echo $total_steps; ?>">
<?php $step_labels=$meta['step_labels']??array();
foreach(array_keys($steps) as $si=>$snum): $lbl=$step_labels[$si]??'Schritt '.($si+1); ?>
<div class="wmf-step-indicator <?php echo $si===0?'active':''; ?>" data-step="<?php echo $si; ?>">
<span class="wmf-step-num"><?php echo $si+1; ?></span>
<span class="wmf-step-lbl"><?php echo esc_html($lbl); ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form class="<?php echo $form_cls; ?>"
id="wmf-form-<?php echo intval($form_id); ?>"
method="post"
enctype="multipart/form-data"
novalidate
data-form-id="<?php echo intval($form_id); ?>"
data-multi-step="<?php echo $multi_step?'1':'0'; ?>"
data-total-steps="<?php echo $total_steps; ?>"
data-fields='<?php echo esc_attr($field_json); ?>'>
<?php wp_nonce_field('wmf_submit_'.$form_id,'wmf_nonce'); ?>
<input type="hidden" name="wmf_form_id" value="<?php echo intval($form_id); ?>">
<input type="hidden" name="wmf_action" value="submit">
<?php // Honeypot
if(!empty($meta['honeypot_enabled'])&&$meta['honeypot_enabled']==='1'):
$hp_name='wmf_hp_'.md5($form_id); ?>
<div class="wmf-hp-field" aria-hidden="true" style="position:absolute;left:-9999px;opacity:0;pointer-events:none;">
<label for="<?php echo esc_attr($hp_name); ?>">Bitte leer lassen</label>
<input type="text" id="<?php echo esc_attr($hp_name); ?>" name="<?php echo esc_attr($hp_name); ?>" value="" tabindex="-1" autocomplete="off">
</div>
<?php endif; ?>
<?php if($multi_step): ?>
<?php foreach(array_values($steps) as $si=>$step_fields): ?>
<div class="wmf-step" data-step="<?php echo $si; ?>" <?php echo $si>0?'style="display:none;"':''; ?>>
<div class="wmf-fields">
<?php self::render_fields($step_fields,$errors,$saved_values); ?>
</div>
<div class="wmf-step-nav">
<?php if($si>0): ?><button type="button" class="wmf-prev-step button button-secondary">← Zurück</button><?php endif; ?>
<?php if($si<$total_steps-1): ?>
<button type="button" class="wmf-next-step button button-primary">Weiter →</button>
<?php else: ?>
<?php self::render_submit($meta,$form_id); ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="wmf-fields">
<?php self::render_fields($fields,$errors,$saved_values); ?>
</div>
<?php self::render_submit($meta,$form_id); ?>
<?php endif; ?>
</form>
</div>
<?php
return ob_get_clean();
}
private static function render_fields($fields,$errors,$saved_values) {
foreach($fields as $field) {
$obj=wmf_get_field($field['type']??'');
if(!$obj) continue;
$value=$saved_values[$field['id']]??'';
$err=$errors[$field['id']]??'';
$wrap_cls='wmf-field-wrap'.($err?' wmf-has-error':'');
echo '<div class="'.esc_attr($wrap_cls).'">';
$obj->render($field,$value);
if($err) echo '<p class="wmf-field-error">'.esc_html($err).'</p>';
echo '</div>';
}
}
private static function render_submit($meta,$form_id) {
// reCAPTCHA
$integrations=wmf_get_integrations();
if(!empty($meta['recaptcha_enabled'])&&$meta['recaptcha_enabled']==='1') {
$rv3=$integrations->get_service('recaptchav3');
$rv2=$integrations->get_service('recaptcha');
if($rv3&&$rv3->is_connected()) {
$creds=$rv3->get_credentials();
echo '<input type="hidden" name="wmf_recaptcha_token" id="wmf-rctoken-'.intval($form_id).'">';
echo '<script>if(typeof grecaptcha!=="undefined"){grecaptcha.ready(function(){grecaptcha.execute("'.esc_js($creds['site_key']).'",{action:"submit"}).then(function(t){document.getElementById("wmf-rctoken-'.intval($form_id).'").value=t;});});}</script>';
} elseif($rv2&&$rv2->is_connected()) {
$creds=$rv2->get_credentials();
echo '<div class="wmf-recaptcha g-recaptcha" data-sitekey="'.esc_attr($creds['site_key']).'"></div>';
echo '<script async defer src="https://www.google.com/recaptcha/api.js"></script>';
}
}
?>
<div class="wmf-submit-wrap">
<button type="submit" class="wmf-submit-button">
<?php echo esc_html($meta['submit_label']?:'Absenden'); ?>
</button>
<span class="wmf-spinner" aria-hidden="true"></span>
</div>
<?php
}
}

View File

@@ -0,0 +1,102 @@
<?php
if(!defined('ABSPATH')) exit;
class WMF_Mailer {
/**
* Admin-Benachrichtigung
*/
public static function notify_admin($form_id, $meta, $fields, $values, $file_values = array()) {
$global = get_option('wmf_global_settings', array());
$from_name = !empty($meta['from_name']) ? $meta['from_name'] : ($global['from_name'] ?? get_bloginfo('name'));
$from_email = !empty($meta['from_email']) ? $meta['from_email'] : ($global['from_email'] ?? get_option('admin_email'));
$to = !empty($meta['admin_email']) ? $meta['admin_email'] : get_option('admin_email');
$subject = !empty($meta['admin_subject']) ? $meta['admin_subject'] : 'Neue Formulareinreichung';
$headers = array(
'Content-Type: text/html; charset=UTF-8',
'From: ' . $from_name . ' <' . $from_email . '>',
);
// Reply-To auf Absender-E-Mail-Feld setzen
if(!empty($meta['admin_reply_to']) && $meta['admin_reply_to'] === '1') {
foreach($fields as $f) {
if(($f['type'] ?? '') === 'email' && !empty($values[$f['id']]) && is_email($values[$f['id']])) {
$headers[] = 'Reply-To: ' . $values[$f['id']];
break;
}
}
}
wp_mail($to, $subject, self::build_body($fields, $values, $file_values), $headers);
}
/**
* Absender-Bestaetigung
*/
public static function notify_sender($form_id, $meta, $fields, $values) {
$global = get_option('wmf_global_settings', array());
$from_name = !empty($meta['from_name']) ? $meta['from_name'] : ($global['from_name'] ?? get_bloginfo('name'));
$from_email = !empty($meta['from_email']) ? $meta['from_email'] : ($global['from_email'] ?? get_option('admin_email'));
$email = '';
foreach($fields as $f) {
if(($f['type'] ?? '') === 'email' && !empty($values[$f['id']]) && is_email($values[$f['id']])) {
$email = $values[$f['id']];
break;
}
}
if(!$email) return;
$headers = array(
'Content-Type: text/html; charset=UTF-8',
'From: ' . $from_name . ' <' . $from_email . '>',
);
$body = '<html><body style="font-family:sans-serif;color:#1d2327;max-width:600px;margin:0 auto;">'
. wpautop(esc_html($meta['sender_message'] ?? ''))
. '<p style="color:#888;font-size:12px;margin-top:24px;">Gesendet ueber ' . get_bloginfo('name') . '</p>'
. '</body></html>';
wp_mail($email, $meta['sender_subject'] ?? 'Ihre Nachricht wurde empfangen', $body, $headers);
}
/**
* E-Mail-Body aufbauen
*/
private static function build_body($fields, $values, $file_values) {
$rows = '';
foreach($fields as $f) {
if(in_array($f['type'] ?? '', array('html','divider','hidden'))) continue;
$lbl = $f['label'] ?? $f['id'];
$val = $values[$f['id']] ?? '';
if(is_array($val)) $val = implode(', ', $val);
if(($f['type'] ?? '') === 'gdpr')
$val = ($val === '1') ? '&#10003; Zugestimmt' : '&#10007; Nicht zugestimmt';
if(($f['type'] ?? '') === 'signature' && !empty($val))
$val = '<img src="' . esc_attr($val) . '" style="max-width:300px;border:1px solid #ddd;">';
if(isset($file_values[$f['id']])) {
$links = array_map(
fn($u) => '<a href="' . esc_url($u['url']) . '">' . esc_html($u['name']) . '</a>',
$file_values[$f['id']]
);
$val = implode('<br>', $links);
}
$rows .= '<tr>'
. '<th style="text-align:left;padding:8px 14px;background:#f6f7f7;border-bottom:1px solid #eee;white-space:nowrap;font-weight:600;">' . esc_html($lbl) . '</th>'
. '<td style="padding:8px 14px;border-bottom:1px solid #eee;">' . $val . '</td>'
. '</tr>';
}
return '<html><body style="font-family:Arial,sans-serif;color:#1d2327;max-width:600px;margin:0 auto;">'
. '<div style="background:#2271b1;padding:20px 24px;border-radius:4px 4px 0 0;">'
. '<h2 style="color:#fff;margin:0;font-size:18px;">Neue Formulareinreichung</h2>'
. '<p style="color:rgba(255,255,255,.8);margin:4px 0 0;font-size:13px;">' . get_bloginfo('name') . ' &middot; ' . current_time('d.m.Y H:i') . '</p>'
. '</div>'
. '<div style="border:1px solid #dcdcde;border-top:none;border-radius:0 0 4px 4px;padding:0;">'
. '<table style="border-collapse:collapse;width:100%;">' . $rows . '</table>'
. '</div>'
. '<p style="color:#888;font-size:11px;margin-top:16px;text-align:center;">Diese E-Mail wurde automatisch gesendet von WP Multi Formular</p>'
. '</body></html>';
}
}

View File

@@ -0,0 +1,43 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
class WMF_Post_Type {
private static $instance = null;
public static function instance() {
if ( is_null( self::$instance ) ) { self::$instance = new self(); self::$instance->hook(); }
return self::$instance;
}
public function hook() {
add_action( 'init', array( $this, 'register' ) );
add_action( 'admin_init', array( $this, 'redirect_cpt' ) );
}
public static function register() {
register_post_type( 'wmf-form', array(
'labels' => array(
'name' => 'Formulare',
'singular_name' => 'Formular',
'not_found' => 'Keine Formulare gefunden.',
),
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'supports' => array( 'title' ),
'capability_type' => 'post',
'has_archive' => false,
'rewrite' => false,
) );
}
public function redirect_cpt() {
if ( ! is_admin() ) return;
$pt = $_GET['post_type'] ?? '';
if ( $pt === 'wmf-form' && basename( $_SERVER['PHP_SELF'] ) === 'edit.php' ) {
wp_redirect( admin_url( 'admin.php?page=wp-multi-formular' ) ); exit;
}
if ( isset( $_GET['post'], $_GET['action'] ) && $_GET['action'] === 'edit' ) {
$p = get_post( intval( $_GET['post'] ) );
if ( $p && $p->post_type === 'wmf-form' ) {
wp_redirect( admin_url( 'admin.php?page=wp-multi-formular&edit=' . $p->ID ) ); exit;
}
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Shortcode {
private static $instance=null;
public static function instance() {
if(is_null(self::$instance)){self::$instance=new self();self::$instance->hook();}
return self::$instance;
}
public function hook() {
add_shortcode('wp_multi_formular',array($this,'render'));
add_action('wp_enqueue_scripts',array($this,'enqueue_assets'));
add_action('init',array($this,'start_session'),1);
}
public function start_session() { if(!session_id()&&!headers_sent()) session_start(); }
public function render($atts) {
$atts=shortcode_atts(array('id'=>0),$atts,'wp_multi_formular');
$id=intval($atts['id']);
if(!$id) return '<p class="wmf-error">Bitte geben Sie eine Formular-ID an.</p>';
return WMF_Form_Renderer::render($id);
}
public function enqueue_assets() {
wp_enqueue_style('wp-multi-formular',WMF_URL.'assets/css/frontend.css',array(),WMF_VERSION);
wp_enqueue_script('wp-multi-formular',WMF_URL.'assets/js/frontend.js',array('jquery'),WMF_VERSION,true);
wp_localize_script('wp-multi-formular','WMF_Frontend',array(
'ajax_url'=>admin_url('admin-ajax.php'),
'i18n'=>array(
'required' =>'Dieses Feld ist ein Pflichtfeld.',
'email' =>'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
'url' =>'Bitte geben Sie eine gültige URL ein.',
'step_of' =>'Schritt %1 von %2',
'next' =>'Weiter',
'prev' =>'Zurück',
),
));
}
}

View File

@@ -0,0 +1,54 @@
<?php
if(!defined('ABSPATH')) exit;
class WMF_SMTP {
private static $instance = null;
public static function instance() {
if(is_null(self::$instance)) {
self::$instance = new self();
self::$instance->hook();
}
return self::$instance;
}
public function hook() {
$opts = get_option('wmf_global_settings', array());
if(!empty($opts['smtp_enabled']) && $opts['smtp_enabled'] === '1' && !empty($opts['smtp_host'])) {
add_action('phpmailer_init', array($this, 'configure_smtp'));
}
}
public function configure_smtp($phpmailer) {
$opts = get_option('wmf_global_settings', array());
if(empty($opts['smtp_host'])) return;
$phpmailer->isSMTP();
$phpmailer->Host = $opts['smtp_host'];
$phpmailer->Port = intval($opts['smtp_port'] ?? 587);
$phpmailer->SMTPSecure = $opts['smtp_enc'] ?? 'tls';
if(!empty($opts['smtp_user'])) {
$phpmailer->SMTPAuth = true;
$phpmailer->Username = $opts['smtp_user'];
$phpmailer->Password = $opts['smtp_pass'] ?? '';
} else {
$phpmailer->SMTPAuth = false;
}
// From-Adresse global setzen falls nicht per Mail-Header gesetzt
if(!empty($opts['from_email'])) {
$phpmailer->From = $opts['from_email'];
$phpmailer->FromName = $opts['from_name'] ?? get_bloginfo('name');
}
$phpmailer->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
)
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Submission {
public static function save($form_id,$values) {
global $wpdb;
$wpdb->insert($wpdb->prefix.'wmf_submissions',array(
'form_id'=>$form_id,'data'=>wp_json_encode($values),
'ip'=>wmf_get_client_ip(),'user_agent'=>$_SERVER['HTTP_USER_AGENT']??'',
'status'=>'neu','created_at'=>current_time('mysql'),
),array('%d','%s','%s','%s','%s','%s'));
return $wpdb->insert_id;
}
public static function delete($id) { global $wpdb; $wpdb->delete($wpdb->prefix.'wmf_submissions',array('id'=>$id),array('%d')); }
public static function update_status($id,$status) {
global $wpdb;
$wpdb->update($wpdb->prefix.'wmf_submissions',array('status'=>sanitize_text_field($status)),array('id'=>$id),array('%s'),array('%d'));
}
}

View File

@@ -0,0 +1,481 @@
<?php
if(!defined('ABSPATH')) exit;
class WMF_Submissions_List {
/* ================================================================
LISTE ALLER EINREICHUNGEN EINES FORMULARS
================================================================ */
public static function render($form_id) {
$meta = wmf_get_form_meta($form_id);
$fields = self::get_display_fields($meta);
$per_page = 25;
$current_page= max(1, intval($_GET['paged'] ?? 1));
$offset = ($current_page - 1) * $per_page;
$status_filter = sanitize_text_field($_GET['sub_status'] ?? '');
$search = sanitize_text_field($_GET['sub_search'] ?? '');
$submissions = wmf_get_submissions($form_id, array(
'limit' => $per_page,
'offset' => $offset,
'status' => $status_filter,
'search' => $search,
));
$total = wmf_count_submissions($form_id, $status_filter, $search);
$total_pages = ceil($total / $per_page);
$base_url = add_query_arg(array(
'page' => 'wp-multi-formular',
'view_submissions' => $form_id,
), admin_url('admin.php'));
?>
<div class="wmf-submissions-wrap">
<!-- Header -->
<div class="wmf-subs-header">
<h2 class="wmf-subs-title">
Einreichungen
<span class="wmf-subs-count"><?php echo intval($total); ?></span>
</h2>
<div class="wmf-subs-actions">
<?php if($submissions): ?>
<a href="<?php echo esc_url(self::export_url($form_id)); ?>"
class="button">&#11015; CSV exportieren</a>
<?php endif; ?>
</div>
</div>
<!-- Filter-Leiste -->
<div class="wmf-subs-filter-bar">
<form method="get" action="<?php echo esc_url(admin_url('admin.php')); ?>">
<input type="hidden" name="page" value="wp-multi-formular">
<input type="hidden" name="view_submissions" value="<?php echo intval($form_id); ?>">
<!-- Status-Tabs -->
<div class="wmf-status-tabs">
<?php
$statuses = array('' => 'Alle', 'neu' => 'Neu', 'gelesen' => 'Gelesen', 'archiviert' => 'Archiviert');
foreach($statuses as $s_key => $s_label):
$cnt = ($s_key === '') ? wmf_count_submissions($form_id) : wmf_count_submissions($form_id, $s_key);
$active = ($status_filter === $s_key) ? 'current' : '';
?>
<a href="<?php echo esc_url(add_query_arg(array('sub_status'=>$s_key,'paged'=>1), $base_url)); ?>"
class="wmf-status-tab <?php echo $active; ?>">
<?php echo esc_html($s_label); ?>
<span class="count">(<?php echo intval($cnt); ?>)</span>
</a>
<?php endforeach; ?>
</div>
<!-- Suche -->
<div class="wmf-subs-search">
<input type="search" name="sub_search" value="<?php echo esc_attr($search); ?>"
placeholder="Einreichungen durchsuchen …" class="wmf-search-input">
<button type="submit" class="button">Suchen</button>
<?php if($search): ?>
<a href="<?php echo esc_url($base_url); ?>" class="button">✕ Zurücksetzen</a>
<?php endif; ?>
</div>
</form>
</div>
<?php if(empty($submissions)): ?>
<div class="wmf-empty-submissions">
<span class="dashicons dashicons-email-alt" style="font-size:48px;width:48px;height:48px;color:#c3c4c7;margin-bottom:12px;display:block;"></span>
<?php if($search||$status_filter): ?>
<p>Keine Einreichungen gefunden.</p>
<a href="<?php echo esc_url($base_url); ?>" class="button">Filter zurücksetzen</a>
<?php else: ?>
<p>Noch keine Einreichungen vorhanden.</p>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Tabelle -->
<div class="wmf-table-wrap">
<table class="wp-list-table widefat fixed striped wmf-submissions-table">
<thead>
<tr>
<th class="wmf-col-id">#</th>
<th class="wmf-col-date">Datum</th>
<?php foreach(array_slice($fields, 0, 4) as $f): ?>
<th><?php echo esc_html($f['label']); ?></th>
<?php endforeach; ?>
<th class="wmf-col-status">Status</th>
<th class="wmf-col-actions">Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach($submissions as $row):
$data = json_decode($row->data, true) ?? array();
$view_url = add_query_arg(array(
'page' => 'wp-multi-formular',
'view_submission' => $row->id,
'form_id' => $form_id,
), admin_url('admin.php'));
?>
<tr class="wmf-sub-row <?php echo $row->status === 'neu' ? 'wmf-sub-new' : ''; ?>">
<td class="wmf-col-id">
<a href="<?php echo esc_url($view_url); ?>" class="wmf-sub-id-link">
#<?php echo intval($row->id); ?>
</a>
</td>
<td class="wmf-col-date">
<a href="<?php echo esc_url($view_url); ?>">
<?php echo esc_html(date_i18n('d.m.Y', strtotime($row->created_at))); ?>
<span class="wmf-time"><?php echo esc_html(date_i18n('H:i', strtotime($row->created_at))); ?></span>
</a>
</td>
<?php foreach(array_slice($fields, 0, 4) as $f):
$v = $data[$f['id']] ?? '';
if(is_array($v)) $v = implode(', ', $v);
if(($f['type']??'') === 'signature') $v = '(Unterschrift)';
$v_short = mb_strlen($v) > 60 ? mb_substr($v, 0, 60).'…' : $v;
?>
<td>
<a href="<?php echo esc_url($view_url); ?>" title="<?php echo esc_attr($v); ?>">
<?php echo esc_html($v_short); ?>
</a>
</td>
<?php endforeach; ?>
<td class="wmf-col-status">
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="wmf-status-form">
<?php wp_nonce_field('wmf_update_status'); ?>
<input type="hidden" name="action" value="wmf_update_submission_status">
<input type="hidden" name="submission_id" value="<?php echo intval($row->id); ?>">
<input type="hidden" name="form_id" value="<?php echo intval($form_id); ?>">
<input type="hidden" name="redirect_url" value="<?php echo esc_attr(add_query_arg(array('page'=>'wp-multi-formular','view_submissions'=>$form_id,'paged'=>$current_page,'sub_status'=>$status_filter), admin_url('admin.php'))); ?>">
<select name="status" class="wmf-status-select wmf-status-<?php echo esc_attr($row->status); ?>" onchange="this.form.submit()">
<option value="neu" <?php selected($row->status,'neu'); ?>>Neu</option>
<option value="gelesen" <?php selected($row->status,'gelesen'); ?>>Gelesen</option>
<option value="archiviert" <?php selected($row->status,'archiviert'); ?>>Archiviert</option>
</select>
</form>
</td>
<td class="wmf-col-actions">
<a href="<?php echo esc_url($view_url); ?>" class="button button-small">
&#128065; Ansehen
</a>
<a href="<?php echo esc_url(wp_nonce_url(
add_query_arg(array('wmf_action'=>'delete_submission','submission_id'=>$row->id,'form_id'=>$form_id,'page'=>'wp-multi-formular'), admin_url('admin.php')),
'wmf_delete_submission'
)); ?>"
class="button button-small wmf-btn-delete"
onclick="return confirm('Einreichung #<?php echo intval($row->id); ?> wirklich löschen?')">
&#128465; Löschen
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if($total_pages > 1): ?>
<div class="wmf-pagination">
<?php if($current_page > 1): ?>
<a href="<?php echo esc_url(add_query_arg('paged', $current_page-1, $base_url)); ?>" class="button">&laquo; Zurück</a>
<?php endif; ?>
<span class="wmf-page-info">
Seite <?php echo $current_page; ?> von <?php echo $total_pages; ?>
</span>
<?php if($current_page < $total_pages): ?>
<a href="<?php echo esc_url(add_query_arg('paged', $current_page+1, $base_url)); ?>" class="button">Weiter &raquo;</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
}
/* ================================================================
DETAILANSICHT EINER EINZELNEN EINREICHUNG
================================================================ */
public static function render_single($submission_id, $form_id) {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wmf_submissions WHERE id = %d AND form_id = %d",
$submission_id, $form_id
));
if(!$row) {
echo '<div class="notice notice-error"><p>Einreichung nicht gefunden.</p></div>';
return;
}
// Als gelesen markieren
if($row->status === 'neu') {
WMF_Submission::update_status($submission_id, 'gelesen');
$row->status = 'gelesen';
}
$data = json_decode($row->data, true) ?? array();
$meta = wmf_get_form_meta($form_id);
$fields = self::get_display_fields($meta);
$form = get_post($form_id);
$back_url = add_query_arg(array(
'page' => 'wp-multi-formular',
'view_submissions' => $form_id,
), admin_url('admin.php'));
$prev_url = self::get_adjacent_url($submission_id, $form_id, 'prev');
$next_url = self::get_adjacent_url($submission_id, $form_id, 'next');
?>
<div class="wmf-submission-detail">
<!-- Navigation -->
<div class="wmf-detail-nav">
<a href="<?php echo esc_url($back_url); ?>" class="button">
&larr; Alle Einreichungen
</a>
<div class="wmf-detail-nav-arrows">
<?php if($prev_url): ?>
<a href="<?php echo esc_url($prev_url); ?>" class="button" title="Vorherige Einreichung">&laquo; Vorherige</a>
<?php else: ?>
<button class="button" disabled>&laquo; Vorherige</button>
<?php endif; ?>
<?php if($next_url): ?>
<a href="<?php echo esc_url($next_url); ?>" class="button" title="Nächste Einreichung">Nächste &raquo;</a>
<?php else: ?>
<button class="button" disabled>Nächste &raquo;</button>
<?php endif; ?>
</div>
</div>
<!-- Meta-Karte -->
<div class="wmf-detail-meta-card">
<div class="wmf-detail-meta-item">
<span class="wmf-detail-meta-label">Einreichung</span>
<span class="wmf-detail-meta-value">#<?php echo intval($row->id); ?></span>
</div>
<div class="wmf-detail-meta-item">
<span class="wmf-detail-meta-label">Formular</span>
<span class="wmf-detail-meta-value"><?php echo esc_html($form ? $form->post_title : 'Unbekannt'); ?></span>
</div>
<div class="wmf-detail-meta-item">
<span class="wmf-detail-meta-label">Datum</span>
<span class="wmf-detail-meta-value"><?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($row->created_at))); ?></span>
</div>
<div class="wmf-detail-meta-item">
<span class="wmf-detail-meta-label">IP-Adresse</span>
<span class="wmf-detail-meta-value"><?php echo esc_html($row->ip ?: '—'); ?></span>
</div>
<div class="wmf-detail-meta-item">
<span class="wmf-detail-meta-label">Status</span>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
<?php wp_nonce_field('wmf_update_status'); ?>
<input type="hidden" name="action" value="wmf_update_submission_status">
<input type="hidden" name="submission_id" value="<?php echo intval($row->id); ?>">
<input type="hidden" name="form_id" value="<?php echo intval($form_id); ?>">
<input type="hidden" name="redirect_url" value="<?php echo esc_attr(add_query_arg(array('page'=>'wp-multi-formular','view_submission'=>$row->id,'form_id'=>$form_id), admin_url('admin.php'))); ?>">
<select name="status" class="wmf-status-select wmf-status-<?php echo esc_attr($row->status); ?>" onchange="this.form.submit()">
<option value="neu" <?php selected($row->status,'neu'); ?>>Neu</option>
<option value="gelesen" <?php selected($row->status,'gelesen'); ?>>Gelesen</option>
<option value="archiviert" <?php selected($row->status,'archiviert'); ?>>Archiviert</option>
</select>
</form>
</div>
<div class="wmf-detail-meta-item wmf-detail-meta-actions">
<a href="<?php echo esc_url(wp_nonce_url(
add_query_arg(array('wmf_action'=>'delete_submission','submission_id'=>$row->id,'form_id'=>$form_id,'page'=>'wp-multi-formular'), admin_url('admin.php')),
'wmf_delete_submission'
)); ?>"
class="button wmf-btn-delete"
onclick="return confirm('Einreichung #<?php echo intval($row->id); ?> wirklich löschen?')">
&#128465; Löschen
</a>
<button onclick="window.print()" class="button">&#128438; Drucken</button>
</div>
</div>
<!-- Felder-Inhalt -->
<div class="wmf-detail-fields">
<h3 class="wmf-detail-section-title">Eingereichte Daten</h3>
<div class="wmf-detail-fields-grid">
<?php foreach($fields as $field):
$val = $data[$field['id']] ?? '';
$type = $field['type'] ?? 'text';
?>
<div class="wmf-detail-field <?php echo 'wmf-detail-field-' . esc_attr($type); ?>">
<div class="wmf-detail-field-label">
<?php echo esc_html($field['label']); ?>
<span class="wmf-detail-field-type"><?php echo esc_html($type); ?></span>
</div>
<div class="wmf-detail-field-value">
<?php self::render_field_value($val, $field); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Technische Infos (aufklappbar) -->
<details class="wmf-detail-technical">
<summary>Technische Informationen</summary>
<table class="widefat" style="margin-top:12px;">
<tr><th>User-Agent</th><td><?php echo esc_html($row->user_agent ?: '—'); ?></td></tr>
<tr><th>Erstellt am</th><td><?php echo esc_html($row->created_at); ?></td></tr>
<tr><th>Datenbank-ID</th><td><?php echo intval($row->id); ?></td></tr>
</table>
</details>
</div>
<style>
@media print {
#adminmenumain,#wpadminbar,.wmf-detail-nav,.wmf-detail-meta-actions,
#screen-meta,#wpfooter,.notice { display:none!important; }
.wmf-submission-detail { margin:0; padding:0; }
.wmf-detail-fields { border:none; }
}
</style>
<?php
}
/* ================================================================
FELDINHALTE RENDERN (je nach Typ)
================================================================ */
private static function render_field_value($val, $field) {
$type = $field['type'] ?? 'text';
if($val === '' || $val === null || $val === array()) {
echo '<span class="wmf-empty-value">—</span>';
return;
}
switch($type) {
case 'checkbox':
$vals = is_array($val) ? $val : explode(', ', $val);
echo '<ul class="wmf-val-list">';
foreach($vals as $v) echo '<li>' . esc_html($v) . '</li>';
echo '</ul>';
break;
case 'gdpr':
$icon = $val === '1' ? '✅' : '❌';
echo $icon . ' ' . ($val === '1' ? 'Zugestimmt' : 'Nicht zugestimmt');
break;
case 'signature':
if(strpos($val, 'data:image') === 0) {
echo '<img src="' . esc_attr($val) . '" style="max-width:300px;border:1px solid #dcdcde;border-radius:4px;background:#fff;">';
} else {
echo '<span class="wmf-empty-value">Keine Unterschrift</span>';
}
break;
case 'file':
$urls = is_array($val) ? $val : explode(', ', $val);
echo '<div class="wmf-val-files">';
foreach($urls as $url) {
$url = trim($url);
if(!$url) continue;
$filename = basename(parse_url($url, PHP_URL_PATH));
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$is_image = in_array($ext, array('jpg','jpeg','png','gif','webp'));
if($is_image) {
echo '<div class="wmf-val-file-img"><a href="' . esc_url($url) . '" target="_blank">';
echo '<img src="' . esc_url($url) . '" style="max-width:200px;max-height:150px;border-radius:4px;"></a></div>';
} else {
echo '<a href="' . esc_url($url) . '" target="_blank" class="wmf-val-file-link">📎 ' . esc_html($filename) . '</a>';
}
}
echo '</div>';
break;
case 'rating':
$stars = intval($val);
$max = intval($field['max_stars'] ?? 5);
echo '<div class="wmf-val-stars">';
for($i = 1; $i <= $max; $i++) {
echo '<span style="color:' . ($i <= $stars ? '#f0b429' : '#ddd') . ';font-size:20px;">★</span>';
}
echo ' <span style="color:#646970;font-size:13px;">' . $stars . '/' . $max . '</span></div>';
break;
case 'url':
echo '<a href="' . esc_url($val) . '" target="_blank">' . esc_html($val) . '</a>';
break;
case 'email':
echo '<a href="mailto:' . esc_attr($val) . '">' . esc_html($val) . '</a>';
break;
case 'textarea':
echo '<div class="wmf-val-textarea">' . nl2br(esc_html($val)) . '</div>';
break;
default:
if(is_array($val)) {
echo '<ul class="wmf-val-list">';
foreach($val as $v) echo '<li>' . esc_html($v) . '</li>';
echo '</ul>';
} else {
echo '<span class="wmf-val-text">' . esc_html($val) . '</span>';
}
}
}
/* ================================================================
HILFSFUNKTIONEN
================================================================ */
private static function get_display_fields($meta) {
return array_values(array_filter($meta['fields'] ?? array(), function($f) {
return !in_array($f['type'] ?? '', array('html','divider','hidden'));
}));
}
private static function get_adjacent_url($submission_id, $form_id, $direction) {
global $wpdb;
$op = $direction === 'prev' ? '>' : '<';
$order = $direction === 'prev' ? 'ASC' : 'DESC';
$adj = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wmf_submissions WHERE form_id = %d AND id {$op} %d ORDER BY id {$order} LIMIT 1",
$form_id, $submission_id
));
if(!$adj) return null;
return add_query_arg(array('page'=>'wp-multi-formular','view_submission'=>$adj,'form_id'=>$form_id), admin_url('admin.php'));
}
public static function status_label($s) {
return array('neu'=>'Neu','gelesen'=>'Gelesen','archiviert'=>'Archiviert')[$s] ?? $s;
}
public static function export_url($form_id) {
return wp_nonce_url(add_query_arg(array('wmf_action'=>'export_csv','form_id'=>$form_id,'page'=>'wp-multi-formular'), admin_url('admin.php')), 'wmf_export_csv');
}
public static function maybe_export($form_id) {
if(empty($_GET['wmf_action']) || $_GET['wmf_action'] !== 'export_csv') return;
if(!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wmf_export_csv')) return;
$meta = wmf_get_form_meta($form_id);
$fields = self::get_display_fields($meta);
$rows = wmf_get_submissions($form_id, array('limit'=>9999));
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="formular-' . $form_id . '-einreichungen.csv"');
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF));
$header = array('ID','Datum','Status','IP');
foreach($fields as $f) $header[] = $f['label'];
fputcsv($out, $header, ';');
foreach($rows as $row) {
$data = json_decode($row->data, true) ?? array();
$line = array($row->id, date_i18n('d.m.Y H:i', strtotime($row->created_at)), self::status_label($row->status), $row->ip);
foreach($fields as $f) {
$v = $data[$f['id']] ?? '';
if(is_array($v)) $v = implode(', ', $v);
if(($f['type']??'') === 'signature') $v = '(Unterschrift)';
$line[] = $v;
}
fputcsv($out, $line, ';');
}
fclose($out);
exit;
}
}

View File

@@ -0,0 +1,158 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
abstract class WMF_Field_Base {
public $type = '';
public $label = '';
public $icon = 'dashicons-edit';
public $category = 'standard';
public function defaults() {
return array(
'id' => '',
'type' => $this->type,
'label' => $this->label,
'name' => '',
'placeholder' => '',
'description' => '',
'required' => '0',
'css_class' => '',
'width' => 'full',
// Bedingte Logik
'conditional_logic' => '0',
'conditional_action' => 'show', // show|hide
'conditional_rules' => array(), // [{field,operator,value}]
'conditional_match' => 'all', // all|any
// Schritt (mehrstufige Formulare)
'step' => 0,
);
}
abstract public function render( $field, $value = '' );
public function validate( $value, $field ) {
if ( ! empty( $field['required'] ) && $field['required'] === '1' ) {
$empty = ( $value === '' || $value === null || $value === array() );
if ( $empty ) return sprintf( 'Das Feld „%s" ist ein Pflichtfeld.', $field['label'] );
}
return true;
}
public function sanitize( $value, $field ) {
return sanitize_text_field( (string) $value );
}
/* Einstellungs-Panel im Builder */
public function settings_panel( $field ) {
?>
<div class="wmf-field-setting">
<label>Bezeichnung</label>
<input type="text" data-setting="label" value="<?php echo esc_attr($field['label']??''); ?>" class="widefat">
</div>
<div class="wmf-field-setting">
<label>Feldname <span class="wmf-hint">(intern, nur a-z, 0-9, _)</span></label>
<input type="text" data-setting="name" value="<?php echo esc_attr($field['name']??''); ?>" class="widefat wmf-slug-input">
</div>
<div class="wmf-field-setting">
<label>Platzhaltertext</label>
<input type="text" data-setting="placeholder" value="<?php echo esc_attr($field['placeholder']??''); ?>" class="widefat">
</div>
<div class="wmf-field-setting">
<label>Hilfetext (unter dem Feld)</label>
<input type="text" data-setting="description" value="<?php echo esc_attr($field['description']??''); ?>" class="widefat">
</div>
<div class="wmf-field-setting wmf-field-inline">
<label><input type="checkbox" data-setting="required" value="1" <?php checked($field['required']??'0','1'); ?>> Pflichtfeld</label>
</div>
<div class="wmf-field-setting">
<label>Breite</label>
<select data-setting="width" class="widefat">
<option value="full" <?php selected($field['width']??'full','full'); ?>>Volle Breite</option>
<option value="half" <?php selected($field['width']??'full','half'); ?>>½ Breite</option>
<option value="third" <?php selected($field['width']??'full','third'); ?>>⅓ Breite</option>
</select>
</div>
<div class="wmf-field-setting">
<label>CSS-Klasse</label>
<input type="text" data-setting="css_class" value="<?php echo esc_attr($field['css_class']??''); ?>" class="widefat">
</div>
<hr class="wmf-settings-sep">
<div class="wmf-field-setting">
<label>Bedingte Logik</label>
<select data-setting="conditional_logic" class="widefat wmf-conditional-toggle">
<option value="0" <?php selected($field['conditional_logic']??'0','0'); ?>>Deaktiviert</option>
<option value="1" <?php selected($field['conditional_logic']??'0','1'); ?>>Aktiviert</option>
</select>
</div>
<div class="wmf-conditional-rules" style="<?php echo ($field['conditional_logic']??'0')==='1'?'':'display:none'; ?>">
<div class="wmf-field-setting">
<label>Dieses Feld</label>
<select data-setting="conditional_action" class="widefat">
<option value="show" <?php selected($field['conditional_action']??'show','show'); ?>>anzeigen</option>
<option value="hide" <?php selected($field['conditional_action']??'show','hide'); ?>>ausblenden</option>
</select>
</div>
<div class="wmf-field-setting">
<label>wenn</label>
<select data-setting="conditional_match" class="widefat">
<option value="all" <?php selected($field['conditional_match']??'all','all'); ?>>alle Bedingungen erfüllt sind</option>
<option value="any" <?php selected($field['conditional_match']??'all','any'); ?>>eine Bedingung erfüllt ist</option>
</select>
</div>
<div class="wmf-conditional-rule-list" data-field-id="<?php echo esc_attr($field['id']??''); ?>">
<?php
$rules = $field['conditional_rules'] ?? array();
if (empty($rules)) $rules = array(array('field'=>'','operator'=>'=','value'=>''));
foreach($rules as $rule): ?>
<div class="wmf-rule-row">
<select class="wmf-rule-field" data-setting-rule="field">
<option value="">— Feld —</option>
</select>
<select class="wmf-rule-op" data-setting-rule="operator">
<option value="=" <?php selected($rule['operator']??'=','='); ?>>ist</option>
<option value="!=" <?php selected($rule['operator']??'=','!='); ?>>ist nicht</option>
<option value="contains" <?php selected($rule['operator']??'=','contains'); ?>>enthält</option>
<option value="not_empty" <?php selected($rule['operator']??'=','not_empty'); ?>>ist ausgefüllt</option>
</select>
<input type="text" class="wmf-rule-value" data-setting-rule="value" value="<?php echo esc_attr($rule['value']??''); ?>" placeholder="Wert">
<button type="button" class="wmf-rule-remove">✕</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="button wmf-add-rule">+ Bedingung hinzufügen</button>
</div>
<?php
}
protected function wrapper_classes( $field ) {
$cls = array( 'wmf-field', 'wmf-field-' . $this->type );
$w = $field['width'] ?? 'full';
if ( $w !== 'full' ) $cls[] = 'wmf-field-width-' . $w;
if ( ! empty($field['css_class']) ) $cls[] = $field['css_class'];
if ( !empty($field['required']) && $field['required']==='1' ) $cls[] = 'wmf-required';
if ( !empty($field['conditional_logic']) && $field['conditional_logic']==='1' ) $cls[] = 'wmf-has-condition';
return implode(' ', $cls);
}
protected function render_label( $field ) {
if ( empty($field['label']) ) return;
$req = (!empty($field['required']) && $field['required']==='1') ? ' <span class="wmf-required-mark" aria-hidden="true">*</span>' : '';
printf( '<label for="%s" class="wmf-label">%s%s</label>', esc_attr($field['id']), esc_html($field['label']), $req );
}
protected function render_description( $field ) {
if ( empty($field['description']) ) return;
printf( '<p class="wmf-description">%s</p>', esc_html($field['description']) );
}
protected function conditional_attrs( $field ) {
if ( empty($field['conditional_logic']) || $field['conditional_logic'] !== '1' ) return '';
return sprintf(
' data-condition="%s" data-condition-action="%s" data-condition-match="%s" data-condition-rules="%s"',
'1',
esc_attr($field['conditional_action'] ?? 'show'),
esc_attr($field['conditional_match'] ?? 'all'),
esc_attr(wp_json_encode($field['conditional_rules'] ?? array()))
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Checkbox extends WMF_Field_Base {
public $type='checkbox'; public $label='Kontrollkästchen'; public $icon='dashicons-yes-alt'; public $category='auswahl';
public function defaults() { return array_merge(parent::defaults(),array('options'=>array(array('label'=>'Option 1','value'=>'option_1')),'layout'=>'vertical')); }
public function render($field,$value='') {
$checked=is_array($value)?$value:($value?array($value):array()); ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<div class="wmf-checkbox-group wmf-layout-<?php echo esc_attr($field['layout']??'vertical'); ?>">
<?php foreach($field['options']??array() as $opt): ?>
<label class="wmf-checkbox-label">
<input type="checkbox" name="wmf_fields[<?php echo esc_attr($field['id']); ?>][]" value="<?php echo esc_attr($opt['value']); ?>" <?php echo in_array($opt['value'],$checked)?'checked':''; ?>>
<span><?php echo esc_html($opt['label']); ?></span>
</label>
<?php endforeach; ?>
</div>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return is_array($v)?array_map('sanitize_text_field',$v):array(); }
}

View File

@@ -0,0 +1,14 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Date extends WMF_Field_Base {
public $type='date'; public $label='Datum'; public $icon='dashicons-calendar-alt';
public function defaults() { return array_merge(parent::defaults(),array('min_date'=>'','max_date'=>'')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="date" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" <?php echo!empty($field['min_date'])?'min="'.esc_attr($field['min_date']).'"':''; ?> <?php echo!empty($field['max_date'])?'max="'.esc_attr($field['max_date']).'"':''; ?> <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
}

View File

@@ -0,0 +1,13 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Divider extends WMF_Field_Base {
public $type='divider'; public $label='Trennlinie / Abschnitt'; public $icon='dashicons-minus'; public $category='layout';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?>>
<?php if(!empty($field['label'])): ?><h4 class="wmf-section-title"><?php echo esc_html($field['label']); ?></h4><?php else: ?><hr class="wmf-divider"><?php endif; ?>
<?php $this->render_description($field); ?>
</div><?php
}
public function validate($v,$f) { return true; }
public function sanitize($v,$f) { return ''; }
}

View File

@@ -0,0 +1,19 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Email extends WMF_Field_Base {
public $type='email'; public $label='E-Mail'; public $icon='dashicons-email-alt';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="email" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($field['placeholder']??''); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input" autocomplete="email">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function validate($value,$field) {
$base=parent::validate($value,$field); if($base!==true) return $base;
if($value!==''&&!is_email($value)) return sprintf('Bitte geben Sie eine gültige E-Mail-Adresse für „%s" ein.',$field['label']);
return true;
}
public function sanitize($value,$field) { return sanitize_email($value); }
}

View File

@@ -0,0 +1,64 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_File extends WMF_Field_Base {
public $type='file'; public $label='Datei-Upload'; public $icon='dashicons-upload';
public function defaults() { return array_merge(parent::defaults(),array('allowed_types'=>'jpg,jpeg,png,pdf,doc,docx','max_size_mb'=>'5','multiple'=>'0','save_to_media'=>'1')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="file" id="<?php echo esc_attr($field['id']); ?>" name="wmf_files[<?php echo esc_attr($field['id']); ?>]<?php echo(!empty($field['multiple'])&&$field['multiple']==='1')?'[]':''; ?>" accept="<?php echo esc_attr($this->accept_attr($field)); ?>" <?php echo(!empty($field['multiple'])&&$field['multiple']==='1')?'multiple':''; ?> <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input wmf-file-input">
<p class="wmf-description">Erlaubt: <?php echo esc_html($field['allowed_types']??''); ?> — Max. <?php echo esc_html($field['max_size_mb']??5); ?> MB</p>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
private function accept_attr($field) {
$types=array_map('trim',explode(',',$field['allowed_types']??''));
return implode(',',array_map(fn($t)=>'.'.$t,$types));
}
public function validate($value,$field) {
$key=$field['id'];
if(!isset($_FILES['wmf_files']['error'][$key])) return true;
$err=$_FILES['wmf_files']['error'][$key];
if(is_array($err)) $err=array_filter($err,fn($e)=>$e!==UPLOAD_ERR_NO_FILE);
if(empty($err)||$err===UPLOAD_ERR_NO_FILE) {
if(!empty($field['required'])&&$field['required']==='1') return sprintf('Bitte laden Sie eine Datei für „%s" hoch.',$field['label']);
return true;
}
$max=intval($field['max_size_mb']??5)*1024*1024;
$sizes=$_FILES['wmf_files']['size'][$key];
if(is_array($sizes)) { foreach($sizes as $s) if($s>$max) return sprintf('Max. %s MB erlaubt.',$field['max_size_mb']??5); }
elseif($sizes>$max) return sprintf('Max. %s MB erlaubt.',$field['max_size_mb']??5);
return true;
}
public function sanitize($v,$f) { return $v; }
public static function handle_upload($field,$form_id) {
$key=$field['id'];
if(!isset($_FILES['wmf_files']['tmp_name'][$key])) return array();
require_once ABSPATH.'wp-admin/includes/image.php';
require_once ABSPATH.'wp-admin/includes/file.php';
require_once ABSPATH.'wp-admin/includes/media.php';
$tmp=$_FILES['wmf_files']['tmp_name'][$key];
$names=$_FILES['wmf_files']['name'][$key];
$results=array();
if(!is_array($tmp)) { $tmp=array($tmp); $names=array($names); }
foreach($tmp as $i=>$t) {
if(empty($t)||!is_uploaded_file($t)) continue;
$upload=wp_handle_upload(array(
'tmp_name'=>$t,'name'=>$names[$i],'type'=>mime_content_type($t),'error'=>0,'size'=>filesize($t)
),array('test_form'=>false));
if(isset($upload['url'])) {
if(!empty($field['save_to_media'])&&$field['save_to_media']==='1') {
$att=array('post_mime_type'=>$upload['type'],'post_title'=>sanitize_file_name($names[$i]),'post_content'=>'','post_status'=>'inherit');
$att_id=wp_insert_attachment($att,$upload['file']);
wp_update_attachment_metadata($att_id,wp_generate_attachment_metadata($att_id,$upload['file']));
$results[]=array('url'=>$upload['url'],'attachment_id'=>$att_id,'name'=>$names[$i]);
} else {
$results[]=array('url'=>$upload['url'],'name'=>$names[$i]);
}
}
}
return $results;
}
}

View File

@@ -0,0 +1,34 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_GDPR extends WMF_Field_Base {
public $type='gdpr'; public $label='DSGVO-Zustimmung'; public $icon='dashicons-shield'; public $category='auswahl';
public function defaults() {
return array_merge(parent::defaults(),array(
'required' => '1',
'gdpr_text' => 'Ich stimme der <a href="/datenschutz" target="_blank">Datenschutzerklärung</a> zu.',
'error_message' => 'Bitte stimmen Sie der Datenschutzerklärung zu.',
));
}
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?> wmf-gdpr-wrap"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<label class="wmf-gdpr-label">
<input type="checkbox" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="1" <?php checked($value,'1'); ?> <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-gdpr-checkbox">
<span class="wmf-gdpr-text"><?php echo wp_kses_post($field['gdpr_text']??''); ?></span>
</label>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function validate($value,$field) {
if(!empty($field['required'])&&$field['required']==='1'&&$value!=='1') {
return $field['error_message']??'Bitte stimmen Sie der Datenschutzerklärung zu.';
}
return true;
}
public function sanitize($v,$f) { return $v==='1'?'1':'0'; }
public function settings_panel($field) {
parent::settings_panel($field); ?>
<div class="wmf-field-setting"><label>DSGVO-Text (HTML erlaubt)</label><textarea data-setting="gdpr_text" class="widefat" rows="3"><?php echo esc_textarea($field['gdpr_text']??''); ?></textarea></div>
<div class="wmf-field-setting"><label>Fehlermeldung</label><input type="text" data-setting="error_message" value="<?php echo esc_attr($field['error_message']??''); ?>" class="widefat"></div>
<?php
}
}

View File

@@ -0,0 +1,15 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Hidden extends WMF_Field_Base {
public $type='hidden'; public $label='Verstecktes Feld'; public $icon='dashicons-hidden'; public $category='layout';
public function defaults() { return array_merge(parent::defaults(),array('default_value'=>'')); }
public function render($field,$value='') {
$v=$value?:($field['default_value']??'');
// Dynamic values
if($v==='{{user_ip}}') $v=wmf_get_client_ip();
if($v==='{{page_url}}') $v=home_url(add_query_arg(array()));
if($v==='{{date}}') $v=date('Y-m-d');
printf('<input type="hidden" name="wmf_fields[%s]" value="%s">',esc_attr($field['id']),esc_attr($v));
}
public function validate($v,$f) { return true; }
}

View File

@@ -0,0 +1,13 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_HTML extends WMF_Field_Base {
public $type='html'; public $label='HTML-Block'; public $icon='dashicons-editor-code'; public $category='layout';
public function defaults() { return array_merge(parent::defaults(),array('content'=>'')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?>>
<?php echo wp_kses_post($field['content']??''); ?>
</div><?php
}
public function validate($v,$f) { return true; }
public function sanitize($v,$f) { return ''; }
}

View File

@@ -0,0 +1,15 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Number extends WMF_Field_Base {
public $type='number'; public $label='Zahl'; public $icon='dashicons-calculator';
public function defaults() { return array_merge(parent::defaults(),array('min'=>'','max'=>'','step'=>'1')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="number" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($field['placeholder']??''); ?>" <?php echo!empty($field['min'])?'min="'.esc_attr($field['min']).'"':''; ?> <?php echo!empty($field['max'])?'max="'.esc_attr($field['max']).'"':''; ?> step="<?php echo esc_attr($field['step']??1); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return is_numeric($v)?$v+0:''; }
}

View File

@@ -0,0 +1,13 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Phone extends WMF_Field_Base {
public $type='phone'; public $label='Telefon'; public $icon='dashicons-phone';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="tel" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($field['placeholder']??''); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input" autocomplete="tel">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
}

View File

@@ -0,0 +1,21 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Radio extends WMF_Field_Base {
public $type='radio'; public $label='Radio-Buttons'; public $icon='dashicons-marker'; public $category='auswahl';
public function defaults() { return array_merge(parent::defaults(),array('options'=>array(array('label'=>'Option 1','value'=>'option_1')),'layout'=>'vertical')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<div class="wmf-radio-group wmf-layout-<?php echo esc_attr($field['layout']??'vertical'); ?>">
<?php foreach($field['options']??array() as $opt): ?>
<label class="wmf-radio-label">
<input type="radio" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($opt['value']); ?>" <?php checked($value,$opt['value']); ?> <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?>>
<span><?php echo esc_html($opt['label']); ?></span>
</label>
<?php endforeach; ?>
</div>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
}

View File

@@ -0,0 +1,20 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Range extends WMF_Field_Base {
public $type='range'; public $label='Schieberegler'; public $icon='dashicons-leftright'; public $category='auswahl';
public function defaults() { return array_merge(parent::defaults(),array('min'=>'0','max'=>'100','step'=>'1','show_value'=>'1')); }
public function render($field,$value='') {
$val=$value!==''?$value:($field['min']??0); ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<div class="wmf-range-wrap">
<input type="range" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($val); ?>" min="<?php echo esc_attr($field['min']??0); ?>" max="<?php echo esc_attr($field['max']??100); ?>" step="<?php echo esc_attr($field['step']??1); ?>" class="wmf-range-input">
<?php if(!empty($field['show_value'])&&$field['show_value']==='1'): ?>
<span class="wmf-range-value"><?php echo esc_html($val); ?></span>
<?php endif; ?>
</div>
<?php $this->render_description($field); ?>
</div><?php
}
public function sanitize($v,$f) { return is_numeric($v)?$v+0:$f['min']??0; }
}

View File

@@ -0,0 +1,21 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Rating extends WMF_Field_Base {
public $type='rating'; public $label='Bewertung (Sterne)'; public $icon='dashicons-star-filled'; public $category='auswahl';
public function defaults() { return array_merge(parent::defaults(),array('max_stars'=>'5','icon'=>'star')); }
public function render($field,$value='') {
$max=intval($field['max_stars']??5); ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<div class="wmf-rating" data-max="<?php echo $max; ?>">
<?php for($i=1;$i<=$max;$i++): ?>
<button type="button" class="wmf-star <?php echo $i<=intval($value)?'active':''; ?>" data-value="<?php echo $i; ?>" aria-label="<?php echo $i; ?> Stern">★</button>
<?php endfor; ?>
<input type="hidden" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" class="wmf-rating-value">
</div>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return max(0,min(intval($f['max_stars']??5),intval($v))); }
}

View File

@@ -0,0 +1,23 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Select extends WMF_Field_Base {
public $type='select'; public $label='Auswahlliste'; public $icon='dashicons-list-view'; public $category='auswahl';
public function defaults() { return array_merge(parent::defaults(),array('options'=>array(array('label'=>'Option 1','value'=>'option_1')),'multiple'=>'0')); }
public function render($field,$value='') {
$multi=!empty($field['multiple'])&&$field['multiple']==='1';
$name='wmf_fields['.esc_attr($field['id']).']'.($multi?'[]':''); ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<select id="<?php echo esc_attr($field['id']); ?>" name="<?php echo $name; ?>" class="wmf-input wmf-select" <?php echo $multi?'multiple':''; ?> <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?>>
<?php if(!$multi): ?><option value=""><?php echo esc_html($field['placeholder']?:'— Bitte wählen —'); ?></option><?php endif; ?>
<?php foreach($field['options']??array() as $opt):
$sel=is_array($value)?in_array($opt['value'],$value):$value===$opt['value']; ?>
<option value="<?php echo esc_attr($opt['value']); ?>" <?php echo $sel?'selected':''; ?>><?php echo esc_html($opt['label']); ?></option>
<?php endforeach; ?>
</select>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return is_array($v)?array_map('sanitize_text_field',$v):sanitize_text_field($v); }
}

View File

@@ -0,0 +1,28 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Signature extends WMF_Field_Base {
public $type='signature'; public $label='Unterschrift'; public $icon='dashicons-edit-page'; public $category='auswahl';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<div class="wmf-signature-wrap">
<canvas class="wmf-signature-canvas" id="sig_<?php echo esc_attr($field['id']); ?>" width="400" height="150"></canvas>
<div class="wmf-signature-controls">
<button type="button" class="wmf-sig-clear button">Löschen</button>
</div>
<input type="hidden" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" class="wmf-signature-data">
</div>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function validate($value,$field) {
if(!empty($field['required'])&&$field['required']==='1'&&empty($value)) return sprintf('Bitte unterschreiben Sie im Feld „%s".',$field['label']);
return true;
}
public function sanitize($v,$f) {
// Base64 PNG-Daten erlaubt, sonst leer
if(strpos($v,'data:image/png;base64,')===0) return sanitize_text_field($v);
return '';
}
}

View File

@@ -0,0 +1,13 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Text extends WMF_Field_Base {
public $type='text'; public $label='Textfeld'; public $icon='dashicons-editor-textcolor';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="text" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($field['placeholder']??''); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input" autocomplete="on">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
}

View File

@@ -0,0 +1,22 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_Textarea extends WMF_Field_Base {
public $type='textarea'; public $label='Textbereich'; public $icon='dashicons-text';
public function defaults() { return array_merge(parent::defaults(),array('rows'=>'5','max_chars'=>'')); }
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<textarea id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" placeholder="<?php echo esc_attr($field['placeholder']??''); ?>" rows="<?php echo intval($field['rows']??5); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> <?php echo(!empty($field['max_chars']))?'maxlength="'.intval($field['max_chars']).'"':''; ?> class="wmf-input wmf-textarea"><?php echo esc_textarea($value); ?></textarea>
<?php if(!empty($field['max_chars'])): ?><div class="wmf-char-count"><span class="wmf-chars-used">0</span>/<?php echo intval($field['max_chars']); ?></div><?php endif; ?>
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return sanitize_textarea_field($v); }
public function settings_panel($field) {
parent::settings_panel($field); ?>
<div class="wmf-field-setting"><label>Zeilen</label><input type="number" min="2" max="30" data-setting="rows" value="<?php echo esc_attr($field['rows']??5); ?>" class="small-text"></div>
<div class="wmf-field-setting"><label>Max. Zeichen (0 = unbegrenzt)</label><input type="number" min="0" data-setting="max_chars" value="<?php echo esc_attr($field['max_chars']??''); ?>" class="small-text"></div>
<?php
}
}

View File

@@ -0,0 +1,14 @@
<?php
if (!defined('ABSPATH')) exit;
class WMF_Field_URL extends WMF_Field_Base {
public $type='url'; public $label='Website-URL'; public $icon='dashicons-admin-site';
public function render($field,$value='') { ?>
<div class="<?php echo esc_attr($this->wrapper_classes($field)); ?>"<?php echo $this->conditional_attrs($field); ?> data-field-id="<?php echo esc_attr($field['id']); ?>">
<?php $this->render_label($field); ?>
<input type="url" id="<?php echo esc_attr($field['id']); ?>" name="wmf_fields[<?php echo esc_attr($field['id']); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($field['placeholder']??'https://'); ?>" <?php echo(!empty($field['required'])&&$field['required']==='1')?'required':''; ?> class="wmf-input">
<?php $this->render_description($field); ?>
<span class="wmf-field-error-msg"></span>
</div><?php
}
public function sanitize($v,$f) { return esc_url_raw($v); }
}

View File

@@ -0,0 +1,118 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
function wmf_create_submissions_table() {
global $wpdb;
$table = $wpdb->prefix . 'wmf_submissions';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
form_id BIGINT(20) UNSIGNED NOT NULL,
data LONGTEXT NOT NULL,
ip VARCHAR(45) DEFAULT '',
user_agent TEXT DEFAULT '',
status VARCHAR(20) DEFAULT 'neu',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY form_id (form_id)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
update_option( 'wmf_db_version', '2.0' );
}
function wmf_get_fields() { return WMF_Field_Registry::instance()->get_fields(); }
function wmf_get_field( $type ) { return WMF_Field_Registry::instance()->get_field( $type ); }
function wmf_get_shortcode( $id ) { return '[wp_multi_formular id="' . intval($id) . '"]'; }
function wmf_unique_id() { return 'field_' . substr( md5( uniqid( '', true ) ), 0, 8 ); }
function wmf_get_form_meta( $form_id ) {
$defaults = array(
'fields' => array(),
'submit_label' => 'Absenden',
'success_message' => 'Vielen Dank! Ihre Nachricht wurde gesendet.',
'error_message' => 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.',
'notify_admin' => '1',
'admin_email' => get_option('admin_email'),
'admin_subject' => 'Neue Formulareinreichung',
'admin_reply_to' => '1',
'notify_sender' => '0',
'sender_subject' => 'Ihre Nachricht wurde empfangen',
'sender_message' => 'Vielen Dank für Ihre Nachricht. Wir melden uns bald.',
'from_name' => get_bloginfo('name'),
'from_email' => get_option('admin_email'),
'save_submissions' => '1',
'recaptcha_enabled' => '0',
'honeypot_enabled' => '1',
'active_email_service' => '',
'email_list_field' => '',
'redirect_url' => '',
'css_class' => '',
'multi_step' => '0',
'step_labels' => array(),
'show_progress' => '1',
);
$meta = get_post_meta( $form_id, '_wmf_form_data', true );
if ( ! is_array( $meta ) ) $meta = array();
return wp_parse_args( $meta, $defaults );
}
function wmf_save_form_meta( $form_id, $data ) {
update_post_meta( $form_id, '_wmf_form_data', $data );
}
function wmf_get_submissions( $form_id, $args = array() ) {
global $wpdb;
$table = $wpdb->prefix . 'wmf_submissions';
$defaults = array( 'limit' => 50, 'offset' => 0, 'status' => '', 'search' => '' );
$args = wp_parse_args( $args, $defaults );
$where = $wpdb->prepare( 'WHERE form_id = %d', $form_id );
if ( $args['status'] )
$where .= $wpdb->prepare( ' AND status = %s', $args['status'] );
if ( $args['search'] )
$where .= $wpdb->prepare( ' AND data LIKE %s', '%' . $wpdb->esc_like( $args['search'] ) . '%' );
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
$args['limit'], $args['offset']
)
);
}
function wmf_count_submissions( $form_id, $status = '', $search = '' ) {
global $wpdb;
$where = $wpdb->prepare( 'WHERE form_id = %d', $form_id );
if ( $status )
$where .= $wpdb->prepare( ' AND status = %s', $status );
if ( $search )
$where .= $wpdb->prepare( ' AND data LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wmf_submissions {$where}" );
}
function wmf_get_client_ip() {
foreach ( array('HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','REMOTE_ADDR') as $k ) {
if ( ! empty( $_SERVER[$k] ) ) {
$ip = trim( explode(',', $_SERVER[$k])[0] );
if ( filter_var($ip, FILTER_VALIDATE_IP) ) return $ip;
}
}
return '';
}
/**
* Sicherer Redirect - funktioniert auch wenn Header bereits gesendet wurden
* (z.B. durch Plugins die Output vor wp_redirect() ausgeben)
*/
function wmf_safe_redirect( $url ) {
$url = esc_url_raw( $url );
if ( ! headers_sent() ) {
wp_redirect( $url );
exit;
}
// JavaScript-Fallback
echo '<script>window.location.href=' . wp_json_encode( $url ) . ';</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_attr( $url ) . '"></noscript>';
exit;
}