FOSSology  4.7.1
Open Source License Compliance by Open Source Software
AdminCustomTextList.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 
15 use Symfony\Component\HttpFoundation\JsonResponse;
16 use Symfony\Component\HttpFoundation\Request;
17 use Symfony\Component\HttpFoundation\Response;
18 
20 {
21  const NAME = "admin_custom_text_list";
22 
23  function __construct()
24  {
25  parent::__construct(self::NAME, array(
26  self::TITLE => "Custom Text List",
27  self::MENU_LIST => "Admin::Text Management::List",
28  self::REQUIRES_LOGIN => true,
29  self::PERMISSION => Auth::PERM_ADMIN
30  ));
31  }
32 
38  protected function handle(Request $request)
39  {
40  $userId = Auth::getUserId();
41  $groupId = Auth::getGroupId();
42 
43  // Check if user is admin
44  if (!Auth::isAdmin()) {
45  return $this->flushContent(_('Access denied. Admin privileges required.'));
46  }
47 
48  $action = $request->get('action');
49 
50  // Handle AJAX requests
51  if ($action == 'fetchData') {
52  return $this->fetchDataServerSide($request);
53  }
54 
55  if ($action == 'get_bulk_data') {
56  return $this->getBulkDataAjax($request);
57  }
58 
59  if ($action == 'delete' && $request->getMethod() == 'POST') {
60  return $this->deletePhraseAjax($request);
61  }
62 
63  if ($action == 'toggle' && $request->getMethod() == 'POST') {
64  return $this->togglePhraseStatusAjax($request);
65  }
66 
67  // Default to table view
68  $vars = array(
69  'formAction' => Traceback_uri() . '?mod=' . self::NAME,
70  'addModuleName' => 'admin_custom_text_management'
71  );
72  return $this->render('admin_custom_text_list.html.twig', $this->mergeWithDefault($vars));
73  }
74 
78  private function fetchDataServerSide(Request $request)
79  {
80  $offset = intval($request->query->get('start', 0));
81  $limit = intval($request->query->get('length', 10));
82  $draw = intval($request->query->get('draw', 1));
83  $searchQuery = $request->query->all('search')['value'] ?? '';
84 
85  $filteredQuery = '';
86  if (!empty($searchQuery)) {
87  $filteredQuery = '%' . $searchQuery . '%';
88  }
89 
90  $totalCount = $this->getTotalPhrasesCount('');
91  $filteredCount = empty($filteredQuery) ? $totalCount : $this->getTotalPhrasesCount($filteredQuery);
92  $phraseArray = $this->getPhrasesServerSide($limit, $offset, $filteredQuery);
93 
94  return new JsonResponse([
95  "draw" => $draw,
96  "recordsTotal" => $totalCount,
97  "recordsFiltered" => $filteredCount,
98  "data" => $phraseArray,
99  ], JsonResponse::HTTP_OK);
100  }
101 
105  private function deletePhraseAjax(Request $request)
106  {
107  $phraseId = intval($request->get('id'));
108 
109  if (!$phraseId) {
110  return new JsonResponse(array('error' => 'Invalid phrase ID'), 400);
111  }
112 
113  try {
115  $dbManager = $this->getObject('db.manager');
116 
117  // Start transaction
118  $dbManager->begin();
119 
120  // Delete license associations first (though CASCADE should handle this)
121  $deleteLicensesSql = "DELETE FROM custom_phrase_license_map WHERE cp_fk = $1";
122  $dbManager->prepare($deleteLicensesStmt = __METHOD__ . ".delete_licenses", $deleteLicensesSql);
123  $dbManager->freeResult($dbManager->execute($deleteLicensesStmt, array($phraseId)));
124 
125  // Delete the phrase itself
126  $sql = "DELETE FROM custom_phrase WHERE cp_pk = $1";
127  $dbManager->prepare($stmt = __METHOD__ . ".delete", $sql);
128  $dbManager->freeResult($dbManager->execute($stmt, array($phraseId)));
129 
130  // Commit transaction
131  $dbManager->commit();
132 
133  return new JsonResponse(array(
134  'success' => true,
135  'message' => 'Custom text deleted successfully'
136  ));
137  } catch (\Exception $e) {
138  $dbManager->rollback();
139  return new JsonResponse(array('error' => 'Failed to delete phrase: ' . $e->getMessage()), 500);
140  }
141  }
142 
146  private function togglePhraseStatusAjax(Request $request)
147  {
148  $phraseId = intval($request->get('id'));
149  $status = intval($request->get('status'));
150 
151  if (!$phraseId) {
152  return new JsonResponse(array('error' => 'Invalid phrase ID'), 400);
153  }
154 
155  try {
157  $dbManager = $this->getObject('db.manager');
158 
159  $sql = "UPDATE custom_phrase SET is_active = $1 WHERE cp_pk = $2";
160  $dbManager->prepare($stmt = __METHOD__ . ".toggle", $sql);
161  $dbManager->freeResult($dbManager->execute($stmt, array($status ? 'true' : 'false', $phraseId)));
162 
163  return new JsonResponse(array(
164  'success' => true,
165  'message' => $status ? 'Custom text activated' : 'Custom text deactivated'
166  ));
167  } catch (\Exception $e) {
168  return new JsonResponse(array('error' => 'Failed to toggle status: ' . $e->getMessage()), 500);
169  }
170  }
171 
172  private function getTotalPhrasesCount($searchQuery = '')
173  {
175  $dbManager = $this->getObject('db.manager');
176 
177  $sql = "SELECT COUNT(DISTINCT cp.cp_pk) AS count
178  FROM custom_phrase cp
179  LEFT JOIN users u ON cp.user_fk = u.user_pk
180  LEFT JOIN custom_phrase_license_map cplm ON cp.cp_pk = cplm.cp_fk
181  LEFT JOIN license_ref lr ON cplm.rf_fk = lr.rf_pk";
182 
183  $params = array();
184 
185  if (!empty($searchQuery)) {
186  $sql .= " WHERE (cp.text ILIKE $1 OR cp.acknowledgement ILIKE $1 OR cp.comments ILIKE $1"
187  . " OR u.user_name ILIKE $1 OR lr.rf_shortname ILIKE $1)";
188  $params[] = $searchQuery;
189  }
190 
191  $result = $dbManager->getSingleRow($sql, $params, __METHOD__);
192  return $result ? intval($result['count']) : 0;
193  }
194 
198  private function getPhrasesServerSide($limit, $offset, $searchQuery = '')
199  {
201  $dbManager = $this->getObject('db.manager');
202 
203  $sql = "SELECT cp.cp_pk, cp.text, cp.text_md5, cp.acknowledgement, cp.comments,
204  cp.user_fk, cp.group_fk, cp.created_date, cp.is_active,
205  u.user_name,
206  STRING_AGG(CASE WHEN cplm.removing = false THEN lr.rf_shortname END, ', ' ORDER BY lr.rf_shortname) as licenses_to_add,
207  STRING_AGG(CASE WHEN cplm.removing = true THEN lr.rf_shortname END, ', ' ORDER BY lr.rf_shortname) as licenses_to_remove
208  FROM custom_phrase cp
209  LEFT JOIN users u ON cp.user_fk = u.user_pk
210  LEFT JOIN custom_phrase_license_map cplm ON cp.cp_pk = cplm.cp_fk
211  LEFT JOIN license_ref lr ON cplm.rf_fk = lr.rf_pk";
212 
213  $params = array();
214 
215  if (!empty($searchQuery)) {
216  $sql .= " WHERE (cp.text ILIKE $1 OR cp.acknowledgement ILIKE $1 OR cp.comments ILIKE $1 OR u.user_name ILIKE $1 OR lr.rf_shortname ILIKE $1)";
217  $params[] = $searchQuery;
218  }
219 
220  $sql .= " GROUP BY cp.cp_pk, cp.text, cp.text_md5, cp.acknowledgement, cp.comments,
221  cp.user_fk, cp.group_fk, cp.created_date, cp.is_active, u.user_name
222  ORDER BY cp.created_date DESC";
223 
224  $sql .= " LIMIT $" . (count($params) + 1) . " OFFSET $" . (count($params) + 2);
225  $params[] = $limit;
226  $params[] = $offset;
227 
228  $result = $dbManager->getRows($sql, $params, __METHOD__);
229  $aaData = array();
230 
231  foreach ($result as $row) {
232  $editLink = '<a href="?mod=admin_custom_text_management&edit=' . $row['cp_pk'] . '"><img border="0" src="images/button_edit.png"></a>';
233 
234  $text = strlen($row['text']) > 100 ?
235  substr($row['text'], 0, 100) . '...' :
236  $row['text'];
237 
238  $acknowledgement = strlen($row['acknowledgement'] ?: '') > 50 ?
239  substr($row['acknowledgement'], 0, 50) . '...' :
240  ($row['acknowledgement'] ?: 'N/A');
241 
242  $comments = strlen($row['comments'] ?: '') > 50 ?
243  substr($row['comments'], 0, 50) . '...' :
244  ($row['comments'] ?: 'N/A');
245 
246  $isActiveFlag = $dbManager->booleanFromDb($row['is_active']);
247  $statusToggle = '<input type="checkbox" class="phrase-status-toggle"'
248  . ' data-phrase-id="' . intval($row['cp_pk']) . '"'
249  . ' data-current-status="' . ($isActiveFlag ? '1' : '0') . '"'
250  . ($isActiveFlag ? ' checked' : '') . '/>';
251 
252  $deleteBtn = '<button type="button" class="custom-text-delete"'
253  . ' data-phrase-id="' . intval($row['cp_pk']) . '">'
254  . '<img class="delete" src="images/space_16.png" alt="Delete"/></button>';
255 
256  // Format licenses display with separate lines for add/remove
257  $licensesDisplay = '';
258  $licensesToAdd = $row['licenses_to_add'] ?: null;
259  $licensesToRemove = $row['licenses_to_remove'] ?: null;
260 
261  if ($licensesToAdd || $licensesToRemove) {
262  if ($licensesToAdd) {
263  $licensesDisplay .= '<div><strong>To be added:</strong> ' . htmlentities($licensesToAdd) . '</div>';
264  }
265  if ($licensesToRemove) {
266  $licensesDisplay .= '<div><strong>To be removed:</strong> ' . htmlentities($licensesToRemove) . '</div>';
267  }
268  } else {
269  $licensesDisplay = 'N/A';
270  }
271 
272  $aaData[] = array(
273  $editLink,
274  $licensesDisplay,
275  '<div style="overflow-y:scroll;max-height:100px;margin:0;">' . nl2br(htmlentities($text)) . '</div>',
276  htmlentities($acknowledgement),
277  htmlentities($comments),
278  htmlentities($row['user_name'] ?: 'N/A'),
279  $row['created_date'],
280  $statusToggle,
281  $deleteBtn
282  );
283  }
284 
285  return $aaData;
286  }
287 
294  private function getTotalBulkCount($searchQuery = '', $userFk = 0)
295  {
297  $dbManager = $this->getObject('db.manager');
298 
299  $sql = "SELECT COUNT(DISTINCT lrb.lrb_pk) AS count
300  FROM license_ref_bulk lrb
301  INNER JOIN license_set_bulk lsb ON lrb.lrb_pk = lsb.lrb_fk
302  INNER JOIN license_ref lr ON lsb.rf_fk = lr.rf_pk
303  LEFT JOIN users u ON lrb.user_fk = u.user_pk
304  LEFT JOIN upload up ON lrb.upload_fk = up.upload_pk
305  WHERE lrb.rf_text IS NOT NULL AND lrb.rf_text != ''";
306 
307  $params = array();
308  $paramIdx = 1;
309 
310  if (!empty($searchQuery)) {
311  $sql .= " AND (lrb.rf_text ILIKE \$$paramIdx OR lr.rf_shortname ILIKE \$$paramIdx OR u.user_name ILIKE \$$paramIdx OR up.upload_filename ILIKE \$$paramIdx)";
312  $params[] = $searchQuery;
313  $paramIdx++;
314  }
315 
316  if ($userFk > 0) {
317  $sql .= " AND lrb.user_fk = \$$paramIdx";
318  $params[] = $userFk;
319  }
320 
321  $result = $dbManager->getSingleRow($sql, $params, __METHOD__);
322  return $result ? intval($result['count']) : 0;
323  }
324 
334  private function getBulkDataAjax(Request $request)
335  {
336  $page = max(1, intval($request->query->get('page', 1)));
337  $limit = max(1, intval($request->query->get('limit', 10)));
338  $searchTerm = trim($request->query->get('search', ''));
339  $userFk = max(0, intval($request->query->get('user_fk', 0)));
340 
341  $searchQuery = '';
342  if (!empty($searchTerm)) {
343  $searchQuery = '%' . $searchTerm . '%';
344  }
345 
346  // Total matching entries (for pagination metadata)
347  $totalRecords = $this->getTotalBulkCount($searchQuery, $userFk);
348  $totalPages = max(1, intval(ceil($totalRecords / $limit)));
349 
350  // Clamp page to valid range
351  if ($page > $totalPages) {
352  $page = $totalPages;
353  }
354  $offset = ($page - 1) * $limit;
355 
357  $dbManager = $this->getObject('db.manager');
358 
359  // Build a subquery that selects the paginated set of distinct lrb_pk values,
360  // then join the full row data. This ensures every license row belonging to
361  // a paginated bulk entry is returned.
362  $whereClause = "WHERE lrb.rf_text IS NOT NULL AND lrb.rf_text != ''";
363  $params = array();
364  $paramIdx = 1;
365 
366  if (!empty($searchQuery)) {
367  $whereClause .= " AND (lrb.rf_text ILIKE \$$paramIdx OR lr.rf_shortname ILIKE \$$paramIdx OR u.user_name ILIKE \$$paramIdx OR up.upload_filename ILIKE \$$paramIdx)";
368  $params[] = $searchQuery;
369  $paramIdx++;
370  }
371 
372  if ($userFk > 0) {
373  $whereClause .= " AND lrb.user_fk = \$$paramIdx";
374  $params[] = $userFk;
375  $paramIdx++;
376  }
377 
378  // Subquery: get the paginated lrb_pk values
379  $subSql = "SELECT DISTINCT lrb.lrb_pk
380  FROM license_ref_bulk lrb
381  INNER JOIN license_set_bulk lsb ON lrb.lrb_pk = lsb.lrb_fk
382  INNER JOIN license_ref lr ON lsb.rf_fk = lr.rf_pk
383  LEFT JOIN users u ON lrb.user_fk = u.user_pk
384  LEFT JOIN upload up ON lrb.upload_fk = up.upload_pk
385  $whereClause
386  ORDER BY lrb.lrb_pk DESC
387  LIMIT \$$paramIdx OFFSET \$" . ($paramIdx + 1);
388  $params[] = $limit;
389  $params[] = $offset;
390  $paramIdx += 2;
391 
392  // Main query: fetch full data only for the paginated lrb_pk set
393  $sql = "SELECT
394  lrb.lrb_pk,
395  lrb.rf_text as bulk_reference_text,
396  lrb.ignore_irrelevant,
397  lrb.bulk_delimiters,
398  lrb.scan_findings,
399 
400  lr.rf_pk as license_id,
401  lr.rf_shortname as license_shortname,
402  lr.rf_fullname as license_fullname,
403  lr.rf_spdx_id as license_spdx_id,
404 
405  lsb.removing as is_removing_license,
406  lsb.comment as license_comment,
407  lsb.reportinfo as license_reportinfo,
408  lsb.acknowledgement as license_acknowledgement,
409 
410  u.user_name as created_by_user,
411  up.upload_filename as upload_file,
412 
413  lrb.user_fk,
414  lrb.group_fk,
415  lrb.upload_fk,
416  lrb.uploadtree_fk
417 
418  FROM license_ref_bulk lrb
419  INNER JOIN license_set_bulk lsb ON lrb.lrb_pk = lsb.lrb_fk
420  INNER JOIN license_ref lr ON lsb.rf_fk = lr.rf_pk
421  LEFT JOIN users u ON lrb.user_fk = u.user_pk
422  LEFT JOIN upload up ON lrb.upload_fk = up.upload_pk
423  WHERE lrb.lrb_pk IN ($subSql)
424  ORDER BY lrb.lrb_pk DESC, lr.rf_shortname";
425 
426  $result = $dbManager->getRows($sql, $params, __METHOD__);
427  $bulkData = array();
428 
429  // Group results by lrb_pk to handle multiple licenses per bulk entry
430  $groupedData = array();
431  foreach ($result as $row) {
432  $lrbPk = $row['lrb_pk'];
433 
434  if (!isset($groupedData[$lrbPk])) {
435  $text = strlen($row['bulk_reference_text']) > 200 ?
436  substr($row['bulk_reference_text'], 0, 200) . '...' :
437  $row['bulk_reference_text'];
438 
439  $ignoreIrrelevant = $dbManager->booleanFromDb($row['ignore_irrelevant']);
440  $scanFindings = $dbManager->booleanFromDb($row['scan_findings']);
441 
442  $groupedData[$lrbPk] = array(
443  'lrb_pk' => $lrbPk,
444  'user_fk' => $row['user_fk'],
445  'group_fk' => $row['group_fk'],
446  'bulk_reference_text' => $row['bulk_reference_text'],
447  'text_preview' => $text,
448  'upload_fk' => $row['upload_fk'],
449  'uploadtree_fk' => $row['uploadtree_fk'],
450  'ignore_irrelevant' => $ignoreIrrelevant,
451  'bulk_delimiters' => $row['bulk_delimiters'],
452  'scan_findings' => $scanFindings,
453  'created_by_user' => $row['created_by_user'] ?: 'Unknown',
454  'upload_file' => $row['upload_file'] ?: 'Unknown',
455  'licenses' => array(),
456  'all_acknowledgements' => array(),
457  'all_comments' => array()
458  );
459  }
460 
461  // Add license information
462  $isRemoving = $dbManager->booleanFromDb($row['is_removing_license']);
463  $licenseInfo = array(
464  'license_id' => $row['license_id'],
465  'license_shortname' => $row['license_shortname'],
466  'license_fullname' => $row['license_fullname'],
467  'license_spdx_id' => $row['license_spdx_id'],
468  'is_removing_license' => $isRemoving,
469  'license_comment' => $row['license_comment'],
470  'license_reportinfo' => $row['license_reportinfo'],
471  'license_acknowledgement' => $row['license_acknowledgement']
472  );
473 
474  $groupedData[$lrbPk]['licenses'][] = $licenseInfo;
475 
476  // Collect unique acknowledgements and comments
477  if (!empty($row['license_acknowledgement'])) {
478  $groupedData[$lrbPk]['all_acknowledgements'][] = $row['license_acknowledgement'];
479  }
480  if (!empty($row['license_comment'])) {
481  $groupedData[$lrbPk]['all_comments'][] = $row['license_comment'];
482  }
483  }
484 
485  // Convert grouped data to final format
486  foreach ($groupedData as $entry) {
487  $licenseNames = array();
488  $addedLicenses = array();
489  $removedLicenses = array();
490 
491  foreach ($entry['licenses'] as $license) {
492  $licenseNames[] = $license['license_shortname'];
493  if ($license['is_removing_license']) {
494  $removedLicenses[] = $license['license_shortname'];
495  } else {
496  $addedLicenses[] = $license['license_shortname'];
497  }
498  }
499 
500  // Create summary strings
501  $entry['license_summary'] = implode(', ', $licenseNames);
502  $entry['added_licenses'] = implode(', ', $addedLicenses);
503  $entry['removed_licenses'] = implode(', ', $removedLicenses);
504  $entry['acknowledgement_summary'] = implode('; ', array_unique($entry['all_acknowledgements']));
505  $entry['comment_summary'] = implode('; ', array_unique($entry['all_comments']));
506 
507  $bulkData[] = $entry;
508  }
509 
510  return new JsonResponse(array(
511  'data' => $bulkData,
512  'totalRecords' => $totalRecords,
513  'totalPages' => $totalPages,
514  'currentPage' => $page,
515  'pageSize' => $limit
516  ));
517  }
518 }
519 
520 register_plugin(new AdminCustomTextList());
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)
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
FUNCTION int max(int permGroup, int permPublic)
Get the maximum group privilege.
Definition: libfossagent.c:295