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