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