Show
Ignore:
Timestamp:
07/09/2008 03:45:47 PM (5 months ago)
Author:
Geert
Message:

Revamped Captcha library big time.

  • Each Captcha style has its own driver which generates and renders a Captcha challenge. The output could be an image, but could be anything else since html output is allowed. I included a Riddle driver to give you an idea of the flexibility.
  • Config groups supported.
  • More polished coding style and structure in general.
  • Still needs work on the image generating part. Mostly GD2 stuff, and quite some copy and paste from the previous version. Any help appreciated. :)
Files:
1 modified

Legend:

Unmodified
Added
Removed
  • trunk/system/libraries/Captcha.php

    r3010 r3015  
    1212class Captcha_Core { 
    1313 
    14         // Config 
    15         protected $font_path        = ''; 
    16         protected $font_name        = ''; 
    17         protected $width            = 150; 
    18         protected $height           = 50; 
    19         protected $background_image = ''; 
    20         protected $style            = 'basic'; 
    21         protected $num_chars        = 4; 
     14        // Style-dependent Captcha driver 
     15        protected $driver; 
    2216 
    23         // Class internal variables 
    24         protected $image; 
    25         protected $color_black; 
    26         protected $color_white; 
    27         protected $spacing; 
    28         protected $captcha_code; 
    29         protected $numerals = array('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'); 
     17        // Config values 
     18        public static $config = array 
     19        ( 
     20                'style'      => 'basic', 
     21                'width'      => 150, 
     22                'height'     => 50, 
     23                'complexity' => 4, 
     24                'background' => '', 
     25                'font'       => '', 
     26        ); 
     27 
     28        // The Captcha challenge answer, the text the user is supposed to enter 
     29        public static $answer; 
    3030 
    3131        /** 
    32          * Creates a new Captcha instance. 
     32         * Constructs a new Captcha object. 
    3333         * 
    3434         * @throws  Kohana_Exception 
    35          * @param   array  configuration 
     35         * @param   string  configuration settings 
    3636         * @return  void 
    3737         */ 
    3838        public function __construct($config = array()) 
    3939        { 
    40                 static $check; 
     40                static $gd2_check; 
    4141 
    42                 // Check that a suitable GD2 library is available 
    43                 ($check === NULL) and $check = function_exists('imagegd2'); 
    44  
    45                 if ($check === FALSE) 
     42                // We need GD2 exclusively 
     43                if ($gd2_check === NULL AND ($gd2_check = function_exists('imagegd2')) === FALSE) 
    4644                        throw new Kohana_Exception('captcha.requires_GD2'); 
    4745 
    48                 // Load configuration 
    49                 $config += Config::item('captcha', FALSE, FALSE); 
    50  
    51                 $this->initialize($config); 
    52  
    53                 // If using a background image, check if it exists. 
    54                 if ($this->background_image) 
     46                // No custom config group name given 
     47                if ( ! isset($config['group'])) 
    5548                { 
    56                         if ( ! file_exists($this->background_image)) 
    57                                 throw new Kohana_Exception('captcha.file_not_found', $this->background_image); 
     49                        $config['group'] = 'default'; 
    5850                } 
    5951 
    60                 // If using a font, check if it exists. 
    61                 if ($this->font_name) 
     52                // Load and validate config group 
     53                if ( ! is_array($group_config = Config::item('captcha.'.$config['group']))) 
     54                        throw new Kohana_Exception('captcha.undefined_group', $config['group']); 
     55 
     56                // All captcha config groups inherit default config group 
     57                if ($config['group'] !== 'default') 
    6258                { 
    63                         if ( ! file_exists($this->font_path.$this->font_name)) 
    64                                 throw new Kohana_Exception('captcha.file_not_found', $this->font_path.$this->font_name); 
     59                        // Load and validate default config group 
     60                        if ( ! is_array($default_config = Config::item('captcha.default'))) 
     61                                throw new Kohana_Exception('captcha.undefined_group', 'default'); 
     62 
     63                        // Merge config group with default config group 
     64                        $group_config += $default_config; 
    6565                } 
     66 
     67                // Merge custom config items with config group 
     68                $config += $group_config; 
     69 
     70                // Assign config values to the object 
     71                foreach ($config as $key => $value) 
     72                { 
     73                        if (array_key_exists($key, self::$config)) 
     74                        { 
     75                                self::$config[$key] = $value; 
     76                        } 
     77                } 
     78 
     79                // If using a background image, check if it exists 
     80                if ( ! empty($config['background_file'])) 
     81                { 
     82                        self::$config['background'] = str_replace('\\', '/', realpath($config['background_path'])).'/'.$config['background_file']; 
     83 
     84                        if ( ! file_exists(self::$config['background'])) 
     85                                throw new Kohana_Exception('captcha.file_not_found', self::$config['background']); 
     86                } 
     87 
     88                // If using a font, check if it exists 
     89                if ( ! empty($config['font_file'])) 
     90                { 
     91                        self::$config['font'] = str_replace('\\', '/', realpath($config['font_path'])).'/'.$config['font_file']; 
     92 
     93                        if ( ! file_exists(self::$config['font'])) 
     94                                throw new Kohana_Exception('captcha.file_not_found', self::$config['font']); 
     95                } 
     96 
     97                // Set driver name 
     98                $driver = 'Captcha_'.ucfirst($config['style']).'_Driver'; 
     99 
     100                // Load the driver 
     101                if ( ! Kohana::auto_load($driver)) 
     102                        throw new Kohana_Exception('core.driver_not_found', $config['style'], get_class($this)); 
     103 
     104                // Initialize the driver 
     105                $this->driver = new $driver(); 
     106 
     107                // Validate the driver 
     108                if ( ! ($this->driver instanceof Captcha_Driver)) 
     109                        throw new Kohana_Exception('core.driver_implements', $type, get_class($this), 'Captcha_Driver'); 
     110 
     111                // Generate a new Captcha challenge 
     112                self::$answer = (string) $this->driver->generate_challenge(); 
     113 
     114                // Store the answer in a session 
     115                Session::instance()->set('captcha_answer', self::$answer); 
    66116 
    67117                Log::add('debug', 'Captcha Library initialized'); 
     
    69119 
    70120        /** 
    71          * Sets or overwrites config values. 
     121         * Validates a Captcha answer. 
    72122         * 
    73          * @param   array  configuration 
    74          * @return  void 
     123         * @param   string   captcha answer 
     124         * @return  boolean 
    75125         */ 
    76         public function initialize($config) 
     126        public static function valid($answer) 
    77127        { 
    78                 // Assign config values to the object 
    79                 foreach ($config as $key => $value) 
    80                 { 
    81                         if (property_exists($this, $key)) 
    82                         { 
    83                                 $this->$key = $value; 
    84                         } 
    85                 } 
     128                return (strtoupper($answer) === strtoupper(Session::instance()->get('captcha_answer'))); 
    86129        } 
    87130 
    88131        /** 
    89          * Sets the Captcha code to use. 
     132         * Output the Captcha challenge. 
    90133         * 
    91          * @param   string  captcha code generated in captcha controller 
    92          * @return  void 
     134         * @param   boolean  TRUE to output html, e.g. <img src="#" /> 
     135         * @return  mixed 
    93136         */ 
    94         public function set_code($str) 
     137        public function render($html = TRUE) 
    95138        { 
    96                 $this->captcha_code = (string) $str; 
     139                return $this->driver->render($html); 
    97140        } 
    98141 
    99142        /** 
    100          * Generates the Captcha image. 
     143         * Magically outputs the Captcha challenge. 
    101144         * 
    102          * @return  void 
     145         * @return  mixed 
    103146         */ 
    104         public function render() 
     147        public function __toString() 
    105148        { 
    106                 // If extending the class with a custom Captcha function, name it 'xyz_captcha'. 
    107                 // Style 'xyz' must be added to config. Now call the method that implements the Captcha. 
    108                 $this->{$this->style.'_captcha'}(); 
    109  
    110                 // Tell browser what to expect 
    111                 // TODO: make this automatic 
    112                 // header('Content-Type: image/jpeg'); 
    113                 header('Content-Type: image/png'); 
    114  
    115                 // Output the captcha image 
    116                 // imagejpeg($this->image); 
    117                 imagepng($this->image); 
    118  
    119                 // Free up resources 
    120                 imagedestroy($this->image); 
    121         } 
    122  
    123         /** 
    124          * Validates the Captcha code against session Captcha code 
    125          * 
    126          * @param   string   captcha code text 
    127          * @return  boolean 
    128          */ 
    129         public static function valid_captcha($str) 
    130         { 
    131                 return (strtoupper($str) === strtoupper(Session::instance()->get('captcha_code'))); 
    132         } 
    133  
    134         /** 
    135          * Creates image resource and allocates some basic colors. 
    136          * If a background image is supplied, the image dimensions are used. 
    137          * 
    138          * @return  void 
    139          */ 
    140         protected function img_create() 
    141         { 
    142                 if ($this->background_image) 
    143                 { 
    144                         // TODO: create from any valid image 
    145                         $this->image = imagecreatefromjpeg($this->background_image); 
    146                         $this->color_white = imagecolorallocate($this->image, 255, 255, 255); 
    147  
    148                         // Get the background image dimensions 
    149                         $this->width  = imagesx($this->image); 
    150                         $this->height = imagesy($this->image); 
    151                 } 
    152                 else 
    153                 { 
    154                         $this->image = imagecreatetruecolor($this->width, $this->height); 
    155                         $this->color_white = imagecolorallocate($this->image, 255, 255, 255); 
    156  
    157                         // Fill the image with a colored gradient (use random colors, but try not to obscure text) 
    158                         $left_color  = array(mt_rand(100,255), 0, 255); 
    159                         $right_color = array(100, 100, mt_rand(100,0)); 
    160                         $this->img_color_gradient($this->image, 0, 0, $this->height, $this->width, $left_color, $right_color); 
    161                 } 
    162         } 
    163  
    164         /** 
    165          * Allocates a background color to image. 
    166          * 
    167          * @param   array  GD image color identifier 
    168          * @return  void 
    169          */ 
    170         protected function img_background($color) 
    171         { 
    172                 imagefill($this->image, 0, 0, $color); 
    173         } 
    174  
    175         /** 
    176          * Draws a very basic Captcha image. 
    177          * Requires only GD. Useful for testing or if you can't use truetype fonts. 
    178          * 
    179          * @return  void 
    180          */ 
    181         protected function basic_captcha() 
    182         { 
    183                 $this->image       = imagecreate($this->width, $this->height); 
    184                 $this->color_white = imagecolorallocate($this->image, 255, 255, 255); 
    185                 $this->color_black = imagecolorallocate($this->image, 0, 0, 0); 
    186  
    187                 imagestring($this->image, 5, 50, 15, $this->captcha_code, $this->color_black); 
    188         } 
    189  
    190         /** 
    191          * Draws the standard Captcha image: 
    192          * Requires GD with freetype and available truetype compatible font files. 
    193          * 
    194          * @param   none 
    195          * @return  void 
    196          */ 
    197         protected function standard_captcha() 
    198         { 
    199                 $this->img_create(); 
    200  
    201                 $font = $this->font_path.$this->font_name; 
    202                 $this->calculate_spacing(); 
    203  
    204                 // Draw each Captcha character with varying attributes 
    205                 for ($i = 0, $strlen = strlen($this->captcha_code); $i < $strlen; $i++) 
    206                 { 
    207                         // Allocate random color, size and rotation attributes to text 
    208                         $text_color = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); 
    209                         $angle = mt_rand(-40, 40); 
    210  
    211                         // Make first char angle inward 
    212                         if ($i === 0) 
    213                         { 
    214                                 $angle = -abs($angle); 
    215                         } 
    216                         // Make last char angle inward 
    217                         if ($i === ($this->num_chars - 1)) 
    218                         { 
    219                                 $angle = abs($angle); 
    220                         } 
    221  
    222                         // Scale the character size on image height 
    223                         $font_size = mt_rand($this->height - 20, $this->height - 12); 
    224                         $char_details = imageftbbox($font_size, $angle, $font, $this->captcha_code[$i], array()); 
    225  
    226                         // Calculate character starting coordinates 
    227                         $iX = $this->spacing / 4 + $i * $this->spacing; 
    228                         $char_height = $char_details[2] - $char_details[5]; 
    229                         $iY = $this->height / 2 + $char_height / 4; 
    230  
    231                         // Write text character to image 
    232                         imagefttext($this->image, $font_size, $angle, $iX, $iY, $text_color, $font, $this->captcha_code[$i], array()); 
    233                 } 
    234         } 
    235  
    236         /** 
    237          * Draws the alphasoup Captcha image: 
    238          * Requires GD with freetype and available truetype compatible font files. 
    239          * 
    240          * @param   none 
    241          * @return  void 
    242          */ 
    243         protected function alphasoup_captcha() 
    244         { 
    245                 $this->img_create(); 
    246                 $font = $this->font_path.$this->font_name; 
    247                 $text_color = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); 
    248                 $color_limit = mt_rand(96, 160); 
    249                 $fill_color = imageColorAllocate($this->image, mt_rand($color_limit, 255), mt_rand($color_limit, 255), mt_rand($color_limit, 255)); 
    250                 imageFilledRectangle($this->image, 0, 0, $this->width, $this->height, $fill_color); 
    251                 $font_width = imageFontWidth(10); 
    252                 $chars = 'ABCDEFGHIJKLMNO'; 
    253  
    254                 for($loop = 0; $loop < 20; $loop++) 
    255                 { 
    256                         $text_color = imageColorAllocate($this->image, mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255)); 
    257                         $char = substr($chars, mt_rand(0, 15), 1); 
    258                         imageTTFtext($this->image, mt_rand(23, 27), mt_rand(160, 200), mt_rand(-10, $this->width + 10), mt_rand(-10, 60), $text_color, $font, $char); 
    259                 } 
    260  
    261                 $this->calculate_spacing(); 
    262                 // Draw each Captcha character with varying attributes 
    263                 for ($i = 0, $strlen = strlen($this->captcha_code); $i < $strlen; $i++) 
    264                 { 
    265                         // Allocate random color, size and rotation attributes to text 
    266                         $text_color = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); 
    267                         $angle = mt_rand(-40, 40); 
    268  
    269                         // Make first char angle inward 
    270                         if ($i === 0) 
    271                         { 
    272                                 $angle = -abs($angle); 
    273                         } 
    274                         // Make last char angle inward 
    275                         if ($i === ($this->num_chars - 1)) 
    276                         { 
    277                                 $angle = abs($angle); 
    278                         } 
    279  
    280                         // Scale the character size on image height 
    281                         $font_size = mt_rand($this->height - 20, $this->height - 12); 
    282                         $char_details = imageftbbox($font_size, $angle, $font, $this->captcha_code[$i], array()); 
    283  
    284                         // Calculate character starting coordinates 
    285                         $iX = $this->spacing / 4 + $i * $this->spacing; 
    286                         $char_height = $char_details[2] - $char_details[5]; 
    287                         $iY = $this->height / 2 + $char_height / 4; 
    288  
    289                         // Write text character to image 
    290                         imagefttext($this->image, $font_size, $angle, $iX, $iY, $text_color, $font, $this->captcha_code[$i], array()); 
    291                 } 
    292  
    293         } 
    294  
    295         /** 
    296          * Draws the math riddle Captcha image. 
    297          * Requires GD with freetype and available truetype compatible font files. 
    298          * 
    299          * @return  void 
    300          */ 
    301         protected function math_captcha() 
    302         { 
    303                 $answer = Session::instance()->get('captcha_code'); 
    304  
    305                 // Convert to numeral 
    306                 $numeral = $this->numerals[substr($answer, -1)]; 
    307  
    308                 // Subtract last digit from answer 
    309                 $number = substr($answer, 0, 2).'0'; 
    310  
    311                 // $number plus $numeral equals $answer 
    312                 $text = $number.' + '.$numeral.' = '; 
    313                 $this->img_create(); 
    314                 $font = $this->font_path.$this->font_name; 
    315  
    316                 // Scale the font size to image height 
    317                 $font_size = $this->height / 3; 
    318                 $text_details = imageftbbox($font_size, 0, $font, $text, array()); 
    319                 $iX = 5; 
    320                 $iY = ($this->height / 2) + 5; 
    321  
    322                 imagefttext($this->image, $font_size, 0, $iX, $iY, $this->color_white, $font, $text, array()); 
    323         } 
    324  
    325         /** 
    326          * Calculates letter spacing for truetype font characters. 
    327          * 
    328          * @return  integer 
    329          */ 
    330         protected function calculate_spacing() 
    331         { 
    332                 return $this->spacing = (int) $this->width / $this->num_chars; 
    333         } 
    334  
    335         /** 
    336          * Fills the image with a colored gradient. 
    337          * 
    338          * @param   resource  gd image resource identifier 
    339          * @param   integer   start X position 
    340          * @param   integer   start Y position 
    341          * @param   integer   height of fill in pixels 
    342          * @param   integer   width of fill in pixels 
    343          * @param   resource  gd image color identifier for left of image 
    344          * @param   resource  gd image color identifier for right of image 
    345          * @return  void 
    346          */ 
    347         protected function img_color_gradient($image, $x1, $y1, $height, $width, $left_color, $right_color) 
    348         { 
    349                 $color0 = ($left_color[0] - $right_color[0]) / $width; 
    350                 $color1 = ($left_color[1] - $right_color[1]) / $width; 
    351                 $color2 = ($left_color[2] - $right_color[2]) / $width; 
    352  
    353                 for ($i = 0; $i <= $width; $i++) 
    354                 { 
    355                         $red   = $left_color[0] - floor($i * $color0); 
    356                         $green = $left_color[1] - floor($i * $color1); 
    357                         $blue  = $left_color[2] - floor($i * $color2); 
    358                         $col   = imagecolorallocate($this->image, $red, $green, $blue); 
    359  
    360                         imageline($this->image, $x1 + $i, $y1, $x1 + $i, $y1 + $height, $col); 
    361                 } 
     149                return $this->render(); 
    362150        } 
    363151