[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |