Extension:FormHandler send form by Email

From MediaWiki.org
Jump to: navigation, search
MediaWiki extensions manual - list
Crystal Clear action run.png
FormHandler send form by Email

Release status: beta

Implementation Tag
Description
Author(s) David Buchmann
dbu (at) users.sourceforge.net
Last version 0.2 (16.4.2007)
MediaWiki 1.5 - 1.12
License No license specified
Download see below
Check usage and version matrix

FormHandler send form by Email extension allows to create forms on wiki pages in a safe way. The contents can be sent by email. It sends all fields and the user IP and - if the user is logged in - his/her username. This extension is suitable for creating simple forms. If you need very sophisticated things, you are probably better off not doing it inside MediaWiki anyway.

History:

  • 0.2: Made it working with newer Mediawiki, having the UserMailer class: Objects for email addresses and changed order of from and to address. Thanks to the people commenting on the discussion page.
  • 0.1: Initial Version

This is not thouroughly tested. There is a version that should work with MediaWiki 1.6 and newer (tested with 1.8) and an older one that was tested with MediaWiki 1.5.7. The only difference between the versions lies in 0.2 using the UserMailer and MailAddress classes as required by MediaWiki since REL_1.6. If you have any questions/comments, please send the author an email.

Security Warning: You should use this extension only if page editing is restricted to logged in and selected people you can trust not to edit a page in a manner to send spam with the help of the extension. If somebody can propose a usable security scheme to control this problem, please tell.

Contents

Installation [edit]

To activate the extension, put the code into a file in your MediaWiki installation and include it from your LocalSettings.php with:

     include("$IP/extensions/FormHandler.php");

To avoid spam, you should make sure that only trusted users can edit the pages which use the extension.

Syntax [edit]

Version 0.1 and 0.2 support the field types text, textarea and select. Additionally hidden can be used to pass additional information with the Email. The syntax is very simple: [type]:(*) [param]="[value]", with the *, you mark entries which are required, the rest is considered optional. The form is included with the extension tag <form>. It has some attributes to configure the form:

  • name: A name to identify the form, useful if you use different forms on a page (required)
  • method: tells how to treat data (for now, only email is implemented) (required)
  • target: target for this form (with method email, this is the email address and required)
  • submit: if present, defines the text of the submit button, otherwise it is "Submit"
  • reset: if present, adds a reset button with its value as caption. Otherwise, no reset button is provided with the form.
  • email: if present, an email field is added to the top of the form with this attributes value as prompt. It is used as reply-to when sending the form. If sender is not set, it is also used as 'from' address.
  • sender: if present, must be a valid email address which is used as 'from' address to avoid problem with user specified email address. If it is present, it is used as 'from' instead of the user suplied email address in the email field.

It is an error to not set at least one of sender or email.

An Example [edit]

<form name="test" method="email" target="admin@domain.com" email="Your Email" sender="web@domain.com" submit="Send Inquiry" reset="Reset">
    hidden: name="testform" value="additional info"
    text:* name="name" prompt="Your [[RealName]]"
    select: name="category" prompt="Please select problem category" option="Linux Software" option="Linux Hardware"
    text:* name="summary" prompt="Problem summary"
    textarea: name="description" prompt="Problem description" rows="10" cols="50"
    select: name="priority" prompt="Priority" option="Low" option="Medium" option="High" value="Medium"
</form>


Implementation [edit]

Hidden fields are not passed to the client at all, but sent as defined in the wiki page. This avoids users changing them. If you have a reasonable example why this could be a problem, i can happily change the behaviour.

Known Bugs [edit]

There is a problem with server side caching of the output of our extension. For now, we pass action=purge with every request which seems to help.

Todo [edit]

  • Develop a security concept. It must ensure that only trusted users can use the extension in the wiki pages. At the very last, a configuration option should limit the possible addresses to send form data to. (This however would require the admin to edit the php file, which is complicated.)
  • Add more supported field types, e.g. radio and checkbox......DONE...check discussion page-- Shabnam Garg
  • It would be easy to implement writing to a file or into into a database.

