Advertisement
Guest User

Untitled

a guest
Mar 17th, 2012
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 44.52 KB | None | 0 0
  1. class Securimage
  2. {
  3.     // All of the public variables below are securimage options
  4.     // They can be passed as an array to the Securimage constructor, set below,
  5.     // or set from securimage_show.php and securimage_play.php
  6.    
  7.     /**
  8.      * Renders captcha as a JPEG image
  9.      * @var int
  10.      */
  11.     const SI_IMAGE_JPEG = 1;
  12.     /**
  13.      * Renders captcha as a PNG image (default)
  14.      * @var int
  15.      */
  16.     const SI_IMAGE_PNG  = 2;
  17.     /**
  18.      * Renders captcha as a GIF image
  19.      * @var int
  20.      */
  21.     const SI_IMAGE_GIF  = 3;
  22.    
  23.     /**
  24.      * Create a normal alphanumeric captcha
  25.      * @var int
  26.      */
  27.     const SI_CAPTCHA_STRING     = 0;
  28.     /**
  29.      * Create a captcha consisting of a simple math problem
  30.      * @var int
  31.      */
  32.     const SI_CAPTCHA_MATHEMATIC = 1;
  33.    
  34.     /**
  35.      * The width of the captcha image
  36.      * @var int
  37.      */
  38.     public $image_width = 215;
  39.     /**
  40.      * The height of the captcha image
  41.      * @var int
  42.      */
  43.     public $image_height = 80;
  44.     /**
  45.      * The type of the image, default = png
  46.      * @var int
  47.      */
  48.     public $image_type   = self::SI_IMAGE_PNG;
  49.  
  50.     /**
  51.      * The background color of the captcha
  52.      * @var Securimage_Color
  53.      */
  54.     public $image_bg_color = '#ffffff';
  55.     /**
  56.      * The color of the captcha text
  57.      * @var Securimage_Color
  58.      */
  59.     public $text_color     = '#707070';
  60.     /**
  61.      * The color of the lines over the captcha
  62.      * @var Securimage_Color
  63.      */
  64.     public $line_color     = '#707070';
  65.     /**
  66.      * The color of the noise that is drawn
  67.      * @var Securimage_Color
  68.      */
  69.     public $noise_color    = '#707070';
  70.    
  71.     /**
  72.      * How transparent to make the text 0 = completely opaque, 100 = invisible
  73.      * @var int
  74.      */
  75.     public $text_transparency_percentage = 50;
  76.     /**
  77.      * Whether or not to draw the text transparently, true = use transparency, false = no transparency
  78.      * @var bool
  79.      */
  80.     public $use_transparent_text         = false;
  81.    
  82.     /**
  83.      * The length of the captcha code
  84.      * @var int
  85.      */
  86.     public $code_length    = 6;
  87.     /**
  88.      * Whether the captcha should be case sensitive (not recommended, use only for maximum protection)
  89.      * @var bool
  90.      */
  91.     public $case_sensitive = false;
  92.     /**
  93.      * The character set to use for generating the captcha code
  94.      * @var string
  95.      */
  96.     public $charset        = 'ABCDEFGHKLMNPRSTUVWYZabcdefghklmnprstuvwyz23456789';
  97.     /**
  98.      * How long in seconds a captcha remains valid, after this time it will not be accepted
  99.      * @var unknown_type
  100.      */
  101.     public $expiry_time    = 900;
  102.    
  103.     /**
  104.      * The session name securimage should use, only set this if your application uses a custom session name
  105.      * It is recommended to set this value below so it is used by all securimage scripts
  106.      * @var string
  107.      */
  108.     public $session_name   = null;
  109.    
  110.     /**
  111.      * true to use the wordlist file, false to generate random captcha codes
  112.      * @var bool
  113.      */
  114.     public $use_wordlist   = false;
  115.  
  116.     /**
  117.      * The level of distortion, 0.75 = normal, 1.0 = very high distortion
  118.      * @var double
  119.      */
  120.     public $perturbation = 0.75;
  121.     /**
  122.      * How many lines to draw over the captcha code to increase security
  123.      * @var int
  124.      */
  125.     public $num_lines    = 8;
  126.     /**
  127.      * The level of noise (random dots) to place on the image, 0-10
  128.      * @var int
  129.      */
  130.     public $noise_level  = 0;
  131.    
  132.     /**
  133.      * The signature text to draw on the bottom corner of the image
  134.      * @var string
  135.      */
  136.     public $image_signature = '';
  137.     /**
  138.      * The color of the signature text
  139.      * @var Securimage_Color
  140.      */
  141.     public $signature_color = '#707070';
  142.     /**
  143.      * The path to the ttf font file to use for the signature text, defaults to $ttf_file (AHGBold.ttf)
  144.      * @var string
  145.      */
  146.     public $signature_font;
  147.    
  148.     /**
  149.      * Use an SQLite database to store data (for users that do not support cookies)
  150.      * @var bool
  151.      */
  152.     public $use_sqlite_db = false;
  153.    
  154.     /**
  155.      * The type of captcha to create, either alphanumeric, or a math problem<br />
  156.      * Securimage::SI_CAPTCHA_STRING or Securimage::SI_CAPTCHA_MATHEMATIC
  157.      * @var int
  158.      */
  159.     public $captcha_type  = self::SI_CAPTCHA_STRING;
  160.    
  161.     /**
  162.      * The captcha namespace, use this if you have multiple forms on a single page, blank if you do not use multiple forms on one page
  163.      * @var string
  164.      * <code>
  165.      * <?php
  166.      * // in securimage_show.php (create one show script for each form)
  167.      * $img->namespace = 'contact_form';
  168.      *
  169.      * // in form validator
  170.      * $img->namespace = 'contact_form';
  171.      * if ($img->check($code) == true) {
  172.      *     echo "Valid!";
  173.      *  }
  174.      * </code>
  175.      */
  176.     public $namespace;
  177.    
  178.     /**
  179.      * The font file to use to draw the captcha code, leave blank for default font AHGBold.ttf
  180.      * @var string
  181.      */
  182.     public $ttf_file;
  183.     /**
  184.      * The path to the wordlist file to use, leave blank for default words/words.txt
  185.      * @var string
  186.      */
  187.     public $wordlist_file;
  188.     /**
  189.      * The directory to scan for background images, if set a random background will be chosen from this folder
  190.      * @var string
  191.      */
  192.     public $background_directory;
  193.     /**
  194.      * The path to the SQLite database file to use, if $use_sqlite_database = true, should be chmod 666
  195.      * @var string
  196.      */
  197.     public $sqlite_database;
  198.     /**
  199.      * The path to the securimage audio directory, can be set in securimage_play.php
  200.      * @var string
  201.      * <code>
  202.      * $img->audio_path = '/home/yoursite/public_html/securimage/audio/';
  203.      * </code>
  204.      */
  205.     public $audio_path;
  206.  
  207.    
  208.    
  209.     protected $im;
  210.     protected $tmpimg;
  211.     protected $bgimg;
  212.     protected $iscale = 5;
  213.    
  214.     protected $securimage_path = null;
  215.    
  216.     protected $code;
  217.     protected $code_display;
  218.    
  219.     protected $captcha_code;
  220.     protected $sqlite_handle;
  221.    
  222.     protected $gdbgcolor;
  223.     protected $gdtextcolor;
  224.     protected $gdlinecolor;
  225.     protected $gdsignaturecolor;
  226.    
  227.     /**
  228.      * Create a new securimage object, pass options to set in the constructor.<br />
  229.      * This can be used to display a captcha, play an audible captcha, or validate an entry
  230.      * @param array $options
  231.      * <code>
  232.      * $options = array(
  233.      *     'text_color' => new Securimage_Color('#013020'),
  234.      *     'code_length' => 5,
  235.      *     'num_lines' => 5,
  236.      *     'noise_level' => 3,
  237.      *     'font_file' => Securimage::getPath() . '/custom.ttf'
  238.      * );
  239.      *
  240.      * $img = new Securimage($options);
  241.      * </code>
  242.      */
  243.     public function __construct($options = array())
  244.     {
  245.         $this->securimage_path = dirname(__FILE__);
  246.        
  247.         if (is_array($options) && sizeof($options) > 0) {
  248.             foreach($options as $prop => $val) {
  249.                 $this->$prop = $val;
  250.             }
  251.         }
  252.  
  253.         $this->image_bg_color  = $this->initColor($this->image_bg_color,  '#ffffff');
  254.         $this->text_color      = $this->initColor($this->text_color,      '#616161');
  255.         $this->line_color      = $this->initColor($this->line_color,      '#616161');
  256.         $this->noise_color     = $this->initColor($this->noise_color,     '#616161');
  257.         $this->signature_color = $this->initColor($this->signature_color, '#616161');
  258.  
  259.         if ($this->ttf_file == null) {
  260.             $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
  261.         }
  262.        
  263.         $this->signature_font = $this->ttf_file;
  264.        
  265.         if ($this->wordlist_file == null) {
  266.             $this->wordlist_file = $this->securimage_path . '/words/words.txt';
  267.         }
  268.        
  269.         if ($this->sqlite_database == null) {
  270.             $this->sqlite_database = $this->securimage_path . '/database/securimage.sqlite';
  271.         }
  272.        
  273.         if ($this->audio_path == null) {
  274.             $this->audio_path = $this->securimage_path . '/audio/';
  275.         }
  276.        
  277.         if ($this->code_length == null || $this->code_length < 1) {
  278.             $this->code_length = 6;
  279.         }
  280.        
  281.         if ($this->perturbation == null || !is_numeric($this->perturbation)) {
  282.             $this->perturbation = 0.75;
  283.         }
  284.        
  285.         if ($this->namespace == null || !is_string($this->namespace)) {
  286.             $this->namespace = 'default';
  287.         }
  288.  
  289.         // Initialize session or attach to existing
  290.         if ( session_id() == '' ) { // no session has been started yet, which is needed for validation
  291.             if ($this->session_name != null && trim($this->session_name) != '') {
  292.                 session_name(trim($this->session_name)); // set session name if provided
  293.             }
  294.             session_start();
  295.         }
  296.     }
  297.    
  298.     /**
  299.      * Return the absolute path to the Securimage directory
  300.      * @return string The path to the securimage base directory
  301.      */
  302.     public static function getPath()
  303.     {
  304.         return dirname(__FILE__);
  305.     }
  306.    
  307.     /**
  308.      * Used to serve a captcha image to the browser
  309.      * @param string $background_image The path to the background image to use
  310.      * <code>
  311.      * $img = new Securimage();
  312.      * $img->code_length = 6;
  313.      * $img->num_lines   = 5;
  314.      * $img->noise_level = 5;
  315.      *
  316.      * $img->show(); // sends the image to browser
  317.      * exit;
  318.      * </code>
  319.      */
  320.     public function show($background_image = '')
  321.     {
  322.         if($background_image != '' && is_readable($background_image)) {
  323.             $this->bgimg = $background_image;
  324.         }
  325.  
  326.         $this->doImage();
  327.     }
  328.    
  329.     /**
  330.      * Check a submitted code against the stored value
  331.      * @param string $code  The captcha code to check
  332.      * <code>
  333.      * $code = $_POST['code'];
  334.      * $img  = new Securimage();
  335.      * if ($img->check($code) == true) {
  336.      *     $captcha_valid = true;
  337.      * } else {
  338.      *     $captcha_valid = false;
  339.      * }
  340.      * </code>
  341.      */
  342.     public function check($code)
  343.     {
  344.         $this->code_entered = $code;
  345.         $this->validate();
  346.         return $this->correct_code;
  347.     }
  348.    
  349.     /**
  350.      * Output a wav file of the captcha code to the browser
  351.      *
  352.      * <code>
  353.      * $img = new Securimage();
  354.      * $img->outputAudioFile(); // outputs a wav file to the browser
  355.      * exit;
  356.      * </code>
  357.      */
  358.     public function outputAudioFile()
  359.     {
  360.         $ext = 'wav'; // force wav - mp3 is insecure
  361.        
  362.         header("Content-Disposition: attachment; filename=\"securimage_audio.{$ext}\"");
  363.         header('Cache-Control: no-store, no-cache, must-revalidate');
  364.         header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
  365.         header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
  366.         header('Content-type: audio/x-wav');
  367.        
  368.         $audio = $this->getAudibleCode($ext);
  369.  
  370.         header('Content-Length: ' . strlen($audio));
  371.  
  372.         echo $audio;
  373.         exit;
  374.     }
  375.    
  376.     /**
  377.      * The main image drawing routing, responsible for constructing the entire image and serving it
  378.      */
  379.     protected function doImage()
  380.     {
  381.         if( ($this->use_transparent_text == true || $this->bgimg != '') && function_exists('imagecreatetruecolor')) {
  382.             $imagecreate = 'imagecreatetruecolor';
  383.         } else {
  384.             $imagecreate = 'imagecreate';
  385.         }
  386.        
  387.         $this->im     = $imagecreate($this->image_width, $this->image_height);
  388.         $this->tmpimg = $imagecreate($this->image_width * $this->iscale, $this->image_height * $this->iscale);
  389.        
  390.         $this->allocateColors();
  391.         imagepalettecopy($this->tmpimg, $this->im);
  392.  
  393.         $this->setBackground();
  394.  
  395.         $this->createCode();
  396.  
  397.         if ($this->noise_level > 0) {
  398.             $this->drawNoise();
  399.         }
  400.        
  401.         $this->drawWord();
  402.        
  403.         if ($this->perturbation > 0 && is_readable($this->ttf_file)) {
  404.             $this->distortedCopy();
  405.         }
  406.  
  407.         if ($this->num_lines > 0) {
  408.             $this->drawLines();
  409.         }
  410.  
  411.         if (trim($this->image_signature) != '') {
  412.             $this->addSignature();
  413.         }
  414.  
  415.         $this->output();
  416.     }
  417.    
  418.     /**
  419.      * Allocate the colors to be used for the image
  420.      */
  421.     protected function allocateColors()
  422.     {
  423.         // allocate bg color first for imagecreate
  424.         $this->gdbgcolor = imagecolorallocate($this->im,
  425.                                               $this->image_bg_color->r,
  426.                                               $this->image_bg_color->g,
  427.                                               $this->image_bg_color->b);
  428.        
  429.         $alpha = intval($this->text_transparency_percentage / 100 * 127);
  430.        
  431.         if ($this->use_transparent_text == true) {
  432.             $this->gdtextcolor = imagecolorallocatealpha($this->im,
  433.                                                          $this->text_color->r,
  434.                                                          $this->text_color->g,
  435.                                                          $this->text_color->b,
  436.                                                          $alpha);
  437.             $this->gdlinecolor = imagecolorallocatealpha($this->im,
  438.                                                          $this->line_color->r,
  439.                                                          $this->line_color->g,
  440.                                                          $this->line_color->b,
  441.                                                          $alpha);
  442.             $this->gdnoisecolor = imagecolorallocatealpha($this->im,
  443.                                                           $this->noise_color->r,
  444.                                                           $this->noise_color->g,
  445.                                                           $this->noise_color->b,
  446.                                                           $alpha);
  447.         } else {
  448.             $this->gdtextcolor = imagecolorallocate($this->im,
  449.                                                     $this->text_color->r,
  450.                                                     $this->text_color->g,
  451.                                                     $this->text_color->b);
  452.             $this->gdlinecolor = imagecolorallocate($this->im,
  453.                                                     $this->line_color->r,
  454.                                                     $this->line_color->g,
  455.                                                     $this->line_color->b);
  456.             $this->gdnoisecolor = imagecolorallocate($this->im,
  457.                                                           $this->noise_color->r,
  458.                                                           $this->noise_color->g,
  459.                                                           $this->noise_color->b);
  460.         }
  461.    
  462.         $this->gdsignaturecolor = imagecolorallocate($this->im,
  463.                                                      $this->signature_color->r,
  464.                                                      $this->signature_color->g,
  465.                                                      $this->signature_color->b);
  466.  
  467.     }
  468.    
  469.     /**
  470.      * The the background color, or background image to be used
  471.      */
  472.     protected function setBackground()
  473.     {
  474.         // set background color of image by drawing a rectangle since imagecreatetruecolor doesn't set a bg color
  475.         imagefilledrectangle($this->im, 0, 0,
  476.                              $this->image_width, $this->image_height,
  477.                              $this->gdbgcolor);
  478.         imagefilledrectangle($this->tmpimg, 0, 0,
  479.                              $this->image_width * $this->iscale, $this->image_height * $this->iscale,
  480.                              $this->gdbgcolor);
  481.    
  482.         if ($this->bgimg == '') {
  483.             if ($this->background_directory != null &&
  484.                 is_dir($this->background_directory) &&
  485.                 is_readable($this->background_directory))
  486.             {
  487.                 $img = $this->getBackgroundFromDirectory();
  488.                 if ($img != false) {
  489.                     $this->bgimg = $img;
  490.                 }
  491.             }
  492.         }
  493.        
  494.         if ($this->bgimg == '') {
  495.             return;
  496.         }
  497.  
  498.         $dat = @getimagesize($this->bgimg);
  499.         if($dat == false) {
  500.             return;
  501.         }
  502.  
  503.         switch($dat[2]) {
  504.             case 1:  $newim = @imagecreatefromgif($this->bgimg); break;
  505.             case 2:  $newim = @imagecreatefromjpeg($this->bgimg); break;
  506.             case 3:  $newim = @imagecreatefrompng($this->bgimg); break;
  507.             default: return;
  508.         }
  509.  
  510.         if(!$newim) return;
  511.  
  512.         imagecopyresized($this->im, $newim, 0, 0, 0, 0,
  513.                          $this->image_width, $this->image_height,
  514.                          imagesx($newim), imagesy($newim));
  515.     }
  516.    
  517.     /**
  518.      * Scan the directory for a background image to use
  519.      */
  520.     protected function getBackgroundFromDirectory()
  521.     {
  522.         $images = array();
  523.  
  524.         if ( ($dh = opendir($this->background_directory)) !== false) {
  525.             while (($file = readdir($dh)) !== false) {
  526.                 if (preg_match('/(jpg|gif|png)$/i', $file)) $images[] = $file;
  527.             }
  528.  
  529.             closedir($dh);
  530.  
  531.             if (sizeof($images) > 0) {
  532.                 return rtrim($this->background_directory, '/') . '/' . $images[rand(0, sizeof($images)-1)];
  533.             }
  534.         }
  535.  
  536.         return false;
  537.     }
  538.    
  539.     /**
  540.      * Generates the code or math problem and saves the value to the session
  541.      */
  542.     protected function createCode()
  543.     {
  544.         $this->code = false;
  545.  
  546.         switch($this->captcha_type) {
  547.             case self::SI_CAPTCHA_MATHEMATIC:
  548.             {
  549.                 $signs = array('+', '-', 'x');
  550.                 $left  = rand(1, 10);
  551.                 $right = rand(1, 5);
  552.                 $sign  = $signs[rand(0, 2)];
  553.                
  554.                 switch($sign) {
  555.                     case 'x': $c = $left * $right; break;
  556.                     case '-': $c = $left - $right; break;
  557.                     default:  $c = $left + $right; break;
  558.                 }
  559.                
  560.                 $this->code         = $c;
  561.                 $this->code_display = "$left $sign $right";
  562.                 break;
  563.             }
  564.            
  565.             default:
  566.             {
  567.                 if ($this->use_wordlist && is_readable($this->wordlist_file)) {
  568.                     $this->code = $this->readCodeFromFile();
  569.                 }
  570.  
  571.                 if ($this->code == false) {
  572.                     $this->code = $this->generateCode($this->code_length);
  573.                 }
  574.                
  575.                 $this->code_display = $this->code;
  576.                 $this->code         = ($this->case_sensitive) ? $this->code : strtolower($this->code);
  577.             } // default
  578.         }
  579.        
  580.         $this->saveData();
  581.     }
  582.    
  583.     /**
  584.      * Draws the captcha code on the image
  585.      */
  586.     protected function drawWord()
  587.     {
  588.         $width2  = $this->image_width * $this->iscale;
  589.         $height2 = $this->image_height * $this->iscale;
  590.          
  591.         if (!is_readable($this->ttf_file)) {
  592.             imagestring($this->im, 4, 10, ($this->image_height / 2) - 5, 'Failed to load TTF font file!', $this->gdtextcolor);
  593.         } else {
  594.             if ($this->perturbation > 0) {
  595.                 $font_size = $height2 * .4;
  596.                 $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
  597.                 $tx = $bb[4] - $bb[0];
  598.                 $ty = $bb[5] - $bb[1];
  599.                 $x  = floor($width2 / 2 - $tx / 2 - $bb[0]);
  600.                 $y  = round($height2 / 2 - $ty / 2 - $bb[1]);
  601.  
  602.                 imagettftext($this->tmpimg, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
  603.             } else {
  604.                 $font_size = $this->image_height * .4;
  605.                 $bb = imageftbbox($font_size, 0, $this->ttf_file, $this->code_display);
  606.                 $tx = $bb[4] - $bb[0];
  607.                 $ty = $bb[5] - $bb[1];
  608.                 $x  = floor($this->image_width / 2 - $tx / 2 - $bb[0]);
  609.                 $y  = round($this->image_height / 2 - $ty / 2 - $bb[1]);
  610.  
  611.                 imagettftext($this->im, $font_size, 0, $x, $y, $this->gdtextcolor, $this->ttf_file, $this->code_display);
  612.             }
  613.         }
  614.        
  615.         // DEBUG
  616.         //$this->im = $this->tmpimg;
  617.         //$this->output();
  618.        
  619.     }
  620.    
  621.     /**
  622.      * Copies the captcha image to the final image with distortion applied
  623.      */
  624.     protected function distortedCopy()
  625.     {
  626.         $numpoles = 3; // distortion factor
  627.         // make array of poles AKA attractor points
  628.         for ($i = 0; $i < $numpoles; ++ $i) {
  629.             $px[$i]  = rand($this->image_width  * 0.2, $this->image_width  * 0.8);
  630.             $py[$i]  = rand($this->image_height * 0.2, $this->image_height * 0.8);
  631.             $rad[$i] = rand($this->image_height * 0.2, $this->image_height * 0.8);
  632.             $tmp     = ((- $this->frand()) * 0.15) - .15;
  633.             $amp[$i] = $this->perturbation * $tmp;
  634.         }
  635.        
  636.         $bgCol = imagecolorat($this->tmpimg, 0, 0);
  637.         $width2 = $this->iscale * $this->image_width;
  638.         $height2 = $this->iscale * $this->image_height;
  639.         imagepalettecopy($this->im, $this->tmpimg); // copy palette to final image so text colors come across
  640.         // loop over $img pixels, take pixels from $tmpimg with distortion field
  641.         for ($ix = 0; $ix < $this->image_width; ++ $ix) {
  642.             for ($iy = 0; $iy < $this->image_height; ++ $iy) {
  643.                 $x = $ix;
  644.                 $y = $iy;
  645.                 for ($i = 0; $i < $numpoles; ++ $i) {
  646.                     $dx = $ix - $px[$i];
  647.                     $dy = $iy - $py[$i];
  648.                     if ($dx == 0 && $dy == 0) {
  649.                         continue;
  650.                     }
  651.                     $r = sqrt($dx * $dx + $dy * $dy);
  652.                     if ($r > $rad[$i]) {
  653.                         continue;
  654.                     }
  655.                     $rscale = $amp[$i] * sin(3.14 * $r / $rad[$i]);
  656.                     $x += $dx * $rscale;
  657.                     $y += $dy * $rscale;
  658.                 }
  659.                 $c = $bgCol;
  660.                 $x *= $this->iscale;
  661.                 $y *= $this->iscale;
  662.                 if ($x >= 0 && $x < $width2 && $y >= 0 && $y < $height2) {
  663.                     $c = imagecolorat($this->tmpimg, $x, $y);
  664.                 }
  665.                 if ($c != $bgCol) { // only copy pixels of letters to preserve any background image
  666.                     imagesetpixel($this->im, $ix, $iy, $c);
  667.                 }
  668.             }
  669.         }
  670.     }
  671.    
  672.     /**
  673.      * Draws distorted lines on the image
  674.      */
  675.     protected function drawLines()
  676.     {
  677.         for ($line = 0; $line < $this->num_lines; ++ $line) {
  678.             $x = $this->image_width * (1 + $line) / ($this->num_lines + 1);
  679.             $x += (0.5 - $this->frand()) * $this->image_width / $this->num_lines;
  680.             $y = rand($this->image_height * 0.1, $this->image_height * 0.9);
  681.            
  682.             $theta = ($this->frand() - 0.5) * M_PI * 0.7;
  683.             $w = $this->image_width;
  684.             $len = rand($w * 0.4, $w * 0.7);
  685.             $lwid = rand(0, 2);
  686.            
  687.             $k = $this->frand() * 0.6 + 0.2;
  688.             $k = $k * $k * 0.5;
  689.             $phi = $this->frand() * 6.28;
  690.             $step = 0.5;
  691.             $dx = $step * cos($theta);
  692.             $dy = $step * sin($theta);
  693.             $n = $len / $step;
  694.             $amp = 1.5 * $this->frand() / ($k + 5.0 / $len);
  695.             $x0 = $x - 0.5 * $len * cos($theta);
  696.             $y0 = $y - 0.5 * $len * sin($theta);
  697.            
  698.             $ldx = round(- $dy * $lwid);
  699.             $ldy = round($dx * $lwid);
  700.            
  701.             for ($i = 0; $i < $n; ++ $i) {
  702.                 $x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
  703.                 $y = $y0 + $i * $dy - $amp * $dx * sin($k * $i * $step + $phi);
  704.                 imagefilledrectangle($this->im, $x, $y, $x + $lwid, $y + $lwid, $this->gdlinecolor);
  705.             }
  706.         }
  707.     }
  708.    
  709.     /**
  710.      * Draws random noise on the image
  711.      */
  712.     protected function drawNoise()
  713.     {
  714.         if ($this->noise_level > 10) {
  715.             $noise_level = 10;
  716.         } else {
  717.             $noise_level = $this->noise_level;
  718.         }
  719.  
  720.         $t0 = microtime(true);
  721.        
  722.         $noise_level *= 125; // an arbitrary number that works well on a 1-10 scale
  723.        
  724.         $points = $this->image_width * $this->image_height * $this->iscale;
  725.         $height = $this->image_height * $this->iscale;
  726.         $width  = $this->image_width * $this->iscale;
  727.         for ($i = 0; $i < $noise_level; ++$i) {
  728.             $x = rand(10, $width);
  729.             $y = rand(10, $height);
  730.             $size = rand(7, 10);
  731.             if ($x - $size <= 0 && $y - $size <= 0) continue; // dont cover 0,0 since it is used by imagedistortedcopy
  732.             imagefilledarc($this->tmpimg, $x, $y, $size, $size, 0, 360, $this->gdnoisecolor, IMG_ARC_PIE);
  733.         }
  734.        
  735.         $t1 = microtime(true);
  736.        
  737.         $t = $t1 - $t0;
  738.        
  739.         /*
  740.         // DEBUG
  741.         imagestring($this->tmpimg, 5, 25, 30, "$t", $this->gdnoisecolor);
  742.         header('content-type: image/png');
  743.         imagepng($this->tmpimg);
  744.         exit;
  745.         */
  746.     }
  747.    
  748.     /**
  749.     * Print signature text on image
  750.     */
  751.     protected function addSignature()
  752.     {
  753.         $bbox = imagettfbbox(10, 0, $this->signature_font, $this->image_signature);
  754.         $textlen = $bbox[2] - $bbox[0];
  755.         $x = $this->image_width - $textlen - 5;
  756.         $y = $this->image_height - 3;
  757.              
  758.         imagettftext($this->im, 10, 0, $x, $y, $this->gdsignaturecolor, $this->signature_font, $this->image_signature);
  759.     }
  760.    
  761.     /**
  762.      * Sends the appropriate image and cache headers and outputs image to the browser
  763.      */
  764.     protected function output()
  765.     {
  766.         header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
  767.         header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT");
  768.         header("Cache-Control: no-store, no-cache, must-revalidate");
  769.         header("Cache-Control: post-check=0, pre-check=0", false);
  770.         header("Pragma: no-cache");
  771.        
  772.         switch ($this->image_type) {
  773.             case self::SI_IMAGE_JPEG:
  774.                 header("Content-Type: image/jpeg");
  775.                 imagejpeg($this->im, null, 90);
  776.                 break;
  777.             case self::SI_IMAGE_GIF:
  778.                 header("Content-Type: image/gif");
  779.                 imagegif($this->im);
  780.                 break;
  781.             default:
  782.                 header("Content-Type: image/png");
  783.                 imagepng($this->im);
  784.                 break;
  785.         }
  786.        
  787.         imagedestroy($this->im);
  788.         exit();
  789.     }
  790.    
  791.     /**
  792.      * Gets the code and returns the binary audio file for the stored captcha code
  793.      * @param string $format WAV only
  794.      */
  795.     protected function getAudibleCode($format = 'wav')
  796.     {
  797.         // override any format other than wav for now
  798.         // this is due to security issues with MP3 files
  799.         $format  = 'wav';
  800.        
  801.         $letters = array();
  802.         $code    = $this->getCode();
  803.  
  804.         if ($code == '') {
  805.             $this->createCode();
  806.             $code = $this->getCode();
  807.         }
  808.  
  809.         for($i = 0; $i < strlen($code); ++$i) {
  810.             $letters[] = $code{$i};
  811.         }
  812.        
  813.         if ($format == 'mp3') {
  814.             return $this->generateMP3($letters);
  815.         } else {
  816.             return $this->generateWAV($letters);
  817.         }
  818.     }
  819.  
  820.     /**
  821.      * Gets a captcha code from a wordlist
  822.      */
  823.     protected function readCodeFromFile()
  824.     {
  825.         $fp = @fopen($this->wordlist_file, 'rb');
  826.         if (!$fp) return false;
  827.  
  828.         $fsize = filesize($this->wordlist_file);
  829.         if ($fsize < 128) return false; // too small of a list to be effective
  830.  
  831.         fseek($fp, rand(0, $fsize - 64), SEEK_SET); // seek to a random position of file from 0 to filesize-64
  832.         $data = fread($fp, 64); // read a chunk from our random position
  833.         fclose($fp);
  834.         $data = preg_replace("/\r?\n/", "\n", $data);
  835.  
  836.         $start = @strpos($data, "\n", rand(0, 56)) + 1; // random start position
  837.         $end   = @strpos($data, "\n", $start);          // find end of word
  838.        
  839.         if ($start === false) {
  840.             return false;
  841.         } else if ($end === false) {
  842.             $end = strlen($data);
  843.         }
  844.  
  845.         return strtolower(substr($data, $start, $end - $start)); // return a line of the file
  846.     }
  847.    
  848.     /**
  849.      * Generates a random captcha code from the set character set
  850.      */
  851.     protected function generateCode()
  852.     {
  853.         $code = '';
  854.  
  855.         for($i = 1, $cslen = strlen($this->charset); $i <= $this->code_length; ++$i) {
  856.             $code .= $this->charset{rand(0, $cslen - 1)};
  857.         }
  858.        
  859.         //return 'testing';  // debug, set the code to given string
  860.        
  861.         return $code;
  862.     }
  863.    
  864.     /**
  865.      * Checks the entered code against the value stored in the session or sqlite database, handles case sensitivity
  866.      * Also clears the stored codes if the code was entered correctly to prevent re-use
  867.      */
  868.     protected function validate()
  869.     {
  870.         $code = $this->getCode();
  871.         // returns stored code, or an empty string if no stored code was found
  872.         // checks the session and sqlite database if enabled
  873.        
  874.         if ($this->case_sensitive == false && preg_match('/[A-Z]/', $code)) {
  875.             // case sensitive was set from securimage_show.php but not in class
  876.             // the code saved in the session has capitals so set case sensitive to true
  877.             $this->case_sensitive = true;
  878.         }
  879.        
  880.         $code_entered = trim( (($this->case_sensitive) ? $this->code_entered
  881.                                                        : strtolower($this->code_entered))
  882.                         );
  883.         $this->correct_code = false;
  884.        
  885.         if ($code != '') {
  886.             if ($code == $code_entered) {
  887.                 $this->correct_code = true;
  888.                 $_SESSION['securimage_code_value'][$this->namespace] = '';
  889.                 $_SESSION['securimage_code_ctime'][$this->namespace] = '';
  890.                 $this->clearCodeFromDatabase();
  891.             }
  892.         }
  893.     }
  894.    
  895.     /**
  896.      * Return the code from the session or sqlite database if used.  If none exists yet, an empty string is returned
  897.      */
  898.     protected function getCode()
  899.     {
  900.         $code = '';
  901.        
  902.         if (isset($_SESSION['securimage_code_value'][$this->namespace]) &&
  903.          trim($_SESSION['securimage_code_value'][$this->namespace]) != '') {
  904.             if ($this->isCodeExpired(
  905.             $_SESSION['securimage_code_ctime'][$this->namespace]) == false) {
  906.                 $code = $_SESSION['securimage_code_value'][$this->namespace];
  907.             }
  908.         } else if ($this->use_sqlite_db == true && function_exists('sqlite_open')) {
  909.             // no code in session - may mean user has cookies turned off
  910.             $this->openDatabase();
  911.             $code = $this->getCodeFromDatabase();
  912.         } else { /* no code stored in session or sqlite database, validation will fail */ }
  913.        
  914.         return $code;
  915.     }
  916.    
  917.     /**
  918.      * Save data to session namespace and database if used
  919.      */
  920.     protected function saveData()
  921.     {
  922.         $_SESSION['securimage_code_value'][$this->namespace] = $this->code;
  923.         $_SESSION['securimage_code_ctime'][$this->namespace] = time();
  924.        
  925.         $this->saveCodeToDatabase();
  926.     }
  927.    
  928.     /**
  929.      * Saves the code to the sqlite database
  930.      */
  931.     protected function saveCodeToDatabase()
  932.     {
  933.         $success = false;
  934.        
  935.         $this->openDatabase();
  936.        
  937.         if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
  938.             $ip      = $_SERVER['REMOTE_ADDR'];
  939.             $time    = time();
  940.             $code    = $_SESSION['securimage_code_value'][$this->namespace]; // if cookies are disabled the session still exists at this point
  941.             $success = sqlite_query($this->sqlite_handle,
  942.                                     "INSERT OR REPLACE INTO codes(ip, code, namespace, created)
  943.                                    VALUES('$ip', '$code', '{$this->namespace}', $time)");
  944.         }
  945.        
  946.         return $success !== false;
  947.     }
  948.    
  949.     /**
  950.      * Open sqlite database
  951.      */
  952.     protected function openDatabase()
  953.     {
  954.         $this->sqlite_handle = false;
  955.        
  956.         if ($this->use_sqlite_db && function_exists('sqlite_open')) {
  957.             $this->sqlite_handle = sqlite_open($this->sqlite_database, 0666, $error);
  958.            
  959.             if ($this->sqlite_handle !== false) {
  960.                 $res = sqlite_query($this->sqlite_handle, "PRAGMA table_info(codes)");
  961.                 if (sqlite_num_rows($res) == 0) {
  962.                     sqlite_query($this->sqlite_handle, "CREATE TABLE codes (ip VARCHAR(32) PRIMARY KEY, code VARCHAR(32) NOT NULL, namespace VARCHAR(32) NOT NULL, created INTEGER)");
  963.                 }
  964.             }
  965.            
  966.             return $this->sqlite_handle != false;
  967.         }
  968.        
  969.         return $this->sqlite_handle;
  970.     }
  971.    
  972.     /**
  973.      * Get a code from the sqlite database for ip address
  974.      */
  975.     protected function getCodeFromDatabase()
  976.     {
  977.         $code = '';
  978.  
  979.         if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
  980.             $ip = $_SERVER['REMOTE_ADDR'];
  981.             $ns = sqlite_escape_string($this->namespace);
  982.  
  983.             $res = sqlite_query($this->sqlite_handle, "SELECT * FROM codes WHERE ip = '$ip' AND namespace = '$ns'");
  984.             if ($res && sqlite_num_rows($res) > 0) {
  985.                 $res = sqlite_fetch_array($res);
  986.  
  987.                 if ($this->isCodeExpired($res['created']) == false) {
  988.                     $code = $res['code'];
  989.                 }
  990.             }
  991.         }
  992.         return $code;
  993.     }
  994.    
  995.     /**
  996.      * Remove an entered code from the database
  997.      */
  998.     protected function clearCodeFromDatabase()
  999.     {
  1000.         if (is_resource($this->sqlite_handle)) {
  1001.             $ip = $_SERVER['REMOTE_ADDR'];
  1002.             $ns = sqlite_escape_string($this->namespace);
  1003.            
  1004.             sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE ip = '$ip' AND namespace = '$ns'");
  1005.         }
  1006.     }
  1007.    
  1008.     /**
  1009.      * Deletes old codes from sqlite database
  1010.      */
  1011.     protected function purgeOldCodesFromDatabase()
  1012.     {
  1013.         if ($this->use_sqlite_db && $this->sqlite_handle !== false) {
  1014.             $now   = time();
  1015.             $limit = (!is_numeric($this->expiry_time) || $this->expiry_time < 1) ? 86400 : $this->expiry_time;
  1016.            
  1017.             sqlite_query($this->sqlite_handle, "DELETE FROM codes WHERE $now - created > $limit");
  1018.         }
  1019.     }
  1020.    
  1021.     /**
  1022.      * Checks to see if the captcha code has expired and cannot be used
  1023.      * @param unknown_type $creation_time
  1024.      */
  1025.     protected function isCodeExpired($creation_time)
  1026.     {
  1027.         $expired = true;
  1028.        
  1029.         if (!is_numeric($this->expiry_time) || $this->expiry_time < 1) {
  1030.             $expired = false;
  1031.         } else if (time() - $creation_time < $this->expiry_time) {
  1032.             $expired = false;
  1033.         }
  1034.        
  1035.         return $expired;
  1036.     }
  1037.    
  1038.     /**
  1039.      *
  1040.      * Generate an MP3 audio file of the captcha image
  1041.      *
  1042.      * @deprecated 3.0
  1043.      */
  1044.     protected function generateMP3()
  1045.     {
  1046.         return false;
  1047.     }
  1048.    
  1049.     /**
  1050.      * Generate a wav file given the $letters in the code
  1051.      * @todo Add ability to merge 2 sound files together to have random background sounds
  1052.      * @param array $letters
  1053.      * @return string The binary contents of the wav file
  1054.      */
  1055.     protected function generateWAV($letters)
  1056.     {
  1057.         $data_len       = 0;
  1058.         $files          = array();
  1059.         $out_data       = '';
  1060.         $out_channels   = 0;
  1061.         $out_samplert   = 0;
  1062.         $out_bpersample = 0;
  1063.         $numSamples     = 0;
  1064.         $removeChunks   = array('LIST', 'DISP', 'NOTE');
  1065.  
  1066.         for ($i = 0; $i < sizeof($letters); ++$i) {
  1067.             $letter   = $letters[$i];
  1068.             $filename = $this->audio_path . strtoupper($letter) . '.wav';
  1069.             $file     = array();
  1070.             $data     = @file_get_contents($filename);
  1071.            
  1072.             if ($data === false) {
  1073.                 // echo "Failed to read $filename";
  1074.                 return $this->audioError();
  1075.             }
  1076.  
  1077.             $header = substr($data, 0, 36);
  1078.             $info   = unpack('NChunkID/VChunkSize/NFormat/NSubChunk1ID/'
  1079.                             .'VSubChunk1Size/vAudioFormat/vNumChannels/'
  1080.                             .'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
  1081.                              $header);
  1082.            
  1083.             $dataPos        = strpos($data, 'data');
  1084.             $out_channels   = $info['NumChannels'];
  1085.             $out_samplert   = $info['SampleRate'];
  1086.             $out_bpersample = $info['BitsPerSample'];
  1087.            
  1088.             if ($dataPos === false) {
  1089.                 // wav file with no data?
  1090.                 // echo "Failed to find DATA segment in $filename";
  1091.                 return $this->audioError();
  1092.             }
  1093.            
  1094.             if ($info['AudioFormat'] != 1) {
  1095.                 // only work with PCM audio
  1096.                 // echo "$filename was not PCM audio, only PCM is supported";
  1097.                 return $this->audioError();
  1098.             }
  1099.            
  1100.             if ($info['SubChunk1Size'] != 16 && $info['SubChunk1Size'] != 18) {
  1101.                 // probably unsupported extension
  1102.                 // echo "Bad SubChunk1Size in $filename - Size was {$info['SubChunk1Size']}";
  1103.                 return $this->audioError();
  1104.             }
  1105.            
  1106.             if ($info['SubChunk1Size'] > 16) {
  1107.                 $header .= substr($data, 36, $info['SubChunk1Size'] - 16);
  1108.             }
  1109.            
  1110.             if ($i == 0) {
  1111.                 // create the final file's header, size will be adjusted later
  1112.                 $out_data = $header . 'data';
  1113.             }
  1114.            
  1115.             $removed = 0;
  1116.            
  1117.             foreach($removeChunks as $chunk) {
  1118.                 $chunkPos = strpos($data, $chunk);
  1119.                 if ($chunkPos !== false) {
  1120.                     $listSize = unpack('VSize', substr($data, $chunkPos + 4, 4));
  1121.                    
  1122.                     $data = substr($data, 0, $chunkPos) .
  1123.                             substr($data, $chunkPos + 8 + $listSize['Size']);
  1124.                            
  1125.                     $removed += $listSize['Size'] + 8;
  1126.                 }
  1127.             }
  1128.            
  1129.             $dataSize    = unpack('VSubchunk2Size', substr($data, $dataPos + 4, 4));
  1130.             $dataSize['Subchunk2Size'] -= $removed;
  1131.             $out_data   .= substr($data, $dataPos + 8, $dataSize['Subchunk2Size'] * ($out_bpersample / 8));
  1132.             $numSamples += $dataSize['Subchunk2Size'];
  1133.         }
  1134.  
  1135.         $filesize  = strlen($out_data);
  1136.         $chunkSize = $filesize - 8;
  1137.         $dataCSize = $numSamples;
  1138.        
  1139.         $out_data = substr_replace($out_data, pack('V', $chunkSize), 4, 4);
  1140.         $out_data = substr_replace($out_data, pack('V', $numSamples), 40 + ($info['SubChunk1Size'] - 16), 4);
  1141.  
  1142.         $this->scrambleAudioData($out_data, 'wav');
  1143.        
  1144.         return $out_data;
  1145.     }
  1146.    
  1147.     /**
  1148.      * Randomizes the audio data to add noise and prevent binary recognition
  1149.      * @param string $data  The binary audio file data
  1150.      * @param string $format The format of the sound file (wav only)
  1151.      */
  1152.     protected function scrambleAudioData(&$data, $format)
  1153.     {
  1154.         $start = strpos($data, 'data') + 4; // look for "data" indicator
  1155.         if ($start === false) $start = 44;  // if not found assume 44 byte header
  1156.          
  1157.         $start  += rand(1, 4); // randomize starting offset
  1158.         $datalen = strlen($data) - $start;
  1159.         $step    = 1;
  1160.        
  1161.         for ($i = $start; $i < $datalen; $i += $step) {
  1162.             $ch = ord($data{$i});
  1163.             if ($ch == 0 || $ch == 255) continue;
  1164.            
  1165.             if ($ch < 16 || $ch > 239) {
  1166.                 $ch += rand(-6, 6);
  1167.             } else {
  1168.                 $ch += rand(-12, 12);
  1169.             }
  1170.            
  1171.             if ($ch < 0) $ch = 0; else if ($ch > 255) $ch = 255;
  1172.  
  1173.             $data{$i} = chr($ch);
  1174.            
  1175.             $step = rand(1,4);
  1176.         }
  1177.  
  1178.         return $data;
  1179.     }
  1180.    
  1181.     /**
  1182.      * Return a wav file saying there was an error generating file
  1183.      *
  1184.      * @return string The binary audio contents
  1185.      */
  1186.     protected function audioError()
  1187.     {
  1188.         return @file_get_contents(dirname(__FILE__) . '/audio/error.wav');
  1189.     }
  1190.    
  1191.     function frand()
  1192.     {
  1193.         return 0.0001 * rand(0,9999);
  1194.     }
  1195.    
  1196.     /**
  1197.      * Convert an html color code to a Securimage_Color
  1198.      * @param string $color
  1199.      * @param Securimage_Color $default The defalt color to use if $color is invalid
  1200.      */
  1201.     protected function initColor($color, $default)
  1202.     {
  1203.         if ($color == null) {
  1204.             return new Securimage_Color($default);
  1205.         } else if (is_string($color)) {
  1206.             try {
  1207.                 return new Securimage_Color($color);
  1208.             } catch(Exception $e) {
  1209.                 return new Securimage_Color($default);
  1210.             }
  1211.         } else if (is_array($color) && sizeof($color) == 3) {
  1212.             return new Securimage_Color($color[0], $color[1], $color[2]);
  1213.         } else {
  1214.             return new Securimage_Color($default);
  1215.         }
  1216.     }
  1217. }
  1218.  
  1219.  
  1220. /**
  1221.  * Color object for Securimage CAPTCHA
  1222.  *
  1223.  * @version 3.0
  1224.  * @since 2.0
  1225.  * @package Securimage
  1226.  * @subpackage classes
  1227.  *
  1228.  */
  1229. class Securimage_Color
  1230. {
  1231.     public $r;
  1232.     public $g;
  1233.     public $b;
  1234.  
  1235.     /**
  1236.      * Create a new Securimage_Color object.<br />
  1237.      * Constructor expects 1 or 3 arguments.<br />
  1238.      * When passing a single argument, specify the color using HTML hex format,<br />
  1239.      * when passing 3 arguments, specify each RGB component (from 0-255) individually.<br />
  1240.      * $color = new Securimage_Color('#0080FF') or <br />
  1241.      * $color = new Securimage_Color(0, 128, 255)
  1242.      *
  1243.      * @param string $color
  1244.      * @throws Exception
  1245.      */
  1246.     public function __construct($color = '#ffffff')
  1247.     {
  1248.         $args = func_get_args();
  1249.        
  1250.         if (sizeof($args) == 0) {
  1251.             $this->r = 255;
  1252.             $this->g = 255;
  1253.             $this->b = 255;
  1254.         } else if (sizeof($args) == 1) {
  1255.             // set based on html code
  1256.             if (substr($color, 0, 1) == '#') {
  1257.                 $color = substr($color, 1);
  1258.             }
  1259.            
  1260.             if (strlen($color) != 3 && strlen($color) != 6) {
  1261.                 throw new InvalidArgumentException(
  1262.                   'Invalid HTML color code passed to Securimage_Color'
  1263.                 );
  1264.             }
  1265.            
  1266.             $this->constructHTML($color);
  1267.         } else if (sizeof($args) == 3) {
  1268.             $this->constructRGB($args[0], $args[1], $args[2]);
  1269.         } else {
  1270.             throw new InvalidArgumentException(
  1271.               'Securimage_Color constructor expects 0, 1 or 3 arguments; ' . sizeof($args) . ' given'
  1272.             );
  1273.         }
  1274.     }
  1275.    
  1276.     /**
  1277.      * Construct from an rgb triplet
  1278.      * @param int $red The red component, 0-255
  1279.      * @param int $green The green component, 0-255
  1280.      * @param int $blue The blue component, 0-255
  1281.      */
  1282.     protected function constructRGB($red, $green, $blue)
  1283.     {
  1284.         if ($red < 0)     $red   = 0;
  1285.         if ($red > 255)   $red   = 255;
  1286.         if ($green < 0)   $green = 0;
  1287.         if ($green > 255) $green = 255;
  1288.         if ($blue < 0)    $blue  = 0;
  1289.         if ($blue > 255)  $blue  = 255;
  1290.        
  1291.         $this->r = $red;
  1292.         $this->g = $green;
  1293.         $this->b = $blue;
  1294.     }
  1295.    
  1296.     /**
  1297.      * Construct from an html hex color code
  1298.      * @param string $color
  1299.      */
  1300.     protected function constructHTML($color)
  1301.     {
  1302.         if (strlen($color) == 3) {
  1303.             $red   = str_repeat(substr($color, 0, 1), 2);
  1304.             $green = str_repeat(substr($color, 1, 1), 2);
  1305.             $blue  = str_repeat(substr($color, 2, 1), 2);
  1306.         } else {
  1307.             $red   = substr($color, 0, 2);
  1308.             $green = substr($color, 2, 2);
  1309.             $blue  = substr($color, 4, 2);
  1310.         }
  1311.        
  1312.         $this->r = hexdec($red);
  1313.         $this->g = hexdec($green);
  1314.         $this->b = hexdec($blue);
  1315.     }
  1316. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement