25require
'../../main.inc.php';
34require_once DOL_DOCUMENT_ROOT .
'/core/lib/admin.lib.php';
35require_once DOL_DOCUMENT_ROOT .
'/core/lib/date.lib.php';
36require_once DOL_DOCUMENT_ROOT .
'/core/class/html.form.class.php';
39$langs->loadLangs(array(
"admin",
"other"));
42$action =
GETPOST(
'action',
'aZ09');
43$massaction =
GETPOST(
'massaction',
'alpha');
44$confirm =
GETPOST(
'confirm',
'alpha');
45$toselect =
GETPOST(
'toselect',
'array');
46$contextpage =
GETPOST(
'contextpage',
'aZ') ?
GETPOST(
'contextpage',
'aZ') :
'ailoglist';
47$optioncss =
GETPOST(
'optioncss',
'alpha');
48$mode =
GETPOST(
'mode',
'alpha');
53$search_user =
GETPOST(
'search_user',
'alpha');
54$search_query =
GETPOST(
'search_query',
'alpha');
55$search_tool =
GETPOST(
'search_tool',
'alpha');
56$search_provider =
GETPOST(
'search_provider',
'alpha');
57$search_time_min =
GETPOST(
'search_time_min',
'alpha');
58$search_time_max =
GETPOST(
'search_time_max',
'alpha');
59$search_status =
GETPOST(
'search_status',
'alpha');
63$sortfield =
GETPOST(
'sortfield',
'aZ09comma');
64$sortorder =
GETPOST(
'sortorder',
'aZ09comma');
66if (empty($page) || $page == -1) {
69$offset = $limit * $page;
70if (!$sortfield) $sortfield =
"l.date_request";
71if (!$sortorder) $sortorder =
"DESC";
75 'search_date_start' => $search_date_start,
76 'search_date_end' => $search_date_end,
77 'search_user' => $search_user,
78 'search_query' => $search_query,
79 'search_tool' => $search_tool,
80 'search_provider' => $search_provider,
81 'search_time_min' => $search_time_min,
82 'search_time_max' => $search_time_max,
83 'search_status' => $search_status
100if ($action ==
'purge' && $confirm ==
'yes') {
103 $sql =
"DELETE FROM " . MAIN_DB_PREFIX .
"ai_request_log";
104 $sql .=
" WHERE entity IN (" .
getEntity(
'airequestlog') .
")";
106 $resql =
$db->query($sql);
109 $nbDeleted =
$db->affected_rows($resql);
111 setEventMessages($langs->trans(
"LogsCleared") .
" (" . $nbDeleted .
")",
null,
'mesgs');
117 header(
'Location: ' . $_SERVER[
"PHP_SELF"]);
122if ($massaction ==
'purge' && !empty($toselect) && is_array($toselect)) {
125 foreach ($toselect as
$id) {
126 $sql =
"DELETE FROM " . MAIN_DB_PREFIX .
"ai_request_log";
127 $sql .=
" WHERE rowid = " . ((int)
$id);
128 $sql .=
" AND entity IN (" .
getEntity(
'airequestlog') .
")";
130 $resql =
$db->query($sql);
151if (
GETPOST(
'button_removefilter',
'alpha') ||
GETPOST(
'button_removefilter_x',
'alpha')) {
152 $search_date_start =
'';
153 $search_date_end =
'';
157 $search_provider =
'';
158 $search_time_min =
'';
159 $search_time_max =
'';
172if ($contextpage != $_SERVER[
"PHP_SELF"]) {
173 $param .=
'&contextpage='.urlencode($contextpage);
175if ($limit > 0 && $limit !=
$conf->liste_limit) {
176 $param .=
'&limit='.((int) $limit);
178foreach ($search_array as $key => $val) {
179 if (!empty($val) || $val ===
'0') {
180 $param .=
'&' . urlencode($key) .
'=' . urlencode($val);
184llxHeader(
'', $langs->trans(
"AIRequestLogs"),
'');
190$where[] =
"l.entity IN (" .
getEntity(
'airequestlog') .
")";
192if ($search_date_start) {
193 $where[] =
"l.date_request >= '" .
$db->escape(
date(
'Y-m-d H:i:s', $search_date_start)) .
"'";
195if ($search_date_end) {
196 $where[] =
"l.date_request <= '" .
$db->escape(
date(
'Y-m-d H:i:s', $search_date_end)) .
"'";
199 $where[] =
"u.login LIKE '%" .
$db->escape($search_user) .
"%'";
202 $where[] =
"l.query_text LIKE '%" .
$db->escape($search_query) .
"%'";
205 $where[] =
"l.tool_name LIKE '%" .
$db->escape($search_tool) .
"%'";
207if ($search_provider) {
208 $where[] =
"l.provider LIKE '%" .
$db->escape($search_provider) .
"%'";
210if ($search_time_min) {
211 $where[] =
"l.execution_time >= " . floatval($search_time_min);
213if ($search_time_max) {
214 $where[] =
"l.execution_time <= " . floatval($search_time_max);
217 $where[] =
"l.status = '" .
$db->escape($search_status) .
"'";
222 $whereSQL =
' WHERE ' . implode(
' AND ', $where);
226$sqlCount =
"SELECT COUNT(l.rowid) as total
227 FROM " . MAIN_DB_PREFIX .
"ai_request_log as l
228 LEFT JOIN " . MAIN_DB_PREFIX .
"user as u ON l.fk_user = u.rowid";
229$sqlCount .= $whereSQL;
230$resqlCount =
$db->query($sqlCount);
231$totalRecords = $resqlCount ?
$db->fetch_object($resqlCount)->total : 0;
234$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
235 FROM " . MAIN_DB_PREFIX .
"ai_request_log as l
236 LEFT JOIN " . MAIN_DB_PREFIX .
"user as u ON l.fk_user = u.rowid";
238$sql .=
$db->order($sortfield, $sortorder);
239$sql .=
$db->plimit($limit, $offset);
241$resql =
$db->query($sql);
242$num =
$db->num_rows($resql);
248$title = $langs->trans(
"AIRequestLogs");
249print_barre_liste($title, $page, $_SERVER[
"PHP_SELF"], $param, $sortfield, $sortorder,
'', $num, $totalRecords,
'title_ai', 0,
'',
'', $limit, 1, 0, 0,
'');
251print
'<form method="POST" action="' . $_SERVER[
"PHP_SELF"] .
'" name="limitform">';
252print
'<input type="hidden" name="token" value="' . newToken() .
'">';
253print
'<input type="hidden" name="action" value="list">';
256foreach ($search_array as $key => $val) {
257 if (!empty($val) || $val ===
'0') {
258 print
'<input type="hidden" name="' . $key .
'" value="' .
dol_escape_htmltag($val) .
'">';
262print
'<div class="div-table-responsive-no-min">';
263print
'<table class="noborder" width="100%">';
265print
'<td class="right">';
266print $langs->trans(
"Show") .
': ';
267print
'<input type="hidden" name="contextpage" value="'.$contextpage.
'">';
268print
'<input type="hidden" name="sortfield" value="'.$sortfield.
'">';
269print
'<input type="hidden" name="sortorder" value="'.$sortorder.
'">';
270print
'<input type="hidden" name="page" value="'.$page.
'">';
273 $arrayoflimit = array(5, 10, 20, 50, 100, 500, 1000);
274print
'<select class="flat" name="limit" onchange="this.form.submit()">';
275foreach ($arrayoflimit as $val) {
276 print
'<option value="'.$val.
'"';
277 if ($limit == $val) print
' selected';
278 print
'>'.$val.
'</option>';
282print
' ' . $langs->trans(
"Entries");
290print
'<form method="POST" action="' . $_SERVER[
"PHP_SELF"] .
'" name="search_form">';
291print
'<input type="hidden" name="token" value="' . newToken() .
'">';
292print
'<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
293print
'<input type="hidden" name="sortfield" value="' . $sortfield .
'">';
294print
'<input type="hidden" name="sortorder" value="' . $sortorder .
'">';
295print
'<input type="hidden" name="page" value="' . $page .
'">';
296print
'<input type="hidden" name="contextpage" value="'.$contextpage.
'">';
297print
'<input type="hidden" name="page_y" value="">';
298print
'<input type="hidden" name="mode" value="'.$mode.
'">';
300print
'<div class="div-table-responsive">';
301print
'<table class="tagtable liste listwithfilterbefore">'.
"\n";
304print
'<tr class="liste_titre">';
305print_liste_field_titre(
"Date", $_SERVER[
"PHP_SELF"],
"l.date_request",
"", $param,
'', $sortfield, $sortorder);
308print_liste_field_titre(
"MCPTool", $_SERVER[
"PHP_SELF"],
"l.tool_name",
"", $param,
'', $sortfield, $sortorder);
309print_liste_field_titre(
"Provider", $_SERVER[
"PHP_SELF"],
"l.provider",
"", $param,
'', $sortfield, $sortorder);
310print_liste_field_titre(
"Time", $_SERVER[
"PHP_SELF"],
"l.execution_time",
"", $param,
'align="center"', $sortfield, $sortorder);
311print_liste_field_titre(
"Status", $_SERVER[
"PHP_SELF"],
"l.status",
"", $param,
'align="center"', $sortfield, $sortorder);
315print
'<tr class="liste_titre_filter">';
317print
'<td class="liste_titre">';
318print $form->selectDate($search_date_start,
'search_date_start', 0, 0, 1,
'', 1, 0, 0,
'',
'',
'',
'', 1,
'', $langs->trans(
"From"));
320print $form->selectDate($search_date_end,
'search_date_end', 0, 0, 1,
'', 1, 0, 0,
'',
'',
'',
'', 1,
'', $langs->trans(
"To"));
323print
'<td class="liste_titre"><input type="text" name="search_user" value="' .
dol_escape_htmltag($search_user) .
'" class="maxwidth100"></td>';
325print
'<td class="liste_titre"><input type="text" name="search_query" value="' .
dol_escape_htmltag($search_query) .
'" class="maxwidth150"></td>';
327print
'<td class="liste_titre"><input type="text" name="search_tool" value="' .
dol_escape_htmltag($search_tool) .
'" class="maxwidth100"></td>';
329print
'<td class="liste_titre"><input type="text" name="search_provider" value="' .
dol_escape_htmltag($search_provider) .
'" class="maxwidth100"></td>';
331print
'<td class="liste_titre center">';
335print
'<td class="liste_titre center">';
336$status_options = array(
'' => $langs->trans(
"All"),
'success' => $langs->trans(
"Success"),
'confirm' => $langs->trans(
"Confirm"),
'error' => $langs->trans(
"Error"));
337print $form->selectarray(
'search_status', $status_options, $search_status, 0, 0, 0,
'', 1);
340print
'<td class="liste_titre center">';
341$searchpicto =
img_picto($langs->trans(
"Search"),
'search.png',
'', 0, 1);
342print
'<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")) .
'">';
343$clearpicto =
img_picto($langs->trans(
"RemoveFilter"),
'searchclear.png',
'', 0, 1);
344print
'<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")) .
'">';
349print
'<tr class="liste_titre">';
350print
'<td class="liste_titre" colspan="8">';
351print
'<div class="center">';
352print
'<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>';
357if ($resql && $num > 0) {
359 while ($obj = $db->fetch_object($resql)) {
360 print '<tr class="oddeven
">';
363 print '<td>' . dol_print_date($db->jdate($obj->date_request), 'dayhour') . '</td>';
366 print '<td>' . ($obj->login ? dol_escape_htmltag($obj->login) : $langs->trans("Unknown
")) . '</td>';
368 // Query - properly escaped
369 $shortQuery = dol_trunc($obj->query_text, 60);
370 print '<td title="' . dol_escape_htmltag($obj->query_text) . '">' . dol_escape_htmltag($shortQuery) . '</td>';
373 print '<td>' . dol_escape_htmltag($obj->tool_name) . '</td>';
376 print '<td>' . dol_escape_htmltag($obj->provider) . '</td>';
379 $timeColor = ($obj->execution_time > 5) ? 'color:red;' : '';
380 print '<td style="' . $timeColor . '" align="center
">' . round($obj->execution_time, 2) . 's</td>';
383 $badge = 'badge-status0';
384 if ($obj->status == $langs->transnoentitiesnoconv("Success
")) {
385 $badge = 'badge-status4'; // Green
387 if ($obj->status == $langs->transnoentitiesnoconv("Confirm
")) {
388 $badge = 'badge-status3'; // Yellow
390 if ($obj->status == $langs->transnoentitiesnoconv('Error')) {
391 $badge = 'badge-status8'; // Red
393 print '<td align="center
"><span class="badge
' . $badge . '">' . dol_escape_htmltag($obj->status) . '</span></td>';
395 // Details Button (Triggers Modal)
396 // We embed data attributes securely with proper UTF-8 handling
397 $reqSafe = base64_encode($obj->raw_request_payload);
398 $resSafe = base64_encode($obj->raw_response_payload);
399 $errSafe = base64_encode($obj->error_msg);
401 print '<td align="center
">';
402 print '<a href="#
" class="button button-small
" onclick="openLogModal(
this)
"
403 data-req="' . dol_escape_htmltag($reqSafe) . '"
404 data-res="' . dol_escape_htmltag($resSafe) . '"
405 data-err="' . dol_escape_htmltag($errSafe) . '">';
406 print '<span class="fa fa-search-plus
"></span> ' . $langs->trans("View
");
415 print '<tr><td colspan="' . $colspan . '" class="opacitymedium
">' . $langs->trans("NoLogsFound
");
416 if (!empty($where)) {
417 print ' ' . $langs->trans("MatchingSearchCriteria
");
419 print '. ' . $langs->trans("TryAskingAI
") . '.</td></tr>';
421print '</table></div>';
425// --- MODAL HTML & JS ---
427<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);
">
428 <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);
">
429 <span style="float:right; font-size:28px; font-weight:bold; cursor:pointer;
" onclick="document.getElementById(
'logModal').style.display=
'none'">×</span>
430 <h2><?php echo $langs->trans("LogDetails
"); ?></h2>
432 <h3><?php echo $langs->trans("ErrorWarning
"); ?></h3>
433 <div id="modalError
" style="background:#fff0f0; border:1px solid #ffcdd2; color:#d32f2f; padding:10px; border-radius:4px; display:none;
"></div>
435 <div style="display:flex; gap:20px; margin-top:15px;
">
436 <div style="flex:1;
">
437 <h3><?php echo $langs->trans("RequestPayload
"); ?></h3>
438 <textarea id="modalReq
" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;
" readonly></textarea>
440 <div style="flex:1;
">
441 <h3><?php echo $langs->trans("ResponsePayload
"); ?></h3>
442 <textarea id="modalRes
" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;
" readonly></textarea>
446 <div style="text-align:right; margin-top:15px;
">
447 <button class="button" onclick="document.getElementById(
'logModal').style.display=
'none'"><?php echo $langs->trans("Close
"); ?></button>
453// UTF-8 safe base64 decoding function
454function base64ToUtf8(str) {
455 // Going backwards: from bytestream, to percent-encoding, to original string.
456 return decodeURIComponent(atob(str).split('').map(function(c) {
457 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
461// Function to decode Unicode escape sequences in JSON strings
462function decodeUnicodeEscapes(str) {
463 // First, try to parse as JSON to handle escaped Unicode properly
465 // If it's a JSON string, parse and stringify to decode escapes
466 const parsed = JSON.parse(str);
467 return JSON.stringify(parsed, null, 2);
469 // If not valid JSON, try to decode Unicode escapes in the string
470 return str.replace(/\\u([0-9a-fA-F]{4})/g, function(match, p1) {
471 return String.fromCharCode(parseInt(p1, 16));
476// Sanitize HTML to prevent XSS
477function escapeHtml(text) {
478 const div = document.createElement('div');
479 div.textContent = text;
480 return div.innerHTML;
483function openLogModal(btn) {
484 // Decode Base64 safely with UTF-8 support
485 const reqBase64 = btn.getAttribute('data-req') || '';
486 const resBase64 = btn.getAttribute('data-res') || '';
487 const errBase64 = btn.getAttribute('data-err') || '';
494 // Decode Unicode escape sequences
496 req = base64ToUtf8(reqBase64);
497 req = decodeUnicodeEscapes(req);
500 res = base64ToUtf8(resBase64);
501 res = decodeUnicodeEscapes(res);
504 err = base64ToUtf8(errBase64);
505 err = decodeUnicodeEscapes(err);
508 console.error('Error decoding base64:', e);
509 // Fallback to regular atob if UTF-8 decoding fails
510 req = reqBase64 ? atob(reqBase64) : '';
511 res = resBase64 ? atob(resBase64) : '';
512 err = errBase64 ? atob(errBase64) : '';
515 // Sanitize content before setting it
516 document.getElementById('modalReq').value = req || '(<?php echo $langs->trans("NoRequestPayload
"); ?>)';
517 document.getElementById('modalRes').value = res || '(<?php echo $langs->trans("NoResponsePayload
"); ?>)';
519 const errDiv = document.getElementById('modalError');
521 errDiv.innerText = err;
522 errDiv.style.display = 'block';
524 errDiv.style.display = 'none';
527 document.getElementById('logModal').style.display = 'block';
$id
Support class for third parties, contacts, members, users or resources.
if(! $sortfield) if(! $sortorder) $object
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.
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...
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
print_liste_field_titre($name, $file="", $field="", $begin="", $param="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $tooltip="", $forcenowrapcolumntitle=0)
Show title line of an array.
print_barre_liste($title, $page, $file, $options='', $sortfield='', $sortorder='', $morehtmlcenter='', $num=-1, $totalnboflines='', $picto='generic', $pictoisfullpath=0, $morehtmlright='', $morecss='', $limit=-1, $selectlimitsuffix=0, $hidenavigation=0, $pagenavastextinput=0, $morehtmlrightbeforearrow='')
Print a title with navigation controls for pagination.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
GETPOSTINT($paramname, $method=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0)
Return value of a param into GET or POST supervariable.
isModEnabled($module)
Is Dolibarr module enabled.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
multi select button
0 = Do not include form tag and submit button -1 = Do not include form tag but include submit button
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
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.