Index: trunk/extensions/ConfirmAccount/SpecialConfirmAccount.php |
— | — | @@ -28,15 +28,22 @@ |
29 | 29 | $wgAutoWelcomeNewUsers = true; |
30 | 30 | # Make the username of the real name? |
31 | 31 | $wgUseRealNamesOnly = true; |
32 | | -$wgSaveRejectedAccountReqs = true; |
33 | 32 | $wgRejectedAccountMaxAge = 7 * 24 * 3600; // One week |
34 | 33 | # How many requests can an IP make at once? |
35 | 34 | $wgAccountRequestThrottle = 1; |
36 | 35 | # Minimum biography specs |
37 | 36 | $wgAccountRequestMinWords = 50; |
38 | 37 | |
| 38 | +# Location of attached files |
| 39 | +$wgAllowAccountRequestFiles = true; |
| 40 | +$wgAccountRequestExts = array('txt','pdf','doc','latex','rtf','text','wp','wpd' ); |
| 41 | +$wgFileStore['accountreqs']['directory'] = "{$wgUploadDirectory}/accountreqs"; |
| 42 | +$wgFileStore['accountreqs']['url'] = null; // Private |
| 43 | +$wgFileStore['accountreqs']['hash'] = 3; |
| 44 | + |
39 | 45 | $wgGroupPermissions['*']['createaccount'] = false; |
40 | 46 | $wgGroupPermissions['bureaucrat']['confirmaccount'] = true; |
| 47 | +$wgGroupPermissions['bureaucrat']['requestips'] = true; |
41 | 48 | |
42 | 49 | # Internationalisation |
43 | 50 | function efLoadConfirmAccountsMessages() { |
— | — | @@ -88,3 +95,21 @@ |
89 | 96 | $wgHooks['UserLoginForm'][] = 'efAddRequestLoginText'; |
90 | 97 | # Check for collisions |
91 | 98 | $wgHooks['AbortNewAccount'][] = 'efCheckIfAccountNameIsPending'; |
| 99 | +$wgHooks['LoadExtensionSchemaUpdates'][] = 'efConfirmAccountSchemaUpdates'; |
| 100 | + |
| 101 | +function efConfirmAccountSchemaUpdates() { |
| 102 | + global $wgDBtype, $wgExtNewFields, $wgExtPGNewFields; |
| 103 | + |
| 104 | + $base = dirname(__FILE__); |
| 105 | + if ($wgDBtype == 'mysql') { |
| 106 | + $wgExtNewFields[] = array('account_requests', 'acr_filename', |
| 107 | + "$base/archives/patch-acr_filename.sql" ); |
| 108 | + } else { |
| 109 | + $wgExtPGNewFields[] = array('account_requests', 'acr_filename', "TEXT" ); |
| 110 | + $wgExtPGNewFields[] = array('account_requests', 'acr_held', "TIMESTAMPTZ" ); |
| 111 | + $wgExtPGNewFields[] = array('account_requests', 'acr_storage_key', "TEXT" ); |
| 112 | + $wgExtPGNewFields[] = array('account_requests', 'acr_comment', "TEXT" ); |
| 113 | + } |
| 114 | + |
| 115 | + return true; |
| 116 | +} |
Index: trunk/extensions/ConfirmAccount/archives/patch-acr_filename.sql |
— | — | @@ -0,0 +1,7 @@ |
| 2 | +-- (c) Aaron Schulz, 2007 |
| 3 | + |
| 4 | +ALTER TABLE /*$wgDBprefix*/account_requests |
| 5 | + ADD acr_filename VARCHAR(255) NULL, |
| 6 | + ADD acr_storage_key VARCHAR(64) NULL, |
| 7 | + ADD acr_held binary(14), |
| 8 | + ADD acr_reason VARCHAR(255) NULL; |
Property changes on: trunk/extensions/ConfirmAccount/archives/patch-acr_filename.sql |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 9 | + native |
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.sql |
— | — | @@ -35,6 +35,9 @@ |
36 | 36 | acr_urls mediumblob NOT NULL, |
37 | 37 | -- IP address |
38 | 38 | acr_ip VARCHAR(255) NULL default '', |
| 39 | + --Name of attached file (.pdf,.doc,.txt ect...) |
| 40 | + acr_filename VARCHAR(255) NULL, |
| 41 | + acr_storage_key VARCHAR(64) NULL, |
39 | 42 | |
40 | 43 | -- Timestamp of account registration. |
41 | 44 | acr_registration char(14) NOT NULL, |
— | — | @@ -43,8 +46,12 @@ |
44 | 47 | acr_deleted bool NOT NULL, |
45 | 48 | -- Time of rejection (if rejected) |
46 | 49 | acr_rejected binary(14), |
47 | | - -- The user who rejected it |
| 50 | + -- Time request was put on hold (if held) |
| 51 | + acr_held binary(14), |
| 52 | + -- The user who rejected/held it |
48 | 53 | acr_user int unsigned NOT NULL default 0, |
| 54 | + -- Reason |
| 55 | + acr_comment varchar(255) NOT NULL default '', |
49 | 56 | |
50 | 57 | PRIMARY KEY (acr_id), |
51 | 58 | UNIQUE KEY (acr_name), |
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.pg.sql |
— | — | @@ -18,9 +18,13 @@ |
19 | 19 | acr_notes TEXT, |
20 | 20 | acr_urls TEXT, |
21 | 21 | acr_ip CIDR, |
| 22 | + acr_filename TEXT, |
| 23 | + acr_storage_key TEXT, |
22 | 24 | acr_deleted BOOL NOT NULL DEFAULT 'false', |
23 | 25 | acr_rejected TIMESTAMPTZ, |
24 | | - acr_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL |
| 26 | + acr_held TIMESTAMPTZ, |
| 27 | + acr_user INTEGER REFERENCES mwuser(user_id) ON DELETE SET NULL, |
| 28 | + acr_comment TEXT NOT NULL default '' |
25 | 29 | ); |
26 | 30 | |
27 | 31 | CREATE INDEX acr_deleted_reg ON account_requests (acr_deleted,acr_registration); |
Index: trunk/extensions/ConfirmAccount/ConfirmAccount_body.php |
— | — | @@ -33,6 +33,10 @@ |
34 | 34 | $this->mUsername = $this->mRealName; |
35 | 35 | else |
36 | 36 | $this->mUsername = $wgRequest->getText( 'wpUsername' ); |
| 37 | + # Attachments... |
| 38 | + $this->initializeUpload( $wgRequest ); |
| 39 | + $this->mPrevAttachment = $wgRequest->getText( 'attachment' ); |
| 40 | + $this->mForgotAttachment = $wgRequest->getBool( 'forgotAttachment' ); |
37 | 41 | # Other fields... |
38 | 42 | $this->mEmail = $wgRequest->getText( 'wpEmail' ); |
39 | 43 | $this->mBio = $wgRequest->getText( 'wpBio', '' ); |
— | — | @@ -43,6 +47,8 @@ |
44 | 48 | $emailCode = $wgRequest->getText( 'wpEmailToken' ); |
45 | 49 | |
46 | 50 | if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { |
| 51 | + if( !$this->mPrevAttachment ) |
| 52 | + $this->mPrevAttachment = $this->mSrcName; |
47 | 53 | $this->doSubmit(); |
48 | 54 | } else if( $action == 'confirmemail' ) { |
49 | 55 | $this->confirmEmailToken( $emailCode ); |
— | — | @@ -51,13 +57,15 @@ |
52 | 58 | } |
53 | 59 | } |
54 | 60 | |
55 | | - function showForm( $msg='' ) { |
| 61 | + function showForm( $msg='', $forgetFile=0 ) { |
56 | 62 | global $wgOut, $wgUser, $wgTitle, $wgAuth, $wgUseRealNamesOnly; |
57 | 63 | |
| 64 | + $this->mForgotAttachment = $forgetFile; |
| 65 | + |
58 | 66 | $wgOut->setPagetitle( wfMsgHtml( "requestaccount" ) ); |
59 | | - # Output failure message |
| 67 | + # Output failure message if any |
60 | 68 | if( $msg ) { |
61 | | - $wgOut->addHTML( '<div class="errorbox">' . $msg . '</div><div class="visualClear"></div>' ); |
| 69 | + $wgOut->addHTML( '<div class="errorbox">'.$msg.'</div><div class="visualClear"></div>' ); |
62 | 70 | } |
63 | 71 | # Give notice to users that are logged in |
64 | 72 | if( $wgUser->getID() ) { |
— | — | @@ -67,8 +75,8 @@ |
68 | 76 | $wgOut->addWikiText( wfMsgHtml( "requestaccount-text" ) ); |
69 | 77 | |
70 | 78 | $action = $wgTitle->escapeLocalUrl( 'action=submit' ); |
71 | | - $form = "<form name='accountrequest' action='$action' method='post'><fieldset>"; |
72 | | - $form .= '<legend>' . wfMsgHtml('requestaccount-legend1') . '</legend>'; |
| 79 | + $form = "<form name='accountrequest' action='$action' enctype='multipart/form-data' method='post'>"; |
| 80 | + $form .= '<fieldset><legend>' . wfMsgHtml('requestaccount-legend1') . '</legend>'; |
73 | 81 | $form .= wfMsgExt( 'requestaccount-acc-text', array('parse') )."\n"; |
74 | 82 | $form .= '<table cellpadding=\'4\'>'; |
75 | 83 | if( $wgUseRealNamesOnly ) { |
— | — | @@ -99,6 +107,9 @@ |
100 | 108 | $form .= '<legend>' . wfMsgHtml('requestaccount-legend3') . '</legend>'; |
101 | 109 | $form .= wfMsgExt( 'requestaccount-ext-text', array('parse') )."\n"; |
102 | 110 | |
| 111 | + $form .= "<p>".wfMsgHtml('requestaccount-attach')." "; |
| 112 | + $form .= Xml::input( 'wpUploadFile', 35, '', array('id' => 'wpUploadFile', 'type' => 'file') )."</p>\n"; |
| 113 | + |
103 | 114 | $form .= "<p>".wfMsgHtml('requestaccount-notes')."</p>\n"; |
104 | 115 | $form .= "<p><textarea tabindex='1' name='wpNotes' id='wpNotes' rows='3' cols='80' style='width:100%'>" . |
105 | 116 | htmlspecialchars($this->mNotes) . |
— | — | @@ -124,10 +135,12 @@ |
125 | 136 | } |
126 | 137 | $form .= "<p>".Xml::check( 'wpToS', $this->mToS, array('id' => 'wpToS') ). |
127 | 138 | ' <label for="wpToS">'.wfMsgExt( 'requestaccount-tos', array('parseinline') )."</label></p>\n"; |
128 | | - $form .= Xml::hidden( 'title', $wgTitle->getPrefixedText() )."\n"; |
| 139 | + $form .= Xml::hidden( 'title', $wgTitle->getPrefixedUrl() )."\n"; |
129 | 140 | $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken() )."\n"; |
130 | | - $form .= "<p>".Xml::submitButton( wfMsgHtml( 'requestaccount-submit') ) . "</p>"; |
131 | | - $form .= '</form>'; |
| 141 | + $form .= Xml::hidden( 'attachment', $this->mPrevAttachment )."\n"; |
| 142 | + $form .= Xml::hidden( 'forgotAttachment', $this->mForgotAttachment )."\n"; |
| 143 | + $form .= "<p>".Xml::submitButton( wfMsgHtml( 'requestaccount-submit') )."</p>"; |
| 144 | + $form .= '</form>'; |
132 | 145 | |
133 | 146 | $wgOut->addHTML( $form ); |
134 | 147 | } |
— | — | @@ -184,7 +197,7 @@ |
185 | 198 | # used for more than just username validation |
186 | 199 | $u->setEmail( $this->mEmail ); |
187 | 200 | $u->setRealName( $this->mRealName ); |
188 | | - # Let captchas confirm |
| 201 | + # Let captchas deny request... |
189 | 202 | global $wgCaptcha; |
190 | 203 | if( isset($wgCaptcha) ) { |
191 | 204 | $abortError = ''; |
— | — | @@ -194,10 +207,66 @@ |
195 | 208 | return false; |
196 | 209 | } |
197 | 210 | } |
| 211 | + # Per security reasons, file dir cannot be pulled from client, |
| 212 | + # so ask them to resubmit it then... |
| 213 | + global $wgAllowAccountRequestFiles; |
| 214 | + if( $wgAllowAccountRequestFiles && $this->mPrevAttachment && !$this->mSrcName ) { |
| 215 | + # If the user is submitting forgotAttachment as true with no file, |
| 216 | + # then they saw the notice and choose not to re-select the file. |
| 217 | + # Assume that they don't want to send one anymore. |
| 218 | + if( !$this->mForgotAttachment ) { |
| 219 | + $this->mPrevAttachment = ''; |
| 220 | + $this->showForm( wfMsgHtml('requestaccount-resub'), 1 ); |
| 221 | + return false; |
| 222 | + } |
| 223 | + } |
| 224 | + # Process upload... |
| 225 | + if( $wgAllowAccountRequestFiles && $this->mSrcName ) { |
| 226 | + $ext = explode('.',$this->mSrcName); |
| 227 | + $finalExt = $ext[count($ext)-1]; |
| 228 | + # File must have size. |
| 229 | + if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { |
| 230 | + $this->mPrevAttachment = ''; |
| 231 | + $this->showForm( wfMsgHtml( 'emptyfile' ) ); |
| 232 | + return false; |
| 233 | + } |
| 234 | + # Look at the contents of the file; if we can recognize the |
| 235 | + # type but it's corrupt or data of the wrong type, we should |
| 236 | + # probably not accept it. |
| 237 | + global $wgAccountRequestExts; |
| 238 | + if( !in_array($finalExt,$wgAccountRequestExts) ) { |
| 239 | + $this->mPrevAttachment = ''; |
| 240 | + $this->showForm( wfMsgHtml( 'requestaccount-exts' ) ); |
| 241 | + return false; |
| 242 | + } |
| 243 | + $fileProps = File::getPropsFromPath( $this->mTempPath, $finalExt ); |
| 244 | + $veri = $this->verify( $this->mTempPath, $finalExt ); |
| 245 | + if( $veri !== true ) { |
| 246 | + $this->mPrevAttachment = ''; |
| 247 | + $this->showForm( wfMsgHtml( 'uploadcorrupt' ) ); |
| 248 | + return false; |
| 249 | + } |
| 250 | + # Start a transaction, move file from temp to account request directory. |
| 251 | + $transaction = new FSTransaction(); |
| 252 | + if( !FileStore::lock() ) { |
| 253 | + wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); |
| 254 | + return false; |
| 255 | + } |
| 256 | + $store = FileStore::get( 'accountreqs' ); |
| 257 | + $key = FileStore::calculateKey( $this->mTempPath, $finalExt ); |
| 258 | + |
| 259 | + $transaction->add( $store->insert( $key, $this->mTempPath, FileStore::DELETE_ORIGINAL ) ); |
| 260 | + if( $transaction === false ) { |
| 261 | + // Failed to move? |
| 262 | + wfDebug( __METHOD__.": import to file store failed, aborting\n" ); |
| 263 | + throw new MWException( "Could not archive and delete file $oldpath" ); |
| 264 | + return false; |
| 265 | + } |
| 266 | + } |
198 | 267 | # Insert into pending requests... |
199 | 268 | $dbw->begin(); |
200 | 269 | $dbw->insert( 'account_requests', |
201 | | - array( |
| 270 | + array( |
202 | 271 | 'acr_name' => $u->mName, |
203 | 272 | 'acr_email' => $u->mEmail, |
204 | 273 | 'acr_real_name' => $u->mRealName, |
— | — | @@ -205,6 +274,8 @@ |
206 | 275 | 'acr_bio' => $this->mBio, |
207 | 276 | 'acr_notes' => $this->mNotes, |
208 | 277 | 'acr_urls' => $this->mUrls, |
| 278 | + 'acr_filename' => isset($this->mSrcName) ? $this->mSrcName : null, |
| 279 | + 'acr_storage_key' => isset($key) ? $key : null, |
209 | 280 | 'acr_ip' => wfGetIP() // Possible use for spam blocking |
210 | 281 | ), |
211 | 282 | __METHOD__ |
— | — | @@ -212,12 +283,18 @@ |
213 | 284 | # Send confirmation, required! |
214 | 285 | $result = $this->sendConfirmationMail( $u ); |
215 | 286 | if( WikiError::isError( $result ) ) { |
| 287 | + $dbw->rollback(); // Nevermind |
| 288 | + #$transaction->rollback(); |
216 | 289 | $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) ); |
217 | 290 | $this->showForm( $error ); |
218 | | - $dbw->rollback(); // Nevermind |
219 | 291 | return false; |
220 | 292 | } |
221 | 293 | $dbw->commit(); |
| 294 | + if( isset($transaction) ) { |
| 295 | + wfDebug( __METHOD__.": set db items, applying file transactions\n" ); |
| 296 | + $transaction->commit(); |
| 297 | + FileStore::unlock(); |
| 298 | + } |
222 | 299 | # No request spamming... |
223 | 300 | # BC: check if isPingLimitable() exists |
224 | 301 | if( $wgAccountRequestThrottle && ( !method_exists($wgUser,'isPingLimitable') || $wgUser->isPingLimitable() ) ) { |
— | — | @@ -241,6 +318,101 @@ |
242 | 319 | } |
243 | 320 | |
244 | 321 | /** |
| 322 | + * Initialize the uploaded file from PHP data |
| 323 | + * @access private |
| 324 | + */ |
| 325 | + function initializeUpload( $request ) { |
| 326 | + $this->mTempPath = $request->getFileTempName( 'wpUploadFile' ); |
| 327 | + $this->mFileSize = $request->getFileSize( 'wpUploadFile' ); |
| 328 | + $this->mSrcName = $request->getFileName( 'wpUploadFile' ); |
| 329 | + $this->mRemoveTempFile = false; // PHP will handle this |
| 330 | + } |
| 331 | + |
| 332 | + /** |
| 333 | + * Verifies that it's ok to include the uploaded file |
| 334 | + * |
| 335 | + * @param string $tmpfile the full path of the temporary file to verify |
| 336 | + * @param string $extension The filename extension that the file is to be served with |
| 337 | + * @return mixed true of the file is verified, a WikiError object otherwise. |
| 338 | + */ |
| 339 | + function verify( $tmpfile, $extension ) { |
| 340 | + #magically determine mime type |
| 341 | + $magic=& MimeMagic::singleton(); |
| 342 | + $mime = $magic->guessMimeType($tmpfile,false); |
| 343 | + |
| 344 | + #check mime type, if desired |
| 345 | + global $wgVerifyMimeType; |
| 346 | + if ($wgVerifyMimeType) { |
| 347 | + |
| 348 | + wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n"); |
| 349 | + #check mime type against file extension |
| 350 | + if( !$this->verifyExtension( $mime, $extension ) ) { |
| 351 | + return new WikiErrorMsg( 'uploadcorrupt' ); |
| 352 | + } |
| 353 | + |
| 354 | + #check mime type blacklist |
| 355 | + global $wgMimeTypeBlacklist; |
| 356 | + if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) |
| 357 | + && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { |
| 358 | + return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) ); |
| 359 | + } |
| 360 | + } |
| 361 | + |
| 362 | + wfDebug( __METHOD__.": all clear; passing.\n" ); |
| 363 | + return true; |
| 364 | + } |
| 365 | + |
| 366 | + /** |
| 367 | + * Checks if the mime type of the uploaded file matches the file extension. |
| 368 | + * |
| 369 | + * @param string $mime the mime type of the uploaded file |
| 370 | + * @param string $extension The filename extension that the file is to be served with |
| 371 | + * @return bool |
| 372 | + */ |
| 373 | + function verifyExtension( $mime, $extension ) { |
| 374 | + $magic =& MimeMagic::singleton(); |
| 375 | + |
| 376 | + if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) |
| 377 | + if ( ! $magic->isRecognizableExtension( $extension ) ) { |
| 378 | + wfDebug( __METHOD__.": passing file with unknown detected mime type; " . |
| 379 | + "unrecognized extension '$extension', can't verify\n" ); |
| 380 | + return true; |
| 381 | + } else { |
| 382 | + wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". |
| 383 | + "recognized extension '$extension', so probably invalid file\n" ); |
| 384 | + return false; |
| 385 | + } |
| 386 | + |
| 387 | + $match = $magic->isMatchingExtension($extension,$mime); |
| 388 | + |
| 389 | + if ($match===NULL) { |
| 390 | + wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); |
| 391 | + return true; |
| 392 | + } elseif ($match===true) { |
| 393 | + wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); |
| 394 | + |
| 395 | + #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! |
| 396 | + return true; |
| 397 | + |
| 398 | + } else { |
| 399 | + wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); |
| 400 | + return false; |
| 401 | + } |
| 402 | + } |
| 403 | + |
| 404 | + /** |
| 405 | + * Perform case-insensitive match against a list of file extensions. |
| 406 | + * Returns true if the extension is in the list. |
| 407 | + * |
| 408 | + * @param string $ext |
| 409 | + * @param array $list |
| 410 | + * @return bool |
| 411 | + */ |
| 412 | + function checkFileExtension( $ext, $list ) { |
| 413 | + return in_array( strtolower( $ext ), $list ); |
| 414 | + } |
| 415 | + |
| 416 | + /** |
245 | 417 | * @private |
246 | 418 | */ |
247 | 419 | function throttleHit( $limit ) { |
— | — | @@ -267,7 +439,7 @@ |
268 | 440 | $wgOut->addWikiText( wfMsg( $message ) ); |
269 | 441 | if( !$wgUser->isLoggedIn() ) { |
270 | 442 | $title = SpecialPage::getTitleFor( 'Userlogin' ); |
271 | | - $wgOut->returnToMain( true, $title->getPrefixedText() ); |
| 443 | + $wgOut->returnToMain( true, $title->getPrefixedUrl() ); |
272 | 444 | } |
273 | 445 | } else { |
274 | 446 | $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) ); |
— | — | @@ -383,6 +555,8 @@ |
384 | 556 | $this->setHeaders(); |
385 | 557 | # A target user |
386 | 558 | $this->acrID = $wgRequest->getIntOrNull( 'acrid' ); |
| 559 | + # Attachments |
| 560 | + $this->file = $wgRequest->getVal( 'file' ); |
387 | 561 | # For renaming to alot for collisions with other local requests |
388 | 562 | # that were added to some global $wgAuth system first. |
389 | 563 | $this->mUsername = $wgRequest->getText( 'wpNewName' ); |
— | — | @@ -396,6 +570,8 @@ |
397 | 571 | |
398 | 572 | if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { |
399 | 573 | $this->doSubmit(); |
| 574 | + } else if( $this->file ) { |
| 575 | + $this->showFile( $this->file ); |
400 | 576 | } else if( $this->acrID ) { |
401 | 577 | $this->showForm(); |
402 | 578 | } else { |
— | — | @@ -403,6 +579,25 @@ |
404 | 580 | } |
405 | 581 | } |
406 | 582 | |
| 583 | + /** |
| 584 | + * Show a private file requested by the visitor. |
| 585 | + */ |
| 586 | + function showFile( $key ) { |
| 587 | + global $wgOut, $wgRequest; |
| 588 | + $wgOut->disable(); |
| 589 | + |
| 590 | + # We mustn't allow the output to be Squid cached, otherwise |
| 591 | + # if an admin previews a private image, and it's cached, then |
| 592 | + # a user without appropriate permissions can toddle off and |
| 593 | + # nab the image, and Squid will serve it |
| 594 | + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
| 595 | + $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); |
| 596 | + $wgRequest->response()->header( 'Pragma: no-cache' ); |
| 597 | + |
| 598 | + $store = FileStore::get( 'accountreqs' ); |
| 599 | + $store->stream( $key ); |
| 600 | + } |
| 601 | + |
407 | 602 | function doSubmit() { |
408 | 603 | global $wgOut, $wgTitle, $wgAuth; |
409 | 604 | |
— | — | @@ -414,7 +609,6 @@ |
415 | 610 | } |
416 | 611 | |
417 | 612 | if( $this->submitType == 'reject' ) { |
418 | | - global $wgSaveRejectedAccountReqs; |
419 | 613 | # Make proxy user to email a rejection message :( |
420 | 614 | $u = User::newFromName( $row->acr_name, 'creatable' ); |
421 | 615 | $u->setEmail( $row->acr_email ); |
— | — | @@ -434,19 +628,15 @@ |
435 | 629 | } |
436 | 630 | } |
437 | 631 | $dbw = wfGetDB( DB_MASTER ); |
438 | | - # Either mark off the row as deleted or wipe it completely |
439 | | - if( $wgSaveRejectedAccountReqs ) { |
440 | | - global $wgUser; |
441 | | - # Request can later be recovered |
442 | | - $dbw->update( 'account_requests', |
443 | | - array( 'acr_rejected' => $dbw->timestamp(), |
444 | | - 'acr_user' => $wgUser->getID(), |
445 | | - 'acr_deleted' => 1 ), |
446 | | - array( 'acr_id' => $this->acrID, 'acr_deleted' => 0 ), |
447 | | - __METHOD__ ); |
448 | | - } else { |
449 | | - $dbw->delete( 'account_requests', array('acr_id' => $this->acrID), __METHOD__ ); |
450 | | - } |
| 632 | + # Mark off the row as deleted |
| 633 | + global $wgUser; |
| 634 | + # Request can later be recovered |
| 635 | + $dbw->update( 'account_requests', |
| 636 | + array( 'acr_rejected' => $dbw->timestamp(), |
| 637 | + 'acr_user' => $wgUser->getID(), |
| 638 | + 'acr_deleted' => 1 ), |
| 639 | + array( 'acr_id' => $this->acrID, 'acr_deleted' => 0 ), |
| 640 | + __METHOD__ ); |
451 | 641 | |
452 | 642 | $this->showSuccess( $action ); |
453 | 643 | } else if( $this->submitType == 'accept' ) { |
— | — | @@ -516,17 +706,49 @@ |
517 | 707 | } |
518 | 708 | if( $wgAutoWelcomeNewUsers ) { |
519 | 709 | $utalk = new Article( $user->getTalkPage() ); |
520 | | - $utalk->doEdit( wfMsg('confirmaccount-welc') . ' ~~~~', wfMsg('confirmaccount-wsum'), EDIT_MINOR ); |
| 710 | + $utalk->doEdit( wfMsg('confirmaccount-welc') . ' ~~~~', |
| 711 | + wfMsg('confirmaccount-wsum'), EDIT_MINOR ); |
521 | 712 | } |
522 | 713 | |
523 | 714 | $this->showSuccess( $action, $user->getName() ); |
| 715 | + } else if( $this->submitType == 'hold' ) { |
| 716 | + global $wgUser; |
| 717 | + # Make proxy user to email a message |
| 718 | + $u = User::newFromName( $row->acr_name, 'creatable' ); |
| 719 | + $u->setEmail( $row->acr_email ); |
| 720 | + # Pointless without a summary... |
| 721 | + if( !$this->reason ) { |
| 722 | + $error = wfMsg( 'confirmaccount-needreason' ); |
| 723 | + $this->showForm( $error ); |
| 724 | + return false; |
| 725 | + } |
| 726 | + # If not already held or deleted, mark as held |
| 727 | + $dbw = wfGetDB( DB_MASTER ); |
| 728 | + $dbw->begin(); |
| 729 | + $dbw->update( 'account_requests', |
| 730 | + array( 'acr_held' => $dbw->timestamp(), |
| 731 | + 'acr_user' => $wgUser->getID() ), |
| 732 | + array( 'acr_id' => $this->acrID, 'acr_held IS NULL', 'acr_deleted' => 0 ), |
| 733 | + __METHOD__ ); |
| 734 | + # Do not send multiple times |
| 735 | + if( !$row->acr_held && !$row->acr_deleted ) { |
| 736 | + $result = $u->sendMail( wfMsg( 'confirmaccount-email-subj' ), |
| 737 | + wfMsgExt( 'confirmaccount-email-body5', array('parsemag'), $u->getName(), $this->reason ) ); |
| 738 | + if( WikiError::isError( $result ) ) { |
| 739 | + $dbw->rollback(); |
| 740 | + $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) ); |
| 741 | + $this->showForm( $error ); |
| 742 | + return false; |
| 743 | + } |
| 744 | + } |
| 745 | + $this->showSuccess( $action ); |
524 | 746 | } else { |
525 | 747 | $this->showForm(); |
526 | 748 | } |
527 | 749 | } |
528 | 750 | |
529 | 751 | function showForm( $msg='' ) { |
530 | | - global $wgOut, $wgTitle, $wgUser; |
| 752 | + global $wgOut, $wgTitle, $wgUser, $wgLang; |
531 | 753 | |
532 | 754 | # Output failure message |
533 | 755 | if( $msg ) { |
— | — | @@ -549,6 +771,16 @@ |
550 | 772 | |
551 | 773 | $wgOut->addWikiText( wfMsg( "confirmaccount-text" ) ); |
552 | 774 | |
| 775 | + if( $this->showRejects ) { |
| 776 | + $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_rejected), true ); |
| 777 | + $wgOut->addHTML('<b>'.wfMsgExt( 'confirmaccount-reject', array('parseinline'), |
| 778 | + User::whoIs($row->acr_user), $time ).'</b>'); |
| 779 | + } else if( $row->acr_held ) { |
| 780 | + $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_held), true ); |
| 781 | + $wgOut->addHTML('<b>'.wfMsgExt( 'confirmaccount-held', array('parseinline'), |
| 782 | + User::whoIs($row->acr_user), $time ).'</b>'); |
| 783 | + } |
| 784 | + |
553 | 785 | $action = $wgTitle->escapeLocalUrl( 'action=submit' ); |
554 | 786 | $form = "<form name='accountconfirm' action='$action' method='post'><fieldset>"; |
555 | 787 | $form .= '<legend>' . wfMsgHtml('requestaccount-legend1') . '</legend>'; |
— | — | @@ -575,12 +807,22 @@ |
576 | 808 | |
577 | 809 | $form .= '<fieldset>'; |
578 | 810 | $form .= '<legend>' . wfMsgHtml('requestaccount-legend3') . '</legend>'; |
| 811 | + $form .= '<p>'.wfMsgHtml('confirmaccount-attach') . ' ' . |
| 812 | + $this->skin->makeKnownLinkObj( $wgTitle, htmlspecialchars($row->acr_filename), |
| 813 | + 'file=' . $row->acr_storage_key ); |
| 814 | + |
579 | 815 | $form .= "<p>".wfMsgHtml('confirmaccount-notes')."</p>\n"; |
580 | 816 | $form .= "<p><textarea tabindex='1' readonly name='wpNotes' id='wpNotes' rows='3' cols='80' style='width:100%'>" . |
581 | 817 | htmlspecialchars($row->acr_notes) . |
582 | 818 | "</textarea></p>\n"; |
583 | 819 | $form .= "<p>".wfMsgHtml('confirmaccount-urls')."</p>\n"; |
584 | 820 | $form .= "<p>".$this->parseLinks($row->acr_urls)."</p>"; |
| 821 | + if( $wgUser->isAllowed( 'requestips' ) ) { |
| 822 | + $blokip = SpecialPage::getTitleFor( 'blockip' ); |
| 823 | + $form .= "<p>".wfMsgHtml('confirmaccount-ip')." ".htmlspecialchars($row->acr_ip). |
| 824 | + " (" . $this->skin->makeKnownLinkObj( $blokip, wfMsgHtml('blockip'), |
| 825 | + 'ip=' . $row->acr_ip ).")</p>\n"; |
| 826 | + } |
585 | 827 | $form .= '</fieldset>'; |
586 | 828 | |
587 | 829 | $form .= "<p>".wfMsgExt( 'confirmaccount-confirm', array('parse') )."</p>\n"; |
— | — | @@ -588,13 +830,15 @@ |
589 | 831 | $form .= ' '.Xml::label( wfMsg('confirmaccount-create'), 'submitCreate' )."</p>\n"; |
590 | 832 | $form .= "<p>".Xml::radio( 'wpSubmitType', 'reject', $this->submitType=='reject', array('id' => 'submitDeny') ); |
591 | 833 | $form .= ' '.Xml::label( wfMsg('confirmaccount-deny'), 'submitDeny' )."</p>\n"; |
| 834 | + $form .= "<p>".Xml::radio( 'wpSubmitType', 'hold', $this->submitType=='hold', array('id' => 'submitHold') ); |
| 835 | + $form .= ' '.Xml::label( wfMsg('confirmaccount-hold'), 'submitHold' )."</p>\n"; |
592 | 836 | |
593 | 837 | $form .= "<p>".wfMsgHtml('confirmaccount-reason')."</p>\n"; |
594 | 838 | $form .= "<p><textarea tabindex='1' name='wpReason' id='wpReason' rows='3' cols='80' style='width:80%'>" . |
595 | 839 | htmlspecialchars($this->reason) . |
596 | 840 | "</textarea></p>\n"; |
597 | 841 | $form .= "<p>".Xml::submitButton( wfMsgHtml( 'confirmaccount-submit') )."</p>\n"; |
598 | | - $form .= Xml::hidden( 'title', $wgTitle->getPrefixedText() )."\n"; |
| 842 | + $form .= Xml::hidden( 'title', $wgTitle->getPrefixedUrl() )."\n"; |
599 | 843 | $form .= Xml::hidden( 'action', 'reject' ); |
600 | 844 | $form .= Xml::hidden( 'acrid', $row->acr_id ); |
601 | 845 | $form .= Xml::hidden( 'wpShowRejects', $this->showRejects ); |
— | — | @@ -655,33 +899,53 @@ |
656 | 900 | $wgOut->addWikiText( wfMsg( "confirmaccount-acc", $name ) ); |
657 | 901 | else if( $this->submitType == 'reject' ) |
658 | 902 | $wgOut->addWikiText( wfMsg( "confirmaccount-rej" ) ); |
| 903 | + else |
| 904 | + $wgOut->redirect( $wgTitle->getFullUrl() ); |
659 | 905 | |
660 | 906 | $wgOut->returnToMain( true, $wgTitle ); |
661 | 907 | } |
662 | 908 | |
663 | 909 | function showList() { |
664 | | - global $wgOut, $wgUser, $wgTitle, $wgLang, $wgSaveRejectedAccountReqs; |
| 910 | + global $wgOut, $wgUser, $wgTitle, $wgLang; |
665 | 911 | |
666 | 912 | if( $this->showRejects ) { |
667 | 913 | $listLink = $this->skin->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'confirmaccount-back' ) ); |
668 | | - } else if( $wgSaveRejectedAccountReqs ) { |
| 914 | + } else { |
669 | 915 | $listLink = $this->skin->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'confirmaccount-back2' ), |
670 | 916 | wfArrayToCGI( array('wpShowRejects' => 1 ) ) ); |
671 | 917 | } |
672 | 918 | $wgOut->setSubtitle( '<p>'.$listLink.'</p>' ); |
673 | 919 | |
674 | | - if( $wgSaveRejectedAccountReqs ) { |
675 | | - # Every 100th view, prune old deleted items |
676 | | - wfSeedRandom(); |
677 | | - if( 0 == mt_rand( 0, 99 ) ) { |
678 | | - global $wgRejectedAccountMaxAge; |
| 920 | + # Every 100th view, prune old deleted items |
| 921 | + wfSeedRandom(); |
| 922 | + if( 0 == mt_rand( 0, 99 ) ) { |
| 923 | + global $wgRejectedAccountMaxAge; |
679 | 924 | |
680 | | - $dbw = wfGetDB( DB_MASTER ); |
681 | | - $cutoff = $dbw->timestamp( time() - $wgRejectedAccountMaxAge ); |
682 | | - $accountrequests = $dbw->tableName( 'account_requests' ); |
683 | | - $sql = "DELETE FROM $accountrequests WHERE acr_rejected < '{$cutoff}'"; |
684 | | - $dbw->query( $sql ); |
| 925 | + $dbw = wfGetDB( DB_MASTER ); |
| 926 | + $transaction = new FSTransaction(); |
| 927 | + if( !FileStore::lock() ) { |
| 928 | + wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); |
| 929 | + return false; |
685 | 930 | } |
| 931 | + # Select all items older than time $cutoff |
| 932 | + $cutoff = $dbw->timestamp( time() - $wgRejectedAccountMaxAge ); |
| 933 | + $accountrequests = $dbw->tableName( 'account_requests' ); |
| 934 | + $sql = "SELECT acr_storage_key,acr_id FROM $accountrequests WHERE acr_rejected < '{$cutoff}'"; |
| 935 | + $res = $dbw->query( $sql ); |
| 936 | + |
| 937 | + $store = FileStore::get( 'accountreqs' ); |
| 938 | + # Clear out any associated attachments and delete those rows |
| 939 | + while( $row = $dbw->fetchObject( $res ) ) { |
| 940 | + $key = $row->acr_storage_key; |
| 941 | + if( $key ) { |
| 942 | + $path = $store->filePath( $key ); |
| 943 | + if( $path && file_exists($path) ) { |
| 944 | + $transaction->addCommit( FSTransaction::DELETE_FILE, $path ); |
| 945 | + $dbw->query( "DELETE FROM $tbl_arch WHERE acr_id = {$row->acr_id}" ); |
| 946 | + } |
| 947 | + } |
| 948 | + } |
| 949 | + $transaction->commit(); |
686 | 950 | } |
687 | 951 | |
688 | 952 | $pager = new ConfirmAccountsPager( $this, array(), $this->showRejects ); |
— | — | @@ -710,15 +974,23 @@ |
711 | 975 | $link = $this->skin->makeKnownLinkObj( $title, wfMsgHtml('confirmaccount-review'), |
712 | 976 | 'acrid='.$row->acr_id.'&wpShowRejects=1' ); |
713 | 977 | } else { |
714 | | - $link = $this->skin->makeKnownLinkObj( $title, wfMsgHtml('confirmaccount-review'), 'acrid='.$row->acr_id ); |
| 978 | + $link = $this->skin->makeKnownLinkObj( $title, wfMsgHtml('confirmaccount-review'), |
| 979 | + 'acrid='.$row->acr_id ); |
715 | 980 | } |
716 | 981 | $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_registration), true ); |
717 | 982 | |
718 | 983 | $r = '<li>'; |
| 984 | + if( $row->acr_held ) { |
| 985 | + $r .= '<span class="confirmaccount-held">'; |
| 986 | + } |
| 987 | + |
719 | 988 | $r .= $time." ($link)"; |
720 | 989 | if( $this->showRejects ) { |
721 | 990 | $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_rejected), true ); |
722 | 991 | $r .= ' <b>'.wfMsgExt( 'confirmaccount-reject', array('parseinline'), $row->user_name, $time ).'</b>'; |
| 992 | + } else if( $row->acr_held ) { |
| 993 | + $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_held), true ); |
| 994 | + $r .= ' <b>'.wfMsgExt( 'confirmaccount-held', array('parseinline'), User::whoIs($row->acr_user), $time ).'</b>'; |
723 | 995 | } |
724 | 996 | $r .= '<br/><table cellspacing=\'1\' cellpadding=\'3\' border=\'1\' width=\'100%\'>'; |
725 | 997 | if( !$wgUseRealNamesOnly ) { |
— | — | @@ -737,8 +1009,13 @@ |
738 | 1010 | $preview = substr( $preview, 0, strrpos($preview,' ') ); |
739 | 1011 | $preview .= " . . ."; |
740 | 1012 | } |
741 | | - $r .= '<tr><td><strong>'.wfMsgHtml('confirmaccount-bio-q').'</strong></td><td width=\'100%\'><i>'.$preview.'</i></td></tr>'; |
742 | | - $r .= '</table></li>'; |
| 1013 | + $r .= '<tr><td><strong>'.wfMsgHtml('confirmaccount-bio-q') . |
| 1014 | + '</strong></td><td width=\'100%\'><i>'.$preview.'</i></td></tr>'; |
| 1015 | + $r .= '</table>'; |
| 1016 | + if( $row->acr_held ) { |
| 1017 | + $r .= '</span>'; |
| 1018 | + } |
| 1019 | + $r .= '</li>'; |
743 | 1020 | |
744 | 1021 | return $r; |
745 | 1022 | } |
— | — | @@ -772,8 +1049,8 @@ |
773 | 1050 | function getQueryInfo() { |
774 | 1051 | $conds = $this->mConds; |
775 | 1052 | $tables = array('account_requests'); |
776 | | - $fields = array('acr_id','acr_name','acr_real_name','acr_registration', |
777 | | - 'acr_email','acr_email_authenticated','acr_bio','acr_notes','acr_urls'); |
| 1053 | + $fields = array('acr_id','acr_name','acr_real_name','acr_registration','acr_held', |
| 1054 | + 'acr_email','acr_email_authenticated','acr_bio','acr_notes','acr_urls','acr_user'); |
778 | 1055 | if( $this->rejects ) { |
779 | 1056 | $tables[] = 'user'; |
780 | 1057 | $conds[] = 'acr_user = user_id'; |
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.i18n.php |
— | — | @@ -30,14 +30,18 @@ |
31 | 31 | 'requestaccount-same' => '(same as real name)', |
32 | 32 | 'requestaccount-email' => 'Email address:', |
33 | 33 | 'requestaccount-bio' => 'Personal biography:', |
| 34 | + 'requestaccount-attach' => 'Resume or CV (optional):', |
34 | 35 | 'requestaccount-notes' => 'Additional notes:', |
35 | 36 | 'requestaccount-urls' => 'List of websites, if any (separate with newlines):', |
36 | 37 | 'requestaccount-agree' => 'You must certify that your real name is correct and that you agree to our Terms of Service.', |
37 | 38 | 'requestaccount-inuse' => 'Username is already in use in a pending account request.', |
38 | 39 | 'requestaccount-tooshort' => 'Your biography must be at least be $1 words long.', |
| 40 | + 'requestaccount-exts' => 'Attachment file type is disallowed.', |
| 41 | + 'requestaccount-resub' => 'Your CV/resume file must be re-selected for security reasons. Leave the field blank |
| 42 | + if you no longer want to include one.', |
39 | 43 | 'requestaccount-tos' => 'I have read and agree to abide by the [[{{NS:PROJECT}}:Terms of Service|Terms of Service]] of {{SITENAME}}. |
40 | 44 | The name I have specified under "Real name" is in fact my own real name.', |
41 | | - 'requestaccount-submit' => 'Request account', |
| 45 | + 'requestaccount-submit' => 'Request account', |
42 | 46 | 'requestaccount-sent' => 'Your account request has successfully been sent and is now pending review.', |
43 | 47 | 'request-account-econf' => 'Your e-mail address has been confirmed and will be listed as such in your account |
44 | 48 | request.', |
— | — | @@ -82,17 +86,22 @@ |
83 | 87 | 'confirmaccount-email-q' => 'Email', |
84 | 88 | 'confirmaccount-bio' => 'Biography:', |
85 | 89 | 'confirmaccount-bio-q' => 'Biography', |
| 90 | + 'confirmaccount-attach' => 'Resume/CV:', |
86 | 91 | 'confirmaccount-notes' => 'Additional notes:', |
87 | 92 | 'confirmaccount-urls' => 'List of websites:', |
88 | 93 | 'confirmaccount-nourls' => '(None provided)', |
89 | 94 | 'confirmaccount-review' => 'Approve/Reject', |
90 | | - 'confirmaccount-confirm' => 'Use the buttons below to accept this request or deny it.', |
| 95 | + 'confirmaccount-confirm' => 'Use the options below to accept, deny, or hold this request:', |
91 | 96 | 'confirmaccount-econf' => '(confirmed)', |
92 | 97 | 'confirmaccount-reject' => '(rejected by [[User:$1|$1]] on $2)', |
| 98 | + 'confirmaccount-held' => '(marked "on hold" by [[User:$1|$1]] on $2)', |
93 | 99 | 'confirmaccount-create' => 'Accept (create account)', |
94 | 100 | 'confirmaccount-deny' => 'Reject (delist)', |
| 101 | + 'confirmaccount-hold' => 'Mark as "on hold"', |
95 | 102 | 'confirmaccount-reason' => 'Comment (will be included in email):', |
| 103 | + 'confirmaccount-ip' => 'IP address:', |
96 | 104 | 'confirmaccount-submit' => 'Confirm', |
| 105 | + 'confirmaccount-needreason' => 'You must provide a reason in the comment box below.', |
97 | 106 | 'confirmaccount-acc' => 'Account request confirmed successfully; created new user account [[User:$1]].', |
98 | 107 | 'confirmaccount-rej' => 'Account request rejected successfully.', |
99 | 108 | 'confirmaccount-summary' => 'Creating user page with biography of new user.', |
— | — | @@ -128,6 +137,12 @@ |
129 | 138 | $2 |
130 | 139 | |
131 | 140 | There may be contact lists on site that you can use if you want to know more about user account policy.', |
| 141 | + 'confirmaccount-email-body5' => 'Before your request for an account "$1" can be accepted on {{SITENAME}} |
| 142 | + you must first provide some additional information. |
| 143 | + |
| 144 | +$2 |
| 145 | + |
| 146 | +There may be contact lists on site that you can use if you want to know more about user account policy.', |
132 | 147 | ); |
133 | 148 | |
134 | 149 | $wgConfirmAccountMessages['ar'] = array( |