Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 118 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
TextConflictHelper | |
0.00% |
0 / 118 |
|
0.00% |
0 / 14 |
1122 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
setTextboxes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setContentModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setContentFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
incrementConflictStats | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
incrementResolvedStats | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
incrementStatsByUserEdits | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getUserBucket | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
getExplainHeader | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getEditConflictMainTextBox | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
getEditFormHtmlBeforeContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getEditFormHtmlAfterContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showEditFormTextAfterFooters | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
toEditContent | |
0.00% |
0 / 6 |
|
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 | |
21 | namespace MediaWiki\EditPage; |
22 | |
23 | use MediaWiki\Content\Content; |
24 | use MediaWiki\Content\ContentHandler; |
25 | use MediaWiki\Content\IContentHandlerFactory; |
26 | use MediaWiki\Html\Html; |
27 | use MediaWiki\Output\OutputPage; |
28 | use MediaWiki\Title\Title; |
29 | use MediaWiki\User\User; |
30 | use MWUnknownContentModelException; |
31 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
32 | use 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 | */ |
40 | class 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 | } |