Code of Form Handler 0.2 (Mediawiki 1.8 and newer) [edit]

For old versions of Mediawiki, please see below for Form Handler 0.1.

Copy the following into a file named extensions/FormHandler.php

<?php
 
/* FormHandler extension, Version 0.2
 * David Buchmann, 16.4.2006
 *
 * See http://meta.wikimedia.org/wiki/User:Dbu for explanations and updates.
 */
 
$wgExtensionFunctions[] = "wfFormHandler";
 
function wfFormHandler() {
  global $wgParser;
  # register the extension with the WikiText parser
  # the first parameter is the name of the new tag.
  # In this case it defines the tag <example> ... </example>
  # the second parameter is the callback function for
  # processing the text between the tags
  $wgParser->setHook( "form", "renderForm" );
}
 
/* The callback function for converting the input text to HTML output
 * $argv is an array containing any arguments passed to the
 * extension like <example argument="foo" bar>..
 * Put this on the sandbox page:  (works in MediaWiki 1.5.5)
 *   <example argument="foo" argument2="bar">Testing text **example** in between the new tags</example>
 */
function renderForm( $input, $argv) { // parser is not passed?? , &$wgParser ) {
  global $wgParser, $wgRequest;
 
  $handler = new FormHandler($wgParser, $wgRequest, $input, $argv);
  return $handler->render();
}
 
 
class FormHandler {
  var $defaultMethod = 'email';
  // Mediawiki objects
  var $request, $input, $argv;
  // Form parameters
  var $reset, $submit, $target, $sender, $email;
 
  /*
   * array of arrays with parsed field info.
   * type: text, hidden, textarea, select
   * name: name in form
   * required: whether this field is required
   * value: default value
   * option: array of options for select
   * prompt: text for prompt of that field
   */
  var $fields;
 
