Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 152
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 / 152
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 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getHtml
0.00% covered (danger)
0.00%
0 / 147
0.00% covered (danger)
0.00%
0 / 1
650
1<?php
2
3use MediaWiki\CommentStore\CommentStore;
4use MediaWiki\Context\IContextSource;
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;
12
13/**
14 * Reject confirmation review form UI
15 *
16 * TODO inject dependencies
17 */
18class RejectConfirmationFormUI {
19    private readonly ?RevisionRecord $oldRevRecord;
20    private readonly ?RevisionRecord $newRevRecord;
21    private readonly RevisionStore $revisionStore;
22
23    public function __construct(
24        private readonly RevisionReviewForm $form,
25        private readonly IContextSource $context,
26    ) {
27        $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
28        $title = $form->getTitle();
29        $this->newRevRecord = $revisionStore->getRevisionByTitle( $title, $form->getOldId() );
30        $this->oldRevRecord = $revisionStore->getRevisionByTitle( $title, $form->getRefId() );
31        $this->revisionStore = $revisionStore;
32    }
33
34    /**
35     * Get the "are you sure you want to reject these changes?" form
36     * @return array (html string, error string or true)
37     */
38    public function getHtml(): array {
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                    ? $this->context->msg( '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 .= $this->context->msg( 'revreview-reject-text-list' )
112            ->numParams( count( $rejectIds ) )
113            ->params( $oldTitle->getPrefixedText() )->parse();
114        $formHTML .= '<ul>';
115
116        $list = new RevisionList( $this->context, $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            $message = $this->context->msg( 'revreview-reject-text-revto' );
130            $message->params(
131                $oldTitle->getPrefixedDBkey(),
132                $oldRevRecord->getId(),
133                $message->getLanguage()->timeanddate( $oldRevRecord->getTimestamp(), true )
134            );
135            $formHTML .= $message->parse();
136        }
137
138        $comment = $this->form->getComment(); // convenience
139        // Determine the default edit summary...
140        if ( $oldRevRecord->isDeleted( RevisionRecord::DELETED_USER ) ) {
141            $oldRevAuthor = $this->context->msg( 'rev-deleted-user' )->text();
142            $oldRevAuthorUsername = '.';
143        } else {
144            $oldRevAuthor = $oldRevRecord->getUser() ?
145                $oldRevRecord->getUser()->getName() :
146                '';
147            $oldRevAuthorUsername = $oldRevAuthor;
148        }
149        // NOTE: *-cur msg wording not safe for (unlikely) edit auto-merge
150        $msg = $newRevRecord->isCurrent()
151            ? 'revreview-reject-summary-cur'
152            : 'revreview-reject-summary-old';
153        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
154        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
155        $defaultSummary = $this->context->msg(
156            $msg,
157            $contLang->formatNum( count( $rejectIds ) ),
158            $contLang->listToText( $rejectAuthors ),
159            $oldRevRecord->getId(),
160            $oldRevAuthor,
161            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable last author not null iff there are authors
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 = $this->context->msg( '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 = $this->context->msg( $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 .= Html::label(
203            $this->context->msg( 'revreview-reject-summary' )->text(),
204            'wpReason'
205        );
206        $formHTML .= "\u{00A0}";
207        $formHTML .= Html::input(
208            'wpReason',
209            $defaultSummary,
210            'text',
211            [ 'id' => 'wpReason', 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, 'size' => 120 ]
212        );
213        $formHTML .= "<br />";
214        $formHTML .= Html::input(
215            'wpSubmit',
216            $this->context->msg( 'revreview-reject-confirm' )->text(),
217            'submit'
218        );
219        $formHTML .= ' ';
220        $formHTML .= $linkRenderer->makeLink(
221            $this->form->getTitle(),
222            $this->context->msg( 'revreview-reject-cancel' )->text(),
223            [ 'onClick' => 'history.back(); return history.length <= 1;' ],
224            [ 'oldid' => $this->form->getRefId(), 'diff' => $this->form->getOldId() ]
225        );
226        $formHTML .= Html::closeElement( 'form' );
227
228        return [ $formHTML, true ];
229    }
230}