dolibarr 24.0.1
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 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
23if (!defined('NOTOKENRENEWAL')) {
24 define('NOTOKENRENEWAL', '1');
25}
26if (!defined('NOREQUIREMENU')) {
27 define('NOREQUIREMENU', '1');
28}
29if (!defined('NOREQUIREHTML')) {
30 define('NOREQUIREHTML', '1');
31}
32if (!defined('NOREQUIREAJAX')) {
33 define('NOREQUIREAJAX', '1');
34}
35
36// Load Dolibarr environment
37require '../../main.inc.php';
44require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
45require_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
46
47$langs->loadLangs(array("admin", "errors"));
48
49if (!$user->admin && !$user->hasRight('bockedlog', 'read')) {
51}
52
53$file = GETPOST('file', 'alphanohtml');
54$algo = GETPOST('algo', 'aZ09');
55$expectedhash = GETPOST('expectedhash', 'aZ09');
56
57if (!in_array($algo, array('md5', 'sha256'), true)) {
58 $algo = 'sha256';
59}
60
61
70function filecheckLineDiff($a, $b)
71{
72 $na = count($a);
73 $nb = count($b);
74
75 // Trim common prefix
76 $start = 0;
77 while ($start < $na && $start < $nb && $a[$start] === $b[$start]) {
78 $start++;
79 }
80 // Trim common suffix
81 $enda = $na - 1;
82 $endb = $nb - 1;
83 while ($enda >= $start && $endb >= $start && $a[$enda] === $b[$endb]) {
84 $enda--;
85 $endb--;
86 }
87
88 $result = array();
89 for ($i = 0; $i < $start; $i++) {
90 $result[] = array(' ', $a[$i]);
91 }
92
93 $midA = array_slice($a, $start, $enda - $start + 1);
94 $midB = array_slice($b, $start, $endb - $start + 1);
95
96 foreach (filecheckMiddleDiff($midA, $midB) as $d) {
97 $result[] = $d;
98 }
99
100 for ($i = $enda + 1; $i < $na; $i++) {
101 $result[] = array(' ', $a[$i]);
102 }
103
104 return $result;
105}
106
116function filecheckMiddleDiff($a, $b)
117{
118 $a = array_values($a);
119 $b = array_values($b);
120 $na = count($a);
121 $nb = count($b);
122
123 if ($na == 0 || $nb == 0) {
124 $result = array();
125 foreach ($a as $line) {
126 $result[] = array('-', $line);
127 }
128 foreach ($b as $line) {
129 $result[] = array('+', $line);
130 }
131 return $result;
132 }
133
134 // Small enough region: a direct LCS gives the optimal line-level diff.
135 if ($na * $nb <= 1000000) {
136 return filecheckLcs($a, $b);
137 }
138
139 // Large region: split on a unique common line (anchor) and recurse on both sides.
140 $anchor = filecheckFindAnchor($a, $b);
141 if ($anchor !== null) {
142 return array_merge(
143 filecheckMiddleDiff(array_slice($a, 0, $anchor[0]), array_slice($b, 0, $anchor[1])),
144 array(array(' ', $a[$anchor[0]])),
145 filecheckMiddleDiff(array_slice($a, $anchor[0] + 1), array_slice($b, $anchor[1] + 1))
146 );
147 }
148
149 // No anchor available in a very large region: fall back to a plain block replacement.
150 $result = array();
151 foreach ($a as $line) {
152 $result[] = array('-', $line);
153 }
154 foreach ($b as $line) {
155 $result[] = array('+', $line);
156 }
157 return $result;
158}
159
168function filecheckFindAnchor($a, $b)
169{
170 $countA = array();
171 foreach ($a as $line) {
172 $countA[$line] = (isset($countA[$line]) ? $countA[$line] : 0) + 1;
173 }
174 $countB = array();
175 $posB = array();
176 foreach ($b as $j => $line) {
177 $countB[$line] = (isset($countB[$line]) ? $countB[$line] : 0) + 1;
178 $posB[$line] = $j;
179 }
180
181 $middle = (int) (count($a) / 2);
182 $best = null;
183 $bestdist = -1;
184 foreach ($a as $i => $line) {
185 if ($countA[$line] == 1 && isset($countB[$line]) && $countB[$line] == 1) {
186 $dist = abs($i - $middle);
187 if ($bestdist < 0 || $dist < $bestdist) {
188 $bestdist = $dist;
189 $best = array($i, $posB[$line]);
190 }
191 }
192 }
193
194 return $best;
195}
196
204function filecheckLcs($a, $b)
205{
206 $na = count($a);
207 $nb = count($b);
208
209 // LCS length matrix
210 $lcs = array();
211 for ($i = 0; $i <= $na; $i++) {
212 $lcs[$i] = array_fill(0, $nb + 1, 0);
213 }
214 for ($i = $na - 1; $i >= 0; $i--) {
215 for ($j = $nb - 1; $j >= 0; $j--) {
216 if ($a[$i] === $b[$j]) {
217 $lcs[$i][$j] = $lcs[$i + 1][$j + 1] + 1;
218 } else {
219 $lcs[$i][$j] = max($lcs[$i + 1][$j], $lcs[$i][$j + 1]);
220 }
221 }
222 }
223
224 // Backtrack to build the diff
225 $result = array();
226 $i = 0;
227 $j = 0;
228 while ($i < $na && $j < $nb) {
229 if ($a[$i] === $b[$j]) {
230 $result[] = array(' ', $a[$i]);
231 $i++;
232 $j++;
233 } elseif ($lcs[$i + 1][$j] >= $lcs[$i][$j + 1]) {
234 $result[] = array('-', $a[$i]);
235 $i++;
236 } else {
237 $result[] = array('+', $b[$j]);
238 $j++;
239 }
240 }
241 while ($i < $na) {
242 $result[] = array('-', $a[$i]);
243 $i++;
244 }
245 while ($j < $nb) {
246 $result[] = array('+', $b[$j]);
247 $j++;
248 }
249
250 return $result;
251}
252
261{
262 $n = count($diff);
263 $keep = array_fill(0, $n, false);
264 for ($i = 0; $i < $n; $i++) {
265 if ($diff[$i][0] !== ' ') {
266 $from = max(0, $i - $context);
267 $to = min($n - 1, $i + $context);
268 for ($k = $from; $k <= $to; $k++) {
269 $keep[$k] = true;
270 }
271 }
272 }
273
274 $result = array();
275 $ingap = false;
276 for ($i = 0; $i < $n; $i++) {
277 if ($keep[$i]) {
278 $result[] = $diff[$i];
279 $ingap = false;
280 } elseif (!$ingap) {
281 $result[] = array('@', '');
282 $ingap = true;
283 }
284 }
285
286 return $result;
287}
288
289
290top_httphead('text/html');
291
292print '<!-- filecheck_diff.php fragment -->'."\n";
293
294// Validate the requested file: must be a relative path inside DOL_DOCUMENT_ROOT, with no traversal.
295$errormsg = '';
296$reallocal = '';
297if (empty($file) || $file[0] !== '/' || strpos($file, '..') !== false || !preg_match('/^[A-Za-z0-9_\/.\-]+$/', $file)) {
298 $errormsg = $langs->trans("ErrorBadValueForParameter", dol_escape_htmltag($file), "file");
299}
300
301if (empty($errormsg)) {
302 $reallocal = realpath(DOL_DOCUMENT_ROOT.$file);
303 if ($reallocal === false || strpos($reallocal, realpath(DOL_DOCUMENT_ROOT).'/') !== 0 || !is_file($reallocal)) {
304 $errormsg = $langs->trans("ErrorFileNotFound", $file);
305 }
306}
307// Only text files can be diffed
308if (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)) {
309 $errormsg = $langs->trans("DiffNotAvailableForBinaryFiles");
310}
311
312if (!empty($errormsg)) {
313 print '<div class="warning">'.$errormsg.'</div>';
315}
316
317// Build the URL of the original file for the running version.
318// For a stable version (eg 24.0.0) the matching git tag exists, so we compare against that tag.
319// For an alpha/beta/rc version no tag exists yet, so we compare against the develop branch.
320$baseurl = getDolGlobalString('MAIN_FILECHECK_DIFF_BASEURL', 'https://raw.githubusercontent.com/Dolibarr/dolibarr');
321if (preg_match('/alpha|beta|rc/i', DOL_VERSION)) {
322 $ref = 'develop';
323} else {
324 $ref = DOL_VERSION;
325}
326$ref = getDolGlobalString('MAIN_FILECHECK_DIFF_REF', $ref);
327$originurl = $baseurl.'/'.$ref.'/htdocs'.$file;
328
329$res = getURLContent($originurl, 'GET', '', 1, array(), array('http', 'https'), 0); // Accept http or https links on external remote server only.
330if (!empty($res['curl_error_no']) || (isset($res['http_code']) && !in_array((int) $res['http_code'], array(0, 200), true))) {
331 print '<div class="warning">'.$langs->trans("CouldNotFetchOriginalFile").': '.dol_escape_htmltag($originurl);
332 print ' ('.dol_escape_htmltag((string) (empty($res['http_code']) ? $res['curl_error_msg'] : $res['http_code'])).')</div>';
334}
335
336$origincontent = (string) $res['content'];
337$localcontent = (string) file_get_contents($reallocal);
338
339// Confirm the fetched original is the genuine reference file expected by the signature.
340$verified = true;
341if (!empty($expectedhash)) {
342 $verified = (hash($algo, $origincontent) === $expectedhash);
343}
344if (!$verified) {
345 print '<div class="warning">'.$langs->trans("OriginalFileChecksumMismatch").'</div>';
346}
347
348if ($origincontent === $localcontent) {
349 print '<div class="opacitymedium">'.$langs->trans("NoDifferenceFound").'</div>';
351}
352
353$diff = filecheckLineDiff(explode("\n", $origincontent), explode("\n", $localcontent));
354$diff = filecheckCollapseContext($diff, 3);
355
356print '<div class="opacitymedium" style="margin-bottom:4px">';
357print img_picto('', 'split', 'class="pictofixedwidth"').dol_escape_htmltag($file).' &mdash; '.dol_escape_htmltag($originurl);
358print '</div>';
359
360print '<table class="filecheckdiff" style="width:100%;border-collapse:collapse;font-family:monospace;font-size:0.85em">';
361foreach ($diff as $line) {
362 $type = $line[0];
363 if ($type === '@') {
364 print '<tr><td style="background:#eef;color:#888;padding:1px 6px">&hellip;</td></tr>'."\n";
365 continue;
366 }
367 $bg = '';
368 $sign = ' ';
369 if ($type === '+') {
370 $bg = 'background:#e6ffed';
371 $sign = '+';
372 } elseif ($type === '-') {
373 $bg = 'background:#ffeef0';
374 $sign = '-';
375 }
376 print '<tr><td style="white-space:pre-wrap;word-break:break-all;padding:0 6px;'.$bg.'">';
377 print dol_escape_htmltag($sign.' '.$line[1]);
378 print '</td></tr>'."\n";
379}
380print '</table>';
381
383
384
391{
392 global $db;
393 if (is_object($db)) {
394 $db->close();
395 }
396 exit;
397}
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.