1<?php
2
3// error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
4
5/**
6 * Project:  Securimage: A PHP class dealing with CAPTCHA images, audio, and validation
7 * File:     securimage.php
8 *
9 * Copyright (c) 2014, Drew Phillips
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without modification,
13 * are permitted provided that the following conditions are met:
14 *
15 *  - Redistributions of source code must retain the above copyright notice,
16 *    this list of conditions and the following disclaimer.
17 *  - Redistributions in binary form must reproduce the above copyright notice,
18 *    this list of conditions and the following disclaimer in the documentation
19 *    and/or other materials provided with the distribution.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 *
33 * Any modifications to the library should be indicated clearly in the source code
34 * to inform users that the changes are not a part of the original software.
35 *
36 * If you found this script useful, please take a quick moment to rate it.
37 * http://www.hotscripts.com/rate/49400.html  Thanks.
38 *
39 * @link http://www.phpcaptcha.org Securimage PHP CAPTCHA
40 * @link http://www.phpcaptcha.org/latest.zip Download Latest Version
41 * @link http://www.phpcaptcha.org/Securimage_Docs/ Online Documentation
42 * @copyright 2014 Drew Phillips
43 * @author Drew Phillips <drew@drew-phillips.com>
44 * @version 3.5.4 (Aug 27, 2014)
45 * @package Securimage
46 *
47 */
48
49/**
50
51 TODO:
52 - Implement HTML5 playback of audio using Javascript, DOM, and HTML5 <audio> with Flash fallback
53
54 ChangeLog
55
56 3.5.4
57 - Fix email validation code in example form files
58 - Fix backslashes in getCaptchaHtml for img attribute on Windows systems
59
60 3.5.3
61 - Add options for audio button to getCaptchaHtml(), fix urlencoding of flash parameters that was breaking button
62
63 3.5.2
64
65 - Add Securimage::getCaptchaHtml() for getting automatically generated captcha html code
66 - Option for using SoX to add effects to captcha audio to make identification by neural networks more difficult
67 - Add setNamespace() method
68 - Add getTimeToSolve() method
69 - Add session_status() check so session still starts if one had previously been opened and closed
70 - Add .htaccess file to audio directory to deny access; update audio files
71 - Option to skip checking of database tables during connection
72 - Add composer.json to package, submit to packagist
73 - Add font_ratio variable to determine size of font (github.com/wilkor)
74 - Add hint if sqlite3 database is not writeable.  Improve database error handling, add example database options to securimage_play.php
75 - Fixed issue regarding database storage and math captcha breaking audio output (github.com/SoftwareAndOutsourcing)
76
77 3.5.1
78 - Fix XSS vulnerability in example_form.php (discovered by Gjoko Krstic - <gjoko@zeroscience.mk>)
79
80 3.5
81 - Release new version
82 - MB string support for charlist
83 - Modify audio file path to use language directories
84 - Changed default captcha appearance
85
86 3.2RC4
87 - Add MySQL, PostgreSQL, and SQLite3 support for database storage
88 - Deprecate "use_sqlite_db" option and remove SQLite2/sqlite_* functions
89 - Add new captcha type that displays 2 dictionary words on one image
90 - Update examples
91
92 3.2RC3
93 - Fix canSendHeaders() check which was breaking if a PHP startup error was issued
94
95 3.2RC2
96 - Add error handler (https://github.com/dapphp/securimage/issues/15)
97 - Fix flash examples to use the correct value name for audio parameter
98
99 3.2RC1
100 - New audio captcha code.  Faster, fully dynamic audio, full WAV support
101   (Paul Voegler, Drew Phillips) <http://voegler.eu/pub/audio>
102 - New Flash audio streaming button.  User defined image and size supported
103 - Additional options for customizing captcha (noise_level, send_headers,
104   no_exit, no_session, display_value
105 - Add captcha ID support.  Uses sqlite and unique captcha IDs to track captchas,
106   no session used
107 - Add static methods for creating and validating captcha by ID
108 - Automatic clearing of old codes from SQLite database
109
110 3.0.3Beta
111 - Add improved mixing function to WavFile class (Paul Voegler)
112 - Improve performance and security of captcha audio (Paul Voegler, Drew Phillips)
113 - Add option to use random file as background noise in captcha audio
114 - Add new securimage options for audio files
115
116 3.0.2Beta
117 - Fix issue with session variables when upgrading from 2.0 - 3.0
118 - Improve audio captcha, switch to use WavFile class, make mathematical captcha audio work
119
120 3.0.1
121 - Bugfix: removed use of deprecated variable in addSignature method that would cause errors with display_errors on
122
123 3.0
124 - Rewrite class using PHP5 OOP
125 - Remove support for GD fonts, require FreeType
126 - Remove support for multi-color codes
127 - Add option to make codes case-sensitive
128 - Add namespaces to support multiple captchas on a single page or page specific captchas
129 - Add option to show simple math problems instead of codes
130 - Remove support for mp3 files due to vulnerability in decoding mp3 audio files
131 - Create new flash file to stream wav files instead of mp3
132 - Changed to BSD license
133
134 2.0.2
135 - Fix pathing to make integration into libraries easier (Nathan Phillip Brink ohnobinki@ohnopublishing.net)
136
137 2.0.1
138 - Add support for browsers with cookies disabled (requires php5, sqlite) maps users to md5 hashed ip addresses and md5 hashed codes for security
139 - Add fallback to gd fonts if ttf support is not enabled or font file not found (Mike Challis http://www.642weather.com/weather/scripts.php)
140 - Check for previous definition of image type constants (Mike Challis)
141 - Fix mime type settings for audio output
142 - Fixed color allocation issues with multiple colors and background images, consolidate allocation to one function
143 - Ability to let codes expire after a given length of time
144 - Allow HTML color codes to be passed to Securimage_Color (suggested by Mike Challis)
145
146 2.0.0
147 - Add mathematical distortion to characters (using code from HKCaptcha)
148 - Improved session support
149 - Added Securimage_Color class for easier color definitions
150 - Add distortion to audio output to prevent binary comparison attack (proposed by Sven "SavageTiger" Hagemann [insecurity.nl])
151 - Flash button to stream mp3 audio (Douglas Walsh www.douglaswalsh.net)
152 - Audio output is mp3 format by default
153 - Change font to AlteHaasGrotesk by yann le coroller
154 - Some code cleanup
155
156 1.0.4 (unreleased)
157 - Ability to output audible codes in mp3 format to stream from flash
158
159 1.0.3.1
160 - Error reading from wordlist in some cases caused words to be cut off 1 letter short
161
162 1.0.3
163 - Removed shadow_text from code which could cause an undefined property error due to removal from previous version
164
165 1.0.2
166 - Audible CAPTCHA Code wav files
167 - Create codes from a word list instead of random strings
168
169 1.0
170 - Added the ability to use a selected character set, rather than a-z0-9 only.
171 - Added the multi-color text option to use different colors for each letter.
172 - Switched to automatic session handling instead of using files for code storage
173 - Added GD Font support if ttf support is not available.  Can use internal GD fonts or load new ones.
174 - Added the ability to set line thickness
175 - Added option for drawing arced lines over letters
176 - Added ability to choose image type for output
177
178 */
179
180
181/**
182 * Securimage CAPTCHA Class.
183 *
184 * A class for creating and validating secure CAPTCHA images and audio.
185 *
186 * The class contains many options regarding appearance, security, storage of
187 * captcha data and image/audio generation options.
188 *
189 * @version    3.5.2
190 * @package    Securimage
191 * @subpackage classes
192 * @author     Drew Phillips <drew@drew-phillips.com>
193 *
194 */
195class Securimage
196{
197    // All of the public variables below are securimage options
198    // They can be passed as an array to the Securimage constructor, set below,
199    // or set from securimage_show.php and securimage_play.php
200
201    /**
202     * Constant for rendering captcha as a JPEG image
203     * @var int
204     */
205    const SI_IMAGE_JPEG = 1;
206
207    /**
208     * Constant for rendering captcha as a PNG image (default)
209     * @var int
210     */
211
212    const SI_IMAGE_PNG  = 2;
213    /**
214     * Constant for rendering captcha as a GIF image
215     * @var int
216     */
217    const SI_IMAGE_GIF  = 3;
218
219    /**
220     * Constant for generating a normal alphanumeric captcha based on the
221     * character set
222     *
223     * @see Securimage::$charset charset property
224     * @var int
225     */
226    const SI_CAPTCHA_STRING     = 0;
227
228    /**
229     * Constant for generating a captcha consisting of a simple math problem
230     *
231     * @var int
232     */
233    const SI_CAPTCHA_MATHEMATIC = 1;
234
235    /**
236     * Constant for generating a word based captcha using 2 words from a list
237     *
238     * @var int
239     */
240    const SI_CAPTCHA_WORDS      = 2;
241
242    /**
243     * MySQL option identifier for database storage option
244     *
245     * @var string
246     */
247    const SI_DRIVER_MYSQL   = 'mysql';
248
249    /**
250     * PostgreSQL option identifier for database storage option
251     *
252     * @var string
253     */
254    const SI_DRIVER_PGSQL   = 'pgsql';
255
256    /**
257     * SQLite option identifier for database storage option
258     *
259     * @var string
260     */
261    const SI_DRIVER_SQLITE3 = 'sqlite';
262
263    /*%*********************************************************************%*/
264    // Properties
265
266    /**
267     * The width of the captcha image
268     * @var int
269     */
270    public $image_width = 215;
271
272    /**
273     * The height of the captcha image
274     * @var int
275     */
276    public $image_height = 80;
277
278    /**
279     * Font size is calculated by image height and this ratio.  Leave blank for
280     * default ratio of 0.4.
281     *
282     * Valid range: 0.1 - 0.99.
283     *
284     * Depending on image_width, values > 0.6 are probably too large and
285     * values < 0.3 are too small.
286     *
287     * @var float
288     */
289    public $font_ratio;
290
291    /**
292     * The type of the image, default = png
293     *
294     * @see Securimage::SI_IMAGE_PNG SI_IMAGE_PNG
295     * @see Securimage::SI_IMAGE_JPEG SI_IMAGE_JPEG
296     * @see Securimage::SI_IMAGE_GIF SI_IMAGE_GIF
297     * @var int
298     */
299    public $image_type   = self::SI_IMAGE_PNG;
300
301    /**
302     * The background color of the captcha
303     * @var Securimage_Color
304     */
305    public $image_bg_color = '#ffffff';
306
307    /**
308     * The color of the captcha text
309     * @var Securimage_Color
310     */
311    public $text_color     = '#707070';
312
313    /**
314     * The color of the lines over the captcha
315     * @var Securimage_Color
316     */
317    public $line_color     = '#707070';
318
319    /**
320     * The color of the noise that is drawn
321     * @var Securimage_Color
322     */
323    public $noise_color    = '#707070';
324
325    /**
326     * How transparent to make the text.
327     *
328     * 0 = completely opaque, 100 = invisible
329     *
330     * @var int
331     */
332    public $text_transparency_percentage = 20;
333
334    /**
335     * Whether or not to draw the text transparently.
336     *
337     * true = use transparency, false = no transparency
338     *
339     * @var bool
340     */
341    public $use_transparent_text         = true;
342
343    /**
344     * The length of the captcha code
345     * @var int
346     */
347    public $code_length    = 6;
348
349    /**
350     * Whether the captcha should be case sensitive or not.
351     *
352     * Not recommended, use only for maximum protection.
353     *
354     * @var bool
355     */
356    public $case_sensitive = false;
357
358    /**
359     * The character set to use for generating the captcha code
360     * @var StringHelper
361     */
362    public $charset        = 'ABCDEFGHKLMNPRSTUVWYZabcdefghklmnprstuvwyz23456789';
363
364    /**
365     * How long in seconds a captcha remains valid, after this time it will be
366     * considered incorrect.
367     *
368     * @var int
369     */
370    public $expiry_time    = 900;
371
372    /**
373     * The session name securimage should use.
374     *
375     * Only use if your application uses a custom session name (e.g. Joomla).
376     * It is recommended to set this value here so it is used by all securimage
377     * scripts (i.e. securimage_show.php)
378     *
379     * @var StringHelper
380     */
381    public $session_name   = null;
382
383    /**
384     * true to use the wordlist file, false to generate random captcha codes
385     * @var bool
386     */
387    public $use_wordlist   = false;
388
389    /**
390     * The level of distortion.
391     *
392     * 0.75 = normal, 1.0 = very high distortion
393     *
394     * @var double
395     */
396    public $perturbation = 0.85;
397
398    /**
399     * How many lines to draw over the captcha code to increase security
400     * @var int
401     */
402    public $num_lines    = 5;
403
404    /**
405     * The level of noise (random dots) to place on the image, 0-10
406     * @var int
407     */
408    public $noise_level  = 2;
409
410    /**
411     * The signature text to draw on the bottom corner of the image
412     * @var StringHelper
413     */
414    public $image_signature = '';
415
416    /**
417     * The color of the signature text
418     * @var Securimage_Color
419     */
420    public $signature_color = '#707070';
421
422    /**
423     * The path to the ttf font file to use for the signature text.
424     * Defaults to $ttf_file (AHGBold.ttf)
425     *
426     * @see Securimage::$ttf_file
427     * @var StringHelper
428     */
429    public $signature_font;
430
431    /**
432     * No longer used.
433     *
434     * Use an SQLite database to store data (for users that do not support cookies)
435     *
436     * @var bool
437     * @see Securimage::$database_driver database_driver property
438     * @deprecated 3.2RC4
439     */
440    public $use_sqlite_db = false;
441
442    /**
443     * Use a database backend for code storage.
444     * Provides a fallback to users with cookies disabled.
445     * Required when using captcha IDs.
446     *
447     * @see Securimage::$database_driver
448     * @var bool
449     */
450    public $use_database = false;
451
452    /**
453     * Whether or not to skip checking if Securimage tables exist when using a
454     * database.
455     *
456     * Turn this to true once database functionality is working to improve
457     * performance.
458     *
459     * @var bool true to not check if captcha_codes tables are set up, false
460     * to check (and create if necessary)
461     */
462    public $skip_table_check = false;
463
464    /**
465     * Database driver to use for database support.
466     * Allowable values: *mysql*, *pgsql*, *sqlite*.
467     * Default: sqlite
468     *
469     * @var StringHelper
470     */
471    public $database_driver = self::SI_DRIVER_SQLITE3;
472
473    /**
474     * Database host to connect to when using mysql or postgres
475     *
476     * On Linux use "localhost" for Unix domain socket, otherwise uses TCP/IP
477     *
478     * Does not apply to SQLite
479     *
480     * @var StringHelper
481     */
482    public $database_host   = 'localhost';
483
484    /**
485     * Database username for connection (mysql, postgres only)
486     * Default is an empty string
487     *
488     * @var StringHelper
489     */
490    public $database_user   = '';
491
492    /**
493     * Database password for connection (mysql, postgres only)
494     * Default is empty string
495     *
496     * @var StringHelper
497     */
498    public $database_pass   = '';
499
500    /**
501     * Name of the database to select (mysql, postgres only)
502     *
503     * @see Securimage::$database_file for SQLite
504     * @var StringHelper
505     */
506    public $database_name   = '';
507
508    /**
509     * Database table where captcha codes are stored
510     *
511     * Note: Securimage will attempt to create this table for you if it does
512     * not exist.  If the table cannot be created, an E_USER_WARNING is emitted
513     *
514     * @var StringHelper
515     */
516    public $database_table  = 'captcha_codes';
517
518    /**
519     * Fully qualified path to the database file when using SQLite3.
520     *
521     * This value is only used when $database_driver == sqlite and does
522     * not apply when no database is used, or when using MySQL or PostgreSQL.
523     *
524     * On *nix, file must have permissions of 0666.
525     *
526     * **Make sure the directory containing this file is NOT web accessible**
527     *
528     * @var StringHelper
529     */
530    public $database_file;
531
532    /**
533     * The type of captcha to create.
534     *
535     * Either alphanumeric based on *charset*, a simple math problem, or an
536     * image consisting of 2 words from the word list.
537     *
538     * @see Securimage::SI_CAPTCHA_STRING SI_CAPTCHA_STRING
539     * @see Securimage::SI_CAPTCHA_MATHEMATIC SI_CAPTCHA_MATHEMATIC
540     * @see Securimage::SI_CAPTCHA_WORDS SI_CAPTCHA_WORDS
541     * @see Securimage::$charset charset property
542     * @see Securimage::$wordlist_file wordlist_file property
543     * @var int
544     */
545    public $captcha_type  = self::SI_CAPTCHA_STRING; // or self::SI_CAPTCHA_MATHEMATIC, or self::SI_CAPTCHA_WORDS;
546
547    /**
548     * The captcha namespace used for having multiple captchas on a page or
549     * to separate captchas from differen forms on your site.
550     * Example:
551     *
552     *     <?php
553     *     // use <img src="securimage_show.php?namespace=contact_form">
554     *     // or manually in securimage_show.php
555     *     $img->setNamespace('contact_form');
556     *
557     *     // in form validator
558     *     $img->setNamespace('contact_form');
559     *     if ($img->check($code) == true) {
560     *         echo "Valid!";
561     *     }
562     *
563     * @var StringHelper
564     */
565    public $namespace;
566
567    /**
568     * The TTF font file to use to draw the captcha code.
569     *
570     * Leave blank for default font AHGBold.ttf
571     *
572     * @var StringHelper
573     */
574    public $ttf_file;
575
576    /**
577     * The path to the wordlist file to use.
578     *
579     * Leave blank for default words/words.txt
580     *
581     * @var StringHelper
582     */
583    public $wordlist_file;
584
585    /**
586     * The directory to scan for background images, if set a random background
587     * will be chosen from this folder
588     *
589     * @var StringHelper
590     */
591    public $background_directory;
592
593    /**
594     * No longer used
595     *
596     * The path to the SQLite database file to use
597     *
598     * @deprecated 3.2RC4
599     * @see Securimage::$database_file database_file property
600     * @var StringHelper
601     */
602    public $sqlite_database;
603
604    /**
605     * The path to the audio files to be used for audio captchas.
606     *
607     * Can also be set in securimage_play.php
608     *
609     * Example:
610     *
611     *     $img->audio_path = '/home/yoursite/public_html/securimage/audio/en/';
612     *
613     * @var StringHelper
614     */
615    public $audio_path;
616
617    /**
618     * Use SoX (The Swiss Army knife of audio manipulation) for audio effects
619     * and processing.
620     *
621     * Using SoX should make it more difficult for bots to solve audio captchas
622     *
623     * @see Securimage::$sox_binary_path sox_binary_path property
624     * @var bool true to use SoX, false to use PHP
625     */
626    public $audio_use_sox = false;
627
628    /**
629     * The path to the SoX binary on your system
630     *
631     * @var StringHelper
632     */
633    public $sox_binary_path = '/usr/bin/sox';
634
635    /**
636     * The path to the directory containing audio files that will be selected
637     * randomly and mixed with the captcha audio.
638     *
639     * @var StringHelper
640     */
641    public $audio_noise_path;
642
643    /**
644     * Whether or not to mix background noise files into captcha audio
645     *
646     * Mixing random background audio with noise can help improve security of
647     * audio captcha.
648     *
649     * Default: securimage/audio/noise
650     *
651     * @since 3.0.3
652     * @see Securimage::$audio_noise_path audio_noise_path property
653     * @var bool true = mix, false = no
654     */
655    public $audio_use_noise;
656
657    /**
658     * The method and threshold (or gain factor) used to normalize the mixing
659     * with background noise.
660     *
661     * See http://www.voegler.eu/pub/audio/ for more information.
662     *
663     * Default: 0.6
664     *
665     * Valid:
666     *     >= 1
667     *     Normalize by multiplying by the threshold (boost - positive gain).
668     *     A value of 1 in effect means no normalization (and results in clipping).
669     *
670     *     <= -1
671     *     Normalize by dividing by the the absolute value of threshold (attenuate - negative gain).
672     *     A factor of 2 (-2) is about 6dB reduction in volume.
673     *
674     *     [0, 1)  (open inverval - not including 1)
675     *     The threshold above which amplitudes are comressed logarithmically.
676     *     e.g. 0.6 to leave amplitudes up to 60% "as is" and compressabove.
677     *
678     *     (-1, 0) (open inverval - not including -1 and 0)
679     *     The threshold above which amplitudes are comressed linearly.
680     *     e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above.
681     *
682     * @since 3.0.4
683     * @var float
684     */
685    public $audio_mix_normalization = 0.8;
686
687    /**
688     * Whether or not to degrade audio by introducing random noise.
689     *
690     * Current research shows this may not increase the security of audible
691     * captchas.
692     *
693     * Default: true
694     *
695     * @since 3.0.3
696     * @var bool
697     */
698    public $degrade_audio;
699
700    /**
701     * Minimum delay to insert between captcha audio letters in milliseconds
702     *
703     * @since 3.0.3
704     * @var float
705     */
706    public $audio_gap_min = 0;
707
708    /**
709     * Maximum delay to insert between captcha audio letters in milliseconds
710     *
711     * @since 3.0.3
712     * @var float
713     */
714    public $audio_gap_max = 3000;
715
716    /**
717     * Captcha ID if using static captcha
718     * @var StringHelper Unique captcha id
719     */
720    protected static $_captchaId = null;
721
722    /**
723     * The GD image resource of the captcha image
724     *
725     * @var resource
726     */
727    protected $im;
728
729    /**
730     * A temporary GD image resource of the captcha image for distortion
731     *
732     * @var resource
733     */
734    protected $tmpimg;
735
736    /**
737     * The background image GD resource
738     * @var resource
739     */
740    protected $bgimg;
741
742    /**
743     * Scale factor for magnification of distorted captcha image
744     *
745     * @var int
746     */
747    protected $iscale = 5;
748
749    /**
750     * Absolute path to securimage directory.
751     *
752     * This is calculated at runtime
753     *
754     * @var StringHelper
755     */
756    public $securimage_path = null;
757
758    /**
759     * The captcha challenge value.
760     *
761     * Either the case-sensitive/insensitive word captcha, or the solution to
762     * the math captcha.
763     *
764     * @var StringHelper Captcha challenge value
765     */
766    protected $code;
767
768    /**
769     * The display value of the captcha to draw on the image
770     *
771     * Either the word captcha or the math equation to present to the user
772     *
773     * @var StringHelper Captcha display value to draw on the image
774     */
775    protected $code_display;
776
777    /**
778     * Alternate text to draw as the captcha image text
779     *
780     * A value that can be passed to the constructor that can be used to
781     * generate a captcha image with a given value.
782     *
783     * This value does not get stored in the session or database and is only
784     * used when calling Securimage::show().
785     *
786     * If a display_value was passed to the constructor and the captcha image
787     * is generated, the display_value will be used as the string to draw on
788     * the captcha image.
789     *
790     * Used only if captcha codes are generated and managed by a 3rd party
791     * app/library
792     *
793     * @var StringHelper Captcha code value to display on the image
794     */
795    public $display_value;
796
797    /**
798     * Captcha code supplied by user [set from Securimage::check()]
799     *
800     * @var StringHelper
801     */
802    protected $captcha_code;
803
804    /**
805     * Time (in seconds) that the captcha was solved in (correctly or incorrectly).
806     *
807     * This is from the time of code creation, to when validation was attempted.
808     *
809     * @var int
810     */
811    protected $_timeToSolve = 0;
812
813    /**
814     * Flag that can be specified telling securimage not to call exit after
815     * generating a captcha image or audio file
816     *
817     * @var bool If true, script will not terminate; if false script will terminate (default)
818     */
819    protected $no_exit;
820
821    /**
822     * Flag indicating whether or not a PHP session should be started and used
823     *
824     * @var bool If true, no session will be started; if false, session will be started and used to store data (default)
825     */
826    protected $no_session;
827
828    /**
829     * Flag indicating whether or not HTTP headers will be sent when outputting
830     * captcha image/audio
831     *
832     * @var bool If true (default) headers will be sent, if false, no headers are sent
833     */
834    protected $send_headers;
835
836    /**
837     * PDO connection when a database is used
838     *
839     * @var resource
840     */
841    protected $pdo_conn;
842
843    /**
844     * The GD color resource for the background color
845     *
846     * @var resource
847     */
848    protected $gdbgcolor;
849
850    /**
851     * The GD color resource for the text color
852     *
853     * @var resource
854     */
855    protected $gdtextcolor;
856
857    /**
858     * The GD color resource for the line color
859     *
860     * @var resource
861     */
862    protected $gdlinecolor;
863
864    /**
865     * The GD color resource for the signature text color
866     *
867     * @var resource
868     */
869    protected $gdsignaturecolor;
870
871    /**
872     * Create a new securimage object, pass options to set in the constructor.
873     *
874     * The object can then be used to display a captcha, play an audible captcha, or validate a submission.
875     *
876     * @param array $options  Options to initialize the class.  May be any class property.
877     *
878     *     $options = array(
879     *         'text_color' => new Securimage_Color('#013020'),
880     *         'code_length' => 5,
881     *         'num_lines' => 5,
882     *         'noise_level' => 3,
883     *         'font_file' => Securimage::getPath() . '/custom.ttf'
884     *     );
885     *
886     *     $img = new Securimage($options);
887     *
888     */
889    public function __construct($options = array())
890    {
891        $this->securimage_path = dirname(__FILE__);
892
893        if (is_array($options) && sizeof($options) > 0) {
894            foreach($options as $prop => $val) {
895                if ($prop == 'captchaId') {
896                    Securimage::$_captchaId = $val;
897                    $this->use_database     = true;
898                } else if ($prop == 'use_sqlite_db') {
899                    trigger_error("The use_sqlite_db option is deprecated, use 'use_database' instead", E_USER_NOTICE);
900                } else {
901                    $this->$prop = $val;
902                }
903            }
904        }
905
906        $this->image_bg_color  = $this->initColor($this->image_bg_color,  '#ffffff');
907        $this->text_color      = $this->initColor($this->text_color,      '#616161');
908        $this->line_color      = $this->initColor($this->line_color,      '#616161');
909        $this->noise_color     = $this->initColor($this->noise_color,     '#616161');
910        $this->signature_color = $this->initColor($this->signature_color, '#616161');
911
912        if (is_null($this->ttf_file)) {
913            $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
914        }
915
916        $this->signature_font = $this->ttf_file;
917
918        if (is_null($this->wordlist_file)) {
919            $this->wordlist_file = $this->securimage_path . '/words/words.txt';
920        }
921
922        if (is_null($this->database_file)) {
923            $this->database_file = $this->securimage_path . '/database/securimage.sq3';
924        }
925
926        if (is_null($this->audio_path)) {
927            $this->audio_path = $this->securimage_path . '/audio/en/';
928        }
929
930        if (is_null($this->audio_noise_path)) {
931            $this->audio_noise_path = $this->securimage_path . '/audio/noise/';
932        }
933
934        if (is_null($this->audio_use_noise)) {
935            $this->audio_use_noise = true;
936        }
937
938        if (is_null($this->degrade_audio)) {
939            $this->degrade_audio = true;
940        }
941
942        if (is_null($this->code_length) || (int)$this->code_length < 1) {
943            $this->code_length = 6;
944        }
945
946        if (is_null($this->perturbation) || !is_numeric($this->perturbation)) {
947            $this->perturbation = 0.75;
948        }
949
950        if (is_null($this->namespace) || !is_string($this->namespace)) {
951            $this->namespace = 'default';
952        }
953
954        if (is_null($this->no_exit)) {
955            $this->no_exit = false;
956        }
957
958        if (is_null($this->no_session)) {
959            $this->no_session = false;
960        }
961
962        if (is_null($this->send_headers)) {
963            $this->send_headers = true;
964        }
965
966        if ($this->no_session != true) {
967            // Initialize session or attach to existing
968            if ( session_id() == '' || (function_exists('session_status') && PHP_SESSION_NONE == session_status()) ) { // no session has been started yet (or it was previousy closed), which is needed for validation
969                if (!is_null($this->session_name) && trim($this->session_name) != '') {
970                    session_name(trim($this->session_name)); // set session name if provided
971                }
972                session_start();
973            }
974        }
975    }
976
977    /**
978     * Return the absolute path to the Securimage directory.
979     *
980     * @return StringHelper The path to the securimage base directory
981     */
982    public static function getPath()
983    {
984        return dirname(__FILE__);
985    }
986
987    /**
988     * Generate a new captcha ID or retrieve the current ID (if exists).
989     *
990     * @param bool $new If true, generates a new challenge and returns and ID.  If false, the existing captcha ID is returned, or null if none exists.
991     * @param array $options Additional options to be passed to Securimage.
992     *   $options must include database settings if they are not set directly in securimage.php
993     *
994     * @return null|StringHelper Returns null if no captcha id set and new was false, or the captcha ID
995     */
996    public static function getCaptchaId($new = true, array $options = array())
997    {
998        if (is_null($new) || (bool)$new == true) {
999            $id = sha1(uniqid($_SERVER['REMOTE_ADDR'], true));
1000            $opts = array('no_session'    => true,
1001                          'use_database'  => true);
1002            if (sizeof($options) > 0) $opts = array_merge($options, $opts);
1003            $si = new self($opts);
1004            Securimage::$_captchaId = $id;
1005            $si->createCode();
1006
1007            return $id;
1008        } else {
1009            return Securimage::$_captchaId;
1010        }
1011    }
1012
1013    /**
1014     * Validate a captcha code input against a captcha ID
1015     *
1016     * @param StringHelper $id       The captcha ID to check
1017     * @param StringHelper $value    The captcha value supplied by the user
1018     * @param array  $options  Array of options to construct Securimage with.
1019     *   Options must include database options if they are not set in securimage.php
1020     *
1021     * @see Securimage::$database_driver
1022     * @return bool true if the code was valid for the given captcha ID, false if not or if database failed to open
1023     */
1024    public static function checkByCaptchaId($id, $value, array $options = array())
1025    {
1026        $opts = array('captchaId'    => $id,
1027                      'no_session'   => true,
1028                      'use_database' => true);
1029
1030        if (sizeof($options) > 0) $opts = array_merge($options, $opts);
1031
1032        $si = new self($opts);
1033
1034        if ($si->openDatabase()) {
1035            $code = $si->getCodeFromDatabase();
1036
1037            if (is_array($code)) {
1038                $si->code         = $code['code'];
1039                $si->code_display = $code['code_disp'];
1040            }
1041
1042            if ($si->check($value)) {
1043                $si->clearCodeFromDatabase();
1044
1045                return true;
1046            } else {
1047                return false;
1048            }
1049        } else {
1050            return false;
1051        }
1052    }
1053
1054
1055    /**
1056     * Generates a new challenge and serves a captcha image.
1057     *
1058     * Appropriate headers will be sent to the browser unless the *send_headers* option is false.
1059     *
1060     * @param StringHelper $background_image The absolute or relative path to the background image to use as the background of the captcha image.
1061     *
1062     *     $img = new Securimage();
1063     *     $img->code_length = 6;
1064     *     $img->num_lines   = 5;
1065     *     $img->noise_level = 5;
1066     *
1067     *     $img->show(); // sends the image and appropriate headers to browser
1068     *     exit;
1069     */
1070    public function show($background_image = '')
1071    {
1072        set_error_handler(array(&$this, 'errorHandler'));
1073
1074        if($background_image != '' && is_readable($background_image)) {
1075            $this->bgimg = $background_image;
1076        }
1077
1078        $this->doImage();
1079    }
1080
1081    /**
1082     * Checks a given code against the correct value from the session and/or database.
1083     *
1084     * @param StringHelper $code  The captcha code to check
1085     *
1086     *     $code = $_POST['code'];
1087     *     $img  = new Securimage();
1088     *     if ($img->check($code) == true) {
1089     *         $captcha_valid = true;
1090     *     } else {
1091     *         $captcha_valid = false;
1092     *     }
1093     *
1094     * @return bool true if the given code was correct, false if not.
1095     */
1096    public function check($code)
1097    {
1098        $this->code_entered = $code;
1099        $this->validate();
1100        return $this->correct_code;
1101    }
1102
1103    /**
1104     * Returns HTML code for displaying the captcha image, audio button, and form text input.
1105     *
1106     * Options can be specified to modify the output of the HTML.  Accepted options:
1107     *
1108     *     'securimage_path':
1109     *         Optional: The URI to where securimage is installed (e.g. /securimage)
1110     *     'image_id':
1111     *          A string that sets the "id" attribute of the captcha image (default: captcha_image)
1112     *     'image_alt_text':
1113     *         The alt text of the captcha image (default: CAPTCHA Image)
1114     *     'show_audio_button':
1115     *         true/false  Whether or not to show the audio button (default: true)
1116     *     'show_refresh_button':
1117     *         true/false  Whether or not to show a button to refresh the image (default: true)
1118     *     'show_text_input':
1119     *         true/false  Whether or not to show the text input for the captcha (default: true)
1120     *     'refresh_alt_text':
1121     *         Alt text for the refresh image (default: Refresh Image)
1122     *     'refresh_title_text':
1123     *         Title text for the refresh image link (default: Refresh Image)
1124     *     'input_id':
1125     *         A string that sets the "id" attribute of the captcha text input (default: captcha_code)
1126     *     'input_name':
1127     *         A string that sets the "name" attribute of the captcha text input (default: same as input_id)
1128     *     'input_text':
1129     *         A string that sets the text of the label for the captcha text input (default: Type the text:)
1130     *     'input_attributes':
1131     *         An array of additional HTML tag attributes to pass to the text input tag (default: empty)
1132     *     'image_attributes':
1133     *         An array of additional HTML tag attributes to pass to the captcha image tag (default: empty)
1134     *     'error_html':
1135     *         Optional HTML markup to be shown above the text input field
1136     *     'namespace':
1137     *         The optional captcha namespace to use for showing the image and playing back the audio. Namespaces are for using multiple captchas on the same page.
1138     *
1139     * @param array $options Array of options for modifying the HTML code.
1140     *
1141     * @return StringHelper  The generated HTML code for displaying the captcha
1142     */
1143    public static function getCaptchaHtml($options = array())
1144    {
1145        if (!isset($options['securimage_path'])) {
1146            $docroot = (isset($_SERVER['DOCUMENT_ROOT'])) ? $_SERVER['DOCUMENT_ROOT'] : substr($_SERVER['SCRIPT_FILENAME'], 0, -strlen($_SERVER['SCRIPT_NAME']));
1147            $docroot = realpath($docroot);
1148            $sipath  = dirname(__FILE__);
1149            $securimage_path = str_replace($docroot, '', $sipath);
1150        } else {
1151            $securimage_path = $options['securimage_path'];
1152        }
1153
1154        $image_id          = (isset($options['image_id'])) ? $options['image_id'] : 'captcha_image';
1155        $image_alt         = (isset($options['image_alt_text'])) ? $options['image_alt_text'] : 'CAPTCHA Image';
1156        $show_audio_btn    = (isset($options['show_audio_button'])) ? (bool)$options['show_audio_button'] : true;
1157        $show_refresh_btn  = (isset($options['show_refresh_button'])) ? (bool)$options['show_refresh_button'] : true;
1158        $audio_but_bg_col  = (isset($options['audio_button_bgcol'])) ? $options['audio_button_bgcol'] : '#ffffff';
1159        $audio_icon_url    = (isset($options['audio_icon_url'])) ? $options['audio_icon_url'] : null;
1160        $audio_play_url    = (isset($options['audio_play_url'])) ? $options['audio_play_url'] : null;
1161        $audio_swf_url     = (isset($options['audio_swf_url'])) ? $options['audio_swf_url'] : null;
1162        $show_input        = (isset($options['show_text_input'])) ? (bool)$options['show_text_input'] : true;
1163        $refresh_alt       = (isset($options['refresh_alt_text'])) ? $options['refresh_alt_text'] : 'Refresh Image';
1164        $refresh_title     = (isset($options['refresh_title_text'])) ? $options['refresh_title_text'] : 'Refresh Image';
1165        $input_text        = (isset($options['input_text'])) ? $options['input_text'] : 'Type the text:';
1166        $input_id          = (isset($options['input_id'])) ? $options['input_id'] : 'captcha_code';
1167        $input_name        = (isset($options['input_name'])) ? $options['input_name'] :  $input_id;
1168        $input_attrs       = (isset($options['input_attributes'])) ? $options['input_attributes'] : array();
1169        $image_attrs       = (isset($options['image_attributes'])) ? $options['image_attributes'] : array();
1170        $error_html        = (isset($options['error_html'])) ? $options['error_html'] : null;
1171        $namespace         = (isset($options['namespace'])) ? $options['namespace'] : '';
1172
1173        $rand              = md5(uniqid($_SERVER['REMOTE_PORT'], true));
1174        $securimage_path   = rtrim($securimage_path, '/\\');
1175        $securimage_path   = str_replace('\\', '/', $securimage_path);
1176
1177        $image_attr = '';
1178        if (!is_array($image_attrs)) $image_attrs = array();
1179        if (!isset($image_attrs['align'])) $image_attrs['align'] = 'left';
1180        $image_attrs['id']  = $image_id;
1181
1182        $show_path = $securimage_path . '/securimage_show.php?';
1183        if (!empty($namespace)) {
1184            $show_path .= sprintf('namespace=%s&', $namespace);
1185        }
1186        $image_attrs['src'] = $show_path . $rand;
1187
1188        $image_attrs['alt'] = $image_alt;
1189
1190        foreach($image_attrs as $name => $val) {
1191            $image_attr .= sprintf('%s="%s" ', $name, htmlspecialchars($val));
1192        }
1193
1194        $html = sprintf('<img %s/>', $image_attr);
1195
1196        if ($show_audio_btn) {
1197            $swf_path  = $securimage_path . '/securimage_play.swf';
1198            $play_path = $securimage_path . '/securimage_play.php';
1199            $icon_path = $securimage_path . '/images/audio_icon.png';
1200
1201            if (!empty($audio_icon_url)) {
1202                $icon_path = $audio_icon_url;
1203            }
1204
1205            if (!empty($audio_play_url)) {
1206                $play_path = $audio_play_url;
1207            }
1208
1209            if (!empty($audio_swf_url)) {
1210                $swf_path = $audio_swf_url;
1211            }
1212
1213            $html .= sprintf('<object type="application/x-shockwave-flash" data="%s?bgcol=%s&amp;icon_file=%s&amp;audio_file=%s" height="32" width="32">',
1214                    htmlspecialchars($swf_path),
1215                    urlencode($audio_but_bg_col),
1216                    urlencode($icon_path),
1217                    urlencode($play_path)
1218            );
1219
1220            $html .= sprintf('<param name="movie" value="%s?bgcol=%s&amp;icon_file=%s&amp;audio_file=%s" />',
1221                    htmlspecialchars($swf_path),
1222                    urlencode($audio_but_bg_col),
1223                    urlencode($icon_path),
1224                    urlencode($play_path)
1225            );
1226
1227            $html .= '</object><br />';
1228        }
1229
1230        if ($show_refresh_btn) {
1231            $icon_path = $securimage_path . '/images/refresh.png';
1232            $img_tag = sprintf('<img height="32" width="32" src="%s" alt="%s" onclick="this.blur()" align="bottom" border="0" />',
1233                               htmlspecialchars($icon_path), htmlspecialchars($refresh_alt));
1234
1235            $html .= sprintf('<a tabindex="-1" style="border: 0"  title="%s" onclick="document.getElementById(\'%s\').src = \'%s\' + Math.random(); this.blur(); return false">%s</a><br />',
1236                    htmlspecialchars($refresh_title),
1237                    $image_id,
1238                    $show_path,
1239                    $img_tag
1240            );
1241        }
1242
1243        $html .= '<div style="clear: both"></div>';
1244
1245        $html .= sprintf('<label for="%s">%s</label> ',
1246                htmlspecialchars($input_id),
1247                htmlspecialchars($input_text));
1248
1249        if (!empty($error_html)) {
1250            $html .= $error_html;
1251        }
1252
1253        $input_attr = '';
1254        if (!is_array($input_attrs)) $input_attrs = array();
1255        $input_attrs['type'] = 'text';
1256        $input_attrs['name'] = $input_name;
1257        $input_attrs['id']   = $input_id;
1258
1259        foreach($input_attrs as $name => $val) {
1260            $input_attr .= sprintf('%s="%s" ', $name, htmlspecialchars($val));
1261        }
1262
1263        $html .= sprintf('<input %s/>', $input_attr);
1264
1265        return $html;
1266    }
1267
1268    /**
1269     * Get the time in seconds that it took to solve the captcha.
1270     *
1271     * @return int The time in seconds from when the code was created, to when it was solved
1272     */
1273    public function getTimeToSolve()
1274    {
1275        return $this->_timeToSolve;
1276    }
1277
1278    /**
1279     * Set the namespace for the captcha being stored in the session or database.
1280     *
1281     * Namespaces are useful when multiple captchas need to be displayed on a single page.
1282     *
1283     * @param StringHelper $namespace  Namespace value, String consisting of characters "a-zA-Z0-9_-"
1284     */
1285    public function setNamespace($namespace)
1286    {
1287        $namespace = preg_replace('/[^a-z0-9-_]/i', '', $namespace);
1288        $namespace = substr($namespace, 0, 64);
1289
1290        if (!empty($namespace)) {
1291            $this->namespace = $namespace;
1292        } else {
1293            $this->namespace = 'default';
1294        }
1295    }
1296
1297    /**
1298     * Generate an audible captcha in WAV format and send it to the browser with appropriate headers.
1299     * Example:
1300     *
1301     *     $img = new Securimage();
1302     *     $img->outputAudioFile(); // outputs a wav file to the browser
1303     *     exit;
1304     *
1305     */
1306    public function outputAudioFile()
1307    {
1308        set_error_handler(array(&$this, 'errorHandler'));
1309
1310        require_once dirname(__FILE__) . '/WavFile.php';
1311
1312        try {
1313            $audio = $this->getAudibleCode();
1314        } catch (Exception $ex) {
1315            if (($fp = @fopen(dirname(__FILE__) . '/si.error_log', 'a+')) !== false) {
1316                fwrite($fp, date('Y-m-d H:i:s') . ': Securimage audio error "' . $ex->getMessage() . '"' . "\n");
1317                fclose($fp);
1318            }
1319
1320            $audio = $this->audioError();
1321        }
1322
1323        if ($this->canSendHeaders() || $this->send_headers == false) {
1324            if ($this->send_headers) {
1325                $uniq = md5(uniqid(microtime()));
1326                header("Content-Disposition: attachment; filename=\"securimage_audio-{$uniq}.wav\"");
1327                header('Cache-Control: no-store, no-cache, must-revalidate');
1328                header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
1329                header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
1330                header('Content-type: audio/x-wav');
1331
1332                if (extension_loaded('zlib')) {
1333                    ini_set('zlib.output_compression', true);  // compress output if supported by browser
1334                } else {
1335                    header('Content-Length: ' . strlen($audio));
1336                }
1337            }
1338
1339            echo $audio;
1340        } else {
1341            echo '<hr /><strong>'
1342                .'Failed to generate audio file, content has already been '
1343                .'output.<br />This is most likely due to misconfiguration or '
1344                .'a PHP error was sent to the browser.</strong>';
1345        }
1346
1347        restore_error_handler();
1348
1349        if (!$this->no_exit) exit;
1350    }
1351
1352    /**
1353     * Return the code from the session or database (if configured).  If none exists or was found, an empty string is returned.
1354     *
1355     * @param bool $array  true to receive an array containing the code and properties, false to receive just the code.
1356     * @param bool $returnExisting If true, and the class property *code* is set, it will be returned instead of getting the code from the session or database.
1357     * @return array|StringHelper Return is an array if $array = true, otherwise a string containing the code
1358     */
1359    public function getCode($array = false, $returnExisting = false)
1360    {
1361        $code = array();
1362        $time = 0;
1363        $disp = 'error';
1364
1365        if ($returnExisting && strlen($this->code) > 0) {
1366            if ($array) {
1367                return array(
1368                    'code'         => $this->code,
1369                    'display'      => $this->code_display,
1370                    'code_display' => $this->code_display,
1371                    'time'         => 0);
1372            } else {
1373                return $this->code;
1374            }
1375        }
1376
1377        if ($this->no_session != true) {
1378            if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
1379                    trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
1380                if ($this->isCodeExpired(
1381                        $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
1382                    $code['code'] = $_SESSION['securimage_code_value'][$this->namespace];
1383                    $code['time'] = $_SESSION['securimage_code_ctime'][$this->namespace];
1384                    $code['display'] = $_SESSION['securimage_code_disp'] [$this->namespace];
1385                }
1386            }
1387        }
1388
1389        if (empty($code) && $this->use_database) {
1390            // no code in session - may mean user has cookies turned off
1391            $this->openDatabase();
1392            $code = $this->getCodeFromDatabase();
1393
1394            if (!empty($code)) {
1395                $code['display'] = $code['code_disp'];
1396                unset($code['code_disp']);
1397            }
1398        } else { /* no code stored in session or sqlite database, validation will fail */ }
1399
1400        if ($array == true) {
1401            return $code;
1402        } else {
1403            return $code['code'];
1404        }
1405    }
1406
1407    /**
1408     * The main image drawing routing, responsible for constructing the entire image and serving it
1409     */
1410    protected function doImage()
1411    {
1412        if( ($this->use_transparent_text == true || $this->bgimg != '') && function_exists('imagecreatetruecolor')) {
1413            $imagecreate = 'imagecreatetruecolor';
1414        } else {
1415            $imagecreate = 'imagecreate';
1416        }
1417
1418        $this->im     = $imagecreate($this->image_width, $this->image_height);
1419        $this->tmpimg = $imagecreate($this->image_width * $this->iscale, $this->image_height * $this->iscale);
1420
1421        $this->allocateColors();
1422        imagepalettecopy($this->tmpimg, $this->im);
1423
1424        $this->setBackground();
1425
1426        $code = '';
1427
1428        if ($this->getCaptchaId(false) !== null) {
1429            // a captcha Id was supplied
1430
1431            // check to see if a display_value for the captcha image was set
1432            if (is_string($this->display_value) && strlen($this->display_value) > 0) {
1433                $this->code_display = $this->display_value;
1434                $this->code         = ($this->case_sensitive) ?
1435                                       $this->display_value   :
1436                                       strtolower($this->display_value);
1437                $code = $this->code;
1438            } else if ($this->openDatabase()) {
1439                // no display_value, check the database for existing captchaId
1440                $code = $this->getCodeFromDatabase();
1441
1442                // got back a result from the database with a valid code for captchaId
1443                if (is_array($code)) {
1444                    $this->code         = $code['code'];
1445                    $this->code_display = $code['code_disp'];
1446                    $code = $code['code'];
1447                }
1448            }
1449        }
1450
1451        if ($code == '') {
1452            // if the code was not set using display_value or was not found in
1453            // the database, create a new code
1454            $this->createCode();
1455        }
1456
1457        if ($this->noise_level > 0) {
1458            $this->drawNoise();
1459        }
1460
1461        $this->drawWord();
1462
1463        if ($this->perturbation > 0 && is_readable($this->ttf_file)) {
1464            $this->distortedCopy();
1465        }
1466
1467        if ($this->num_lines > 0) {
1468            $this->drawLines();
1469        }
1470
1471        if (trim($this->image_signature) != '') {
1472            $this->addSignature();
1473        }
1474
1475        $this->output();
1476    }
1477
1478    /**
1479     * Allocate the colors to be used for the image
1480     */
1481    protected function allocateColors()
1482    {
1483        // allocate bg color first for imagecreate
1484        $this->gdbgcolor = imagecolorallocate($this->im,
1485                                              $this->image_bg_color->r,
1486                                              $this->image_bg_color->g,
1487                                              $this->image_bg_color->b);
1488
1489        $alpha = intval($this->text_transparency_percentage / 100 * 127);
1490
1491        if ($this->use_transparent_text == true) {
1492            $this->gdtextcolor = imagecolorallocatealpha($this->im,
1493                                                         $this->text_color->r,
1494                                                         $this->text_color->g,
1495                                                         $this->text_color->b,
1496                                                         $alpha);
1497            $this->gdlinecolor = imagecolorallocatealpha($this->im,
1498                                                         $this->line_color->r,
1499                                                         $this->line_color->g,
1500                                                         $this->line_color->b,
1501                                                         $alpha);
1502            $this->gdnoisecolor = imagecolorallocatealpha($this->im,
1503                                                          $this->noise_color->r,
1504                                                          $this->noise_color->g,
1505                                                          $this->noise_color->b,
1506                                                          $alpha);
1507        } else {
1508            $this->gdtextcolor = imagecolorallocate($this->im,
1509                                                    $this->text_color->r,
1510                                                    $this->text_color->g,
1511                                                    $this->text_color->b);
1512            $this->gdlinecolor = imagecolorallocate($this->im,
1513                                                    $this->line_color->r,
1514                                                    $this->line_color->g,
1515                                                    $this->line_color->b);
1516            $this->gdnoisecolor = imagecolorallocate($this->im,
1517                                                          $this->noise_color->r,
1518                                                          $this->noise_color->g,
1519                                                          $this->noise_color->b);
1520        }
1521
1522        $this->gdsignaturecolor = imagecolorallocate($this->im,
1523                                                     $this->signature_color->r,
1524                                                     $this->signature_color->g,
1525                                                     $this->signature_color->b);
1526
1527    }
1528
1529    /**
1530     * The the background color, or background image to be used
1531     */
1532    protected function setBackground()
1533    {
1534        // set background color of image by drawing a rectangle since imagecreatetruecolor doesn't set a bg color
1535        imagefilledrectangle($this->im, 0, 0,
1536                             $this->image_width, $this->image_height,
1537                             $this->gdbgcolor);
1538        imagefilledrectangle($this->tmpimg, 0, 0,
1539                             $this->image_width * $this->iscale, $this->image_height * $this->iscale,
1540                             $this->gdbgcolor);
1541
1542        if ($this->bgimg == '') {
1543            if ($this->background_directory != null &&
1544                is_dir($this->background_directory) &&
1545                is_readable($this->background_directory))
1546            {
1547                $img = $this->getBackgroundFromDirectory();
1548                if ($img != false) {
1549                    $this->bgimg = $img;
1550                }
1551            }
1552        }
1553
1554        if ($this->bgimg == '') {
1555            return;
1556        }
1557
1558        $dat = @getimagesize($this->bgimg);
1559        if($dat == false) {
1560            return;
1561        }
1562
1563        switch($dat[2]) {
1564            case 1:  $newim = @imagecreatefromgif($this->bgimg); break;
1565            case 2:  $newim = @imagecreatefromjpeg($this->bgimg); break;
1566            case 3:  $newim = @imagecreatefrompng($this->bgimg); break;
1567            default: return;
1568        }
1569
1570        if(!$newim) return;
1571
1572        imagecopyresized($this->im, $newim, 0, 0, 0, 0,
1573                         $this->image_width, $this->image_height,
1574                         imagesx($newim), imagesy($newim));
1575    }
1576
1577    /**
1578     * Scan the directory for a background image to use
1579     */
1580    protected function getBackgroundFromDirectory()
1581    {
1582        $images = array();
1583
1584        if ( ($dh = opendir($this->background_directory)) !== false) {
1585            while (($file = readdir($dh)) !== false) {
1586                if (preg_match('/(jpg|gif|png)$/i', $file)) $images[] = $file;
1587            }
1588
1589            closedir($dh);
1590
1591            if (sizeof($images) > 0) {
1592                return rtrim($this->background_directory, '/') . '/' . $images[mt_rand(0, sizeof($images)-1)];
1593            }
1594        }
1595
1596        return false;
1597    }
1598
1599    /**
1600     * This method generates a new captcha code.
1601     *
1602     * Generates a random captcha code based on *charset*, math problem, or captcha from the wordlist and saves the value to the session and/or database.
1603     */
1604    public function createCode()
1605    {
1606        $this->code = false;
1607
1608        switch($this->captcha_type) {
1609            case self::SI_CAPTCHA_MATHEMATIC:
1610            {
1611                do {
1612                    $signs = array('+', '-', 'x');
1613                    $left  = mt_rand(1, 10);
1614                    $right = mt_rand(1, 5);
1615                    $sign  = $signs[mt_rand(0, 2)];
1616
1617                    switch($sign) {
1618                        case 'x': $c = $left * $right; break;
1619                        case '-': $c = $left - $right; break;
1620                        default:  $c = $left + $right; break;
1621                    }
1622                } while ($c <= 0); // no negative #'s or 0
1623
1624                $this->code         = $c;
1625                $this->code_display = "$left $sign $right";
1626                break;
1627            }
1628
1629            case self::SI_CAPTCHA_WORDS:
1630                $words = $this->readCodeFromFile(2);
1631                $this->code = implode(' ', $words);
1632                $this->code_display = $this->code;
1633                break;
1634
1635            default:
1636            {
1637                if ($this->use_wordlist && is_readable($this->wordlist_file)) {
1638                    $this->code = $this->readCodeFromFile();
1639                }
1640
1641                if ($this->code == false) {
1642                    $this->code = $this->generateCode($this->code_length);
1643                }
1644
1645                $this->code_display = $this->code;
1646                $this->code         = ($this->case_sensitive) ? $this->code : strtolower($this->code);
1647            } // default
1648        }
1649
1650        $this->saveData();
1651    }
1652
1653    /**
1654     * Draws the captcha code on the image
1655     */
1656    protected function drawWord()
1657    {
1658        $width2  = $this->image_width * $this->iscale;
1659        $height2 = $this->image_height * $this->iscale;
1660        $ratio   = ($this->font_ratio) ? $this->font_ratio : 0.4;
1661
1662        if ((float)$ratio < 0.1 || (float)$ratio >= 1) {
1663            $ratio = 0.4;
1664        }
1665
1666        if (!is_readable($this->ttf_file)) {
1667            imagestring($this->im, 4, 10, ($this->image_height / 2) - 5, 'Failed to load TTF font file!', $this->gdtextcolor);
1668        } else {
1669            if ($this->perturbation > 0) {
1670                $font_size = $height2 * $ratio;
1671                $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
1672                $tx = $bb[4] - $bb[0];
1673                $ty = $bb[5] - $bb[1];
1674                $x  = floor($width2 / 2 - $tx / 2 - $bb[0]);
1675                $y  = round($height2 / 2 - $ty / 2 - $bb[1]);
1676
1677                imagettftext($this->tmpimg, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
1678            } else {
1679                $font_size = $this->image_height * $ratio;
1680                $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
1681                $tx = $bb[4] - $bb[0];
1682                $ty = $bb[5] - $bb[1];
1683                $x  = floor($this->image_width / 2 - $tx / 2 - $bb[0]);
1684                $y  = round($this->image_height / 2 - $ty / 2 - $bb[1]);
1685
1686                imagettftext($this->im, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
1687            }
1688        }
1689
1690        // DEBUG
1691        //$this->im = $this->tmpimg;
1692        //$this->output();
1693
1694    }
1695
1696    /**
1697     * Copies the captcha image to the final image with distortion applied
1698     */
1699    protected function distortedCopy()
1700    {
1701        $numpoles = 3; // distortion factor
1702        // make array of poles AKA attractor points
1703        for ($i = 0; $i < $numpoles; ++ $i) {
1704            $px[$i]  = mt_rand($this->image_width  * 0.2, $this->image_width  * 0.8);
1705            $py[$i]  = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
1706            $rad[$i] = mt_rand($this->image_height * 0.2, $this->image_height * 0.8);
1707            $tmp     = ((- $this->frand()) * 0.15) - .15;
1708            $amp[$i] = $this->perturbation * $tmp;
1709        }
1710
1711        $bgCol = imagecolorat($this->tmpimg, 0, 0);
1712        $width2 = $this->iscale * $this->image_width;
1713        $height2 = $this->iscale * $this->image_height;
1714        imagepalettecopy($this->im, $this->tmpimg); // copy palette to final image so text colors come across
1715        // loop over $img pixels, take pixels from $tmpimg with distortion field
1716        for ($ix = 0; $ix < $this->image_width; ++ $ix) {
1717            for ($iy = 0; $iy < $this->image_height; ++ $iy) {
1718                $x = $ix;
1719                $y = $iy;
1720                for ($i = 0; $i < $numpoles; ++ $i) {
1721                    $dx = $ix - $px[$i];
1722                    $dy = $iy - $py[$i];
1723                    if ($dx == 0 && $dy == 0) {
1724                        continue;
1725                    }
1726                    $r = sqrt($dx * $dx + $dy * $dy);
1727                    if ($r > $rad[$i]) {
1728                        continue;
1729                    }
1730                    $rscale = $amp[$i] * sin(3.14 * $r / $rad[$i]);
1731                    $x += $dx * $rscale;
1732                    $y += $dy * $rscale;
1733                }
1734                $c = $bgCol;
1735                $x *= $this->iscale;
1736                $y *= $this->iscale;
1737                if ($x >= 0 && $x < $width2 && $y >= 0 && $y < $height2) {
1738                    $c = imagecolorat($this->tmpimg, $x, $y);
1739                }
1740                if ($c != $bgCol) { // only copy pixels of letters to preserve any background image
1741                    imagesetpixel($this->im, $ix, $iy, $c);
1742                }
1743            }
1744        }
1745    }
1746
1747    /**
1748     * Draws distorted lines on the image
1749     */
1750    protected function drawLines()
1751    {
1752        for ($line = 0; $line < $this->num_lines; ++ $line) {
1753            $x = $this->image_width * (1 + $line) / ($this->num_lines + 1);
1754            $x += (0.5 - $this->frand()) * $this->image_width / $this->num_lines;
1755            $y = mt_rand($this->image_height * 0.1, $this->image_height * 0.9);
1756
1757            $theta = ($this->frand() - 0.5) * M_PI * 0.7;
1758            $w = $this->image_width;
1759            $len = mt_rand($w * 0.4, $w * 0.7);
1760            $lwid = mt_rand(0, 2);
1761
1762            $k = $this->frand() * 0.6 + 0.2;
1763            $k = $k * $k * 0.5;
1764            $phi = $this->frand() * 6.28;
1765            $step = 0.5;
1766            $dx = $step * cos($theta);
1767            $dy = $step * sin($theta);
1768            $n = $len / $step;
1769            $amp = 1.5 * $this->frand() / ($k + 5.0 / $len);
1770            $x0 = $x - 0.5 * $len * cos($theta);
1771            $y0 = $y - 0.5 * $len * sin($theta);
1772
1773            $ldx = round(- $dy * $lwid);
1774            $ldy = round($dx * $lwid);
1775
1776            for ($i = 0; $i < $n; ++ $i) {
1777                $x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
1778                $y = $y0 + $i * $dy - $amp * $dx * sin($k * $i * $step + $phi);
1779                imagefilledrectangle($this->im, $x, $y, $x + $lwid, $y + $lwid, $this->gdlinecolor);
1780            }
1781        }
1782    }
1783
1784    /**
1785     * Draws random noise on the image
1786     */
1787    protected function drawNoise()
1788    {
1789        if ($this->noise_level > 10) {
1790            $noise_level = 10;
1791        } else {
1792            $noise_level = $this->noise_level;
1793        }
1794
1795        $t0 = microtime(true);
1796
1797        $noise_level *= 125; // an arbitrary number that works well on a 1-10 scale
1798
1799        $points = $this->image_width * $this->image_height * $this->iscale;
1800        $height = $this->image_height * $this->iscale;
1801        $width  = $this->image_width * $this->iscale;
1802        for ($i = 0; $i < $noise_level; ++$i) {
1803            $x = mt_rand(10, $width);
1804            $y = mt_rand(10, $height);
1805            $size = mt_rand(7, 10);
1806            if ($x - $size <= 0 && $y - $size <= 0) continue; // dont cover 0,0 since it is used by imagedistortedcopy
1807            imagefilledarc($this->tmpimg, $x, $y, $size, $size, 0, 360, $this->gdnoisecolor, IMG_ARC_PIE);
1808        }
1809
1810        $t1 = microtime(true);
1811
1812        $t = $t1 - $t0;
1813
1814        /*
1815        // DEBUG
1816        imagestring($this->tmpimg, 5, 25, 30, "$t", $this->gdnoisecolor);
1817        header('content-type: image/png');
1818        imagepng($this->tmpimg);
1819        exit;
1820        */
1821    }
1822
1823    /**
1824    * Print signature text on image
1825    */
1826    protected function addSignature()
1827    {
1828        $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
1829        $textlen = $bbox[2] - $bbox[0];
1830        $x = $this->image_width - $textlen - 5;
1831        $y = $this->image_height - 3;
1832
1833        imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
1834    }
1835
1836    /**
1837     * Sends the appropriate image and cache headers and outputs image to the browser
1838     */
1839    protected function output()
1840    {
1841        if ($this->canSendHeaders() || $this->send_headers == false) {
1842            if ($this->send_headers) {
1843                // only send the content-type headers if no headers have been output
1844                // this will ease debugging on misconfigured servers where warnings
1845                // may have been output which break the image and prevent easily viewing
1846                // source to see the error.
1847                header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
1848                header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
1849                header("Cache-Control: no-store, no-cache, must-revalidate");
1850                header("Cache-Control: post-check=0, pre-check=0", false);
1851                header("Pragma: no-cache");
1852            }
1853
1854            switch ($this->image_type) {
1855                case self::SI_IMAGE_JPEG:
1856                    if ($this->send_headers) header("Content-Type: image/jpeg");
1857                    imagejpeg($this->im, null, 90);
1858                    break;
1859                case self::SI_IMAGE_GIF:
1860                    if ($this->send_headers) header("Content-Type: image/gif");
1861                    imagegif($this->im);
1862                    break;
1863                default:
1864                    if ($this->send_headers) header("Content-Type: image/png");
1865                    imagepng($this->im);
1866                    break;
1867            }
1868        } else {
1869            echo '<hr /><strong>'
1870                .'Failed to generate captcha image, content has already been '
1871                .'output.<br />This is most likely due to misconfiguration or '
1872                .'a PHP error was sent to the browser.</strong>';
1873        }
1874
1875        imagedestroy($this->im);
1876        restore_error_handler();
1877
1878        if (!$this->no_exit) exit;
1879    }
1880
1881    /**
1882     * Generates an audio captcha in WAV format
1883     *
1884     * @return StringHelper The audio representation of the captcha in Wav format
1885     */
1886    protected function getAudibleCode()
1887    {
1888        $letters = array();
1889        $code    = $this->getCode(true, true);
1890
1891        if (empty($code) || $code['code'] == '') {
1892            if (strlen($this->display_value) > 0) {
1893                $code = array('code' => $this->display_value, 'display' => $this->display_value);
1894            } else {
1895                $this->createCode();
1896                $code = $this->getCode(true);
1897            }
1898        }
1899
1900        if (empty($code)) {
1901            $error = 'Failed to get audible code (are database settings correct?).  Check the error log for details';
1902            trigger_error($error, E_USER_WARNING);
1903            throw new Exception($error);
1904        }
1905
1906        if (preg_match('/(\d+) (\+|-|x) (\d+)/i', $code['display'], $eq)) {
1907            $math = true;
1908
1909            $left  = $eq[1];
1910            $sign  = str_replace(array('+', '-', 'x'), array('plus', 'minus', 'times'), $eq[2]);
1911            $right = $eq[3];
1912
1913            $letters = array($left, $sign, $right);
1914        } else {
1915            $math = false;
1916
1917            $length = strlen($code['display']);
1918
1919            for($i = 0; $i < $length; ++$i) {
1920                $letter    = $code['display']{$i};
1921                $letters[] = $letter;
1922            }
1923        }
1924
1925        try {
1926            return $this->generateWAV($letters);
1927        } catch(Exception $ex) {
1928            throw $ex;
1929        }
1930    }
1931
1932    /**
1933     * Gets a captcha code from a file containing a list of words.
1934     *
1935     * Seek to a random offset in the file and reads a block of data and returns a line from the file.
1936     *
1937     * @param int $numWords Number of words (lines) to read from the file
1938     * @return StringHelper|array  Returns a string if only one word is to be read, or an array of words
1939     */
1940    protected function readCodeFromFile($numWords = 1)
1941    {
1942        $fp = fopen($this->wordlist_file, 'rb');
1943        if (!$fp) return false;
1944
1945        $fsize = filesize($this->wordlist_file);
1946        if ($fsize < 128) return false; // too small of a list to be effective
1947
1948        if ((int)$numWords < 1 || (int)$numWords > 5) $numWords = 1;
1949
1950        $words = array();
1951        $i = 0;
1952        do {
1953            fseek($fp, mt_rand(0, $fsize - 64), SEEK_SET); // seek to a random position of file from 0 to filesize-64
1954            $data = fread($fp, 64); // read a chunk from our random position
1955            $data = preg_replace("/\r?\n/", "\n", $data);
1956
1957            $start = @strpos($data, "\n", mt_rand(0, 56)) + 1; // random start position
1958            $end   = @strpos($data, "\n", $start);          // find end of word
1959
1960            if ($start === false) {
1961                // picked start position at end of file
1962                continue;
1963            } else if ($end === false) {
1964                $end = strlen($data);
1965            }
1966
1967            $word = strtolower(substr($data, $start, $end - $start)); // return a line of the file
1968            $words[] = $word;
1969        } while (++$i < $numWords);
1970
1971        fclose($fp);
1972
1973        if ($numWords < 2) {
1974            return $words[0];
1975        } else {
1976            return $words;
1977        }
1978    }
1979
1980    /**
1981     * Generates a random captcha code from the set character set
1982     *
1983     * @see Securimage::$charset  Charset option
1984     * @return StringHelper A randomly generated CAPTCHA code
1985     */
1986    protected function generateCode()
1987    {
1988        $code = '';
1989
1990        if (function_exists('mb_strlen')) {
1991            for($i = 1, $cslen = mb_strlen($this->charset); $i <= $this->code_length; ++$i) {
1992                $code .= mb_substr($this->charset, mt_rand(0, $cslen - 1), 1, 'UTF-8');
1993            }
1994        } else {
1995            for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
1996                $code .= substr($this->charset, mt_rand(0, $cslen - 1), 1);
1997            }
1998        }
1999
2000        return $code;
2001    }
2002
2003    /**
2004     * Validate a code supplied by the user
2005     *
2006     * Checks the entered code against the value stored in the session and/or database (if configured).  Handles case sensitivity.
2007     * Also removes the code from session/database if the code was entered correctly to prevent re-use attack.
2008     *
2009     * This function does not return a value.
2010     *
2011     * @see Securimage::$correct_code 'correct_code' property
2012     */
2013    protected function validate()
2014    {
2015        if (!is_string($this->code) || strlen($this->code) == 0) {
2016            $code = $this->getCode(true);
2017            // returns stored code, or an empty string if no stored code was found
2018            // checks the session and database if enabled
2019        } else {
2020            $code = $this->code;
2021        }
2022
2023        if (is_array($code)) {
2024            if (!empty($code)) {
2025                $ctime = $code['time'];
2026                $code  = $code['code'];
2027
2028                $this->_timeToSolve = time() - $ctime;
2029            } else {
2030                $code = '';
2031            }
2032        }
2033
2034        if ($this->case_sensitive == false && preg_match('/[A-Z]/', $code)) {
2035            // case sensitive was set from securimage_show.php but not in class
2036            // the code saved in the session has capitals so set case sensitive to true
2037            $this->case_sensitive = true;
2038        }
2039
2040        $code_entered = trim( (($this->case_sensitive) ? $this->code_entered
2041                                                       : strtolower($this->code_entered))
2042                        );
2043        $this->correct_code = false;
2044
2045        if ($code != '') {
2046            if (strpos($code, ' ') !== false) {
2047                // for multi word captchas, remove more than once space from input
2048                $code_entered = preg_replace('/\s+/', ' ', $code_entered);
2049                $code_entered = strtolower($code_entered);
2050            }
2051
2052            if ((string)$code === (string)$code_entered) {
2053                $this->correct_code = true;
2054                if ($this->no_session != true) {
2055                    $_SESSION['securimage_code_disp'] [$this->namespace] = '';
2056                    $_SESSION['securimage_code_value'][$this->namespace] = '';
2057                    $_SESSION['securimage_code_ctime'][$this->namespace] = '';
2058                }
2059                $this->clearCodeFromDatabase();
2060            }
2061        }
2062    }
2063
2064    /**
2065     * Save CAPTCHA data to session and database (if configured)
2066     */
2067    protected function saveData()
2068    {
2069        if ($this->no_session != true) {
2070            if (isset($_SESSION['securimage_code_value']) && is_scalar($_SESSION['securimage_code_value'])) {
2071                // fix for migration from v2 - v3
2072                unset($_SESSION['securimage_code_value']);
2073                unset($_SESSION['securimage_code_ctime']);
2074            }
2075
2076            $_SESSION['securimage_code_disp'] [$this->namespace] = $this->code_display;
2077            $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
2078            $_SESSION['securimage_code_ctime'][$this->namespace] = time();
2079        }
2080
2081        if ($this->use_database) {
2082            $this->saveCodeToDatabase();
2083        }
2084    }
2085
2086    /**
2087     * Saves the CAPTCHA data to the configured database.
2088     */
2089    protected function saveCodeToDatabase()
2090    {
2091        $success = false;
2092        $this->openDatabase();
2093
2094        if ($this->use_database && $this->pdo_conn) {
2095            $id = $this->getCaptchaId(false);
2096            $ip = $_SERVER['REMOTE_ADDR'];
2097
2098            if (empty($id)) {
2099                $id = $ip;
2100            }
2101
2102            $time      = time();
2103            $code      = $this->code;
2104            $code_disp = $this->code_display;
2105
2106            // This is somewhat expensive in PDO Sqlite3 (when there is something to delete)
2107            $this->clearCodeFromDatabase();
2108
2109            $query = "INSERT INTO {$this->database_table} ("
2110                    ."id, code, code_display, namespace, created) "
2111                    ."VALUES(?, ?, ?, ?, ?)";
2112
2113            $stmt    = $this->pdo_conn->prepare($query);
2114            $success = $stmt->execute(array($id, $code, $code_disp, $this->namespace, $time));
2115
2116            if (!$success) {
2117                $err   = $stmt->errorInfo();
2118                $error = "Failed to insert code into database. {$err[1]}: {$err[2]}.";
2119
2120                if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
2121                    $err14 = ($err[1] == 14);
2122                    if ($err14) $error .= sprintf(" Ensure database directory and file are writeable by user '%s' (%d).",
2123                                                   get_current_user(), getmyuid());
2124                }
2125
2126                trigger_error($error, E_USER_WARNING);
2127            }
2128        }
2129
2130        return $success !== false;
2131    }
2132
2133    /**
2134     * Opens a connection to the configured database.
2135     *
2136     * @see Securimage::$use_database Use database
2137     * @see Securimage::$database_driver Database driver
2138     * @see Securimage::$pdo_conn pdo_conn
2139     * @return bool true if the database connection was successful, false if not
2140     */
2141    protected function openDatabase()
2142    {
2143        $this->pdo_conn = false;
2144
2145        if ($this->use_database) {
2146            $pdo_extension = 'PDO_' . strtoupper($this->database_driver);
2147
2148            if (!extension_loaded($pdo_extension)) {
2149                trigger_error("Database support is turned on in Securimage, but the chosen extension $pdo_extension is not loaded in PHP.", E_USER_WARNING);
2150                return false;
2151            }
2152        }
2153
2154        if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
2155            if (!file_exists($this->database_file)) {
2156                $fp = fopen($this->database_file, 'w+');
2157                if (!$fp) {
2158                    $err = error_get_last();
2159                    trigger_error("Securimage failed to create SQLite3 database file '{$this->database_file}'. Reason: {$err['message']}", E_USER_WARNING);
2160                    return false;
2161                }
2162                fclose($fp);
2163                chmod($this->database_file, 0666);
2164            } else if (!is_writeable($this->database_file)) {
2165                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);
2166                return false;
2167            }
2168        }
2169
2170        try {
2171            $dsn = $this->getDsn();
2172
2173            $options = array();
2174            $this->pdo_conn = new PDO($dsn, $this->database_user, $this->database_pass, $options);
2175        } catch (PDOException $pdoex) {
2176            trigger_error("Database connection failed: " . $pdoex->getMessage(), E_USER_WARNING);
2177            return false;
2178        } catch (Exception $ex) {
2179            trigger_error($ex->getMessage(), E_USER_WARNING);
2180            return false;
2181        }
2182
2183        try {
2184            if (!$this->skip_table_check && !$this->checkTablesExist()) {
2185                // create tables...
2186                $this->createDatabaseTables();
2187            }
2188        } catch (Exception $ex) {
2189            trigger_error($ex->getMessage(), E_USER_WARNING);
2190            $this->pdo_conn = null;
2191            return false;
2192        }
2193
2194        if (mt_rand(0, 100) / 100.0 == 1.0) {
2195            $this->purgeOldCodesFromDatabase();
2196        }
2197
2198        return $this->pdo_conn;
2199    }
2200
2201    /**
2202     * Get the PDO DSN string for connecting to the database
2203     *
2204     * @see Securimage::$database_driver Database driver
2205     * @throws Exception  If database specific options are not configured
2206     * @return StringHelper     The DSN for connecting to the database
2207     */
2208    protected function getDsn()
2209    {
2210        $dsn = sprintf('%s:', $this->database_driver);
2211
2212        switch($this->database_driver) {
2213            case self::SI_DRIVER_SQLITE3:
2214                $dsn .= $this->database_file;
2215                break;
2216
2217            case self::SI_DRIVER_MYSQL:
2218            case self::SI_DRIVER_PGSQL:
2219                if (empty($this->database_host)) {
2220                    throw new Exception('Securimage::database_host is not set');
2221                } else if (empty($this->database_name)) {
2222                    throw new Exception('Securimage::database_name is not set');
2223                }
2224
2225                $dsn .= sprintf('host=%s;dbname=%s',
2226                                $this->database_host,
2227                                $this->database_name);
2228                break;
2229
2230        }
2231
2232        return $dsn;
2233    }
2234
2235    /**
2236     * Checks if the necessary database tables for storing captcha codes exist
2237     *
2238     * @throws Exception If the table check failed for some reason
2239     * @return boolean true if the database do exist, false if not
2240     */
2241    protected function checkTablesExist()
2242    {
2243        $table = $this->pdo_conn->quote($this->database_table);
2244
2245        switch($this->database_driver) {
2246            case self::SI_DRIVER_SQLITE3:
2247                // query row count for sqlite, PRAGMA queries seem to return no
2248                // rowCount using PDO even if there are rows returned
2249                $query = "SELECT COUNT(id) FROM $table";
2250                break;
2251
2252            case self::SI_DRIVER_MYSQL:
2253                $query = "SHOW TABLES LIKE $table";
2254                break;
2255
2256            case self::SI_DRIVER_PGSQL:
2257                $query = "SELECT * FROM information_schema.columns WHERE table_name = $table;";
2258                break;
2259        }
2260
2261        $result = $this->pdo_conn->query($query);
2262
2263        if (!$result) {
2264            $err = $this->pdo_conn->errorInfo();
2265
2266            if ($this->database_driver == self::SI_DRIVER_SQLITE3 &&
2267                $err[1] === 1 && strpos($err[2], 'no such table') !== false)
2268            {
2269                return false;
2270            }
2271
2272            throw new Exception("Failed to check tables: {$err[0]} - {$err[1]}: {$err[2]}");
2273        } else if ($this->database_driver == self::SI_DRIVER_SQLITE3) {
2274            // successful here regardless of row count for sqlite
2275            return true;
2276        } else if ($result->rowCount() == 0) {
2277            return false;
2278        } else {
2279            return true;
2280        }
2281    }
2282
2283    /**
2284     * Create the necessary databaes table for storing captcha codes.
2285     *
2286     * Based on the database adapter used, the tables will created in the existing connection.
2287     *
2288     * @see Securimage::$database_driver Database driver
2289     * @return boolean true if the tables were created, false if not
2290     */
2291    protected function createDatabaseTables()
2292    {
2293        $queries = array();
2294
2295        switch($this->database_driver) {
2296            case self::SI_DRIVER_SQLITE3:
2297                $queries[] = "CREATE TABLE \"{$this->database_table}\" (
2298                                id VARCHAR(40),
2299                                namespace VARCHAR(32) NOT NULL,
2300                                code VARCHAR(32) NOT NULL,
2301                                code_display VARCHAR(32) NOT NULL,
2302                                created INTEGER NOT NULL,
2303                                PRIMARY KEY(id, namespace)
2304                              )";
2305
2306                $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created)";
2307                break;
2308
2309            case self::SI_DRIVER_MYSQL:
2310                $queries[] = "CREATE TABLE `{$this->database_table}` (
2311                                `id` VARCHAR(40) NOT NULL,
2312                                `namespace` VARCHAR(32) NOT NULL,
2313                                `code` VARCHAR(32) NOT NULL,
2314                                `code_display` VARCHAR(32) NOT NULL,
2315                                `created` INT NOT NULL,
2316                                PRIMARY KEY(id, namespace),
2317                                INDEX(created)
2318                              )";
2319                break;
2320
2321            case self::SI_DRIVER_PGSQL:
2322                $queries[] = "CREATE TABLE {$this->database_table} (
2323                                id character varying(40) NOT NULL,
2324                                namespace character varying(32) NOT NULL,
2325                                code character varying(32) NOT NULL,
2326                                code_display character varying(32) NOT NULL,
2327                                created integer NOT NULL,
2328                                CONSTRAINT pkey_id_namespace PRIMARY KEY (id, namespace)
2329                              )";
2330
2331                $queries[] = "CREATE INDEX ndx_created ON {$this->database_table} (created);";
2332                break;
2333        }
2334
2335        $this->pdo_conn->beginTransaction();
2336
2337        foreach($queries as $query) {
2338            $result = $this->pdo_conn->query($query);
2339
2340            if (!$result) {
2341                $err = $this->pdo_conn->errorInfo();
2342                trigger_error("Failed to create table.  {$err[1]}: {$err[2]}", E_USER_WARNING);
2343                $this->pdo_conn->rollBack();
2344                $this->pdo_conn = false;
2345                return false;
2346            }
2347        }
2348
2349        $this->pdo_conn->commit();
2350
2351        return true;
2352    }
2353
2354    /**
2355     * Retrieves a stored code from the database for based on the captchaId or
2356     * IP address if captcha ID not used.
2357     *
2358     * @return StringHelper|array Empty string if no code was found or has expired,
2359     * otherwise returns array of code information.
2360     */
2361    protected function getCodeFromDatabase()
2362    {
2363        $code = '';
2364
2365        if ($this->use_database == true && $this->pdo_conn) {
2366            if (Securimage::$_captchaId !== null) {
2367                $query  = "SELECT * FROM {$this->database_table} WHERE id = ?";
2368                $stmt   = $this->pdo_conn->prepare($query);
2369                $result = $stmt->execute(array(Securimage::$_captchaId));
2370            } else {
2371                $ip = $_SERVER['REMOTE_ADDR'];
2372                $ns = $this->namespace;
2373
2374                // ip is stored in id column when no captchaId
2375                $query  = "SELECT * FROM {$this->database_table} WHERE id = ? AND namespace = ?";
2376                $stmt   = $this->pdo_conn->prepare($query);
2377                $result = $stmt->execute(array($ip, $ns));
2378            }
2379
2380            if (!$result) {
2381                $err = $this->pdo_conn->errorInfo();
2382                trigger_error("Failed to select code from database.  {$err[0]}: {$err[1]}", E_USER_WARNING);
2383            } else {
2384                if ( ($row = $stmt->fetch()) !== false ) {
2385                    if (false == $this->isCodeExpired($row['created'])) {
2386                        $code = array(
2387                            'code'      => $row['code'],
2388                            'code_disp' => $row['code_display'],
2389                            'time'      => $row['created'],
2390                        );
2391                    }
2392                }
2393            }
2394        }
2395
2396        return $code;
2397    }
2398
2399    /**
2400     * Remove a stored code from the database based on captchaId or IP address.
2401     */
2402    protected function clearCodeFromDatabase()
2403    {
2404        if ($this->pdo_conn) {
2405            $ip = $_SERVER['REMOTE_ADDR'];
2406            $ns = $this->pdo_conn->quote($this->namespace);
2407            $id = Securimage::$_captchaId;
2408
2409            if (empty($id)) {
2410                $id = $ip; // if no captchaId set, IP address is captchaId.
2411            }
2412
2413            $id = $this->pdo_conn->quote($id);
2414
2415            $query = sprintf("DELETE FROM %s WHERE id = %s AND namespace = %s",
2416                             $this->database_table, $id, $ns);
2417
2418            $result = $this->pdo_conn->query($query);
2419            if (!$result) {
2420                trigger_error("Failed to delete code from database.", E_USER_WARNING);
2421            }
2422        }
2423    }
2424
2425    /**
2426     * Deletes old (expired) codes from the database
2427     */
2428    protected function purgeOldCodesFromDatabase()
2429    {
2430        if ($this->use_database && $this->pdo_conn) {
2431            $now   = time();
2432            $limit = (!is_numeric($this->expiry_time) || $this->expiry_time < 1) ? 86400 : $this->expiry_time;
2433
2434            $query = sprintf("DELETE FROM %s WHERE %s - created > %s",
2435                             $this->database_table,
2436                             $this->pdo_conn->quote($now, PDO::PARAM_INT),
2437                             $this->pdo_conn->quote($limit, PDO::PARAM_INT));
2438
2439            $result = $this->pdo_conn->query($query);
2440        }
2441    }
2442
2443    /**
2444     * Checks to see if the captcha code has expired and can no longer be used.
2445     *
2446     * @see Securimage::$expiry_time expiry_time
2447     * @param int $creation_time  The Unix timestamp of when the captcha code was created
2448     * @return bool true if the code is expired, false if it is still valid
2449     */
2450    protected function isCodeExpired($creation_time)
2451    {
2452        $expired = true;
2453
2454        if (!is_numeric($this->expiry_time) || $this->expiry_time < 1) {
2455            $expired = false;
2456        } else if (time() - $creation_time < $this->expiry_time) {
2457            $expired = false;
2458        }
2459
2460        return $expired;
2461    }
2462
2463    /**
2464     * Generate a wav file given the $letters in the code
2465     *
2466     * @param array $letters  The letters making up the captcha
2467     * @return StringHelper The audio content in WAV format
2468     */
2469    protected function generateWAV($letters)
2470    {
2471        $wavCaptcha = new WavFile();
2472        $first      = true;     // reading first wav file
2473
2474        if ($this->audio_use_sox && !is_executable($this->sox_binary_path)) {
2475            throw new Exception("Path to SoX binary is incorrect or not executable");
2476        }
2477
2478        foreach ($letters as $letter) {
2479            $letter = strtoupper($letter);
2480
2481            try {
2482                $letter_file = realpath($this->audio_path) . DIRECTORY_SEPARATOR . $letter . '.wav';
2483
2484                if ($this->audio_use_sox) {
2485                    $sox_cmd = sprintf("%s %s -t wav - %s",
2486                                       $this->sox_binary_path,
2487                                       $letter_file,
2488                                       $this->getSoxEffectChain());
2489
2490                    $data = `$sox_cmd`;
2491
2492                    $l = new WavFile();
2493                    $l->setIgnoreChunkSizes(true);
2494                    $l->setWavData($data);
2495                } else {
2496                    $l = new WavFile($letter_file);
2497                }
2498
2499                if ($first) {
2500                    // set sample rate, bits/sample, and # of channels for file based on first letter
2501                    $wavCaptcha->setSampleRate($l->getSampleRate())
2502                               ->setBitsPerSample($l->getBitsPerSample())
2503                               ->setNumChannels($l->getNumChannels());
2504                    $first = false;
2505                }
2506
2507                // append letter to the captcha audio
2508                $wavCaptcha->appendWav($l);
2509
2510                // random length of silence between $audio_gap_min and $audio_gap_max
2511                if ($this->audio_gap_max > 0 && $this->audio_gap_max > $this->audio_gap_min) {
2512                    $wavCaptcha->insertSilence( mt_rand($this->audio_gap_min, $this->audio_gap_max) / 1000.0 );
2513                }
2514            } catch (Exception $ex) {
2515                // failed to open file, or the wav file is broken or not supported
2516                // 2 wav files were not compatible, different # channels, bits/sample, or sample rate
2517                throw new Exception("Error generating audio captcha on letter '$letter': " . $ex->getMessage());
2518            }
2519        }
2520
2521        /********* Set up audio filters *****************************/
2522        $filters = array();
2523
2524        if ($this->audio_use_noise == true) {
2525            // use background audio - find random file
2526            $wavNoise   = false;
2527            $randOffset = 0;
2528
2529            /*
2530            // uncomment to try experimental SoX noise generation.
2531            // warning: sounds may be considered annoying
2532            if ($this->audio_use_sox) {
2533                $duration = $wavCaptcha->getDataSize() / ($wavCaptcha->getBitsPerSample() / 8) /
2534                            $wavCaptcha->getNumChannels() / $wavCaptcha->getSampleRate();
2535                $duration = round($duration, 2);
2536                $wavNoise = new WavFile();
2537                $wavNoise->setIgnoreChunkSizes(true);
2538                $noiseData = $this->getSoxNoiseData($duration,
2539                                                    $wavCaptcha->getNumChannels(),
2540                                                    $wavCaptcha->getSampleRate(),
2541                                                    $wavCaptcha->getBitsPerSample());
2542                $wavNoise->setWavData($noiseData, true);
2543
2544            } else
2545            */
2546            if ( ($noiseFile = $this->getRandomNoiseFile()) !== false) {
2547                try {
2548                    $wavNoise = new WavFile($noiseFile, false);
2549                } catch(Exception $ex) {
2550                    throw $ex;
2551                }
2552
2553                // start at a random offset from the beginning of the wavfile
2554                // in order to add more randomness
2555
2556                $randOffset = 0;
2557
2558                if ($wavNoise->getNumBlocks() > 2 * $wavCaptcha->getNumBlocks()) {
2559                    $randBlock = mt_rand(0, $wavNoise->getNumBlocks() - $wavCaptcha->getNumBlocks());
2560                    $wavNoise->readWavData($randBlock * $wavNoise->getBlockAlign(), $wavCaptcha->getNumBlocks() * $wavNoise->getBlockAlign());
2561                } else {
2562                    $wavNoise->readWavData();
2563                    $randOffset = mt_rand(0, $wavNoise->getNumBlocks() - 1);
2564                }
2565            }
2566
2567            if ($wavNoise !== false) {
2568                $mixOpts = array('wav'  => $wavNoise,
2569                                 'loop' => true,
2570                                 'blockOffset' => $randOffset);
2571
2572                $filters[WavFile::FILTER_MIX]       = $mixOpts;
2573                $filters[WavFile::FILTER_NORMALIZE] = $this->audio_mix_normalization;
2574            }
2575        }
2576
2577        if ($this->degrade_audio == true) {
2578            // add random noise.
2579            // any noise level below 95% is intensely distorted and not pleasant to the ear
2580            $filters[WavFile::FILTER_DEGRADE] = mt_rand(95, 98) / 100.0;
2581        }
2582
2583        if (!empty($filters)) {
2584            $wavCaptcha->filter($filters);  // apply filters to captcha audio
2585        }
2586
2587        return $wavCaptcha->__toString();
2588    }
2589
2590    /**
2591     * Gets and returns the path to a random noise file from the audio noise directory.
2592     *
2593     * @return bool|StringHelper  false if a file could not be found, or a string containing the path to the file.
2594     */
2595    public function getRandomNoiseFile()
2596    {
2597        $return = false;
2598
2599        if ( ($dh = opendir($this->audio_noise_path)) !== false ) {
2600            $list = array();
2601
2602            while ( ($file = readdir($dh)) !== false ) {
2603                if ($file == '.' || $file == '..') continue;
2604                if (strtolower(substr($file, -4)) != '.wav') continue;
2605
2606                $list[] = $file;
2607            }
2608
2609            closedir($dh);
2610
2611            if (sizeof($list) > 0) {
2612                $file   = $list[array_rand($list, 1)];
2613                $return = $this->audio_noise_path . DIRECTORY_SEPARATOR . $file;
2614
2615                if (!is_readable($return)) $return = false;
2616            }
2617        }
2618
2619        return $return;
2620    }
2621
2622    /**
2623     * Get a random effect or chain of effects to apply to a segment of the
2624     * audio file.
2625     *
2626     * These effects should increase the randomness of the audio for
2627     * a particular letter/number by modulating the signal.  The SoX effects
2628     * used are *bend*, *chorus*, *overdrive*, *pitch*, *reverb*, *tempo*, and
2629     * *tremolo*.
2630     *
2631     * For each effect selected, random parameters are supplied to the effect.
2632     *
2633     * @param int $numEffects  How many effects to chain together
2634     * @return StringHelper  A string of valid SoX effects and their respective options.
2635     */
2636    protected function getSoxEffectChain($numEffects = 2)
2637    {
2638        $effectsList = array('bend', 'chorus', 'overdrive', 'pitch', 'reverb', 'tempo', 'tremolo');
2639        $effects     = array_rand($effectsList, $numEffects);
2640        $outEffects  = array();
2641
2642        if (!is_array($effects)) $effects = array($effects);
2643
2644        foreach($effects as $effect) {
2645            $effect = $effectsList[$effect];
2646
2647            switch($effect)
2648            {
2649                case 'bend':
2650                    $delay = mt_rand(0, 15) / 100.0;
2651                    $cents = mt_rand(-120, 120);
2652                    $dur   = mt_rand(75, 400) / 100.0;
2653                    $outEffects[] = "$effect $delay,$cents,$dur";
2654                    break;
2655
2656                case 'chorus':
2657                    $gainIn  = mt_rand(75, 90) / 100.0;
2658                    $gainOut = mt_rand(70, 95) / 100.0;
2659                    $chorStr = "$effect $gainIn $gainOut";
2660
2661                    for ($i = 0; $i < mt_rand(2, 3); ++$i) {
2662                        $delay = mt_rand(20, 100);
2663                        $decay = mt_rand(10, 100) / 100.0;
2664                        $speed = mt_rand(20, 50) / 100.0;
2665                        $depth = mt_rand(150, 250) / 100.0;
2666
2667                        $chorStr .= " $delay $decay $speed $depth -s";
2668                    }
2669
2670                    $outEffects[] = $chorStr;
2671                    break;
2672
2673                case 'overdrive':
2674                    $gain = mt_rand(5, 25);
2675                    $color = mt_rand(20, 70);
2676                    $outEffects[] = "$effect $gain $color";
2677                    break;
2678
2679                case 'pitch':
2680                    $cents = mt_rand(-300, 300);
2681                    $outEffects[] = "$effect $cents";
2682                    break;
2683
2684                case 'reverb':
2685                    $reverberance = mt_rand(20, 80);
2686                    $damping      = mt_rand(10, 80);
2687                    $scale        = mt_rand(85, 100);
2688                    $depth        = mt_rand(90, 100);
2689                    $predelay     = mt_rand(0, 5);
2690                    $outEffects[] = "$effect $reverberance $damping $scale $depth $predelay";
2691                    break;
2692
2693                case 'tempo':
2694                    $factor = mt_rand(65, 135) / 100.0;
2695                    $outEffects[] = "$effect -s $factor";
2696                    break;
2697
2698                case 'tremolo':
2699                    $hz    = mt_rand(10, 30);
2700                    $depth = mt_rand(40, 85);
2701                    $outEffects[] = "$effect $hz $depth";
2702                    break;
2703            }
2704        }
2705
2706        return implode(' ', $outEffects);
2707    }
2708
2709    /**
2710     * This function is not yet used.
2711     *
2712     * Generate random background noise from sweeping oscillators
2713     *
2714     * @param float $duration  How long in seconds the generated sound will be
2715     * @param int $numChannels Number of channels in output wav
2716     * @param int $sampleRate  Sample rate of output wav
2717     * @param int $bitRate     Bits per sample (8, 16, 24)
2718     * @return StringHelper          Audio data in wav format
2719     */
2720    protected function getSoxNoiseData($duration, $numChannels, $sampleRate, $bitRate)
2721    {
2722        $shapes = array('sine', 'square', 'triangle', 'sawtooth', 'trapezium');
2723        $steps  = array(':', '+', '/', '-');
2724        $selShapes = array_rand($shapes, 2);
2725        $selSteps  = array_rand($steps, 2);
2726        $sweep0    = array();
2727        $sweep0[0] = mt_rand(100, 700);
2728        $sweep0[1] = mt_rand(1500, 2500);
2729        $sweep1    = array();
2730        $sweep1[0] = mt_rand(500, 1000);
2731        $sweep1[1] = mt_rand(1200, 2000);
2732
2733        if (mt_rand(0, 10) % 2 == 0)
2734            $sweep0 = array_reverse($sweep0);
2735
2736        if (mt_rand(0, 10) % 2 == 0)
2737            $sweep1 = array_reverse($sweep1);
2738
2739        $cmd = sprintf("%s -c %d -r %d -b %d -n -t wav - synth noise create vol 0.3 synth %.2f %s mix %d%s%d vol 0.3 synth %.2f %s fmod %d%s%d vol 0.3",
2740                       $this->sox_binary_path,
2741                       $numChannels,
2742                       $sampleRate,
2743                       $bitRate,
2744                       $duration,
2745                       $shapes[$selShapes[0]],
2746                       $sweep0[0],
2747                       $steps[$selSteps[0]],
2748                       $sweep0[1],
2749                       $duration,
2750                       $shapes[$selShapes[1]],
2751                       $sweep1[0],
2752                       $steps[$selSteps[1]],
2753                       $sweep1[1]
2754                       );
2755        $data = `$cmd`;
2756
2757        return $data;
2758    }
2759
2760    /**
2761     * Return a wav file saying there was an error generating file
2762     *
2763     * @return StringHelper The binary audio contents
2764     */
2765    protected function audioError()
2766    {
2767        return @file_get_contents(dirname(__FILE__) . '/audio/en/error.wav');
2768    }
2769
2770    /**
2771     * Checks to see if headers can be sent and if any error has been output
2772     * to the browser
2773     *
2774     * @return bool true if it is safe to send headers, false if not
2775     */
2776    protected function canSendHeaders()
2777    {
2778        if (headers_sent()) {
2779            // output has been flushed and headers have already been sent
2780            return false;
2781        } else if (strlen((string)ob_get_contents()) > 0) {
2782            // headers haven't been sent, but there is data in the buffer that will break image and audio data
2783            return false;
2784        }
2785
2786        return true;
2787    }
2788
2789    /**
2790     * Return a random float between 0 and 0.9999
2791     *
2792     * @return float Random float between 0 and 0.9999
2793     */
2794    function frand()
2795    {
2796        return 0.0001 * mt_rand(0,9999);
2797    }
2798
2799    /**
2800     * Convert an html color code to a Securimage_Color
2801     * @param StringHelper $color
2802     * @param Securimage_Color $default The defalt color to use if $color is invalid
2803     */
2804    protected function initColor($color, $default)
2805    {
2806        if ($color == null) {
2807            return new Securimage_Color($default);
2808        } else if (is_string($color)) {
2809            try {
2810                return new Securimage_Color($color);
2811            } catch(Exception $e) {
2812                return new Securimage_Color($default);
2813            }
2814        } else if (is_array($color) && sizeof($color) == 3) {
2815            return new Securimage_Color($color[0], $color[1], $color[2]);
2816        } else {
2817            return new Securimage_Color($default);
2818        }
2819    }
2820
2821    /**
2822     * The error handling function used when outputting captcha image or audio.
2823     *
2824     * This error handler helps determine if any errors raised would
2825     * prevent captcha image or audio from displaying.  If they have
2826     * no effect on the output buffer or headers, true is returned so
2827     * the script can continue processing.
2828     *
2829     * See https://github.com/dapphp/securimage/issues/15
2830     *
2831     * @param int $errno  PHP error number
2832     * @param StringHelper $errstr  String description of the error
2833     * @param StringHelper $errfile  File error occurred in
2834     * @param int $errline  Line the error occurred on in file
2835     * @param array $errcontext  Additional context information
2836     * @return boolean true if the error was handled, false if PHP should handle the error
2837     */
2838    public function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array())
2839    {
2840        // get the current error reporting level
2841        $level = error_reporting();
2842
2843        // if error was supressed or $errno not set in current error level
2844        if ($level == 0 || ($level & $errno) == 0) {
2845            return true;
2846        }
2847
2848        return false;
2849    }
2850}
2851
2852
2853/**
2854 * Color object for Securimage CAPTCHA
2855 *
2856 * @version 3.0
2857 * @since 2.0
2858 * @package Securimage
2859 * @subpackage classes
2860 *
2861 */
2862class Securimage_Color
2863{
2864    /**
2865     * Red value (0-255)
2866     * @var int
2867     */
2868    public $r;
2869
2870    /**
2871     * Gree value (0-255)
2872     * @var int
2873     */
2874    public $g;
2875
2876    /**
2877     * Blue value (0-255)
2878     * @var int
2879     */
2880    public $b;
2881
2882    /**
2883     * Create a new Securimage_Color object.
2884     *
2885     * Constructor expects 1 or 3 arguments.
2886     *
2887     * When passing a single argument, specify the color using HTML hex format.
2888     *
2889     * When passing 3 arguments, specify each RGB component (from 0-255)
2890     * individually.
2891     *
2892     * Examples:
2893     *
2894     *     $color = new Securimage_Color('#0080FF');
2895     *     $color = new Securimage_Color(0, 128, 255);
2896     *
2897     * @param StringHelper $color  The html color code to use
2898     * @throws Exception  If any color value is not valid
2899     */
2900    public function __construct($color = '#ffffff')
2901    {
2902        $args = func_get_args();
2903
2904        if (sizeof($args) == 0) {
2905            $this->r = 255;
2906            $this->g = 255;
2907            $this->b = 255;
2908        } else if (sizeof($args) == 1) {
2909            // set based on html code
2910            if (substr($color, 0, 1) == '#') {
2911                $color = substr($color, 1);
2912            }
2913
2914            if (strlen($color) != 3 && strlen($color) != 6) {
2915                throw new InvalidArgumentException(
2916                  'Invalid HTML color code passed to Securimage_Color'
2917                );
2918            }
2919
2920            $this->constructHTML($color);
2921        } else if (sizeof($args) == 3) {
2922            $this->constructRGB($args[0], $args[1], $args[2]);
2923        } else {
2924            throw new InvalidArgumentException(
2925              'Securimage_Color constructor expects 0, 1 or 3 arguments; ' . sizeof($args) . ' given'
2926            );
2927        }
2928    }
2929
2930    /**
2931     * Construct from an rgb triplet
2932     *
2933     * @param int $red The red component, 0-255
2934     * @param int $green The green component, 0-255
2935     * @param int $blue The blue component, 0-255
2936     */
2937    protected function constructRGB($red, $green, $blue)
2938    {
2939        if ($red < 0)     $red   = 0;
2940        if ($red > 255)   $red   = 255;
2941        if ($green < 0)   $green = 0;
2942        if ($green > 255) $green = 255;
2943        if ($blue < 0)    $blue  = 0;
2944        if ($blue > 255)  $blue  = 255;
2945
2946        $this->r = $red;
2947        $this->g = $green;
2948        $this->b = $blue;
2949    }
2950
2951    /**
2952     * Construct from an html hex color code
2953     *
2954     * @param StringHelper $color
2955     */
2956    protected function constructHTML($color)
2957    {
2958        if (strlen($color) == 3) {
2959            $red   = str_repeat(substr($color, 0, 1), 2);
2960            $green = str_repeat(substr($color, 1, 1), 2);
2961            $blue  = str_repeat(substr($color, 2, 1), 2);
2962        } else {
2963            $red   = substr($color, 0, 2);
2964            $green = substr($color, 2, 2);
2965            $blue  = substr($color, 4, 2);
2966        }
2967
2968        $this->r = hexdec($red);
2969        $this->g = hexdec($green);
2970        $this->b = hexdec($blue);
2971    }
2972}
2973