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