FOSSology  4.7.1
Open Source License Compliance by Open Source Software
LicenseCsvImport.php
Go to the documentation of this file.
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2014-2015 Siemens AG
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 
9 
15 
26 {
29  protected $dbManager;
32  protected $userDao;
35  protected $delimiter = ',';
38  protected $enclosure = '"';
41  protected $headrow = null;
44  protected $nkMap = array();
47  protected $mdkMap = array();
50  protected $alias = array(
51  'shortname'=>array('shortname','Short Name'),
52  'licensetype'=>array('licensetype','license_type','License Type'),
53  'fullname'=>array('fullname','Long Name'),
54  'spdx_id'=>array('spdx_id', 'SPDX ID'),
55  'text'=>array('text','Full Text'),
56  'parent_shortname'=>array('parent_shortname','Decider Short Name'),
57  'report_shortname'=>array('report_shortname','Regular License Text Short Name'),
58  'url'=>array('url','URL'),
59  'notes'=>array('notes'),
60  'source'=>array('source','Foreign ID'),
61  'risk'=>array('risk','risk_level'),
62  'group'=>array('group','License group'),
63  'obligations'=>array('obligations','License obligations'),
64  'external_id'=>array('id','LicenseDB Id'),
65  );
66 
69  protected $obligationMap;
70 
77  {
78  $this->dbManager = $dbManager;
79  $this->userDao = $userDao;
80  $this->obligationMap = $obligationMap;
81  }
86  public function setDelimiter($delimiter=',')
87  {
88  $this->delimiter = substr($delimiter,0,1);
89  }
90 
95  public function setEnclosure($enclosure='"')
96  {
97  $this->enclosure = substr($enclosure,0,1);
98  }
99 
106  public function handleFile($filename, $fileExtension)
107  {
108  if (!is_file($filename) || ($handle = fopen($filename, 'r')) === false) {
109  return _('Internal error');
110  }
111  $cnt = -1;
112  $msg = '';
113  try {
114  if ($fileExtension == 'csv') {
115  while (($row = fgetcsv($handle,0,$this->delimiter,$this->enclosure)) !== false) {
116  $log = $this->handleCsv($row);
117  if (!empty($log)) {
118  $msg .= "$log\n";
119  }
120  $cnt++;
121  }
122  $msg .= _('Read csv').(": $cnt ")._('licenses');
123  } else {
124  $jsonContent = fread($handle, filesize($filename));
125  $data = json_decode($jsonContent, true);
126  if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
127  $msg .= "Error decoding JSON: " . json_last_error_msg() . "\n";
128  }
129  $msg = $this->importJsonData($data, $msg);
130  $msg .= _('Read json').(":". count($data) ." ")._('licenses');
131  }
132  } catch(\Exception $e) {
133  fclose($handle);
134  return $msg .= _('Error while parsing file').': '.$e->getMessage();
135  }
136  fclose($handle);
137  return $msg;
138  }
139 
146  function handleRowJson($row)
147  {
148  $defaultValues = array(
149  'parent_shortname' => null,
150  'report_shortname' => null,
151  'url' => '',
152  'notes' => '',
153  'source' => '',
154  'risk' => 0,
155  'group' => null,
156  'spdx_id' => null
157  );
158  $newArray = array();
159  foreach ($row as $key => $value) {
160  $newKey = $key;
161  foreach ($this->alias as $aliasKey => $aliasValues) {
162  if (in_array($key, $aliasValues)) {
163  $newKey = $aliasKey;
164  break;
165  }
166  }
167  $newArray[$newKey] = $value;
168  }
169  foreach ($defaultValues as $key => $defaultValue) {
170  if (!array_key_exists($key, $newArray)) {
171  $newArray[$key] = $defaultValue;
172  }
173  }
174  return $newArray;
175  }
176 
183  private function handleCsv($row)
184  {
185  if ($this->headrow === null) {
186  $this->headrow = $this->handleHeadCsv($row);
187  return 'head okay';
188  }
189 
190  $mRow = array();
191  foreach (array('shortname','fullname','text') as $needle) {
192  $mRow[$needle] = $row[$this->headrow[$needle]];
193  }
194  foreach (array('parent_shortname' => null, 'report_shortname' => null,
195  'url' => '', 'notes' => '', 'source' => '', 'risk' => 0,
196  'group' => null, 'spdx_id' => null, 'licensetype' => null
197  ) as $optNeedle=>$defaultValue) {
198  $mRow[$optNeedle] = $defaultValue;
199  if ($this->headrow[$optNeedle]!==false && array_key_exists($this->headrow[$optNeedle], $row)) {
200  $mRow[$optNeedle] = $row[$this->headrow[$optNeedle]];
201  }
202  }
203 
204  return $this->handleCsvLicense($mRow);
205  }
206 
213  private function handleJsonLicense($row)
214  {
215  $mRow = $this->handleRowJson($row);
216  foreach (array('parent_shortname' => null, 'report_shortname' => null,
217  'url' => '', 'notes' => '', 'source' => '', 'risk' => 0, 'active' => true,
218  'group' => null, 'spdx_id' => null, 'licensetype' => null,
219  'obligation_ids' => array()
220  ) as $optNeedle => $defaultValue) {
221  if (!array_key_exists($optNeedle, $mRow)) {
222  $mRow[$optNeedle] = $defaultValue;
223  }
224  }
225 
226  if (empty($mRow['external_id'])) {
227  return $this->handleCsvLicense($mRow);
228  }
229 
230  return $this->handleLicenseDBLicenseImport($mRow);
231  }
232 
239  private function handleHeadCsv($row)
240  {
241  $headrow = array();
242  $row[0] = trim($row[0], "\xEF\xBB\xBF"); // Remove BOM
243  foreach (array('shortname','fullname','text') as $needle) {
244  $col = ArrayOperation::multiSearch($this->alias[$needle], $row);
245  if (false === $col) {
246  throw new \Exception("Undetermined position of $needle");
247  }
248  $headrow[$needle] = $col;
249  }
250  foreach (array('parent_shortname', 'report_shortname', 'url', 'notes',
251  'source', 'risk', 'group', 'spdx_id', 'licensetype') as $optNeedle) {
252  $headrow[$optNeedle] = ArrayOperation::multiSearch($this->alias[$optNeedle], $row);
253  }
254  return $headrow;
255  }
256 
263  private function updateLicense($row, $rfPk)
264  {
265  $stmt = __METHOD__ . '.getOldLicense';
266  $oldLicense = $this->dbManager->getSingleRow('SELECT ' .
267  'rf_shortname, rf_fullname, rf_spdx_id, rf_text, rf_url, rf_notes, rf_source, rf_risk, rf_licensetype, rf_active ' .
268  'FROM license_ref WHERE rf_pk = $1', array($rfPk), $stmt);
269 
270  $log = "License '$row[shortname]' already exists in DB (id = $rfPk)";
271  $stmt = __METHOD__ . '.updateLicense';
272  $sql = "UPDATE license_ref SET ";
273  if (! empty($row['group'])) {
274  $sql = "UPDATE license_candidate SET ";
275  }
276  $extraParams = array();
277  $param = array($rfPk);
278  if (isset($row['fullname']) && $row['fullname'] != $oldLicense['rf_fullname']) {
279  $param[] = $row['fullname'];
280  $stmt .= '.fullN';
281  $extraParams[] = "rf_fullname=$" . count($param);
282  $log .= ", updated fullname";
283  }
284  // update shortname only if import from licensedb
285  if (isset($row['external_id']) && isset($row['shortname']) && $row['shortname'] !== $oldLicense['rf_shortname']) {
286  $param[] = $row['shortname'];
287  $stmt .= '.shortN';
288  $extraParams[] = "rf_shortname=$" . count($param);
289  $log .= ", updated shortname";
290  }
291  if (isset($row['spdx_id']) && $row['spdx_id'] != $oldLicense['rf_spdx_id']) {
292  $param[] = $row['spdx_id'];
293  $stmt .= '.spId';
294  $extraParams[] = "rf_spdx_id=$" . count($param);
295  $log .= ", updated SPDX ID";
296  }
297  if (isset($row['text']) && $row['text'] != $oldLicense['rf_text'] && $row['text'] != LicenseMap::TEXT_MAX_CHAR_LIMIT) {
298  $param[] = $row['text'];
299  $stmt .= '.text';
300  $extraParams[] = "rf_text=$" . count($param) . ",rf_md5=md5($" .
301  count($param) . ")";
302  $log .= ", updated text";
303  }
304  if (isset($row['url']) && $row['url'] != $oldLicense['rf_url']) {
305  $param[] = $row['url'];
306  $stmt .= '.url';
307  $extraParams[] = "rf_url=$" . count($param);
308  $log .= ", updated URL";
309  }
310  if (isset($row['notes']) && $row['notes'] != $oldLicense['rf_notes']) {
311  $param[] = $row['notes'];
312  $stmt .= '.notes';
313  $extraParams[] = "rf_notes=$" . count($param);
314  $log .= ", updated notes";
315  }
316  if (isset($row['source']) && $row['source'] != $oldLicense['rf_source']) {
317  $param[] = $row['source'];
318  $stmt .= '.updSource';
319  $extraParams[] = "rf_source=$".count($param);
320  $log .= ', updated the source';
321  }
322  if (isset($row['risk']) && $row['risk'] != $oldLicense['rf_risk']) {
323  $param[] = $row['risk'];
324  $stmt .= '.updRisk';
325  $extraParams[] = "rf_risk=$".count($param);
326  $log .= ', updated the risk level';
327  }
328  if (isset($row['licensetype']) && $row['licensetype'] != $oldLicense['rf_licensetype']) {
329  $param[] = $row['licensetype'];
330  $stmt .= '.types';
331  $extraParams[] = "rf_licensetype=$".count($param);
332  $log .= ', updated the licensetype';
333  }
334  if (isset($row['active']) && $row['active'] !=
335  $this->dbManager->booleanFromDb($oldLicense['rf_active'])) {
336  $param[] = $this->dbManager->booleanToDb($row['active']);
337  $stmt .= '.active';
338  $extraParams[] = 'rf_active=$' . count($param);
339  $log .= ', updated active';
340  }
341  if (count($param) > 1) {
342  $sql .= join(",", $extraParams);
343  $sql .= " WHERE rf_pk=$1;";
344  $this->dbManager->getSingleRow($sql, $param, $stmt);
345  if (!isset($row['external_id'])) {
346  $this->mdkMap[md5($row['text'])] = $rfPk;
347  }
348  }
349 
350  // edit license maps only in case of normal import
351  // In case of import from licensedb, the mapping info must come from licensedb
352  if ($row['external_id'] === null) {
353  $stmt = __METHOD__ . '.getOldMapping';
354  $sql = 'SELECT rf_parent FROM license_map WHERE rf_fk = $1 AND usage = $2;';
355  $oldParent = null;
356  $oldParentRow = $this->dbManager->getSingleRow($sql, array($rfPk,
357  LicenseMap::CONCLUSION), $stmt);
358  if (!empty($oldParentRow)) {
359  $oldParent = $oldParentRow['rf_parent'];
360  }
361  $oldReport = null;
362  $oldReportRow = $this->dbManager->getSingleRow($sql, array($rfPk,
363  LicenseMap::REPORT), $stmt);
364  if (!empty($oldReportRow)) {
365  $oldReport = $oldReportRow['rf_parent'];
366  }
367 
368  $newParent = null;
369  $newParent = ($row['parent_shortname'] == null) ? null :
370  $this->getKeyFromShortname($row['parent_shortname']);
371 
372  $newReport = null;
373  $newReport = ($row['report_shortname'] == null) ? null :
374  $this->getKeyFromShortname($row['report_shortname']);
375 
376  if (($oldParent != $newParent) && $this->setMap($newParent, $rfPk, LicenseMap::CONCLUSION)) {
377  $log .= " with conclusion '$row[parent_shortname]'";
378  }
379  if (($oldReport != $newReport) && $this->setMap($newReport, $rfPk, LicenseMap::REPORT)) {
380  $log .= " reporting '$row[report_shortname]'";
381  }
382  }
383  return $log;
384  }
385 
394  private function handleCsvLicense($row)
395  {
396  if (empty($row['risk'])) {
397  $row['risk'] = 0;
398  }
399  if (empty($row['external_id'])) {
400  $row['external_id'] = null;
401  }
402  $rfPk = $this->getKeyFromShortname($row['shortname'], $row['group']);
403  $md5Match = $this->getKeyFromMd5($row['text']);
404 
405  // If shortname exists, does not collide with other texts and is not
406  // candidate
407  if ($rfPk !== false) {
408  if (! empty($row['group']) || ($md5Match == $rfPk || $md5Match === false)) {
409  return $this->updateLicense($row, $rfPk);
410  } else {
411  return "Error: MD5 checksum of '" . $row['shortname'] .
412  "' collides with license id=$md5Match";
413  }
414  }
415  if ($md5Match !== false && empty($row['group'])) {
416  return "Error: MD5 checksum of '" . $row['shortname'] .
417  "' collides with license id=$md5Match";
418  }
419 
420  $return = "";
421  if (!empty($row['group'])) {
422  $return = $this->insertNewLicense($row, "license_candidate");
423  } else {
424  $return = $this->insertNewLicense($row, "license_ref");
425  }
426  // insertNewLicense() returns a plain string on error (e.g. missing group)
427  // or an array on success.
428  if (is_string($return)) {
429  return $return;
430  }
431  return $return['log'];
432  }
433 
446  private function handleLicenseDBLicenseImport($row)
447  {
448  if (empty($row["external_id"])) {
449  return "Error: external_id cannot be empty";
450  }
451  $stmt = __METHOD__ . ".getExistingLicense";
452  $rfPk = $this->dbManager->getSingleRow("SELECT rf_pk FROM " .
453  "license_ref WHERE rf_external_id=$1", array($row["external_id"]), $stmt);
454  $log = '';
455  if (!empty($rfPk)) {
456  // update license
457  $log .= $this->updateLicense($row, $rfPk['rf_pk']);
458 
459  $this->dbManager->begin();
460  // fetch new obligation associations
461  $stmt = __METHOD__ . "getNewLicenseObligations";
462  $newObIds = array();
463  foreach ($row['obligation_ids'] as $obExternalId) {
464  $obPk = $this->dbManager->getSingleRow("SELECT ob_pk FROM obligation_ref WHERE ob_external_id = $1;",
465  array($obExternalId), $stmt);
466  if (!empty($obPk)) {
467  $newObIds[] = $obPk['ob_pk'];
468  } else {
469  $log .= \sprintf('obligation with ob_external_id %s not found', $obExternalId);
470  }
471  }
472 
473  // fetch old obligation associations
474  $stmt = __METHOD__ . "getOldObligationAssociations";
475  $oldObIdsDB = $this->dbManager->getRows("SELECT ob_fk FROM obligation_map WHERE rf_fk = $1", array($rfPk['rf_pk']), $stmt);
476  $oldObIds = array();
477  foreach ($oldObIdsDB as $obId) {
478  $oldObIds[] = $obId['ob_fk'];
479  }
480 
481  // create diff of obligations between current associated obligations and licensedb
482  // associated obligations
483  $diff = ArrayOperation::getArrayDiffs($oldObIds, $newObIds);
484 
485  if (!empty($diff['remove'])) {
486  $log .= ' unlinked obligation ids [' . implode(',', $diff['remove']) . ']';
487  }
488  if (!empty($diff['add'])) {
489  $log .= ' linked obligation ids [' . implode(',', $diff['add']) . ']';
490  }
491 
492  // delete associations
493  foreach ($diff['remove'] as $obid) {
494  $this->obligationMap->unassociateLicenseFromObligation($obid, $rfPk['rf_pk']);
495  }
496 
497  // insert associations
498  foreach ($diff['add'] as $obid) {
499  $this->obligationMap->associateLicenseWithObligation($obid, $rfPk['rf_pk']);
500  }
501  $this->dbManager->commit();
502  } else {
503  $licRetVal = $this->insertNewLicense($row, "license_ref");
504  $log .= $licRetVal['log'];
505 
506  $this->dbManager->begin();
507  $newObIds = array();
508  $stmt = __METHOD__ . "getNewLicenseObligationsForInsert";
509  foreach ($row['obligation_ids'] as $obExternalId) {
510  $obPk = $this->dbManager->getSingleRow("SELECT ob_pk FROM obligation_ref WHERE ob_external_id = $1;",
511  array($obExternalId), $stmt);
512  if (!empty($obPk)) {
513  $newObIds[] = $obPk['ob_pk'];
514  } else {
515  $log .= \sprintf('obligation with ob_external_id %s not found', $obExternalId);
516  }
517  }
518 
519  // insert associations
520  if (!empty($newObIds)) {
521  $log .= ' linked obligation ids [' . implode(',', $newObIds) . ']';
522  }
523  foreach ($newObIds as $obid) {
524  $this->obligationMap->associateLicenseWithObligation($obid, $licRetVal['pkey']);
525  }
526  $this->dbManager->commit();
527  }
528 
529  return $log;
530  }
531 
543  private function insertMapIfNontrivial($fromName,$toName,$usage)
544  {
545  $isNontrivial = ($fromName!==null && $fromName!=$toName && $this->getKeyFromShortname($fromName)!==false);
546  if ($isNontrivial) {
547  $this->dbManager->insertTableRow('license_map',
548  array('rf_fk'=>$this->getKeyFromShortname($toName),
549  'rf_parent'=>$this->getKeyFromShortname($fromName),
550  'usage'=> $usage));
551  }
552  return $isNontrivial;
553  }
554 
560  private function getKeyFromShortname($shortname, $groupFk = null)
561  {
562  $keyName = $shortname;
563  $tableName = "license_ref";
564  $addCondition = "AND rf_external_id IS NULL";
565  $statement = __METHOD__ . ".getId";
566  $params = array($shortname);
567 
568  if ($groupFk != null) {
569  $keyName .= $groupFk;
570  $tableName = "license_candidate";
571  $addCondition = "AND group_fk = $2";
572  $statement .= ".candidate";
573  $params[] = $this->userDao->getGroupIdByName($groupFk);
574  }
575  $sql = "SELECT rf_pk FROM ONLY $tableName WHERE rf_shortname = $1 $addCondition;";
576 
577  if (array_key_exists($keyName, $this->nkMap)) {
578  return $this->nkMap[$keyName];
579  }
580  $row = $this->dbManager->getSingleRow($sql, $params, $statement);
581  $this->nkMap[$keyName] = ($row===false) ? false : $row['rf_pk'];
582  return $this->nkMap[$keyName];
583  }
584 
590  private function getKeyFromMd5($licenseText)
591  {
592  $md5 = md5($licenseText);
593  if (array_key_exists($md5, $this->mdkMap)) {
594  return $this->mdkMap[$md5];
595  }
596  $row = $this->dbManager->getSingleRow("SELECT rf_pk " .
597  "FROM ONLY license_ref WHERE rf_md5=md5($1) AND rf_external_id IS NULL",
598  array($licenseText));
599  $this->mdkMap[$md5] = (empty($row)) ? false : $row['rf_pk'];
600  return $this->mdkMap[$md5];
601  }
602 
613  private function setMap($from, $to, $usage)
614  {
615  $return = false;
616  if (!empty($from)) {
617  $sql = "SELECT license_map_pk, rf_parent FROM license_map WHERE rf_fk = $1 AND usage = $2;";
618  $statement = __METHOD__ . ".getCurrentMapping";
619  $row = $this->dbManager->getSingleRow($sql, array($to, $usage), $statement);
620  if (!empty($row) && $row['rf_parent'] != $from) {
621  $this->dbManager->updateTableRow("license_map", array(
622  'rf_fk' => $to,
623  'rf_parent' => $from,
624  'usage' => $usage
625  ), 'license_map_pk', $row['license_map_pk']);
626  $return = true;
627  } elseif (empty($row)) {
628  $this->dbManager->insertTableRow('license_map', array(
629  'rf_fk' => $to,
630  'rf_parent' => $from,
631  'usage' => $usage
632  ));
633  $return = true;
634  }
635  }
636  return $return;
637  }
638 
648  private function insertNewLicense($row, $tableName = "license_ref")
649  {
650  $stmtInsert = __METHOD__ . '.insert.' . $tableName;
651  $columns = array(
652  "rf_shortname" => $row['shortname'],
653  "rf_licensetype" => $row['licensetype'] ?: 'Unknown',
654  "rf_fullname" => $row['fullname'],
655  "rf_spdx_id" => $row['spdx_id'],
656  "rf_text" => $row['text'],
657  "rf_md5" => md5($row['text']),
658  "rf_detector_type" => 1,
659  "rf_url" => $row['url'],
660  "rf_notes" => $row['notes'],
661  "rf_source" => $row['source'],
662  "rf_risk" => $row['risk'],
663  "rf_external_id" => $row['external_id'],
664  "rf_active" => $this->dbManager->booleanToDb($row['active'] ?: true),
665  );
666 
667  $as = "";
668  if ($tableName == "license_candidate") {
669  $groupId = $this->userDao->getGroupIdByName($row['group']);
670  if (empty($groupId)) {
671  return "Error: Unable to insert candidate license " . $row['shortname'] .
672  " as group " . $row['group'] . " does not exist";
673  }
674  $columns["group_fk"] = $groupId;
675  $columns["marydone"] = $this->dbManager->booleanToDb(true);
676  $as = " as candidate license under group " . $row["group"];
677  }
678 
679  $newPk = $this->dbManager->insertTableRow($tableName, $columns, $stmtInsert, 'rf_pk');
680 
681  // populate license maps and cache only when the import is not from licensedb
682  $log = "Inserted '$row[shortname]' in DB" . $as;
683  if ($row['external_id'] === null) {
684  if ($tableName == "license_candidate") {
685  $this->nkMap[$row['shortname'].$row['group']] = $newPk;
686  } else {
687  $this->nkMap[$row['shortname']] = $newPk;
688  }
689  $this->mdkMap[md5($row['text'])] = $newPk;
690 
691  if ($this->insertMapIfNontrivial($row['parent_shortname'], $row['shortname'], LicenseMap::CONCLUSION)) {
692  $log .= " with conclusion '$row[parent_shortname]'";
693  }
694  if ($this->insertMapIfNontrivial($row['report_shortname'], $row['shortname'], LicenseMap::REPORT)) {
695  $log .= " reporting '$row[report_shortname]'";
696  }
697  }
698 
699  $return = array();
700  $return['log'] = $log;
701  $return['pkey'] = $newPk;
702  return $return;
703  }
704 
710  public function importJsonData($data, string $msg): string
711  {
712  foreach ($data as $row) {
713  $log = $this->handleJsonLicense($row);
714  if (!empty($log)) {
715  $msg .= "$log\n";
716  }
717  }
718  return $msg;
719  }
720 }
insertMapIfNontrivial($fromName, $toName, $usage)
Insert in license_map table if the license conclusion is non-trivial.
handleFile($filename, $fileExtension)
Read the CSV line by line and import it.
handleCsvLicense($row)
Handle a single row from CSV.
updateLicense($row, $rfPk)
Update the license info in the DB.
handleLicenseDBLicenseImport($row)
Handle a single row from csv import form LicenseDB.
setEnclosure($enclosure='"')
Update the enclosure.
getKeyFromShortname($shortname, $groupFk=null)
Get the license id using license shortname from DB or nkMap.
__construct(DbManager $dbManager, UserDao $userDao, $obligationMap=null)
handleHeadCsv($row)
Handle a row as head row.
insertNewLicense($row, $tableName="license_ref")
Insert a new license in DB.
setDelimiter($delimiter=',')
Update the delimiter.
setMap($from, $to, $usage)
Update license mappings.
Wrapper class for license map.
Definition: LicenseMap.php:19
Wrapper class for obligation map.
Fossology exception.
Definition: Exception.php:15
static getArrayDiffs(array $oldArray, array $newArray)
Get list of additions/removals to make $oldArray equal to $newArray.
char * trim(char *ptext)
Trimming whitespace.
Definition: fossconfig.c:690
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
Utility functions for specific applications.