<?php
/**
 * mail/mail_example.php
 * 
 * Visa ett formulär, verifiera befintlighet och säkerhet på indata, skicka mejl.
 * 
 * @author     Lars Gunther <gunther@keryx.se>
 * @copyright  Lars Gunther <gunther@keryx.se>
 * @license    Creative Commons Attribution-Noncommercial-Share Alike 3.0
 *             {@link http://creativecommons.org/licenses/by-nc-sa/3.0/}
 * @package    bokdemo
 * @subpackage mail
 * @filesource
 *
 * @version    0.01 proof of concept
 */

// Vi har en session för att förhindra botar och dubbelpostning
session_start();

// Fixa ev. magic quotes som lätt förstör både mejltext och regexp-matchning
if ( get_magic_quotes_gpc() && ( ! ini_get('magic_quotes_sybase') ) ) {
    array_walk_recursive($_POST,'stripslashes');
}

/**
 * Denna funktion letar efter försök till SMTP-header injection i en sträng
 * 
 * @param $string Den sträng som skall undersökas
 * @return bool
 */
function smtp_header_injection_check($string) {
    // Regexp som letar efter rader som börjar med något skumt
    $regexp = "/^cc:|bcc:|to:|content-type:/im";
    return preg_match($regexp,$string);
}
/**
 * Denna funktion sätter $_SESSION['protect']
 * till ett extremt svårgissat värde
 * 
 * $return string Det som värdet satts till
 */
function set_session_protect() {
    return $_SESSION['protect'] = sha1(uniqid(mt_rand(), true));
}

/**
 * Denna variabel håller reda på sidans MIME-deklaration för http-huvudet
 * men den kan också användas för att meddela fel och förbjuden access
 * @var
 */
$HTTP_HEADER = 'text/html; charset=iso-8859-1'; // Byt till det du önskar

/**
 * Denna variabel håller reda på vem som står som mottgare till det mejl som skickas
 * @var
 */
$MAIL_RCPT = "foo.bar@aexample.com";

/**
 * Denna array håller koll på alla felmeddelanden
 * @var
 */
$ERROR_MSG = array();

/**
 * Denna variabel är vårt standardfelmeddelande
 * @var
 */
$STD_ERROR = 'Saknas eller felaktigt.';

/**
 * Den här funktionen används i mallen och skriver ut felmmeddelanden
 * 
 * Den använder den globala arrayen $ERROR_MSG
 * @param string $key Index till $ERROR_MSG
 * @return void
 */
function explain_error($key) {
    global $ERROR_MSG;
    if ( array_key_exists($key,$ERROR_MSG) ) {
        echo '<strong class="error">'.$ERROR_MSG[$key].'</strong>';
    }
}

/**
 * Denna array rymmer indata som gått igenom en förstakontroll
 * men ännu inte garanterat är "kosher"
 * @var
 */
$PROBABLY_SAFE_INDATA = array();

/**
 * Den här funktionen skriver ut ev. befintligt värde som ett attribut till en input tagg
 * 
 * Den använder den globala arrayen $PROBABLY_SAFE_INDATA
 * @param string $key Index till $PROBABLY_SAFE_INDATA
 * @return void
 */
function old_value($key) {
    global $PROBABLY_SAFE_INDATA;
    if ( array_key_exists($key,$PROBABLY_SAFE_INDATA) ) {
        echo ' value="'.$PROBABLY_SAFE_INDATA[$key].'"';
    }
}

