Changeset 3015

Show
Ignore:
Timestamp:
07/09/08 15:45:47 (7 weeks 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. :)
Location:
trunk/system
Files:
4 modified

Legend:

Unmodified
Added
Removed
  • trunk/system/config/captcha.php

    r2652 r3015  
    11<?php defined('SYSPATH') or die('No direct access allowed.'); 
    22/** 
    3  * @package  Captcha 
     3 * @package  Core 
    44 * 
    5  * Configure the Captcha 
    6  * Custom styles can be added by extending the Captcha class 
     5 * Captcha configuration is defined in groups which allows you to easily switch 
     6 * between different Captcha settings for different forms on your website. 
     7 * Note: all groups inherit and overwrite the default group. 
     8 * 
     9 * Group Options: 
     10 *  style           - Style driver 
     11 *  width           - Width of the Captcha image 
     12 *  height          - Height of the Captcha image 
     13 *  complexity      - Difficulty of the Captcha, concrete usage depends on chosen style 
     14 *  background_path - Path to folder in which background image reside 
     15 *  background_file - Image file name 
     16 *  font_path       - Path to folder in which fonts reside 
     17 *  font_file       - Font file name 
    718 */ 
    8  
    9 /** 
    10  * Width and height of the Captcha image. 
    11  * These settings are ignored if using a background image. 
    12  */ 
    13 $config['width']  = 150; 
    14 $config['height'] = 50; 
    15  
    16 /** 
    17  * Captcha style to use. Default is 'basic' and is only for testing as it 
    18  * does not require any truetype fonts installed. 
    19  * 'standard' is the recommended style. A font must be supplied. A background 
    20  * image is optional. 
    21  * 'alphasoup' is an alternative style. A font must be supplied. 
    22  * 'math' is a 'solve the question' style. 
    23  * A font must be supplied. A background image is optional. 
    24  * Custom styles can be added easily by extending the library. 
    25  */ 
    26 $config['style'] = 'standard'; 
    27  
    28 /** 
    29  * Number of characters to use for the Captcha (4 or 5 recommended). 
    30  * This setting is ignored if using style 'math'. 
    31  */ 
    32 $config['num_chars'] = 4; 
    33  
    34 /** 
    35  * Path to font files. Example: 'application/fonts/'. 
    36  * If using 'standard' style, you must supply a valid truetype font file. 
    37  */ 
    38 $config['font_path'] = SYSPATH.'fonts/'; 
    39  
    40 /** 
    41  * Name of the font, with the file extension. Case sensitive. 
    42  */ 
    43 $config['font_name'] = 'DejaVuSerif.ttf'; 
    44  
    45 /** 
    46  * Background image. Example: 'application/images/pattern.jpg'. 
    47  * The dimensions of this image will be used. 
    48  */ 
    49 $config['background_image'] = ''; 
     19$config['default'] = array 
     20( 
     21    'style'           => 'riddle', 
     22    'width'           => 150, 
     23    'height'          => 50, 
     24    'complexity'      => 4, 
     25    'background_path' => '', 
     26    'background_file' => '', 
     27    'font_path'       => SYSPATH.'fonts/', 
     28    'font_file'       => 'DejaVuSerif.ttf', 
     29); 
  • trunk/system/controllers/captcha.php

    r3010 r3015  
    11<?php defined('SYSPATH') or die('No direct script access.'); 
    22/** 
    3  * Allows a CAPTCHA image to be displayed dynamically. 
    4  * Captcha library is called to output the image. 
    5  * 
    6  * 
    7  * Usage: Call the captcha controller from a view. 
    8  *        `echo html::image(url::site().'captcha');` 
     3 * Outputs the dynamic Captcha image. 
     4 * Usage: Call the Captcha controller from a view, e.g. 
     5 *        <img src="<?php echo url::site('captcha') ?>" /> 
    96 * 
    107 * $Id$ 
    118 * 
    12  * @package    Core 
     9 * @package    Captcha 
    1310 * @author     Kohana Team 
    1411 * @copyright  (c) 2007-2008 Kohana Team 
     
    1714class Captcha_Controller extends Controller { 
    1815 
    19     public $session; 
    20     public $captcha; 
    21  
    22     protected $captcha_code; 
    23  
    2416    public function index() 
    2517    { 
    26         $this->session = Session::instance(); 
    27         $this->captcha = new Captcha; 
    28  
    29         // Create and store a random captcha string 
    30         $this->captcha_code = $this->create_code(); 
    31         $this->captcha->set_code($this->captcha_code); 
    32         $this->session->set('captcha_code', $this->captcha_code); 
    33  
    34         // Output the image 
    35         $this->captcha->render(); 
    36     } 
    37  
    38     private function create_code() 
    39     { 
    40         if (Config::item('captcha.style') === 'math') 
    41         { 
    42             $code = (string) mt_rand(101, 991); 
    43         } 
    44         else 
    45         { 
    46             $code = text::random('distinct', Config::item('captcha.num_chars')); 
    47         } 
    48  
    49         return $code; 
     18        // Output the Captcha challenge resource (no html) 
     19        $captcha = new Captcha; 
     20        $captcha->render(FALSE); 
    5021    } 
    5122 
  • 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 
  • trunk/system/libraries/Pagination.php

    r2935 r3015  
    4747 
    4848    /** 
    49      * Constructs the Pagination object. 
     49     * Constructs a new Pagination object. 
    5050     * 
    5151     * @param   array  configuration settings 
     
    6969     * Sets config values. 
    7070     * 
     71     * @throws  Kohana_Exception 
    7172     * @param   array  configuration settings 
    7273     * @return  void 
     
    198199 
    199200    /** 
    200      * Magically converts pagination object to string. 
     201     * Magically converts Pagination object to string. 
    201202     * 
    202203     * @return  string  pagination html