Gentics Portal.Node PHP API
 All Classes Namespaces Functions Variables Pages
securimage.php
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 for creating and managing form CAPTCHA images<br />
7  * File: securimage.php<br />
8  *
9  * Copyright (c) 2012, 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.<br /><br />
35  *
36  * If you found this script useful, please take a quick moment to rate it.<br />
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 2012 Drew Phillips
43  * @author Drew Phillips <drew@drew-phillips.com>
44  * @version 3.2RC3 (May 2012)
45  * @package Securimage
46  *
47  */
48 
49 /**
50  ChangeLog
51 
52  3.2RC3
53  - Fix canSendHeaders() check which was breaking if a PHP startup error was issued
54 
55  3.2RC2
56  - Add error handler (https://github.com/dapphp/securimage/issues/15)
57  - Fix flash examples to use the correct value name for audio parameter
58 
59  3.2RC1
60  - New audio captcha code. Faster, fully dynamic audio, full WAV support
61  (Paul Voegler, Drew Phillips) <http://voegler.eu/pub/audio>
62  - New Flash audio streaming button. User defined image and size supported
63  - Additional options for customizing captcha (noise_level, send_headers,
64  no_exit, no_session, display_value
65  - Add captcha ID support. Uses sqlite and unique captcha IDs to track captchas,
66  no session used
67  - Add static methods for creating and validating captcha by ID
68  - Automatic clearing of old codes from SQLite database
69 
70  3.0.3Beta
71  - Add improved mixing function to WavFile class (Paul Voegler)
72  - Improve performance and security of captcha audio (Paul Voegler, Drew Phillips)
73  - Add option to use random file as background noise in captcha audio
74  - Add new securimage options for audio files
75 
76  3.0.2Beta
77  - Fix issue with session variables when upgrading from 2.0 - 3.0
78  - Improve audio captcha, switch to use WavFile class, make mathematical captcha audio work
79 
80  3.0.1
81  - Bugfix: removed use of deprecated variable in addSignature method that would cause errors with display_errors on
82 
83  3.0
84  - Rewrite class using PHP5 OOP
85  - Remove support for GD fonts, require FreeType
86  - Remove support for multi-color codes
87  - Add option to make codes case-sensitive
88  - Add namespaces to support multiple captchas on a single page or page specific captchas
89  - Add option to show simple math problems instead of codes
90  - Remove support for mp3 files due to vulnerability in decoding mp3 audio files
91  - Create new flash file to stream wav files instead of mp3
92  - Changed to BSD license
93 
94  2.0.2
95  - Fix pathing to make integration into libraries easier (Nathan Phillip Brink ohnobinki@ohnopublishing.net)
96 
97  2.0.1
98  - Add support for browsers with cookies disabled (requires php5, sqlite) maps users to md5 hashed ip addresses and md5 hashed codes for security
99  - 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)
100  - Check for previous definition of image type constants (Mike Challis)
101  - Fix mime type settings for audio output
102  - Fixed color allocation issues with multiple colors and background images, consolidate allocation to one function
103  - Ability to let codes expire after a given length of time
104  - Allow HTML color codes to be passed to Securimage_Color (suggested by Mike Challis)
105 
106  2.0.0
107  - Add mathematical distortion to characters (using code from HKCaptcha)
108  - Improved session support
109  - Added Securimage_Color class for easier color definitions
110  - Add distortion to audio output to prevent binary comparison attack (proposed by Sven "SavageTiger" Hagemann [insecurity.nl])
111  - Flash button to stream mp3 audio (Douglas Walsh www.douglaswalsh.net)
112  - Audio output is mp3 format by default
113  - Change font to AlteHaasGrotesk by yann le coroller
114  - Some code cleanup
115 
116  1.0.4 (unreleased)
117  - Ability to output audible codes in mp3 format to stream from flash
118 
119  1.0.3.1
120  - Error reading from wordlist in some cases caused words to be cut off 1 letter short
121 
122  1.0.3
123  - Removed shadow_text from code which could cause an undefined property error due to removal from previous version
124 
125  1.0.2
126  - Audible CAPTCHA Code wav files
127  - Create codes from a word list instead of random strings
128 
129  1.0
130  - Added the ability to use a selected character set, rather than a-z0-9 only.
131  - Added the multi-color text option to use different colors for each letter.
132  - Switched to automatic session handling instead of using files for code storage
133  - Added GD Font support if ttf support is not available. Can use internal GD fonts or load new ones.
134  - Added the ability to set line thickness
135  - Added option for drawing arced lines over letters
136  - Added ability to choose image type for output
137 
138  */
139 
140 
141 /**
142  * Securimage CAPTCHA Class.
143  *
144  * @version 3.0
145  * @package Securimage
146  * @subpackage classes
147  * @author Drew Phillips <drew@drew-phillips.com>
148  *
149  */
151 {
152  // All of the public variables below are securimage options
153  // They can be passed as an array to the Securimage constructor, set below,
154  // or set from securimage_show.php and securimage_play.php
155 
156  /**
157  * Renders captcha as a JPEG image
158  * @var int
159  */
160  const SI_IMAGE_JPEG = 1;
161  /**
162  * Renders captcha as a PNG image (default)
163  * @var int
164  */
165  const SI_IMAGE_PNG = 2;
166  /**
167  * Renders captcha as a GIF image
168  * @var int
169  */
170  const SI_IMAGE_GIF = 3;
171 
172  /**
173  * Create a normal alphanumeric captcha
174  * @var int
175  */
176  const SI_CAPTCHA_STRING = 0;
177  /**
178  * Create a captcha consisting of a simple math problem
179  * @var int
180  */
181  const SI_CAPTCHA_MATHEMATIC = 1;
182 
183  /**
184  * The width of the captcha image
185  * @var int
186  */
187  public $image_width = 215;
188  /**
189  * The height of the captcha image
190  * @var int
191  */
192  public $image_height = 80;
193  /**
194  * The type of the image, default = png
195  * @var int
196  */
197  public $image_type = self::SI_IMAGE_PNG;
198 
199  /**
200  * The background color of the captcha
201  * @var Securimage_Color
202  */
203  public $image_bg_color = '#ffffff';
204  /**
205  * The color of the captcha text
206  * @var Securimage_Color
207  */
208  public $text_color = '#707070';
209  /**
210  * The color of the lines over the captcha
211  * @var Securimage_Color
212  */
213  public $line_color = '#707070';
214  /**
215  * The color of the noise that is drawn
216  * @var Securimage_Color
217  */
218  public $noise_color = '#707070';
219 
220  /**
221  * How transparent to make the text 0 = completely opaque, 100 = invisible
222  * @var int
223  */
224  public $text_transparency_percentage = 50;
225  /**
226  * Whether or not to draw the text transparently, true = use transparency, false = no transparency
227  * @var bool
228  */
229  public $use_transparent_text = false;
230 
231  /**
232  * The length of the captcha code
233  * @var int
234  */
235  public $code_length = 6;
236  /**
237  * Whether the captcha should be case sensitive (not recommended, use only for maximum protection)
238  * @var bool
239  */
240  public $case_sensitive = false;
241  /**
242  * The character set to use for generating the captcha code
243  * @var string
244  */
245  public $charset = 'ABCDEFGHKLMNPRSTUVWYZabcdefghklmnprstuvwyz23456789';
246  /**
247  * How long in seconds a captcha remains valid, after this time it will not be accepted
248  * @var unknown_type
249  */
250  public $expiry_time = 900;
251 
252  /**
253  * The session name securimage should use, only set this if your application uses a custom session name
254  * It is recommended to set this value below so it is used by all securimage scripts
255  * @var string
256  */
257  public $session_name = null;
258 
259  /**
260  * true to use the wordlist file, false to generate random captcha codes
261  * @var bool
262  */
263  public $use_wordlist = false;
264 
265  /**
266  * The level of distortion, 0.75 = normal, 1.0 = very high distortion
267  * @var double
268  */
269  public $perturbation = 0.75;
270  /**
271  * How many lines to draw over the captcha code to increase security
272  * @var int
273  */
274  public $num_lines = 8;
275  /**
276  * The level of noise (random dots) to place on the image, 0-10
277  * @var int
278  */
279  public $noise_level = 0;
280 
281  /**
282  * The signature text to draw on the bottom corner of the image
283  * @var string
284  */
285  public $image_signature = '';
286  /**
287  * The color of the signature text
288  * @var Securimage_Color
289  */
290  public $signature_color = '#707070';
291  /**
292  * The path to the ttf font file to use for the signature text, defaults to $ttf_file (AHGBold.ttf)
293  * @var string
294  */
295  public $signature_font;
296 
297  /**
298  * Use an SQLite database to store data (for users that do not support cookies)
299  * @var bool
300  */
301  public $use_sqlite_db = false;
302 
303  /**
304  * The type of captcha to create, either alphanumeric, or a math problem<br />
305  * Securimage::SI_CAPTCHA_STRING or Securimage::SI_CAPTCHA_MATHEMATIC
306  * @var int
307  */
308  public $captcha_type = self::SI_CAPTCHA_STRING; // or self::SI_CAPTCHA_MATHEMATIC;
309 
310  /**
311  * The captcha namespace, use this if you have multiple forms on a single page, blank if you do not use multiple forms on one page
312  * @var string
313  * <code>
314  * <?php
315  * // in securimage_show.php (create one show script for each form)
316  * $img->namespace = 'contact_form';
317  *
318  * // in form validator
319  * $img->namespace = 'contact_form';
320  * if ($img->check($code) == true) {
321  * echo "Valid!";
322  * }
323  * </code>
324  */
325  public $namespace;
326 
327  /**
328  * The font file to use to draw the captcha code, leave blank for default font AHGBold.ttf
329  * @var string
330  */
331  public $ttf_file;
332  /**
333  * The path to the wordlist file to use, leave blank for default words/words.txt
334  * @var string
335  */
336  public $wordlist_file;
337  /**
338  * The directory to scan for background images, if set a random background will be chosen from this folder
339  * @var string
340  */
341  public $background_directory;
342  /**
343  * The path to the SQLite database file to use, if $use_sqlite_database = true, should be chmod 666
344  * @var string
345  */
346  public $sqlite_database;
347  /**
348  * The path to the securimage audio directory, can be set in securimage_play.php
349  * @var string
350  * <code>
351  * $img->audio_path = '/home/yoursite/public_html/securimage/audio/';
352  * </code>
353  */
354  public $audio_path;
355  /**
356  * The path to the directory containing audio files that will be selected
357  * randomly and mixed with the captcha audio.
358  *
359  * @var string
360  */
361  public $audio_noise_path;
362  /**
363  * Whether or not to mix background noise files into captcha audio (true = mix, false = no)
364  * Mixing random background audio with noise can help improve security of audio captcha.
365  * Default: securimage/audio/noise
366  *
367  * @since 3.0.3
368  * @see Securimage::$audio_noise_path
369  * @var bool
370  */
371  public $audio_use_noise;
372  /**
373  * The method and threshold (or gain factor) used to normalize the mixing with background noise.
374  * See http://www.voegler.eu/pub/audio/ for more information.
375  *
376  * Valid: <ul>
377  * <li> >= 1 - Normalize by multiplying by the threshold (boost - positive gain). <br />
378  * A value of 1 in effect means no normalization (and results in clipping). </li>
379  * <li> <= -1 - Normalize by dividing by the the absolute value of threshold (attenuate - negative gain). <br />
380  * A factor of 2 (-2) is about 6dB reduction in volume.</li>
381  * <li> [0, 1) - (open inverval - not including 1) - The threshold
382  * above which amplitudes are comressed logarithmically. <br />
383  * e.g. 0.6 to leave amplitudes up to 60% "as is" and compress above. </li>
384  * <li> (-1, 0) - (open inverval - not including -1 and 0) - The threshold
385  * above which amplitudes are comressed linearly. <br />
386  * e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above. </li></ul>
387  *
388  * Default: 0.6
389  *
390  * @since 3.0.4
391  * @var float
392  */
393  public $audio_mix_normalization = 0.6;
394  /**
395  * Whether or not to degrade audio by introducing random noise (improves security of audio captcha)
396  * Default: true
397  *
398  * @since 3.0.3
399  * @var bool
400  */
401  public $degrade_audio;
402  /**
403  * Minimum delay to insert between captcha audio letters in milliseconds
404  *
405  * @since 3.0.3
406  * @var float
407  */
408  public $audio_gap_min = 0;
409  /**
410  * Maximum delay to insert between captcha audio letters in milliseconds
411  *
412  * @since 3.0.3
413  * @var float
414  */
415  public $audio_gap_max = 600;
416 
417  /**
418  * Captcha ID if using static captcha
419  * @var string Unique captcha id
420  */
421  protected static $_captchaId = null;
422 
423  protected $im;
424  protected $tmpimg;
425  protected $bgimg;
426  protected $iscale = 5;
427 
428  protected $securimage_path = null;
429 
430  /**
431  * The captcha challenge value (either the case-sensitive/insensitive word captcha, or the solution to the math captcha)
432  *
433  * @var string Captcha challenge value
434  */
435  protected $code;
436 
437  /**
438  * The display value of the captcha to draw on the image (the word captcha, or the math equation to present to the user)
439  *
440  * @var string Captcha display value to draw on the image
441  */
442  protected $code_display;
443 
444  /**
445  * A value that can be passed to the constructor that can be used to generate a captcha image with a given value
446  * This value does not get stored in the session or database and is only used when calling Securimage::show().
447  * If a display_value was passed to the constructor and the captcha image is generated, the display_value will be used
448  * as the string to draw on the captcha image. Used only if captcha codes are generated and managed by a 3rd party app/library
449  *
450  * @var string Captcha code value to display on the image
451  */
452  protected $display_value;
453 
454  /**
455  * Captcha code supplied by user [set from Securimage::check()]
456  *
457  * @var string
458  */
459  protected $captcha_code;
460 
461  /**
462  * Flag that can be specified telling securimage not to call exit after generating a captcha image or audio file
463  *
464  * @var bool If true, script will not terminate; if false script will terminate (default)
465  */
466  protected $no_exit;
467 
468  /**
469  * Flag indicating whether or not a PHP session should be started and used
470  *
471  * @var bool If true, no session will be started; if false, session will be started and used to store data (default)
472  */
473  protected $no_session;
474 
475  /**
476  * Flag indicating whether or not HTTP headers will be sent when outputting captcha image/audio
477  *
478  * @var bool If true (default) headers will be sent, if false, no headers are sent
479  */
480  protected $send_headers;
481 
482  // sqlite database handle (if using sqlite)
483  protected $sqlite_handle;
484 
485  // gd color resources that are allocated for drawing the image
486  protected $gdbgcolor;
487  protected $gdtextcolor;
488  protected $gdlinecolor;
489  protected $gdsignaturecolor;
490 
491  /**
492  * Create a new securimage object, pass options to set in the constructor.<br />
493  * This can be used to display a captcha, play an audible captcha, or validate an entry
494  * @param array $options
495  * <code>
496  * $options = array(
497  * 'text_color' => new Securimage_Color('#013020'),
498  * 'code_length' => 5,
499  * 'num_lines' => 5,
500  * 'noise_level' => 3,
501  * 'font_file' => Securimage::getPath() . '/custom.ttf'
502  * );
503  *
504  * $img = new Securimage($options);
505  * </code>
506  */
507  public function __construct($options = array())
508  {
509  $this->securimage_path = dirname(__FILE__);
510 
511  if (is_array($options) && sizeof($options) > 0) {
512  foreach($options as $prop => $val) {
513  if ($prop == 'captchaId') {
514  Securimage::$_captchaId = $val;
515  $this->use_sqlite_db = true;
516  } else {
517  $this->$prop = $val;
518  }
519  }
520  }
521 
522  $this->image_bg_color = $this->initColor($this->image_bg_color, '#ffffff');
523  $this->text_color = $this->initColor($this->text_color, '#616161');
524  $this->line_color = $this->initColor($this->line_color, '#616161');
525  $this->noise_color = $this->initColor($this->noise_color, '#616161');
526  $this->signature_color = $this->initColor($this->signature_color, '#616161');
527 
528  if (is_null($this->ttf_file)) {
529  $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
530  }
531 
532  $this->signature_font = $this->ttf_file;
533 
534  if (is_null($this->wordlist_file)) {
535  $this->wordlist_file = $this->securimage_path . '/words/words.txt';
536  }
537 
538  if (is_null($this->sqlite_database)) {
539  $this->sqlite_database = $this->securimage_path . '/database/securimage.sqlite';
540  }
541 
542  if (is_null($this->audio_path)) {
543  $this->audio_path = $this->securimage_path . '/audio/';
544  }
545 
546  if (is_null($this->audio_noise_path)) {
547  $this->audio_noise_path = $this->audio_path . '/noise/';
548  }
549 
550  if (is_null($this->audio_use_noise)) {
551  $this->audio_use_noise = true;
552  }
553 
554  if (is_null($this->degrade_audio)) {
555  $this->degrade_audio = true;
556  }
557 
558  if (is_null($this->code_length) || (int)$this->code_length < 1) {
559  $this->code_length = 6;
560  }
561 
562  if (is_null($this->perturbation) || !is_numeric($this->perturbation)) {
563  $this->perturbation = 0.75;
564  }
565 
566  if (is_null($this->namespace) || !is_string($this->namespace)) {
567  $this->namespace = 'default';
568  }
569 
570  if (is_null($this->no_exit)) {
571  $this->no_exit = false;
572  }
573 
574  if (is_null($this->no_session)) {
575  $this->no_session = false;
576  }
577 
578  #$this->no_session = true;
579 
580  if (is_null($this->send_headers)) {
581  $this->send_headers = true;
582  }
583 
584  if ($this->no_session != true) {
585  // Initialize session or attach to existing
586  if ( session_id() == '' ) { // no session has been started yet, which is needed for validation
587  if (!is_null($this->session_name) && trim($this->session_name) != '') {
588  session_name(trim($this->session_name)); // set session name if provided
589  }
590  session_start();
591  }
592  }
593  }
594 
595  /**
596  * Return the absolute path to the Securimage directory
597  * @return string The path to the securimage base directory
598  */
599  public static function getPath()
600  {
601  return dirname(__FILE__);
602  }
603 
604  /**
605  * Generate a new captcha ID or retrieve the current ID
606  *
607  * @param $new bool If true, generates a new challenge and returns and ID
608  * @param $options array Additional options to be passed to Securimage
609  *
610  * @return null|string Returns null if no captcha id set and new was false, or string captcha ID
611  */
612  public static function getCaptchaId($new = true, array $options = array())
613  {
614  if ((bool)$new == true) {
615  $id = sha1(uniqid($_SERVER['REMOTE_ADDR'], true));
616  $opts = array('no_session' => true,
617  'use_sqlite_db' => true);
618  if (sizeof($options) > 0) $opts = array_merge($opts, $options);
619  $si = new self($opts);
620  Securimage::$_captchaId = $id;
621  $si->createCode();
622 
623  return $id;
624  } else {
625  return Securimage::$_captchaId;
626  }
627  }
628 
629  /**
630  * Validate a captcha code input against a captcha ID
631  * @param string $id The captcha ID to check
632  * @param string $value The captcha value supplied by the user
633  *
634  * @return bool true if the code was valid for the given captcha ID, false if not or if database failed to open
635  */
636  public static function checkByCaptchaId($id, $value)
637  {
638  $si = new self(array('captchaId' => $id,
639  'no_session' => true,
640  'use_sqlite_db' => true));
641 
642  if ($si->openDatabase()) {
643  $code = $si->getCodeFromDatabase();
644 
645  if (is_array($code)) {
646  $si->code = $code['code'];
647  $si->code_display = $code['code_disp'];
648  }
649 
650  if ($si->check($value)) {
651  return true;
652  } else {
653  return false;
654  }
655  } else {
656  return false;
657  }
658  }
659 
660 
661  /**
662  * Used to serve a captcha image to the browser
663  * @param string $background_image The path to the background image to use
664  * <code>
665  * $img = new Securimage();
666  * $img->code_length = 6;
667  * $img->num_lines = 5;
668  * $img->noise_level = 5;
669  *
670  * $img->show(); // sends the image to browser
671  * exit;
672  * </code>
673  */
674  public function show($background_image = '')
675  {
676  set_error_handler(array(&$this, 'errorHandler'));
677 
678  if($background_image != '' && is_readable($background_image)) {
679  $this->bgimg = $background_image;
680  }
681 
682  $this->doImage();
683  }
684 
685  /**
686  * Check a submitted code against the stored value
687  * @param string $code The captcha code to check
688  * <code>
689  * $code = $_POST['code'];
690  * $img = new Securimage();
691  * if ($img->check($code) == true) {
692  * $captcha_valid = true;
693  * } else {
694  * $captcha_valid = false;
695  * }
696  * </code>
697  */
698  public function check($code)
699  {
700  $this->code_entered = $code;
701  $this->validate();
702  return $this->correct_code;
703  }
704 
705  /**
706  * Output a wav file of the captcha code to the browser
707  *
708  * <code>
709  * $img = new Securimage();
710  * $img->outputAudioFile(); // outputs a wav file to the browser
711  * exit;
712  * </code>
713  */
714  public function outputAudioFile()
715  {
716  set_error_handler(array(&$this, 'errorHandler'));
717 
718  require_once dirname(__FILE__) . '/WavFile.php';
719 
720  try {
721  $audio = $this->getAudibleCode();
722  } catch (Exception $ex) {
723  if (($fp = @fopen(dirname(__FILE__) . '/si.error_log', 'a+')) !== false) {
724  fwrite($fp, date('Y-m-d H:i:s') . ': Securimage audio error "' . $ex->getMessage() . '"' . "\n");
725  fclose($fp);
726  }
727 
728  $audio = $this->audioError();
729  }
730 
731  if ($this->canSendHeaders() || $this->send_headers == false) {
732  if ($this->send_headers) {
733  $uniq = md5(uniqid(microtime()));
734  header("Content-Disposition: attachment; filename=\"securimage_audio-{$uniq}.wav\"");
735  header('Cache-Control: no-store, no-cache, must-revalidate');
736  header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
737  header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
738  header('Content-type: audio/x-wav');
739 
740  if (extension_loaded('zlib')) {
741  ini_set('zlib.output_compression', true); // compress output if supported by browser
742  } else {
743  header('Content-Length: ' . strlen($audio));
744  }
745  }
746 
747  echo $audio;
748  } else {
749  echo '<hr /><strong>'
750  .'Failed to generate audio file, content has already been '
751  .'output.<br />This is most likely due to misconfiguration or '
752  .'a PHP error was sent to the browser.</strong>';
753  }
754 
755  restore_error_handler();
756 
757  if (!$this->no_exit) exit;
758  }
759 
760  /**
761  * Return the code from the session or sqlite database if used. If none exists yet, an empty string is returned
762  *
763  * @param $array bool True to receive an array containing the code and properties
764  * @return array|string Array if $array = true, otherwise a string containing the code
765  */
766  public function getCode($array = false)
767  {
768  $code = '';
769  $time = 0;
770  $disp = 'error';
771 
772  if ($this->no_session != true) {
773  if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
774  trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
775  if ($this->isCodeExpired(
776  $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
777  $code = $_SESSION['securimage_code_value'][$this->namespace];
778  $time = $_SESSION['securimage_code_ctime'][$this->namespace];
779  $disp = $_SESSION['securimage_code_disp'] [$this->namespace];
780  }
781  }
782  }
783 
784  if ($this->use_sqlite_db == true && function_exists('sqlite_open')) {
785  // no code in session - may mean user has cookies turned off
786  $this->openDatabase();
787  $code = $this->getCodeFromDatabase();
788  } else { /* no code stored in session or sqlite database, validation will fail */ }
789 
790  if ($array == true) {
791  return array('code' => $code, 'ctime' => $time, 'display' => $disp);
792  } else {
793  return $code;
794  }
795  }
796 
797  /**
798  * The main image drawing routing, responsible for constructing the entire image and serving it
799  */
800  protected function doImage()
801  {
802  if( ($this->use_transparent_text == true || $this->bgimg != '') && function_exists('imagecreatetruecolor')) {
803  $imagecreate = 'imagecreatetruecolor';
804  } else {
805  $imagecreate = 'imagecreate';
806  }
807 
808  $this->im = $imagecreate($this->image_width, $this->image_height);
809  $this->tmpimg = $imagecreate($this->image_width * $this->iscale, $this->image_height * $this->iscale);
810 
811  $this->allocateColors();
812  imagepalettecopy($this->tmpimg, $this->im);
813 
814  $this->setBackground();
815 
816  $code = '';
817 
818  if ($this->getCaptchaId(false) !== null) {
819  // a captcha Id was supplied
820 
821  // check to see if a display_value for the captcha image was set
822  if (is_string($this->display_value) && strlen($this->display_value) > 0) {
823  $this->code_display = $this->display_value;
824  $this->code = ($this->case_sensitive) ?
825  $this->display_value :
826  strtolower($this->display_value);
827  $code = $this->code;
828  } else if ($this->openDatabase()) {
829  // no display_value, check the database for existing captchaId
830  $code = $this->getCodeFromDatabase();
831 
832  // got back a result from the database with a valid code for captchaId
833  if (is_array($code)) {
834  $this->code = $code['code'];
835  $this->code_display = $code['code_disp'];
836  $code = $code['code'];
837  }
838  }
839  }
840 
841  if ($code == '') {
842  // if the code was not set using display_value or was not found in
843  // the database, create a new code
844  $this->createCode();
845  }
846 
847  if ($this->noise_level > 0) {
848  $this->drawNoise();
849  }
850 
851  $this->drawWord();
852 
853  if ($this->perturbation > 0 && is_readable($this->ttf_file)) {
854  $this->distortedCopy();
855  }
856 
857  if ($this->num_lines > 0) {
858  $this->drawLines();
859  }
860 
861  if (trim($this->image_signature) != '') {
862  $this->addSignature();
863  }
864 
865  $this->output();
866  }
867 
868  /**
869  * Allocate the colors to be used for the image
870  */
871  protected function allocateColors()
872  {
873  // allocate bg color first for imagecreate
874  $this->gdbgcolor = imagecolorallocate($this->im,
875  $this->image_bg_color->r,
876  $this->image_bg_color->g,
877  $this->image_bg_color->b);
878 
879  $alpha = intval($this->text_transparency_percentage / 100 * 127);
880 
881  if ($this->use_transparent_text == true) {
882  $this->gdtextcolor = imagecolorallocatealpha($this->im,
883  $this->text_color->r,
884  $this->text_color->g,
885  $this->text_color->b,
886  $alpha);
887  $this->gdlinecolor = imagecolorallocatealpha($this->im,
888  $this->line_color->r,
889  $this->line_color->g,
890  $this->line_color->b,
891  $alpha);
892  $this->gdnoisecolor = imagecolorallocatealpha($this->im,
893  $this->noise_color->r,
894  $this->noise_color->g,
895  $this->noise_color->b,
896  $alpha);
897  } else {
898  $this->gdtextcolor = imagecolorallocate($this->im,
899  $this->text_color->r,
900  $this->text_color->g,
901  $this->text_color->b);
902  $this->gdlinecolor = imagecolorallocate($this->im,
903  $this->line_color->r,
904  $this->line_color->g,
905  $this->line_color->b);
906  $this->gdnoisecolor = imagecolorallocate($this->im,
907  $this->noise_color->r,
908  $this->noise_color->g,
909  $this->noise_color->b);
910  }
911 
912  $this->gdsignaturecolor = imagecolorallocate($this->im,
913  $this->signature_color->r,
914  $this->signature_color->g,
915  $this->signature_color->b);
916 
917  }
918 
919  /**
920  * The the background color, or background image to be used
921  */
922  protected function setBackground()
923  {
924  // set background color of image by drawing a rectangle since imagecreatetruecolor doesn't set a bg color
925  imagefilledrectangle($this->im, 0, 0,
926  $this->image_width, $this->image_height,
927  $this->gdbgcolor);
928  imagefilledrectangle($this->tmpimg, 0, 0,
929  $this->image_width * $this->iscale, $this->image_height * $this->iscale,
930  $this->gdbgcolor);
931 
932  if ($this->bgimg == '') {
933  if ($this->background_directory != null &&
934  is_dir($this->background_directory) &&
935  is_readable($this->background_directory))
936  {
937  $img = $this->getBackgroundFromDirectory();
938  if ($img != false) {
939  $this->bgimg = $img;
940  }
941  }
942  }
943 
944  if ($this->bgimg == '') {
945  return;
946  }
947 
948  $dat = @getimagesize($this->bgimg);
949  if($dat == false) {
950  return;
951  }
952 
953  switch($dat[2]) {
954  case 1: $newim = @imagecreatefromgif($this->bgimg); break;
955  case 2: $newim = @imagecreatefromjpeg($this->bgimg); break;
956  case 3: $newim = @imagecreatefrompng($this->bgimg); break;
957  default: return;
958  }
959 
960  if(!$newim) return;
961 
962  imagecopyresized($this->im, $newim, 0, 0, 0, 0,
963  $this->image_width, $this->image_height,
964  imagesx($newim), imagesy($newim));
965  }
966 
967  /**
968  * Scan the directory for a background image to use
969  */
970  protected function getBackgroundFromDirectory()
971  {
972  $images = array();
973 
974  if ( ($dh = opendir($this->background_directory)) !== false) {
975  while (($file = readdir($dh)) !== false) {
976  if (preg_match('/(jpg|gif|png)$/i', $file)) $images[] = $file;
977  }
978 
979  closedir($dh);
980 
981  if (sizeof($images) > 0) {
982  return rtrim($this->background_directory, '/') . '/' . $images[rand(0, sizeof($images)-1)];
983  }
984  }
985 
986  return false;
987  }
988 
989  /**
990  * Generates the code or math problem and saves the value to the session
991  */
992  protected function createCode()
993  {
994  $this->code = false;
995 
996  switch($this->captcha_type) {
997  case self::SI_CAPTCHA_MATHEMATIC:
998  {
999  do {
1000  $signs = array('+', '-', 'x');
1001  $left = rand(1, 10);
1002  $right = rand(1, 5);
1003  $sign = $signs[rand(0, 2)];
1004 
1005  switch($sign) {
1006  case 'x': $c = $left * $right; break;
1007  case '-': $c = $left - $right; break;
1008  default: $c = $left + $right; break;
1009  }
1010  } while ($c <= 0); // no negative #'s or 0
1011 
1012  $this->code = $c;
1013  $this->code_display = "$left $sign $right";
1014  break;
1015  }
1016 
1017  default:
1018  {
1019  if ($this->use_wordlist && is_readable($this->wordlist_file)) {
1020  $this->code = $this->readCodeFromFile();
1021  }
1022 
1023  if ($this->code == false) {
1024  $this->code = $this->generateCode($this->code_length);
1025  }
1026 
1027  $this->code_display = $this->code;
1028  $this->code = ($this->case_sensitive) ? $this->code : strtolower($this->code);
1029  } // default
1030  }
1031 
1032  $this->saveData();
1033  }
1034 
1035  /**
1036  * Draws the captcha code on the image
1037  */
1038  protected function drawWord()
1039  {
1040  $width2 = $this->image_width * $this->iscale;
1041  $height2 = $this->image_height * $this->iscale;
1042 
1043  if (!is_readable($this->ttf_file)) {
1044  imagestring($this->im, 4, 10, ($this->image_height / 2) - 5, 'Failed to load TTF font file!', $this->gdtextcolor);
1045  } else {
1046  if ($this->perturbation > 0) {
1047  $font_size = $height2 * .4;
1048  $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
1049  $tx = $bb[4] - $bb[0];
1050  $ty = $bb[5] - $bb[1];
1051  $x = floor($width2 / 2 - $tx / 2 - $bb[0]);
1052  $y = round($height2 / 2 - $ty / 2 - $bb[1]);
1053 
1054  imagettftext($this->tmpimg, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
1055  } else {
1056  $font_size = $this->image_height * .4;
1057  $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
1058  $tx = $bb[4] - $bb[0];
1059  $ty = $bb[5] - $bb[1];
1060  $x = floor($this->image_width / 2 - $tx / 2 - $bb[0]);
1061  $y = round($this->image_height / 2 - $ty / 2 - $bb[1]);
1062 
1063  imagettftext($this->im, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
1064  }
1065  }
1066 
1067  // DEBUG
1068  //$this->im = $this->tmpimg;
1069  //$this->output();
1070 
1071  }
1072 
1073  /**
1074  * Copies the captcha image to the final image with distortion applied
1075  */
1076  protected function distortedCopy()
1077  {
1078  $numpoles = 3; // distortion factor
1079  // make array of poles AKA attractor points
1080  for ($i = 0; $i < $numpoles; ++ $i) {
1081  $px[$i] = rand($this->image_width * 0.2, $this->image_width * 0.8);
1082  $py[$i] = rand($this->image_height * 0.2, $this->image_height * 0.8);
1083  $rad[$i] = rand($this->image_height * 0.2, $this->image_height * 0.8);
1084  $tmp = ((- $this->frand()) * 0.15) - .15;
1085  $amp[$i] = $this->perturbation * $tmp;
1086  }
1087 
1088  $bgCol = imagecolorat($this->tmpimg, 0, 0);
1089  $width2 = $this->iscale * $this->image_width;
1090  $height2 = $this->iscale * $this->image_height;
1091  imagepalettecopy($this->im, $this->tmpimg); // copy palette to final image so text colors come across
1092  // loop over $img pixels, take pixels from $tmpimg with distortion field
1093  for ($ix = 0; $ix < $this->image_width; ++ $ix) {
1094  for ($iy = 0; $iy < $this->image_height; ++ $iy) {
1095  $x = $ix;
1096  $y = $iy;
1097  for ($i = 0; $i < $numpoles; ++ $i) {
1098  $dx = $ix - $px[$i];
1099  $dy = $iy - $py[$i];
1100  if ($dx == 0 && $dy == 0) {
1101  continue;
1102  }
1103  $r = sqrt($dx * $dx + $dy * $dy);
1104  if ($r > $rad[$i]) {
1105  continue;
1106  }
1107  $rscale = $amp[$i] * sin(3.14 * $r / $rad[$i]);
1108  $x += $dx * $rscale;
1109  $y += $dy * $rscale;
1110  }
1111  $c = $bgCol;
1112  $x *= $this->iscale;
1113  $y *= $this->iscale;
1114  if ($x >= 0 && $x < $width2 && $y >= 0 && $y < $height2) {
1115  $c = imagecolorat($this->tmpimg, $x, $y);
1116  }
1117  if ($c != $bgCol) { // only copy pixels of letters to preserve any background image
1118  imagesetpixel($this->im, $ix, $iy, $c);
1119  }
1120  }
1121  }
1122  }
1123 
1124  /**
1125  * Draws distorted lines on the image
1126  */
1127  protected function drawLines()
1128  {
1129  for ($line = 0; $line < $this->num_lines; ++ $line) {
1130  $x = $this->image_width * (1 + $line) / ($this->num_lines + 1);
1131  $x += (0.5 - $this->frand()) * $this->image_width / $this->num_lines;
1132  $y = rand($this->image_height * 0.1, $this->image_height * 0.9);
1133 
1134  $theta = ($this->frand() - 0.5) * M_PI * 0.7;
1135  $w = $this->image_width;
1136  $len = rand($w * 0.4, $w * 0.7);
1137  $lwid = rand(0, 2);
1138 
1139  $k = $this->frand() * 0.6 + 0.2;
1140  $k = $k * $k * 0.5;
1141  $phi = $this->frand() * 6.28;
1142  $step = 0.5;
1143  $dx = $step * cos($theta);
1144  $dy = $step * sin($theta);
1145  $n = $len / $step;
1146  $amp = 1.5 * $this->frand() / ($k + 5.0 / $len);
1147  $x0 = $x - 0.5 * $len * cos($theta);
1148  $y0 = $y - 0.5 * $len * sin($theta);
1149 
1150  $ldx = round(- $dy * $lwid);
1151  $ldy = round($dx * $lwid);
1152 
1153  for ($i = 0; $i < $n; ++ $i) {
1154  $x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
1155  $y = $y0 + $i * $dy - $amp * $dx * sin($k * $i * $step + $phi);
1156  imagefilledrectangle($this->im, $x, $y, $x + $lwid, $y + $lwid, $this->gdlinecolor);
1157  }
1158  }
1159  }
1160 
1161  /**
1162  * Draws random noise on the image
1163  */
1164  protected function drawNoise()
1165  {
1166  if ($this->noise_level > 10) {
1167  $noise_level = 10;
1168  } else {
1169  $noise_level = $this->noise_level;
1170  }
1171 
1172  $t0 = microtime(true);
1173 
1174  $noise_level *= 125; // an arbitrary number that works well on a 1-10 scale
1175 
1176  $points = $this->image_width * $this->image_height * $this->iscale;
1177  $height = $this->image_height * $this->iscale;
1178  $width = $this->image_width * $this->iscale;
1179  for ($i = 0; $i < $noise_level; ++$i) {
1180  $x = rand(10, $width);
1181  $y = rand(10, $height);
1182  $size = rand(7, 10);
1183  if ($x - $size <= 0 && $y - $size <= 0) continue; // dont cover 0,0 since it is used by imagedistortedcopy
1184  imagefilledarc($this->tmpimg, $x, $y, $size, $size, 0, 360, $this->gdnoisecolor, IMG_ARC_PIE);
1185  }
1186 
1187  $t1 = microtime(true);
1188 
1189  $t = $t1 - $t0;
1190 
1191  /*
1192  // DEBUG
1193  imagestring($this->tmpimg, 5, 25, 30, "$t", $this->gdnoisecolor);
1194  header('content-type: image/png');
1195  imagepng($this->tmpimg);
1196  exit;
1197  */
1198  }
1199 
1200  /**
1201  * Print signature text on image
1202  */
1203  protected function addSignature()
1204  {
1205  $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
1206  $textlen = $bbox[2] - $bbox[0];
1207  $x = $this->image_width - $textlen - 5;
1208  $y = $this->image_height - 3;
1209 
1210  imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
1211  }
1212 
1213  /**
1214  * Sends the appropriate image and cache headers and outputs image to the browser
1215  */
1216  protected function output()
1217  {
1218  if ($this->canSendHeaders() || $this->send_headers == false) {
1219  if ($this->send_headers) {
1220  // only send the content-type headers if no headers have been output
1221  // this will ease debugging on misconfigured servers where warnings
1222  // may have been output which break the image and prevent easily viewing
1223  // source to see the error.
1224  header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
1225  header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
1226  header("Cache-Control: no-store, no-cache, must-revalidate");
1227  header("Cache-Control: post-check=0, pre-check=0", false);
1228  header("Pragma: no-cache");
1229  }
1230 
1231  switch ($this->image_type) {
1232  case self::SI_IMAGE_JPEG:
1233  if ($this->send_headers) header("Content-Type: image/jpeg");
1234  imagejpeg($this->im, null, 90);
1235  break;
1236  case self::SI_IMAGE_GIF:
1237  if ($this->send_headers) header("Content-Type: image/gif");
1238  imagegif($this->im);
1239  break;
1240  default:
1241  if ($this->send_headers) header("Content-Type: image/png");
1242  imagepng($this->im);
1243  break;
1244  }
1245  } else {
1246  echo '<hr /><strong>'
1247  .'Failed to generate captcha image, content has already been '
1248  .'output.<br />This is most likely due to misconfiguration or '
1249  .'a PHP error was sent to the browser.</strong>';
1250  }
1251 
1252  imagedestroy($this->im);
1253  restore_error_handler();
1254 
1255  if (!$this->no_exit) exit;
1256  }
1257 
1258  /**
1259  * Gets the code and returns the binary audio file for the stored captcha code
1260  *
1261  * @return The audio representation of the captcha in Wav format
1262  */
1263  protected function getAudibleCode()
1264  {
1265  $letters = array();
1266  $code = $this->getCode(true);
1267 
1268  if ($code['code'] == '') {
1269  $this->createCode();
1270  $code = $this->getCode(true);
1271  }
1272 
1273  if (preg_match('/(\d+) (\+|-|x) (\d+)/i', $code['display'], $eq)) {
1274  $math = true;
1275 
1276  $left = $eq[1];
1277  $sign = str_replace(array('+', '-', 'x'), array('plus', 'minus', 'times'), $eq[2]);
1278  $right = $eq[3];
1279 
1280  $letters = array($left, $sign, $right);
1281  } else {
1282  $math = false;
1283 
1284  $length = strlen($code['display']);
1285 
1286  for($i = 0; $i < $length; ++$i) {
1287  $letter = $code['display']{$i};
1288  $letters[] = $letter;
1289  }
1290  }
1291 
1292  try {
1293  return $this->generateWAV($letters);
1294  } catch(Exception $ex) {
1295  throw $ex;
1296  }
1297  }
1298 
1299  /**
1300  * Gets a captcha code from a wordlist
1301  */
1302  protected function readCodeFromFile()
1303  {
1304  $fp = @fopen($this->wordlist_file, 'rb');
1305  if (!$fp) return false;
1306 
1307  $fsize = filesize($this->wordlist_file);
1308  if ($fsize < 128) return false; // too small of a list to be effective
1309 
1310  fseek($fp, rand(0, $fsize - 64), SEEK_SET); // seek to a random position of file from 0 to filesize-64
1311  $data = fread($fp, 64); // read a chunk from our random position
1312  fclose($fp);
1313  $data = preg_replace("/\r?\n/", "\n", $data);
1314 
1315  $start = @strpos($data, "\n", rand(0, 56)) + 1; // random start position
1316  $end = @strpos($data, "\n", $start); // find end of word
1317 
1318  if ($start === false) {
1319  return false;
1320  } else if ($end === false) {
1321  $end = strlen($data);
1322  }
1323 
1324  return strtolower(substr($data, $start, $end - $start)); // return a line of the file
1325  }
1326 
1327  /**
1328  * Generates a random captcha code from the set character set
1329  */
1330  protected function generateCode()
1331  {
1332  $code = '';
1333 
1334  for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
1335  $code .= $this->charset{rand(0, $cslen - 1)};
1336  }
1337 
1338  //return 'testing'; // debug, set the code to given string
1339 
1340  return $code;
1341  }
1342 
1343  /**
1344  * Checks the entered code against the value stored in the session or sqlite database, handles case sensitivity
1345  * Also clears the stored codes if the code was entered correctly to prevent re-use
1346  */
1347  protected function validate()
1348  {
1349  if (!is_string($this->code) || strlen($this->code) == 0) {
1350  $code = $this->getCode();
1351  // returns stored code, or an empty string if no stored code was found
1352  // checks the session and sqlite database if enabled
1353  } else {
1354  $code = $this->code;
1355  }
1356 
1357  if ($this->case_sensitive == false && preg_match('/[A-Z]/', $code)) {
1358  // case sensitive was set from securimage_show.php but not in class
1359  // the code saved in the session has capitals so set case sensitive to true
1360  $this->case_sensitive = true;
1361  }
1362 
1363  $code_entered = trim( (($this->case_sensitive) ? $this->code_entered
1364  : strtolower($this->code_entered))
1365  );
1366  $this->correct_code = false;
1367 
1368  if ($code != '') {
1369  if ($code == $code_entered) {
1370  $this->correct_code = true;
1371  if ($this->no_session != true) {
1372  $_SESSION['securimage_code_value'][$this->namespace] = '';
1373  $_SESSION['securimage_code_ctime'][$this->namespace] = '';
1374  }
1375  $this->clearCodeFromDatabase();
1376  }
1377  }
1378  }
1379 
1380  /**
1381  * Save data to session namespace and database if used
1382  */
1383  protected function saveData()
1384  {
1385  if ($this->no_session != true) {
1386  if (isset($_SESSION['securimage_code_value']) && is_scalar($_SESSION['securimage_code_value'])) {
1387  // fix for migration from v2 - v3
1388  unset($_SESSION['securimage_code_value']);
1389  unset($_SESSION['securimage_code_ctime']);
1390  }
1391 
1392  $_SESSION['securimage_code_disp'] [$this->namespace] = $this->code_display;
1393  $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
1394  $_SESSION['securimage_code_ctime'][$this->namespace] = time();
1395  }
1396 
1397  $this->saveCodeToDatabase();
1398  }
1399 
1400  /**
1401  * Saves the code to the sqlite database
1402  */
1403  protected function saveCodeToDatabase()
1404  {
1405  $success = false;
1406 
1407  $this->openDatabase();
1408 
1409  if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
1410  $id = $this->getCaptchaId(false);
1411  $ip = $_SERVER['REMOTE_ADDR'];
1412 
1413  if (empty($id)) {
1414  $id = $ip;
1415  }
1416 
1417  $time = time();
1418  $code = $this->code;
1419  $code_disp = $this->code_display;
1420 
1421  $query = "INSERT OR REPLACE INTO codes(id, ip, code, code_display,"
1422  ."namespace, created) VALUES('$id', '$ip', '$code', "
1423  ."'$code_disp', '{$this->namespace}', $time)";
1424 
1425  $success = sqlite_query($this->sqlite_handle, $query);
1426  }
1427 
1428  return $success !== false;
1429  }
1430 
1431  /**
1432  * Open sqlite database
1433  */
1434  protected function openDatabase()
1435  {
1436  $this->sqlite_handle = false;
1437 
1438  if ($this->use_sqlite_db == true && !function_exists('sqlite_open')) {
1439  trigger_error('Securimage use_sqlite_db option is enable, but SQLIte is not supported by this PHP installation', E_USER_WARNING);
1440  }
1441 
1442  if ($this->use_sqlite_db && function_exists('sqlite_open')) {
1443  if (!file_exists($this->sqlite_database)) {
1444  $fp = fopen($this->sqlite_database, 'w+');
1445  if (!$fp) {
1446  trigger_error('Securimage failed to open sqlite database "' . $this->sqlite_database, E_USER_WARNING);
1447  return false;
1448  }
1449  fclose($fp);
1450  chmod($this->sqlite_database, 0666);
1451  }
1452 
1453  $this->sqlite_handle = sqlite_open($this->sqlite_database, 0666, $error);
1454 
1455  if ($this->sqlite_handle !== false) {
1456  $res = sqlite_query($this->sqlite_handle, "PRAGMA table_info(codes)");
1457 
1458  if (sqlite_num_rows($res) == 0) {
1459  $res = sqlite_query(
1460  $this->sqlite_handle,
1461  "CREATE TABLE codes (id VARCHAR(40) PRIMARY KEY, ip VARCHAR(32),
1462  code VARCHAR(32) NOT NULL, code_display VARCHAR(32) NOT NULL,
1463  namespace VARCHAR(32) NOT NULL, created INTEGER)"
1464  );
1465  }
1466 
1467  if (mt_rand(0, 100) / 100.0 == 1.0) {
1468  // randomly purge old codes
1469  $this->purgeOldCodesFromDatabase();
1470  }
1471  }
1472 
1473  return $this->sqlite_handle != false;
1474  }
1475 
1476  return $this->sqlite_handle;
1477  }
1478 
1479  /**
1480  * Get a code from the sqlite database for ip address/captchaId.
1481  *
1482  * @return string|array Empty string if no code was found or has expired,
1483  * otherwise returns the stored captcha code. If a captchaId is set, this
1484  * returns an array with indices "code" and "code_disp"
1485  */
1486  protected function getCodeFromDatabase()
1487  {
1488  $code = '';
1489 
1490  if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
1491  if (Securimage::$_captchaId !== null) {
1492  $query = "SELECT * FROM codes WHERE id = '" . sqlite_escape_string(Securimage::$_captchaId) . "'";
1493  } else {
1494  $ip = $_SERVER['REMOTE_ADDR'];
1495  $ns = sqlite_escape_string($this->namespace);
1496  $query = "SELECT * FROM codes WHERE ip = '$ip' AND namespace = '$ns'";
1497  }
1498 
1499  $res = sqlite_query($this->sqlite_handle, $query);
1500  if ($res && sqlite_num_rows($res) > 0) {
1501  $res = sqlite_fetch_array($res);
1502 
1503  if ($this->isCodeExpired($res['created']) == false) {
1504  if (Securimage::$_captchaId !== null) {
1505  // return an array when using captchaId
1506  $code = array('code' => $res['code'],
1507  'code_disp' => $res['code_display']);
1508  } else {
1509  // return only the code if no captchaId specified
1510  $code = $res['code'];
1511  }
1512  }
1513  }
1514  }
1515  return $code;
1516  }
1517 
1518  /**
1519  * Remove an entered code from the database
1520  */
1521  protected function clearCodeFromDatabase()
1522  {
1523  if (is_resource($this->sqlite_handle)) {
1524  $ip = $_SERVER['REMOTE_ADDR'];
1525  $ns = sqlite_escape_string($this->namespace);
1526 
1527  sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE ip = '$ip' AND namespace = '$ns'");
1528  }
1529  }
1530 
1531  /**
1532  * Deletes old codes from sqlite database
1533  */
1534  protected function purgeOldCodesFromDatabase()
1535  {
1536  if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
1537  $now = time();
1538  $limit = (!is_numeric($this->expiry_time) || $this->expiry_time < 1) ? 86400 : $this->expiry_time;
1539 
1540  sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE $now - created > $limit");
1541  }
1542  }
1543 
1544  /**
1545  * Checks to see if the captcha code has expired and cannot be used
1546  * @param unknown_type $creation_time
1547  */
1548  protected function isCodeExpired($creation_time)
1549  {
1550  $expired = true;
1551 
1552  if (!is_numeric($this->expiry_time) || $this->expiry_time < 1) {
1553  $expired = false;
1554  } else if (time() - $creation_time < $this->expiry_time) {
1555  $expired = false;
1556  }
1557 
1558  return $expired;
1559  }
1560 
1561  /**
1562  * Generate a wav file given the $letters in the code
1563  * @todo Add ability to merge 2 sound files together to have random background sounds
1564  * @param array $letters
1565  * @return string The binary contents of the wav file
1566  */
1567  protected function generateWAV($letters)
1568  {
1569  $wavCaptcha = new WavFile();
1570  $first = true; // reading first wav file
1571 
1572  foreach ($letters as $letter) {
1573  $letter = strtoupper($letter);
1574 
1575  try {
1576  $l = new WavFile($this->audio_path . '/' . $letter . '.wav');
1577 
1578  if ($first) {
1579  // set sample rate, bits/sample, and # of channels for file based on first letter
1580  $wavCaptcha->setSampleRate($l->getSampleRate())
1581  ->setBitsPerSample($l->getBitsPerSample())
1582  ->setNumChannels($l->getNumChannels());
1583  $first = false;
1584  }
1585 
1586  // append letter to the captcha audio
1587  $wavCaptcha->appendWav($l);
1588 
1589  // random length of silence between $audio_gap_min and $audio_gap_max
1590  if ($this->audio_gap_max > 0 && $this->audio_gap_max > $this->audio_gap_min) {
1591  $wavCaptcha->insertSilence( mt_rand($this->audio_gap_min, $this->audio_gap_max) / 1000.0 );
1592  }
1593  } catch (Exception $ex) {
1594  // failed to open file, or the wav file is broken or not supported
1595  // 2 wav files were not compatible, different # channels, bits/sample, or sample rate
1596  throw $ex;
1597  }
1598  }
1599 
1600  /********* Set up audio filters *****************************/
1601  $filters = array();
1602 
1603  if ($this->audio_use_noise == true) {
1604  // use background audio - find random file
1605  $noiseFile = $this->getRandomNoiseFile();
1606 
1607  if ($noiseFile !== false && is_readable($noiseFile)) {
1608  try {
1609  $wavNoise = new WavFile($noiseFile, false);
1610  } catch(Exception $ex) {
1611  throw $ex;
1612  }
1613 
1614  // start at a random offset from the beginning of the wavfile
1615  // in order to add more randomness
1616  $randOffset = 0;
1617  if ($wavNoise->getNumBlocks() > 2 * $wavCaptcha->getNumBlocks()) {
1618  $randBlock = rand(0, $wavNoise->getNumBlocks() - $wavCaptcha->getNumBlocks());
1619  $wavNoise->readWavData($randBlock * $wavNoise->getBlockAlign(), $wavCaptcha->getNumBlocks() * $wavNoise->getBlockAlign());
1620  } else {
1621  $wavNoise->readWavData();
1622  $randOffset = rand(0, $wavNoise->getNumBlocks() - 1);
1623  }
1624 
1625 
1626  $mixOpts = array('wav' => $wavNoise,
1627  'loop' => true,
1628  'blockOffset' => $randOffset);
1629 
1630  $filters[WavFile::FILTER_MIX] = $mixOpts;
1631  $filters[WavFile::FILTER_NORMALIZE] = $this->audio_mix_normalization;
1632  }
1633  }
1634 
1635  if ($this->degrade_audio == true) {
1636  // add random noise.
1637  // any noise level below 95% is intensely distorted and not pleasant to the ear
1638  $filters[WavFile::FILTER_DEGRADE] = rand(95, 98) / 100.0;
1639  }
1640 
1641  if (!empty($filters)) {
1642  $wavCaptcha->filter($filters); // apply filters to captcha audio
1643  }
1644 
1645  return $wavCaptcha->__toString();
1646  }
1647 
1648  public function getRandomNoiseFile()
1649  {
1650  $return = false;
1651 
1652  if ( ($dh = opendir($this->audio_noise_path)) !== false ) {
1653  $list = array();
1654 
1655  while ( ($file = readdir($dh)) !== false ) {
1656  if ($file == '.' || $file == '..') continue;
1657  if (strtolower(substr($file, -4)) != '.wav') continue;
1658 
1659  $list[] = $file;
1660  }
1661 
1662  closedir($dh);
1663 
1664  if (sizeof($list) > 0) {
1665  $file = $list[array_rand($list, 1)];
1666  $return = $this->audio_noise_path . DIRECTORY_SEPARATOR . $file;
1667  }
1668  }
1669 
1670  return $return;
1671  }
1672 
1673  /**
1674  * Return a wav file saying there was an error generating file
1675  *
1676  * @return string The binary audio contents
1677  */
1678  protected function audioError()
1679  {
1680  return @file_get_contents(dirname(__FILE__) . '/audio/error.wav');
1681  }
1682 
1683  /**
1684  * Checks to see if headers can be sent and if any error has been output to the browser
1685  *
1686  * @return bool true if headers haven't been sent and no output/errors will break audio/images, false if unsafe
1687  */
1688  protected function canSendHeaders()
1689  {
1690  if (headers_sent()) {
1691  // output has been flushed and headers have already been sent
1692  return false;
1693  } else if (strlen((string)ob_get_contents()) > 0) {
1694  // headers haven't been sent, but there is data in the buffer that will break image and audio data
1695  return false;
1696  }
1697 
1698  return true;
1699  }
1700 
1701  /**
1702  * Return a random float between 0 and 0.9999
1703  *
1704  * @return float Random float between 0 and 0.9999
1705  */
1706  function frand()
1707  {
1708  return 0.0001 * rand(0,9999);
1709  }
1710 
1711  /**
1712  * Convert an html color code to a Securimage_Color
1713  * @param string $color
1714  * @param Securimage_Color $default The defalt color to use if $color is invalid
1715  */
1716  protected function initColor($color, $default)
1717  {
1718  if ($color == null) {
1719  return new Securimage_Color($default);
1720  } else if (is_string($color)) {
1721  try {
1722  return new Securimage_Color($color);
1723  } catch(Exception $e) {
1724  return new Securimage_Color($default);
1725  }
1726  } else if (is_array($color) && sizeof($color) == 3) {
1727  return new Securimage_Color($color[0], $color[1], $color[2]);
1728  } else {
1729  return new Securimage_Color($default);
1730  }
1731  }
1732 
1733  /**
1734  * Error handler used when outputting captcha image or audio.
1735  * This error handler helps determine if any errors raised would
1736  * prevent captcha image or audio from displaying. If they have
1737  * no effect on the output buffer or headers, true is returned so
1738  * the script can continue processing.
1739  * See https://github.com/dapphp/securimage/issues/15
1740  *
1741  * @param int $errno
1742  * @param string $errstr
1743  * @param string $errfile
1744  * @param int $errline
1745  * @param array $errcontext
1746  * @return boolean true if handled, false if PHP should handle
1747  */
1748  public function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array())
1749  {
1750  // get the current error reporting level
1751  $level = error_reporting();
1752 
1753  // if error was supressed or $errno not set in current error level
1754  if ($level == 0 || ($level & $errno) == 0) {
1755  return true;
1756  }
1757 
1758  return false;
1759  }
1760 }
1761 
1762 
1763 /**
1764  * Color object for Securimage CAPTCHA
1765  *
1766  * @version 3.0
1767  * @since 2.0
1768  * @package Securimage
1769  * @subpackage classes
1770  *
1771  */
1773 {
1774  public $r;
1775  public $g;
1776  public $b;
1777 
1778  /**
1779  * Create a new Securimage_Color object.<br />
1780  * Constructor expects 1 or 3 arguments.<br />
1781  * When passing a single argument, specify the color using HTML hex format,<br />
1782  * when passing 3 arguments, specify each RGB component (from 0-255) individually.<br />
1783  * $color = new Securimage_Color('#0080FF') or <br />
1784  * $color = new Securimage_Color(0, 128, 255)
1785  *
1786  * @param string $color
1787  * @throws Exception
1788  */
1789  public function __construct($color = '#ffffff')
1790  {
1791  $args = func_get_args();
1792 
1793  if (sizeof($args) == 0) {
1794  $this->r = 255;
1795  $this->g = 255;
1796  $this->b = 255;
1797  } else if (sizeof($args) == 1) {
1798  // set based on html code
1799  if (substr($color, 0, 1) == '#') {
1800  $color = substr($color, 1);
1801  }
1802 
1803  if (strlen($color) != 3 && strlen($color) != 6) {
1804  throw new InvalidArgumentException(
1805  'Invalid HTML color code passed to Securimage_Color'
1806  );
1807  }
1808 
1809  $this->constructHTML($color);
1810  } else if (sizeof($args) == 3) {
1811  $this->constructRGB($args[0], $args[1], $args[2]);
1812  } else {
1813  throw new InvalidArgumentException(
1814  'Securimage_Color constructor expects 0, 1 or 3 arguments; ' . sizeof($args) . ' given'
1815  );
1816  }
1817  }
1818 
1819  /**
1820  * Construct from an rgb triplet
1821  * @param int $red The red component, 0-255
1822  * @param int $green The green component, 0-255
1823  * @param int $blue The blue component, 0-255
1824  */
1825  protected function constructRGB($red, $green, $blue)
1826  {
1827  if ($red < 0) $red = 0;
1828  if ($red > 255) $red = 255;
1829  if ($green < 0) $green = 0;
1830  if ($green > 255) $green = 255;
1831  if ($blue < 0) $blue = 0;
1832  if ($blue > 255) $blue = 255;
1833 
1834  $this->r = $red;
1835  $this->g = $green;
1836  $this->b = $blue;
1837  }
1838 
1839  /**
1840  * Construct from an html hex color code
1841  * @param string $color
1842  */
1843  protected function constructHTML($color)
1844  {
1845  if (strlen($color) == 3) {
1846  $red = str_repeat(substr($color, 0, 1), 2);
1847  $green = str_repeat(substr($color, 1, 1), 2);
1848  $blue = str_repeat(substr($color, 2, 1), 2);
1849  } else {
1850  $red = substr($color, 0, 2);
1851  $green = substr($color, 2, 2);
1852  $blue = substr($color, 4, 2);
1853  }
1854 
1855  $this->r = hexdec($red);
1856  $this->g = hexdec($green);
1857  $this->b = hexdec($blue);
1858  }
1859 }