// Kontroll om man kommit via post = formuläret har (troligen) skickats
if ( count($_POST) ) {
    
    // -------------------------- indata filtrering start ----------------------

    // Bort med onödig whitespace
    array_walk_recursive($_POST,'trim');

    // Kontrollera först att data skickats från det egna formuläret
    // och stoppa all dubbelpostning via reload-funktionen
    if ( $_POST['protect'] !== $_SESSION['protect'] ) {
        // Bot eller reload
        $HTTP_HEADER    = 'HTTP/1.1 403 forbidden'; // Botar skall få en felkod de förstår
        $bot_or_reload  = true;
    } else {
        $bot_or_reload = false;
        unset($_SESSION['protect']);

        $ERROR_MSG = array(); // Om vi hittar något fel fylls denna på

        // Kontrollera om det gjorts några regelrätta header-attacker
        $highly_suspicious  = array_filter($_POST,'smtp_header_injection_check');
        if ( count($highly_suspicious) ) {
            // Trolig attack!
            foreach ( $highly_suspicious as $key => $value ) {
                // Meddelandearean är meningslös att använda för injektion
                // så som mejlen plockas ihop i detta skript
                // och det kan faktiskt tänbkas att en förbjuden fras finns där utan illvilja
                if ( $key == 'msg' ) {
                    unset ($highly_suspicious[$key]);
                    continue;
                }
                $ERROR_MSG[$key] = 'Misstänkt försök till header-injektion.';
            }
            // Överväg att logga attacken: Tidpunkt, IP-nummer, etc.
        }
        // Vi är någorlunda säkra, men alla värden måste kollas också av användbarhetsskäl
        $PROBABLY_SAFE_INDATA = array_diff_key($_POST,$highly_suspicious);

        // Namn kan innehålla alla bokstäver + utländska dito + bindestreck
        // Vi kan alltså inte förlita oss på locale
        // Av detta skäl kan vi inte använda ctype_alpha eller preg_match med [[:alpha:]] eller \w
        // Jag nöjer mig med att alla tecken skall vara "synliga" i förnamnet
        if ( empty($ERROR_MSG['firstname']) ) {
            // Inget fel upptäckt ovan, vi gör nya kontroller
	        if ( empty($_POST['firstname']) || 
	             strlen($PROBABLY_SAFE_INDATA['firstname']) > 50 || 
	             !ctype_graph($PROBABLY_SAFE_INDATA['firstname']) )
	        {
	            $ERROR_MSG['firstname'] = $STD_ERROR;
	        } else {
	            $safe_indata['firstname'] =& $PROBABLY_SAFE_INDATA['firstname'];
	        }
        }
        // Vi måste ta bort tillåtna mellanslag för att kunna testa med ctype_graph
        if ( empty($ERROR_MSG['lastname']) ) {
	        $lastname_check = str_replace(' ','',$PROBABLY_SAFE_INDATA['lastname']);
	        if ( empty($_POST['lastname']) || 
	             strlen($PROBABLY_SAFE_INDATA['lastname']) > 50 || 
	             !ctype_graph($lastname_check) )
	        {
	            $ERROR_MSG['lastname'] = $STD_ERROR;
	        } else {
	            $safe_indata['lastname'] =& $PROBABLY_SAFE_INDATA['lastname'];
	        }
        }
        // Personnummer kollas högst bristfälligt men principen illustreras
        if ( empty($ERROR_MSG['socsecnum']) ) {
            // Äldre än hundra år? Plustecken!
            if ( empty($_POST['socsecnum']) || 
                !preg_match('/[0-9]{6}[+-][0-9]{4}/',$PROBABLY_SAFE_INDATA['socsecnum']) )
            {
                $ERROR_MSG['socsecnum'] = $STD_ERROR;
            } else {
                $safe_indata['socsecnum'] =& $PROBABLY_SAFE_INDATA['socsecnum'];
            }
        }
        // Postnummer kollas att de är av typen ### ##
        if ( empty($ERROR_MSG['zip']) ) {
            if ( empty($_POST['zip']) || 
                !preg_match('/^[0-9]{3} [0-9]{2}$/',$PROBABLY_SAFE_INDATA['zip']) )
            {
                $ERROR_MSG['zip'] = $STD_ERROR;
            } else {
                $safe_indata['zip'] =& $PROBABLY_SAFE_INDATA['zip'];
            }
        }
        // Telefonnummer kollas att de bara har siffror, +/- eller mellanslag
        if ( empty($ERROR_MSG['phone']) ) {
            if ( empty($_POST['phone']) || 
                !preg_match('/[0-9 +-]{10}/',$PROBABLY_SAFE_INDATA['phone']) )
            {
                $ERROR_MSG['phone'] = $STD_ERROR;
            } else {
                $safe_indata['phone'] =& $PROBABLY_SAFE_INDATA['zip'];
            }
        }
        // Svenska stadsnamn kan innehålla bindestreck och mellanslag
        if ( empty($ERROR_MSG['town']) ) {
	        $town_check = str_replace(' ','',$PROBABLY_SAFE_INDATA['town']);
	        if ( empty($_POST['town']) || 
	             strlen($PROBABLY_SAFE_INDATA['town']) > 50 || 
	             !ctype_graph($town_check) )
	        {
	            $ERROR_MSG['town'] = $STD_ERROR;
	        } else {
	            $safe_indata['town'] =& $PROBABLY_SAFE_INDATA['town'];
	        }
        }
        // Enkel mejladresskoll
        if ( empty($ERROR_MSG['mail']) ) {
	        if ( empty($_POST['mail']) ) {
	            $ERROR_MSG['mail'] = 'Värde saknas.';
	        } elseif ( !filter_var($PROBABLY_SAFE_INDATA['mail'],FILTER_VALIDATE_EMAIL) ) {
	            $ERROR_MSG['mail'] = 'Inte en fungerande address.';
	        } else {
	            $safe_indata['mail'] =& $PROBABLY_SAFE_INDATA['mail'];
	        }
        }
        // Meddelande är frivilligt, men viss kontroll skall göras
        if ( isset($PROBABLY_SAFE_INDATA['msg']) && 
             strlen($PROBABLY_SAFE_INDATA['msg']) > 1 
        ) {
            // Användaren har skrivit något, men vi tar bort ev. skadliga tecken
            $safe_indata['msg'] = filter_var($PROBABLY_SAFE_INDATA['msg'],
                                             FILTER_SANITIZE_STRING,
                                             FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH);
        }
        // -------------------------- indata filtrering slut ----------------------
    }
    if ( count($ERROR_MSG) || $bot_or_reload ) {
        // Fel finns - meddelandet skall inte skickas
        $MAIL_SENT = false;
    } else {
        // Plocka ihop meddelandet
        $message  = <<<MSG
En användare har registrerat sig.

Namn: {$safe_indata['firstname']} {$safe_indata['lastname']}
Personnummer: {$safe_indata['socsecnum']}
Postnummer och ort: {$safe_indata['zip']} {$safe_indata['town']}
Telefon: {$safe_indata['phone']}
Mejladdress: {$safe_indata['mail']}
Meddelande:
{$safe_indata['msg']}
MSG;
        $subject = 'Registrering på min webbplats';
        $from    = "FROM: {$safe_indata['firstname']} {$safe_indata['lastname']} <{$safe_indata['mail']}>";
    
        // Dags att skicka!
        $success = mail($MAIL_RCPT,$subject,$message,$from);
        if ( $success ) {
            $MAIL_SENT = 'success';
        } else {
            $MAIL_SENT  = 'failure';
            $HTTP_HEADER = 'HTTP/1.1 500 Internal Server Error';
        }
    }
        
} // endif count($_POST)
else {
    // Fräsh sidvisning
    $bot_or_reload = false;
    $MAIL_SENT = false;
}
// Skicka headers
header($HTTP_HEADER);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Demo av registrering via mejl</title>
<style type="text/css">
/*<![CDATA[*/
body {
    max-width: 50em;
    margin: auto; 
}
/* Inspiration: http://www.cssplay.co.uk/menu/form.html */
label {
    clear: left;
    display: block;
    float: left;
    text-align: right;
    padding-right: 10px;
    width: 150px;
    margin-bottom: 15px;
}
input {
    margin-bottom: 15px;
    width: 200px;
}
strong {
    margin-bottom: 15px;
}
.formlabelgroup {
    clear: left;
    display: block;
    float: left;
    text-align: right;
    padding-right: 10px;
    width: 150px;
}
.formlabelgroup label {
    clear: none;
    display: inline;
    float: none;
    width: auto;
    padding-right: 0;
}
input#zip {
    width: 40px;
    margin: 0 0 15px;
}
input#town {
    width: 150px;
    margin: 0 0 15px 6px;
}
textarea {
    width: 200px;
    height: 3em;
}
input[type=submit] {
    width: 100px;
    margin: 20px 0 0 130px;
}
/*]]>*/
</style>
  </head>
  <body>
