Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
76.53% |
75 / 98 |
|
61.11% |
11 / 18 |
CRAP | |
0.00% |
0 / 1 |
TextSlotDiffRenderer | |
76.53% |
75 / 98 |
|
61.11% |
11 / 18 |
47.08 | |
0.00% |
0 / 1 |
getExtraCacheKeys | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
diff | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setStatsdDataFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setStatsFactory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setHookContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setEngine | |
61.90% |
13 / 21 |
|
0.00% |
0 / 1 |
11.54 | |||
setFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTextDiffer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTextDiffer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setInlineToggleEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDiff | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
localizeDiff | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTablePrefix | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
6 | |||
getTextDiff | |
70.00% |
14 / 20 |
|
0.00% |
0 / 1 |
3.24 | |||
getTextDiffInternal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Renders a slot diff by doing a text diff on the native representation. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup DifferenceEngine |
22 | */ |
23 | |
24 | use MediaWiki\Content\Content; |
25 | use MediaWiki\Content\TextContent; |
26 | use MediaWiki\Context\IContextSource; |
27 | use MediaWiki\Context\RequestContext; |
28 | use MediaWiki\Diff\TextDiffer\ManifoldTextDiffer; |
29 | use MediaWiki\Diff\TextDiffer\TextDiffer; |
30 | use MediaWiki\Exception\FatalError; |
31 | use MediaWiki\HookContainer\HookContainer; |
32 | use MediaWiki\HookContainer\HookRunner; |
33 | use MediaWiki\Html\Html; |
34 | use MediaWiki\Language\Language; |
35 | use MediaWiki\MediaWikiServices; |
36 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\Title\Title; |
39 | use OOUI\ToggleSwitchWidget; |
40 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
41 | use Wikimedia\Stats\StatsFactory; |
42 | |
43 | /** |
44 | * Renders a slot diff by doing a text diff on the native representation. |
45 | * |
46 | * If you want to use this without content objects (to call getTextDiff() on some |
47 | * non-content-related texts), obtain an instance with |
48 | * ContentHandler::getForModelID( CONTENT_MODEL_TEXT ) |
49 | * ->getSlotDiffRenderer( RequestContext::getMain() ) |
50 | * |
51 | * @ingroup DifferenceEngine |
52 | */ |
53 | class TextSlotDiffRenderer extends SlotDiffRenderer { |
54 | |
55 | /** Use the PHP diff implementation (DiffEngine). */ |
56 | public const ENGINE_PHP = 'php'; |
57 | |
58 | /** Use the wikidiff2 PHP module. */ |
59 | public const ENGINE_WIKIDIFF2 = 'wikidiff2'; |
60 | |
61 | /** Use the wikidiff2 PHP module. */ |
62 | public const ENGINE_WIKIDIFF2_INLINE = 'wikidiff2inline'; |
63 | |
64 | /** Use an external executable. */ |
65 | public const ENGINE_EXTERNAL = 'external'; |
66 | |
67 | public const INLINE_LEGEND_KEY = '10_mw-diff-inline-legend'; |
68 | |
69 | public const INLINE_SWITCHER_KEY = '60_mw-diff-inline-switch'; |
70 | |
71 | /** @var StatsFactory|null */ |
72 | private $statsFactory; |
73 | |
74 | /** @var HookRunner|null */ |
75 | private $hookRunner; |
76 | |
77 | /** @var string|null */ |
78 | private $format; |
79 | |
80 | /** @var string */ |
81 | private $contentModel; |
82 | |
83 | /** @var TextDiffer|null */ |
84 | private $textDiffer; |
85 | |
86 | /** @var bool */ |
87 | private $inlineToggleEnabled = false; |
88 | |
89 | /** @inheritDoc */ |
90 | public function getExtraCacheKeys() { |
91 | return $this->textDiffer->getCacheKeys( [ $this->format ] ); |
92 | } |
93 | |
94 | /** |
95 | * Convenience helper to use getTextDiff without an instance. |
96 | * @param string $oldText |
97 | * @param string $newText |
98 | * @param array $options |
99 | * @return string |
100 | */ |
101 | public static function diff( $oldText, $newText, $options = [] ) { |
102 | /** @var TextSlotDiffRenderer $slotDiffRenderer */ |
103 | $slotDiffRenderer = MediaWikiServices::getInstance() |
104 | ->getContentHandlerFactory() |
105 | ->getContentHandler( CONTENT_MODEL_TEXT ) |
106 | ->getSlotDiffRenderer( RequestContext::getMain(), $options ); |
107 | '@phan-var TextSlotDiffRenderer $slotDiffRenderer'; |
108 | return $slotDiffRenderer->getTextDiff( $oldText, $newText ); |
109 | } |
110 | |
111 | /** |
112 | * This has no effect since MW 1.43. |
113 | * |
114 | * @internal Use ContentHandler::createTextSlotDiffRenderer instead |
115 | * @param IBufferingStatsdDataFactory $statsdDataFactory |
116 | */ |
117 | public function setStatsdDataFactory( IBufferingStatsdDataFactory $statsdDataFactory ) { |
118 | wfDeprecated( __METHOD__, '1.43' ); |
119 | } |
120 | |
121 | /** |
122 | * @internal Use ContentHandler::createTextSlotDiffRenderer instead |
123 | * @param StatsFactory $statsFactory |
124 | */ |
125 | public function setStatsFactory( StatsFactory $statsFactory ) { |
126 | $this->statsFactory = $statsFactory; |
127 | } |
128 | |
129 | /** |
130 | * This has no effect since MW 1.41. The language is now injected via setTextDiffer(). |
131 | * |
132 | * @param Language $language |
133 | * @deprecated since 1.41 |
134 | */ |
135 | public function setLanguage( Language $language ) { |
136 | wfDeprecated( __METHOD__, '1.41' ); |
137 | } |
138 | |
139 | /** |
140 | * @internal Use ContentHandler::createTextSlotDiffRenderer instead |
141 | * @since 1.41 |
142 | * @param HookContainer $hookContainer |
143 | */ |
144 | public function setHookContainer( HookContainer $hookContainer ): void { |
145 | $this->hookRunner = new HookRunner( $hookContainer ); |
146 | } |
147 | |
148 | /** |
149 | * @param string $contentModel |
150 | * @since 1.41 |
151 | */ |
152 | public function setContentModel( string $contentModel ) { |
153 | $this->contentModel = $contentModel; |
154 | } |
155 | |
156 | /** |
157 | * Set which diff engine to use. |
158 | * |
159 | * @param string $type One of the ENGINE_* constants. |
160 | * @param null $executable Must be null since 1.41. Previously a path to execute. |
161 | */ |
162 | public function setEngine( $type, $executable = null ) { |
163 | if ( $executable !== null ) { |
164 | throw new \InvalidArgumentException( |
165 | 'The $executable parameter is no longer supported and must be null' |
166 | ); |
167 | } |
168 | switch ( $type ) { |
169 | case self::ENGINE_PHP: |
170 | $engine = 'php'; |
171 | $format = 'table'; |
172 | break; |
173 | |
174 | case self::ENGINE_WIKIDIFF2: |
175 | $engine = 'wikidiff2'; |
176 | $format = 'table'; |
177 | break; |
178 | |
179 | case self::ENGINE_EXTERNAL: |
180 | $engine = 'external'; |
181 | $format = 'external'; |
182 | break; |
183 | |
184 | case self::ENGINE_WIKIDIFF2_INLINE: |
185 | $engine = 'wikidiff2'; |
186 | $format = 'inline'; |
187 | break; |
188 | |
189 | default: |
190 | throw new \InvalidArgumentException( '$type ' . |
191 | 'must be one of the TextSlotDiffRenderer::ENGINE_* constants' ); |
192 | } |
193 | if ( $this->textDiffer instanceof ManifoldTextDiffer ) { |
194 | $this->textDiffer->setEngine( $engine ); |
195 | } |
196 | $this->setFormat( $format ); |
197 | } |
198 | |
199 | /** |
200 | * Set the TextDiffer format |
201 | * |
202 | * @since 1.41 |
203 | * @param string $format |
204 | */ |
205 | public function setFormat( $format ) { |
206 | $this->format = $format; |
207 | } |
208 | |
209 | public function setTextDiffer( TextDiffer $textDiffer ) { |
210 | $this->textDiffer = $textDiffer; |
211 | } |
212 | |
213 | /** |
214 | * Get the current TextDiffer, or throw an exception if setTextDiffer() has |
215 | * not been called. |
216 | */ |
217 | private function getTextDiffer(): TextDiffer { |
218 | return $this->textDiffer; |
219 | } |
220 | |
221 | /** |
222 | * Set a flag indicating whether the inline toggle switch is shown. |
223 | * |
224 | * @since 1.41 |
225 | * @param bool $enabled |
226 | */ |
227 | public function setInlineToggleEnabled( $enabled = true ) { |
228 | $this->inlineToggleEnabled = $enabled; |
229 | } |
230 | |
231 | /** |
232 | * Get the content model ID that this renderer acts on |
233 | * |
234 | * @since 1.41 |
235 | * @return string |
236 | */ |
237 | public function getContentModel(): string { |
238 | return $this->contentModel; |
239 | } |
240 | |
241 | /** @inheritDoc */ |
242 | public function getDiff( ?Content $oldContent = null, ?Content $newContent = null ) { |
243 | $this->normalizeContents( $oldContent, $newContent, TextContent::class ); |
244 | |
245 | $oldText = $oldContent->serialize(); |
246 | $newText = $newContent->serialize(); |
247 | |
248 | return $this->getTextDiff( $oldText, $newText ); |
249 | } |
250 | |
251 | public function localizeDiff( $diff, $options = [] ) { |
252 | return $this->textDiffer->localize( $this->format, $diff, $options ); |
253 | } |
254 | |
255 | /** |
256 | * @inheritDoc |
257 | */ |
258 | public function getTablePrefix( IContextSource $context, Title $newTitle ): array { |
259 | $parts = $this->getTextDiffer()->getTablePrefixes( $this->format ); |
260 | |
261 | $showDiffToggleSwitch = $this->inlineToggleEnabled && $this->getTextDiffer()->hasFormat( 'inline' ); |
262 | // If we support the inline type, add a toggle switch |
263 | if ( $showDiffToggleSwitch ) { |
264 | $values = $context->getRequest()->getQueryValues(); |
265 | $isInlineDiffType = $this->format === 'inline'; |
266 | $values[ 'diff-type' ] = $isInlineDiffType ? 'table' : 'inline'; |
267 | unset( $values[ 'title' ] ); |
268 | $parts[self::INLINE_SWITCHER_KEY] = Html::rawElement( 'div', |
269 | [ 'class' => 'mw-diffPage-inlineToggle-container' ], |
270 | new OOUI\FieldLayout( |
271 | new ToggleSwitchWidget( [ |
272 | 'id' => 'mw-diffPage-inline-toggle-switch', |
273 | 'href' => $newTitle->getLocalURL( $values ), |
274 | 'value' => $isInlineDiffType, |
275 | 'title' => $context->msg( 'diff-inline-switch-desc' )->plain() |
276 | ] ), |
277 | [ |
278 | 'id' => 'mw-diffPage-inline-toggle-switch-layout', |
279 | 'label' => $context->msg( 'diff-inline-format-label' )->plain(), |
280 | 'infusable' => true, |
281 | 'title' => $context->msg( 'diff-inline-switch-desc' )->plain() |
282 | ] |
283 | ), |
284 | ); |
285 | } |
286 | // Add an empty placeholder for the legend is added when it's not in |
287 | // use and other items have been added. |
288 | $parts += [ self::INLINE_LEGEND_KEY => null, self::INLINE_SWITCHER_KEY => null ]; |
289 | |
290 | // Allow extensions to add other parts to this area (or modify the legend). |
291 | $this->hookRunner->onTextSlotDiffRendererTablePrefix( $this, $context, $parts ); |
292 | if ( count( $parts ) > 1 && $parts[self::INLINE_LEGEND_KEY] === null ) { |
293 | $parts[self::INLINE_LEGEND_KEY] = Html::element( 'div' ); |
294 | } |
295 | return $parts; |
296 | } |
297 | |
298 | /** |
299 | * Diff the text representations of two content objects (or just two pieces of text in general). |
300 | * @param string $oldText |
301 | * @param string $newText |
302 | * @return string HTML. One or more <tr> tags, or an empty string if the inputs are identical. |
303 | */ |
304 | public function getTextDiff( string $oldText, string $newText ) { |
305 | $diff = function () use ( $oldText, $newText ) { |
306 | $time = microtime( true ); |
307 | |
308 | $result = $this->getTextDiffInternal( $oldText, $newText ); |
309 | |
310 | $time = intval( ( microtime( true ) - $time ) * 1000 ); |
311 | |
312 | if ( $this->statsFactory ) { |
313 | $this->statsFactory->getTiming( 'diff_text_seconds' ) |
314 | ->copyToStatsdAt( 'diff_time' ) |
315 | ->observe( $time ); |
316 | } |
317 | |
318 | return $result; |
319 | }; |
320 | |
321 | /** |
322 | * @param Status $status |
323 | * @throws FatalError |
324 | * @return never |
325 | */ |
326 | $error = static function ( $status ) { |
327 | throw new FatalError( $status->getWikiText() ); |
328 | }; |
329 | |
330 | // Use PoolCounter if the diff looks like it can be expensive |
331 | if ( strlen( $oldText ) + strlen( $newText ) > 20000 ) { |
332 | $work = new PoolCounterWorkViaCallback( 'diff', |
333 | md5( $oldText ) . md5( $newText ), |
334 | [ 'doWork' => $diff, 'error' => $error ] |
335 | ); |
336 | return $work->execute(); |
337 | } |
338 | |
339 | return $diff(); |
340 | } |
341 | |
342 | /** |
343 | * Diff the text representations of two content objects (or just two pieces of text in general). |
344 | * This does the actual diffing, getTextDiff() wraps it with logging and resource limiting. |
345 | * @param string $oldText |
346 | * @param string $newText |
347 | * @return string |
348 | * @throws Exception |
349 | */ |
350 | protected function getTextDiffInternal( $oldText, $newText ) { |
351 | $oldText = str_replace( "\r\n", "\n", $oldText ); |
352 | $newText = str_replace( "\r\n", "\n", $newText ); |
353 | |
354 | if ( $oldText === $newText ) { |
355 | return ''; |
356 | } |
357 | |
358 | $textDiffer = $this->getTextDiffer(); |
359 | $diffText = $textDiffer->render( $oldText, $newText, $this->format ); |
360 | return $textDiffer->addRowWrapper( $this->format, $diffText ); |
361 | } |
362 | |
363 | } |