MediaWiki  master
TextSlotDiffRenderer.php
Go to the documentation of this file.
1 <?php
26 
38 
40  const ENGINE_PHP = 'php';
41 
43  const ENGINE_WIKIDIFF2 = 'wikidiff2';
44 
46  const ENGINE_WIKIDIFF2_INLINE = 'wikidiff2inline';
47 
49  const ENGINE_EXTERNAL = 'external';
50 
53 
55  private $language;
56 
58  private $engine = self::ENGINE_PHP;
59 
61  private $externalEngine;
62 
67  public function getExtraCacheKeys() {
68  // Tell DifferenceEngine this is a different variant from the standard wikidiff2 variant
69  return $this->engine === self::ENGINE_WIKIDIFF2_INLINE ? [
70  phpversion( 'wikidiff2' ), 'inline'
71  ] : [];
72  }
73 
80  public static function diff( $oldText, $newText ) {
83  ->getSlotDiffRenderer( RequestContext::getMain() );
84  '@phan-var TextSlotDiffRenderer $slotDiffRenderer';
85  return $slotDiffRenderer->getTextDiff( $oldText, $newText );
86  }
87 
89  $this->statsdDataFactory = $statsdDataFactory;
90  }
91 
92  public function setLanguage( Language $language ) {
93  $this->language = $language;
94  }
95 
101  public function setEngine( $type, $executable = null ) {
102  $engines = [ self::ENGINE_PHP, self::ENGINE_WIKIDIFF2, self::ENGINE_EXTERNAL,
103  self::ENGINE_WIKIDIFF2_INLINE ];
104  Assert::parameter( in_array( $type, $engines, true ), '$type',
105  'must be one of the TextSlotDiffRenderer::ENGINE_* constants' );
106  if ( $type === self::ENGINE_EXTERNAL ) {
107  Assert::parameter( is_string( $executable ) && is_executable( $executable ), '$executable',
108  'must be a path to a valid executable' );
109  } else {
110  Assert::parameter( is_null( $executable ), '$executable',
111  'must not be set unless $type is ENGINE_EXTERNAL' );
112  }
113  $this->engine = $type;
114  $this->externalEngine = $executable;
115  }
116 
118  public function getDiff( Content $oldContent = null, Content $newContent = null ) {
119  $this->normalizeContents( $oldContent, $newContent, TextContent::class );
120 
121  $oldText = $oldContent->serialize();
122  $newText = $newContent->serialize();
123 
124  return $this->getTextDiff( $oldText, $newText );
125  }
126 
133  public function getTextDiff( $oldText, $newText ) {
134  Assert::parameterType( 'string', $oldText, '$oldText' );
135  Assert::parameterType( 'string', $newText, '$newText' );
136 
137  $diff = function () use ( $oldText, $newText ) {
138  $time = microtime( true );
139 
140  $result = $this->getTextDiffInternal( $oldText, $newText );
141 
142  $time = intval( ( microtime( true ) - $time ) * 1000 );
143  if ( $this->statsdDataFactory ) {
144  $this->statsdDataFactory->timing( 'diff_time', $time );
145  }
146 
147  // TODO reimplement this using T142313
148  /*
149  // Log requests slower than 99th percentile
150  if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
151  wfDebugLog( 'diff',
152  "$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
153  }
154  */
155 
156  return $result;
157  };
158 
163  $error = function ( $status ) {
164  throw new FatalError( $status->getWikiText() );
165  };
166 
167  // Use PoolCounter if the diff looks like it can be expensive
168  if ( strlen( $oldText ) + strlen( $newText ) > 20000 ) {
169  $work = new PoolCounterWorkViaCallback( 'diff',
170  md5( $oldText ) . md5( $newText ),
171  [ 'doWork' => $diff, 'error' => $error ]
172  );
173  return $work->execute();
174  }
175 
176  return $diff();
177  }
178 
187  protected function getTextDiffInternal( $oldText, $newText ) {
188  // TODO move most of this into three parallel implementations of a text diff generator
189  // class, choose which one to use via dependecy injection
190 
191  $oldText = str_replace( "\r\n", "\n", $oldText );
192  $newText = str_replace( "\r\n", "\n", $newText );
193 
194  // Better external diff engine, the 2 may some day be dropped
195  // This one does the escaping and segmenting itself
196  if ( $this->engine === self::ENGINE_WIKIDIFF2 ) {
197  $wikidiff2Version = phpversion( 'wikidiff2' );
198  if (
199  $wikidiff2Version !== false &&
200  version_compare( $wikidiff2Version, '1.5.0', '>=' ) &&
201  version_compare( $wikidiff2Version, '1.8.0', '<' )
202  ) {
203  $text = wikidiff2_do_diff(
204  $oldText,
205  $newText,
206  2,
207  0
208  );
209  } else {
210  // Don't pass the 4th parameter introduced in version 1.5.0 and removed in version 1.8.0
211  $text = wikidiff2_do_diff(
212  $oldText,
213  $newText,
214  2
215  );
216  }
217 
218  return $text;
219  } elseif ( $this->engine === self::ENGINE_EXTERNAL ) {
220  # Diff via the shell
221  $tmpDir = wfTempDir();
222  $tempName1 = tempnam( $tmpDir, 'diff_' );
223  $tempName2 = tempnam( $tmpDir, 'diff_' );
224 
225  $tempFile1 = fopen( $tempName1, "w" );
226  if ( !$tempFile1 ) {
227  return false;
228  }
229  $tempFile2 = fopen( $tempName2, "w" );
230  if ( !$tempFile2 ) {
231  return false;
232  }
233  fwrite( $tempFile1, $oldText );
234  fwrite( $tempFile2, $newText );
235  fclose( $tempFile1 );
236  fclose( $tempFile2 );
237  $cmd = [ $this->externalEngine, $tempName1, $tempName2 ];
238  $result = Shell::command( $cmd )
239  ->execute();
240  $exitCode = $result->getExitCode();
241  if ( $exitCode !== 0 ) {
242  throw new Exception( "External diff command returned code {$exitCode}. Stderr: "
243  . wfEscapeWikiText( $result->getStderr() )
244  );
245  }
246  $difftext = $result->getStdout();
247  unlink( $tempName1 );
248  unlink( $tempName2 );
249 
250  return $difftext;
251  } elseif ( $this->engine === self::ENGINE_PHP ) {
252  if ( $this->language ) {
253  $oldText = $this->language->segmentForDiff( $oldText );
254  $newText = $this->language->segmentForDiff( $newText );
255  }
256  $ota = explode( "\n", $oldText );
257  $nta = explode( "\n", $newText );
258  $diffs = new Diff( $ota, $nta );
259  $formatter = new TableDiffFormatter();
260  $difftext = $formatter->format( $diffs );
261  if ( $this->language ) {
262  $difftext = $this->language->unsegmentForDiff( $difftext );
263  }
264 
265  return $difftext;
266  } elseif ( $this->engine === self::ENGINE_WIKIDIFF2_INLINE ) {
267  // Note wikidiff2_inline_diff returns an element sans table.
268  // Due to the way other diffs work (return a table with before and after), we need to wrap
269  // the output in a row that spans the 4 columns that are expected, so that our diff appears in
270  // the correct place!
271  return '<tr><td colspan="4">' . wikidiff2_inline_diff( $oldText, $newText, 2 ) . '</td></tr>';
272  }
273  throw new LogicException( 'Invalid engine: ' . $this->engine );
274  }
275 
276 }
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
setEngine( $type, $executable=null)
Set which diff engine to use.
string $externalEngine
Path to an executable to be used as the diff engine.
setLanguage(Language $language)
getTextDiff( $oldText, $newText)
Diff the text representations of two content objects (or just two pieces of text in general)...
const CONTENT_MODEL_TEXT
Definition: Defines.php:218
normalizeContents(Content &$oldContent=null, Content &$newContent=null, $allowedClasses=null)
Helper method to normalize the input of getDiff().
getTextDiffInternal( $oldText, $newText)
Diff the text representations of two content objects (or just two pieces of text in general)...
MediaWiki default table style diff formatter.
Convenience class for dealing with PoolCounters using callbacks.
wfTempDir()
Tries to get the system directory for temporary files.
static getMain()
Get the RequestContext object associated with the main request.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Renders a slot diff by doing a text diff on the native representation.
Language null $language
The language this content is in.
Renders a diff for a single slot (that is, a diff between two content objects).
Class representing a &#39;diff&#39; between two sequences of strings.
Definition: Diff.php:32
string $engine
One of the ENGINE_* constants.
static diff( $oldText, $newText)
Convenience helper to use getTextDiff without an instance.
const ENGINE_PHP
Use the PHP diff implementation (DiffEngine).
Abort the web request with a custom HTML string that will represent the entire response.
Definition: FatalError.php:35
const ENGINE_WIKIDIFF2
Use the wikidiff2 PHP module.
getDiff(Content $oldContent=null, Content $newContent=null)
setStatsdDataFactory(IBufferingStatsdDataFactory $statsdDataFactory)
const ENGINE_EXTERNAL
Use an external executable.
const ENGINE_WIKIDIFF2_INLINE
Use the wikidiff2 PHP module.
IBufferingStatsdDataFactory null $statsdDataFactory