Das runde Wordpress-Logo. Ein weisses W mit Serifen in einem hellblauen Kreis.

Wie du alle Formularfelder in WordPress abfangen und verarbeiten kannst

Oft werden in WordPress Formulare eingebunden, die Mails erzeugen, hochgeladene Dateien anhängen oder die Daten in irgendeiner Form verarbeiten. Ob für Bewerbungsformulare, Kursanmeldungen oder für einen einfachen Dateiaustausch zwischen Besuchern und den Seitenbetreibern.

Einige Formular-Plugins unterstützen jedoch nicht die Speicherung der gesendeten Daten. Noch problematischer wird es, wenn die Daten selbst in irgendeiner Weise weiter verarbeitet werden sollen. In diesem kleinen Tutorial möchte ich dir daher zeigen, wie du die Eingaben eines jeden Formulars inklusive aller Dateien separat abfangen und zusätzlich verarbeiten kannst.

Als Hilfestellung habe ich ein kleines Beispiel-Plugin erstellt, welches alle Eingaben und hochgeladenen Dateien abfängt und auf dem Server speichert. Über das Backend sind die Daten einsehbar und das Plugin konfigurierbar. Du kannst es zum Beispiel als Boilerplate für deine Projekte nutzen. Du findest das Plugin hier auf GitHub.

Das hier behandelte Herzstück des Plugins findest du in der Datei storage.php. Das Git-Repository werde ich von Zeit zu Zeit aktualisieren und Verbesserungen direkt dort veröffentlichen.

Update: Ich habe im Repository eine Unterstützung für Honeypod-Felder implementiert. So ist es möglich die Verarbeitung nur dann auszulösen, wenn diese nicht ausgefüllt wurden.

Die Formulardaten abfangen

Es gibt grob zwei Arten von Formular-Daten, die du in einem WordPress-Plugin mit PHP abgreifen kannst. Einmal die Formularfelder in Form der globalen Variablen $_GET, $_POST oder $_REQUEST und einmal die hochgeladenen Dateien die über die $_FILES Variable verfürbar sind.

Hierbei ist es wichtig, die Daten so früh wie möglich zu verarbeiten, damit diese nicht von den eigentlichen Formular-Plugins verändert werden. Die genannten Variablen sind nämlich theoretisch veränderbar. Einige Formular-Plugins löschen zum Beispiel die $_REQUEST Daten nach der Eingabe. Noch wichtiger ist es jedoch hochgeladene Dateien gleich am Anfang des WordPress Call-Stacks abzufangen, da viele Plugins die PHP-Funktion move_uploaded_file() verwenden nach deren Aufruf die Dateien für andere Verarbeitungsprozesse nicht mehr zur Verfügung stehen.

Deswegen beginnt das Abgreifen der Daten so früh wie möglich im WordPress-Call-Stack. Hierzu verwende ich die Action plugins_loaded. Das bedeutet ich greife die Daten ab, gleich nachdem alle Plugins geladen wurden, jedoch sonst noch nichts passiert ist. Hier ist der Code des ersten Hooks. Weiter unten erläutere ich die einzelnen Teile genauer:

$spfs_store_post_tmp = false;
$spfs_store_files_tmp = [];

