PageRenderTime 68ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/securimage.php

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