Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like DifferenceEngine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DifferenceEngine, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 36 | class DifferenceEngine extends ContextSource { |
||
| 37 | |||
| 38 | /** @var int */ |
||
| 39 | public $mOldid; |
||
| 40 | |||
| 41 | /** @var int */ |
||
| 42 | public $mNewid; |
||
| 43 | |||
| 44 | private $mOldTags; |
||
| 45 | private $mNewTags; |
||
| 46 | |||
| 47 | /** @var Content */ |
||
| 48 | public $mOldContent; |
||
| 49 | |||
| 50 | /** @var Content */ |
||
| 51 | public $mNewContent; |
||
| 52 | |||
| 53 | /** @var Language */ |
||
| 54 | protected $mDiffLang; |
||
| 55 | |||
| 56 | /** @var Title */ |
||
| 57 | public $mOldPage; |
||
| 58 | |||
| 59 | /** @var Title */ |
||
| 60 | public $mNewPage; |
||
| 61 | |||
| 62 | /** @var Revision */ |
||
| 63 | public $mOldRev; |
||
| 64 | |||
| 65 | /** @var Revision */ |
||
| 66 | public $mNewRev; |
||
| 67 | |||
| 68 | /** @var bool Have the revisions IDs been loaded */ |
||
| 69 | private $mRevisionsIdsLoaded = false; |
||
| 70 | |||
| 71 | /** @var bool Have the revisions been loaded */ |
||
| 72 | public $mRevisionsLoaded = false; |
||
| 73 | |||
| 74 | /** @var int How many text blobs have been loaded, 0, 1 or 2? */ |
||
| 75 | public $mTextLoaded = 0; |
||
| 76 | |||
| 77 | /** @var bool Was the diff fetched from cache? */ |
||
| 78 | public $mCacheHit = false; |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Set this to true to add debug info to the HTML output. |
||
| 82 | * Warning: this may cause RSS readers to spuriously mark articles as "new" |
||
| 83 | * (bug 20601) |
||
| 84 | */ |
||
| 85 | public $enableDebugComment = false; |
||
| 86 | |||
| 87 | /** @var bool If true, line X is not displayed when X is 1, for example |
||
| 88 | * to increase readability and conserve space with many small diffs. |
||
| 89 | */ |
||
| 90 | protected $mReducedLineNumbers = false; |
||
| 91 | |||
| 92 | /** @var string Link to action=markpatrolled */ |
||
| 93 | protected $mMarkPatrolledLink = null; |
||
| 94 | |||
| 95 | /** @var bool Show rev_deleted content if allowed */ |
||
| 96 | protected $unhide = false; |
||
| 97 | |||
| 98 | /** @var bool Refresh the diff cache */ |
||
| 99 | protected $mRefreshCache = false; |
||
| 100 | |||
| 101 | /**#@-*/ |
||
| 102 | |||
| 103 | /** |
||
| 104 | * Constructor |
||
| 105 | * @param IContextSource $context Context to use, anything else will be ignored |
||
| 106 | * @param int $old Old ID we want to show and diff with. |
||
| 107 | * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0. |
||
| 108 | * @param int $rcid Deprecated, no longer used! |
||
| 109 | * @param bool $refreshCache If set, refreshes the diff cache |
||
| 110 | * @param bool $unhide If set, allow viewing deleted revs |
||
| 111 | */ |
||
| 112 | public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, |
||
| 113 | $refreshCache = false, $unhide = false |
||
| 114 | ) { |
||
| 115 | if ( $context instanceof IContextSource ) { |
||
| 116 | $this->setContext( $context ); |
||
| 117 | } |
||
| 118 | |||
| 119 | wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" ); |
||
| 120 | |||
| 121 | $this->mOldid = $old; |
||
| 122 | $this->mNewid = $new; |
||
| 123 | $this->mRefreshCache = $refreshCache; |
||
| 124 | $this->unhide = $unhide; |
||
| 125 | } |
||
| 126 | |||
| 127 | /** |
||
| 128 | * @param bool $value |
||
| 129 | */ |
||
| 130 | public function setReducedLineNumbers( $value = true ) { |
||
| 131 | $this->mReducedLineNumbers = $value; |
||
| 132 | } |
||
| 133 | |||
| 134 | /** |
||
| 135 | * @return Language |
||
| 136 | */ |
||
| 137 | public function getDiffLang() { |
||
| 138 | if ( $this->mDiffLang === null ) { |
||
| 139 | # Default language in which the diff text is written. |
||
| 140 | $this->mDiffLang = $this->getTitle()->getPageLanguage(); |
||
| 141 | } |
||
| 142 | |||
| 143 | return $this->mDiffLang; |
||
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * @return bool |
||
| 148 | */ |
||
| 149 | public function wasCacheHit() { |
||
| 150 | return $this->mCacheHit; |
||
| 151 | } |
||
| 152 | |||
| 153 | /** |
||
| 154 | * @return int |
||
| 155 | */ |
||
| 156 | public function getOldid() { |
||
| 157 | $this->loadRevisionIds(); |
||
| 158 | |||
| 159 | return $this->mOldid; |
||
| 160 | } |
||
| 161 | |||
| 162 | /** |
||
| 163 | * @return bool|int |
||
| 164 | */ |
||
| 165 | public function getNewid() { |
||
| 166 | $this->loadRevisionIds(); |
||
| 167 | |||
| 168 | return $this->mNewid; |
||
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Look up a special:Undelete link to the given deleted revision id, |
||
| 173 | * as a workaround for being unable to load deleted diffs in currently. |
||
| 174 | * |
||
| 175 | * @param int $id Revision ID |
||
| 176 | * |
||
| 177 | * @return mixed URL or false |
||
| 178 | */ |
||
| 179 | public function deletedLink( $id ) { |
||
| 180 | if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { |
||
| 181 | $dbr = wfGetDB( DB_SLAVE ); |
||
| 182 | $row = $dbr->selectRow( 'archive', '*', |
||
| 183 | [ 'ar_rev_id' => $id ], |
||
| 184 | __METHOD__ ); |
||
| 185 | if ( $row ) { |
||
| 186 | $rev = Revision::newFromArchiveRow( $row ); |
||
|
|
|||
| 187 | $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); |
||
| 188 | |||
| 189 | return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [ |
||
| 190 | 'target' => $title->getPrefixedText(), |
||
| 191 | 'timestamp' => $rev->getTimestamp() |
||
| 192 | ] ); |
||
| 193 | } |
||
| 194 | } |
||
| 195 | |||
| 196 | return false; |
||
| 197 | } |
||
| 198 | |||
| 199 | /** |
||
| 200 | * Build a wikitext link toward a deleted revision, if viewable. |
||
| 201 | * |
||
| 202 | * @param int $id Revision ID |
||
| 203 | * |
||
| 204 | * @return string Wikitext fragment |
||
| 205 | */ |
||
| 206 | public function deletedIdMarker( $id ) { |
||
| 207 | $link = $this->deletedLink( $id ); |
||
| 208 | if ( $link ) { |
||
| 209 | return "[$link $id]"; |
||
| 210 | } else { |
||
| 211 | return $id; |
||
| 212 | } |
||
| 213 | } |
||
| 214 | |||
| 215 | private function showMissingRevision() { |
||
| 216 | $out = $this->getOutput(); |
||
| 217 | |||
| 218 | $missing = []; |
||
| 219 | if ( $this->mOldRev === null || |
||
| 220 | ( $this->mOldRev && $this->mOldContent === null ) |
||
| 221 | ) { |
||
| 222 | $missing[] = $this->deletedIdMarker( $this->mOldid ); |
||
| 223 | } |
||
| 224 | if ( $this->mNewRev === null || |
||
| 225 | ( $this->mNewRev && $this->mNewContent === null ) |
||
| 226 | ) { |
||
| 227 | $missing[] = $this->deletedIdMarker( $this->mNewid ); |
||
| 228 | } |
||
| 229 | |||
| 230 | $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); |
||
| 231 | $msg = $this->msg( 'difference-missing-revision' ) |
||
| 232 | ->params( $this->getLanguage()->listToText( $missing ) ) |
||
| 233 | ->numParams( count( $missing ) ) |
||
| 234 | ->parseAsBlock(); |
||
| 235 | $out->addHTML( $msg ); |
||
| 236 | } |
||
| 237 | |||
| 238 | public function showDiffPage( $diffOnly = false ) { |
||
| 239 | |||
| 240 | # Allow frames except in certain special cases |
||
| 241 | $out = $this->getOutput(); |
||
| 242 | $out->allowClickjacking(); |
||
| 243 | $out->setRobotPolicy( 'noindex,nofollow' ); |
||
| 244 | |||
| 245 | if ( !$this->loadRevisionData() ) { |
||
| 246 | $this->showMissingRevision(); |
||
| 247 | |||
| 248 | return; |
||
| 249 | } |
||
| 250 | |||
| 251 | $user = $this->getUser(); |
||
| 252 | $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user ); |
||
| 253 | if ( $this->mOldPage ) { # mOldPage might not be set, see below. |
||
| 254 | $permErrors = wfMergeErrorArrays( $permErrors, |
||
| 255 | $this->mOldPage->getUserPermissionsErrors( 'read', $user ) ); |
||
| 256 | } |
||
| 257 | if ( count( $permErrors ) ) { |
||
| 258 | throw new PermissionsError( 'read', $permErrors ); |
||
| 259 | } |
||
| 260 | |||
| 261 | $rollback = ''; |
||
| 262 | |||
| 263 | $query = []; |
||
| 264 | # Carry over 'diffonly' param via navigation links |
||
| 265 | if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) { |
||
| 266 | $query['diffonly'] = $diffOnly; |
||
| 267 | } |
||
| 268 | # Cascade unhide param in links for easy deletion browsing |
||
| 269 | if ( $this->unhide ) { |
||
| 270 | $query['unhide'] = 1; |
||
| 271 | } |
||
| 272 | |||
| 273 | # Check if one of the revisions is deleted/suppressed |
||
| 274 | $deleted = $suppressed = false; |
||
| 275 | $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user ); |
||
| 276 | |||
| 277 | $revisionTools = []; |
||
| 278 | |||
| 279 | # mOldRev is false if the difference engine is called with a "vague" query for |
||
| 280 | # a diff between a version V and its previous version V' AND the version V |
||
| 281 | # is the first version of that article. In that case, V' does not exist. |
||
| 282 | if ( $this->mOldRev === false ) { |
||
| 283 | $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); |
||
| 284 | $samePage = true; |
||
| 285 | $oldHeader = ''; |
||
| 286 | } else { |
||
| 287 | Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] ); |
||
| 288 | |||
| 289 | if ( $this->mNewPage->equals( $this->mOldPage ) ) { |
||
| 290 | $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); |
||
| 291 | $samePage = true; |
||
| 292 | } else { |
||
| 293 | $out->setPageTitle( $this->msg( 'difference-title-multipage', |
||
| 294 | $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) ); |
||
| 295 | $out->addSubtitle( $this->msg( 'difference-multipage' ) ); |
||
| 296 | $samePage = false; |
||
| 297 | } |
||
| 298 | |||
| 299 | if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) { |
||
| 300 | if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { |
||
| 301 | $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() ); |
||
| 302 | if ( $rollbackLink ) { |
||
| 303 | $out->preventClickjacking(); |
||
| 304 | $rollback = '   ' . $rollbackLink; |
||
| 305 | } |
||
| 306 | } |
||
| 307 | |||
| 308 | if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && |
||
| 309 | !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) |
||
| 310 | ) { |
||
| 311 | $undoLink = Html::element( 'a', [ |
||
| 312 | 'href' => $this->mNewPage->getLocalURL( [ |
||
| 313 | 'action' => 'edit', |
||
| 314 | 'undoafter' => $this->mOldid, |
||
| 315 | 'undo' => $this->mNewid |
||
| 316 | ] ), |
||
| 317 | 'title' => Linker::titleAttrib( 'undo' ), |
||
| 318 | ], |
||
| 319 | $this->msg( 'editundo' )->text() |
||
| 320 | ); |
||
| 321 | $revisionTools['mw-diff-undo'] = $undoLink; |
||
| 322 | } |
||
| 323 | } |
||
| 324 | |||
| 325 | # Make "previous revision link" |
||
| 326 | View Code Duplication | if ( $samePage && $this->mOldRev->getPrevious() ) { |
|
| 327 | $prevlink = Linker::linkKnown( |
||
| 328 | $this->mOldPage, |
||
| 329 | $this->msg( 'previousdiff' )->escaped(), |
||
| 330 | [ 'id' => 'differences-prevlink' ], |
||
| 331 | [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query |
||
| 332 | ); |
||
| 333 | } else { |
||
| 334 | $prevlink = ' '; |
||
| 335 | } |
||
| 336 | |||
| 337 | if ( $this->mOldRev->isMinor() ) { |
||
| 338 | $oldminor = ChangesList::flag( 'minor' ); |
||
| 339 | } else { |
||
| 340 | $oldminor = ''; |
||
| 341 | } |
||
| 342 | |||
| 343 | $ldel = $this->revisionDeleteLink( $this->mOldRev ); |
||
| 344 | $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' ); |
||
| 345 | $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() ); |
||
| 346 | |||
| 347 | $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . |
||
| 348 | '<div id="mw-diff-otitle2">' . |
||
| 349 | Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' . |
||
| 350 | '<div id="mw-diff-otitle3">' . $oldminor . |
||
| 351 | Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' . |
||
| 352 | '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' . |
||
| 353 | '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; |
||
| 354 | |||
| 355 | View Code Duplication | if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { |
|
| 356 | $deleted = true; // old revisions text is hidden |
||
| 357 | if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { |
||
| 358 | $suppressed = true; // also suppressed |
||
| 359 | } |
||
| 360 | } |
||
| 361 | |||
| 362 | # Check if this user can see the revisions |
||
| 363 | if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) { |
||
| 364 | $allowed = false; |
||
| 365 | } |
||
| 366 | } |
||
| 367 | |||
| 368 | # Make "next revision link" |
||
| 369 | # Skip next link on the top revision |
||
| 370 | View Code Duplication | if ( $samePage && !$this->mNewRev->isCurrent() ) { |
|
| 371 | $nextlink = Linker::linkKnown( |
||
| 372 | $this->mNewPage, |
||
| 373 | $this->msg( 'nextdiff' )->escaped(), |
||
| 374 | [ 'id' => 'differences-nextlink' ], |
||
| 375 | [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query |
||
| 376 | ); |
||
| 377 | } else { |
||
| 378 | $nextlink = ' '; |
||
| 379 | } |
||
| 380 | |||
| 381 | if ( $this->mNewRev->isMinor() ) { |
||
| 382 | $newminor = ChangesList::flag( 'minor' ); |
||
| 383 | } else { |
||
| 384 | $newminor = ''; |
||
| 385 | } |
||
| 386 | |||
| 387 | # Handle RevisionDelete links... |
||
| 388 | $rdel = $this->revisionDeleteLink( $this->mNewRev ); |
||
| 389 | |||
| 390 | # Allow extensions to define their own revision tools |
||
| 391 | Hooks::run( 'DiffRevisionTools', |
||
| 392 | [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] ); |
||
| 393 | $formattedRevisionTools = []; |
||
| 394 | // Put each one in parentheses (poor man's button) |
||
| 395 | foreach ( $revisionTools as $key => $tool ) { |
||
| 396 | $toolClass = is_string( $key ) ? $key : 'mw-diff-tool'; |
||
| 397 | $element = Html::rawElement( |
||
| 398 | 'span', |
||
| 399 | [ 'class' => $toolClass ], |
||
| 400 | $this->msg( 'parentheses' )->rawParams( $tool )->escaped() |
||
| 401 | ); |
||
| 402 | $formattedRevisionTools[] = $element; |
||
| 403 | } |
||
| 404 | $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . |
||
| 405 | ' ' . implode( ' ', $formattedRevisionTools ); |
||
| 406 | $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() ); |
||
| 407 | |||
| 408 | $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . |
||
| 409 | '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . |
||
| 410 | " $rollback</div>" . |
||
| 411 | '<div id="mw-diff-ntitle3">' . $newminor . |
||
| 412 | Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . |
||
| 413 | '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' . |
||
| 414 | '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; |
||
| 415 | |||
| 416 | View Code Duplication | if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { |
|
| 417 | $deleted = true; // new revisions text is hidden |
||
| 418 | if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { |
||
| 419 | $suppressed = true; // also suppressed |
||
| 420 | } |
||
| 421 | } |
||
| 422 | |||
| 423 | # If the diff cannot be shown due to a deleted revision, then output |
||
| 424 | # the diff header and links to unhide (if available)... |
||
| 425 | if ( $deleted && ( !$this->unhide || !$allowed ) ) { |
||
| 426 | $this->showDiffStyle(); |
||
| 427 | $multi = $this->getMultiNotice(); |
||
| 428 | $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); |
||
| 429 | if ( !$allowed ) { |
||
| 430 | $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; |
||
| 431 | # Give explanation for why revision is not visible |
||
| 432 | $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", |
||
| 433 | [ $msg ] ); |
||
| 434 | } else { |
||
| 435 | # Give explanation and add a link to view the diff... |
||
| 436 | $query = $this->getRequest()->appendQueryValue( 'unhide', '1' ); |
||
| 437 | $link = $this->getTitle()->getFullURL( $query ); |
||
| 438 | $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; |
||
| 439 | $out->wrapWikiMsg( |
||
| 440 | "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", |
||
| 441 | [ $msg, $link ] |
||
| 442 | ); |
||
| 443 | } |
||
| 444 | # Otherwise, output a regular diff... |
||
| 445 | } else { |
||
| 446 | # Add deletion notice if the user is viewing deleted content |
||
| 447 | $notice = ''; |
||
| 448 | if ( $deleted ) { |
||
| 449 | $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; |
||
| 450 | $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . |
||
| 451 | $this->msg( $msg )->parse() . |
||
| 452 | "</div>\n"; |
||
| 453 | } |
||
| 454 | $this->showDiff( $oldHeader, $newHeader, $notice ); |
||
| 455 | if ( !$diffOnly ) { |
||
| 456 | $this->renderNewRevision(); |
||
| 457 | } |
||
| 458 | } |
||
| 459 | } |
||
| 460 | |||
| 461 | /** |
||
| 462 | * Build a link to mark a change as patrolled. |
||
| 463 | * |
||
| 464 | * Returns empty string if there's either no revision to patrol or the user is not allowed to. |
||
| 465 | * Side effect: When the patrol link is build, this method will call |
||
| 466 | * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax. |
||
| 467 | * |
||
| 468 | * @return string HTML or empty string |
||
| 469 | */ |
||
| 470 | protected function markPatrolledLink() { |
||
| 492 | |||
| 493 | /** |
||
| 494 | * Returns an array of meta data needed to build a "mark as patrolled" link and |
||
| 495 | * adds the mediawiki.page.patrol.ajax to the output. |
||
| 496 | * |
||
| 497 | * @return array|false An array of meta data for a patrol link (rcid & token) |
||
| 498 | * or false if no link is needed |
||
| 499 | */ |
||
| 500 | protected function getMarkPatrolledLinkInfo() { |
||
| 501 | global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; |
||
| 502 | |||
| 503 | $user = $this->getUser(); |
||
| 504 | |||
| 505 | // Prepare a change patrol link, if applicable |
||
| 506 | if ( |
||
| 507 | // Is patrolling enabled and the user allowed to? |
||
| 508 | $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) && |
||
| 509 | // Only do this if the revision isn't more than 6 hours older |
||
| 510 | // than the Max RC age (6h because the RC might not be cleaned out regularly) |
||
| 511 | RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 ) |
||
| 512 | ) { |
||
| 513 | // Look for an unpatrolled change corresponding to this diff |
||
| 514 | $db = wfGetDB( DB_SLAVE ); |
||
| 515 | $change = RecentChange::newFromConds( |
||
| 516 | [ |
||
| 517 | 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), |
||
| 518 | 'rc_this_oldid' => $this->mNewid, |
||
| 519 | 'rc_patrolled' => 0 |
||
| 520 | ], |
||
| 521 | __METHOD__ |
||
| 522 | ); |
||
| 523 | |||
| 524 | if ( $change && !$change->getPerformer()->equals( $user ) ) { |
||
| 525 | $rcid = $change->getAttribute( 'rc_id' ); |
||
| 526 | } else { |
||
| 527 | // None found or the page has been created by the current user. |
||
| 528 | // If the user could patrol this it already would be patrolled |
||
| 529 | $rcid = 0; |
||
| 530 | } |
||
| 531 | // Build the link |
||
| 532 | if ( $rcid ) { |
||
| 533 | $this->getOutput()->preventClickjacking(); |
||
| 534 | if ( $wgEnableAPI && $wgEnableWriteAPI |
||
| 535 | && $user->isAllowed( 'writeapi' ) |
||
| 536 | ) { |
||
| 537 | $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' ); |
||
| 538 | } |
||
| 539 | |||
| 540 | $token = $user->getEditToken( $rcid ); |
||
| 541 | return [ |
||
| 542 | 'rcid' => $rcid, |
||
| 543 | 'token' => $token, |
||
| 544 | ]; |
||
| 545 | } |
||
| 546 | } |
||
| 547 | |||
| 548 | // No mark as patrolled link applicable |
||
| 549 | return false; |
||
| 550 | } |
||
| 551 | |||
| 552 | /** |
||
| 553 | * @param Revision $rev |
||
| 554 | * |
||
| 555 | * @return string |
||
| 556 | */ |
||
| 557 | protected function revisionDeleteLink( $rev ) { |
||
| 558 | $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); |
||
| 559 | if ( $link !== '' ) { |
||
| 560 | $link = '   ' . $link . ' '; |
||
| 561 | } |
||
| 562 | |||
| 563 | return $link; |
||
| 564 | } |
||
| 565 | |||
| 566 | /** |
||
| 567 | * Show the new revision of the page. |
||
| 568 | */ |
||
| 569 | public function renderNewRevision() { |
||
| 570 | $out = $this->getOutput(); |
||
| 571 | $revHeader = $this->getRevisionHeader( $this->mNewRev ); |
||
| 572 | # Add "current version as of X" title |
||
| 573 | $out->addHTML( "<hr class='diff-hr' id='mw-oldid' /> |
||
| 574 | <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); |
||
| 575 | # Page content may be handled by a hooked call instead... |
||
| 576 | # @codingStandardsIgnoreStart Ignoring long lines. |
||
| 577 | if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) { |
||
| 578 | $this->loadNewText(); |
||
| 579 | $out->setRevisionId( $this->mNewid ); |
||
| 580 | $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); |
||
| 581 | $out->setArticleFlag( true ); |
||
| 582 | |||
| 583 | // NOTE: only needed for B/C: custom rendering of JS/CSS via hook |
||
| 584 | if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { |
||
| 585 | // This needs to be synchronised with Article::showCssOrJsPage(), which sucks |
||
| 586 | // Give hooks a chance to customise the output |
||
| 587 | // @todo standardize this crap into one function |
||
| 588 | if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ] ) ) { |
||
| 589 | // NOTE: deprecated hook, B/C only |
||
| 590 | // use the content object's own rendering |
||
| 591 | $cnt = $this->mNewRev->getContent(); |
||
| 592 | $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null; |
||
| 593 | if ( $po ) { |
||
| 594 | $out->addParserOutputContent( $po ); |
||
| 595 | } |
||
| 596 | } |
||
| 597 | } elseif ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) { |
||
| 598 | // Handled by extension |
||
| 599 | } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) { |
||
| 600 | // NOTE: deprecated hook, B/C only |
||
| 601 | // Handled by extension |
||
| 602 | } else { |
||
| 603 | // Normal page |
||
| 604 | if ( $this->getTitle()->equals( $this->mNewPage ) ) { |
||
| 605 | // If the Title stored in the context is the same as the one |
||
| 606 | // of the new revision, we can use its associated WikiPage |
||
| 607 | // object. |
||
| 608 | $wikiPage = $this->getWikiPage(); |
||
| 609 | } else { |
||
| 610 | // Otherwise we need to create our own WikiPage object |
||
| 611 | $wikiPage = WikiPage::factory( $this->mNewPage ); |
||
| 612 | } |
||
| 613 | |||
| 614 | $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); |
||
| 615 | |||
| 616 | # WikiPage::getParserOutput() should not return false, but just in case |
||
| 617 | if ( $parserOutput ) { |
||
| 618 | $out->addParserOutput( $parserOutput ); |
||
| 619 | } |
||
| 620 | } |
||
| 621 | } |
||
| 622 | # @codingStandardsIgnoreEnd |
||
| 623 | |||
| 624 | # Add redundant patrol link on bottom... |
||
| 625 | $out->addHTML( $this->markPatrolledLink() ); |
||
| 626 | |||
| 627 | } |
||
| 628 | |||
| 629 | protected function getParserOutput( WikiPage $page, Revision $rev ) { |
||
| 630 | $parserOptions = $page->makeParserOptions( $this->getContext() ); |
||
| 631 | |||
| 632 | if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) { |
||
| 633 | $parserOptions->setEditSection( false ); |
||
| 634 | } |
||
| 635 | |||
| 636 | $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); |
||
| 637 | |||
| 638 | return $parserOutput; |
||
| 639 | } |
||
| 640 | |||
| 641 | /** |
||
| 642 | * Get the diff text, send it to the OutputPage object |
||
| 643 | * Returns false if the diff could not be generated, otherwise returns true |
||
| 644 | * |
||
| 645 | * @param string|bool $otitle Header for old text or false |
||
| 646 | * @param string|bool $ntitle Header for new text or false |
||
| 647 | * @param string $notice HTML between diff header and body |
||
| 648 | * |
||
| 649 | * @return bool |
||
| 650 | */ |
||
| 651 | public function showDiff( $otitle, $ntitle, $notice = '' ) { |
||
| 652 | $diff = $this->getDiff( $otitle, $ntitle, $notice ); |
||
| 653 | if ( $diff === false ) { |
||
| 654 | $this->showMissingRevision(); |
||
| 655 | |||
| 656 | return false; |
||
| 657 | } else { |
||
| 658 | $this->showDiffStyle(); |
||
| 659 | $this->getOutput()->addHTML( $diff ); |
||
| 660 | |||
| 661 | return true; |
||
| 662 | } |
||
| 663 | } |
||
| 664 | |||
| 665 | /** |
||
| 666 | * Add style sheets and supporting JS for diff display. |
||
| 667 | */ |
||
| 668 | public function showDiffStyle() { |
||
| 669 | $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); |
||
| 670 | } |
||
| 671 | |||
| 672 | /** |
||
| 673 | * Get complete diff table, including header |
||
| 674 | * |
||
| 675 | * @param string|bool $otitle Header for old text or false |
||
| 676 | * @param string|bool $ntitle Header for new text or false |
||
| 677 | * @param string $notice HTML between diff header and body |
||
| 678 | * |
||
| 679 | * @return mixed |
||
| 680 | */ |
||
| 681 | public function getDiff( $otitle, $ntitle, $notice = '' ) { |
||
| 682 | $body = $this->getDiffBody(); |
||
| 683 | if ( $body === false ) { |
||
| 684 | return false; |
||
| 685 | } |
||
| 686 | |||
| 687 | $multi = $this->getMultiNotice(); |
||
| 688 | // Display a message when the diff is empty |
||
| 689 | if ( $body === '' ) { |
||
| 690 | $notice .= '<div class="mw-diff-empty">' . |
||
| 691 | $this->msg( 'diff-empty' )->parse() . |
||
| 692 | "</div>\n"; |
||
| 693 | } |
||
| 694 | |||
| 695 | return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); |
||
| 696 | } |
||
| 697 | |||
| 698 | /** |
||
| 699 | * Get the diff table body, without header |
||
| 700 | * |
||
| 701 | * @return mixed (string/false) |
||
| 702 | */ |
||
| 703 | public function getDiffBody() { |
||
| 704 | $this->mCacheHit = true; |
||
| 705 | // Check if the diff should be hidden from this user |
||
| 706 | if ( !$this->loadRevisionData() ) { |
||
| 707 | return false; |
||
| 708 | } elseif ( $this->mOldRev && |
||
| 709 | !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) |
||
| 710 | ) { |
||
| 711 | return false; |
||
| 712 | } elseif ( $this->mNewRev && |
||
| 713 | !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) |
||
| 714 | ) { |
||
| 715 | return false; |
||
| 716 | } |
||
| 717 | // Short-circuit |
||
| 718 | if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev |
||
| 719 | && $this->mOldRev->getId() == $this->mNewRev->getId() ) |
||
| 720 | ) { |
||
| 721 | return ''; |
||
| 722 | } |
||
| 723 | // Cacheable? |
||
| 724 | $key = false; |
||
| 725 | $cache = ObjectCache::getMainWANInstance(); |
||
| 726 | if ( $this->mOldid && $this->mNewid ) { |
||
| 727 | $key = $this->getDiffBodyCacheKey(); |
||
| 728 | |||
| 729 | // Try cache |
||
| 730 | if ( !$this->mRefreshCache ) { |
||
| 731 | $difftext = $cache->get( $key ); |
||
| 732 | if ( $difftext ) { |
||
| 733 | wfIncrStats( 'diff_cache.hit' ); |
||
| 734 | $difftext = $this->localiseLineNumbers( $difftext ); |
||
| 735 | $difftext .= "\n<!-- diff cache key $key -->\n"; |
||
| 736 | |||
| 737 | return $difftext; |
||
| 738 | } |
||
| 739 | } // don't try to load but save the result |
||
| 740 | } |
||
| 741 | $this->mCacheHit = false; |
||
| 742 | |||
| 743 | // Loadtext is permission safe, this just clears out the diff |
||
| 744 | if ( !$this->loadText() ) { |
||
| 745 | return false; |
||
| 746 | } |
||
| 747 | |||
| 748 | $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); |
||
| 749 | |||
| 750 | // Save to cache for 7 days |
||
| 751 | if ( !Hooks::run( 'AbortDiffCache', [ &$this ] ) ) { |
||
| 752 | wfIncrStats( 'diff_cache.uncacheable' ); |
||
| 753 | } elseif ( $key !== false && $difftext !== false ) { |
||
| 754 | wfIncrStats( 'diff_cache.miss' ); |
||
| 755 | $cache->set( $key, $difftext, 7 * 86400 ); |
||
| 756 | } else { |
||
| 757 | wfIncrStats( 'diff_cache.uncacheable' ); |
||
| 758 | } |
||
| 759 | // Replace line numbers with the text in the user's language |
||
| 760 | if ( $difftext !== false ) { |
||
| 761 | $difftext = $this->localiseLineNumbers( $difftext ); |
||
| 762 | } |
||
| 763 | |||
| 764 | return $difftext; |
||
| 765 | } |
||
| 766 | |||
| 767 | /** |
||
| 768 | * Returns the cache key for diff body text or content. |
||
| 769 | * |
||
| 770 | * @since 1.23 |
||
| 771 | * |
||
| 772 | * @throws MWException |
||
| 773 | * @return string |
||
| 774 | */ |
||
| 775 | protected function getDiffBodyCacheKey() { |
||
| 776 | if ( !$this->mOldid || !$this->mNewid ) { |
||
| 777 | throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' ); |
||
| 778 | } |
||
| 779 | |||
| 780 | return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, |
||
| 781 | 'oldid', $this->mOldid, 'newid', $this->mNewid ); |
||
| 782 | } |
||
| 783 | |||
| 784 | /** |
||
| 785 | * Generate a diff, no caching. |
||
| 786 | * |
||
| 787 | * This implementation uses generateTextDiffBody() to generate a diff based on the default |
||
| 788 | * serialization of the given Content objects. This will fail if $old or $new are not |
||
| 789 | * instances of TextContent. |
||
| 790 | * |
||
| 791 | * Subclasses may override this to provide a different rendering for the diff, |
||
| 792 | * perhaps taking advantage of the content's native form. This is required for all content |
||
| 793 | * models that are not text based. |
||
| 794 | * |
||
| 795 | * @since 1.21 |
||
| 796 | * |
||
| 797 | * @param Content $old Old content |
||
| 798 | * @param Content $new New content |
||
| 799 | * |
||
| 800 | * @throws MWException If old or new content is not an instance of TextContent. |
||
| 801 | * @return bool|string |
||
| 802 | */ |
||
| 803 | public function generateContentDiffBody( Content $old, Content $new ) { |
||
| 804 | View Code Duplication | if ( !( $old instanceof TextContent ) ) { |
|
| 805 | throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " . |
||
| 806 | "override generateContentDiffBody to fix this." ); |
||
| 807 | } |
||
| 808 | |||
| 809 | View Code Duplication | if ( !( $new instanceof TextContent ) ) { |
|
| 810 | throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " |
||
| 811 | . "override generateContentDiffBody to fix this." ); |
||
| 812 | } |
||
| 813 | |||
| 814 | $otext = $old->serialize(); |
||
| 815 | $ntext = $new->serialize(); |
||
| 816 | |||
| 817 | return $this->generateTextDiffBody( $otext, $ntext ); |
||
| 818 | } |
||
| 819 | |||
| 820 | /** |
||
| 821 | * Generate a diff, no caching |
||
| 822 | * |
||
| 823 | * @param string $otext Old text, must be already segmented |
||
| 824 | * @param string $ntext New text, must be already segmented |
||
| 825 | * |
||
| 826 | * @return bool|string |
||
| 827 | * @deprecated since 1.21, use generateContentDiffBody() instead! |
||
| 828 | */ |
||
| 829 | public function generateDiffBody( $otext, $ntext ) { |
||
| 830 | ContentHandler::deprecated( __METHOD__, "1.21" ); |
||
| 831 | |||
| 832 | return $this->generateTextDiffBody( $otext, $ntext ); |
||
| 833 | } |
||
| 834 | |||
| 835 | /** |
||
| 836 | * Generate a diff, no caching |
||
| 837 | * |
||
| 838 | * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point. |
||
| 839 | * |
||
| 840 | * @param string $otext Old text, must be already segmented |
||
| 841 | * @param string $ntext New text, must be already segmented |
||
| 842 | * |
||
| 843 | * @return bool|string |
||
| 844 | */ |
||
| 845 | public function generateTextDiffBody( $otext, $ntext ) { |
||
| 877 | |||
| 878 | /** |
||
| 879 | * Generates diff, to be wrapped internally in a logging/instrumentation |
||
| 880 | * |
||
| 881 | * @param string $otext Old text, must be already segmented |
||
| 882 | * @param string $ntext New text, must be already segmented |
||
| 883 | * @return bool|string |
||
| 884 | */ |
||
| 885 | protected function textDiff( $otext, $ntext ) { |
||
| 886 | global $wgExternalDiffEngine, $wgContLang; |
||
| 887 | |||
| 888 | $otext = str_replace( "\r\n", "\n", $otext ); |
||
| 889 | $ntext = str_replace( "\r\n", "\n", $ntext ); |
||
| 890 | |||
| 891 | if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) { |
||
| 892 | wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' ); |
||
| 893 | $wgExternalDiffEngine = false; |
||
| 894 | } |
||
| 895 | |||
| 896 | if ( $wgExternalDiffEngine == 'wikidiff2' ) { |
||
| 897 | if ( function_exists( 'wikidiff2_do_diff' ) ) { |
||
| 898 | # Better external diff engine, the 2 may some day be dropped |
||
| 941 | |||
| 942 | /** |
||
| 943 | * Generate a debug comment indicating diff generating time, |
||
| 944 | * server node, and generator backend. |
||
| 945 | * |
||
| 946 | * @param string $generator : What diff engine was used |
||
| 947 | * |
||
| 948 | * @return string |
||
| 949 | */ |
||
| 950 | protected function debug( $generator = "internal" ) { |
||
| 965 | |||
| 966 | /** |
||
| 967 | * Replace line numbers with the text in the user's language |
||
| 968 | * |
||
| 969 | * @param string $text |
||
| 970 | * |
||
| 971 | * @return mixed |
||
| 972 | */ |
||
| 973 | public function localiseLineNumbers( $text ) { |
||
| 980 | |||
| 981 | public function localiseLineNumbersCb( $matches ) { |
||
| 988 | |||
| 989 | /** |
||
| 990 | * If there are revisions between the ones being compared, return a note saying so. |
||
| 991 | * |
||
| 992 | * @return string |
||
| 993 | */ |
||
| 994 | public function getMultiNotice() { |
||
| 1027 | |||
| 1028 | /** |
||
| 1029 | * Get a notice about how many intermediate edits and users there are |
||
| 1030 | * |
||
| 1031 | * @param int $numEdits |
||
| 1032 | * @param int $numUsers |
||
| 1033 | * @param int $limit |
||
| 1034 | * |
||
| 1035 | * @return string |
||
| 1036 | */ |
||
| 1037 | public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { |
||
| 1049 | |||
| 1050 | /** |
||
| 1051 | * Get a header for a specified revision. |
||
| 1052 | * |
||
| 1053 | * @param Revision $rev |
||
| 1054 | * @param string $complete 'complete' to get the header wrapped depending |
||
| 1055 | * the visibility of the revision and a link to edit the page. |
||
| 1056 | * |
||
| 1057 | * @return string HTML fragment |
||
| 1058 | */ |
||
| 1059 | protected function getRevisionHeader( Revision $rev, $complete = '' ) { |
||
| 1111 | |||
| 1112 | /** |
||
| 1113 | * Add the header to a diff body |
||
| 1114 | * |
||
| 1115 | * @param string $diff Diff body |
||
| 1116 | * @param string $otitle Old revision header |
||
| 1117 | * @param string $ntitle New revision header |
||
| 1118 | * @param string $multi Notice telling user that there are intermediate |
||
| 1119 | * revisions between the ones being compared |
||
| 1120 | * @param string $notice Other notices, e.g. that user is viewing deleted content |
||
| 1121 | * |
||
| 1122 | * @return string |
||
| 1123 | */ |
||
| 1124 | public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { |
||
| 1172 | |||
| 1173 | /** |
||
| 1174 | * Use specified text instead of loading from the database |
||
| 1175 | * @param Content $oldContent |
||
| 1176 | * @param Content $newContent |
||
| 1177 | * @since 1.21 |
||
| 1178 | */ |
||
| 1179 | public function setContent( Content $oldContent, Content $newContent ) { |
||
| 1186 | |||
| 1187 | /** |
||
| 1188 | * Set the language in which the diff text is written |
||
| 1189 | * (Defaults to page content language). |
||
| 1190 | * @param Language|string $lang |
||
| 1191 | * @since 1.19 |
||
| 1192 | */ |
||
| 1193 | public function setTextLanguage( $lang ) { |
||
| 1196 | |||
| 1197 | /** |
||
| 1198 | * Maps a revision pair definition as accepted by DifferenceEngine constructor |
||
| 1199 | * to a pair of actual integers representing revision ids. |
||
| 1200 | * |
||
| 1201 | * @param int $old Revision id, e.g. from URL parameter 'oldid' |
||
| 1202 | * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff' |
||
| 1203 | * |
||
| 1204 | * @return int[] List of two revision ids, older first, later second. |
||
| 1205 | * Zero signifies invalid argument passed. |
||
| 1206 | * false signifies that there is no previous/next revision ($old is the oldest/newest one). |
||
| 1207 | */ |
||
| 1208 | public function mapDiffPrevNext( $old, $new ) { |
||
| 1224 | |||
| 1225 | /** |
||
| 1226 | * Load revision IDs |
||
| 1227 | */ |
||
| 1228 | private function loadRevisionIds() { |
||
| 1250 | |||
| 1251 | /** |
||
| 1252 | * Load revision metadata for the specified articles. If newid is 0, then compare |
||
| 1253 | * the old article in oldid to the current article; if oldid is 0, then |
||
| 1254 | * compare the current article to the immediately previous one (ignoring the |
||
| 1255 | * value of newid). |
||
| 1256 | * |
||
| 1257 | * If oldid is false, leave the corresponding revision object set |
||
| 1258 | * to false. This is impossible via ordinary user input, and is provided for |
||
| 1259 | * API convenience. |
||
| 1260 | * |
||
| 1261 | * @return bool |
||
| 1262 | */ |
||
| 1263 | public function loadRevisionData() { |
||
| 1337 | |||
| 1338 | /** |
||
| 1339 | * Load the text of the revisions, as well as revision data. |
||
| 1340 | * |
||
| 1341 | * @return bool |
||
| 1342 | */ |
||
| 1343 | public function loadText() { |
||
| 1371 | |||
| 1372 | /** |
||
| 1373 | * Load the text of the new revision, not the old one |
||
| 1374 | * |
||
| 1375 | * @return bool |
||
| 1376 | */ |
||
| 1377 | public function loadNewText() { |
||
| 1392 | |||
| 1393 | } |
||
| 1394 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.