Zum Inhalt

Beispielarbeiten · WordPress

WordPress-Plugin: Anfrageformular mit Admin-Liste und CSV-Export

Ein eigenes Plugin mit Shortcode, eigener Tabelle, Einstellungsseite und Export. Sicherheitsprüfungen wie Nonce, Bereinigung und Maskierung wurden auf einer frischen WordPress-Installation getestet.

Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.

Was gebaut wurde

  • Formular per Shortcode, Einträge in einer eigenen Tabelle
  • Einstellungsseite und Liste im Adminbereich
  • CSV-Export nur für Administratoren, Formeln in Zellen entschärft

gl-anfragen.zip herunterladen

Wie geprüft wurde

Automatischer Prüflauf vom 18.10.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):

  • Eigenes Plugin GL Anfragen (Shortcode, Tabelle, Einstellungsseite, Admin-Liste, CSV) auf WordPress 7.1.3 als ZIP installiert
  • 14/14 Pruefungen gruen (Nonce, Validierung, Bereinigung/Maskierung, CSV nur fuer Admin, Formel-Injektion entschaerft)
Alle Prüffälle (14 von 14 grün)
  • ✓ php lint
  • ✓ plugin aktiv
  • ✓ tabelle angelegt
  • ✓ einstellung gespeichert
  • ✓ formular mit nonce
  • ✓ gueltig weiter und danke
  • ✓ zweite gespeichert
  • ✓ ungueltige mail abgewiesen
  • ✓ falsche nonce 403
  • ✓ liste maskiert
  • ✓ csv admin
  • ✓ csv formel entschaerft
  • ✓ csv anonym kein csv
  • ✓ protokoll sauber

Grenzen

Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:

  • lokale Kopie mit SQLite statt MySQL und php -S statt Apache/nginx; dbDelta-SQL ist MySQL-Syntax und laeuft beim Kunden auf MySQL/MariaDB, hier durch den SQLite-Uebersetzer
  • ein Formular-/Tabellen-/CSV-Plugin geprueft; Gutenberg-Bloecke (React/JS-Build) und Page-Builder-Module (Elementor, Divi) sind nicht Teil der Probe
  • kein Spam-Schutz ueber Nonce hinaus (kein Captcha/Honeypot-Dienst eingebaut); Mailversand nicht geprueft (lokal gibt es keinen Mailserver)
  • keine Einreichung ins wordpress.org-Verzeichnis (Review, SVN) - Lieferung als ZIP an den Kunden

Der Plugin-Code

<?php
/**
 * Plugin Name: GL Anfragen
 * Description: Anfrageformular per Shortcode [gl_anfrage], speichert in eine eigene Tabelle, Liste und CSV-Export im Admin.
 * Version: 1.0.0
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * Author: GL Tools
 * License: GPL-2.0-or-later
 * Text Domain: gl-anfragen
 */
if (!defined('ABSPATH')) { exit; }

const GL_ANFRAGEN_DB_VERSION = '1';

function gl_anfragen_tabelle() { global $wpdb; return $wpdb->prefix . 'gl_anfragen'; }

register_activation_hook(__FILE__, 'gl_anfragen_aktivieren');
function gl_anfragen_aktivieren() {
    global $wpdb;
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    $t = gl_anfragen_tabelle();
    $cs = $wpdb->get_charset_collate();
    dbDelta("CREATE TABLE $t (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        erstellt datetime NOT NULL,
        name varchar(120) NOT NULL,
        email varchar(190) NOT NULL,
        nachricht text NOT NULL,
        PRIMARY KEY  (id)
    ) $cs;");
    add_option('gl_anfragen_danke', 'Danke, wir melden uns.');
    update_option('gl_anfragen_db_version', GL_ANFRAGEN_DB_VERSION);
}

register_uninstall_hook(__FILE__, 'gl_anfragen_deinstallieren');
function gl_anfragen_deinstallieren() {
    global $wpdb;
    $wpdb->query('DROP TABLE IF EXISTS ' . gl_anfragen_tabelle());
    delete_option('gl_anfragen_danke');
    delete_option('gl_anfragen_db_version');
}

/* ---------- Frontend: Shortcode ---------- */
add_shortcode('gl_anfrage', 'gl_anfragen_shortcode');
function gl_anfragen_shortcode() {
    $status = isset($_GET['gl']) ? sanitize_key(wp_unslash($_GET['gl'])) : '';
    $out = '';
    if ($status === 'danke') {
        $out .= '<p class="gl-anfrage-danke">' . esc_html(get_option('gl_anfragen_danke')) . '</p>';
    } elseif ($status === 'fehler') {
        $out .= '<p class="gl-anfrage-fehler" role="alert">Bitte Name, gueltige E-Mail und Nachricht angeben.</p>';
    }
    $ziel = esc_url(admin_url('admin-post.php'));
    $zurueck = esc_url(get_permalink());
    $out .= '<form class="gl-anfrage" method="post" action="' . $ziel . '">'
        . '<input type="hidden" name="action" value="gl_anfrage_senden">'
        . '<input type="hidden" name="zurueck" value="' . $zurueck . '">'
        . wp_nonce_field('gl_anfrage_senden', 'gl_nonce', true, false)
        . '<p><label for="gl-name">Name</label><input id="gl-name" name="gl_name" required maxlength="120"></p>'
        . '<p><label for="gl-email">E-Mail</label><input id="gl-email" name="gl_email" type="email" required maxlength="190"></p>'
        . '<p><label for="gl-nachricht">Nachricht</label><textarea id="gl-nachricht" name="gl_nachricht" required maxlength="5000"></textarea></p>'
        . '<p><button type="submit">Anfrage senden</button></p></form>';
    return $out;
}

