r86180 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r86179‎ | r86180 | r86181 >
Date:04:22, 16 April 2011
Author:aaron
Status:ok
Tags:
Comment:
Moved FlaggedRevsHooks to /dataclasses and split off new FlaggedRevsTestHooks class under /tests
Modified paths:
  • /trunk/extensions/FlaggedRevs/FlaggedRevs.hooks.php (deleted) (history)
  • /trunk/extensions/FlaggedRevs/FlaggedRevs.php (modified) (history)
  • /trunk/extensions/FlaggedRevs/dataclasses/FlaggedRevs.hooks.php (added) (history)
  • /trunk/extensions/FlaggedRevs/tests/FlaggedRevsTest.hooks.php (added) (history)

Diff [purge]

Index: trunk/extensions/FlaggedRevs/FlaggedRevs.hooks.php
@@ -1,1028 +0,0 @@
2 -<?php
3 -/**
4 - * Class containing hooked functions for a FlaggedRevs environment
5 - */
6 -class FlaggedRevsHooks {
7 - /**
8 - * Update flaggedrevs table on revision restore
9 - */
10 - public static function onRevisionRestore( $title, Revision $revision, $oldPageID ) {
11 - $dbw = wfGetDB( DB_MASTER );
12 - # Some revisions may have had null rev_id values stored when deleted.
13 - # This hook is called after insertOn() however, in which case it is set
14 - # as a new one.
15 - $dbw->update( 'flaggedrevs',
16 - array( 'fr_page_id' => $revision->getPage() ),
17 - array( 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revision->getID() ),
18 - __METHOD__
19 - );
20 - return true;
21 - }
22 -
23 - /**
24 - * Update flaggedrevs page/tracking tables (revision moving)
25 - */
26 - public static function onArticleMergeComplete( Title $sourceTitle, Title $destTitle ) {
27 - $oldPageID = $sourceTitle->getArticleID();
28 - $newPageID = $destTitle->getArticleID();
29 - # Get flagged revisions from old page id that point to destination page
30 - $dbw = wfGetDB( DB_MASTER );
31 - $result = $dbw->select(
32 - array( 'flaggedrevs', 'revision' ),
33 - array( 'fr_rev_id' ),
34 - array( 'fr_page_id' => $oldPageID,
35 - 'fr_rev_id = rev_id',
36 - 'rev_page' => $newPageID ),
37 - __METHOD__
38 - );
39 - # Update these rows
40 - $revIDs = array();
41 - foreach( $result as $row ) {
42 - $revIDs[] = $row->fr_rev_id;
43 - }
44 - if ( !empty( $revIDs ) ) {
45 - $dbw->update( 'flaggedrevs',
46 - array( 'fr_page_id' => $newPageID ),
47 - array( 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revIDs ),
48 - __METHOD__
49 - );
50 - }
51 - # Update pages...stable versions possibly lost to another page
52 - FlaggedRevs::stableVersionUpdates( $sourceTitle );
53 - FlaggedRevs::HTMLCacheUpdates( $sourceTitle );
54 - FlaggedRevs::stableVersionUpdates( $destTitle );
55 - FlaggedRevs::HTMLCacheUpdates( $destTitle );
56 - return true;
57 - }
58 -
59 - /**
60 - * (a) Update flaggedrevs page/tracking tables
61 - * (b) Autoreview pages moved into content NS
62 - */
63 - public static function onTitleMoveComplete(
64 - Title $otitle, Title $ntitle, $user, $pageId
65 - ) {
66 - $fa = FlaggedPage::getTitleInstance( $ntitle );
67 - $fa->loadFromDB( FR_MASTER );
68 - // Re-validate NS/config (new title may not be reviewable)
69 - if ( $fa->isReviewable() ) {
70 - // Moved from non-reviewable to reviewable NS?
71 - // Auto-review such edits like new pages...
72 - if ( !FlaggedRevs::inReviewNamespace( $otitle )
73 - && FlaggedRevs::autoReviewNewPages()
74 - && $ntitle->userCan( 'autoreview' ) )
75 - {
76 - $rev = Revision::newFromTitle( $ntitle );
77 - if ( $rev ) { // sanity
78 - FlaggedRevs::autoReviewEdit( $fa, $user, $rev );
79 - }
80 - }
81 - }
82 - # Update page and tracking tables and clear cache
83 - FlaggedRevs::stableVersionUpdates( $otitle );
84 - FlaggedRevs::HTMLCacheUpdates( $otitle );
85 - FlaggedRevs::stableVersionUpdates( $ntitle );
86 - FlaggedRevs::HTMLCacheUpdates( $ntitle );
87 - return true;
88 - }
89 -
90 - /**
91 - * (a) Update flaggedrevs page/tracking tables
92 - * (b) Pages with stable versions that use this page will be purged
93 - * Note: pages with current versions that use this page should already be purged
94 - */
95 - public static function onArticleEditUpdates( Article $article ) {
96 - FlaggedRevs::stableVersionUpdates( $article->getTitle() );
97 - FlaggedRevs::extraHTMLCacheUpdate( $article->getTitle() );
98 - return true;
99 - }
100 -
101 - /**
102 - * (a) Update flaggedrevs page/tracking tables
103 - * (b) Pages with stable versions that use this page will be purged
104 - * Note: pages with current versions that use this page should already be purged
105 - */
106 - public static function onArticleDelete( Article $article, $user, $reason, $id ) {
107 - FlaggedRevs::clearTrackingRows( $id );
108 - FlaggedRevs::extraHTMLCacheUpdate( $article->getTitle() );
109 - return true;
110 - }
111 -
112 - /**
113 - * (a) Update flaggedrevs page/tracking tables
114 - * (b) Pages with stable versions that use this page will be purged
115 - * Note: pages with current versions that use this page should already be purged
116 - */
117 - public static function onArticleUndelete( Title $title ) {
118 - FlaggedRevs::stableVersionUpdates( $title );
119 - FlaggedRevs::HTMLCacheUpdates( $title );
120 - return true;
121 - }
122 -
123 - /**
124 - * (a) Update flaggedrevs page/tracking tables
125 - * (b) Pages with stable versions that use this page will be purged
126 - * Note: pages with current versions that use this page should already be purged
127 - */
128 - public static function onFileUpload( File $file ) {
129 - FlaggedRevs::stableVersionUpdates( $file->getTitle() );
130 - FlaggedRevs::extraHTMLCacheUpdate( $file->getTitle() );
131 - return true;
132 - }
133 -
134 - /**
135 - * Update flaggedrevs page/tracking tables
136 - */
137 - public static function onRevisionDelete( Title $title ) {
138 - $changed = FlaggedRevs::stableVersionUpdates( $title );
139 - if ( $changed ) {
140 - FlaggedRevs::HTMLCacheUpdates( $title );
141 - }
142 - return true;
143 - }
144 -
145 - /**
146 - * Select the desired templates based on the selected stable revision IDs
147 - * Note: $parser can be false
148 - */
149 - public static function parserFetchStableTemplate( $parser, Title $title, &$skip, &$id ) {
150 - if ( !( $parser instanceof Parser ) || $title->getNamespace() < 0 ) {
151 - return true; // nothing to do
152 - }
153 - $incManager = FRInclusionManager::singleton();
154 - if ( !$incManager->parserOutputIsStabilized() ) {
155 - return true; // trigger for stable version parsing only
156 - }
157 - $id = false; // current version
158 - # Check for the version of this template used when reviewed...
159 - $maybeId = $incManager->getReviewedTemplateVersion( $title );
160 - if ( $maybeId !== null ) {
161 - $id = (int)$maybeId; // use if specified (even 0)
162 - }
163 - # Check for stable version of template if this feature is enabled...
164 - if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE ) {
165 - $maybeId = $incManager->getStableTemplateVersion( $title );
166 - # Take the newest of these two...
167 - if ( $maybeId && $maybeId > $id ) {
168 - $id = (int)$maybeId;
169 - }
170 - }
171 - # If $id is zero, don't bother loading it (use blue/red link)
172 - if ( $id === 0 ) {
173 - $skip = true;
174 - }
175 - return true;
176 - }
177 -
178 - /**
179 - * Select the desired images based on the selected stable version time/SHA-1
180 - */
181 - public static function parserFetchStableFile( $parser, Title $title, &$time, &$sha1, &$query ) {
182 - if ( !( $parser instanceof Parser ) ) {
183 - return true; // nothing to do
184 - }
185 - $incManager = FRInclusionManager::singleton();
186 - if ( !$incManager->parserOutputIsStabilized() ) {
187 - return true; // trigger for stable version parsing only
188 - }
189 - # Normalize NS_MEDIA to NS_FILE
190 - if ( $title->getNamespace() == NS_MEDIA ) {
191 - $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
192 - $title->resetArticleId( $title->getArticleId() ); // avoid extra queries
193 - } else {
194 - $title =& $title;
195 - }
196 - $time = $sha1 = false; // current version
197 - # Check for the version of this file used when reviewed...
198 - list( $maybeTS, $maybeSha1 ) = $incManager->getReviewedFileVersion( $title );
199 - if ( $maybeTS !== null ) {
200 - $time = $maybeTS; // use if specified (even '0')
201 - $sha1 = $maybeSha1;
202 - }
203 - # Check for stable version of file if this feature is enabled...
204 - if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE ) {
205 - list( $maybeTS, $maybeSha1 ) = $incManager->getStableFileVersion( $title );
206 - # Take the newest of these two...
207 - if ( $maybeTS && $maybeTS > $time ) {
208 - $time = $maybeTS;
209 - $sha1 = $maybeSha1;
210 - }
211 - }
212 - # Stabilize the file link
213 - if ( $time ) {
214 - if ( $query != '' ) $query .= '&';
215 - $query = "filetimestamp=" . urlencode( wfTimestamp( TS_MW, $time ) );
216 - }
217 - return true;
218 - }
219 -
220 - public static function onParserFirstCallInit( &$parser ) {
221 - $parser->setFunctionHook( 'pagesusingpendingchanges',
222 - 'FlaggedRevsHooks::parserPagesUsingPendingChanges' );
223 - return true;
224 - }
225 -
226 - public static function onLanguageGetMagic( &$magicWords, $langCode ) {
227 - $magicWords['pagesusingpendingchanges'] = array( 0, 'pagesusingpendingchanges' );
228 - $magicWords['pendingchangelevel'] = array( 0, 'pendingchangelevel' );
229 - return true;
230 - }
231 -
232 - public static function onParserGetVariableValueSwitch( &$parser, &$cache, &$word, &$ret ) {
233 - if ( $word == 'pendingchangelevel' ) {
234 - $title = $parser->getTitle();
235 - if ( !FlaggedRevs::inReviewNamespace( $title ) ) {
236 - $ret = '';
237 - } else {
238 - $config = FlaggedPageConfig::getStabilitySettings( $title );
239 - $ret = $config['autoreview'];
240 - }
241 - }
242 - return true;
243 - }
244 -
245 - public static function onMagicWordwgVariableIDs( &$words ) {
246 - $words[] = 'pendingchangelevel';
247 - return true;
248 - }
249 -
250 - public static function parserPagesUsingPendingChanges( &$parser, $ns = '' ) {
251 - $nsList = FlaggedRevs::getReviewNamespaces();
252 - if ( !$nsList ) {
253 - return 0;
254 - }
255 -
256 - if ( $ns !== '' ) {
257 - $ns = intval( $ns );
258 - if ( !in_array( $ns, $nsList ) ) {
259 - return 0;
260 - }
261 - }
262 -
263 - static $pcCounts = null;
264 - if ( !$pcCounts ) {
265 - $dbr = wfGetDB( DB_SLAVE );
266 - $res = $dbr->select( 'flaggedrevs_stats', '*', array(), __METHOD__ );
267 - $totalCount = 0;
268 - foreach( $res as $row ) {
269 - $nsList[ "ns-{$row->namespace}" ] = $row->reviewed;
270 - $totalCount += $row->reviewed;
271 - }
272 - $nsList[ 'all' ] = $totalCount;
273 - }
274 -
275 - if ( $ns === '' ) {
276 - return $nsList['all'];
277 - } else {
278 - return $nsList[ "ns-$ns" ];
279 - }
280 - }
281 -
282 - /**
283 - * Check page move and patrol permissions for FlaggedRevs
284 - */
285 - public static function onUserCan( Title $title, $user, $action, &$result ) {
286 - if ( $result === false ) {
287 - return true; // nothing to do
288 - }
289 - # Don't let users vandalize pages by moving them...
290 - if ( $action === 'move' ) {
291 - if ( !FlaggedRevs::inReviewNamespace( $title ) || !$title->exists() ) {
292 - return true; // extra short-circuit
293 - }
294 - $flaggedArticle = FlaggedPage::getTitleInstance( $title );
295 - # If the draft shows by default anyway, nothing to do...
296 - if ( !$flaggedArticle->isStableShownByDefault() ) {
297 - return true;
298 - }
299 - $frev = $flaggedArticle->getStableRev();
300 - if ( $frev && !$user->isAllowed( 'review' ) && !$user->isAllowed( 'movestable' ) ) {
301 - # Allow for only editors/reviewers to move this page
302 - $result = false;
303 - return false;
304 - }
305 - # Don't let users patrol pages not in $wgFlaggedRevsPatrolNamespaces
306 - } else if ( $action === 'patrol' || $action === 'autopatrol' ) {
307 - $flaggedArticle = FlaggedPage::getTitleInstance( $title );
308 - # For a page to be patrollable it must not be reviewable.
309 - # Note: normally, edits to non-reviewable, non-patrollable, pages are
310 - # silently marked patrolled automatically. With $wgUseNPPatrol on, the
311 - # first edit to those pages is left as being unpatrolled.
312 - if ( $flaggedArticle->isReviewable() ) {
313 - $result = false;
314 - return false;
315 - }
316 - # Enforce autoreview/review restrictions
317 - } else if ( $action === 'autoreview' || $action === 'review' ) {
318 - # Get autoreview restriction settings...
319 - $fa = FlaggedPage::getTitleInstance( $title );
320 - $config = $fa->getStabilitySettings();
321 - # Convert Sysop -> protect
322 - $right = ( $config['autoreview'] === 'sysop' ) ?
323 - 'protect' : $config['autoreview'];
324 - # Check if the user has the required right, if any
325 - if ( $right != '' && !$user->isAllowed( $right ) ) {
326 - $result = false;
327 - return false;
328 - }
329 - }
330 - return true;
331 - }
332 -
333 - /**
334 - * When an edit is made by a user, review it if either:
335 - * (a) The user can 'autoreview' and the edit's base revision is a checked
336 - * (b) The edit is a self-revert to the stable version (by anyone)
337 - * (c) The user can 'autoreview' new pages and this edit is a new page
338 - * (d) The user can 'review' and the "review pending edits" checkbox was checked
339 - *
340 - * Note: RC items not inserted yet, RecentChange_save hook does rc_patrolled bit...
341 - * Note: $article one of Article, ImagePage, Category page as appropriate.
342 - */
343 - public static function maybeMakeEditReviewed(
344 - Article $article, $rev, $baseRevId = false, $user = null
345 - ) {
346 - global $wgRequest;
347 - # Edit must be non-null, to a reviewable page, with $user set
348 - $fa = FlaggedPage::getArticleInstance( $article );
349 - $fa->loadFromDB( FR_MASTER );
350 - if ( !$rev || !$user || !$fa->isReviewable() ) {
351 - return true;
352 - }
353 - $title = $article->getTitle(); // convenience
354 - $title->resetArticleID( $rev->getPage() ); // Avoid extra DB hit and lag issues
355 - # Get what was just the current revision ID
356 - $prevRevId = $rev->getParentId();
357 - # Get edit timestamp. Existance already validated by EditPage.php.
358 - $editTimestamp = $wgRequest->getVal( 'wpEdittime' );
359 - # Is the page manually checked off to be reviewed?
360 - if ( $editTimestamp
361 - && $wgRequest->getCheck( 'wpReviewEdit' )
362 - && $title->getUserPermissionsErrors( 'review', $user ) === array() )
363 - {
364 - if ( self::editCheckReview( $article, $rev, $user, $editTimestamp ) ) {
365 - return true; // reviewed...done!
366 - }
367 - }
368 - # All cases below require auto-review of edits to be enabled
369 - if ( !FlaggedRevs::autoReviewEnabled() ) {
370 - return true; // short-circuit
371 - }
372 - # If a $baseRevId is passed in, the edit is using an old revision's text
373 - $isOldRevCopy = (bool)$baseRevId; // null edit or rollback
374 - # Get the revision ID the incoming one was based off...
375 - if ( !$baseRevId && $prevRevId ) {
376 - $prevTimestamp = Revision::getTimestampFromId( $title, $prevRevId );
377 - # The user just made an edit. The one before that should have
378 - # been the current version. If not reflected in wpEdittime, an
379 - # edit may have been auto-merged in between, in that case, discard
380 - # the baseRevId given from the client.
381 - if ( !$editTimestamp || $prevTimestamp == $editTimestamp ) {
382 - $baseRevId = intval( trim( $wgRequest->getVal( 'baseRevId' ) ) );
383 - }
384 - # If baseRevId not given, assume the previous revision ID (for bots).
385 - # For auto-merges, this also occurs since the given ID is ignored.
386 - if ( !$baseRevId ) {
387 - $baseRevId = $prevRevId;
388 - }
389 - }
390 - $frev = null; // flagged rev this edit was based on
391 - $flags = null; // review flags (null => default flags)
392 - # Case A: this user can auto-review edits. Check if either:
393 - # (a) this new revision creates a new page and new page autoreview is enabled
394 - # (b) this new revision is based on an old, reviewed, revision
395 - if ( $title->getUserPermissionsErrors( 'autoreview', $user ) === array() ) {
396 - // New pages
397 - if ( !$prevRevId ) {
398 - $reviewableNewPage = FlaggedRevs::autoReviewNewPages();
399 - $reviewableChange = false;
400 - // Edits to existing pages
401 - } elseif ( $baseRevId ) {
402 - $reviewableNewPage = false; // had previous rev
403 - # Check if the base revision was reviewed...
404 - if ( FlaggedRevs::autoReviewEdits() ) {
405 - $frev = FlaggedRevision::newFromTitle( $title, $baseRevId, FR_MASTER );
406 - }
407 - $reviewableChange = (bool)$frev;
408 - }
409 - // Is this an edit directly to a reviewed version or a new page?
410 - if ( $reviewableNewPage || $reviewableChange ) {
411 - if ( $isOldRevCopy && $frev ) {
412 - $flags = $frev->getTags(); // null edits & rollbacks keep previous tags
413 - }
414 - # Review this revision of the page...
415 - FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
416 - }
417 - # Case B: the user cannot autoreview edits. Check if either:
418 - # (a) this is a rollback to the stable version
419 - # (b) this is a self-reversion to the stable version
420 - # These are subcases of making a new revision based on an old, reviewed, revision.
421 - } elseif ( FlaggedRevs::autoReviewEdits() && $fa->getStableRev() ) {
422 - $srev = $fa->getStableRev();
423 - # Check for rollbacks...
424 - $reviewableChange = (
425 - $isOldRevCopy && // rollback or null edit
426 - $baseRevId != $prevRevId && // not a null edit
427 - $baseRevId == $srev->getRevId() && // restored stable rev
428 - $title->getUserPermissionsErrors( 'autoreviewrestore', $user ) === array()
429 - );
430 - # Check for self-reversions...
431 - if ( !$reviewableChange ) {
432 - $reviewableChange = self::isSelfRevertToStable( $rev, $srev, $baseRevId, $user );
433 - }
434 - // Is this a rollback or self-reversion to the stable rev?
435 - if ( $reviewableChange ) {
436 - $flags = $srev->getTags(); // use old tags
437 - # Review this revision of the page...
438 - FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
439 - }
440 - }
441 - return true;
442 - }
443 -
444 - // Review $rev if $editTimestamp matches the previous revision's timestamp.
445 - // Otherwise, review the revision that has $editTimestamp as its timestamp value.
446 - protected static function editCheckReview(
447 - Article $article, $rev, $user, $editTimestamp
448 - ) {
449 - $prevTimestamp = $flags = null;
450 - $prevRevId = $rev->getParentId(); // revision before $rev
451 - $title = $article->getTitle(); // convenience
452 - # Check wpEdittime against the former current rev for verification
453 - if ( $prevRevId ) {
454 - $prevTimestamp = Revision::getTimestampFromId( $title, $prevRevId );
455 - }
456 - # Was $rev is an edit to an existing page?
457 - if ( $prevTimestamp ) {
458 - # Check wpEdittime against the former current revision's time.
459 - # If an edit was auto-merged in between, then the new revision
460 - # has content different than what the user expected. However, if
461 - # the auto-merged edit was reviewed, then assume that it's OK.
462 - if ( $editTimestamp != $prevTimestamp
463 - && !FlaggedRevision::revIsFlagged( $title, $prevRevId, FR_MASTER )
464 - ) {
465 - return false; // not flagged?
466 - }
467 - }
468 - # Review this revision of the page...
469 - return FlaggedRevs::autoReviewEdit(
470 - $article, $user, $rev, $flags, false /* manual */ );
471 - }
472 -
473 - /**
474 - * Check if a user reverted himself to the stable version
475 - */
476 - protected static function isSelfRevertToStable(
477 - Revision $rev, $srev, $baseRevId, $user
478 - ) {
479 - if ( !$srev || $baseRevId != $srev->getRevId() ) {
480 - return false; // user reports they are not the same
481 - }
482 - $dbw = wfGetDB( DB_MASTER );
483 - # Such a revert requires 1+ revs between it and the stable
484 - $revertedRevs = $dbw->selectField( 'revision', '1',
485 - array(
486 - 'rev_page' => $rev->getPage(),
487 - 'rev_id > ' . intval( $baseRevId ), // stable rev
488 - 'rev_id < ' . intval( $rev->getId() ), // this rev
489 - 'rev_user_text' => $user->getName()
490 - ), __METHOD__
491 - );
492 - if ( !$revertedRevs ) {
493 - return false; // can't be a revert
494 - }
495 - # Check that this user is ONLY reverting his/herself.
496 - $otherUsers = $dbw->selectField( 'revision', '1',
497 - array(
498 - 'rev_page' => $rev->getPage(),
499 - 'rev_id > ' . intval( $baseRevId ),
500 - 'rev_user_text != ' . $dbw->addQuotes( $user->getName() )
501 - ), __METHOD__
502 - );
503 - if ( $otherUsers ) {
504 - return false; // only looking for self-reverts
505 - }
506 - # Confirm the text because we can't trust this user.
507 - return ( $rev->getText() == $srev->getRevText() );
508 - }
509 -
510 - /**
511 - * When an user makes a null-edit we sometimes want to review it...
512 - * (a) Null undo or rollback
513 - * (b) Null edit with review box checked
514 - * Note: called after edit ops are finished
515 - */
516 - public static function maybeNullEditReview(
517 - Article $article, $user, $text, $s, $m, $a, $b, $flags, $rev, &$status, $baseId
518 - ) {
519 - global $wgRequest;
520 - # Revision must *be* null (null edit). We also need the user who made the edit.
521 - if ( !$user || $rev !== null ) {
522 - return true;
523 - }
524 - $fa = FlaggedPage::getArticleInstance( $article );
525 - $fa->loadFromDB( FR_MASTER );
526 - if ( !$fa->isReviewable() ) {
527 - return true; // page is not reviewable
528 - }
529 - $title = $article->getTitle(); // convenience
530 - # Get the current revision ID
531 - $rev = Revision::newFromTitle( $title );
532 - if ( !$rev ) {
533 - return true; // wtf?
534 - }
535 - $flags = null;
536 - # Is this a rollback/undo that didn't change anything?
537 - if ( $baseId > 0 ) {
538 - $frev = FlaggedRevision::newFromTitle( $title, $baseId );
539 - # Was the edit that we tried to revert to reviewed?
540 - if ( $frev ) {
541 - # Review this revision of the page...
542 - $ok = FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
543 - if ( $ok ) {
544 - FlaggedRevs::markRevisionPatrolled( $rev ); // reviewed -> patrolled
545 - FlaggedRevs::extraHTMLCacheUpdate( $title );
546 - return true;
547 - }
548 - }
549 - }
550 - # Get edit timestamp, it must exist.
551 - $editTimestamp = $wgRequest->getVal( 'wpEdittime' );
552 - # Is the page checked off to be reviewed?
553 - if ( $editTimestamp
554 - && $wgRequest->getCheck( 'wpReviewEdit' )
555 - && $title->userCan( 'review' ) )
556 - {
557 - # Check wpEdittime against current revision's time.
558 - # If an edit was auto-merged in between, review only up to what
559 - # was the current rev when this user started editing the page.
560 - if ( $rev->getTimestamp() != $editTimestamp ) {
561 - $dbw = wfGetDB( DB_MASTER );
562 - $rev = Revision::loadFromTimestamp( $dbw, $title, $editTimestamp );
563 - if ( !$rev ) {
564 - return true; // deleted?
565 - }
566 - }
567 - # Review this revision of the page...
568 - $ok = FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags, false );
569 - if ( $ok ) {
570 - FlaggedRevs::markRevisionPatrolled( $rev ); // reviewed -> patrolled
571 - FlaggedRevs::extraHTMLCacheUpdate( $title );
572 - }
573 - }
574 - return true;
575 - }
576 -
577 - /**
578 - * When an edit is made to a page:
579 - * (a) If the page is reviewable, silently mark the edit patrolled if it was auto-reviewed
580 - * (b) If the page can be patrolled, auto-patrol the edit patrolled as normal
581 - * (c) If the page is new and $wgUseNPPatrol is on, auto-patrol the edit patrolled as normal
582 - * (d) If the edit is neither reviewable nor patrolleable, silently mark it patrolled
583 - */
584 - public static function autoMarkPatrolled( RecentChange &$rc ) {
585 - global $wgUser;
586 - if ( empty( $rc->mAttribs['rc_this_oldid'] ) ) {
587 - return true;
588 - }
589 - $fa = FlaggedPage::getTitleInstance( $rc->getTitle() );
590 - $fa->loadFromDB( FR_MASTER );
591 - // Is the page reviewable?
592 - if ( $fa->isReviewable() ) {
593 - $revId = $rc->mAttribs['rc_this_oldid'];
594 - $quality = FlaggedRevision::getRevQuality(
595 - $rc->mAttribs['rc_cur_id'], $revId, FR_MASTER );
596 - // Reviewed => patrolled
597 - if ( $quality !== false && $quality >= FR_CHECKED ) {
598 - RevisionReviewForm::updateRecentChanges( $rc->getTitle(), $revId );
599 - $rc->mAttribs['rc_patrolled'] = 1; // make sure irc/email notifs know status
600 - }
601 - return true;
602 - }
603 - global $wgFlaggedRevsRCCrap;
604 - if ( $wgFlaggedRevsRCCrap ) {
605 - // Is this page in patrollable namespace?
606 - if ( FlaggedRevs::inPatrolNamespace( $rc->getTitle() ) ) {
607 - # Bots and users with 'autopatrol' have edits to patrollable
608 - # pages marked automatically on edit.
609 - $patrol = $wgUser->isAllowed( 'autopatrol' ) || $wgUser->isAllowed( 'bot' );
610 - $record = true; // record if patrolled
611 - } else {
612 - global $wgUseNPPatrol;
613 - // Is this is a new page edit and $wgUseNPPatrol is enabled?
614 - if ( $wgUseNPPatrol && !empty( $rc->mAttribs['rc_new'] ) ) {
615 - # Automatically mark it patrolled if the user can do so
616 - $patrol = $wgUser->isAllowed( 'autopatrol' );
617 - $record = true;
618 - // Otherwise, this edit is not patrollable
619 - } else {
620 - # Silently mark it "patrolled" so that it doesn't show up as being unpatrolled
621 - $patrol = true;
622 - $record = false;
623 - }
624 - }
625 - // Set rc_patrolled flag and add log entry as needed
626 - if ( $patrol ) {
627 - $rc->reallyMarkPatrolled();
628 - $rc->mAttribs['rc_patrolled'] = 1; // make sure irc/email notifs now status
629 - if ( $record ) {
630 - PatrolLog::record( $rc->mAttribs['rc_id'], true );
631 - }
632 - }
633 - }
634 - return true;
635 - }
636 -
637 - public static function incrementRollbacks(
638 - Article $article, $user, $goodRev, Revision $badRev
639 - ) {
640 - # Mark when a user reverts another user, but not self-reverts
641 - $badUserId = $badRev->getRawUser();
642 - if ( $badUserId && $user->getId() != $badUserId ) {
643 - $p = FRUserCounters::getUserParams( $badUserId, FR_FOR_UPDATE );
644 - if ( !isset( $p['revertedEdits'] ) ) {
645 - $p['revertedEdits'] = 0;
646 - }
647 - $p['revertedEdits']++;
648 - FRUserCounters::saveUserParams( $badUserId, $p );
649 - }
650 - return true;
651 - }
652 -
653 - public static function incrementReverts(
654 - Article $article, $rev, $baseRevId = false, $user = null
655 - ) {
656 - global $wgRequest;
657 - # Was this an edit by an auto-sighter that undid another edit?
658 - $undid = $wgRequest->getInt( 'undidRev' );
659 - if ( $rev && $undid && $user->isAllowed( 'autoreview' ) ) {
660 - // Note: $rev->getTitle() might be undefined (no rev id?)
661 - $badRev = Revision::newFromTitle( $article->getTitle(), $undid );
662 - # Don't count self-reverts
663 - if ( $badRev && $badRev->getRawUser()
664 - && $badRev->getRawUser() != $rev->getRawUser() )
665 - {
666 - $p = FRUserCounters::getUserParams( $badRev->getRawUser(), FR_FOR_UPDATE );
667 - if ( !isset( $p['revertedEdits'] ) ) {
668 - $p['revertedEdits'] = 0;
669 - }
670 - $p['revertedEdits']++;
671 - FRUserCounters::saveUserParams( $badRev->getRawUser(), $p );
672 - }
673 - }
674 - return true;
675 - }
676 -
677 - /*
678 - * Check if a user meets the edit spacing requirements.
679 - * If the user does not, return a *lower bound* number of seconds
680 - * that must elapse for it to be possible for the user to meet them.
681 - * @param int $spacingReq days apart (of edit points)
682 - * @param int $pointsReq number of edit points
683 - * @param User $user
684 - * @return mixed (true if passed, int seconds on failure)
685 - */
686 - protected static function editSpacingCheck( $spacingReq, $pointsReq, $user ) {
687 - $benchmarks = 0; // actual edit points
688 - # Convert days to seconds...
689 - $spacingReq = $spacingReq * 24 * 3600;
690 - # Check the oldest edit
691 - $dbr = wfGetDB( DB_SLAVE );
692 - $lower = $dbr->selectField( 'revision', 'rev_timestamp',
693 - array( 'rev_user' => $user->getId() ),
694 - __METHOD__,
695 - array( 'ORDER BY' => 'rev_timestamp ASC', 'USE INDEX' => 'user_timestamp' )
696 - );
697 - # Recursively check for an edit $spacingReq seconds later, until we are done.
698 - if ( $lower ) {
699 - $benchmarks++; // the first edit above counts
700 - while ( $lower && $benchmarks < $pointsReq ) {
701 - $next = wfTimestamp( TS_UNIX, $lower ) + $spacingReq;
702 - $lower = $dbr->selectField( 'revision', 'rev_timestamp',
703 - array( 'rev_user' => $user->getId(),
704 - 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $next ) ) ),
705 - __METHOD__,
706 - array( 'ORDER BY' => 'rev_timestamp ASC', 'USE INDEX' => 'user_timestamp' )
707 - );
708 - if ( $lower !== false ) $benchmarks++;
709 - }
710 - }
711 - if ( $benchmarks >= $pointsReq ) {
712 - return true;
713 - } else {
714 - // Does not add time for the last required edit point; it could be a
715 - // fraction of $spacingReq depending on the last actual edit point time.
716 - return ( $spacingReq * ($pointsReq - $benchmarks - 1) );
717 - }
718 - }
719 -
720 - /**
721 - * Check if a user has enough implicitly reviewed edits (before stable version)
722 - * @param $user User
723 - * @param $editsReq int
724 - * @param $cutoff_unixtime int exclude edits after this timestamp
725 - * @return bool
726 - */
727 - protected static function reviewedEditsCheck( $user, $editsReq, $cutoff_unixtime = 0 ) {
728 - $dbr = wfGetDB( DB_SLAVE );
729 - $encCutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
730 - $res = $dbr->select( array( 'revision', 'flaggedpages' ), '1',
731 - array( 'rev_user' => $user->getId(),
732 - "rev_timestamp < $encCutoff",
733 - 'fp_page_id = rev_page',
734 - 'fp_pending_since IS NULL OR fp_pending_since > rev_timestamp' // bug 15515
735 - ),
736 - __METHOD__,
737 - array( 'USE INDEX' => array( 'revision' => 'user_timestamp' ), 'LIMIT' => $editsReq )
738 - );
739 - return ( $dbr->numRows( $res ) >= $editsReq );
740 - }
741 -
742 - /**
743 - * Checks if $user was previously blocked
744 - */
745 - public static function wasPreviouslyBlocked( $user ) {
746 - $dbr = wfGetDB( DB_SLAVE );
747 - return (bool)$dbr->selectField( 'logging', '1',
748 - array(
749 - 'log_namespace' => NS_USER,
750 - 'log_title' => $user->getUserPage()->getDBkey(),
751 - 'log_type' => 'block',
752 - 'log_action' => 'block' ),
753 - __METHOD__,
754 - array( 'USE INDEX' => 'page_time' )
755 - );
756 - }
757 -
758 - /**
759 - * Grant 'autoreview' rights to users with the 'bot' right
760 - */
761 - public static function onUserGetRights( $user, array &$rights ) {
762 - # Make sure bots always have the 'autoreview' right
763 - if ( in_array( 'bot', $rights ) && !in_array( 'autoreview', $rights ) ) {
764 - $rights[] = 'autoreview';
765 - }
766 - return true;
767 - }
768 -
769 - /**
770 - * Callback that autopromotes user according to the setting in
771 - * $wgFlaggedRevsAutopromote. This also handles user stats tallies.
772 - */
773 - public static function onArticleSaveComplete(
774 - Article $article, $user, $text, $summary, $m, $a, $b, &$f, $rev
775 - ) {
776 - global $wgFlaggedRevsAutopromote, $wgFlaggedRevsAutoconfirm;
777 - # Ignore NULL edits or edits by anon users
778 - if ( !$rev || !$user->getId() ) {
779 - return true;
780 - # No sense in running counters if nothing uses them
781 - } elseif ( !$wgFlaggedRevsAutopromote && !$wgFlaggedRevsAutoconfirm ) {
782 - return true;
783 - }
784 - $p = FRUserCounters::getUserParams( $user->getId(), FR_FOR_UPDATE );
785 - $changed = FRUserCounters::updateUserParams( $p, $article, $summary );
786 - if ( $changed ) {
787 - FRUserCounters::saveUserParams( $user->getId(), $p ); // save any updates
788 - }
789 - if ( is_array( $wgFlaggedRevsAutopromote ) ) {
790 - self::maybeMakeEditor( $user, $p, $wgFlaggedRevsAutopromote );
791 - }
792 - return true;
793 - }
794 -
795 - /**
796 - * Grant implicit 'autoreview' group to users meeting the
797 - * $wgFlaggedRevsAutoconfirm requirements. This lets people who
798 - * opt-out as Editors still have their own edits automatically reviewed.
799 - *
800 - * Note: some unobtrusive caching is used to avoid DB hits.
801 - */
802 - public static function checkAutoPromote( $user, array &$promote ) {
803 - global $wgFlaggedRevsAutoconfirm, $wgMemc;
804 - $conds = $wgFlaggedRevsAutoconfirm; // convenience
805 - if ( !is_array( $conds ) || !$user->getId() ) {
806 - return true; // $wgFlaggedRevsAutoconfirm not applicable
807 - }
808 - $p = FRUserCounters::getUserParams( $user->getId() );
809 - $regTime = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
810 - if (
811 - # Check if user edited enough unique pages
812 - $conds['uniqueContentPages'] > count( $p['uniqueContentPages'] ) ||
813 - # Check edit comment use
814 - $conds['editComments'] > $p['editComments'] ||
815 - # Check user edit count
816 - $conds['edits'] > $user->getEditCount() ||
817 - # Check account age
818 - ( $regTime && $conds['days'] > ( ( time() - $regTime ) / 86400 ) ) ||
819 - # Check user email
820 - $conds['email'] && !$user->isEmailConfirmed() ||
821 - # Don't grant to currently blocked users...
822 - $user->isBlocked()
823 - ) {
824 - return true;
825 - }
826 - # Check if user edited enough content pages
827 - $failedContentEdits = ( $conds['totalContentEdits'] > $p['totalContentEdits'] );
828 -
829 - # Check if results are cached to avoid DB queries.
830 - # Checked basic, already available, promotion heuristics first...
831 - $APSkipKey = wfMemcKey( 'flaggedrevs', 'autoreview-skip', $user->getId() );
832 - if ( $wgMemc->get( $APSkipKey ) === 'true' ) {
833 - return true;
834 - }
835 - # Check if user was ever blocked before
836 - if ( $conds['neverBlocked'] && self::wasPreviouslyBlocked( $user ) ) {
837 - $wgMemc->set( $APSkipKey, 'true', 3600 * 24 * 7 ); // cache results
838 - return true;
839 - }
840 - # Check for edit spacing. This lets us know that the account has
841 - # been used over N different days, rather than all in one lump.
842 - if ( $conds['spacing'] > 0 && $conds['benchmarks'] > 1 ) {
843 - $sTestKey = wfMemcKey( 'flaggedrevs', 'autoreview-spacing-ok', $user->getId() );
844 - # Hit the DB only if the result is not cached...
845 - if ( $wgMemc->get( $sTestKey ) !== 'true' ) {
846 - $pass = self::editSpacingCheck( $conds['spacing'], $conds['benchmarks'], $user );
847 - # Make a key to store the results
848 - if ( $pass === true ) {
849 - $wgMemc->set( $sTestKey, 'true', 7 * 24 * 3600 );
850 - } else {
851 - $wgMemc->set( $APSkipKey, 'true', $pass /* wait time */ );
852 - return true;
853 - }
854 - }
855 - }
856 - # Check implicitly checked edits
857 - if ( $failedContentEdits && $conds['totalCheckedEdits'] > 0 ) {
858 - if ( !self::reviewedEditsCheck( $user, $conds['totalCheckedEdits'] ) ) {
859 - return true;
860 - }
861 - }
862 - $promote[] = 'autoreview'; // add the group
863 - return true;
864 - }
865 -
866 - /**
867 - * Autopromotes user according to the setting in $wgFlaggedRevsAutopromote.
868 - * @param $user User
869 - * @param $p array user tallies
870 - * @param $conds array $wgFlaggedRevsAutopromote
871 - */
872 - protected static function maybeMakeEditor( User $user, array $p, array $conds ) {
873 - global $wgMemc, $wgContentNamespaces;
874 - $groups = $user->getGroups(); // current groups
875 - $regTime = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
876 - if (
877 - !$user->getId() ||
878 - # Do not give this to current holders
879 - in_array( 'editor', $groups ) ||
880 - # Do not give this right to bots
881 - $user->isAllowed( 'bot' ) ||
882 - # Do not re-add status if it was previously removed!
883 - ( isset( $p['demoted'] ) && $p['demoted'] ) ||
884 - # Check if user edited enough unique pages
885 - $conds['uniqueContentPages'] > count( $p['uniqueContentPages'] ) ||
886 - # Check edit summary usage
887 - $conds['editComments'] > $p['editComments'] ||
888 - # Check reverted edits
889 - $conds['maxRevertedEditRatio']*$user->getEditCount() < $p['revertedEdits'] ||
890 - # Check user edit count
891 - $conds['edits'] > $user->getEditCount() ||
892 - # Check account age
893 - ( $regTime && $conds['days'] > ( ( time() - $regTime ) / 86400 ) ) ||
894 - # See if the page actually has sufficient content...
895 - $conds['userpageBytes'] > $user->getUserPage()->getLength() ||
896 - # Don't grant to currently blocked users...
897 - $user->isBlocked()
898 - ) {
899 - return true; // not ready
900 - }
901 - # User needs to meet 'totalContentEdits' OR 'totalCheckedEdits'
902 - $failedContentEdits = ( $conds['totalContentEdits'] > $p['totalContentEdits'] );
903 -
904 - # More expensive checks below...
905 - # Check if results are cached to avoid DB queries
906 - $APSkipKey = wfMemcKey( 'flaggedrevs', 'autopromote-skip', $user->getId() );
907 - if ( $wgMemc->get( $APSkipKey ) === 'true' ) {
908 - return true;
909 - }
910 - # Check if user was ever blocked before
911 - if ( $conds['neverBlocked'] && self::wasPreviouslyBlocked( $user ) ) {
912 - $wgMemc->set( $APSkipKey, 'true', 3600 * 24 * 7 ); // cache results
913 - return true;
914 - }
915 - $dbr = wfGetDB( DB_SLAVE );
916 - $cutoff_ts = 0;
917 - # Check to see if the user has enough non-"last minute" edits.
918 - if ( $conds['excludeLastDays'] > 0 ) {
919 - $minDiffAll = $user->getEditCount() - $conds['edits'] + 1;
920 - # Get cutoff timestamp
921 - $cutoff_ts = time() - 86400*$conds['excludeLastDays'];
922 - $encCutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_ts ) );
923 - # Check all recent edits...
924 - $res = $dbr->select( 'revision', '1',
925 - array( 'rev_user' => $user->getId(), "rev_timestamp > $encCutoff" ),
926 - __METHOD__,
927 - array( 'USE INDEX' => 'user_timestamp', 'LIMIT' => $minDiffAll )
928 - );
929 - if ( $dbr->numRows( $res ) >= $minDiffAll ) {
930 - return true; // delay promotion
931 - }
932 - # Check recent content edits...
933 - if ( !$failedContentEdits && $wgContentNamespaces ) {
934 - $minDiffContent = $p['totalContentEdits'] - $conds['totalContentEdits'] + 1;
935 - $res = $dbr->select( array( 'revision', 'page' ), '1',
936 - array( 'rev_user' => $user->getId(),
937 - "rev_timestamp > $encCutoff",
938 - 'rev_page = page_id',
939 - 'page_namespace' => $wgContentNamespaces ),
940 - __METHOD__,
941 - array( 'USE INDEX' => array( 'revision' => 'user_timestamp' ),
942 - 'LIMIT' => $minDiffContent )
943 - );
944 - if ( $dbr->numRows( $res ) >= $minDiffContent ) {
945 - $failedContentEdits = true; // totalCheckedEdits needed
946 - }
947 - }
948 - }
949 - # Check for edit spacing. This lets us know that the account has
950 - # been used over N different days, rather than all in one lump.
951 - if ( $conds['spacing'] > 0 && $conds['benchmarks'] > 1 ) {
952 - $pass = self::editSpacingCheck( $conds['spacing'], $conds['benchmarks'], $user );
953 - if ( $pass !== true ) {
954 - $wgMemc->set( $APSkipKey, 'true', $pass /* wait time */ ); // cache results
955 - return true;
956 - }
957 - }
958 - # Check if there are enough implicitly reviewed edits
959 - if ( $failedContentEdits && $conds['totalCheckedEdits'] > 0 ) {
960 - if ( !self::reviewedEditsCheck( $user, $conds['totalCheckedEdits'], $cutoff_ts ) ) {
961 - return true;
962 - }
963 - }
964 -
965 - # Add editor rights...
966 - $newGroups = $groups;
967 - array_push( $newGroups, 'editor' );
968 - $log = new LogPage( 'rights', false /* $rc */ );
969 - $log->addEntry( 'rights',
970 - $user->getUserPage(),
971 - wfMsgForContent( 'rights-editor-autosum' ),
972 - array( implode( ', ', $groups ), implode( ', ', $newGroups ) )
973 - );
974 - $user->addGroup( 'editor' );
975 -
976 - return true;
977 - }
978 -
979 - /**
980 - * Record demotion so that auto-promote will be disabled
981 - */
982 - public static function recordDemote( $user, array $addgroup, array $removegroup ) {
983 - if ( $removegroup && in_array( 'editor', $removegroup ) ) {
984 - $dbName = false; // this wiki
985 - // Cross-wiki rights changes...
986 - if ( $user instanceof UserRightsProxy ) {
987 - $dbName = $user->getDBName(); // use foreign DB of the user
988 - }
989 - $p = FRUserCounters::getUserParams( $user->getId(), FR_FOR_UPDATE, $dbName );
990 - $p['demoted'] = 1;
991 - FRUserCounters::saveUserParams( $user->getId(), $p, $dbName );
992 - }
993 - return true;
994 - }
995 -
996 - public static function stableDumpQuery( array &$tables, array &$opts, array &$join ) {
997 - $namespaces = FlaggedRevs::getReviewNamespaces();
998 - if ( $namespaces ) {
999 - $tables[] = 'flaggedpages';
1000 - $opts['ORDER BY'] = 'fp_page_id ASC';
1001 - $opts['USE INDEX'] = array( 'flaggedpages' => 'PRIMARY' );
1002 - $join['page'] = array( 'INNER JOIN',
1003 - array( 'page_id = fp_page_id', 'page_namespace' => $namespaces )
1004 - );
1005 - $join['revision'] = array( 'INNER JOIN',
1006 - 'rev_page = fp_page_id AND rev_id = fp_stable' );
1007 - }
1008 - return false; // final
1009 - }
1010 -
1011 - public static function getUnitTests( &$files ) {
1012 - $files[] = dirname( __FILE__ ) . '/tests/FRInclusionManagerTest.php';
1013 - $files[] = dirname( __FILE__ ) . '/tests/FRUserCountersTest.php';
1014 - return true;
1015 - }
1016 -
1017 - public static function onParserTestTables( array &$tables ) {
1018 - $tables[] = 'flaggedpages';
1019 - $tables[] = 'flaggedrevs';
1020 - $tables[] = 'flaggedpage_pending';
1021 - $tables[] = 'flaggedpage_config';
1022 - $tables[] = 'flaggedtemplates';
1023 - $tables[] = 'flaggedimages';
1024 - $tables[] = 'flaggedrevs_promote';
1025 - $tables[] = 'flaggedrevs_tracking';
1026 - $tables[] = 'valid_tag'; // we need this core table
1027 - return true;
1028 - }
1029 -}
Index: trunk/extensions/FlaggedRevs/FlaggedRevs.php
@@ -258,10 +258,11 @@
259259 $wgAutoloadClasses['FRParserCacheStable'] = $accessDir . 'FRParserCacheStable.php';
260260
261261 # Event handler classes...
262 -$wgAutoloadClasses['FlaggedRevsHooks'] = $dir . 'FlaggedRevs.hooks.php';
 262+$wgAutoloadClasses['FlaggedRevsHooks'] = $dir . 'dataclasses/FlaggedRevs.hooks.php';
