display attempts to admin
This commit is contained in:
22
css/dataTables.css
Normal file
22
css/dataTables.css
Normal file
File diff suppressed because one or more lines are too long
73
database.php
Normal file
73
database.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
function GetDbConnection(): ?PDO
|
||||||
|
{
|
||||||
|
$host = 'localhost'; // ou l'adresse IP du serveur MariaDB
|
||||||
|
$dbname = 'ldap'; // nom de votre base de donn<6E>es
|
||||||
|
$username = 'root'; // nom d'utilisateur MariaDB
|
||||||
|
$password = '4321'; // mot de passe pour l'utilisateur
|
||||||
|
try {
|
||||||
|
// Cr<43>ation d'une instance PDO pour la connexion <20> la base de donn<6E>es
|
||||||
|
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
|
||||||
|
// Configuration du mode d'erreur de PDO pour les exceptions
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
return $pdo;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Échec de la connexion : " . $e->getMessage();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthAttempt
|
||||||
|
{
|
||||||
|
public string $username;
|
||||||
|
public string $status;
|
||||||
|
public string $timestamp;
|
||||||
|
public string $ip_address;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $username,
|
||||||
|
string $status,
|
||||||
|
string $ip_address,
|
||||||
|
string $timestamp = "",
|
||||||
|
) {
|
||||||
|
$this->username = $username;
|
||||||
|
$this->status = $status;
|
||||||
|
$this->ip_address = $ip_address;
|
||||||
|
$this->timestamp = $timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function InsertLine(AuthAttempt $attempt)
|
||||||
|
{
|
||||||
|
$table_name = "authentification_attempts";
|
||||||
|
$pdo = GetDbConnection();
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO $table_name(`username`, `status`, `ip_address`) VALUES (:user, :status, :ip);");
|
||||||
|
|
||||||
|
$query->bindValue(":user", $attempt->username, PDO::PARAM_STR);
|
||||||
|
$query->bindValue(":status", $attempt->status, PDO::PARAM_STR);
|
||||||
|
$query->bindValue(":ip", $attempt->ip_address, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetLines() : array {
|
||||||
|
$table_name = "authentification_attempts";
|
||||||
|
$pdo = GetDbConnection();
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM $table_name;");
|
||||||
|
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$lines = $query->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach($lines as $line) {
|
||||||
|
array_push($result, new AuthAttempt($line["username"], $line["status"], $line["ip_address"], $line["timestamp"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
15
db_test.php
15
db_test.php
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
$host = 'localhost'; // ou l'adresse IP du serveur MariaDB
|
|
||||||
$dbname = 'mysql'; // nom de votre base de donn<6E>es
|
|
||||||
$username = 'root'; // nom d'utilisateur MariaDB
|
|
||||||
$password = '4321'; // mot de passe pour l'utilisateur
|
|
||||||
try {
|
|
||||||
// Cr<43>ation d'une instance PDO pour la connexion <20> la base de donn<6E>es
|
|
||||||
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
|
|
||||||
// Configuration du mode d'erreur de PDO pour les exceptions
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
echo "Connexion réussie <20> MariaDB avec PDO!";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "<EFBFBD>chec de la connexion : " . $e->getMessage();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
36
js/datatables.min.js
vendored
Normal file
36
js/datatables.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
245
lang/fr.json
Normal file
245
lang/fr.json
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"emptyTable": "Aucune donnée disponible dans le tableau",
|
||||||
|
"loadingRecords": "Chargement...",
|
||||||
|
"processing": "Traitement...",
|
||||||
|
"select": {
|
||||||
|
"rows": {
|
||||||
|
"_": "%d lignes sélectionnées",
|
||||||
|
"1": "1 ligne sélectionnée"
|
||||||
|
},
|
||||||
|
"cells": {
|
||||||
|
"1": "1 cellule sélectionnée",
|
||||||
|
"_": "%d cellules sélectionnées"
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"1": "1 colonne sélectionnée",
|
||||||
|
"_": "%d colonnes sélectionnées"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoFill": {
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"fill": "Remplir toutes les cellules avec <i>%d<\/i>",
|
||||||
|
"fillHorizontal": "Remplir les cellules horizontalement",
|
||||||
|
"fillVertical": "Remplir les cellules verticalement"
|
||||||
|
},
|
||||||
|
"searchBuilder": {
|
||||||
|
"conditions": {
|
||||||
|
"date": {
|
||||||
|
"after": "Après le",
|
||||||
|
"before": "Avant le",
|
||||||
|
"between": "Entre",
|
||||||
|
"empty": "Vide",
|
||||||
|
"not": "Différent de",
|
||||||
|
"notBetween": "Pas entre",
|
||||||
|
"notEmpty": "Non vide",
|
||||||
|
"equals": "Égal à"
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"between": "Entre",
|
||||||
|
"empty": "Vide",
|
||||||
|
"gt": "Supérieur à",
|
||||||
|
"gte": "Supérieur ou égal à",
|
||||||
|
"lt": "Inférieur à",
|
||||||
|
"lte": "Inférieur ou égal à",
|
||||||
|
"not": "Différent de",
|
||||||
|
"notBetween": "Pas entre",
|
||||||
|
"notEmpty": "Non vide",
|
||||||
|
"equals": "Égal à"
|
||||||
|
},
|
||||||
|
"string": {
|
||||||
|
"contains": "Contient",
|
||||||
|
"empty": "Vide",
|
||||||
|
"endsWith": "Se termine par",
|
||||||
|
"not": "Différent de",
|
||||||
|
"notEmpty": "Non vide",
|
||||||
|
"startsWith": "Commence par",
|
||||||
|
"equals": "Égal à",
|
||||||
|
"notContains": "Ne contient pas",
|
||||||
|
"notEndsWith": "Ne termine pas par",
|
||||||
|
"notStartsWith": "Ne commence pas par"
|
||||||
|
},
|
||||||
|
"array": {
|
||||||
|
"empty": "Vide",
|
||||||
|
"contains": "Contient",
|
||||||
|
"not": "Différent de",
|
||||||
|
"notEmpty": "Non vide",
|
||||||
|
"without": "Sans",
|
||||||
|
"equals": "Égal à"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"add": "Ajouter une condition",
|
||||||
|
"button": {
|
||||||
|
"0": "Recherche avancée",
|
||||||
|
"_": "Recherche avancée (%d)"
|
||||||
|
},
|
||||||
|
"clearAll": "Effacer tout",
|
||||||
|
"condition": "Condition",
|
||||||
|
"data": "Donnée",
|
||||||
|
"deleteTitle": "Supprimer la règle de filtrage",
|
||||||
|
"logicAnd": "Et",
|
||||||
|
"logicOr": "Ou",
|
||||||
|
"title": {
|
||||||
|
"0": "Recherche avancée",
|
||||||
|
"_": "Recherche avancée (%d)"
|
||||||
|
},
|
||||||
|
"value": "Valeur",
|
||||||
|
"leftTitle": "Désindenter le critère",
|
||||||
|
"rightTitle": "Indenter le critère"
|
||||||
|
},
|
||||||
|
"searchPanes": {
|
||||||
|
"clearMessage": "Effacer tout",
|
||||||
|
"count": "{total}",
|
||||||
|
"title": "Filtres actifs - %d",
|
||||||
|
"collapse": {
|
||||||
|
"0": "Volet de recherche",
|
||||||
|
"_": "Volet de recherche (%d)"
|
||||||
|
},
|
||||||
|
"countFiltered": "{shown} ({total})",
|
||||||
|
"emptyPanes": "Pas de volet de recherche",
|
||||||
|
"loadMessage": "Chargement du volet de recherche...",
|
||||||
|
"collapseMessage": "Réduire tout",
|
||||||
|
"showMessage": "Montrer tout"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"collection": "Collection",
|
||||||
|
"colvis": "Visibilité colonnes",
|
||||||
|
"colvisRestore": "Rétablir visibilité",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copySuccess": {
|
||||||
|
"1": "1 ligne copiée dans le presse-papier",
|
||||||
|
"_": "%d lignes copiées dans le presse-papier"
|
||||||
|
},
|
||||||
|
"copyTitle": "Copier dans le presse-papier",
|
||||||
|
"csv": "CSV",
|
||||||
|
"excel": "Excel",
|
||||||
|
"pageLength": {
|
||||||
|
"-1": "Afficher toutes les lignes",
|
||||||
|
"_": "Afficher %d lignes",
|
||||||
|
"1": "Afficher 1 ligne"
|
||||||
|
},
|
||||||
|
"pdf": "PDF",
|
||||||
|
"print": "Imprimer",
|
||||||
|
"copyKeys": "Appuyez sur ctrl ou u2318 + C pour copier les données du tableau dans votre presse-papier.",
|
||||||
|
"createState": "Créer un état",
|
||||||
|
"removeAllStates": "Supprimer tous les états",
|
||||||
|
"removeState": "Supprimer",
|
||||||
|
"renameState": "Renommer",
|
||||||
|
"savedStates": "États sauvegardés",
|
||||||
|
"stateRestore": "État %d",
|
||||||
|
"updateState": "Mettre à jour"
|
||||||
|
},
|
||||||
|
"decimal": ",",
|
||||||
|
"datetime": {
|
||||||
|
"previous": "Précédent",
|
||||||
|
"next": "Suivant",
|
||||||
|
"hours": "Heures",
|
||||||
|
"minutes": "Minutes",
|
||||||
|
"seconds": "Secondes",
|
||||||
|
"unknown": "-",
|
||||||
|
"amPm": [
|
||||||
|
"am",
|
||||||
|
"pm"
|
||||||
|
],
|
||||||
|
"months": {
|
||||||
|
"0": "Janvier",
|
||||||
|
"2": "Mars",
|
||||||
|
"3": "Avril",
|
||||||
|
"4": "Mai",
|
||||||
|
"5": "Juin",
|
||||||
|
"6": "Juillet",
|
||||||
|
"8": "Septembre",
|
||||||
|
"9": "Octobre",
|
||||||
|
"10": "Novembre",
|
||||||
|
"1": "Février",
|
||||||
|
"11": "Décembre",
|
||||||
|
"7": "Août"
|
||||||
|
},
|
||||||
|
"weekdays": [
|
||||||
|
"Dim",
|
||||||
|
"Lun",
|
||||||
|
"Mar",
|
||||||
|
"Mer",
|
||||||
|
"Jeu",
|
||||||
|
"Ven",
|
||||||
|
"Sam"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"close": "Fermer",
|
||||||
|
"create": {
|
||||||
|
"title": "Créer une nouvelle entrée",
|
||||||
|
"button": "Nouveau",
|
||||||
|
"submit": "Créer"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"button": "Editer",
|
||||||
|
"title": "Editer Entrée",
|
||||||
|
"submit": "Mettre à jour"
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"button": "Supprimer",
|
||||||
|
"title": "Supprimer",
|
||||||
|
"submit": "Supprimer",
|
||||||
|
"confirm": {
|
||||||
|
"_": "Êtes-vous sûr de vouloir supprimer %d lignes ?",
|
||||||
|
"1": "Êtes-vous sûr de vouloir supprimer 1 ligne ?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"multi": {
|
||||||
|
"title": "Valeurs multiples",
|
||||||
|
"info": "Les éléments sélectionnés contiennent différentes valeurs pour cette entrée. Pour modifier et définir tous les éléments de cette entrée à la même valeur, cliquez ou tapez ici, sinon ils conserveront leurs valeurs individuelles.",
|
||||||
|
"restore": "Annuler les modifications",
|
||||||
|
"noMulti": "Ce champ peut être modifié individuellement, mais ne fait pas partie d'un groupe. "
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"system": "Une erreur système s'est produite (<a target=\"\\\" rel=\"nofollow\" href=\"\\\">Plus d'information<\/a>)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stateRestore": {
|
||||||
|
"removeSubmit": "Supprimer",
|
||||||
|
"creationModal": {
|
||||||
|
"button": "Créer",
|
||||||
|
"order": "Tri",
|
||||||
|
"paging": "Pagination",
|
||||||
|
"scroller": "Position du défilement",
|
||||||
|
"search": "Recherche",
|
||||||
|
"select": "Sélection",
|
||||||
|
"columns": {
|
||||||
|
"search": "Recherche par colonne",
|
||||||
|
"visible": "Visibilité des colonnes"
|
||||||
|
},
|
||||||
|
"name": "Nom :",
|
||||||
|
"searchBuilder": "Recherche avancée",
|
||||||
|
"title": "Créer un nouvel état",
|
||||||
|
"toggleLabel": "Inclus :"
|
||||||
|
},
|
||||||
|
"renameButton": "Renommer",
|
||||||
|
"duplicateError": "Il existe déjà un état avec ce nom.",
|
||||||
|
"emptyError": "Le nom ne peut pas être vide.",
|
||||||
|
"emptyStates": "Aucun état sauvegardé",
|
||||||
|
"removeConfirm": "Voulez vous vraiment supprimer %s ?",
|
||||||
|
"removeError": "Échec de la suppression de l'état.",
|
||||||
|
"removeJoiner": "et",
|
||||||
|
"removeTitle": "Supprimer l'état",
|
||||||
|
"renameLabel": "Nouveau nom pour %s :",
|
||||||
|
"renameTitle": "Renommer l'état"
|
||||||
|
},
|
||||||
|
"info": "Affichage de _START_ à _END_ sur _TOTAL_ entrées",
|
||||||
|
"infoEmpty": "Affichage de 0 à 0 sur 0 entrées",
|
||||||
|
"infoFiltered": "(filtrées depuis un total de _MAX_ entrées)",
|
||||||
|
"lengthMenu": "Afficher _MENU_ entrées",
|
||||||
|
"paginate": {
|
||||||
|
"first": "Première",
|
||||||
|
"last": "Dernière",
|
||||||
|
"next": "Suivante",
|
||||||
|
"previous": "Précédente"
|
||||||
|
},
|
||||||
|
"zeroRecords": "Aucune entrée correspondante trouvée",
|
||||||
|
"aria": {
|
||||||
|
"sortAscending": " : activer pour trier la colonne par ordre croissant",
|
||||||
|
"sortDescending": " : activer pour trier la colonne par ordre décroissant"
|
||||||
|
},
|
||||||
|
"infoThousands": " ",
|
||||||
|
"search": "Rechercher :",
|
||||||
|
"thousands": " "
|
||||||
|
}
|
||||||
20
ldap.php
20
ldap.php
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once "database.php";
|
||||||
|
|
||||||
$ldap_domain_name = "woodywood";
|
$ldap_domain_name = "woodywood";
|
||||||
|
|
||||||
$handle = ldap_connect("ldap://$ldap_domain_name.local");
|
$handle = ldap_connect("ldap://$ldap_domain_name.local");
|
||||||
@@ -18,15 +20,25 @@ class UserInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function LdapConnect(string $domain, string $username, string $password, ?array $controls): LDAP\Result|false
|
function LdapConnect(string $domain, string $username, string $password): LDAP\Result|false
|
||||||
{
|
{
|
||||||
global $handle;
|
global $handle;
|
||||||
$bind = ldap_bind_ext($handle, $username . '@' . $domain, $password, $controls);
|
$bind = ldap_bind_ext($handle, $username . '@' . $domain, $password);
|
||||||
LogConnection();
|
|
||||||
return $bind;
|
return $bind;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogConnection() {}
|
function LdapIsConnected(string $domain, string $username, string $password) {
|
||||||
|
global $handle;
|
||||||
|
$result = LdapConnect($domain, $username, $password);
|
||||||
|
ldap_parse_result($handle, $result, $error_code, $matched_dn, $error_message, $referrals, $controls);
|
||||||
|
$success = $error_code == 0;
|
||||||
|
LogConnection($username, $success);
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogConnection(string $username, bool $success) {
|
||||||
|
InsertLine(new AuthAttempt($username, $success ? "success" : "failure", $_SERVER['REMOTE_ADDR']));
|
||||||
|
}
|
||||||
|
|
||||||
function LdapConnectAndBind()
|
function LdapConnectAndBind()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ $password = rtrim($_POST["password"]);
|
|||||||
|
|
||||||
require_once "ldap.php";
|
require_once "ldap.php";
|
||||||
|
|
||||||
$result = LdapConnect($domain, $user, $password, []);
|
$result = LdapIsConnected($domain, $user, $password, []);
|
||||||
|
|
||||||
ldap_parse_result($handle, $result, $error_code, $matched_dn, $error_message, $referrals, $controls);
|
if (!$result) {
|
||||||
|
|
||||||
|
|
||||||
if ($error_code != 0) {
|
|
||||||
require_once "templates/login_failed.html";
|
require_once "templates/login_failed.html";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -29,5 +26,4 @@ $body = PrintLoginInfo($info);
|
|||||||
|
|
||||||
require_once "templates/login_success.html.php";
|
require_once "templates/login_success.html.php";
|
||||||
|
|
||||||
// TODO: Mettre les tentatives dans la db
|
|
||||||
// TODO: ldaps
|
// TODO: ldaps
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="../css/index.css">
|
<link rel="stylesheet" href="../css/index.css">
|
||||||
<link rel="stylesheet" href="../css/main.css">
|
<link rel="stylesheet" href="../css/main.css">
|
||||||
|
<!-- Datatables CSS -->
|
||||||
|
<link rel="stylesheet" href="../css/dataTables.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
$admin_account = "Administrateur";
|
||||||
|
|
||||||
function PrintListFirsts(string $title, array $liste): string
|
function PrintListFirsts(string $title, array $liste): string
|
||||||
{
|
{
|
||||||
$result = '<div class="list-group-item"><h5>' . $title . '</h5>';
|
$result = '<div class="list-group-item"><h5>' . $title . '</h5>';
|
||||||
@@ -13,7 +15,11 @@ function PrintListFirsts(string $title, array $liste): string
|
|||||||
|
|
||||||
function PrintLoginInfo($info)
|
function PrintLoginInfo($info)
|
||||||
{
|
{
|
||||||
|
global $admin_account;
|
||||||
$body = '<h2 class="text-center pt-3">Bienvenue ' . $info->fullName . " !</h2>";
|
$body = '<h2 class="text-center pt-3">Bienvenue ' . $info->fullName . " !</h2>";
|
||||||
|
if ($info->fullName == $admin_account) {
|
||||||
|
return $body .= PrintAdminInterface();
|
||||||
|
}
|
||||||
$body .= '<div class="list-group ">';
|
$body .= '<div class="list-group ">';
|
||||||
foreach ($info->ous as $ou) {
|
foreach ($info->ous as $ou) {
|
||||||
$body .= '<div class="list-group-item"><h3>' . $ou . "</h3>";
|
$body .= '<div class="list-group-item"><h3>' . $ou . "</h3>";
|
||||||
@@ -22,8 +28,48 @@ function PrintLoginInfo($info)
|
|||||||
$body .= PrintListFirsts("Groupes", LdapGetGroupsInOU($ou));
|
$body .= PrintListFirsts("Groupes", LdapGetGroupsInOU($ou));
|
||||||
$body .= "</div>";
|
$body .= "</div>";
|
||||||
$body .= "</div>";
|
$body .= "</div>";
|
||||||
|
|
||||||
}
|
}
|
||||||
$body .= "</div>";
|
$body .= "</div>";
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function translateSuccess(string $success) {
|
||||||
|
return $success == "success" ? "Succès" : "Échec";
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrintAdminInterface(): string
|
||||||
|
{
|
||||||
|
$auth_attempts = GetLines();
|
||||||
|
$body = '<h5 class="text-center">Historique des connexions</h5>';
|
||||||
|
$body .= '<table id="attempts" class="table table-striped table-bordered">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Utilisateur</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Adresse IP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>';
|
||||||
|
foreach ($auth_attempts as $attempt) {
|
||||||
|
$body .= '<tr>';
|
||||||
|
$body .= '<td>' . $attempt->username . '</td>';
|
||||||
|
$body .= '<td>' . translateSuccess($attempt->status) . '</td>';
|
||||||
|
$body .= '<td>' . $attempt->timestamp . '</td>';
|
||||||
|
$body .= '<td>' . $attempt->ip_address . '</td>';
|
||||||
|
$body .= '</tr>';
|
||||||
|
}
|
||||||
|
$body .= "</table>";
|
||||||
|
$body .= '
|
||||||
|
<script src="/js/datatables.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let table = new DataTable("#attempts", {
|
||||||
|
language: {
|
||||||
|
url: "/lang/fr.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
';
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user