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