|
Server IP : 62.171.151.215 / Your IP : 216.73.217.19 Web Server : nginx/1.18.0 System : Linux vmi3128365 5.15.0-176-generic #186-Ubuntu SMP Fri Mar 13 11:01:42 UTC 2026 x86_64 User : alex ( 1000) PHP Version : 8.4.18 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF Directory (1777) : /tmp/plugin-ga-google-analytics/../ |
| [ Home ] | [ C0mmand ] | [ Upload File ] |
|---|
<?php
/**
* Plugin Name: WP Aggregator (Custom Table + RSS)
* Description: Stocke des items d'agrégation dans une table dédiée (anti-doublon) + endpoint REST + flux RSS conforme.
* Version: 2.1.1
* Author: Hermes
*/
if (!defined('ABSPATH')) exit;
global $wpdb;
define('AGG_TABLE', $wpdb->prefix . 'agg_items');
if (!defined('AGG_TOKEN')) {
$token_file = '/etc/camarche-agg-token';
$token = is_readable($token_file) ? trim((string) file_get_contents($token_file)) : '';
define('AGG_TOKEN', $token !== '' ? $token : 'change-moi-en-long-aleatoire');
}
if (!defined('AGG_FEED_LIMIT')) {
define('AGG_FEED_LIMIT', 120);
}
function agg_install_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$sql = "CREATE TABLE " . AGG_TABLE . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
url TEXT NOT NULL,
url_hash CHAR(32) NOT NULL,
title TEXT NOT NULL,
teaser TEXT NULL,
tags TEXT NULL,
date_gmt DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_url_hash (url_hash),
KEY idx_date_gmt (date_gmt)
) $charset_collate;";
dbDelta($sql);
update_option('agg_db_version', '1');
flush_rewrite_rules(false);
}
register_activation_hook(__FILE__, 'agg_install_table');
function agg_ensure_table_exists() {
global $wpdb;
$exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', AGG_TABLE));
if ($exists !== AGG_TABLE) {
agg_install_table();
}
}
add_action('plugins_loaded', 'agg_ensure_table_exists');
function agg_norm_url(string $url): string {
$url = trim($url);
if ($url === '') return '';
$parts = wp_parse_url($url);
if (!$parts || empty($parts['scheme']) || empty($parts['host'])) return $url;
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = $parts['path'] ?? '';
return $scheme . '://' . $host . $path;
}
function agg_url_hash(string $url): string { return md5($url); }
function agg_upsert_items(array $items): int {
global $wpdb;
$affected = 0;
foreach ($items as $it) {
$url = isset($it['url']) ? esc_url_raw($it['url']) : '';
$title = isset($it['title']) ? wp_strip_all_tags($it['title']) : '';
if (!$url || !$title) { continue; }
$url_norm = agg_norm_url($url);
$uhash = agg_url_hash($url_norm);
$teaser = isset($it['teaser']) ? wp_strip_all_tags((string) $it['teaser']) : '';
$tags = isset($it['tags']) ? wp_strip_all_tags((string) $it['tags']) : '';
$date_gmt = null;
if (!empty($it['date_gmt'])) {
$ts = strtotime((string) $it['date_gmt']);
if ($ts) $date_gmt = gmdate('Y-m-d H:i:s', $ts);
}
$sql = "
INSERT INTO " . AGG_TABLE . " (url, url_hash, title, teaser, tags, date_gmt)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
teaser = VALUES(teaser),
tags = VALUES(tags),
date_gmt = IFNULL(VALUES(date_gmt), date_gmt),
updated_at = CURRENT_TIMESTAMP
";
$prepared = $wpdb->prepare(
$sql,
$url_norm,
$uhash,
$title,
$teaser !== '' ? $teaser : null,
$tags !== '' ? $tags : null,
$date_gmt
);
$res = $wpdb->query($prepared);
if ($res === 1 || $res === 2) $affected++;
}
return $affected;
}
function agg_fetch_beebom_news_items(int $limit = 40): array {
$index_url = 'https://beebom.com/sitemap_index.xml';
$fetch_xml = function(string $url) {
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => 'CaMarcheAggregator/1.1',
'Accept' => 'application/xml,text/xml,*/*',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message(), 'body' => null];
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300 || !$body) {
return ['error' => 'bad_response_' . $code, 'body' => null];
}
return ['error' => null, 'body' => $body];
};
$load_dom = function(string $body) {
libxml_use_internal_errors(true);
$dom = new DOMDocument();
if (!$dom->loadXML($body)) return null;
return $dom;
};
// 1) Lire l'index Yoast Beebom et sélectionner automatiquement les 3 derniers post-sitemapN.xml.
$idx = $fetch_xml($index_url);
if (!empty($idx['error'])) return ['error' => 'index_' . $idx['error'], 'items' => [], 'sitemaps' => []];
$dom = $load_dom($idx['body']);
if (!$dom) return ['error' => 'invalid_index_xml', 'items' => [], 'sitemaps' => []];
$xp = new DOMXPath($dom);
$xp->registerNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
$post_sitemaps = [];
foreach ($xp->query('//sm:sitemap') as $sm) {
$locNode = $xp->query('sm:loc', $sm)->item(0);
$lastNode = $xp->query('sm:lastmod', $sm)->item(0);
$loc = $locNode ? trim($locNode->textContent) : '';
if (preg_match('~/post-sitemap(\d+)\.xml$~', $loc, $m)) {
$post_sitemaps[] = [
'num' => intval($m[1]),
'url' => $loc,
'lastmod' => $lastNode ? trim($lastNode->textContent) : '',
];
}
}
if (!$post_sitemaps) return ['error' => 'no_post_sitemaps_found', 'items' => [], 'sitemaps' => []];
usort($post_sitemaps, function($a, $b) { return $b['num'] <=> $a['num']; });
$selected = array_slice($post_sitemaps, 0, 3); // ex: 50,49,48 puis automatiquement 51,50,49 quand 51 apparaît.
// 2) Parser les URLs des 3 sitemaps sélectionnés, trier par lastmod descendant, limiter.
$items_by_url = [];
foreach ($selected as $sm) {
$res = $fetch_xml($sm['url']);
if (!empty($res['error'])) continue;
$sd = $load_dom($res['body']);
if (!$sd) continue;
$sx = new DOMXPath($sd);
$sx->registerNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
foreach ($sx->query('//sm:url') as $u) {
$locNode = $sx->query('sm:loc', $u)->item(0);
$lastNode = $sx->query('sm:lastmod', $u)->item(0);
$loc = $locNode ? trim($locNode->textContent) : '';
$lastmod = $lastNode ? trim($lastNode->textContent) : '';
if (!$loc) continue;
$path = wp_parse_url($loc, PHP_URL_PATH);
$slug = $path ? basename(untrailingslashit($path)) : '';
$title = $slug ? ucwords(str_replace(['-', '_'], ' ', $slug)) : $loc;
$date_gmt = $lastmod ?: gmdate('c');
// Si la même URL est présente plusieurs fois, garder la version la plus récente.
$key = agg_norm_url($loc);
if (!isset($items_by_url[$key]) || strcmp($date_gmt, $items_by_url[$key]['date_gmt']) > 0) {
$items_by_url[$key] = [
'url' => $loc,
'title' => html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'teaser' => '',
'tags' => 'beebom,tech,gaming',
'date_gmt' => $date_gmt,
];
}
}
}
$items = array_values($items_by_url);
usort($items, function($a, $b) { return strcmp((string)($b['date_gmt'] ?? ''), (string)($a['date_gmt'] ?? '')); });
return [
'error' => null,
'items' => array_slice($items, 0, max(1, $limit)),
'sitemaps' => array_map(function($s) { return $s['url']; }, $selected),
];
}
add_action('rest_api_init', function () {
register_rest_route('agg/v1', '/sync-beebom', [
'methods' => ['POST', 'GET'],
'callback' => function (WP_REST_Request $request) {
$token = $request->get_header('X-Agg-Token');
if (!AGG_TOKEN || !$token || !hash_equals(AGG_TOKEN, $token)) {
return new WP_Error('forbidden', 'Bad token', ['status' => 403]);
}
$limit = intval($request->get_param('limit') ?: 40);
$result = agg_fetch_beebom_news_items($limit);
if (!empty($result['error'])) {
return new WP_Error('sync_error', $result['error'], ['status' => 502]);
}
$items = $result['items'];
$n = agg_upsert_items($items);
return ['ok' => true, 'source' => 'beebom_post_sitemaps_latest3', 'sitemaps' => $result['sitemaps'] ?? [], 'received' => count($items), 'affected' => $n, 'first' => $items[0] ?? null];
},
'permission_callback' => '__return_true',
]);
register_rest_route('agg/v1', '/bulk', [
'methods' => 'POST',
'callback' => function (WP_REST_Request $request) {
$token = $request->get_header('X-Agg-Token');
if (!AGG_TOKEN || !$token || !hash_equals(AGG_TOKEN, $token)) {
return new WP_Error('forbidden', 'Bad token', ['status' => 403]);
}
$items = $request->get_json_params();
if (!is_array($items)) {
return new WP_Error('bad_request', 'JSON array attendu', ['status' => 400]);
}
$n = agg_upsert_items($items);
return ['ok' => true, 'received' => count($items), 'affected' => $n];
},
'permission_callback' => '__return_true',
]);
});
add_action('init', function () {
add_feed('agg', 'agg_render_feed');
add_feed('aggregator', 'agg_render_feed');
});
add_action('do_feed_agg', 'agg_render_feed', 10, 1);
add_action('do_feed_aggregator', 'agg_render_feed', 10, 1);
function agg_render_feed($for_comments = false) {
global $wpdb;
if (function_exists('ob_get_level')) {
while (ob_get_level()) { ob_end_clean(); }
}
nocache_headers();
header('Content-Type: application/rss+xml; charset=' . get_option('blog_charset'), true);
status_header(200);
$limit = intval(AGG_FEED_LIMIT);
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT url, title, teaser, tags, COALESCE(date_gmt, updated_at) AS pub, updated_at, created_at
FROM " . AGG_TABLE . " ORDER BY pub DESC LIMIT %d",
$limit
));
$site_name = get_bloginfo('name');
$site_url = home_url('/');
$charset = get_option('blog_charset') ?: 'UTF-8';
$lang = get_locale() ? str_replace('_', '-', get_locale()) : 'fr-FR';
$self_url = home_url(add_query_arg('feed', (get_query_var('feed') ?: 'agg'), ''));
echo '<?xml version="1.0" encoding="' . esc_attr($charset) . '"?>' . "\n";
?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title><?php echo esc_html($site_name); ?> – Aggregator</title>
<link><?php echo esc_url($site_url); ?></link>
<description><?php echo esc_html('Flux agrégé personnalisé (table dédiée, anti-doublon)'); ?></description>
<language><?php echo esc_html($lang); ?></language>
<generator>WP Aggregator (Custom Table) 2.1.1</generator>
<ttl>10</ttl>
<lastBuildDate><?php echo esc_html(gmdate('r')); ?></lastBuildDate>
<atom:link href="<?php echo esc_url($self_url); ?>" rel="self" type="application/rss+xml" />
<?php if ($rows) : foreach ($rows as $r) :
$link = esc_url($r->url);
$title = wp_strip_all_tags((string) $r->title);
$teaser = (string) ($r->teaser ?? '');
$tags = (string) ($r->tags ?? '');
$guid = 'agg-' . md5($r->url);
$pub_ts = strtotime($r->pub ?: $r->updated_at ?: $r->created_at ?: 'now');
$pub_r = gmdate('r', $pub_ts);
$cats = array_filter(array_map('trim', $tags !== '' ? explode(',', $tags) : []));
?>
<item>
<title><?php echo esc_html($title); ?></title>
<link><?php echo $link; ?></link>
<guid isPermaLink="false"><?php echo esc_html($guid); ?></guid>
<pubDate><?php echo esc_html($pub_r); ?></pubDate>
<?php if ($teaser !== ''): ?>
<description><![CDATA[<?php echo $teaser; ?>]]></description>
<content:encoded><![CDATA[<?php echo $teaser; ?>]]></content:encoded>
<?php endif; ?>
<?php foreach ($cats as $cat): ?>
<category><?php echo esc_html($cat); ?></category>
<?php endforeach; ?>
</item>
<?php endforeach; endif; ?>
</channel>
</rss>
<?php
exit;
}