FOSSology  4.7.1
Open Source License Compliance by Open Source Software
AdminCustomTextManagement.php
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 
9 namespace Fossology\UI\Page;
10 
17 use Symfony\Component\HttpFoundation\JsonResponse;
18 use Symfony\Component\HttpFoundation\Request;
19 use Symfony\Component\HttpFoundation\Response;
20 use Symfony\Component\HttpFoundation\RedirectResponse;
21 
23 {
24  const NAME = "admin_custom_text_management";
25 
26  function __construct()
27  {
28  parent::__construct(self::NAME, array(
29  self::TITLE => "Add Custom Text",
30  self::MENU_LIST => "Admin::Text Management::Add",
31  self::REQUIRES_LOGIN => true,
32  self::PERMISSION => Auth::PERM_ADMIN
33  ));
34  }
35 
41  protected function handle(Request $request)
42  {
43  $userId = Auth::getUserId();
44  $groupId = Auth::getGroupId();
46  $userDao = $this->getObject('dao.user');
47 
48  // Check if user is admin
49  if (!Auth::isAdmin()) {
50  return $this->flushContent(_('Access denied. Admin privileges required.'));
51  }
52 
53  $action = $request->get('action');
54 
55  // Handle AJAX requests
56  if ($action == 'check_duplicate' && $request->getMethod() == 'POST') {
57  return $this->checkDuplicateAjax($request);
58  }
59 
60  // Handle form submissions
61  if ($request->get('updateit') || $request->get('addit')) {
62  $result = $this->savePhrase($request, $userId, $groupId);
63  if (!$result['success']) {
64  $vars = $this->getEditFormVarsFromRequest($request);
65  $vars['message'] = $result['message'];
66  return $this->render('admin_custom_text_edit.html.twig', $this->mergeWithDefault($vars));
67  }
68  // Redirect to list view after successful save
69  $redirectUrl = Traceback_uri() . '?mod=admin_custom_text_list';
70  return new RedirectResponse($redirectUrl);
71  }
72 
73  // Handle edit form display
74  if ($request->get('edit') !== null) {
75  $cp_pk = intval($request->get('edit'));
76  $vars = $this->getEditFormVars($cp_pk);
77  return $this->render('admin_custom_text_edit.html.twig', $this->mergeWithDefault($vars));
78  }
79 
80  // Default to add form (edit with cp_pk=0)
81  $vars = $this->getEditFormVars(0);
82  return $this->render('admin_custom_text_edit.html.twig', $this->mergeWithDefault($vars));
83  }
84 
88  private function getEditFormVars($cp_pk)
89  {
90  $vars = array();
91 
92  $phraseData = $cp_pk > 0 ? $this->getPhraseData($cp_pk) : false;
93 
94  if ($phraseData) {
95  // Edit existing phrase
96  $vars = array_merge($vars, $phraseData);
97  $vars['isEdit'] = true;
98  // Get associated licenses for this phrase
99  $vars['selectedLicenses'] = $this->getAssociatedLicenses($cp_pk);
100  } else {
101  // Add new phrase, or an edit whose phrase has since been deleted
102  $vars['isEdit'] = false;
103  $vars['cp_pk'] = 0;
104  $vars['text'] = '';
105  $vars['selectedLicenses'] = array();
106  }
107 
108  $vars['formAction'] = Traceback_uri() . '?mod=' . self::NAME;
109  $vars['updateParam'] = $vars['isEdit'] ? 'updateit' : 'addit';
110  $vars['textParam'] = 'text';
111  $vars['isActiveParam'] = 'is_active';
112 
113  // Get license options for dropdown
114  $vars['licenseOptions'] = $this->getLicenseOptions();
115 
116  // Get users for bulk data filter dropdown
118  $userDao = $this->getObject('dao.user');
119  $vars['bulkDataUsers'] = $userDao->getUsersByGroup();
120 
121  return $vars;
122  }
123 
127  private function getEditFormVarsFromRequest(Request $request)
128  {
129  $vars = $this->getEditFormVars(intval($request->get('cp_pk', 0)));
130  $vars['text'] = $this->stringValue($request->get('text'));
131  $vars['is_active'] = $request->get('is_active') == 'on';
132 
133  $selectedLicenses = array();
134  foreach ($this->parseLicenseData($request->get('license_data')) as $mapping) {
135  $shortname = $mapping['rf_shortname'];
136  if ($shortname === '') {
137  $shortname = isset($vars['licenseOptions'][$mapping['rf_pk']]) ?
138  $vars['licenseOptions'][$mapping['rf_pk']] : '';
139  }
140  $mapping['rf_shortname'] = $shortname;
141  $selectedLicenses[] = $mapping;
142  }
143  $vars['selectedLicenses'] = $selectedLicenses;
144 
145  return $vars;
146  }
147 
151  private function parseLicenseData($licenseData)
152  {
153  $mappings = array();
154  if (empty($licenseData) || !is_string($licenseData)) {
155  return $mappings;
156  }
157  $decodedData = json_decode($licenseData, true);
158  if (!is_array($decodedData)) {
159  return $mappings;
160  }
161 
162  foreach ($decodedData as $item) {
163  if (!is_array($item) || empty($item['licenseId'])) {
164  continue;
165  }
166  $mappings[] = array(
167  'rf_pk' => intval($item['licenseId']),
168  'rf_shortname' => $this->stringValue($item['licenseName'] ?? null),
169  'removing' => ($this->stringValue($item['action'] ?? null) === 'Remove'),
170  'comment' => $this->stringValue($item['comment'] ?? null),
171  'reportinfo' => $this->stringValue($item['reportinfo'] ?? null),
172  'acknowledgement' => $this->stringValue($item['acknowledgement'] ?? null)
173  );
174  }
175  return $mappings;
176  }
177 
181  private function stringValue($value)
182  {
183  return is_string($value) ? trim($value) : '';
184  }
185 
189  private function checkDuplicateAjax(Request $request)
190  {
191  $text = StringOperation::replaceUnicodeControlChar(trim($request->get('text')));
192  $currentCpPk = intval($request->get('cp_pk'));
193 
194  if (empty($text)) {
195  return new JsonResponse(array('duplicate' => false));
196  }
197 
198  $isDuplicate = $this->checkDuplicateTextMd5(md5($text), $currentCpPk > 0 ? $currentCpPk : null);
199 
200  return new JsonResponse(array('duplicate' => $isDuplicate));
201  }
202 
206  private function checkDuplicateTextMd5($textMd5, $excludeCpPk = null)
207  {
209  $dbManager = $this->getObject('db.manager');
210 
211  $sql = "SELECT cp_pk FROM custom_phrase WHERE text_md5 = $1";
212  $params = array($textMd5);
213 
214  if ($excludeCpPk) {
215  $sql .= " AND cp_pk != $2";
216  $params[] = $excludeCpPk;
217  }
218 
219  $result = $dbManager->getSingleRow($sql, $params, __METHOD__);
220 
221  return $result !== false;
222  }
223 
227  private function getPhraseData($cp_pk)
228  {
230  $dbManager = $this->getObject('db.manager');
231 
232  $sql = "SELECT * FROM custom_phrase WHERE cp_pk = $1";
233  $row = $dbManager->getSingleRow($sql, array($cp_pk), __METHOD__);
234 
235  if ($row) {
236  $row['is_active'] = $dbManager->booleanFromDb($row['is_active']);
237  }
238 
239  return $row;
240  }
241 
242 
246  private function getAssociatedLicenses($cp_pk)
247  {
249  $dbManager = $this->getObject('db.manager');
250 
251  $sql = "SELECT lr.rf_pk, lr.rf_shortname, cplm.removing,
252  cplm.comment, cplm.reportinfo, cplm.acknowledgement
253  FROM custom_phrase_license_map cplm
254  JOIN license_ref lr ON cplm.rf_fk = lr.rf_pk
255  WHERE cplm.cp_fk = $1
256  ORDER BY lr.rf_shortname";
257 
258  $result = $dbManager->getRows($sql, array($cp_pk));
259 
260  $licenses = array();
261  foreach ($result as $row) {
262  $licenses[] = array(
263  'rf_pk' => $row['rf_pk'],
264  'rf_shortname' => $row['rf_shortname'],
265  'removing' => $dbManager->booleanFromDb($row['removing']),
266  'comment' => $row['comment'] ?? '',
267  'reportinfo' => $row['reportinfo'] ?? '',
268  'acknowledgement' => $row['acknowledgement'] ?? ''
269  );
270  }
271 
272  return $licenses;
273  }
274 
280  private function savePhrase(Request $request, $userId, $groupId)
281  {
282  $cp_pk = intval($request->get('cp_pk'));
283  $isUpdate = $cp_pk > 0;
284  $text = StringOperation::replaceUnicodeControlChar($this->stringValue($request->get('text')));
285  $user_fk = intval($request->get('user_fk'));
286  $group_fk = intval($request->get('group_fk'));
287  $is_active = $request->get('is_active') == 'on' ? 'true' : 'false';
288 
289  if (empty($text)) {
290  return array('success' => false,
291  'message' => _("ERROR: The text field cannot be empty."));
292  }
293 
294  $licenseMappings = $this->parseLicenseData($request->get('license_data'));
295 
296  // Validate that at least one license is associated
297  if (empty($licenseMappings)) {
298  return array('success' => false,
299  'message' => _("ERROR: At least one license must be associated with the custom text."));
300  }
301 
302  // Generate MD5 hash of the text
303  $textMd5 = md5($text);
304 
305  // Check for duplicate text (exclude current record when updating)
306  if ($this->checkDuplicateTextMd5($textMd5, $isUpdate ? $cp_pk : null)) {
307  return array('success' => false,
308  'message' => _("ERROR: A custom text with the same content already exists in the database. Please modify the text or use the existing entry."));
309  }
310 
311  // Set defaults for user and group if not provided
312  if (empty($user_fk)) {
313  $user_fk = $userId;
314  }
315  if (empty($group_fk)) {
316  $group_fk = $groupId;
317  }
318 
320  $dbManager = $this->getObject('db.manager');
321 
322  try {
323  // Start transaction
324  $dbManager->begin();
325 
326  if ($isUpdate) {
327  // Update existing phrase. acknowledgement/comments are not set here:
328  // metadata lives on the license map now, not the phrase.
329  $sql = "UPDATE custom_phrase SET
330  text = $2, text_md5 = $3, user_fk = $4, group_fk = $5, is_active = $6
331  WHERE cp_pk = $1";
332  $params = array($cp_pk, $text, $textMd5, $user_fk, $group_fk, $is_active);
333  $dbManager->prepare($stmt = __METHOD__ . ".update", $sql);
334  $dbManager->freeResult($dbManager->execute($stmt, $params));
335 
336  // Delete existing license associations
337  $deleteSql = "DELETE FROM custom_phrase_license_map WHERE cp_fk = $1";
338  $dbManager->prepare($deleteStmt = __METHOD__ . ".delete_licenses", $deleteSql);
339  $dbManager->freeResult($dbManager->execute($deleteStmt, array($cp_pk)));
340 
341  } else {
342  // Insert new phrase. acknowledgement/comments stay NULL by omission.
343  $sql = "INSERT INTO custom_phrase
344  (text, text_md5, user_fk, group_fk, is_active, created_date)
345  VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) RETURNING cp_pk";
346  $params = array($text, $textMd5, $user_fk, $group_fk, $is_active);
347  $dbManager->prepare($stmt = __METHOD__ . ".insert", $sql);
348  $result = $dbManager->execute($stmt, $params);
349  $row = $dbManager->fetchArray($result);
350  $cp_pk = $row['cp_pk'];
351  $dbManager->freeResult($result);
352  }
353 
354  if (!empty($licenseMappings)) {
355  $insertLicenseSql = "INSERT INTO custom_phrase_license_map
356  (cp_fk, rf_fk, removing, comment, reportinfo, acknowledgement)
357  VALUES ($1, $2, $3, $4, $5, $6)";
358  $dbManager->prepare($insertLicenseStmt = __METHOD__ . ".insert_license", $insertLicenseSql);
359 
360  foreach ($licenseMappings as $mapping) {
361  if (!empty($mapping['rf_pk'])) {
362  $dbManager->freeResult($dbManager->execute($insertLicenseStmt, array(
363  $cp_pk,
364  $mapping['rf_pk'],
365  $mapping['removing'] ? 'true' : 'false',
366  $mapping['comment'] ?: null,
367  $mapping['reportinfo'] ?: null,
368  $mapping['acknowledgement'] ?: null
369  )));
370  }
371  }
372  }
373 
374  // Commit transaction
375  $dbManager->commit();
376 
377  return array('success' => true,
378  'message' => $isUpdate ? _("Custom text updated successfully.") :
379  _("Custom text added successfully."));
380 
381  } catch (\Throwable $e) {
382  $dbManager->rollback();
383  return array('success' => false,
384  'message' => _("ERROR: Failed to save custom text: ") . $e->getMessage());
385  }
386  }
387 
388  private function getLicenseOptions()
389  {
391  $licenseDao = $this->getObject('dao.license');
392 
393  return $licenseDao->getActiveLicensesForGroup(Auth::getGroupId());
394  }
395 }
396 
397 register_plugin(new AdminCustomTextManagement());
398 
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
static isAdmin()
Check if user is admin.
Definition: Auth.php:92
render($templateName, $vars=null, $headers=null)
static replaceUnicodeControlChar($input, $replace="")
Traceback_uri()
Get the URI without query to this location.
Definition: common-parm.php:97
char * trim(char *ptext)
Trimming whitespace.
Definition: fossconfig.c:690