diff --git a/README.md b/README.md
index e7a99a4..227e776 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,9 @@ kaubuntu.re est une interface web responsive qui permet de consulter et recherch
- Recherche de contenu
- Interface responsive (mobile et desktop)
- Intégration avec une instance PeerTube
+- **Progressive Web App (PWA)** avec installation native
+- Mode hors ligne avec cache intelligent
+- Détection automatique d'état de connexion
## Technologies utilisées
@@ -21,6 +24,8 @@ kaubuntu.re est une interface web responsive qui permet de consulter et recherch
- CSS3 avec Media Queries pour le responsive design
- PHP pour le backend
- JavaScript pour les interactions côté client
+- **Service Worker** pour le cache offline et PWA
+- **Web App Manifest** pour l'installation native
- Bibliothèques externes via CDN:
- Font Awesome (icônes)
- jQuery
@@ -45,11 +50,15 @@ kaubuntu.re est une interface web responsive qui permet de consulter et recherch
│ ├── mobile-menu.php
│ ├── featured-videos.php
│ ├── recent-videos.php
-│ └── categories.php
+│ ├── categories.php
+│ └── pwa-init.php
├── index.php
├── video.php
├── categories.php
├── search.php
+├── sw.js # Service Worker pour PWA
+├── site.webmanifest # Manifest PWA
+├── browserconfig.xml # Configuration Windows
└── README.md
```
@@ -57,6 +66,7 @@ kaubuntu.re est une interface web responsive qui permet de consulter et recherch
1. Clonez ce dépôt
2. Configurez votre serveur web (Apache, Nginx, etc.) pour pointer vers le répertoire racine
+3. **Important :** Assurez-vous que votre serveur supporte HTTPS (requis pour PWA)
## Configuration
@@ -120,14 +130,49 @@ Les fichiers `sitemap.xml`, `robots.txt` et `site.webmanifest` contiennent le no
Ces fichiers sont listés dans le `.gitignore` afin que vos modifications ne soient pas suivies par Git, ce qui vous permet de personnaliser votre instance sans affecter le code source principal.
+## Progressive Web App (PWA)
+
+Cette plateforme est une PWA complète offrant :
+
+### Fonctionnalités PWA
+
+- **Installation native** : Bouton d'installation automatique dans l'interface
+- **Mode hors ligne** : Cache intelligent des pages et ressources visitées
+- **Détection d'état** : Indicateur visuel en cas de perte de connexion
+- **Performance** : Chargement instantané des ressources en cache
+- **Responsive** : Interface adaptée pour l'utilisation en application mobile
+
+### Comment installer l'application
+
+1. **Automatique** : Un bouton "Installer" apparaît dans le header lors de la première visite
+2. **Manuel** :
+ - **Chrome/Edge** : Menu → "Installer kaubuntu.re"
+ - **Safari iOS** : Partager → "Ajouter à l'écran d'accueil"
+ - **Firefox Android** : Menu → "Installer"
+
+### Compatibilité PWA
+
+- ✅ Chrome/Edge (Android/Desktop)
+- ✅ Safari (iOS 11.3+)
+- ✅ Firefox (Android)
+- ✅ Samsung Internet
+
+### Fichiers PWA
+
+- `sw.js` : Service Worker gérant le cache et mode offline
+- `site.webmanifest` : Configuration de l'application (nom, icônes, etc.)
+- `browserconfig.xml` : Support des tuiles Windows
+
## Déploiement
Pour déployer sur un serveur mutualisé:
1. Assurez-vous que votre hébergeur supporte PHP (version 7.0 minimum recommandée)
-2. Transférez tous les fichiers via FTP dans le répertoire racine de votre site
-3. Vérifiez que les permissions des fichiers sont correctement définies (644 pour les fichiers, 755 pour les dossiers)
-4. Configurez votre domaine pour pointer vers le dossier où vous avez installé l'application
+2. **Configurez HTTPS** (obligatoire pour les fonctionnalités PWA)
+3. Transférez tous les fichiers via FTP dans le répertoire racine de votre site
+4. Vérifiez que les permissions des fichiers sont correctement définies (644 pour les fichiers, 755 pour les dossiers)
+5. Configurez votre domaine pour pointer vers le dossier où vous avez installé l'application
+6. Testez l'installation PWA via les outils de développement du navigateur
## Développement
diff --git a/browserconfig.xml b/browserconfig.xml
new file mode 100644
index 0000000..1a9e2af
--- /dev/null
+++ b/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #FF0000
+
+
+
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
index b53d80a..b22f8ce 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -98,6 +98,64 @@ img {
background-color: rgba(0, 0, 0, 0.05);
}
+/* PWA Install Button */
+.install-pwa-button {
+ background: var(--primary-red);
+ color: white;
+ border: none;
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ margin-right: 10px;
+}
+
+.install-pwa-button:hover {
+ background: #cc0000;
+}
+
+.install-pwa-button:active {
+ transform: scale(0.95);
+}
+
+/* PWA Styles */
+@media (display-mode: standalone) {
+ body {
+ padding-top: env(safe-area-inset-top);
+ }
+
+ .header {
+ padding-top: env(safe-area-inset-top);
+ }
+}
+
+/* Offline indicator */
+.offline-indicator {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background: #ff4444;
+ color: white;
+ text-align: center;
+ padding: 10px;
+ z-index: 1000;
+ display: none;
+}
+
+.offline-indicator.show {
+ display: block;
+}
+
+/* Enhanced mobile experience */
+@media (max-width: 768px) {
+ .install-pwa-button {
+ padding: 6px 10px;
+ font-size: 14px;
+ }
+}
+
/* Styles pour la page active dans la sidebar */
.nav-item.active {
background-color: rgba(255, 0, 0, 0.08);
@@ -307,7 +365,8 @@ img {
.icon-button i.icon-youtube,
.icon-button i.icon-instagram,
.icon-button i.icon-tiktok,
-.icon-button i.icon-twitter {
+.icon-button i.icon-twitter,
+.icon-button i.icon-mastodon {
color: inherit;
}
@@ -1698,6 +1757,11 @@ i.icon-x,
color: #000000 !important; /* Noir X (anciennement Twitter) */
}
+i.icon-mastodon,
+.fab.fa-mastodon.icon-mastodon {
+ color: #563ACC !important; /* Violet Mastodon */
+}
+
/* Maintenir la couleur par défaut pour les icônes dans le footer */
.footer-social a {
margin: 0 10px;
@@ -2297,4 +2361,6 @@ i.icon-x,
width: 30px;
height: 4px;
}
-}
\ No newline at end of file
+}
+
+
\ No newline at end of file
diff --git a/includes/config.default.php b/includes/config.default.php
index e84bdc0..b25660c 100644
--- a/includes/config.default.php
+++ b/includes/config.default.php
@@ -60,6 +60,7 @@ if (!defined('X_URL')) define('X_URL', '#');
if (!defined('INSTAGRAM_URL')) define('INSTAGRAM_URL', '#');
if (!defined('YOUTUBE_URL')) define('YOUTUBE_URL', '#');
if (!defined('TIKTOK_URL')) define('TIKTOK_URL', '#');
+if (!defined('MASTODON_URL')) define('MASTODON_URL', 'https://koze.kaubuntu.re');
// Contacts
if (!defined('CONTACT_EMAIL')) define('CONTACT_EMAIL', 'contact@kaubuntu.re');
diff --git a/includes/config.php b/includes/config.php
index 9e4a6a9..2ec8a30 100644
--- a/includes/config.php
+++ b/includes/config.php
@@ -25,13 +25,13 @@ define('PEERTUBE_CATEGORIES', $peertube_categories);
/**
* Initialise et récupère les catégories depuis l'API PeerTube
- *
+ *
* @return array Liste des catégories
*/
function initCategories() {
// Récupérer la liste des catégories depuis l'API
$categories = callPeerTubeApi('videos/categories');
-
+
// Tableau de correspondance pour traduire les catégories en français
$translations = [
'Music' => 'Musique',
@@ -53,7 +53,7 @@ function initCategories() {
'Kids' => 'Enfants',
'Food' => 'Cuisine',
];
-
+
// Si une constante PRIORITY_CATEGORIES est définie, utiliser ces traductions
if (defined('PRIORITY_CATEGORIES')) {
$priorityCategories = PRIORITY_CATEGORIES;
@@ -65,20 +65,20 @@ function initCategories() {
}
}
}
-
+
$result = [];
foreach ($categories as $key => $name) {
// Utiliser la traduction si disponible, sinon garder le nom original
$translatedName = isset($translations[$name]) ? $translations[$name] : $name;
$result[$key] = $translatedName;
}
-
+
return $result;
}
/**
* Fonction utilitaire pour appeler l'API PeerTube
- *
+ *
* @param string $endpoint Point de terminaison de l'API
* @param array $params Paramètres optionnels pour la requête
* @return array Données retournées par l'API
@@ -89,21 +89,21 @@ function callPeerTubeApi($endpoint, $params = []) {
error_log('SECURITY: Invalid PeerTube URL detected: ' . PEERTUBE_URL);
return [];
}
-
+
// Nettoyer et valider l'endpoint
$endpoint = ltrim($endpoint, '/');
if (!isValidApiEndpoint($endpoint)) {
error_log('SECURITY: Invalid API endpoint detected: ' . $endpoint);
return [];
}
-
+
$url = PEERTUBE_URL . '/api/v1/' . $endpoint;
-
+
// Ajouter les paramètres à l'URL
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
-
+
// Initialiser cURL avec options de sécurité
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
@@ -115,40 +115,40 @@ function callPeerTubeApi($endpoint, $params = []) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_USERAGENT, 'KaubuntuRe/1.0');
-
+
// Ajouter la clé API si définie
if (defined('API_KEY') && !empty(API_KEY)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: ApiKey ' . API_KEY
]);
}
-
+
// Exécuter la requête
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
-
+
// Traiter la réponse
if ($response === false || !empty($error)) {
error_log('PeerTube API error: ' . $error);
return [];
}
-
+
if ($httpCode < 200 || $httpCode >= 300) {
error_log('PeerTube API HTTP error: ' . $httpCode);
return [];
}
-
+
// Décoder la réponse JSON
$data = json_decode($response, true);
-
+
return $data ?: [];
}
/**
* Valide l'URL PeerTube pour prévenir les attaques SSRF
- *
+ *
* @param string $url URL à valider
* @return bool True si l'URL est valide et sûre
*/
@@ -158,12 +158,12 @@ function isValidPeerTubeUrl($url) {
if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
return false;
}
-
+
// Autoriser uniquement HTTPS (ou HTTP en développement)
if (!in_array($parsed['scheme'], ['https', 'http'])) {
return false;
}
-
+
// Bloquer les adresses IP privées et locales
$host = $parsed['host'];
if (filter_var($host, FILTER_VALIDATE_IP)) {
@@ -171,19 +171,19 @@ function isValidPeerTubeUrl($url) {
return false;
}
}
-
+
// Bloquer localhost et autres domaines dangereux
$blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal'];
if (in_array(strtolower($host), $blockedHosts)) {
return false;
}
-
+
return true;
}
/**
* Valide l'endpoint API pour prévenir l'injection de chemins
- *
+ *
* @param string $endpoint Endpoint à valider
* @return bool True si l'endpoint est valide
*/
@@ -192,12 +192,12 @@ function isValidApiEndpoint($endpoint) {
if (strpos($endpoint, '..') !== false || strpos($endpoint, '//') !== false) {
return false;
}
-
+
// Autoriser uniquement les caractères alphanumériques, tirets, underscores et slashes
if (!preg_match('/^[a-zA-Z0-9\/_-]+$/', $endpoint)) {
return false;
}
-
+
// Liste blanche des endpoints autorisés
$allowedEndpoints = [
'videos',
@@ -208,7 +208,7 @@ function isValidApiEndpoint($endpoint) {
'accounts',
'accounts/.*/videos' // Pour les vidéos d'un compte spécifique
];
-
+
foreach ($allowedEndpoints as $pattern) {
// Remplacer les .* par des marqueurs temporaires
$tempPattern = str_replace('.*', '__WILDCARD__', $pattern);
@@ -216,24 +216,24 @@ function isValidApiEndpoint($endpoint) {
$escapedPattern = preg_quote($tempPattern, '/');
// Remettre les wildcards en place
$regexPattern = str_replace('__WILDCARD__', '.*', $escapedPattern);
-
+
if (preg_match('/^' . $regexPattern . '$/', $endpoint)) {
return true;
}
}
-
+
return false;
}
/**
* Récupère les catégories depuis l'API PeerTube
- *
+ *
* @return array Liste des catégories
*/
function getCategories() {
// Utiliser les catégories déjà récupérées
$categories = PEERTUBE_CATEGORIES;
-
+
$result = [];
foreach ($categories as $key => $name) {
$result[] = [
@@ -241,13 +241,13 @@ function getCategories() {
'name' => $name
];
}
-
+
return $result;
}
/**
* Récupère les vidéos récentes depuis l'API PeerTube
- *
+ *
* @param int $count Nombre de vidéos à récupérer
* @return array Liste des vidéos récentes
*/
@@ -258,13 +258,13 @@ function getRecentVideos($count = RECENT_VIDEOS_COUNT) {
'count' => $count,
'isLocal' => true
]);
-
+
return formatVideosData($data['data'] ?? []);
}
/**
* Récupère les vidéos tendances depuis l'API PeerTube
- *
+ *
* @param int $count Nombre de vidéos à récupérer
* @return array Liste des vidéos tendances
*/
@@ -275,13 +275,13 @@ function getTrendingVideos($count = TRENDING_VIDEOS_COUNT) {
'count' => $count,
'isLocal' => true
]);
-
+
return formatVideosData($data['data'] ?? []);
}
/**
* Récupère les vidéos avec un tag spécifique depuis l'API PeerTube
- *
+ *
* @param string $tag Tag à filtrer
* @param int $count Nombre de vidéos à récupérer
* @return array Liste des vidéos
@@ -293,7 +293,7 @@ function getVideosByTag($tag, $count) {
'count' => $count,
'isLocal' => true
]);
-
+
return formatVideosData($data['data'] ?? []);
}
@@ -311,10 +311,10 @@ function getShorts($count = SHORTS_COUNT) {
'count' => SHORTS_COUNT_SEARCH,
'isLocal' => true
]);
-
+
// Formater les données
$allVideos = formatVideosData($data['data'] ?? []);
-
+
// Filtrer pour ne garder que les vidéos de moins de 2 minutes (120 secondes) et en mode portrait
$shortVideos = array_filter($allVideos, function($video) {
// Vérifier la durée (moins de 2 minutes)
@@ -322,10 +322,10 @@ function getShorts($count = SHORTS_COUNT) {
// Vérifier le ratio (mode portrait)
$ratioOk = isset($video['aspectRatio']) && $video['aspectRatio'] <= 1;
-
+
return $durationOk && $ratioOk;
});
-
+
// Limiter au nombre demandé
return array_slice($shortVideos, 0, $count);
}
@@ -343,7 +343,7 @@ function getIndependenceVideos($count = INDEPENDENCE_VIDEOS_COUNT) {
/**
* Vérifie s'il y a un direct en cours du compte LIVE_ACCOUNT_NAME sur l'instance PeerTube
- *
+ *
* @return array|null Informations sur le direct en cours ou null si aucun direct
*/
function getLiveStream() {
@@ -355,44 +355,44 @@ function getLiveStream() {
'isLive' => true, // Filtrer uniquement les lives
'sort' => '-publishedAt' // Les plus récents en premier
]);
-
+
// Vérifier si on a des résultats
if (empty($data['data']) || count($data['data']) === 0) {
return null;
}
-
+
// Formater les données du live
$liveData = formatVideosData($data['data']);
-
+
// Filtrer pour ne garder que les lives en cours
$activeLives = array_filter($liveData, function($video) {
return isset($video['isLive']) && $video['isLive'] === true;
});
-
+
// Retourner le premier live trouvé
return !empty($activeLives) ? reset($activeLives) : null;
}
/**
* Formate les données brutes des vidéos venant de l'API
- *
+ *
* @param array $videosData Données brutes des vidéos
* @return array Données formatées
*/
function formatVideosData($videosData) {
$videos = [];
-
+
foreach ($videosData as $video) {
// Récupérer la vignette (thumbnail)
- $thumbnail = isset($video['previewPath'])
- ? PEERTUBE_URL . $video['previewPath']
+ $thumbnail = isset($video['previewPath'])
+ ? PEERTUBE_URL . $video['previewPath']
: 'img/default-thumbnail.jpg';
-
+
// Récupérer l'avatar de la chaîne
$channelAvatar = isset($video['channel']['avatars'][0]['path']) && isset($video['channel']['avatars'][0]['path'])
? PEERTUBE_URL . $video['channel']['avatars'][0]['path']
: 'img/default-avatar.png';
-
+
// Formater les données
$videos[] = [
'id' => $video['uuid'],
@@ -409,7 +409,7 @@ function formatVideosData($videosData) {
'isLive' => isset($video['isLive']) ? $video['isLive'] : false
];
}
-
+
return $videos;
}
@@ -418,7 +418,7 @@ function formatDuration($seconds) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$remainingSeconds = $seconds % 60;
-
+
if ($hours > 0) {
return sprintf('%d:%02d:%02d', $hours, $minutes, $remainingSeconds);
} else {
@@ -440,7 +440,7 @@ function formatDate($dateString) {
$date = new DateTime($dateString);
$now = new DateTime();
$interval = $now->diff($date);
-
+
if ($interval->days == 0) {
return 'Aujourd\'hui';
} elseif ($interval->days == 1) {
@@ -474,7 +474,7 @@ function getVideosByCategory($categoryId, $count = CATEGORY_VIDEOS_COUNT) {
'sort' => '-publishedAt', // Les plus récentes d'abord
'isLocal' => true
]);
-
+
return formatVideosData($data['data'] ?? []);
}
@@ -487,11 +487,11 @@ function getDisplayCategories() {
$categories = [];
$priorityCategories = PRIORITY_CATEGORIES;
$allCategories = PEERTUBE_CATEGORIES;
-
+
// Ajouter uniquement les catégories prioritaires dans l'ordre défini
foreach ($priorityCategories as $catId => $categoryName) {
$videos = getVideosByCategory($catId);
-
+
// N'ajouter que les catégories qui ont des vidéos
if (!empty($videos)) {
$categories[] = [
@@ -501,7 +501,7 @@ function getDisplayCategories() {
];
}
}
-
+
return $categories;
}
@@ -513,11 +513,11 @@ function getDisplayCategories() {
function getVideoComments($videoId) {
$endpoint = "videos/{$videoId}/comment-threads";
$response = callPeerTubeApi($endpoint);
-
+
if (!$response || !isset($response['data'])) {
return [];
}
-
+
return $response['data'];
}
@@ -529,9 +529,9 @@ function getVideoComments($videoId) {
function getVideoDownloadOptions($videoId) {
// Récupérer les informations complètes de la vidéo
$videoData = callPeerTubeApi('videos/' . $videoId);
-
+
$downloadOptions = [];
-
+
// Ajouter les fichiers directs s'ils existent
if (isset($videoData['files']) && !empty($videoData['files'])) {
foreach ($videoData['files'] as $file) {
@@ -545,7 +545,7 @@ function getVideoDownloadOptions($videoId) {
}
}
}
-
+
// Ajouter les playlists de streaming s'ils existent
if (isset($videoData['streamingPlaylists']) && !empty($videoData['streamingPlaylists'])) {
foreach ($videoData['streamingPlaylists'] as $playlist) {
@@ -563,7 +563,7 @@ function getVideoDownloadOptions($videoId) {
}
}
}
-
+
return $downloadOptions;
}
@@ -574,19 +574,19 @@ function getVideoDownloadOptions($videoId) {
*/
function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
-
+
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
-
+
$bytes /= (1 << (10 * $pow));
-
+
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* Recherche des vidéos selon un critère
- *
+ *
* @param string $query Terme de recherche
* @param int $count Nombre de vidéos à récupérer
* @param int $start Index de départ pour la pagination
@@ -596,13 +596,13 @@ function searchVideos($query, $count = COUNT_VIDEO_SEARCH, $start = 0) {
if (empty($query)) {
return [];
}
-
+
// Vérifier si la recherche concerne un tag (commence par #)
$isTagSearch = false;
if (substr($query, 0, 1) === '#') {
$isTagSearch = true;
$tag = substr($query, 1); // Enlever le # du début
-
+
// Récupérer les vidéos avec ce tag via l'API
$data = callPeerTubeApi('videos', [
'tagsOneOf' => $tag,
@@ -611,10 +611,10 @@ function searchVideos($query, $count = COUNT_VIDEO_SEARCH, $start = 0) {
'isLocal' => true, // Uniquement les vidéos locales
'sort' => '-publishedAt' // Les plus récentes d'abord
]);
-
+
return formatVideosData($data['data'] ?? []);
}
-
+
// Recherche normale (pas un tag)
$data = callPeerTubeApi('search/videos', [
'search' => $query,
@@ -623,7 +623,6 @@ function searchVideos($query, $count = COUNT_VIDEO_SEARCH, $start = 0) {
'isLocal' => true, // Uniquement les vidéos locales
'sort' => '-publishedAt' // Les plus récentes d'abord
]);
-
return formatVideosData($data['data'] ?? []);
}
-?>
\ No newline at end of file
+?>
diff --git a/includes/footer.php b/includes/footer.php
index 23320c1..f8a02c7 100644
--- a/includes/footer.php
+++ b/includes/footer.php
@@ -66,6 +66,7 @@
+
@@ -27,6 +28,9 @@
+
diff --git a/includes/pwa-init.php b/includes/pwa-init.php
new file mode 100644
index 0000000..8635105
--- /dev/null
+++ b/includes/pwa-init.php
@@ -0,0 +1,82 @@
+' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+ echo '
' . "\n";
+
+ // Manifest
+ echo '
' . "\n";
+}
+
+function addPWAScripts() {
+ ?>
+
+
+
\ No newline at end of file
diff --git a/index.php b/index.php
index 701264f..3651220 100644
--- a/index.php
+++ b/index.php
@@ -14,14 +14,23 @@ setSecurityHeaders();
-
+
-
+
+
+
+
+
+
+
+
+
+
@@ -32,10 +41,10 @@ setSecurityHeaders();
-
@@ -43,9 +52,9 @@ setSecurityHeaders();
DIRECT
-
-
@@ -88,24 +97,24 @@ setSecurityHeaders();
-
+
-
+
-
+
Shorts
-
+
- Aucun short disponible pour le moment
';
@@ -137,12 +146,12 @@ setSecurityHeaders();
-
-
+
1): ?>
@@ -152,10 +161,10 @@ setSecurityHeaders();
-
+
-
+
Dernières vidéos
-
+
Aucune vidéo disponible pour le moment
';
@@ -202,18 +211,18 @@ setSecurityHeaders();
-
-
+
-
+
-
+
Tendances
-
+
Aucune vidéo disponible pour le moment
';
@@ -260,23 +269,23 @@ setSecurityHeaders();
-
-
+
-
+
-
+
-
+
@@ -320,25 +329,25 @@ setSecurityHeaders();
-
+
-
+
-
-
-
+