FOSSology  4.7.1
Open Source License Compliance by Open Source Software
CustomTextImport.php
Go to the documentation of this file.
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2025 Harshit Gandhi <gandhiharshit716@gmail.com>
4  SPDX-FileCopyrightText: © Fossology contributors
5 
6  SPDX-License-Identifier: GPL-2.0-only
7 */
8 
11 
16 use Exception;
17 
28 {
31  protected $dbManager;
34  protected $userDao;
36  protected $licenseDao;
39  protected $delimiter = ',';
42  protected $enclosure = '"';
45  protected $headrow = null;
48  protected $alias = array(
49  'text'=>array('text','Text'),
50  'acknowledgement'=>array('acknowledgement','Acknowledgement','acknowledgements'),
51  'comments'=>array('comments','Comments'),
52  'is_active'=>array('is_active','Is Active','active'),
53  'created_by'=>array('created_by','Created By','user_name'),
54  'group'=>array('group','Group','group_name'),
55  'licenses_to_add'=>array('licenses_to_add','Licenses To Add','add_licenses'),
56  'licenses_to_remove'=>array('licenses_to_remove','Licenses To Remove','remove_licenses')
57  );
58 
64  public function __construct(DbManager $dbManager, UserDao $userDao, LicenseDao $licenseDao = null)
65  {
66  $this->dbManager = $dbManager;
67  $this->userDao = $userDao;
68  $this->licenseDao = $licenseDao ?: $GLOBALS['container']->get('dao.license');
69  }
70 
75  public function setDelimiter($delimiter=',')
76  {
77  $this->delimiter = substr($delimiter,0,1);
78  }
79 
84  public function setEnclosure($enclosure='"')
85  {
86  $this->enclosure = substr($enclosure,0,1);
87  }
88 
96  public function handleFile($filename, $fileExtension)
97  {
98  if ($fileExtension === 'json') {
99  return $this->handleJsonFile($filename);
100  } else {
101  return $this->handleCsvFile($filename);
102  }
103  }
104 
110  private function handleJsonFile($filename)
111  {
112  $content = file_get_contents($filename);
113  if ($content === false) {
114  return _("Could not read JSON file");
115  }
116 
117  $data = json_decode($content, true);
118  if (json_last_error() !== JSON_ERROR_NONE) {
119  return _("Invalid JSON format: ") . json_last_error_msg();
120  }
121 
122  if (!is_array($data)) {
123  return _("JSON file must contain an array of phrases");
124  }
125 
126  return $this->importPhrases($data);
127  }
128 
134  private function handleCsvFile($filename)
135  {
136  $handle = fopen($filename, 'r');
137  if ($handle === false) {
138  return _("Could not open CSV file");
139  }
140 
141  $this->headrow = fgetcsv($handle, 0, $this->delimiter, $this->enclosure);
142  if ($this->headrow === false) {
143  fclose($handle);
144  return _("Could not read CSV header");
145  }
146 
147  // Strip BOM from the first header column if present
148  $bom = chr(0xEF) . chr(0xBB) . chr(0xBF);
149  if (isset($this->headrow[0]) && strpos($this->headrow[0], $bom) === 0) {
150  $this->headrow[0] = substr($this->headrow[0], 3);
151  }
152 
153  $data = array();
154  $lineNumber = 1;
155  while (($row = fgetcsv($handle, 0, $this->delimiter, $this->enclosure)) !== false) {
156  $lineNumber++;
157  if (count($row) !== count($this->headrow)) {
158  fclose($handle);
159  return sprintf(_("CSV line %d has %d columns, expected %d"),
160  $lineNumber, count($row), count($this->headrow));
161  }
162 
163  $data[] = array_combine($this->headrow, $row);
164  }
165  fclose($handle);
166 
167  return $this->importPhrases($data);
168  }
169 
175  private function importPhrases($data)
176  {
177  $imported = 0;
178  $errors = array();
179 
180  foreach ($data as $index => $phraseData) {
181  try {
182  $result = $this->importSinglePhrase($phraseData);
183  if ($result['success']) {
184  $imported++;
185  } else {
186  $errors[] = sprintf(_("Row %d: %s"), $index + 1, $result['message']);
187  }
188  } catch (Exception $e) {
189  $errors[] = sprintf(_("Row %d: %s"), $index + 1, $e->getMessage());
190  }
191  }
192 
193  $message = sprintf(_("Read file: %d phrases"), $imported);
194  if (!empty($errors)) {
195  $message .= "\n" . _("Errors:") . "\n" . implode("\n", $errors);
196  }
197 
198  return $message;
199  }
200 
206  private function importSinglePhrase($phraseData)
207  {
208  // Map headers to standard names
209  $mappedData = $this->mapHeaders($phraseData);
210 
211  // Validate required fields
212  if (empty($mappedData['text'])) {
213  return array('success' => false, 'message' => _("Text is required"));
214  }
215 
216  // Get current user info
217  $userId = Auth::getUserId();
218  $groupId = Auth::getGroupId();
219 
220  // Check for duplicate text
221  $textMd5 = md5($mappedData['text']);
222  $existingSql = "SELECT cp_pk FROM custom_phrase WHERE text_md5 = $1";
223  $existing = $this->dbManager->getSingleRow($existingSql, array($textMd5), __METHOD__ . '.duplicateCheck');
224 
225  if ($existing) {
226  return array('success' => false, 'message' => _("Duplicate text already exists"));
227  }
228 
229  // Insert the phrase
230  $insertSql = "INSERT INTO custom_phrase (text, text_md5, acknowledgement, comments, user_fk, group_fk, is_active)
231  VALUES ($1, $2, $3, $4, $5, $6, $7)";
232 
233  $params = array(
234  $mappedData['text'],
235  $textMd5,
236  $mappedData['acknowledgement'] ?? '',
237  $mappedData['comments'] ?? '',
238  $userId,
239  $groupId,
240  $this->parseBoolean($mappedData['is_active'] ?? false) ? 'true' : 'false'
241  );
242 
243  try {
244  $cpPk = $this->dbManager->insertPreparedAndReturn(__METHOD__ . '.insertPhrase', $insertSql, $params, 'cp_pk');
245  $message = _("Phrase imported successfully");
246 
247  $totalAssociated = 0;
248  $allFailed = array();
249  $allCreated = array();
250 
251  // Handle licenses to add
252  if (!empty($mappedData['licenses_to_add'])) {
253  $licenseResult = $this->associateLicenses($cpPk, $mappedData['licenses_to_add'], false);
254  $totalAssociated += $licenseResult['associated'];
255  $allFailed = array_merge($allFailed, $licenseResult['failed']);
256  $allCreated = array_merge($allCreated, $licenseResult['created']);
257  }
258 
259  // Handle licenses to remove
260  if (!empty($mappedData['licenses_to_remove'])) {
261  $licenseResult = $this->associateLicenses($cpPk, $mappedData['licenses_to_remove'], true);
262  $totalAssociated += $licenseResult['associated'];
263  $allFailed = array_merge($allFailed, $licenseResult['failed']);
264  $allCreated = array_merge($allCreated, $licenseResult['created']);
265  }
266 
267  if (!empty($allCreated)) {
268  $message .= ". " . sprintf(_("Created new licenses: %s"), implode(', ', $allCreated));
269  }
270  if (!empty($allFailed)) {
271  $message .= ". " . sprintf(_("Warning: Could not create/find licenses: %s"), implode(', ', $allFailed));
272  }
273  if ($totalAssociated > 0) {
274  $message .= ". " . sprintf(_("Associated %d licenses"), $totalAssociated);
275  }
276 
277  return array('success' => true, 'message' => $message);
278  } catch (Exception $e) {
279  return array('success' => false, 'message' => _("Failed to import phrase: ") . $e->getMessage());
280  }
281  }
282 
288  private function mapHeaders($data)
289  {
290  $mapped = array();
291 
292  foreach ($this->alias as $standardName => $aliases) {
293  foreach ($aliases as $alias) {
294  if (isset($data[$alias])) {
295  $mapped[$standardName] = $data[$alias];
296  break;
297  }
298  }
299  }
300 
301  // Normalize array/pipe-separated values from bulk text export format
302  $mapped = $this->normalizeBulkExportValues($mapped);
303 
304  return $mapped;
305  }
306 
319  private function normalizeBulkExportValues($mapped)
320  {
321  // Join array values to single strings for acknowledgement and comments
322  foreach (array('acknowledgement', 'comments') as $field) {
323  if (isset($mapped[$field])) {
324  if (is_array($mapped[$field])) {
325  $mapped[$field] = implode('; ', array_filter($mapped[$field]));
326  } elseif (is_string($mapped[$field]) && strpos($mapped[$field], '|') !== false) {
327  // Handle pipe-separated values from bulk CSV export
328  $parts = array_map('trim', explode('|', $mapped[$field]));
329  $mapped[$field] = implode('; ', array_filter($parts));
330  }
331  }
332  }
333 
334  // Restore literal '\n' escape sequences back to real newlines in text
335  if (isset($mapped['text']) && is_string($mapped['text'])) {
336  $mapped['text'] = str_replace('\\n', "\n", $mapped['text']);
337  }
338 
339  return $mapped;
340  }
341 
347  private function parseBoolean($value)
348  {
349  if (is_bool($value)) {
350  return $value;
351  }
352 
353  $value = strtolower(trim($value));
354  return in_array($value, array('true', '1', 'yes', 'on', 'active'));
355  }
356 
357  private function associateLicenses($cpPk, $licenseNames, $removing = false, $allowCreate = false)
358  {
359  if (is_array($licenseNames)) {
360  $licenseArray = $licenseNames;
361  } else {
362  // Handle multiple possible separators: comma-space, comma, semicolon, pipe
363  $separators = array(', ', ',', ';', '|');
364  $licenseArray = array($licenseNames); // Default to single license
365 
366  foreach ($separators as $separator) {
367  if (strpos($licenseNames, $separator) !== false) {
368  $licenseArray = array_map('trim', explode($separator, $licenseNames));
369  break;
370  }
371  }
372  }
373 
374  $associatedCount = 0;
375  $failedLicenses = array();
376  $createdLicenses = array();
377 
378  foreach ($licenseArray as $licenseName) {
379  if (empty($licenseName)) {
380  continue;
381  }
382 
383  $normalizedLicenseName = trim($licenseName);
384 
385  $license = $this->licenseDao->getLicenseByShortName($normalizedLicenseName);
386 
387  if (!$license) {
388  if (!$allowCreate || !$this->isValidLicenseShortname($normalizedLicenseName)) {
389  $failedLicenses[] = $licenseName . " (unknown)";
390  continue;
391  }
392 
393  try {
394  $newLicenseId = $this->licenseDao->insertLicense(
395  $normalizedLicenseName,
396  '',
397  null
398  );
399  $license = $this->licenseDao->getLicenseById($newLicenseId);
400  $createdLicenses[] = $normalizedLicenseName;
401  } catch (Exception $e) {
402  $failedLicenses[] = $licenseName . " (creation failed)";
403  continue;
404  }
405  }
406 
407  if ($license) {
408  $licenseId = $license->getId();
409 
410  // Check if association already exists
411  $checkSql = "SELECT 1 FROM custom_phrase_license_map WHERE cp_fk = $1 AND rf_fk = $2 LIMIT 1";
412  $existing = $this->dbManager->getSingleRow($checkSql, array($cpPk, $licenseId),
413  __METHOD__ . '.check.' . $cpPk . '.' . $licenseId);
414 
415  if (!$existing) {
416  // Insert the license association with removing flag
417  $insertData = array(
418  'cp_fk' => $cpPk,
419  'rf_fk' => $licenseId,
420  'removing' => $removing ? 'true' : 'false'
421  );
422 
423  try {
424  $this->dbManager->insertTableRow('custom_phrase_license_map', $insertData);
425  $associatedCount++;
426  } catch (Exception $e) {
427  $failedLicenses[] = $licenseName . " (insert failed)";
428  }
429  } else {
430  $associatedCount++; // Already exists, count as successful
431  }
432  }
433  }
434 
435  return array('associated' => $associatedCount, 'failed' => $failedLicenses, 'created' => $createdLicenses);
436  }
437 
443  private function isValidLicenseShortname($shortname)
444  {
445  // Must not be empty
446  if (empty(trim($shortname))) {
447  return false;
448  }
449 
450  // Must not exceed 256 characters
451  if (strlen($shortname) > 256) {
452  return false;
453  }
454 
455  // Must not contain control characters (except spaces)
456  if (preg_match('/[\x00-\x1F\x7F]/', $shortname)) {
457  return false;
458  }
459 
460  return true;
461  }
462 
469  public function importJsonData($data, string &$msg): string
470  {
471  $msg = $this->importPhrases($data);
472  return $msg;
473  }
474 }
Import custom text phrases from CSV/JSON.
importSinglePhrase($phraseData)
Import a single phrase.
importJsonData($data, string &$msg)
Import JSON data directly.
parseBoolean($value)
Parse boolean value from string.
normalizeBulkExportValues($mapped)
Normalize values from bulk text export format.
setEnclosure($enclosure='"')
Update the enclosure.
setDelimiter($delimiter=',')
Update the delimiter.
isValidLicenseShortname($shortname)
Validate a license shortname before auto-creating it.
handleFile($filename, $fileExtension)
Read the CSV/JSON file and import it.
mapHeaders($data)
Map CSV headers to standard field names.
__construct(DbManager $dbManager, UserDao $userDao, LicenseDao $licenseDao=null)
handleCsvFile($filename)
Handle CSV file import.
handleJsonFile($filename)
Handle JSON file import.
importPhrases($data)
Import phrases from data array.
Contains the constants and helpers for authentication of user.
Definition: Auth.php:24
static getUserId()
Get the current user's id.
Definition: Auth.php:68
static getGroupId()
Get the current user's group id.
Definition: Auth.php:80
Fossology exception.
Definition: Exception.php:15
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.