Index: extensions/CryptograPHP/securimage/securimage.php
===================================================================
--- extensions/CryptograPHP/securimage/securimage.php	(revision 12617)
+++ extensions/CryptograPHP/securimage/securimage.php	(revision 26554)
@@ -1,13 +1,16 @@
 <?php
+
+// error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
+
 /**
- * Project:     Securimage: A PHP class for creating and managing form CAPTCHA images
- * File:        securimage.php
+ * Project:     Securimage: A PHP class for creating and managing form CAPTCHA images<br />
+ * File:        securimage.php<br />
  *
- * Copyright (c) 2011, Drew Phillips
+ * Copyright (c) 2013, Drew Phillips
  * All rights reserved.
- * 
+ *
  * Redistribution and use in source and binary forms, with or without modification,
  * are permitted provided that the following conditions are met:
- * 
+ *
  *  - Redistributions of source code must retain the above copyright notice,
  *    this list of conditions and the following disclaimer.
@@ -15,5 +18,5 @@
  *    this list of conditions and the following disclaimer in the documentation
  *    and/or other materials provided with the distribution.
- * 
+ *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@@ -29,7 +32,7 @@
  *
  * Any modifications to the library should be indicated clearly in the source code
- * to inform users that the changes are not a part of the original software.
+ * to inform users that the changes are not a part of the original software.<br /><br />
  *
- * If you found this script useful, please take a quick moment to rate it.
+ * If you found this script useful, please take a quick moment to rate it.<br />
  * http://www.hotscripts.com/rate/49400.html  Thanks.
  *
@@ -37,7 +40,7 @@
  * @link http://www.phpcaptcha.org/latest.zip Download Latest Version
  * @link http://www.phpcaptcha.org/Securimage_Docs/ Online Documentation
- * @copyright 2011 Drew Phillips
+ * @copyright 2013 Drew Phillips
  * @author Drew Phillips <drew@drew-phillips.com>
- * @version 3.0 (October 2011)
+ * @version 3.5.1 (June 21, 2013)
  * @package Securimage
  *
@@ -46,5 +49,51 @@
 /**
  ChangeLog
- 
+
+ 3.5.1
+ - Fix XSS vulnerability in example_form.php (discovered by Gjoko Krstic - <gjoko@zeroscience.mk>)
+
+ 3.5
+ - Release new version
+ - MB string support for charlist
+ - Modify audio file path to use language directories
+ - Changed default captcha appearance
+
+ 3.2RC4
+ - Add MySQL, PostgreSQL, and SQLite3 support for database storage
+ - Deprecate "use_sqlite_db" option and remove SQLite2/sqlite_* functions
+ - Add new captcha type that displays 2 dictionary words on one image
+ - Update examples
+
+ 3.2RC3
+ - Fix canSendHeaders() check which was breaking if a PHP startup error was issued
+
+ 3.2RC2
+ - Add error handler (https://github.com/dapphp/securimage/issues/15)
+ - Fix flash examples to use the correct value name for audio parameter
+
+ 3.2RC1
+ - New audio captcha code.  Faster, fully dynamic audio, full WAV support
+   (Paul Voegler, Drew Phillips) <http://voegler.eu/pub/audio>
+ - New Flash audio streaming button.  User defined image and size supported
+ - Additional options for customizing captcha (noise_level, send_headers,
+   no_exit, no_session, display_value
+ - Add captcha ID support.  Uses sqlite and unique captcha IDs to track captchas,
+   no session used
+ - Add static methods for creating and validating captcha by ID
+ - Automatic clearing of old codes from SQLite database
+
+ 3.0.3Beta
+ - Add improved mixing function to WavFile class (Paul Voegler)
+ - Improve performance and security of captcha audio (Paul Voegler, Drew Phillips)
+ - Add option to use random file as background noise in captcha audio
+ - Add new securimage options for audio files
+
+ 3.0.2Beta
+ - Fix issue with session variables when upgrading from 2.0 - 3.0
+ - Improve audio captcha, switch to use WavFile class, make mathematical captcha audio work
+
+ 3.0.1
+ - Bugfix: removed use of deprecated variable in addSignature method that would cause errors with display_errors on
+
  3.0
  - Rewrite class using PHP5 OOP
@@ -78,5 +127,5 @@
  - Audio output is mp3 format by default
  - Change font to AlteHaasGrotesk by yann le coroller
- - Some code cleanup 
+ - Some code cleanup
 
  1.0.4 (unreleased)
@@ -108,5 +157,5 @@
  * Securimage CAPTCHA Class.
  *
- * @version    3.0
+ * @version    3.5
  * @package    Securimage
  * @subpackage classes
@@ -116,8 +165,8 @@
 class Securimage
 {
-	// All of the public variables below are securimage options
-	// They can be passed as an array to the Securimage constructor, set below,
-	// or set from securimage_show.php and securimage_play.php
-	
+    // All of the public variables below are securimage options
+    // They can be passed as an array to the Securimage constructor, set below,
+    // or set from securimage_show.php and securimage_play.php
+
     /**
      * Renders captcha as a JPEG image
@@ -135,5 +184,5 @@
      */
     const SI_IMAGE_GIF  = 3;
-    
+
     /**
      * Create a normal alphanumeric captcha
@@ -146,5 +195,34 @@
      */
     const SI_CAPTCHA_MATHEMATIC = 1;
-    
+    /**
+     * Create a word based captcha using 2 words
+     * @var int
+     */
+    const SI_CAPTCHA_WORDS      = 2;
+
+    /**
+     * MySQL option identifier for database storage option
+     *
+     * @var string
+     */
+    const SI_DRIVER_MYSQL   = 'mysql';
+
+    /**
+     * PostgreSQL option identifier for database storage option
+     *
+     * @var string
+     */
+    const SI_DRIVER_PGSQL   = 'pgsql';
+
+    /**
+     * SQLite option identifier for database storage option
+     *
+     * @var string
+     */
+    const SI_DRIVER_SQLITE3 = 'sqlite';
+
+    /*%*********************************************************************%*/
+    // Properties
+
     /**
      * The width of the captcha image
@@ -180,19 +258,19 @@
     /**
      * The color of the noise that is drawn
-     * @var Securimage_Color 
+     * @var Securimage_Color
      */
     public $noise_color    = '#707070';
-    
+
     /**
      * How transparent to make the text 0 = completely opaque, 100 = invisible
      * @var int
      */
-    public $text_transparency_percentage = 50;
+    public $text_transparency_percentage = 20;
     /**
      * Whether or not to draw the text transparently, true = use transparency, false = no transparency
      * @var bool
      */
-    public $use_transparent_text         = false;
-    
+    public $use_transparent_text         = true;
+
     /**
      * The length of the captcha code
@@ -215,5 +293,5 @@
      */
     public $expiry_time    = 900;
-    
+
     /**
      * The session name securimage should use, only set this if your application uses a custom session name
@@ -222,5 +300,5 @@
      */
     public $session_name   = null;
-    
+
     /**
      * true to use the wordlist file, false to generate random captcha codes
@@ -233,16 +311,16 @@
      * @var double
      */
-    public $perturbation = 0.75;
+    public $perturbation = 0.85;
     /**
      * How many lines to draw over the captcha code to increase security
      * @var int
      */
-    public $num_lines    = 8;
+    public $num_lines    = 5;
     /**
      * The level of noise (random dots) to place on the image, 0-10
      * @var int
      */
-    public $noise_level  = 0;
-    
+    public $noise_level  = 2;
+
     /**
      * The signature text to draw on the bottom corner of the image
@@ -260,11 +338,84 @@
      */
     public $signature_font;
-    
-    /**
+
+    /**
+     * DO NOT USE!!!
      * Use an SQLite database to store data (for users that do not support cookies)
      * @var bool
+     * @see Securimage::$use_sqlite_db
+     * @deprecated 3.2RC4
      */
     public $use_sqlite_db = false;
-    
+
+    /**
+     * Use a database backend for code storage.
+     * Provides a fallback to users with cookies disabled.
+     * Required when using captcha IDs.
+     *
+     * @see Securimage::$database_driver
+     * @var bool
+     */
+    public $use_database = false;
+
+    /**
+     * Database driver to use for database support.
+     * Allowable values: 'mysql', 'pgsql', 'sqlite'.
+     * Default: sqlite
+     *
+     * @var string
+     */
+    public $database_driver = self::SI_DRIVER_SQLITE3;
+
+    /**
+     * Database host to connect to when using mysql or postgres
+     * On Linux use "localhost" for Unix domain socket, otherwise uses TCP/IP
+     * Does not apply to SQLite
+     *
+     * @var string
+     */
+    public $database_host   = 'localhost';
+
+    /**
+     * Database username for connection (mysql, postgres only)
+     * Default is an empty string
+     *
+     * @var string
+     */
+    public $database_user   = '';
+
+    /**
+     * Database password for connection (mysql, postgres only)
+     * Default is empty string
+     *
+     * @var string
+     */
+    public $database_pass   = '';
+
+    /**
+     * Name of the database to select (mysql, postgres only)
+     *
+     * @see Securimage::$database_file for SQLite
+     * @var string
+     */
+    public $database_name   = '';
+
+    /**
+     * Database table where captcha codes are stored
+     * Note: Securimage will attempt to create this table for you if it does
+     * not exist.  If the table cannot be created, an E_USER_WARNING is emitted.
+     *
+     * @var string
+     */
+    public $database_table  = 'captcha_codes';
+
+    /**
+     * Fully qualified path to the database file when using SQLite3.
+     * This value is only used when $database_driver == sqlite3 and does
+     * not apply when no database is used, or when using MySQL or PostgreSQL.
+     *
+     * @var string
+     */
+    public $database_file;
+
     /**
      * The type of captcha to create, either alphanumeric, or a math problem<br />
@@ -272,6 +423,6 @@
      * @var int
      */
