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 normalizeComparisonRoots(array $items): array
176  {
177  foreach ($items as $idx => $itemPk) {
178  $row = $this->dbManager->getSingleRow(
179  "SELECT u.uploadtree_tablename
180  FROM uploadtree ut
181  JOIN upload u ON u.upload_pk = ut.upload_fk
182  WHERE ut.uploadtree_pk = \$1",
183  [$itemPk], __METHOD__ . '.table'
184  );
185  if (!$row) {
186  continue;
187  }
188 
189  $children = GetNonArtifactChildren($itemPk, $row['uploadtree_tablename']);
190  $items[$idx] = NormalizeMultiCompareRoot($itemPk, $children);
191  }
192 
193  return $items;
194  }
195 
201  private function AddDataStr(array $treeInfo, array &$children, string $mode): void
202  {
203  if ($mode === 'license') {
204  $licAgentPks = array_values(array_filter([
205  $treeInfo['nomos_agent_pk'],
206  $treeInfo['monk_agent_pk'],
207  $treeInfo['ojo_agent_pk'],
208  ]));
209 
210  /* Batch-fetch licenses for all leaf files in this column at once */
211  $licByPfile = [];
212  if (!empty($licAgentPks)) {
213  $pfileUniq = array_values(array_unique(array_filter(
214  array_map(function ($c) {
215  return intval($c['pfile_fk'] ?? 0);
216  }, $children)
217  )));
218  if (!empty($pfileUniq)) {
219  $params = [];
220  $agentPh = [];
221  foreach ($licAgentPks as $apk) {
222  $params[] = $apk;
223  $agentPh[] = '$' . count($params);
224  }
225  $pfilePh = [];
226  foreach ($pfileUniq as $pf) {
227  $params[] = $pf;
228  $pfilePh[] = '$' . count($params);
229  }
230  $agentIn = implode(',', $agentPh);
231  $pfileIn = implode(',', $pfilePh);
232  $stmt = __METHOD__ . ".licbatch." . implode('_', $licAgentPks) . ".p" . count($pfileUniq);
233  $this->dbManager->prepare($stmt,
234  "SELECT lf.pfile_fk, lr.rf_pk, lr.rf_shortname
235  FROM ONLY license_ref lr, license_file lf
236  WHERE lf.rf_fk = lr.rf_pk
237  AND lf.agent_fk IN ($agentIn)
238  AND lf.pfile_fk IN ($pfileIn)"
239  );
240  $res = $this->dbManager->execute($stmt, $params);
241  while ($row = $this->dbManager->fetchArray($res)) {
242  $pf = intval($row['pfile_fk']);
243  /* rf_pk key deduplicates same license found by multiple agents */
244  $licByPfile[$pf][intval($row['rf_pk'])] = $row['rf_shortname'];
245  }
246  $this->dbManager->freeResult($res);
247  }
248  }
249 
250  foreach ($children as &$child) {
251  $pf = intval($child['pfile_fk'] ?? 0);
252  if ($pf > 0) {
253  $dataarray = $licByPfile[$pf] ?? [];
254  } else {
255  /* Directory: fall back to per-item call to preserve subtree aggregation */
256  $dataarray = [];
257  foreach ($licAgentPks as $agentPk) {
258  $dataarray += GetFileLicenses($agentPk, 0, $child['uploadtree_pk'],
259  $treeInfo['uploadtree_tablename']);
260  }
261  }
262  $child['dataarray'] = $dataarray;
263  $child['datastr'] = implode(", ", $dataarray);
264  if (empty($child['datastr'])) {
265  $child['datastr'] = "No_license_found";
266  $child['dataarray'] = ["No_license_found" => "No_license_found"];
267  }
268  }
269  unset($child);
270 
271  } elseif ($mode === 'copyright' || $mode === 'ecc') {
272  $table = ($mode === 'ecc') ? 'ecc' : 'copyright';
273  $agentPk = ($mode === 'ecc')
274  ? $treeInfo['ecc_agent_pk']
275  : $treeInfo['copyright_agent_pk'];
276 
277  /* Batch-fetch all pfile content in one query */
278  $dataByPfile = [];
279  if ($agentPk > 0) {
280  $pfileUniq = array_values(array_unique(array_filter(
281  array_map(function ($c) {
282  return intval($c['pfile_fk'] ?? 0);
283  }, $children)
284  )));
285  if (!empty($pfileUniq)) {
286  $params = [intval($agentPk)];
287  $pfilePh = [];
288  foreach ($pfileUniq as $pf) {
289  $params[] = $pf;
290  $pfilePh[] = '$' . count($params);
291  }
292  $pfileIn = implode(',', $pfilePh);
293  $stmt = __METHOD__ . ".$table.batch.p" . count($pfileUniq);
294  $this->dbManager->prepare($stmt,
295  "SELECT pfile_fk, content FROM $table
296  WHERE agent_fk=\$1 AND pfile_fk IN ($pfileIn)
297  AND content IS NOT NULL AND content!=''
298  ORDER BY pfile_fk, content"
299  );
300  $res = $this->dbManager->execute($stmt, $params);
301  while ($row = $this->dbManager->fetchArray($res)) {
302  $pf = intval($row['pfile_fk']);
303  $dataByPfile[$pf][$row['content']] = $row['content'];
304  }
305  $this->dbManager->freeResult($res);
306  }
307  }
308 
309  foreach ($children as &$child) {
310  $pf = intval($child['pfile_fk'] ?? 0);
311  $dataarray = $dataByPfile[$pf] ?? [];
312  $child['dataarray'] = $dataarray;
313  $child['datastr'] = implode(", ", $dataarray);
314  }
315  unset($child);
316  }
317  }
318 
319  // ── Filters ────────────────────────────────────────────────────────────
320 
321  private function FilterN(string $filter, array &$Master, int $N): void
322  {
323  switch ($filter) {
324  case 'samehash':
325  $this->filterSamehashN($Master, $N);
326  break;
327  case 'samelic':
328  $this->filterSamehashN($Master, $N);
329  $this->filterSamelicN($Master, $N);
330  break;
331  case 'samelicfuzzy':
332  $this->filterSamehashN($Master, $N);
333  $this->filterSamelicFuzzyN($Master, $N);
334  break;
335  case 'nolics':
336  $this->filterSamehashN($Master, $N);
337  $this->filterSamelicFuzzyN($Master, $N);
338  $this->filterNolicsN($Master);
339  break;
340  case 'allsame':
341  $this->filterSamehashN($Master, $N);
342  $this->filterAllsame($Master, $N);
343  break;
344  }
345  }
346 
351  private function filterSamehashN(array &$Master, int $N): void
352  {
353  foreach ($Master as $key => $row) {
354  $pfiles = [];
355  foreach ($row as $child) {
356  if (!empty($child) && !empty($child['pfile_fk'])) {
357  $pfiles[] = $child['pfile_fk'];
358  }
359  }
360  if (count($pfiles) === $N && count(array_unique($pfiles)) === 1) {
361  unset($Master[$key]);
362  }
363  }
364  }
365 
370  private function filterSamelicN(array &$Master, int $N): void
371  {
372  foreach ($Master as $key => $row) {
373  $present = array_filter($row, fn($c) => !empty($c));
374  if (count($present) !== $N) {
375  continue;
376  }
377  if (count(array_unique(array_column($present, 'ufile_name'))) === 1 &&
378  count(array_unique(array_column($present, 'datastr'))) === 1) {
379  unset($Master[$key]);
380  }
381  }
382  }
383 
387  private function filterSamelicFuzzyN(array &$Master, int $N): void
388  {
389  foreach ($Master as $key => $row) {
390  $present = array_filter($row, fn($c) => !empty($c));
391  if (count($present) !== $N) {
392  continue;
393  }
394  if (count(array_unique(array_column($present, 'fuzzyname'))) === 1 &&
395  count(array_unique(array_column($present, 'datastr'))) === 1) {
396  unset($Master[$key]);
397  }
398  }
399  }
400 
406  private function filterNolicsN(array &$Master): void
407  {
408  foreach ($Master as $key => $row) {
409  $present = array_filter($row, fn($c) => !empty($c));
410  if (empty($present)) {
411  continue;
412  }
413  $allEmpty = true;
414  foreach ($present as $child) {
415  $ds = $child['datastr'];
416  if ($ds !== '' && $ds !== 'No_license_found') {
417  $allEmpty = false;
418  break;
419  }
420  }
421  if ($allEmpty) {
422  unset($Master[$key]);
423  }
424  }
425  }
426 
430  private function filterAllsame(array &$Master, int $N): void
431  {
432  foreach ($Master as $key => $row) {
433  $present = array_filter($row, fn($c) => !empty($c));
434  if (count($present) !== $N) {
435  continue;
436  }
437  if (count(array_unique(array_column($present, 'datastr'))) === 1) {
438  unset($Master[$key]);
439  }
440  }
441  }
442 
443  // ── Table row rendering ────────────────────────────────────────────────
444 
448  private function ChildElt(array $child, int $colIdx, array $row,
449  array $treeInfoArray, string $mode, int $baseline): string
450  {
451  $dataarray = $child['dataarray'] ?? [];
452 
453  $refKeys = [];
454  if ($baseline > 0) {
455  $bIdx = $baseline - 1;
456  if (isset($row[$bIdx]) && !empty($row[$bIdx]['dataarray'])) {
457  $refKeys = array_keys($row[$bIdx]['dataarray']);
458  }
459  } else {
460  foreach ($row as $c => $other) {
461  if ($c === $colIdx || empty($other) || empty($other['dataarray'])) {
462  continue;
463  }
464  foreach (array_keys($other['dataarray']) as $k) {
465  $refKeys[] = $k;
466  }
467  }
468  $refKeys = array_unique($refKeys);
469  }
470  $refKeySet = array_flip($refKeys);
471 
472  $badges = [];
473  foreach ($dataarray as $k => $val) {
474  $missing = !empty($refKeys) && !isset($refKeySet[$k]);
475  if ($missing) {
476  $badges[] = "<span class='badge badge-pill'"
477  . " style='background-color:#ffd6cc;color:#333;font-weight:normal'>"
478  . htmlspecialchars($val) . "</span>";
479  } else {
480  $badges[] = "<span class='badge badge-pill badge-light border'>"
481  . htmlspecialchars($val) . "</span>";
482  }
483  }
484  $dataStr = implode(" ", $badges);
485 
486  $ColStr = "<td class='align-top py-1' id='c{$child['uploadtree_pk']}'>";
487  $ColStr .= $child['linkurl'] ?? htmlspecialchars($child['ufile_name']);
488  if (!empty($dataStr)) {
489  $ColStr .= "<div class='mt-1 ml-1'>$dataStr</div>";
490  }
491  $ColStr .= "</td>";
492 
493  $agentPk = $treeInfoArray[$colIdx]['agent_pk'] ?? 0;
494  $uploadtree_tablename = $treeInfoArray[$colIdx]['uploadtree_tablename'] ?? 'uploadtree';
495  $ColStr .= "<td class='align-top py-1' style='white-space:nowrap'>";
496  $uniqueTagArray = [];
497  $ColStr .= FileListLinks(
498  $child['upload_fk'], $child['uploadtree_pk'],
499  $agentPk, $child['pfile_fk'], true,
500  $uniqueTagArray, $uploadtree_tablename
501  );
502  $ColStr .= "</td>";
503 
504  return $ColStr;
505  }
506 
510  private function ItemComparisonRows(array $Master, array $treeInfoArray,
511  string $mode, int $baseline, string $view): string
512  {
513  $N = count($treeInfoArray);
514 
515  if ($view === 'matrix') {
516  return $this->FileMatrixRows($Master, $N);
517  }
518 
519  $parts = [];
520  foreach ($Master as $row) {
521  $parts[] = "<tr>";
522  for ($c = 0; $c < $N; $c++) {
523  if ($c > 0) {
524  $parts[] = "<td class='border-left border-success p-0' style='width:3px'></td>";
525  }
526  if (empty($row[$c])) {
527  $parts[] = "<td class='text-muted py-1'>&mdash;</td><td></td>";
528  } else {
529  $parts[] = $this->ChildElt($row[$c], $c, $row, $treeInfoArray, $mode, $baseline);
530  }
531  }
532  $parts[] = "</tr>";
533  }
534  return implode("", $parts);
535  }
536 
537  private function FileMatrixRows(array $Master, int $N): string
538  {
539  $parts = [];
540  foreach ($Master as $row) {
541  $parts[] = "<tr>";
542  $firstName = "";
543  for ($c = 0; $c < $N && empty($firstName); $c++) {
544  if (!empty($row[$c])) {
545  $firstName = htmlspecialchars($row[$c]['ufile_name']);
546  }
547  }
548  $parts[] = "<td class='py-1'>$firstName</td>";
549 
550  /* Detect whether all present columns share the same pfile_fk */
551  $pfiles = [];
552  for ($c = 0; $c < $N; $c++) {
553  if (!empty($row[$c]) && !empty($row[$c]['pfile_fk'])) {
554  $pfiles[] = $row[$c]['pfile_fk'];
555  }
556  }
557  $allSameHash = count($pfiles) >= 2 && count(array_unique($pfiles)) === 1;
558 
559  for ($c = 0; $c < $N; $c++) {
560  if (!empty($row[$c])) {
561  /* Green = content differs or unique to this column; gray = identical everywhere */
562  $parts[] = $allSameHash
563  ? "<td class='text-center py-1'><span class='badge badge-light border text-muted' title='identical'>&#10004;</span></td>"
564  : "<td class='text-center py-1'><span class='badge badge-success' title='differs'>&#10004;</span></td>";
565  } else {
566  $parts[] = "<td class='text-center py-1 text-muted'>&mdash;</td>";
567  }
568  }
569  $parts[] = "</tr>";
570  }
571  return implode("", $parts);
572  }
573 
574  // ── Twig data builders ─────────────────────────────────────────────────
575 
581  private function buildSummaryData(array $Master, int $N,
582  array $treeInfoArray, string $mode): array
583  {
584  $uniqueFiles = array_fill(0, $N, 0);
585  $missingFiles = array_fill(0, $N, 0);
586  $uniqueEntries = array_fill(0, $N, []);
587 
588  foreach ($Master as $row) {
589  /* Which columns have data in this row? */
590  $presentSet = [];
591  for ($c = 0; $c < $N; $c++) {
592  if (!empty($row[$c])) {
593  $presentSet[$c] = true;
594  }
595  }
596  $nPresent = count($presentSet);
597 
598  for ($c = 0; $c < $N; $c++) {
599  if (!isset($presentSet[$c])) {
600  $missingFiles[$c]++;
601  }
602  }
603  if ($nPresent === 1) {
604  $uniqueFiles[key($presentSet)]++;
605  }
606 
607  /* Count how many columns contain each data key — O(N·K) per row */
608  $keyColCount = [];
609  foreach ($presentSet as $c => $_) {
610  foreach ($row[$c]['dataarray'] ?? [] as $k => $val) {
611  $keyColCount[$k] = ($keyColCount[$k] ?? 0) + 1;
612  }
613  }
614 
615  /* Keys appearing in exactly one column are unique to that column */
616  foreach ($presentSet as $c => $_) {
617  foreach ($row[$c]['dataarray'] ?? [] as $k => $val) {
618  if ($keyColCount[$k] === 1) {
619  $uniqueEntries[$c][$k] = $val;
620  }
621  }
622  }
623  }
624 
625  $summaryData = [];
626  for ($c = 0; $c < $N; $c++) {
627  $uniqList = array_unique($uniqueEntries[$c]);
628  $uniqCount = count($uniqList);
629  $shown = array_map('htmlspecialchars', array_slice($uniqList, 0, 10));
630  $summaryData[] = [
631  'name' => htmlspecialchars($treeInfoArray[$c]['display_name'] ?? ("Col " . ($c + 1))),
632  'uniqueFiles' => $uniqueFiles[$c],
633  'missingFiles' => $missingFiles[$c],
634  'uniqueEntries' => $shown,
635  'moreCount' => max(0, $uniqCount - 10),
636  ];
637  }
638  return $summaryData;
639  }
640 
645  private function buildHistData(array $items, array $treeInfoArray, string $mode): array
646  {
647  $N = count($items);
648  $histData = [];
649 
650  for ($c = 0; $c < $N; $c++) {
651  $treeInfo = $treeInfoArray[$c];
652 
653  if ($mode === 'license') {
654  $licAgentPks = array_values(array_filter([
655  $treeInfo['nomos_agent_pk'],
656  $treeInfo['monk_agent_pk'],
657  $treeInfo['ojo_agent_pk'],
658  ]));
659  if (empty($licAgentPks)) {
660  continue;
661  }
662  $lft = intval($treeInfo['lft']);
663  $rgt = intval($treeInfo['rgt']);
664  $upPk = intval($treeInfo['upload_fk']);
665  $table = $treeInfo['uploadtree_tablename'];
666 
667  $params = [$lft, $rgt];
668  $upClause = '';
669  if ($table === 'uploadtree_a' || $table === 'uploadtree') {
670  $params[] = $upPk;
671  $upClause = "upload_fk=\$" . count($params) . " AND ";
672  }
673  /* Build IN-list for the agent PKs */
674  $agentPlaceholders = [];
675  foreach ($licAgentPks as $apk) {
676  $params[] = $apk;
677  $agentPlaceholders[] = "\$" . count($params);
678  }
679  $agentIn = implode(",", $agentPlaceholders);
680  $sql = "SELECT rf_shortname AS entry, count(DISTINCT pfile_fk) AS cnt
681  FROM ONLY license_ref, license_file,
682  (SELECT DISTINCT(pfile_fk) AS PF FROM $table
683  WHERE {$upClause}{$table}.lft BETWEEN \$1 AND \$2) AS SS
684  WHERE PF=pfile_fk AND agent_fk IN ($agentIn) AND rf_fk=rf_pk
685  GROUP BY rf_shortname ORDER BY cnt DESC";
686  $stmt = __METHOD__ . ".lic.$table.$c." . implode("_", $licAgentPks);
687  $this->dbManager->prepare($stmt, $sql);
688  $res = $this->dbManager->execute($stmt, $params);
689  while ($row = $this->dbManager->fetchArray($res)) {
690  $histData[$row['entry']][$c] = (int)$row['cnt'];
691  }
692  $this->dbManager->freeResult($res);
693 
694  } elseif ($mode === 'copyright' || $mode === 'ecc') {
695  $table = ($mode === 'ecc') ? 'ecc' : 'copyright';
696  $agentPk = ($mode === 'ecc')
697  ? intval($treeInfo['ecc_agent_pk'])
698  : intval($treeInfo['copyright_agent_pk']);
699  if ($agentPk == 0) {
700  continue;
701  }
702  $lft = intval($treeInfo['lft']);
703  $rgt = intval($treeInfo['rgt']);
704  $upPk = intval($treeInfo['upload_fk']);
705  $utbl = $treeInfo['uploadtree_tablename'];
706 
707  $params = [$agentPk, $lft, $rgt];
708  $upClause = '';
709  if ($utbl === 'uploadtree_a' || $utbl === 'uploadtree') {
710  $params[] = $upPk;
711  $upClause = "AND UT.upload_fk=\$" . count($params);
712  }
713  $sql = "SELECT C.content AS entry, count(*) AS cnt
714  FROM $table C
715  INNER JOIN $utbl UT ON C.pfile_fk = UT.pfile_fk
716  WHERE C.agent_fk=\$1
717  AND C.content IS NOT NULL AND C.content!=''
718  AND UT.lft BETWEEN \$2 AND \$3
719  $upClause
720  GROUP BY C.content ORDER BY cnt DESC LIMIT 100";
721  $stmt = __METHOD__ . ".$mode.$utbl.$c";
722  $this->dbManager->prepare($stmt, $sql);
723  $res = $this->dbManager->execute($stmt, $params);
724  while ($row = $this->dbManager->fetchArray($res)) {
725  $histData[$row['entry']][$c] = (int)$row['cnt'];
726  }
727  $this->dbManager->freeResult($res);
728  }
729  }
730 
731  /* Sort by total count across all columns, descending */
732  uasort($histData, function (array $a, array $b): int {
733  return array_sum($b) - array_sum($a);
734  });
735  return $histData;
736  }
737 
738  // ── Request handler ────────────────────────────────────────────────────
739 
740  protected function handle(Request $request): Response
741  {
742  $this->createFilePickerMultiTable();
743 
744  $itemsRaw = $request->get('items', '');
745  $items = [];
746  if (!empty($itemsRaw)) {
747  $items = array_values(array_unique(array_filter(
748  array_map('intval', explode(',', $itemsRaw)),
749  fn($v) => $v > 0
750  )));
751  }
752 
753  $filter = $request->get('filter', 'samehash');
754  $mode = $request->get('mode', 'license');
755  $view = $request->get('view', 'diff');
756  $baseline = (int)($request->get('baseline', 0));
757  $updcache = (int)($request->get('updcache', 0));
758 
759  if (!in_array($mode, ['license', 'copyright', 'ecc'])) {
760  $mode = 'license';
761  }
762  if (!in_array($view, ['diff', 'matrix'])) {
763  $view = 'diff';
764  }
765 
766  if (count($items) < 2) {
767  return $this->flushContent(
768  "<h3>" . _("Please select at least 2 components to compare.") . "</h3>"
769  . "<p><a href='javascript:history.back()'>" . _("Go back") . "</a></p>"
770  );
771  }
772 
773  foreach ($items as $idx => $itemPk) {
774  /* Lightweight query: only upload_fk is needed for the access check.
775  * This must happen before the cache check to avoid serving cached pages
776  * to users who lost access since the page was cached. */
777  $permRow = $this->dbManager->getSingleRow(
778  "SELECT upload_fk FROM uploadtree WHERE uploadtree_pk = \$1",
779  [$itemPk], __METHOD__ . '.perm'
780  );
781  if (!$permRow || !$this->uploadDao->isAccessible($permRow['upload_fk'], Auth::getGroupId())) {
782  return $this->flushContent(
783  "<h2>" . _("Permission Denied") . " (item " . ($idx + 1) . ")</h2>"
784  );
785  }
786  }
787 
788  /* Freeze support: replace one column's item with a frozen pk.
789  * $freezeCol is 1-based; $clickedCol is 0-based from the link's &col= param.
790  * Default -1 is a sentinel meaning "toolbar navigation, not a column click",
791  * so the freeze is always preserved on filter/mode/view/baseline changes. */
792  $freezeCol = (int)($request->get('freeze', 0));
793  $frozenItem = (int)($request->get('itemf', 0));
794  $clickedCol = (int)($request->get('col', -1));
795  if ($freezeCol > 0 && $frozenItem > 0 && ($freezeCol - 1) !== $clickedCol) {
796  $colIdx0 = $freezeCol - 1;
797  if (isset($items[$colIdx0])) {
798  $frozenUploadFk = $this->dbManager->getSingleRow(
799  "SELECT upload_fk FROM uploadtree WHERE uploadtree_pk = \$1",
800  [$frozenItem], __METHOD__ . '.freeze'
801  )['upload_fk'] ?? 0;
802  if ($frozenUploadFk && $this->uploadDao->isAccessible($frozenUploadFk, Auth::getGroupId())) {
803  $items[$colIdx0] = $frozenItem;
804  }
805  }
806  }
807 
808  $items = $this->normalizeComparisonRoots($items);
809 
810  $cacheKey = "?mod=" . self::NAME
811  . "&items=" . implode(",", $items)
812  . "&filter=$filter&mode=$mode&view=$view&baseline=$baseline"
813  . ($freezeCol > 0 ? "&freeze=$freezeCol&itemf=$frozenItem" : "");
814 
815  if ($updcache) {
816  ReportCachePurgeByKey($cacheKey);
817  } else {
818  $cached = ReportCacheGet($cacheKey);
819  if (!empty($cached)) {
820  return new Response($cached, Response::HTTP_OK, $this->getDefaultHeaders());
821  }
822  }
823 
824  /* ── Build data ─────────────────────────────────────────────────── */
825  $treeInfoArray = [];
826  $agentPks = [];
827  $ErrMsg = "";
828  $N = count($items);
829 
830  foreach ($items as $c => $itemPk) {
831  $treeInfo = $this->GetTreeInfo($itemPk);
832  if (empty($treeInfo)) {
833  return $this->flushContent(
834  "<div class='alert alert-danger'>"
835  . sprintf(_("Could not load data for item %d. The item may have been deleted."), $itemPk)
836  . "</div>"
837  );
838  }
839  $agentPk = 0;
840  if ($mode === 'license') {
841  $agentPk = $treeInfo['nomos_agent_pk'] ?: $treeInfo['monk_agent_pk'] ?: $treeInfo['ojo_agent_pk'];
842  if ($agentPk == 0 && empty($ErrMsg)) {
843  $ErrMsg = sprintf(
844  _("No license scan data for component %d (%s). Schedule a nomos, monk, or ojo scan first."),
845  $c + 1, htmlspecialchars($treeInfo['display_name'])
846  );
847  }
848  } elseif ($mode === 'copyright') {
849  $agentPk = $treeInfo['copyright_agent_pk'];
850  } elseif ($mode === 'ecc') {
851  $agentPk = $treeInfo['ecc_agent_pk'];
852  }
853  $treeInfo['agent_pk'] = $agentPk;
854  $agentPks[$c] = $agentPk;
855  $treeInfoArray[$c] = $treeInfo;
856  }
857 
858  if (!empty($ErrMsg)) {
859  return $this->flushContent("<div class='alert alert-warning'>$ErrMsg</div>");
860  }
861 
862  $allChildren = [];
863  foreach ($items as $c => $itemPk) {
864  $children = GetNonArtifactChildren($itemPk, $treeInfoArray[$c]['uploadtree_tablename']);
865  FuzzyName($children);
866  $this->AddDataStr($treeInfoArray[$c], $children, $mode);
867  $allChildren[$c] = $children;
868  }
869 
870  $Master = MakeMasterN($allChildren);
871 
872  FileListN($Master, $agentPks, $filter, self::NAME, $items, $mode, $baseline);
873 
874  /* Summary must be computed BEFORE filtering */
875  $summaryData = $this->buildSummaryData($Master, $N, $treeInfoArray, $mode);
876 
877  /* Matrix uses the unfiltered master: filters remove rows, which makes the
878  * matrix lie about file presence (a filtered-out same-hash file would appear
879  * as absent instead of present). Diff view is filtered normally. */
880  if ($view === 'matrix') {
881  $tableRows = $this->ItemComparisonRows($Master, $treeInfoArray, $mode, $baseline, $view);
882  $this->FilterN($filter, $Master, $N);
883  } else {
884  $this->FilterN($filter, $Master, $N);
885  $tableRows = $this->ItemComparisonRows($Master, $treeInfoArray, $mode, $baseline, $view);
886  }
887 
888  /* Path banners per column */
889  $pathBanners = [];
890  for ($c = 0; $c < $N; $c++) {
891  $tableName = $treeInfoArray[$c]['uploadtree_tablename'] ?? 'uploadtree';
892  $path = Dir2Path($items[$c], $tableName);
893  $pathBanners[] = Dir2BrowseDiffN($path, $filter, $c, self::NAME, $items, $mode, $baseline);
894  }
895 
896  /* Histogram data */
897  $histData = $this->buildHistData($items, $treeInfoArray, $mode);
898  $colNames = array_map(
899  fn($ti) => htmlspecialchars($ti['display_name'] ?? ''),
900  $treeInfoArray
901  );
902  $modeLabel = $mode === 'license' ? _("License")
903  : ($mode === 'ecc' ? _("ECC") : _("Copyright"));
904 
905  /* ── Twig vars ──────────────────────────────────────────────────── */
906  $filters = [
907  'none' => _("0. Remove nothing"),
908  'samehash' => _("1. Remove identical files (same hash)"),
909  'samelic' => _("2. Remove files with unchanged data"),
910  'samelicfuzzy' => _("2b. Same as 2 but fuzzy name match"),
911  'nolics' => _("3. Same as 2b + remove no-license files"),
912  'allsame' => _("4. Remove rows where all columns agree"),
913  ];
914  $filterDescriptions = [
915  'none' => _("Show every file. Nothing is hidden."),
916  'samehash' => _("Hide files with identical content across all columns."),
917  'samelic' => _("Also hide files that share the same name and the same license/copyright data."),
918  'samelicfuzzy' => _("Like above, but uses fuzzy filename matching — ignores version numbers in filenames."),
919  'nolics' => _("Show only files where a license difference exists. Files with no license found are also hidden."),
920  'allsame' => _("Hide any row where every column reports identical data, regardless of filename."),
921  ];
922  $modes = [
923  'license' => _("Licenses"),
924  'copyright' => _("Copyrights"),
925  'ecc' => _("ECC"),
926  ];
927  $views = [
928  'diff' => _("Diff"),
929  'matrix' => _("Matrix"),
930  ];
931 
932  $vars = [
933  'pluginName' => self::NAME,
934  'items' => $items,
935  'N' => $N,
936  'filter' => $filter,
937  'mode' => $mode,
938  'view' => $view,
939  'baseline' => $baseline,
940  'freezeCol' => $freezeCol,
941  'frozenItem' => $frozenItem,
942  'filters' => $filters,
943  'filterDescriptions' => $filterDescriptions,
944  'modes' => $modes,
945  'views' => $views,
946  'treeInfoArray' => $treeInfoArray,
947  'summaryData' => $summaryData,
948  'pathBanners' => $pathBanners,
949  'tableRows' => $tableRows,
950  'histData' => $histData,
951  'colNames' => $colNames,
952  'modeLabel' => $modeLabel,
953  ];
954 
955  $response = $this->render(
956  'multicompare.html.twig',
957  $this->mergeWithDefault($vars)
958  );
959 
960  if (strlen($response->getContent()) > 0) {
961  ReportCachePut($cacheKey, $response->getContent());
962  }
963 
964  return $response;
965  }
966 }
967 
968 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)
normalizeComparisonRoots(array $items)
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.
NormalizeMultiCompareRoot(int $selectedItem, array $children)
Select the effective comparison root for a tree item.
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