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