PageRenderTime 54ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/core/xpdo/revision/xpdorevisioncontrol.class.php

http://github.com/modxcms/revolution
PHP | 363 lines | 241 code | 44 blank | 78 comment | 85 complexity | c6b64933f9eb70ea9277e7b189211b9f MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * The xPDORevisionControl class provides utilities for content versioning.
  4. *
  5. * @package xpdo
  6. * @subpackage revision
  7. */
  8. /**
  9. * Utility class for creating, merging, and managing diffs for versioning.
  10. *
  11. * BASED ON:
  12. * Implementation of a GNU diff alike function from scratch.
  13. * Copyright (C) 2003 Nils Knappmeier <nk@knappi.org>
  14. *
  15. * Permission is hereby granted, free of charge, to any person obtaining
  16. * a copy of this software and associated documentation files (the
  17. * "Software"), to deal in the Software without restriction, including
  18. * without limitation the rights to use, copy, modify, merge, publish,
  19. * distribute, sublicense, and/or sell copies of the Software, and to
  20. * permit persons to whom the Software is furnished to do so, subject to
  21. * the following conditions:
  22. *
  23. * The above copyright notice and this permission notice shall be
  24. * included in all copies or substantial portions of the Software.
  25. *
  26. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  27. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  28. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  29. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  30. * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  31. * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  32. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  33. * SOFTWARE.
  34. *
  35. * @package xpdo
  36. * @subpackage revision
  37. *
  38. */
  39. class xPDORevisionControl {
  40. /**
  41. * Computes the difference between two string linewise.
  42. * The output is the same format
  43. * as the GNU diff command without any parameters.
  44. * Note: Since GNU diff starts counting with 1, and arrays start counting with
  45. * 0, you have to subtract one from each line number to get the real one.
  46. */
  47. function diff($text1, $text2) {
  48. $array1= $this->_split($text1);
  49. $array2= $this->_split($text2);
  50. /* Build up array with the line as key and a list of line numbers as value */
  51. foreach ($array1 as $nr => $line) {
  52. $r_array1[$line][]= $nr;
  53. }
  54. foreach ($array2 as $nr => $line) {
  55. $r_array2[$line][]= $nr;
  56. }
  57. $result= "";
  58. $a[1]= 0; /* counter for array1 */
  59. $a[2]= 0; /* counter for array2 */
  60. $actions= array ();
  61. while ($a[1] < sizeof($array1) && $a[2] < sizeof($array2)) {
  62. # print "$a[1]:$a[2]\n";
  63. if ($array1[$a[1]] == $array2[$a[2]]) {
  64. $a[1]++;
  65. $a[2]++;
  66. $actions[]= 'copy';
  67. } else {
  68. /*
  69. * $tmp1/2 is the next line in $array1/2 that equals
  70. * $array2/1[$a2/1+1];
  71. */
  72. $best[1]= count($array1);
  73. $best[2]= count($array2);
  74. $scan= $a;
  75. # echo "distBest = ".dist($a, $best)."\n";
  76. # echo "distScan = ".dist($a, $scan)."\n";
  77. while ($this->dist($a, $scan) < $this->dist($a, $best)) {
  78. # echo "A $a[1]:$a[2]\t";
  79. # echo "Scan $scan[1]:$scan[2] ";
  80. $tmp[1]= $this->nextOccurence($array2[$scan[2]], $r_array1, $scan[1]);
  81. $tmp[2]= $scan[2];
  82. if ($tmp[1] && $this->dist($a, $tmp) < $this->dist($a, $best))
  83. $best= $tmp;
  84. # echo "Tmp1 $tmp[1]:$tmp[2]\t";;
  85. $tmp[1]= $scan[1];
  86. $tmp[2]= $this->nextOccurence($array1[$scan[1]], $r_array2, $scan[2]);
  87. if ($tmp[2] && $this->dist($a, $tmp) < $this->dist($a, $best))
  88. $best= $tmp;
  89. # echo "Tmp2 $tmp[1]:$tmp[2]\t";
  90. # echo "Best $best[1]:$best[2]\n";
  91. $scan[1]++;
  92. $scan[2]++;
  93. }
  94. for ($i= $a[1]; $i < $best[1]; $i++) {
  95. $actions[]= 'del';
  96. }
  97. for ($i= $a[2]; $i < $best[2]; $i++) {
  98. $actions[]= 'add';
  99. }
  100. $a= $best;
  101. }
  102. }
  103. # Here, we may still not be at the bottom left corner.
  104. # So we have to get there. (This case happens, when we just append something to the page)
  105. for ($i= $a[1]; $i < sizeof($array1); $i++) {
  106. $actions[]= 'del';
  107. }
  108. for ($i= $a[2]; $i < sizeof($array2); $i++) {
  109. $actions[]= 'add';
  110. }
  111. $actions[]= 'finish';
  112. /* Now follow the way back and report connected (unequal) pieces */
  113. $x= $xold= 0;
  114. $y= $yold= 0;
  115. $realAction= ""; /* the current action */
  116. foreach ($actions as $action) {
  117. if ($action == 'del') {
  118. if ($realAction == "" || $realAction == "d") {
  119. $realAction= "d";
  120. } else {
  121. $realAction= "c";
  122. }
  123. $x++;
  124. }
  125. if ($action == 'add') {
  126. if ($realAction == "" || $realAction == "a") {
  127. $realAction= "a";
  128. } else {
  129. $realAction= "c";
  130. }
  131. $y++;
  132. }
  133. if ($action == 'copy' || $action == 'finish') {
  134. /* Prepare header for diff entry */
  135. if ($xold +1 == $x) {
  136. $xstr= $x;
  137. } else {
  138. $xstr= ($xold +1) . ",$x";
  139. }
  140. if ($yold +1 == $y) {
  141. $ystr= $y;
  142. } else {
  143. $ystr= ($yold +1) . ",$y";
  144. }
  145. /* "Print" entry to result */
  146. if ($realAction == "a") {
  147. $result .= ($x) . "a$ystr\n";
  148. for ($i= $yold; $i < $y; $i++) {
  149. $result .= "> " . $array2[$i];
  150. }
  151. } else
  152. if ($realAction == "d") {
  153. $result .= ($xstr) . "d" . ($y) . "\n";
  154. for ($i= $xold; $i < $x; $i++) {
  155. $result .= "< " . $array1[$i];
  156. }
  157. } else
  158. if ($realAction == "c") {
  159. $result .= "$xstr$realAction$ystr\n";
  160. for ($i= $xold; $i < $x; $i++) {
  161. $result .= "< " . $array1[$i];
  162. }
  163. $result .= "---\n";
  164. for ($i= $yold; $i < $y; $i++) {
  165. $result .= "> " . $array2[$i];
  166. }
  167. }
  168. $x++;
  169. $y++;
  170. $realAction= "";
  171. $xold= $x;
  172. $yold= $y;
  173. }
  174. }
  175. return $result;
  176. }
  177. function cut_head(& $str, $key, $prefix) {
  178. if (strpos($str, $prefix) === 0) {
  179. $str= substr($str, strlen($prefix));
  180. } else {
  181. print "Something is wrong in the patch: ";
  182. print "'$str' should begin with '$prefix'\n";
  183. exit;
  184. }
  185. }
  186. function restore($revisions= array (), $restore= 0) {
  187. reset($revisions);
  188. $restored= current($revisions);
  189. if ($restore && next($revisions)) {
  190. while (list($k, $v)= each($revisions)) {
  191. if ($k > $restore)
  192. break;
  193. $restored= $this->patch($restored, $v);
  194. }
  195. }
  196. return $restored;
  197. }
  198. function patch($text, $patch) {
  199. $array= $this->_split($text);
  200. /* Modify patch to an array so that it
  201. * is compatible to the modification */
  202. if ($patch == "")
  203. return $text;
  204. if (substr($patch, -1) == "\n")
  205. $patch= substr($patch, 0, strlen($patch) - 1);
  206. $patch_array= explode("\n", $patch);
  207. for ($i= 0; $i < count($patch_array); $i++) {
  208. $patch_array[$i]= $patch_array[$i] . "\n";
  209. }
  210. $i= 0;
  211. $nlIndex= array_search("\\ No newline at end of file\n", $patch_array);
  212. while ($nlIndex != false && $i < 2) {
  213. /* This shouldn't be happening more than two times in a valid patch */
  214. $newEntry= $patch_array[$nlIndex -1] . $patch_array[$nlIndex];
  215. array_splice($patch_array, $nlIndex -1, 2, $newEntry);
  216. $nlIndex= array_search("\\ No newline at end of file\n", $patch_array);
  217. $i++;
  218. }
  219. /* Start computing */
  220. $current= 0;
  221. do {
  222. if (preg_match("/^([\d,]+)([adc])([\d,]+)$/", $patch_array[$current], $matches) == 0) {
  223. print "<pre>Error in line $current: " . $patch_array[$current] . " not a command\n" . sizeof($patch_array);
  224. print "</pre>";
  225. exit;
  226. }
  227. list ($full, $left, $action, $right)= $matches;
  228. /* Compute start and end of each side */
  229. list ($left_start, $left_end)= explode(",", $left);
  230. list ($right_start, $right_end)= explode(",", $right);
  231. if ($left_end == "") {
  232. $left_end= $left_start;
  233. }
  234. if ($right_end == "") {
  235. $right_end= $right_start;
  236. }
  237. /* Perform action and switch to next patch */
  238. if ($action == "a") {
  239. $replace= array_slice($patch_array, $current +1, $right_end - $right_start +1);
  240. array_walk($replace, array ($this, 'cut_head'), '> ');
  241. array_splice($array, $right_start -1, 0, $replace);
  242. $current += $right_end - $right_start +2;
  243. } else
  244. if ($action == "d") {
  245. /* Check whether lines in patch are like in file */
  246. $should= array_slice($patch_array, $current +1, $left_end - $left_start +1);
  247. array_walk($should, array ($this, 'cut_head'), '< ');
  248. $is= array_splice($array, $right_start, $left_end - $left_start +1);
  249. if ($should !== $is) {
  250. print "<pre>According to the patch, in lines $left_start to ";
  251. print "$left_end there should be a\n";
  252. print urlencode(implode("", $should)) . "\n";
  253. print "but I only find a\n";
  254. print urlencode(implode("", $is)) . "\n</pre>";
  255. }
  256. $current += $left_end - $left_start +2;
  257. } else
  258. if ($action == "c") {
  259. $replace= array_slice($patch_array, $current +1 + $left_end - $left_start +2, $right_end - $right_start +1);
  260. array_walk($replace, array ($this, 'cut_head'), '> ');
  261. $is= array_splice($array, $right_start -1, $left_end - $left_start +1, $replace);
  262. /* Check whether lines in patch are like in text */
  263. $should= array_slice($patch_array, $current +1, $left_end - $left_start +1);
  264. array_walk($should, array ($this, 'cut_head'), '< ');
  265. if ($should !== $is) {
  266. print "<pre>According to the patch, in lines $left_start to";
  267. print "$left_end there should be a\n";
  268. print implode("", $should);
  269. print "but I only find a\n";
  270. print implode("", $is) . "</pre>";
  271. }
  272. $current += 1 + $left_end - $left_start +1 + 1 + $right_end - $right_start +1;
  273. }
  274. } while ($current < count($patch_array));
  275. $result= implode("", $array);
  276. $suffix= "\n\\ No newline at end of file\n";
  277. if (substr($result, -strlen($suffix)) == $suffix) {
  278. $result= substr($result, 0, strlen($result) - strlen($suffix));
  279. }
  280. return $result;
  281. }
  282. /**
  283. * Checks, if there is a line number-entry in $r_array for $line,
  284. * that is behind $where.
  285. * $where will be assigned the line-number, if true
  286. */
  287. function nextOccurence($line, & $r_array, $where) {
  288. $tmp= $r_array[$line];
  289. if (!$tmp)
  290. return false;
  291. foreach ($tmp as $nr) {
  292. if ($where <= $nr) {
  293. $where= $nr;
  294. return $nr;
  295. }
  296. }
  297. return false;
  298. }
  299. /**
  300. * Compute the manhatten distance of two points
  301. */
  302. function dist($a, $b) {
  303. $d1= $b[1] - $a[1];
  304. $d2= $b[2] - $a[2];
  305. return $d2 + $d1;
  306. }
  307. function _split($text) {
  308. $array= explode("\n", $text);
  309. for ($i= 0; $i < count($array); $i++) {
  310. $array[$i]= $array[$i] . "\n";
  311. }
  312. if ($array[count($array) - 1] == "\n") {
  313. array_pop($array);
  314. } else {
  315. $array[count($array) - 1]= $array[count($array) - 1] . "\\ No newline at end of file\n";
  316. }
  317. return $array;
  318. }
  319. }