-    public $captcha_type  = self::SI_CAPTCHA_STRING;
-    
+    public $captcha_type  = self::SI_CAPTCHA_STRING; // or self::SI_CAPTCHA_MATHEMATIC;
+
     /**
      * The captcha namespace, use this if you have multiple forms on a single page, blank if you do not use multiple forms on one page
@@ -281,5 +432,5 @@
      * // in securimage_show.php (create one show script for each form)
      * $img->namespace = 'contact_form';
-     * 
+     *
      * // in form validator
      * $img->namespace = 'contact_form';
@@ -290,5 +441,5 @@
      */
     public $namespace;
-    
+
     /**
      * The font file to use to draw the captcha code, leave blank for default font AHGBold.ttf
@@ -308,4 +459,5 @@
     /**
      * The path to the SQLite database file to use, if $use_sqlite_database = true, should be chmod 666
+     * @deprecated 3.2RC4
      * @var string
      */
@@ -315,29 +467,148 @@
      * @var string
      * <code>
-     * $img->audio_path = '/home/yoursite/public_html/securimage/audio/';
+     * $img->audio_path = '/home/yoursite/public_html/securimage/audio/en/';
      * </code>
      */
     public $audio_path;
-
-    
-    
+    /**
+     * The path to the directory containing audio files that will be selected
+     * randomly and mixed with the captcha audio.
+     *
+     * @var string
+     */
+    public $audio_noise_path;
+    /**
+     * Whether or not to mix background noise files into captcha audio (true = mix, false = no)
+     * Mixing random background audio with noise can help improve security of audio captcha.
+     * Default: securimage/audio/noise
+     *
+     * @since 3.0.3
+     * @see Securimage::$audio_noise_path
+     * @var bool
+     */
+    public $audio_use_noise;
+    /**
+     * The method and threshold (or gain factor) used to normalize the mixing with background noise.
+     * See http://www.voegler.eu/pub/audio/ for more information.
+     *
+     * Valid: <ul>
+     *     <li> >= 1 - Normalize by multiplying by the threshold (boost - positive gain). <br />
+     *            A value of 1 in effect means no normalization (and results in clipping). </li>
+     *     <li> <= -1 - Normalize by dividing by the the absolute value of threshold (attenuate - negative gain). <br />
+     *            A factor of 2 (-2) is about 6dB reduction in volume.</li>
+     *     <li> [0, 1) - (open inverval - not including 1) - The threshold
+     *            above which amplitudes are comressed logarithmically. <br />
+     *            e.g. 0.6 to leave amplitudes up to 60% "as is" and compress above. </li>
+     *     <li> (-1, 0) - (open inverval - not including -1 and 0) - The threshold
+     *            above which amplitudes are comressed linearly. <br />
+     *            e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above. </li></ul>
+     *
+     * Default: 0.6
+     *
+     * @since 3.0.4
+     * @var float
+     */
+    public $audio_mix_normalization = 0.6;
+    /**
+     * Whether or not to degrade audio by introducing random noise (improves security of audio captcha)
+     * Default: true
+     *
+     * @since 3.0.3
+     * @var bool
+     */
+    public $degrade_audio;
+    /**
+     * Minimum delay to insert between captcha audio letters in milliseconds
+     *
+     * @since 3.0.3
+     * @var float
+     */
+    public $audio_gap_min = 0;
+    /**
+     * Maximum delay to insert between captcha audio letters in milliseconds
+     *
+     * @since 3.0.3
+     * @var float
+     */
+    public $audio_gap_max = 600;
+
+    /**
+     * Captcha ID if using static captcha
+     * @var string Unique captcha id
+     */
+    protected static $_captchaId = null;
+
     protected $im;
     protected $tmpimg;
     protected $bgimg;
     protected $iscale = 5;
-    
-    protected $securimage_path = null;
-    
+
+    public $securimage_path = null;
+
+    /**
+     * The captcha challenge value (either the case-sensitive/insensitive word captcha, or the solution to the math captcha)
+     *
+     * @var string Captcha challenge value
+     */
     protected $code;
+
+    /**
+     * The display value of the captcha to draw on the image (the word captcha, or the math equation to present to the user)
+     *
+     * @var string Captcha display value to draw on the image
+     */
     protected $code_display;
-    
+
+    /**
+     * A value that can be passed to the constructor that can be used to generate a captcha image with a given value
+     * This value does not get stored in the session or database and is only used when calling Securimage::show().
+     * If a display_value was passed to the constructor and the captcha image is generated, the display_value will be used
+     * as the string to draw on the captcha image.  Used only if captcha codes are generated and managed by a 3rd party app/library
+     *
+     * @var string Captcha code value to display on the image
+     */
+    public $display_value;
+
+    /**
+     * Captcha code supplied by user [set from Securimage::check()]
+     *
+     * @var string
+     */
     protected $captcha_code;
-    protected $sqlite_handle;
-    
+
+    /**
+     * Flag that can be specified telling securimage not to call exit after generating a captcha image or audio file
+     *
+     * @var bool If true, script will not terminate; if false script will terminate (default)
+     */
+    protected $no_exit;
+
+    /**
+     * Flag indicating whether or not a PHP session should be started and used
+     *
+     * @var bool If true, no session will be started; if false, session will be started and used to store data (default)
+     */
+    protected $no_session;
+
+    /**
+     * Flag indicating whether or not HTTP headers will be sent when outputting captcha image/audio
+     *
+     * @var bool If true (default) headers will be sent, if false, no headers are sent
+     */
+    protected $send_headers;
+
+    /**
+     * PDO connection when a database is used
+     *
+     * @var resource
+     */
+    protected $pdo_conn;
+
+    // gd color resources that are allocated for drawing the image
     protected $gdbgcolor;
     protected $gdtextcolor;
     protected $gdlinecolor;
     protected $gdsignaturecolor;