<?php if ( $bot_or_reload ): ?>
    <h1>Dämpa dig en aning!</h1>
    <p>Om du ser detta, så har du antagligen tryckt på <q>ladda om</q> i din webbläsare.</p>
    <p>Alternativt har du kommit in på sidan via en postning från en icke-auktoriserad plats.</p>
    <p><a href="<?php echo $_SERVER['PHP_SELF']; ?>">Visa formuläret på nytt</a>.</p>

<?php elseif ( 'success' == $MAIL_SENT ): ?>
    <h1>Din registrering har nu skickats</h1>
    <p>Följande värden har sänts:</p>
    <dl>
      <dt>Förnamn:</dt>
      <dd><?php htmlspecialchars($kosher['firstname']); ?></dd>
      <!-- Fyll på med resten här -->
    </dl>

<?php elseif ( 'failure' == $MAIL_SENT ): ?>
    <h1>Tekniskt fel</h1>
    <p>Ditt meddelande kunde tyvärr inte skickas.</p>

<?php else: ?>
    <h1>Demo av registrering via mejl</h1>
    <form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
      <fieldset>
        <legend>Persondata</legend>
        <label for= "firstname">Förnamn</label>
        <input id = "firstname" name="firstname" type="text" maxlength="50"<?php old_value('firstname'); ?> />
        <?php explain_error('firstname'); ?>
        <br />
        <label for= "lastname">Efternamn</label>
        <input id = "lastname" name="lastname" type="text" maxlength="50"<?php old_value('lastname'); ?> />
        <?php explain_error('lastname'); ?>
        <br />
        <label for= "socsecnum">Personnummer</label>
        <input id = "socsecnum" name="socsecnum" type="text" maxlength="11" <?php old_value('socsecnum'); ?>/>
        <?php explain_error('socsecnum'); ?>
        <br />
        <div class="formlabelgroup">
          <label for="zip">Postnummer</label> och <label for="town">stad</label>
        </div>
        <input id = "zip" name="zip" type="text" maxlength="6" <?php old_value('zip'); 
        ?>/><?php explain_error('zip'); // Ingren whitespace mellan våra fält därför knepig kodstil
        ?><input id = "town" name="town" type="text" maxlength="50" <?php old_value('town'); ?>/>
        <?php explain_error('town'); ?>
        <br />
        <label for= "phone">Telefon</label>
        <input id = "phone" name="phone" type="text" maxlength="15" <?php old_value('phone'); ?>/>
        <?php explain_error('phone'); ?>
        <br />
        <label for= "mail">Mejladress</label>
        <input id = "mail" name="mail" type="text" maxlength="100" <?php old_value('mail'); ?>/>
        <?php explain_error('mail'); ?>
        <br />
        <label for= "msg"><abbr>Ev.</abbr> meddelande</label>
        <textarea name="msg"><?php if ( isset($PROBABLY_SAFE_INDATA['msg']) ):
            echo $PROBABLY_SAFE_INDATA['msg'];
        endif;
        ?></textarea><br />
        <input type="submit" value="Registrera" />
        <input type="hidden" name="protect" value="<?php echo set_session_protect(); ?>" />
      </fieldset>
    </form>
<?php endif; ?>
  </body>
</html>
