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