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