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_EXTERNAL = 'external';
47 
50 
52  private $language;
53 
55  private $engine = self::ENGINE_PHP;
56 
58  private $externalEngine;
59 
66  public static function diff( $oldText, $newText ) {
69  ->getSlotDiffRenderer( RequestContext::getMain() );
70  '@phan-var TextSlotDiffRenderer $slotDiffRenderer';
71  return $slotDiffRenderer->getTextDiff( $oldText, $newText );
72  }
73 
75  $this->statsdDataFactory = $statsdDataFactory;
76  }
77 
78  public function setLanguage( Language $language ) {
79  $this->language = $language;
80  }
81 
87  public function setEngine( $type, $executable = null ) {
88  $engines = [ self::ENGINE_PHP, self::ENGINE_WIKIDIFF2, self::ENGINE_EXTERNAL ];
89  Assert::parameter( in_array( $type, $engines, true ), '$type',
90  'must be one of the TextSlotDiffRenderer::ENGINE_* constants' );
91  if ( $type === self::ENGINE_EXTERNAL ) {
92  Assert::parameter( is_string( $executable ) && is_executable( $executable ), '$executable',
93  'must be a path to a valid executable' );
94  } else {
95  Assert::parameter( is_null( $executable ), '$executable',
96  'must not be set unless $type is ENGINE_EXTERNAL' );
97  }
98  $this->engine = $type;
99  $this->externalEngine = $executable;
100  }
101 
103  public function getDiff( Content $oldContent = null, Content $newContent = null ) {
104  $this->normalizeContents( $oldContent, $newContent, TextContent::class );
105 
106  $oldText = $oldContent->serialize();
107  $newText = $newContent->serialize();
108 
109  return $this->getTextDiff( $oldText, $newText );
110  }
111 
118  public function getTextDiff( $oldText, $newText ) {
119  Assert::parameterType( 'string', $oldText, '$oldText' );
120  Assert::parameterType( 'string', $newText, '$newText' );
121 
122  $diff = function () use ( $oldText, $newText ) {
123  $time = microtime( true );
124 
125  $result = $this->getTextDiffInternal( $oldText, $newText );
126 
127  $time = intval( ( microtime( true ) - $time ) * 1000 );
128  if ( $this->statsdDataFactory ) {
129  $this->statsdDataFactory->timing( 'diff_time', $time );
130  }
131 
132  // TODO reimplement this using T142313
133  /*
134  // Log requests slower than 99th percentile
135  if ( $time > 100 && $this->mOldPage && $this->mNewPage ) {
136  wfDebugLog( 'diff',
137  "$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" );
138  }
139  */
140 
141  return $result;
142  };
143 
148  $error = function ( $status ) {
149  throw new FatalError( $status->getWikiText() );
150  };
151 
152  // Use PoolCounter if the diff looks like it can be expensive
153  if ( strlen( $oldText ) + strlen( $newText ) > 20000 ) {
154  $work = new PoolCounterWorkViaCallback( 'diff',
155  md5( $oldText ) . md5( $newText ),
156  [ 'doWork' => $diff, 'error' => $error ]
157  );
158  return $work->execute();
159  }
160 
161  return $diff();
162  }
163 
172  protected function getTextDiffInternal( $oldText, $newText ) {
173  // TODO move most of this into three parallel implementations of a text diff generator
174  // class, choose which one to use via dependecy injection
175 
176  $oldText = str_replace( "\r\n", "\n", $oldText );
177  $newText = str_replace( "\r\n", "\n", $newText );
178 
179  // Better external diff engine, the 2 may some day be dropped
180  // This one does the escaping and segmenting itself
181  if ( $this->engine === self::ENGINE_WIKIDIFF2 ) {
182  $wikidiff2Version = phpversion( 'wikidiff2' );
183  if (
184  $wikidiff2Version !== false &&
185  version_compare( $wikidiff2Version, '1.5.0', '>=' ) &&
186  version_compare( $wikidiff2Version, '1.8.0', '<' )
187  ) {
188  $text = wikidiff2_do_diff(
189  $oldText,
190  $newText,
191  2,
192  0
193  );
194  } else {
195  // Don't pass the 4th parameter introduced in version 1.5.0 and removed in version 1.8.0
196  $text = wikidiff2_do_diff(
197  $oldText,
198  $newText,
199  2
200  );
201  }
202 
203  return $text;
204  } elseif ( $this->engine === self::ENGINE_EXTERNAL ) {
205  # Diff via the shell
206  $tmpDir = wfTempDir();
207  $tempName1 = tempnam( $tmpDir, 'diff_' );
208  $tempName2 = tempnam( $tmpDir, 'diff_' );
209 
210  $tempFile1 = fopen( $tempName1, "w" );
211  if ( !$tempFile1 ) {
212  return false;
213  }
214  $tempFile2 = fopen( $tempName2, "w" );
215  if ( !$tempFile2 ) {
216  return false;
217  }
218  fwrite( $tempFile1, $oldText );
219  fwrite( $tempFile2, $newText );
220  fclose( $tempFile1 );
221  fclose( $tempFile2 );
222  $cmd = [ $this->externalEngine, $tempName1, $tempName2 ];
223  $result = Shell::command( $cmd )
224  ->execute();
225  $exitCode = $result->getExitCode();
226  if ( $exitCode !== 0 ) {
227  throw new Exception( "External diff command returned code {$exitCode}. Stderr: "
228  . wfEscapeWikiText( $result->getStderr() )
229  );
230  }
231  $difftext = $result->getStdout();
232  unlink( $tempName1 );
233  unlink( $tempName2 );
234 
235  return $difftext;
236  } elseif ( $this->engine === self::ENGINE_PHP ) {
237  if ( $this->language ) {
238  $oldText = $this->language->segmentForDiff( $oldText );
239  $newText = $this->language->segmentForDiff( $newText );
240  }
241  $ota = explode( "\n", $oldText );
242  $nta = explode( "\n", $newText );
243  $diffs = new Diff( $ota, $nta );
244  $formatter = new TableDiffFormatter();
245  $difftext = $formatter->format( $diffs );
246  if ( $this->language ) {
247  $difftext = $this->language->unsegmentForDiff( $difftext );
248  }
249 
250  return $difftext;
251  }
252  throw new LogicException( 'Invalid engine: ' . $this->engine );
253  }
254 
255 }
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.
IBufferingStatsdDataFactory null $statsdDataFactory