FOSSology  4.7.1
Open Source License Compliance by Open Source Software
ReuseFileDiffViewPlugin.php
Go to the documentation of this file.
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2026 Siemens AG
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 
14 namespace Fossology\Reuser;
15 
20 use Symfony\Component\HttpFoundation\Request;
21 use Symfony\Component\HttpFoundation\Response;
22 use Symfony\Component\HttpFoundation\RedirectResponse;
23 
30 {
31  const NAME = "reusediffview";
32 
34  private $dbManager;
35 
37  private $uploadDao;
38 
39  function __construct()
40  {
41  parent::__construct(self::NAME, [
42  self::TITLE => _("File Diff View"),
43  self::PERMISSION => Auth::PERM_READ,
44  self::DEPENDENCIES => ["browse"],
45  ]);
46  $this->dbManager = $this->getObject('db.manager');
47  $this->uploadDao = $this->getObject('dao.upload');
48  }
49 
54  protected function handle(Request $request)
55  {
56  $uploadId = intval($request->get('upload'));
57  $item = intval($request->get('item'));
58  $currentPfile = intval($request->get('currentPfile'));
59  $reusedPfile = intval($request->get('reusedPfile'));
60  $reuseId = intval($request->get('reuse', 0));
61 
62  if ($uploadId <= 0) {
63  return new Response(
64  '<div class="alert alert-danger">' . _("Invalid parameters for diff view") . '</div>',
65  Response::HTTP_BAD_REQUEST
66  );
67  }
68 
69  if ($currentPfile <= 0 || $reusedPfile <= 0) {
70  $reuseParam = $reuseId > 0 ? '&reuse=' . $reuseId : '';
71  return new RedirectResponse(
72  Traceback_uri() . "?mod=reusecompare&upload=$uploadId&item=$item$reuseParam"
73  );
74  }
75 
76  $groupId = Auth::getGroupId();
77  if (!$this->uploadDao->isAccessible($uploadId, $groupId)) {
78  return new Response(
79  '<div class="alert alert-danger">' . _("Permission Denied") . '</div>',
80  Response::HTTP_FORBIDDEN
81  );
82  }
83 
84  /* Get pfile data */
85  $currentFile = $this->getPfileData($currentPfile);
86  $reusedFile = $this->getPfileData($reusedPfile);
87 
88  if (!$currentFile || !$reusedFile) {
89  return new Response(
90  '<div class="alert alert-danger">' . _("File not found") . '</div>',
91  Response::HTTP_NOT_FOUND
92  );
93  }
94 
95  /* Generate diff */
96  $diffData = $this->generateDiff($currentPfile, $reusedPfile);
97 
98  /* Get scanner findings and clearing-decision license sets */
99  $curScanner = $this->getScannerFindings($currentPfile);
100  $reScanner = $this->getScannerFindings($reusedPfile);
101  list($curAdded, $curRemoved) = $this->getClearingDecisionSets($currentPfile, $uploadId);
102  list($reAdded, $reRemoved) = $this->getClearingDecisionSets($reusedPfile, $reuseId);
103 
104  $curScannerF = array_flip($curScanner);
105  $reScannerF = array_flip($reScanner);
106  $curAddedF = array_flip($curAdded);
107  $reAddedF = array_flip($reAdded);
108  $curRemovedF = array_flip($curRemoved);
109  $reRemovedF = array_flip($reRemoved);
110 
111  /* Collect all unique license names from every source */
112  $allNames = array_unique(array_merge(
113  $curScanner, $reScanner, $curAdded, $reAdded, $curRemoved, $reRemoved
114  ));
115  sort($allNames);
116 
117  /* Determine source for each license:
118  Priority: removed > both scanners > from-reuse > added-on-target */
119  $allLicenses = [];
120  foreach ($allNames as $name) {
121  $inCurScanner = isset($curScannerF[$name]);
122  $inReScanner = isset($reScannerF[$name]);
123  $inCurAdded = isset($curAddedF[$name]);
124  $inReAdded = isset($reAddedF[$name]);
125  $inCurRemoved = isset($curRemovedF[$name]);
126  $inReRemoved = isset($reRemovedF[$name]);
127 
128  $inReEffective = $inReScanner || $inReAdded;
129 
130  if ($inReRemoved) {
131  $status = 'removed-in-reuse';
132  } elseif ($inCurRemoved) {
133  $status = 'removed-in-current';
134  } elseif ($inCurScanner && $inReScanner) {
135  $status = 'both';
136  } elseif ($inReEffective && !$inCurScanner) {
137  $status = 'added-in-reuse';
138  } elseif ($inCurAdded && !$inReEffective) {
139  $status = 'added-in-current';
140  } else {
141  continue;
142  }
143  $allLicenses[$name] = ['status' => $status];
144  }
145  ksort($allLicenses);
146 
147  $tableName = $this->uploadDao->getUploadtreeTableName($uploadId);
148 
149  /* Look up the actual file's uploadtree_pk for the breadcrumb path */
150  $fileItem = $this->uploadDao->getUploadtreeIdFromPfile($uploadId, $currentPfile);
151  if (empty($fileItem)) {
152  $fileItem = $item;
153  }
154 
155  $micromenu = Dir2Browse("license", $fileItem, null, 0, "View",
156  -1, '', '', $tableName);
157 
158  $vars = [
159  "uploadId" => $uploadId,
160  "item" => $item,
161  "reuse" => $reuseId,
162  "currentFile" => $currentFile,
163  "reusedFile" => $reusedFile,
164  "diffData" => $diffData,
165  "allLicenses" => $allLicenses,
166  "pageMenu" => $micromenu,
167  "micromenu" => $micromenu,
168  ];
169 
170  $defaultVars = $this->mergeWithDefault([]);
171  $defaultVars['styles'] .= "<link rel='stylesheet' href='css/highlights.css'>\n";
172  $vars = array_merge($defaultVars, $vars);
173 
174  return $this->render("reusediffview.html.twig", $vars);
175  }
176 
182  private function generateDiff($pfileA, $pfileB)
183  {
184  $pathReused = RepPath($pfileB);
185  $pathCurrent = RepPath($pfileA);
186 
187  if (!$pathReused || !file_exists($pathReused) ||
188  !$pathCurrent || !file_exists($pathCurrent)) {
189  return [['type' => 'unchanged', 'line_number' => 1,
190  'content' => _('Unable to load files for diff.')]];
191  }
192 
193  $tmpReused = tempnam(sys_get_temp_dir(), 'fosdiff_');
194  $tmpCurrent = tempnam(sys_get_temp_dir(), 'fosdiff_');
195  if ($tmpReused === false || $tmpCurrent === false) {
196  return [['type' => 'unchanged', 'line_number' => 1,
197  'content' => _('Unable to create temp files for diff.')]];
198  }
199  if (!copy($pathReused, $tmpReused) || !copy($pathCurrent, $tmpCurrent)) {
200  @unlink($tmpReused);
201  @unlink($tmpCurrent);
202  return [['type' => 'unchanged', 'line_number' => 1,
203  'content' => _('Unable to copy files for diff.')]];
204  }
205 
206  $diffOutput = null;
207  $exitCode = -1;
208  $cmd = "diff -u " . escapeshellarg($tmpReused) . " " . escapeshellarg($tmpCurrent);
209  exec($cmd, $diffOutput, $exitCode);
210 
211  $contentCurrent = file_get_contents($pathCurrent);
212  $linesCurrent = explode("\n", $contentCurrent);
213 
214  unlink($tmpReused);
215  unlink($tmpCurrent);
216 
217  if ($exitCode === 0) {
218  $diff = [];
219  foreach ($linesCurrent as $i => $line) {
220  $diff[] = ['type' => 'unchanged', 'line_number' => $i + 1, 'content' => $line];
221  }
222  return $diff;
223  }
224 
225  if ($exitCode === 2 || empty($diffOutput)) {
226  return [['type' => 'unchanged', 'line_number' => 1,
227  'content' => _('Unable to compute diff for this file.')]];
228  }
229 
230  return $this->parseUnifiedDiff($diffOutput, $linesCurrent);
231  }
232 
237  private function parseUnifiedDiff(array $diffLines, array $linesCurrent)
238  {
239  /* Parse hunk headers to get line number offsets */
240  $hunks = [];
241  foreach ($diffLines as $raw) {
242  if (preg_match('/^@@ -(\d+),?\d* \+(\d+),?\d* @@/', $raw, $m)) {
243  $hunks[] = ['old_start' => (int)$m[1], 'new_start' => (int)$m[2]];
244  }
245  }
246 
247  if (empty($hunks)) {
248  return [];
249  }
250 
251  /* Build the per-hunk change data */
252  $hunkData = [];
253  $hunkIdx = -1;
254  $inHunk = false;
255 
256  foreach ($diffLines as $raw) {
257  if (preg_match('/^@@ -(\d+),?\d* \+(\d+),?\d* @@/', $raw)) {
258  $hunkIdx++;
259  $hunkData[$hunkIdx] = ['changes' => []];
260  $inHunk = true;
261  continue;
262  }
263  if (!$inHunk || strlen($raw) === 0) {
264  continue;
265  }
266  $prefix = $raw[0];
267  if ($prefix === ' ' || $prefix === '-' || $prefix === '+') {
268  $hunkData[$hunkIdx]['changes'][] = $raw;
269  }
270  }
271 
272  /* Build the complete diff by walking line-by-line through the current file */
273  $diff = [];
274  $currentLineIdx = 1;
275 
276  foreach ($hunkData as $hIdx => $hunk) {
277  $newStart = $hunks[$hIdx]['new_start'];
278 
279  /* Fill in unchanged lines before this hunk */
280  while ($currentLineIdx < $newStart) {
281  $diff[] = ['type' => 'unchanged', 'line_number' => $currentLineIdx,
282  'content' => $linesCurrent[$currentLineIdx - 1]];
283  $currentLineIdx++;
284  }
285 
286  /* Process the hunk changes */
287  foreach ($hunk['changes'] as $raw) {
288  $prefix = $raw[0];
289  $content = substr($raw, 1);
290  if ($prefix === ' ') {
291  $diff[] = ['type' => 'unchanged', 'line_number' => $currentLineIdx,
292  'content' => $content];
293  $currentLineIdx++;
294  } elseif ($prefix === '-') {
295  $diff[] = ['type' => 'deleted', 'line_number' => $currentLineIdx,
296  'content' => $content];
297  } else {
298  $diff[] = ['type' => 'added', 'line_number' => $currentLineIdx,
299  'content' => $content];
300  $currentLineIdx++;
301  }
302  }
303  }
304 
305  /* Fill in remaining unchanged lines after last hunk */
306  while ($currentLineIdx <= count($linesCurrent)) {
307  $diff[] = ['type' => 'unchanged', 'line_number' => $currentLineIdx,
308  'content' => $linesCurrent[$currentLineIdx - 1]];
309  $currentLineIdx++;
310  }
311 
312  return $diff;
313  }
314 
318  private function getPfileData($pfileId)
319  {
320  $sql = "SELECT pfile_pk, pfile_size, pfile_sha1, pfile_md5, pfile_sha256, pfile_mimetypefk
321  FROM pfile WHERE pfile_pk = $1";
322  $this->dbManager->prepare($stmt = __METHOD__ . '_pfile', $sql);
323  $row = $this->dbManager->getSingleRow($sql, [$pfileId], $stmt);
324  return $row ?: null;
325  }
326 
333  private function getScannerFindings($pfileId)
334  {
335  if (empty($pfileId)) {
336  return [];
337  }
338 
339  $sql = "SELECT DISTINCT lr.rf_shortname
340  FROM license_file lf
341  JOIN license_ref lr ON lr.rf_pk = lf.rf_fk
342  WHERE lf.pfile_fk = $1
343  AND lr.rf_shortname IS NOT NULL
344  AND lr.rf_shortname != 'Void'";
345  $this->dbManager->prepare($stmt = __METHOD__ . '_scan', $sql);
346  $res = $this->dbManager->execute($stmt, [$pfileId]);
347  $findings = [];
348  while ($row = $this->dbManager->fetchArray($res)) {
349  $findings[$row['rf_shortname']] = true;
350  }
351  $this->dbManager->freeResult($res);
352  return array_keys($findings);
353  }
354 
364  private function getClearingDecisionSets($pfileId, $uploadId)
365  {
366  $added = [];
367  $removed = [];
368 
369  if (empty($pfileId) || empty($uploadId)) {
370  return [$added, $removed];
371  }
372 
373  $uploadtreePk = $this->uploadDao->getUploadtreeIdFromPfile($uploadId, $pfileId);
374  if (empty($uploadtreePk)) {
375  return [$added, $removed];
376  }
377 
378  /* User-added licenses (type_fk=1 USER, removed=false or null) */
379  $sql = "SELECT DISTINCT lr.rf_shortname
380  FROM clearing_decision cd
381  JOIN clearing_decision_event cde ON cde.clearing_decision_fk = cd.clearing_decision_pk
382  JOIN clearing_event ce ON ce.clearing_event_pk = cde.clearing_event_fk
383  JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
384  WHERE cd.uploadtree_fk = $1
385  AND (ce.removed IS NULL OR ce.removed = false)
386  AND ce.type_fk = 1
387  AND lr.rf_shortname IS NOT NULL
388  AND lr.rf_shortname != 'Void'";
389  $this->dbManager->prepare($stmt = __METHOD__ . '_add', $sql);
390  $res = $this->dbManager->execute($stmt, [$uploadtreePk]);
391  while ($row = $this->dbManager->fetchArray($res)) {
392  $added[$row['rf_shortname']] = true;
393  }
394  $this->dbManager->freeResult($res);
395 
396  /* User-removed licenses (removed = true) */
397  $sql = "SELECT DISTINCT lr.rf_shortname
398  FROM clearing_decision cd
399  JOIN clearing_decision_event cde ON cde.clearing_decision_fk = cd.clearing_decision_pk
400  JOIN clearing_event ce ON ce.clearing_event_pk = cde.clearing_event_fk
401  JOIN license_ref lr ON lr.rf_pk = ce.rf_fk
402  WHERE cd.uploadtree_fk = $1
403  AND ce.removed = true
404  AND lr.rf_shortname IS NOT NULL
405  AND lr.rf_shortname != 'Void'";
406  $this->dbManager->prepare($stmt = __METHOD__ . '_rem', $sql);
407  $res = $this->dbManager->execute($stmt, [$uploadtreePk]);
408  while ($row = $this->dbManager->fetchArray($res)) {
409  $removed[$row['rf_shortname']] = true;
410  }
411  $this->dbManager->freeResult($res);
412 
413  return [array_keys($added), array_keys($removed)];
414  }
415 }
416 
417 register_plugin(new ReuseFileDiffViewPlugin());
Contains the constants and helpers for authentication of user.
Definition: Auth.php:24
static getGroupId()
Get the current user's group id.
Definition: Auth.php:80
render($templateName, $vars=null, $headers=null)
Display a two-column file diff view for reuse comparison, matching the EnhancedReuser diff view struc...
parseUnifiedDiff(array $diffLines, array $linesCurrent)
Dir2Browse($Mod, $UploadtreePk, $LinkLast=NULL, $ShowBox=1, $ShowMicro=NULL, $Enumerate=-1, $PreText='', $PostText='', $uploadtree_tablename="uploadtree")
Get an html linked string of a file browse path.
Definition: common-dir.php:263
Traceback_uri()
Get the URI without query to this location.
Definition: common-parm.php:97
RepPath($PfilePk, $Repo="files")
Given a pfile id, retrieve the pfile path.
Definition: common-repo.php:58
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
list_t type structure used to keep various lists. (e.g. there are multiple lists).
Definition: nomos.h:308