Joomla Platform  13.1
Documentation des API du framework Joomla Platform
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
patcher.php
Aller à la documentation de ce fichier.
1 <?php
2 
3 /**
4  * @package Joomla.Platform
5  * @subpackage FileSystem
6  *
7  * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
8  * @license GNU General Public License version 2 or later; see LICENSE
9  */
10 
11 defined('JPATH_PLATFORM') or die;
12 
13 jimport('joomla.filesystem.file');
14 
15 /**
16  * A Unified Diff Format Patcher class
17  *
18  * @package Joomla.Platform
19  * @subpackage FileSystem
20  *
21  * @link http://sourceforge.net/projects/phppatcher/ This has been derived from the PhpPatcher version 0.1.1 written by Giuseppe Mazzotta
22  * @since 12.1
23  */
25 {
26  /**
27  * Regular expression for searching source files
28  */
29  const SRC_FILE = '/^---\\s+(\\S+)\s+\\d{1,4}-\\d{1,2}-\\d{1,2}\\s+\\d{1,2}:\\d{1,2}:\\d{1,2}(\\.\\d+)?\\s+(\+|-)\\d{4}/A';
30 
31  /**
32  * Regular expression for searching destination files
33  */
34  const DST_FILE = '/^\\+\\+\\+\\s+(\\S+)\s+\\d{1,4}-\\d{1,2}-\\d{1,2}\\s+\\d{1,2}:\\d{1,2}:\\d{1,2}(\\.\\d+)?\\s+(\+|-)\\d{4}/A';
35 
36  /**
37  * Regular expression for searching hunks of differences
38  */
39  const HUNK = '/@@ -(\\d+)(,(\\d+))?\\s+\\+(\\d+)(,(\\d+))?\\s+@@($)/A';
40 
41  /**
42  * Regular expression for splitting lines
43  */
44  const SPLIT = '/(\r\n)|(\r)|(\n)/';
45 
46  /**
47  * @var array sources files
48  * @since 12.1
49  */
50  protected $sources = array();
51 
52  /**
53  * @var array destination files
54  * @since 12.1
55  */
56  protected $destinations = array();
57 
58  /**
59  * @var array removal files
60  * @since 12.1
61  */
62  protected $removals = array();
63 
64  /**
65  * @var array patches
66  * @since 12.1
67  */
68  protected $patches = array();
69 
70  /**
71  * @var array instance of this class
72  * @since 12.1
73  */
74  protected static $instance;
75 
76  /**
77  * Constructor
78  *
79  * The constructor is protected to force the use of JFilesystemPatcher::getInstance()
80  *
81  * @since 12.1
82  */
83  protected function __construct()
84  {
85  }
86 
87  /**
88  * Method to get a patcher
89  *
90  * @return JFilesystemPatcher an instance of the patcher
91  *
92  * @since 12.1
93  */
94  public static function getInstance()
95  {
96  if (!isset(static::$instance))
97  {
98  static::$instance = new static;
99  }
100 
101  return static::$instance;
102  }
103 
104  /**
105  * Reset the pacher
106  *
107  * @return JFilesystemPatcher This object for chaining
108  *
109  * @since 12.1
110  */
111  public function reset()
112  {
113  $this->sources = array();
114  $this->destinations = array();
115  $this->removals = array();
116  $this->patches = array();
117 
118  return $this;
119  }
120 
121  /**
122  * Apply the patches
123  *
124  * @return integer The number of files patched
125  *
126  * @since 12.1
127  * @throws RuntimeException
128  */
129  public function apply()
130  {
131  foreach ($this->patches as $patch)
132  {
133  // Separate the input into lines
134  $lines = self::splitLines($patch['udiff']);
135 
136  // Loop for each header
137  while (self::findHeader($lines, $src, $dst))
138  {
139  $done = false;
140 
141  if ($patch['strip'] === null)
142  {
143  $src = $patch['root'] . preg_replace('#^([^/]*/)*#', '', $src);
144  $dst = $patch['root'] . preg_replace('#^([^/]*/)*#', '', $dst);
145  }
146  else
147  {
148  $src = $patch['root'] . preg_replace('#^([^/]*/){' . (int) $patch['strip'] . '}#', '', $src);
149  $dst = $patch['root'] . preg_replace('#^([^/]*/){' . (int) $patch['strip'] . '}#', '', $dst);
150  }
151 
152  // Loop for each hunk of differences
153  while (self::findHunk($lines, $src_line, $src_size, $dst_line, $dst_size))
154  {
155  $done = true;
156 
157  // Apply the hunk of differences
158  $this->applyHunk($lines, $src, $dst, $src_line, $src_size, $dst_line, $dst_size);
159  }
160 
161  // If no modifications were found, throw an exception
162  if (!$done)
163  {
164  throw new RuntimeException('Invalid Diff');
165  }
166  }
167  }
168 
169  // Initialize the counter
170  $done = 0;
171 
172  // Patch each destination file
173  foreach ($this->destinations as $file => $content)
174  {
175  if (JFile::write($file, implode("\n", $content)))
176  {
177  if (isset($this->sources[$file]))
178  {
179  $this->sources[$file] = $content;
180  }
181 
182  $done++;
183  }
184  }
185 
186  // Remove each removed file
187  foreach ($this->removals as $file)
188  {
189  if (JFile::delete($file))
190  {
191  if (isset($this->sources[$file]))
192  {
193  unset($this->sources[$file]);
194  }
195 
196  $done++;
197  }
198  }
199 
200  // Clear the destinations cache
201  $this->destinations = array();
202 
203  // Clear the removals
204  $this->removals = array();
205 
206  // Clear the patches
207  $this->patches = array();
208 
209  return $done;
210  }
211 
212  /**
213  * Add a unified diff file to the patcher
214  *
215  * @param string $filename Path to the unified diff file
216  * @param string $root The files root path
217  * @param string $strip The number of '/' to strip
218  *
219  * @return JFilesystemPatch $this for chaining
220  *
221  * @since 12.1
222  */
223  public function addFile($filename, $root = JPATH_BASE, $strip = 0)
224  {
225  return $this->add(file_get_contents($filename), $root, $strip);
226  }
227 
228  /**
229  * Add a unified diff string to the patcher
230  *
231  * @param string $udiff Unified diff input string
232  * @param string $root The files root path
233  * @param string $strip The number of '/' to strip
234  *
235  * @return JFilesystemPatch $this for chaining
236  *
237  * @since 12.1
238  */
239  public function add($udiff, $root = JPATH_BASE, $strip = 0)
240  {
241  $this->patches[] = array(
242  'udiff' => $udiff,
243  'root' => isset($root) ? rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : '',
244  'strip' => $strip
245  );
246 
247  return $this;
248  }
249 
250  /**
251  * Separate CR or CRLF lines
252  *
253  * @param string $data Input string
254  *
255  * @return array The lines of the inputdestination file
256  *
257  * @since 12.1
258  */
259  protected static function splitLines($data)
260  {
261  return preg_split(self::SPLIT, $data);
262  }
263 
264  /**
265  * Find the diff header
266  *
267  * The internal array pointer of $lines is on the next line after the finding
268  *
269  * @param array &$lines The udiff array of lines
270  * @param string &$src The source file
271  * @param string &$dst The destination file
272  *
273  * @return boolean TRUE in case of success, FALSE in case of failure
274  *
275  * @since 12.1
276  * @throws RuntimeException
277  */
278  protected static function findHeader(&$lines, &$src, &$dst)
279  {
280  // Get the current line
281  $line = current($lines);
282 
283  // Search for the header
284  while ($line !== false && !preg_match(self::SRC_FILE, $line, $m))
285  {
286  $line = next($lines);
287  }
288  if ($line === false)
289  {
290  // No header found, return false
291  return false;
292  }
293  else
294  {
295  // Set the source file
296  $src = $m[1];
297 
298  // Advance to the next line
299  $line = next($lines);
300 
301  if ($line === false)
302  {
303  throw new RuntimeException('Unexpected EOF');
304  }
305 
306  // Search the destination file
307  if (!preg_match(self::DST_FILE, $line, $m))
308  {
309  throw new RuntimeException('Invalid Diff file');
310  }
311 
312  // Set the destination file
313  $dst = $m[1];
314 
315  // Advance to the next line
316  if (next($lines) === false)
317  {
318  throw new RuntimeException('Unexpected EOF');
319  }
320  return true;
321  }
322  }
323 
324  /**
325  * Find the next hunk of difference
326  *
327  * The internal array pointer of $lines is on the next line after the finding
328  *
329  * @param array &$lines The udiff array of lines
330  * @param string &$src_line The beginning of the patch for the source file
331  * @param string &$src_size The size of the patch for the source file
332  * @param string &$dst_line The beginning of the patch for the destination file
333  * @param string &$dst_size The size of the patch for the destination file
334  *
335  * @return boolean TRUE in case of success, false in case of failure
336  *
337  * @since 12.1
338  * @throws RuntimeException
339  */
340  protected static function findHunk(&$lines, &$src_line, &$src_size, &$dst_line, &$dst_size)
341  {
342  $line = current($lines);
343 
344  if (preg_match(self::HUNK, $line, $m))
345  {
346  $src_line = (int) $m[1];
347 
348  if ($m[3] === '')
349  {
350  $src_size = 1;
351  }
352  else
353  {
354  $src_size = (int) $m[3];
355  }
356 
357  $dst_line = (int) $m[4];
358 
359  if ($m[6] === '')
360  {
361  $dst_size = 1;
362  }
363  else
364  {
365  $dst_size = (int) $m[6];
366  }
367 
368  if (next($lines) === false)
369  {
370  throw new RuntimeException('Unexpected EOF');
371  }
372 
373  return true;
374  }
375  else
376  {
377  return false;
378  }
379  }
380 
381  /**
382  * Apply the patch
383  *
384  * @param array &$lines The udiff array of lines
385  * @param string $src The source file
386  * @param string $dst The destination file
387  * @param string $src_line The beginning of the patch for the source file
388  * @param string $src_size The size of the patch for the source file
389  * @param string $dst_line The beginning of the patch for the destination file
390  * @param string $dst_size The size of the patch for the destination file
391  *
392  * @return void
393  *
394  * @since 12.1
395  * @throws RuntimeException
396  */
397  protected function applyHunk(&$lines, $src, $dst, $src_line, $src_size, $dst_line, $dst_size)
398  {
399  $src_line--;
400  $dst_line--;
401  $line = current($lines);
402 
403  // Source lines (old file)
404  $source = array();
405 
406  // New lines (new file)
407  $destin = array();
408  $src_left = $src_size;
409  $dst_left = $dst_size;
410 
411  do
412  {
413  if (!isset($line[0]))
414  {
415  $source[] = '';
416  $destin[] = '';
417  $src_left--;
418  $dst_left--;
419  }
420  elseif ($line[0] == '-')
421  {
422  if ($src_left == 0)
423  {
424  throw new RuntimeException(JText::sprintf('JLIB_FILESYSTEM_PATCHER_REMOVE_LINE', key($lines)));
425  }
426 
427  $source[] = substr($line, 1);
428  $src_left--;
429  }
430  elseif ($line[0] == '+')
431  {
432  if ($dst_left == 0)
433  {
434  throw new RuntimeException(JText::sprintf('JLIB_FILESYSTEM_PATCHER_ADD_LINE', key($lines)));
435  }
436 
437  $destin[] = substr($line, 1);
438  $dst_left--;
439  }
440  elseif ($line != '\\ No newline at end of file')
441  {
442  $line = substr($line, 1);
443  $source[] = $line;
444  $destin[] = $line;
445  $src_left--;
446  $dst_left--;
447  }
448 
449  if ($src_left == 0 && $dst_left == 0)
450  {
451  // Now apply the patch, finally!
452  if ($src_size > 0)
453  {
454  $src_lines = & $this->getSource($src);
455 
456  if (!isset($src_lines))
457  {
458  throw new RuntimeException(JText::sprintf('JLIB_FILESYSTEM_PATCHER_UNEXISING_SOURCE', $src));
459  }
460  }
461 
462  if ($dst_size > 0)
463  {
464  if ($src_size > 0)
465  {
466  $dst_lines = & $this->getDestination($dst, $src);
467  $src_bottom = $src_line + count($source);
468 
469  for ($l = $src_line;$l < $src_bottom;$l++)
470  {
471  if ($src_lines[$l] != $source[$l - $src_line])
472  {
473  throw new RuntimeException(JText::sprintf('JLIB_FILESYSTEM_PATCHER_FAILED_VERIFY', $src, $l));
474  }
475  }
476 
477  array_splice($dst_lines, $dst_line, count($source), $destin);
478  }
479  else
480  {
481  $this->destinations[$dst] = $destin;
482  }
483  }
484  else
485  {
486  $this->removals[] = $src;
487  }
488 
489  next($lines);
490 
491  return;
492  }
493 
494  $line = next($lines);
495  }
496 
497  while ($line !== false);
498  throw new RuntimeException('Unexpected EOF');
499  }
500 
501  /**
502  * Get the lines of a source file
503  *
504  * @param string $src The path of a file
505  *
506  * @return array The lines of the source file
507  *
508  * @since 12.1
509  */
510  protected function &getSource($src)
511  {
512  if (!isset($this->sources[$src]))
513  {
514  if (is_readable($src))
515  {
516  $this->sources[$src] = self::splitLines(file_get_contents($src));
517  }
518  else
519  {
520  $this->sources[$src] = null;
521  }
522  }
523 
524  return $this->sources[$src];
525  }
526 
527  /**
528  * Get the lines of a destination file
529  *
530  * @param string $dst The path of a destination file
531  * @param string $src The path of a source file
532  *
533  * @return array The lines of the destination file
534  *
535  * @since 12.1
536  */
537  protected function &getDestination($dst, $src)
538  {
539  if (!isset($this->destinations[$dst]))
540  {
541  $this->destinations[$dst] = $this->getSource($src);
542  }
543 
544  return $this->destinations[$dst];
545  }
546 }