  function FormHandler(&$parser, $request, $input, $argv) {
    $parser->disableCache();
    $this->request = $request;
    $this->input = $input;
    $this->argv = $argv;
  }
 
 
  function parseInput() {
    //parse form setup
    $argv = $this->argv;
 
    $this->reset = isset($argv['reset']) ? $argv['reset'] : false;
    $this->submit = isset($argv['submit']) ? $argv['submit'] : 'Submit';    
    $this->email = isset($argv['email']) ? $argv['email'] : false;
    $this->sender = isset($argv['sender']) ? $argv['sender'] : false;
    $this->target = isset($argv['target']) ? $argv['target'] : false;
    $this->method = isset($argv['method']) ? $argv['method'] : $this->defaultMethod;
 
    $lines = preg_split('/(\r\n|\n|\r)/', $this->input);
    foreach($lines as $num=>$line) {
      $line = trim($line);
      if (strlen($line)==0) continue;
 
      $pos = strpos($line, ':');
      if (! $pos) { //0 or false:not found
        $this->fields[$num]['type'] = 'invalid';
        $this->fields[$num]['value'] = $line;
        continue;
      }
 
      $type = substr($line, 0, $pos);
 
      if ($line{$pos+1}==='*') {
        $required = true;
        $pos++;
      } else {
        $required = false;
      }
 
      $pattern = '/(\S+="[^"]*")/';
      $attributes = preg_split ($pattern, trim(substr($line, $pos+1)), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
      $atar = array();
      foreach($attributes as $attr) {
        list($name, $value) = explode('=', $attr, 2); //limit to 2: not break if attribute value contains =
        if ($name==='option') {
          $atar['option'][] = substr($value, 1, -1); //remove quotation marks
        } else {
          $atar[$name] = substr($value, 1, -1); //remove quotation marks
        }
      }
 
      if (isset($atar['name'])) {
        $this->fields[$atar['name']] = $atar;
        $this->fields[$atar['name']]['type'] = $type;
        if ($required) $this->fields[$atar['name']]['required'] = $required;
      } else {
        $this->fields[$num]['type'] = 'invalid';
        $this->fields[$num]['value'] = "No name: '$line'";
      }
    }
  }
 
  function render() {
    $this->parseInput();
    if ($this->sender===false && $this->email===false) 
      return 'Sorry, this form is invalid. Either sender or caption to get user email have to be set.';
    if ($this->method==='email') {
      if ($this->target===false)
        return 'Sorry, this form is invalid. For the email method, a target email adress must be specified.';
      if (! $this->isValidEmail($this->target)) 
        return 'Sorry, this form is invalid. For the email method, the target must be a valid email adress, however, '.$this->target.' is not valid';
 
        if ($this->sender!==false && ! $this->isValidEmail($this->sender)) 
          return 'Sorry, this form is invalid. The sender adress is invalid: '.$this->sender;
    }
 
    if ($this->request->wasPosted()) {
      return $this->submit();
    } else {
      return $this->show();
    }
 
  }
 
  function show($error=false) {
    global $wgOut, $wgTitle, $wgUser;
 
    $output='';
    if ($error !== false) $output = "<h2>An Error Occurred</h2><p>$error</p>";
 
    $output .= '<form action="'.$wgTitle->mTextform.'" method="post">
<input type="hidden" name="action" value="purge" />
              <table class="FormHandler">';
    if ($this->email !== false) $output .= '<tr><td>'.$this->email.'</td><td><input type="text" name="FormHandlerEmail" value="'. 
      (($this->request->getText('FormHandlerEmail') == '') ? $wgUser->mEmail : 
       $this->request->getText('FormHandlerEmail')) .
      '"/></td></tr>'."\n";
 
    foreach ($this->fields as $name => $field) {
      $output .= '<tr><td>'.$wgOut->parse($field['prompt'], false)."</td><td>\n";
 
      switch($field['type']) {
        case 'text':
          $output .= '<input type="text" name="FormHandler_'.$field['name'].'" value="'.$field['value'].'" />';
          break;
        case 'hidden':
          //saver not to pass by client at all. $output .= '<input type="hidden" name="FormHandler_'.$field['name'].'" value="'.$field['value'].'" />';   
          break;
        case 'textarea':
          $output .= '<textarea name="FormHandler_'.$field['name'].'">'.$field['value'].'</textarea>';
          break;
        case 'select':
          $output .= '<select name="FormHandler_'.$field['name'].'">';
          foreach($field['option'] as $option) {
            $output .= '<option '.($option===$field['value'] ? 'selected="true"' : '').">$option</option>";
          }
          $output .= '</select>';
          break;
        case 'invalid':
          $output .= "Could not understand line $name: '".$field['value']."'";
          break;
        default:
          $output .= 'Unknown field type '.$field['type'];
          break;
      }
      if (isset($field['required'])) $output .= '*';
      $output .= "</td></tr>\n";
    }
 
    $output .= '<tr><td style="text-align:center; padding-top:15px;">';
    if (isset($this->argv['reset'])) $output .= '<input type="reset" value="'.$this->argv['reset'].'" />';
    $output .= '</td><td style="text-align:center; padding-top:15px;"><input type="submit" /></td></tr>
     </table></form>';
    return $output;
  }
 
  function submit() {
    global $wgUser, $wgDBname, $wgIP;
    $error = '';
    foreach($this->fields as $field) {
      $this->fields[$field['name']]['value'] = $this->request->getText('FormHandler_'.$field['name']);
      if (isset($field['required'])) {
        if (empty($_POST['FormHandler_'.$field['name']])) {
          $error .= $field['prompt'] . '<br />'; //todo: better would be to highlight the fields. for this we would keep a list of required fields here.
        }
      }
    }
    if (! empty($error)) {
      return $this->show("Not all required fields have been filled out:<br />\n$error");
    }
 
    if ( 0 != $wgUser->getID() ) {
      $username = $wgUser->getName();
    } else {
      $username = '(not logged in)';
    }
 
    $usermail = $this->request->getText('FormHandlerEmail');
    if (empty($usermail)) $usermail=false;
 
 
    $message = 'Form '.$this->argv['name']." has been submitted by $username (IP: $wgIP, Email: " . ($usermail ? $usermail : 'not specified') .')
This Email is sent to you by MediaWiki FormHandler extension from http://'.$_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF']."\n\n"; 
 
    foreach($this->fields as $field) {
      $message .= $field['name'] . ': ';
      switch($field['type']) {
        case 'text':
        case 'select':
        case 'textarea':
          $value = $this->request->getText('FormHandler_'.$field['name']);
          break;
        case 'hidden':
          $value = $field['value']; //we do not put it into form and not treat it, but keep it at server side...
          break;
        case 'invalid':
          $value = 'There is an invalid line in the form: '.$field['value'];
          break;
        default:
          $value = 'Implementation Error in FormHandler: unexpected field type '.$field['type'];
          break;
      }
      $message .= (empty($value) ? '[not set]' : $value) . "\n";
    }
 
    switch ($this->argv['method']) {
      case 'email':
        require_once('UserMailer.php');
        if ($usermail!==false && ! $this->isValidEmail($usermail)) return $this->show('Your specified Email adress is invalid: '.$usermail); //sender is either == usermail or tested above
 
        if (! $this->sender) {
          if (! $usermail) return $this->show("The Email field is required, please fill in.");
          $this->sender=$usermail;
        }
 
        //16.4.2008: use new mailer class. new order $to, $from
        //fixme: there are no sanity checks for email adresses, neither here nor in MailAddress
        $error = UserMailer::send(new MailAddress($this->target), 
                             new MailAddress($this->sender),
                             'Contact form '.$this->argv['name'],
                             $message,
                             new MailAddress($usermail));
        if ($error===true) {
          return 'Thank you for sending a message to '.$this->target."<br /><br />\n&lt;pre>".nl2br($message).'&lt;/pre>';
        } else {
          return "Sorry, sending the form failed.\n" . $error->getMessage();
        }
        break;
      default:
        return 'Sorry, this is an invalid form, i do not know the method to store the information: '.$this->argv['method'];
    }
  }
  /* 
   * Check Email for validity, using a regular expression.
   */  
  function isValidEmail($candidate) {
    return (eregi("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $candidate));
  }
}

Code of Form Handler 0.1 (Mediawiki 1.5.7 and others) [edit]

Copy the following into a file named extensions/FormHandler.php

<?php
 
/* FormHandler extension, Version 0.1
 * David Buchmann, 16.3.2006
 *
 * See http://meta.wikimedia.org/wiki/User:Dbu for explanations and updates.
 */
 
$wgExtensionFunctions[] = "wfFormHandler";
 
function wfFormHandler() {
  global $wgParser;
  # register the extension with the WikiText parser
  # the first parameter is the name of the new tag.
  # In this case it defines the tag <example> ... </example>
  # the second parameter is the callback function for
  # processing the text between the tags
  $wgParser->setHook( "form", "renderForm" );
}
 
/* The callback function for converting the input text to HTML output
 * $argv is an array containing any arguments passed to the
 * extension like <example argument="foo" bar>..
 * Put this on the sandbox page:  (works in MediaWiki 1.5.5)
 *   <example argument="foo" argument2="bar">Testing text **example** in between the new tags</example>
 */
function renderForm( $input, $argv) { // parser is not passed?? , &$wgParser ) {
  global $wgParser, $wgRequest;
 
  $handler = new FormHandler($wgParser, $wgRequest, $input, $argv);
  return $handler->render();
}
 
 
class FormHandler {
  var $defaultMethod = 'email';
  // Mediawiki objects
  var $request, $input, $argv;
  // Form parameters
  var $reset, $submit, $target, $sender, $email;
 
  /*
   * array of arrays with parsed field info.
   * type: text, hidden, textarea, select
   * name: name in form
   * required: whether this field is required
   * value: default value
   * option: array of options for select
   * prompt: text for prompt of that field
   */
  var $fields;
 
  function FormHandler(&$parser, $request, $input, $argv) {
    $parser->disableCache();
    $this->request = $request;
    $this->input = $input;
    $this->argv = $argv;
  }
 
 
  function parseInput() {
    //parse form setup
    $argv = $this->argv;
 
    $this->reset = isset($argv['reset']) ? $argv['reset'] : false;
    $this->submit = isset($argv['submit']) ? $argv['submit'] : 'Submit';    
    $this->email = isset($argv['email']) ? $argv['email'] : false;
    $this->sender = isset($argv['sender']) ? $argv['sender'] : false;
    $this->target = isset($argv['target']) ? $argv['target'] : false;
    $this->method = isset($argv['method']) ? $argv['method'] : $this->defaultMethod;
 
    $lines = preg_split('/(\r\n|\n|\r)/', $this->input);
    foreach($lines as $num=>$line) {
      $line = trim($line);
      if (strlen($line)==0) continue;
 
      $pos = strpos($line, ':');
      if (! $pos) { //0 or false:not found
        $this->fields[$num]['type'] = 'invalid';
        $this->fields[$num]['value'] = $line;
        continue;
      }
 
      $type = substr($line, 0, $pos);
 
      if ($line{$pos+1}==='*') {
        $required = true;
        $pos++;
      } else {
        $required = false;
      }
 
      $pattern = '/(\S+="[^"]*")/';
      $attributes = preg_split ($pattern, trim(substr($line, $pos+1)), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
      $atar = array();
      foreach($attributes as $attr) {
        list($name, $value) = explode('=', $attr, 2); //limit to 2: not break if attribute value contains =
        if ($name==='option') {
          $atar['option'][] = substr($value, 1, -1); //remove quotation marks
        } else {
          $atar[$name] = substr($value, 1, -1); //remove quotation marks
        }
      }
 
      if (isset($atar['name'])) {
        $this->fields[$atar['name']] = $atar;
        $this->fields[$atar['name']]['type'] = $type;
        if ($required) $this->fields[$atar['name']]['required'] = $required;
      } else {
        $this->fields[$num]['type'] = 'invalid';
        $this->fields[$num]['value'] = "No name: '$line'";
      }
    }
  }
 
  function render() {
    $this->parseInput();
    if ($this->sender===false && $this->email===false) 
      return 'Sorry, this form is invalid. Either sender or caption to get user email have to be set.';
    if ($this->method==='email') {
      if ($this->target===false)
        return 'Sorry, this form is invalid. For the email method, a target email adress must be specified.';
      if (! $this->isValidEmail($this->target)) 
        return 'Sorry, this form is invalid. For the email method, the target must be a valid email adress, however, '.$this->target.' is not valid';
 
        if ($this->sender!==false && ! $this->isValidEmail($this->sender)) 
          return 'Sorry, this form is invalid. The sender adress is invalid: '.$this->sender;
    }
 
    if ($this->request->wasPosted()) {
      return $this->submit();
    } else {
      return $this->show();
    }
 
  }
 
  function show($error=false) {
    global $wgOut, $wgTitle, $wgUser;
 
    $output='';
    if ($error !== false) $output = "<h2>An Error Occurred</h2><p>$error</p>";
 
    $output .= '<form action="'.$wgTitle->mTextform.'" method="post">
<input type="hidden" name="action" value="purge" />
              <table class="FormHandler">';
    if ($this->email !== false) $output .= '<tr><td>'.$this->email.'</td><td><input type="text" name="FormHandlerEmail" value="'. 
      (($this->request->getText('FormHandlerEmail') == '') ? $wgUser->mEmail : 
       $this->request->getText('FormHandlerEmail')) .
      '"/></td></tr>'."\n";
 
    foreach ($this->fields as $name => $field) {
      $output .= '<tr><td>'.$wgOut->parse($field['prompt'], false)."</td><td>\n";
 
      switch($field['type']) {
        case 'text':
          $output .= '<input type="text" name="FormHandler_'.$field['name'].'" value="'.$field['value'].'" />';
          break;
        case 'hidden':
          //saver not to pass by client at all. $output .= '<input type="hidden" name="FormHandler_'.$field['name'].'" value="'.$field['value'].'" />';   
          break;
        case 'textarea':
          $output .= '<textarea name="FormHandler_'.$field['name'].'">'.$field['value'].'</textarea>';
          break;
        case 'select':
          $output .= '<select name="FormHandler_'.$field['name'].'">';
          foreach($field['option'] as $option) {
            $output .= '<option '.($option===$field['value'] ? 'selected="true"' : '').">$option</option>";
          }
          $output .= '</select>';
          break;
        case 'invalid':
          $output .= "Could not understand line $name: '".$field['value']."'";
          break;
        default:
          $output .= 'Unknown field type '.$field['type'];
          break;
      }
      if (isset($field['required'])) $output .= '*';
      $output .= "</td></tr>\n";
    }
 
    $output .= '<tr><td style="text-align:center; padding-top:15px;">';
    if (isset($this->argv['reset'])) $output .= '<input type="reset" value="'.$this->argv['reset'].'" />';
    $output .= '</td><td style="text-align:center; padding-top:15px;"><input type="submit" /></td></tr>
     </table></form>';
    return $output;
  }
 
  function submit() {
    global $wgUser, $wgDBname, $wgIP;
    $error = '';
    foreach($this->fields as $field) {
      $this->fields[$field['name']]['value'] = $this->request->getText('FormHandler_'.$field['name']);
      if (isset($field['required'])) {
        if (empty($_POST['FormHandler_'.$field['name']])) {
          $error .= $field['prompt'] . '<br />'; //todo: better would be to highlight the fields. for this we would keep a list of required fields here.
        }
      }
    }
    if (! empty($error)) {
      return $this->show("Not all required fields have been filled out:<br />\n$error");
    }
 
    if ( 0 != $wgUser->getID() ) {
      $username = $wgUser->getName();
    } else {
      $username = '(not logged in)';
    }
 
    $usermail = $this->request->getText('FormHandlerEmail');
    if (empty($usermail)) $usermail=false;
 
 
    $message = 'Form '.$this->argv['name']." has been submitted by $username (IP: $wgIP, Email: " . ($usermail ? $usermail : 'not specified') .')
This Email is sent to you by MediaWiki FormHandler extension from http://'.$_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF']."\n\n"; 
 
    foreach($this->fields as $field) {
      $message .= $field['name'] . ': ';
      switch($field['type']) {
        case 'text':
        case 'select':
        case 'textarea':
          $value = $this->request->getText('FormHandler_'.$field['name']);
          break;
        case 'hidden':
          $value = $field['value']; //we do not put it into form and not treat it, but keep it at server side...
          break;
        case 'invalid':
          $value = 'There is an invalid line in the form: '.$field['value'];
          break;
        default:
          $value = 'Implementation Error in FormHandler: unexpected field type '.$field['type'];
          break;
      }
      $message .= (empty($value) ? '[not set]' : $value) . "\n";
    }
 
    switch ($this->argv['method']) {
      case 'email':
        require_once('UserMailer.php');
        if ($usermail!==false && ! $this->isValidEmail($usermail)) return $this->show('Your specified Email adress is invalid: '.$usermail); //sender is either == usermail or tested above
 
        if (! $this->sender) {
          if (! $usermail) return $this->show("The Email field is required, please fill in.");
          $this->sender=$usermail;
        }
 
        $error = userMailer( $this->sender, 
                             $this->target, 
                             'Contact form '.$this->argv['name'],
                             $message,
                             $usermail);
        if (empty($error)) {
          return 'Thank you for sending a message to '.$this->target."<br /><br />\n&lt;pre>".nl2br($message).'&lt;/pre>';
        } else {
          return "Sorry, sending the form failed.\n" . htmlspecialchars($error);
        }
        break;
      default:
        return 'Sorry, this is an invalid form, i do not know the method to store the information: '.$this->argv['method'];
    }
  }
  /* 
   * Check Email for validity, using a regular expression.
   */  
  function isValidEmail($candidate) {
    return (eregi("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$", $candidate));
  }
}