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