Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.03% covered (success)
99.03%
102 / 103
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
HtmlSplitConflictHeader
99.03% covered (success)
99.03%
102 / 103
88.89% covered (warning)
88.89%
8 / 9
19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getLatestRevision
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getHtml
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
2.00
 buildCurrentVersionHeader
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 buildYourVersionHeader
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 buildVersionHeader
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 getCopyLink
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 getFormattedDateTime
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getMessageBox
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace TwoColConflict\Html;
4
5use IDBAccessObject;
6use Language;
7use MediaWiki\CommentFormatter\CommentFormatter;
8use MediaWiki\Html\Html;
9use MediaWiki\Linker\Linker;
10use MediaWiki\Linker\LinkRenderer;
11use MediaWiki\Linker\LinkTarget;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Page\WikiPageFactory;
14use MediaWiki\Permissions\Authority;
15use MediaWiki\Revision\RevisionRecord;
16use MediaWiki\SpecialPage\SpecialPage;
17use Message;
18use MessageLocalizer;
19use OOUI\HtmlSnippet;
20use OOUI\MessageWidget;
21use TwoColConflict\SplitConflictUtils;
22use Wikimedia\Timestamp\ConvertibleTimestamp;
23
24/**
25 * @license GPL-2.0-or-later
26 * @author Andrew Kostka <andrew.kostka@wikimedia.de>
27 */
28class HtmlSplitConflictHeader {
29
30    private LinkTarget $linkTarget;
31    private ?RevisionRecord $revision;
32    private Authority $authority;
33    private Language $language;
34    private MessageLocalizer $messageLocalizer;
35    private ConvertibleTimestamp $now;
36    private string $newEditSummary;
37    private LinkRenderer $linkRenderer;
38    private WikiPageFactory $wikiPageFactory;
39    private CommentFormatter $commentFormatter;
40
41    /**
42     * @param LinkTarget $linkTarget
43     * @param Authority $authority
44     * @param string $newEditSummary
45     * @param Language $language
46     * @param MessageLocalizer $messageLocalizer
47     * @param CommentFormatter $commentFormatter
48     * @param string|int|false $now Current time for testing. Any value the ConvertibleTimestamp
49     *  class accepts. False for the current time.
50     * @param RevisionRecord|null $revision Latest revision for testing, derived from the
51     *  title otherwise.
52     */
53    public function __construct(
54        LinkTarget $linkTarget,
55        Authority $authority,
56        string $newEditSummary,
57        Language $language,
58        MessageLocalizer $messageLocalizer,
59        CommentFormatter $commentFormatter,
60        $now = false,
61        RevisionRecord $revision = null
62    ) {
63        // TODO inject?
64        $services = MediaWikiServices::getInstance();
65        $this->linkRenderer = $services->getLinkRenderer();
66        $this->wikiPageFactory = $services->getWikiPageFactory();
67
68        $this->linkTarget = $linkTarget;
69        $this->revision = $revision ?? $this->getLatestRevision();
70        $this->authority = $authority;
71        $this->language = $language;
72        $this->messageLocalizer = $messageLocalizer;
73        $this->commentFormatter = $commentFormatter;
74        $this->now = new ConvertibleTimestamp( $now );
75        $this->newEditSummary = $newEditSummary;
76    }
77
78    private function getLatestRevision(): ?RevisionRecord {
79        $wikiPage = $this->wikiPageFactory->newFromLinkTarget( $this->linkTarget );
80        /** @see https://phabricator.wikimedia.org/T203085 */
81        $wikiPage->loadPageData( IDBAccessObject::READ_LATEST );
82        return $wikiPage->getRevisionRecord();
83    }
84
85    /**
86     * @param bool $isUsedAsBetaFeature
87     *
88     * @return string HTML
89     */
90    public function getHtml( bool $isUsedAsBetaFeature = false ): string {
91        $hintMsg = $isUsedAsBetaFeature
92            ? 'twocolconflict-split-header-hint-beta'
93            : 'twocolconflict-split-header-hint';
94
95        $out = $this->getMessageBox(
96            'twocolconflict-split-header-overview', 'error', 'mw-twocolconflict-overview' );
97        $out .= $this->getMessageBox( $hintMsg, 'notice' );
98        $out .= Html::rawElement(
99            'div',
100            [ 'class' => 'mw-twocolconflict-split-header' ],
101            Html::rawElement(
102                'div',
103                [ 'class' => 'mw-twocolconflict-split-flex-header' ],
104                $this->buildCurrentVersionHeader() .
105                    ( new HtmlSideSelectorComponent( $this->messageLocalizer ) )->getHeaderHtml() .
106                    $this->buildYourVersionHeader()
107            )
108        );
109        return $out;
110    }
111
112    private function buildCurrentVersionHeader(): string {
113        $dateTime = $this->messageLocalizer->msg( 'just-now' )->text();
114        $userTools = '';
115        $summary = '';
116
117        if ( $this->revision ) {
118            $dateTime = $this->getFormattedDateTime( $this->revision->getTimestamp() );
119            // FIXME: This blocks us from having pure unit tests for this class
120            $userTools = Linker::revUserTools( $this->revision );
121
122            $comment = $this->revision->getComment( RevisionRecord::FOR_THIS_USER, $this->authority );
123            if ( $comment ) {
124                $summary = $comment->text;
125            }
126        }
127
128        return $this->buildVersionHeader(
129            $this->messageLocalizer->msg( 'twocolconflict-split-current-version-header', $dateTime ),
130            $this->messageLocalizer->msg( 'twocolconflict-split-saved-at' )->rawParams( $userTools ),
131            $summary,
132            'mw-twocolconflict-split-current-version-header'
133        );
134    }
135
136    private function buildYourVersionHeader(): string {
137        return $this->buildVersionHeader(
138            $this->messageLocalizer->msg( 'twocolconflict-split-your-version-header' ),
139            $this->messageLocalizer->msg( 'twocolconflict-split-not-saved-at' ),
140            $this->newEditSummary,
141            'mw-twocolconflict-split-your-version-header',
142            true
143        );
144    }
145
146    /**
147     * @param Message $dateMsg
148     * @param Message $userMsg
149     * @param string $summary
150     * @param string $class
151     * @param bool|null $showCopy
152     *
153     * @return string HTML
154     */
155    private function buildVersionHeader(
156        Message $dateMsg,
157        Message $userMsg,
158        string $summary,
159        string $class,
160        ?bool $showCopy = false
161    ): string {
162        $html = Html::element(
163                'span',
164                [ 'class' => 'mw-twocolconflict-revision-label' ],
165                $dateMsg->text()
166            );
167        if ( $showCopy ) {
168            $html .= $this->getCopyLink();
169        }
170        $html .= Html::element( 'br' ) .
171            Html::rawElement( 'span', [], $userMsg->escaped() );
172
173        if ( $summary !== '' ) {
174            $summaryMsg = $this->messageLocalizer->msg( 'parentheses' )
175                ->rawParams( $this->commentFormatter->format( $summary, $this->linkTarget ) );
176            $html .= Html::element( 'br' ) .
177                Html::rawElement( 'span', [ 'class' => 'comment' ], $summaryMsg->escaped() );
178        }
179
180        return Html::rawElement( 'div', [ 'class' => $class ], $html );
181    }
182
183    private function getCopyLink(): string {
184        $specialPage = SpecialPage::getTitleValueFor(
185            'TwoColConflictProvideSubmittedText',
186            (string)$this->linkTarget
187        );
188        $label = $this->messageLocalizer->msg( 'twocolconflict-copy-tab-action' )->text();
189        $tooltip = $this->messageLocalizer->msg( 'twocolconflict-copy-tab-tooltip' )->text();
190
191        $link = $this->linkRenderer->makeKnownLink(
192            $specialPage,
193            $label,
194            [ 'title' => $tooltip, 'target' => '_blank' ]
195        );
196
197        return ' ' . Html::rawElement(
198            'span',
199            [ 'class' => 'mw-twocolconflict-copy-link' ],
200            $this->messageLocalizer->msg( 'parentheses' )->rawParams( $link )
201        );
202    }
203
204    private function getFormattedDateTime( ?string $timestamp ): string {
205        $diff = ( new ConvertibleTimestamp( $timestamp ?: false ) )->diff( $this->now );
206
207        if ( $diff->days ) {
208            return $this->language->userTimeAndDate( $timestamp, $this->authority->getUser() );
209        }
210
211        if ( $diff->h ) {
212            $minutes = $diff->i + $diff->s / 60;
213            return $this->messageLocalizer->msg( 'hours-ago', round( $diff->h + $minutes / 60 ) )->text();
214        }
215
216        if ( $diff->i ) {
217            return $this->messageLocalizer->msg( 'minutes-ago', round( $diff->i + $diff->s / 60 ) )->text();
218        }
219
220        if ( $diff->s ) {
221            return $this->messageLocalizer->msg( 'seconds-ago', $diff->s )->text();
222        }
223
224        return $this->messageLocalizer->msg( 'just-now' )->text();
225    }
226
227    private function getMessageBox( string $messageKey, string $type, string ...$classes ): string {
228        $html = $this->messageLocalizer->msg( $messageKey )->parse();
229        return ( new MessageWidget( [
230            'label' => new HtmlSnippet( SplitConflictUtils::addTargetBlankToLinks( $html ) ),
231            'type' => $type,
232        ] ) )
233            ->addClasses( [ 'mw-twocolconflict-messageWidget', ...$classes ] )
234            ->toString();
235    }
236
237}