FOSSology  4.5.1
Open Source License Compliance by Open Source Software
SpdxTwoImportSource.php
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2015-2017,2023-2024 Siemens AG
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 
8 namespace Fossology\ReportImport;
9 
10 use EasyRdf\Graph;
11 use EasyRdf\Literal;
12 use EasyRdf\RdfNamespace;
13 use EasyRdf\Resource;
17 
18 require_once 'ReportImportData.php';
19 require_once 'ReportImportDataItem.php';
20 require_once 'ImportSource.php';
21 
23 {
24  const TERMS = 'http://spdx.org/rdf/terms#';
25  const SPDX_URL = 'http://spdx.org/licenses/';
26  const SPDX_FILE = 'spdx:File';
27 
29  private $filename;
31  private $uri;
33  private $graph;
35  private $spdxDoc;
36 
37  function __construct($filename, $uri = null)
38  {
39  $this->filename = $filename;
40  $this->uri = $uri;
41  }
42 
46  public function parse()
47  {
48  RdfNamespace::set('spdx', self::TERMS);
49  $this->graph = $this->loadGraph($this->filename, $this->uri);
50  $this->spdxDoc = $this->getSpdxDoc();
51  return $this->graph !== null && $this->spdxDoc !== null;
52  }
53 
54  public function getVersion(){
55  RdfNamespace::set('spdx', self::TERMS);
56  $this->graph = $this->loadGraph($this->filename, $this->uri);
57  $this->spdxDoc = (count($docs = $this->graph->allOfType("spdx:SpdxDocument"))) == 1 ? $docs[0] : null;
58  if ($this->spdxDoc !== null){
59  $specVersion = explode('-', $this->spdxDoc->getLiteral("spdx:specVersion"))[1];
60  return $specVersion;
61  } else {
62  return null;
63  }
64  }
65 
66  private function loadGraph($filename, $uri = null)
67  {
69  $graph = new Graph();
70  if (StringOperation::stringEndsWith($filename, ".rdf") || StringOperation::stringEndsWith($filename, ".rdf.xml")) {
71  $graph->parseFile($filename, 'rdfxml', $uri);
72  } elseif (StringOperation::stringEndsWith($filename, ".ttl")) {
73  $graph->parseFile($filename, 'turtle', $uri);
74  } else {
75  $graph->parseFile($filename, 'guess', $uri);
76  }
77  return $graph;
78  }
79 
80  private function getSpdxDoc()
81  {
82  $docs = $this->graph->allOfType("spdx:SpdxDocument");
83  if (count($docs) == 1) {
84  return $docs[0];
85  } else {
86  error_log("ERROR: Expected exactly one SPDX document, found " . count($docs));
87  return null;
88  }
89  }
90 
94  public function getAllFiles()
95  {
96  $relationship = $this->spdxDoc->getResource("spdx:relationship");
97  $element = $relationship->getResource("spdx:relatedSpdxElement");
98  /* @var $files Resource[] */
99  $files = $element->allResources("spdx:hasFile");
100  if (count($files) < 1) {
101  $files = $this->getNestedFiles($element);
102  }
103  $fileIds = array();
104  foreach ($files as $file) {
105  $fileIds[$file->getUri()] = trim($file->getLiteral("spdx:fileName")->getValue());
106  }
107  return $fileIds;
108  }
109 
114  private function getNestedFiles($element): array
115  {
116  $nestedFiles = [];
118  $nestedRelations = $element->allResources("spdx:relationship");
119  foreach ($nestedRelations as $nestedRelation) {
120  $relationType = $nestedRelation->getResource("spdx:relationshipType");
121  if (StringOperation::stringEndsWith($relationType->getUri(), "contains")) {
122  $fileResource = $nestedRelation->getResource("spdx:relatedSpdxElement");
123  if ($fileResource !== null) {
124  $nestedFiles[] = $fileResource;
125  }
126  }
127  }
128  return $nestedFiles;
129  }
130 
135  public function getHashesMap($fileId)
136  {
137  $fileNode = $this->graph->resource($fileId, self::SPDX_FILE);
138  if ($fileNode->getLiteral("spdx:fileName") == null) {
139  return [];
140  }
141 
142  $hashes = [];
143 
144  $algoKeyPrefix = self::TERMS . 'checksumAlgorithm_';
145 
146  /* @var $checksums Resource[] */
147  $checksums = $fileNode->allResources("spdx:checksum");
148  foreach ($checksums as $checksum) {
149  $algorithm = $checksum->getResource("spdx:algorithm");
150  $value = $checksum->getLiteral("spdx:checksumValue");
151  if ($algorithm != null && $value != null) {
152  if (StringOperation::stringStartsWith($algorithm->getUri(), $algoKeyPrefix)) {
153  $algo = substr($algorithm->getUri(), strlen($algoKeyPrefix));
154  } else {
155  $algo = $algorithm->getUri();
156  }
157  $hashes[$algo] = trim($value->getValue());
158  }
159  }
160  return $hashes;
161  }
162 
167  public function getDataForFile($fileid): ReportImportData
168  {
169  return new ReportImportData($this->getLicenseInfoInFileForFile($fileid),
170  $this->getConcludedLicenseInfoForFile($fileid),
171  $this->getCopyrightTextsForFile($fileid));
172  }
173 
178  public function getLicenseInfoInFileForFile($propertyId)
179  {
180  return $this->getLicenseInfoForFile($propertyId, 'licenseInfoInFile');
181  }
182 
188  private function getLicenseInfoForFile($fileId, $kind)
189  {
190  $fileNode = $this->graph->resource($fileId, self::SPDX_FILE);
191  /* @var $licenses Resource[] */
192  $licenses = $fileNode->allResources("spdx:$kind");
193 
194  $output = [];
195  foreach ($licenses as $license) {
196  if (!$this->isNotNoassertion($license->getUri())) {
197  continue;
198  }
199  $innerOutput = $this->parseLicense($license);
200  foreach ($innerOutput as $innerItem) {
201  $output[] = $innerItem;
202  }
203  }
204  return $output;
205  }
206 
207  private function isNotNoassertion($str)
208  {
209  return !(strtolower($str) === self::TERMS . "noassertion" ||
210  strtolower($str) === "http://spdx.org/licenses/noassertion");
211  }
212 
223  private function parseLicense($license)
224  {
225  if (is_string($license)) {
226  return $this->parseLicenseId($license);
227  } elseif ($license->isA('spdx:ExtractedLicensingInfo') ||
228  $license->isA('spdx:License') ||
229  $license->isA('spdx:ListedLicense')) {
230  return $this->handleLicenseInfo($license);
231  } elseif ($license->isA('spdx:DisjunctiveLicenseSet') ||
232  $license->isA('spdx:ConjunctiveLicenseSet')) {
233  return $this->handleLicenseSet($license);
234  } elseif ($license->isA('spdx:OrLaterOperator')) {
235  return $this->handleOrLaterOperator($license);
236  }
237  if ($license instanceof Resource || $license instanceof Graph) {
238  return $this->parseLicenseId($license->getUri());
239  } else {
240  error_log("ERROR: can not handle license=[" . $license . "] of class=[" .
241  get_class($license) . "]");
242  return [];
243  }
244  }
245 
246  private function parseLicenseId($licenseId)
247  {
248  if (!is_string($licenseId)) {
249  error_log("ERROR: Id not a string: " . $licenseId);
250  return [];
251  }
252  if (!$this->isNotNoassertion($licenseId)) {
253  return [];
254  }
255 
256  if (StringOperation::stringStartsWith($licenseId, self::SPDX_URL)) {
257  $spdxId = urldecode(substr($licenseId, strlen(self::SPDX_URL)));
258  $item = new ReportImportDataItem($spdxId);
259  return [$item];
260  } else {
261  error_log("ERROR: can not handle license with ID=" . $licenseId);
262  return [];
263  }
264  }
265 
273  private function handleLicenseInfo($license)
274  {
275  $licenseIdLiteral = $license->getLiteral("spdx:licenseId");
276  $licenseNameLiteral = $license->getLiteral("spdx:name");
277  if ($license->isA('spdx:ExtractedLicensingInfo')) {
278  $licenseTextLiteral = $license->getLiteral("spdx:extractedText");
279  } else {
280  $licenseTextLiteral = $license->getLiteral("spdx:licenseText");
281  }
282  if ($licenseIdLiteral != null && $licenseNameLiteral != null &&
283  $licenseTextLiteral != null) {
284  $seeAlsoLiteral = $license->getLiteral("rdfs:seeAlso");
285  $rawLicenseId = $licenseIdLiteral->getValue();
286  $licenseId = $this->stripLicenseRefPrefix($rawLicenseId);
287 
288  if ($license->isA('spdx:ExtractedLicensingInfo') &&
289  (strlen($licenseId) > 33 &&
290  substr($licenseId, -33, 1) === "-" &&
291  ctype_alnum(substr($licenseId, -32))
292  )) {
293  $licenseId = substr($licenseId, 0, -33);
294  $item = new ReportImportDataItem($licenseId);
295  $item->setCustomText($licenseTextLiteral->getValue());
296  } else {
297  $item = new ReportImportDataItem($licenseId);
298  $item->setLicenseCandidate($licenseNameLiteral->getValue(),
299  $licenseTextLiteral->getValue(),
300  strpos($rawLicenseId, LicenseRef::SPDXREF_PREFIX),
301  ($seeAlsoLiteral != null) ? $seeAlsoLiteral->getValue() : ""
302  );
303  }
304  return [$item];
305  }
306  return [];
307  }
308 
309  private function stripLicenseRefPrefix($licenseId)
310  {
311  if (StringOperation::stringStartsWith($licenseId, LicenseRef::SPDXREF_PREFIX)) {
312  if (StringOperation::stringStartsWith($licenseId, LicenseRef::SPDXREF_PREFIX_FOSSOLOGY)) {
313  return urldecode(substr($licenseId, strlen(LicenseRef::SPDXREF_PREFIX_FOSSOLOGY)));
314  }
315  return urldecode(substr($licenseId, strlen(LicenseRef::SPDXREF_PREFIX)));
316  } else {
317  return urldecode($licenseId);
318  }
319  }
320 
321  private function handleLicenseSet($license)
322  {
323  $output = [];
324  $subLicenses = $license->allResources("spdx:member");
325  if (sizeof($subLicenses) > 1 && $license->isA('spdx:DisjunctiveLicenseSet')) {
326  $output[] = new ReportImportDataItem("Dual-license");
327  }
328  foreach ($subLicenses as $subLicense) {
329  $innerOutput = $this->parseLicense($subLicense);
330  $output = array_merge($output, $innerOutput);
331  }
332  return $output;
333  }
334 
335  private function handleOrLaterOperator($license)
336  {
337  $output = [];
338  $subLicenses = $license->allResources("spdx:member");
339  foreach ($subLicenses as $subLicense) {
341  $innerOutput = $this->parseLicense($subLicense);
342  foreach ($innerOutput as $innerItem) {
344  $item = new ReportImportDataItem($innerItem->getLicenseId() . "-or-later");
345 
346  $innerLicenseCandidate = $innerItem->getLicenseCandidate();
347  $item->setLicenseCandidate($innerLicenseCandidate->getFullName() . " or later",
348  $innerLicenseCandidate->getText(), false,
349  $innerLicenseCandidate->getUrl()
350  );
351  $output[] = $item;
352  }
353  }
354  return $output;
355  }
356 
361  public function getConcludedLicenseInfoForFile($propertyId)
362  {
363  return $this->getLicenseInfoForFile($propertyId, 'licenseConcluded');
364  }
365 
370  private function getCopyrightTextsForFile($fileId): array
371  {
372  $fileNode = $this->graph->resource($fileId, self::SPDX_FILE);
373  /* @var $licenses Literal[] */
374  $copyrights = $fileNode->allLiterals("spdx:copyrightText");
375  if (count($copyrights) == 1 && $copyrights[0] instanceof Literal) {
376  # There should be only 1 copyright element containing 1 copyright per line
377  $copyrights = explode("\n", trim($copyrights[0]->getValue()));
378  return array_map('trim', $copyrights);
379  }
380  # There is only 1 copyright literal or noAssertion resource
381  return [];
382  }
383 }
static stringEndsWith($haystack, $needle)
static stringStartsWith($haystack, $needle)
char * trim(char *ptext)
Trimming whitespace.
Definition: fossconfig.c:690