Compare commits

...

6 Commits

10 changed files with 2335 additions and 0 deletions

107
.htaccess Normal file

@ -0,0 +1,107 @@
# FavMasToKey - Configuration Apache
# Activer le moteur de réécriture
RewriteEngine On
# Forcer HTTPS (à activer en production en supprimant le commentaire)
# RewriteCond %{HTTPS} !=on
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Protéger le répertoire includes
<IfModule mod_rewrite.c>
RewriteRule ^includes/ - [F,L]
</IfModule>
# Bloquer l'accès aux fichiers sensibles
<FilesMatch "^(config\.php|functions\.php|app_data\.php)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Protéger .htaccess et tout fichier commençant par un point
<FilesMatch "^\.">
Order Allow,Deny
Deny from all
</FilesMatch>
# Désactiver l'accès aux anciens fichiers d'authentification (qui n'existent plus)
<FilesMatch "^(oauth\.php|callback\.php)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Désactiver l'affichage du contenu des répertoires
Options -Indexes
# Limiter les méthodes HTTP autorisées
<LimitExcept GET POST HEAD>
Order Allow,Deny
Deny from all
</LimitExcept>
# Headers de sécurité
<IfModule mod_headers.c>
# Protection contre le clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
# Protection XSS
Header always set X-XSS-Protection "1; mode=block"
# Prévention MIME sniffing
Header always set X-Content-Type-Options "nosniff"
# Referrer Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Content Security Policy - Ajusté pour les ressources externes utilisées
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'"
# Désactiver la détection automatique du cache
Header unset ETag
FileETag None
# Mise en cache des ressources statiques
<FilesMatch "\.(css|js)$">
Header set Cache-Control "max-age=604800, public"
</FilesMatch>
# En production, activer HSTS (HTTP Strict Transport Security)
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS
</IfModule>
# Configuration PHP
<IfModule mod_php.c>
# Masquer la version de PHP et autres informations
php_flag expose_php Off
# Désactiver l'affichage des erreurs en production (à décommenter en production)
# php_flag display_errors Off
# php_flag display_startup_errors Off
# php_value error_reporting 0
# Limiter le temps d'exécution des scripts
php_value max_execution_time 120
# Limiter la taille des téléchargements
php_value upload_max_filesize 10M
php_value post_max_size 10M
# Sécurité des sessions
php_value session.cookie_httponly 1
php_value session.use_only_cookies 1
php_value session.cookie_samesite "Lax"
# Utiliser des cookies sécurisés en production (à décommenter en production)
# php_value session.cookie_secure 1
</IfModule>
# Compresser les fichiers texte pour réduire la taille de transfert
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json
</IfModule>
# Protection contre les scans de vulnérabilités communes
RedirectMatch 404 (?i)\.php\.suspected
RedirectMatch 404 (?i)wp-login\.php
RedirectMatch 404 (?i)wp-admin
RedirectMatch 404 (?i)xmlrpc\.php

274
css/styles.css Normal file

@ -0,0 +1,274 @@
/* FavMasToKey - Thème sombre */
:root {
--bg-dark: #121212;
--bg-card: #1e1e1e;
--bg-input: #2a2a2a;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #8a8a8a;
--primary-color: #7e57c2;
--primary-hover: #9575cd;
--success-color: #4caf50;
--info-color: #29b6f6;
--warning-color: #ffb74d;
--danger-color: #f44336;
--border-color: #333333;
--card-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
}
.container {
max-width: 900px;
}
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary);
margin-bottom: 0.5rem;
}
h1 {
color: var(--primary-color);
}
p {
color: var(--text-secondary);
}
/* Cards */
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: var(--card-shadow);
overflow: hidden;
}
.card-title {
color: var(--primary-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
.card-body {
color: var(--text-primary);
}
/* Form elements */
.form-control, .form-select {
background-color: var(--bg-input);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.form-control:focus, .form-select:focus {
background-color: var(--bg-input);
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(126, 87, 194, 0.25);
color: var(--text-primary);
}
.form-control::placeholder {
color: var(--text-muted);
}
.form-text {
color: var(--text-muted);
}
/* Buttons */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover, .btn-primary:focus {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-link {
color: var(--primary-color);
}
.btn-warning {
background-color: var(--warning-color);
border-color: var(--warning-color);
color: #212529;
}
.btn-danger {
background-color: var(--danger-color);
border-color: var(--danger-color);
}
/* Alerts */
.alert {
border-radius: 8px;
border: none;
}
.alert-info {
background-color: rgba(41, 182, 246, 0.2);
color: var(--info-color);
border-left: 4px solid var(--info-color);
}
.alert-success {
background-color: rgba(76, 175, 80, 0.2);
color: var(--success-color);
border-left: 4px solid var(--success-color);
}
.alert-warning {
background-color: rgba(255, 183, 77, 0.2);
color: var(--warning-color);
border-left: 4px solid var(--warning-color);
}
.alert-danger {
background-color: rgba(244, 67, 54, 0.2);
color: var(--danger-color);
border-left: 4px solid var(--danger-color);
}
/* Progress bars */
.progress {
background-color: var(--border-color);
border-radius: 10px;
height: 15px;
overflow: hidden;
}
.progress-bar {
background-color: var(--primary-color);
border-radius: 10px;
}
.progress-bar.bg-info {
background-color: var(--info-color) !important;
}
/* Log container */
#log-container {
background-color: #1a1a1a;
border: 1px solid var(--border-color);
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
padding: 10px;
max-height: 200px;
overflow-y: auto;
}
#operation-log .log-entry {
margin-bottom: 0.5rem;
padding: 2px 5px;
border-radius: 3px;
}
#operation-log .success {
color: var(--success-color);
}
#operation-log .error {
color: var(--danger-color);
}
#operation-log .info {
color: var(--info-color);
}
#operation-log .warning {
color: var(--warning-color);
}
/* Background for help sections */
.bg-light {
background-color: #2a2a2a !important;
color: var(--text-secondary);
}
.step {
transition: all 0.3s ease;
}
/* Animations */
@keyframes progress-pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.progress-bar.active {
animation: progress-pulse 2s infinite;
}
/* Customizing Bootstrap components */
.list-group-item {
background-color: var(--bg-card);
border-color: var(--border-color);
color: var(--text-primary);
}
.table {
color: var(--text-primary);
}
.table-dark {
--bs-table-bg: var(--bg-card);
--bs-table-striped-bg: #2a2a2a;
--bs-table-border-color: var(--border-color);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-card);
}
::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #777;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.card-body {
padding: 1.25rem;
}
h1 {
font-size: 2rem;
}
}

329
diagnostic.php Normal file

