[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/extensions/ConfirmEdit/ -> FancyCaptcha.class.php (source)

   1  <?php
   2  
   3  class FancyCaptcha extends SimpleCaptcha {
   4      /**
   5       * @return FileBackend
   6       */
   7  	public function getBackend() {
   8          global $wgCaptchaFileBackend, $wgCaptchaDirectory;
   9  
  10          if ( $wgCaptchaFileBackend ) {
  11              return FileBackendGroup::singleton()->get( $wgCaptchaFileBackend );
  12          } else {
  13              static $backend = null;
  14              if ( !$backend ) {
  15                  $backend = new FSFileBackend( array(
  16                      'name'           => 'captcha-backend',
  17                      'wikiId'     => wfWikiId(),
  18                      'lockManager'    => new NullLockManager( array() ),
  19                      'containerPaths' => array( 'captcha-render' => $wgCaptchaDirectory ),
  20                      'fileMode'       => 777
  21                  ) );
  22              }
  23              return $backend;
  24          }
  25      }
  26  
  27      /**
  28       * @return integer Estimate of the number of captchas files
  29       */
  30  	public function estimateCaptchaCount() {
  31          global $wgCaptchaDirectoryLevels;
  32  
  33          $factor = 1;
  34          $sampleDir = $this->getBackend()->getRootStoragePath() . '/captcha-render';
  35          if ( $wgCaptchaDirectoryLevels >= 1 ) { // 1/16 sample if 16 shards
  36              $sampleDir .= '/' . dechex( mt_rand( 0, 15 ) );
  37              $factor = 16;
  38          }
  39          if ( $wgCaptchaDirectoryLevels >= 3 ) { // 1/256 sample if 4096 shards
  40              $sampleDir .= '/' . dechex( mt_rand( 0, 15 ) );
  41              $factor = 256;
  42          }
  43  
  44          $count = 0;
  45          foreach ( $this->getBackend()->getFileList( array( 'dir' => $sampleDir ) ) as $file ) {
  46              ++$count;
  47          }
  48  
  49          return ( $count * $factor );
  50      }
  51  
  52      /**
  53       * Check if the submitted form matches the captcha session data provided
  54       * by the plugin when the form was generated.
  55       *
  56       * @param string $answer
  57       * @param array $info
  58       * @return bool
  59       */
  60  	function keyMatch( $answer, $info ) {
  61          global $wgCaptchaSecret;
  62  
  63          $digest = $wgCaptchaSecret . $info['salt'] . $answer . $wgCaptchaSecret . $info['salt'];
  64          $answerHash = substr( md5( $digest ), 0, 16 );
  65  
  66          if ( $answerHash == $info['hash'] ) {
  67              wfDebug( "FancyCaptcha: answer hash matches expected {$info['hash']}\n" );
  68              return true;
  69          } else {
  70              wfDebug( "FancyCaptcha: answer hashes to $answerHash, expected {$info['hash']}\n" );
  71              return false;
  72          }
  73      }
  74  
  75  	function addCaptchaAPI( &$resultArr ) {
  76          $info = $this->pickImage();
  77          if ( !$info ) {
  78              $resultArr['captcha']['error'] = 'Out of images';
  79              return;
  80          }
  81          $index = $this->storeCaptcha( $info );
  82          $title = SpecialPage::getTitleFor( 'Captcha', 'image' );
  83          $resultArr['captcha']['type'] = 'image';
  84          $resultArr['captcha']['mime'] = 'image/png';
  85          $resultArr['captcha']['id'] = $index;
  86          $resultArr['captcha']['url'] = $title->getLocalUrl( 'wpCaptchaId=' . urlencode( $index ) );
  87      }
  88  
  89      /**
  90       * Insert the captcha prompt into the edit form.
  91       */
  92  	function getForm() {
  93          global $wgOut, $wgExtensionAssetsPath, $wgEnableAPI;
  94  
  95          // Uses addModuleStyles so it is loaded when JS is disabled.
  96          $wgOut->addModuleStyles( 'ext.confirmEdit.fancyCaptcha.styles' );
  97  
  98          $title = SpecialPage::getTitleFor( 'Captcha', 'image' );
  99          $index = $this->getCaptchaIndex();
 100  
 101          if ( $wgEnableAPI ) {
 102              // Loaded only if JS is enabled
 103              $wgOut->addModules( 'ext.confirmEdit.fancyCaptcha' );
 104  
 105              $captchaReload = Html::element(
 106                  'small',
 107                  array(
 108                      'class' => 'confirmedit-captcha-reload fancycaptcha-reload'
 109                  ),
 110                  wfMessage( 'fancycaptcha-reload-text' )->text()
 111              );
 112          } else {
 113              $captchaReload = '';
 114          }
 115  
 116          return "<div class='fancycaptcha-wrapper'><div class='fancycaptcha-image-container'>" .
 117              Html::element( 'img', array(
 118                      'class'  => 'fancycaptcha-image',
 119                      'src'    => $title->getLocalUrl( 'wpCaptchaId=' . urlencode( $index ) ),
 120                      'alt'    => ''
 121                  )
 122              ) .
 123              $captchaReload .
 124              "</div>\n" .
 125              '<p>' .
 126              Html::element( 'label', array(
 127                      'for' => 'wpCaptchaWord',
 128                  ),
 129                  parent::getMessage( 'label' ) . wfMessage( 'colon-separator' )->text()
 130              ) .
 131              Html::element( 'input', array(
 132                      'name' => 'wpCaptchaWord',
 133                      'class' => 'mw-ui-input',
 134                      'id'   => 'wpCaptchaWord',
 135                      'type' => 'text',
 136                      'size' => '12',  // max_length in captcha.py plus fudge factor
 137                      'autocomplete' => 'off',
 138                      'autocorrect' => 'off',
 139                      'autocapitalize' => 'off',
 140                      'required' => 'required',
 141                      'tabindex' => 1
 142                  )
 143              ) . // tab in before the edit textarea
 144              Html::element( 'input', array(
 145                      'type'  => 'hidden',
 146                      'name'  => 'wpCaptchaId',
 147                      'id'    => 'wpCaptchaId',
 148                      'value' => $index
 149                  )
 150              ) .
 151              "</p>\n" .
 152              "</div>\n";;
 153      }
 154  
 155      /**
 156       * Get captcha index key
 157       * @return string captcha ID key
 158       */
 159  	function getCaptchaIndex() {
 160          $info = $this->pickImage();
 161          if ( !$info ) {
 162              throw new MWException( "Ran out of captcha images" );
 163          }
 164  
 165          // Generate a random key for use of this captcha image in this session.
 166          // This is needed so multiple edits in separate tabs or windows can
 167          // go through without extra pain.
 168          $index = $this->storeCaptcha( $info );
 169  
 170          return $index;
 171      }
 172  
 173      /**
 174       * Select a previously generated captcha image from the queue.
 175       * @return mixed tuple of (salt key, text hash) or false if no image to find
 176       */
 177  	protected function pickImage() {
 178          global $wgCaptchaDirectoryLevels;
 179  
 180          $lockouts = 0; // number of times another process claimed a file before this one
 181          $baseDir = $this->getBackend()->getRootStoragePath() . '/captcha-render';
 182          return $this->pickImageDir( $baseDir, $wgCaptchaDirectoryLevels, $lockouts );
 183      }
 184  
 185      /**
 186       * @param $directory string
 187       * @param $levels integer
 188       * @param $lockouts integer
 189       * @return Array|bool
 190       */
 191  	protected function pickImageDir( $directory, $levels, &$lockouts ) {
 192          global $wgMemc;
 193  
 194          if ( $levels <= 0 ) { // $directory has regular files
 195              return $this->pickImageFromDir( $directory, $lockouts );
 196          }
 197  
 198          $backend = $this->getBackend();
 199  
 200          $key  = "fancycaptcha:dirlist:{$backend->getWikiId()}:" . sha1( $directory );
 201          $dirs = $wgMemc->get( $key ); // check cache
 202          if ( !is_array( $dirs ) || !count( $dirs ) ) { // cache miss
 203              $dirs = array(); // subdirs actually present...
 204              foreach ( $backend->getTopDirectoryList( array( 'dir' => $directory ) ) as $entry ) {
 205                  if ( ctype_xdigit( $entry ) && strlen( $entry ) == 1 ) {
 206                      $dirs[] = $entry;
 207                  }
 208              }
 209              wfDebug( "Cache miss for $directory subdirectory listing.\n" );
 210              if ( count( $dirs ) ) {
 211                  $wgMemc->set( $key, $dirs, 86400 );
 212              }
 213          }
 214  
 215          if ( !count( $dirs ) ) {
 216              // Remove this directory if empty so callers don't keep looking here
 217              $backend->clean( array( 'dir' => $directory ) );
 218              return false; // none found
 219          }
 220  
 221          $place = mt_rand( 0, count( $dirs ) - 1 ); // pick a random subdir
 222          // In case all dirs are not filled, cycle through next digits...
 223          for ( $j = 0; $j < count( $dirs ); $j++ ) {
 224              $char = $dirs[( $place + $j ) % count( $dirs )];
 225              $info = $this->pickImageDir( "$directory/$char", $levels - 1, $lockouts );
 226              if ( $info ) {
 227                  return $info; // found a captcha
 228              } else {
 229                  wfDebug( "Could not find captcha in $directory.\n" );
 230                  $wgMemc->delete( $key ); // files changed on disk?
 231              }
 232          }
 233  
 234          return false; // didn't find any images in this directory... empty?
 235      }
 236  
 237      /**
 238       * @param $directory string
 239       * @param $lockouts integer
 240       * @return Array|bool
 241       */
 242  	protected function pickImageFromDir( $directory, &$lockouts ) {
 243          global $wgMemc;
 244  
 245          $backend = $this->getBackend();
 246  
 247          $key   = "fancycaptcha:filelist:{$backend->getWikiId()}:" . sha1( $directory );
 248          $files = $wgMemc->get( $key ); // check cache
 249          if ( !is_array( $files ) || !count( $files ) ) { // cache miss
 250              $files = array(); // captcha files
 251              foreach ( $backend->getTopFileList( array( 'dir' => $directory ) ) as $entry ) {
 252                  $files[] = $entry;
 253                  if ( count( $files ) >= 500 ) { // sanity
 254                      wfDebug( 'Skipping some captchas; $wgCaptchaDirectoryLevels set too low?.' );
 255                      break;
 256                  }
 257              }
 258              if ( count( $files ) ) {
 259                  $wgMemc->set( $key, $files, 86400 );
 260              }
 261              wfDebug( "Cache miss for $directory captcha listing.\n" );
 262          }
 263  
 264          if ( !count( $files ) ) {
 265              // Remove this directory if empty so callers don't keep looking here
 266              $backend->clean( array( 'dir' => $directory ) );
 267              return false;
 268          }
 269  
 270          $info = $this->pickImageFromList( $directory, $files, $lockouts );
 271          if ( !$info ) {
 272              wfDebug( "Could not find captcha in $directory.\n" );
 273              $wgMemc->delete( $key ); // files changed on disk?
 274          }
 275  
 276          return $info;
 277      }
 278  
 279      /**
 280       * @param $directory string
 281       * @param $files array
 282       * @param $lockouts integer
 283       * @return boolean
 284       */
 285  	protected function pickImageFromList( $directory, array $files, &$lockouts ) {
 286          global $wgMemc, $wgCaptchaDeleteOnSolve;
 287  
 288          if ( !count( $files ) ) {
 289              return false; // none found
 290          }
 291  
 292          $backend  = $this->getBackend();
 293          $place    = mt_rand( 0, count( $files ) - 1 ); // pick a random file
 294          $misses   = 0; // number of files in listing that don't actually exist
 295          for ( $j = 0; $j < count( $files ); $j++ ) {
 296              $entry = $files[( $place + $j ) % count( $files )];
 297              if ( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $entry, $matches ) ) {
 298                  if ( $wgCaptchaDeleteOnSolve ) { // captcha will be deleted when solved
 299                      $key = "fancycaptcha:filelock:{$backend->getWikiId()}:" . sha1( $entry );
 300                      // Try to claim this captcha for 10 minutes (for the user to solve)...
 301                      if ( ++$lockouts <= 10 && !$wgMemc->add( $key, '1', 600 ) ) {
 302                          continue; // could not acquire (skip it to avoid race conditions)
 303                      }
 304                  }
 305                  if ( !$backend->fileExists( array( 'src' => "$directory/$entry" ) ) ) {
 306                      if ( ++$misses >= 5 ) { // too many files in the listing don't exist
 307                          break; // listing cache too stale? break out so it will be cleared
 308                      }
 309                      continue; // try next file
 310                  }
 311                  return array(
 312                      'salt'   => $matches[1],
 313                      'hash'   => $matches[2],
 314                      'viewed' => false,
 315                  );
 316              }
 317          }
 318  
 319          return false; // none found
 320      }
 321  
 322  	function showImage() {
 323          global $wgOut;
 324  
 325          $wgOut->disable();
 326  
 327          $info = $this->retrieveCaptcha();
 328          if ( $info ) {
 329              $timestamp = new MWTimestamp();
 330              $info['viewed'] = $timestamp->getTimestamp();
 331              $this->storeCaptcha( $info );
 332  
 333              $salt = $info['salt'];
 334              $hash = $info['hash'];
 335  
 336              return $this->getBackend()->streamFile( array(
 337                  'src'     => $this->imagePath( $salt, $hash ),
 338                  'headers' => array( "Cache-Control: private, s-maxage=0, max-age=3600" )
 339              ) )->isOK();
 340          }
 341  
 342          wfHttpError( 500, 'Internal Error', 'Requested bogus captcha image' );
 343          return false;
 344      }
 345  
 346      /**
 347       * @param $salt string
 348       * @param $hash string
 349       * @return string
 350       */
 351  	public function imagePath( $salt, $hash ) {
 352          global $wgCaptchaDirectoryLevels;
 353  
 354          $file = $this->getBackend()->getRootStoragePath() . '/captcha-render/';
 355          for ( $i = 0; $i < $wgCaptchaDirectoryLevels; $i++ ) {
 356              $file .= $hash{ $i } . '/';
 357          }
 358          $file .= "image_{$salt}_{$hash}.png";
 359  
 360          return $file;
 361      }
 362  
 363      /**
 364       * @param $basename string
 365       * @return Array (salt, hash)
 366       * @throws MWException
 367       */
 368  	public function hashFromImageName( $basename ) {
 369          if ( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $basename, $matches ) ) {
 370              return array( $matches[1], $matches[2] );
 371          } else {
 372              throw new MWException( "Invalid filename '$basename'.\n" );
 373          }
 374      }
 375  
 376      /**
 377       * Show a message asking the user to enter a captcha on edit
 378       * The result will be treated as wiki text
 379       *
 380       * @param $action string Action being performed
 381       * @return string
 382       */
 383  	function getMessage( $action ) {
 384          $name = 'fancycaptcha-' . $action;
 385          $text = wfMessage( $name )->text();
 386          # Obtain a more tailored message, if possible, otherwise, fall back to
 387          # the default for edits
 388          return wfMessage( $name, $text )->isDisabled() ?
 389              wfMessage( 'fancycaptcha-edit' )->text() : $text;
 390      }
 391  
 392      /**
 393       * Delete a solved captcha image, if $wgCaptchaDeleteOnSolve is true.
 394       */
 395  	function passCaptcha() {
 396          global $wgCaptchaDeleteOnSolve;
 397  
 398          $info = $this->retrieveCaptcha(); // get the captcha info before it gets deleted
 399          $pass = parent::passCaptcha();
 400  
 401          if ( $pass && $wgCaptchaDeleteOnSolve ) {
 402              $this->getBackend()->quickDelete( array(
 403                  'src' => $this->imagePath( $info['salt'], $info['hash'] )
 404              ) );
 405          }
 406  
 407          return $pass;
 408      }
 409  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1