FOSSology  4.4.0
Open Source License Compliance by Open Source Software
DeciderAgent.php
Go to the documentation of this file.
1 <?php
2 /*
3  Author: Daniele Fognini
4  SPDX-FileCopyrightText: © 2014-2019 Siemens AG
5  SPDX-FileCopyrightText: © 2021 Orange by Piotr Pszczola <piotr.pszczola@orange.com>
6  SPDX-FileCopyrightText: © 2021 Kaushlendra Pratap <kaushlendrapratap.9837@gmail.com>
7 
8  SPDX-License-Identifier: GPL-2.0-only
9 */
45 namespace Fossology\Decider;
46 
61 use Symfony\Component\Process\Process;
62 
63 include_once(__DIR__ . "/version.php");
64 
69 class DeciderAgent extends Agent
70 {
71  const RULES_NOMOS_IN_MONK = 0x1;
72  const RULES_NOMOS_MONK_NINKA = 0x2;
73  const RULES_BULK_REUSE = 0x4;
74  const RULES_WIP_SCANNER_UPDATES = 0x8;
75  const RULES_OJO_NO_CONTRADICTION = 0x10;
76  const RULES_COPYRIGHT_FALSE_POSITIVE = 0x20;
77  const RULES_COPYRIGHT_FALSE_POSITIVE_CLUTTER = 0x40;
78  const RULES_ALL = 0xff; // self::RULES_NOMOS_IN_MONK | self::RULES_NOMOS_MONK_NINKA | ... -> feature not available in php5.3
79 
83  private $activeRules;
87  private $uploadDao;
99  private $clearingDao;
103  private $highlightDao;
107  private $showJobsDao;
111  private $decisionTypes;
115  private $licenseMap = null;
119  private $licenseMapUsage = null;
120 
124  private $copyrightDao;
125 
126  function __construct($licenseMapUsage=null)
127  {
128  parent::__construct(AGENT_DECIDER_NAME, AGENT_DECIDER_VERSION, AGENT_DECIDER_REV);
129 
130  $this->uploadDao = $this->container->get('dao.upload');
131  $this->clearingDao = $this->container->get('dao.clearing');
132  $this->highlightDao = $this->container->get('dao.highlight');
133  $this->showJobsDao = $this->container->get('dao.show_jobs');
134  $this->decisionTypes = $this->container->get('decision.types');
135  $this->clearingDecisionProcessor = $this->container->get('businessrules.clearing_decision_processor');
136  $this->agentLicenseEventProcessor = $this->container->get('businessrules.agent_license_event_processor');
137  $this->copyrightDao = $this->container->get('dao.copyright');
138  $this->licenseMapUsage = $licenseMapUsage;
139  $this->agentSpecifOptions = "r:";
140  }
141 
146  function processUploadId($uploadId)
147  {
148  $args = $this->args;
149  $this->activeRules = array_key_exists('r', $args) ? intval($args['r']) : self::RULES_ALL;
150  $this->licenseMap = new LicenseMap($this->dbManager, $this->groupId, $this->licenseMapUsage);
151 
152  if (array_key_exists("r", $args) && (($this->activeRules&self::RULES_COPYRIGHT_FALSE_POSITIVE)== self::RULES_COPYRIGHT_FALSE_POSITIVE)) {
153  $this->getCopyrightsToDisableFalsePositivesClutter($uploadId, false);
154  }
155  if (array_key_exists("r", $args) && (($this->activeRules&self::RULES_COPYRIGHT_FALSE_POSITIVE_CLUTTER)== self::RULES_COPYRIGHT_FALSE_POSITIVE_CLUTTER)) {
156  $this->getCopyrightsToDisableFalsePositivesClutter($uploadId, true);
157  }
158  if (array_key_exists("r", $args) && (($this->activeRules&self::RULES_BULK_REUSE)== self::RULES_BULK_REUSE)) {
159  $bulkReuser = new BulkReuser();
160  $bulkIds = $this->clearingDao->getPreviousBulkIds($uploadId, $this->groupId, $this->userId);
161  if (count($bulkIds) == 0) {
162  return true;
163  }
164  $jqId=0;
165  $minTime="4";
166  $maxTime="60";
167  foreach ($bulkIds as $bulkId) {
168  $jqId = $bulkReuser->rerunBulkAndDeciderOnUpload($uploadId, $this->groupId, $this->userId, $bulkId, $jqId);
169  $this->heartbeat(1);
170  if (!empty($jqId)) {
171  $jqIdRow = $this->showJobsDao->getDataForASingleJob($jqId);
172  while ($this->showJobsDao->getJobStatus($jqId)) {
173  $this->heartbeat(0);
174  $timeInSec = $this->showJobsDao->getEstimatedTime($jqIdRow['jq_job_fk'],'',0,0,1);
175  if ($timeInSec > $maxTime) {
176  sleep($maxTime);
177  } else if ($timeInSec < $minTime) {
178  sleep($minTime);
179  } else {
180  sleep($timeInSec);
181  }
182  }
183  }
184  }
185  }
186  $parentBounds = $this->uploadDao->getParentItemBounds($uploadId);
187  foreach ($this->uploadDao->getContainedItems($parentBounds) as $item) {
188  $process = $this->processItem($item);
189  $this->heartbeat($process);
190  }
191  return true;
192  }
193 
203  private function processItem(Item $item)
204  {
205  $itemTreeBounds = $item->getItemTreeBounds();
206 
207  $unMappedMatches = $this->agentLicenseEventProcessor->getLatestScannerDetectedMatches($itemTreeBounds);
208  $projectedScannerMatches = $this->remapByProjectedId($unMappedMatches);
209 
210  $lastDecision = $this->clearingDao->getRelevantClearingDecision($itemTreeBounds, $this->groupId);
211 
212  if (null!==$lastDecision && $lastDecision->getType()==DecisionTypes::IRRELEVANT) {
213  return 0;
214  }
215 
216  $currentEvents = $this->clearingDao->getRelevantClearingEvents($itemTreeBounds, $this->groupId);
217 
218  $markAsWip = false;
219  if (null !== $lastDecision && $projectedScannerMatches
220  && ($this->activeRules & self::RULES_WIP_SCANNER_UPDATES) == self::RULES_WIP_SCANNER_UPDATES) {
221  $licensesFromDecision = array();
222  foreach ($lastDecision->getClearingLicenses() as $clearingLicense) {
223  $licenseIdFromEvent = $this->licenseMap->getProjectedId($clearingLicense->getLicenseId());
224  $licensesFromDecision[$licenseIdFromEvent] = $licenseIdFromEvent;
225  }
226  $markAsWip = $this->existsUnhandledMatch($projectedScannerMatches,$licensesFromDecision);
227  }
228 
229  if (null !== $lastDecision && $markAsWip) {
230  $this->clearingDao->markDecisionAsWip($item->getId(), $this->userId, $this->groupId);
231  return 1;
232  }
233 
234  if (null!==$lastDecision || 0<count($currentEvents)) {
235  return 0;
236  }
237 
238  $haveDecided = false;
239 
240  if (($this->activeRules&self::RULES_OJO_NO_CONTRADICTION) == self::RULES_OJO_NO_CONTRADICTION) {
241  $haveDecided = $this->autodecideIfOjoMatchesNoContradiction($itemTreeBounds, $projectedScannerMatches);
242  }
243 
244  if (!$haveDecided && ($this->activeRules&self::RULES_OJO_NO_CONTRADICTION) == self::RULES_OJO_NO_CONTRADICTION) {
245  $haveDecided = $this->autodecideIfResoMatchesNoContradiction($itemTreeBounds, $projectedScannerMatches);
246  }
247 
248  if (!$haveDecided && ($this->activeRules&self::RULES_NOMOS_IN_MONK) == self::RULES_NOMOS_IN_MONK) {
249  $haveDecided = $this->autodecideNomosMatchesInsideMonk($itemTreeBounds, $projectedScannerMatches);
250  }
251 
252  if (!$haveDecided && ($this->activeRules&self::RULES_NOMOS_MONK_NINKA)== self::RULES_NOMOS_MONK_NINKA) {
253  $haveDecided = $this->autodecideNomosMonkNinka($itemTreeBounds, $projectedScannerMatches);
254  }
255 
256  if (!$haveDecided && $markAsWip) {
257  $this->clearingDao->markDecisionAsWip($item->getId(), $this->userId, $this->groupId);
258  }
259 
260  return ($haveDecided||$markAsWip ? 1 : 0);
261  }
262 
269  private function existsUnhandledMatch($projectedScannerMatches, $licensesFromDecision)
270  {
271  foreach (array_keys($projectedScannerMatches) as $projectedLicenseId) {
272  if (!array_key_exists($projectedLicenseId, $licensesFromDecision)) {
273  return true;
274  }
275  }
276  return false;
277  }
278 
287  private function autodecideIfOjoMatchesNoContradiction(ItemTreeBounds $itemTreeBounds, $matches)
288  {
289  $licenseMatchExists = count($matches) > 0;
290  foreach ($matches as $licenseMatches) {
291  $licenseMatchExists = $licenseMatchExists && $this->areOtherScannerFindingsAndOJOAgreed($licenseMatches);
292  }
293 
294  if ($licenseMatchExists) {
295  try {
296  $this->clearingDecisionProcessor->makeDecisionFromLastEvents(
297  $itemTreeBounds, $this->userId, $this->groupId,
298  DecisionTypes::IDENTIFIED, false);
299  } catch (\Exception $e) {
300  echo "Can not auto decide as file '" .
301  $itemTreeBounds->getItemId() . "' contains candidate license.\n";
302  }
303  }
304  return $licenseMatchExists;
305  }
306 
315  private function autodecideIfResoMatchesNoContradiction(ItemTreeBounds $itemTreeBounds, $matches)
316  {
317  $licenseMatchExists = count($matches) > 0;
318  foreach ($matches as $licenseMatches) {
319  $licenseMatchExists = $licenseMatchExists && $this->areOtherScannerFindingsAndRESOAgreed($licenseMatches);
320  }
321 
322  if ($licenseMatchExists) {
323  try {
324  $this->clearingDecisionProcessor->makeDecisionFromLastEvents(
325  $itemTreeBounds, $this->userId, $this->groupId,
326  DecisionTypes::IDENTIFIED, false);
327  } catch (\Exception $e) {
328  echo "Can not auto decide as file '" .
329  $itemTreeBounds->getItemId() . "' contains candidate license.\n";
330  }
331  }
332  return $licenseMatchExists;
333  }
334 
343  private function autodecideNomosMonkNinka(ItemTreeBounds $itemTreeBounds, $matches)
344  {
345  $canDecide = (count($matches)>0);
346 
347  foreach ($matches as $licenseMatches) {
348  if (!$canDecide) { // &= is not lazy
349  break;
350  }
351  $canDecide &= $this->areNomosMonkNinkaAgreed($licenseMatches);
352  }
353 
354  if ($canDecide) {
355  $this->clearingDecisionProcessor->makeDecisionFromLastEvents($itemTreeBounds, $this->userId, $this->groupId, DecisionTypes::IDENTIFIED, $global=true);
356  }
357  return $canDecide;
358  }
359 
368  private function autodecideNomosMatchesInsideMonk(ItemTreeBounds $itemTreeBounds, $matches)
369  {
370  $canDecide = (count($matches)>0);
371 
372  foreach ($matches as $licenseMatches) {
373  if (!$canDecide) { // &= is not lazy
374  break;
375  }
376  $canDecide &= $this->areNomosMatchesInsideAMonkMatch($licenseMatches);
377  }
378 
379  if ($canDecide) {
380  $this->clearingDecisionProcessor->makeDecisionFromLastEvents($itemTreeBounds, $this->userId, $this->groupId, DecisionTypes::IDENTIFIED, $global=true);
381  }
382  return $canDecide;
383  }
384 
391  protected function remapByProjectedId($matches)
392  {
393  $remapped = array();
394  foreach ($matches as $licenseId => $licenseMatches) {
395  $projectedId = $this->licenseMap->getProjectedId($licenseId);
396 
397  foreach ($licenseMatches as $agent => $agentMatches) {
398  $haveId = array_key_exists($projectedId, $remapped);
399  $haveAgent = $haveId && array_key_exists($agent, $remapped[$projectedId]);
400  if ($haveAgent) {
401  $remapped[$projectedId][$agent] = array_merge($remapped[$projectedId][$agent], $agentMatches);
402  } else {
403  $remapped[$projectedId][$agent] = $agentMatches;
404  }
405  }
406  }
407  return $remapped;
408  }
409 
416  private function isRegionIncluded($small, $big)
417  {
418  return ($big[0] >= 0) && ($small[0] >= $big[0]) && ($small[1] <= $big[1]);
419  }
420 
426  private function areNomosMatchesInsideAMonkMatch($licenseMatches)
427  {
428  if (!array_key_exists("nomos", $licenseMatches)) {
429  return false;
430  }
431  if (!array_key_exists("monk", $licenseMatches)) {
432  return false;
433  }
434 
435  foreach ($licenseMatches["nomos"] as $licenseMatch) {
436  $matchId = $licenseMatch->getLicenseFileId();
437  $nomosRegion = $this->highlightDao->getHighlightRegion($matchId);
438 
439  $found = false;
440  foreach ($licenseMatches["monk"] as $monkLicenseMatch) {
441  $monkRegion = $this->highlightDao->getHighlightRegion($monkLicenseMatch->getLicenseFileId());
442  if ($this->isRegionIncluded($nomosRegion, $monkRegion)) {
443  $found = true;
444  break;
445  }
446  }
447  if (!$found) {
448  return false;
449  }
450  }
451 
452  return true;
453  }
454 
460  protected function areNomosMonkNinkaAgreed($licenseMatches)
461  {
462  $scanners = array('nomos','monk','ninka');
463  $vote = array();
464  foreach ($scanners as $scanner) {
465  if (!array_key_exists($scanner, $licenseMatches)) {
466  return false;
467  }
468  foreach ($licenseMatches[$scanner] as $licenseMatch) {
469  $licId = $licenseMatch->getLicenseId();
470  $vote[$licId][$scanner] = true;
471  }
472  }
473 
474  foreach ($vote as $licId=>$voters) {
475  if (count($voters) != 3) {
476  return false;
477  }
478  }
479  return true;
480  }
481 
488  protected function getLicenseIdsOfMatchesForScanner($scanner, $licenseMatches)
489  {
490  if (array_key_exists($scanner, $licenseMatches) === true) {
491  return array_map(
492  function ($match) {
493  return $match->getLicenseId();
494  }, $licenseMatches[$scanner]);
495  }
496  return [];
497  }
498 
504  protected function areOtherScannerFindingsAndOJOAgreed($licenseMatches)
505  {
506  $findingsByOjo = $this->getLicenseIdsOfMatchesForScanner('ojo', $licenseMatches);
507  if (count($findingsByOjo) == 0) {
508  // nothing to do
509  return false;
510  }
511 
512  $findingsByOtherScanner = $this->getLicenseIdsOfMatchesForScanner('nomos', $licenseMatches);
513  if (count($findingsByOtherScanner) == 0) {
514  // nothing found by other scanner, so no contradiction
515  return true;
516  }
517  foreach ($findingsByOtherScanner as $findingsByScanner) {
518  if (in_array($findingsByScanner, $findingsByOjo) === false) {
519  // contradiction found
520  return false;
521  }
522  }
523  return true;
524  }
525 
531  protected function areOtherScannerFindingsAndRESOAgreed($licenseMatches)
532  {
533  $findingsByReso = $this->getLicenseIdsOfMatchesForScanner('reso', $licenseMatches);
534  if (count($findingsByReso) == 0) {
535  // nothing to do
536  return false;
537  }
538 
539  $findingsByOtherScanner = $this->getLicenseIdsOfMatchesForScanner('nomos', $licenseMatches);
540  if (count($findingsByOtherScanner) == 0) {
541  // nothing found by other scanner, so no contradiction
542  return true;
543  }
544  foreach ($findingsByOtherScanner as $findingsByScanner) {
545  if (in_array($findingsByScanner, $findingsByReso) === false) {
546  // contradiction found
547  return false;
548  }
549  }
550  return true;
551  }
552 
560  $clutter_flag): void
561  {
562  if (empty($uploadId)) {
563  return;
564  }
565  $agentName = 'copyright';
566  $uploadTreeTableName = $this->uploadDao->getUploadtreeTableName($uploadId);
567  $scanJobProxy = new ScanJobProxy($GLOBALS['container']->get('dao.agent'), $uploadId);
568  $scanJobProxy->createAgentStatus(array($agentName));
569  $selectedScanners = $scanJobProxy->getLatestSuccessfulAgentIds();
570  if (!array_key_exists($agentName, $selectedScanners)) {
571  return;
572  }
573  $latestXpAgentId = $selectedScanners[$agentName];
574  $extrawhere = ' agent_fk='.$latestXpAgentId;
575  $allCopyrights = $this->copyrightDao->getScannerEntries('copyright',
576  $uploadTreeTableName, $uploadId, null, $extrawhere);
577 
578  $copyrightJSON = json_encode($allCopyrights);
579  $tmpFile = tmpfile();
580  $tmpFilePath = stream_get_meta_data($tmpFile)['uri'];
581  fwrite($tmpFile, $copyrightJSON);
582  $deactivatedCopyrightData = $this->callCopyrightDeactivationClutterRemovalScript($tmpFilePath, $clutter_flag);
583  if (empty($deactivatedCopyrightData)) {
584  fclose($tmpFile);
585  return;
586  }
587  $deactivatedCopyrights = json_decode($deactivatedCopyrightData, true);
588  foreach ($deactivatedCopyrights as $deactivatedCopyright) {
589  $item = $deactivatedCopyright['uploadtree_pk'];
590  $itemTreeBounds = $this->uploadDao->getItemTreeBounds($item, $uploadTreeTableName);
591  $hash = $deactivatedCopyright['hash'];
592  $content = $deactivatedCopyright['content'];
593  $cpTable = 'copyright';
594  if ($deactivatedCopyright['is_copyright'] == "t") {
595  $action = '';
596  if (array_key_exists('decluttered_content', $deactivatedCopyright) &&
597  !empty($deactivatedCopyright['decluttered_content'])) {
598  $content = $deactivatedCopyright['decluttered_content'];
599  } else {
600  // No text update. Nothing to do.
601  $this->heartbeat(1);
602  continue;
603  }
604  } else {
605  $action = 'delete';
606  }
607  $this->copyrightDao->updateTable($itemTreeBounds, $hash, $content, $this->userId, $cpTable, $action);
608  $this->heartbeat(1);
609  }
610  fclose($tmpFile);
611  }
612 
619  private function callCopyrightDeactivationClutterRemovalScript($tmpFilePath,
620  $clutter_flag): string
621  {
622  $script = "copyrightDeactivationClutterRemovalScript.py";
623  $args = ["python3", __DIR__ . "/$script", "--file", $tmpFilePath];
624  if ($clutter_flag) {
625  $args[] = "--clutter";
626  }
627 
628  $sleepTime = 5;
629  $maxSleepTime = 25;
630 
631  $process = new Process($args);
632  $process->setTimeout(null); // Remove timeout to run indefinitely.
633  $process->setEnv(set_python_path());
634  $process->start();
635 
636  do {
637  $this->heartbeat(0);
638  sleep($sleepTime);
639  if ($sleepTime < $maxSleepTime) {
640  $sleepTime += 5;
641  }
642  } while ($process->isRunning());
643 
644  echo $process->getErrorOutput();
645 
646  return $process->getOutput();
647  }
648 }
Prepares bulk licenses for an upload and run DeciderJob on it.
Definition: BulkReuser.php:23
Agent to decide license findings in an upload.
existsUnhandledMatch($projectedScannerMatches, $licensesFromDecision)
Check if matches contains unhandled match.
areOtherScannerFindingsAndRESOAgreed($licenseMatches)
Check if the finding by only contains one single license and that no other scanner (nomos) has produc...
autodecideIfResoMatchesNoContradiction(ItemTreeBounds $itemTreeBounds, $matches)
Auto decide matches which are in nomos, monk, OJO and Reso findings.
processItem(Item $item)
Given an item, check with the $activeRules and apply rules to it.
areOtherScannerFindingsAndOJOAgreed($licenseMatches)
Check if the finding by only contains one single license and that no other scanner (nomos) has produc...
callCopyrightDeactivationClutterRemovalScript($tmpFilePath, $clutter_flag)
autodecideNomosMatchesInsideMonk(ItemTreeBounds $itemTreeBounds, $matches)
Auto decide matches by nomos which are in monk findings.
autodecideNomosMonkNinka(ItemTreeBounds $itemTreeBounds, $matches)
Auto decide matches which are in nomos, monk and ninka findings.
processUploadId($uploadId)
Given an upload ID, process the items in it.
getLicenseIdsOfMatchesForScanner($scanner, $licenseMatches)
extracts the matches corresponding to a scanner from a $licenseMatches structure
isRegionIncluded($small, $big)
Check if the small highlight region is inside big one.
autodecideIfOjoMatchesNoContradiction(ItemTreeBounds $itemTreeBounds, $matches)
Auto decide matches which are in nomos, monk and OJO findings.
areNomosMatchesInsideAMonkMatch($licenseMatches)
Check if matches by nomos are inside monk findings.
getCopyrightsToDisableFalsePositivesClutter($uploadId, $clutter_flag)
remapByProjectedId($matches)
Given a set of matches, remap according to project id instead of license id.
areNomosMonkNinkaAgreed($licenseMatches)
Check if findings by all agents are same or not.
Structure of an Agent with all required parameters.
Definition: Agent.php:41
heartbeat($newProcessed)
Send hear beat to the scheduler.
Definition: Agent.php:203
Utility functions to process ClearingDecision.
Wrapper class for license map.
Definition: LicenseMap.php:19
set_python_path()
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
Namespace for decider agent.
Definition: BulkReuser.php:8
$GLOBALS['xyyzzzDeciderJob']