1<?php
2/***********************************************************
3 Copyright (C) 2008-2014 Hewlett-Packard Development Company, L.P.
4 Copyright (C) 2015-2018, Siemens AG
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 version 2 as published by the Free Software Foundation.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 ***********************************************************/
19
20use Fossology\Lib\BusinessRules\LicenseMap;
21use Fossology\Lib\Db\DbManager;
22use Symfony\Component\HttpFoundation\Response;
23use Fossology\Lib\BusinessRules\ObligationMap;
24use Fossology\Lib\Util\StringOperation;
25
26define("TITLE_ADMIN_LICENSE_FILE", _("License Administration"));
27
28class admin_license_file extends FO_Plugin
29{
30  /** @var DbManager */
31  private $dbManager;
32
33  function __construct()
34  {
35    $this->Name       = "admin_license";
36    $this->Title      = TITLE_ADMIN_LICENSE_FILE;
37    $this->MenuList   = "Admin::License Admin";
38    $this->DBaccess   = PLUGIN_DB_ADMIN;
39    $this->vars       = array();
40    $this->obligationSelectorName = "assocObligations"; ///< Selector name for obligation list
41    $this->obligationSelectorId = "assocObligations";   ///< Selector id for obligation list
42    parent::__construct();
43
44    $this->dbManager = $GLOBALS['container']->get('db.manager');
45  }
46
47  /**
48   * \brief Customize submenus.
49   */
50  function RegisterMenus()
51  {
52    if ($this->State != PLUGIN_STATE_READY) {
53      return(0);
54    }
55
56    $URL = $this->Name."&add=y";
57    $text = _("Add new license");
58    menu_insert("Main::".$this->MenuList."::Add License",0, $URL, $text);
59    $URL = $this->Name;
60    $text = _("Select license family");
61    menu_insert("Main::".$this->MenuList."::Select License",0, $URL, $text);
62  }
63
64  public function Output()
65  {
66    $V = "";
67    $errorstr = "License not added";
68
69    // update the db
70    if (@$_POST["updateit"]) {
71      $resultstr = $this->Updatedb($_POST);
72      $this->vars['message'] = $resultstr;
73      if (strstr($resultstr, $errorstr)) {
74        return $this->Updatefm(0);
75      } else {
76        return $this->Inputfm();
77      }
78    }
79
80    if (@$_REQUEST['add'] == 'y') {
81      return $this->Updatefm(0);
82    }
83
84    // Add new rec to db
85    if (@$_POST["addit"]) {
86      $resultstr = $this->Adddb();
87      $this->vars['message'] = $resultstr;
88      if (strstr($resultstr, $errorstr)) {
89        return $this->Updatefm(0);
90      } else {
91        return $this->Inputfm();
92      }
93    }
94
95    // bring up the update form
96    $rf_pk = @$_REQUEST['rf_pk'];
97    if ($rf_pk) {
98      return $this->Updatefm($rf_pk);
99    }
100
101    // return a license text
102    if (@$_GET["getLicenseText"] && @$_GET["licenseID"]) {
103      $licenseText = $this->getLicenseTextForID(@$_GET["licenseID"]);
104      if (! $licenseText) {
105        return new Response("Error in querying license text.",
106          Response::HTTP_BAD_REQUEST, array(
107            'Content-type' => 'text/plain'
108          ));
109      }
110      return new Response($licenseText, Response::HTTP_OK,
111          array(
112            'Content-type' => 'text/plain'
113          ));
114    }
115
116    if (@$_POST["req_shortname"]) {
117      $this->vars += $this->getLicenseListData($_POST["req_shortname"], $_POST["req_marydone"]);
118    }
119    $this->vars['Inputfm'] = $this->Inputfm();
120    return $this->render('admin_license_file.html.twig');
121  }
122
123  /**
124   * \brief Build the input form
125   *
126   * \return The input form as a string
127   */
128  function Inputfm()
129  {
130    $V = "<FORM name='Inputfm' action='?mod=" . $this->Name . "' method='POST'>";
131    $V.= _("What license family do you wish to view:<br>");
132
133    // qualify by marydone, short name and long name
134    // all are optional
135    $V.= "<p>";
136    $V.= _("Filter: ");
137    $V.= "<select name='req_marydone'>\n";
138    $Selected =  (@$_REQUEST['req_marydone'] == 'all') ? " SELECTED ": "";
139    $text = _("All");
140    $V.= "<option value='all' $Selected> $text </option>";
141    $Selected =  (@$_REQUEST['req_marydone'] == 'done') ? " SELECTED ": "";
142    $text = _("Checked");
143    $V.= "<option value='done' $Selected> $text </option>";
144    $Selected =  (@$_REQUEST['req_marydone'] == 'notdone') ? " SELECTED ": "";
145    $text = _("Not Checked");
146    $V.= "<option value='notdone' $Selected> $text </option>";
147    $V.= "</select>";
148    $V.= "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
149
150    // by short name -ajax-> fullname
151    $V.= _("License family name: ");
152    $Shortnamearray = $this->FamilyNames();
153    $Shortnamearray = array("All"=>"All") + $Shortnamearray;
154    $Selected = @$_REQUEST['req_shortname'];
155    $Pulldown = Array2SingleSelect($Shortnamearray, "req_shortname", $Selected);
156    $V.= $Pulldown;
157    $V.= "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
158    $text = _("Find");
159    $V.= "<INPUT type='submit' value='$text'>\n";
160    $V .= "</FORM>\n";
161    $V.= "<hr>";
162
163    return $V;
164  }
165
166
167  private function getLicenseData($where)
168  {
169    $sql = "SELECT rf_pk, marydone, rf_shortname, rf_spdx_compatible, " .
170      "rf_shortname, rf_fullname, rf_url, rf_text, ".
171      "string_agg(ob_topic, ';') AS ob_topic " .
172      "FROM ONLY license_ref " .
173      "LEFT OUTER JOIN obligation_map ON rf_fk = rf_pk " .
174      "LEFT OUTER JOIN obligation_ref ON ob_fk = ob_pk " .
175      "$where GROUP BY rf_pk ORDER BY rf_shortname";
176
177    return $this->dbManager->getRows($sql);
178  }
179
180  /**
181   * \brief Build the input form
182   *
183   * \param $namestr - license family name
184   * \param $filter - marydone value requested
185   *
186   * \return The input form as a string
187   */
188  function getLicenseListData($namestr, $filter)
189  {
190    // look at all
191    if ($namestr == "All") {
192      $where = "";
193    } else {
194      $where = "where rf_shortname like '" . pg_escape_string($namestr) . "%' ";
195    }
196
197    // $filter is one of these: "all", "done", "notdone"
198    if ($filter != "all") {
199      if (empty($where)) {
200        $where .= "where ";
201      } else {
202        $where .= " and ";
203      }
204      if ($filter == "done") {
205        $where .= " marydone=true";
206      }
207      if ($filter == "notdone") {
208        $where .= " marydone=false";
209      }
210    }
211
212    $data = $this->getLicenseData($where);
213    if (! $data) {
214      $dataMessage = _(
215        "No licenses matching the filter and name pattern were found");
216    } else {
217      $dataSize = sizeof($data);
218      $plural = "";
219
220      if ($dataSize > 1) {
221        $plural = "s";
222      }
223      $dataMessage = $dataSize . _(" License$plural found");
224    }
225
226    return array(
227      'data' => $data,
228      'dataMessage' => $dataMessage,
229      'message' => "",
230      'tracebackURI' => Traceback_uri());
231  }
232
233  function Updatefm($rf_pk)
234  {
235    $this->vars += $this->getUpdatefmData($rf_pk);
236    return $this->render('admin_license-upload_form.html.twig', $this->vars);
237  }
238
239  /**
240   * @brief Update forms
241   * @param int $rf_pk - for the license to update, empty to add
242   * @return string The input form
243   */
244  function getUpdatefmData($rf_pk)
245  {
246    $vars = array();
247
248    $rf_pk_update = "";
249
250    if (0 < count($_POST)) {
251      $rf_pk_update = $_POST['rf_pk'];
252      if (! empty($rf_pk)) {
253        $rf_pk_update = $rf_pk;
254      } else if (empty($rf_pk_update)) {
255        $rf_pk_update = $_GET['rf_pk'];
256      }
257    }
258
259    $vars['obligationSelectorName'] = $this->obligationSelectorName . "[]";
260    $vars['obligationSelectorId'] = $this->obligationSelectorId;
261
262    $vars['actionUri'] = "?mod=" . $this->Name . "&rf_pk=$rf_pk_update";
263    $vars['req_marydone'] = array_key_exists('req_marydone', $_POST) ? $_POST['req_marydone'] : '';
264    $vars['req_shortname'] = array_key_exists('req_shortname', $_POST) ? $_POST['req_shortname'] : '';
265    $vars['req_marydone'] = array_key_exists('req_marydone', $_POST) ? $_POST['req_marydone'] : '';
266    $vars['rf_shortname'] = array_key_exists('rf_shortname', $_POST) ? $_POST['rf_shortname'] : '';
267    $vars['rf_fullname'] = array_key_exists('rf_fullname', $_POST) ? $_POST['rf_fullname'] : '';
268    $vars['rf_text'] = array_key_exists('rf_text', $_POST) ? $_POST['rf_text'] : '';
269    $selectedObligations = array_key_exists($this->obligationSelectorName,
270      $_POST) ? $_POST[$this->obligationSelectorName] : [];
271
272    $parentMap = new LicenseMap($this->dbManager, 0, LicenseMap::CONCLUSION);
273    $parentLicenes = $parentMap->getTopLevelLicenseRefs();
274    $vars['parentMap'] = array(0=>'[self]');
275    foreach ($parentLicenes as $licRef) {
276      $vars['parentMap'][$licRef->getId()] = $licRef->getShortName();
277    }
278
279    $reportMap = new LicenseMap($this->dbManager, 0, LicenseMap::REPORT);
280    $reportLicenes = $reportMap->getTopLevelLicenseRefs();
281    $vars['reportMap'] = array(0=>'[self]');
282    foreach ($reportLicenes as $licRef) {
283      $vars['reportMap'][$licRef->getId()] = $licRef->getShortName();
284    }
285
286    $obligationMap = new ObligationMap($this->dbManager);
287    $obligations = $obligationMap->getObligations();
288    foreach ($obligations as $obligation) {
289      $vars['obligationTopics'][$obligation['ob_pk']] = $obligation['ob_topic'];
290    }
291    foreach ($selectedObligations as $obligation) {
292      $row['obligationSelected'][$obligation] = $obligationMap->getTopicNameFromId(
293        $obligation);
294    }
295
296    if ($rf_pk > 0) { // true if this is an update
297      $row = $this->dbManager->getSingleRow(
298        "SELECT * FROM ONLY license_ref WHERE rf_pk=$1", array($rf_pk),
299        __METHOD__ . '.forUpdate');
300      if ($row === false) {
301        $text = _("ERROR: No licenses matching this key");
302        $text1 = _("was found");
303        return ["error" => "$text ($rf_pk) $text1."];
304      }
305      $row['rf_parent'] = $parentMap->getProjectedId($rf_pk);
306      $row['rf_report'] = $reportMap->getProjectedId($rf_pk);
307
308      $obligationsAssigned = $parentMap->getObligationsForLicenseRef($rf_pk);
309      foreach ($obligationsAssigned as $obligation) {
310        $row['obligationSelected'][$obligation] = $obligationMap->getTopicNameFromId(
311          $obligation);
312      }
313    } else {
314      $row = array(
315        'rf_active' => 't',
316        'marydone' => 'f',
317        'rf_text_updatable' => 't',
318        'rf_parent' => 0,
319        'rf_report' => 0,
320        'rf_risk' => 0,
321        'rf_spdx_compatible' => 'f',
322        'rf_url' => '',
323        'rf_detector_type' => 1,
324        'rf_notes' => ''
325      );
326    }
327
328    foreach (array_keys($row) as $key) {
329      if (array_key_exists($key, $_POST)) {
330        $row[$key] = $_POST[$key];
331      }
332    }
333
334    $vars['boolYesNoMap'] = array("true"=>"Yes", "false"=>"No");
335    $row['rf_active'] = $this->isTrue($row['rf_active']) ? 'true' : 'false';
336    $row['marydone'] = $this->isTrue($row['marydone']) ? 'true' : 'false';
337    $row['rf_text_updatable'] = $this->isTrue($row['rf_text_updatable']) ? 'true' : 'false';
338    $row['rf_spdx_compatible'] = $this->isTrue($row['rf_spdx_compatible']) ? 'true' : 'false';
339    $vars['risk_level'] = array_key_exists('risk_level', $_POST) ? intval($_POST['risk_level']) : $row['rf_risk'];
340    $vars['isReadOnly'] = !(empty($rf_pk) || $row['rf_text_updatable']=='true');
341    $vars['detectorTypes'] = array("1"=>"Reference License", "2"=>"Nomos", "3"=>"Unconcrete License");
342
343    $vars['rfId'] = $rf_pk?:$rf_pk_update;
344
345    return array_merge($vars,$row);
346  }
347
348  /** @brief Check if a variable is true
349   * @param mixed $value
350   * @return boolean
351   */
352  private function isTrue($value)
353  {
354    if (is_bool($value)) {
355      return $value;
356    } else {
357      $value = strtolower($value);
358      return ($value === 't' || $value === 'true');
359    }
360  }
361
362  /** @brief check if shortname or license text of this license is existing */
363  private function isShortnameBlocked($rfId, $shortname, $text)
364  {
365    $sql = "SELECT count(*) from license_ref where rf_pk <> $1 and (LOWER(rf_shortname) = LOWER($2) or (rf_text <> ''
366      and rf_text = $3 and LOWER(rf_text) NOT LIKE 'license by nomos.'))";
367    $check_count = $this->dbManager->getSingleRow($sql,
368      array(
369        $rfId,
370        $shortname,
371        $text
372      ), __METHOD__ . '.countLicensesByNomos');
373    return (0 < $check_count['count']);
374  }
375
376  /** @brief check if shortname is changed */
377  private function isShortNameExists($rfId, $shortname)
378  {
379    $sql = "SELECT LOWER(rf_shortname) AS rf_shortname FROM license_ref WHERE rf_pk=$1";
380    $row = $this->dbManager->getSingleRow($sql,array($rfId),__METHOD__.'.DoNotChnageShortName');
381    if ($row['rf_shortname'] === strtolower($shortname)) {
382      return 1;
383    } else {
384      return 0;
385    }
386  }
387
388  /**
389   * \brief Update the database
390   *
391   * \return An update status string
392   */
393  function Updatedb()
394  {
395    $rfId = intval($_POST['rf_pk']);
396    $shortname = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_shortname']));
397    $fullname = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_fullname']));
398    $url = $_POST['rf_url'];
399    $notes = $_POST['rf_notes'];
400    $text = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_text']));
401    $parent = $_POST['rf_parent'];
402    $report = $_POST['rf_report'];
403    $riskLvl = intval($_POST['risk_level']);
404    $selectedObligations = array_key_exists($this->obligationSelectorName,
405      $_POST) ? $_POST[$this->obligationSelectorName] : [];
406
407    if (empty($shortname)) {
408      $text = _("ERROR: The license shortname is empty. License not added.");
409      return "<b>$text</b><p>";
410    }
411
412    if (!$this->isShortNameExists($rfId,$shortname)) {
413      $text = _("ERROR: The shortname can not be changed. License not added.");
414      return "<b>$text</b><p>";
415    }
416
417    $md5term = (empty($text) || stristr($text, "License by Nomos")) ? 'null' : 'md5($10)';
418
419    $sql = "UPDATE license_ref SET
420        rf_active=$2, marydone=$3,  rf_shortname=$4, rf_fullname=$5,
421        rf_url=$6,  rf_notes=$7,  rf_text_updatable=$8,   rf_detector_type=$9,  rf_text=$10,
422        rf_md5=$md5term, rf_risk=$11, rf_spdx_compatible=$12, rf_flag=$13
423          WHERE rf_pk=$1";
424    $params = array($rfId,
425      $_POST['rf_active'],$_POST['marydone'],$shortname,$fullname,
426      $url,$notes,$_POST['rf_text_updatable'],$_POST['rf_detector_type'],$text,
427      $riskLvl,$_POST['rf_spdx_compatible'],2);
428    $statement = __METHOD__ . ".updateLicense";
429    if ($md5term == "null") {
430      $statement .= ".nullMD5";
431    }
432    $this->dbManager->prepare($statement, $sql);
433    $this->dbManager->freeResult($this->dbManager->execute($statement, $params));
434
435    $licenseMapDelStatement = __METHOD__ . '.deleteFromMap';
436    $licenseMapDelSql = 'DELETE FROM license_map WHERE rf_fk=$1 AND usage=$2';
437    $this->dbManager->prepare($licenseMapDelStatement, $licenseMapDelSql);
438
439    $parentMap = new LicenseMap($this->dbManager, 0, LicenseMap::CONCLUSION);
440    if ($parent == 0) {
441      // Update conclusion to self
442      $this->dbManager->execute($licenseMapDelStatement,
443        array($rfId, LicenseMap::CONCLUSION));
444    } else {
445      $parentLicenses = $parentMap->getTopLevelLicenseRefs();
446      if (array_key_exists($parent, $parentLicenses) &&
447        $parent != $parentMap->getProjectedId($rfId)) {
448        $this->dbManager->execute($licenseMapDelStatement,
449          array($rfId, LicenseMap::CONCLUSION));
450        $this->dbManager->insertTableRow('license_map',
451          array('rf_fk'=>$rfId, 'rf_parent'=>$parent, 'usage'=>LicenseMap::CONCLUSION));
452      }
453    }
454
455    if ($report == 0) {
456      // Update report to self
457      $this->dbManager->execute($licenseMapDelStatement,
458        array($rfId, LicenseMap::REPORT));
459    } else {
460      $reportMap = new LicenseMap($this->dbManager, 0, LicenseMap::REPORT);
461      $reportLicenses = $parentMap->getTopLevelLicenseRefs();
462      if (array_key_exists($report, $reportLicenses) &&
463        $report != $reportMap->getProjectedId($rfId)) {
464        $this->dbManager->execute($licenseMapDelStatement,
465          array($rfId, LicenseMap::REPORT));
466        $this->dbManager->insertTableRow('license_map',
467          array('rf_fk'=>$rfId, 'rf_parent'=>$report, 'usage'=>LicenseMap::REPORT));
468      }
469    }
470
471    $obligationMap = new ObligationMap($this->dbManager);
472    foreach ($selectedObligations as $obligation) {
473      $obligationMap->associateLicenseWithObligation($obligation, $rfId);
474    }
475
476    $allObligations = $parentMap->getObligationsForLicenseRef($rfId);
477    $removedObligations = array_diff($allObligations, $selectedObligations);
478    foreach ($removedObligations as $obligation) {
479      $obligationMap->unassociateLicenseFromObligation($obligation, $rfId);
480    }
481
482    $ob = "License $_POST[rf_shortname] updated.<p>";
483    return $ob;
484  }
485
486
487  /**
488   * \brief Add a new license_ref to the database
489   *
490   * \return An add status string
491   */
492  function Adddb()
493  {
494    $rf_shortname = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_shortname']));
495    $rf_fullname = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_fullname']));
496    $rf_url = $_POST['rf_url'];
497    $rf_notes = $_POST['rf_notes'];
498    $rf_text = StringOperation::replaceUnicodeControlChar(trim($_POST['rf_text']));
499    $parent = $_POST['rf_parent'];
500    $report = $_POST['rf_report'];
501    $riskLvl = intval($_POST['risk_level']);
502    $selectedObligations = array_key_exists($this->obligationSelectorName,
503      $_POST) ? $_POST[$this->obligationSelectorName] : [];
504
505    if (empty($rf_shortname)) {
506      $text = _("ERROR: The license shortname is empty. License not added.");
507      return "<b>$text</b><p>";
508    }
509
510    if ($this->isShortnameBlocked(0,$rf_shortname,$rf_text)) {
511      $text = _("ERROR: The shortname or license text already exist in the license list. License not added.");
512      return "<b>$text</b><p>";
513    }
514
515    $md5term = (empty($rf_text) || stristr($rf_text, "License by Nomos")) ? 'null' : 'md5($7)';
516    $stmt = __METHOD__.'.rf';
517    $sql = "INSERT into license_ref (
518        rf_active, marydone, rf_shortname, rf_fullname,
519        rf_url, rf_notes, rf_md5, rf_text, rf_text_updatable,
520        rf_detector_type, rf_risk, rf_spdx_compatible)
521          VALUES (
522              $1, $2, $3, $4, $5, $6, $md5term, $7, $8, $9, $10, $11) RETURNING rf_pk";
523    $this->dbManager->prepare($stmt,$sql);
524    $res = $this->dbManager->execute($stmt,
525      array(
526        $_POST['rf_active'],
527        $_POST['marydone'],
528        $rf_shortname,
529        $rf_fullname,
530        $rf_url,
531        $rf_notes,
532        $rf_text,
533        $_POST['rf_text_updatable'],
534        $_POST['rf_detector_type'],
535        $riskLvl,
536        $_POST['rf_spdx_compatible']
537      ));
538    $row = $this->dbManager->fetchArray($res);
539    $rfId = $row['rf_pk'];
540
541    $parentMap = new LicenseMap($this->dbManager, 0, LicenseMap::CONCLUSION);
542    $parentLicenses = $parentMap->getTopLevelLicenseRefs();
543    if (array_key_exists($parent, $parentLicenses)) {
544      $this->dbManager->insertTableRow('license_map',
545        array(
546          'rf_fk' => $rfId,
547          'rf_parent' => $parent,
548          'usage' => LicenseMap::CONCLUSION
549        ));
550    }
551
552    $reportMap = new LicenseMap($this->dbManager, 0, LicenseMap::REPORT);
553    $reportLicenses = $reportMap->getTopLevelLicenseRefs();
554    if (array_key_exists($report, $reportLicenses)) {
555      $this->dbManager->insertTableRow('license_map',
556        array(
557          'rf_fk' => $rfId,
558          'rf_parent' => $report,
559          'usage' => LicenseMap::REPORT
560        ));
561    }
562
563    $obligationMap = new ObligationMap($this->dbManager);
564    foreach ($selectedObligations as $obligation) {
565      $obligationMap->associateLicenseWithObligation($obligation, $rfId);
566    }
567
568    return "License $_POST[rf_shortname] (id=$rfId) added.<p>";
569  }
570
571
572  /**
573   * \brief get an array of family names based on the
574   *
575   * \return an array of family names based on the
576   * license_ref.shortname.
577   * A family name is the name before most punctuation.
578   *
579   * \example the family name of "GPL V2" is "GPL"
580   */
581  function FamilyNames()
582  {
583    $familynamearray = array();
584    $Shortnamearray = DB2KeyValArray("license_ref", "rf_pk", "rf_shortname", " order by rf_shortname");
585
586    // truncate each name to the family name
587    foreach ($Shortnamearray as $shortname) {
588      // start with exceptions
589      if (($shortname == "No_license_found") || ($shortname == "Unknown license")) {
590        $familynamearray[$shortname] = $shortname;
591      } else {
592        $tok = strtok($shortname, " _-([/");
593        $familynamearray[$tok] = $tok;
594      }
595    }
596
597    return ($familynamearray);
598  }
599
600  private function getLicenseTextForID($licenseID)
601  {
602    $sql = "select rf_text from license_ref where rf_pk=$1";
603    $result = $this->dbManager->getSingleRow($sql, array($licenseID));
604
605    if (! $result) {
606      return false;
607    }
608    return $result['rf_text'];
609  }
610}
611
612$NewPlugin = new admin_license_file();
613