Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.60% covered (warning)
62.60%
82 / 131
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SplitTwoColConflictHelper
62.60% covered (warning)
62.60%
82 / 131
69.23% covered (warning)
69.23%
9 / 13
61.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 incrementConflictStats
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 incrementResolvedStats
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getExplainHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditConflictMainTextBox
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 showEditFormTextAfterFooters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditFormHtmlBeforeContent
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
3.03
 getEditFormHtmlAfterContent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getPreSaveTransformedLines
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 buildEditConflictView
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 buildResolutionSuggestionView
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 buildRawTextsHiddenFields
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 setSubmittedTextCache
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace TwoColConflict;
4
5use BagOStuff;
6use IBufferingStatsdDataFactory;
7use MediaWiki\CommentFormatter\CommentFormatter;
8use MediaWiki\Content\IContentHandlerFactory;
9use MediaWiki\EditPage\TextConflictHelper;
10use MediaWiki\Html\Html;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Output\OutputPage;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15use ParserOptions;
16use TwoColConflict\Html\HtmlEditableTextComponent;
17use TwoColConflict\Html\HtmlSplitConflictHeader;
18use TwoColConflict\Html\HtmlSplitConflictView;
19use TwoColConflict\Html\HtmlTalkPageResolutionView;
20use TwoColConflict\ProvideSubmittedText\SubmittedTextCache;
21use TwoColConflict\TalkPageConflict\ResolutionSuggester;
22use TwoColConflict\TalkPageConflict\TalkPageResolution;
23use Wikimedia\Stats\StatsFactory;
24use WikitextContent;
25
26/**
27 * @license GPL-2.0-or-later
28 * @author Andrew Kostka <andrew.kostka@wikimedia.de>
29 */
30class SplitTwoColConflictHelper extends TextConflictHelper {
31
32    private TwoColConflictContext $twoColContext;
33    private ResolutionSuggester $resolutionSuggester;
34    private CommentFormatter $commentFormatter;
35    private ?SubmittedTextCache $textCache;
36    private string $newEditSummary;
37    private ?string $editFontOption;
38
39    /**
40     * @param Title $title
41     * @param OutputPage $out
42     * @param IBufferingStatsdDataFactory|StatsFactory $stats
43     * @param string $submitLabel Message key for submit button's label
44     * @param IContentHandlerFactory $contentHandlerFactory
45     * @param TwoColConflictContext $twoColContext
46     * @param ResolutionSuggester $resolutionSuggester
47     * @param CommentFormatter $commentFormatter
48     * @param BagOStuff|null $textCache
49     * @param string $newEditSummary
50     * @param string|null $editFontOption
51     */
52    public function __construct(
53        Title $title,
54        OutputPage $out,
55        $stats,
56        string $submitLabel,
57        IContentHandlerFactory $contentHandlerFactory,
58        TwoColConflictContext $twoColContext,
59        ResolutionSuggester $resolutionSuggester,
60        CommentFormatter $commentFormatter,
61        BagOStuff $textCache = null,
62        string $newEditSummary = '',
63        string $editFontOption = null
64    ) {
65        parent::__construct( $title, $out, $stats, $submitLabel, $contentHandlerFactory );
66
67        $this->twoColContext = $twoColContext;
68        $this->resolutionSuggester = $resolutionSuggester;
69        $this->commentFormatter = $commentFormatter;
70        $this->textCache = $textCache ? new SubmittedTextCache( $textCache ) : null;
71        $this->newEditSummary = $newEditSummary;
72        $this->editFontOption = $editFontOption;
73
74        $this->out->enableOOUI();
75        $this->out->addModuleStyles( [
76            'oojs-ui.styles.icons-editing-core',
77            'oojs-ui.styles.icons-editing-advanced',
78            'oojs-ui.styles.icons-movement'
79        ] );
80    }
81
82    /**
83     * @inheritDoc
84     */
85    public function incrementConflictStats( User $user = null ) {
86        parent::incrementConflictStats( $user );
87        // XXX This is copied largely from core and we may be able to refactor something here.
88        $namespace = 'n/a';
89        $userBucket = 'n/a';
90        $statsdNamespaces = [ 'TwoColConflict.conflict' ];
91        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
92        if (
93            $this->title->getNamespace() >= NS_MAIN &&
94            $this->title->getNamespace() <= NS_CATEGORY_TALK
95        ) {
96            // getNsText() returns empty string if getNamespace() === NS_MAIN
97            $namespace = $this->title->getNsText() ?: 'Main';
98            $statsdNamespaces[] = 'TwoColConflict.conflict.byNamespaceId.' . $this->title->getNamespace();
99        }
100        if ( $user ) {
101            $userBucket = $this->getUserBucket( $user->getEditCount() );
102            $statsdNamespaces[] = 'TwoColConflict.conflict.byUserEdits.' . $userBucket;
103        }
104
105        $this->stats->withComponent( 'TwoColConflict' )
106            ->getCounter( 'edit_failure_total' )
107            ->setLabel( 'namespace', $namespace )
108            ->setLabel( 'user_bucket', $userBucket )
109            ->copyToStatsdAt( $statsdNamespaces )
110            ->increment();
111    }
112
113    /**
114     * @inheritDoc
115     */
116    public function incrementResolvedStats( User $user = null ) {
117        parent::incrementResolvedStats( $user );
118        // XXX This is copied largely from core and we may be able to refactor something here.
119        $namespace = 'n/a';
120        $userBucket = 'n/a';
121        $statsdNamespaces = [ 'TwoColConflict.conflict.resolved' ];
122        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
123        if (
124            $this->title->getNamespace() >= NS_MAIN &&
125            $this->title->getNamespace() <= NS_CATEGORY_TALK
126        ) {
127            // getNsText() returns empty string if getNamespace() === NS_MAIN
128            $namespace = $this->title->getNsText() ?: 'Main';
129            $statsdNamespaces[] = 'TwoColConflict.conflict.resolved.byNamespaceId.' . $this->title->getNamespace();
130        }
131        if ( $user ) {
132            $userBucket = $this->getUserBucket( $user->getEditCount() );
133            $statsdNamespaces[] = 'TwoColConflict.conflict.resolved.byUserEdits.' . $userBucket;
134        }
135
136        $this->stats->withComponent( 'TwoColConflict' )
137            ->getCounter( 'edit_failure_resolved_total' )
138            ->setLabel( 'namespace', $namespace )
139            ->setLabel( 'user_bucket', $userBucket )
140            ->copyToStatsdAt( $statsdNamespaces )
141            ->increment();
142    }
143
144    /**
145     * Replace default header for explaining the conflict screen.
146     *
147     * @return string
148     */
149    public function getExplainHeader() {
150        // TODO
151        return '';
152    }
153
154    /**
155     * Shows the diff part in the original conflict handling. Is not
156     * used and overwritten by a simple container for the result text.
157     *
158     * @param array $customAttribs
159     *
160     * @return string HTML
161     */
162    public function getEditConflictMainTextBox( array $customAttribs = [] ) {
163        return '';
164    }
165
166    /**
167     * Shows the diff part in the original conflict handling. Is not
168     * used and overwritten.
169     */
170    public function showEditFormTextAfterFooters() {
171    }
172
173    /**
174     * Build HTML that will be added before the default edit form.
175     *
176     * @return string
177     */
178    public function getEditFormHtmlBeforeContent() {
179        $storedLines = SplitConflictUtils::splitText( $this->storedversion );
180        $yourLines = SplitConflictUtils::splitText( $this->yourtext );
181
182        $page = $this->out->getWikiPage();
183        $user = $this->out->getUser();
184        $suggestion = $this->twoColContext->shouldTalkPageSuggestionBeConsidered( $page, $user )
185            ? $this->resolutionSuggester->getResolutionSuggestion( $storedLines, $yourLines )
186            : null;
187        if ( $suggestion ) {
188            $conflictView = $this->buildResolutionSuggestionView( $suggestion );
189        } else {
190            $conflictView = $this->buildEditConflictView( $storedLines, $yourLines );
191        }
192
193        $this->setSubmittedTextCache();
194
195        return Html::hidden( 'wpTextbox1', $this->storedversion ) .
196            $conflictView .
197            $this->buildRawTextsHiddenFields();
198    }
199
200    /**
201     * Build HTML content that will be added after the default edit form.
202     *
203     * @return string
204     */
205    public function getEditFormHtmlAfterContent() {
206        $this->out->addModuleStyles( 'ext.TwoColConflict.SplitCss' );
207        $this->out->addModules( 'ext.TwoColConflict.SplitJs' );
208        return '';
209    }
210
211    private function getPreSaveTransformedLines(): array {
212        $user = $this->out->getUser();
213
214        $content = new WikitextContent( $this->yourtext );
215        $parserOptions = new ParserOptions( $user, $this->out->getLanguage() );
216        $contentTransformer = MediaWikiServices::getInstance()->getContentTransformer();
217        $content = $contentTransformer->preSaveTransform(
218            $content,
219            $this->title,
220            $user,
221            $parserOptions
222        );
223        // @phan-suppress-next-line PhanUndeclaredMethod
224        $previewWikitext = $content->getText();
225
226        return SplitConflictUtils::splitText( $previewWikitext );
227    }
228
229    /**
230     * Build HTML that will add the textbox with the unified diff.
231     *
232     * @param string[] $storedLines
233     * @param string[] $yourLines
234     *
235     * @return string
236     */
237    private function buildEditConflictView( array $storedLines, array $yourLines ): string {
238        $user = $this->out->getUser();
239        $language = $this->out->getLanguage();
240        $formatter = new AnnotatedHtmlDiffFormatter();
241        $diff = $formatter->format( $storedLines, $yourLines, $this->getPreSaveTransformedLines() );
242
243        $out = ( new HtmlSplitConflictHeader(
244            $this->title,
245            $user,
246            $this->newEditSummary,
247            $language,
248            $this->out->getContext(),
249            $this->commentFormatter
250        ) )->getHtml( $this->twoColContext->isUsedAsBetaFeature() );
251        // @phan-suppress-next-line SecurityCheck-DoubleEscaped
252        $out .= ( new HtmlSplitConflictView(
253            new HtmlEditableTextComponent(
254                $this->out->getContext(),
255                $language,
256                $this->editFontOption
257            ),
258            $this->out->getContext()
259        ) )->getHtml(
260            $diff,
261            // Note: Can't use getBool() because that discards arrays
262            (bool)$this->out->getRequest()->getArray( 'mw-twocolconflict-split-content' )
263        );
264        return $out;
265    }
266
267    private function buildResolutionSuggestionView( TalkPageResolution $suggestion ): string {
268        // @phan-suppress-next-line SecurityCheck-DoubleEscaped
269        return ( new HtmlTalkPageResolutionView(
270            new HtmlEditableTextComponent(
271                $this->out->getContext(),
272                $this->out->getLanguage(),
273                $this->editFontOption
274            ),
275            $this->out->getContext()
276        ) )->getHtml(
277            $suggestion->getDiff(),
278            $suggestion->getOtherIndex(),
279            $suggestion->getYourIndex(),
280            $this->twoColContext->isUsedAsBetaFeature()
281        );
282    }
283
284    /**
285     * Build HTML for the hidden field with the text the user submitted.
286     *
287     * @return string
288     */
289    private function buildRawTextsHiddenFields(): string {
290        return Html::textarea(
291                'mw-twocolconflict-your-text',
292                $this->yourtext,
293                [
294                    'class' => 'mw-twocolconflict-your-text',
295                    'readonly' => true,
296                    'tabindex' => '-1',
297                ]
298            );
299    }
300
301    private function setSubmittedTextCache(): void {
302        if ( $this->textCache && !$this->textCache->stashText(
303            $this->title->getPrefixedDBkey(),
304            $this->out->getUser(),
305            $this->out->getRequest()->getSessionId(),
306            $this->yourtext
307        ) ) {
308            // TODO: Log error?
309        }
310    }
311
312}