-    
+
     /**
      * Create a new securimage object, pass options to set in the constructor.<br />
@@ -352,5 +623,5 @@
      *     'font_file' => Securimage::getPath() . '/custom.ttf'
      * );
-     * 
+     *
      * $img = new Securimage($options);
      * </code>
@@ -359,8 +630,15 @@
     {
         $this->securimage_path = dirname(__FILE__);
-        
+
         if (is_array($options) && sizeof($options) > 0) {
             foreach($options as $prop => $val) {
-                $this->$prop = $val;
+                if ($prop == 'captchaId') {
+                    Securimage::$_captchaId = $val;
+                    $this->use_database     = true;
+                } else if ($prop == 'use_sqlite_db') {
+                    trigger_error("The use_sqlite_db option is deprecated, use 'use_database' instead", E_USER_NOTICE);
+                } else {
+                    $this->$prop = $val;
+                }
             }
         }
@@ -372,43 +650,69 @@
         $this->signature_color = $this->initColor($this->signature_color, '#616161');
 
-        if ($this->ttf_file == null) {
+        if (is_null($this->ttf_file)) {
             $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
         }
-        
+
         $this->signature_font = $this->ttf_file;
-        
-        if ($this->wordlist_file == null) {
+
+        if (is_null($this->wordlist_file)) {
             $this->wordlist_file = $this->securimage_path . '/words/words.txt';
         }
-        
-        if ($this->sqlite_database == null) {
-            $this->sqlite_database = $this->securimage_path . '/database/securimage.sqlite';
-        }
-        
-        if ($this->audio_path == null) {
-            $this->audio_path = $this->securimage_path . '/audio/';
-        }
-        
-        if ($this->code_length == null || $this->code_length < 1) {
+
+        if (is_null($this->database_file)) {
+            $this->database_file = $this->securimage_path . '/database/securimage.sq3';
+        }
+
+        if (is_null($this->audio_path)) {
+            $this->audio_path = $this->securimage_path . '/audio/en/';
+        }
+
+        if (is_null($this->audio_noise_path)) {
+            $this->audio_noise_path = $this->securimage_path . '/audio/noise/';
+        }
+
+        if (is_null($this->audio_use_noise)) {
+            $this->audio_use_noise = true;
+        }
+
+        if (is_null($this->degrade_audio)) {
+            $this->degrade_audio = true;
+        }
+
+        if (is_null($this->code_length) || (int)$this->code_length < 1) {
             $this->code_length = 6;
         }
-        
-        if ($this->perturbation == null || !is_numeric($this->perturbation)) {
+
+        if (is_null($this->perturbation) || !is_numeric($this->perturbation)) {
             $this->perturbation = 0.75;
         }
-        
-        if ($this->namespace == null || !is_string($this->namespace)) {
+
+        if (is_null($this->namespace) || !is_string($this->namespace)) {
             $this->namespace = 'default';
         }
 
-        // Initialize session or attach to existing
-        if ( session_id() == '' ) { // no session has been started yet, which is needed for validation
-            if ($this->session_name != null && trim($this->session_name) != '') {
-                session_name(trim($this->session_name)); // set session name if provided
-            }
-            session_start();
-        }
-    }
-    
+        if (is_null($this->no_exit)) {
+            $this->no_exit = false;
+        }
+
+        if (is_null($this->no_session)) {
+            $this->no_session = false;
+        }
+
+        if (is_null($this->send_headers)) {
+            $this->send_headers = true;
+        }
+
+        if ($this->no_session != true) {
+            // Initialize session or attach to existing
+            if ( session_id() == '' ) { // no session has been started yet, which is needed for validation
+                if (!is_null($this->session_name) && trim($this->session_name) != '') {
+                    session_name(trim($this->session_name)); // set session name if provided
+                }
+                session_start();
+            }
+        }
+    }
+
     /**
      * Return the absolute path to the Securimage directory
@@ -419,14 +723,82 @@
         return dirname(__FILE__);
     }
-    
+
+    /**
+     * Generate a new captcha ID or retrieve the current ID
+     *
+     * @param $new bool If true, generates a new challenge and returns and ID
+     * @param $options array Additional options to be passed to Securimage.
+     * Must include database options if not set directly in securimage.php
+     *
+     * @return null|string Returns null if no captcha id set and new was false, or string captcha ID
+     */
+    public static function getCaptchaId($new = true, array $options = array())
+    {
+        if (is_null($new) || (bool)$new == true) {
+            $id = sha1(uniqid($_SERVER['REMOTE_ADDR'], true));
+            $opts = array('no_session'    => true,
+                          'use_database'  => true);
+            if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+            $si = new self($opts);
+            Securimage::$_captchaId = $id;
+            $si->createCode();
+
+            return $id;
+        } else {
+            return Securimage::$_captchaId;
+        }
+    }
+
+    /**
+     * Validate a captcha code input against a captcha ID
+     *
+     * @param string $id       The captcha ID to check
+     * @param string $value    The captcha value supplied by the user
+     * @param array  $options  Array of options to construct Securimage with.
+     * Options must include database options if they are not set in securimage.php
+     *
+     * @see Securimage::$database_driver
+     * @return bool true if the code was valid for the given captcha ID, false if not or if database failed to open
+     */
+    public static function checkByCaptchaId($id, $value, array $options = array())
+    {
+        $opts = array('captchaId'    => $id,
+                      'no_session'   => true,
+                      'use_database' => true);
+
+        if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+
+        $si = new self($opts);
+
+        if ($si->openDatabase()) {
+            $code = $si->getCodeFromDatabase();
+
+            if (is_array($code)) {
+                $si->code         = $code['code'];
+                $si->code_display = $code['code_disp'];
+            }
+
+            if ($si->check($value)) {
+                $si->clearCodeFromDatabase();
+
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
     /**
      * Used to serve a captcha image to the browser
      * @param string $background_image The path to the background image to use
-     * <code> 
+     * <code>
      * $img = new Securimage();
      * $img->code_length = 6;
      * $img->num_lines   = 5;
      * $img->noise_level = 5;
-     * 
+     *
      * $img->show(); // sends the image to browser
      * exit;
@@ -435,4 +807,6 @@
     public function show($background_image = '')
     {
+        set_error_handler(array(&$this, 'errorHandler'));
+
         if($background_image != '' && is_readable($background_image)) {
             $this->bgimg = $background_image;
@@ -441,5 +815,5 @@
         $this->doImage();
     }
-    
+
     /**
      * Check a submitted code against the stored value
@@ -461,8 +835,8 @@
         return $this->correct_code;
     }
-    
+
     /**
      * Output a wav file of the captcha code to the browser
-     * 
+     *
      * <code>
      * $img = new Securimage();
@@ -473,20 +847,96 @@
     public function outputAudioFile()
     {
-        $ext = 'wav'; // force wav - mp3 is insecure
-        
-        header("Content-Disposition: attachment; filename=\"securimage_audio.{$ext}\"");
-        header('Cache-Control: no-store, no-cache, must-revalidate');
-        header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
-        header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
-        header('Content-type: audio/x-wav');
-        
-        $audio = $this->getAudibleCode($ext);
-
-        header('Content-Length: ' . strlen($audio));
-
-        echo $audio;
-        exit;
-    }
-    
+        set_error_handler(array(&$this, 'errorHandler'));
+
+        require_once dirname(__FILE__) . '/WavFile.php';
+
+        try {
+            $audio = $this->getAudibleCode();
+        } catch (Exception $ex) {
+            if (($fp = @fopen(dirname(__FILE__) . '/si.error_log', 'a+')) !== false) {
+                fwrite($fp, date('Y-m-d H:i:s') . ': Securimage audio error "' . $ex->getMessage() . '"' . "\n");
+                fclose($fp);
+            }
+
+            $audio = $this->audioError();
+        }
+
+        if ($this->canSendHeaders() || $this->send_headers == false) {
+            if ($this->send_headers) {
+                $uniq = md5(uniqid(microtime()));
+                header("Content-Disposition: attachment; filename=\"securimage_audio-{$uniq}.wav\"");
+                header('Cache-Control: no-store, no-cache, must-revalidate');
+                header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
+                header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
+                header('Content-type: audio/x-wav');
+
+                if (extension_loaded('zlib')) {
+                    ini_set('zlib.output_compression', true);  // compress output if supported by browser
+                } else {
+                    header('Content-Length: ' . strlen($audio));
+                }
+            }
+
+            echo $audio;
+        } else {
+            echo '<hr /><strong>'
+                .'Failed to generate audio file, content has already been '
+                .'output.<br />This is most likely due to misconfiguration or '
+                .'a PHP error was sent to the browser.</strong>';
+        }
+
+        restore_error_handler();
+
+        if (!$this->no_exit) exit;
+    }
+
+    /**
+     * Return the code from the session or sqlite database if used.  If none exists yet, an empty string is returned
+     *
+     * @param $array bool   True to receive an array containing the code and properties
+     * @return array|string Array if $array = true, otherwise a string containing the code
+     */
+    public function getCode($array = false, $returnExisting = false)
+    {
+        $code = '';
+        $time = 0;
+        $disp = 'error';
+
+        if ($returnExisting && strlen($this->code) > 0) {
+            if ($array) {
+                return array('code' => $this->code,
+                             'display' => $this->code_display,
+                             'code_display' => $this->code_display,
+                             'time' => 0);
+            } else {
+                return $this->code;
+            }
+        }
+
+        if ($this->no_session != true) {
+            if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
+                    trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
+                if ($this->isCodeExpired(
+                        $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
+                    $code = $_SESSION['securimage_code_value'][$this->namespace];
+                    $time = $_SESSION['securimage_code_ctime'][$this->namespace];
+                    $disp = $_SESSION['securimage_code_disp'] [$this->namespace];
+                }
+            }
+        }
+
+        if (empty($code) && $this->use_database) {
+            // no code in session - may mean user has cookies turned off
+            $this->openDatabase();
+            $code = $this->getCodeFromDatabase();
+        } else { /* no code stored in session or sqlite database, validation will fail */ }
+
+        if ($array == true) {
+            return array('code' => $code, 'ctime' => $time, 'display' => $disp);
+        } else {
+            return $code;
+        }
+    }
+
     /**
      * The main image drawing routing, responsible for constructing the entire image and serving it
@@ -499,8 +949,8 @@
             $imagecreate = 'imagecreate';
         }
-        
+
         $this->im     = $imagecreate($this->image_width, $this->image_height);
         $this->tmpimg = $imagecreate($this->image_width * $this->iscale, $this->image_height * $this->iscale);
-        
+
         $this->allocateColors();
         imagepalettecopy($this->tmpimg, $this->im);
@@ -508,12 +958,41 @@
         $this->setBackground();
 
-        $this->createCode();
-        
-        $this->drawWord();
+        $code = '';
+
+        if ($this->getCaptchaId(false) !== null) {
+            // a captcha Id was supplied
+
+            // check to see if a display_value for the captcha image was set
+            if (is_string($this->display_value) && strlen($this->display_value) > 0) {
+                $this->code_display = $this->display_value;
+                $this->code         = ($this->case_sensitive) ?
+                                       $this->display_value   :
+                                       strtolower($this->display_value);
+                $code = $this->code;
+            } else if ($this->openDatabase()) {
+                // no display_value, check the database for existing captchaId
+                $code = $this->getCodeFromDatabase();
+
+                // got back a result from the database with a valid code for captchaId
+                if (is_array($code)) {
+                    $this->code         = $code['code'];
+                    $this->code_display = $code['code_disp'];
+                    $code = $code['code'];
+                }
+            }
+        }
+
+        if ($code == '') {
+            // if the code was not set using display_value or was not found in
+            // the database, create a new code
+            $this->createCode();
+        }
 
         if ($this->noise_level > 0) {
             $this->drawNoise();
         }
-        
+
+        $this->drawWord();
+
         if ($this->perturbation > 0 && is_readable($this->ttf_file)) {
             $this->distortedCopy();
@@ -530,5 +1009,5 @@
         $this->output();
     }
-    
+
     /**
      * Allocate the colors to be used for the image
@@ -541,7 +1020,7 @@
                                               $this->image_bg_color->g,
                                               $this->image_bg_color->b);
-        
+
         $alpha = intval($this->text_transparency_percentage / 100 * 127);
-        
+
         if ($this->use_transparent_text == true) {
             $this->gdtextcolor = imagecolorallocatealpha($this->im,
@@ -574,5 +1053,5 @@
                                                           $this->noise_color->b);
         }
-    
+
         $this->gdsignaturecolor = imagecolorallocate($this->im,
                                                      $this->signature_color->r,
@@ -581,5 +1060,5 @@
 
     }
-    
+
     /**
      * The the background color, or background image to be used
@@ -594,7 +1073,7 @@
                              $this->image_width * $this->iscale, $this->image_height * $this->iscale,
                              $this->gdbgcolor);
-    
+
         if ($this->bgimg == '') {
-            if ($this->background_directory != null && 
+            if ($this->background_directory != null &&
                 is_dir($this->background_directory) &&
                 is_readable($this->background_directory))
@@ -606,5 +1085,5 @@
             }
         }
-        
+
         if ($this->bgimg == '') {
             return;
@@ -612,5 +1091,5 @@
 
         $dat = @getimagesize($this->bgimg);
-        if($dat == false) { 
+        if($dat == false) {
             return;
         }
@@ -629,5 +1108,5 @@
                          imagesx($newim), imagesy($newim));
     }
-    
+
     /**
      * Scan the directory for a background image to use
@@ -645,5 +1124,5 @@
 
             if (sizeof($images) > 0) {
-                return rtrim($this->background_directory, '/') . '/' . $images[rand(0, sizeof($images)-1)];
+                return rtrim($this->background_directory, '/') . '/' . $images[mt_rand(0, sizeof($images)-1)];
             }
         }
@@ -651,9 +1130,9 @@
         return false;
     }
-    
+
     /**
      * Generates the code or math problem and saves the value to the session
      */
-    protected function createCode()
+    public function createCode()
     {
         $this->code = false;
@@ -662,20 +1141,28 @@
             case self::SI_CAPTCHA_MATHEMATIC:
             {
-                $signs = array('+', '-', 'x');
-                $left  = rand(1, 10);
-                $right = rand(1, 5);
-                $sign  = $signs[rand(0, 2)];
-                
-                switch($sign) {
-                    case 'x': $c = $left * $right; break;
-                    case '-': $c = $left - $right; break;
-                    default:  $c = $left + $right; break;
-                }
-                
+                do {
+                    $signs = array('+', '-', 'x');
+                    $left  = mt_rand(1, 10);
+                    $right = mt_rand(1, 5);
+                    $sign  = $signs[mt_rand(0, 2)];
+
+                    switch($sign) {
+                        case 'x': $c = $left * $right; break;
+                        case '-': $c = $left - $right; break;
+                        default:  $c = $left + $right; break;
+                    }
+                } while ($c <= 0); // no negative #'s or 0
+
                 $this->code         = $c;
                 $this->code_display = "$left $sign $right";
                 break;
             }
-            
+
+            case self::SI_CAPTCHA_WORDS:
+                $words = $this->readCodeFromFile(2);
+                $this->code = implode(' ', $words);
+                $this->code_display = $this->code;
+                break;
+
             default:
             {
@@ -687,13 +1174,13 @@
                     $this->code = $this->generateCode($this->code_length);
                 }
-                
+
                 $this->code_display = $this->code;
                 $this->code         = ($this->case_sensitive) ? $this->code : strtolower($this->code);
             } // default
         }
-        
+
         $this->saveData();
     }
-    
+
     /**
      * Draws the captcha code on the image
@@ -703,5 +1190,5 @@
         $width2  = $this->image_width * $this->iscale;
         $height2 = $this->image_height * $this->iscale;
-         
+
         if (!is_readable($this->ttf_file)) {
             imagestring($this->im, 4, 10, ($this->image_height / 2) - 5, 'Failed to load TTF font file!', $this->gdtextcolor);
@@ -727,11 +1214,11 @@
             }
         }
-        
+
         // DEBUG
         //$this->im = $this->tmpimg;
         //$this->output();
-        
-    }
-    
+
+    }
+
     /**
      * Copies the captcha image to the final image with distortion applied
@@ -742,11 +1229,11 @@
         // make array of poles AKA attractor points
         for ($i = 0; $i < $numpoles; ++ $i) {
-            $px[$i]  = rand($this->image_width  * 0.2, $this->image_width  * 0.8);
-            $py[$i]  = rand($this->image_height * 0.2, $this->image_height * 0.8);
-            $rad[$i] = rand($this->image_height * 0.2, $this->image_height * 0.8);
+            $px[$i]  = mt_rand($this->image_width  * 0.2, $this->image_width  * 0.8);
+            $py[$i]  = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
+            $rad[$i] = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
             $tmp     = ((- $this->frand()) * 0.15) - .15;
             $amp[$i] = $this->perturbation * $tmp;
         }
-        
+
         $bgCol = imagecolorat($this->tmpimg, 0, 0);
         $width2 = $this->iscale * $this->image_width;
@@ -784,5 +1271,5 @@
         }
     }
-    
+
     /**
      * Draws distorted lines on the image
@@ -793,11 +1280,11 @@
             $x = $this->image_width * (1 + $line) / ($this->num_lines + 1);
             $x += (0.5 - $this->frand()) * $this->image_width / $this->num_lines;
-            $y = rand($this->image_height * 0.1, $this->image_height * 0.9);
-            
+            $y = mt_rand($this->image_height * 0.1, $this->image_height * 0.9);
+
             $theta = ($this->frand() - 0.5) * M_PI * 0.7;
             $w = $this->image_width;
-            $len = rand($w * 0.4, $w * 0.7);
-            $lwid = rand(0, 2);
-            
+            $len = mt_rand($w * 0.4, $w * 0.7);
+            $lwid = mt_rand(0, 2);
+
             $k = $this->frand() * 0.6 + 0.2;
             $k = $k * $k * 0.5;
@@ -810,8 +1297,8 @@
             $x0 = $x - 0.5 * $len * cos($theta);
             $y0 = $y - 0.5 * $len * sin($theta);
-            
+
             $ldx = round(- $dy * $lwid);
             $ldy = round($dx * $lwid);
-            
+
             for ($i = 0; $i < $n; ++ $i) {
                 $x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
@@ -821,5 +1308,5 @@
         }
     }
-    
+
     /**
      * Draws random noise on the image
@@ -834,22 +1321,22 @@
 
         $t0 = microtime(true);
-        
+
         $noise_level *= 125; // an arbitrary number that works well on a 1-10 scale
-        
+
         $points = $this->image_width * $this->image_height * $this->iscale;
         $height = $this->image_height * $this->iscale;
         $width  = $this->image_width * $this->iscale;
         for ($i = 0; $i < $noise_level; ++$i) {
-            $x = rand(10, $width);
-            $y = rand(10, $height);
-            $size = rand(7, 10);
+            $x = mt_rand(10, $width);
+            $y = mt_rand(10, $height);
+            $size = mt_rand(7, 10);
             if ($x - $size <= 0 && $y - $size <= 0) continue; // dont cover 0,0 since it is used by imagedistortedcopy
             imagefilledarc($this->tmpimg, $x, $y, $size, $size, 0, 360, $this->gdnoisecolor, IMG_ARC_PIE);
         }
-        
+
         $t1 = microtime(true);
-        
+
         $t = $t1 - $t0;
-        
+
         /*
         // DEBUG
@@ -860,79 +1347,105 @@
         */
     }