@ -0,0 +1,329 @@
<?php
/**
* FavMasToKey - Page de diagnostic
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Inclure les fichiers requis
require_once 'includes/config.php';
require_once 'includes/functions.php';
// Traitement des actions AVANT tout envoi de contenu HTML
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['reset_session'])) {
// Conserver l'ID de session pour les messages
$old_session_id = session_id();
// Détruire la session
session_destroy();
// Redémarrer une nouvelle session
session_start();
$session_message = 'Session réinitialisée. Ancien ID: ' . $old_session_id . ', Nouvel ID: ' . session_id();
}
if (isset($_POST['test_connection']) && ENVIRONMENT === 'development') {
if (isset($_SESSION['misskey_token']) && isset($_SESSION['misskey_instance'])) {
$test_result = validate_misskey_token($_SESSION['misskey_instance'], $_SESSION['misskey_token']);
if ($test_result['success']) {
$connection_message = 'Connexion à l\'API Misskey réussie!';
$connection_status = 'success';
$connection_data = isset($test_result['data']) ? $test_result['data'] : null;
} else {
$connection_message = 'Échec de la connexion à l\'API Misskey: ' . (is_string($test_result['message']) ? $test_result['message'] : json_encode($test_result['message']));
$connection_status = 'danger';
}
} else {
$connection_message = 'Aucun token ou instance disponible dans la session.';
$connection_status = 'warning';
}
}
}
// Fonction pour tester si une URL est accessible
function test_url($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $http_code;
}
// Déterminer les URLs importantes
$current_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$root_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']);
// Tester l'API Misskey si un token est disponible
$misskey_api_status = 'Non testé';
$misskey_api_details = '';
if (isset($_SESSION['misskey_token']) && isset($_SESSION['misskey_instance'])) {
$token_test = validate_misskey_token($_SESSION['misskey_instance'], $_SESSION['misskey_token']);
if ($token_test['success']) {
$misskey_api_status = 'OK';
$misskey_api_details = 'Token valide, API accessible';
} else {
$misskey_api_status = 'Erreur';
$misskey_api_details = isset($token_test['message']) ? (is_string($token_test['message']) ? $token_test['message'] : json_encode($token_test['message'])) : 'Erreur inconnue';
}
}
// Test de connexion à l'API process.php
// Remarque : process.php accepte seulement les requêtes POST, donc un échec avec HTTP 405 est normal ici
$process_url = $root_url . '/process.php';
$process_status = test_url($process_url);
?>
<!DOCTYPE html>
<html lang="fr" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#7e57c2">
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
<title>Diagnostic - FavMasToKey</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container py-5">
<header class="text-center mb-4">
<h1>FavMasToKey - Diagnostic</h1>
<p class="lead">Informations de débogage pour l'application</p>
<p><a href="index.php" class="btn btn-primary">Retour à l'application</a></p>
</header>
<div class="row">
<div class="col-md-12">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Configuration actuelle</h2>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Environnement</dt>
<dd class="col-sm-9"><?php echo ENVIRONMENT; ?></dd>
<dt class="col-sm-3">URL de l'application (config)</dt>
<dd class="col-sm-9"><?php echo $config['app_url']; ?></dd>
<dt class="col-sm-3">URL courante</dt>
<dd class="col-sm-9"><?php echo $current_url; ?></dd>
<dt class="col-sm-3">Test de l'API process.php</dt>
<dd class="col-sm-9">
<?php
if ($process_status === 405) {
echo '<span class="badge bg-success">OK (405)</span> - L\'API process.php n\'accepte que les requêtes POST';
} elseif ($process_status >= 200 && $process_status < 400) {
echo '<span class="badge bg-success">OK (' . $process_status . ')</span>';
} else {
echo '<span class="badge bg-danger">Échec (' . $process_status . ')</span> - L\'API process.php n\'est pas accessible';
}
?>
</dd>
<dt class="col-sm-3">Version de l'application</dt>
<dd class="col-sm-9"><?php echo $config['app_version']; ?></dd>
<dt class="col-sm-3">Instance Misskey connectée</dt>
<dd class="col-sm-9">
<?php
if (isset($_SESSION['misskey_instance'])) {
echo htmlspecialchars($_SESSION['misskey_instance']);
} else {
echo '<span class="text-muted">Non connecté</span>';
}
?>
</dd>
<dt class="col-sm-3">Statut de l'API Misskey</dt>
<dd class="col-sm-9">
<?php
if ($misskey_api_status === 'OK') {
echo '<span class="badge bg-success">OK</span> ' . htmlspecialchars($misskey_api_details);
} elseif ($misskey_api_status === 'Erreur') {
echo '<span class="badge bg-danger">Erreur</span> ' . htmlspecialchars($misskey_api_details);
} else {
echo '<span class="badge bg-secondary">Non testé</span> (Aucun jeton disponible)';
}
?>
</dd>
</dl>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">État de la session</h2>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">ID de session</dt>
<dd class="col-sm-9"><?php echo session_id(); ?></dd>
<dt class="col-sm-3">Token stocké</dt>
<dd class="col-sm-9">
<?php
if (isset($_SESSION['misskey_token'])) {
$token = $_SESSION['misskey_token'];
$masked_token = substr($token, 0, 4) . '...' . substr($token, -4);
echo '<code>' . htmlspecialchars($masked_token) . '</code>';
} else {
echo '<span class="text-muted">Non défini</span>';
}
?>
</dd>
<dt class="col-sm-3">Instance Misskey</dt>
<dd class="col-sm-9">
<?php
if (isset($_SESSION['misskey_instance'])) {
echo htmlspecialchars($_SESSION['misskey_instance']);
} else {
echo '<span class="text-muted">Non défini</span>';
}
?>
</dd>
<dt class="col-sm-3">Données de localStorage</dt>
<dd class="col-sm-9">
<div id="local-storage-info">
<span class="text-muted">Chargement...</span>
</div>
</dd>
</dl>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Actions</h2>
</div>
<div class="card-body">
<form method="post" class="mb-3">
<button type="submit" name="reset_session" class="btn btn-warning">Réinitialiser la session</button>
<button type="button" id="clear-localstorage" class="btn btn-danger">Effacer les données localStorage</button>
<?php if (ENVIRONMENT === 'development'): ?>
<button type="submit" name="test_connection" class="btn btn-info">Tester la connexion à l'API Misskey</button>
<?php endif; ?>
</form>
<?php
// Afficher les messages des actions traitées au début du script
if (isset($session_message)) {
echo '<div class="alert alert-success">' . $session_message . '</div>';
}
if (isset($connection_message)) {
echo '<div class="alert alert-' . $connection_status . '">' . $connection_message . '</div>';
if (ENVIRONMENT === 'development' && isset($connection_data)) {
echo '<pre class="bg-dark text-light p-3">';
print_r($connection_data);
echo '</pre>';
}
}
?>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title h5 mb-0">Instructions pour résoudre les problèmes</h2>
</div>
<div class="card-body">
<h5>Problème de connexion avec le jeton</h5>
<p>Si vous obtenez une erreur lors de la connexion avec votre jeton Misskey :</p>
<ol>
<li>Vérifiez que vous avez copié correctement le jeton d'accès complet, sans espaces supplémentaires.</li>
<li>Assurez-vous que vous avez accordé les permissions suivantes :
<ul>
<li><strong>Afficher les informations du compte</strong></li>
<li><strong>Afficher les favoris</strong></li>
<li><strong>Gérer les favoris</strong></li>
</ul>
</li>
<li>Essayez de générer un nouveau jeton d'accès depuis votre instance Misskey.</li>
<li>Vérifiez que votre instance Misskey est accessible et fonctionne correctement.</li>
</ol>
<h5>Problème avec localStorage</h5>
<p>Si l'application ne se souvient pas de votre progression :</p>
<ol>
<li>Assurez-vous que JavaScript est activé dans votre navigateur.</li>
<li>Vérifiez que votre navigateur autorise l'utilisation de localStorage pour ce site.</li>
<li>Essayez d'effacer les données localStorage en utilisant le bouton ci-dessus, puis recommencez le processus.</li>
</ol>
<h5>Problème de traitement des favoris</h5>
<p>Si certains favoris ne peuvent pas être ajoutés :</p>
<ol>
<li>Vérifiez que votre instance Misskey peut se fédérer avec les instances des publications originales.</li>
<li>Certaines publications peuvent avoir été supprimées ou rendues privées, ce qui les rend inaccessibles.</li>
<li>Votre instance peut avoir des limitations de taux (rate limiting). Dans ce cas, essayez de réduire la vitesse de traitement en modifiant le délai entre les requêtes dans le fichier de configuration.</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Afficher les informations de localStorage
document.addEventListener('DOMContentLoaded', function() {
const localStorageInfo = document.getElementById('local-storage-info');
const clearLocalStorageBtn = document.getElementById('clear-localstorage');
function updateLocalStorageInfo() {
let html = '<ul class="list-unstyled mb-0">';
if (localStorage.getItem('favmastokey_favorites')) {
const favorites = JSON.parse(localStorage.getItem('favmastokey_favorites'));
html += '<li><strong>Favoris stockés:</strong> ' + favorites.length + ' URLs</li>';
} else {
html += '<li><strong>Favoris stockés:</strong> <span class="text-muted">Aucun</span></li>';
}
if (localStorage.getItem('favmastokey_migration')) {
const migration = JSON.parse(localStorage.getItem('favmastokey_migration'));
html += '<li><strong>État de la migration:</strong> ' + migration.status + '</li>';
html += '<li><strong>Progression:</strong> ' + migration.progress.current + '/' + migration.progress.total + ' (' + migration.progress.percentage.toFixed(1) + '%)</li>';
if (migration.lastUpdateTime) {
const date = new Date(migration.lastUpdateTime);
html += '<li><strong>Dernière mise à jour:</strong> ' + date.toLocaleString() + '</li>';
}
} else {
html += '<li><strong>État de la migration:</strong> <span class="text-muted">Aucune migration en cours</span></li>';
}
html += '</ul>';
localStorageInfo.innerHTML = html;
}
// Mettre à jour au chargement
updateLocalStorageInfo();
// Gérer le bouton d'effacement
clearLocalStorageBtn.addEventListener('click', function() {
if (confirm('Êtes-vous sûr de vouloir effacer toutes les données de migration stockées localement ?')) {
localStorage.removeItem('favmastokey_favorites');
localStorage.removeItem('favmastokey_migration');
updateLocalStorageInfo();
alert('Données localStorage effacées avec succès.');
}
});
});
</script>
</body>
</html>

164
doc.php Normal file

@ -0,0 +1,164 @@
<?php
/**
* FavMasToKey - Documentation d'utilisation
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Inclure les fichiers requis
require_once 'includes/config.php';
?>
<!DOCTYPE html>
<html lang="fr" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#7e57c2">
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
<meta name="description" content="FavMasToKey - Documentation d'utilisation pour transférer vos favoris de Mastodon vers Misskey">
<title>Documentation - FavMasToKey</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container py-5">
<header class="text-center mb-5">
<h1>FavMasToKey</h1>
<p class="lead">Documentation d'utilisation</p>
<p><a href="index.php" class="btn btn-primary">Retour à l'application</a></p>
</header>
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow-sm mb-4">
<div class="card-body">
<h2 class="card-title">Guide étape par étape</h2>
<h3>1. Préparation</h3>
<div class="mb-4">
<h4>1.1 Obtenir vos favoris depuis Mastodon</h4>
<ol>
<li>Connectez-vous à votre compte Mastodon</li>
<li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li>
<li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li>
<li>Une fois le fichier prêt, téléchargez-le</li>
</ol>
<h4>1.2 Préparer votre compte Misskey</h4>
<ol>
<li>Assurez-vous d'être connecté à votre compte Misskey</li>
<li>Vérifiez que vous avez suffisamment d'espace pour de nouveaux favoris</li>
</ol>
</div>
<h3>2. Utilisation de FavMasToKey</h3>
<div class="mb-4">
<h4>2.1 Télécharger le fichier JSON</h4>
<ol>
<li>Sur la page d'accueil de FavMasToKey, cliquez sur "Parcourir" pour sélectionner votre fichier JSON de favoris</li>
<li>Cliquez sur "Analyser le fichier" pour continuer</li>
</ol>
<h4>2.2 Connexion à votre compte Misskey</h4>
<ol>
<li>Entrez l'URL de votre instance Misskey (ex: misskey.io)</li>
<li>Générez un jeton d'accès depuis les paramètres de votre compte Misskey:
<ol type="a">
<li>Connectez-vous à votre compte Misskey</li>
<li>Allez dans <strong>Paramètres</strong> > <strong>API</strong></li>
<li>Cliquez sur <strong>Générer un nouveau jeton d'accès</strong></li>
<li>Donnez un nom à votre jeton (ex: "FavMasToKey")</li>
<li>Accordez les permissions suivantes :
<ul>
<li><strong>Afficher les informations du compte</strong></li>
<li><strong>Afficher les favoris</strong></li>
<li><strong>Gérer les favoris</strong></li>
</ul>
</li>
<li>Cliquez sur <strong>Générer</strong> et copiez le jeton</li>
</ol>
</li>
<li>Collez le jeton d'accès dans le champ correspondant</li>
<li>Cliquez sur "Se connecter à Misskey" pour continuer</li>
</ol>
<h4>2.3 Migration des favoris</h4>
<ol>
<li>Une fois l'autorisation accordée, vous serez redirigé vers l'écran de migration</li>
<li>Cliquez sur "Démarrer la migration" pour commencer le processus</li>
<li>Vous pouvez mettre en pause, reprendre ou annuler la migration à tout moment</li>
<li>Le journal des opérations vous montre l'état de chaque favori traité</li>
</ol>
</div>
<h3>3. Résolution des problèmes courants</h3>
<div class="mb-4">
<h4>3.1 Publications non trouvées</h4>
<p>Certaines publications peuvent ne pas être trouvées sur le réseau fédéré pour diverses raisons :</p>
<ul>
<li>La publication a été supprimée</li>
<li>L'instance d'origine est hors ligne</li>
<li>L'utilisateur a changé ses paramètres de confidentialité</li>
<li>Votre instance Misskey ne s'est jamais fédérée avec l'instance d'origine</li>
</ul>
<p>Solution : Malheureusement, il n'y a pas de solution simple pour ce problème, car il s'agit d'une limitation du réseau fédéré. Vous pouvez essayer de visiter manuellement les URLs qui ont échoué.</p>
<h4>3.2 Erreurs d'authentification</h4>
<p>Si vous rencontrez des problèmes avec votre jeton d'accès :</p>
<ul>
<li>Vérifiez que vous avez copié le jeton entier et sans espaces supplémentaires</li>
<li>Assurez-vous d'avoir sélectionné les permissions correctes lors de la génération du jeton</li>
<li>Essayez de générer un nouveau jeton d'accès</li>
<li>Vérifiez que votre instance Misskey est accessible et fonctionne correctement</li>
</ul>
<h4>3.3 Migration interrompue</h4>
<p>Si votre migration est interrompue (par exemple, en fermant l'onglet ou en perdant la connexion Internet), FavMasToKey peut la reprendre :</p>
<ul>
<li>Retournez simplement sur la page de FavMasToKey</li>
<li>Authentifiez-vous à nouveau si nécessaire</li>
<li>L'application détectera automatiquement la migration en cours</li>
<li>Confirmez que vous souhaitez reprendre vous vous êtes arrêté</li>
</ul>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<h2 class="card-title">Informations techniques</h2>
<h3>Comment ça marche ?</h3>
<p>FavMasToKey fonctionne en suivant ces étapes :</p>
<ol>
<li><strong>Analyse du fichier JSON</strong> - L'application extrait les URLs des favoris depuis votre fichier Mastodon</li>
<li><strong>Authentification par jeton</strong> - L'application utilise le jeton d'accès que vous avez généré pour s'authentifier auprès de votre instance Misskey</li>
<li><strong>Recherche fédérée</strong> - Pour chaque favori, l'application recherche la publication équivalente sur le réseau fédéré</li>
<li><strong>Ajout aux favoris</strong> - Si la publication est trouvée, elle est ajoutée à vos favoris Misskey</li>
</ol>
<h3>Confidentialité et sécurité</h3>
<p>FavMasToKey a été conçu en mettant l'accent sur la confidentialité et la sécurité :</p>
<ul>
<li>Aucune donnée n'est stockée sur le serveur, tout est traité localement dans votre navigateur</li>
<li>Les jetons d'accès sont temporaires et ne sont stockés que pendant la durée de votre session</li>
<li>Le code est open source et peut être audité</li>
<li>L'application ne demande que les permissions minimales nécessaires (ajouter aux favoris)</li>
</ul>
<h3>Limitations connues</h3>
<ul>
<li>Les publications qui n'existent plus ou qui sont privées ne peuvent pas être retrouvées</li>
<li>Les instances Misskey peuvent avoir des limites de taux (rate limits) qui ralentissent le processus</li>
<li>Les grandes collections de favoris peuvent prendre du temps à migrer</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

32
images/favicon.svg Normal file

@ -0,0 +1,32 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1999 4846 c-2 -2 -85 -9 -184 -15 -208 -14 -278 -23 -430 -58 -318
-74 -569 -230 -763 -476 -148 -187 -206 -333 -247 -621 -26 -186 -27 -196 -21
-560 3 -204 10 -441 15 -526 5 -85 14 -267 20 -404 6 -149 19 -299 31 -375 52
-318 62 -380 76 -436 77 -311 211 -524 464 -740 112 -95 199 -151 362 -230
168 -83 346 -136 586 -175 259 -42 468 -42 738 0 270 42 549 133 648 211 46
36 56 75 56 217 0 136 -10 186 -40 202 -28 15 -88 12 -203 -10 -58 -11 -147
-24 -198 -30 -52 -5 -141 -16 -199 -25 -146 -23 -398 -16 -530 14 -161 37
-291 110 -351 198 -38 55 -89 192 -89 238 0 26 6 39 25 51 23 15 32 15 113 1
357 -65 299 -61 795 -60 408 1 475 4 620 23 383 50 577 95 722 167 325 162
543 453 601 803 29 170 36 336 41 910 4 544 3 568 -17 670 -55 276 -151 455
-345 646 -201 198 -479 322 -790 352 -306 31 -596 43 -1041 42 -253 0 -463 -2
-465 -4z m804 -824 c87 -30 116 -58 157 -152 18 -40 24 -74 25 -125 0 -100
-30 -155 -150 -277 -52 -54 -95 -99 -95 -101 0 -1 44 -3 98 -2 89 0 101 -2
144 -28 133 -80 271 -286 330 -493 32 -112 32 -314 -1 -428 -108 -383 -414
-651 -783 -687 -242 -24 -426 20 -619 147 -172 114 -321 320 -374 519 -23 84
-23 385 0 470 21 81 86 214 142 294 54 76 807 832 852 856 48 24 133 44 166
39 17 -3 65 -17 108 -32z"/>
<path d="M2363 2992 c-106 -104 -190 -196 -204 -222 -20 -38 -24 -59 -24 -140
0 -86 3 -100 29 -147 55 -99 158 -150 281 -141 120 10 206 71 243 171 45 119
20 242 -69 344 -68 78 -97 170 -79 250 6 30 10 57 7 59 -2 3 -85 -76 -184
-174z"/>
</g>
</svg>

After

(image error) Size: 1.8 KiB

79
includes/config.php Normal file

@ -0,0 +1,79 @@
<?php
/**
* FavMasToKey - Configuration
*/
// Empêcher l'accès direct au fichier
if (!defined('FAVMASTOKEY')) {
die('Accès direct interdit');
}
// Environnement (development ou production)
define('ENVIRONMENT', 'development');
// Gestion des erreurs selon l'environnement
if (ENVIRONMENT === 'development') {
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Activer la journalisation pour le débogage
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/../debug.log');
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Configuration de l'application
$config = [
// Informations de l'application
'app_name' => 'FavMasToKey',
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
'app_version' => '0.3.0', // Mise à jour de la version pour la nouvelle méthode d'authentification
// URL de base - Utilisée pour les liens dans l'application
'app_url' => 'https://concepts.esenjin.xyz/favmastokey', // Remplacez par l'URL exacte de votre application
// Paramètres Misskey API
'misskey_api_endpoint' => '/api/notes/favorites/create',
// Paramètres pour le traitement
'batch_size' => 2,
'timeout' => 90,
'max_retries' => 3,
'delay_between_requests' => 3000
];
// Session
session_start();
/**
* Affiche ou journalise des informations de débogage
*
* @param mixed $data Les données à déboguer
* @param string $title Titre optionnel pour faciliter l'identification des logs
* @param bool $log_to_file Journaliser dans un fichier plutôt que d'afficher
*/
function debug($data, $title = '', $log_to_file = false) {
if (ENVIRONMENT === 'development') {
$output = '';
if (!empty($title)) {
$output .= "=== {$title} ===\n";
}
if (is_array($data) || is_object($data)) {
$output .= print_r($data, true);
} else {
$output .= $data;
}
if ($log_to_file) {
error_log($output);
} else {
echo '<pre style="background:#111; color:#eee; padding:10px; border-radius:5px; overflow:auto; max-height:500px;">';
echo htmlspecialchars($output);
echo '</pre>';
}
}
}

374
includes/functions.php Normal file

@ -0,0 +1,374 @@
<?php
/**
* FavMasToKey - Fonctions utilitaires
*/
// Empêcher l'accès direct au fichier
if (!defined('FAVMASTOKEY')) {
die('Accès direct interdit');
}
/**
* Valide un fichier JSON de favoris Mastodon
*
* @param string $file_path Chemin vers le fichier JSON
* @return array|bool Tableau contenant les données du fichier ou false en cas d'erreur
*/
function validate_mastodon_json($file_path) {
// Vérifier si le fichier existe
if (!file_exists($file_path)) {
return [
'success' => false,
'message' => 'Le fichier n\'existe pas.'
];
}
// Lire le contenu du fichier
$content = file_get_contents($file_path);
if (!$content) {
return [
'success' => false,
'message' => 'Impossible de lire le contenu du fichier.'
];
}
// Décoder le JSON
$json = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'success' => false,
'message' => 'Le fichier n\'est pas un JSON valide: ' . json_last_error_msg()
];
}
// Vérifier la structure du fichier
if (!isset($json['@context']) || !isset($json['type']) || !isset($json['orderedItems'])) {
return [
'success' => false,
'message' => 'Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.'
];
}
// Vérifier que orderedItems est un tableau
if (!is_array($json['orderedItems'])) {
return [
'success' => false,
'message' => 'Le format des favoris dans le fichier est invalide.'
];
}
// Tout est OK
return [
'success' => true,
'data' => $json,
'count' => count($json['orderedItems'])
];
}
/**
* Extrait les identifiants de publications à partir des URLs Mastodon
*
* @param array $urls Tableau d'URLs Mastodon
* @return array Tableau d'identifiants extraits
*/
function extract_toot_ids($urls) {
$ids = [];
foreach ($urls as $url) {
// Format attendu: https://instance.tld/users/username/statuses/id
$parts = explode('/', $url);
// L'ID devrait être le dernier élément après "statuses"
$id = end($parts);
if (is_numeric($id)) {
$ids[] = [
'original_url' => $url,
'toot_id' => $id,
'instance' => parse_url($url, PHP_URL_HOST),
'username' => isset($parts[count($parts) - 3]) ? $parts[count($parts) - 3] : null
];
}
}
return $ids;
}
/**
* Effectue une requête cURL vers l'API Misskey
*
* @param string $instance Instance Misskey (ex: misskey.io)
* @param string $endpoint Point d'accès API (ex: /api/notes/favorites/create)
* @param array $data Données à envoyer
* @param string $token Token d'accès OAuth
* @return array Résultat de la requête
*/
function misskey_api_request($instance, $endpoint, $data, $token) {
global $config;
// Construire l'URL complète
$url = "https://{$instance}{$endpoint}";
// Ajouter le token d'accès aux données
$data['i'] = $token;
// Initialiser cURL
$ch = curl_init();
// Configurer la requête
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'User-Agent: FavMasToKey/' . $config['app_version']
],
CURLOPT_TIMEOUT => $config['timeout'],
CURLOPT_SSL_VERIFYPEER => true
]);
// Exécuter la requête
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
// Fermer la session cURL
curl_close($ch);
// Vérifier les erreurs
if ($error) {
return [
'success' => false,
'message' => 'Erreur cURL: ' . $error,
'http_code' => $http_code
];
}
// Décoder la réponse
$response_data = json_decode($response, true);
// Vérifier si la requête a réussi
if ($http_code >= 200 && $http_code < 300) {
return [
'success' => true,
'data' => $response_data,
'http_code' => $http_code
];
} else {
return [
'success' => false,
'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur API Misskey',
'http_code' => $http_code,
'data' => $response_data
];
}
}
/**
* Valide un jeton d'accès Misskey en effectuant une requête test
*
* @param string $instance Instance Misskey
* @param string $token Jeton d'accès à valider
* @return array Résultat de la validation
*/
function validate_misskey_token($instance, $token) {
// Test basique de connexion avec un simple ping
$ping_result = misskey_api_request($instance, '/api/ping', [], $token);
if (!$ping_result['success']) {
return $ping_result;
}
// Test de récupération d'informations du compte actuel (permission la plus basique)
$account_test = misskey_api_request($instance, '/api/i', [], $token);
if (!$account_test['success']) {
return [
'success' => false,
'message' => 'Le jeton est valide mais n\'a pas accès aux informations du compte. Assurez-vous d\'avoir accordé la permission "Afficher les informations du compte".',
'data' => $account_test['data']
];
}
// Test de validation pour vérifier la permission d'ajouter aux favoris
// On n'a pas besoin d'ajouter un favori réel, juste de vérifier si on peut voir les favoris
$favorites_test = misskey_api_request($instance, '/api/i/favorites', ['limit' => 1], $token);
if (!$favorites_test['success']) {
return [
'success' => false,
'message' => 'Le jeton est valide mais n\'a pas accès aux favoris. Assurez-vous d\'avoir accordé les permissions "Afficher les favoris" et "Gérer les favoris".',
'data' => $favorites_test['data']
];
}
// Tout est bon
return [
'success' => true,
'message' => 'Jeton valide avec toutes les permissions nécessaires',
'data' => [
'account' => isset($account_test['data']['username']) ? $account_test['data']['username'] : 'Compte validé',
'ping' => $ping_result['data']
]
];
}
/**
* Recherche une note Mastodon sur le réseau fédéré de Misskey
* Utilise l'endpoint ap/show qui s'est avéré le plus fiable avec différentes instances
*
* @param string $instance Instance Misskey
* @param string $url URL de la publication Mastodon
* @param string $token Token d'accès
* @return array Résultat de la recherche
*/
function search_federated_note($instance, $url, $token) {
// Nettoyer l'URL (enlever les éventuels paramètres)
$cleanUrl = strtok($url, '?');
// Journal de débogage
error_log("Recherche fédérée pour: " . $cleanUrl);
// Méthode principale: Utiliser ap/show qui fonctionne avec la plupart des instances
$endpoint = '/api/ap/show';
$data = [
'uri' => $cleanUrl
];
// Effectuer la requête
$result = misskey_api_request($instance, $endpoint, $data, $token);
// Journal pour le format de la réponse
if ($result['success'] && isset($result['data'])) {
error_log("Format de réponse ap/show: " . json_encode(array_keys($result['data'])));
}
// Si la méthode principale a réussi et renvoie un ID, retourner le résultat
if ($result['success'] && isset($result['data'])) {
// Vérifier si l'ID existe directement
if (isset($result['data']['id'])) {
return $result;
}
// Certaines instances peuvent avoir l'ID dans 'note'
if (isset($result['data']['note']) && isset($result['data']['note']['id'])) {
// Remonter l'ID au niveau principal
$result['data']['id'] = $result['data']['note']['id'];
return $result;
}
// Pour les instances plus récentes qui utilisent un format différent
if (!empty($result['data'])) {
// Rechercher un champ qui pourrait contenir l'ID
foreach (['id', 'noteId', 'objectId', 'originalId'] as $possibleIdField) {
if (isset($result['data'][$possibleIdField])) {
$result['data']['id'] = $result['data'][$possibleIdField];
return $result;
}
}
// Si toujours pas d'ID, examiner la structure pour le trouver
foreach ($result['data'] as $key => $value) {
if (is_array($value) && isset($value['id'])) {
$result['data']['id'] = $value['id'];
return $result;
}
}
}
}
// Méthode de secours 1: Essayer notes/search-by-url (parfois utilisé dans les anciennes versions)
$fallback_result = misskey_api_request($instance, '/api/notes/search-by-url', ['url' => $cleanUrl], $token);
if ($fallback_result['success'] && isset($fallback_result['data'])) {
error_log("Format de réponse search-by-url: " . json_encode(array_keys($fallback_result['data'])));
// Vérifier si nous avons un résultat avec un ID
if (isset($fallback_result['data']['id'])) {
return $fallback_result;
}
// Si le résultat est un tableau (certaines instances renvoient un tableau)
if (is_array($fallback_result['data']) && !isset($fallback_result['data']['id'])) {
// Chercher le premier élément avec un ID
foreach ($fallback_result['data'] as $item) {
if (is_array($item) && isset($item['id'])) {
$fallback_result['data'] = $item; // Utiliser cet élément comme résultat
return $fallback_result;
}
}
}
}
// Méthode de secours 2: Extraction et recherche par ID distant
$urlParts = parse_url($cleanUrl);
if (isset($urlParts['path'])) {
$pathParts = explode('/', trim($urlParts['path'], '/'));
if (count($pathParts) >= 4 && $pathParts[count($pathParts) - 2] === 'statuses') {
$statusId = end($pathParts);
$username = $pathParts[count($pathParts) - 3];
$acctDomain = isset($urlParts['host']) ? $urlParts['host'] : '';
if ($statusId && $username && $acctDomain) {
$remoteId = "https://{$acctDomain}/users/{$username}/statuses/{$statusId}";
// Essayer d'abord avec /api/notes/show
$remote_result = misskey_api_request($instance, '/api/notes/show', ['uri' => $remoteId], $token);
if ($remote_result['success'] && isset($remote_result['data']['id'])) {
return $remote_result;
}
// Dernier recours: essayer renotes/search
$renote_result = misskey_api_request($instance, '/api/notes/search', [
'query' => "@{$username}@{$acctDomain} {$statusId}",
'limit' => 10
], $token);
if ($renote_result['success'] && !empty($renote_result['data'])) {
// Parcourir les résultats pour trouver une correspondance
foreach ($renote_result['data'] as $note) {
if (isset($note['id'])) {
$renote_result['data'] = $note;
return $renote_result;
}
}
}
}
}
}
// Si aucune méthode n'a fonctionné, retourner une erreur
return [
'success' => false,
'message' => "Impossible de trouver la publication sur le réseau fédéré après plusieurs tentatives"
];
}
/**
* Ajoute une note aux favoris sur Misskey
*
* @param string $instance Instance Misskey
* @param string $note_id ID de la note à ajouter aux favoris
* @param string $token Token d'accès
* @return array Résultat de l'opération
*/
function add_to_favorites($instance, $note_id, $token) {
global $config;
// Endpoint pour ajouter aux favoris
$endpoint = $config['misskey_api_endpoint'];
// Données pour l'ajout aux favoris
$data = [
'noteId' => $note_id
];
// Effectuer la requête
$result = misskey_api_request($instance, $endpoint, $data, $token);
return $result;
}

