Version: current

      1 <?php
      2    /**
      3     * This code is the work of Edward Eliot.
      4     * I (Adam Walczak) have just integrated it in to SS and did some triliav
      5     * modifications to make the class play nice with it.
      6     */
      7    /***************************************************************/
      8    /* PhpCaptcha - A visual and audio CAPTCHA generation library
      9
     10       Software License Agreement (BSD License)
     11
     12       Copyright (C) 2005-2006, Edward Eliot.
     13       All rights reserved.
     14
     15       Redistribution and use in source and binary forms, with or without
     16       modification, are permitted provided that the following conditions are met:
     17
     18          * Redistributions of source code must retain the above copyright
     19            notice, this list of conditions and the following disclaimer.
     20          * Redistributions in binary form must reproduce the above copyright
     21            notice, this list of conditions and the following disclaimer in the
     22            documentation and/or other materials provided with the distribution.
     23          * Neither the name of Edward Eliot nor the names of its contributors
     24            may be used to endorse or promote products derived from this software
     25            without specific prior written permission of Edward Eliot.
     26
     27       THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND ANY
     28       EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     29       WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     30       DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
     31       DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     32       (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     33       LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     34       ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     35       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     36       SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     37
     38       Last Updated:  18th April 2006                               */
     39    /***************************************************************/
     40
     41    /************************ Documentation ************************/
     42    /*
     43
     44    Documentation is available at http://www.ejeliot.com/pages/2
     45
     46    */
     47    /************************ Default Options **********************/
     48
     49    // start a PHP session - this class uses sessions to store the generated
     50    // code. Comment out if you are calling already from your application
     51
     52    // Adam Walczak: commented to work nice with SS
     53    //session_start();
     54
     55    // class defaults - change to effect globally
     56
     57    define('CAPTCHA_SESSION_ID', 'php_captcha');
     58    define('CAPTCHA_WIDTH', 200); // max 500
     59    define('CAPTCHA_HEIGHT', 50); // max 200
     60    define('CAPTCHA_NUM_CHARS', 5);
     61    define('CAPTCHA_NUM_LINES', 70);
     62    define('CAPTCHA_CHAR_SHADOW', false);
     63    define('CAPTCHA_OWNER_TEXT', '');
     64    define('CAPTCHA_CHAR_SET', ''); // defaults to A-Z
     65    define('CAPTCHA_CASE_INSENSITIVE', true);
     66    define('CAPTCHA_BACKGROUND_IMAGES', '');
     67    define('CAPTCHA_MIN_FONT_SIZE', 16);
     68    define('CAPTCHA_MAX_FONT_SIZE', 24);
     69    define('CAPTCHA_USE_COLOUR', false);
     70    define('CAPTCHA_FILE_TYPE', 'jpeg');
     71    define('CAPTCHA_FLITE_PATH', '/usr/bin/flite');
     72    define('CAPTCHA_AUDIO_PATH', '/tmp/'); // must be writeable by PHP process
     73
     74    /************************ End Default Options **********************/
     75
     76    // don't edit below this line (unless you want to change the class!)
     77
     78    class PhpCaptcha {
     79       var $oImage;
     80       var $aFonts;
     81       var $iWidth;
     82       var $iHeight;
     83       var $iNumChars;
     84       var $iNumLines;
     85       var $iSpacing;
     86       var $bCharShadow;
     87       var $sOwnerText;
     88       var $aCharSet;
     89       var $bCaseInsensitive;
     90       var $vBackgroundImages;
     91       var $iMinFontSize;
     92       var $iMaxFontSize;
     93       var $bUseColour;
     94       var $sFileType;
     95       var $sCode = '';
     96
     97       function PhpCaptcha(
     98          $aFonts, // array of TrueType fonts to use - specify full path
     99          $iWidth = CAPTCHA_WIDTH, // width of image
    100          $iHeight = CAPTCHA_HEIGHT // height of image
    101       ) {
    102          // get parameters
    103          $this->aFonts = $aFonts;
    104          $this->SetNumChars(CAPTCHA_NUM_CHARS);
    105          $this->SetNumLines(CAPTCHA_NUM_LINES);
    106          $this->DisplayShadow(CAPTCHA_CHAR_SHADOW);
    107          $this->SetOwnerText(CAPTCHA_OWNER_TEXT);
    108          $this->SetCharSet(CAPTCHA_CHAR_SET);
    109          $this->CaseInsensitive(CAPTCHA_CASE_INSENSITIVE);
    110          $this->SetBackgroundImages(CAPTCHA_BACKGROUND_IMAGES);
    111          $this->SetMinFontSize(CAPTCHA_MIN_FONT_SIZE);
    112          $this->SetMaxFontSize(CAPTCHA_MAX_FONT_SIZE);
    113          $this->UseColour(CAPTCHA_USE_COLOUR);
    114          $this->SetFileType(CAPTCHA_FILE_TYPE);
    115          $this->SetWidth($iWidth);
    116          $this->SetHeight($iHeight);
    117       }
    118
    119       function CalculateSpacing() {
    120          $this->iSpacing = (int)(($this->iWidth * 0.94) / $this->iNumChars);
    121       }
    122
    123       function SetWidth($iWidth) {
    124          $this->iWidth = $iWidth;
    125          if ($this->iWidth > 500) $this->iWidth = 500; // to prevent perfomance impact
    126          $this->CalculateSpacing();
    127       }
    128
    129       function SetHeight($iHeight) {
    130          $this->iHeight = $iHeight;
    131          if ($this->iHeight > 200) $this->iHeight = 200; // to prevent performance impact
    132       }
    133
    134       function SetNumChars($iNumChars) {
    135          $this->iNumChars = $iNumChars;
    136          $this->CalculateSpacing();
    137       }
    138
    139       function SetNumLines($iNumLines) {
    140          $this->iNumLines = $iNumLines;
    141       }
    142
    143       function DisplayShadow($bCharShadow) {
    144          $this->bCharShadow = $bCharShadow;
    145       }
    146
    147       function SetOwnerText($sOwnerText) {
    148          $this->sOwnerText = $sOwnerText;
    149       }
    150
    151       function SetCharSet($vCharSet) {
    152          // check for input type
    153          if (is_array($vCharSet)) {
    154             $this->aCharSet = $vCharSet;
    155          } else {
    156             if ($vCharSet != '') {
    157                // split items on commas
    158                $aCharSet = explode(',', $vCharSet);
    159
    160                // initialise array
    161                $this->aCharSet = array();
    162
    163                // loop through items
    164                foreach ($aCharSet as $sCurrentItem) {
    165                   // a range should have 3 characters, otherwise is normal character
    166                   if (strlen($sCurrentItem) == 3) {
    167                      // split on range character
    168                      $aRange = explode('-', $sCurrentItem);
    169
    170                      // check for valid range
    171                      if (count($aRange) == 2 && $aRange[0] < $aRange[1]) {
    172                         // create array of characters from range
    173                         $aRange = range($aRange[0], $aRange[1]);
    174
    175                         // add to charset array
    176                         $this->aCharSet = array_merge($this->aCharSet, $aRange);
    177                      }
    178                   } else {
    179                      $this->aCharSet[] = $sCurrentItem;
    180                   }
    181                }
    182             }
    183          }
    184       }
    185
    186       function CaseInsensitive($bCaseInsensitive) {
    187          $this->bCaseInsensitive = $bCaseInsensitive;
    188       }
    189
    190       function SetBackgroundImages($vBackgroundImages) {
    191          $this->vBackgroundImages = $vBackgroundImages;
    192       }
    193
    194       function SetMinFontSize($iMinFontSize) {
    195          $this->iMinFontSize = $iMinFontSize;
    196       }
    197
    198       function SetMaxFontSize($iMaxFontSize) {
    199          $this->iMaxFontSize = $iMaxFontSize;
    200       }
    201
    202       function UseColour($bUseColour) {
    203          $this->bUseColour = $bUseColour;
    204       }
    205
    206       function SetFileType($sFileType) {
    207          // check for valid file type
    208          if (in_array($sFileType, array('gif', 'png', 'jpeg'))) {
    209             $this->sFileType = $sFileType;
    210          } else {
    211             $this->sFileType = 'jpeg';
    212          }
    213       }
    214
    215       function DrawLines() {
    216          for ($i = 0; $i < $this->iNumLines; $i++) {
    217             // allocate colour
    218             if ($this->bUseColour) {
    219                $iLineColour = imagecolorallocate($this->oImage, rand(100, 250), rand(100, 250), rand(100, 250));
    220             } else {
    221                $iRandColour = rand(100, 250);
    222                $iLineColour = imagecolorallocate($this->oImage, $iRandColour, $iRandColour, $iRandColour);
    223             }
    224
    225             // draw line
    226             imageline($this->oImage, rand(0, $this->iWidth), rand(0, $this->iHeight), rand(0, $this->iWidth), rand(0, $this->iHeight), $iLineColour);
    227          }
    228       }
    229
    230       function DrawOwnerText() {
    231          // allocate owner text colour
    232          $iBlack = imagecolorallocate($this->oImage, 0, 0, 0);
    233          // get height of selected font
    234          $iOwnerTextHeight = imagefontheight(2);
    235          // calculate overall height
    236          $iLineHeight = $this->iHeight - $iOwnerTextHeight - 4;
    237
    238          // draw line above text to separate from CAPTCHA
    239          imageline($this->oImage, 0, $iLineHeight, $this->iWidth, $iLineHeight, $iBlack);
    240
    241          // write owner text
    242          imagestring($this->oImage, 2, 3, $this->iHeight - $iOwnerTextHeight - 3, $this->sOwnerText, $iBlack);
    243
    244          // reduce available height for drawing CAPTCHA
    245          $this->iHeight = $this->iHeight - $iOwnerTextHeight - 5;
    246       }
    247
    248       function GenerateCode() {
    249          // reset code
    250          $this->sCode = '';
    251
    252          // loop through and generate the code letter by letter
    253          for ($i = 0; $i < $this->iNumChars; $i++) {
    254             if (count($this->aCharSet) > 0) {
    255                // select random character and add to code string
    256                $this->sCode .= $this->aCharSet[array_rand($this->aCharSet)];
    257             } else {
    258                // select random character and add to code string
    259                $this->sCode .= chr(rand(65, 90));
    260             }
    261          }
    262
    263          // save code in session variable
    264          if ($this->bCaseInsensitive) {
    265             $_SESSION[CAPTCHA_SESSION_ID] = strtoupper($this->sCode);
    266          } else {
    267             $_SESSION[CAPTCHA_SESSION_ID] = $this->sCode;
    268          }
    269       }
    270
    271       function DrawCharacters() {
    272          // loop through and write out selected number of characters
    273          for ($i = 0; $i < strlen($this->sCode); $i++) {
    274             // select random font
    275             $sCurrentFont = $this->aFonts[array_rand($this->aFonts)];
    276
    277             // select random colour
    278             if ($this->bUseColour) {
    279                $iTextColour = imagecolorallocate($this->oImage, rand(0, 100), rand(0, 100), rand(0, 100));
    280
    281                if ($this->bCharShadow) {
    282                   // shadow colour
    283                   $iShadowColour = imagecolorallocate($this->oImage, rand(0, 100), rand(0, 100), rand(0, 100));
    284                }
    285             } else {
    286                $iRandColour = rand(0, 100);
    287                $iTextColour = imagecolorallocate($this->oImage, $iRandColour, $iRandColour, $iRandColour);
    288
    289                if ($this->bCharShadow) {
    290                   // shadow colour
    291                   $iRandColour = rand(0, 100);
    292                   $iShadowColour = imagecolorallocate($this->oImage, $iRandColour, $iRandColour, $iRandColour);
    293                }
    294             }
    295
    296             // select random font size
    297             $iFontSize = rand($this->iMinFontSize, $this->iMaxFontSize);
    298
    299             // select random angle
    300             $iAngle = rand(-30, 30);
    301
    302             // get dimensions of character in selected font and text size
    303             $aCharDetails = imageftbbox($iFontSize, $iAngle, $sCurrentFont, $this->sCode[$i], array());
    304
    305             // calculate character starting coordinates
    306             $iCharRight = max($aCharDetails[2],$aCharDetails[4],$aCharDetails[6]) - $aCharDetails[0];
    307             $iCharLeft = min($aCharDetails[2],$aCharDetails[4],$aCharDetails[6]) - $aCharDetails[0];
    308             $iX = $this->iWidth * 0.04 + $i * $this->iSpacing;
    309             $xPos = ($i + 1) * $this->iSpacing;
    310             if ($iX + $iCharRight > $xPos)
    311                 $iX = $xPos - $this->iSpacing;
    312 //            if ($iX + $iCharLeft < 0)
    313 //                $iX = -$iCharLeft + 6;
    314             if ($iX + $iCharLeft < 0)
    315                 $iX = $this->iSpacing/2;
    316
    317             $iCharHeight = $aCharDetails[3] - $aCharDetails[5];
    318 //            Debug::dump($aCharDetails);
    319             $iY = 4+$this->iHeight / 2 + $iCharHeight / 4;
    320 //            Debug::dump($iFontSize.'|'.$iAngle.'|'.$iX.'|'.$iY.'|'.$iTextColour.'|'.$sCurrentFont.'|'.$this->sCode[$i]); exit;
    321
    322             // write text to image
    323             imagefttext($this->oImage, $iFontSize, $iAngle, $iX, $iY, $iTextColour, $sCurrentFont, $this->sCode[$i], array());
    324
    325             if ($this->bCharShadow) {
    326                $iOffsetAngle = rand(-30, 30);
    327
    328                $iRandOffsetX = rand(-5, 5);
    329                $iRandOffsetY = rand(-5, 5);
    330
    331                imagefttext($this->oImage, $iFontSize, $iOffsetAngle, $iX + $iRandOffsetX, $iY + $iRandOffsetY, $iShadowColour, $sCurrentFont, $this->sCode[$i], array());
    332             }
    333          }
    334 //die();
    335       }
    336
    337       function WriteFile($sFilename) {
    338          if ($sFilename == '') {
    339             // tell browser that data is jpeg
    340             header("Content-type: image/$this->sFileType");
    341          }
    342
    343          switch ($this->sFileType) {
    344             case 'gif':
    345                $sFilename != '' ? imagegif($this->oImage, $sFilename) : imagegif($this->oImage);
    346                break;
    347             case 'png':
    348                $sFilename != '' ? imagepng($this->oImage, $sFilename) : imagepng($this->oImage);
    349                break;
    350             default:
    351                $sFilename != '' ? imagejpeg($this->oImage, $sFilename) : imagejpeg($this->oImage);
    352          }
    353       }
    354
    355       function Create($sFilename = '') {
    356          // check for required gd functions
    357          if (!function_exists('imagecreate') || !function_exists("image$this->sFileType") || ($this->vBackgroundImages != '' && !function_exists('imagecreatetruecolor'))) {
    358             return false;
    359          }
    360
    361          // get background image if specified and copy to CAPTCHA
    362          if (is_array($this->vBackgroundImages) || $this->vBackgroundImages != '') {
    363             // create new image
    364             $this->oImage = imagecreatetruecolor($this->iWidth, $this->iHeight);
    365
    366             // create background image
    367             if (is_array($this->vBackgroundImages)) {
    368                $iRandImage = array_rand($this->vBackgroundImages);
    369                $oBackgroundImage = imagecreatefromjpeg($this->vBackgroundImages[$iRandImage]);
    370             } else {
    371                $oBackgroundImage = imagecreatefromjpeg($this->vBackgroundImages);
    372             }
    373
    374             // copy background image
    375             imagecopy($this->oImage, $oBackgroundImage, 0, 0, 0, 0, $this->iWidth, $this->iHeight);
    376
    377             // free memory used to create background image
    378             imagedestroy($oBackgroundImage);
    379          } else {
    380             // create new image
    381             $this->oImage = imagecreate($this->iWidth, $this->iHeight);
    382          }
    383
    384          // allocate white background colour
    385          imagecolorallocate($this->oImage, 255,255, 255);
    386
    387          // check for owner text
    388          if ($this->sOwnerText != '') {
    389             $this->DrawOwnerText();
    390          }
    391
    392          // check for background image before drawing lines
    393          if (!is_array($this->vBackgroundImages) && $this->vBackgroundImages == '') {
    394             $this->DrawLines();
    395          }
    396
    397          $this->GenerateCode();
    398          $this->DrawCharacters();
    399
    400          // write out image to file or browser
    401          $this->WriteFile($sFilename);
    402
    403          // free memory used in creating image
    404          imagedestroy($this->oImage);
    405
    406          return true;
    407       }
    408
    409       // call this method statically
    410       function Validate($sUserCode, $bCaseInsensitive = true) {
    411          if ($bCaseInsensitive) {
    412             $sUserCode = strtoupper($sUserCode);
    413          }
    414
    415          if (!empty($_SESSION[CAPTCHA_SESSION_ID]) &&
    416                 ($sUserCode == $_SESSION[CAPTCHA_SESSION_ID] || substr($_SESSION[CAPTCHA_SESSION_ID],1) == $sUserCode)
    417             ) {
    418             // clear to prevent re-use
    419             unset($_SESSION[CAPTCHA_SESSION_ID]);
    420
    421             return true;
    422          }
    423
    424          return false;
    425       }
    426    }
    427
    428    // this class will only work correctly if a visual CAPTCHA has been created first using PhpCaptcha
    429    class AudioPhpCaptcha {
    430       var $sFlitePath;
    431       var $sAudioPath;
    432       var $sCode;
    433
    434       function AudioPhpCaptcha(
    435          $sFlitePath = CAPTCHA_FLITE_PATH, // path to flite binary
    436          $sAudioPath = CAPTCHA_AUDIO_PATH // the location to temporarily store the generated audio CAPTCHA
    437       ) {
    438          $this->SetFlitePath($sFlitePath);
    439          $this->SetAudioPath($sAudioPath);
    440
    441          // retrieve code if already set by previous instance of visual PhpCaptcha
    442          if (isset($_SESSION[CAPTCHA_SESSION_ID])) {
    443             $this->sCode = $_SESSION[CAPTCHA_SESSION_ID];
    444          }
    445       }
    446
    447       function SetFlitePath($sFlitePath) {
    448          $this->sFlitePath = $sFlitePath;
    449       }
    450
    451       function SetAudioPath($sAudioPath) {
    452          $this->sAudioPath = $sAudioPath;
    453       }
    454
    455       function Mask($sText) {
    456          $iLength = strlen($sText);
    457
    458          // loop through characters in code and format
    459          $sFormattedText = '';
    460          for ($i = 0; $i < $iLength; $i++) {
    461             // comma separate all but first and last characters
    462             if ($i > 0 && $i < $iLength - 1) {
    463                $sFormattedText .= ', ';
    464             } elseif ($i == $iLength - 1) { // precede last character with "and"
    465                $sFormattedText .= ' and ';
    466             }
    467             $sFormattedText .= $sText[$i];
    468          }
    469
    470          $aPhrases = array(
    471             "The %1\$s characters are as follows: %2\$s",
    472             "%2\$s, are the %1\$s letters",
    473             "Here are the %1\$s characters: %2\$s",
    474             "%1\$s characters are: %2\$s",
    475             "%1\$s letters: %2\$s"
    476          );
    477
    478          $iPhrase = array_rand($aPhrases);
    479
    480          return sprintf($aPhrases[$iPhrase], $iLength, $sFormattedText);
    481       }
    482
    483       function Create() {
    484          $sText = $this->Mask($this->sCode);
    485          $sFile = md5($this->sCode.time());
    486
    487          // create file with flite
    488          shell_exec("$this->sFlitePath -t \"$sText\" -o $this->sAudioPath$sFile.wav");
    489
    490          // set headers
    491          header('Content-type: audio/x-wav');
    492          header("Content-Disposition: attachment;filename=$sFile.wav");
    493
    494          // output to browser
    495          echo file_get_contents("$this->sAudioPath$sFile.wav");
    496
    497          // delete temporary file
    498          @unlink("$this->sAudioPath$sFile.wav");
    499       }
    500    }
    501
    502    // example sub class
    503    class PhpCaptchaColour extends PhpCaptcha {
    504       function PhpCaptchaColour($aFonts, $iWidth = CAPTCHA_WIDTH, $iHeight = CAPTCHA_HEIGHT) {
    505          // call parent constructor
    506          parent::PhpCaptcha($aFonts, $iWidth, $iHeight);
    507
    508          // set options
    509          $this->UseColour(true);
    510       }
    511    }
    512 ?>
    513

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -