dolibarr 25.0.0-alpha
filecheck_diff.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2026 Frédéric France <frederic.france@free.fr>
3 * Copyright (C) 2026 MDW <mdeweerd@users.noreply.github.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
24if (!defined('NOTOKENRENEWAL')) {
25 define('NOTOKENRENEWAL', '1');
26}
27if (!defined('NOREQUIREMENU')) {
28 define('NOREQUIREMENU', '1');
29}
30if (!defined('NOREQUIREHTML')) {
31 define('NOREQUIREHTML', '1');
32}
33if (!defined('NOREQUIREAJAX')) {
34 define('NOREQUIREAJAX', '1');
35}
36
37// Load Dolibarr environment
38require '../../main.inc.php';
45require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
46require_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
47
48$langs->loadLangs(array("admin", "errors"));
49
50if (!$user->admin && !$user->hasRight('bockedlog', 'read')) {
52}
53
54$file = GETPOST('file', 'alphanohtml');
55$algo = GETPOST('algo', 'aZ09');
56$expectedhash = GETPOST('expectedhash', 'aZ09');
57
58if (!in_array($algo, array('md5', 'sha256'), true)) {
59 $algo = 'sha256';
60}
61
62
71function filecheckLineDiff($a, $b)
72{
73 $na = count($a);
74 $nb = count($b);
75
76 // Trim common prefix
77 $start = 0;
78 while ($start < $na && $start < $nb && $a[$start] === $b[$start]) {
79 $start++;
80 }
81 // Trim common suffix
82 $enda = $na - 1;
83 $endb = $nb - 1;
84 while ($enda >= $start && $endb >= $start && $a[$enda] === $b[$endb]) {
85 $enda--;
86 $endb--;
87 }
88
89 $result = array();
90 for ($i = 0; $i < $start; $i++) {
91 $result[] = array(' ', $a[$i]);
92 }
93
94 $midA = array_slice($a, $start, $enda - $start + 1);
95 $midB = array_slice($b, $start, $endb - $start + 1);
96
97 foreach (filecheckMiddleDiff($midA, $midB) as $d) {
98 $result[] = $d;
99 }
100
101 for ($i = $enda + 1; $i < $na; $i++) {
102 $result[] = array(' ', $a[$i]);
103 }
104
105 return $result;
106}
107
117function filecheckMiddleDiff($a, $b)
118{
119 $a = array_values($a);
120 $b = array_values($b);
121 $na = count($a);
122 $nb = count($b);
123
124 if ($na == 0 || $nb == 0) {
125 $result = array();
126 foreach ($a as $line) {
127 $result[] = array('-', $line);
128 }
129 foreach ($b as $line) {
130 $result[] = array('+', $line);
131 }
132 return $result;
133 }
134
135 // Small enough region: a direct LCS gives the optimal line-level diff.
136 if ($na * $nb <= 1000000) {
137 return filecheckLcs($a, $b);
138 }
139
140 // Large region: split on a unique common line (anchor) and recurse on both sides.
141 $anchor = filecheckFindAnchor($a, $b);
142 if ($anchor !== null) {
143 return array_merge(
144 filecheckMiddleDiff(array_slice($a, 0, $anchor[0]), array_slice($b, 0, $anchor[1])),
145 array(array(' ', $a[$anchor[0]])),
146 filecheckMiddleDiff(array_slice($a, $anchor[0] + 1), array_slice($b, $anchor[1] + 1))
147 );
148 }
149
150 // No anchor available in a very large region: fall back to a plain block replacement.
151 $result = array();
152 foreach ($a as $line) {
153 $result[] = array('-', $line);
154 }
155 foreach ($b as $line) {
156 $result[] = array('+', $line);
157 }
158 return $result;
159}
160
169function filecheckFindAnchor($a, $b)
170{
171 $countA = array();
172 foreach ($a as $line) {
173 $countA[$line] = (isset($countA[$line]) ? $countA[$line] : 0) + 1;
174 }
175 $countB = array();
176 $posB = array();
177 foreach ($b as $j => $line) {
178 $countB[$line] = (isset($countB[$line]) ? $countB[$line] : 0) + 1;
179 $posB[$line] = $j;
180 }
181
182 $middle = (int) (count($a) / 2);
183 $best = null;
184 $bestdist = -1;
185 foreach ($a as $i => $line) {
186 if ($countA[$line] == 1 && isset($countB[$line]) && $countB[$line] == 1) {
187 $dist = abs($i - $middle);
188 if ($bestdist < 0 || $dist < $bestdist) {
189 $bestdist = $dist;
190 $best = array($i, $posB[$line]);
191 }
192 }
193 }
194
195 return $best;
196}
197
205function filecheckLcs($a, $b)
206{
207 $na = count($a);
208 $nb = count($b);
209
210 // LCS length matrix
211 $lcs = array();
212 for ($i = 0; $i <= $na; $i++) {
213 $lcs[$i] = array_fill(0, $nb + 1, 0);
214 }
215 for ($i = $na - 1; $i >= 0; $i--) {
216 for ($j = $nb - 1; $j >= 0; $j--) {
217 if ($a[$i] === $b[$j]) {
218 $lcs[$i][$j] = $lcs[$i + 1][$j + 1] + 1;
219 } else {
220 $lcs[$i][$j] = max($lcs[$i + 1][$j], $lcs[$i][$j + 1]);
221 }
222 }
223 }
224
225 // Backtrack to build the diff
226 $result = array();
227 $i = 0;
228 $j = 0;
229 while ($i < $na && $j < $nb) {
230 if ($a[$i] === $b[$j]) {
231 $result[] = array(' ', $a[$i]);
232 $i++;
233 $j++;
234 } elseif ($lcs[$i + 1][$j] >= $lcs[$i][$j + 1]) {
235 $result[] = array('-', $a[$i]);
236 $i++;
237 } else {
238 $result[] = array('+', $b[$j]);
239 $j++;
240 }
241 }
242 while ($i < $na) {
243 $result[] = array('-', $a[$i]);
244 $i++;
245 }
246 while ($j < $nb) {
247 $result[] = array('+', $b[$j]);
248 $j++;
249 }
250
251 return $result;
252}
253
262{
263 $n = count($diff);
264 $keep = array_fill(0, $n, false);
265 for ($i = 0; $i < $n; $i++) {
266 if ($diff[$i][0] !== ' ') {
267 $from_line = max(0, $i - $context);
268 $to_line = min($n - 1, $i + $context);
269 for ($k = $from_line; $k <= $to_line; $k++) {
270 $keep[$k] = true;
271 }
272 }
273 }
274
275 $result = array();
276 $ingap = false;
277 for ($i = 0; $i < $n; $i++) {
278 if ($keep[$i]) {
279 $result[] = $diff[$i];
280 $ingap = false;
281 } elseif (!$ingap) {
282 $result[] = array('@', '');
283 $ingap = true;
284 }
285 }
286
287 return $result;
288}
289
290
291top_httphead('text/html');
292
293print '<!-- filecheck_diff.php fragment -->'."\n";
294
295// Validate the requested file: must be a relative path inside DOL_DOCUMENT_ROOT, with no traversal.
296$errormsg = '';
297$reallocal = '';
298if (empty($file) || $file[0] !== '/' || strpos($file, '..') !== false || !preg_match('/^[A-Za-z0-9_\/.\-]+$/', $file)) {
299 $errormsg = $langs->trans("ErrorBadValueForParameter", dol_escape_htmltag($file), "file");
300}
301
302if (empty($errormsg)) {
303 $reallocal = realpath(DOL_DOCUMENT_ROOT.$file);
304 if ($reallocal === false || strpos($reallocal, realpath(DOL_DOCUMENT_ROOT).'/') !== 0 || !is_file($reallocal)) {
305 $errormsg = $langs->trans("ErrorFileNotFound", $file);
306 }
307}
308// Only text files can be diffed
309if (empty($errormsg) && preg_match('/\.(jpg|jpeg|png|gif|ico|svg|eot|woff|woff2|ttf|mp3|mp4|wav|mkv|z|gz|zip|rar|tar)$/i', $file)) {
310 $errormsg = $langs->trans("DiffNotAvailableForBinaryFiles");
311}
312
313if (!empty($errormsg)) {
314 print '<div class="warning">'.$errormsg.'</div>';
316}
317
318// Build the URL of the original file for the running version.
319// For a stable version (eg 24.0.0) the matching git tag exists, so we compare against that tag.
320// For an alpha/beta/rc version no tag exists yet, so we compare against the develop branch.
321$baseurl = getDolGlobalString('MAIN_FILECHECK_DIFF_BASEURL', 'https://raw.githubusercontent.com/Dolibarr/dolibarr');
322if (preg_match('/alpha|beta|rc/i', DOL_VERSION)) {
323 $ref = 'develop';
324} else {
325 $ref = DOL_VERSION;
326}
327$ref = getDolGlobalString('MAIN_FILECHECK_DIFF_REF', $ref);
328$originurl = $baseurl.'/'.$ref.'/htdocs'.$file;
329
330$res = getURLContent($originurl, 'GET', '', 1, array(), array('http', 'https'), 0); // Accept http or https links on external remote server only.
331if (!empty($res['curl_error_no']) || (isset($res['http_code']) && !in_array((int) $res['http_code'], array(0, 200), true))) {
332 print '<div class="warning">'.$langs->trans("CouldNotFetchOriginalFile").': '.dol_escape_htmltag($originurl);
333 print ' ('.dol_escape_htmltag((string) (empty($res['http_code']) ? $res['curl_error_msg'] : $res['http_code'])).')</div>';
335}
336
337$origincontent = (string) $res['content'];
338$localcontent = (string) file_get_contents($reallocal);
339
340// Confirm the fetched original is the genuine reference file expected by the signature.
341$verified = true;
342if (!empty($expectedhash)) {
343 $verified = (hash($algo, $origincontent) === $expectedhash);
344}
345if (!$verified) {
346 print '<div class="warning">'.$langs->trans("OriginalFileChecksumMismatch").'</div>';
347}
348
349if ($origincontent === $localcontent) {
350 print '<div class="opacitymedium">'.$langs->trans("NoDifferenceFound").'</div>';
352}
353
354$diff = filecheckLineDiff(explode("\n", $origincontent), explode("\n", $localcontent));
355$diff = filecheckCollapseContext($diff, 3);
356
357print '<div class="opacitymedium" style="margin-bottom:4px">';
358print img_picto('', 'split', 'class="pictofixedwidth"').dol_escape_htmltag($file).' &mdash; '.dol_escape_htmltag($originurl);
359print '</div>';
360
361print '<table class="filecheckdiff" style="width:100%;border-collapse:collapse;font-family:monospace;font-size:0.85em">';
362foreach ($diff as $line) {
363 $type = $line[0];
364 if ($type === '@') {
365 print '<tr><td style="background:#eef;color:#888;padding:1px 6px">&hellip;</td></tr>'."\n";
366 continue;
367 }
368 $bg = '';
369 $sign = ' ';
370 if ($type === '+') {
371 $bg = 'background:#e6ffed';
372 $sign = '+';
373 } elseif ($type === '-') {
374 $bg = 'background:#ffeef0';
375 $sign = '-';
376 }
377 print '<tr><td style="white-space:pre-wrap;word-break:break-all;padding:0 6px;'.$bg.'">';
378 print dol_escape_htmltag($sign.' '.$line[1]);
379 print '</td></tr>'."\n";
380}
381print '</table>';
382
384
385
392{
393 global $db;
394 if (is_object($db)) {
395 $db->close();
396 }
397 exit;
398}
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
filecheckCollapseContext($diff, $context=3)
Collapse long runs of unchanged context lines, keeping a few lines around each change.
llxFooterFragment()
Close the fragment output and stop the script.
if(!in_array( $algo, array( 'md5', 'sha256'), true)) filecheckLineDiff($a, $b)
Compute a line based diff between two arrays of lines, restricted to the changed region (common prefi...
filecheckMiddleDiff($a, $b)
Diff of the changed region.
filecheckLcs($a, $b)
Diff of two small arrays of lines using a classic LCS dynamic programming matrix.
filecheckFindAnchor($a, $b)
Find a line that appears exactly once in both arrays (a unique common "anchor"), choosing the candida...
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
$context
@method int call_trigger(string $triggerName, ?User $user)
Definition logout.php:42
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.