277
index.php Normal file

@ -0,0 +1,277 @@
<?php
/**
* FavMasToKey - Page d'accueil
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Inclure les fichiers requis
require_once 'includes/config.php';
require_once 'includes/functions.php';
// Vérifier si l'utilisateur est authentifié
$is_authenticated = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']);
$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
// Traiter le formulaire de connexion Misskey
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'connect_token') {
if (isset($_POST['misskey_instance']) && isset($_POST['misskey_token'])) {
$instance = trim($_POST['misskey_instance']);
$token = trim($_POST['misskey_token']);
// Vérifier que l'instance et le token sont valides
if (empty($instance) || empty($token)) {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Veuillez renseigner à la fois l\'instance Misskey et le jeton d\'accès.'
];
} else {
// Valider le format de l'instance
$instance = preg_replace('/^https?:\/\//', '', $instance);
$instance = rtrim($instance, '/');
if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'L\'URL de l\'instance Misskey semble invalide.'
];
} else {
// Vérifier la validité du token en effectuant une requête test
$validate_result = validate_misskey_token($instance, $token);
if ($validate_result['success']) {
// Stocker le token et l'instance dans la session
$_SESSION['misskey_token'] = $token;
$_SESSION['misskey_instance'] = $instance;
$_SESSION['messages'][] = [
'type' => 'success',
'text' => 'Connecté avec succès à ' . $instance . '.'
];
// Rediriger vers l'étape 3
header('Location: index.php#step3');
exit;
} else {
// Formater le message d'erreur correctement
$errorDetails = isset($validate_result['message']) ? $validate_result['message'] : 'Erreur inconnue';
// Si le message est un tableau, le convertir en chaîne JSON
if (is_array($errorDetails)) {
$errorDetails = json_encode($errorDetails, JSON_PRETTY_PRINT);
}
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Le jeton d\'accès semble invalide ou a expiré. Vérifiez que vous avez bien accordé la permission "i/favorites/create".',
'details' => $errorDetails
];
}
}
}
} else {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Données manquantes.'
];
}
}
// Traitement de la déconnexion
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
// Supprimer les informations d'authentification
unset($_SESSION['misskey_token']);
unset($_SESSION['misskey_instance']);
$_SESSION['messages'][] = [
'type' => 'info',
'text' => 'Vous avez été déconnecté.'
];
// Rediriger vers la page d'accueil
header('Location: index.php');
exit;
}
// Initialiser les messages
$messages = [];
if (isset($_SESSION['messages'])) {
$messages = $_SESSION['messages'];
unset($_SESSION['messages']);
}
?>
<!DOCTYPE html>
<html lang="fr" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#7e57c2">
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
<meta name="description" content="FavMasToKey - Transférez vos favoris de Mastodon vers Misskey en quelques clics">
<title>FavMasToKey - Transférer vos favoris de Mastodon vers Misskey</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container py-5">
<header class="text-center mb-5">
<h1>FavMasToKey</h1>
<p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p>
<p><a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a></p>
</header>
<!-- Messages d'alerte -->
<?php if (!empty($messages)): ?>
<?php foreach ($messages as $message): ?>
<div class="alert alert-<?php echo $message['type']; ?> alert-dismissible fade show" role="alert">
<?php echo $message['text']; ?>
<?php if (isset($message['details']) && ENVIRONMENT === 'development'): ?>
<hr>
<details>
<summary>Détails techniques (mode développement)</summary>
<pre class="mt-2 p-2 bg-dark text-light"><?php echo htmlspecialchars($message['details']); ?></pre>
</details>
<?php endif; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<div class="steps">
<!-- Étape 1: Téléchargement du fichier JSON -->
<div class="step" id="step1">
<h3 class="card-title">1. Importer vos favoris Mastodon</h3>
<p>Téléchargez d'abord votre fichier d'export de favoris depuis Mastodon.</p>
<div class="card bg-light mb-3">
<div class="card-body">
<h5>Comment obtenir mon fichier de favoris ?</h5>
<ol>
<li>Connectez-vous à votre compte Mastodon</li>
<li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li>
<li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li>
<li>Une fois le fichier prêt, téléchargez-le</li>
</ol>
</div>
</div>
<form id="upload-form" class="mt-4">
<div class="mb-3">
<label for="json-file" class="form-label">Fichier JSON des favoris</label>
<input type="file" class="form-control" id="json-file" name="json_file" accept=".json" required>
</div>
<button type="submit" class="btn btn-primary">Analyser le fichier</button>
</form>
</div>
<!-- Étape 2: Connexion à Misskey -->
<div class="step d-none" id="step2">
<h3 class="card-title">2. Connexion à Misskey</h3>
<p>Connectez-vous à votre compte Misskey pour y importer vos favoris.</p>
<div id="file-summary" class="alert alert-info mb-4"></div>
<div class="card bg-light mb-4">
<div class="card-body">
<h5>Comment obtenir un jeton d'accès Misskey ?</h5>
<ol>
<li>Connectez-vous à votre compte Misskey</li>
<li>Allez dans <strong>Paramètres</strong> > <strong>API</strong></li>
<li>Cliquez sur <strong>Générer un nouveau jeton d'accès</strong></li>
<li>Donnez un nom à votre jeton (ex: "FavMasToKey")</li>
<li>Accordez <strong>toutes</strong> les permissions suivantes :
<ul class="mt-2">
<li><code>Afficher les informations du compte</code></li>
<li><code>Afficher les favoris</code></li>
<li><code>Gérer les favoris</code></li>
</ul>
</li>
<li>Cliquez sur <strong>Générer</strong> et copiez le jeton</li>
</ol>
<div class="alert alert-info mt-2">
<strong>Conseil :</strong> Si vous continuez à rencontrer des erreurs de permission, essayez d'accorder des permissions supplémentaires. Les exigences peuvent varier légèrement selon les versions de Misskey.
</div>
<div class="alert alert-warning mt-2 mb-0">
<strong>Important :</strong> Conservez ce jeton en lieu sûr. FavMasToKey ne stocke pas votre jeton de manière permanente, mais uniquement dans votre session de navigation.
</div>
</div>
</div>
<form id="misskey-form" method="post" class="mt-4">
<input type="hidden" name="action" value="connect_token">
<div class="mb-3">
<label for="misskey-instance" class="form-label">Instance Misskey</label>
<input type="text" class="form-control" id="misskey-instance" name="misskey_instance"
placeholder="misskey.io" required>
<div class="form-text">Entrez le nom de domaine de votre instance Misskey (ex: misskey.io)</div>
</div>
<div class="mb-3">
<label for="misskey-token" class="form-label">Jeton d'accès</label>
<input type="password" class="form-control" id="misskey-token" name="misskey_token"
placeholder="Votre jeton d'accès Misskey" required>
<div class="form-text">Collez le jeton d'accès généré dans les paramètres de votre compte Misskey</div>
</div>
<button type="submit" class="btn btn-primary">Se connecter à Misskey</button>
<button type="button" class="btn btn-link" id="back-to-step1">Retour</button>
</form>
</div>
<!-- Étape 3: Migration des favoris -->
<div class="step d-none" id="step3">
<h3 class="card-title">3. Migration des favoris</h3>
<p>Nous allons maintenant transférer vos favoris vers Misskey.</p>
<?php if ($is_authenticated): ?>
<div class="alert alert-success mb-4">
<strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong>
<p class="mb-0">Vous êtes authentifié et prêt à importer vos favoris.</p>
<div class="mt-2">
<a href="index.php?action=logout" class="btn btn-sm btn-outline-dark">Se déconnecter</a>
</div>
</div>
<?php endif; ?>
<div class="mb-4">
<label class="form-label">Progression globale</label>
<div class="progress" style="height: 20px;">
<div id="global-progress" class="progress-bar" role="progressbar"
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</div>
<div class="mb-4">
<label class="form-label">Opération en cours</label>
<div class="progress">
<div id="current-progress" class="progress-bar bg-info" role="progressbar"
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</div>
<div class="mb-4">
<h5>Journal des opérations</h5>
<div id="log-container" class="border p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
<div id="operation-log"></div>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button>
<button type="button" class="btn btn-warning d-none" id="pause-migration">Pause</button>
<button type="button" class="btn btn-danger" id="cancel-migration">Annuler</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

523
js/app.js Normal file

@ -0,0 +1,523 @@
/**
* FavMasToKey - Script JavaScript principal
*/
// Attendre que le DOM soit chargé
document.addEventListener('DOMContentLoaded', function() {
// Éléments DOM
const uploadForm = document.getElementById('upload-form');
const jsonFileInput = document.getElementById('json-file');
const step1 = document.getElementById('step1');
const step2 = document.getElementById('step2');
const step3 = document.getElementById('step3');
const fileSummary = document.getElementById('file-summary');
const backToStep1 = document.getElementById('back-to-step1');
const startMigration = document.getElementById('start-migration');
const pauseMigration = document.getElementById('pause-migration');
const cancelMigration = document.getElementById('cancel-migration');
const globalProgress = document.getElementById('global-progress');
const currentProgress = document.getElementById('current-progress');
const operationLog = document.getElementById('operation-log');
// Variables globales
let favoritesList = [];
let currentIndex = 0;
let totalItems = 0;
let isProcessing = false;
let isPaused = false;
let successCount = 0;
let errorCount = 0;
let skippedCount = 0;
let migration = {
status: 'not_started', // not_started, in_progress, paused, completed, error
startTime: null,
lastUpdateTime: null,
progress: {
current: 0,
total: 0,
percentage: 0
},
stats: {
success: 0,
error: 0,
skipped: 0
}
};
// Gérer le téléchargement et l'analyse du fichier JSON
if (uploadForm) {
uploadForm.addEventListener('submit', function(e) {
e.preventDefault();
const file = jsonFileInput.files[0];
if (!file) {
alert('Veuillez sélectionner un fichier JSON.');
return;
}
// Vérifier l'extension du fichier
if (!file.name.endsWith('.json')) {
alert('Le fichier doit être au format JSON.');
return;
}
// Lire le fichier
const reader = new FileReader();
reader.onload = function(event) {
try {
const json = JSON.parse(event.target.result);
// Vérifier la structure du fichier
if (!json['@context'] || !json.type || !json.orderedItems) {
alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.');
return;
}
favoritesList = json.orderedItems;
totalItems = favoritesList.length;
// Afficher un résumé
fileSummary.innerHTML = `
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
`;
// Passer à l'étape 2
step1.classList.add('d-none');
step2.classList.remove('d-none');
// Stocker les données dans localStorage pour les conserver
localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList));
// Initialiser les données de migration
migration = {
status: 'not_started',
startTime: null,
lastUpdateTime: null,
progress: {
current: 0,
total: totalItems,
percentage: 0
},
stats: {
success: 0,
error: 0,
skipped: 0
}
};
// Sauvegarder les données de migration
localStorage.setItem('favmastokey_migration', JSON.stringify(migration));
} catch (error) {
alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message);
}
};
reader.onerror = function() {
alert('Erreur lors de la lecture du fichier.');
};
reader.readAsText(file);
});
}
// Gestion du retour à l'étape 1
if (backToStep1) {
backToStep1.addEventListener('click', function() {
step2.classList.add('d-none');
step1.classList.remove('d-none');
});
}
// Vérifier si nous sommes à l'étape 3 (basé sur l'ancre dans l'URL)
if (window.location.hash === '#step3' && document.getElementById('step3')) {
// Récupérer les favoris du localStorage
if (localStorage.getItem('favmastokey_favorites')) {
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
totalItems = favoritesList.length;
// Montrer l'étape 3
step1.classList.add('d-none');
step2.classList.add('d-none');
step3.classList.remove('d-none');
// Récupérer les données de migration du localStorage
if (localStorage.getItem('favmastokey_migration')) {
migration = JSON.parse(localStorage.getItem('favmastokey_migration'));
currentIndex = migration.progress.current;
updateProgress(migration.progress.percentage);
}
}
}
/**
* Met à jour les données de migration dans localStorage
*/
function updateMigrationData(status, progress = null) {
migration.status = status;
migration.lastUpdateTime = Date.now();
if (progress) {
migration.progress = progress;
} else {
migration.progress = {
current: currentIndex,
total: totalItems,
percentage: (currentIndex / totalItems) * 100
};
}
migration.stats = {
success: successCount,
error: errorCount,
skipped: skippedCount
};
// Sauvegarder dans localStorage
localStorage.setItem('favmastokey_migration', JSON.stringify(migration));
}
/**
* Réinitialise les données de migration
*/
function resetMigration() {
currentIndex = 0;
successCount = 0;
errorCount = 0;
skippedCount = 0;
migration = {
status: 'not_started',
startTime: null,
lastUpdateTime: null,
progress: {
current: 0,
total: totalItems,
percentage: 0
},
stats: {
success: 0,
error: 0,
skipped: 0
}
};
localStorage.setItem('favmastokey_migration', JSON.stringify(migration));
updateProgress(0);
}
// Gérer le processus de migration
if (startMigration) {
startMigration.addEventListener('click', function() {
if (isProcessing) return;
// Récupérer les favoris et les données de migration depuis localStorage si nécessaire
if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) {
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
totalItems = favoritesList.length;
}
// Vérifier s'il y a une migration en cours à reprendre
if (localStorage.getItem('favmastokey_migration')) {
const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration'));
// Si la migration était en cours ou en pause, proposer de la reprendre
if (savedMigration.status === 'in_progress' || savedMigration.status === 'paused') {
const resumeConfirm = confirm(`Une migration précédente a été trouvée (${savedMigration.progress.percentage.toFixed(1)}% terminée). Voulez-vous la reprendre?`);
if (resumeConfirm) {
// Restaurer l'état de la migration
migration = savedMigration;
currentIndex = migration.progress.current;
// Mettre à jour l'interface avec les données sauvegardées
updateProgress(migration.progress.percentage);
// Restaurer les statistiques
successCount = migration.stats.success;
errorCount = migration.stats.error;
skippedCount = migration.stats.skipped;
// Afficher un résumé
addLogEntry(`Reprise de la migration: ${successCount} réussis, ${errorCount} échecs, ${skippedCount} ignorés.`, 'info');
} else {
// Réinitialiser la migration
resetMigration();
}
} else {
// Réinitialiser la migration
resetMigration();
}
} else {
// Initialiser une nouvelle migration
resetMigration();
}
if (favoritesList.length === 0) {
addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
return;
}
// Démarrer la migration
isProcessing = true;
isPaused = false;
startMigration.classList.add('d-none');
pauseMigration.classList.remove('d-none');
// Initialiser le temps de démarrage si c'est une nouvelle migration
if (migration.status === 'not_started' || migration.startTime === null) {
migration.startTime = Date.now();
}
// Mettre à jour le statut de la migration
updateMigrationData('in_progress');
addLogEntry('Démarrage de la migration...', 'info');
// Lancer le processus de migration
processBatch();
});
}
// Gérer la pause de la migration
if (pauseMigration) {
pauseMigration.addEventListener('click', function() {
if (!isProcessing) return;
isPaused = !isPaused;
if (isPaused) {
pauseMigration.textContent = 'Reprendre';
addLogEntry('Migration en pause.', 'warning');
currentProgress.classList.remove('active');
// Mettre à jour le statut de la migration
updateMigrationData('paused');
} else {
pauseMigration.textContent = 'Pause';
addLogEntry('Reprise de la migration...', 'info');
currentProgress.classList.add('active');
// Mettre à jour le statut de la migration
updateMigrationData('in_progress');
processBatch();
}
});
}
// Gérer l'annulation de la migration
if (cancelMigration) {
cancelMigration.addEventListener('click', function() {
if (!isProcessing && currentIndex === 0) {
// Retour à l'étape 1 si rien n'a commencé
step3.classList.add('d-none');
step1.classList.remove('d-none');
return;
}
const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?');
if (confirmCancel) {
isProcessing = false;
isPaused = false;
addLogEntry('Migration annulée.', 'error');
// Réinitialiser l'interface
startMigration.classList.remove('d-none');
pauseMigration.classList.add('d-none');
pauseMigration.textContent = 'Pause';
currentProgress.classList.remove('active');
// Réinitialiser les données de migration
resetMigration();
}
});
}
/**
* Traite un lot de favoris
*/
function processBatch() {
if (!isProcessing || isPaused || currentIndex >= totalItems) {
if (currentIndex >= totalItems) {
// Migration terminée
isProcessing = false;
const summary = `Migration terminée avec succès ! ${successCount} publications ajoutées aux favoris, ${errorCount} erreurs, ${skippedCount} déjà présentes.`;
addLogEntry(summary, 'success');
startMigration.classList.remove('d-none');
startMigration.textContent = 'Terminer';
startMigration.addEventListener('click', function() {
// Nettoyer localStorage et retourner à l'étape 1
localStorage.removeItem('favmastokey_favorites');
localStorage.removeItem('favmastokey_migration');
step3.classList.add('d-none');
step1.classList.remove('d-none');
});
pauseMigration.classList.add('d-none');
// Mettre à jour la progression à 100%
updateProgress(100);
// Mettre à jour le statut de la migration
updateMigrationData('completed');
}
return;
}
// Nombre d'éléments à traiter dans ce lot
const batchSize = 2; // Réduit de 5 à 2 pour limiter les risques de timeout
const endIndex = Math.min(currentIndex + batchSize, totalItems);
// Préparer les éléments du lot
const batch = favoritesList.slice(currentIndex, endIndex);
// Mettre à jour la progression actuelle
currentProgress.classList.add('active');
updateProgress();
// Ajouter une entrée dans le journal
addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info');
// Compteur de tentatives pour cette requête
let retryAttempt = 0;
const maxRetries = 3;
// Fonction pour envoyer la requête avec retry automatique
function sendRequest() {
fetch('process.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
batch: batch,
currentIndex: currentIndex,
totalItems: totalItems
}),
// Augmenter le timeout pour éviter les erreurs de limite de temps
timeout: 60000
})
.then(response => {
// Vérifier si la réponse est au format JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
// Si ce n'est pas du JSON, récupérer le texte pour déboguer
return response.text().then(text => {
throw new Error(`Réponse non-JSON reçue: ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`);
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Traiter les résultats
if (data.results && data.results.length) {
data.results.forEach(result => {
addLogEntry(result.message, result.status);
// Mettre à jour les compteurs
if (result.status === 'success') {
successCount++;
} else if (result.status === 'error') {
errorCount++;
} else if (result.status === 'info') {
skippedCount++;
}
});
}
// Mettre à jour l'index
currentIndex = endIndex;
// Mettre à jour la progression
updateProgress(data.progress.percentage);
// Mettre à jour les données de migration
updateMigrationData('in_progress', data.progress);
// Traiter le lot suivant après un court délai
setTimeout(processBatch, 1000);
} else {
// Gérer l'erreur
addLogEntry('Erreur: ' + data.message, 'error');
// Pause en cas d'erreur
isPaused = true;
pauseMigration.textContent = 'Reprendre';
currentProgress.classList.remove('active');
// Mettre à jour le statut de la migration
updateMigrationData('error');
}
})
.catch(error => {
// Afficher l'erreur détaillée dans la console pour le débogage
console.error('Erreur complète:', error);
// Ajouter l'erreur au journal
addLogEntry('Erreur de connexion: ' + error.message, 'error');
// Retenter si nous n'avons pas atteint le nombre maximum de tentatives
if (retryAttempt < maxRetries) {
retryAttempt++;
const waitTime = Math.pow(2, retryAttempt) * 1000; // Backoff exponentiel
addLogEntry(`Nouvelle tentative (${retryAttempt}/${maxRetries}) dans ${waitTime/1000} secondes...`, 'warning');
setTimeout(sendRequest, waitTime);
} else {
// Pause après plusieurs échecs
isPaused = true;
pauseMigration.textContent = 'Reprendre';
currentProgress.classList.remove('active');
// Mettre à jour le statut de la migration
updateMigrationData('error');
addLogEntry(`Échec après ${maxRetries} tentatives. Veuillez reprendre manuellement.`, 'error');
}
});
}
// Lancer la requête
sendRequest();
}
/**
* Met à jour la barre de progression
*/
function updateProgress(forcedValue = null) {
const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100;
globalProgress.style.width = progress + '%';
globalProgress.textContent = Math.round(progress) + '%';
globalProgress.setAttribute('aria-valuenow', progress);
if (forcedValue === null) {
// Mettre à jour la progression actuelle
const batchProgress = ((currentIndex % 5) / 5) * 100;
currentProgress.style.width = batchProgress + '%';
currentProgress.setAttribute('aria-valuenow', batchProgress);
} else {
currentProgress.style.width = '100%';
currentProgress.setAttribute('aria-valuenow', 100);
}
}
/**
* Ajoute une entrée dans le journal des opérations
*/
function addLogEntry(message, status = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${status}`;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
operationLog.appendChild(entry);
// Scroll vers le bas pour voir la dernière entrée
const logContainer = document.getElementById('log-container');
logContainer.scrollTop = logContainer.scrollHeight;
}
});

176
process.php Normal file

@ -0,0 +1,176 @@
<?php
/**
* FavMasToKey - Traitement des favoris
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Forcer les en-têtes JSON dès le début pour éviter tout conflit
header('Content-Type: application/json');
// Activer la capture d'erreurs
set_error_handler(function($errno, $errstr, $errfile, $errline) {
echo json_encode([
'success' => false,
'message' => "Erreur PHP: $errstr (ligne $errline dans $errfile)",
]);
exit;
});
try {
// Inclure les fichiers requis
require_once 'includes/config.php';
require_once 'includes/functions.php';
// Vérifier que la requête est en POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
exit;
}
// Vérifier que l'utilisateur est authentifié
if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Non authentifié']);
exit;
}
// Récupérer l'instance Misskey
$misskey_instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
if (empty($misskey_instance)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Instance Misskey non définie']);
exit;
}
// Récupérer le token d'accès
$token = $_SESSION['misskey_token'];
// Récupérer les données envoyées
$input_data = file_get_contents('php://input');
$input = json_decode($input_data, true);
if (!$input || !isset($input['batch']) || !is_array($input['batch'])) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Données invalides',
'debug' => [
'received' => $input_data ? substr($input_data, 0, 200) . '...' : 'Aucune donnée reçue'
]
]);
exit;
}
// Récupérer le lot à traiter
$batch = $input['batch'];
$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0;
$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch);
// Résultats du traitement
$results = [];
// Traiter chaque URL du lot
foreach ($batch as $index => $url) {
// Extraire les informations de l'URL
$urlParts = parse_url($url);
// Vérifier que l'URL est valide
if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) {
$results[] = [
'status' => 'error',
'message' => "URL invalide: $url"
];
continue;
}
// Ajouter un log pour déboguer
error_log("Recherche de l'URL: " . $url . " sur l'instance: " . $misskey_instance);
// Rechercher la note sur Misskey à partir de l'URL
$searchResult = search_federated_note($misskey_instance, $url, $token);
// Vérifier si la recherche a réussi ET si les données contiennent un ID valide
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
// Vérifier et extraire l'ID de manière sécurisée
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
$noteId = $searchResult['data']['id'];
// Tenter d'ajouter la note aux favoris
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
if ($favoriteResult['success']) {
$results[] = [
'status' => 'success',
'message' => "Ajouté aux favoris: $url"
];
} else {
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
$errorMessage = isset($favoriteResult['data']['error']['message'])
? $favoriteResult['data']['error']['message']
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
if (strpos($errorMessage, 'already') !== false) {
$results[] = [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url"
];
} else {
$results[] = [
'status' => 'error',
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage"
];
}
}
} else {
// L'ID est manquant ou vide dans la réponse
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
$results[] = [
'status' => 'error',
'message' => "Note trouvée mais sans ID valide: $url. Clés disponibles: " . implode(', ', $dataKeys)
];
// Log pour déboguer
error_log("Structure de données reçue: " . json_encode($searchResult['data']));
}
} else {
// Note non trouvée ou erreur de recherche
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
$results[] = [
'status' => 'error',
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)"
];
// Ajouter des infos de debug
if (isset($searchResult['data']) && is_array($searchResult['data'])) {
error_log("Données reçues pour URL $url: " . json_encode($searchResult['data']));
}
}
// Pause pour éviter le rate limiting
usleep($config['delay_between_requests'] * 1000);
}
// Renvoyer les résultats
echo json_encode([
'success' => true,
'results' => $results,
'progress' => [
'current' => $currentIndex + count($batch),
'total' => $totalItems,
'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2)
]
]);
} catch (Exception $e) {
// Capturer toutes les exceptions et renvoyer un message d'erreur formaté en JSON
echo json_encode([
'success' => false,
'message' => 'Exception: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}