//This is the earliest hook point in wordpress
//We need it because most of the form plugins will use the function move_uploaded_file
//After this function was called we are not able to grab the uploaded files
add_action('plugins_loaded', function(){

  global $spfs_store_post_tmp;
  global $spfs_store_files_tmp;

  $store_this_request = false;

  $record_trigger_keys = explode(',',get_option('spfs_record_trigger_keys','store_form'));

  foreach($record_trigger_keys as $record_trigger_key){

    $record_trigger_key = trim($record_trigger_key);

    foreach($_REQUEST as $key => $value){
      if ($key==$record_trigger_key) {
        $store_this_request = true;
        break;
      }
    }

  }

  //Get Post Vars
  if($store_this_request){//Store this request

    //Get allowed file extensions
    $allowed_file_extensions = explode(',',get_option('spfs_allowed_file_extensions','pdf, doc, docx, xls, xlsx, txt'));
    foreach($allowed_file_extensions as &$allowed_file_extension){
      $allowed_file_extension = trim($allowed_file_extension);
    }

    //Get allowed request keys
    $allowed_request_keys = explode(',',get_option('spfs_allowed_request_keys',''));

    //Remove whitespace
    foreach($allowed_request_keys as &$allowed_request_key){
      $allowed_request_key = trim($allowed_request_key);
    }

    //Build post text
    $post_text = '';
    foreach($_REQUEST as $key => $value){
      if (in_array($key,$allowed_request_keys)){//If this key is allowed
        if($value==''){
          $value='-----';
        }
        $post_text.='<b>'.$key.':</b> '.$value.'<br/>';
      }
    }

    //Temporary store the post data until the wordpress database is ready
    $spfs_store_post_tmp = [
      'post_title'    =>  'Form submission from '.date('d.m.Y').' at '.date('H:i:s'),
      'post_type'     =>  'sp_form_post',//Lets create a post type for this
      'post_excerpt'  =>  $post_text,
      'post_content'  =>  $post_text
    ];

    //Store uploaded files
    foreach ($_FILES as $key => $file) {

      if (in_array($key,$allowed_request_keys)){//If this is allowed

        if ($file['error'] == UPLOAD_ERR_OK) {

          //Get extension
          $file_extension = explode('.',$file["name"]);
          $file_extension = $file_extension[count($file_extension)-1];

          if (in_array($file_extension,$allowed_file_extensions)) {//Diese Dateiendung ist erlaubt

            $file_name = $post_id.'_'.$file["name"];//Name für die hochgeladene Datei erzeugen
            $uploaddir = wp_upload_dir();
            $uploaddir = $uploaddir['basedir'];

            //Copy the file instead of moving it because other plugins have to work with the files after it was uploaded
            //For example contactform7 will not be able to send the files if you use move_uploaded_file() here
            copy($file["tmp_name"], $uploaddir.'/sp-form-storage/'.$file_name);

            //Store the filedata temporary until the wp database is ready
            array_push($spfs_store_files_tmp, [
              'name' => $file_name,
              'original_name' => $file["name"],
              'download_hash' => md5(rand().time().'9585212882'),//Generate a hash with a seed. This is an random identifier for file downloads
              'request_key' => $key
            ]);

          }

        }

      }

    }

  }

});

Als erstes definiere ich zwei Variablen außerhalb der Action. In ihnen speichere ich die Daten, die ich später weiter verarbeiten möchte zunächst ab. Das ist wichtig, denn in dem ersten Hook kann ich zwar die Daten vor allen anderen Plugins abgreifen, jedoch stehen mir dort noch keine Datenbank-Funktionen zur Verfügung, da diese von WordPress noch nicht initialisiert wurden:

$spfs_store_post_tmp = false;
$spfs_store_files_tmp = [];

Weil ich nicht jeden Request von jedem Formular aufzeichnen möchte bzw. ich auch nicht möchte, dass das Plugin jede Anfrage von experimentierfreudigen Usern oder Bots aufzeichnet, habe ich einen Trigger entwickelt. Dieser prüft, ob ein bestimmter Request-Key gesetzt ist und lößt erst dadurch die Verarbeitung aus. Mit der folgenden Zeile hole ich mir aus meinen Plugin-Optionen die Keys, die eine Verarbeitung auslösen:

$record_trigger_keys = explode(',',get_option('spfs_record_trigger_keys','store_form'));

Wenn du dich für die Definition und Erstellung der Optionen interessierst, schau mal in die Datei plugin_options.php. Diese Methode hat sich bewährt denn in vielen Formularen kann man versteckte Felder hinzufügen und so eine Speicherung oder Verarbeitung im separaten Plugin erst auslösen.

Der nachfolgende Teil entscheidet nur, ob eine Verarbeitung der Daten stattfinden soll oder nicht:

foreach($record_trigger_keys as $record_trigger_key){

    $record_trigger_key = trim($record_trigger_key);

    foreach($_REQUEST as $key => $value){
      if ($key==$record_trigger_key) {
        $store_this_request = true;
        break;
      }
    }

  }

Ich habe zwei weitere Plugin-Optionen hinzugefügt. Darüber kann ich steuern, welche Request-Keys gespeichert werden sollen und welche Dateiendungen in der weiteren Verarbeitung erlaubt sind:

//Get allowed file extensions
    $allowed_file_extensions = explode(',',get_option('spfs_allowed_file_extensions','pdf, doc, docx, xls, xlsx, txt'));
    foreach($allowed_file_extensions as &$allowed_file_extension){
      $allowed_file_extension = trim($allowed_file_extension);
    }

    //Get allowed request keys
    $allowed_request_keys = explode(',',get_option('spfs_allowed_request_keys',''));

//Remove whitespace
    foreach($allowed_request_keys as &$allowed_request_key){
      $allowed_request_key = trim($allowed_request_key);
    }

Danach erstelle ich einen Post. Diesen speichere ich jedoch noch nicht in der Datenbank sondern in der zuvor definierten Variable außerhalb des Hook-Scopes. Er kann erst später gespeichert werden, wenn die Datenbank zur Verfügung steht:

