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