263263 $wgAutoloadClasses['FlaggedRevsUIHooks'] = $dir . 'presentation/FlaggedRevsUI.hooks.php';
264264 $wgAutoloadClasses['FlaggedRevsApiHooks'] = $dir . 'api/FlaggedRevsApi.hooks.php';
265265 $wgAutoloadClasses['FlaggedRevsUpdaterHooks'] = $dir . 'schema/FlaggedRevsUpdater.hooks.php';
 266+$wgAutoloadClasses['FlaggedRevsTestHooks'] = $dir . 'tests/FlaggedRevsTest.hooks.php';
266267
267268 # Business object classes
268269 $wgAutoloadClasses['FRGenericSubmitForm'] = $dir . 'business/FRGenericSubmitForm.php';
@@ -500,9 +501,9 @@
501502 $wgHooks['WikiExporter::dumpStableQuery'][] = 'FlaggedRevsHooks::stableDumpQuery';
502503
503504 # Duplicate flagged* tables in parserTests.php
504 -$wgHooks['ParserTestTables'][] = 'FlaggedRevsHooks::onParserTestTables';
 505+$wgHooks['ParserTestTables'][] = 'FlaggedRevsTestHooks::onParserTestTables';
505506 # Integration tests
506 -$wgHooks['UnitTestsList'][] = 'FlaggedRevsHooks::getUnitTests';
 507+$wgHooks['UnitTestsList'][] = 'FlaggedRevsTestHooks::getUnitTests';
507508
508509 # Database schema changes
509510 $wgHooks['LoadExtensionSchemaUpdates'][] = 'FlaggedRevsUpdaterHooks::addSchemaUpdates';
Index: trunk/extensions/FlaggedRevs/tests/FlaggedRevsTest.hooks.php
@@ -0,0 +1,24 @@
 2+<?php
 3+/**
 4+ * Class containing test related event-handlers for FlaggedRevs
 5+ */
 6+class FlaggedRevsTestHooks {
 7+ public static function getUnitTests( &$files ) {
 8+ $files[] = dirname( __FILE__ ) . '/FRInclusionManagerTest.php';
 9+ $files[] = dirname( __FILE__ ) . '/FRUserCountersTest.php';
 10+ return true;
 11+ }
 12+
 13+ public static function onParserTestTables( array &$tables ) {
 14+ $tables[] = 'flaggedpages';
 15+ $tables[] = 'flaggedrevs';
 16+ $tables[] = 'flaggedpage_pending';
 17+ $tables[] = 'flaggedpage_config';
 18+ $tables[] = 'flaggedtemplates';
 19+ $tables[] = 'flaggedimages';
 20+ $tables[] = 'flaggedrevs_promote';
 21+ $tables[] = 'flaggedrevs_tracking';
 22+ $tables[] = 'valid_tag'; // we need this core table
 23+ return true;
 24+ }
 25+}
