2025-07-22 11:34:02 +04:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fonctions pour générer des données structurées JSON-LD
|
|
|
|
|
* pour améliorer le SEO et l'affichage dans les moteurs de recherche
|
|
|
|
|
*/
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
/**
|
|
|
|
|
* Drapeaux json_encode pour la sortie JSON-LD.
|
|
|
|
|
*
|
|
|
|
|
* Les JSON_HEX_* encodent <, >, &, ' et " en séquences \u00XX : une valeur
|
|
|
|
|
* contenant "</script>" (titre de vidéo, nom de chaîne, titre d'épisode…)
|
|
|
|
|
* ne peut plus fermer le bloc <script> ni injecter de HTML dans la page.
|
|
|
|
|
* Le JSON reste valide et se décode à l'identique.
|
|
|
|
|
*/
|
|
|
|
|
if (!defined('JSONLD_ENCODE_FLAGS')) {
|
|
|
|
|
define('JSONLD_ENCODE_FLAGS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:34:02 +04:00
|
|
|
/**
|
|
|
|
|
* Génère le JSON-LD pour un objet WebSite
|
|
|
|
|
*
|
|
|
|
|
* @return string JSON-LD pour le site web
|
|
|
|
|
*/
|
|
|
|
|
function generateWebSiteJsonLd() {
|
|
|
|
|
$baseUrl = getBaseUrl();
|
|
|
|
|
|
|
|
|
|
$data = [
|
|
|
|
|
"@context" => "https://schema.org",
|
|
|
|
|
"@type" => "WebSite",
|
2025-10-17 12:24:02 +04:00
|
|
|
"name" => SITE_NAME,
|
|
|
|
|
"description" => SITE_DESCRIPTION,
|
2025-07-22 11:34:02 +04:00
|
|
|
"url" => $baseUrl,
|
|
|
|
|
"potentialAction" => [
|
|
|
|
|
"@type" => "SearchAction",
|
|
|
|
|
"target" => [
|
|
|
|
|
"@type" => "EntryPoint",
|
|
|
|
|
"urlTemplate" => $baseUrl . "/recherche.php?q={search_term_string}"
|
|
|
|
|
],
|
|
|
|
|
"query-input" => "required name=search_term_string"
|
|
|
|
|
],
|
|
|
|
|
"publisher" => [
|
|
|
|
|
"@type" => "Organization",
|
2026-07-25 15:12:43 +04:00
|
|
|
"name" => ORGANIZATION_NAME,
|
2025-07-22 11:34:02 +04:00
|
|
|
"url" => $baseUrl,
|
|
|
|
|
"logo" => [
|
|
|
|
|
"@type" => "ImageObject",
|
|
|
|
|
"url" => $baseUrl . "/img/logo.png"
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
return json_encode($data, JSONLD_ENCODE_FLAGS);
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Génère le JSON-LD pour un objet VideoObject
|
|
|
|
|
*
|
|
|
|
|
* @param array $videoData Données de la vidéo depuis l'API PeerTube
|
|
|
|
|
* @param array $video Données formatées de la vidéo
|
|
|
|
|
* @return string JSON-LD pour la vidéo
|
|
|
|
|
*/
|
|
|
|
|
function generateVideoObjectJsonLd($videoData, $video) {
|
|
|
|
|
$baseUrl = getBaseUrl();
|
|
|
|
|
$videoUrl = $baseUrl . "/video.php?id=" . $video['id'];
|
|
|
|
|
|
|
|
|
|
// Construire l'URL de la vignette
|
|
|
|
|
$thumbnailUrl = isset($videoData['thumbnailPath'])
|
|
|
|
|
? PEERTUBE_URL . $videoData['thumbnailPath']
|
|
|
|
|
: $baseUrl . "/img/default-thumbnail.jpg";
|
|
|
|
|
|
|
|
|
|
// Formater la durée en format ISO 8601 (PT1H30M pour 1h30min)
|
|
|
|
|
$duration = formatDurationISO8601($video['duration'] ?? 0);
|
|
|
|
|
|
|
|
|
|
// Construire les données de base
|
|
|
|
|
$data = [
|
|
|
|
|
"@context" => "https://schema.org",
|
|
|
|
|
"@type" => "VideoObject",
|
|
|
|
|
"name" => $video['title'],
|
|
|
|
|
"description" => !empty($video['description'])
|
|
|
|
|
? truncateText(strip_tags($video['description']), 300)
|
2025-10-17 12:24:02 +04:00
|
|
|
: "Regardez cette vidéo sur " . SITE_NAME,
|
2025-07-22 11:34:02 +04:00
|
|
|
"url" => $videoUrl,
|
2026-07-25 15:12:43 +04:00
|
|
|
"embedUrl" => PEERTUBE_URL . "/videos/embed/" . $video['id'],
|
|
|
|
|
"inLanguage" => "fr-FR",
|
2025-07-22 11:34:02 +04:00
|
|
|
"thumbnailUrl" => $thumbnailUrl,
|
|
|
|
|
"uploadDate" => formatDateISO8601($video['date']),
|
|
|
|
|
"duration" => $duration,
|
|
|
|
|
"publisher" => [
|
|
|
|
|
"@type" => "Organization",
|
2025-10-17 12:24:02 +04:00
|
|
|
"name" => SITE_NAME,
|
2025-07-22 11:34:02 +04:00
|
|
|
"url" => $baseUrl,
|
|
|
|
|
"logo" => [
|
|
|
|
|
"@type" => "ImageObject",
|
|
|
|
|
"url" => $baseUrl . "/img/logo.png"
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Ajouter les informations de la chaîne/créateur
|
|
|
|
|
if (!empty($video['channel'])) {
|
|
|
|
|
$data["creator"] = [
|
|
|
|
|
"@type" => "Person",
|
|
|
|
|
"name" => $video['channel'],
|
|
|
|
|
"url" => PEERTUBE_URL . "/c/" . ($videoData['channel']['name'] ?? '')
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les statistiques d'interaction
|
|
|
|
|
if (isset($video['views']) && $video['views'] > 0) {
|
|
|
|
|
$data["interactionStatistic"] = [
|
|
|
|
|
"@type" => "InteractionCounter",
|
|
|
|
|
"interactionType" => [
|
|
|
|
|
"@type" => "WatchAction"
|
|
|
|
|
],
|
|
|
|
|
"userInteractionCount" => $video['views']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les likes si disponibles
|
|
|
|
|
if (isset($video['likes']) && $video['likes'] > 0) {
|
|
|
|
|
if (!isset($data["interactionStatistic"])) {
|
|
|
|
|
$data["interactionStatistic"] = [];
|
|
|
|
|
} else {
|
|
|
|
|
// Convertir en array si c'était un seul élément
|
|
|
|
|
if (isset($data["interactionStatistic"]["@type"])) {
|
|
|
|
|
$data["interactionStatistic"] = [$data["interactionStatistic"]];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data["interactionStatistic"][] = [
|
|
|
|
|
"@type" => "InteractionCounter",
|
|
|
|
|
"interactionType" => [
|
|
|
|
|
"@type" => "LikeAction"
|
|
|
|
|
],
|
|
|
|
|
"userInteractionCount" => $video['likes']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les tags/mots-clés si disponibles
|
|
|
|
|
if (!empty($video['tags'])) {
|
|
|
|
|
$data["keywords"] = implode(", ", $video['tags']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les informations de licence si disponibles
|
|
|
|
|
if (isset($videoData['licence']) && !empty($videoData['licence'])) {
|
|
|
|
|
$licenceId = $videoData['licence']['id'];
|
|
|
|
|
$licenceMapping = [
|
|
|
|
|
1 => "https://creativecommons.org/licenses/by/4.0/",
|
|
|
|
|
2 => "https://creativecommons.org/licenses/by-sa/4.0/",
|
|
|
|
|
3 => "https://creativecommons.org/licenses/by-nd/4.0/",
|
|
|
|
|
4 => "https://creativecommons.org/licenses/by-nc/4.0/",
|
|
|
|
|
5 => "https://creativecommons.org/licenses/by-nc-sa/4.0/",
|
|
|
|
|
6 => "https://creativecommons.org/licenses/by-nc-nd/4.0/",
|
|
|
|
|
7 => "https://creativecommons.org/publicdomain/zero/1.0/"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (isset($licenceMapping[$licenceId])) {
|
|
|
|
|
$data["license"] = $licenceMapping[$licenceId];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter la catégorie si disponible
|
|
|
|
|
if (isset($videoData['category']) && !empty($videoData['category'])) {
|
|
|
|
|
$data["genre"] = $videoData['category']['label'] ?? 'Vidéo';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les dimensions si c'est un short (format portrait)
|
|
|
|
|
if (isset($video['aspectRatio']) && $video['aspectRatio'] <= 1) {
|
|
|
|
|
$data["videoFrameSize"] = "Portrait";
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
return json_encode($data, JSONLD_ENCODE_FLAGS);
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 15:12:43 +04:00
|
|
|
/**
|
|
|
|
|
* Génère le JSON-LD pour un podcast (PodcastSeries + PodcastEpisode + AudioObject)
|
|
|
|
|
*
|
|
|
|
|
* @param array $episodes Épisodes formatés par getCastopodEpisodes()
|
|
|
|
|
* @return string JSON-LD pour le podcast, chaîne vide si aucun épisode
|
|
|
|
|
*/
|
|
|
|
|
function generatePodcastJsonLd($episodes) {
|
|
|
|
|
if (empty($episodes)) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$baseUrl = getBaseUrl();
|
|
|
|
|
$first = $episodes[0];
|
|
|
|
|
|
|
|
|
|
// Informations de la série, déduites du flux RSS
|
|
|
|
|
$seriesName = !empty($first['podcastTitle']) ? $first['podcastTitle'] : SITE_NAME;
|
|
|
|
|
$seriesUrl = !empty($first['podcastLink']) ? $first['podcastLink'] : $baseUrl;
|
|
|
|
|
$seriesImage = !empty($first['image']) ? $first['image'] : $baseUrl . '/img/logo.png';
|
|
|
|
|
$webFeed = !empty($first['podcastSlug'])
|
|
|
|
|
? rtrim(CASTOPOD_URL, '/') . '/@' . $first['podcastSlug'] . '/feed'
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
$series = [
|
|
|
|
|
"@type" => "PodcastSeries",
|
|
|
|
|
"@id" => $seriesUrl . '#podcast',
|
|
|
|
|
"name" => $seriesName,
|
|
|
|
|
"url" => $seriesUrl,
|
|
|
|
|
"image" => $seriesImage,
|
|
|
|
|
"inLanguage" => "fr-FR",
|
|
|
|
|
"author" => [
|
|
|
|
|
"@type" => "Organization",
|
|
|
|
|
"name" => ORGANIZATION_NAME,
|
|
|
|
|
"url" => $baseUrl
|
|
|
|
|
],
|
|
|
|
|
"publisher" => [
|
|
|
|
|
"@type" => "Organization",
|
|
|
|
|
"name" => SITE_NAME,
|
|
|
|
|
"url" => $baseUrl,
|
|
|
|
|
"logo" => [
|
|
|
|
|
"@type" => "ImageObject",
|
|
|
|
|
"url" => $baseUrl . "/img/logo.png"
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($webFeed) {
|
|
|
|
|
$series["webFeed"] = $webFeed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$episodeItems = [];
|
|
|
|
|
foreach ($episodes as $episode) {
|
|
|
|
|
$item = [
|
|
|
|
|
"@type" => "PodcastEpisode",
|
|
|
|
|
"name" => $episode['title'],
|
|
|
|
|
"url" => $episode['link'],
|
|
|
|
|
"partOfSeries" => ["@id" => $seriesUrl . '#podcast']
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (!empty($episode['pubDate'])) {
|
|
|
|
|
$item["datePublished"] = formatDateISO8601($episode['pubDate']);
|
|
|
|
|
}
|
|
|
|
|
if (!empty($episode['image'])) {
|
|
|
|
|
$item["image"] = $episode['image'];
|
|
|
|
|
}
|
|
|
|
|
if (!empty($episode['description'])) {
|
|
|
|
|
$item["description"] = truncateText($episode['description'], 300);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$seconds = parseDurationToSeconds($episode['duration'] ?? '');
|
|
|
|
|
if ($seconds > 0) {
|
|
|
|
|
$item["timeRequired"] = formatDurationISO8601($seconds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($episode['audioUrl'])) {
|
|
|
|
|
$audio = [
|
|
|
|
|
"@type" => "AudioObject",
|
|
|
|
|
"contentUrl" => $episode['audioUrl']
|
|
|
|
|
];
|
|
|
|
|
$format = guessAudioEncodingFormat($episode['audioUrl']);
|
|
|
|
|
if ($format !== null) {
|
|
|
|
|
$audio["encodingFormat"] = $format;
|
|
|
|
|
}
|
|
|
|
|
$item["associatedMedia"] = $audio;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$episodeItems[] = $item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = [
|
|
|
|
|
"@context" => "https://schema.org",
|
|
|
|
|
"@graph" => array_merge([$series], $episodeItems)
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
return json_encode($data, JSONLD_ENCODE_FLAGS);
|
2026-07-25 15:12:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convertit une durée iTunes en secondes
|
|
|
|
|
* Formats acceptés : "3600", "90", "MM:SS", "HH:MM:SS"
|
|
|
|
|
*
|
|
|
|
|
* @param string $duration Durée brute issue du flux RSS
|
|
|
|
|
* @return int Durée en secondes (0 si non parsable)
|
|
|
|
|
*/
|
|
|
|
|
function parseDurationToSeconds($duration) {
|
|
|
|
|
$duration = trim((string) $duration);
|
|
|
|
|
if ($duration === '') {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strpos($duration, ':') !== false) {
|
|
|
|
|
$parts = array_map('intval', explode(':', $duration));
|
|
|
|
|
$seconds = 0;
|
|
|
|
|
foreach ($parts as $part) {
|
|
|
|
|
$seconds = $seconds * 60 + $part;
|
|
|
|
|
}
|
|
|
|
|
return $seconds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return max(0, (int) $duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Déduit le type MIME d'un fichier audio depuis son extension
|
|
|
|
|
*
|
|
|
|
|
* @param string $url URL du fichier audio
|
|
|
|
|
* @return string|null Type MIME (audio/mpeg, audio/mp4, audio/ogg) ou null
|
|
|
|
|
*/
|
|
|
|
|
function guessAudioEncodingFormat($url) {
|
|
|
|
|
$extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));
|
|
|
|
|
$map = [
|
|
|
|
|
'mp3' => 'audio/mpeg',
|
|
|
|
|
'm4a' => 'audio/mp4',
|
|
|
|
|
'mp4' => 'audio/mp4',
|
|
|
|
|
'ogg' => 'audio/ogg',
|
|
|
|
|
'opus' => 'audio/ogg',
|
|
|
|
|
'wav' => 'audio/wav'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return $map[$extension] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:34:02 +04:00
|
|
|
/**
|
|
|
|
|
* Génère le JSON-LD pour les fils d'Ariane (BreadcrumbList)
|
|
|
|
|
*
|
|
|
|
|
* @param array $breadcrumbs Tableau des fils d'Ariane [['name' => 'Nom', 'url' => 'URL']]
|
|
|
|
|
* @return string JSON-LD pour les fils d'Ariane
|
|
|
|
|
*/
|
|
|
|
|
function generateBreadcrumbJsonLd($breadcrumbs) {
|
|
|
|
|
$baseUrl = getBaseUrl();
|
|
|
|
|
|
|
|
|
|
$listItems = [];
|
|
|
|
|
foreach ($breadcrumbs as $index => $crumb) {
|
|
|
|
|
$listItems[] = [
|
|
|
|
|
"@type" => "ListItem",
|
|
|
|
|
"position" => $index + 1,
|
|
|
|
|
"name" => $crumb['name'],
|
|
|
|
|
"item" => $crumb['url']
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = [
|
|
|
|
|
"@context" => "https://schema.org",
|
|
|
|
|
"@type" => "BreadcrumbList",
|
|
|
|
|
"itemListElement" => $listItems
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
return json_encode($data, JSONLD_ENCODE_FLAGS);
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Génère le JSON-LD pour une page de collection de vidéos
|
|
|
|
|
*
|
|
|
|
|
* @param string $name Nom de la collection
|
|
|
|
|
* @param string $description Description de la collection
|
|
|
|
|
* @param array $videos Tableau des vidéos
|
|
|
|
|
* @param string $url URL de la page de collection
|
|
|
|
|
* @return string JSON-LD pour la collection
|
|
|
|
|
*/
|
|
|
|
|
function generateVideoCollectionJsonLd($name, $description, $videos, $url) {
|
|
|
|
|
$baseUrl = getBaseUrl();
|
|
|
|
|
|
|
|
|
|
$videoItems = [];
|
|
|
|
|
foreach ($videos as $video) {
|
|
|
|
|
$videoItems[] = [
|
|
|
|
|
"@type" => "VideoObject",
|
|
|
|
|
"name" => $video['title'],
|
|
|
|
|
"url" => $baseUrl . "/video.php?id=" . $video['id'],
|
|
|
|
|
"thumbnailUrl" => $video['thumbnail'],
|
|
|
|
|
"uploadDate" => formatDateISO8601($video['date']),
|
|
|
|
|
"duration" => formatDurationISO8601($video['duration'] ?? 0)
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = [
|
|
|
|
|
"@context" => "https://schema.org",
|
|
|
|
|
"@type" => "CollectionPage",
|
|
|
|
|
"name" => $name,
|
|
|
|
|
"description" => $description,
|
|
|
|
|
"url" => $url,
|
|
|
|
|
"mainEntity" => [
|
|
|
|
|
"@type" => "ItemList",
|
|
|
|
|
"itemListElement" => $videoItems,
|
|
|
|
|
"numberOfItems" => count($videos)
|
|
|
|
|
],
|
|
|
|
|
"publisher" => [
|
|
|
|
|
"@type" => "Organization",
|
2025-10-17 12:24:02 +04:00
|
|
|
"name" => SITE_NAME,
|
2025-07-22 11:34:02 +04:00
|
|
|
"url" => $baseUrl
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-27 01:54:49 +04:00
|
|
|
return json_encode($data, JSONLD_ENCODE_FLAGS);
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formate une durée en secondes au format ISO 8601 (PTnHnMnS)
|
|
|
|
|
*
|
|
|
|
|
* @param int $seconds Durée en secondes
|
|
|
|
|
* @return string Durée au format ISO 8601
|
|
|
|
|
*/
|
|
|
|
|
function formatDurationISO8601($seconds) {
|
|
|
|
|
$hours = floor($seconds / 3600);
|
|
|
|
|
$minutes = floor(($seconds % 3600) / 60);
|
|
|
|
|
$remainingSeconds = $seconds % 60;
|
|
|
|
|
|
|
|
|
|
$duration = 'PT';
|
|
|
|
|
|
|
|
|
|
if ($hours > 0) {
|
|
|
|
|
$duration .= $hours . 'H';
|
|
|
|
|
}
|
|
|
|
|
if ($minutes > 0) {
|
|
|
|
|
$duration .= $minutes . 'M';
|
|
|
|
|
}
|
|
|
|
|
if ($remainingSeconds > 0 || ($hours === 0 && $minutes === 0)) {
|
|
|
|
|
$duration .= $remainingSeconds . 'S';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $duration;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formate une date au format ISO 8601
|
|
|
|
|
*
|
|
|
|
|
* @param string $dateString Date à formater
|
|
|
|
|
* @return string Date au format ISO 8601
|
|
|
|
|
*/
|
|
|
|
|
function formatDateISO8601($dateString) {
|
|
|
|
|
try {
|
|
|
|
|
$date = new DateTime($dateString);
|
|
|
|
|
return $date->format('c'); // Format ISO 8601
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
// En cas d'erreur, retourner la date actuelle
|
|
|
|
|
return (new DateTime())->format('c');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tronque un texte à une longueur donnée
|
|
|
|
|
*
|
|
|
|
|
* @param string $text Texte à tronquer
|
|
|
|
|
* @param int $length Longueur maximale
|
|
|
|
|
* @return string Texte tronqué
|
|
|
|
|
*/
|
|
|
|
|
function truncateText($text, $length = 200) {
|
|
|
|
|
if (strlen($text) <= $length) {
|
|
|
|
|
return $text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$truncated = substr($text, 0, $length);
|
|
|
|
|
$lastSpace = strrpos($truncated, ' ');
|
|
|
|
|
|
|
|
|
|
if ($lastSpace !== false) {
|
|
|
|
|
$truncated = substr($truncated, 0, $lastSpace);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $truncated . '...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-27 01:54:49 +04:00
|
|
|
* Valide un nom d'hôte applicatif (hostname, IPv4 ou localhost, port optionnel)
|
|
|
|
|
*
|
|
|
|
|
* @param string $host Hôte à valider
|
|
|
|
|
* @return bool True si l'hôte est exploitable en toute sécurité dans une URL
|
|
|
|
|
*/
|
|
|
|
|
function isValidAppHostName($host) {
|
|
|
|
|
if (!is_string($host) || $host === '' || strlen($host) > 253) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Labels alphanumériques/tirets séparés par des points, port optionnel
|
|
|
|
|
return (bool) preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:\d{1,5})?$/i', $host);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retourne le nom d'hôte de l'application, validé une fois à l'initialisation.
|
|
|
|
|
*
|
|
|
|
|
* Utilise la constante APP_HOST_NAME (configuration) et jamais l'en-tête
|
|
|
|
|
* HTTP_HOST fourni par le client, afin d'empêcher l'injection via Host.
|
|
|
|
|
*
|
|
|
|
|
* @return string Nom d'hôte validé ('localhost' en repli)
|
|
|
|
|
*/
|
|
|
|
|
function getAppHostName() {
|
|
|
|
|
static $validatedHost = null;
|
|
|
|
|
|
|
|
|
|
if ($validatedHost === null) {
|
|
|
|
|
$configuredHost = defined('APP_HOST_NAME') ? (string) APP_HOST_NAME : '';
|
|
|
|
|
if (isValidAppHostName($configuredHost)) {
|
|
|
|
|
$validatedHost = $configuredHost;
|
|
|
|
|
} else {
|
|
|
|
|
error_log('SECURITY: APP_HOST_NAME absent ou invalide, repli sur localhost');
|
|
|
|
|
$validatedHost = 'localhost';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $validatedHost;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Obtient l'URL de base du site (schéma + hôte validé)
|
2025-07-22 11:34:02 +04:00
|
|
|
*
|
|
|
|
|
* @return string URL de base
|
|
|
|
|
*/
|
|
|
|
|
function getBaseUrl() {
|
|
|
|
|
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
|
2026-07-27 01:54:49 +04:00
|
|
|
return $scheme . '://' . getAppHostName();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construit l'URL absolue de la page courante.
|
|
|
|
|
*
|
|
|
|
|
* L'hôte provient de getBaseUrl() (APP_HOST_NAME validé, pas l'en-tête Host)
|
|
|
|
|
* et REQUEST_URI est débarrassé des guillemets, chevrons, espaces et
|
|
|
|
|
* caractères de contrôle qui casseraient un attribut HTML ou une URL.
|
|
|
|
|
*
|
|
|
|
|
* @return string URL absolue de la requête courante
|
|
|
|
|
*/
|
|
|
|
|
function getCurrentUrl() {
|
|
|
|
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
|
|
|
$requestUri = preg_replace('/[\x00-\x20"<>\'`]/', '', $requestUri);
|
|
|
|
|
if ($requestUri === '' || $requestUri[0] !== '/') {
|
|
|
|
|
$requestUri = '/' . $requestUri;
|
|
|
|
|
}
|
|
|
|
|
return getBaseUrl() . $requestUri;
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Génère et affiche un script JSON-LD
|
|
|
|
|
*
|
|
|
|
|
* @param string $jsonLd Données JSON-LD
|
|
|
|
|
*/
|
|
|
|
|
function outputJsonLd($jsonLd) {
|
2026-07-08 07:33:40 +04:00
|
|
|
$nonce = getCspNonce();
|
|
|
|
|
echo '<script type="application/ld+json" nonce="' . $nonce . '">' . "\n" . $jsonLd . "\n" . '</script>' . "\n";
|
2025-07-22 11:34:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
?>
|