-    
-	/**
-	* Print signature text on image
-	*/
+
+    /**
+    * Print signature text on image
+    */
     protected function addSignature()
     {
-        if ($this->use_gd_font) {
-            imagestring($this->im, 5, $this->image_width - (strlen($this->image_signature) * 10), $this->image_height - 20, $this->image_signature, $this->gdsignaturecolor);
+        $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
+        $textlen = $bbox[2] - $bbox[0];
+        $x = $this->image_width - $textlen - 5;
+        $y = $this->image_height - 3;
+
+        imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
+    }
+
+    /**
+     * Sends the appropriate image and cache headers and outputs image to the browser
+     */
+    protected function output()
+    {
+        if ($this->canSendHeaders() || $this->send_headers == false) {
+            if ($this->send_headers) {
+                // only send the content-type headers if no headers have been output
+                // this will ease debugging on misconfigured servers where warnings
+                // may have been output which break the image and prevent easily viewing
+                // source to see the error.
+                header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+                header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
+                header("Cache-Control: no-store, no-cache, must-revalidate");
+                header("Cache-Control: post-check=0, pre-check=0", false);
+                header("Pragma: no-cache");
+            }
+
+            switch ($this->image_type) {
+                case self::SI_IMAGE_JPEG:
+                    if ($this->send_headers) header("Content-Type: image/jpeg");
+                    imagejpeg($this->im, null, 90);
+                    break;
+                case self::SI_IMAGE_GIF:
+                    if ($this->send_headers) header("Content-Type: image/gif");
+                    imagegif($this->im);
+                    break;
+                default:
+                    if ($this->send_headers) header("Content-Type: image/png");
+                    imagepng($this->im);
+                    break;
+            }
         } else {
-             
-            $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
-            $textlen = $bbox[2] - $bbox[0];
-            $x = $this->image_width - $textlen - 5;
-            $y = $this->image_height - 3;
-             
-            imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
-        }
-    }
-    
-    /**
-     * Sends the appropriate image and cache headers and outputs image to the browser
-     */
-    protected function output()
-    {
-        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
-        header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
-        header("Cache-Control: no-store, no-cache, must-revalidate");
-        header("Cache-Control: post-check=0, pre-check=0", false);
-        header("Pragma: no-cache");
-        
-        switch ($this->image_type) {
-            case self::SI_IMAGE_JPEG:
-                header("Content-Type: image/jpeg");
-                imagejpeg($this->im, null, 90);
-                break;
-            case self::SI_IMAGE_GIF:
-                header("Content-Type: image/gif");
-                imagegif($this->im);
-                break;
-            default:
-                header("Content-Type: image/png");
-                imagepng($this->im);
-                break;
-        }
-        
+            echo '<hr /><strong>'
+                .'Failed to generate captcha image, content has already been '
+                .'output.<br />This is most likely due to misconfiguration or '
+                .'a PHP error was sent to the browser.</strong>';
+        }
+
         imagedestroy($this->im);
-        exit();
-    }
-    
+        restore_error_handler();
+
+        if (!$this->no_exit) exit;
+    }
+
     /**
      * Gets the code and returns the binary audio file for the stored captcha code
-     * @param string $format WAV only
-     */
-    protected function getAudibleCode($format = 'wav')
-    {
-        // override any format other than wav for now
-        // this is due to security issues with MP3 files
-        $format  = 'wav';
-        
+     *
+     * @return The audio representation of the captcha in Wav format
+     */
+    protected function getAudibleCode()
+    {
         $letters = array();
-        $code    = $this->getCode();
-
-        if ($code == '') {
-            $this->createCode();
-            $code = $this->getCode();
-        }
-
-        for($i = 0; $i < strlen($code); ++$i) {
-            $letters[] = $code{$i};
-        }
-        
-        if ($format == 'mp3') {
-            return $this->generateMP3($letters);
+        $code    = $this->getCode(true, true);
+
+        if ($code['code'] == '') {
+            if (strlen($this->display_value) > 0) {
+                $code = array('code' => $this->display_value, 'display' => $this->display_value);
+            } else {
+                $this->createCode();
+                $code = $this->getCode(true);
+            }
+        }
+
+        if (preg_match('/(\d+) (\+|-|x) (\d+)/i', $code['display'], $eq)) {
+            $math = true;
+
+            $left  = $eq[1];
+            $sign  = str_replace(array('+', '-', 'x'), array('plus', 'minus', 'times'), $eq[2]);
+            $right = $eq[3];
+
+            $letters = array($left, $sign, $right);
         } else {
+            $math = false;
+
+            $length = strlen($code['display']);
+
+            for($i = 0; $i < $length; ++$i) {
+                $letter    = $code['display']{$i};
+                $letters[] = $letter;
+            }
+        }
+
+        try {
             return $this->generateWAV($letters);
+        } catch(Exception $ex) {
+            throw $ex;
         }
     }
@@ -941,7 +1454,7 @@
      * Gets a captcha code from a wordlist
      */
-    protected function readCodeFromFile()
-    {
-        $fp = @fopen($this->wordlist_file, 'rb');
+    protected function readCodeFromFile($numWords = 1)
+    {
+        $fp = fopen($this->wordlist_file, 'rb');
         if (!$fp) return false;
 
@@ -949,21 +1462,36 @@
         if ($fsize < 128) return false; // too small of a list to be effective
 
-        fseek($fp, rand(0, $fsize - 64), SEEK_SET); // seek to a random position of file from 0 to filesize-64
-        $data = fread($fp, 64); // read a chunk from our random position
+        if ((int)$numWords < 1 || (int)$numWords > 5) $numWords = 1;
+
+        $words = array();
+        $i = 0;
+        do {
+            fseek($fp, mt_rand(0, $fsize - 64), SEEK_SET); // seek to a random position of file from 0 to filesize-64
+            $data = fread($fp, 64); // read a chunk from our random position
+            $data = preg_replace("/\r?\n/", "\n", $data);
+
+            $start = @strpos($data, "\n", mt_rand(0, 56)) + 1; // random start position
+            $end   = @strpos($data, "\n", $start);          // find end of word
+
+            if ($start === false) {
+                // picked start position at end of file
+                continue;
+            } else if ($end === false) {
+                $end = strlen($data);
+            }
+
+            $word = strtolower(substr($data, $start, $end - $start)); // return a line of the file
+            $words[] = $word;
+        } while (++$i < $numWords);
+
         fclose($fp);
-        $data = preg_replace("/\r?\n/", "\n", $data);
-
-        $start = @strpos($data, "\n", rand(0, 56)) + 1; // random start position
-        $end   = @strpos($data, "\n", $start);          // find end of word
-        
-        if ($start === false) {
-            return false;
-        } else if ($end === false) {
-            $end = strlen($data);
-        }
-
-        return strtolower(substr($data, $start, $end - $start)); // return a line of the file
-    }
-    
+
+        if ($numWords < 2) {
+            return $words[0];
+        } else {
+            return $words;
+        }
+    }
+
     /**
      * Generates a random captcha code from the set character set
@@ -973,13 +1501,17 @@
         $code = '';
 
-        for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
-            $code .= $this->charset{rand(0, $cslen - 1)};
-        }
-        
-        //return 'testing';  // debug, set the code to given string
-        
+        if (function_exists('mb_strlen')) {
+            for($i = 1, $cslen = mb_strlen($this->charset); $i <= $this->code_length; ++$i) {
+                $code .= mb_substr($this->charset, mt_rand(0, $cslen - 1), 1, 'UTF-8');
+            }
+        } else {
+            for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
+                $code .= substr($this->charset, mt_rand(0, $cslen - 1), 1);
+            }
+        }
+
         return $code;
     }
-    
+
     /**
      * Checks the entered code against the value stored in the session or sqlite database, handles case sensitivity
@@ -988,8 +1520,12 @@
     protected function validate()
     {
-        $code = $this->getCode();
-        // returns stored code, or an empty string if no stored code was found
-        // checks the session and sqlite database if enabled
-        
+        if (!is_string($this->code) || strlen($this->code) == 0) {
+            $code = $this->getCode();
+            // returns stored code, or an empty string if no stored code was found
+            // checks the session and database if enabled
+        } else {
+            $code = $this->code;
+        }
+
         if ($this->case_sensitive == false && preg_match('/[A-Z]/', $code)) {
             // case sensitive was set from securimage_show.php but not in class
@@ -997,133 +1533,348 @@
             $this->case_sensitive = true;
         }
-        
+
         $code_entered = trim( (($this->case_sensitive) ? $this->code_entered
                                                        : strtolower($this->code_entered))
                         );
         $this->correct_code = false;
-        
+
         if ($code != '') {
+            if (strpos($code, ' ') !== false) {
+                // for multi word captchas, remove more than once space from input
+                $code_entered = preg_replace('/\s+/', ' ', $code_entered);
+                $code_entered = strtolower($code_entered);
+            }
+
             if ($code == $code_entered) {
                 $this->correct_code = true;
-                $_SESSION['securimage_code_value'][$this->namespace] = '';
-                $_SESSION['securimage_code_ctime'][$this->namespace] = '';
+                if ($this->no_session != true) {
+                    $_SESSION['securimage_code_value'][$this->namespace] = '';
+                    $_SESSION['securimage_code_ctime'][$this->namespace] = '';
+                }
                 $this->clearCodeFromDatabase();
             }
         }
     }
-    
-    /**
-     * Return the code from the session or sqlite database if used.  If none exists yet, an empty string is returned
-     */
-    protected function getCode()
+
+    /**
+     * Save data to session namespace and database if used
+     */
+    protected function saveData()
+    {
+        if ($this->no_session != true) {
+            if (isset($_SESSION['securimage_code_value']) && is_scalar($_SESSION['securimage_code_value'])) {
+                // fix for migration from v2 - v3
+                unset($_SESSION['securimage_code_value']);
+                unset($_SESSION['securimage_code_ctime']);
+            }
+
+            $_SESSION['securimage_code_disp'] [$this->namespace] = $this->code_display;
+            $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
+            $_SESSION['securimage_code_ctime'][$this->namespace] = time();
+        }
+
+        if ($this->use_database) {
+            $this->saveCodeToDatabase();
+        }
+    }
+
+    /**
+     * Saves the code to the sqlite database
+     */
+    protected function saveCodeToDatabase()
+    {
+        $success = false;
+        $this->openDatabase();
+
+        if ($this->use_database && $this->pdo_conn) {
+            $id = $this->getCaptchaId(false);
+            $ip = $_SERVER['REMOTE_ADDR'];
+
+            if (empty($id)) {
+                $id = $ip;
+            }
+
+            $time      = time();
+            $code      = $this->code;
+            $code_disp = $this->code_display;
+
+            // This is somewhat expensive in PDO Sqlite3 (when there is something to delete)
+            $this->clearCodeFromDatabase();
+
+            $query = "INSERT INTO {$this->database_table} ("
+                    ."id, code, code_display, namespace, created) "
+                    ."VALUES(?, ?, ?, ?, ?)";
+
+            $stmt    = $this->pdo_conn->prepare($query);
+            $success = $stmt->execute(array($id, $code, $code_disp, $this->namespace, $time));
+
+            if (!$success) {
+                $err = $stmt->errorInfo();
+                trigger_error("Failed to insert code into database. {$err[1]}: {$err[2]}", E_USER_WARNING);
+            }
+        }
+
+        return $success !== false;
+    }
+
+    /**
+     * Open sqlite database
+     */
+    protected function openDatabase()
+    {
+        $this->pdo_conn = false;
+
+        if ($this->use_database) {
+            $pdo_extension = 'PDO_' . strtoupper($this->database_driver);
+
+            if (!extension_loaded($pdo_extension)) {
+                trigger_error("Database support is turned on in Securimage, but the chosen extension $pdo_extension is not loaded in PHP.", E_USER_WARNING);
+                return false;
+            }
+        }
+
+        if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
+            if (!file_exists($this->database_file)) {
+                $fp = fopen($this->database_file, 'w+');
+                if (!$fp) {
+                    $err = error_get_last();
+                    trigger_error("Securimage failed to create SQLite3 database file '{$this->database_file}'. Reason: {$err['message']}", E_USER_WARNING);
+                    return false;
+                }
+                fclose($fp);
+                chmod($this->database_file, 0666);
+            } else if (!is_writeable($this->database_file)) {
+                trigger_error("Securimage does not have read/write access to database file '{$this->database_file}. Make sure permissions are 0666 and writeable by user '" . get_current_user() . "'", E_USER_WARNING);
+                return false;
+            }
+        }
+
+        $dsn = $this->getDsn();
+
+        try {
+            $options        = array();
+            $this->pdo_conn = new PDO($dsn, $this->database_user, $this->database_pass, $options);
+        } catch (PDOException $pdoex) {
+            trigger_error("Database connection failed: " . $pdoex->getMessage(), E_USER_WARNING);
+            return false;
+        }
+
+        try {
+            if (!$this->checkTablesExist()) {
+                // create tables...
+                $this->createDatabaseTables();
+            }
+        } catch (Exception $ex) {
+            trigger_error($ex->getMessage(), E_USER_WARNING);
+            $this->pdo_conn = null;
+            return false;
+        }
+
+        if (mt_rand(0, 100) / 100.0 == 1.0) {
+            $this->purgeOldCodesFromDatabase();
+        }
+
+        return $this->pdo_conn;
+    }
+
+    protected function getDsn()
+    {
+        $dsn = sprintf('%s:', $this->database_driver);
+
+        switch($this->database_driver) {
+            case self::SI_DRIVER_SQLITE3:
+                $dsn .= $this->database_file;
+                break;
+
+            case self::SI_DRIVER_MYSQL:
+            case self::SI_DRIVER_PGSQL:
+                $dsn .= sprintf('host=%s;dbname=%s',
+                                $this->database_host,
+                                $this->database_name);
+                break;
+
+        }
+
+        return $dsn;
+    }
+
+    protected function checkTablesExist()
+    {
+        $table = $this->pdo_conn->quote($this->database_table);
+
+        switch($this->database_driver) {
+            case self::SI_DRIVER_SQLITE3:
+                // query row count for sqlite, PRAGMA queries seem to return no
+                // rowCount using PDO even if there are rows returned
+                $query = "SELECT COUNT(id) FROM $table";
+                break;
+
+            case self::SI_DRIVER_MYSQL:
+                $query = "SHOW TABLES LIKE $table";
+                break;
+
+            case self::SI_DRIVER_PGSQL:
+                $query = "SELECT * FROM information_schema.columns WHERE table_name = $table;";
+                break;
+        }
+
+        $result = $this->pdo_conn->query($query);
+
+        if (!$result) {
+            $err = $this->pdo_conn->errorInfo();
+
+            if ($this->database_driver == self::SI_DRIVER_SQLITE3 &&
+                $err[1] === 1 && strpos($err[2], 'no such table') !== false)
+            {
+                return false;
+            }
+
+            throw new Exception("Failed to check tables: {$err[0]} - {$err[1]}: {$err[2]}");
+        } else if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
+            // successful here regardless of row count for sqlite
+            return true;
+        } else if ($result->rowCount() == 0) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    protected function createDatabaseTables()
+    {
+        $queries = array();
+
+        switch($this->database_driver) {
+            case self::SI_DRIVER_SQLITE3:
+                $queries[] = "CREATE TABLE \"{$this->database_table}\" (
+                                id VARCHAR(40),
+                                namespace VARCHAR(32) NOT NULL,
+                                code VARCHAR(32) NOT NULL,
+                                code_display VARCHAR(32) NOT NULL,
+                                created INTEGER NOT NULL,
+                                PRIMARY KEY(id, namespace)
+                              )";
+
+                $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created)";
+                break;
+
+            case self::SI_DRIVER_MYSQL:
+                $queries[] = "CREATE TABLE `{$this->database_table}` (
+                                `id` VARCHAR(40) NOT NULL,
+                                `namespace` VARCHAR(32) NOT NULL,
+                                `code` VARCHAR(32) NOT NULL,
+                                `code_display` VARCHAR(32) NOT NULL,
+                                `created` INT NOT NULL,
+                                PRIMARY KEY(id, namespace),
+                                INDEX(created)
+                              )";
+                break;
+
+            case self::SI_DRIVER_PGSQL:
+                $queries[] = "CREATE TABLE {$this->database_table} (
+                                id character varying(40) NOT NULL,
+                                namespace character varying(32) NOT NULL,
+                                code character varying(32) NOT NULL,
+                                code_display character varying(32) NOT NULL,
+                                created integer NOT NULL,
+                                CONSTRAINT pkey_id_namespace PRIMARY KEY (id, namespace)
+                              )";
+
+                $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created);";
+                break;
+        }
+
+        $this->pdo_conn->beginTransaction();
+
+        foreach($queries as $query) {
+            $result = $this->pdo_conn->query($query);
+
+            if (!$result) {
+                $err = $this->pdo_conn->errorInfo();
+                trigger_error("Failed to create table.  {$err[1]}: {$err[2]}", E_USER_WARNING);
+                $this->pdo_conn->rollBack();
+                $this->pdo_conn = false;
+                return false;
+            }
+        }
+
+        $this->pdo_conn->commit();
+
+        return true;
+    }
+
+    /**
+     * Get a code from the sqlite database for ip address/captchaId.
+     *
+     * @return string|array Empty string if no code was found or has expired,
+     * otherwise returns the stored captcha code.  If a captchaId is set, this
+     * returns an array with indices "code" and "code_disp"
+     */
+    protected function getCodeFromDatabase()
     {
         $code = '';
-        
-        if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
-         trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
-            if ($this->isCodeExpired(
-            $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
-                $code = $_SESSION['securimage_code_value'][$this->namespace];
-            }
-        } else if ($this->use_sqlite_db == true && function_exists('sqlite_open')) {
-            // no code in session - may mean user has cookies turned off
-            $this->openDatabase();
-            $code = $this->getCodeFromDatabase();
-        } else { /* no code stored in session or sqlite database, validation will fail */ }
-        
+
+        if ($this->use_database == true && $this->pdo_conn) {
+            if (Securimage::$_captchaId !== null) {
+                $query  = "SELECT * FROM {$this->database_table} WHERE id = ?";
+                $stmt   = $this->pdo_conn->prepare($query);
+                $result = $stmt->execute(array(Securimage::$_captchaId));
+            } else {
+                $ip = $_SERVER['REMOTE_ADDR'];
+                $ns = $this->namespace;
+
+                // ip is stored in id column when no captchaId
+                $query  = "SELECT * FROM {$this->database_table} WHERE id = ? AND namespace = ?";
+                $stmt   = $this->pdo_conn->prepare($query);
+                $result = $stmt->execute(array($ip, $ns));
+            }
+
+            if (!$result) {
+                $err = $this->pdo_conn->errorInfo();
+                trigger_error("Failed to select code from database.  {$err[0]}: {$err[1]}", E_USER_WARNING);
+            } else {
+                if ( ($row = $stmt->fetch()) !== false ) {
+                    if (false == $this->isCodeExpired($row['created'])) {
+                        if (Securimage::$_captchaId !== null) {
+                            // return an array when using captchaId
+                            $code = array('code'      => $row['code'],
+                                          'code_disp' => $row['code_display']);
+                        } else {
+                            $code = $row['code'];
+                        }
+                    }
+                }
+            }
+        }
+
         return $code;
     }