Property changes on: trunk/extensions/FlaggedRevs/tests/FlaggedRevsTest.hooks.php
___________________________________________________________________
Added: svn:eol-style
126 + native
Index: trunk/extensions/FlaggedRevs/dataclasses/FlaggedRevs.hooks.php
@@ -0,0 +1,1009 @@
 2+<?php
 3+/**
 4+ * Class containing hooked functions for a FlaggedRevs environment
 5+ */
 6+class FlaggedRevsHooks {
 7+ /**
 8+ * Update flaggedrevs table on revision restore
 9+ */
 10+ public static function onRevisionRestore( $title, Revision $revision, $oldPageID ) {
 11+ $dbw = wfGetDB( DB_MASTER );
 12+ # Some revisions may have had null rev_id values stored when deleted.
 13+ # This hook is called after insertOn() however, in which case it is set
 14+ # as a new one.
 15+ $dbw->update( 'flaggedrevs',
 16+ array( 'fr_page_id' => $revision->getPage() ),
 17+ array( 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revision->getID() ),
 18+ __METHOD__
 19+ );
 20+ return true;
 21+ }
 22+
 23+ /**
 24+ * Update flaggedrevs page/tracking tables (revision moving)
 25+ */
 26+ public static function onArticleMergeComplete( Title $sourceTitle, Title $destTitle ) {
 27+ $oldPageID = $sourceTitle->getArticleID();
 28+ $newPageID = $destTitle->getArticleID();
 29+ # Get flagged revisions from old page id that point to destination page
 30+ $dbw = wfGetDB( DB_MASTER );
 31+ $result = $dbw->select(
 32+ array( 'flaggedrevs', 'revision' ),
 33+ array( 'fr_rev_id' ),
 34+ array( 'fr_page_id' => $oldPageID,
 35+ 'fr_rev_id = rev_id',
 36+ 'rev_page' => $newPageID ),
 37+ __METHOD__
 38+ );
 39+ # Update these rows
 40+ $revIDs = array();
 41+ foreach( $result as $row ) {
 42+ $revIDs[] = $row->fr_rev_id;
 43+ }
 44+ if ( !empty( $revIDs ) ) {
 45+ $dbw->update( 'flaggedrevs',
 46+ array( 'fr_page_id' => $newPageID ),
 47+ array( 'fr_page_id' => $oldPageID, 'fr_rev_id' => $revIDs ),
 48+ __METHOD__
 49+ );
 50+ }
 51+ # Update pages...stable versions possibly lost to another page
 52+ FlaggedRevs::stableVersionUpdates( $sourceTitle );
 53+ FlaggedRevs::HTMLCacheUpdates( $sourceTitle );
 54+ FlaggedRevs::stableVersionUpdates( $destTitle );
 55+ FlaggedRevs::HTMLCacheUpdates( $destTitle );
 56+ return true;
 57+ }
 58+
 59+ /**
 60+ * (a) Update flaggedrevs page/tracking tables
 61+ * (b) Autoreview pages moved into content NS
 62+ */
 63+ public static function onTitleMoveComplete(
 64+ Title $otitle, Title $ntitle, $user, $pageId
 65+ ) {
 66+ $fa = FlaggedPage::getTitleInstance( $ntitle );
 67+ $fa->loadFromDB( FR_MASTER );
 68+ // Re-validate NS/config (new title may not be reviewable)
 69+ if ( $fa->isReviewable() ) {
 70+ // Moved from non-reviewable to reviewable NS?
 71+ // Auto-review such edits like new pages...
 72+ if ( !FlaggedRevs::inReviewNamespace( $otitle )
 73+ && FlaggedRevs::autoReviewNewPages()
 74+ && $ntitle->userCan( 'autoreview' ) )
 75+ {
 76+ $rev = Revision::newFromTitle( $ntitle );
 77+ if ( $rev ) { // sanity
 78+ FlaggedRevs::autoReviewEdit( $fa, $user, $rev );
 79+ }
 80+ }
 81+ }
 82+ # Update page and tracking tables and clear cache
 83+ FlaggedRevs::stableVersionUpdates( $otitle );
 84+ FlaggedRevs::HTMLCacheUpdates( $otitle );
 85+ FlaggedRevs::stableVersionUpdates( $ntitle );
 86+ FlaggedRevs::HTMLCacheUpdates( $ntitle );
 87+ return true;
 88+ }
 89+
 90+ /**
 91+ * (a) Update flaggedrevs page/tracking tables
 92+ * (b) Pages with stable versions that use this page will be purged
 93+ * Note: pages with current versions that use this page should already be purged
 94+ */
 95+ public static function onArticleEditUpdates( Article $article ) {
 96+ FlaggedRevs::stableVersionUpdates( $article->getTitle() );
 97+ FlaggedRevs::extraHTMLCacheUpdate( $article->getTitle() );
 98+ return true;
 99+ }
 100+
 101+ /**
 102+ * (a) Update flaggedrevs page/tracking tables
 103+ * (b) Pages with stable versions that use this page will be purged
 104+ * Note: pages with current versions that use this page should already be purged
 105+ */
 106+ public static function onArticleDelete( Article $article, $user, $reason, $id ) {
 107+ FlaggedRevs::clearTrackingRows( $id );
 108+ FlaggedRevs::extraHTMLCacheUpdate( $article->getTitle() );
 109+ return true;
 110+ }
 111+
 112+ /**
 113+ * (a) Update flaggedrevs page/tracking tables
 114+ * (b) Pages with stable versions that use this page will be purged
 115+ * Note: pages with current versions that use this page should already be purged
 116+ */
 117+ public static function onArticleUndelete( Title $title ) {
 118+ FlaggedRevs::stableVersionUpdates( $title );
 119+ FlaggedRevs::HTMLCacheUpdates( $title );
 120+ return true;
 121+ }
 122+
 123+ /**
 124+ * (a) Update flaggedrevs page/tracking tables
 125+ * (b) Pages with stable versions that use this page will be purged
 126+ * Note: pages with current versions that use this page should already be purged
 127+ */
 128+ public static function onFileUpload( File $file ) {
 129+ FlaggedRevs::stableVersionUpdates( $file->getTitle() );
 130+ FlaggedRevs::extraHTMLCacheUpdate( $file->getTitle() );
 131+ return true;
 132+ }
 133+
 134+ /**
 135+ * Update flaggedrevs page/tracking tables
 136+ */
 137+ public static function onRevisionDelete( Title $title ) {
 138+ $changed = FlaggedRevs::stableVersionUpdates( $title );
 139+ if ( $changed ) {
 140+ FlaggedRevs::HTMLCacheUpdates( $title );
 141+ }
 142+ return true;
 143+ }
 144+
 145+ /**
 146+ * Select the desired templates based on the selected stable revision IDs
 147+ * Note: $parser can be false
 148+ */
 149+ public static function parserFetchStableTemplate( $parser, Title $title, &$skip, &$id ) {
 150+ if ( !( $parser instanceof Parser ) || $title->getNamespace() < 0 ) {
 151+ return true; // nothing to do
 152+ }
 153+ $incManager = FRInclusionManager::singleton();
 154+ if ( !$incManager->parserOutputIsStabilized() ) {
 155+ return true; // trigger for stable version parsing only
 156+ }
 157+ $id = false; // current version
 158+ # Check for the version of this template used when reviewed...
 159+ $maybeId = $incManager->getReviewedTemplateVersion( $title );
 160+ if ( $maybeId !== null ) {
 161+ $id = (int)$maybeId; // use if specified (even 0)
 162+ }
 163+ # Check for stable version of template if this feature is enabled...
 164+ if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE ) {
 165+ $maybeId = $incManager->getStableTemplateVersion( $title );
 166+ # Take the newest of these two...
 167+ if ( $maybeId && $maybeId > $id ) {
 168+ $id = (int)$maybeId;
 169+ }
 170+ }
 171+ # If $id is zero, don't bother loading it (use blue/red link)
 172+ if ( $id === 0 ) {
 173+ $skip = true;
 174+ }
 175+ return true;
 176+ }
 177+
 178+ /**
 179+ * Select the desired images based on the selected stable version time/SHA-1
 180+ */
 181+ public static function parserFetchStableFile( $parser, Title $title, &$time, &$sha1, &$query ) {
 182+ if ( !( $parser instanceof Parser ) ) {
 183+ return true; // nothing to do
 184+ }
 185+ $incManager = FRInclusionManager::singleton();
 186+ if ( !$incManager->parserOutputIsStabilized() ) {
 187+ return true; // trigger for stable version parsing only
 188+ }
 189+ # Normalize NS_MEDIA to NS_FILE
 190+ if ( $title->getNamespace() == NS_MEDIA ) {
 191+ $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
 192+ $title->resetArticleId( $title->getArticleId() ); // avoid extra queries
 193+ } else {
 194+ $title =& $title;
 195+ }
 196+ $time = $sha1 = false; // current version
 197+ # Check for the version of this file used when reviewed...
 198+ list( $maybeTS, $maybeSha1 ) = $incManager->getReviewedFileVersion( $title );
 199+ if ( $maybeTS !== null ) {
 200+ $time = $maybeTS; // use if specified (even '0')
 201+ $sha1 = $maybeSha1;
 202+ }
 203+ # Check for stable version of file if this feature is enabled...
 204+ if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE ) {
 205+ list( $maybeTS, $maybeSha1 ) = $incManager->getStableFileVersion( $title );
 206+ # Take the newest of these two...
 207+ if ( $maybeTS && $maybeTS > $time ) {
 208+ $time = $maybeTS;
 209+ $sha1 = $maybeSha1;
 210+ }
 211+ }
 212+ # Stabilize the file link
 213+ if ( $time ) {
 214+ if ( $query != '' ) $query .= '&';
 215+ $query = "filetimestamp=" . urlencode( wfTimestamp( TS_MW, $time ) );
 216+ }
 217+ return true;
 218+ }
 219+
 220+ public static function onParserFirstCallInit( &$parser ) {
 221+ $parser->setFunctionHook( 'pagesusingpendingchanges',
 222+ 'FlaggedRevsHooks::parserPagesUsingPendingChanges' );
 223+ return true;
 224+ }
 225+
 226+ public static function onLanguageGetMagic( &$magicWords, $langCode ) {
 227+ $magicWords['pagesusingpendingchanges'] = array( 0, 'pagesusingpendingchanges' );
 228+ $magicWords['pendingchangelevel'] = array( 0, 'pendingchangelevel' );
 229+ return true;
 230+ }
 231+
 232+ public static function onParserGetVariableValueSwitch( &$parser, &$cache, &$word, &$ret ) {
 233+ if ( $word == 'pendingchangelevel' ) {
 234+ $title = $parser->getTitle();
 235+ if ( !FlaggedRevs::inReviewNamespace( $title ) ) {
 236+ $ret = '';
 237+ } else {
 238+ $config = FlaggedPageConfig::getStabilitySettings( $title );
 239+ $ret = $config['autoreview'];
 240+ }
 241+ }
 242+ return true;
 243+ }
 244+
 245+ public static function onMagicWordwgVariableIDs( &$words ) {
 246+ $words[] = 'pendingchangelevel';
 247+ return true;
 248+ }
 249+
 250+ public static function parserPagesUsingPendingChanges( &$parser, $ns = '' ) {
 251+ $nsList = FlaggedRevs::getReviewNamespaces();
 252+ if ( !$nsList ) {
 253+ return 0;
 254+ }
 255+
 256+ if ( $ns !== '' ) {
 257+ $ns = intval( $ns );
 258+ if ( !in_array( $ns, $nsList ) ) {
 259+ return 0;
 260+ }
 261+ }
 262+
 263+ static $pcCounts = null;
 264+ if ( !$pcCounts ) {
 265+ $dbr = wfGetDB( DB_SLAVE );
 266+ $res = $dbr->select( 'flaggedrevs_stats', '*', array(), __METHOD__ );
 267+ $totalCount = 0;
 268+ foreach( $res as $row ) {
 269+ $nsList[ "ns-{$row->namespace}" ] = $row->reviewed;
 270+ $totalCount += $row->reviewed;
 271+ }
 272+ $nsList[ 'all' ] = $totalCount;
 273+ }
 274+
 275+ if ( $ns === '' ) {
 276+ return $nsList['all'];
 277+ } else {
 278+ return $nsList[ "ns-$ns" ];
 279+ }
 280+ }
 281+
 282+ /**
 283+ * Check page move and patrol permissions for FlaggedRevs
 284+ */
 285+ public static function onUserCan( Title $title, $user, $action, &$result ) {
 286+ if ( $result === false ) {
 287+ return true; // nothing to do
 288+ }
 289+ # Don't let users vandalize pages by moving them...
 290+ if ( $action === 'move' ) {
 291+ if ( !FlaggedRevs::inReviewNamespace( $title ) || !$title->exists() ) {
 292+ return true; // extra short-circuit
 293+ }
 294+ $flaggedArticle = FlaggedPage::getTitleInstance( $title );
 295+ # If the draft shows by default anyway, nothing to do...
 296+ if ( !$flaggedArticle->isStableShownByDefault() ) {
 297+ return true;
 298+ }
 299+ $frev = $flaggedArticle->getStableRev();
 300+ if ( $frev && !$user->isAllowed( 'review' ) && !$user->isAllowed( 'movestable' ) ) {
 301+ # Allow for only editors/reviewers to move this page
 302+ $result = false;
 303+ return false;
 304+ }
 305+ # Don't let users patrol pages not in $wgFlaggedRevsPatrolNamespaces
 306+ } else if ( $action === 'patrol' || $action === 'autopatrol' ) {
 307+ $flaggedArticle = FlaggedPage::getTitleInstance( $title );
 308+ # For a page to be patrollable it must not be reviewable.
 309+ # Note: normally, edits to non-reviewable, non-patrollable, pages are
 310+ # silently marked patrolled automatically. With $wgUseNPPatrol on, the
 311+ # first edit to those pages is left as being unpatrolled.
 312+ if ( $flaggedArticle->isReviewable() ) {
 313+ $result = false;
 314+ return false;
 315+ }
 316+ # Enforce autoreview/review restrictions
 317+ } else if ( $action === 'autoreview' || $action === 'review' ) {
 318+ # Get autoreview restriction settings...
 319+ $fa = FlaggedPage::getTitleInstance( $title );
 320+ $config = $fa->getStabilitySettings();
 321+ # Convert Sysop -> protect
 322+ $right = ( $config['autoreview'] === 'sysop' ) ?
 323+ 'protect' : $config['autoreview'];
 324+ # Check if the user has the required right, if any
 325+ if ( $right != '' && !$user->isAllowed( $right ) ) {
 326+ $result = false;
 327+ return false;
 328+ }
 329+ }
 330+ return true;
 331+ }
 332+
 333+ /**
 334+ * When an edit is made by a user, review it if either:
 335+ * (a) The user can 'autoreview' and the edit's base revision is a checked
 336+ * (b) The edit is a self-revert to the stable version (by anyone)
 337+ * (c) The user can 'autoreview' new pages and this edit is a new page
 338+ * (d) The user can 'review' and the "review pending edits" checkbox was checked
 339+ *
 340+ * Note: RC items not inserted yet, RecentChange_save hook does rc_patrolled bit...
 341+ * Note: $article one of Article, ImagePage, Category page as appropriate.
 342+ */
 343+ public static function maybeMakeEditReviewed(
 344+ Article $article, $rev, $baseRevId = false, $user = null
 345+ ) {
 346+ global $wgRequest;
 347+ # Edit must be non-null, to a reviewable page, with $user set
 348+ $fa = FlaggedPage::getArticleInstance( $article );
 349+ $fa->loadFromDB( FR_MASTER );
 350+ if ( !$rev || !$user || !$fa->isReviewable() ) {
 351+ return true;
 352+ }
 353+ $title = $article->getTitle(); // convenience
 354+ $title->resetArticleID( $rev->getPage() ); // Avoid extra DB hit and lag issues
 355+ # Get what was just the current revision ID
 356+ $prevRevId = $rev->getParentId();
 357+ # Get edit timestamp. Existance already validated by EditPage.php.
 358+ $editTimestamp = $wgRequest->getVal( 'wpEdittime' );
 359+ # Is the page manually checked off to be reviewed?
 360+ if ( $editTimestamp
 361+ && $wgRequest->getCheck( 'wpReviewEdit' )
 362+ && $title->getUserPermissionsErrors( 'review', $user ) === array() )
 363+ {
 364+ if ( self::editCheckReview( $article, $rev, $user, $editTimestamp ) ) {
 365+ return true; // reviewed...done!
 366+ }
 367+ }
 368+ # All cases below require auto-review of edits to be enabled
 369+ if ( !FlaggedRevs::autoReviewEnabled() ) {
 370+ return true; // short-circuit
 371+ }
 372+ # If a $baseRevId is passed in, the edit is using an old revision's text
 373+ $isOldRevCopy = (bool)$baseRevId; // null edit or rollback
 374+ # Get the revision ID the incoming one was based off...
 375+ if ( !$baseRevId && $prevRevId ) {
 376+ $prevTimestamp = Revision::getTimestampFromId( $title, $prevRevId );
 377+ # The user just made an edit. The one before that should have
 378+ # been the current version. If not reflected in wpEdittime, an
 379+ # edit may have been auto-merged in between, in that case, discard
 380+ # the baseRevId given from the client.
 381+ if ( !$editTimestamp || $prevTimestamp == $editTimestamp ) {
 382+ $baseRevId = intval( trim( $wgRequest->getVal( 'baseRevId' ) ) );
 383+ }
 384+ # If baseRevId not given, assume the previous revision ID (for bots).
 385+ # For auto-merges, this also occurs since the given ID is ignored.
 386+ if ( !$baseRevId ) {
 387+ $baseRevId = $prevRevId;
 388+ }
 389+ }
 390+ $frev = null; // flagged rev this edit was based on
 391+ $flags = null; // review flags (null => default flags)
 392+ # Case A: this user can auto-review edits. Check if either:
 393+ # (a) this new revision creates a new page and new page autoreview is enabled
 394+ # (b) this new revision is based on an old, reviewed, revision
 395+ if ( $title->getUserPermissionsErrors( 'autoreview', $user ) === array() ) {
 396+ // New pages
 397+ if ( !$prevRevId ) {
 398+ $reviewableNewPage = FlaggedRevs::autoReviewNewPages();
 399+ $reviewableChange = false;
 400+ // Edits to existing pages
 401+ } elseif ( $baseRevId ) {
 402+ $reviewableNewPage = false; // had previous rev
 403+ # Check if the base revision was reviewed...
 404+ if ( FlaggedRevs::autoReviewEdits() ) {
 405+ $frev = FlaggedRevision::newFromTitle( $title, $baseRevId, FR_MASTER );
 406+ }
 407+ $reviewableChange = (bool)$frev;
 408+ }
 409+ // Is this an edit directly to a reviewed version or a new page?
 410+ if ( $reviewableNewPage || $reviewableChange ) {
 411+ if ( $isOldRevCopy && $frev ) {
 412+ $flags = $frev->getTags(); // null edits & rollbacks keep previous tags
 413+ }
 414+ # Review this revision of the page...
 415+ FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
 416+ }
 417+ # Case B: the user cannot autoreview edits. Check if either:
 418+ # (a) this is a rollback to the stable version
 419+ # (b) this is a self-reversion to the stable version
 420+ # These are subcases of making a new revision based on an old, reviewed, revision.
 421+ } elseif ( FlaggedRevs::autoReviewEdits() && $fa->getStableRev() ) {
 422+ $srev = $fa->getStableRev();
 423+ # Check for rollbacks...
 424+ $reviewableChange = (
 425+ $isOldRevCopy && // rollback or null edit
 426+ $baseRevId != $prevRevId && // not a null edit
 427+ $baseRevId == $srev->getRevId() && // restored stable rev
 428+ $title->getUserPermissionsErrors( 'autoreviewrestore', $user ) === array()
 429+ );
 430+ # Check for self-reversions...
 431+ if ( !$reviewableChange ) {
 432+ $reviewableChange = self::isSelfRevertToStable( $rev, $srev, $baseRevId, $user );
 433+ }
 434+ // Is this a rollback or self-reversion to the stable rev?
 435+ if ( $reviewableChange ) {
 436+ $flags = $srev->getTags(); // use old tags
 437+ # Review this revision of the page...
 438+ FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
 439+ }
 440+ }
 441+ return true;
 442+ }
 443+
 444+ // Review $rev if $editTimestamp matches the previous revision's timestamp.
 445+ // Otherwise, review the revision that has $editTimestamp as its timestamp value.
 446+ protected static function editCheckReview(
 447+ Article $article, $rev, $user, $editTimestamp
 448+ ) {
 449+ $prevTimestamp = $flags = null;
 450+ $prevRevId = $rev->getParentId(); // revision before $rev
 451+ $title = $article->getTitle(); // convenience
 452+ # Check wpEdittime against the former current rev for verification
 453+ if ( $prevRevId ) {
 454+ $prevTimestamp = Revision::getTimestampFromId( $title, $prevRevId );
 455+ }
 456+ # Was $rev is an edit to an existing page?
 457+ if ( $prevTimestamp ) {
 458+ # Check wpEdittime against the former current revision's time.
 459+ # If an edit was auto-merged in between, then the new revision
 460+ # has content different than what the user expected. However, if
 461+ # the auto-merged edit was reviewed, then assume that it's OK.
 462+ if ( $editTimestamp != $prevTimestamp
 463+ && !FlaggedRevision::revIsFlagged( $title, $prevRevId, FR_MASTER )
 464+ ) {
 465+ return false; // not flagged?
 466+ }
 467+ }
 468+ # Review this revision of the page...
 469+ return FlaggedRevs::autoReviewEdit(
 470+ $article, $user, $rev, $flags, false /* manual */ );
 471+ }
 472+
 473+ /**
 474+ * Check if a user reverted himself to the stable version
 475+ */
 476+ protected static function isSelfRevertToStable(
 477+ Revision $rev, $srev, $baseRevId, $user
 478+ ) {
 479+ if ( !$srev || $baseRevId != $srev->getRevId() ) {
 480+ return false; // user reports they are not the same
 481+ }
 482+ $dbw = wfGetDB( DB_MASTER );
 483+ # Such a revert requires 1+ revs between it and the stable
 484+ $revertedRevs = $dbw->selectField( 'revision', '1',
 485+ array(
 486+ 'rev_page' => $rev->getPage(),
 487+ 'rev_id > ' . intval( $baseRevId ), // stable rev
 488+ 'rev_id < ' . intval( $rev->getId() ), // this rev
 489+ 'rev_user_text' => $user->getName()
 490+ ), __METHOD__
 491+ );
 492+ if ( !$revertedRevs ) {
 493+ return false; // can't be a revert
 494+ }
 495+ # Check that this user is ONLY reverting his/herself.
 496+ $otherUsers = $dbw->selectField( 'revision', '1',
 497+ array(
 498+ 'rev_page' => $rev->getPage(),
 499+ 'rev_id > ' . intval( $baseRevId ),
 500+ 'rev_user_text != ' . $dbw->addQuotes( $user->getName() )
 501+ ), __METHOD__
 502+ );
 503+ if ( $otherUsers ) {
 504+ return false; // only looking for self-reverts
 505+ }
 506+ # Confirm the text because we can't trust this user.
 507+ return ( $rev->getText() == $srev->getRevText() );
 508+ }
 509+
 510+ /**
 511+ * When an user makes a null-edit we sometimes want to review it...
 512+ * (a) Null undo or rollback
 513+ * (b) Null edit with review box checked
 514+ * Note: called after edit ops are finished
 515+ */
 516+ public static function maybeNullEditReview(
 517+ Article $article, $user, $text, $s, $m, $a, $b, $flags, $rev, &$status, $baseId
 518+ ) {
 519+ global $wgRequest;
 520+ # Revision must *be* null (null edit). We also need the user who made the edit.
 521+ if ( !$user || $rev !== null ) {
 522+ return true;
 523+ }
 524+ $fa = FlaggedPage::getArticleInstance( $article );
 525+ $fa->loadFromDB( FR_MASTER );
 526+ if ( !$fa->isReviewable() ) {
 527+ return true; // page is not reviewable
 528+ }
 529+ $title = $article->getTitle(); // convenience
 530+ # Get the current revision ID
 531+ $rev = Revision::newFromTitle( $title );
 532+ if ( !$rev ) {
 533+ return true; // wtf?
 534+ }
 535+ $flags = null;
 536+ # Is this a rollback/undo that didn't change anything?
 537+ if ( $baseId > 0 ) {
 538+ $frev = FlaggedRevision::newFromTitle( $title, $baseId );
 539+ # Was the edit that we tried to revert to reviewed?
 540+ if ( $frev ) {
 541+ # Review this revision of the page...
 542+ $ok = FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags );
 543+ if ( $ok ) {
 544+ FlaggedRevs::markRevisionPatrolled( $rev ); // reviewed -> patrolled
 545+ FlaggedRevs::extraHTMLCacheUpdate( $title );
 546+ return true;
 547+ }
 548+ }
 549+ }
 550+ # Get edit timestamp, it must exist.
 551+ $editTimestamp = $wgRequest->getVal( 'wpEdittime' );
 552+ # Is the page checked off to be reviewed?
 553+ if ( $editTimestamp
 554+ && $wgRequest->getCheck( 'wpReviewEdit' )
 555+ && $title->userCan( 'review' ) )
 556+ {
 557+ # Check wpEdittime against current revision's time.
 558+ # If an edit was auto-merged in between, review only up to what
 559+ # was the current rev when this user started editing the page.
 560+ if ( $rev->getTimestamp() != $editTimestamp ) {
 561+ $dbw = wfGetDB( DB_MASTER );
 562+ $rev = Revision::loadFromTimestamp( $dbw, $title, $editTimestamp );
 563+ if ( !$rev ) {
 564+ return true; // deleted?
 565+ }
 566+ }
 567+ # Review this revision of the page...
 568+ $ok = FlaggedRevs::autoReviewEdit( $article, $user, $rev, $flags, false );
 569+ if ( $ok ) {
 570+ FlaggedRevs::markRevisionPatrolled( $rev ); // reviewed -> patrolled
 571+ FlaggedRevs::extraHTMLCacheUpdate( $title );
 572+ }
 573+ }
 574+ return true;
 575+ }
 576+
 577+ /**
 578+ * When an edit is made to a page:
 579+ * (a) If the page is reviewable, silently mark the edit patrolled if it was auto-reviewed
 580+ * (b) If the page can be patrolled, auto-patrol the edit patrolled as normal
 581+ * (c) If the page is new and $wgUseNPPatrol is on, auto-patrol the edit patrolled as normal
 582+ * (d) If the edit is neither reviewable nor patrolleable, silently mark it patrolled
 583+ */
 584+ public static function autoMarkPatrolled( RecentChange &$rc ) {
 585+ global $wgUser;
 586+ if ( empty( $rc->mAttribs['rc_this_oldid'] ) ) {
 587+ return true;
 588+ }
 589+ $fa = FlaggedPage::getTitleInstance( $rc->getTitle() );
 590+ $fa->loadFromDB( FR_MASTER );
 591+ // Is the page reviewable?
 592+ if ( $fa->isReviewable() ) {
 593+ $revId = $rc->mAttribs['rc_this_oldid'];
 594+ $quality = FlaggedRevision::getRevQuality(
 595+ $rc->mAttribs['rc_cur_id'], $revId, FR_MASTER );
 596+ // Reviewed => patrolled
 597+ if ( $quality !== false && $quality >= FR_CHECKED ) {
 598+ RevisionReviewForm::updateRecentChanges( $rc->getTitle(), $revId );
 599+ $rc->mAttribs['rc_patrolled'] = 1; // make sure irc/email notifs know status
 600+ }
 601+ return true;
 602+ }
 603+ global $wgFlaggedRevsRCCrap;
 604+ if ( $wgFlaggedRevsRCCrap ) {
 605+ // Is this page in patrollable namespace?
 606+ if ( FlaggedRevs::inPatrolNamespace( $rc->getTitle() ) ) {
 607+ # Bots and users with 'autopatrol' have edits to patrollable
 608+ # pages marked automatically on edit.
 609+ $patrol = $wgUser->isAllowed( 'autopatrol' ) || $wgUser->isAllowed( 'bot' );
 610+ $record = true; // record if patrolled
 611+ } else {
 612+ global $wgUseNPPatrol;
 613+ // Is this is a new page edit and $wgUseNPPatrol is enabled?
 614+ if ( $wgUseNPPatrol && !empty( $rc->mAttribs['rc_new'] ) ) {
 615+ # Automatically mark it patrolled if the user can do so
 616+ $patrol = $wgUser->isAllowed( 'autopatrol' );
 617+ $record = true;
 618+ // Otherwise, this edit is not patrollable
 619+ } else {
 620+ # Silently mark it "patrolled" so that it doesn't show up as being unpatrolled
 621+ $patrol = true;
 622+ $record = false;
 623+ }
 624+ }
 625+ // Set rc_patrolled flag and add log entry as needed
 626+ if ( $patrol ) {
 627+ $rc->reallyMarkPatrolled();
 628+ $rc->mAttribs['rc_patrolled'] = 1; // make sure irc/email notifs now status
 629+ if ( $record ) {
 630+ PatrolLog::record( $rc->mAttribs['rc_id'], true );
 631+ }
 632+ }
 633+ }
 634+ return true;
 635+ }
 636+
 637+ public static function incrementRollbacks(
 638+ Article $article, $user, $goodRev, Revision $badRev
 639+ ) {
 640+ # Mark when a user reverts another user, but not self-reverts
 641+ $badUserId = $badRev->getRawUser();
 642+ if ( $badUserId && $user->getId() != $badUserId ) {
 643+ $p = FRUserCounters::getUserParams( $badUserId, FR_FOR_UPDATE );
 644+ if ( !isset( $p['revertedEdits'] ) ) {
 645+ $p['revertedEdits'] = 0;
 646+ }
 647+ $p['revertedEdits']++;
 648+ FRUserCounters::saveUserParams( $badUserId, $p );
 649+ }
 650+ return true;
 651+ }
 652+
 653+ public static function incrementReverts(
 654+ Article $article, $rev, $baseRevId = false, $user = null
 655+ ) {
 656+ global $wgRequest;
 657+ # Was this an edit by an auto-sighter that undid another edit?
 658+ $undid = $wgRequest->getInt( 'undidRev' );
 659+ if ( $rev && $undid && $user->isAllowed( 'autoreview' ) ) {
 660+ // Note: $rev->getTitle() might be undefined (no rev id?)
 661+ $badRev = Revision::newFromTitle( $article->getTitle(), $undid );
 662+ # Don't count self-reverts
 663+ if ( $badRev && $badRev->getRawUser()
 664+ && $badRev->getRawUser() != $rev->getRawUser() )
 665+ {
 666+ $p = FRUserCounters::getUserParams( $badRev->getRawUser(), FR_FOR_UPDATE );
 667+ if ( !isset( $p['revertedEdits'] ) ) {
 668+ $p['revertedEdits'] = 0;
 669+ }
 670+ $p['revertedEdits']++;
 671+ FRUserCounters::saveUserParams( $badRev->getRawUser(), $p );
 672+ }
 673+ }
 674+ return true;
 675+ }
 676+
 677+ /*
 678+ * Check if a user meets the edit spacing requirements.
 679+ * If the user does not, return a *lower bound* number of seconds
 680+ * that must elapse for it to be possible for the user to meet them.
 681+ * @param int $spacingReq days apart (of edit points)
 682+ * @param int $pointsReq number of edit points
 683+ * @param User $user
 684+ * @return mixed (true if passed, int seconds on failure)
 685+ */
 686+ protected static function editSpacingCheck( $spacingReq, $pointsReq, $user ) {
 687+ $benchmarks = 0; // actual edit points
 688+ # Convert days to seconds...
 689+ $spacingReq = $spacingReq * 24 * 3600;
 690+ # Check the oldest edit
 691+ $dbr = wfGetDB( DB_SLAVE );
 692+ $lower = $dbr->selectField( 'revision', 'rev_timestamp',
 693+ array( 'rev_user' => $user->getId() ),
 694+ __METHOD__,
 695+ array( 'ORDER BY' => 'rev_timestamp ASC', 'USE INDEX' => 'user_timestamp' )
 696+ );
 697+ # Recursively check for an edit $spacingReq seconds later, until we are done.
 698+ if ( $lower ) {
 699+ $benchmarks++; // the first edit above counts
 700+ while ( $lower && $benchmarks < $pointsReq ) {
 701+ $next = wfTimestamp( TS_UNIX, $lower ) + $spacingReq;
 702+ $lower = $dbr->selectField( 'revision', 'rev_timestamp',
 703+ array( 'rev_user' => $user->getId(),
 704+ 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $next ) ) ),
 705+ __METHOD__,
 706+ array( 'ORDER BY' => 'rev_timestamp ASC', 'USE INDEX' => 'user_timestamp' )
 707+ );
 708+ if ( $lower !== false ) $benchmarks++;
 709+ }
 710+ }
 711+ if ( $benchmarks >= $pointsReq ) {
 712+ return true;
 713+ } else {
 714+ // Does not add time for the last required edit point; it could be a
 715+ // fraction of $spacingReq depending on the last actual edit point time.
 716+ return ( $spacingReq * ($pointsReq - $benchmarks - 1) );
 717+ }
 718+ }
 719+
 720+ /**
 721+ * Check if a user has enough implicitly reviewed edits (before stable version)
 722+ * @param $user User
 723+ * @param $editsReq int
 724+ * @param $cutoff_unixtime int exclude edits after this timestamp
 725+ * @return bool
 726+ */
 727+ protected static function reviewedEditsCheck( $user, $editsReq, $cutoff_unixtime = 0 ) {
 728+ $dbr = wfGetDB( DB_SLAVE );
 729+ $encCutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
 730+ $res = $dbr->select( array( 'revision', 'flaggedpages' ), '1',
 731+ array( 'rev_user' => $user->getId(),
 732+ "rev_timestamp < $encCutoff",
 733+ 'fp_page_id = rev_page',
 734+ 'fp_pending_since IS NULL OR fp_pending_since > rev_timestamp' // bug 15515
 735+ ),
 736+ __METHOD__,
 737+ array( 'USE INDEX' => array( 'revision' => 'user_timestamp' ), 'LIMIT' => $editsReq )
 738+ );
 739+ return ( $dbr->numRows( $res ) >= $editsReq );
 740+ }
 741+
 742+ /**
 743+ * Checks if $user was previously blocked
 744+ */
 745+ public static function wasPreviouslyBlocked( $user ) {
 746+ $dbr = wfGetDB( DB_SLAVE );
 747+ return (bool)$dbr->selectField( 'logging', '1',
 748+ array(
 749+ 'log_namespace' => NS_USER,
 750+ 'log_title' => $user->getUserPage()->getDBkey(),
 751+ 'log_type' => 'block',
 752+ 'log_action' => 'block' ),
 753+ __METHOD__,
 754+ array( 'USE INDEX' => 'page_time' )
 755+ );
 756+ }
 757+
 758+ /**
 759+ * Grant 'autoreview' rights to users with the 'bot' right
 760+ */
 761+ public static function onUserGetRights( $user, array &$rights ) {
 762+ # Make sure bots always have the 'autoreview' right
 763+ if ( in_array( 'bot', $rights ) && !in_array( 'autoreview', $rights ) ) {
 764+ $rights[] = 'autoreview';
 765+ }
 766+ return true;
 767+ }
 768+
 769+ /**
 770+ * Callback that autopromotes user according to the setting in
 771+ * $wgFlaggedRevsAutopromote. This also handles user stats tallies.
 772+ */
 773+ public static function onArticleSaveComplete(
 774+ Article $article, $user, $text, $summary, $m, $a, $b, &$f, $rev
 775+ ) {
 776+ global $wgFlaggedRevsAutopromote, $wgFlaggedRevsAutoconfirm;
 777+ # Ignore NULL edits or edits by anon users
 778+ if ( !$rev || !$user->getId() ) {
 779+ return true;
 780+ # No sense in running counters if nothing uses them
 781+ } elseif ( !$wgFlaggedRevsAutopromote && !$wgFlaggedRevsAutoconfirm ) {
 782+ return true;
 783+ }
 784+ $p = FRUserCounters::getUserParams( $user->getId(), FR_FOR_UPDATE );
 785+ $changed = FRUserCounters::updateUserParams( $p, $article, $summary );
 786+ if ( $changed ) {
 787+ FRUserCounters::saveUserParams( $user->getId(), $p ); // save any updates
 788+ }
 789+ if ( is_array( $wgFlaggedRevsAutopromote ) ) {
 790+ self::maybeMakeEditor( $user, $p, $wgFlaggedRevsAutopromote );
 791+ }
 792+ return true;
 793+ }
 794+
 795+ /**
 796+ * Grant implicit 'autoreview' group to users meeting the
 797+ * $wgFlaggedRevsAutoconfirm requirements. This lets people who
 798+ * opt-out as Editors still have their own edits automatically reviewed.
 799+ *
 800+ * Note: some unobtrusive caching is used to avoid DB hits.
 801+ */
 802+ public static function checkAutoPromote( $user, array &$promote ) {
 803+ global $wgFlaggedRevsAutoconfirm, $wgMemc;
 804+ $conds = $wgFlaggedRevsAutoconfirm; // convenience
 805+ if ( !is_array( $conds ) || !$user->getId() ) {
 806+ return true; // $wgFlaggedRevsAutoconfirm not applicable
 807+ }
 808+ $p = FRUserCounters::getUserParams( $user->getId() );
 809+ $regTime = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
 810+ if (
 811+ # Check if user edited enough unique pages
 812+ $conds['uniqueContentPages'] > count( $p['uniqueContentPages'] ) ||
 813+ # Check edit comment use
 814+ $conds['editComments'] > $p['editComments'] ||
 815+ # Check user edit count
 816+ $conds['edits'] > $user->getEditCount() ||
 817+ # Check account age
 818+ ( $regTime && $conds['days'] > ( ( time() - $regTime ) / 86400 ) ) ||
 819+ # Check user email
 820+ $conds['email'] && !$user->isEmailConfirmed() ||
 821+ # Don't grant to currently blocked users...
 822+ $user->isBlocked()
 823+ ) {
 824+ return true;
 825+ }
 826+ # Check if user edited enough content pages
 827+ $failedContentEdits = ( $conds['totalContentEdits'] > $p['totalContentEdits'] );
 828+
 829+ # Check if results are cached to avoid DB queries.
 830+ # Checked basic, already available, promotion heuristics first...
 831+ $APSkipKey = wfMemcKey( 'flaggedrevs', 'autoreview-skip', $user->getId() );
 832+ if ( $wgMemc->get( $APSkipKey ) === 'true' ) {
 833+ return true;
 834+ }
 835+ # Check if user was ever blocked before
 836+ if ( $conds['neverBlocked'] && self::wasPreviouslyBlocked( $user ) ) {
 837+ $wgMemc->set( $APSkipKey, 'true', 3600 * 24 * 7 ); // cache results
 838+ return true;
 839+ }
 840+ # Check for edit spacing. This lets us know that the account has
 841+ # been used over N different days, rather than all in one lump.
 842+ if ( $conds['spacing'] > 0 && $conds['benchmarks'] > 1 ) {
 843+ $sTestKey = wfMemcKey( 'flaggedrevs', 'autoreview-spacing-ok', $user->getId() );
 844+ # Hit the DB only if the result is not cached...
 845+ if ( $wgMemc->get( $sTestKey ) !== 'true' ) {
 846+ $pass = self::editSpacingCheck( $conds['spacing'], $conds['benchmarks'], $user );
 847+ # Make a key to store the results
 848+ if ( $pass === true ) {
 849+ $wgMemc->set( $sTestKey, 'true', 7 * 24 * 3600 );
 850+ } else {
 851+ $wgMemc->set( $APSkipKey, 'true', $pass /* wait time */ );
 852+ return true;
 853+ }
 854+ }
 855+ }
 856+ # Check implicitly checked edits
 857+ if ( $failedContentEdits && $conds['totalCheckedEdits'] > 0 ) {
 858+ if ( !self::reviewedEditsCheck( $user, $conds['totalCheckedEdits'] ) ) {
 859+ return true;
 860+ }
 861+ }
 862+ $promote[] = 'autoreview'; // add the group
 863+ return true;
 864+ }
 865+
 866+ /**
 867+ * Autopromotes user according to the setting in $wgFlaggedRevsAutopromote.
 868+ * @param $user User
 869+ * @param $p array user tallies
 870+ * @param $conds array $wgFlaggedRevsAutopromote
 871+ */
 872+ protected static function maybeMakeEditor( User $user, array $p, array $conds ) {
 873+ global $wgMemc, $wgContentNamespaces;
 874+ $groups = $user->getGroups(); // current groups
 875+ $regTime = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
 876+ if (
 877+ !$user->getId() ||
 878+ # Do not give this to current holders
 879+ in_array( 'editor', $groups ) ||
 880+ # Do not give this right to bots
 881+ $user->isAllowed( 'bot' ) ||
 882+ # Do not re-add status if it was previously removed!
 883+ ( isset( $p['demoted'] ) && $p['demoted'] ) ||
 884+ # Check if user edited enough unique pages
 885+ $conds['uniqueContentPages'] > count( $p['uniqueContentPages'] ) ||
 886+ # Check edit summary usage
 887+ $conds['editComments'] > $p['editComments'] ||
 888+ # Check reverted edits
 889+ $conds['maxRevertedEditRatio']*$user->getEditCount() < $p['revertedEdits'] ||
 890+ # Check user edit count
 891+ $conds['edits'] > $user->getEditCount() ||
 892+ # Check account age
 893+ ( $regTime && $conds['days'] > ( ( time() - $regTime ) / 86400 ) ) ||
 894+ # See if the page actually has sufficient content...
 895+ $conds['userpageBytes'] > $user->getUserPage()->getLength() ||
 896+ # Don't grant to currently blocked users...
 897+ $user->isBlocked()
 898+ ) {
 899+ return true; // not ready
 900+ }
 901+ # User needs to meet 'totalContentEdits' OR 'totalCheckedEdits'
 902+ $failedContentEdits = ( $conds['totalContentEdits'] > $p['totalContentEdits'] );
 903+
 904+ # More expensive checks below...
 905+ # Check if results are cached to avoid DB queries
 906+ $APSkipKey = wfMemcKey( 'flaggedrevs', 'autopromote-skip', $user->getId() );
 907+ if ( $wgMemc->get( $APSkipKey ) === 'true' ) {
 908+ return true;
 909+ }
 910+ # Check if user was ever blocked before
 911+ if ( $conds['neverBlocked'] && self::wasPreviouslyBlocked( $user ) ) {
 912+ $wgMemc->set( $APSkipKey, 'true', 3600 * 24 * 7 ); // cache results
 913+ return true;
 914+ }
 915+ $dbr = wfGetDB( DB_SLAVE );
 916+ $cutoff_ts = 0;
 917+ # Check to see if the user has enough non-"last minute" edits.
 918+ if ( $conds['excludeLastDays'] > 0 ) {
 919+ $minDiffAll = $user->getEditCount() - $conds['edits'] + 1;
 920+ # Get cutoff timestamp
 921+ $cutoff_ts = time() - 86400*$conds['excludeLastDays'];
 922+ $encCutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_ts ) );
 923+ # Check all recent edits...
 924+ $res = $dbr->select( 'revision', '1',
 925+ array( 'rev_user' => $user->getId(), "rev_timestamp > $encCutoff" ),
 926+ __METHOD__,
 927+ array( 'USE INDEX' => 'user_timestamp', 'LIMIT' => $minDiffAll )
 928+ );
 929+ if ( $dbr->numRows( $res ) >= $minDiffAll ) {
 930+ return true; // delay promotion
 931+ }
 932+ # Check recent content edits...
 933+ if ( !$failedContentEdits && $wgContentNamespaces ) {
 934+ $minDiffContent = $p['totalContentEdits'] - $conds['totalContentEdits'] + 1;
 935+ $res = $dbr->select( array( 'revision', 'page' ), '1',
 936+ array( 'rev_user' => $user->getId(),
 937+ "rev_timestamp > $encCutoff",
 938+ 'rev_page = page_id',
 939+ 'page_namespace' => $wgContentNamespaces ),
 940+ __METHOD__,
 941+ array( 'USE INDEX' => array( 'revision' => 'user_timestamp' ),
 942+ 'LIMIT' => $minDiffContent )
 943+ );
 944+ if ( $dbr->numRows( $res ) >= $minDiffContent ) {
 945+ $failedContentEdits = true; // totalCheckedEdits needed
 946+ }
 947+ }
 948+ }
 949+ # Check for edit spacing. This lets us know that the account has
 950+ # been used over N different days, rather than all in one lump.
 951+ if ( $conds['spacing'] > 0 && $conds['benchmarks'] > 1 ) {
 952+ $pass = self::editSpacingCheck( $conds['spacing'], $conds['benchmarks'], $user );
 953+ if ( $pass !== true ) {
 954+ $wgMemc->set( $APSkipKey, 'true', $pass /* wait time */ ); // cache results
 955+ return true;
 956+ }
 957+ }
 958+ # Check if there are enough implicitly reviewed edits
 959+ if ( $failedContentEdits && $conds['totalCheckedEdits'] > 0 ) {
 960+ if ( !self::reviewedEditsCheck( $user, $conds['totalCheckedEdits'], $cutoff_ts ) ) {
 961+ return true;
 962+ }
 963+ }
 964+
 965+ # Add editor rights...
 966+ $newGroups = $groups;
 967+ array_push( $newGroups, 'editor' );
 968+ $log = new LogPage( 'rights', false /* $rc */ );
 969+ $log->addEntry( 'rights',
 970+ $user->getUserPage(),
 971+ wfMsgForContent( 'rights-editor-autosum' ),
 972+ array( implode( ', ', $groups ), implode( ', ', $newGroups ) )
 973+ );
 974+ $user->addGroup( 'editor' );
 975+
 976+ return true;
 977+ }
 978+
 979+ /**
 980+ * Record demotion so that auto-promote will be disabled
 981+ */
 982+ public static function recordDemote( $user, array $addgroup, array $removegroup ) {
 983+ if ( $removegroup && in_array( 'editor', $removegroup ) ) {
 984+ $dbName = false; // this wiki
 985+ // Cross-wiki rights changes...
 986+ if ( $user instanceof UserRightsProxy ) {
 987+ $dbName = $user->getDBName(); // use foreign DB of the user
 988+ }
 989+ $p = FRUserCounters::getUserParams( $user->getId(), FR_FOR_UPDATE, $dbName );
 990+ $p['demoted'] = 1;
 991+ FRUserCounters::saveUserParams( $user->getId(), $p, $dbName );
 992+ }
 993+ return true;
 994+ }
 995+
 996+ public static function stableDumpQuery( array &$tables, array &$opts, array &$join ) {
 997+ $namespaces = FlaggedRevs::getReviewNamespaces();
 998+ if ( $namespaces ) {
 999+ $tables[] = 'flaggedpages';
 1000+ $opts['ORDER BY'] = 'fp_page_id ASC';
 1001+ $opts['USE INDEX'] = array( 'flaggedpages' => 'PRIMARY' );
 1002+ $join['page'] = array( 'INNER JOIN',
 1003+ array( 'page_id = fp_page_id', 'page_namespace' => $namespaces )
 1004+ );
 1005+ $join['revision'] = array( 'INNER JOIN',
 1006+ 'rev_page = fp_page_id AND rev_id = fp_stable' );
 1007+ }
 1008+ return false; // final
 1009+ }
 1010+}
Property changes on: trunk/extensions/FlaggedRevs/dataclasses/FlaggedRevs.hooks.php
___________________________________________________________________
Added: svn:eol-style
11011 + native

Status & tagging log