* Allow blocks on anonymous users only.
[lhc/web/wiklou.git] / includes / Block.php
1 <?php
2 /**
3 * Blocks and bans object
4 * @package MediaWiki
5 */
6
7 /**
8 * The block class
9 * All the functions in this class assume the object is either explicitly
10 * loaded or filled. It is not load-on-demand. There are no accessors.
11 *
12 * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags
13 *
14 * @todo This could be used everywhere, but it isn't.
15 * @package MediaWiki
16 */
17 class Block
18 {
19 /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry,
20 $mRangeStart, $mRangeEnd, $mAnonOnly;
21 /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName;
22
23 const EB_KEEP_EXPIRED = 1;
24 const EB_FOR_UPDATE = 2;
25 const EB_RANGE_ONLY = 4;
26
27 function Block( $address = '', $user = '', $by = 0, $reason = '',
28 $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0 )
29 {
30 $this->mId = 0;
31 $this->mAddress = $address;
32 $this->mUser = $user;
33 $this->mBy = $by;
34 $this->mReason = $reason;
35 $this->mTimestamp = wfTimestamp(TS_MW,$timestamp);
36 $this->mAuto = $auto;
37 $this->mAnonOnly = $anonOnly;
38 $this->mCreateAccount = $createAccount;
39 $this->mExpiry = self::decodeExpiry( $expiry );
40
41 $this->mForUpdate = false;
42 $this->mFromMaster = false;
43 $this->mByName = false;
44 $this->initialiseRange();
45 }
46
47 static function newFromDB( $address, $user = 0, $killExpired = true )
48 {
49 $block = new Block();
50 $block->load( $address, $user, $killExpired );
51 if ( $block->isValid() ) {
52 return $block;
53 } else {
54 return null;
55 }
56 }
57
58 static function newFromID( $id )
59 {
60 $dbr =& wfGetDB( DB_SLAVE );
61 $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*',
62 array( 'ipb_id' => $id ), __METHOD__ ) );
63 $block = new Block;
64 if ( $block->loadFromResult( $res ) ) {
65 return $block;
66 } else {
67 return null;
68 }
69 }
70
71 function clear()
72 {
73 $this->mAddress = $this->mReason = $this->mTimestamp = '';
74 $this->mId = $this->mAnonOnly = $this->mCreateAccount =
75 $this->mAuto = $this->mUser = $this->mBy = 0;
76 $this->mByName = false;
77 }
78
79 /**
80 * Get the DB object and set the reference parameter to the query options
81 */
82 function &getDBOptions( &$options )
83 {
84 global $wgAntiLockFlags;
85 if ( $this->mForUpdate || $this->mFromMaster ) {
86 $db =& wfGetDB( DB_MASTER );
87 if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) {
88 $options = array();
89 } else {
90 $options = array( 'FOR UPDATE' );
91 }
92 } else {
93 $db =& wfGetDB( DB_SLAVE );
94 $options = array();
95 }
96 return $db;
97 }
98
99 /**
100 * Get a ban from the DB, with either the given address or the given username
101 *
102 * @param string $address The IP address of the user, or blank to skip IP blocks
103 * @param integer $user The user ID, or zero for anonymous users
104 * @param bool $killExpired Whether to delete expired rows while loading
105 *
106 */
107 function load( $address = '', $user = 0, $killExpired = true )
108 {
109 wfDebug( "Block::load: '$address', '$user', $killExpired\n" );
110
111 $options = array();
112 $db =& $this->getDBOptions( $options );
113
114 $ret = false;
115 $killed = false;
116
117 if ( 0 == $user && $address == '' ) {
118 # Invalid user specification, not blocked
119 $this->clear();
120 return false;
121 }
122
123 # Try user block
124 if ( $user ) {
125 $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ),
126 __METHOD__, $options ) );
127 if ( $this->loadFromResult( $res, $killExpired ) ) {
128 return true;
129 }
130 }
131
132 # Try IP block
133 if ( $address ) {
134 $conds = array( 'ipb_address' => $address );
135 if ( $user ) {
136 $conds['ipb_anon_only'] = 0;
137 }
138 $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) );
139 if ( $this->loadFromResult( $res, $killExpired ) ) {
140 return true;
141 }
142 }
143
144 # Try range block
145 if ( $this->loadRange( $address, $killExpired, $user == 0 ) ) {
146 return true;
147 }
148
149 # Give up
150 $this->clear();
151 return false;
152 }
153
154 /**
155 * Fill in member variables from a result wrapper
156 */
157 function loadFromResult( ResultWrapper $res, $killExpired = true ) {
158 $ret = false;
159 if ( 0 != $res->numRows() ) {
160 # Get first block
161 $row = $res->fetchObject();
162 $this->initFromRow( $row );
163
164 if ( $killExpired ) {
165 # If requested, delete expired rows
166 do {
167 $killed = $this->deleteIfExpired();
168 if ( $killed ) {
169 $row = $res->fetchObject();
170 if ( $row ) {
171 $this->initFromRow( $row );
172 }
173 }
174 } while ( $killed && $row );
175
176 # If there were any left after the killing finished, return true
177 if ( $row ) {
178 $ret = true;
179 }
180 } else {
181 $ret = true;
182 }
183 }
184 $res->free();
185 return $ret;
186 }
187
188 /**
189 * Search the database for any range blocks matching the given address, and
190 * load the row if one is found.
191 */
192 function loadRange( $address, $killExpired = true, $isAnon = true )
193 {
194 $iaddr = wfIP2Hex( $address );
195 if ( $iaddr === false ) {
196 # Invalid address
197 return false;
198 }
199
200 # Only scan ranges which start in this /16, this improves search speed
201 # Blocks should not cross a /16 boundary.
202 $range = substr( $iaddr, 0, 4 );
203
204 $options = array();
205 $db =& $this->getDBOptions( $options );
206 $conds = array(
207 "ipb_range_start LIKE '$range%'",
208 "ipb_range_start <= '$iaddr'",
209 "ipb_range_end >= '$iaddr'"
210 );
211 if ( !$isAnon ) {
212 $conds['ipb_anon_only'] = 0;
213 }
214
215 $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) );
216 $success = $this->loadFromResult( $res, $killExpired );
217 return $success;
218 }
219
220 /**
221 * Determine if a given integer IPv4 address is in a given CIDR network
222 */
223 function isAddressInRange( $addr, $range ) {
224 list( $network, $bits ) = wfParseCIDR( $range );
225 if ( $network !== false && $addr >> ( 32 - $bits ) == $network >> ( 32 - $bits ) ) {
226 return true;
227 } else {
228 return false;
229 }
230 }
231
232 function initFromRow( $row )
233 {
234 $this->mAddress = $row->ipb_address;
235 $this->mReason = $row->ipb_reason;
236 $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp);
237 $this->mUser = $row->ipb_user;
238 $this->mBy = $row->ipb_by;
239 $this->mAuto = $row->ipb_auto;
240 $this->mAnonOnly = $row->ipb_anon_only;
241 $this->mCreateAccount = $row->ipb_create_account;
242 $this->mId = $row->ipb_id;
243 $this->mExpiry = self::decodeExpiry( $row->ipb_expiry );
244 if ( isset( $row->user_name ) ) {
245 $this->mByName = $row->user_name;
246 } else {
247 $this->mByName = false;
248 }
249 $this->mRangeStart = $row->ipb_range_start;
250 $this->mRangeEnd = $row->ipb_range_end;
251 }
252
253 function initialiseRange()
254 {
255 $this->mRangeStart = '';
256 $this->mRangeEnd = '';
257 if ( $this->mUser == 0 ) {
258 list( $network, $bits ) = wfParseCIDR( $this->mAddress );
259 if ( $network !== false ) {
260 $this->mRangeStart = sprintf( '%08X', $network );
261 $this->mRangeEnd = sprintf( '%08X', $network + (1 << (32 - $bits)) - 1 );
262 }
263 }
264 }
265
266 /**
267 * Callback with a Block object for every block
268 * @return integer number of blocks;
269 */
270 /*static*/ function enumBlocks( $callback, $tag, $flags = 0 )
271 {
272 global $wgAntiLockFlags;
273
274 $block = new Block();
275 if ( $flags & Block::EB_FOR_UPDATE ) {
276 $db =& wfGetDB( DB_MASTER );
277 if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) {
278 $options = '';
279 } else {
280 $options = 'FOR UPDATE';
281 }
282 $block->forUpdate( true );
283 } else {
284 $db =& wfGetDB( DB_SLAVE );
285 $options = '';
286 }
287 if ( $flags & Block::EB_RANGE_ONLY ) {
288 $cond = " AND ipb_range_start <> ''";
289 } else {
290 $cond = '';
291 }
292
293 $now = wfTimestampNow();
294
295 extract( $db->tableNames( 'ipblocks', 'user' ) );
296
297 $sql = "SELECT $ipblocks.*,user_name FROM $ipblocks,$user " .
298 "WHERE user_id=ipb_by $cond ORDER BY ipb_timestamp DESC $options";
299 $res = $db->query( $sql, 'Block::enumBlocks' );
300 $num_rows = $db->numRows( $res );
301
302 while ( $row = $db->fetchObject( $res ) ) {
303 $block->initFromRow( $row );
304 if ( ( $flags & Block::EB_RANGE_ONLY ) && $block->mRangeStart == '' ) {
305 continue;
306 }
307
308 if ( !( $flags & Block::EB_KEEP_EXPIRED ) ) {
309 if ( $block->mExpiry && $now > $block->mExpiry ) {
310 $block->delete();
311 } else {
312 call_user_func( $callback, $block, $tag );
313 }
314 } else {
315 call_user_func( $callback, $block, $tag );
316 }
317 }
318 wfFreeResult( $res );
319 return $num_rows;
320 }
321
322 function delete()
323 {
324 if (wfReadOnly()) {
325 return false;
326 }
327 if ( !$this->mId ) {
328 throw new MWException( "Block::delete() now requires that the mId member be filled\n" );
329 }
330
331 $dbw =& wfGetDB( DB_MASTER );
332 $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ );
333 return $dbw->affectedRows() > 0;
334 }
335
336 function insert()
337 {
338 wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
339 $dbw =& wfGetDB( DB_MASTER );
340 $dbw->begin();
341
342 # Don't collide with expired blocks
343 Block::purgeExpired();
344
345 $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val');
346 $dbw->insert( 'ipblocks',
347 array(
348 'ipb_id' => $ipb_id,
349 'ipb_address' => $this->mAddress,
350 'ipb_user' => $this->mUser,
351 'ipb_by' => $this->mBy,
352 'ipb_reason' => $this->mReason,
353 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
354 'ipb_auto' => $this->mAuto,
355 'ipb_anon_only' => $this->mAnonOnly,
356 'ipb_create_account' => $this->mCreateAccount,
357 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ),
358 'ipb_range_start' => $this->mRangeStart,
359 'ipb_range_end' => $this->mRangeEnd,
360 ), 'Block::insert', array( 'IGNORE' )
361 );
362 $affected = $dbw->affectedRows();
363 $dbw->commit();
364 return $affected;
365 }
366
367 function deleteIfExpired()
368 {
369 $fname = 'Block::deleteIfExpired';
370 wfProfileIn( $fname );
371 if ( $this->isExpired() ) {
372 wfDebug( "Block::deleteIfExpired() -- deleting\n" );
373 $this->delete();
374 $retVal = true;
375 } else {
376 wfDebug( "Block::deleteIfExpired() -- not expired\n" );
377 $retVal = false;
378 }
379 wfProfileOut( $fname );
380 return $retVal;
381 }
382
383 function isExpired()
384 {
385 wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" );
386 if ( !$this->mExpiry ) {
387 return false;
388 } else {
389 return wfTimestampNow() > $this->mExpiry;
390 }
391 }
392
393 function isValid()
394 {
395 return $this->mAddress != '';
396 }
397
398 function updateTimestamp()
399 {
400 if ( $this->mAuto ) {
401 $this->mTimestamp = wfTimestamp();
402 $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp );
403
404 $dbw =& wfGetDB( DB_MASTER );
405 $dbw->update( 'ipblocks',
406 array( /* SET */
407 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
408 'ipb_expiry' => $dbw->timestamp($this->mExpiry),
409 ), array( /* WHERE */
410 'ipb_address' => $this->mAddress
411 ), 'Block::updateTimestamp'
412 );
413 }
414 }
415
416 /*
417 function getIntegerAddr()
418 {
419 return $this->mIntegerAddr;
420 }
421
422 function getNetworkBits()
423 {
424 return $this->mNetworkBits;
425 }*/
426
427 function getByName()
428 {
429 if ( $this->mByName === false ) {
430 $this->mByName = User::whoIs( $this->mBy );
431 }
432 return $this->mByName;
433 }
434
435 function forUpdate( $x = NULL ) {
436 return wfSetVar( $this->mForUpdate, $x );
437 }
438
439 function fromMaster( $x = NULL ) {
440 return wfSetVar( $this->mFromMaster, $x );
441 }
442
443 function getRedactedName() {
444 if ( $this->mAuto ) {
445 return '#' . $this->mId;
446 } else {
447 return $this->mAddress;
448 }
449 }
450
451 /**
452 * Encode expiry for DB
453 */
454 static function encodeExpiry( $expiry, $db ) {
455 if ( $expiry == '' || $expiry == Block::infinity() ) {
456 return Block::infinity();
457 } else {
458 return $db->timestamp( $expiry );
459 }
460 }
461
462 /**
463 * Decode expiry which has come from the DB
464 */
465 static function decodeExpiry( $expiry ) {
466 if ( $expiry == '' || $expiry == Block::infinity() ) {
467 return Block::infinity();
468 } else {
469 return wfTimestamp( TS_MW, $expiry );
470 }
471 }
472
473 static function getAutoblockExpiry( $timestamp )
474 {
475 global $wgAutoblockExpiry;
476 return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry );
477 }
478
479 static function normaliseRange( $range )
480 {
481 $parts = explode( '/', $range );
482 if ( count( $parts ) == 2 ) {
483 $shift = 32 - $parts[1];
484 $ipint = wfIP2Unsigned( $parts[0] );
485 $ipint = $ipint >> $shift << $shift;
486 $newip = long2ip( $ipint );
487 $range = "$newip/{$parts[1]}";
488 }
489 return $range;
490 }
491
492 /**
493 * Purge expired blocks from the ipblocks table
494 */
495 static function purgeExpired() {
496 $dbw =& wfGetDB( DB_MASTER );
497 $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ );
498 }
499
500 static function infinity() {
501 # This is a special keyword for timestamps in PostgreSQL, and
502 # works with CHAR(14) as well because "i" sorts after all numbers.
503 return 'infinity';
504
505 /*
506 static $infinity;
507 if ( !isset( $infinity ) ) {
508 $dbr =& wfGetDB( DB_SLAVE );
509 $infinity = $dbr->bigTimestamp();
510 }
511 return $infinity;
512 */
513 }
514
515 }
516 ?>