Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
5.28% |
40 / 758 |
|
7.69% |
4 / 52 |
CRAP | |
0.00% |
0 / 1 |
| FlaggablePageView | |
5.28% |
40 / 758 |
|
7.69% |
4 / 52 |
73755.60 | |
0.00% |
0 / 1 |
| getInstanceCache | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| newFromTitle | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| singleton | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| __construct | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| __clone | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| clear | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| diffRevRecordsAreSet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| showingStable | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
| useSimpleUI | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| getPageViewStabilityModeForUser | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| isPageViewOrDiff | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| isPageView | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| displayTag | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
| addStatusIndicator | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
132 | |||
| setPageContent | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
156 | |||
| addTagNoticeIfApplicable | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
| determineRequestedRevision | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| setRobotPolicy | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
56 | |||
| makeParserOptions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| showDraftVersion | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
240 | |||
| showStableVersion | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
272 | |||
| showUnreviewedVersion | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
| getTopDiffToggle | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
| addToHistView | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| getEditNotices | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
272 | |||
| pendingEditNoticeMessage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| stabilityLogNotice | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| addToNoSuchSection | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| addToCategoryView | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| addReviewForm | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
182 | |||
| addStabilizationLink | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
| setActionTabs | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
72 | |||
| setViewTabs | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
156 | |||
| addDraftTab | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
182 | |||
| pageWriteOpRequested | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| getOldIDFromRequest | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| setPendingNotice | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| addToDiffView | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
272 | |||
| buildDiffHeaderItems | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| diffLinkAndMarkers | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
| diffToStableLink | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
| diffReviewMarkers | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
| getDiffRevMsgAndClass | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
| setViewFlags | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
90 | |||
| isDiffToStable | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
| injectPostEditURLParams | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
| changeSaveButton | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| editWillRequireReview | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
| editWillBeAutoreviewed | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
| addReviewCheck | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
| getBaseRevId | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| getAltBaseRevId | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Context\ContextSource; |
| 4 | use MediaWiki\Context\RequestContext; |
| 5 | use MediaWiki\EditPage\EditPage; |
| 6 | use MediaWiki\HookContainer\HookRunner; |
| 7 | use MediaWiki\Html\Html; |
| 8 | use MediaWiki\MediaWikiServices; |
| 9 | use MediaWiki\Message\Message; |
| 10 | use MediaWiki\Output\OutputPage; |
| 11 | use MediaWiki\Page\Article; |
| 12 | use MediaWiki\Page\CacheKeyHelper; |
| 13 | use MediaWiki\Page\PageIdentity; |
| 14 | use MediaWiki\Parser\ParserOptions; |
| 15 | use MediaWiki\Parser\ParserOutput; |
| 16 | use MediaWiki\Request\WebRequest; |
| 17 | use MediaWiki\Revision\RevisionRecord; |
| 18 | use MediaWiki\Skin\Skin; |
| 19 | use MediaWiki\SpecialPage\SpecialPage; |
| 20 | use MediaWiki\Title\Title; |
| 21 | use MediaWiki\User\User; |
| 22 | use MediaWiki\User\UserIdentity; |
| 23 | use OOUI\ButtonInputWidget; |
| 24 | use Wikimedia\Rdbms\IDBAccessObject; |
| 25 | |
| 26 | /** |
| 27 | * Class representing a web view of a MediaWiki page |
| 28 | */ |
| 29 | class FlaggablePageView extends ContextSource { |
| 30 | private static ?MapCacheLRU $instances = null; |
| 31 | |
| 32 | private OutputPage $out; |
| 33 | private FlaggableWikiPage $article; |
| 34 | /** @var RevisionRecord[]|null Array of `old` and `new` RevisionsRecords for diffs */ |
| 35 | private ?array $diffRevRecords = null; |
| 36 | private bool $isReviewableDiff = false; |
| 37 | private bool $isDiffFromStable = false; |
| 38 | private bool $isMultiPageDiff = false; |
| 39 | private string $reviewNotice = ''; |
| 40 | private string $diffNoticeBox = ''; |
| 41 | private string $diffIncChangeBox = ''; |
| 42 | private ?RevisionRecord $reviewFormRevRecord = null; |
| 43 | /** |
| 44 | * The stable revision. |
| 45 | */ |
| 46 | private ?FlaggedRevision $srev = null; |
| 47 | |
| 48 | /** |
| 49 | * The flagged revision being viewed. |
| 50 | */ |
| 51 | private ?FlaggedRevision $frev = null; |
| 52 | |
| 53 | /** |
| 54 | * @return MapCacheLRU |
| 55 | */ |
| 56 | private static function getInstanceCache(): MapCacheLRU { |
| 57 | if ( !self::$instances ) { |
| 58 | self::$instances = new MapCacheLRU( 10 ); |
| 59 | } |
| 60 | return self::$instances; |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Get a FlaggableWikiPage for a given title |
| 65 | * |
| 66 | * @param PageIdentity $title |
| 67 | * |
| 68 | * @return self |
| 69 | */ |
| 70 | public static function newFromTitle( PageIdentity $title ): FlaggablePageView { |
| 71 | $cache = self::getInstanceCache(); |
| 72 | $key = CacheKeyHelper::getKeyForPage( $title ); |
| 73 | $view = $cache->get( $key ); |
| 74 | if ( !$view ) { |
| 75 | $view = new self( $title ); |
| 76 | $cache->set( $key, $view ); |
| 77 | } |
| 78 | return $view; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Get the FlaggablePageView for this request |
| 83 | * |
| 84 | * @deprecated Use ::newFromTitle() instead |
| 85 | * @return self |
| 86 | */ |
| 87 | public static function singleton(): FlaggablePageView { |
| 88 | return self::newFromTitle( RequestContext::getMain()->getTitle() ); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * @param Title|PageIdentity $title |
| 93 | */ |
| 94 | private function __construct( PageIdentity $title ) { |
| 95 | // Title is injected (a step up from everything being global), but |
| 96 | // the rest is still implicitly uses RequestContext::getMain() |
| 97 | // via parent class ContextSource::getContext(). |
| 98 | // TODO: Inject $context and call setContext() here. |
| 99 | |
| 100 | if ( !$title->canExist() ) { |
| 101 | throw new InvalidArgumentException( 'FlaggablePageView needs a proper page' ); |
| 102 | } |
| 103 | $this->article = FlaggableWikiPage::getTitleInstance( $title ); |
| 104 | $this->out = $this->getOutput(); // convenience |
| 105 | } |
| 106 | |
| 107 | private function __clone() { |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Clear the FlaggablePageView for this request. |
| 112 | * Only needed when page redirection changes the environment. |
| 113 | */ |
| 114 | public function clear(): void { |
| 115 | self::$instances = null; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Check if the old and new diff revs are set for this page view |
| 120 | */ |
| 121 | public function diffRevRecordsAreSet(): bool { |
| 122 | return (bool)$this->diffRevRecords; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Assuming that the current request is a page view (see isPageView()), |
| 127 | * check if a stable version exists and should be displayed. |
| 128 | */ |
| 129 | public function showingStable(): bool { |
| 130 | $request = $this->getRequest(); |
| 131 | |
| 132 | $canShowStable = ( |
| 133 | // Page is reviewable and has a stable version |
| 134 | $this->article->getStableRev() && |
| 135 | // No parameters requesting a different version of the page |
| 136 | !$request->getCheck( 'oldid' ) |
| 137 | ); |
| 138 | if ( !$canShowStable ) { |
| 139 | return false; |
| 140 | } |
| 141 | |
| 142 | // Check if a stable or unstable version is explicitly requested (?stable=1 or ?stable=0). |
| 143 | $stableQuery = $request->getIntOrNull( 'stable' ); |
| 144 | if ( $stableQuery !== null ) { |
| 145 | return $stableQuery === 1; |
| 146 | } |
| 147 | |
| 148 | // Otherwise follow site/page config and user preferences. |
| 149 | $reqUser = $this->getUser(); |
| 150 | $defaultForUser = $this->getPageViewStabilityModeForUser( $reqUser ); |
| 151 | return ( |
| 152 | // User is not configured to prefer current versions |
| 153 | $defaultForUser !== FR_SHOW_STABLE_NEVER && |
| 154 | // User explicitly prefers stable versions of pages |
| 155 | ( |
| 156 | $defaultForUser === FR_SHOW_STABLE_ALWAYS || |
| 157 | // Check if the stable version overrides the draft |
| 158 | $this->article->getStabilitySettings()['override'] |
| 159 | ) |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Should this be using a simple icon-based UI? |
| 165 | * Check the user's preferences first, using the site settings as the default. |
| 166 | */ |
| 167 | private function useSimpleUI(): bool { |
| 168 | $default = (int)$this->getConfig()->get( 'SimpleFlaggedRevsUI' ); |
| 169 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
| 170 | return (bool)$userOptionsLookup->getOption( |
| 171 | $this->getUser(), |
| 172 | 'flaggedrevssimpleui', |
| 173 | $default |
| 174 | ); |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * What version of pages should this user see by default? |
| 179 | * |
| 180 | * @param UserIdentity $user The user to get the stability mode for. |
| 181 | * @return int One of the FR_SHOW_STABLE_* constants |
| 182 | */ |
| 183 | private function getPageViewStabilityModeForUser( UserIdentity $user ): int { |
| 184 | $services = MediaWikiServices::getInstance(); |
| 185 | |
| 186 | # Check user preferences (e.g. "show stable version by default?") |
| 187 | $userOptionsLookup = $services->getUserOptionsLookup(); |
| 188 | $preference = (int)$userOptionsLookup->getOption( $user, 'flaggedrevsstable' ); |
| 189 | if ( $preference === FR_SHOW_STABLE_ALWAYS || $preference === FR_SHOW_STABLE_NEVER ) { |
| 190 | return $preference; |
| 191 | } |
| 192 | |
| 193 | $userIdentityUtils = $services->getUserIdentityUtils(); |
| 194 | |
| 195 | return $userIdentityUtils->isNamed( $user ) ? FR_SHOW_STABLE_NEVER : FR_SHOW_STABLE_DEFAULT; |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * Is this a view page action (including diffs)? |
| 200 | */ |
| 201 | private function isPageViewOrDiff(): bool { |
| 202 | $action = $this->getActionName(); |
| 203 | return $action === 'view' || $action === 'render'; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Is this a view page action (not including diffs)? |
| 208 | */ |
| 209 | private function isPageView(): bool { |
| 210 | $request = $this->getRequest(); |
| 211 | return $this->isPageViewOrDiff() |
| 212 | && $request->getVal( 'diff' ) === null; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Output review notice |
| 217 | */ |
| 218 | public function displayTag(): void { |
| 219 | // Sanity check that this is a reviewable page |
| 220 | if ( $this->article->isReviewable() && $this->reviewNotice ) { |
| 221 | $this->out->addSubtitle( $this->reviewNotice ); |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Adds a visual indicator to the page view based on the review status of the current page revision. |
| 227 | * |
| 228 | * This indicator helps users quickly identify whether the current revision is stable, a draft, |
| 229 | * or unchecked. The indicator is displayed using an appropriate icon and message at the top |
| 230 | * of the page, near the title. |
| 231 | * |
| 232 | * @return void |
| 233 | */ |
| 234 | public function addStatusIndicator(): void { |
| 235 | if ( $this->getSkin()->getSkinName() === 'minerva' ) { |
| 236 | return; |
| 237 | } |
| 238 | |
| 239 | if ( !$this->article->isReviewable() ) { |
| 240 | return; |
| 241 | } |
| 242 | |
| 243 | // Determine the requested revision type |
| 244 | $requestedRevision = $this->determineRequestedRevision(); |
| 245 | |
| 246 | // Determine the message key, icon class, and indicator ID based on the requested revision type |
| 247 | $statusMessageKey = ''; |
| 248 | $iconClass = ''; |
| 249 | $indicatorId = 'mw-fr-revision-toggle'; // Default ID for the indicator |
| 250 | |
| 251 | switch ( $requestedRevision ) { |
| 252 | case 'stable': |
| 253 | $statusMessageKey = 'revreview-quick-basic-same-title'; |
| 254 | $iconClass = 'cdx-fr-css-icon-review--status--stable'; |
| 255 | break; |
| 256 | case 'draft': |
| 257 | $statusMessageKey = 'revreview-draft-indicator-title'; |
| 258 | $iconClass = 'cdx-fr-css-icon-review--status--pending'; |
| 259 | break; |
| 260 | case 'unreviewed': |
| 261 | if ( !$this->out->isPrintable() ) { |
| 262 | $statusMessageKey = $this->useSimpleUI() ? 'revreview-quick-none' : 'revreview-noflagged'; |
| 263 | $iconClass = 'cdx-fr-css-icon-review--status--unchecked'; |
| 264 | } |
| 265 | break; |
| 266 | case 'invalid': |
| 267 | case 'old': |
| 268 | break; |
| 269 | } |
| 270 | |
| 271 | // Only proceed if a valid status message key was determined |
| 272 | if ( $statusMessageKey ) { |
| 273 | // Prepare the attributes for the indicator element |
| 274 | $attributes = [ |
| 275 | 'name' => 'fr-review-status', |
| 276 | 'class' => 'mw-fr-review-status-indicator', |
| 277 | 'id' => $indicatorId, |
| 278 | ]; |
| 279 | |
| 280 | // Generate the HTML for the indicator |
| 281 | $indicatorHtml = Html::rawElement( |
| 282 | 'indicator', |
| 283 | $attributes, |
| 284 | Html::element( 'span', [ 'class' => $iconClass ] ) . $this->msg( $statusMessageKey )->parse() |
| 285 | ); |
| 286 | |
| 287 | // Add the indicator to the output page |
| 288 | $this->out->setIndicators( [ 'indicator-fr-review-status' => $indicatorHtml ] ); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Determines what page content to display, prioritizing the most recent stable version if |
| 294 | * $wgFlaggedRevsOverride is set to true. |
| 295 | * |
| 296 | * Handles regular page views (?action=view) only. Does not handle oldids or diffs. VisualEditor |
| 297 | * publishes also trigger this behaviour. |
| 298 | * |
| 299 | * This method replaces the current page view with the last stable version if conditions allow. |
| 300 | * It determines the type of revision requested by the user (e.g., 'stable', 'draft', 'unreviewed'), |
| 301 | * and adjusts the page content accordingly. Depending on the revision type, it may display tags, |
| 302 | * notices, and a review form. |
| 303 | * |
| 304 | * The method also controls whether the parser cache should be used and whether the parser output |
| 305 | * is completed. |
| 306 | * |
| 307 | * @param bool|ParserOutput|null &$outputDone Indicates whether the parser output is completed. |
| 308 | * @param bool &$useParserCache Controls whether the parser cache should be used for this page view. |
| 309 | * |
| 310 | * @return void |
| 311 | */ |
| 312 | public function setPageContent( &$outputDone, &$useParserCache ): void { |
| 313 | $request = $this->getRequest(); |
| 314 | |
| 315 | // Only proceed if this is a page view without an oldid parameter, and the page exists and is reviewable |
| 316 | if ( !$this->isPageView() || $request->getVal( 'oldid' ) || !$this->article->exists() || |
| 317 | !$this->article->isReviewable() ) { |
| 318 | return; |
| 319 | } |
| 320 | |
| 321 | // Initialize $tag as an empty string |
| 322 | $tag = ''; |
| 323 | |
| 324 | // Determine the requested revision type |
| 325 | $requestedRevision = $this->determineRequestedRevision(); |
| 326 | |
| 327 | switch ( $requestedRevision ) { |
| 328 | // "Stable" means that a reviewed version of the page is being displayed. This can happen |
| 329 | // if the top revision has been marked reviewed, or if $wgFlaggedRevsOverride is set to |
| 330 | // true and a non-reviewer is viewing a page with unreviewed edits. In the latter case, |
| 331 | // the unreviewed edits will be hidden and replaced with the "stable", reviewed version. |
| 332 | case 'stable': |
| 333 | $outputDone = $this->showStableVersion( $this->srev, $tag ); |
| 334 | $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-stable-synced' : |
| 335 | 'mw-fr-stable-not-synced'; |
| 336 | $useParserCache = false; |
| 337 | break; |
| 338 | |
| 339 | // "Draft" means that a reviewer is viewing a page with some unreviewed edits. Unreviewed |
| 340 | // edits are being displayed. If $wgFlaggedRevsOverride is set to true, unreviewed edits |
| 341 | // are only displayed to reviewers. |
| 342 | case 'draft': |
| 343 | $this->showDraftVersion( $this->srev, $tag ); |
| 344 | $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-draft-synced' : |
| 345 | 'mw-fr-draft-not-synced'; |
| 346 | break; |
| 347 | |
| 348 | // A new article that has never been reviewed. No revisions will be hidden regardless of |
| 349 | // settings. We don't have a "stable", reviewed revision yet, so we have to show an |
| 350 | // unreviewed revision. |
| 351 | case 'unreviewed': |
| 352 | default: |
| 353 | $outputDone = $this->showUnreviewedVersion( $tag ); |
| 354 | $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-stable-unreviewed' : |
| 355 | 'mw-fr-stable-not-unreviewed'; |
| 356 | break; |
| 357 | } |
| 358 | |
| 359 | $this->addTagNoticeIfApplicable( $tag, $tagTypeClass ); |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Add the tag notice if applicable. |
| 364 | * |
| 365 | * @param string $tag The tag message. |
| 366 | * @param string $tagTypeClass The CSS class for the tag type. |
| 367 | */ |
| 368 | private function addTagNoticeIfApplicable( string $tag, string $tagTypeClass ): void { |
| 369 | if ( $tag !== '' ) { |
| 370 | $notice = Html::openElement( 'div', [ 'id' => 'mw-fr-revision-messages' ] ); |
| 371 | if ( $this->useSimpleUI() ) { |
| 372 | $this->addStatusIndicator(); |
| 373 | $notice .= $tag; |
| 374 | } else { |
| 375 | $cssClasses = "mw-fr-basic $tagTypeClass plainlinks noprint"; |
| 376 | $notice .= FlaggedRevsHTML::addMessageBox( 'block', $tag, [ |
| 377 | 'class' => $cssClasses, |
| 378 | ] ); |
| 379 | } |
| 380 | $notice .= Html::closeElement( 'div' ); |
| 381 | $this->reviewNotice .= $notice; |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Determines the type of revision requested based on the current request. |
| 387 | * |
| 388 | * This method determines whether to show a stable, old reviewed, draft, or unreviewed version of |
| 389 | * the page. If no specific revision is requested, it falls back on the user's preferences and |
| 390 | * site configuration to decide which version to show. |
| 391 | * |
| 392 | * The method updates the stable and flagged revision properties (`$srev` and `$frev`) accordingly. |
| 393 | * |
| 394 | * @return string The type of revision requested: 'invalid', 'old', 'stable', 'draft', or 'unreviewed'. |
| 395 | */ |
| 396 | private function determineRequestedRevision(): string { |
| 397 | $this->srev = $this->article->getStableRev(); |
| 398 | $this->frev = $this->srev; |
| 399 | |
| 400 | // Determine whether to show the draft or unreviewed version |
| 401 | if ( $this->frev ) { |
| 402 | if ( $this->showingStable() || $this->article->stableVersionIsSynced() ) { |
| 403 | return 'stable'; |
| 404 | } |
| 405 | return 'draft'; |
| 406 | } else { |
| 407 | return 'unreviewed'; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * If the page has a stable version and it shows by default, |
| 413 | * tell search crawlers to index only that version of the page. |
| 414 | * Also index the draft as well if they are synced (bug 27173). |
| 415 | */ |
| 416 | public function setRobotPolicy(): void { |
| 417 | $request = $this->getRequest(); |
| 418 | if ( $this->article->getStableRev() && $this->article->isStableShownByDefault() ) { |
| 419 | if ( $this->isPageView() && $this->showingStable() ) { |
| 420 | return; // stable version - index this |
| 421 | } elseif ( $this->out->getRevisionId() == $this->article->getStable() |
| 422 | && $this->article->stableVersionIsSynced() |
| 423 | ) { |
| 424 | return; // draft that is synced with the stable version - index this |
| 425 | } |
| 426 | $this->out->setRobotPolicy( 'noindex,nofollow' ); // don't index this version |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * @param User $reqUser |
| 432 | * @return ParserOptions |
| 433 | */ |
| 434 | private function makeParserOptions( User $reqUser ): ParserOptions { |
| 435 | $parserOptions = $this->article->makeParserOptions( $reqUser ); |
| 436 | # T349037: The ArticleParserOptions hook should be broadened to take |
| 437 | # a WikiPage (aka $this->article) instead of an Article. But for now |
| 438 | # fake the Article. |
| 439 | $article = Article::newFromWikiPage( $this->article, RequestContext::getMain() ); |
| 440 | # Allow extensions to vary parser options used for article rendering, |
| 441 | # in the same way Article does |
| 442 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
| 443 | ->onArticleParserOptions( $article, $parserOptions ); |
| 444 | |
| 445 | return $parserOptions; |
| 446 | } |
| 447 | |
| 448 | /** |
| 449 | * Displays the draft version of a page. |
| 450 | * |
| 451 | * This method outputs the draft version of a page, which may differ from the stable version. |
| 452 | * It adds a notice indicating that the page is pending review and optionally displays a diff |
| 453 | * between the stable and draft versions. If the stable and draft versions are synchronized, |
| 454 | * the method does not display a diff. It also adds a "your edit will pending" notice for users |
| 455 | * who have made unreviewed edits, especially if the user lacks review rights. |
| 456 | * |
| 457 | * The method also adjusts the tag that is used for the review box/bar info. |
| 458 | * |
| 459 | * @param FlaggedRevision $srev The stable revision. |
| 460 | * @param string &$tag Reference to the variable holding the review box/bar info. |
| 461 | * |
| 462 | * @return void |
| 463 | */ |
| 464 | private function showDraftVersion( FlaggedRevision $srev, string &$tag ): void { |
| 465 | $request = $this->getRequest(); |
| 466 | $reqUser = $this->getUser(); |
| 467 | if ( $this->out->isPrintable() ) { |
| 468 | return; // all this function does is add notices; don't show them |
| 469 | } |
| 470 | $time = $this->getLanguage()->date( $srev->getTimestamp(), true ); |
| 471 | # Get stable version sync status |
| 472 | $synced = $this->article->stableVersionIsSynced(); |
| 473 | if ( $synced ) { // draft == stable |
| 474 | $diffToggle = ''; // no diff to show |
| 475 | } else { // draft != stable |
| 476 | # The user may want the diff (via prefs) |
| 477 | $diffToggle = $this->getTopDiffToggle( $srev ); |
| 478 | if ( $diffToggle != '' ) { |
| 479 | $diffToggle = " $diffToggle"; |
| 480 | } |
| 481 | # Make sure there is always a notice bar when viewing the draft. |
| 482 | if ( $this->useSimpleUI() ) { // we already one for detailed UI |
| 483 | $this->setPendingNotice( $srev, $diffToggle ); |
| 484 | } |
| 485 | } |
| 486 | # Give a "your edit is pending" notice to newer users if |
| 487 | # an unreviewed edit was completed... |
| 488 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 489 | if ( $request->getVal( 'shownotice' ) |
| 490 | && $this->article->getUserText( RevisionRecord::RAW ) == $reqUser->getName() |
| 491 | && $this->article->revsArePending() |
| 492 | && !$pm->userHasRight( $reqUser, 'review' ) |
| 493 | ) { |
| 494 | $revsSince = $this->article->getPendingRevCount(); |
| 495 | $pending = $this->msg( 'revreview-edited', $srev->getRevId() ) |
| 496 | ->numParams( $revsSince )->parse(); |
| 497 | $anchor = $request->getVal( 'fromsection' ); |
| 498 | if ( $anchor != null ) { |
| 499 | // Hack: reverse some of the Sanitizer::escapeId() encoding |
| 500 | $section = urldecode( str_replace( // bug 35661 |
| 501 | [ ':', '.' ], [ '%3A', '%' ], $anchor |
| 502 | ) ); |
| 503 | $section = str_replace( '_', ' ', $section ); // prettify |
| 504 | $pending .= $this->msg( 'revreview-edited-section', $anchor, $section ) |
| 505 | ->parseAsBlock(); |
| 506 | } |
| 507 | # Notice should always use subtitle |
| 508 | $this->reviewNotice = Html::openElement( 'div', [ |
| 509 | 'id' => 'mw-fr-reviewnotice', |
| 510 | 'class' => 'cdx-message cdx-message--block cdx-message--notice |
| 511 | flaggedrevs_preview plainlinks noprint', |
| 512 | ] ) |
| 513 | . Html::element( 'span', [ 'class' => 'cdx-message__icon' ] ) |
| 514 | . Html::rawElement( 'div', [ 'class' => 'cdx-message__content' ], $pending ) |
| 515 | . Html::closeElement( 'div' ); |
| 516 | # Otherwise, construct some tagging info for non-printable outputs. |
| 517 | # Also, if low profile UI is enabled and the page is synced, skip the tag. |
| 518 | # Note: the "your edit is pending" notice has all this info, so we never add both. |
| 519 | } elseif ( !( $this->article->lowProfileUI() && $synced ) ) { |
| 520 | $revsSince = $this->article->getPendingRevCount(); |
| 521 | // Simple icon-based UI |
| 522 | if ( $this->useSimpleUI() ) { |
| 523 | $revisionId = $srev->getRevId(); |
| 524 | $tag .= FlaggedRevsHTML::reviewDialog( $srev, $revisionId, $revsSince, 'draft', $synced ); |
| 525 | // Standard UI |
| 526 | } else { |
| 527 | if ( $synced ) { |
| 528 | $msg = 'revreview-basic-same'; |
| 529 | } else { |
| 530 | $msg = !$revsSince ? 'revreview-newest-basic-i' : 'revreview-newest-basic'; |
| 531 | } |
| 532 | $msgHTML = $this->msg( $msg, $srev->getRevId(), $time ) |
| 533 | ->numParams( $revsSince )->parse(); |
| 534 | $tag .= $msgHTML . $diffToggle; |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | /** |
| 540 | * Displays the stable version of a page. |
| 541 | * |
| 542 | * This method outputs the stable version of a page, which is the version that has been flagged as reviewed. |
| 543 | * It generates and caches the `ParserOutput` for the stable version, if not already cached. If the stable |
| 544 | * version is synchronized with the current draft, it skips the diff display. The method also adds relevant |
| 545 | * tags and notices based on the stability and synchronization status. |
| 546 | * |
| 547 | * The method ensures that only the stable version or a synced draft is indexed by search engines. |
| 548 | * |
| 549 | * @param FlaggedRevision $srev The stable revision. |
| 550 | * @param string &$tag Reference to the variable holding the review box/bar info. |
| 551 | * |
| 552 | * @return ?ParserOutput The generated ParserOutput for the stable version, or null if generation fails. |
| 553 | */ |
| 554 | private function showStableVersion( FlaggedRevision $srev, string &$tag ): ?ParserOutput { |
| 555 | $reqUser = $this->getUser(); |
| 556 | $time = $this->getLanguage()->date( $srev->getTimestamp(), true ); |
| 557 | # Set display revision ID |
| 558 | $this->out->setRevisionId( $srev->getRevId() ); |
| 559 | $synced = $this->article->stableVersionIsSynced(); |
| 560 | # Construct some tagging |
| 561 | if ( |
| 562 | !$this->out->isPrintable() && |
| 563 | !( $this->article->lowProfileUI() && $synced ) |
| 564 | ) { |
| 565 | $revsSince = $this->article->getPendingRevCount(); |
| 566 | // Simple icon-based UI |
| 567 | if ( $this->useSimpleUI() ) { |
| 568 | # For protection based configs, show lock only if it's not redundant. |
| 569 | $revisionId = $srev->getRevId(); |
| 570 | $tag = FlaggedRevsHTML::reviewDialog( |
| 571 | $srev, |
| 572 | $revisionId, |
| 573 | $revsSince, |
| 574 | 'stable', |
| 575 | $synced |
| 576 | ); |
| 577 | // Standard UI |
| 578 | } else { |
| 579 | if ( $synced ) { |
| 580 | $msg = 'revreview-basic-same'; |
| 581 | } else { |
| 582 | $msg = !$revsSince ? 'revreview-basic-i' : 'revreview-basic'; |
| 583 | } |
| 584 | $tag = $this->msg( $msg, $srev->getRevId(), $time ) |
| 585 | ->numParams( $revsSince )->parse(); |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | // TODO: Rewrite to use ParserOutputAccess |
| 590 | $parserOptions = $this->makeParserOptions( $reqUser ); |
| 591 | $stableParserCache = FlaggedRevs::getParserCacheInstance( $parserOptions ); |
| 592 | // Check the stable version cache for the parser output |
| 593 | $parserOut = $stableParserCache->get( $this->article, $parserOptions ); |
| 594 | |
| 595 | if ( !$parserOut ) { |
| 596 | if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT && $synced ) { |
| 597 | # Stable and draft version are identical; check the draft version cache |
| 598 | $draftParserCache = MediaWikiServices::getInstance()->getParserCache(); |
| 599 | $parserOut = $draftParserCache->get( $this->article, $parserOptions ); |
| 600 | } |
| 601 | |
| 602 | if ( !$parserOut ) { |
| 603 | # Regenerate the parser output, debouncing parse requests via PoolCounter |
| 604 | $status = FlaggedRevs::parseStableRevisionPooled( $srev, $parserOptions ); |
| 605 | if ( !$status->isGood() ) { |
| 606 | $this->out->disableClientCache(); |
| 607 | $this->out->setRobotPolicy( 'noindex,nofollow' ); |
| 608 | $statusFormatter = MediaWikiServices::getInstance() |
| 609 | ->getFormatterFactory() |
| 610 | ->getStatusFormatter( $this->getContext() ); |
| 611 | $errorText = $statusFormatter->getMessage( $status ); |
| 612 | $this->out->addHTML( |
| 613 | Html::errorBox( $this->out->parseAsContent( $errorText ) ) |
| 614 | ); |
| 615 | return null; |
| 616 | } |
| 617 | $parserOut = $status->getValue(); |
| 618 | } |
| 619 | |
| 620 | if ( $parserOut instanceof ParserOutput ) { |
| 621 | # Update the stable version cache |
| 622 | $stableParserCache->save( $parserOut, $this->article, $parserOptions ); |
| 623 | } |
| 624 | } |
| 625 | |
| 626 | if ( !$parserOut ) { |
| 627 | $this->out->disableClientCache(); |
| 628 | $this->out->setRobotPolicy( 'noindex,nofollow' ); |
| 629 | |
| 630 | $this->out->addWikiMsg( |
| 631 | 'missing-article', |
| 632 | $this->article->getTitle()->getPrefixedText(), |
| 633 | $this->msg( 'missingarticle-rev', $srev->getRevId() )->plain() |
| 634 | ); |
| 635 | return null; |
| 636 | } |
| 637 | |
| 638 | # Add the parser output to the page view |
| 639 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 640 | $poOptions = [ |
| 641 | // (T391788) This should always be used for full page views |
| 642 | 'includeDebugInfo' => true, |
| 643 | ]; |
| 644 | if ( $this->out->isPrintable() || |
| 645 | !$pm->quickUserCan( 'edit', $reqUser, $this->article->getTitle() ) |
| 646 | ) { |
| 647 | $poOptions['enableSectionEditLinks'] = false; |
| 648 | } |
| 649 | |
| 650 | $this->out->addParserOutput( $parserOut, $parserOptions, $poOptions ); |
| 651 | return $parserOut; |
| 652 | } |
| 653 | |
| 654 | /** |
| 655 | * Displays a page with no reviewed revisions. Will display the most recent revision. |
| 656 | * |
| 657 | * This method handles the display of an unreviewed version of the page, showing the appropriate |
| 658 | * UI elements based on user preferences (simple or detailed). It sets the appropriate tag and |
| 659 | * tag type class to indicate the page's unreviewed status. The method generates a `ParserOutput` |
| 660 | * for the unreviewed version and adds it to the output page. |
| 661 | * |
| 662 | * The method is used when a page has no reviewed revisions in its history. In other words, it |
| 663 | * is used for newly created pages by non-reviewers. |
| 664 | * |
| 665 | * @param string &$tag Reference to the variable holding the review box/bar info. |
| 666 | * |
| 667 | * @return ParserOutput|bool The generated ParserOutput for the unreviewed version, or null if none is available. |
| 668 | */ |
| 669 | private function showUnreviewedVersion( string &$tag ) { |
| 670 | $reqUser = $this->getUser(); |
| 671 | |
| 672 | if ( $this->useSimpleUI() ) { |
| 673 | $this->addStatusIndicator(); |
| 674 | $this->out->addHTML( FlaggedRevsHTML::reviewDialog( null, 0, 0, 'unreviewed' ) ); |
| 675 | } else { |
| 676 | $tag = $this->msg( 'revreview-noflagged' )->parse(); |
| 677 | } |
| 678 | |
| 679 | // Generate the ParserOutput for the unreviewed version |
| 680 | $parserOptions = $this->makeParserOptions( $reqUser ); |
| 681 | $parserOut = $this->article->getParserOutput( $parserOptions ); |
| 682 | |
| 683 | // Set the output revision ID so that the "Permanent link" link works. T384778 |
| 684 | // (T394381) Note that there is a tiny delay between publishing in VisualEditor |
| 685 | // and a Revision ID being available, which affects the page reload right after |
| 686 | // VisualEditor saves an edit; if so, skip. |
| 687 | $rev = $this->article->getRevisionRecord(); |
| 688 | if ( $rev ) { |
| 689 | $this->out->setRevisionId( $rev->getId() ); |
| 690 | } |
| 691 | |
| 692 | // Add the ParserOutput to the output page |
| 693 | if ( $parserOut ) { |
| 694 | $this->out->addParserOutput( $parserOut, $parserOptions, [ |
| 695 | // (T391788) This should always be used for full page views |
| 696 | 'includeDebugInfo' => true, |
| 697 | ] ); |
| 698 | } |
| 699 | |
| 700 | return $parserOut; |
| 701 | } |
| 702 | |
| 703 | /** |
| 704 | * Get a toggle for a collapsible diff-to-stable to add to the review notice as needed |
| 705 | * @param FlaggedRevision $srev stable version |
| 706 | * @return string|false the html line (either "" or "<diff toggle>") |
| 707 | */ |
| 708 | private function getTopDiffToggle( FlaggedRevision $srev ) { |
| 709 | $reqUser = $this->getUser(); |
| 710 | if ( !MediaWikiServices::getInstance()->getUserOptionsLookup() |
| 711 | ->getBoolOption( $reqUser, 'flaggedrevsviewdiffs' ) |
| 712 | ) { |
| 713 | return false; // nothing to do here |
| 714 | } |
| 715 | # Diff should only show for the draft |
| 716 | $oldid = $this->getOldIDFromRequest(); |
| 717 | $latest = $this->article->getLatest(); |
| 718 | if ( $oldid && $oldid != $latest ) { |
| 719 | return false; // not viewing the draft |
| 720 | } |
| 721 | $revsSince = $this->article->getPendingRevCount(); |
| 722 | if ( !$revsSince ) { |
| 723 | return false; // no pending changes |
| 724 | } |
| 725 | |
| 726 | $title = $this->article->getTitle(); // convenience |
| 727 | if ( $srev->getRevId() !== $latest ) { |
| 728 | $nEdits = $revsSince - 1; // full diff-to-stable, no need for query |
| 729 | if ( $nEdits ) { |
| 730 | $limit = 100; |
| 731 | try { |
| 732 | $latestRevObj = MediaWikiServices::getInstance() |
| 733 | ->getRevisionLookup() |
| 734 | ->getRevisionById( $latest ); |
| 735 | $users = MediaWikiServices::getInstance() |
| 736 | ->getRevisionStore() |
| 737 | ->getAuthorsBetween( |
| 738 | $title->getArticleID(), |
| 739 | $srev->getRevisionRecord(), |
| 740 | $latestRevObj, |
| 741 | null, |
| 742 | $limit |
| 743 | ); |
| 744 | $nUsers = count( $users ); |
| 745 | } catch ( InvalidArgumentException ) { |
| 746 | $nUsers = 0; |
| 747 | } |
| 748 | $multiNotice = DifferenceEngine::intermediateEditsMsg( $nEdits, $nUsers, $limit ); |
| 749 | } else { |
| 750 | $multiNotice = ''; |
| 751 | } |
| 752 | $this->isDiffFromStable = true; // alter default review form tags |
| 753 | // @phan-suppress-next-line SecurityCheck-DoubleEscaped multiNotice is used in a-tag |
| 754 | return FlaggedRevsHTML::diffToggle( $title, $srev->getRevId(), $latest, $multiNotice ); |
| 755 | } |
| 756 | |
| 757 | return ''; |
| 758 | } |
| 759 | |
| 760 | /** |
| 761 | * Adds stable version tags to page when viewing history |
| 762 | */ |
| 763 | public function addToHistView(): void { |
| 764 | # Add a notice if there are pending edits... |
| 765 | $srev = $this->article->getStableRev(); |
| 766 | if ( $srev && $this->article->revsArePending() ) { |
| 767 | $revsSince = $this->article->getPendingRevCount(); |
| 768 | $noticeContent = $this->pendingEditNoticeMessage( $srev, $revsSince )->parse(); |
| 769 | $tag = FlaggedRevsHTML::addMessageBox( 'block', $noticeContent, [ |
| 770 | 'id' => 'mw-fr-revision-tag-edit', |
| 771 | 'class' => 'flaggedrevs_notice plainlinks' |
| 772 | ] ); |
| 773 | $this->out->addHTML( $tag ); |
| 774 | } |
| 775 | } |
| 776 | |
| 777 | /** |
| 778 | * @param Title $title |
| 779 | * @param int $oldid |
| 780 | * @param string[] &$notices |
| 781 | */ |
| 782 | public function getEditNotices( Title $title, int $oldid, array &$notices ): void { |
| 783 | if ( !$this->article->isReviewable() ) { |
| 784 | return; |
| 785 | } |
| 786 | // HACK fake EditPage |
| 787 | $editPage = new EditPage( new Article( $title, $oldid ) ); |
| 788 | $editPage->oldid = $oldid; |
| 789 | $reqUser = $this->getUser(); |
| 790 | |
| 791 | $lines = []; |
| 792 | |
| 793 | $log = $this->stabilityLogNotice(); |
| 794 | if ( $log ) { |
| 795 | $lines[] = $log; |
| 796 | } elseif ( $this->editWillRequireReview( $editPage ) ) { |
| 797 | $lines[] = $this->msg( 'revreview-editnotice' )->parseAsBlock(); |
| 798 | } |
| 799 | $frev = $this->article->getStableRev(); |
| 800 | if ( $frev && $this->article->revsArePending() ) { |
| 801 | $revsSince = $this->article->getPendingRevCount(); |
| 802 | $pendingMsg = $this->pendingEditNoticeMessage( $frev, $revsSince ); |
| 803 | $lines[] = $pendingMsg->parseAsBlock(); |
| 804 | } |
| 805 | $latestId = $this->article->getLatest(); |
| 806 | $revId = $oldid ?: $latestId; |
| 807 | if ( $frev && $frev->getRevId() < $latestId // changes were made |
| 808 | && MediaWikiServices::getInstance()->getUserOptionsLookup() |
| 809 | ->getBoolOption( $reqUser, 'flaggedrevseditdiffs' ) // not disabled via prefs |
| 810 | && $revId === $latestId // only for current rev |
| 811 | ) { |
| 812 | $lines[] = '<p>' . $this->msg( 'review-edit-diff' )->parse() . ' ' . |
| 813 | FlaggedRevsHTML::diffToggle( $this->article->getTitle(), $frev->getRevId(), $revId ) . '</p>'; |
| 814 | } |
| 815 | |
| 816 | $srev = $this->article->getStableRev(); |
| 817 | $revsSince = $this->article->getPendingRevCount(); |
| 818 | |
| 819 | if ( $frev && $this->article->onlyTemplatesPending() && $revsSince === 0 && $srev ) { |
| 820 | $time = $this->getLanguage()->userTimeAndDate( $srev->getTimestamp(), $this->getUser() ); |
| 821 | $lines[] = '<p>' . $this->msg( 'revreview-newest-basic-i', |
| 822 | $srev->getRevId(), $time )->numParams( $revsSince )->parse() . '</p>'; |
| 823 | } |
| 824 | |
| 825 | if ( $lines ) { |
| 826 | $lineMessages = FlaggedRevsHTML::addMessageBox( 'block', implode( "\n", $lines ) ); |
| 827 | |
| 828 | $notices['flaggedrevs_editnotice'] = Html::rawElement( 'div', [ |
| 829 | 'class' => 'mw-fr-edit-messages', |
| 830 | ], $lineMessages ); |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | /** |
| 835 | * Creates "stable rev reviewed on"/"x pending edits" message |
| 836 | */ |
| 837 | private function pendingEditNoticeMessage( FlaggedRevision $frev, int $revsSince ): Message { |
| 838 | $time = $this->getLanguage()->date( $frev->getTimestamp(), true ); |
| 839 | # Add message text for pending edits |
| 840 | return $this->msg( 'revreview-pending-basic', $frev->getRevId(), $time )->numParams( $revsSince ); |
| 841 | } |
| 842 | |
| 843 | private function stabilityLogNotice(): string { |
| 844 | if ( $this->article->isPageLocked() ) { |
| 845 | $msg = 'revreview-locked'; |
| 846 | } elseif ( $this->article->isPageUnlocked() ) { |
| 847 | $msg = 'revreview-unlocked'; |
| 848 | } else { |
| 849 | return ''; |
| 850 | } |
| 851 | $s = $this->msg( $msg )->parseAsBlock(); |
| 852 | return $s . FlaggedRevsHTML::stabilityLogExcerpt( $this->article->getTitle() ); |
| 853 | } |
| 854 | |
| 855 | public function addToNoSuchSection( string &$s ): void { |
| 856 | $srev = $this->article->getStableRev(); |
| 857 | # Add notice for users that may have clicked "edit" for a |
| 858 | # section in the stable version that isn't in the draft. |
| 859 | if ( $srev && $this->article->revsArePending() ) { |
| 860 | $revsSince = $this->article->getPendingRevCount(); |
| 861 | if ( $revsSince ) { |
| 862 | $s .= "<div class='flaggedrevs_editnotice plainlinks'>" . |
| 863 | $this->msg( 'revreview-pending-nosection', |
| 864 | $srev->getRevId() )->numParams( $revsSince )->parse() . "</div>"; |
| 865 | } |
| 866 | } |
| 867 | } |
| 868 | |
| 869 | /** |
| 870 | * Add unreviewed pages links |
| 871 | */ |
| 872 | public function addToCategoryView(): void { |
| 873 | $reqUser = $this->getUser(); |
| 874 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 875 | if ( !$pm->userHasRight( $reqUser, 'review' ) ) { |
| 876 | return; |
| 877 | } |
| 878 | |
| 879 | if ( !FlaggedRevs::useOnlyIfProtected() ) { |
| 880 | # Add links to lists of unreviewed pages and pending changes in this category |
| 881 | $category = $this->article->getTitle()->getText(); |
| 882 | $this->out->addSubtitle( |
| 883 | Html::rawElement( |
| 884 | 'span', |
| 885 | [ 'class' => 'plainlinks', 'id' => 'mw-fr-category-oldreviewed' ], |
| 886 | $this->msg( 'flaggedrevs-categoryview', urlencode( $category ) )->parse() |
| 887 | ) |
| 888 | ); |
| 889 | } |
| 890 | } |
| 891 | |
| 892 | /** |
| 893 | * Add review form to pages when necessary on a regular page view (action=view). |
| 894 | * If $output is an OutputPage then this prepends the form onto it. |
| 895 | * If $output is a string then this appends the review form to it. |
| 896 | * @param string|OutputPage &$output |
| 897 | */ |
| 898 | public function addReviewForm( &$output ): void { |
| 899 | if ( $this->out->isPrintable() ) { |
| 900 | // Must be on non-printable output |
| 901 | return; |
| 902 | } |
| 903 | |
| 904 | # User must have review rights |
| 905 | $reqUser = $this->getUser(); |
| 906 | if ( !MediaWikiServices::getInstance()->getPermissionManager() |
| 907 | ->userHasRight( $reqUser, 'review' ) |
| 908 | ) { |
| 909 | return; |
| 910 | } |
| 911 | # Page must exist and be reviewable |
| 912 | if ( !$this->article->exists() || !$this->article->isReviewable() ) { |
| 913 | return; |
| 914 | } |
| 915 | # Must be a page view action... |
| 916 | if ( !$this->isPageViewOrDiff() ) { |
| 917 | return; |
| 918 | } |
| 919 | // Determine the revision to be reviewed, either from the current output or fallback to |
| 920 | // the latest revision for unchecked pages |
| 921 | $revisionId = $this->out->getRevisionId() ?: $this->article->getLatest(); |
| 922 | $revRecord = $this->reviewFormRevRecord ?: MediaWikiServices::getInstance() |
| 923 | ->getRevisionLookup() |
| 924 | ->getRevisionById( $revisionId ); |
| 925 | |
| 926 | # Build the review form as needed |
| 927 | if ( $revRecord && ( !$this->diffRevRecords || $this->isReviewableDiff ) ) { |
| 928 | $form = new RevisionReviewFormUI( |
| 929 | $this->getContext(), |
| 930 | $this->article, |
| 931 | $revRecord |
| 932 | ); |
| 933 | # Default tags and existence of "reject" button depend on context |
| 934 | if ( $this->diffRevRecords ) { |
| 935 | $oldRevRecord = $this->diffRevRecords['old']; |
| 936 | $form->setDiffPriorRevRecord( $oldRevRecord ); |
| 937 | } |
| 938 | # Review notice box goes in top of form |
| 939 | $form->setTopNotice( $this->diffNoticeBox ); |
| 940 | $form->setBottomNotice( $this->diffIncChangeBox ); |
| 941 | |
| 942 | [ $html, ] = $form->getHtml(); |
| 943 | # Diff action: place the form at the top of the page |
| 944 | if ( $output instanceof OutputPage ) { |
| 945 | $output->prependHTML( $html ); |
| 946 | # View action: place the form at the bottom of the page |
| 947 | } else { |
| 948 | $output .= $html; |
| 949 | } |
| 950 | } |
| 951 | } |
| 952 | |
| 953 | /** |
| 954 | * Add link to stable version setting to protection form |
| 955 | */ |
| 956 | public function addStabilizationLink(): void { |
| 957 | $request = $this->getRequest(); |
| 958 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
| 959 | // Simple custom levels set for action=protect |
| 960 | return; |
| 961 | } |
| 962 | # Check only if the title is reviewable |
| 963 | if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) { |
| 964 | return; |
| 965 | } |
| 966 | $action = $request->getVal( 'action', 'view' ); |
| 967 | if ( $action == 'protect' || $action == 'unprotect' ) { |
| 968 | $title = SpecialPage::getTitleFor( 'Stabilization' ); |
| 969 | # Give a link to the page to configure the stable version |
| 970 | $frev = $this->article->getStableRev(); |
| 971 | if ( !$frev ) { |
| 972 | $msg = 'revreview-visibility-nostable'; |
| 973 | } elseif ( $frev->getRevId() == $this->article->getLatest() ) { |
| 974 | $msg = 'revreview-visibility-synced'; |
| 975 | } else { |
| 976 | $msg = 'revreview-visibility-outdated'; |
| 977 | } |
| 978 | $this->out->prependHTML( "<span class='revreview-visibility $msg plainlinks'>" . |
| 979 | $this->msg( $msg, $title->getPrefixedText() )->parse() . '</span>' ); |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | /** |
| 984 | * Modify an array of action links, as used by SkinTemplateNavigation and |
| 985 | * SkinTemplateTabs, to include flagged revs UI elements |
| 986 | * |
| 987 | * @param array &$actions |
| 988 | */ |
| 989 | public function setActionTabs( array &$actions ): void { |
| 990 | $reqUser = $this->getUser(); |
| 991 | |
| 992 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
| 993 | return; // simple custom levels set for action=protect |
| 994 | } |
| 995 | |
| 996 | if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) { |
| 997 | return; // Only reviewable pages need these tabs |
| 998 | } |
| 999 | |
| 1000 | // Check if we should show a stabilization tab |
| 1001 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 1002 | if ( |
| 1003 | !$this->article->getTitle()->isTalkPage() && |
| 1004 | !isset( $actions['protect'] ) && |
| 1005 | !isset( $actions['unprotect'] ) && |
| 1006 | $pm->userHasRight( $reqUser, 'stablesettings' ) && |
| 1007 | $this->article->exists() |
| 1008 | ) { |
| 1009 | $stableTitle = SpecialPage::getTitleFor( 'Stabilization' ); |
| 1010 | // Add the tab |
| 1011 | $actions['default'] = [ |
| 1012 | 'class' => false, |
| 1013 | 'text' => $this->msg( 'stabilization-tab' )->text(), |
| 1014 | 'href' => $stableTitle->getLocalURL( 'page=' . $this->article->getTitle()->getPrefixedURL() ) |
| 1015 | ]; |
| 1016 | } |
| 1017 | } |
| 1018 | |
| 1019 | /** |
| 1020 | * Modify an array of tab links to include flagged revs UI elements |
| 1021 | * @param Skin $skin |
| 1022 | * @param array[] &$views |
| 1023 | */ |
| 1024 | public function setViewTabs( Skin $skin, array &$views ): void { |
| 1025 | if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) { |
| 1026 | // Short-circuit for non-reviewable pages |
| 1027 | return; |
| 1028 | } |
| 1029 | # Hack for bug 16734 (some actions update and view all at once) |
| 1030 | if ( $this->pageWriteOpRequested() && |
| 1031 | MediaWikiServices::getInstance()->getDBLoadBalancer()->hasOrMadeRecentPrimaryChanges() |
| 1032 | ) { |
| 1033 | # Tabs need to reflect the new stable version so users actually |
| 1034 | # see the results of their action (i.e. "delete"/"rollback") |
| 1035 | $this->article->loadPageData( IDBAccessObject::READ_LATEST ); |
| 1036 | } |
| 1037 | $srev = $this->article->getStableRev(); |
| 1038 | if ( !$srev ) { |
| 1039 | // No stable revision exists |
| 1040 | return; |
| 1041 | } |
| 1042 | $synced = $this->article->stableVersionIsSynced(); |
| 1043 | $pendingEdits = !$synced && $this->article->isStableShownByDefault(); |
| 1044 | // Set the edit tab names as needed... |
| 1045 | if ( $pendingEdits && $this->isPageView() && $this->showingStable() ) { |
| 1046 | // bug 31489; direct user to current |
| 1047 | if ( isset( $views['edit'] ) ) { |
| 1048 | $views['edit']['href'] = $skin->getTitle()->getFullURL( 'action=edit' ); |
| 1049 | } |
| 1050 | if ( isset( $views['viewsource'] ) ) { |
| 1051 | $views['viewsource']['href'] = $skin->getTitle()->getFullURL( 'action=edit' ); |
| 1052 | } |
| 1053 | // Instruct alternative editors like VisualEditor to load the latest ("current") |
| 1054 | // revision for editing, rather than the one from 'wgRevisionId' |
| 1055 | $skin->getOutput()->addJsConfigVars( 'wgEditLatestRevision', true ); |
| 1056 | } |
| 1057 | # Add "pending changes" tab if the page is not synced |
| 1058 | if ( !$synced ) { |
| 1059 | $this->addDraftTab( $views, $srev ); |
| 1060 | } |
| 1061 | } |
| 1062 | |
| 1063 | /** |
| 1064 | * Add "pending changes" tab and set tab selection CSS |
| 1065 | * @param array[] &$views |
| 1066 | * @param FlaggedRevision $srev |
| 1067 | */ |
| 1068 | private function addDraftTab( array &$views, FlaggedRevision $srev ): void { |
| 1069 | $request = $this->getRequest(); |
| 1070 | $title = $this->article->getTitle(); // convenience |
| 1071 | $tabs = [ |
| 1072 | 'read' => [ // view stable |
| 1073 | 'text' => '', // unused |
| 1074 | 'href' => $title->getLocalURL( 'stable=1' ), |
| 1075 | 'class' => '' |
| 1076 | ], |
| 1077 | 'draft' => [ // view draft |
| 1078 | 'text' => $this->msg( 'revreview-current' )->text(), |
| 1079 | 'href' => $title->getLocalURL( 'stable=0&redirect=no' ), |
| 1080 | 'class' => 'collapsible' |
| 1081 | ], |
| 1082 | ]; |
| 1083 | // Set tab selection CSS |
| 1084 | if ( ( $this->isPageView() && $this->showingStable() ) || $request->getVal( 'stableid' ) ) { |
| 1085 | // We are looking a the stable version or an old reviewed one |
| 1086 | $tabs['read']['class'] = 'selected'; |
| 1087 | } elseif ( $this->isPageViewOrDiff() ) { |
| 1088 | $ts = null; |
| 1089 | if ( $this->out->getRevisionId() ) { // @TODO: avoid same query in Skin.php |
| 1090 | if ( $this->out->getRevisionId() == $this->article->getLatest() ) { |
| 1091 | $ts = $this->article->getTimestamp(); // skip query |
| 1092 | } else { |
| 1093 | $ts = MediaWikiServices::getInstance() |
| 1094 | ->getRevisionLookup() |
| 1095 | ->getTimestampFromId( $this->out->getRevisionId() ); |
| 1096 | } |
| 1097 | } |
| 1098 | // Are we looking at a pending revision? |
| 1099 | if ( $ts > $srev->getRevTimestamp() ) { // bug 15515 |
| 1100 | $tabs['draft']['class'] .= ' selected'; |
| 1101 | // Are there *just* pending template changes. |
| 1102 | } elseif ( $this->article->onlyTemplatesPending() |
| 1103 | && $this->out->getRevisionId() == $this->article->getStable() |
| 1104 | ) { |
| 1105 | $tabs['draft']['class'] .= ' selected'; |
| 1106 | // Otherwise, fallback to regular tab behavior |
| 1107 | } else { |
| 1108 | $tabs['read']['class'] = 'selected'; |
| 1109 | } |
| 1110 | } |
| 1111 | $newViews = []; |
| 1112 | // Rebuild tabs array |
| 1113 | $previousTab = null; |
| 1114 | foreach ( $views as $tabAction => $data ) { |
| 1115 | // The 'view' tab. Make it go to the stable version... |
| 1116 | if ( $tabAction == 'view' ) { |
| 1117 | // 'view' for content page; make it go to the stable version |
| 1118 | $newViews[$tabAction]['text'] = $data['text']; // keep tab name |
| 1119 | $newViews[$tabAction]['href'] = $tabs['read']['href']; |
| 1120 | $newViews[$tabAction]['class'] = $tabs['read']['class']; |
| 1121 | // All other tabs... |
| 1122 | } else { |
| 1123 | if ( $previousTab == 'view' ) { |
| 1124 | $newViews['current'] = $tabs['draft']; |
| 1125 | } |
| 1126 | $newViews[$tabAction] = $data; |
| 1127 | } |
| 1128 | $previousTab = $tabAction; |
| 1129 | } |
| 1130 | // Replaces old tabs with new tabs |
| 1131 | $views = $newViews; |
| 1132 | } |
| 1133 | |
| 1134 | /** |
| 1135 | * Check if a flaggedrevs relevant write op was done this page view |
| 1136 | */ |
| 1137 | private function pageWriteOpRequested(): bool { |
| 1138 | $request = $this->getRequest(); |
| 1139 | # Hack for bug 16734 (some actions update and view all at once) |
| 1140 | $action = $request->getVal( 'action' ); |
| 1141 | return $action === 'rollback' || |
| 1142 | ( $action === 'delete' && $request->wasPosted() ); |
| 1143 | } |
| 1144 | |
| 1145 | private function getOldIDFromRequest(): int { |
| 1146 | $article = Article::newFromWikiPage( $this->article, RequestContext::getMain() ); |
| 1147 | return $article->getOldIDFromRequest(); |
| 1148 | } |
| 1149 | |
| 1150 | /** |
| 1151 | * Adds a notice saying that this revision is pending review |
| 1152 | * |
| 1153 | * @param FlaggedRevision $srev The stable version |
| 1154 | * @param string $diffToggle either "" or " <diff toggle><diff div>" |
| 1155 | */ |
| 1156 | private function setPendingNotice( FlaggedRevision $srev, string $diffToggle = '' ): void { |
| 1157 | $time = $this->getLanguage()->date( $srev->getTimestamp(), true ); |
| 1158 | $revsSince = $this->article->getPendingRevCount(); |
| 1159 | $msg = !$revsSince ? 'revreview-newest-basic-i' : 'revreview-newest-basic'; |
| 1160 | # Add bar msg to the top of the page... |
| 1161 | $msgHTML = $this->msg( $msg, $srev->getRevId(), $time )->numParams( $revsSince )->parse(); |
| 1162 | |
| 1163 | if ( !$this->useSimpleUI() ) { |
| 1164 | $this->reviewNotice .= FlaggedRevsHTML::addMessageBox( 'block', $msgHTML . $diffToggle ); |
| 1165 | } |
| 1166 | } |
| 1167 | |
| 1168 | /** |
| 1169 | * When viewing a diff: |
| 1170 | * (a) Add the review form to the top of the page |
| 1171 | * (b) Mark off which versions are checked or not |
| 1172 | * (c) When comparing the stable revision to the current: |
| 1173 | * (i) Show a tag with some explanation for the diff |
| 1174 | */ |
| 1175 | public function addToDiffView( ?RevisionRecord $oldRevRecord, ?RevisionRecord $newRevRecord ): void { |
| 1176 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 1177 | $request = $this->getRequest(); |
| 1178 | $reqUser = $this->getUser(); |
| 1179 | # Exempt printer-friendly output |
| 1180 | if ( $this->out->isPrintable() ) { |
| 1181 | return; |
| 1182 | # Multi-page diffs are useless and misbehave (bug 19327). Sanity check $newRevRecord. |
| 1183 | } elseif ( $this->isMultiPageDiff || !$newRevRecord ) { |
| 1184 | return; |
| 1185 | # Page must be reviewable. |
| 1186 | } elseif ( !$this->article->isReviewable() ) { |
| 1187 | return; |
| 1188 | } |
| 1189 | $srev = $this->article->getStableRev(); |
| 1190 | if ( $srev && $this->isReviewableDiff ) { |
| 1191 | $this->reviewFormRevRecord = $newRevRecord; |
| 1192 | } |
| 1193 | # Check if this is a diff-to-stable. If so: |
| 1194 | # (a) prompt reviewers to review the changes |
| 1195 | if ( $srev |
| 1196 | && $this->isDiffFromStable |
| 1197 | && !$this->article->stableVersionIsSynced() // pending changes |
| 1198 | ) { |
| 1199 | # If there are pending revs, notify the user... |
| 1200 | if ( $this->article->revsArePending() ) { |
| 1201 | # If the user can review then prompt them to review them... |
| 1202 | if ( $pm->userHasRight( $reqUser, 'review' ) ) { |
| 1203 | // Reviewer just edited... |
| 1204 | if ( $request->getInt( 'shownotice' ) |
| 1205 | && $newRevRecord->isCurrent() |
| 1206 | && $newRevRecord->getUser( RevisionRecord::RAW ) |
| 1207 | ->equals( $reqUser ) |
| 1208 | ) { |
| 1209 | $title = $this->article->getTitle(); // convenience |
| 1210 | // @TODO: make diff class cache this |
| 1211 | $n = MediaWikiServices::getInstance() |
| 1212 | ->getRevisionStore() |
| 1213 | ->countRevisionsBetween( |
| 1214 | $title->getArticleID(), |
| 1215 | $oldRevRecord, |
| 1216 | $newRevRecord |
| 1217 | ); |
| 1218 | if ( $n ) { |
| 1219 | $msg = 'revreview-update-edited-prev'; // previous pending edits |
| 1220 | } else { |
| 1221 | $msg = 'revreview-update-edited'; // just couldn't autoreview |
| 1222 | } |
| 1223 | // All other cases... |
| 1224 | } else { |
| 1225 | $msg = 'revreview-update'; // generic "please review" notice... |
| 1226 | } |
| 1227 | // add as part of form |
| 1228 | $this->diffNoticeBox = $this->msg( $msg )->parseAsBlock(); |
| 1229 | } |
| 1230 | } |
| 1231 | } |
| 1232 | # Add a link to diff from stable to current as needed. |
| 1233 | # Show review status of the diff revision(s). Uses a <table>. |
| 1234 | $this->out->addHTML( |
| 1235 | '<div id="mw-fr-diff-headeritems">' . |
| 1236 | self::diffLinkAndMarkers( |
| 1237 | $this->article, |
| 1238 | $oldRevRecord, |
| 1239 | $newRevRecord |
| 1240 | ) . |
| 1241 | '</div>' |
| 1242 | ); |
| 1243 | } |
| 1244 | |
| 1245 | /** |
| 1246 | * get new diff header items for in-place page review |
| 1247 | */ |
| 1248 | public static function buildDiffHeaderItems( int $oldid, int $newid ): string { |
| 1249 | $revLookup = MediaWikiServices::getInstance()->getRevisionLookup(); |
| 1250 | $newRevRecord = $revLookup->getRevisionById( $newid ); |
| 1251 | if ( $newRevRecord && $newRevRecord->getPageAsLinkTarget() ) { |
| 1252 | $oldRevRecord = $revLookup->getRevisionById( $oldid ); |
| 1253 | $fa = FlaggableWikiPage::getTitleInstance( |
| 1254 | Title::newFromLinkTarget( $newRevRecord->getPageAsLinkTarget() ) |
| 1255 | ); |
| 1256 | return self::diffLinkAndMarkers( $fa, $oldRevRecord, $newRevRecord ); |
| 1257 | } |
| 1258 | return ''; |
| 1259 | } |
| 1260 | |
| 1261 | /** |
| 1262 | * (a) Add a link to diff from stable to current as needed |
| 1263 | * (b) Show review status of the diff revision(s). Uses a <table>. |
| 1264 | * Note: used by ajax function to rebuild diff page |
| 1265 | */ |
| 1266 | private static function diffLinkAndMarkers( |
| 1267 | FlaggableWikiPage $article, |
| 1268 | ?RevisionRecord $oldRevRecord, |
| 1269 | ?RevisionRecord $newRevRecord |
| 1270 | ): string { |
| 1271 | $s = '<form id="mw-fr-diff-dataform">'; |
| 1272 | $s .= Html::hidden( 'oldid', $oldRevRecord ? $oldRevRecord->getId() : 0 ); |
| 1273 | $s .= Html::hidden( 'newid', $newRevRecord ? $newRevRecord->getId() : 0 ); |
| 1274 | $s .= "</form>\n"; |
| 1275 | if ( $newRevRecord && $oldRevRecord ) { // sanity check |
| 1276 | $s .= self::diffToStableLink( $article, $oldRevRecord, $newRevRecord ); |
| 1277 | $s .= self::diffReviewMarkers( $article, $oldRevRecord, $newRevRecord ); |
| 1278 | } |
| 1279 | return $s; |
| 1280 | } |
| 1281 | |
| 1282 | /** |
| 1283 | * Add a link to diff-to-stable for reviewable pages |
| 1284 | */ |
| 1285 | private static function diffToStableLink( |
| 1286 | FlaggableWikiPage $article, |
| 1287 | RevisionRecord $oldRevRecord, |
| 1288 | RevisionRecord $newRevRecord |
| 1289 | ): string { |
| 1290 | $srev = $article->getStableRev(); |
| 1291 | if ( !$srev ) { |
| 1292 | return ''; // nothing to do |
| 1293 | } |
| 1294 | $review = ''; |
| 1295 | # Is this already the full diff-to-stable? |
| 1296 | $fullStableDiff = $newRevRecord->isCurrent() |
| 1297 | && self::isDiffToStable( |
| 1298 | $srev, |
| 1299 | $oldRevRecord, |
| 1300 | $newRevRecord |
| 1301 | ); |
| 1302 | # Make a link to the full diff-to-stable if: |
| 1303 | # (a) Actual revs are pending and (b) We are not viewing the full diff-to-stable |
| 1304 | if ( $article->revsArePending() && !$fullStableDiff ) { |
| 1305 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
| 1306 | $reviewLink = $linkRenderer->makeKnownLink( |
| 1307 | $article->getTitle(), |
| 1308 | wfMessage( 'review-diff2stable' )->text(), |
| 1309 | [], |
| 1310 | [ 'oldid' => $srev->getRevId(), 'diff' => 'cur' ] |
| 1311 | ); |
| 1312 | $reviewWrapped = wfMessage( 'parentheses' )->rawParams( $reviewLink )->escaped(); |
| 1313 | $review = "<div class='fr-diff-to-stable' style='text-align: center;'>$reviewWrapped</div>"; |
| 1314 | } |
| 1315 | return $review; |
| 1316 | } |
| 1317 | |
| 1318 | /** |
| 1319 | * Add [checked version] and such to left and right side of diff |
| 1320 | */ |
| 1321 | private static function diffReviewMarkers( |
| 1322 | FlaggableWikiPage $article, |
| 1323 | ?RevisionRecord $oldRevRecord, |
| 1324 | ?RevisionRecord $newRevRecord |
| 1325 | ): string { |
| 1326 | $table = ''; |
| 1327 | $srev = $article->getStableRev(); |
| 1328 | # Diff between two revisions |
| 1329 | if ( $oldRevRecord && $newRevRecord ) { |
| 1330 | [ $msg, $class ] = self::getDiffRevMsgAndClass( $oldRevRecord, $srev ); |
| 1331 | $table .= "<table class='fr-diff-ratings'><tr>"; |
| 1332 | $table .= "<td style='text-align: center; width: 50%;'>"; |
| 1333 | // @todo i18n FIXME: Hard coded brackets |
| 1334 | $table .= "<span class='$class'>[" . |
| 1335 | wfMessage( $msg )->escaped() . "]</span>"; |
| 1336 | |
| 1337 | [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev ); |
| 1338 | $table .= "</td><td style='text-align: center; width: 50%;'>"; |
| 1339 | // @todo i18n FIXME: Hard coded brackets |
| 1340 | $table .= "<span class='$class'>[" . |
| 1341 | wfMessage( $msg )->escaped() . "]</span>"; |
| 1342 | |
| 1343 | $table .= "</td></tr></table>\n"; |
| 1344 | # New page "diffs" - just one rev |
| 1345 | } elseif ( $newRevRecord ) { |
| 1346 | [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev ); |
| 1347 | $table .= "<table class='fr-diff-ratings'>"; |
| 1348 | $table .= "<tr><td style='text-align: center;'><span class='$class'>"; |
| 1349 | // @todo i18n FIXME: Hard coded brackets |
| 1350 | $table .= '[' . wfMessage( $msg )->escaped() . ']'; |
| 1351 | $table .= "</span></td></tr></table>\n"; |
| 1352 | } |
| 1353 | return $table; |
| 1354 | } |
| 1355 | |
| 1356 | /** |
| 1357 | * @return string[] |
| 1358 | */ |
| 1359 | private static function getDiffRevMsgAndClass( |
| 1360 | RevisionRecord $revRecord, ?FlaggedRevision $srev |
| 1361 | ): array { |
| 1362 | $checked = FlaggedRevision::revIsFlagged( $revRecord->getId() ); |
| 1363 | if ( $checked ) { |
| 1364 | $msg = 'revreview-hist-basic'; |
| 1365 | } else { |
| 1366 | $msg = ( $srev && $revRecord->getTimestamp() > $srev->getRevTimestamp() ) ? // bug 15515 |
| 1367 | 'revreview-hist-pending' : |
| 1368 | 'revreview-hist-draft'; |
| 1369 | } |
| 1370 | return [ $msg, $checked ? 'flaggedrevs-color-1' : 'flaggedrevs-color-0' ]; |
| 1371 | } |
| 1372 | |
| 1373 | /** |
| 1374 | * Set $this->isDiffFromStable and $this->isMultiPageDiff fields |
| 1375 | */ |
| 1376 | public function setViewFlags( |
| 1377 | DifferenceEngine $diff, |
| 1378 | ?RevisionRecord $oldRevRecord, |
| 1379 | ?RevisionRecord $newRevRecord |
| 1380 | ): void { |
| 1381 | // We only want valid diffs that actually make sense... |
| 1382 | if ( !( $newRevRecord |
| 1383 | && $oldRevRecord |
| 1384 | && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() ) |
| 1385 | ) { |
| 1386 | return; |
| 1387 | } |
| 1388 | |
| 1389 | // Is this a diff between two pages? |
| 1390 | if ( $newRevRecord->getPageId() != $oldRevRecord->getPageId() ) { |
| 1391 | $this->isMultiPageDiff = true; |
| 1392 | // Is there a stable version? |
| 1393 | } elseif ( $this->article->isReviewable() ) { |
| 1394 | $srev = $this->article->getStableRev(); |
| 1395 | // Is this a diff of a draft rev against the stable rev? |
| 1396 | if ( self::isDiffToStable( |
| 1397 | $srev, |
| 1398 | $oldRevRecord, |
| 1399 | $newRevRecord |
| 1400 | ) ) { |
| 1401 | $this->isDiffFromStable = true; |
| 1402 | $this->isReviewableDiff = true; |
| 1403 | // Is this a diff of a draft rev against a reviewed rev? |
| 1404 | } elseif ( |
| 1405 | FlaggedRevision::newFromTitle( |
| 1406 | $diff->getTitle(), |
| 1407 | $oldRevRecord->getId() |
| 1408 | ) || |
| 1409 | FlaggedRevision::newFromTitle( |
| 1410 | $diff->getTitle(), |
| 1411 | $newRevRecord->getId() |
| 1412 | ) |
| 1413 | ) { |
| 1414 | $this->isReviewableDiff = true; |
| 1415 | } |
| 1416 | } |
| 1417 | |
| 1418 | $this->diffRevRecords = [ |
| 1419 | 'old' => $oldRevRecord, |
| 1420 | 'new' => $newRevRecord |
| 1421 | ]; |
| 1422 | } |
| 1423 | |
| 1424 | /** |
| 1425 | * Is a diff from $oldRev to $newRev a diff-to-stable? |
| 1426 | */ |
| 1427 | private static function isDiffToStable( |
| 1428 | ?FlaggedRevision $srev, |
| 1429 | ?RevisionRecord $oldRevRecord, |
| 1430 | ?RevisionRecord $newRevRecord |
| 1431 | ): bool { |
| 1432 | return ( $srev |
| 1433 | && $oldRevRecord |
| 1434 | && $newRevRecord |
| 1435 | && $oldRevRecord->getPageId() === $newRevRecord->getPageId() // no multipage diffs |
| 1436 | && $oldRevRecord->getId() == $srev->getRevId() |
| 1437 | && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() // no backwards diffs |
| 1438 | ); |
| 1439 | } |
| 1440 | |
| 1441 | /** |
| 1442 | * Redirect users out to review the changes to the stable version. |
| 1443 | * Only for people who can review and for pages that have a stable version. |
| 1444 | */ |
| 1445 | public function injectPostEditURLParams( string &$sectionAnchor, string &$extraQuery ): void { |
| 1446 | $reqUser = $this->getUser(); |
| 1447 | $this->article->loadPageData( IDBAccessObject::READ_LATEST ); |
| 1448 | # Get the stable version from the primary DB |
| 1449 | $frev = $this->article->getStableRev(); |
| 1450 | if ( !$frev ) { |
| 1451 | // Only for pages with stable versions |
| 1452 | return; |
| 1453 | } |
| 1454 | |
| 1455 | $params = []; |
| 1456 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
| 1457 | // If the edit was not autoreviewed, and the user can actually make a |
| 1458 | // new stable version, then go to the diff... |
| 1459 | if ( $this->article->revsArePending() && $frev->userCanSetTag( $reqUser ) ) { |
| 1460 | $params += [ 'oldid' => $frev->getRevId(), 'diff' => 'cur', 'shownotice' => 1 ]; |
| 1461 | // ...otherwise, go to the draft revision after completing an edit. |
| 1462 | // This allows for users to immediately see their changes. Even if the stable |
| 1463 | // and draft page match, we can avoid a parse due to FR_INCLUDES_STABLE. |
| 1464 | } else { |
| 1465 | $params += [ 'stable' => 0 ]; |
| 1466 | // Show a notice at the top of the page for non-reviewers... |
| 1467 | if ( $this->article->revsArePending() |
| 1468 | && $this->article->isStableShownByDefault() |
| 1469 | && !$pm->userHasRight( $reqUser, 'review' ) |
| 1470 | ) { |
| 1471 | $params += [ 'shownotice' => 1 ]; |
| 1472 | if ( $sectionAnchor ) { |
| 1473 | // Pass a section parameter in the URL as needed to add a link to |
| 1474 | // the "your changes are pending" box on the top of the page... |
| 1475 | $params += [ 'fromsection' => substr( $sectionAnchor, 1 ) ]; // strip # |
| 1476 | $sectionAnchor = ''; // go to the top of the page to see notice |
| 1477 | } |
| 1478 | } |
| 1479 | } |
| 1480 | if ( $extraQuery !== '' ) { |
| 1481 | $extraQuery .= '&'; |
| 1482 | } |
| 1483 | $extraQuery .= wfArrayToCgi( $params ); // note: EditPage will add initial "&" |
| 1484 | } |
| 1485 | |
| 1486 | /** |
| 1487 | * If submitting the edit will leave it pending, then change the button text |
| 1488 | * Note: interacts with 'review pending changes' checkbox |
| 1489 | * @param EditPage $editPage |
| 1490 | * @param ButtonInputWidget[] $buttons |
| 1491 | */ |
| 1492 | public function changeSaveButton( EditPage $editPage, array $buttons ): void { |
| 1493 | if ( !$this->editWillRequireReview( $editPage ) ) { |
| 1494 | // Edit will go live or be reviewed on save |
| 1495 | return; |
| 1496 | } |
| 1497 | if ( isset( $buttons['save'] ) ) { |
| 1498 | $buttonLabel = $this->msg( 'revreview-submitedit' )->text(); |
| 1499 | $buttons['save']->setLabel( $buttonLabel ); |
| 1500 | $buttonTitle = $this->msg( 'revreview-submitedit-title' )->text(); |
| 1501 | $buttons['save']->setTitle( $buttonTitle ); |
| 1502 | } |
| 1503 | } |
| 1504 | |
| 1505 | /** |
| 1506 | * If this edit will not go live on submit (accounting for wpReviewEdit) |
| 1507 | */ |
| 1508 | private function editWillRequireReview( EditPage $editPage ): bool { |
| 1509 | $request = $this->getRequest(); // convenience |
| 1510 | $title = $this->article->getTitle(); // convenience |
| 1511 | if ( !$this->article->editsRequireReview() || $this->editWillBeAutoreviewed( $editPage ) ) { |
| 1512 | return false; // edit will go live immediately |
| 1513 | } elseif ( $request->getCheck( 'wpReviewEdit' ) && |
| 1514 | MediaWikiServices::getInstance()->getPermissionManager() |
| 1515 | ->userCan( 'review', $this->getUser(), $title ) |
| 1516 | ) { |
| 1517 | return false; // edit checked off to be reviewed on save |
| 1518 | } |
| 1519 | return true; // edit needs review |
| 1520 | } |
| 1521 | |
| 1522 | /** |
| 1523 | * If this edit will be auto-reviewed on submit |
| 1524 | * Note: checking wpReviewEdit does not count as auto-reviewed |
| 1525 | */ |
| 1526 | private function editWillBeAutoreviewed( EditPage $editPage ): bool { |
| 1527 | $title = $this->article->getTitle(); // convenience |
| 1528 | if ( !$this->article->isReviewable() ) { |
| 1529 | return false; |
| 1530 | } |
| 1531 | if ( MediaWikiServices::getInstance()->getPermissionManager() |
| 1532 | ->quickUserCan( 'autoreview', $this->getUser(), $title ) |
| 1533 | ) { |
| 1534 | if ( FlaggedRevs::autoReviewNewPages() && !$this->article->exists() ) { |
| 1535 | return true; // edit will be autoreviewed |
| 1536 | } |
| 1537 | |
| 1538 | $baseRevId = self::getBaseRevId( $editPage, $this->getRequest() ); |
| 1539 | $baseRevId2 = self::getAltBaseRevId( $editPage, $this->getRequest() ); |
| 1540 | $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId ); |
| 1541 | if ( !$baseFRev && $baseRevId2 ) { |
| 1542 | $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId2 ); |
| 1543 | } |
| 1544 | |
| 1545 | if ( $baseFRev ) { |
| 1546 | return true; // edit will be autoreviewed |
| 1547 | } |
| 1548 | } |
| 1549 | return false; // edit won't be autoreviewed |
| 1550 | } |
| 1551 | |
| 1552 | /** |
| 1553 | * Add a "review pending changes" checkbox to the edit form iff: |
| 1554 | * (a) there are currently any revisions pending (bug 16713) |
| 1555 | * (b) this is an unreviewed page (bug 23970) |
| 1556 | */ |
| 1557 | public function addReviewCheck( EditPage $editPage, array &$checkboxes ): void { |
| 1558 | $request = $this->getRequest(); |
| 1559 | $title = $this->article->getTitle(); // convenience |
| 1560 | if ( !$this->article->isReviewable() || |
| 1561 | !MediaWikiServices::getInstance()->getPermissionManager() |
| 1562 | ->userCan( 'review', $this->getUser(), $title ) |
| 1563 | ) { |
| 1564 | // Not needed |
| 1565 | return; |
| 1566 | } elseif ( $this->editWillBeAutoreviewed( $editPage ) ) { |
| 1567 | // Edit will be auto-reviewed |
| 1568 | return; |
| 1569 | } |
| 1570 | if ( self::getBaseRevId( $editPage, $request ) == $this->article->getLatest() ) { |
| 1571 | # For pages with either no stable version, or an outdated one, let |
| 1572 | # the user decide if he/she wants it reviewed on the spot. One might |
| 1573 | # do this if he/she just saw the diff-to-stable and *then* decided to edit. |
| 1574 | # Note: check not shown when editing old revisions, which is confusing. |
| 1575 | $name = 'wpReviewEdit'; |
| 1576 | $options = [ |
| 1577 | 'id' => $name, |
| 1578 | 'default' => $request->getCheck( $name ), |
| 1579 | 'legacy-name' => 'reviewed', |
| 1580 | ]; |
| 1581 | // For reviewed pages... |
| 1582 | if ( $this->article->getStable() ) { |
| 1583 | // For pending changes... |
| 1584 | if ( $this->article->revsArePending() ) { |
| 1585 | $n = $this->article->getPendingRevCount(); |
| 1586 | $options['title-message'] = 'revreview-check-flag-p-title'; |
| 1587 | $options['label-message'] = $this->msg( 'revreview-check-flag-p' ) |
| 1588 | ->numParams( $n ); |
| 1589 | // For just the user's changes... |
| 1590 | } else { |
| 1591 | $options['title-message'] = 'revreview-check-flag-y-title'; |
| 1592 | $options['label-message'] = 'revreview-check-flag-y'; |
| 1593 | } |
| 1594 | // For unreviewed pages... |
| 1595 | } else { |
| 1596 | $options['title-message'] = 'revreview-check-flag-u-title'; |
| 1597 | $options['label-message'] = 'revreview-check-flag-u'; |
| 1598 | } |
| 1599 | $checkboxes[$name] = $options; |
| 1600 | } |
| 1601 | } |
| 1602 | |
| 1603 | /** |
| 1604 | * Guess the rev ID the text of this form is based off |
| 1605 | */ |
| 1606 | private static function getBaseRevId( EditPage $editPage, WebRequest $request ): int { |
| 1607 | if ( $editPage->isConflict ) { |
| 1608 | return 0; // throw away these values (bug 33481) |
| 1609 | } |
| 1610 | |
| 1611 | $article = $editPage->getArticle(); // convenience |
| 1612 | $latestId = $article->getPage()->getLatest(); // current rev |
| 1613 | # Undoing edits... |
| 1614 | if ( $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' ) ) { |
| 1615 | $revId = $latestId; // current rev is the base rev |
| 1616 | # Other edits... |
| 1617 | } else { |
| 1618 | # If we are editing via oldid=X, then use that rev ID. |
| 1619 | $revId = $article->getOldID(); |
| 1620 | } |
| 1621 | # Zero oldid => draft revision |
| 1622 | return $revId ?: $latestId; |
| 1623 | } |
| 1624 | |
| 1625 | /** |
| 1626 | * Guess the alternative rev ID the text of this form is based off. |
| 1627 | * When undoing the top X edits, the base can be though of as either |
| 1628 | * the current or the edit X edits prior to the latest. |
| 1629 | */ |
| 1630 | private static function getAltBaseRevId( EditPage $editPage, WebRequest $request ): int { |
| 1631 | if ( $editPage->isConflict ) { |
| 1632 | return 0; // throw away these values (bug 33481) |
| 1633 | } |
| 1634 | |
| 1635 | $article = $editPage->getArticle(); // convenience |
| 1636 | $latestId = $article->getPage()->getLatest(); // current rev |
| 1637 | $undo = $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' ); |
| 1638 | # Undoing consecutive top edits... |
| 1639 | if ( $undo && $undo === $latestId ) { |
| 1640 | # Treat this like a revert to a base revision. |
| 1641 | # We are undoing all edits *after* some rev ID (undoafter). |
| 1642 | # If undoafter is not given, then it is the previous rev ID. |
| 1643 | $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); |
| 1644 | $revision = $revisionLookup->getRevisionById( $latestId ); |
| 1645 | $previousRevision = $revision ? $revisionLookup->getPreviousRevision( $revision ) : null; |
| 1646 | $altBaseRevId = $request->getInt( 'wpUndoAfter', $request->getInt( 'undoafter', |
| 1647 | $previousRevision ? $previousRevision->getId() : null |
| 1648 | ) ); |
| 1649 | } else { |
| 1650 | $altBaseRevId = 0; |
| 1651 | } |
| 1652 | |
| 1653 | return $altBaseRevId; |
| 1654 | } |
| 1655 | } |