Files
wp-thumbor/wp-thumbor.php
2025-05-22 16:43:11 +00:00

393 lines
17 KiB
PHP

<?php
/*
Plugin Name: WP Thumbor Integration
Description: Intègre Thumbor dans WordPress en transformant les URLs des images présentes dans le <body> (contenu, header, backgrounds, etc.) et dans les liens preload d'image dans le <head>. L'URL de Thumbor, les filtres, la clé secrète et un éventuel chemin local sont configurables. Les images sont redimensionnées si une taille est spécifiée dans le nom du fichier ou dans les attributs HTML. La transformation est désactivée dans certains cas (ex. Optimize More) pour éviter des erreurs getimagesize(). Le favicon, les scripts et les images en dehors du <body> (sauf preload) ne sont pas modifiés.
Version: 1.7
Author: MrRaph_
*/
if ( ! defined('ABSPATH') ) {
exit; // Sécurisation
}
/**
* Récupère les options du plugin avec des valeurs par défaut.
*/
function wp_thumbor_get_options() {
$defaults = array(
'thumbor_base_url' => '',
'thumbor_filters' => '',
'thumbor_secret_key' => '',
'thumbor_local_path' => '',
'thumbor_debug' => false, // Option ajoutée pour le mode debug
);
$options = get_option('wp_thumbor_options', $defaults);
return wp_parse_args($options, $defaults);
}
/**
* Fonction de log pour le mode debug.
* Les messages sont enregistrés dans un fichier situé dans le répertoire wp-content.
*/
function wp_thumbor_log($message) {
$options = wp_thumbor_get_options();
if ( ! empty($options['thumbor_debug']) ) {
// Définir le chemin du fichier de log
$log_file = WP_CONTENT_DIR . '/wp-thumbor.log';
// Préfixer le message avec la date et l'heure
$date = date('Y-m-d H:i:s');
$log_message = "[$date] WP Thumbor Debug: $message" . PHP_EOL;
// Ajoute le message à la fin du fichier
file_put_contents($log_file, $log_message, FILE_APPEND);
}
}
/**
* Vérifie si la transformation Thumbor doit être désactivée pour l'appelant.
* Par exemple, si l'appel provient du plugin Optimize More.
*
* @return bool
*/
function wp_thumbor_should_skip_transform() {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
foreach ( $trace as $frame ) {
if ( isset($frame['file']) && stripos($frame['file'], 'optimize-more') !== false ) {
return true;
}
}
return false;
}
/**
* Ajoute un menu de réglages dans l'administration de WordPress.
*/
function wp_thumbor_admin_menu() {
add_options_page(
'Réglages WP Thumbor Integration',
'WP Thumbor Integration',
'manage_options',
'wp-thumbor-integration',
'wp_thumbor_settings_page'
);
}
add_action('admin_menu', 'wp_thumbor_admin_menu');
/**
* Affiche la page de réglages du plugin dans l'administration.
*/
function wp_thumbor_settings_page() {
if ( ! current_user_can('manage_options') ) {
wp_die(__('Vous n\'avez pas l\'autorisation d\'accéder à cette page.'));
}
if ( isset($_POST['wp_thumbor_nonce']) && wp_verify_nonce($_POST['wp_thumbor_nonce'], 'wp_thumbor_save_settings') ) {
$options = array();
$options['thumbor_base_url'] = isset($_POST['thumbor_base_url']) ? esc_url_raw(trim($_POST['thumbor_base_url'])) : '';
$options['thumbor_filters'] = isset($_POST['thumbor_filters']) ? sanitize_text_field(trim($_POST['thumbor_filters'])) : '';
$options['thumbor_secret_key'] = isset($_POST['thumbor_secret_key']) ? sanitize_text_field(trim($_POST['thumbor_secret_key'])) : '';
$options['thumbor_local_path'] = isset($_POST['thumbor_local_path']) ? sanitize_text_field(trim($_POST['thumbor_local_path'])) : '';
$options['thumbor_debug'] = isset($_POST['thumbor_debug']) ? true : false;
update_option('wp_thumbor_options', $options);
echo '<div class="updated"><p>Les paramètres ont été mis à jour.</p></div>';
}
$options = wp_thumbor_get_options();
?>
<div class="wrap">
<h1>WP Thumbor Integration - Réglages</h1>
<form method="post" action="">
<?php wp_nonce_field('wp_thumbor_save_settings', 'wp_thumbor_nonce'); ?>
<table class="form-table">
<tr valign="top">
<th scope="row">Thumbor Base URL</th>
<td>
<input type="text" name="thumbor_base_url" value="<?php echo esc_attr($options['thumbor_base_url']); ?>" class="regular-text" placeholder="http://thumbor.example.com" />
<p class="description">Entrez l'URL de base de votre serveur Thumbor (sans barre oblique finale).</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Filtres Thumbor</th>
<td>
<input type="text" name="thumbor_filters" value="<?php echo esc_attr($options['thumbor_filters']); ?>" class="regular-text" placeholder="ex: quality(85),contrast(10)" />
<p class="description">Entrez la <a href="https://thumbor.readthedocs.io/en/latest/filters.html">liste des filtres à appliquer</a>, séparés par des virgules, sans le préfixe "filters:" ni le suffixe ":/".</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Clé secrète Thumbor</th>
<td>
<input type="text" name="thumbor_secret_key" value="<?php echo esc_attr($options['thumbor_secret_key']); ?>" class="regular-text" placeholder="Votre clé secrète" />
<p class="description">Entrez la clé secrète pour signer les URLs Thumbor. Laissez vide pour utiliser l'endpoint "unsafe".</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Chemin local Thumbor</th>
<td>
<input type="text" name="thumbor_local_path" value="<?php echo esc_attr($options['thumbor_local_path']); ?>" class="regular-text" placeholder="/home" />
<p class="description">Optionnel. Si renseigné, ce chemin sera préfixé au chemin de l'image sur le filesystem local de Thumbor. Par exemple, si vous indiquez "/home", alors au lieu de transmettre l'URL publique, le plugin transmettra "home/wp-content/uploads/…".</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Activer le mode Debug</th>
<td>
<input type="checkbox" name="thumbor_debug" value="1" <?php checked($options['thumbor_debug'], true); ?> />
<p class="description">Active l'enregistrement des logs dans un fichier.</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
/**
* Extrait les dimensions d'une image à partir de son URL (ex. "image-200x300.jpg").
*
* @param string $url L'URL de l'image.
* @return array|false Tableau avec 'width' et 'height' ou false si aucun pattern n'est trouvé.
*/
function wp_thumbor_extract_dimensions_from_url($url) {
if ( preg_match('/-([0-9]+)x([0-9]+)(?=\.(jpg|jpeg|png|gif|webp)$)/i', $url, $matches) ) {
return array( 'width' => intval($matches[1]), 'height' => intval($matches[2]) );
}
return false;
}
/**
* Transforme l'URL d'une image en ajoutant l'endpoint Thumbor (signé si clé secrète renseignée),
* les options de redimensionnement et les filtres configurés.
*
* Le format généré est :
* {thumbor_base_url}/{endpoint}/[width]x[height]/[filters:...]/{image_path}
*
* Si aucune dimension n'est précisée, la partie redimensionnement est omise.
*
* Si le paramètre "Chemin local Thumbor" est renseigné, l'image est référencée par
* ce chemin suivi du chemin (PATH) de l'URL d'origine.
*
* IMPORTANT : Si l'URL contient une taille (ex. "-1024x764"), on la retire AVANT de
* générer le hash_hmac afin que la signature soit calculée sur l'image en pleine taille.
*
* @param string $url L'URL originale de l'image.
* @param int|null $width La largeur souhaitée.
* @param int|null $height La hauteur souhaitée.
* @return string L'URL transformée.
*/
function wp_thumbor_transform_url($url, $width = null, $height = null) {
// Ne pas transformer dans l'administration
if ( is_admin() ) {
return $url;
}
// Ignorer le favicon
if ( basename(parse_url($url, PHP_URL_PATH)) === 'favicon.ico' ) {
return $url;
}
if ( wp_thumbor_should_skip_transform() ) {
return $url;
}
// Vérifie que l'URL se termine par une extension image attendue
if ( ! preg_match('/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i', $url) ) {
return $url;
}
$options = wp_thumbor_get_options();
$thumbor_base_url = isset($options['thumbor_base_url']) ? rtrim($options['thumbor_base_url'], '/') : '';
if ( empty($thumbor_base_url) ) {
return $url;
}
// Gestion de la taille intégrée dans le nom de fichier :
// Si l'URL contient un pattern "-WIDTHxHEIGHT" à la fin avant l'extension,
// on l'extrait (si aucune dimension n'est fournie) et on le retire de l'URL
if ( preg_match('/-[0-9]+x[0-9]+(?=\.(jpg|jpeg|png|gif|webp)$)/i', $url) ) {
if ( empty($width) && empty($height) ) {
$dimensions = wp_thumbor_extract_dimensions_from_url($url);
if ( $dimensions ) {
$width = $dimensions['width'];
$height = $dimensions['height'];
}
}
// Retire la taille du nom du fichier pour que le hash soit généré sur l'image en pleine taille
$url = preg_replace('/-[0-9]+x[0-9]+(?=\.(jpg|jpeg|png|gif|webp)$)/i', '', $url);
}
// Préparation de la partie redimensionnement
if ( $width || $height ) {
$w = $width ? $width : 0;
$h = $height ? $height : 0;
$resize = $w . 'x' . $h . '/';
} else {
$resize = '';
}
// Préparation de la partie filtres à partir des réglages
$thumbor_filters = isset($options['thumbor_filters']) ? trim($options['thumbor_filters']) : '';
$filterPart = ! empty($thumbor_filters) ? 'filters:' . $thumbor_filters . '/' : '';
// Détermine le chemin de l'image pour Thumbor.
// Par défaut, on utilise l'URL complète (qui est maintenant dépourvue de la taille si présente).
$image_path = $url;
// Si un chemin local est configuré, on remplace la partie de l'URL par ce chemin local suivi du chemin de l'image
if ( ! empty($options['thumbor_local_path']) ) {
$local = ltrim($options['thumbor_local_path'], '/');
$path = parse_url($url, PHP_URL_PATH);
$image_path = $local . $path;
}
// Construction de la chaîne d'options à signer
$path_to_sign = $resize . $filterPart . $image_path;
// Détermine l'endpoint : si une clé secrète est renseignée, on signe, sinon on utilise "unsafe"
if ( ! empty($options['thumbor_secret_key']) ) {
$hmac = hash_hmac('sha1', $path_to_sign, $options['thumbor_secret_key'], true);
// on enlève aussi les '=' de padding pour que Thumbor accepte la signature
$encoded_signature = rtrim(strtr(base64_encode($hmac), '+/', '-_'), '=');
$endpoint = $encoded_signature;
} else {
$endpoint = 'unsafe';
}
$final_url = $thumbor_base_url . '/' . $endpoint . '/' . $path_to_sign;
wp_thumbor_log("Transformation de l'URL : $url en $final_url");
// Encodage manuel des parenthèses pour éviter que esc_url tronque l'URL
//$final_url = str_replace(['(', ')'], ['%28', '%29'], $final_url);
return $final_url;
}
/**
* Callback de traitement de la sortie (output buffer).
* Parcourt le document HTML complet et transforme :
* - Les URLs des balises <img>
* - Les URLs en background définies dans le style inline
* - Les attributs data-image et data-src utilisés pour les backgrounds
* - Les URLs des liens preload d'image dans le <head>
*
* Les balises <script> sont ignorées.
*/
function wp_thumbor_output_buffer_callback($buffer) {
if ( empty($buffer) ) {
return $buffer;
}
wp_thumbor_log("Début du traitement du output buffer.");
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$html = mb_convert_encoding($buffer, 'HTML-ENTITIES', 'UTF-8');
if ( empty($html) ) {
return $buffer;
}
$dom->loadHTML($html);
// Traitement des balises <img>
$images = $dom->getElementsByTagName('img');
foreach ( $images as $img ) {
$width = $img->getAttribute('width');
$height = $img->getAttribute('height');
$width = $width ? intval($width) : null;
$height = $height ? intval($height) : null;
if ( $img->hasAttribute('src') && preg_match('/^https?:\/\//', $img->getAttribute('src')) ) {
$img->setAttribute('src', wp_thumbor_transform_url($img->getAttribute('src'), $width, $height));
}
if ( $img->hasAttribute('data-src') && preg_match('/^https?:\/\//', $img->getAttribute('data-src')) ) {
$img->setAttribute('data-src', wp_thumbor_transform_url($img->getAttribute('data-src'), $width, $height));
}
if ( $img->hasAttribute('srcset') ) {
$srcsetItems = explode(',', $img->getAttribute('srcset'));
$newSrcset = array();
foreach ( $srcsetItems as $item ) {
$parts = preg_split('/\s+/', trim($item));
if ( ! empty($parts[0]) && preg_match('/^https?:\/\//', $parts[0]) ) {
$newURL = wp_thumbor_transform_url($parts[0]);
$newSrcset[] = isset($parts[1]) ? $newURL . ' ' . $parts[1] : $newURL;
} else {
$newSrcset[] = $item;
}
}
$img->setAttribute('srcset', implode(', ', $newSrcset));
}
if ( $img->hasAttribute('data-srcset') ) {
$srcsetItems = explode(',', $img->getAttribute('data-srcset'));
$newSrcset = array();
foreach ( $srcsetItems as $item ) {
$parts = preg_split('/\s+/', trim($item));
if ( ! empty($parts[0]) && preg_match('/^https?:\/\//', $parts[0]) ) {
$newURL = wp_thumbor_transform_url($parts[0]);
$newSrcset[] = isset($parts[1]) ? $newURL . ' ' . $parts[1] : $newURL;
} else {
$newSrcset[] = $item;
}
}
$img->setAttribute('data-srcset', implode(', ', $newSrcset));
}
}
// Traitement des éléments pour les images de background
$allElements = $dom->getElementsByTagName('*');
foreach ( $allElements as $element ) {
if ( strtolower($element->tagName) === 'script' ) {
continue;
}
/*$style = $element->getAttribute('style');
if ( ! empty($style) ) {
$newStyle = preg_replace_callback('/url\((["\']?)(https?:\/\/[^"\'\)]+)\1\)/i', function($matches) {
$url = $matches[2];
$transformed = wp_thumbor_transform_url($url);
return 'url("' . $transformed . '")';
}, $style);
$element->setAttribute('style', $newStyle);
}*/
if ( $element->hasAttribute('data-image') ) {
$dataImage = $element->getAttribute('data-image');
if ( preg_match('/^https?:\/\//', $dataImage) ) {
$element->setAttribute('data-image', wp_thumbor_transform_url($dataImage));
}
}
if ( $element->hasAttribute('data-src') ) {
$dataSrc = $element->getAttribute('data-src');
if ( preg_match('/^https?:\/\//', $dataSrc) ) {
$element->setAttribute('data-src', wp_thumbor_transform_url($dataSrc));
}
}
/*if ( $element->hasAttribute('data-bg') ) {
$dataBg = $element->getAttribute('data-bg');
if ( preg_match('/^https?:\/\//', $dataBg) ) {
$element->setAttribute('data-bg', wp_thumbor_transform_url($dataBg));
}
}*/
}
// Traitement spécifique pour les liens preload d'image dans le <head>
$links = $dom->getElementsByTagName('link');
foreach ( $links as $link ) {
if ( strtolower($link->getAttribute('rel')) === 'preload' && strtolower($link->getAttribute('as')) === 'image' ) {
$href = $link->getAttribute('href');
if ( preg_match('/^https?:\/\//', $href) ) {
$link->setAttribute('href', wp_thumbor_transform_url($href));
}
}
}
return $dom->saveHTML();
}
/**
* Démarre la mise en tampon de la sortie pour traiter toutes les images présentes dans le HTML final.
*/
function wp_thumbor_start_output_buffer() {
if ( ! is_admin() && ! defined('DOING_AJAX') ) {
wp_thumbor_log("Démarrage de l'output buffer.");
ob_start('wp_thumbor_output_buffer_callback');
}
}
add_action('template_redirect', 'wp_thumbor_start_output_buffer');
/**
* Initialisation du plugin (chargement des traductions, etc.).
*/
function wp_thumbor_init() {
load_plugin_textdomain('wp-thumbor-integration', false, dirname(plugin_basename(__FILE__)) . '/languages/');
}
add_action('plugins_loaded', 'wp_thumbor_init');