FOSSology  4.7.1
Open Source License Compliance by Open Source Software
exportLicenseRefUsingSPDX.php
Go to the documentation of this file.
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2019,2021,2022 Siemens AG
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 
9 
18 {
23  private $mapArrayData = array(
24  'licenses' => array('licenseId', 'licenseText', 'name'),
25  'exceptions' => array('licenseExceptionId', 'licenseExceptionText', 'name')
26  );
27 
31  const BOA_LIST_URL = 'https://blueoakcouncil.org/list.json';
32 
33 
34  function startProcessingLicenseData()
35  {
36  global $argv;
37  $updateWithNew = '';
38  $updateExisting = '';
39  $addNewLicense = '';
40  $deleteDeprecated = false;
41  $newLicenseRefData = array();
42  $showUsage = '';
43  $scanList = array(
44  'licenses' => 'https://spdx.org/licenses/licenses.json',
45  'exceptions' => 'https://spdx.org/licenses/exceptions.json'
46  );
47  $usage = "Usage: " . basename($argv[0]) . " [options]
48 
49  Create new licenseref.json file. Options are:
50  -E Update all existing licenses and also add new licenses.
51  (NOTE: there may be failure of test cases)
52 
53  -e Only update existing licenses.
54  (NOTE: there may be failure of test cases)
55 
56  -n Only add new licenses.
57 
58  -d Delete deprecated licenses.
59 
60  --type Usually licenses/exceptions (optional)
61  (ex: --type 'licenses')
62 
63  --url From where you want to download (optional)
64  (ex: --url 'https://spdx.org/licenses/licenses.json')
65 
66  Additional note:
67  (if --type and --url is empty then the script will automatically download the from below)
68  For type 'licenses' URL is : $scanList[licenses]
69 
70  For type 'exceptions' URL is : $scanList[exceptions]";
71 
72  $options = getopt("hcEend", array("type:", "url:"));
73  /* get type and url if exists, if not set them to empty */
74  $type = array_key_exists("type", $options) ? $options["type"] : '';
75  $URL = array_key_exists("url", $options) ? $options["url"] : '';
76 
77  foreach ($options as $option => $optVal) {
78  switch ($option) {
79  case 'c': /* used by fo_wrapper */
80  break;
81  case 'E': /* Update all existing licenses and also add new licenses */
82  $updateWithNew = $option;
83  break;
84  case 'e': /* Only update existing licenses */
85  $updateExisting = $option;
86  break;
87  case 'n': /* only add new licenses */
88  $addNewLicense = $option;
89  break;
90  case 'd': /* Delete deprecated licenses */
91  $deleteDeprecated = true;
92  break;
93  case 'h': /* help */
94  $showUsage = true;
95  break;
96  }
97  }
98 
99  if ($showUsage) {
100  print "$usage\n";
101  exit;
102  }
103 
104  if (!empty($updateWithNew) || !empty($updateExisting) || !empty($addNewLicense)) {
105  if (!empty($type) && !empty($URL)) {
106  $newLicenseRefData = $this->getListSPDX($type, $URL, $updateWithNew, $updateExisting, $addNewLicense,
107  $newLicenseRefData, $deleteDeprecated);
108  } else if (!empty($type) && empty($URL)) {
109  echo "Notice: --url cannot be empty if --type is provided \n";
110  } else if (empty($type) && !empty($URL)) {
111  echo "Notice: --type cannot be empty if --url is provided \n";
112  } else {
113  foreach ($scanList as $type => $URL) {
114  $newLicenseRefData = $this->getListSPDX($type, $URL, $updateWithNew, $updateExisting, $addNewLicense,
115  $newLicenseRefData, $deleteDeprecated);
116  }
117  }
118  if (empty($newLicenseRefData)) {
119  echo "\nERROR: No license data collected. Verify \$LIBEXECDIR ";
120  exit(1);
121  }
122  $newFileName = "licenseRefNew.json";
123  if (file_exists($newFileName)) {
124  unlink($newFileName);
125  }
126  $this->sanitizeRefData($newLicenseRefData);
127  file_put_contents($newFileName, json_encode($newLicenseRefData, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES));
128  echo "\n\n INFO: new $newFileName file created \n\n";
129  } else {
130  echo "\nINVALID OPTION PROVIDED\n\n";
131  print "$usage\n";
132  exit;
133  }
134  }
135 
146  function getListSPDX($type, $URL, $updateWithNew, $updateExisting, $addNewLicense, $existingLicenseRefData,
147  $deleteDeprecated)
148  {
149  global $LIBEXECDIR;
150 
151  if (!is_dir($LIBEXECDIR)) {
152  print "FATAL: Directory '$LIBEXECDIR' does not exist.\n";
153  return [];
154  }
155 
156  if (!is_readable($LIBEXECDIR)) {
157  print "FATAL: Unable to access '$LIBEXECDIR'.\n";
158  return [];
159  }
160  /* check if licenseref.json exists */
161  $fileName = "$LIBEXECDIR/licenseRef.json";
162  if (!file_exists($fileName)) {
163  print "FATAL: File '$fileName' does not exist.\n";
164  return [];
165  }
166 
167  if (empty($existingLicenseRefData)) {
168  echo "INFO: get existing licenseRef.json from $LIBEXECDIR\n";
169  $getExistingLicenseRefData = file_get_contents("$fileName");
170  /* dump all the data from licenseRef.json file to an array */
171  $existingLicenseRefData = (array) json_decode($getExistingLicenseRefData, true);
172  }
173  $boaIds = [];
174  if ($type === 'licenses') {
175  $boaRaw = fetchBoaList(BOA_LIST_URL);
176  if ($boaRaw !== false) {
177  $boaJson = json_decode($boaRaw, true);
178  foreach (($boaJson['ratings'] ?? []) as $rating) {
179  foreach (($rating['licenses'] ?? []) as $lic) {
180  if (!empty($lic['id'])) {
181  $boaIds[$lic['id']] = true;
182  }
183  }
184  }
185  }
186  $boaCount = count($boaIds);
187  if ($boaCount === 0) {
188  echo "WARNING: Blue Oak Council fetch failed or returned empty — Permissive classification degraded.\n";
189  } else {
190  echo "INFO: loaded $boaCount permissive licenses from Blue Oak Council\n";
191  }
192  }
193 
194  /* get license list and each license's URL */
195  $rawList = file_get_contents($URL, false, $httpCtx);
196  if ($rawList === false) {
197  print "FATAL: Unable to fetch data from '$URL'.\n";
198  return [];
199  }
200  $getList = json_decode($rawList);
201  if ($getList === null || !isset($getList->$type)) {
202  print "FATAL: Invalid data received from '$URL'.\n";
203  return [];
204  }
205  foreach ($getList->$type as $listValue) {
206  /* get current license data from given URL */
207  if (strstr($URL, "spdx.org") !== false) {
208  // If fetching exceptions from spdx, fix the detailsUrl
209  if (substr_compare($listValue->detailsUrl, ".html", -5) === 0) {
210  if (!isset($listValue->reference)) {
211  echo "WARNING: Missing 'reference' field for '" . ($listValue->licenseExceptionId ?? '?') . "', skipping.\n";
212  continue;
213  }
214  $baseUrl = str_replace("exceptions.json", "", $URL);
215  $listValue->detailsUrl = $baseUrl . str_replace("./", "", $listValue->reference);
216  }
217  }
218  $rawCurrentData = file_get_contents($listValue->detailsUrl, false, $httpCtx);
219  if ($rawCurrentData === false) {
220  echo "WARNING: Unable to fetch license details from '" . $listValue->detailsUrl . "', skipping.\n";
221  continue;
222  }
223  $getCurrentData = json_decode($rawCurrentData, true);
224  if (!is_array($getCurrentData) || empty($getCurrentData[$this->mapArrayData[$type][0]])) {
225  echo "WARNING: Invalid or missing data from '" . $listValue->detailsUrl . "', skipping.\n";
226  continue;
227  }
228  $getCurrentData = (array) $getCurrentData;
229  echo "INFO: search for license " . $getCurrentData[$this->mapArrayData[$type][0]] . "\n";
230  /* check if the licenseid of the current license exists in old license data */
231  $licenseIdCheck = array_search($getCurrentData[$this->mapArrayData[$type][0]],
232  array_column($existingLicenseRefData, 'rf_shortname'));
233  $currentText = $this->replaceUnicode($getCurrentData[$this->mapArrayData[$type][1]]);
234  $textCheck = array_search($currentText, array_column($existingLicenseRefData, 'rf_text'));
235  if ($deleteDeprecated && $listValue->isDeprecatedLicenseId && (
236  is_numeric($licenseIdCheck) &&
237  (!empty($updateWithNew) || !empty($updateExisting)))) {
238  // Existing deprecated license, delete it
239  echo "INFO: removing deprecated license " .
240  $getCurrentData[$this->mapArrayData[$type][0]] . "\n";
241  unset($existingLicenseRefData[$licenseIdCheck]);
242  $existingLicenseRefData = array_values($existingLicenseRefData);
243  continue;
244  } elseif ($listValue->isDeprecatedLicenseId) {
245  continue;
246  }
247  if (is_numeric($licenseIdCheck) &&
248  (!empty($updateWithNew) || !empty($updateExisting))) {
249  // License exists, just remove old fields
250  $existingLicenseRefData[$licenseIdCheck]['rf_spdx_compatible'] =
251  $listValue->isDeprecatedLicenseId ? "f" : "t";
252  $existingLicenseRefData[$licenseIdCheck]['rf_licensetype'] =
253  $this->getLicenseType($type, $getCurrentData, $boaIds);
254  }
255  if (
256  is_numeric($licenseIdCheck) &&
257  !is_numeric($textCheck) &&
258  (!empty($updateWithNew) ||
259  !empty($updateExisting)
260  )
261  ) {
262  $existingLicenseRefData[$licenseIdCheck]['rf_fullname'] = $getCurrentData[$this->mapArrayData[$type][2]];
263  $existingLicenseRefData[$licenseIdCheck]['rf_text'] = $currentText;
264  $existingLicenseRefData[$licenseIdCheck]['rf_url'] = isset($getCurrentData['seeAlso'][0]) ? $getCurrentData['seeAlso'][0] : $existingLicenseRefData[$licenseIdCheck]['rf_url'];
265  $existingLicenseRefData[$licenseIdCheck]['rf_notes'] = (array_key_exists("licenseComments", $getCurrentData) ? $getCurrentData['licenseComments'] : $existingLicenseRefData[$licenseIdCheck]['rf_notes']);
266  echo "INFO: license " . $getCurrentData[$this->mapArrayData[$type][0]] . " updated\n\n";
267  }
268  if (
269  !is_numeric($licenseIdCheck) &&
270  !is_numeric($textCheck) &&
271  (!empty($updateWithNew) ||
272  !empty($addNewLicense)
273  )
274  ) {
275  $existingLicenseRefData[] = array(
276  'rf_shortname' => $getCurrentData[$this->mapArrayData[$type][0]],
277  'rf_text' => $currentText,
278  'rf_url' => isset($getCurrentData['seeAlso'][0]) ? $getCurrentData['seeAlso'][0] : null,
279  'rf_add_date' => null,
280  'rf_copyleft' => null,
281  'rf_OSIapproved' => null,
282  'rf_fullname' => $getCurrentData[$this->mapArrayData[$type][2]],
283  'rf_FSFfree' => null,
284  'rf_GPLv2compatible' => null,
285  'rf_GPLv3compatible' => null,
286  'rf_notes' => (array_key_exists("licenseComments", $getCurrentData) ? $getCurrentData['licenseComments'] : null),
287  'rf_Fedora' => null,
288  'marydone' => "f",
289  'rf_active' => "t",
290  'rf_text_updatable' => "f",
291  'rf_detector_type' => 1,
292  'rf_source' => null,
293  'rf_risk' => null,
294  'rf_spdx_compatible' => $listValue->isDeprecatedLicenseId ? "f" : "t",
295  'rf_flag' => "1",
296  'rf_licensetype' => $this->getLicenseType($type, $getCurrentData, $boaIds),
297  );
298  echo "INFO: new license " . $getCurrentData[$this->mapArrayData[$type][0]] . " added\n\n";
299  }
300  }
301  return array_values($existingLicenseRefData);
302  }
303 
309  private function getLicenseType($type, $licenseData, $boaIds)
310  {
311  if ($type === 'exceptions') {
312  return 'Exception';
313  }
314 
315  $licenseId = $licenseData[$this->mapArrayData[$type][0]] ?? '';
316  $name = strtolower($licenseData[$this->mapArrayData[$type][2]] ?? '');
317  $comments = strtolower($licenseData['licenseComments'] ?? '');
318 
319  // Check public domain before Creative Commons to catch CC0/PDM.
320  if (strpos($name, 'public domain') !== false ||
321  strpos($name, 'unlicense') !== false ||
322  strpos($comments, 'public domain') !== false) {
323  return 'Public Domain';
324  }
325 
326  // Classify Creative Commons by variant.
327  if (strpos($name, 'creative commons') !== false) {
328  if (strpos($licenseId, 'CC0') === 0 || strpos($licenseId, 'CC-PDM') === 0 ||
329  strpos($licenseId, 'CC-PDDC') === 0) {
330  return 'Public Domain';
331  }
332  if (strpos($licenseId, '-NC-') !== false ||
333  strpos($name, 'noncommercial') !== false ||
334  strpos($name, 'non-commercial') !== false) {
335  return 'Non-commercial';
336  }
337  // ND variants prohibit modification; treat as Source Available.
338  if (strpos($licenseId, '-ND') !== false ||
339  strpos($name, 'noderivatives') !== false ||
340  strpos($name, 'no derivatives') !== false) {
341  return 'Source Available';
342  }
343  if (strpos($licenseId, '-SA') !== false || strpos($name, 'sharealike') !== false) {
344  return 'Weak Copyleft';
345  }
346  return 'Permissive';
347  }
348 
349  if (strpos($licenseId, 'GFDL') === 0 ||
350  strpos($name, 'free documentation license') !== false) {
351  return 'Weak Copyleft';
352  }
353 
354  // Export/military restriction licenses are not freely usable; keep Unknown.
355  if (strpos($licenseId, 'No-Nuclear') !== false ||
356  strpos($licenseId, 'No-Military') !== false ||
357  strpos($name, 'no nuclear') !== false ||
358  strpos($name, 'no military') !== false) {
359  return 'Unknown';
360  }
361 
362  if (preg_match('/\bfont\b/i', $name)) {
363  return 'Font';
364  }
365 
366  if (strpos($name, 'database') !== false ||
367  (strpos($name, 'data') !== false && strpos($name, 'license') !== false)) {
368  return 'Data';
369  }
370 
371  if (isset($boaIds[$licenseId])) {
372  return 'Permissive';
373  }
374 
375  $isCopyleft = strpos($name, 'copyleft') !== false ||
376  strpos($comments, 'copyleft') !== false ||
377  strpos($name, 'general public license') !== false;
378  if ($isCopyleft) {
379  $isNetwork = strpos($name, 'affero') !== false ||
380  strpos($name, 'network copyleft') !== false ||
381  strpos($comments, 'affero') !== false;
382  $isWeak = strpos($name, 'lesser') !== false ||
383  strpos($name, 'limited') !== false ||
384  strpos($name, 'weak copyleft') !== false ||
385  strpos($comments, 'weak copyleft') !== false ||
386  strpos($comments, 'lesser general public') !== false;
387  if ($isNetwork) {
388  return 'Network Copyleft';
389  }
390  if ($isWeak) {
391  return 'Weak Copyleft';
392  }
393  return 'Strong Copyleft';
394  }
395 
396  if (strpos($name, 'non-commercial') !== false ||
397  strpos($name, 'noncommercial') !== false) {
398  return 'Non-commercial';
399  }
400 
401  return 'Unknown';
402  }
403 
410  private function replaceUnicode($text)
411  {
412  if ($text === null) {
413  return null;
414  }
415  $search = [
416  "\u{00a0}", // no break space
417  "\u{2018}", // Left single quote
418  "\u{2019}", // Right single quote
419  "\u{201c}", // Left double quote
420  "\u{201d}", // Right double quote
421  "\u{2013}", // em dash
422  "\u{2028}", // line separator
423  ];
424 
425  $replace = [
426  " ",
427  "'",
428  "'",
429  '"',
430  '"',
431  "-",
432  "\n",
433  ];
434 
435  return StringOperation::replaceUnicodeControlChar(str_replace($search,
436  $replace, $text));
437  }
438 
444  private function sanitizeRefData(&$newLicenseRefData)
445  {
446  for ($i = 0; $i < count($newLicenseRefData); $i++) {
447  $newLicenseRefData[$i]["rf_fullname"] = $this->replaceUnicode($newLicenseRefData[$i]["rf_fullname"]);
448  $newLicenseRefData[$i]["rf_text"] = $this->replaceUnicode($newLicenseRefData[$i]["rf_text"]);
449  $newLicenseRefData[$i]["rf_notes"] = $this->replaceUnicode($newLicenseRefData[$i]["rf_notes"]);
450  }
451  }
452 
459  private function fetchBoaList($url)
460  {
461  try {
462  $boaRaw = file_get_contents($url, false, stream_context_create([
463  'http' => [
464  'timeout' => 30,
465  'user_agent' => 'FOSSology/SPDX'
466  ]
467  ]));
468 
469  if ($boaRaw === false) {
470  throw new Exception("Failed to fetch BOA list");
471  }
472 
473  return $boaRaw;
474  } catch (Exception $e) {
475  error_log("BOA List fetch error: " . $e->getMessage());
476  return null;
477  }
478  }
479 }
480 $obj = new exportLicenseRef();
481 $obj->startProcessingLicenseData();
sanitizeRefData(&$newLicenseRefData)
getLicenseType($type, $licenseData, $boaIds)
Classify a license into rf_licensetype using name, comments, and BOA membership.
getListSPDX($type, $URL, $updateWithNew, $updateExisting, $addNewLicense, $existingLicenseRefData, $deleteDeprecated)
get SPDX license or exception list and update licenseref.json