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