-    
-    /**
-     * Save data to session namespace and database if used
-     */
-    protected function saveData()
-    {
-        $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
-        $_SESSION['securimage_code_ctime'][$this->namespace] = time();
-        
-        $this->saveCodeToDatabase();
-    }
-    
-    /**
-     * Saves the code to the sqlite database
-     */
-    protected function saveCodeToDatabase()
-    {
-        $success = false;
-        
-        $this->openDatabase();
-        
-        if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
-            $ip      = $_SERVER['REMOTE_ADDR'];
-            $time    = time();
-            $code    = $_SESSION['securimage_code_value'][$this->namespace]; // if cookies are disabled the session still exists at this point
-            $success = sqlite_query($this->sqlite_handle,
-                                    "INSERT OR REPLACE INTO codes(ip, code, namespace, created)
-                                    VALUES('$ip', '$code', '{$this->namespace}', $time)");
-        }
-        
-        return $success !== false;
-    }
-    
-    /**
-     * Open sqlite database
-     */
-    protected function openDatabase()
-    {
-        $this->sqlite_handle = false;
-        
-        if ($this->use_sqlite_db && function_exists('sqlite_open')) {
-            $this->sqlite_handle = sqlite_open($this->sqlite_database, 0666, $error);
-            
-            if ($this->sqlite_handle !== false) {
-                $res = sqlite_query($this->sqlite_handle, "PRAGMA table_info(codes)");
-                if (sqlite_num_rows($res) == 0) {
-                    sqlite_query($this->sqlite_handle, "CREATE TABLE codes (ip VARCHAR(32) PRIMARY KEY, code VARCHAR(32) NOT NULL, namespace VARCHAR(32) NOT NULL, created INTEGER)");
-                }
-            }
-            
-            return $this->sqlite_handle != false;
-        }
-        
-        return $this->sqlite_handle;
-    }
-    
-    /**
-     * Get a code from the sqlite database for ip address
-     */
-    protected function getCodeFromDatabase()
-    {
-        $code = '';
-
-        if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
+
+    /**
+     * Remove an entered code from the database
+     */
+    protected function clearCodeFromDatabase()
+    {
+        if ($this->pdo_conn) {
             $ip = $_SERVER['REMOTE_ADDR'];
-            $ns = sqlite_escape_string($this->namespace);
-
-            $res = sqlite_query($this->sqlite_handle, "SELECT * FROM codes WHERE ip = '$ip' AND namespace = '$ns'");
-            if ($res && sqlite_num_rows($res) > 0) {
-                $res = sqlite_fetch_array($res);
-
-                if ($this->isCodeExpired($res['created']) == false) {
-                    $code = $res['code'];
-                }
-            }
-        }
-        return $code;
-    }
-    
-    /**
-     * Remove an entered code from the database
-     */
-    protected function clearCodeFromDatabase()
-    {
-        if (is_resource($this->sqlite_handle)) {
-            $ip = $_SERVER['REMOTE_ADDR'];
-            $ns = sqlite_escape_string($this->namespace);
-            
-            sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE ip = '$ip' AND namespace = '$ns'");
-        }
-    }
-    
+            $ns = $this->pdo_conn->quote($this->namespace);
+            $id = Securimage::$_captchaId;
+
+            if (empty($id)) {
+                $id = $ip; // if no captchaId set, IP address is captchaId.
+            }
+
+            $id = $this->pdo_conn->quote($id);
+
+            $query = sprintf("DELETE FROM %s WHERE id = %s AND namespace = %s",
+                             $this->database_table, $id, $ns);
+
+            $result = $this->pdo_conn->query($query);
+            if (!$result) {
+                trigger_error("Failed to delete code from database.", E_USER_WARNING);
+            }
+        }
+    }
+
     /**
      * Deletes old codes from sqlite database
@@ -1131,12 +1882,17 @@
     protected function purgeOldCodesFromDatabase()
     {
-        if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
+        if ($this->use_database && $this->pdo_conn) {
             $now   = time();
             $limit = (!is_numeric($this->expiry_time) || $this->expiry_time < 1) ? 86400 : $this->expiry_time;
-            
-            sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE $now - created > $limit");
-        }
-    }
-    
+
+            $query = sprintf("DELETE FROM %s WHERE %s - created > %s",
+                             $this->database_table,
+                             $this->pdo_conn->quote($now, PDO::PARAM_INT),
+                             $this->pdo_conn->quote($limit, PDO::PARAM_INT));
+
+            $result = $this->pdo_conn->query($query);
+        }
+    }
+
     /**
      * Checks to see if the captcha code has expired and cannot be used
@@ -1146,5 +1902,5 @@
     {
         $expired = true;
-        
+
         if (!is_numeric($this->expiry_time) || $this->expiry_time < 1) {
             $expired = false;
@@ -1152,19 +1908,8 @@
             $expired = false;
         }
-        
+
         return $expired;
     }
-    
-    /**
-     * 
-     * Generate an MP3 audio file of the captcha image
-     * 
-     * @deprecated 3.0
-     */
-    protected function generateMP3()
-    {
-        return false;
-    }
-    
+
     /**
      * Generate a wav file given the $letters in the code
@@ -1175,143 +1920,146 @@
     protected function generateWAV($letters)
     {
-        $data_len       = 0;
-        $files          = array();
-        $out_data       = '';
-        $out_channels   = 0;
-        $out_samplert   = 0;
-        $out_bpersample = 0;
-        $numSamples     = 0;
-        $removeChunks   = array('LIST', 'DISP', 'NOTE');
-
-        for ($i = 0; $i < sizeof($letters); ++$i) {
-            $letter   = $letters[$i];
-            $filename = $this->audio_path . strtoupper($letter) . '.wav';
-            $file     = array();
-            $data     = @file_get_contents($filename);
-            
-            if ($data === false) {
-                // echo "Failed to read $filename";
-                return $this->audioError();
-            }
-
-            $header = substr($data, 0, 36);
-            $info   = unpack('NChunkID/VChunkSize/NFormat/NSubChunk1ID/'
-                            .'VSubChunk1Size/vAudioFormat/vNumChannels/'
-                            .'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
-                             $header);
-            
-            $dataPos        = strpos($data, 'data');
-            $out_channels   = $info['NumChannels'];
-            $out_samplert   = $info['SampleRate'];
-            $out_bpersample = $info['BitsPerSample'];
-            
-            if ($dataPos === false) {
-                // wav file with no data?
-                // echo "Failed to find DATA segment in $filename";
-                return $this->audioError();
-            }
-            
-            if ($info['AudioFormat'] != 1) {
-                // only work with PCM audio
-                // echo "$filename was not PCM audio, only PCM is supported";
-                return $this->audioError();
-            }
-            
-            if ($info['SubChunk1Size'] != 16 && $info['SubChunk1Size'] != 18) {
-                // probably unsupported extension
-                // echo "Bad SubChunk1Size in $filename - Size was {$info['SubChunk1Size']}";
-                return $this->audioError();
-            }
-            
-            if ($info['SubChunk1Size'] > 16) {
-                $header .= substr($data, 36, $info['SubChunk1Size'] - 16);
-            }
-            
-            if ($i == 0) {
-                // create the final file's header, size will be adjusted later
-                $out_data = $header . 'data';
-            }
-            
-            $removed = 0;
-            
-            foreach($removeChunks as $chunk) {
-                $chunkPos = strpos($data, $chunk);
-                if ($chunkPos !== false) {
-                    $listSize = unpack('VSize', substr($data, $chunkPos + 4, 4));
-                    
-                    $data = substr($data, 0, $chunkPos) .
-                            substr($data, $chunkPos + 8 + $listSize['Size']);
-                            
-                    $removed += $listSize['Size'] + 8;
+        $wavCaptcha = new WavFile();
+        $first      = true;     // reading first wav file
+
+        foreach ($letters as $letter) {
+            $letter = strtoupper($letter);
+
+            try {
+                $l = new WavFile($this->audio_path . '/' . $letter . '.wav');
+
+                if ($first) {
+                    // set sample rate, bits/sample, and # of channels for file based on first letter
+                    $wavCaptcha->setSampleRate($l->getSampleRate())
+                               ->setBitsPerSample($l->getBitsPerSample())
+                               ->setNumChannels($l->getNumChannels());
+                    $first = false;
                 }
-            }
-            
-            $dataSize    = unpack('VSubchunk2Size', substr($data, $dataPos + 4, 4));
-            $dataSize['Subchunk2Size'] -= $removed;
-            $out_data   .= substr($data, $dataPos + 8, $dataSize['Subchunk2Size'] * ($out_bpersample / 8));
-            $numSamples += $dataSize['Subchunk2Size'];
-        }
-
-        $filesize  = strlen($out_data);
-        $chunkSize = $filesize - 8;
-        $dataCSize = $numSamples;
-        
-        $out_data = substr_replace($out_data, pack('V', $chunkSize), 4, 4);
-        $out_data = substr_replace($out_data, pack('V', $numSamples), 40 + ($info['SubChunk1Size'] - 16), 4);
-
-        $this->scrambleAudioData($out_data, 'wav');
-        
-        return $out_data;
-    }
-    
-    /**
-     * Randomizes the audio data to add noise and prevent binary recognition
-     * @param string $data  The binary audio file data
-     * @param string $format The format of the sound file (wav only)
-     */
-    protected function scrambleAudioData(&$data, $format)
-    {
-        $start = strpos($data, 'data') + 4; // look for "data" indicator
-        if ($start === false) $start = 44;  // if not found assume 44 byte header
-         
-        $start  += rand(1, 4); // randomize starting offset
-        $datalen = strlen($data) - $start;
-        $step    = 1;
-        
-        for ($i = $start; $i < $datalen; $i += $step) {
-            $ch = ord($data{$i});
-            if ($ch == 0 || $ch == 255) continue;
-            
-            if ($ch < 16 || $ch > 239) {
-                $ch += rand(-6, 6);
-            } else {
-                $ch += rand(-12, 12);
-            }
-            
-            if ($ch < 0) $ch = 0; else if ($ch > 255) $ch = 255;
-
-            $data{$i} = chr($ch);
-            
-            $step = rand(1,4);
-        }
-
-        return $data;
-    }
-    
+
+                // append letter to the captcha audio
+                $wavCaptcha->appendWav($l);
+
+                // random length of silence between $audio_gap_min and $audio_gap_max
+                if ($this->audio_gap_max > 0 && $this->audio_gap_max > $this->audio_gap_min) {
+                    $wavCaptcha->insertSilence( mt_rand($this->audio_gap_min, $this->audio_gap_max) / 1000.0 );
+                }
+            } catch (Exception $ex) {
+                // failed to open file, or the wav file is broken or not supported
+                // 2 wav files were not compatible, different # channels, bits/sample, or sample rate
+                throw $ex;
+            }
+        }
+
+        /********* Set up audio filters *****************************/
+        $filters = array();
+
+        if ($this->audio_use_noise == true) {
+            // use background audio - find random file
+            $noiseFile = $this->getRandomNoiseFile();
+
+            if ($noiseFile !== false && is_readable($noiseFile)) {
+                try {
+                    $wavNoise = new WavFile($noiseFile, false);
+                } catch(Exception $ex) {
+                    throw $ex;
+                }
+
+                // start at a random offset from the beginning of the wavfile
+                // in order to add more randomness
+                $randOffset = 0;
+                if ($wavNoise->getNumBlocks() > 2 * $wavCaptcha->getNumBlocks()) {
+                    $randBlock = mt_rand(0, $wavNoise->getNumBlocks() - $wavCaptcha->getNumBlocks());
+                    $wavNoise->readWavData($randBlock * $wavNoise->getBlockAlign(), $wavCaptcha->getNumBlocks() * $wavNoise->getBlockAlign());
+                } else {
+                    $wavNoise->readWavData();
+                    $randOffset = mt_rand(0, $wavNoise->getNumBlocks() - 1);
+                }
+
+
+                $mixOpts = array('wav'  => $wavNoise,
+                                 'loop' => true,
+                                 'blockOffset' => $randOffset);
+
+                $filters[WavFile::FILTER_MIX]       = $mixOpts;
+                $filters[WavFile::FILTER_NORMALIZE] = $this->audio_mix_normalization;
+            }
+        }
+
+        if ($this->degrade_audio == true) {
+            // add random noise.
+            // any noise level below 95% is intensely distorted and not pleasant to the ear
+            $filters[WavFile::FILTER_DEGRADE] = mt_rand(95, 98) / 100.0;
+        }
+
+        if (!empty($filters)) {
+            $wavCaptcha->filter($filters);  // apply filters to captcha audio
+        }
+
+        return $wavCaptcha->__toString();
+    }
+
+    public function getRandomNoiseFile()
+    {
+        $return = false;
+
+        if ( ($dh = opendir($this->audio_noise_path)) !== false ) {
+            $list = array();
+
+            while ( ($file = readdir($dh)) !== false ) {
+                if ($file == '.' || $file == '..') continue;
+                if (strtolower(substr($file, -4)) != '.wav') continue;
+
+                $list[] = $file;
+            }
+
+            closedir($dh);
+
+            if (sizeof($list) > 0) {
+                $file   = $list[array_rand($list, 1)];
+                $return = $this->audio_noise_path . DIRECTORY_SEPARATOR . $file;
+            }
+        }
+
+        return $return;
+    }
+
     /**
      * Return a wav file saying there was an error generating file
-     * 
+     *
      * @return string The binary audio contents
      */
     protected function audioError()
     {
-        return @file_get_contents(dirname(__FILE__) . '/audio/error.wav');
-    }
-    
+        return @file_get_contents(dirname(__FILE__) . '/audio/en/error.wav');
+    }
+
+    /**
+     * Checks to see if headers can be sent and if any error has been output to the browser
+     *
+     * @return bool true if headers haven't been sent and no output/errors will break audio/images, false if unsafe
+     */
+    protected function canSendHeaders()
+    {
+        if (headers_sent()) {
+            // output has been flushed and headers have already been sent
+            return false;
+        } else if (strlen((string)ob_get_contents()) > 0) {
+            // headers haven't been sent, but there is data in the buffer that will break image and audio data
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return a random float between 0 and 0.9999
+     *
+     * @return float Random float between 0 and 0.9999
+     */
     function frand()
     {
-        return 0.0001 * rand(0,9999);
-    }
-    
+        return 0.0001 * mt_rand(0,9999);
+    }
+
     /**
      * Convert an html color code to a Securimage_Color
@@ -1334,4 +2082,32 @@
             return new Securimage_Color($default);
         }
+    }
+
+    /**
+     * Error handler used when outputting captcha image or audio.
+     * This error handler helps determine if any errors raised would
+     * prevent captcha image or audio from displaying.  If they have
+     * no effect on the output buffer or headers, true is returned so
+     * the script can continue processing.
+     * See https://github.com/dapphp/securimage/issues/15
+     *
+     * @param int $errno
+     * @param string $errstr
+     * @param string $errfile
+     * @param int $errline
+     * @param array $errcontext
+     * @return boolean true if handled, false if PHP should handle
+     */
+    public function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array())
+    {
+        // get the current error reporting level
+        $level = error_reporting();
+
+        // if error was supressed or $errno not set in current error level
+        if ($level == 0 || ($level & $errno) == 0) {
+            return true;
+        }
+
+        return false;
     }
 }
