FOSSology  4.7.1
Open Source License Compliance by Open Source Software
ReuserDatabaseHandler.cc
1 /*
2  SPDX-License-Identifier: GPL-2.0-only
3  Author: Dietmar Helmut Leher <helmut.leher.ext@vaillant-group.com>
4  SPDX-FileCopyrightText: © 2026 Vaillant GmbH
5 */
6 
7 #include "ReuserDatabaseHandler.hpp"
8 
9 #include <algorithm>
10 #include <cstdio>
11 #include <cstdlib>
12 #include <cstring>
13 #include <set>
14 #include <sstream>
15 #include <sys/wait.h>
16 
17 #include <unicode/unistr.h>
18 
19 extern "C" {
20 #include "libfossagent.h"
21 }
22 
23 using namespace fo;
24 
25 // Construction
26 
27 ReuserDatabaseHandler::ReuserDatabaseHandler(DbManager dbManager)
29 {
30 }
31 
33 {
35 }
36 
37 // Private helpers
38 
39 /* Mirror of DecisionTypes.php; keep in sync if the PHP enum changes.
40  * Types 1 and 2 do not exist in that enum; default covers them. */
41 namespace {
42  constexpr int DT_WIP = 0;
43  constexpr int DT_TO_BE_DISCUSSED = 3;
44  constexpr int DT_IRRELEVANT = 4;
45  constexpr int DT_IDENTIFIED = 5;
46  constexpr int DT_DO_NOT_USE = 6;
47  constexpr int DT_NON_FUNCTIONAL = 7;
48 }
49 
51 {
52  switch (decisionType)
53  {
54  case DT_IDENTIFIED: return 5;
55  case DT_DO_NOT_USE: return 4;
56  case DT_NON_FUNCTIONAL: return 3;
57  case DT_IRRELEVANT: return 2;
58  case DT_TO_BE_DISCUSSED: return 1;
59  default: return 0;
60  }
61 }
62 
64 {
65  if (s.empty()) return false;
66  for (char c : s)
67  if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
68  (c >= '0' && c <= '9') || c == '_'))
69  return false;
70  return true;
71 }
72 
74  const std::string& input)
75 {
76  icu::UnicodeString us = icu::UnicodeString::fromUTF8(input);
77  icu::UnicodeString result;
78  for (int32_t i = 0; i < us.length(); ++i)
79  {
80  UChar32 cp = us.char32At(i);
81  if (cp > 0xFFFF) ++i; // surrogate pair: char32At already consumed 2 units
82  bool isControl = (cp <= 0x08)
83  || (cp == 0x0B)
84  || (cp == 0x0C)
85  || (cp >= 0x0E && cp <= 0x1F)
86  || (cp >= 0x7F && cp <= 0x9F);
87  if (!isControl)
88  result.append(cp);
89  }
90  std::string out;
91  result.toUTF8String(out);
92  return out;
93 }
94 
95 std::string ReuserDatabaseHandler::shellEscape(const std::string& s)
96 {
97  std::string r = "'";
98  for (char c : s)
99  r += (c == '\'') ? std::string("'\\''") : std::string(1, c);
100  r += "'";
101  return r;
102 }
103 
104 int ReuserDatabaseHandler::diffLineCount(const std::string& a,
105  const std::string& b)
106 {
107  if (a.empty() || b.empty()) return -1;
108 
109  // Run diff directly (no pipeline) so pclose() returns diff's own exit code.
110  // Redirect diff's stderr to suppress "no such file" noise.
111  std::string cmd = "diff -- " + shellEscape(a) + " " + shellEscape(b)
112  + " 2>/dev/null";
113  FILE* pipe = popen(cmd.c_str(), "r");
114  if (!pipe) return -1;
115 
116  int lines = 0;
117  char buf[4096];
118  while (fgets(buf, sizeof(buf), pipe))
119  ++lines;
120 
121  int status = pclose(pipe);
122  // diff exit codes: 0 = identical, 1 = differences found, 2 = error.
123  if (WIFEXITED(status) && WEXITSTATUS(status) == 2)
124  return -1;
125 
126  return lines;
127 }
128 
130 {
131  char* pfileName =
132  getPFileNameForFileId(static_cast<unsigned long>(pfileId));
133  if (!pfileName) return {};
134  char* filePath = fo_RepMkPath("files", pfileName);
135  free(pfileName);
136  if (!filePath) return {};
137  std::string result(filePath);
138  free(filePath);
139  return result;
140 }
141 
142 // Upload-tree helpers
143 
145  ItemTreeBounds& out)
146 {
147  std::string table = queryUploadTreeTableName(uploadId);
148  if (!isValidIdentifier(table)) return false;
149 
150  bool needsUploadFilter =
151  (table == "uploadtree" || table == "uploadtree_a");
152 
153  QueryResult result =
154  needsUploadFilter
156  "SELECT uploadtree_pk, upload_fk, lft, rgt"
157  " FROM %s WHERE parent IS NULL AND upload_fk=%d",
158  table.c_str(), uploadId)
160  "SELECT uploadtree_pk, upload_fk, lft, rgt"
161  " FROM %s WHERE parent IS NULL",
162  table.c_str());
163 
164  if (!result || result.getRowCount() == 0) return false;
165 
166  auto row = result.getRow(0);
167  out.uploadtree_pk = std::stoi(row[0]);
168  out.uploadTreeTableName = table;
169  out.upload_fk = std::stoi(row[1]);
170  out.lft = std::stoi(row[2]);
171  out.rgt = std::stoi(row[3]);
172  return true;
173 }
174 
175 // Reuse relationship queries
176 
178  int uploadId, int groupId)
179 {
180  std::vector<ReuseTriple> result;
181 
183  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
184  "reuserGetReusedUploads",
185  "SELECT reused_upload_fk, reused_group_fk, reuse_mode"
186  " FROM upload_reuse"
187  " WHERE upload_fk=$1 AND group_fk=$2"
188  " ORDER BY date_added DESC",
189  int, int),
190  uploadId, groupId);
191 
192  for (int i = 0; i < qr.getRowCount(); ++i)
193  {
194  auto row = qr.getRow(i);
195  result.push_back({std::stoi(row[0]), std::stoi(row[1]),
196  std::stoi(row[2])});
197  }
198  return result;
199 }
200 
202  int uploadId, int groupId)
203 {
204  std::map<int, int> result;
205 
206  std::string table = queryUploadTreeTableName(uploadId);
207  if (!isValidIdentifier(table)) return result;
208 
209  bool needsUploadFilter =
210  (table == "uploadtree" || table == "uploadtree_a");
211 
212  // Determine whether global (REPO) decisions should be applied.
213  bool applyGlobal = true;
215  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
216  "reuserGetGlobalDecision",
217  // Cast int2 to boolean so PostgreSQL returns 't'/'f' regardless of storage
218  // format ('1'/'0' vs 'true'/'false'); stringToBool only recognises 't'/'true'.
219  "SELECT (ri_globaldecision != 0) FROM report_info WHERE upload_fk=$1",
220  int),
221  uploadId);
222  if (globalQr && globalQr.getRowCount() > 0)
223  applyGlobal = fo::stringToBool(globalQr.getRow(0)[0].c_str());
224 
225  // Omit scope=0 check to mirror PHP ClearingDao::getRelevantDecisionsCte.
226  std::string joinCond =
227  applyGlobal
228  ? "(ut.pfile_fk = cd.pfile_fk AND cd.scope = 1)"
229  " OR (ut.uploadtree_pk = cd.uploadtree_fk"
230  " AND cd.scope = 0 AND cd.group_fk = " + std::to_string(groupId) + ")"
231  : "(ut.uploadtree_pk = cd.uploadtree_fk"
232  " AND cd.group_fk = " + std::to_string(groupId) + ")";
233 
234  std::string uploadFilter =
235  needsUploadFilter
236  ? " AND ut.upload_fk = " + std::to_string(uploadId)
237  : "";
238 
239  // Inner CTE: best decision per uploadtree_pk (ITEM before REPO, newest first).
240  // Outer CTE: all rows kept for priority-based conflict resolution.
242  "WITH per_item AS ("
243  " SELECT DISTINCT ON(ut.uploadtree_pk)"
244  " cd.clearing_decision_pk AS id,"
245  " cd.pfile_fk AS pfile_id,"
246  " cd.decision_type AS dec_type"
247  " FROM clearing_decision cd"
248  " INNER JOIN %s ut ON (%s)%s"
249  " WHERE cd.decision_type != 0"
250  " ORDER BY ut.uploadtree_pk, cd.scope ASC,"
251  " cd.clearing_decision_pk DESC"
252  "),"
253  " per_pfile AS ("
254  " SELECT id, pfile_id, dec_type"
255  " FROM per_item"
256  " ORDER BY pfile_id, id DESC"
257  ")"
258  " SELECT id, pfile_id, dec_type FROM per_pfile",
259  table.c_str(), joinCond.c_str(), uploadFilter.c_str());
260 
261  std::map<int, int> resultTypes;
262 
263  for (int i = 0; i < qr.getRowCount(); ++i)
264  {
265  auto row = qr.getRow(i);
266  int decId = std::stoi(row[0]);
267  int pfileId = std::stoi(row[1]);
268  int decType = std::stoi(row[2]);
269  if (pfileId > 0) {
270  auto it = result.find(pfileId);
271  if (it == result.end()) {
272  result[pfileId] = decId;
273  resultTypes[pfileId] = decType;
274  } else if (getDecisionTypePriority(decType) >
275  getDecisionTypePriority(resultTypes[pfileId])) {
276  LOG_NOTICE("Reuser: conflicting decisions for pfile %d,"
277  " applying stronger decision type %d over %d.",
278  pfileId, decType, resultTypes[pfileId]);
279  result[pfileId] = decId;
280  resultTypes[pfileId] = decType;
281  }
282  }
283  }
284  return result;
285 }
286 
287 std::map<int, std::vector<int>>
289  int uploadId, const std::vector<int>& pfileIds)
290 {
291  std::map<int, std::vector<int>> result;
292  if (pfileIds.empty()) return result;
293 
294  std::string table = queryUploadTreeTableName(uploadId);
295  if (!isValidIdentifier(table)) return result;
296 
297  // Integer-only array; no user input embedded.
298  std::string arr;
299  for (size_t i = 0; i < pfileIds.size(); ++i)
300  {
301  if (i > 0) arr += ",";
302  arr += std::to_string(pfileIds[i]);
303  }
304 
305  bool needsUploadFilter =
306  (table == "uploadtree" || table == "uploadtree_a");
307 
308  QueryResult qr =
309  needsUploadFilter
311  "SELECT uploadtree_pk, pfile_fk FROM %s"
312  " WHERE upload_fk=%d AND pfile_fk=ANY('{%s}'::int[])",
313  table.c_str(), uploadId, arr.c_str())
315  "SELECT uploadtree_pk, pfile_fk FROM %s"
316  " WHERE pfile_fk=ANY('{%s}'::int[])",
317  table.c_str(), arr.c_str());
318 
319  for (int i = 0; i < qr.getRowCount(); ++i)
320  {
321  auto row = qr.getRow(i);
322  int pk = std::stoi(row[0]);
323  int pfileId = std::stoi(row[1]);
324  if (pk > 0 && pfileId > 0)
325  result[pfileId].push_back(pk);
326  }
327  return result;
328 }
329 
330 // Clearing-decision operations
331 
333  int uploadId, int uploadTreeId, int userId, int groupId,
334  int licenseId, bool removed, int type,
335  const std::string& reportInfo, const std::string& comment,
336  const std::string& ack, int jobId)
337 {
338  // Strip Unicode control characters (mirrors PHP StringOperation).
339  std::string safeReport = replaceUnicodeControlChars(reportInfo);
340  std::string safeComment = replaceUnicodeControlChars(comment);
341  std::string safeAck = replaceUnicodeControlChars(ack);
342  const char* removedStr = removed ? "t" : "f";
343 
344  if (jobId <= 0)
345  {
346  // Mark existing decision as WIP first (mirrors ClearingDao::markDecisionAsWip).
347  std::string table = queryUploadTreeTableName(uploadId);
348  if (!isValidIdentifier(table))
349  table = "uploadtree";
350 
352  "INSERT INTO clearing_decision"
353  " (uploadtree_fk, pfile_fk, user_fk, group_fk, decision_type, scope)"
354  " VALUES (%d,"
355  " (SELECT pfile_fk FROM %s WHERE uploadtree_pk=%d),"
356  " %d, %d, 0, 0)",
357  uploadTreeId, table.c_str(), uploadTreeId, userId, groupId);
358 
360  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
361  "reuserInsertClearingEvent",
362  "INSERT INTO clearing_event"
363  " (uploadtree_fk, user_fk, group_fk, type_fk, rf_fk,"
364  " removed, reportinfo, comment, acknowledgement)"
365  " VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)"
366  " RETURNING clearing_event_pk",
367  int, int, int, int, int, char*, char*, char*, char*),
368  uploadTreeId, userId, groupId, type, licenseId,
369  removedStr, safeReport.c_str(), safeComment.c_str(), safeAck.c_str());
370 
371  if (!qr || qr.getRowCount() == 0) return 0;
372  return std::stoi(qr.getRow(0)[0]);
373  }
374  else
375  {
377  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
378  "reuserInsertClearingEventWithJob",
379  "INSERT INTO clearing_event"
380  " (uploadtree_fk, user_fk, group_fk, type_fk, rf_fk,"
381  " removed, reportinfo, comment, acknowledgement, job_fk)"
382  " VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)"
383  " RETURNING clearing_event_pk",
384  int, int, int, int, int, char*, char*, char*, char*, int),
385  uploadTreeId, userId, groupId, type, licenseId,
386  removedStr, safeReport.c_str(), safeComment.c_str(), safeAck.c_str(),
387  jobId);
388 
389  if (!qr || qr.getRowCount() == 0) return 0;
390  return std::stoi(qr.getRow(0)[0]);
391  }
392 }
393 
395  int uploadId, int uploadTreeId, int userId, int groupId,
396  int decType, int scope, const std::vector<int>& eventIds)
397 {
398  if (eventIds.empty()) return 0;
399 
400  if (!begin()) return 0;
401 
402  // Remove stale WIP decisions for this item/group.
404  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
405  "reuserRemoveWipDecision",
406  "DELETE FROM clearing_decision"
407  " WHERE uploadtree_fk=$1 AND group_fk=$2 AND decision_type=0",
408  int, int),
409  uploadTreeId, groupId);
410 
411  if (!rRem) { rollback(); return 0; }
412 
413  std::string table = queryUploadTreeTableName(uploadId);
414  if (!isValidIdentifier(table))
415  table = "uploadtree";
416 
418  "INSERT INTO clearing_decision"
419  " (uploadtree_fk, pfile_fk, user_fk, group_fk, decision_type, scope)"
420  " VALUES (%d,"
421  " (SELECT pfile_fk FROM %s WHERE uploadtree_pk=%d),"
422  " %d, %d, %d, %d)"
423  " RETURNING clearing_decision_pk",
424  uploadTreeId, table.c_str(), uploadTreeId, userId, groupId, decType, scope);
425 
426  if (!rIns || rIns.getRowCount() == 0) { rollback(); return 0; }
427  int decisionPk = std::stoi(rIns.getRow(0)[0]);
428 
429  // Link events to the new decision.
430  // Former PHP's ClearingDao::createDecisionFromEvents did not check individual
431  // insert results in the loop (freeResult without error check), so we match
432  // that behaviour: log a warning on failure but continue and commit.
433  auto* stmtLink = fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
434  "reuserInsertClearingDecisionEvent",
435  "INSERT INTO clearing_decision_event"
436  " (clearing_decision_fk, clearing_event_fk) VALUES($1,$2)",
437  int, int);
438 
439  for (int evPk : eventIds)
440  {
441  QueryResult rLink = dbManager.execPrepared(stmtLink, decisionPk, evPk);
442  if (!rLink)
443  LOG_WARNING("Reuser: failed to link clearing_event %d to"
444  " clearing_decision %d, continuing.", evPk, decisionPk);
445  }
446 
447  if (!commit()) { rollback(); return 0; }
448  return decisionPk;
449 }
450 
452  int uploadId, int newItemUploadTreePk, int userId, int groupId,
453  int originalDecisionPk)
454 {
455  // Fetch decision meta (type and scope).
457  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
458  "reuserGetDecisionMeta",
459  "SELECT decision_type, scope FROM clearing_decision"
460  " WHERE clearing_decision_pk=$1",
461  int),
462  originalDecisionPk);
463 
464  if (!rMeta || rMeta.getRowCount() == 0) return 0;
465  int decType = std::stoi(rMeta.getRow(0)[0]);
466  int scope = std::stoi(rMeta.getRow(0)[1]);
467 
468  // type_fk and job_fk are not copied; copies always use USER type (1).
470  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
471  "reuserGetEventsForDecision",
472  "SELECT ce.rf_fk, ce.removed,"
473  " ce.reportinfo, ce.comment, ce.acknowledgement"
474  " FROM clearing_event ce"
475  " INNER JOIN clearing_decision_event cde"
476  " ON cde.clearing_event_fk = ce.clearing_event_pk"
477  " WHERE cde.clearing_decision_fk=$1"
478  " ORDER BY ce.clearing_event_pk ASC",
479  int),
480  originalDecisionPk);
481 
482  if (!rEvents) return 0;
483 
484  int jobId = fo_scheduler_jobId();
485  std::vector<int> newEventIds;
486 
487  for (int i = 0; i < rEvents.getRowCount(); ++i)
488  {
489  auto row = rEvents.getRow(i);
490  int rfFk = std::stoi(row[0]);
491  bool isRemoved = (row[1] == "t" || row[1] == "true");
492  // Always use USER type (1) for copied events - mirrors PHP behavior.
493  int evType = 1;
494  int evPk = insertClearingEvent(uploadId, newItemUploadTreePk,
495  userId, groupId,
496  rfFk, isRemoved, evType,
497  row[2], row[3], row[4], jobId);
498  if (evPk > 0)
499  newEventIds.push_back(evPk);
500  }
501 
502  if (newEventIds.empty()) return 0;
503  return createDecisionFromEvents(uploadId, newItemUploadTreePk, userId, groupId,
504  decType, scope, newEventIds);
505 }
506 
507 // ARS record
508 
509 int ReuserDatabaseHandler::writeArsRecord(int agentId, int uploadId,
510  int arsId, bool success)
511 {
512  return fo_WriteARS(dbManager.getConnection(), arsId, uploadId, agentId,
513  "reuser_ars", nullptr, success ? 1 : 0);
514 }
515 
516 // Reuse operations
517 
519  int uploadId, int reusedUploadId,
520  int groupId, int reusedGroupId, int userId)
521 {
522  auto reusedMap = getClearingDecisionMapByPfile(reusedUploadId, reusedGroupId);
523  if (reusedMap.empty()) return true;
524 
525  auto currentMap = getClearingDecisionMapByPfile(uploadId, groupId);
526 
527  // Collect pfiles present in the reused upload but not yet cleared here.
528  std::vector<int> toImport;
529  for (const auto& kv : reusedMap)
530  if (currentMap.find(kv.first) == currentMap.end())
531  toImport.push_back(kv.first);
532 
533  if (toImport.empty()) return true;
534 
535  constexpr size_t chunkSize = 100;
536  for (size_t i = 0; i < toImport.size(); i += chunkSize)
537  {
538  size_t end = std::min(i + chunkSize, toImport.size());
539  std::vector<int> chunk(toImport.begin() + i, toImport.begin() + end);
540  auto pkMap = getUploadTreePksForPfiles(uploadId, chunk);
541 
542  for (const auto& entry : pkMap)
543  {
544  int originalDecision = reusedMap.at(entry.first);
545  for (int uploadtreePk : entry.second)
546  {
547  int newDecision = createCopyOfClearingDecision(
548  uploadId, uploadtreePk, userId, groupId, originalDecision);
549  if (newDecision > 0)
551  }
552  }
553  }
554  return true;
555 }
556 
558  int uploadId, int reusedUploadId,
559  int groupId, int reusedGroupId, int userId)
560 {
561  auto reusedMap = getClearingDecisionMapByPfile(reusedUploadId, reusedGroupId);
562  if (reusedMap.empty()) return true;
563 
564  auto currentMap = getClearingDecisionMapByPfile(uploadId, groupId);
565 
566  std::vector<int> toImport;
567  for (const auto& kv : reusedMap)
568  if (currentMap.find(kv.first) == currentMap.end())
569  toImport.push_back(kv.first);
570 
571  if (toImport.empty()) return true;
572 
573  std::string tableReused = queryUploadTreeTableName(reusedUploadId);
574  std::string tableTarget = queryUploadTreeTableName(uploadId);
575  if (!isValidIdentifier(tableReused) || !isValidIdentifier(tableTarget))
576  return true;
577 
578  bool reusedNeedsFilter = (tableReused == "uploadtree" || tableReused == "uploadtree_a");
579  bool targetNeedsFilter = (tableTarget == "uploadtree" || tableTarget == "uploadtree_a");
580 
581  std::string reusedFilter = reusedNeedsFilter
582  ? " AND ur.upload_fk=" + std::to_string(reusedUploadId) : "";
583  std::string targetFilter = targetNeedsFilter
584  ? " AND ut.upload_fk=" + std::to_string(uploadId) : "";
585 
586  for (int pfileFk : toImport)
587  {
588  int originalDecision = reusedMap.at(pfileFk);
589 
590  std::string reusedPath = getRepoPathOfPfile(pfileFk);
591  if (reusedPath.empty()) continue;
592 
593  // Find items in target upload with matching filename.
595  "SELECT ut.uploadtree_pk, ut.pfile_fk"
596  " FROM %s ur, %s ut"
597  " WHERE ur.pfile_fk=%d%s"
598  " AND ut.ufile_name=ur.ufile_name%s",
599  tableReused.c_str(), tableTarget.c_str(),
600  pfileFk, reusedFilter.c_str(),
601  targetFilter.c_str());
602 
603  for (int i = 0; i < rr.getRowCount(); ++i)
604  {
605  auto row = rr.getRow(i);
606  int newItemPk = std::stoi(row[0]);
607  int newPfileFk = std::stoi(row[1]);
608  if (newItemPk <= 0 || newPfileFk <= 0) continue;
609 
610  std::string newPath = getRepoPathOfPfile(newPfileFk);
611  if (newPath.empty()) continue;
612 
613  int diffCount = diffLineCount(reusedPath, newPath);
614  if (diffCount < 0) return false; // diff failed
615  if (diffCount < 5)
616  {
617  int newDecision = createCopyOfClearingDecision(
618  uploadId, newItemPk, userId, groupId, originalDecision);
619  if (newDecision > 0)
621  }
622  }
623  }
624  return true;
625 }
626 
628  int uploadId, int groupId, int reusedUploadId, int reusedGroupId)
629 {
631  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
632  "reuserGetReusedMainLicenses",
633  "SELECT rf_fk FROM upload_clearing_license"
634  " WHERE upload_fk=$1 AND group_fk=$2",
635  int, int),
636  reusedUploadId, reusedGroupId);
637 
638  std::set<int> reusedSet;
639  for (int i = 0; i < r1.getRowCount(); ++i)
640  reusedSet.insert(std::stoi(r1.getRow(i)[0]));
641 
642  if (reusedSet.empty()) return true;
643 
645  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
646  "reuserGetTargetMainLicenses",
647  "SELECT rf_fk FROM upload_clearing_license"
648  " WHERE upload_fk=$1 AND group_fk=$2",
649  int, int),
650  uploadId, groupId);
651 
652  std::set<int> existingSet;
653  for (int i = 0; i < r2.getRowCount(); ++i)
654  existingSet.insert(std::stoi(r2.getRow(i)[0]));
655 
656  for (int rf : reusedSet)
657  {
658  if (existingSet.count(rf)) continue;
660  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
661  "reuserInsertMainLicense",
662  "INSERT INTO upload_clearing_license (upload_fk, group_fk, rf_fk)"
663  " VALUES ($1,$2,$3)",
664  int, int, int),
665  uploadId, groupId, rf);
666  if (!rIns) return false;
667  }
668  return true;
669 }
670 
672  int uploadId, int reusedUploadId)
673 {
674  // Check that the reused upload has a report_info row.
676  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
677  "reuserConfSettingsCheck",
678  "SELECT 1 FROM report_info WHERE upload_fk=$1 LIMIT 1",
679  int),
680  reusedUploadId);
681 
682  if (!rCheck || rCheck.getRowCount() == 0) return true;
683 
684  if (!begin()) return false;
685 
686  // Remove any existing report_info for the target upload.
688  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
689  "reuserConfSettingsDelete",
690  "DELETE FROM report_info WHERE upload_fk=$1",
691  int),
692  uploadId);
693 
694  if (!rDel) { rollback(); return false; }
695 
696  // Dynamically discover columns (excluding pk and upload_fk).
697  // quote_ident ensures safe embedding in the subsequent INSERT.
699  "SELECT string_agg(quote_ident(column_name), ',')"
700  " FROM information_schema.columns"
701  " WHERE table_schema = current_schema()"
702  " AND table_name = 'report_info'"
703  " AND column_name != 'ri_pk'"
704  " AND column_name != 'upload_fk'");
705 
706  if (!rCols || rCols.getRowCount() == 0) { rollback(); return false; }
707  std::string cols = rCols.getRow(0)[0];
708  if (cols.empty()) { rollback(); return false; }
709 
710  // INSERT … SELECT copies all remaining columns from the reused upload.
712  "INSERT INTO report_info(upload_fk, %s)"
713  " SELECT %d, %s FROM report_info WHERE upload_fk=%d",
714  cols.c_str(), uploadId, cols.c_str(), reusedUploadId);
715 
716  if (!rCopy) { rollback(); return false; }
717 
718  if (!commit()) { rollback(); return false; }
719  return true;
720 }
721 
723  int uploadId, int reusedUploadId, int userId)
724 {
725  const std::string agentName = "copyright";
726 
727  // Resolve copyright agent id for both uploads.
729  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
730  "reuserCopyrightTargetAgentId",
731  "SELECT agent_pk AS agent_id FROM agent"
732  " LEFT JOIN copyright_ars ON agent_fk=agent_pk"
733  " WHERE agent_name=$2 AND agent_enabled"
734  " AND upload_fk=$1 AND ars_success"
735  " ORDER BY agent_pk DESC LIMIT 1",
736  int, char*),
737  uploadId, agentName.c_str());
738 
739  if (!rAgentT || rAgentT.getRowCount() == 0) return true;
740  int targetAgentId = std::stoi(rAgentT.getRow(0)[0]);
741 
743  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
744  "reuserCopyrightReusedAgentId",
745  "SELECT agent_pk AS agent_id FROM agent"
746  " LEFT JOIN copyright_ars ON agent_fk=agent_pk"
747  " WHERE agent_name=$2 AND agent_enabled"
748  " AND upload_fk=$1 AND ars_success"
749  " ORDER BY agent_pk DESC LIMIT 1",
750  int, char*),
751  reusedUploadId, agentName.c_str());
752 
753  if (!rAgentR || rAgentR.getRowCount() == 0) return true;
754  int reusedAgentId = std::stoi(rAgentR.getRow(0)[0]);
755 
756  // Fetch existing copyright entries in the target upload, keyed by hash.
757  std::string table = queryUploadTreeTableName(uploadId);
758  if (!isValidIdentifier(table)) return true;
759 
760  bool needsUploadFilter = (table == "uploadtree" || table == "uploadtree_a");
761  std::string uploadFilter = needsUploadFilter
762  ? " AND UT.upload_fk = " + std::to_string(uploadId) : "";
763 
765  "SELECT DISTINCT ON (C.copyright_pk, UT.uploadtree_pk)"
766  " C.copyright_pk, UT.uploadtree_pk, UT.upload_fk,"
767  " (CASE WHEN (CE.content IS NULL OR CE.content = '')"
768  " THEN C.content ELSE CE.content END) AS content,"
769  " (CASE WHEN (CE.hash IS NULL OR CE.hash = '')"
770  " THEN C.hash ELSE CE.hash END) AS hash"
771  " FROM copyright C"
772  " INNER JOIN %s UT ON C.pfile_fk = UT.pfile_fk%s"
773  " LEFT JOIN copyright_event CE"
774  " ON CE.copyright_fk = C.copyright_pk"
775  " AND CE.upload_fk = %d"
776  " AND CE.uploadtree_fk = UT.uploadtree_pk"
777  " WHERE C.content IS NOT NULL AND C.content <> ''"
778  " AND (CE.is_enabled IS NULL OR CE.is_enabled = 'true')"
779  " AND C.agent_fk = %d"
780  " ORDER BY C.copyright_pk, UT.uploadtree_pk, content DESC",
781  table.c_str(), uploadFilter.c_str(), uploadId, targetAgentId);
782 
783  // Index existing copyrights by hash.
784  // hash -> list of {copyright_pk, uploadtree_pk, upload_fk}
785  using Row3 = std::array<int, 3>;
786  std::map<std::string, std::vector<Row3>> allMap;
787  for (int i = 0; i < rAll.getRowCount(); ++i)
788  {
789  auto row = rAll.getRow(i);
790  std::string hash = row[4];
791  if (!hash.empty())
792  allMap[hash].push_back(Row3{std::stoi(row[0]), std::stoi(row[1]),
793  std::stoi(row[2])});
794  }
795  if (allMap.empty()) return true;
796 
797  // Fetch copyright events that were modified in the reused upload.
798  // scope 1 == REPO (global events only).
800  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
801  "reuserGetReusedCopyrightEvents",
802  "SELECT C.copyright_pk, CE.is_enabled, C.hash,"
803  " CE.content AS contentedited"
804  " FROM copyright_event CE"
805  " INNER JOIN copyright C ON C.copyright_pk = CE.copyright_fk"
806  " WHERE CE.upload_fk=$1 AND CE.scope=$3 AND C.agent_fk=$2",
807  int, int, int),
808  reusedUploadId, reusedAgentId, 1 /* REPO scope */);
809 
810  for (int i = 0; i < rReused.getRowCount(); ++i)
811  {
812  auto rRow = rReused.getRow(i);
813  std::string hash = rRow[2];
814  if (hash.empty()) continue;
815 
816  auto it = allMap.find(hash);
817  if (it == allMap.end() || it->second.empty()) continue;
818 
819  Row3 entry = it->second.back();
820  it->second.pop_back();
821  int copyrightPk = entry[0];
822  int uploadtreePk = entry[1];
823  int uploadFk = entry[2];
824  bool isEnabled = fo::stringToBool(rRow[1].c_str());
825  const std::string& contentEdited = rRow[3];
826 
827  // Check if a copyright_event already exists for this combination.
829  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
830  "reuserCopyrightEventExists",
831  "SELECT EXISTS("
832  " SELECT 1 FROM copyright_event"
833  " WHERE copyright_fk=$1 AND upload_fk=$2 AND uploadtree_fk=$3"
834  ")::int",
835  int, int, int),
836  copyrightPk, uploadFk, uploadtreePk);
837 
838  bool eventExists = rExists && rExists.getRowCount() > 0
839  && std::stoi(rExists.getRow(0)[0]) != 0;
840 
841  // Former PHP's CopyrightDao::updateTable() called getSingleRow() without
842  // checking the return value, and reuseCopyrights() always returned true
843  // regardless. We match that behaviour: log a warning on failure but continue.
844  if (!isEnabled)
845  {
846  QueryResult rWrite =
847  eventExists
849  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
850  "reuserCopyrightEventDisableUpdate",
851  "UPDATE copyright_event SET scope=$4, is_enabled=false"
852  " WHERE upload_fk=$1 AND copyright_fk=$2 AND uploadtree_fk=$3",
853  int, int, int, int),
854  uploadFk, copyrightPk, uploadtreePk, 1)
856  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
857  "reuserCopyrightEventDisableInsert",
858  "INSERT INTO copyright_event"
859  " (upload_fk, copyright_fk, uploadtree_fk, is_enabled, scope)"
860  " VALUES($1,$2,$3,'f',$4)",
861  int, int, int, int),
862  uploadFk, copyrightPk, uploadtreePk, 1);
863  if (!rWrite)
864  LOG_WARNING("Reuser: failed to disable copyright_event"
865  " (copyright_fk=%d, uploadtree_fk=%d), continuing.",
866  copyrightPk, uploadtreePk);
867  }
868  else
869  {
870  QueryResult rWrite =
871  eventExists
873  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
874  "reuserCopyrightEventUpdateContent",
875  "UPDATE copyright_event SET upload_fk=$1, content=$4,"
876  " hash=md5($4)"
877  " WHERE copyright_fk=$2 AND uploadtree_fk=$3",
878  int, int, int, char*),
879  uploadFk, copyrightPk, uploadtreePk, contentEdited.c_str())
881  fo_dbManager_PrepareStamement(dbManager.getStruct_dbManager(),
882  "reuserCopyrightEventInsertContent",
883  "INSERT INTO copyright_event"
884  " (upload_fk, uploadtree_fk, copyright_fk,"
885  " is_enabled, content, hash)"
886  " VALUES($1,$3,$2,'true',$4,md5($4))",
887  int, int, int, char*),
888  uploadFk, copyrightPk, uploadtreePk, contentEdited.c_str());
889  if (!rWrite)
890  LOG_WARNING("Reuser: failed to update copyright_event content"
891  " (copyright_fk=%d, uploadtree_fk=%d), continuing.",
892  copyrightPk, uploadtreePk);
893  }
895  }
896  return true;
897 }
Database handler for the reuser agent.
virtual bool processUploadReuse(int uploadId, int reusedUploadId, int groupId, int reusedGroupId, int userId)
virtual bool getParentItemBounds(int uploadId, ItemTreeBounds &out)
Fetch the parent item bounds for a given upload.
virtual int createCopyOfClearingDecision(int uploadId, int newItemUploadTreePk, int userId, int groupId, int originalDecisionPk)
Copy an existing clearing decision to a new uploadtree item.
virtual int createDecisionFromEvents(int uploadId, int uploadTreeId, int userId, int groupId, int decType, int scope, const std::vector< int > &eventIds)
Create a clearing_decision linked to eventIds.
virtual bool reuseMainLicense(int uploadId, int groupId, int reusedUploadId, int reusedGroupId)
virtual ReuserDatabaseHandler spawn() const
virtual std::map< int, int > getClearingDecisionMapByPfile(int uploadId, int groupId)
Build a pfile_fk to clearing_decision_pk map for uploadId.
static int getDecisionTypePriority(int decisionType)
Priority for decision types during reuse conflict resolution.
virtual bool processEnhancedUploadReuse(int uploadId, int reusedUploadId, int groupId, int reusedGroupId, int userId)
static bool isValidIdentifier(const std::string &s)
Validate that s contains only characters safe for SQL identifiers.
std::string getRepoPathOfPfile(int pfileId)
virtual int insertClearingEvent(int uploadId, int uploadTreeId, int userId, int groupId, int licenseId, bool removed, int type, const std::string &reportInfo, const std::string &comment, const std::string &ack, int jobId)
Insert a new clearing event and return its primary key (0 on error).
static std::string replaceUnicodeControlChars(const std::string &input)
Strip Unicode control characters (C0, C1, DEL) from input.
virtual bool reuseCopyrights(int uploadId, int reusedUploadId, int userId)
virtual int writeArsRecord(int agentId, int uploadId, int arsId=0, bool success=false)
Write (insert or update) an ARS record.
virtual std::vector< ReuseTriple > getReusedUploads(int uploadId, int groupId)
Return the list of uploads that should be reused for uploadId.
virtual bool reuseConfSettings(int uploadId, int reusedUploadId)
virtual std::map< int, std::vector< int > > getUploadTreePksForPfiles(int uploadId, const std::vector< int > &pfileIds)
For a set of pfile ids, return a map pfile_fk to [uploadtree_pk].
Database handler for agents.
std::string queryUploadTreeTableName(int uploadId)
Get the upload tree table name for a given upload id.
bool commit() const
COMMIT a transaction block in DB.
bool begin() const
BEGIN a transaction block in DB.
char * getPFileNameForFileId(unsigned long pfileId) const
Get the file name of a give pfile id.
DbManager dbManager
DbManager to use.
bool rollback() const
ROLLBACK a transaction block in DB.
DB wrapper for agents.
QueryResult execPrepared(fo_dbManager_PreparedStatement *stmt,...) const
Execute a prepared statement with new parameters.
QueryResult queryPrintf(const char *queryFormat,...) const
Execute a query in printf format.
PGconn * getConnection() const
fo_dbManager * getStruct_dbManager() const
DbManager spawn() const
Wrapper for DB result.
std::vector< std::string > getRow(int i) const
int s
The socket that the CLI will use to communicate.
Definition: fo_cli.c:37
FUNCTION int min(int user_perm, int permExternal)
Get the minimum permission level required.
Definition: libfossagent.c:306
FUNCTION int fo_WriteARS(PGconn *pgConn, int ars_pk, int upload_pk, int agent_pk, const char *tableName, const char *ars_status, int ars_success)
Write ars record.
Definition: libfossagent.c:214
char * fo_RepMkPath(const char *Type, char *Filename)
Given a filename, construct the full path to the file.
Definition: libfossrepo.c:352
void fo_scheduler_heart(int i)
This function must be called by agents to let the scheduler know they are alive and how many items th...
int jobId
The id of the job.
int fo_scheduler_jobId()
Gets the id of the job that the agent is running.
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
fo namespace holds the FOSSology library functions.
bool stringToBool(const char *string)
Definition: libfossUtils.cc:32
Bounds of an item within an uploadtree table.
Definition: ReuserTypes.hpp:14