Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
62.60% |
82 / 131 |
|
69.23% |
9 / 13 |
CRAP | |
0.00% |
0 / 1 |
SplitTwoColConflictHelper | |
62.60% |
82 / 131 |
|
69.23% |
9 / 13 |
61.38 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
incrementConflictStats | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
incrementResolvedStats | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getExplainHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditConflictMainTextBox | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showEditFormTextAfterFooters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditFormHtmlBeforeContent | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
3.03 | |||
getEditFormHtmlAfterContent | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getPreSaveTransformedLines | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
buildEditConflictView | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
1 | |||
buildResolutionSuggestionView | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
buildRawTextsHiddenFields | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
setSubmittedTextCache | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace TwoColConflict; |
4 | |
5 | use MediaWiki\CommentFormatter\CommentFormatter; |
6 | use MediaWiki\Content\IContentHandlerFactory; |
7 | use MediaWiki\Content\WikitextContent; |
8 | use MediaWiki\EditPage\TextConflictHelper; |
9 | use MediaWiki\Html\Html; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Output\OutputPage; |
12 | use MediaWiki\Parser\ParserOptions; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\User\User; |
15 | use TwoColConflict\Html\HtmlEditableTextComponent; |
16 | use TwoColConflict\Html\HtmlSplitConflictHeader; |
17 | use TwoColConflict\Html\HtmlSplitConflictView; |
18 | use TwoColConflict\Html\HtmlTalkPageResolutionView; |
19 | use TwoColConflict\ProvideSubmittedText\SubmittedTextCache; |
20 | use TwoColConflict\TalkPageConflict\ResolutionSuggester; |
21 | use TwoColConflict\TalkPageConflict\TalkPageResolution; |
22 | use Wikimedia\ObjectCache\BagOStuff; |
23 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
24 | use Wikimedia\Stats\StatsFactory; |
25 | |
26 | /** |
27 | * @license GPL-2.0-or-later |
28 | * @author Andrew Kostka <andrew.kostka@wikimedia.de> |
29 | */ |
30 | class 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 | $out .= ( new HtmlSplitConflictView( |
252 | new HtmlEditableTextComponent( |
253 | $this->out->getContext(), |
254 | $language, |
255 | $this->editFontOption |
256 | ), |
257 | $this->out->getContext() |
258 | ) )->getHtml( |
259 | $diff, |
260 | // Note: Can't use getBool() because that discards arrays |
261 | (bool)$this->out->getRequest()->getArray( 'mw-twocolconflict-split-content' ) |
262 | ); |
263 | return $out; |
264 | } |
265 | |
266 | private function buildResolutionSuggestionView( TalkPageResolution $suggestion ): string { |
267 | return ( new HtmlTalkPageResolutionView( |
268 | new HtmlEditableTextComponent( |
269 | $this->out->getContext(), |
270 | $this->out->getLanguage(), |
271 | $this->editFontOption |
272 | ), |
273 | $this->out->getContext() |
274 | ) )->getHtml( |
275 | $suggestion->getDiff(), |
276 | $suggestion->getOtherIndex(), |
277 | $suggestion->getYourIndex(), |
278 | $this->twoColContext->isUsedAsBetaFeature() |
279 | ); |
280 | } |
281 | |
282 | /** |
283 | * Build HTML for the hidden field with the text the user submitted. |
284 | * |
285 | * @return string |
286 | */ |
287 | private function buildRawTextsHiddenFields(): string { |
288 | return Html::textarea( |
289 | 'mw-twocolconflict-your-text', |
290 | $this->yourtext, |
291 | [ |
292 | 'class' => 'mw-twocolconflict-your-text', |
293 | 'readonly' => true, |
294 | 'tabindex' => '-1', |
295 | ] |
296 | ); |
297 | } |
298 | |
299 | private function setSubmittedTextCache(): void { |
300 | if ( $this->textCache && !$this->textCache->stashText( |
301 | $this->title->getPrefixedDBkey(), |
302 | $this->out->getUser(), |
303 | $this->out->getRequest()->getSessionId(), |
304 | $this->yourtext |
305 | ) ) { |
306 | // TODO: Log error? |
307 | } |
308 | } |
309 | |
310 | } |