1<?php 2/** 3 * Generate fancy captchas using a python script and copy them into storage. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @author Aaron Schulz 22 * @ingroup Maintenance 23 */ 24if ( getenv( 'MW_INSTALL_PATH' ) ) { 25 $IP = getenv( 'MW_INSTALL_PATH' ); 26} else { 27 $IP = __DIR__ . '/../../..'; 28} 29 30require_once "$IP/maintenance/Maintenance.php"; 31 32/** 33 * Maintenance script to generate fancy captchas using a python script and copy them into storage. 34 * 35 * @ingroup Maintenance 36 */ 37class GenerateFancyCaptchas extends Maintenance { 38 public function __construct() { 39 parent::__construct(); 40 41 // See captcha.py for argument usage 42 $this->addOption( "wordlist", 'A list of words', true, true ); 43 $this->addOption( "font", "The font to use", true, true ); 44 $this->addOption( "font-size", "The font size ", false, true ); 45 $this->addOption( "blacklist", "A blacklist of words that should not be used", false, true ); 46 $this->addOption( "fill", "Fill the captcha container to N files", true, true ); 47 $this->addOption( 48 "verbose", 49 "Show debugging information when running the captcha python script" 50 ); 51 $this->addOption( 52 "oldcaptcha", 53 "Whether to use captcha-old.py which doesn't have OCR fighting improvements" 54 ); 55 $this->addOption( "delete", "Deletes all the old captchas" ); 56 $this->addOption( "threads", "The number of threads to use to generate the images", 57 false, true ); 58 $this->addDescription( "Generate new fancy captchas and move them into storage" ); 59 60 $this->requireExtension( "FancyCaptcha" ); 61 } 62 63 public function execute() { 64 global $wgCaptchaSecret, $wgCaptchaDirectoryLevels; 65 66 $totalTime = -microtime( true ); 67 68 $instance = ConfirmEditHooks::getInstance(); 69 if ( !( $instance instanceof FancyCaptcha ) ) { 70 $this->fatalError( "\$wgCaptchaClass is not FancyCaptcha.\n", 1 ); 71 } 72 $backend = $instance->getBackend(); 73 74 $deleteOldCaptchas = $this->getOption( 'delete' ); 75 76 $countGen = (int)$this->getOption( 'fill' ); 77 if ( !$deleteOldCaptchas ) { 78 $countAct = $instance->getCaptchaCount(); 79 $this->output( "Current number of captchas is $countAct.\n" ); 80 $countGen -= $countAct; 81 } 82 83 if ( $countGen <= 0 ) { 84 $this->output( "No need to generate anymore captchas.\n" ); 85 return; 86 } 87 88 $tmpDir = wfTempDir() . '/mw-fancycaptcha-' . time() . '-' . wfRandomString( 6 ); 89 if ( !wfMkdirParents( $tmpDir ) ) { 90 $this->fatalError( "Could not create temp directory.\n", 1 ); 91 } 92 93 $captchaScript = 'captcha.py'; 94 95 if ( $this->hasOption( 'oldcaptcha' ) ) { 96 $captchaScript = 'captcha-old.py'; 97 } 98 99 $cmd = sprintf( "python %s --key %s --output %s --count %s --dirs %s", 100 wfEscapeShellArg( dirname( __DIR__ ) . '/' . $captchaScript ), 101 wfEscapeShellArg( $wgCaptchaSecret ), 102 wfEscapeShellArg( $tmpDir ), 103 wfEscapeShellArg( (string)$countGen ), 104 wfEscapeShellArg( $wgCaptchaDirectoryLevels ) 105 ); 106 foreach ( 107 [ 'wordlist', 'font', 'font-size', 'blacklist', 'verbose', 'threads' ] as $par 108 ) { 109 if ( $this->hasOption( $par ) ) { 110 $cmd .= " --$par " . wfEscapeShellArg( $this->getOption( $par ) ); 111 } 112 } 113 114 $this->output( "Generating $countGen new captchas.." ); 115 $retVal = 1; 116 $captchaTime = -microtime( true ); 117 wfShellExec( $cmd, $retVal, [], [ 'time' => 0 ] ); 118 if ( $retVal != 0 ) { 119 $this->output( " Failed.\n" ); 120 wfRecursiveRemoveDir( $tmpDir ); 121 $this->fatalError( "An error occured when running $captchaScript.\n", 1 ); 122 } 123 124 $captchaTime += microtime( true ); 125 $this->output( " Done.\n" ); 126 127 $this->output( 128 sprintf( 129 "\nGenerated %d captchas in %.1f seconds\n", 130 $countGen, 131 $captchaTime 132 ) 133 ); 134 135 $filesToDelete = []; 136 if ( $deleteOldCaptchas ) { 137 $this->output( "Getting a list of old captchas to delete..." ); 138 $path = $backend->getRootStoragePath() . '/captcha-render'; 139 foreach ( $backend->getFileList( [ 'dir' => $path ] ) as $file ) { 140 $filesToDelete[] = [ 141 'op' => 'delete', 142 'src' => $path . '/' . $file, 143 ]; 144 } 145 $this->output( " Done.\n" ); 146 } 147 148 $this->output( "Copying the new captchas to storage..." ); 149 150 $storeTime = -microtime( true ); 151 $iter = new RecursiveIteratorIterator( 152 new RecursiveDirectoryIterator( 153 $tmpDir, 154 FilesystemIterator::SKIP_DOTS 155 ), 156 RecursiveIteratorIterator::LEAVES_ONLY 157 ); 158 159 $captchasGenerated = iterator_count( $iter ); 160 $filesToStore = []; 161 /** 162 * @var $fileInfo SplFileInfo 163 */ 164 foreach ( $iter as $fileInfo ) { 165 if ( !$fileInfo->isFile() ) { 166 continue; 167 } 168 list( $salt, $hash ) = $instance->hashFromImageName( $fileInfo->getBasename() ); 169 $dest = $instance->imagePath( $salt, $hash ); 170 $backend->prepare( [ 'dir' => dirname( $dest ) ] ); 171 $filesToStore[] = [ 172 'op' => 'store', 173 'src' => $fileInfo->getPathname(), 174 'dst' => $dest, 175 ]; 176 } 177 178 $ret = $backend->doQuickOperations( $filesToStore ); 179 180 $storeTime += microtime( true ); 181 182 $storeSucceeded = true; 183 if ( $ret->isOK() ) { 184 $this->output( " Done.\n" ); 185 $this->output( 186 sprintf( 187 "\nCopied %d captchas to storage in %.1f seconds\n", 188 $ret->successCount, 189 $storeTime 190 ) 191 ); 192 if ( !$ret->isGood() ) { 193 $this->output( 194 "Non fatal errors:\n" . 195 Status::wrap( $ret )->getWikiText( false, false, 'en' ) . 196 "\n" 197 ); 198 } 199 if ( $ret->failCount ) { 200 $storeSucceeded = false; 201 $this->error( sprintf( "\nFailed to copy %d captchas\n", $ret->failCount ) ); 202 } 203 if ( $ret->successCount + $ret->failCount !== $captchasGenerated ) { 204 $storeSucceeded = false; 205 $this->error( 206 sprintf( "Internal error: captchasGenerated: %d, successCount: %d, failCount: %d\n", 207 $captchasGenerated, $ret->successCount, $ret->failCount 208 ) 209 ); 210 } 211 } else { 212 $storeSucceeded = false; 213 $this->output( "Errored.\n" ); 214 $this->error( 215 Status::wrap( $ret )->getWikiText( false, false, 'en' ) . 216 "\n" 217 ); 218 } 219 220 if ( $storeSucceeded && $deleteOldCaptchas ) { 221 $numOriginalFiles = count( $filesToDelete ); 222 $this->output( "Deleting {$numOriginalFiles} old captchas...\n" ); 223 $deleteTime = -microtime( true ); 224 $ret = $backend->doQuickOperations( $filesToDelete ); 225 226 $deleteTime += microtime( true ); 227 if ( $ret->isOK() ) { 228 $this->output( "Done.\n" ); 229 $this->output( 230 sprintf( 231 "\nDeleted %d old captchas in %.1f seconds\n", 232 $numOriginalFiles, 233 $deleteTime 234 ) 235 ); 236 if ( !$ret->isGood() ) { 237 $this->output( 238 "Non fatal errors:\n" . 239 Status::wrap( $ret )->getWikiText( false, false, 'en' ) . 240 "\n" 241 ); 242 } 243 } else { 244 $this->output( "Errored.\n" ); 245 $this->error( 246 Status::wrap( $ret )->getWikiText( false, false, 'en' ) . 247 "\n" 248 ); 249 } 250 251 } 252 $this->output( "Removing temporary files..." ); 253 wfRecursiveRemoveDir( $tmpDir ); 254 $this->output( " Done.\n" ); 255 256 $totalTime += microtime( true ); 257 $this->output( 258 sprintf( 259 "\nWhole captchas generation process took %.1f seconds\n", 260 $totalTime 261 ) 262 ); 263 } 264} 265 266$maintClass = GenerateFancyCaptchas::class; 267require_once RUN_MAINTENANCE_IF_MAIN; 268