add_action('admin_post_nopriv_gl_anfrage_senden', 'gl_anfragen_senden');
add_action('admin_post_gl_anfrage_senden', 'gl_anfragen_senden');
function gl_anfragen_senden() {
    if (!isset($_POST['gl_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['gl_nonce'])), 'gl_anfrage_senden')) {
        wp_die('Sicherheitspruefung fehlgeschlagen.', 'Abgewiesen', array('response' => 403));
    }
    $zurueck = isset($_POST['zurueck']) ? esc_url_raw(wp_unslash($_POST['zurueck'])) : home_url('/');
    $zurueck = wp_validate_redirect($zurueck, home_url('/'));
    $name = isset($_POST['gl_name']) ? sanitize_text_field(wp_unslash($_POST['gl_name'])) : '';
    $email = isset($_POST['gl_email']) ? sanitize_email(wp_unslash($_POST['gl_email'])) : '';
    $nachricht = isset($_POST['gl_nachricht']) ? sanitize_textarea_field(wp_unslash($_POST['gl_nachricht'])) : '';
    if ($name === '' || !is_email($email) || $nachricht === '' || mb_strlen($nachricht) > 5000) {
        wp_safe_redirect(add_query_arg('gl', 'fehler', $zurueck)); exit;
    }
    global $wpdb;
    $wpdb->insert(gl_anfragen_tabelle(), array(
        'erstellt' => current_time('mysql'), 'name' => mb_substr($name, 0, 120),
        'email' => $email, 'nachricht' => $nachricht,
    ), array('%s', '%s', '%s', '%s'));
    wp_safe_redirect(add_query_arg('gl', 'danke', $zurueck)); exit;
}

/* ---------- Admin: Einstellungen, Liste, CSV ---------- */
add_action('admin_init', function () {
    register_setting('gl_anfragen', 'gl_anfragen_danke', array(
        'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'Danke, wir melden uns.'));
    add_settings_section('gl_anfragen_haupt', 'Formular', '__return_false', 'gl-anfragen');
    add_settings_field('gl_anfragen_danke', 'Danke-Text', function () {
        printf('<input type="text" class="regular-text" name="gl_anfragen_danke" value="%s">',
            esc_attr(get_option('gl_anfragen_danke')));
    }, 'gl-anfragen', 'gl_anfragen_haupt');
});

add_action('admin_menu', function () {
    add_options_page('GL Anfragen', 'GL Anfragen', 'manage_options', 'gl-anfragen', 'gl_anfragen_einstellungen');
    add_menu_page('Anfragen', 'Anfragen', 'manage_options', 'gl-anfragen-liste', 'gl_anfragen_liste', 'dashicons-email');
});

function gl_anfragen_einstellungen() {
    if (!current_user_can('manage_options')) { return; }
    echo '<div class="wrap"><h1>GL Anfragen</h1><form method="post" action="options.php">';
    settings_fields('gl_anfragen');
    do_settings_sections('gl-anfragen');
    submit_button();
    echo '</form></div>';
}

function gl_anfragen_liste() {
    if (!current_user_can('manage_options')) { return; }
    global $wpdb;
    $zeilen = $wpdb->get_results('SELECT id, erstellt, name, email, nachricht FROM ' . gl_anfragen_tabelle() . ' ORDER BY id DESC LIMIT 200');
    $csv = wp_nonce_url(admin_url('admin-post.php?action=gl_anfragen_csv'), 'gl_anfragen_csv');
    echo '<div class="wrap"><h1>Anfragen</h1><p><a class="button" href="' . esc_url($csv) . '">CSV exportieren</a></p>';
    echo '<table class="widefat striped gl-anfragen"><thead><tr><th>ID</th><th>Datum</th><th>Name</th><th>E-Mail</th><th>Nachricht</th></tr></thead><tbody>';
    foreach ($zeilen as $z) {
        printf('<tr><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>', (int) $z->id, esc_html($z->erstellt),
            esc_html($z->name), esc_html($z->email), esc_html($z->nachricht));
    }
    echo '</tbody></table></div>';
}

function gl_anfragen_csv_wert($v) {
    $v = (string) $v;
    return ($v !== '' && strpos('=+-@', $v[0]) !== false) ? "'" . $v : $v;  // Formel-Injektion in Excel verhindern
}

add_action('admin_post_gl_anfragen_csv', function () {
    if (!current_user_can('manage_options')) { wp_die('Keine Berechtigung.', 'Abgewiesen', array('response' => 403)); }
    check_admin_referer('gl_anfragen_csv');
    global $wpdb;
    $zeilen = $wpdb->get_results('SELECT id, erstellt, name, email, nachricht FROM ' . gl_anfragen_tabelle() . ' ORDER BY id', ARRAY_A);
    nocache_headers();
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename=anfragen-' . gmdate('Y-m-d') . '.csv');
    $f = fopen('php://output', 'w');
    fwrite($f, "\xEF\xBB\xBF");
    fputcsv($f, array('id', 'erstellt', 'name', 'email', 'nachricht'), ';', '"', '\\');
    foreach ($zeilen as $z) { fputcsv($f, array_map('gl_anfragen_csv_wert', $z), ';', '"', '\\'); }
    fclose($f);
    exit;
});

So etwas für deinen Betrieb?

Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.

Zur Werkstatt