Compare commits
6 Commits
c43c42a34e
...
49e3af77b8
Author | SHA1 | Date | |
---|---|---|---|
49e3af77b8 | |||
d258105f03 | |||
a9e2c04507 | |||
8fdef7ee2f | |||
e6cee6d426 | |||
32a61fdb81 |
107
.htaccess
Normal file
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
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
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
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 là où 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
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
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
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
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
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
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()
|
||||
]);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user