FOSSology  4.7.1
Open Source License Compliance by Open Source Software
MultiComparePlugin.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 
13 use Symfony\Component\HttpFoundation\Request;
14 use Symfony\Component\HttpFoundation\Response;
15 
22 {
23  const NAME = 'multicompare';
24 
26  private $dbManager;
28  private $uploadDao;
30  private $agentDao;
31 
32  public function __construct()
33  {
34  parent::__construct(self::NAME, [
35  self::TITLE => _("Multi-Component Comparison"),
36  self::DEPENDENCIES => ["browse", "view"],
37  self::PERMISSION => Auth::PERM_READ,
38  self::REQUIRES_LOGIN => true,
39  ]);
40  $this->dbManager = $this->getObject('db.manager');
41  $this->uploadDao = $this->getObject('dao.upload');
42  $this->agentDao = $this->getObject('dao.agent');
43  }
44 
45  // ── DB setup ───────────────────────────────────────────────────────────
46 
47  private function createFilePickerMultiTable(): void
48  {
49  if ($this->dbManager->existsTable('file_picker_multi')) {
50  return;
51  }
52  $this->dbManager->queryOnce(
53  "CREATE TABLE file_picker_multi (
54  file_picker_multi_pk serial NOT NULL PRIMARY KEY,
55  user_fk integer NOT NULL,
56  items text NOT NULL,
57  last_access_date date NOT NULL
58  )",
59  __METHOD__
60  );
61  }
62 
63  // ── Data helpers ───────────────────────────────────────────────────────
64 
70  private function GetTreeInfo(int $uploadtree_pk): array
71  {
72  /* Bootstrap: query the parent table to get the upload metadata and table
73  * name in a single round-trip. PostgreSQL inheritance ensures rows stored
74  * in uploadtree_a are found here via PK index. */
75  $TreeInfo = $this->dbManager->getSingleRow(
76  "SELECT ut.*, u.uploadtree_tablename, u.upload_filename
77  FROM uploadtree ut
78  JOIN upload u ON u.upload_pk = ut.upload_fk
79  WHERE ut.uploadtree_pk = \$1",
80  [$uploadtree_pk], __METHOD__ . '.bootstrap'
81  );
82  if (!$TreeInfo) {
83  return [];
84  }
85 
86  /* Re-fetch lft/rgt and key columns from the upload-specific table so
87  * subtree queries in buildHistData and AddDataStr operate on that table
88  * directly rather than through the inheritance umbrella. */
89  $tableName = $TreeInfo['uploadtree_tablename'];
90  if ($tableName !== 'uploadtree') {
91  $specific = $this->dbManager->getSingleRow(
92  "SELECT lft, rgt, ufile_mode, ufile_name, pfile_fk, parent,
93  uploadtree_pk, upload_fk
94  FROM $tableName WHERE uploadtree_pk = \$1",
95  [$uploadtree_pk], __METHOD__ . ".$tableName"
96  );
97  if ($specific) {
98  $TreeInfo = array_merge($TreeInfo, $specific);
99  }
100  }
101 
102  $upload_pk = intval($TreeInfo['upload_fk']);
103  $TreeInfo['display_name'] = !empty($TreeInfo['upload_filename'])
104  ? basename($TreeInfo['upload_filename'])
105  : $TreeInfo['ufile_name'];
106 
107  /* Fetch all 5 agent PKs in a single SQL round-trip instead of 5 separate
108  * agentARSList() calls (each of which issues its own existsTable check +
109  * ARS query). The static cache avoids repeating existsTable checks across
110  * multiple GetTreeInfo calls in the same request. */
111  $agentPks = $this->batchAgentPks($upload_pk);
112  $TreeInfo = array_merge($TreeInfo, $agentPks);
113  $TreeInfo['agent_pk'] = $TreeInfo['nomos_agent_pk'];
114 
115  return $TreeInfo;
116  }
117 
125  private function batchAgentPks(int $uploadPk): array
126  {
127  static $existsCache = [];
128 
129  $agentDef = [
130  'nomos_pk' => 'nomos_ars',
131  'monk_pk' => 'monk_ars',
132  'ojo_pk' => 'ojo_ars',
133  'copyright_pk' => 'copyright_ars',
134  'ecc_pk' => 'ecc_ars',
135  ];
136 
137  $parts = [];
138  $existMask = '';
139  foreach ($agentDef as $alias => $table) {
140  if (!array_key_exists($table, $existsCache)) {
141  $existsCache[$table] = $this->dbManager->existsTable($table);
142  }
143  if ($existsCache[$table]) {
144  $parts[] = "(SELECT a.agent_fk FROM $table a"
145  . " JOIN agent ON agent_pk=a.agent_fk"
146  . " WHERE a.upload_fk=\$1 AND a.ars_success AND agent_enabled"
147  . " ORDER BY agent_ts DESC LIMIT 1) AS $alias";
148  $existMask .= '1';
149  } else {
150  $parts[] = "NULL::integer AS $alias";
151  $existMask .= '0';
152  }
153  }
154 
155  $stmt = __METHOD__ . '.' . $existMask;
156  $this->dbManager->prepare($stmt, "SELECT " . implode(",\n", $parts));
157  $res = $this->dbManager->execute($stmt, [$uploadPk]);
158  $row = $this->dbManager->fetchArray($res) ?: [];
159  $this->dbManager->freeResult($res);
160 
161  return [
162  'nomos_agent_pk' => intval($row['nomos_pk'] ?? 0),
163  'monk_agent_pk' => intval($row['monk_pk'] ?? 0),
164  'ojo_agent_pk' => intval($row['ojo_pk'] ?? 0),
165  'copyright_agent_pk' => intval($row['copyright_pk'] ?? 0),
166  'ecc_agent_pk' => intval($row['ecc_pk'] ?? 0),
167  ];
168  }
169 
175  private function AddDataStr(array $treeInfo, array &$children, string $mode): void
176  {
177  if ($mode === 'license') {
178  $licAgentPks = array_values(array_filter([
179  $treeInfo['nomos_agent_pk'],
180  $treeInfo['monk_agent_pk'],
181  $treeInfo['ojo_agent_pk'],
182  ]));
183 
184  /* Batch-fetch licenses for all leaf files in this column at once */
185  $licByPfile = [];
186  if (!empty($licAgentPks)) {
187  $pfileUniq = array_values(array_unique(array_filter(
188  array_map(function ($c) {
189  return intval($c['pfile_fk'] ?? 0);
190  }, $children)
191  )));
192  if (!empty($pfileUniq)) {
193  $params = [];
194  $agentPh = [];
195  foreach ($licAgentPks as $apk) {
196  $params[] = $apk;
197  $agentPh[] = '$' . count($params);
198  }
199  $pfilePh = [];
200  foreach ($pfileUniq as $pf) {
201  $params[] = $pf;
202  $pfilePh[] = '$' . count($params);
203  }
204  $agentIn = implode(',', $agentPh);
205  $pfileIn = implode(',', $pfilePh);
206  $stmt = __METHOD__ . ".licbatch." . implode('_', $licAgentPks) . ".p" . count($pfileUniq);
207  $this->dbManager->prepare($stmt,
208  "SELECT lf.pfile_fk, lr.rf_pk, lr.rf_shortname
209  FROM ONLY license_ref lr, license_file lf
210  WHERE lf.rf_fk = lr.rf_pk
211  AND lf.agent_fk IN ($agentIn)
212  AND lf.pfile_fk IN ($pfileIn)"
213  );
214  $res = $this->dbManager->execute($stmt, $params);
215  while ($row = $this->dbManager->fetchArray($res)) {
216  $pf = intval($row['pfile_fk']);
217  /* rf_pk key deduplicates same license found by multiple agents */
218  $licByPfile[$pf][intval($row['rf_pk'])] = $row['rf_shortname'];
219  }
220  $this->dbManager->freeResult($res);
221  }
222  }
223 
224  foreach ($children as &$child) {
225  $pf = intval($child['pfile_fk'] ?? 0);
226  if ($pf > 0) {
227  $dataarray = $licByPfile[$pf] ?? [];
228  } else {
229  /* Directory: fall back to per-item call to preserve subtree aggregation */
230  $dataarray = [];
231  foreach ($licAgentPks as $agentPk) {
232  $dataarray += GetFileLicenses($agentPk, 0, $child['uploadtree_pk'],
233  $treeInfo['uploadtree_tablename']);
234  }
235  }
236  $child['dataarray'] = $dataarray;
237  $child['datastr'] = implode(", ", $dataarray);
238  if (empty($child['datastr'])) {
239  $child['datastr'] = "No_license_found";
240  $child['dataarray'] = ["No_license_found" => "No_license_found"];
241  }
242  }
243  unset($child);
244 
245  } elseif ($mode === 'copyright' || $mode === 'ecc') {
246  $table = ($mode === 'ecc') ? 'ecc' : 'copyright';
247  $agentPk = ($mode === 'ecc')
248  ? $treeInfo['ecc_agent_pk']
249  : $treeInfo['copyright_agent_pk'];
250 
251  /* Batch-fetch all pfile content in one query */
252  $dataByPfile = [];
253  if ($agentPk > 0) {
254  $pfileUniq = array_values(array_unique(array_filter(
255  array_map(function ($c) {
256  return intval($c['pfile_fk'] ?? 0);
257  }, $children)
258  )));
259  if (!empty($pfileUniq)) {
260  $params = [intval($agentPk)];
261  $pfilePh = [];
262  foreach ($pfileUniq as $pf) {
263  $params[] = $pf;
264  $pfilePh[] = '$' . count($params);
265  }
266  $pfileIn = implode(',', $pfilePh);
267  $stmt = __METHOD__ . ".$table.batch.p" . count($pfileUniq);
268  $this->dbManager->prepare($stmt,
269  "SELECT pfile_fk, content FROM $table
270  WHERE agent_fk=\$1 AND pfile_fk IN ($pfileIn)
271  AND content IS NOT NULL AND content!=''
272  ORDER BY pfile_fk, content"
273  );
274  $res = $this->dbManager->execute($stmt, $params);
275  while ($row = $this->dbManager->fetchArray($res)) {
276  $pf = intval($row['pfile_fk']);
277  $dataByPfile[$pf][$row['content']] = $row['content'];
278  }
279  $this->dbManager->freeResult($res);
280  }
281  }
282 
283  foreach ($children as &$child) {
284  $pf = intval($child['pfile_fk'] ?? 0);
285  $dataarray = $dataByPfile[$pf] ?? [];
286  $child['dataarray'] = $dataarray;
287  $child['datastr'] = implode(", ", $dataarray);
288  }
289  unset($child);
290  }
291  }
292 
293  // ── Filters ────────────────────────────────────────────────────────────
294 
295  private function FilterN(string $filter, array &$Master, int $N): void
296  {
297  switch ($filter) {
298  case 'samehash':
299  $this->filterSamehashN($Master, $N);
300  break;
301  case 'samelic':
302  $this->filterSamehashN($Master, $N);
303  $this->filterSamelicN($Master, $N);
304  break;
305  case 'samelicfuzzy':
306  $this->filterSamehashN($Master, $N);
307  $this->filterSamelicFuzzyN($Master, $N);
308  break;
309  case 'nolics':
310  $this->filterSamehashN($Master, $N);
311  $this->filterSamelicFuzzyN($Master, $N);
312  $this->filterNolicsN($Master);
313  break;
314  case 'allsame':
315  $this->filterSamehashN($Master, $N);
316  $this->filterAllsame($Master, $N);
317  break;
318  }
319  }
320 
325  private function filterSamehashN(array &$Master, int $N): void
326  {
327  foreach ($Master as $key => $row) {
328  $pfiles = [];
329  foreach ($row as $child) {
330  if (!empty($child) && !empty($child['pfile_fk'])) {
331  $pfiles[] = $child['pfile_fk'];
332  }
333  }
334  if (count($pfiles) === $N && count(array_unique($pfiles)) === 1) {
335  unset($Master[$key]);
336  }
337  }
338  }
339 
344  private function filterSamelicN(array &$Master, int $N): void
345  {
346  foreach ($Master as $key => $row) {
347  $present = array_filter($row, fn($c) => !empty($c));
348  if (count($present) !== $N) {
349  continue;
350  }
351  if (count(array_unique(array_column($present, 'ufile_name'))) === 1 &&
352  count(array_unique(array_column($present, 'datastr'))) === 1) {
353  unset($Master[$key]);
354  }
355  }
356  }
357 
361  private function filterSamelicFuzzyN(array &$Master, int $N): void
362  {
363  foreach ($Master as $key => $row) {
364  $present = array_filter($row, fn($c) => !empty($c));
365  if (count($present) !== $N) {
366  continue;
367  }
368  if (count(array_unique(array_column($present, 'fuzzyname'))) === 1 &&
369  count(array_unique(array_column($present, 'datastr'))) === 1) {
370  unset($Master[$key]);
371  }
372  }
373  }
374 
380  private function filterNolicsN(array &$Master): void
381  {
382  foreach ($Master as $key => $row) {
383  $present = array_filter($row, fn($c) => !empty($c));
384  if (empty($present)) {
385  continue;
386  }
387  $allEmpty = true;
388  foreach ($present as $child) {
389  $ds = $child['datastr'];
390  if ($ds !== '' && $ds !== 'No_license_found') {
391  $allEmpty = false;
392  break;
393  }
394  }
395  if ($allEmpty) {
396  unset($Master[$key]);
397  }
398  }
399  }
400 
404  private function filterAllsame(array &$Master, int $N): void
405  {
406  foreach ($Master as $key => $row) {
407  $present = array_filter($row, fn($c) => !empty($c));
408  if (count($present) !== $N) {
409  continue;
410  }
411  if (count(array_unique(array_column($present, 'datastr'))) === 1) {
412  unset($Master[$key]);
413  }
414  }
415  }
416 
417  // ── Table row rendering ────────────────────────────────────────────────
418 
422  private function ChildElt(array $child, int $colIdx, array $row,
423  array $treeInfoArray, string $mode, int $baseline): string
424  {
425  $dataarray = $child['dataarray'] ?? [];
426 
427  $refKeys = [];
428  if ($baseline > 0) {
429  $bIdx = $baseline - 1;
430  if (isset($row[$bIdx]) && !empty($row[$bIdx]['dataarray'])) {
431  $refKeys = array_keys($row[$bIdx]['dataarray']);
432  }
433  } else {
434  foreach ($row as $c => $other) {
435  if ($c === $colIdx || empty($other) || empty($other['dataarray'])) {
436  continue;
437  }
438  foreach (array_keys($other['dataarray']) as $k) {
439  $refKeys[] = $k;
440  }
441  }
442  $refKeys = array_unique($refKeys);
443  }
444  $refKeySet = array_flip($refKeys);
445 
446  $badges = [];
447  foreach ($dataarray as $k => $val) {
448  $missing = !empty($refKeys) && !isset($refKeySet[$k]);
449  if ($missing) {
450  $badges[] = "<span class='badge badge-pill'"
451  . " style='background-color:#ffd6cc;color:#333;font-weight:normal'>"
452  . htmlspecialchars($val) . "</span>";
453  } else {
454  $badges[] = "<span class='badge badge-pill badge-light border'>"
455  . htmlspecialchars($val) . "</span>";
456  }
457  }
458  $dataStr = implode(" ", $badges);
459 
460  $ColStr = "<td class='align-top py-1' id='c{$child['uploadtree_pk']}'>";
461  $ColStr .= $child['linkurl'] ?? htmlspecialchars($child['ufile_name']);
462  if (!empty($dataStr)) {
463  $ColStr .= "<div class='mt-1 ml-1'>$dataStr</div>";
464  }
465  $ColStr .= "</td>";
466 
467  $agentPk = $treeInfoArray[$colIdx]['agent_pk'] ?? 0;
468  $uploadtree_tablename = $treeInfoArray[$colIdx]['uploadtree_tablename'] ?? 'uploadtree';
469  $ColStr .= "<td class='align-top py-1' style='white-space:nowrap'>";
470  $uniqueTagArray = [];
471  $ColStr .= FileListLinks(
472  $child['upload_fk'], $child['uploadtree_pk'],
473  $agentPk, $child['pfile_fk'], true,
474  $uniqueTagArray, $uploadtree_tablename
475  );
476  $ColStr .= "</td>";
477 
478  return $ColStr;
479  }
480 
484  private function ItemComparisonRows(array $Master, array $treeInfoArray,
485  string $mode, int $baseline, string $view): string
486  {
487  $N = count($treeInfoArray);
488 
489  if ($view === 'matrix') {
490  return $this->FileMatrixRows($Master, $N);
491  }
492 
493  $parts = [];
494  foreach ($Master as $row) {
495  $parts[] = "<tr>";
496  for ($c = 0; $c < $N; $c++) {
497  if ($c > 0) {
498  $parts[] = "<td class='border-left border-success p-0' style='width:3px'></td>";
499  }
500  if (empty($row[$c])) {
501  $parts[] = "<td class='text-muted py-1'>&mdash;</td><td></td>";
502  } else {
503  $parts[] = $this->ChildElt($row[$c], $c, $row, $treeInfoArray, $mode, $baseline);
504  }
505  }
506  $parts[] = "</tr>";
507  }
508  return implode("", $parts);
509  }
510 
511  private function FileMatrixRows(array $Master, int $N): string
512  {
513  $parts = [];
514  foreach ($Master as $row) {
515  $parts[] = "<tr>";
516  $firstName = "";
517  for ($c = 0; $c < $N && empty($firstName); $c++) {
518  if (!empty($row[$c])) {
519  $firstName = htmlspecialchars($row[$c]['ufile_name']);
520  }
521  }
522  $parts[] = "<td class='py-1'>$firstName</td>";
523 
524  /* Detect whether all present columns share the same pfile_fk */
525  $pfiles = [];
526  for ($c = 0; $c < $N; $c++) {
527  if (!empty($row[$c]) && !empty($row[$c]['pfile_fk'])) {
528  $pfiles[] = $row[$c]['pfile_fk'];
529  }
530  }
531  $allSameHash = count($pfiles) >= 2 && count(array_unique($pfiles)) === 1;
532 
533  for ($c = 0; $c < $N; $c++) {
534  if (!empty($row[$c])) {
535  /* Green = content differs or unique to this column; gray = identical everywhere */
536  $parts[] = $allSameHash
537  ? "<td class='text-center py-1'><span class='badge badge-light border text-muted' title='identical'>&#10004;</span></td>"
538  : "<td class='text-center py-1'><span class='badge badge-success' title='differs'>&#10004;</span></td>";
539  } else {
540  $parts[] = "<td class='text-center py-1 text-muted'>&mdash;</td>";
541  }
542  }
543  $parts[] = "</tr>";
544  }
545  return implode("", $parts);
546  }
547 
548  // ── Twig data builders ─────────────────────────────────────────────────
549 
555  private function buildSummaryData(array $Master, int $N,
556  array $treeInfoArray, string $mode): array
557  {
558  $uniqueFiles = array_fill(0, $N, 0);
559  $missingFiles = array_fill(0, $N, 0);
560  $uniqueEntries = array_fill(0, $N, []);
561 
562  foreach ($Master as $row) {
563  /* Which columns have data in this row? */
564  $presentSet = [];
565  for ($c = 0; $c < $N; $c++) {
566  if (!empty($row[$c])) {
567  $presentSet[$c] = true;
568  }
569  }
570  $nPresent = count($presentSet);
571 
572  for ($c = 0; $c < $N; $c++) {
573  if (!isset($presentSet[$c])) {
574  $missingFiles[$c]++;
575  }
576  }
577  if ($nPresent === 1) {
578  $uniqueFiles[key($presentSet)]++;
579  }
580 
581  /* Count how many columns contain each data key — O(N·K) per row */
582  $keyColCount = [];
583  foreach ($presentSet as $c => $_) {
584  foreach ($row[$c]['dataarray'] ?? [] as $k => $val) {
585  $keyColCount[$k] = ($keyColCount[$k] ?? 0) + 1;
586  }
587  }
588 
589  /* Keys appearing in exactly one column are unique to that column */
590  foreach ($presentSet as $c => $_) {
591  foreach ($row[$c]['dataarray'] ?? [] as $k => $val) {
592  if ($keyColCount[$k] === 1) {
593  $uniqueEntries[$c][$k] = $val;
594  }
595  }
596  }
597  }
598 
599  $summaryData = [];
600  for ($c = 0; $c < $N; $c++) {
601  $uniqList = array_unique($uniqueEntries[$c]);
602  $uniqCount = count($uniqList);
603  $shown = array_map('htmlspecialchars', array_slice($uniqList, 0, 10));
604  $summaryData[] = [
605  'name' => htmlspecialchars($treeInfoArray[$c]['display_name'] ?? ("Col " . ($c + 1))),
606  'uniqueFiles' => $uniqueFiles[$c],
607  'missingFiles' => $missingFiles[$c],
608  'uniqueEntries' => $shown,
609  'moreCount' => max(0, $uniqCount - 10),
610  ];
611  }
612  return $summaryData;
613  }
614 
619  private function buildHistData(array $items, array $treeInfoArray, string $mode): array
620  {
621  $N = count($items);
622  $histData = [];
623 
624  for ($c = 0; $c < $N; $c++) {
625  $treeInfo = $treeInfoArray[$c];
626 
627  if ($mode === 'license') {
628  $licAgentPks = array_values(array_filter([
629  $treeInfo['nomos_agent_pk'],
630  $treeInfo['monk_agent_pk'],
631  $treeInfo['ojo_agent_pk'],
632  ]));
633  if (empty($licAgentPks)) {
634  continue;
635  }
636  $lft = intval($treeInfo['lft']);
637  $rgt = intval($treeInfo['rgt']);
638  $upPk = intval($treeInfo['upload_fk']);
639  $table = $treeInfo['uploadtree_tablename'];
640 
641  $params = [$lft, $rgt];
642  $upClause = '';
643  if ($table === 'uploadtree_a' || $table === 'uploadtree') {
644  $params[] = $upPk;
645  $upClause = "upload_fk=\$" . count($params) . " AND ";
646  }
647  /* Build IN-list for the agent PKs */
648  $agentPlaceholders = [];
649  foreach ($licAgentPks as $apk) {
650  $params[] = $apk;
651  $agentPlaceholders[] = "\$" . count($params);
652  }
653  $agentIn = implode(",", $agentPlaceholders);
654  $sql = "SELECT rf_shortname AS entry, count(DISTINCT pfile_fk) AS cnt
655  FROM ONLY license_ref, license_file,
656  (SELECT DISTINCT(pfile_fk) AS PF FROM $table
657  WHERE {$upClause}{$table}.lft BETWEEN \$1 AND \$2) AS SS
658  WHERE PF=pfile_fk AND agent_fk IN ($agentIn) AND rf_fk=rf_pk
659  GROUP BY rf_shortname ORDER BY cnt DESC";
660  $stmt = __METHOD__ . ".lic.$table.$c." . implode("_", $licAgentPks);
661  $this->dbManager->prepare($stmt, $sql);
662  $res = $this->dbManager->execute($stmt, $params);
663  while ($row = $this->dbManager->fetchArray($res)) {
664  $histData[$row['entry']][$c] = (int)$row['cnt'];
665  }
666  $this->dbManager->freeResult($res);
667 
668  } elseif ($mode === 'copyright' || $mode === 'ecc') {
669  $table = ($mode === 'ecc') ? 'ecc' : 'copyright';
670  $agentPk = ($mode === 'ecc')
671  ? intval($treeInfo['ecc_agent_pk'])
672  : intval($treeInfo['copyright_agent_pk']);
673  if ($agentPk == 0) {
674  continue;
675  }
676  $lft = intval($treeInfo['lft']);
677  $rgt = intval($treeInfo['rgt']);
678  $upPk = intval($treeInfo['upload_fk']);
679  $utbl = $treeInfo['uploadtree_tablename'];
680 
681  $params = [$agentPk, $lft, $rgt];
682  $upClause = '';
683  if ($utbl === 'uploadtree_a' || $utbl === 'uploadtree') {
684  $params[] = $upPk;
685  $upClause = "AND UT.upload_fk=\$" . count($params);
686  }
687  $sql = "SELECT C.content AS entry, count(*) AS cnt
688  FROM $table C
689  INNER JOIN $utbl UT ON C.pfile_fk = UT.pfile_fk
690  WHERE C.agent_fk=\$1
691  AND C.content IS NOT NULL AND C.content!=''
692  AND UT.lft BETWEEN \$2 AND \$3
693  $upClause
694  GROUP BY C.content ORDER BY cnt DESC LIMIT 100";
695  $stmt = __METHOD__ . ".$mode.$utbl.$c";
696  $this->dbManager->prepare($stmt, $sql);
697  $res = $this->dbManager->execute($stmt, $params);
698  while ($row = $this->dbManager->fetchArray($res)) {
699  $histData[$row['entry']][$c] = (int)$row['cnt'];
700  }
701  $this->dbManager->freeResult($res);
702  }
703  }
704 
705  /* Sort by total count across all columns, descending */
706  uasort($histData, function (array $a, array $b): int {
707  return array_sum($b) - array_sum($a);
708  });
709  return $histData;
710  }
711 
712  // ── Request handler ────────────────────────────────────────────────────
713 
714  protected function handle(Request $request): Response
715  {
716  $this->createFilePickerMultiTable();
717 
718  $itemsRaw = $request->get('items', '');
719  $items = [];
720  if (!empty($itemsRaw)) {
721  $items = array_values(array_unique(array_filter(
722  array_map('intval', explode(',', $itemsRaw)),
723  fn($v) => $v > 0
724  )));
725  }
726 
727  $filter = $request->get('filter', 'samehash');
728  $mode = $request->get('mode', 'license');
729  $view = $request->get('view', 'diff');
730  $baseline = (int)($request->get('baseline', 0));
731  $updcache = (int)($request->get('updcache', 0));
732 
733  if (!in_array($mode, ['license', 'copyright', 'ecc'])) {
734  $mode = 'license';
735  }
736  if (!in_array($view, ['diff', 'matrix'])) {
737  $view = 'diff';
738  }
739 
740  if (count($items) < 2) {
741  return $this->flushContent(
742  "<h3>" . _("Please select at least 2 components to compare.") . "</h3>"
743  . "<p><a href='javascript:history.back()'>" . _("Go back") . "</a></p>"
744  );
745  }
746 
747  foreach ($items as $idx => $itemPk) {
748  /* Lightweight query: only upload_fk is needed for the access check.
749  * This must happen before the cache check to avoid serving cached pages
750  * to users who lost access since the page was cached. */
751  $permRow = $this->dbManager->getSingleRow(
752  "SELECT upload_fk FROM uploadtree WHERE uploadtree_pk = \$1",
753  [$itemPk], __METHOD__ . '.perm'
754  );
755  if (!$permRow || !$this->uploadDao->isAccessible($permRow['upload_fk'], Auth::getGroupId())) {
756  return $this->flushContent(
757  "<h2>" . _("Permission Denied") . " (item " . ($idx + 1) . ")</h2>"
758  );
759  }
760  }
761 
762  /* Freeze support: replace one column's item with a frozen pk.
763  * $freezeCol is 1-based; $clickedCol is 0-based from the link's &col= param.
764  * Default -1 is a sentinel meaning "toolbar navigation, not a column click",
765  * so the freeze is always preserved on filter/mode/view/baseline changes. */
766  $freezeCol = (int)($request->get('freeze', 0));
767  $frozenItem = (int)($request->get('itemf', 0));
768  $clickedCol = (int)($request->get('col', -1));
769  if ($freezeCol > 0 && $frozenItem > 0 && ($freezeCol - 1) !== $clickedCol) {
770  $colIdx0 = $freezeCol - 1;
771  if (isset($items[$colIdx0])) {
772  $frozenUploadFk = $this->dbManager->getSingleRow(
773  "SELECT upload_fk FROM uploadtree WHERE uploadtree_pk = \$1",
774  [$frozenItem], __METHOD__ . '.freeze'
775  )['upload_fk'] ?? 0;
776  if ($frozenUploadFk && $this->uploadDao->isAccessible($frozenUploadFk, Auth::getGroupId())) {
777  $items[$colIdx0] = $frozenItem;
778  }
779  }
780  }
781 
782  $cacheKey = "?mod=" . self::NAME
783  . "&items=" . implode(",", $items)
784  . "&filter=$filter&mode=$mode&view=$view&baseline=$baseline"
785  . ($freezeCol > 0 ? "&freeze=$freezeCol&itemf=$frozenItem" : "");
786 
787  if ($updcache) {
788  ReportCachePurgeByKey($cacheKey);
789  } else {
790  $cached = ReportCacheGet($cacheKey);
791  if (!empty($cached)) {
792  return new Response($cached, Response::HTTP_OK, $this->getDefaultHeaders());
793  }
794  }
795 
796  /* ── Build data ─────────────────────────────────────────────────── */
797  $treeInfoArray = [];
798  $agentPks = [];
799  $ErrMsg = "";
800  $N = count($items);
801 
802  foreach ($items as $c => $itemPk) {
803  $treeInfo = $this->GetTreeInfo($itemPk);
804  if (empty($treeInfo)) {
805  return $this->flushContent(
806  "<div class='alert alert-danger'>"
807  . sprintf(_("Could not load data for item %d. The item may have been deleted."), $itemPk)
808  . "</div>"
809  );
810  }
811  $agentPk = 0;
812  if ($mode === 'license') {
813  $agentPk = $treeInfo['nomos_agent_pk'] ?: $treeInfo['monk_agent_pk'] ?: $treeInfo['ojo_agent_pk'];
814  if ($agentPk == 0 && empty($ErrMsg)) {
815  $ErrMsg = sprintf(
816  _("No license scan data for component %d (%s). Schedule a nomos, monk, or ojo scan first."),
817  $c + 1, htmlspecialchars($treeInfo['display_name'])
818  );
819  }
820  } elseif ($mode === 'copyright') {
821  $agentPk = $treeInfo['copyright_agent_pk'];
822  } elseif ($mode === 'ecc') {
823  $agentPk = $treeInfo['ecc_agent_pk'];
824  }
825  $treeInfo['agent_pk'] = $agentPk;
826  $agentPks[$c] = $agentPk;
827  $treeInfoArray[$c] = $treeInfo;
828  }
829 
830  if (!empty($ErrMsg)) {
831  return $this->flushContent("<div class='alert alert-warning'>$ErrMsg</div>");
832  }
833 
834  $allChildren = [];
835  foreach ($items as $c => $itemPk) {
836  $children = GetNonArtifactChildren($itemPk, $treeInfoArray[$c]['uploadtree_tablename']);
837  FuzzyName($children);
838  $this->AddDataStr($treeInfoArray[$c], $children, $mode);
839  $allChildren[$c] = $children;
840  }
841 
842  $Master = MakeMasterN($allChildren);
843 
844  FileListN($Master, $agentPks, $filter, self::NAME, $items, $mode, $baseline);
845 
846  /* Summary must be computed BEFORE filtering */
847  $summaryData = $this->buildSummaryData($Master, $N, $treeInfoArray, $mode);
848 
849  /* Matrix uses the unfiltered master: filters remove rows, which makes the
850  * matrix lie about file presence (a filtered-out same-hash file would appear
851  * as absent instead of present). Diff view is filtered normally. */
852  if ($view === 'matrix') {
853  $tableRows = $this->ItemComparisonRows($Master, $treeInfoArray, $mode, $baseline, $view);
854  $this->FilterN($filter, $Master, $N);
855  } else {
856  $this->FilterN($filter, $Master, $N);
857  $tableRows = $this->ItemComparisonRows($Master, $treeInfoArray, $mode, $baseline, $view);
858  }
859 
860  /* Path banners per column */
861  $pathBanners = [];
862  for ($c = 0; $c < $N; $c++) {
863  $tableName = $treeInfoArray[$c]['uploadtree_tablename'] ?? 'uploadtree';
864  $path = Dir2Path($items[$c], $tableName);
865  $pathBanners[] = Dir2BrowseDiffN($path, $filter, $c, self::NAME, $items, $mode, $baseline);
866  }
867 
868  /* Histogram data */
869  $histData = $this->buildHistData($items, $treeInfoArray, $mode);
870  $colNames = array_map(
871  fn($ti) => htmlspecialchars($ti['display_name'] ?? ''),
872  $treeInfoArray
873  );
874  $modeLabel = $mode === 'license' ? _("License")
875  : ($mode === 'ecc' ? _("ECC") : _("Copyright"));
876 
877  /* ── Twig vars ──────────────────────────────────────────────────── */
878  $filters = [
879  'none' => _("0. Remove nothing"),
880  'samehash' => _("1. Remove identical files (same hash)"),
881  'samelic' => _("2. Remove files with unchanged data"),
882  'samelicfuzzy' => _("2b. Same as 2 but fuzzy name match"),
883  'nolics' => _("3. Same as 2b + remove no-license files"),
884  'allsame' => _("4. Remove rows where all columns agree"),
885  ];
886  $filterDescriptions = [
887  'none' => _("Show every file. Nothing is hidden."),
888  'samehash' => _("Hide files with identical content across all columns."),
889  'samelic' => _("Also hide files that share the same name and the same license/copyright data."),
890  'samelicfuzzy' => _("Like above, but uses fuzzy filename matching — ignores version numbers in filenames."),
891  'nolics' => _("Show only files where a license difference exists. Files with no license found are also hidden."),
892  'allsame' => _("Hide any row where every column reports identical data, regardless of filename."),
893  ];
894  $modes = [
895  'license' => _("Licenses"),
896  'copyright' => _("Copyrights"),
897  'ecc' => _("ECC"),
898  ];
899  $views = [
900  'diff' => _("Diff"),
901  'matrix' => _("Matrix"),
902  ];
903 
904  $vars = [
905  'pluginName' => self::NAME,
906  'items' => $items,
907  'N' => $N,
908  'filter' => $filter,
909  'mode' => $mode,
910  'view' => $view,
911  'baseline' => $baseline,
912  'freezeCol' => $freezeCol,
913  'frozenItem' => $frozenItem,
914  'filters' => $filters,
915  'filterDescriptions' => $filterDescriptions,
916  'modes' => $modes,
917  'views' => $views,
918  'treeInfoArray' => $treeInfoArray,
919  'summaryData' => $summaryData,
920  'pathBanners' => $pathBanners,
921  'tableRows' => $tableRows,
922  'histData' => $histData,
923  'colNames' => $colNames,
924  'modeLabel' => $modeLabel,
925  ];
926 
927  $response = $this->render(
928  'multicompare.html.twig',
929  $this->mergeWithDefault($vars)
930  );
931 
932  if (strlen($response->getContent()) > 0) {
933  ReportCachePut($cacheKey, $response->getContent());
934  }
935 
936  return $response;
937  }
938 }
939 
940 register_plugin(new MultiComparePlugin());
Contains the constants and helpers for authentication of user.
Definition: Auth.php:24
render($templateName, $vars=null, $headers=null)
ChildElt(array $child, int $colIdx, array $row, array $treeInfoArray, string $mode, int $baseline)
filterSamehashN(array &$Master, int $N)
buildSummaryData(array $Master, int $N, array $treeInfoArray, string $mode)
filterSamelicN(array &$Master, int $N)
filterSamelicFuzzyN(array &$Master, int $N)
filterAllsame(array &$Master, int $N)
GetTreeInfo(int $uploadtree_pk)
handle(Request $request)
filterNolicsN(array &$Master)
ItemComparisonRows(array $Master, array $treeInfoArray, string $mode, int $baseline, string $view)
batchAgentPks(int $uploadPk)
AddDataStr(array $treeInfo, array &$children, string $mode)
buildHistData(array $items, array $treeInfoArray, string $mode)
ReportCacheGet($CacheKey)
This function is used by Output() to see if the requested report is in the report cache.
ReportCachePut($CacheKey, $CacheValue)
This function is used to write a record to the report cache. If the record already exists,...
ReportCachePurgeByKey($CacheKey)
Purge from the report cache the record with $CacheKey.
FuzzyName(&$Children)
Add fuzzyname and fuzzynameext to $Children.
Dir2Path($uploadtree_pk, $uploadtree_tablename='uploadtree')
Return the path (without artifacts) of an uploadtree_pk.
Definition: common-dir.php:222
FileListLinks($upload_fk, $uploadtree_pk, $napk, $pfile_pk, $Recurse=True, &$UniqueTagArray=array(), $uploadtree_tablename="uploadtree", $wantTags=true)
Get list of links: [View][Info][Download]
GetFileLicenses($agent, $pfile_pk, $uploadtree_pk, $uploadtree_tablename='uploadtree', $duplicate="")
get all the licenses for a single file or uploadtree
Dir2BrowseDiffN(array $path, string $filter, int $colIdx, string $pluginName, array $items, string $mode, int $baseline)
Render the folder/path breadcrumb banner for one column.
MakeMasterN(array $ChildrenArrays)
Build the master array for N file lists using hashmap-based O(M·N) matching.
FileListN(array &$Master, array $agentPks, string $filter, string $pluginName, array $items, string $mode, int $baseline)
Attach linkurl to every cell in Master (N-way version of FileList()).
FUNCTION int max(int permGroup, int permPublic)
Get the maximum group privilege.
Definition: libfossagent.c:295
#define PERM_READ
Read-only permission.
Definition: libfossology.h:32
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16