Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 118
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextConflictHelper
0.00% covered (danger)
0.00%
0 / 118
0.00% covered (danger)
0.00%
0 / 14
1122
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 setTextboxes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setContentModel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setContentFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 incrementConflictStats
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 incrementResolvedStats
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 incrementStatsByUserEdits
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getUserBucket
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getExplainHeader
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getEditConflictMainTextBox
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 getEditFormHtmlBeforeContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEditFormHtmlAfterContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showEditFormTextAfterFooters
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 toEditContent
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\EditPage;
22
23use Content;
24use ContentHandler;
25use IBufferingStatsdDataFactory;
26use MediaWiki\Content\IContentHandlerFactory;
27use MediaWiki\Html\Html;
28use MediaWiki\Output\OutputPage;
29use MediaWiki\Title\Title;
30use MediaWiki\User\User;
31use MWUnknownContentModelException;
32use Wikimedia\Stats\StatsFactory;
33
34/**
35 * Helper for displaying edit conflicts in text content models to users
36 *
37 * @since 1.31
38 * @author Kunal Mehta <legoktm@debian.org>
39 */
40class TextConflictHelper {
41
42    /**
43     * @var Title
44     */
45    protected $title;
46
47    /**
48     * @var null|string
49     */
50    public $contentModel;
51
52    /**
53     * @var null|string
54     */
55    public $contentFormat;
56
57    /**
58     * @var OutputPage
59     */
60    protected $out;
61
62    /**
63     * @var IBufferingStatsdDataFactory|StatsFactory
64     */
65    protected $stats;
66
67    /**
68     * @var string Message key for submit button's label
69     */
70    protected $submitLabel;
71
72    /**
73     * @var string
74     */
75    protected $yourtext = '';
76
77    /**
78     * @var string
79     */
80    protected $storedversion = '';
81
82    /**
83     * @var IContentHandlerFactory
84     */
85    private $contentHandlerFactory;
86
87    /**
88     * @param Title $title
89     * @param OutputPage $out
90     * @param IBufferingStatsdDataFactory|StatsFactory $stats
91     * @param string $submitLabel
92     * @param IContentHandlerFactory $contentHandlerFactory Required param with legacy support
93     *
94     * @throws MWUnknownContentModelException
95     */
96    public function __construct(
97        Title $title, OutputPage $out, $stats, $submitLabel,
98        IContentHandlerFactory $contentHandlerFactory
99    ) {
100        $this->title = $title;
101        $this->out = $out;
102        $this->stats = $stats;
103        $this->submitLabel = $submitLabel;
104        $this->contentModel = $title->getContentModel();
105        $this->contentHandlerFactory = $contentHandlerFactory;
106
107        $this->contentFormat = $this->contentHandlerFactory
108            ->getContentHandler( $this->contentModel )
109            ->getDefaultFormat();
110    }
111
112    /**
113     * @param string $yourtext
114     * @param string $storedversion
115     */
116    public function setTextboxes( $yourtext, $storedversion ) {
117        $this->yourtext = $yourtext;
118        $this->storedversion = $storedversion;
119    }
120
121    /**
122     * @param string $contentModel
123     */
124    public function setContentModel( $contentModel ) {
125        $this->contentModel = $contentModel;
126    }
127
128    /**
129     * @param string $contentFormat
130     */
131    public function setContentFormat( $contentFormat ) {
132        $this->contentFormat = $contentFormat;
133    }
134
135    /**
136     * Record a user encountering an edit conflict
137     * @param User|null $user
138     */
139    public function incrementConflictStats( User $user = null ) {
140        $namespace = 'n/a';
141        $userBucket = 'n/a';
142        $statsdMetrics = [ 'edit.failures.conflict' ];
143
144        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
145        if (
146            $this->title->getNamespace() >= NS_MAIN &&
147            $this->title->getNamespace() <= NS_CATEGORY_TALK
148        ) {
149            // getNsText() returns empty string if getNamespace() === NS_MAIN
150            $namespace = $this->title->getNsText() ?: 'Main';
151            $statsdMetrics[] = 'edit.failures.conflict.byNamespaceId.' . $this->title->getNamespace();
152        }
153        if ( $user ) {
154            $userBucket = $this->getUserBucket( $user->getEditCount() );
155            $statsdMetrics[] = 'edit.failures.conflict.byUserEdits.' . $userBucket;
156        }
157        if ( $this->stats instanceof StatsFactory ) {
158            $this->stats->getCounter( 'edit_failure_total' )
159                ->setLabel( 'cause', 'conflict' )
160                ->setLabel( 'namespace', $namespace )
161                ->setLabel( 'user_bucket', $userBucket )
162                ->copyToStatsdAt( $statsdMetrics )
163                ->increment();
164        }
165
166        if ( $this->stats instanceof IBufferingStatsdDataFactory ) {
167            foreach ( $statsdMetrics as $metric ) {
168                $this->stats->increment( $metric );
169            }
170        }
171    }
172
173    /**
174     * Record when a user has resolved an edit conflict
175     * @param User|null $user
176     */
177    public function incrementResolvedStats( User $user = null ) {
178        $namespace = 'n/a';
179        $userBucket = 'n/a';
180        $statsdMetrics = [ 'edit.failures.conflict.resolved' ];
181
182        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
183        if (
184            $this->title->getNamespace() >= NS_MAIN &&
185            $this->title->getNamespace() <= NS_CATEGORY_TALK
186        ) {
187            // getNsText() returns empty string if getNamespace() === NS_MAIN
188            $namespace = $this->title->getNsText() ?: 'Main';
189            $statsdMetrics[] = 'edit.failures.conflict.resolved.byNamespaceId.' . $this->title->getNamespace();
190        }
191
192        if ( $user ) {
193            $userBucket = $this->getUserBucket( $user->getEditCount() );
194            $statsdMetrics[] = 'edit.failures.conflict.resolved.byUserEdits.' . $userBucket;
195        }
196
197        if ( $this->stats instanceof StatsFactory ) {
198            $this->stats->getCounter( 'edit_failure_resolved_total' )
199                ->setLabel( 'cause', 'conflict' )
200                ->setLabel( 'namespace', $namespace )
201                ->setLabel( 'user_bucket', $userBucket )
202                ->copyToStatsdAt( $statsdMetrics )
203                ->increment();
204        }
205
206        if ( $this->stats instanceof IBufferingStatsdDataFactory ) {
207            foreach ( $statsdMetrics as $metric ) {
208                $this->stats->increment( $metric );
209            }
210        }
211    }
212
213    /**
214     * Retained temporarily for backwards-compatibility.
215     *
216     * This action should be moved into incrementConflictStats, incrementResolvedStats.
217     *
218     * @deprecated since 1.42, do not use
219     * @param int|null $userEdits
220     * @param string $keyPrefixBase
221     */
222    protected function incrementStatsByUserEdits( $userEdits, $keyPrefixBase ) {
223        if ( $this->stats instanceof IBufferingStatsdDataFactory ) {
224            $this->stats->increment( $keyPrefixBase . '.byUserEdits.' . $this->getUserBucket( $userEdits ) );
225        }
226    }
227
228    /**
229     * @param int|null $userEdits
230     * @return string
231     */
232    protected function getUserBucket( ?int $userEdits ): string {
233        if ( $userEdits === null ) {
234            return 'anon';
235        } elseif ( $userEdits > 200 ) {
236            return 'over200';
237        } elseif ( $userEdits > 100 ) {
238            return 'over100';
239        } elseif ( $userEdits > 10 ) {
240            return 'over10';
241        } else {
242            return 'under11';
243        }
244    }
245
246    /**
247     * @return string HTML
248     */
249    public function getExplainHeader() {
250        return Html::rawElement(
251            'div',
252            [ 'class' => 'mw-explainconflict' ],
253            $this->out->msg( 'explainconflict', $this->out->msg( $this->submitLabel )->text() )->parse()
254        );
255    }
256
257    /**
258     * HTML to build the textbox1 on edit conflicts
259     *
260     * @param array $customAttribs
261     * @return string HTML
262     */
263    public function getEditConflictMainTextBox( array $customAttribs = [] ) {
264        $builder = new TextboxBuilder();
265        $classes = $builder->getTextboxProtectionCSSClasses( $this->title );
266
267        $attribs = [
268            'aria-label' => $this->out->msg( 'edit-textarea-aria-label' )->text(),
269            'tabindex' => 1,
270        ];
271        $attribs += $customAttribs;
272
273        $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
274
275        $attribs = $builder->buildTextboxAttribs(
276            'wpTextbox1',
277            $attribs,
278            $this->out->getUser(),
279            $this->title
280        );
281
282        return Html::textarea(
283            'wpTextbox1',
284            $builder->addNewLineAtEnd( $this->storedversion ),
285            $attribs
286        );
287    }
288
289    /**
290     * Content to go in the edit form before textbox1
291     *
292     * @see EditPage::$editFormTextBeforeContent
293     * @return string HTML
294     */
295    public function getEditFormHtmlBeforeContent() {
296        return '';
297    }
298
299    /**
300     * Content to go in the edit form after textbox1
301     *
302     * @see EditPage::$editFormTextAfterContent
303     * @return string HTML
304     */
305    public function getEditFormHtmlAfterContent() {
306        return '';
307    }
308
309    /**
310     * Content to go in the edit form after the footers
311     * (templates on this page, hidden categories, limit report)
312     */
313    public function showEditFormTextAfterFooters() {
314        $this->out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
315
316        $yourContent = $this->toEditContent( $this->yourtext );
317        $storedContent = $this->toEditContent( $this->storedversion );
318        $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
319        $diffEngine = $handler->createDifferenceEngine( $this->out );
320
321        $diffEngine->setContent( $yourContent, $storedContent );
322        $diffEngine->showDiff(
323            $this->out->msg( 'yourtext' )->parse(),
324            $this->out->msg( 'storedversion' )->text()
325        );
326
327        $this->out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
328
329        $builder = new TextboxBuilder();
330        $attribs = $builder->buildTextboxAttribs(
331            'wpTextbox2',
332            [ 'tabindex' => 6, 'readonly' ],
333            $this->out->getUser(),
334            $this->title
335        );
336
337        $this->out->addHTML(
338            Html::textarea( 'wpTextbox2', $builder->addNewLineAtEnd( $this->yourtext ), $attribs )
339        );
340    }
341
342    /**
343     * @param string $text
344     * @return Content
345     */
346    private function toEditContent( $text ) {
347        return ContentHandler::makeContent(
348            $text,
349            $this->title,
350            $this->contentModel,
351            $this->contentFormat
352        );
353    }
354}