Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
125 / 150
42.31% covered (danger)
42.31%
11 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageStabilityForm
83.33% covered (warning)
83.33%
125 / 150
42.31% covered (danger)
42.31%
11 / 26
79.80
0.00% covered (danger)
0.00%
0 / 1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setWatchThis
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReasonExtra
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setReasonExtra
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReasonSelection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setReasonSelection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExpiryCustom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExpiryCustom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExpirySelection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExpirySelection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAutoreview
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAutoreview
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExpiry
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
8.23
 getReason
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 doCheckTargetGiven
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 doCheckTarget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 doCheckParameters
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 reallyDoCheckParameters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isAllowed
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 doPreloadParameters
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 reallyDoPreloadParameters
n/a
0 / 0
n/a
0 / 0
0
 doSubmit
97.56% covered (success)
97.56%
40 / 41
0.00% covered (danger)
0.00%
0 / 1
12
 updateLogsAndHistory
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
4
 getOldConfig
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 getNewConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 updateWatchlist
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
1<?php
2
3use MediaWiki\CommentStore\CommentStoreComment;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\Revision\RevisionRecord;
6use MediaWiki\Title\Title;
7use Wikimedia\Rdbms\IDBAccessObject;
8use Wikimedia\Timestamp\ConvertibleTimestamp;
9
10/**
11 * Class containing stability settings form business logic
12 */
13abstract class PageStabilityForm extends FRGenericSubmitForm {
14
15    /** @var Title|false Target page obj */
16    protected $title = false;
17
18    /** @var bool|null Watch checkbox */
19    protected $watchThis = null;
20
21    /** @var bool|null Auto-review option */
22    protected $reviewThis = null;
23
24    /** @var string Custom/extra reason */
25    protected $reasonExtra = '';
26
27    /** @var string Reason dropdown key */
28    protected $reasonSelection = '';
29
30    /** @var string Custom expiry */
31    protected $expiryCustom = '';
32
33    /** @var string Expiry dropdown key */
34    protected $expirySelection = '';
35
36    /** @var int Default version */
37    protected $override = -1;
38
39    /** @var string Autoreview restrictions */
40    protected $autoreview = '';
41
42    /** @var array Old page config */
43    protected $oldConfig = [];
44
45    /**
46     * @return Title|false
47     */
48    public function getTitle() {
49        return $this->title;
50    }
51
52    /**
53     * @param Title $value
54     */
55    public function setTitle( Title $value ) {
56        $this->trySet( $this->title, $value );
57    }
58
59    /**
60     * @param bool|null $value
61     */
62    public function setWatchThis( $value ) {
63        $this->trySet( $this->watchThis, $value );
64    }
65
66    /**
67     * @return string
68     */
69    public function getReasonExtra() {
70        return $this->reasonExtra;
71    }
72
73    /**
74     * @param string $value
75     */
76    public function setReasonExtra( $value ) {
77        $this->trySet( $this->reasonExtra, $value );
78    }
79
80    /**
81     * @return string
82     */
83    public function getReasonSelection() {
84        return $this->reasonSelection;
85    }
86
87    /**
88     * @param string $value
89     */
90    public function setReasonSelection( $value ) {
91        $this->trySet( $this->reasonSelection, $value );
92    }
93
94    /**
95     * @return string
96     */
97    public function getExpiryCustom() {
98        return $this->expiryCustom;
99    }
100
101    /**
102     * @param string $value
103     */
104    public function setExpiryCustom( $value ) {
105        $this->trySet( $this->expiryCustom, $value );
106    }
107
108    /**
109     * @return string
110     */
111    public function getExpirySelection() {
112        return $this->expirySelection;
113    }
114
115    /**
116     * @param string $value
117     */
118    public function setExpirySelection( $value ) {
119        $this->trySet( $this->expirySelection, $value );
120    }
121
122    /**
123     * @return string
124     */
125    public function getAutoreview() {
126        return $this->autoreview;
127    }
128
129    /**
130     * @param string $value
131     */
132    public function setAutoreview( $value ) {
133        $this->trySet( $this->autoreview, $value );
134    }
135
136    /**
137     * Get the final expiry, all inputs considered
138     * Note: does not check if the expiration is less than wfTimestampNow()
139     * @return string|bool 14-char timestamp or "infinity", or false if the input was invalid
140     */
141    public function getExpiry() {
142        $oldConfig = $this->getOldConfig();
143        if ( $this->expirySelection == 'existing' ) {
144            return $oldConfig['expiry'];
145        } elseif ( $this->expirySelection == 'othertime' ) {
146            $value = $this->expiryCustom;
147        } else {
148            $value = $this->expirySelection;
149        }
150        if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) {
151            $time = 'infinity';
152        } else {
153            $unix = strtotime( $value, ConvertibleTimestamp::time() );
154            # On error returns -1 for PHP <5.1 and false for PHP >=5.1
155            if ( !$unix || $unix === -1 ) {
156                return false;
157            }
158            // FIXME: non-qualified absolute times are not in users
159            // specified timezone and there isn't notice about it in the ui
160            $time = wfTimestamp( TS_MW, $unix );
161        }
162        return $time;
163    }
164
165    /**
166     * Get the final reason, all inputs considered
167     * @return string
168     */
169    private function getReason() {
170        # Custom reason replaces dropdown
171        if ( $this->reasonSelection != 'other' ) {
172            $comment = $this->reasonSelection; // start with dropdown reason
173            if ( $this->reasonExtra != '' ) {
174                # Append custom reason
175                $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
176                    $this->reasonExtra;
177            }
178        } else {
179            $comment = $this->reasonExtra; // just use custom reason
180        }
181        return $comment;
182    }
183
184    /**
185     * Check that a target is given (e.g. from GET/POST request)
186     * @return true|string true on success, error string on failure
187     */
188    protected function doCheckTargetGiven() {
189        if ( $this->title === null ) {
190            return 'stabilize_page_invalid';
191        }
192        return true;
193    }
194
195    /**
196     * Check that the target page is valid
197     * @param int $flags FOR_SUBMISSION (set on submit)
198     * @return true|string true on success, error string on failure
199     */
200    protected function doCheckTarget( $flags = 0 ) {
201        $flgs = ( $flags & self::FOR_SUBMISSION ) ? IDBAccessObject::READ_LATEST : 0;
202        if ( !$this->title->getArticleID( $flgs ) ) {
203            return 'stabilize_page_notexists';
204        } elseif ( !FlaggedRevs::inReviewNamespace( $this->title ) ) {
205            return 'stabilize_page_unreviewable';
206        }
207        return true;
208    }
209
210    /**
211     * Verify and clean up parameters (e.g. from POST request)
212     * @return true|string true on success, error string on failure
213     */
214    protected function doCheckParameters() {
215        # Load old config settings from the primary DB
216        $this->oldConfig = FRPageConfig::getStabilitySettings( $this->title, IDBAccessObject::READ_LATEST );
217        if ( $this->expiryCustom != '' ) {
218            // Custom expiry takes precedence
219            $this->expirySelection = 'othertime';
220        }
221        // check other params...
222        return $this->reallyDoCheckParameters();
223    }
224
225    /**
226     * @return true|string true on success, error string on failure
227     */
228    protected function reallyDoCheckParameters() {
229        return true;
230    }
231
232    /**
233     * Can the user change the settings for this page?
234     * Note: if the current autoreview restriction is too high for this user
235     *       then this will return false. Useful for form selectors.
236     * @return bool
237     */
238    public function isAllowed() {
239        # Users who cannot edit or review the page cannot set this
240        $pm = MediaWikiServices::getInstance()->getPermissionManager();
241        return ( $this->getTitle()
242            && $pm->userCan( 'stablesettings', $this->getUser(), $this->getTitle() )
243            && $pm->userCan( 'review', $this->getUser(), $this->getTitle() )
244        );
245    }
246
247    /**
248     * Preload existing page settings (e.g. from GET request).
249     */
250    protected function doPreloadParameters() {
251        $oldConfig = $this->getOldConfig();
252        if ( $oldConfig['expiry'] == 'infinity' ) {
253            $this->expirySelection = 'infinite'; // no settings set OR indefinite
254        } else {
255            $this->expirySelection = 'existing'; // settings set and NOT indefinite
256        }
257        $this->reallyDoPreloadParameters();
258    }
259
260    /**
261     * Override this in subclasses to preload parameters other than expirySelection
262     */
263    abstract protected function reallyDoPreloadParameters();
264
265    /**
266     * Submit the form parameters for the page config to the DB.
267     *
268     * @return true|string true on success, error string on failure
269     */
270    protected function doSubmit() {
271        # Double-check permissions
272        if ( !$this->isAllowed() ) {
273            return 'stabilize_denied';
274        }
275        # Parse and cleanup the expiry time given...
276        $expiry = $this->getExpiry();
277        if ( $expiry === false ) {
278            return 'stabilize_expiry_invalid';
279        } elseif ( $expiry !== 'infinity' && $expiry < wfTimestampNow() ) {
280            return 'stabilize_expiry_old';
281        }
282        # Update the DB row with the new config...
283        $changed = FRPageConfig::setStabilitySettings( $this->title, $this->getNewConfig() );
284        # Log if this actually changed anything...
285        if ( $changed ) {
286            // Inform other code that the stabilisation settings for a page have changed.
287            $services = MediaWikiServices::getInstance();
288            ( new FlaggedRevsHookRunner( $services->getHookContainer() ) )->onFlaggedRevsStabilitySettingsChanged(
289                $this->getTitle(), $this->getNewConfig(), $this->getUser(), $this->getReason()
290            );
291
292            $article = FlaggableWikiPage::newInstance( $this->title );
293            if ( FlaggedRevs::useOnlyIfProtected() ) {
294                # Config may have changed to allow stable versions, so refresh
295                # the tracking table to account for any hidden reviewed versions...
296                $frev = FlaggedRevision::determineStable( $this->title );
297                if ( $frev ) {
298                    $article->updateStableVersion( $frev );
299                } else {
300                    $article->clearStableVersion();
301                }
302            }
303            # Update logs and make a null edit
304            $nullRevRecord = $this->updateLogsAndHistory( $article );
305            # Null edit may have been auto-reviewed already
306            $frev = FlaggedRevision::newFromTitle(
307                $this->title,
308                $nullRevRecord->getId(),
309                IDBAccessObject::READ_LATEST
310            );
311            $updatesDone = (bool)$frev; // stableVersionUpdates() already called?
312            # Check if this null edit is to be reviewed...
313            if ( $this->reviewThis && !$frev ) {
314                $flags = null;
315                # Review this revision of the page...
316                $ok = FlaggedRevs::autoReviewEdit(
317                    $article,
318                    $this->user,
319                    $nullRevRecord,
320                    $flags
321                );
322                if ( $ok ) {
323                    FlaggedRevs::markRevisionPatrolled( $nullRevRecord ); // reviewed -> patrolled
324                    $updatesDone = true; // stableVersionUpdates() already called
325                }
326            }
327            # Update page and tracking tables and clear cache.
328            if ( !$updatesDone ) {
329                FlaggedRevs::stableVersionUpdates( $this->title );
330            }
331        }
332        # Apply watchlist checkbox value (may be NULL)
333        $this->updateWatchlist();
334        return true;
335    }
336
337    /**
338     * Do history & log updates:
339     * (a) Add a new stability log entry
340     * (b) Add a null edit like the log entry
341     * @param FlaggableWikiPage $article
342     * @return RevisionRecord
343     */
344    private function updateLogsAndHistory( FlaggableWikiPage $article ) {
345        $newConfig = $this->getNewConfig();
346        $oldConfig = $this->getOldConfig();
347        $reason = $this->getReason();
348
349        # Insert stability log entry...
350        FlaggedRevsLog::updateStabilityLog( $this->title, $newConfig, $oldConfig, $reason, $this->user );
351
352        # Build null-edit comment...<action: reason [settings] (expiry)>
353        if ( FRPageConfig::configIsReset( $newConfig ) ) {
354            $type = "stable-logentry-reset";
355            $settings = ''; // no level, expiry info
356        } else {
357            $type = "stable-logentry-config";
358            // Settings message in text form (e.g. [x=a,y=b,z])
359            $params = FlaggedRevsLog::stabilityLogParams( $newConfig );
360            $settings = FlaggedRevsStableLogFormatter::stabilitySettings( $params, true /*content*/ );
361        }
362        // action
363        $services = MediaWikiServices::getInstance();
364        $comment = $services->getContentLanguage()->ucfirst(
365            wfMessage( $type, $this->title->getPrefixedText() )->inContentLanguage()->text()
366        );
367        if ( $reason != '' ) {
368            $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; // add reason
369        }
370        if ( $settings != '' ) {
371            $comment .= ' ' . $settings;
372        }
373
374        # Insert a null revision...
375        $revStore = $services->getRevisionStore();
376        $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
377        $nullRevRecord = $revStore->newNullRevision(
378            $dbw,
379            $article->getTitle(),
380            CommentStoreComment::newUnsavedComment( $comment ),
381            true, // minor
382            $this->user
383        );
384        $insertedRevRecord = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
385        # Update page record and touch page
386        $oldLatest = $insertedRevRecord->getParentId();
387
388        $article->updateRevisionOn( $dbw, $insertedRevRecord, $oldLatest );
389
390        $tags = []; // passed by reference
391        $hookContainer = $services->getHookContainer();
392
393        $hookRunner = new FlaggedRevsHookRunner( $hookContainer );
394        $hookRunner->onRevisionFromEditComplete(
395            $article, $insertedRevRecord, $oldLatest, $this->user, $tags
396        );
397
398        # Return null RevisionRecord object for autoreview check
399        return $insertedRevRecord;
400    }
401
402    /**
403     * Get current stability config array
404     * @return array
405     */
406    public function getOldConfig() {
407        if ( $this->getState() == self::FORM_UNREADY ) {
408            throw new LogicException( __CLASS__ . " input fields not set yet.\n" );
409        }
410        if ( $this->oldConfig === [] && $this->title ) {
411            $this->oldConfig = FRPageConfig::getStabilitySettings( $this->title );
412        }
413        return $this->oldConfig;
414    }
415
416    /**
417     * Get proposed stability config array
418     * @return array
419     */
420    public function getNewConfig() {
421        return [
422            'override'   => $this->override,
423            'autoreview' => $this->autoreview,
424            'expiry'     => $this->getExpiry(), // TS_MW/infinity
425        ];
426    }
427
428    /**
429     * (a) Watch page if $watchThis is true
430     * (b) Unwatch if $watchThis is false
431     */
432    private function updateWatchlist() {
433        # Apply watchlist checkbox value (may be NULL)
434        $watchlistManager = MediaWikiServices::getInstance()->getWatchlistManager();
435        if ( $this->watchThis === true ) {
436            $watchlistManager->addWatch( $this->user, $this->title );
437        } elseif ( $this->watchThis === false ) {
438            $watchlistManager->removeWatch( $this->user, $this->title );
439        }
440    }
441}