Index: trunk/extensions/CheckUser/cu_changes.sql |
— | — | @@ -51,6 +51,9 @@ |
52 | 52 | -- User agent |
53 | 53 | cuc_agent VARCHAR(255) BINARY default NULL, |
54 | 54 | |
| 55 | + -- Last username from session/cookie data |
| 56 | + cuc_cookie_user VARCHAR(255) BINARY default NULL, |
| 57 | + |
55 | 58 | PRIMARY KEY cuc_id (cuc_id), |
56 | 59 | INDEX cuc_ip_hex_time (cuc_ip_hex,cuc_timestamp), |
57 | 60 | INDEX cuc_user_time (cuc_user,cuc_timestamp), |
Index: trunk/extensions/CheckUser/patch-cu_cookies.sql |
— | — | @@ -0,0 +1,6 @@ |
| 2 | +-- Adds the cookie data column needed to enable $wgCURecordCookies |
| 3 | +-- vim: autoindent syn=mysql sts=2 sw=2 |
| 4 | +-- Replace /*$wgDBprefix*/ with the proper prefix |
| 5 | + |
| 6 | +ALTER TABLE /*$wgDBprefix*/cu_changes |
| 7 | + ADD cuc_cookie_user VARCHAR(255) BINARY default NULL; |
Property changes on: trunk/extensions/CheckUser/patch-cu_cookies.sql |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 8 | + native |
Index: trunk/extensions/CheckUser/CheckUser_body.php |
— | — | @@ -120,6 +120,19 @@ |
121 | 121 | # Output form |
122 | 122 | $wgOut->addHTML( $form ); |
123 | 123 | } |
| 124 | + |
| 125 | + /** |
| 126 | + * As we use the same small set of messages in various methods and that |
| 127 | + * they are called often, we call them once and save them in $this->message |
| 128 | + */ |
| 129 | + function preCacheMessages() { |
| 130 | + // Precache various messages |
| 131 | + if( !isset( $this->message ) ) { |
| 132 | + foreach( explode(' ', 'diff hist minoreditletter newpageletter blocklink checkuser-pagelogs' ) as $msg ) { |
| 133 | + $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); |
| 134 | + } |
| 135 | + } |
| 136 | + } |
124 | 137 | |
125 | 138 | /** |
126 | 139 | * @param string $ip |
— | — | @@ -181,24 +194,11 @@ |
182 | 195 | } |
183 | 196 | |
184 | 197 | /** |
185 | | - * As we use the same small set of messages in various methods and that |
186 | | - * they are called often, we call them once and save them in $this->message |
187 | | - */ |
188 | | - function preCacheMessages() { |
189 | | - // Precache various messages |
190 | | - if( !isset( $this->message ) ) { |
191 | | - foreach( explode(' ', 'diff hist minoreditletter newpageletter blocklink log' ) as $msg ) { |
192 | | - $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); |
193 | | - } |
194 | | - } |
195 | | - } |
196 | | - |
197 | | - /** |
198 | 198 | * @param $row |
199 | 199 | * @return a streamlined recent changes line with IP data |
200 | 200 | */ |
201 | 201 | function CUChangesLine( $row ) { |
202 | | - global $wgLang; |
| 202 | + global $wgLang, $wgCURecordCookieData; |
203 | 203 | |
204 | 204 | # Add date headers |
205 | 205 | $date = $wgLang->date( $row->cuc_timestamp, true, true ); |
— | — | @@ -226,21 +226,27 @@ |
227 | 227 | $line .= $this->skin->commentBlock( $row->cuc_comment ); |
228 | 228 | |
229 | 229 | $cuTitle = SpecialPage::getTitleFor( 'CheckUser' ); |
230 | | - $line .= '<br/> <small>'; |
231 | | - # IP |
232 | | - $line .= ' <strong>IP</strong>: '.$this->skin->makeKnownLinkObj( $cuTitle, |
233 | | - htmlspecialchars( $row->cuc_ip ), |
234 | | - "user=" . urlencode( $row->cuc_ip ) ); |
235 | | - # XFF |
| 230 | + $line .= '<br/> <small>'; |
| 231 | + # IP address |
| 232 | + $line .= ' <strong>' . wfMsgHtml('checkuser-ip') . '</strong> ' . |
| 233 | + $this->skin->makeKnownLinkObj( $cuTitle, htmlspecialchars($row->cuc_ip), |
| 234 | + "user=" . urlencode($row->cuc_ip) ); |
| 235 | + # XFF chain |
236 | 236 | if( $row->cuc_xff !=null ) { |
237 | 237 | # Flag our trusted proxies |
238 | 238 | list($client,$trusted) = wfGetClientIPfromXFF($row->cuc_xff,$row->cuc_ip); |
239 | 239 | $c = $trusted ? '#F0FFF0' : '#FFFFCC'; |
240 | | - $line .= '</span> <span style="background-color: '.$c.'"><strong>XFF</strong>: '; |
| 240 | + $line .= '</span> <span style="background-color: '.$c.'">' . |
| 241 | + '<strong>' . wfMsgHtml('checkuser-xff') . '</strong> '; |
241 | 242 | $line .= $this->skin->makeKnownLinkObj( $cuTitle, |
242 | 243 | htmlspecialchars( $row->cuc_xff ), |
243 | 244 | "user=" . urlencode( $client ) . "/xff" )."</span>"; |
244 | 245 | } |
| 246 | + # Previous username |
| 247 | + if( $wgCURecordCookieData && $row->cuc_cookie_user !=null ) { |
| 248 | + $line .= ' <strong>' . wfMsgHtml('checkuser-prev-name') . '</strong> ' . |
| 249 | + $this->skin->userLink( -1, $row->cuc_cookie_user ); |
| 250 | + } |
245 | 251 | $line .= "</small></li>\n"; |
246 | 252 | |
247 | 253 | return $line; |
— | — | @@ -252,19 +258,22 @@ |
253 | 259 | */ |
254 | 260 | function getLinkFromRow( $row ) { |
255 | 261 | if( $row->cuc_type == RC_LOG && $row->cuc_namespace == NS_SPECIAL ) { |
256 | | - //Log items (old format) and events to logs |
| 262 | + // Log items (old format) and events to logs |
257 | 263 | list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $row->cuc_title ); |
258 | 264 | $logname = LogPage::logName( $logtype ); |
259 | 265 | $title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title ); |
260 | 266 | $links = '(' . $this->skin->makeKnownLinkObj( $title, $logname ) . ')'; |
261 | 267 | } elseif( $row->cuc_type == RC_LOG ) { |
262 | | - //Log items |
| 268 | + // Log items |
263 | 269 | $specialTitle = SpecialPage::getTitleFor( 'Log' ); |
264 | 270 | $title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title ); |
265 | | - $links = '(' . $this->skin->makeKnownLinkObj( $specialTitle, $this->message['log'], |
| 271 | + $links = '(' . $this->skin->makeKnownLinkObj( $specialTitle, $this->message['checkuser-pagelogs'], |
266 | 272 | wfArrayToCGI( array('page' => $title->getPrefixedText() ) ) ) . ')'; |
| 273 | + } elseif( $row->cuc_type == CU_ALT_LOGIN ) { |
| 274 | + // Login events |
| 275 | + $links = '(' . wfMsg('checkuser-login') . ')'; |
267 | 276 | } elseif( !is_null( $row->cuc_this_oldid ) ) { |
268 | | - //Everything else |
| 277 | + // Everything else |
269 | 278 | $title = Title::makeTitle( $row->cuc_namespace, $row->cuc_title ); |
270 | 279 | #new pages |
271 | 280 | if( $row->cuc_type == RC_NEW ) { |
— | — | @@ -397,7 +406,7 @@ |
398 | 407 | # Flag our trusted proxies |
399 | 408 | list($client,$trusted) = wfGetClientIPfromXFF($set[1],$set[0]); |
400 | 409 | $c = $trusted ? '#F0FFF0' : '#FFFFCC'; |
401 | | - $s .= ' <span style="background-color: '.$c.'"><strong>XFF</strong>: '; |
| 410 | + $s .= ' <span style="background-color: '.$c.'"><strong>'.wfMsgHtml('checkuser-ip').'</strong> '; |
402 | 411 | $s .= $sk->makeKnownLinkObj( $wgTitle, |
403 | 412 | htmlspecialchars( $set[1] ), |
404 | 413 | "user=" . urlencode( $client ) . "/xff" )."</span>"; |
Index: trunk/extensions/CheckUser/CheckUser.i18n.php |
— | — | @@ -31,6 +31,11 @@ |
32 | 32 | 'checkuser-log-fail' => 'Unable to add log entry', |
33 | 33 | 'checkuser-nolog' => 'No log file found.', |
34 | 34 | 'checkuser-blocked' => 'Blocked', |
| 35 | + 'checkuser-login' => 'Logged in, but had a valid session for another account', |
| 36 | + 'checkuser-xff' => 'XFF:', |
| 37 | + 'checkuser-ip' => 'IP:', |
| 38 | + 'checkuser-prev-name' => 'Previous session:', |
| 39 | + 'checkuser-pagelogs' => 'page logs', |
35 | 40 | ); |
36 | 41 | $wgCheckUserMessages['ar'] = array( |
37 | 42 | 'checkuser' => 'افحص مستخدم', |
Index: trunk/extensions/CheckUser/CheckUser.php |
— | — | @@ -6,6 +6,9 @@ |
7 | 7 | exit(1); |
8 | 8 | } |
9 | 9 | |
| 10 | +# RC_MOVE not used in this table, so no overlap |
| 11 | +define( 'CU_ALT_LOGIN', 2); |
| 12 | + |
10 | 13 | # Internationalisation file |
11 | 14 | require_once( 'CheckUser.i18n.php' ); |
12 | 15 | |
— | — | @@ -23,53 +26,75 @@ |
24 | 27 | |
25 | 28 | # How long to keep CU data? |
26 | 29 | $wgCUDMaxAge = 3 * 30 * 24 * 3600; |
| 30 | +# If set to true, usernames from leftover sessions for IP edits will be stored. |
| 31 | +# It will also be stored during login if the old session data for the usre is |
| 32 | +# for a different account. (Note that user renames can cause this). |
| 33 | +# If you have an older version of checkuser without the cuc_cookie_user column, |
| 34 | +# run patch-cu_cookies.sql before enabling this |
| 35 | +$wgCURecordCookieData = false; |
27 | 36 | |
28 | 37 | #Recent changes data hook |
29 | 38 | global $wgHooks; |
30 | 39 | $wgHooks['RecentChange_save'][] = 'efUpdateCheckUserData'; |
31 | 40 | $wgHooks['ParserTestTables'][] = 'efCheckUserParserTestTables'; |
| 41 | +$wgHooks['LoginAuthenticateAudit'][] = 'efCheckUserRecordLogin'; |
32 | 42 | |
33 | 43 | /** |
34 | 44 | * Hook function for RecentChange_save |
35 | 45 | * Saves user data into the cu_changes table |
36 | 46 | */ |
37 | 47 | function efUpdateCheckUserData( $rc ) { |
38 | | - $dbw = wfGetDB( DB_MASTER ); |
| 48 | + global $wgUser, $wgCURecordCookieData; |
| 49 | + // Extract params |
39 | 50 | extract( $rc->mAttribs ); |
40 | | - |
41 | | - // Convert all IPs to IPv6 if needed |
| 51 | + // Get IP |
42 | 52 | $ip = wfGetIP(); |
43 | | - |
| 53 | + // Get XFF header |
44 | 54 | $xff = wfGetForwardedFor(); |
45 | | - list($xff_ip,$trusted) = wfGetClientIPfromXFF( $xff ); |
| 55 | + list($xff_ip,$trusted) = efGetClientIPfromXFF( $xff ); |
46 | 56 | // Our squid XFFs can flood this up sometimes |
47 | | - $isSquidOnly = wfXFFChainIsSquid( $xff ); |
48 | | - |
| 57 | + $isSquidOnly = efXFFChainIsSquid( $xff ); |
| 58 | + // Get agent |
49 | 59 | $agent = wfGetAgent(); |
50 | | - |
51 | | - $dbw->insert( 'cu_changes', |
52 | | - array( |
53 | | - 'cuc_namespace' => $rc_namespace, |
54 | | - 'cuc_title' => $rc_title, |
55 | | - 'cuc_minor' => $rc_minor, |
56 | | - 'cuc_user' => $rc_user, |
57 | | - 'cuc_user_text' => $rc_user_text, |
58 | | - 'cuc_actiontext' => '', |
59 | | - 'cuc_comment' => $rc_comment, |
60 | | - 'cuc_page_id' => $rc_cur_id, |
61 | | - 'cuc_this_oldid' => $rc_this_oldid, |
62 | | - 'cuc_last_oldid' => $rc_last_oldid, |
63 | | - 'cuc_type' => $rc_type, |
64 | | - 'cuc_timestamp' => $rc_timestamp, |
65 | | - 'cuc_ip' => $ip, |
66 | | - 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, |
67 | | - 'cuc_xff' => !$isSquidOnly ? $xff : '', |
68 | | - 'cuc_xff_hex' => ($xff_ip && !$isSquidOnly) ? IP::toHex( $xff_ip ) : null, |
69 | | - 'cuc_agent' => $agent, |
70 | | - ), __METHOD__ |
| 60 | + // Store the log action text for log events |
| 61 | + // $rc_comment should just be the log_comment |
| 62 | + // BC: check if log_type and log_action exists |
| 63 | + // If not, then $rc_comment is the actiontext and comment |
| 64 | + if( isset($rc_log_type) && $rc_type==RC_LOG ) { |
| 65 | + $target = Title::makeTitle( $rc_namespace, $rc_title ); |
| 66 | + $actionText = LogPage::actionText( $rc_log_type, $rc_log_action, $target, NULL, explode('\n',$rc_params) ); |
| 67 | + } else { |
| 68 | + $actionText = ''; |
| 69 | + } |
| 70 | + |
| 71 | + $rcRow = array( |
| 72 | + 'cuc_namespace' => $rc_namespace, |
| 73 | + 'cuc_title' => $rc_title, |
| 74 | + 'cuc_minor' => $rc_minor, |
| 75 | + 'cuc_user' => $rc_user, |
| 76 | + 'cuc_user_text' => $rc_user_text, |
| 77 | + 'cuc_actiontext' => $actionText, |
| 78 | + 'cuc_comment' => $rc_comment, |
| 79 | + 'cuc_page_id' => $rc_cur_id, |
| 80 | + 'cuc_this_oldid' => $rc_this_oldid, |
| 81 | + 'cuc_last_oldid' => $rc_last_oldid, |
| 82 | + 'cuc_type' => $rc_type, |
| 83 | + 'cuc_timestamp' => $rc_timestamp, |
| 84 | + 'cuc_ip' => $ip, |
| 85 | + 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, |
| 86 | + 'cuc_xff' => !$isSquidOnly ? $xff : '', |
| 87 | + 'cuc_xff_hex' => ($xff_ip && !$isSquidOnly) ? IP::toHex( $xff_ip ) : null, |
| 88 | + 'cuc_agent' => $agent |
71 | 89 | ); |
| 90 | + // Fetch and add user cookie data |
| 91 | + if( $wgCURecordCookieData && $wgUser->isAnon() ) { |
| 92 | + $rcRow['cuc_cookie_user'] = efGetUsernameFromCookie(); |
| 93 | + } |
72 | 94 | |
73 | | - # Every 1000th edit, prune the checkuser changes table. |
| 95 | + $dbw = wfGetDB( DB_MASTER ); |
| 96 | + $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); |
| 97 | + |
| 98 | + # Every 100th edit, prune the checkuser changes table. |
74 | 99 | wfSeedRandom(); |
75 | 100 | if ( 0 == mt_rand( 0, 99 ) ) { |
76 | 101 | # Periodically flush old entries from the recentchanges table. |
— | — | @@ -85,13 +110,63 @@ |
86 | 111 | return true; |
87 | 112 | } |
88 | 113 | |
| 114 | +function efCheckUserRecordLogin( &$user, &$mPassword, &$retval ) { |
| 115 | + global $wgCURecordCookieData, $wgUser; |
| 116 | + // $wgCURecordCookieData must be enabled |
| 117 | + // Also, we only care for valid login attempts |
| 118 | + if( !$wgCURecordCookieData || $retval != LoginForm::SUCCESS ) |
| 119 | + return true; |
| 120 | + // Do not record re-logins |
| 121 | + if( $wgUser->getName() != $user->getName() ) |
| 122 | + return true; |
| 123 | + // Get IP |
| 124 | + $ip = wfGetIP(); |
| 125 | + // Get XFF header |
| 126 | + $xff = wfGetForwardedFor(); |
| 127 | + list($xff_ip,$trusted) = efGetClientIPfromXFF( $xff ); |
| 128 | + // Our squid XFFs can flood this up sometimes |
| 129 | + $isSquidOnly = efXFFChainIsSquid( $xff ); |
| 130 | + // Get agent |
| 131 | + $agent = wfGetAgent(); |
| 132 | + // Get cookie data |
| 133 | + $cuc_cookie_name = efGetUsernameFromCookie(); |
| 134 | + if( $cuc_cookie_name == $user->getName() ) |
| 135 | + return true; // Nothing special... |
| 136 | + |
| 137 | + $rcRow = array( |
| 138 | + 'cuc_namespace' => NS_USER, |
| 139 | + 'cuc_title' => $user->getName(), |
| 140 | + 'cuc_minor' => 0, |
| 141 | + 'cuc_user' => $user->getId(), |
| 142 | + 'cuc_user_text' => $user->getName(), |
| 143 | + 'cuc_actiontext' => '', |
| 144 | + 'cuc_comment' => '', |
| 145 | + 'cuc_page_id' => 0, |
| 146 | + 'cuc_this_oldid' => 0, |
| 147 | + 'cuc_last_oldid' => 0, |
| 148 | + 'cuc_type' => CU_RC_LOGIN, |
| 149 | + 'cuc_timestamp' => wfTimestampNow(), |
| 150 | + 'cuc_ip' => $ip, |
| 151 | + 'cuc_ip_hex' => $ip ? IP::toHex( $ip ) : null, |
| 152 | + 'cuc_xff' => !$isSquidOnly ? $xff : '', |
| 153 | + 'cuc_xff_hex' => ($xff_ip && !$isSquidOnly) ? IP::toHex( $xff_ip ) : null, |
| 154 | + 'cuc_agent' => $agent, |
| 155 | + 'cuc_cookie_user' => efGetUsernameFromCookie() |
| 156 | + ); |
| 157 | + |
| 158 | + $dbw = wfGetDB( DB_MASTER ); |
| 159 | + $dbw->insert( 'cu_changes', $rcRow, __METHOD__ ); |
| 160 | + |
| 161 | + return true; |
| 162 | +} |
| 163 | + |
89 | 164 | /** |
90 | 165 | * Locates the client IP within a given XFF string |
91 | 166 | * @param string $xff |
92 | 167 | * @param string $address, the ip that sent this header (optional) |
93 | 168 | * @return array( string, bool ) |
94 | 169 | */ |
95 | | -function wfGetClientIPfromXFF( $xff, $address=NULL ) { |
| 170 | +function efGetClientIPfromXFF( $xff, $address=NULL ) { |
96 | 171 | if ( !$xff ) return array(null, false); |
97 | 172 | // Avoid annoyingly long xff hacks |
98 | 173 | $xff = trim( substr( $xff, 0, 255 ) ); |
— | — | @@ -121,7 +196,12 @@ |
122 | 197 | return array( $client, $trusted ); |
123 | 198 | } |
124 | 199 | |
125 | | -function wfXFFChainIsSquid( $xff ) { |
| 200 | +/** |
| 201 | + * Determines if an XFF string is just local squid IPs |
| 202 | + * @param string $xff |
| 203 | + * @return bool |
| 204 | + */ |
| 205 | +function efXFFChainIsSquid( $xff ) { |
126 | 206 | global $wgSquidServers, $wgSquidServersNoPurge; |
127 | 207 | |
128 | 208 | if ( !$xff ) false; |
— | — | @@ -147,6 +227,41 @@ |
148 | 228 | } |
149 | 229 | |
150 | 230 | /** |
| 231 | + * Gets the username from client cookie |
| 232 | + * If the token is invalid for this user, this will return false |
| 233 | + * @return string |
| 234 | + */ |
| 235 | +function efGetUsernameFromCookie() { |
| 236 | + global $wgCookiePrefix; |
| 237 | + |
| 238 | + // Try to get name from session |
| 239 | + if( isset( $_SESSION['wsUserName'] ) ) { |
| 240 | + $name = $_SESSION['wsUserName']; |
| 241 | + // Try cookie |
| 242 | + } else if( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) { |
| 243 | + $name = $_COOKIE["{$wgCookiePrefix}UserName"]; |
| 244 | + } else { |
| 245 | + return false; |
| 246 | + } |
| 247 | + // Load the supposed user |
| 248 | + $u = User::newFromName( $name ); |
| 249 | + $u->load(); |
| 250 | + // Validate the token |
| 251 | + if( isset( $_SESSION['wsToken'] ) ) { |
| 252 | + $passwordCorrect = $_SESSION['wsToken'] == $u->mToken; |
| 253 | + } else if( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) { |
| 254 | + $passwordCorrect = $u->mToken == $_COOKIE["{$wgCookiePrefix}Token"]; |
| 255 | + } else { |
| 256 | + return false; |
| 257 | + } |
| 258 | + // User must have proper credentials |
| 259 | + if( !$passwordCorrect ) |
| 260 | + return false; |
| 261 | + |
| 262 | + return $name; |
| 263 | +} |
| 264 | + |
| 265 | +/** |
151 | 266 | * Tell the parser test engine to create a stub cu_changes table, |
152 | 267 | * or temporary pages won't save correctly during the test run. |
153 | 268 | */ |
— | — | @@ -159,5 +274,3 @@ |
160 | 275 | require( dirname(__FILE__) . '/../ExtensionFunctions.php' ); |
161 | 276 | } |
162 | 277 | extAddSpecialPage( dirname(__FILE__) . '/CheckUser_body.php', 'CheckUser', 'CheckUser' ); |
163 | | - |
164 | | - |