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