dolibarr 25.0.0-alpha
log_viewer.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2004-2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY, without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
26require '../../main.inc.php';
35require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php';
36require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
37require_once DOL_DOCUMENT_ROOT . '/core/class/html.form.class.php';
38
39// Load translations
40$langs->loadLangs(array("admin", "other"));
41
42// Parameters
43$action = GETPOST('action', 'aZ09');
44$massaction = GETPOST('massaction', 'alpha');
45$confirm = GETPOST('confirm', 'alpha');
46$toselect = GETPOST('toselect', 'array');
47$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'ailoglist';
48$optioncss = GETPOST('optioncss', 'alpha');
49$mode = GETPOST('mode', 'alpha');
50
51// Search parameters for all columns
52$search_date_start = dol_mktime(0, 0, 0, GETPOSTINT('search_date_startmonth'), GETPOSTINT('search_date_startday'), GETPOSTINT('search_date_startyear'));
53$search_date_end = dol_mktime(23, 59, 59, GETPOSTINT('search_date_endmonth'), GETPOSTINT('search_date_endday'), GETPOSTINT('search_date_endyear'));
54$search_user = GETPOST('search_user', 'alpha');
55$search_query = GETPOST('search_query', 'alpha');
56$search_tool = GETPOST('search_tool', 'alpha');
57$search_provider = GETPOST('search_provider', 'alpha');
58$search_time_min = GETPOST('search_time_min', 'alpha');
59$search_time_max = GETPOST('search_time_max', 'alpha');
60$search_status = GETPOST('search_status', 'alpha');
61
62// Pagination parameters
63$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
64$sortfield = GETPOST('sortfield', 'aZ09comma');
65$sortorder = GETPOST('sortorder', 'aZ09comma');
66$page = GETPOSTINT("page");
67if (empty($page) || $page == -1) {
68 $page = 0;
69}
70$offset = $limit * $page;
71if (!$sortfield) $sortfield = "l.date_request";
72if (!$sortorder) $sortorder = "DESC";
73
74// Initialize array of search criteria
75$search_array = array(
76 'search_date_start' => $search_date_start,
77 'search_date_end' => $search_date_end,
78 'search_user' => $search_user,
79 'search_query' => $search_query,
80 'search_tool' => $search_tool,
81 'search_provider' => $search_provider,
82 'search_time_min' => $search_time_min,
83 'search_time_max' => $search_time_max,
84 'search_status' => $search_status
85);
86
87// Access Control
88if (!$user->admin) {
90}
91if (!isModEnabled('ai')) {
92 accessforbidden('Module AI not activated.');
93}
94
95
96/*
97 * Actions
98 */
99
100$error = '';
101if ($action == 'purge' && $confirm == 'yes') {
102 $db->begin();
103
104 $sql = "DELETE FROM " . MAIN_DB_PREFIX . "ai_request_log";
105 $sql .= " WHERE entity IN (" . getEntity('airequestlog') . ")";
106
107 $resql = $db->query($sql);
108
109 if ($resql) {
110 $nbDeleted = $db->affected_rows($resql);
111 $db->commit();
112 setEventMessages($langs->trans("LogsCleared") . " (" . $nbDeleted . ")", null, 'mesgs');
113 } else {
114 $db->rollback();
115 setEventMessages($db->lasterror(), null, 'errors');
116 }
117
118 header('Location: ' . $_SERVER["PHP_SELF"]);
119 exit;
120}
121
122// Purge selection
123if ($massaction == 'purge' && !empty($toselect) && is_array($toselect)) {
124 $db->begin();
125
126 foreach ($toselect as $id) {
127 $sql = "DELETE FROM " . MAIN_DB_PREFIX . "ai_request_log";
128 $sql .= " WHERE rowid = " . ((int) $id);
129 $sql .= " AND entity IN (" . getEntity('airequestlog') . ")";
130
131 $resql = $db->query($sql);
132 if (!$resql) {
133 $error++;
134 $db->rollback();
135 setEventMessages($db->lasterror(), null, 'errors');
136 break;
137 }
138 }
139
140 if (!$error) {
141 $db->commit();
142 setEventMessages($langs->trans("SelectedLogsDeleted"), null, 'mesgs');
143 } else {
144 $db->rollback();
145 }
146
147 $action = 'list';
148 $massaction = '';
149}
150
151// Clear filter action
152if (GETPOST('button_removefilter', 'alpha') || GETPOST('button_removefilter_x', 'alpha')) {
153 $search_date_start = '';
154 $search_date_end = '';
155 $search_user = '';
156 $search_query = '';
157 $search_tool = '';
158 $search_provider = '';
159 $search_time_min = '';
160 $search_time_max = '';
161 $search_status = '';
162 // Reset page
163 $page = 0;
164}
165
166
167/*
168 * View
169 */
170
171// Initialize array of search criteria for the view
172$param = '';
173if ($contextpage != $_SERVER["PHP_SELF"]) {
174 $param .= '&contextpage='.urlencode($contextpage);
175}
176if ($limit > 0 && $limit != $conf->liste_limit) {
177 $param .= '&limit='.((int) $limit);
178}
179foreach ($search_array as $key => $val) {
180 if (!empty($val) || $val === '0') {
181 $param .= '&' . urlencode($key) . '=' . urlencode($val);
182 }
183}
184
185llxHeader('', $langs->trans("AIRequestLogs"), '');
186
187
188// Build WHERE clause
189$where = array();
190
191$where[] = "l.entity IN (" . getEntity('airequestlog') . ")";
192
193if ($search_date_start) {
194 $where[] = "l.date_request >= '" . $db->escape(date('Y-m-d H:i:s', $search_date_start)) . "'";
195}
196if ($search_date_end) {
197 $where[] = "l.date_request <= '" . $db->escape(date('Y-m-d H:i:s', $search_date_end)) . "'";
198}
199if ($search_user) {
200 $where[] = "u.login LIKE '%" . $db->escape($search_user) . "%'";
201}
202if ($search_query) {
203 $where[] = "l.query_text LIKE '%" . $db->escape($search_query) . "%'";
204}
205if ($search_tool) {
206 $where[] = "l.tool_name LIKE '%" . $db->escape($search_tool) . "%'";
207}
208if ($search_provider) {
209 $where[] = "l.provider LIKE '%" . $db->escape($search_provider) . "%'";
210}
211if ($search_time_min) {
212 $where[] = "l.execution_time >= " . floatval($search_time_min);
213}
214if ($search_time_max) {
215 $where[] = "l.execution_time <= " . floatval($search_time_max);
216}
217if ($search_status) {
218 $where[] = "l.status = '" . $db->escape($search_status) . "'";
219}
220
221$whereSQL = '';
222if (!empty($where)) {
223 $whereSQL = ' WHERE ' . implode(' AND ', $where);
224}
225
226// Get total count for pagination
227$sqlCount = "SELECT COUNT(l.rowid) as total
228 FROM " . MAIN_DB_PREFIX . "ai_request_log as l
229 LEFT JOIN " . MAIN_DB_PREFIX . "user as u ON l.fk_user = u.rowid";
230$sqlCount .= $whereSQL;
231$resqlCount = $db->query($sqlCount);
232$totalRecords = $resqlCount ? $db->fetch_object($resqlCount)->total : 0;
233
234// Token/cost totals over the SAME filter (not only the displayed page),
235// grouped by model so the cost uses each model's own configured price.
236$sqlSum = "SELECT l.model, SUM(l.tokens_input) as tin, SUM(l.tokens_output) as tout
237 FROM " . MAIN_DB_PREFIX . "ai_request_log as l
238 LEFT JOIN " . MAIN_DB_PREFIX . "user as u ON l.fk_user = u.rowid";
239$sqlSum .= $whereSQL;
240$sqlSum .= " GROUP BY l.model";
241$resqlSum = $db->query($sqlSum);
242
252function aiModelPrice($model)
253{
254 static $prices = null;
255 if ($prices === null) {
256 $prices = json_decode(getDolGlobalString('AI_MODEL_PRICES_JSON', '{}'), true);
257 if (!is_array($prices)) {
258 $prices = array();
259 }
260 }
261 $entry = $prices[$model] ?? ($prices['default'] ?? null);
262 return (is_array($entry) && isset($entry['in'], $entry['out'])) ? array('in' => (float) $entry['in'], 'out' => (float) $entry['out']) : null;
263}
264
265
266$sql = "SELECT l.rowid, u.login, l.date_request, l.query_text, l.tool_name, l.provider, l.execution_time, l.status, l.error_msg, l.raw_request_payload, l.raw_response_payload, l.tokens_input, l.tokens_output, l.model
267 FROM " . MAIN_DB_PREFIX . "ai_request_log as l
268 LEFT JOIN " . MAIN_DB_PREFIX . "user as u ON l.fk_user = u.rowid";
269$sql .= $whereSQL;
270$sql .= $db->order($sortfield, $sortorder);
271$sql .= $db->plimit($limit, $offset);
272
273$resql = $db->query($sql);
274$num = $db->num_rows($resql);
275
276// Create object for list
277$object = new stdClass();
278$object->total = $totalRecords;
279
280$title = $langs->trans("AIRequestLogs");
281print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $totalRecords, 'title_ai', 0, '', '', $limit, 1, 0, 0, '');
282
283// Filtered totals: tokens and, when prices are configured, the exact cost
284// (each model's tokens priced with its own AI_MODEL_PRICES_JSON entry).
285if ($resqlSum) {
286 $totIn = 0;
287 $totOut = 0;
288 $totCost = 0.0;
289 $costComplete = true;
290 while ($sumrow = $db->fetch_object($resqlSum)) {
291 $totIn += (int) $sumrow->tin;
292 $totOut += (int) $sumrow->tout;
293 $price = aiModelPrice((string) $sumrow->model);
294 if ($price) {
295 $totCost += ((int) $sumrow->tin * $price['in'] + (int) $sumrow->tout * $price['out']) / 1000000;
296 } elseif ((int) $sumrow->tin + (int) $sumrow->tout > 0) {
297 $costComplete = false; // tokens exist for a model without a configured price
298 }
299 }
300 if ($totIn + $totOut > 0) {
301 print '<div class="opacitymedium paddingbottom">';
302 print $langs->trans("Total").': '.number_format($totIn, 0, '', ' ').' in / '.number_format($totOut, 0, '', ' ').' out';
303 print ' &nbsp;&mdash;&nbsp; '.price($totCost, 0, $langs, 1, -1, 4).($costComplete ? '' : ' (+ '.$langs->trans("Unknown").')');
304 print ' &nbsp;<span title="AI_MODEL_PRICES_JSON, price per 1M tokens, e.g. {&quot;gemini-2.5-flash&quot;:{&quot;in&quot;:0.30,&quot;out&quot;:2.50},&quot;default&quot;:{&quot;in&quot;:1,&quot;out&quot;:3}}">'.img_info('').'</span>';
305 print '</div>';
306 }
307}
308
309print '<form method="POST" action="' . $_SERVER["PHP_SELF"] . '" name="limitform">';
310print '<input type="hidden" name="token" value="' . newToken() . '">';
311print '<input type="hidden" name="action" value="list">';
312
313// Add all search parameters to preserve them when changing the limit
314foreach ($search_array as $key => $val) {
315 if (!empty($val) || $val === '0') {
316 print '<input type="hidden" name="' . $key . '" value="' . dol_escape_htmltag($val) . '">';
317 }
318}
319
320print '<div class="div-table-responsive-no-min">';
321print '<table class="noborder" width="100%">';
322print '<tr>';
323print '<td class="right">';
324print $langs->trans("Show") . ': ';
325print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
326print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
327print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
328print '<input type="hidden" name="page" value="'.$page.'">';
329
330// Create array of options for limit
331 $arrayoflimit = array(5, 10, 20, 50, 100, 500, 1000);
332print '<select class="flat" name="limit" onchange="this.form.submit()">';
333foreach ($arrayoflimit as $val) {
334 print '<option value="'.$val.'"';
335 if ($limit == $val) print ' selected';
336 print '>'.$val.'</option>';
337}
338
339print '</select>';
340print ' ' . $langs->trans("Entries");
341print '</td>';
342print '</tr>';
343print '</table>';
344print '</div>';
345print '</form>';
346
347// Display form for filters
348print '<form method="POST" action="' . $_SERVER["PHP_SELF"] . '" name="search_form">';
349print '<input type="hidden" name="token" value="' . newToken() . '">';
350print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
351print '<input type="hidden" name="sortfield" value="' . $sortfield . '">';
352print '<input type="hidden" name="sortorder" value="' . $sortorder . '">';
353print '<input type="hidden" name="page" value="' . $page . '">';
354print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
355print '<input type="hidden" name="page_y" value="">';
356print '<input type="hidden" name="mode" value="'.$mode.'">';
357
358print '<div class="div-table-responsive">';
359print '<table class="tagtable liste listwithfilterbefore">'."\n";
360
361// Fields title
362print '<tr class="liste_titre">';
363print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "l.date_request", "", $param, '', $sortfield, $sortorder);
364print_liste_field_titre("User", $_SERVER["PHP_SELF"], "u.login", "", $param, '', $sortfield, $sortorder);
365print_liste_field_titre("Query", $_SERVER["PHP_SELF"], "l.query_text", "", $param, '', $sortfield, $sortorder);
366print_liste_field_titre("MCPTool", $_SERVER["PHP_SELF"], "l.tool_name", "", $param, '', $sortfield, $sortorder);
367print_liste_field_titre("Provider", $_SERVER["PHP_SELF"], "l.provider", "", $param, '', $sortfield, $sortorder);
368print_liste_field_titre("Time", $_SERVER["PHP_SELF"], "l.execution_time", "", $param, 'align="center"', $sortfield, $sortorder);
369print_liste_field_titre("Tokens", $_SERVER["PHP_SELF"], "l.tokens_output", "", $param, 'align="center" title="input / output (thinking included)"', $sortfield, $sortorder);
370print_liste_field_titre("Cost", $_SERVER["PHP_SELF"], "", "", $param, 'align="center"');
371print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "l.status", "", $param, 'align="center"', $sortfield, $sortorder);
372print_liste_field_titre('', $_SERVER["PHP_SELF"], "", "", $param, 'align="center"');
373print '</tr>';
374// Search row
375print '<tr class="liste_titre_filter">';
376// Date search
377print '<td class="liste_titre">';
378print $form->selectDate($search_date_start, 'search_date_start', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("From"));
379print ' - ';
380print $form->selectDate($search_date_end, 'search_date_end', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("To"));
381print '</td>';
382// User search
383print '<td class="liste_titre"><input type="text" name="search_user" value="' . dol_escape_htmltag($search_user) . '" class="maxwidth100"></td>';
384// Query search
385print '<td class="liste_titre"><input type="text" name="search_query" value="' . dol_escape_htmltag($search_query) . '" class="maxwidth150"></td>';
386// Tool search
387print '<td class="liste_titre"><input type="text" name="search_tool" value="' . dol_escape_htmltag($search_tool) . '" class="maxwidth100"></td>';
388// Provider search
389print '<td class="liste_titre"><input type="text" name="search_provider" value="' . dol_escape_htmltag($search_provider) . '" class="maxwidth100"></td>';
390// Time search
391print '<td class="liste_titre center">';
392print '<input type="text" name="search_time_min" value="' . dol_escape_htmltag($search_time_min) . '" size="3" placeholder="' . dol_escape_htmltag($langs->trans('Min')) . '">';
393print '<input type="text" name="search_time_max" value="' . dol_escape_htmltag($search_time_max) . '" size="3" placeholder="' . dol_escape_htmltag($langs->trans('Max')) . '">';
394// Tokens and Cost columns carry no filter
395print '<td class="liste_titre"></td>';
396print '<td class="liste_titre"></td>';
397// Status search
398print '<td class="liste_titre center">';
399$status_options = array('' => $langs->trans("All"), 'success' => $langs->trans("Success"), 'confirm' => $langs->trans("Confirm"), 'error' => $langs->trans("Error"));
400print $form->selectarray('search_status', $status_options, $search_status, 0, 0, 0, '', 1); // @phan-suppress-current-line PhanPluginSuspiciousParamOrder
401print '</td>';
402// Search buttons
403print '<td class="liste_titre center">';
404$searchpicto = img_picto($langs->trans("Search"), 'search.png', '', 0, 1);
405print '<input type="image" class="liste_titre" name="button_search" src="' . $searchpicto . '" value="' . dol_escape_htmltag($langs->trans("Search")) . '" title="' . dol_escape_htmltag($langs->trans("Search")) . '">';
406$clearpicto = img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', 0, 1);
407print '<input type="image" class="liste_titre" name="button_removefilter" src="' . $clearpicto . '" value="' . dol_escape_htmltag($langs->trans("RemoveFilter")) . '" title="' . dol_escape_htmltag($langs->trans("RemoveFilter")) . '">';
408print '</td>';
409print '</tr>';
410
411// Mass action buttons
412print '<tr class="liste_titre">';
413print '<td class="liste_titre" colspan="10">';
414print '<div class="center">';
415print '<div class="inline-block divButAction"><a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=purge&token='.newToken().'" onclick="return confirm(\''.$langs->trans("ConfirmDeleteAllLogs").'\');">'.$langs->trans("ClearAllLogs").'</a></div>';
416print '</div>';
417print '</td>';
418print '</tr>';
419
420if ($resql && $num > 0) {
421 $i = 0;
422 while ($obj = $db->fetch_object($resql)) {
423 print '<tr class="oddeven">';
424
425 // Date
426 print '<td>' . dol_print_date($db->jdate($obj->date_request), 'dayhour') . '</td>';
427
428 // User
429 print '<td>' . ($obj->login ? dol_escape_htmltag($obj->login) : $langs->trans("Unknown")) . '</td>';
430
431 // Query - properly escaped
432 $shortQuery = dol_trunc($obj->query_text, 60);
433 print '<td title="' . dol_escape_htmltag($obj->query_text) . '">' . dol_escape_htmltag($shortQuery) . '</td>';
434
435 // Tool
436 print '<td>' . dol_escape_htmltag($obj->tool_name) . '</td>';
437
438 // Provider
439 print '<td>' . dol_escape_htmltag($obj->provider) . '</td>';
440
441 // Time
442 $timeColor = ($obj->execution_time > 5) ? 'color:red;' : '';
443 print '<td style="' . $timeColor . '" align="center">' . round($obj->execution_time, 2) . 's</td>';
444
445 // Tokens (input / output) and cost, when the provider reported usage
446 if (isset($obj->tokens_input) || isset($obj->tokens_output)) {
447 print '<td align="center" class="nowrap" title="' . dol_escape_htmltag((string) $obj->model) . '">' . ((int) $obj->tokens_input) . ' / ' . ((int) $obj->tokens_output) . '</td>';
448 $price = aiModelPrice((string) $obj->model);
449 if ($price) {
450 $cost = ((int) $obj->tokens_input * $price['in'] + (int) $obj->tokens_output * $price['out']) / 1000000;
451 print '<td align="center" class="nowrap">' . price($cost, 0, $langs, 1, -1, 4) . '</td>';
452 } else {
453 print '<td align="center" class="opacitymedium">-</td>';
454 }
455 } else {
456 print '<td align="center" class="opacitymedium">-</td><td align="center" class="opacitymedium">-</td>';
457 }
458
459 // Status
460 $badge = 'badge-status0';
461 if ($obj->status == $langs->transnoentitiesnoconv("Success")) {
462 $badge = 'badge-status4'; // Green
463 }
464 if ($obj->status == $langs->transnoentitiesnoconv("Confirm")) {
465 $badge = 'badge-status3'; // Yellow
466 }
467 if ($obj->status == $langs->transnoentitiesnoconv('Error')) {
468 $badge = 'badge-status8'; // Red
469 }
470 print '<td align="center"><span class="badge ' . $badge . '">' . dol_escape_htmltag($obj->status) . '</span></td>';
471
472 // Details Button (Triggers Modal)
473 // We embed data attributes securely with proper UTF-8 handling
474 $reqSafe = base64_encode($obj->raw_request_payload);
475 $resSafe = base64_encode($obj->raw_response_payload);
476 $errSafe = base64_encode($obj->error_msg);
477
478 print '<td align="center">';
479 print '<a href="#" class="button button-small" onclick="openLogModal(this)"
480 data-req="' . dol_escape_htmltag($reqSafe) . '"
481 data-res="' . dol_escape_htmltag($resSafe) . '"
482 data-err="' . dol_escape_htmltag($errSafe) . '">';
483 print '<span class="fa fa-search-plus"></span> ' . $langs->trans("View");
484 print '</a>';
485 print '</td>';
486
487 print '</tr>';
488 $i++;
489 }
490} else {
491 $colspan = 10;
492 print '<tr><td colspan="' . $colspan . '" class="opacitymedium">' . $langs->trans("NoLogsFound");
493 if (!empty($where)) {
494 print ' ' . $langs->trans("MatchingSearchCriteria");
495 }
496 print '. ' . $langs->trans("TryAskingAI") . '.</td></tr>';
497}
498print '</table></div>';
499
500print '</form>';
501
502// --- MODAL HTML & JS ---
503?>
504<div id="logModal" style="display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; overflow:auto; background-color:rgba(0,0,0,0.5);">
505 <div style="background-color:#fff; margin:5% auto; padding:20px; border:1px solid #888; width:80%; max-width:900px; border-radius:8px; box-shadow:0 4px 8px rgba(0,0,0,0.2);">
506 <span style="float:right; font-size:28px; font-weight:bold; cursor:pointer;" onclick="document.getElementById('logModal').style.display='none'">&times;</span>
507 <h2><?php echo $langs->trans("LogDetails"); ?></h2>
508
509 <h3><?php echo $langs->trans("ErrorWarning"); ?></h3>
510 <div id="modalError" style="background:#fff0f0; border:1px solid #ffcdd2; color:#d32f2f; padding:10px; border-radius:4px; display:none;"></div>
511
512 <div style="display:flex; gap:20px; margin-top:15px;">
513 <div style="flex:1;">
514 <h3><?php echo $langs->trans("RequestPayload"); ?></h3>
515 <textarea id="modalReq" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;" readonly></textarea>
516 </div>
517 <div style="flex:1;">
518 <h3><?php echo $langs->trans("ResponsePayload"); ?></h3>
519 <textarea id="modalRes" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;" readonly></textarea>
520 </div>
521 </div>
522
523 <div style="text-align:right; margin-top:15px;">
524 <button class="button" onclick="document.getElementById('logModal').style.display='none'"><?php echo $langs->trans("Close"); ?></button>
525 </div>
526 </div>
527</div>
528
529<script>
530// UTF-8 safe base64 decoding function
531function base64ToUtf8(str) {
532 // Going backwards: from bytestream, to percent-encoding, to original string.
533 return decodeURIComponent(atob(str).split('').map(function(c) {
534 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
535 }).join(''));
536}
537
538// Function to decode Unicode escape sequences in JSON strings
539function decodeUnicodeEscapes(str) {
540 // First, try to parse as JSON to handle escaped Unicode properly
541 try {
542 // If it's a JSON string, parse and stringify to decode escapes
543 const parsed = JSON.parse(str);
544 return JSON.stringify(parsed, null, 2);
545 } catch (e) {
546 // If not valid JSON, try to decode Unicode escapes in the string
547 return str.replace(/\\u([0-9a-fA-F]{4})/g, function(match, p1) {
548 return String.fromCharCode(parseInt(p1, 16));
549 });
550 }
551}
552
553// Sanitize HTML to prevent XSS
554function escapeHtml(text) {
555 const div = document.createElement('div');
556 div.textContent = text;
557 return div.innerHTML;
558}
559
560function openLogModal(btn) {
561 // Decode Base64 safely with UTF-8 support
562 const reqBase64 = btn.getAttribute('data-req') || '';
563 const resBase64 = btn.getAttribute('data-res') || '';
564 const errBase64 = btn.getAttribute('data-err') || '';
565
566 let req = '';
567 let res = '';
568 let err = '';
569
570 try {
571 // Decode Unicode escape sequences
572 if (reqBase64) {
573 req = base64ToUtf8(reqBase64);
574 req = decodeUnicodeEscapes(req);
575 }
576 if (resBase64) {
577 res = base64ToUtf8(resBase64);
578 res = decodeUnicodeEscapes(res);
579 }
580 if (errBase64) {
581 err = base64ToUtf8(errBase64);
582 err = decodeUnicodeEscapes(err);
583 }
584 } catch (e) {
585 console.error('Error decoding base64:', e);
586 // Fallback to regular atob if UTF-8 decoding fails
587 req = reqBase64 ? atob(reqBase64) : '';
588 res = resBase64 ? atob(resBase64) : '';
589 err = errBase64 ? atob(errBase64) : '';
590 }
591
592 // Sanitize content before setting it
593 document.getElementById('modalReq').value = req || '(<?php echo $langs->trans("NoRequestPayload"); ?>)';
594 document.getElementById('modalRes').value = res || '(<?php echo $langs->trans("NoResponsePayload"); ?>)';
595
596 const errDiv = document.getElementById('modalError');
597 if (err) {
598 errDiv.innerText = err;
599 errDiv.style.display = 'block';
600 } else {
601 errDiv.style.display = 'none';
602 }
603
604 document.getElementById('logModal').style.display = 'block';
605}
606</script>
607
608<?php
609llxFooter();
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
if(!defined('NOREQUIRESOC')) if(!defined( 'NOREQUIRETRAN')) if(!defined('NOTOKENRENEWAL')) if(!defined( 'NOREQUIREMENU')) if(!defined('NOREQUIREHTML')) if(!defined( 'NOREQUIREAJAX')) llxHeader($head='', $title='', $help_url='', $target='', $disablejs=0, $disablehead=0, $arrayofjs='', $arrayofcss='', $morequerystring='', $morecssonbody='', $replacemainareaby='', $disablenofollow=0, $disablenoindex=0)
Empty header.
Definition wrapper.php:73
print $object position
Definition edit.php:206
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
newToken()
Return the value of token currently saved into session with name 'newtoken'.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
GETPOSTINT($paramname, $method=0, $nodefault=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
multi select button
0 = Do not include form tag and submit button -1 = Do not include form tag but include submit button
print_liste_field_titre($name, $file="", $field="", $begin="", $param="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $tooltip="", $forcenowrapcolumntitle=0)
Show title line of an array.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Definition html.lib.php:181
aiModelPrice($model)
Price (in/out, per 1M tokens, in the admin's own currency and contract) for a model,...
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
Definition receipt.php:492
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.