1<?php
2/*
3Copyright (C) 2014-2017, Siemens AG
4
5This program is free software; you can redistribute it and/or
6modify it under the terms of the GNU General Public License
7version 2 as published by the Free Software Foundation.
8
9This program is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License along
15with this program; if not, write to the Free Software Foundation, Inc.,
1651 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17*/
18
19namespace Fossology\Lib\Application;
20
21use Fossology\Lib\Db\DbManager;
22use Fossology\Lib\Util\ArrayOperation;
23
24/**
25 * @file
26 * @brief Helper class for Obligation CSV Import
27 */
28
29/**
30 * @class ObligationCsvImport
31 * @brief Helper class for Obligation CSV Import
32 */
33class ObligationCsvImport
34{
35  /** @var DbManager $dbManager
36   * DB manager to be used */
37  protected $dbManager;
38  /** @var string $delimiter
39   * Delimiter used in the CSV */
40  protected $delimiter = ',';
41  /** @var string $enclosure
42   * Ecnlosure used in the CSV */
43  protected $enclosure = '"';
44  /** @var null|array $headrow
45   * Header of CSV */
46  protected $headrow = null;
47  /** @var array $alias
48   * Alias for headers */
49  protected $alias = array(
50      'type'=>array('type','Type'),
51      'topic'=>array('topic','Obligation or Risk topic'),
52      'text'=>array('text','Full Text'),
53      'classification'=>array('classification','Classification'),
54      'modifications'=>array('modifications','Apply on modified source code'),
55      'comment'=>array('comment','Comment'),
56      'licnames'=>array('licnames','Associated Licenses'),
57      'candidatenames'=>array('candidatenames','Associated candidate Licenses')
58    );
59
60  /**
61   * Constructor
62   * @param DbManager $dbManager DB manager to use
63   */
64  public function __construct(DbManager $dbManager)
65  {
66    $this->dbManager = $dbManager;
67    $this->obligationMap = $GLOBALS['container']->get('businessrules.obligationmap');
68  }
69
70  /**
71   * @brief Update the delimiter
72   * @param string $delimiter New delimiter to use.
73   */
74  public function setDelimiter($delimiter=',')
75  {
76    $this->delimiter = substr($delimiter,0,1);
77  }
78
79  /**
80   * @brief Update the enclosure
81   * @param string $enclosure New enclosure to use.
82   */
83  public function setEnclosure($enclosure='"')
84  {
85    $this->enclosure = substr($enclosure,0,1);
86  }
87
88  /**
89   * @brief Read the CSV line by line and import it.
90   * @param string $filename Location of the CSV file.
91   * @return string message Error message, if any. Otherwise
92   *         `Read csv: <count> licenses` on success.
93   */
94  public function handleFile($filename)
95  {
96    if (!is_file($filename) || ($handle = fopen($filename, 'r')) === false) {
97      return _('Internal error');
98    }
99    $cnt = -1;
100    $msg = '';
101    try {
102      while (($row = fgetcsv($handle,0,$this->delimiter,$this->enclosure)) !== false) {
103        $log = $this->handleCsv($row);
104        if (!empty($log)) {
105          $msg .= "$log\n";
106        }
107        $cnt++;
108      }
109      $msg .= _('Read csv').(": $cnt ")._('obligations');
110    } catch(\Exception $e) {
111      fclose($handle);
112      return $msg .= _('Error while parsing file').': '.$e->getMessage();
113    }
114    fclose($handle);
115    return $msg;
116  }
117
118  /**
119   * Handle a single row read from the CSV. If headrow is not set, then handle
120   * current row as head row.
121   * @param array $row   Single row from CSV
122   * @return string $log Log messages
123   */
124  private function handleCsv($row)
125  {
126    if ($this->headrow===null) {
127      $this->headrow = $this->handleHeadCsv($row);
128      return 'head okay';
129    }
130
131    $mRow = array();
132    foreach (array('type','topic','text','classification','modifications','comment','licnames','candidatenames') as $needle) {
133      $mRow[$needle] = $row[$this->headrow[$needle]];
134    }
135
136    return $this->handleCsvObligation($mRow);
137  }
138
139  /**
140   * @brief Handle a row as head row.
141   * @param array $row  Head row to be handled.
142   * @throws \Exception
143   * @return boolean[]|mixed[] Parsed head row.
144   */
145  private function handleHeadCsv($row)
146  {
147    $headrow = array();
148    foreach (array('type','topic','text','classification','modifications','comment','licnames','candidatenames') as $needle) {
149      $col = ArrayOperation::multiSearch($this->alias[$needle], $row);
150      if (false === $col) {
151        throw new \Exception("Undetermined position of $needle");
152      }
153      $headrow[$needle] = $col;
154    }
155    return $headrow;
156  }
157
158  /**
159   * @brief Get the Obligation key from obligation topic and obligation text
160   * @param array $row CSV array with `topic` and `text` keys
161   * @return boolean|int False if not found, key otherwise.
162   */
163  private function getKeyFromTopicAndText($row)
164  {
165    $req = array($row['topic'], $row['text']);
166    $row = $this->dbManager->getSingleRow('SELECT ob_pk FROM obligation_ref WHERE ob_topic=$1 AND ob_md5=md5($2)',$req);
167    return ($row === false) ? false : $row['ob_pk'];
168  }
169
170  /**
171   * @brief Compare licenses from Database and CSV
172   * @param bool $exists       Existing license id
173   * @param array $listFromCsv List of obligations from CSV
174   * @param bool $candidate    Is a candidate obligation?
175   * @param array $row         Unused
176   * @return number strcmp() diff
177   */
178  private function compareLicList($exists, $listFromCsv, $candidate, $row)
179  {
180    $getList = $this->obligationMap->getLicenseList($exists, $candidate);
181    $listFromDb = $this->reArrangeString($getList);
182    $listFromCsv = $this->reArrangeString($listFromCsv);
183    $diff = strcmp($listFromDb, $listFromCsv);
184    return $diff;
185  }
186
187  /**
188   * The function takes a string delimited by `;`, explodes it, sort the result
189   * and joins them back using `,` as new delimiter.
190   * @param string $string String to be rearranged.
191   * @return string Rearranged string.
192   */
193  private function reArrangeString($string)
194  {
195    $string = explode(";", $string);
196    sort($string);
197    $string = implode(",", $string);
198    return $string;
199  }
200
201  /**
202   * @brief Clear all license maps for given obligation
203   * @param int $exists     Existing obligation key
204   * @param bool $candidate Is a candidate obligation?
205   * @return boolean Always true
206   */
207  private function clearListFromDb($exists, $candidate)
208  {
209    $licId = 0;
210    $this->obligationMap->unassociateLicenseFromObligation($exists, $licId, $candidate);
211    return true;
212  }
213
214  /**
215   * @brief Handle a single row from CSV.
216   *
217   * The function checks if the obligation text hash is already in the DB, then
218   * update the license associations. Otherwise make a new entry in the DB.
219   * @param array $row CSV row to be inserted.
220   * @return string Log messages.
221   */
222  private function handleCsvObligation($row)
223  {
224    /* @var $dbManager DbManager */
225    $dbManager = $this->dbManager;
226    $exists = $this->getKeyFromTopicAndText($row);
227    $associatedLicenses = "";
228    $candidateLicenses = "";
229    $msg = "";
230    if ($exists !== false) {
231      $msg = "Obligation topic '$row[topic]' already exists in DB (id=".$exists."),";
232      if ( $this->compareLicList($exists, $row['licnames'], false, $row) === 0 ) {
233        $msg .=" No Changes in AssociateLicense";
234      } else {
235        $this->clearListFromDb($exists, false);
236        if (!empty($row['licnames'])) {
237          $associatedLicenses .= $this->AssociateWithLicenses($row['licnames'], $exists, false);
238        }
239        $msg .=" Updated AssociatedLicense license";
240      }
241      if ($this->compareLicList($exists, $row['candidatenames'], true, $row) === 0) {
242        $msg .=" No Changes in CandidateLicense";
243      } else {
244        $this->clearListFromDb($exists, true);
245        if (!empty($row['candidatenames'])) {
246          $associatedLicenses .= $this->AssociateWithLicenses($row['candidatenames'], $exists, true);
247        }
248        $msg .=" Updated CandidateLicense";
249      }
250      $this->updateOtherFields($exists, $row);
251      return $msg . "\n" . $associatedLicenses . "\n";
252    }
253
254    $stmtInsert = __METHOD__.'.insert';
255    $dbManager->prepare($stmtInsert,'INSERT INTO obligation_ref (ob_type,ob_topic,ob_text,ob_classification,ob_modifications,ob_comment,ob_md5)'
256            . ' VALUES ($1,$2,$3,$4,$5,$6,md5($3)) RETURNING ob_pk');
257    $resi = $dbManager->execute($stmtInsert,array($row['type'],$row['topic'],$row['text'],$row['classification'],$row['modifications'],$row['comment']));
258    $new = $dbManager->fetchArray($resi);
259    $dbManager->freeResult($resi);
260
261    if (!empty($row['licnames'])) {
262      $associatedLicenses .= $this->AssociateWithLicenses($row['licnames'], $new['ob_pk']);
263    }
264    if (!empty($row['candidatenames'])) {
265      $candidateLicenses = $this->AssociateWithLicenses($row['candidatenames'], $new['ob_pk'], true);
266    }
267
268    $message = "License association results for obligation '$row[topic]':\n";
269    $message .= "$associatedLicenses";
270    $message .= "$candidateLicenses";
271    $message .= "Obligation with id=$new[ob_pk] was added successfully.\n";
272    return $message;
273  }
274
275  /**
276   * @brief Associate selected licenses to the obligation
277   *
278   * @param array   $licList List of licenses to be associated
279   * @param int     $obPk The id of the newly created obligation
280   * @param boolean $candidate Do we handle candidate licenses?
281   * @return string The list of associated licences
282   */
283  function AssociateWithLicenses($licList, $obPk, $candidate=False)
284  {
285    $associatedLicenses = "";
286    $message = "";
287
288    $licenses = explode(";",$licList);
289    foreach ($licenses as $license) {
290      $licIds = $this->obligationMap->getIdFromShortname($license, $candidate);
291      $updated = false;
292      if (empty($licIds)) {
293        $message .= "License $license could not be found in the DB.\n";
294      } else {
295        $updated = $this->obligationMap->associateLicenseFromLicenseList($obPk,
296          $licIds, $candidate);
297      }
298      if ($updated) {
299        if ($associatedLicenses == "") {
300          $associatedLicenses = "$license";
301        } else {
302          $associatedLicenses .= ";$license";
303        }
304      }
305    }
306
307    if (!empty($associatedLicenses)) {
308      $message .= "$associatedLicenses were associated.\n";
309    } else {
310      $message .= "No ";
311      $message .= $candidate ? "candidate": "";
312      $message .= "licenses were associated.\n";
313    }
314    return $message;
315  }
316
317  /**
318   * @brief Update other fields of the obligation.
319   *
320   * Fields updated are:
321   * - classification
322   * - modifications
323   * - comment
324   * @param int $exists Obligation key
325   * @param array $row  Row from CSV.
326   */
327  function updateOtherFields($exists, $row)
328  {
329    $this->dbManager->getSingleRow('UPDATE obligation_ref SET ob_classification=$2, ob_modifications=$3, ob_comment=$4 where ob_pk=$1',
330      array($exists, $row['classification'], $row['modifications'], $row['comment']),
331      __METHOD__ . '.updateOtherOb');
332  }
333}
334