@@ -1360,5 +2136,5 @@
      * $color = new Securimage_Color('#0080FF') or <br />
      * $color = new Securimage_Color(0, 128, 255)
-     * 
+     *
      * @param string $color
      * @throws Exception
@@ -1367,5 +2143,5 @@
     {
         $args = func_get_args();
-        
+
         if (sizeof($args) == 0) {
             $this->r = 255;
@@ -1377,5 +2153,5 @@
                 $color = substr($color, 1);
             }
-            
+
             if (strlen($color) != 3 && strlen($color) != 6) {
                 throw new InvalidArgumentException(
@@ -1383,5 +2159,5 @@
                 );
             }
-            
+
             $this->constructHTML($color);
         } else if (sizeof($args) == 3) {
@@ -1393,5 +2169,5 @@
         }
     }
-    
+
     /**
      * Construct from an rgb triplet
@@ -1408,10 +2184,10 @@
         if ($blue < 0)    $blue  = 0;
         if ($blue > 255)  $blue  = 255;
-        
+
         $this->r = $red;
         $this->g = $green;
         $this->b = $blue;
     }
-    
+
     /**
      * Construct from an html hex color code
@@ -1427,7 +2203,7 @@
             $red   = substr($color, 0, 2);
             $green = substr($color, 2, 2);
-            $blue  = substr($color, 4, 2); 
-        }
-        
+            $blue  = substr($color, 4, 2);
+        }
+
         $this->r = hexdec($red);
         $this->g = hexdec($green);
@@ -1435,3 +2211,2 @@
     }
 }
-?>
