Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
RejectConfirmationFormUI
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 2
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getHtml
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 1
650
1<?php
2
3use MediaWiki\CommentStore\CommentStore;
4use MediaWiki\Html\Html;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Revision\RevisionRecord;
7use MediaWiki\Revision\RevisionStore;
8use MediaWiki\SpecialPage\SpecialPage;
9use MediaWiki\Title\Title;
10
11/**
12 * Reject confirmation review form UI
13 *
14 * TODO inject dependencies
15 */
16class RejectConfirmationFormUI {
17    private RevisionReviewForm $form;
18    private ?RevisionRecord $oldRevRecord;
19    private ?RevisionRecord $newRevRecord;
20    private RevisionStore $revisionStore;
21
22    public function __construct( RevisionReviewForm $form ) {
23        $this->form = $form;
24
25        $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
26        $title = $form->getTitle();
27        $this->newRevRecord = $revisionStore->getRevisionByTitle( $title, $form->getOldId() );
28        $this->oldRevRecord = $revisionStore->getRevisionByTitle( $title, $form->getRefId() );
29        $this->revisionStore = $revisionStore;
30    }
31
32    /**
33     * Get the "are you sure you want to reject these changes?" form
34     * @return array (html string, error string or true)
35     */
36    public function getHtml(): array {
37        global $wgLang;
38
39        $status = $this->form->checkTarget();
40        if ( $status !== true ) {
41            return [ '', $status ]; // not a reviewable existing page
42        }
43        $oldRevRecord = $this->oldRevRecord; // convenience
44        $newRevRecord = $this->newRevRecord; // convenience
45        # Do not mess with archived/deleted revisions
46        if ( !$oldRevRecord ||
47            $oldRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ||
48            !$newRevRecord ||
49            $newRevRecord->isDeleted( RevisionRecord::DELETED_TEXT )
50        ) {
51            return [ '', 'review_bad_oldid' ];
52        }
53
54        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
55
56        $revQuery = $this->revisionStore->getQueryInfo();
57        $res = $dbr->newSelectQueryBuilder()
58            ->tables( $revQuery['tables'] )
59            ->fields( $revQuery['fields'] )
60            ->where( [
61                'rev_page' => $oldRevRecord->getPageId(),
62                $dbr->expr( 'rev_timestamp', '>', $dbr->timestamp( $oldRevRecord->getTimestamp() ) ),
63                $dbr->expr( 'rev_timestamp', '<=', $dbr->timestamp( $newRevRecord->getTimestamp() ) ),
64            ] )
65            ->orderBy( 'rev_timestamp' )
66            ->limit( 251 ) // sanity check
67            ->joinConds( $revQuery['joins'] )
68            ->caller( __METHOD__ )
69            ->fetchResultSet();
70        if ( !$res->numRows() ) {
71            return [ '', 'review_bad_oldid' ];
72        } elseif ( $res->numRows() > 250 ) {
73            return [ '', 'review_reject_excessive' ];
74        }
75
76        $contribs = SpecialPage::getTitleFor( 'Contributions' )->getPrefixedText();
77
78        $lastRevRecord = null;
79        $rejectIds = [];
80        $rejectAuthors = [];
81        $lastRejectAuthor = null;
82        foreach ( $res as $row ) {
83            $revRecord = $this->revisionStore->newRevisionFromRow( $row );
84
85            // skip null edits; if $lastRevRecord is null then this is the first
86            // edit being checked, otherwise compare the content to the previous
87            // revision record
88            if ( $lastRevRecord === null || !$revRecord->hasSameContent( $lastRevRecord ) ) {
89                $rejectIds[] = $revRecord->getId();
90                $user = $revRecord->getUser();
91                $userText = $user ? $user->getName() : '';
92
93                $rejectAuthors[] = $revRecord->isDeleted( RevisionRecord::DELETED_USER )
94                    ? wfMessage( 'rev-deleted-user' )->text()
95                    : "[[{$contribs}/{$userText}|{$userText}]]";
96                // Used for GENDER support for revreview-reject-summary-*
97                $lastRejectAuthor = $userText;
98            }
99            $lastRevRecord = $revRecord;
100        }
101        $rejectAuthors = array_values( array_unique( $rejectAuthors ) );
102
103        if ( !$rejectIds ) { // all null edits? (this shouldn't happen)
104            return [ '', 'review_reject_nulledits' ];
105        }
106
107        // List of revisions being undone...
108        $oldTitle = Title::newFromLinkTarget( $oldRevRecord->getPageAsLinkTarget() );
109
110        $formHTML = '<div class="plainlinks">';
111        $formHTML .= wfMessage( 'revreview-reject-text-list' )
112            ->numParams( count( $rejectIds ) )
113            ->params( $oldTitle->getPrefixedText() )->parse();
114        $formHTML .= '<ul>';
115
116        $list = new RevisionList( RequestContext::getMain(), $oldTitle );
117        $list->filterByIds( $rejectIds );
118
119        for ( $list->reset(); $list->current(); $list->next() ) {
120            $item = $list->current();
121            if ( $item->canView() ) {
122                $formHTML .= $item->getHTML();
123            }
124        }
125        $formHTML .= '</ul>';
126
127        if ( $newRevRecord->isCurrent() ) {
128            // Revision this will revert to (when reverting the top X revs)...
129            $formHTML .= wfMessage( 'revreview-reject-text-revto',
130                $oldTitle->getPrefixedDBkey(),
131                $oldRevRecord->getId(),
132                $wgLang->timeanddate( $oldRevRecord->getTimestamp(), true )
133            )->parse();
134        }
135
136        $comment = $this->form->getComment(); // convenience
137        // Determine the default edit summary...
138        if ( $oldRevRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
139            $oldRevAuthor = wfMessage( 'rev-deleted-user' )->text();
140            $oldRevAuthorUsername = '.';
141        } else {
142            $oldRevAuthor = $oldRevRecord->getUser() ?
143                $oldRevRecord->getUser()->getName() :
144                '';
145            $oldRevAuthorUsername = $oldRevAuthor;
146        }
147        // NOTE: *-cur msg wording not safe for (unlikely) edit auto-merge
148        $msg = $newRevRecord->isCurrent()
149            ? 'revreview-reject-summary-cur'
150            : 'revreview-reject-summary-old';
151        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
152        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
153        $defaultSummary = wfMessage(
154            $msg,
155            $contLang->formatNum( count( $rejectIds ) ),
156            $contLang->listToText( $rejectAuthors ),
157            $oldRevRecord->getId(),
158            $oldRevAuthor,
159            count( $rejectAuthors ) === 1 ? $lastRejectAuthor : '.',
160            $oldRevAuthorUsername
161        )->numParams( count( $rejectAuthors ) )->inContentLanguage()->text();
162        // If the message is too big, then fallback to the shorter one
163        $colonSeparator = wfMessage( 'colon-separator' )->text();
164        $maxLen = CommentStore::COMMENT_CHARACTER_LIMIT - strlen( $colonSeparator ) - strlen( $comment );
165        if ( strlen( $defaultSummary ) > $maxLen ) {
166            $msg = $newRevRecord->isCurrent()
167                ? 'revreview-reject-summary-cur-short'
168                : 'revreview-reject-summary-old-short';
169            $defaultSummary = wfMessage( $msg,
170                $contLang->formatNum( count( $rejectIds ) ),
171                $oldRevRecord->getId(),
172                $oldRevAuthor,
173                $oldRevAuthorUsername
174            )->inContentLanguage()->text();
175        }
176        // Append any review comment...
177        if ( $comment != '' ) {
178            if ( $defaultSummary != '' ) {
179                $defaultSummary .= $colonSeparator;
180            }
181            $defaultSummary .= $comment;
182        }
183
184        $formHTML .= '</div>';
185
186        $reviewTitle = SpecialPage::getTitleFor( 'RevisionReview' );
187        $formHTML .= Html::openElement(
188            'form',
189            [ 'method' => 'POST', 'action' => $reviewTitle->getLocalURL() ]
190        );
191        $formHTML .= Html::hidden( 'action', RevisionReviewForm::ACTION_REJECT );
192        $formHTML .= Html::hidden( 'wpReject', 1 );
193        $formHTML .= Html::hidden( 'wpRejectConfirm', 1 );
194        $formHTML .= Html::hidden( 'oldid', $this->form->getOldId() );
195        $formHTML .= Html::hidden( 'refid', $this->form->getRefId() );
196        $formHTML .= Html::hidden( 'target', $oldTitle->getPrefixedDBkey() );
197        $formHTML .= Html::hidden( 'wpEditToken', $this->form->getUser()->getEditToken() );
198        $formHTML .= Html::hidden( 'changetime', $newRevRecord->getTimestamp() );
199        $formHTML .= Xml::inputLabel(
200            wfMessage( 'revreview-reject-summary' )->text(),
201            'wpReason',
202            'wpReason',
203            120,
204            $defaultSummary,
205            [ 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT ]
206        );
207        $formHTML .= "<br />";
208        $formHTML .= Html::input( 'wpSubmit', wfMessage( 'revreview-reject-confirm' )->text(), 'submit' );
209        $formHTML .= ' ';
210        $formHTML .= $linkRenderer->makeLink(
211            $this->form->getTitle(),
212            wfMessage( 'revreview-reject-cancel' )->text(),
213            [ 'onClick' => 'history.back(); return history.length <= 1;' ],
214            [ 'oldid' => $this->form->getRefId(), 'diff' => $this->form->getOldId() ]
215        );
216        $formHTML .= Html::closeElement( 'form' );
217
218        return [ $formHTML, true ];
219    }
220}