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;
7use Wikimedia\Rdbms\IDBAccessObject;
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 );
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            $article = FlaggableWikiPage::newInstance( $this->title );
286            if ( FlaggedRevs::useOnlyIfProtected() ) {
287                # Config may have changed to allow stable versions, so refresh
288                # the tracking table to account for any hidden reviewed versions...
289                $frev = FlaggedRevision::determineStable( $this->title );
290                if ( $frev ) {
291                    $article->updateStableVersion( $frev );
292                } else {
293                    $article->clearStableVersion();
294                }
295            }
296            # Update logs and make a null edit
297            $nullRevRecord = $this->updateLogsAndHistory( $article );
298            # Null edit may have been auto-reviewed already
299            $frev = FlaggedRevision::newFromTitle(
300                $this->title,
301                $nullRevRecord->getId(),
302                IDBAccessObject::READ_LATEST
303            );
304            $updatesDone = (bool)$frev; // stableVersionUpdates() already called?
305            # Check if this null edit is to be reviewed...
306            if ( $this->reviewThis && !$frev ) {
307                $flags = null;
308                # Review this revision of the page...
309                $ok = FlaggedRevs::autoReviewEdit(
310                    $article,
311                    $this->user,
312                    $nullRevRecord,
313                    $flags
314                );
315                if ( $ok ) {
316                    FlaggedRevs::markRevisionPatrolled( $nullRevRecord ); // reviewed -> patrolled
317                    $updatesDone = true; // stableVersionUpdates() already called
318                }
319            }
320            # Update page and tracking tables and clear cache.
321            if ( !$updatesDone ) {
322                FlaggedRevs::stableVersionUpdates( $this->title );
323            }
324        }
325        # Apply watchlist checkbox value (may be NULL)
326        $this->updateWatchlist();
327        return true;
328    }
329
330    /**
331     * Do history & log updates:
332     * (a) Add a new stability log entry
333     * (b) Add a null edit like the log entry
334     * @param FlaggableWikiPage $article
335     * @return RevisionRecord
336     */
337    private function updateLogsAndHistory( FlaggableWikiPage $article ) {
338        $newConfig = $this->getNewConfig();
339        $oldConfig = $this->getOldConfig();
340        $reason = $this->getReason();
341
342        # Insert stability log entry...
343        FlaggedRevsLog::updateStabilityLog( $this->title, $newConfig, $oldConfig, $reason, $this->user );
344
345        # Build null-edit comment...<action: reason [settings] (expiry)>
346        if ( FRPageConfig::configIsReset( $newConfig ) ) {
347            $type = "stable-logentry-reset";
348            $settings = ''; // no level, expiry info
349        } else {
350            $type = "stable-logentry-config";
351            // Settings message in text form (e.g. [x=a,y=b,z])
352            $params = FlaggedRevsLog::stabilityLogParams( $newConfig );
353            $settings = FlaggedRevsStableLogFormatter::stabilitySettings( $params, true /*content*/ );
354        }
355        // action
356        $services = MediaWikiServices::getInstance();
357        $comment = $services->getContentLanguage()->ucfirst(
358            wfMessage( $type, $this->title->getPrefixedText() )->inContentLanguage()->text()
359        );
360        if ( $reason != '' ) {
361            $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; // add reason
362        }
363        if ( $settings != '' ) {
364            $comment .= ' ' . $settings;
365        }
366
367        # Insert a null revision...
368        $revStore = $services->getRevisionStore();
369        $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
370        $nullRevRecord = $revStore->newNullRevision(
371            $dbw,
372            $article->getTitle(),
373            CommentStoreComment::newUnsavedComment( $comment ),
374            true, // minor
375            $this->user
376        );
377        $insertedRevRecord = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
378        # Update page record and touch page
379        $oldLatest = $insertedRevRecord->getParentId();
380
381        $article->updateRevisionOn( $dbw, $insertedRevRecord, $oldLatest );
382
383        $tags = []; // passed by reference
384        $hookContainer = $services->getHookContainer();
385
386        $hookRunner = new FlaggedRevsHookRunner( $hookContainer );
387        $hookRunner->onRevisionFromEditComplete(
388            $article, $insertedRevRecord, $oldLatest, $this->user, $tags
389        );
390
391        # Return null RevisionRecord object for autoreview check
392        return $insertedRevRecord;
393    }
394
395    /**
396     * Get current stability config array
397     * @return array
398     */
399    public function getOldConfig() {
400        if ( $this->getState() == self::FORM_UNREADY ) {
401            throw new LogicException( __CLASS__ . " input fields not set yet.\n" );
402        }
403        if ( $this->oldConfig === [] && $this->title ) {
404            $this->oldConfig = FRPageConfig::getStabilitySettings( $this->title );
405        }
406        return $this->oldConfig;
407    }
408
409    /**
410     * Get proposed stability config array
411     * @return array
412     */
413    public function getNewConfig() {
414        return [
415            'override'   => $this->override,
416            'autoreview' => $this->autoreview,
417            'expiry'     => $this->getExpiry(), // TS_MW/infinity
418        ];
419    }
420
421    /**
422     * (a) Watch page if $watchThis is true
423     * (b) Unwatch if $watchThis is false
424     */
425    private function updateWatchlist() {
426        # Apply watchlist checkbox value (may be NULL)
427        $watchlistManager = MediaWikiServices::getInstance()->getWatchlistManager();
428        if ( $this->watchThis === true ) {
429            $watchlistManager->addWatch( $this->user, $this->title );
430        } elseif ( $this->watchThis === false ) {
431            $watchlistManager->removeWatch( $this->user, $this->title );
432        }
433    }
434}