Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 472
0.00% covered (danger)
0.00%
0 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeRevisionView
0.00% covered (danger)
0.00%
0 / 472
0.00% covered (danger)
0.00%
0 / 38
19460
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 ltrimIntval
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 stringToRevList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 1
650
 navigationLinks
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 checkPostings
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 canPostComments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 formatPathLine
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
 tagForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 splitTags
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 listTags
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 statusForm
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 buildStatusList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 addTagForm
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 formatTag
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 formatDiff
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 formatImgDiff
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 formatImgCell
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 stubDiffLoader
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 formatSignoffs
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 formatComments
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 formatPropChanges
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 formatReferences
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 formatSignoffInline
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 formatCommentInline
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 formatChangeInline
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 formatReferenceInline
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 commentLink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 revLink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 previewComment
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 formatComment
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 commentStyle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 commentReplyLink
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 postCommentForm
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 signoffButtons
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getUserSignoffs
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 referenceButtons
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 addActionButtons
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CodeReview\UI;
4
5use Html;
6use Linker;
7use MediaWiki\Extension\CodeReview\Backend\CodeComment;
8use MediaWiki\Extension\CodeReview\Backend\CodeDiffHighlighter;
9use MediaWiki\Extension\CodeReview\Backend\CodePropChange;
10use MediaWiki\Extension\CodeReview\Backend\CodeRepository;
11use MediaWiki\Extension\CodeReview\Backend\CodeRevision;
12use MediaWiki\Extension\CodeReview\Backend\CodeSignoff;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Permissions\Authority;
15use RequestContext;
16use ResourceLoader;
17use SpecialPage;
18use stdClass;
19use Title;
20use Xml;
21
22/**
23 * Special:Code/MediaWiki/40696
24 */
25class CodeRevisionView extends CodeView {
26    protected $showButtonsFormatReference = false;
27    protected $showButtonsFormatSignoffs = false;
28    protected $referenceInputName = '';
29    protected $performer;
30
31    /**
32     * @param string|CodeRepository $repo
33     * @param string|CodeRevision $rev
34     * @param Authority $performer
35     * @param null $replyTarget
36     */
37    public function __construct( $repo, $rev, Authority $performer, $replyTarget = null ) {
38        parent::__construct( $repo );
39        global $wgRequest;
40
41        $this->performer = $performer;
42        if ( $rev instanceof CodeRevision ) {
43            $this->mRevId = $rev->getId();
44            $this->mRev = $rev;
45        } else {
46            $this->mRevId = intval( ltrim( $rev, 'r' ) );
47            $this->mRev = $this->mRepo
48                ? $this->mRepo->getRevision( $this->mRevId )
49                : null;
50        }
51
52        $this->mPreviewText = false;
53        # Search path for navigation links
54        $this->mPath = trim( $wgRequest->getVal( 'path', '' ) );
55        if ( strlen( $this->mPath ) && $this->mPath[0] !== '/' ) {
56            // make sure this is a valid path
57            $this->mPath = "/{$this->mPath}";
58        }
59        # URL params...
60        $this->mAddTags = $wgRequest->getText( 'wpTag' );
61        $this->mRemoveTags = $wgRequest->getText( 'wpRemoveTag' );
62        $this->mStatus = $wgRequest->getText( 'wpStatus' );
63        $this->jumpToNext = $wgRequest->getCheck( 'wpSaveAndNext' )
64            || $wgRequest->getCheck( 'wpNext' );
65        $this->mReplyTarget = $replyTarget ?
66            (int)$replyTarget : $wgRequest->getIntOrNull( 'wpParent' );
67        $this->text = $wgRequest->getText( "wpReply{$this->mReplyTarget}" );
68        $this->mSkipCache = ( $wgRequest->getVal( 'action' ) == 'purge' );
69        # Make tag arrays
70        $this->mAddTags = $this->splitTags( $this->mAddTags );
71        $this->mRemoveTags = $this->splitTags( $this->mRemoveTags );
72        $this->mSignoffFlags = $wgRequest->getCheck( 'wpSignoff' ) ?
73            $wgRequest->getArray( 'wpSignoffFlags' ) : [];
74        $this->mSelectedSignoffs = $wgRequest->getArray( 'wpSignoffs' );
75        $this->mStrikeSignoffs = $wgRequest->getCheck( 'wpStrikeSignoffs' ) ?
76            $this->mSelectedSignoffs : [];
77
78        $this->mAddReferences = $wgRequest->getCheck( 'wpAddReferencesSubmit' )
79            ? $this->stringToRevList( $wgRequest->getText( 'wpAddReferences' ) )
80            : [];
81
82        $this->mRemoveReferences = $wgRequest->getCheck( 'wpRemoveReferences' ) ?
83            $wgRequest->getIntArray( 'wpReferences', [] ) : [];
84
85        $this->mAddReferenced = $wgRequest->getCheck( 'wpAddReferencedSubmit' )
86            ? $this->stringToRevList( $wgRequest->getText( 'wpAddReferenced' ) )
87            : [];
88
89        $this->mRemoveReferenced = $wgRequest->getCheck( 'wpRemoveReferenced' ) ?
90            $wgRequest->getIntArray( 'wpReferenced', [] ) : [];
91    }
92
93    /**
94     * @param string $item
95     * @return int
96     */
97    private function ltrimIntval( $item ) {
98        $item = ltrim( trim( $item ), 'r' );
99        return intval( $item );
100    }
101
102    /**
103     * @param string $input
104     * @return array
105     */
106    private function stringToRevList( $input ) {
107        return array_map( [ $this, 'ltrimIntval' ], explode( ',', $input ) );
108    }
109
110    public function execute() {
111        global $wgOut, $wgLang;
112        if ( !$this->mRepo ) {
113            $view = new CodeRepoListView();
114            $view->execute();
115            return;
116        }
117        if ( !$this->mRev ) {
118            if ( $this->mRevId !== 0 ) {
119                $wgOut->addWikiMsg( 'code-rev-not-found', $this->mRevId );
120            }
121
122            $view = new CodeRevisionListView( $this->mRepo->getName() );
123            $view->execute();
124            return;
125        }
126        if ( $this->mStatus == '' ) {
127            $this->mStatus = $this->mRev->getStatus();
128        }
129
130        $redirectOnPost = $this->checkPostings();
131        if ( $redirectOnPost ) {
132            $wgOut->redirect( $redirectOnPost );
133            return;
134        }
135
136        $performer = $this->performer;
137
138        $pageTitle = $this->mRepo->getName() . wfMessage( 'word-separator' )->text() .
139            $this->mRev->getIdString();
140        $htmlTitle = $this->mRev->getIdString() . wfMessage( 'word-separator' )->text() .
141            $this->mRepo->getName();
142        $wgOut->setPageTitle( wfMessage( 'code-rev-title', $pageTitle ) );
143        $wgOut->setHTMLTitle( wfMessage( 'code-rev-title', $htmlTitle ) );
144
145        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
146        $repoLink = $linkRenderer->makeLink(
147            SpecialPage::getTitleFor( 'Code', $this->mRepo->getName() ),
148            $this->mRepo->getName()
149        );
150        $revText = $this->navigationLinks();
151        $paths = '';
152        $modifiedPaths = $this->mRev->getModifiedPaths();
153        foreach ( $modifiedPaths as $row ) {
154            // Don't output NOOP paths
155            if ( strtolower( $row->cp_action ) == 'n' ) {
156                continue;
157            }
158            $paths .= $this->formatPathLine( $row->cp_path, $row->cp_action );
159        }
160        if ( $paths ) {
161            $paths = "<div class='mw-codereview-paths mw-content-ltr'><ul>\n$paths</ul></div>\n";
162        }
163        $comments = $this->formatComments( $performer );
164        $commentsLink = '';
165        if ( $comments ) {
166            $commentsLink = " (<a href=\"#code-comments\">" .
167                wfMessage( 'code-comments' )->escaped() . "</a>)\n";
168        }
169        $fields = [
170            'code-rev-repo' => $repoLink,
171            'code-rev-rev' => $revText,
172            'code-rev-date' => $wgLang->timeanddate( $this->mRev->getTimestamp(), true ),
173            'code-rev-author' => $this->authorLink( $this->mRev->getAuthor() ),
174            'code-rev-status' => $this->statusForm() . $commentsLink,
175            'code-rev-tags' => $this->tagForm(),
176            'code-rev-message' => Html::rawElement( 'div', [ 'class' => 'mw-codereview-message' ],
177                $this->formatMessage( $this->mRev->getMessage() ) ),
178            'code-rev-paths' => $paths,
179        ];
180        $special = SpecialPage::getTitleFor( 'Code',
181            $this->mRepo->getName() . '/' . $this->mRev->getId() );
182
183        $html = '';
184        if ( $this->mPath != '' ) {
185            $links = [];
186            foreach ( explode( '|', $this->mPath ) as $path ) {
187                $links[] = $linkRenderer->makeLink(
188                    SpecialPage::getTitleFor( 'Code', $this->mRepo->getName() ),
189                    $path,
190                    [],
191                    [ 'path' => $path ]
192                );
193            }
194            $html .= wfMessage( 'code-browsing-path' )->rawParams( $wgLang->commaList( $links ) )
195                ->parseAsBlock();
196        }
197        # Output form
198        $html .= Xml::openElement( 'form',
199            [ 'action' => $special->getLocalURL(), 'method' => 'post' ] );
200
201        $canPostComments = $this->canPostComments( $performer );
202        if ( $canPostComments ) {
203            $html .= $this->addActionButtons();
204        }
205
206        $html .= $this->formatMetaData( $fields );
207        # Output diff
208        if ( $this->mRev->isDiffable() ) {
209            $diffHtml = $this->formatDiff();
210            // @todo FIXME: Hard coded brackets.
211            // Even in the cases of DIFFRESULT_TooManyPaths or too large,
212            // users can purge the diff to regenerate it after the relevant
213            // variables have been changed
214            $html .=
215                "<h2>" . wfMessage( 'code-rev-diff' )->escaped() .
216                ' <small>[' . $linkRenderer->makeLink(
217                    $special,
218                    wfMessage( 'code-rev-purge-link' )->text(),
219                    [],
220                    [ 'action' => 'purge' ]
221                ) . ']</small></h2>' .
222                "<div class='mw-codereview-diff' id='mw-codereview-diff'>" . $diffHtml . "</div>\n";
223            $html .= $this->formatImgDiff();
224        }
225
226        # Show sign-offs
227        $userCanSignoff = $performer->isAllowed( 'codereview-signoff' ) && !$performer->getBlock();
228        $signOffs = $this->mRev->getSignoffs();
229        if ( count( $signOffs ) || $userCanSignoff ) {
230            $html .= "<h2 id='code-signoffs'>" . wfMessage( 'code-signoffs' )->escaped() .
231                "</h2>\n" . $this->formatSignoffs( $signOffs, $userCanSignoff );
232        }
233
234        # Show code relations
235        $userCanAssociate = $performer->isAllowed( 'codereview-associate' ) && !$performer->getBlock();
236        $references = $this->mRev->getFollowupRevisions();
237        if ( count( $references ) || $userCanAssociate ) {
238            $html .= "<h2 id='code-references'>" . wfMessage( 'code-references' )->escaped() .
239                "</h2>\n" . $this->formatReferences( $references, $userCanAssociate, 'References' );
240        }
241
242        $referenced = $this->mRev->getFollowedUpRevisions();
243        if ( count( $referenced ) || $userCanAssociate ) {
244            $html .= "<h2 id='code-referenced'>" . wfMessage( 'code-referenced' )->escaped() .
245                "</h2>\n" . $this->formatReferences( $referenced, $userCanAssociate, 'Referenced' );
246        }
247
248        # Add revision comments
249        if ( $comments ) {
250            $html .= "<h2 id='code-comments'>" . wfMessage( 'code-comments' )->escaped() .
251                "</h2>\n" . $comments;
252        }
253
254        if ( $canPostComments ) {
255            $html .= $this->addActionButtons();
256        }
257
258        $changes = $this->formatPropChanges();
259        if ( $changes ) {
260            $html .= "<h2 id='code-changes'>" . wfMessage( 'code-prop-changes' )->escaped() .
261                "</h2>\n" . $changes;
262        }
263        $html .= Xml::closeElement( 'form' );
264
265        // @phan-suppress-next-line SecurityCheck-XSS
266        $wgOut->addHTML( $html );
267    }
268
269    protected function navigationLinks() {
270        global $wgLang;
271
272        $rev = $this->mRev->getId();
273        $prev = $this->mRev->getPrevious( $this->mPath );
274        $next = $this->mRev->getNext( $this->mPath );
275        $repo = $this->mRepo->getName();
276
277        $links = [];
278
279        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
280        if ( $prev ) {
281            $prevTarget = SpecialPage::getTitleFor( 'Code', "$repo/$prev" );
282            $links[] = '&lt;&#160;' .
283                $linkRenderer->makeLink( $prevTarget, $this->mRev->getIdString( $prev ),
284                    [], [ 'path' => $this->mPath ] ) . $wgLang->getDirMark();
285        }
286
287        $revText = "<b>" . $this->mRev->getIdString( $rev ) . "</b>";
288        $viewvc = $this->mRepo->getViewVcBase();
289        if ( $viewvc ) {
290            $url = htmlspecialchars( "$viewvc/?view=rev&revision=$rev" );
291            $viewvcTxt = wfMessage( 'code-rev-rev-viewvc' )->escaped();
292            $revText .= " (<a href=\"$url\" title=\"revision $rev\">$viewvcTxt</a>)" .
293                $wgLang->getDirMark();
294        }
295        $links[] = $revText;
296
297        if ( $next ) {
298            $nextTarget = SpecialPage::getTitleFor( 'Code', "$repo/$next" );
299            $links[] = $linkRenderer->makeLink( $nextTarget, $this->mRev->getIdString( $next ),
300                [], [ 'path' => $this->mPath ] ) . '&#160;&gt;';
301        }
302
303        return $wgLang->pipeList( $links );
304    }
305
306    protected function checkPostings() {
307        global $wgRequest;
308        $userToken = RequestContext::getMain()->getCsrfTokenSet();
309        if ( $wgRequest->wasPosted()
310            && $userToken->matchToken( $wgRequest->getVal( 'wpEditToken' ) )
311        ) {
312            // Look for a posting...
313            $text = $wgRequest->getText( "wpReply{$this->mReplyTarget}" );
314            $isPreview = $wgRequest->getCheck( 'wpPreview' );
315            if ( $isPreview ) {
316                // Save the text for reference on later comment display...
317                $this->mPreviewText = $text;
318            }
319        }
320        return false;
321    }
322
323    /**
324     * @param Authority $performer
325     * @return bool
326     */
327    protected function canPostComments( Authority $performer ) {
328        return $performer->isAllowed( 'codereview-post-comment' ) && !$performer->getBlock();
329    }
330
331    protected function formatPathLine( $path, $action ) {
332        $action = strtolower( $action );
333
334        // If NOOP passed, return ''
335        if ( $action == 'n' ) {
336            return '';
337        }
338        // Uses messages 'code-rev-modified-a', 'code-rev-modified-r', 'code-rev-modified-d',
339        // 'code-rev-modified-m'
340        $desc = wfMessage( 'code-rev-modified-' . $action )->escaped();
341        // Find any ' (from x)' from rename comment in the path.
342        $matches = [];
343        preg_match( '/ \([^\)]+\)$/', $path, $matches );
344        $from = $matches[0] ?? '';
345        // Remove ' (from x)' from rename comment in the path.
346        $path = preg_replace( '/ \([^\)]+\)$/', '', $path );
347        $viewvc = $this->mRepo->getViewVcBase();
348        $diff = '';
349        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
350        $hist = $linkRenderer->makeLink(
351            SpecialPage::getTitleFor( 'Code', $this->mRepo->getName() ),
352            wfMessage( 'code-rev-history-link' )->text(), [], [ 'path' => $path ]
353        );
354        $safePath = wfUrlEncode( $path );
355        if ( $viewvc ) {
356            $rev = $this->mRev->getId();
357            $prev = $rev - 1;
358            if ( $action === 'd' ) {
359                if ( $rev > 1 ) {
360                    $link = Linker::makeExternalLink(
361                        // last rev
362                        "{$viewvc}{$safePath}?view=markup&pathrev=" . ( $rev - 1 ),
363                        $path . $from );
364                } else {
365                    // imported to SVN or something
366                    $link = $safePath;
367                }
368            } else {
369                $link = Linker::makeExternalLink(
370                    "{$viewvc}{$safePath}?view=markup&pathrev=$rev",
371                    $path . $from );
372            }
373            if ( $action !== 'a' && $action !== 'd' ) {
374                $diff = ' (' .
375                    Linker::makeExternalLink(
376                        "$viewvc$safePath?&pathrev=$rev&r1=$prev&r2=$rev",
377                        wfMessage( 'code-rev-diff-link' )->text() ) .
378                    ')';
379            }
380        } else {
381            $link = $safePath;
382        }
383        return "<li><b>$link</b> ($desc) ($hist)$diff</li>\n";
384    }
385
386    protected function tagForm() {
387        $tags = $this->mRev->getTags();
388        $list = '';
389        if ( count( $tags ) ) {
390            $list = implode( ', ',
391                array_map(
392                    [ $this, 'formatTag' ],
393                    $tags )
394            ) . '&#160;';
395        }
396        if ( $this->performer->isAllowed( 'codereview-add-tag' ) ) {
397            $list .= self::addTagForm( $this->mAddTags, $this->mRemoveTags );
398        }
399        return $list;
400    }
401
402    /**
403     * @param string $input
404     * @return array|null
405     */
406    protected function splitTags( $input ) {
407        if ( !$this->mRev ) {
408            return [];
409        }
410        $tags = array_map( 'trim', explode( ',', $input ) );
411        foreach ( $tags as $key => $tag ) {
412            $normal = $this->mRev->normalizeTag( $tag );
413            if ( $normal === false ) {
414                return null;
415            }
416            $tags[$key] = $normal;
417        }
418        return $tags;
419    }
420
421    /**
422     * @param array $tags
423     * @return string
424     */
425    private static function listTags( $tags ) {
426        if ( empty( $tags ) ) {
427            return '';
428        }
429        return implode( ',', $tags );
430    }
431
432    /**
433     * @return string
434     */
435    protected function statusForm() {
436        if ( $this->performer->isAllowed( 'codereview-set-status' ) ) {
437            return Xml::openElement( 'select', [ 'name' => 'wpStatus' ] ) .
438                self::buildStatusList( $this->mStatus, $this ) .
439                Xml::closeElement( 'select' );
440        } else {
441            return htmlspecialchars( $this->statusDesc( $this->mRev->getStatus() ) );
442        }
443    }
444
445    /**
446     * @param string $status
447     * @param CodeView $view
448     * @return string
449     */
450    public static function buildStatusList( $status, $view ) {
451        $states = CodeRevision::getPossibleStates();
452        $out = '';
453        foreach ( $states as $state ) {
454            $out .= Xml::option( $view->statusDesc( $state ), $state,
455                        $status === $state );
456        }
457        return $out;
458    }
459
460    /**
461     * Parameters are the tags to be added/removed sent with the request
462     * @param array $addTags
463     * @param array $removeTags
464     * @return string
465     */
466    public static function addTagForm( $addTags, $removeTags ) {
467        return '<div><table><tr><td>' .
468            Xml::inputLabel( wfMessage( 'code-rev-tag-add' )->text(), 'wpTag', 'wpTag', 20,
469                self::listTags( $addTags ) ) . '</td><td>&#160;</td><td>' .
470            Xml::inputLabel( wfMessage( 'code-rev-tag-remove' )->text(), 'wpRemoveTag',
471                'wpRemoveTag', 20, self::listTags( $removeTags ) ) . '</td></tr></table></div>';
472    }
473
474    /**
475     * @param string $tag
476     * @return string
477     */
478    protected function formatTag( $tag ) {
479        $repo = $this->mRepo->getName();
480        $special = SpecialPage::getTitleFor( 'Code', "$repo/tag/$tag" );
481        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
482        return $linkRenderer->makeLink( $special, $tag );
483    }
484
485    /**
486     * @return string
487     */
488    protected function formatDiff() {
489        global $wgCodeReviewMaxDiffSize;
490
491        if ( $this->mSkipCache ) {
492            // We're purging the cache on purpose, probably
493            // because the cached data was corrupt.
494            $cache = 'skipcache';
495        } else {
496            // If data is already cached, we'll take it now;
497            // otherwise defer the load to an AJAX request.
498            // This lets the page be manipulable even if the
499            // SVN connection is slow or uncooperative.
500            $cache = 'cached';
501        }
502        $diff = $this->mRepo->getDiff( $this->mRev->getId(), $cache );
503
504        // If there isn't anything to diff, or if it's too large, don't AJAX load
505        if ( is_string( $diff ) && strlen( $diff ) > $wgCodeReviewMaxDiffSize ) {
506            return wfMessage( 'code-rev-diff-too-large' )->escaped();
507        } elseif ( is_int( $diff )
508            && in_array( $diff, [
509                CodeRepository::DIFFRESULT_NOTHINGTOCOMPARE, CodeRepository::DIFFRESULT_TOOMANYPATHS
510            ] )
511        ) {
512            // Some other error condition, no diff required
513            return '';
514        } elseif ( $diff === CodeRepository::DIFFRESULT_NOTINCACHE ) {
515            // Api Enabled || Not cached => Load via JS via API
516            return $this->stubDiffLoader();
517        }
518
519        // CodeRepository::DIFFRESULT_NoDataReturned still ends up here, and we can't differentiate
520        $hilite = new CodeDiffHighlighter();
521        return $hilite->render( $diff );
522    }
523
524    /**
525     * @return string
526     */
527    protected function formatImgDiff() {
528        global $wgCodeReviewImgRegex;
529        $viewvc = $this->mRepo->getViewVcBase();
530        if ( !$viewvc ) {
531            return '';
532        }
533        // Get image diffs
534        $imgDiffs = $html = '';
535        $modifiedPaths = $this->mRev->getModifiedPaths();
536        foreach ( $modifiedPaths as $row ) {
537            // Typical image file?
538            if ( preg_match( $wgCodeReviewImgRegex, $row->cp_path ) ) {
539                $imgDiffs .= 'Index: ' . htmlspecialchars( $row->cp_path ) . "\n";
540                $imgDiffs .= '<table border="1px" style="background:white;"><tr>';
541                if ( $row->cp_action !== 'A' ) {
542                    // old
543                    // What was done to it?
544                    $action = $row->cp_action == 'D'
545                        ? 'code-rev-modified-d' : 'code-rev-modified-r';
546                    // Link to old image
547                    $imgDiffs .= $this->formatImgCell(
548                        $viewvc, $row->cp_path, $this->mRev->getPrevious(), $action );
549                }
550                if ( $row->cp_action !== 'D' ) {
551                    // new
552                    // What was done to it?
553                    $action = $row->cp_action == 'A'
554                        ? 'code-rev-modified-a' : 'code-rev-modified-m';
555                    // Link to new image
556                    $imgDiffs .= $this->formatImgCell(
557                        $viewvc, $row->cp_path, $this->mRev->getId(), $action );
558                }
559                $imgDiffs .= "</tr></table>\n";
560            }
561        }
562        if ( $imgDiffs ) {
563            $html = '<h2>' . wfMessage( 'code-rev-imagediff' )->escaped() . '</h2>';
564            $html .= "<div class='mw-codereview-imgdiff'>$imgDiffs</div>\n";
565        }
566        return $html;
567    }
568
569    /**
570     * @param string $viewvc
571     * @param string $path
572     * @param string $rev
573     * @param string $message
574     * @return string
575     */
576    protected function formatImgCell( $viewvc, $path, $rev, $message ) {
577        $safePath = wfUrlEncode( $path );
578        $url = "{$viewvc}{$safePath}?&pathrev=$rev&revision=$rev";
579
580        $alt = wfMessage( $message )->text();
581
582        return Xml::tags( 'td',
583            [],
584            Xml::tags( 'a',
585                [ 'href' => $url ],
586                Xml::element( 'img',
587                    [
588                        'src' => $url,
589                        'alt' => $alt,
590                        'title' => $alt,
591                        'border' => '0'
592                    ]
593                )
594            )
595        );
596    }
597
598    /**
599     * @return bool|string
600     */
601    protected function stubDiffLoader() {
602        global $wgOut;
603        $encRepo = Xml::encodeJsVar( $this->mRepo->getName() );
604        $encRev = Xml::encodeJsVar( $this->mRev->getId() );
605        $wgOut->addHTML( Html::inlineScript( ResourceLoader::makeInlineCodeWithModule(
606            'ext.codereview.loaddiff',
607            "CodeReview.loadDiff($encRepo,$encRev);"
608        ) ) );
609        return wfMessage( 'code-load-diff' )->escaped();
610    }
611
612    /**
613     * Format the sign-offs table
614     * @param array $signOffs
615     * @param bool $showButtons Whether the buttons to strike and submit sign-offs should be shown
616     * @return string HTML
617     */
618    protected function formatSignoffs( $signOffs, $showButtons ) {
619        $this->showButtonsFormatSignoffs = $showButtons;
620
621        $header = '';
622        if ( count( $signOffs ) ) {
623            if ( $showButtons ) {
624                $header = '<th></th>';
625            }
626            $signoffs = implode( "\n",
627                array_map( [ $this, 'formatSignoffInline' ], $signOffs )
628            );
629            $header .= '<th>' . wfMessage( 'code-signoff-field-user' )->escaped() . '</th>';
630            $header .= '<th>' . wfMessage( 'code-signoff-field-flag' )->escaped() . '</th>';
631            $header .= '<th>' . wfMessage( 'code-signoff-field-date' )->escaped() . '</th>';
632        } else {
633            $signoffs = '';
634        }
635        $buttonrow = $showButtons ? $this->signoffButtons( $signOffs ) : '';
636        return "<table border='1' class='wikitable'><tr>$header</tr>$signoffs$buttonrow</table>";
637    }
638
639    /**
640     * @param Authority $performer
641     * @return bool|string
642     */
643    protected function formatComments( Authority $performer ) {
644        $comments = array_map( function ( $comment ) use ( $performer ) {
645            return $this->formatCommentInline( $comment, $performer );
646        }, $this->mRev->getComments() );
647        $comments = implode( "\n", $comments );
648        if ( !$this->mReplyTarget ) {
649            $comments .= $this->postCommentForm();
650        }
651        if ( !$comments ) {
652            return false;
653        }
654        return "<div class='mw-codereview-comments'>$comments</div>";
655    }
656
657    /**
658     * @return bool|string
659     */
660    protected function formatPropChanges() {
661        $changes = implode( "\n",
662            array_map( [ $this, 'formatChangeInline' ], $this->mRev->getPropChanges() )
663        );
664        if ( !$changes ) {
665            return false;
666        }
667        return "<ul class='mw-codereview-changes'>$changes</ul>";
668    }
669
670    /**
671     * @param array $references
672     * @param bool $showButtons
673     * @param string $inputName
674     * @return string
675     */
676    protected function formatReferences( $references, $showButtons, $inputName ) {
677        $this->showButtonsFormatReference = $showButtons;
678        $this->referenceInputName = $inputName;
679        $refs = implode( "\n",
680            array_map( [ $this, 'formatReferenceInline' ], $references )
681        );
682
683        $header = '';
684        if ( $showButtons ) {
685            $header = '<th></th>';
686        }
687        $header .= '<th>' . wfMessage( 'code-field-id' )->escaped() . '</th>';
688        $header .= '<th>' . wfMessage( 'code-field-message' )->escaped() . '</th>';
689        $header .= '<th>' . wfMessage( 'code-field-author' )->escaped() . '</th>';
690        $header .= '<th>' . wfMessage( 'code-field-timestamp' )->escaped() . '</th>';
691        $buttonrow = $showButtons ? $this->referenceButtons( $inputName ) : '';
692        return "<table border='1' class='wikitable'><tr>{$header}</tr>{$refs}{$buttonrow}</table>";
693    }
694
695    /**
696     * Format a single sign-off row. Helper function for formatSignoffs()
697     * @param CodeSignoff $signoff
698     * @return string HTML
699     */
700    protected function formatSignoffInline( $signoff ) {
701        global $wgLang;
702        $user = Linker::userLink( $signoff->user, $signoff->userText );
703        $flag = htmlspecialchars( $signoff->flag );
704        $signoffDate = $wgLang->timeanddate( $signoff->timestamp, true );
705        $class = "mw-codereview-signoff-flag-$flag";
706        if ( $signoff->isStruck() ) {
707            $class .= ' mw-codereview-struck';
708            $struckDate = $wgLang->timeanddate( $signoff->getTimestampStruck(), true );
709            $date = wfMessage( 'code-signoff-struckdate', $signoffDate, $struckDate )->escaped();
710        } else {
711            $date = htmlspecialchars( $signoffDate );
712        }
713
714        $ret = "<tr class='$class'>";
715        if ( $this->showButtonsFormatSignoffs ) {
716            $checkbox = Html::input( 'wpSignoffs[]', $signoff->getID(), 'checkbox' );
717            $ret .= "<td>$checkbox</td>";
718        }
719        $ret .= "<td>$user</td><td>$flag</td><td>$date</td></tr>";
720        return $ret;
721    }
722
723    /**
724     * @param CodeComment $comment
725     * @param Authority $performer
726     * @return string
727     */
728    protected function formatCommentInline( $comment, Authority $performer ) {
729        if ( $comment->id === $this->mReplyTarget ) {
730            return $this->formatComment( $comment, $performer,
731                $this->postCommentForm( $comment->id ) );
732        } else {
733            return $this->formatComment( $comment, $performer );
734        }
735    }
736
737    /**
738     * @param CodePropChange $change
739     * @return string
740     */
741    protected function formatChangeInline( $change ) {
742        global $wgLang;
743        $revId = $change->rev->getIdString();
744        $line = $wgLang->timeanddate( $change->timestamp, true );
745        $line .= '&#160;' . Linker::userLink( $change->user, $change->userText );
746        $line .= Linker::userToolLinks( $change->user, $change->userText );
747        // Uses messages 'code-change-status', 'code-change-tags'
748        $line .= '&#160;' . wfMessage( "code-change-{$change->attrib}", $revId )->parse();
749        $line .= " <i>[";
750        // Items that were changed or set...
751        if ( $change->removed ) {
752            $line .= '<b>' . wfMessage( 'code-change-removed' )->escaped() . '</b> ';
753            // Status changes...
754            if ( $change->attrib == 'status' ) {
755                // Give grep a chance to find the usages:
756                // code-status-new, code-status-fixme, code-status-reverted, code-status-resolved,
757                // code-status-ok, code-status-deferred, code-status-old
758                $line .= wfMessage( 'code-status-' . $change->removed )->escaped();
759                $line .= $change->added ? "&#160;" : "";
760            // Tag changes
761            } elseif ( $change->attrib == 'tags' ) {
762                $line .= htmlspecialchars( $change->removed );
763                $line .= $change->added ? "&#160;" : "";
764            }
765        }
766        // Items that were changed to something else...
767        if ( $change->added ) {
768            $line .= '<b>' . wfMessage( 'code-change-added' )->escaped() . '</b> ';
769            // Status changes...
770            if ( $change->attrib == 'status' ) {
771                // Give grep a chance to find the usages:
772                // code-status-new, code-status-fixme, code-status-reverted, code-status-resolved,
773                // code-status-ok, code-status-deferred, code-status-old
774                $line .= wfMessage( 'code-status-' . $change->added )->escaped();
775            // Tag changes...
776            } else {
777                $line .= htmlspecialchars( $change->added );
778            }
779        }
780        $line .= "]</i>";
781        return "<li>$line</li>";
782    }
783
784    /**
785     * @param stdClass $row
786     * @return string
787     */
788    protected function formatReferenceInline( $row ) {
789        global $wgLang;
790        $rev = intval( $row->cr_id );
791        $repo = $this->mRepo->getName();
792        // Borrow the code revision list css
793        $css = 'mw-codereview-status-' . htmlspecialchars( $row->cr_status );
794        $date = $wgLang->timeanddate( $row->cr_timestamp, true );
795        $title = SpecialPage::getTitleFor( 'Code', "$repo/$rev" );
796        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
797        $revLink = $linkRenderer->makeLink( $title, $this->mRev->getIdString( $rev ) );
798        $summary = $this->messageFragment( $row->cr_message );
799        $author = $this->authorLink( $row->cr_author );
800
801        $ret = "<tr class='$css'>";
802        if ( $this->showButtonsFormatReference ) {
803            $checkbox = Html::input( "wp{$this->referenceInputName}[]", $rev, 'checkbox' );
804            $ret .= "<td>$checkbox</td>";
805        }
806        $ret .= "<td>$revLink</td><td>$summary</td><td>$author</td><td>$date</td></tr>";
807        return $ret;
808    }
809
810    /**
811     * @param int $commentId
812     * @return Title
813     */
814    protected function commentLink( $commentId ) {
815        $repo = $this->mRepo->getName();
816        $rev = $this->mRev->getId();
817        return SpecialPage::getTitleFor( 'Code', "$repo/$rev", "c{$commentId}" );
818    }
819
820    /**
821     * @return Title
822     */
823    protected function revLink() {
824        $repo = $this->mRepo->getName();
825        $rev = $this->mRev->getId();
826        return SpecialPage::getTitleFor( 'Code', "$repo/$rev" );
827    }
828
829    /**
830     * @param string $text
831     * @param Authority $performer
832     * @return string
833     */
834    protected function previewComment( $text, Authority $performer ) {
835        $comment = $this->mRev->previewComment( $text, $performer );
836        return $this->formatComment( $comment, $performer );
837    }
838
839    /**
840     * @param CodeComment $comment
841     * @param Authority $performer
842     * @param string $replyForm
843     * @return string
844     */
845    public function formatComment( $comment, Authority $performer, $replyForm = '' ) {
846        global $wgOut, $wgLang;
847
848        $services = MediaWikiServices::getInstance();
849        $dir = $services->getContentLanguage()->getDir();
850
851        if ( $comment->id === 0 ) {
852            $linkId = 'cpreview';
853            $permaLink = '<strong>' .
854                wfMessage( 'code-rev-inline-preview' )->escaped() . '</strong> ';
855        } else {
856            $linkId = 'c' . intval( $comment->id );
857            $linkRenderer = $services->getLinkRenderer();
858            $permaLink = $linkRenderer->makeLink( $this->commentLink( $comment->id ), '#' );
859        }
860
861        return Xml::openElement( 'div',
862            [
863                'class' => 'mw-codereview-comment',
864                'id' => $linkId,
865                'style' => $this->commentStyle( $comment ) ] ) .
866            '<div class="mw-codereview-comment-meta">' .
867            $permaLink .
868            wfMessage( 'code-rev-comment-by' )->rawParams(
869                Linker::userLink( $comment->user, $comment->userText ) .
870                    Linker::userToolLinks( $comment->user, $comment->userText )
871            )->escaped() .
872            ' &#160; ' .
873            $wgLang->timeanddate( $comment->timestamp, true ) .
874            ' ' .
875            $this->commentReplyLink( $performer, $comment->id ) .
876            '</div>' .
877            '<div class="mw-codereview-comment-text mw-content-' . htmlspecialchars( $dir ) . '">' .
878            $wgOut->parseAsContent( $this->codeCommentLinkerWiki->link( $comment->text ) ) .
879            '</div>' .
880            $replyForm .
881            '</div>';
882    }
883
884    /**
885     * @param CodeComment $comment
886     * @return string
887     */
888    protected function commentStyle( $comment ) {
889        global $wgLang;
890        $align = $wgLang->alignStart();
891        $depth = $comment->threadDepth();
892        $margin = ( $depth - 1 ) * 48;
893        return "margin-$align{$margin}px";
894    }
895
896    /**
897     * @param Authority $performer
898     * @param int $id
899     * @return string
900     */
901    protected function commentReplyLink( Authority $performer, $id ) {
902        if ( !$this->canPostComments( $performer ) ) {
903            return '';
904        }
905        $repo = $this->mRepo->getName();
906        $rev = $this->mRev->getId();
907        $self = SpecialPage::getTitleFor( 'Code', "$repo/$rev/reply/$id", "c$id" );
908        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
909        // @todo FIXME: Hard coded brackets.
910        return '[' .
911            $linkRenderer->makeLink( $self, wfMessage( 'codereview-reply-link' )->text() ) . ']';
912    }
913
914    protected function postCommentForm( $parent = null ) {
915        $performer = $this->performer;
916        $userToken = RequestContext::getMain()->getCsrfTokenSet();
917
918        if ( !$this->canPostComments( $performer ) ) {
919            return '';
920        }
921
922        if ( $this->mPreviewText !== false && $parent === $this->mReplyTarget ) {
923            $preview = $this->previewComment( $this->mPreviewText, $performer );
924            $text = $this->mPreviewText;
925        } else {
926            $preview = '';
927            $text = $this->text;
928        }
929
930        return '<div class="mw-codereview-post-comment">' .
931            $preview .
932            Html::hidden( 'wpEditToken', $userToken->getToken() ) .
933            Html::hidden( 'path', $this->mPath ) .
934            ( $parent ? Html::hidden( 'wpParent', $parent ) : '' ) .
935            '<div>' .
936            Xml::openElement( 'textarea', [
937                'name' => "wpReply{$parent}",
938                'id' => "wpReplyTo{$parent}",
939                'cols' => 40,
940                'rows' => 10 ] ) .
941            htmlspecialchars( $text ) .
942            '</textarea>' .
943            '</div>' .
944            '</div>';
945    }
946
947    /**
948     * Render the bottom row of the sign-offs table containing the buttons to
949     * strike and submit sign-offs
950     *
951     * @param array $signOffs
952     * @return string HTML
953     */
954    protected function signoffButtons( $signOffs ) {
955        $userSignOffs = $this->getUserSignoffs( $signOffs );
956        $strikeButton = count( $userSignOffs )
957            ? Xml::submitButton( wfMessage( 'code-signoff-strike' )->text(),
958                [ 'name' => 'wpStrikeSignoffs' ] )
959            : '';
960        $signoffText = wfMessage( 'code-signoff-signoff' )->escaped();
961        $signoffButton = Xml::submitButton( wfMessage( 'code-signoff-submit' )->text(),
962            [ 'name' => 'wpSignoff' ] );
963        $checks = '';
964
965        foreach ( CodeRevision::getPossibleFlags() as $flag ) {
966            $checks .= Html::input( 'wpSignoffFlags[]', $flag, 'checkbox',
967                [
968                    'id' => "wpSignoffFlags-$flag",
969                    'disabled' => isset( $userSignOffs[$flag] ),
970                ] ) .
971                ' ' . Xml::label( wfMessage( "code-signoff-flag-$flag" )->text(),
972                "wpSignoffFlags-$flag" ) . ' ';
973        }
974        return "<tr class='mw-codereview-signoffbuttons'><td colspan='4'>$strikeButton " .
975            "<div class='mw-codereview-signoffchecks'>$signoffText $checks $signoffButton" .
976            "</div></td></tr>";
977    }
978
979    /**
980     * Gets all the current signoffs the user has against this revision
981     *
982     * @param array $signOffs
983     * @return array
984     */
985    protected function getUserSignoffs( $signOffs ) {
986        $ret = [];
987        /**
988         * @var $s CodeSignoff
989         */
990        foreach ( $signOffs as $s ) {
991            if ( $s->userText == $this->performer->getUser()->getName() && !$s->isStruck() ) {
992                $ret[$s->flag] = true;
993            }
994        }
995        return $ret;
996    }
997
998    /**
999     * Render the bottom row of the follow-up revisions table containing the buttons and
1000     * textbox to add and remove follow-up associations
1001     * @param string $inputName
1002     * @return string HTML
1003     */
1004    protected function referenceButtons( $inputName ) {
1005        $removeButton = Xml::submitButton( wfMessage( 'code-reference-remove' )->text(),
1006            [ 'name' => "wpRemove{$inputName}" ] );
1007        $associateText = wfMessage( 'code-reference-associate' )->escaped();
1008        $associateButton = Xml::submitButton(
1009            wfMessage( 'code-reference-associate-submit' )->text(),
1010            [ 'name' => "wpAdd{$inputName}Submit" ]
1011        );
1012        $textbox = Html::input( "wpAdd{$inputName}" );
1013        return "<tr class='mw-codereview-associatebuttons'><td colspan='5'>$removeButton " .
1014            "<div class='mw-codereview-associateform'>$associateText $textbox $associateButton" .
1015            "</div></td></tr>";
1016    }
1017
1018    /**
1019     * @return string
1020     */
1021    protected function addActionButtons() {
1022        return '<div id="mw-codereview-comment-buttons">' .
1023            Xml::submitButton( wfMessage( 'code-rev-submit' )->text(),
1024                [ 'name' => 'wpSave',
1025                    'accesskey' => wfMessage( 'code-rev-submit-accesskey' )->text() ]
1026            ) . ' ' .
1027            Xml::submitButton( wfMessage( 'code-rev-submit-next' )->text(),
1028                [ 'name' => 'wpSaveAndNext',
1029                    'accesskey' => wfMessage( 'code-rev-submit-next-accesskey' )->text() ]
1030            ) . ' ' .
1031            Xml::submitButton( wfMessage( 'code-rev-next' )->text(),
1032                [ 'name' => 'wpNext',
1033                    'accesskey' => wfMessage( 'code-rev-next-accesskey' )->text() ]
1034            ) . ' ' .
1035            Xml::submitButton( wfMessage( 'code-rev-comment-preview' )->text(),
1036                [ 'name' => 'wpPreview',
1037                    'accesskey' => wfMessage( 'code-rev-comment-preview-accesskey' )->text() ]
1038            ) .
1039            '</div>';
1040    }
1041}