//Build post text
    $post_text = '';
    foreach($_REQUEST as $key => $value){
      if (in_array($key,$allowed_request_keys)){//If this key is allowed
        if($value==''){
          $value='-----';
        }
        $post_text.='<b>'.$key.':</b> '.$value.'<br/>';
      }
    }

    //Temporary store the post data until the wordpress database is ready
    $spfs_store_post_tmp = [
      'post_title'    =>  'Form submission from '.date('d.m.Y').' at '.date('H:i:s'),
      'post_type'     =>  'sp_form_post',//Lets create a post type for this
      'post_excerpt'  =>  $post_text,
      'post_content'  =>  $post_text
    ];

Dann folgt die Speicherung der Dateien in einem separaten Ordner. Dabei fällt euch sicher auf, dass ich die Funktion move_uploaded_file() nicht verwendet habe. Denn nach ihrer Benutzung wären die Daten für das eigentliche Formular-Plugin nicht mehr verfügbar. Stattdessen verwende ich die Funktion copy um die Datei aus ihrem temporären Ordner heraus zu kopieren anstelle diese zu verschieben. Auch wenn die Datei später von einem anderen Plugin nicht weiter verarbeitet werden sollte: PHP löscht die temporären Dateien automatisch. Auch hier teste ich wieder ob der Feldname der Datei und deren Extension für die Verarbeitung vorgesehen ist:

//Store uploaded files
    foreach ($_FILES as $key => $file) {

      if (in_array($key,$allowed_request_keys)){//If this is allowed

        if ($file['error'] == UPLOAD_ERR_OK) {

          //Get extension
          $file_extension = explode('.',$file["name"]);
          $file_extension = $file_extension[count($file_extension)-1];

          if (in_array($file_extension,$allowed_file_extensions)) {//Diese Dateiendung ist erlaubt

            $file_name = $post_id.'_'.$file["name"];//Name für die hochgeladene Datei erzeugen
            $uploaddir = wp_upload_dir();
            $uploaddir = $uploaddir['basedir'];

            //Copy the file instead of moving it because other plugins have to work with the files after it was uploaded
            //For example contactform7 will not be able to send the files if you use move_uploaded_file() here
            copy($file["tmp_name"], $uploaddir.'/sp-form-storage/'.$file_name);

            //Store the filedata temporary until the wp database is ready
            array_push($spfs_store_files_tmp, [
              'name' => $file_name,
              'original_name' => $file["name"],
              'download_hash' => md5(rand().time().'9585212882'),//Generate a hash with a seed. This is an random identifier for file downloads
              'request_key' => $key
            ]);

          }

        }

      }

    }

Die Formulardaten weiter verarbeiten

Theoretisch kannst du die abgefangenen Daten auch gleich im plugins_loaded Hook weiter verarbeiten. Jedoch stehen zu diesem frühen Zeitpunkt einige WordPress-Funktionen noch nicht zur Verfügung. Daher habe ich für mein Demo-Plugin einen zweiten Hook angelegt, welcher die Daten aus den Variablen $spfs_store_post_tmp und $spfs_store_files_tmp holt und in der Datenbank abspeichert. Hierbei wird das gesendete Formular als eigener Post-Typ gespeichert und die Dateien in einer separaten Tabelle.

add_action('init', function(){

  global $spfs_store_post_tmp;
  global $spfs_store_files_tmp;

  //Now the wp database is ready. Lets insert the post and its files
  if($spfs_store_post_tmp){

    global $wpdb;

    $post_id = wp_insert_post($spfs_store_post_tmp);

    if(count($spfs_store_files_tmp)){

      foreach($spfs_store_files_tmp as $store_file){

        $store_file['wordpress_post'] = $post_id;

        //Add post meta for this file
        add_post_meta($post_id, 'post_file', $store_file['file_name']);

        //Insert file to db
        $count = $wpdb->insert( $wpdb->prefix.'spfs_files', $store_file);

      }
    }

  }

});

Wenn du dich für die Plugin-Optionen, das Setup oder den custom post type interessierst kannst du gerne noch einen Blick in die restlichen Dateien werfen.

Nachteile dieser Methode

Dadurch, dass die zusätzliche Verarbeitung der Daten in einem separaten Plugin stattfindet, gibt es leider keine wirklich gute Möglichkeit dem User Rückmeldungen über seine gesendeten Daten zu geben. Es wäre zwar denkbar einen Shortcode zu erstellen und diesen unter dem Formular anzuzeigen, aber eine wirkliche Interaktion mit dem Formular-Frontend ist evtl. nicht möglich. Eine serverseitige Validierung ist zwar grundsätzlich möglich, eine Fehlerausgabe im Formular aber wahrscheinlich nicht ohne größeren Aufwand machbar.

Bitte habt auch im Hinterkopf, dass evtl. schon Daten von diesem Plugin verarbeitet werden, obwohl das eigentliche Formularplugin einen Fehler anzeigt, da die Verarbeitung erfolgt bevor das jeweilige Formular-Plugin die Daten verarbeitet.

Titelbild: https://pixabay.com/de/vectors/wordpress-wordpress-logo-6942722/