améliorations pour la mise en production
This commit is contained in:
parent
32a61fdb81
commit
e6cee6d426
99
.htaccess
99
.htaccess
@ -3,58 +3,75 @@
|
||||
# Activer le moteur de réécriture
|
||||
RewriteEngine On
|
||||
|
||||
# Protéger les fichiers sensibles
|
||||
<FilesMatch "^(config\.php|functions\.php)$">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
# Forcer HTTPS (à activer en production en supprimant le commentaire)
|
||||
# RewriteCond %{HTTPS} !=on
|
||||
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Bloquer l'accès au répertoire includes
|
||||
# Protéger le répertoire includes
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^includes/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Bloquer l'accès aux fichiers cachés
|
||||
# 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>
|
||||
|
||||
# Forcer HTTPS (décommenter en production)
|
||||
# RewriteCond %{HTTPS} !=on
|
||||
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Headers de sécurité
|
||||
<IfModule mod_headers.c>
|
||||
# Empêcher le clickjacking
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
|
||||
# Prévention XSS
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
|
||||
# Empêcher le MIME sniffing
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Référer policy
|
||||
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Content Security Policy (CSP)
|
||||
Header 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';"
|
||||
</IfModule>
|
||||
|
||||
# Désactiver l'affichage du contenu des répertoires
|
||||
Options -Indexes
|
||||
|
||||
# Limiter les méthodes HTTP
|
||||
# Limiter les méthodes HTTP autorisées
|
||||
<LimitExcept GET POST HEAD>
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</LimitExcept>
|
||||
|
||||
# PHP settings
|
||||
# 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>
|
||||
# Désactiver l'affichage des erreurs en production
|
||||
# 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
|
||||
@ -63,10 +80,22 @@ Options -Indexes
|
||||
php_value upload_max_filesize 10M
|
||||
php_value post_max_size 10M
|
||||
|
||||
# Sécuriser les cookies de session
|
||||
# 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
|
||||
# Utiliser des cookies sécurisés en production (à décommenter en production)
|
||||
# php_value session.cookie_secure 1
|
||||
</IfModule>
|
||||
</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
|
85
callback.php
85
callback.php
@ -40,83 +40,50 @@ if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance']
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance = $_SESSION['misskey_instance'];
|
||||
$code = $_GET['code'];
|
||||
|
||||
// En production, ici nous échangerions le code contre un token d'accès
|
||||
// Pour cette version initiale, nous simulons l'échange
|
||||
|
||||
// Simulation : générer un token fictif
|
||||
// Dans une implémentation réelle, nous appellerions l'API Misskey pour obtenir un vrai token
|
||||
$_SESSION['misskey_token'] = 'DEMO_' . bin2hex(random_bytes(16));
|
||||
|
||||
// Dans un cas réel, nous aurions un code similaire à celui-ci:
|
||||
/*
|
||||
// Construire l'URL pour l'échange du code
|
||||
$token_url = "https://{$instance}/oauth/token";
|
||||
|
||||
// Paramètres de la requête
|
||||
$params = [
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $config['app_url'] . '/callback.php'
|
||||
];
|
||||
|
||||
// Initialiser cURL
|
||||
$ch = curl_init();
|
||||
|
||||
// Configurer la requête
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $token_url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($params),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
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 || $http_code !== 200) {
|
||||
// Récupérer le client_secret depuis la session
|
||||
if (!isset($_SESSION['misskey_client_secret']) || empty($_SESSION['misskey_client_secret'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . ($error ?: 'HTTP ' . $http_code)
|
||||
'text' => 'Informations d\'application manquantes. Veuillez recommencer.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Décoder la réponse
|
||||
$response_data = json_decode($response, true);
|
||||
$instance = $_SESSION['misskey_instance'];
|
||||
$code = $_GET['code'];
|
||||
$client_secret = $_SESSION['misskey_client_secret'];
|
||||
|
||||
// Vérifier que le token est présent
|
||||
if (!isset($response_data['access_token']) || empty($response_data['access_token'])) {
|
||||
// Récupérer le client_id depuis les données d'application
|
||||
if (!isset($app_data['instances'][$instance]) || !isset($app_data['instances'][$instance]['client_id'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Aucun token d\'accès reçu. L\'authentification a échoué.'
|
||||
'text' => 'Informations d\'application introuvables pour ' . $instance . '. Veuillez recommencer.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$client_id = $app_data['instances'][$instance]['client_id'];
|
||||
|
||||
// Échanger le code contre un token d'accès
|
||||
$exchange_result = exchange_oauth_code($instance, $code, $client_id, $client_secret);
|
||||
|
||||
if (!$exchange_result['success']) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . $exchange_result['message']
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Stocker le token dans la session
|
||||
$_SESSION['misskey_token'] = $response_data['access_token'];
|
||||
*/
|
||||
$_SESSION['misskey_token'] = $exchange_result['access_token'];
|
||||
|
||||
// Nettoyer l'état OAuth
|
||||
// Nettoyer les données temporaires de la session
|
||||
unset($_SESSION['oauth_state']);
|
||||
unset($_SESSION['misskey_client_secret']);
|
||||
|
||||
// Ajouter un message de succès
|
||||
$_SESSION['messages'][] = [
|
||||
|
287
css/styles.css
287
css/styles.css
@ -1,61 +1,217 @@
|
||||
/* FavMasToKey - Styles personnalisés */
|
||||
/* 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: #f8f9fa;
|
||||
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 {
|
||||
color: #563d7c;
|
||||
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;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #563d7c;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#log-container {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#operation-log .log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#operation-log .success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
#operation-log .error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
#operation-log .info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
#operation-log .warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
/* Animation pour montrer le progrès */
|
||||
/* Animations */
|
||||
@keyframes progress-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
@ -66,18 +222,43 @@ h1 {
|
||||
animation: progress-pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Styles pour les boutons */
|
||||
.btn-primary {
|
||||
background-color: #563d7c;
|
||||
border-color: #563d7c;
|
||||
/* Customizing Bootstrap components */
|
||||
.list-group-item {
|
||||
background-color: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover, .btn-primary:focus {
|
||||
background-color: #452d6b;
|
||||
border-color: #452d6b;
|
||||
.table {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
.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;
|
||||
@ -86,4 +267,8 @@ h1 {
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
15
deploy.sh
Normal file
15
deploy.sh
Normal file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Script de déploiement simplifié pour FavMasToKey
|
||||
|
||||
# Créer les répertoires nécessaires
|
||||
mkdir -p includes css js images
|
||||
|
||||
# Définir les permissions
|
||||
chmod 755 ./ ./includes ./css ./js ./images
|
||||
chmod 644 ./*.php ./css/*.css ./js/*.js ./.htaccess
|
||||
|
||||
# Créer app_data.php
|
||||
touch includes/app_data.php
|
||||
chmod 666 includes/app_data.php
|
||||
|
||||
echo "Déploiement terminé avec succès!"
|
147
doc.php
Normal file
147
doc.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?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>Cliquez sur "Se connecter à Misskey"</li>
|
||||
<li>Vous serez redirigé vers votre instance Misskey pour autoriser l'application</li>
|
||||
<li>Suivez les instructions à l'écran pour autoriser FavMasToKey</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 lors de l'authentification avec Misskey :</p>
|
||||
<ul>
|
||||
<li>Vérifiez que vous êtes bien connecté à votre compte Misskey</li>
|
||||
<li>Assurez-vous que vous autorisez les cookies tiers dans votre navigateur</li>
|
||||
<li>Essayez de vous déconnecter puis de vous reconnecter à votre compte Misskey</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>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 OAuth</strong> - L'application s'enregistre auprès de votre instance Misskey et obtient votre autorisation</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 tokens d'authentification 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 |
0
includes/app_data.php
Normal file
0
includes/app_data.php
Normal file
@ -22,14 +22,10 @@ if (ENVIRONMENT === 'development') {
|
||||
|
||||
// Configuration de l'application
|
||||
$config = [
|
||||
// Informations de l'application (à remplir lors de la création de l'app sur Misskey)
|
||||
// Informations de l'application
|
||||
'app_name' => 'FavMasToKey',
|
||||
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
|
||||
'app_version' => '0.1.0',
|
||||
|
||||
// Paramètres OAuth - À CONFIGURER
|
||||
'client_id' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey
|
||||
'client_secret' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey
|
||||
'app_version' => '0.2.0',
|
||||
|
||||
// URLs de base
|
||||
'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
|
||||
@ -40,15 +36,43 @@ $config = [
|
||||
// Paramètres pour le traitement
|
||||
'batch_size' => 10, // Nombre de favoris à traiter en une fois
|
||||
'timeout' => 30, // Timeout des requêtes en secondes
|
||||
'max_retries' => 3 // Nombre maximal de tentatives par favori
|
||||
'max_retries' => 3, // Nombre maximal de tentatives par favori
|
||||
'delay_between_requests' => 500 // Délai entre les requêtes en millisecondes (pour éviter le rate limiting)
|
||||
];
|
||||
|
||||
// Fichier de stockage des informations d'application par instance
|
||||
$app_data_file = __DIR__ . '/app_data.php';
|
||||
|
||||
// Charger ou créer le fichier de données d'application
|
||||
if (file_exists($app_data_file)) {
|
||||
include $app_data_file;
|
||||
} else {
|
||||
// Structure initiale pour les données d'application
|
||||
$app_data = [
|
||||
'instances' => []
|
||||
];
|
||||
|
||||
// Créer le fichier avec une structure protégée
|
||||
$app_data_content = "<?php\n// Généré automatiquement - Ne pas modifier manuellement\nif (!defined('FAVMASTOKEY')) { die('Accès direct interdit'); }\n\$app_data = " . var_export($app_data, true) . ";\n?>";
|
||||
file_put_contents($app_data_file, $app_data_content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde les données d'application
|
||||
*/
|
||||
function save_app_data() {
|
||||
global $app_data, $app_data_file;
|
||||
|
||||
$app_data_content = "<?php\n// Généré automatiquement - Ne pas modifier manuellement\nif (!defined('FAVMASTOKEY')) { die('Accès direct interdit'); }\n\$app_data = " . var_export($app_data, true) . ";\n?>";
|
||||
file_put_contents($app_data_file, $app_data_content);
|
||||
}
|
||||
|
||||
// Session
|
||||
session_start();
|
||||
|
||||
// Fonctions utilitaires
|
||||
function debug($data) {
|
||||
if (ENVIRONMENT === 'development') {
|
||||
if (ENVIRONMENT === 'production') {
|
||||
echo '<pre>';
|
||||
print_r($data);
|
||||
echo '</pre>';
|
||||
|
@ -166,13 +166,221 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une application sur l'instance Misskey
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @return array Résultat de l'opération contenant client_id et client_secret
|
||||
*/
|
||||
function register_misskey_app($instance) {
|
||||
global $config;
|
||||
|
||||
// URL de l'API pour créer une application
|
||||
$url = "https://{$instance}/api/app/create";
|
||||
|
||||
// Construire l'URL de callback
|
||||
$callback_url = $config['app_url'] . '/callback.php';
|
||||
|
||||
// Données pour la création d'application
|
||||
$data = [
|
||||
'name' => $config['app_name'],
|
||||
'description' => $config['app_description'],
|
||||
'permission' => ['write:favorites'],
|
||||
'callbackUrl' => $callback_url
|
||||
];
|
||||
|
||||
// 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 && isset($response_data['id'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'client_id' => $response_data['id'],
|
||||
'client_secret' => $response_data['secret'],
|
||||
'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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Échange un code d'autorisation contre un token d'accès
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @param string $code Code d'autorisation reçu du serveur OAuth
|
||||
* @param string $client_id ID de l'application
|
||||
* @param string $client_secret Secret de l'application
|
||||
* @return array Résultat de l'opération contenant le token d'accès
|
||||
*/
|
||||
function exchange_oauth_code($instance, $code, $client_id, $client_secret) {
|
||||
global $config;
|
||||
|
||||
// URL pour l'échange du code
|
||||
$url = "https://{$instance}/oauth/token";
|
||||
|
||||
// Construire l'URL de callback (doit correspondre à celle utilisée pour l'autorisation)
|
||||
$callback_url = $config['app_url'] . '/callback.php';
|
||||
|
||||
// Données pour l'échange du code
|
||||
$data = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $callback_url
|
||||
];
|
||||
|
||||
// Initialiser cURL
|
||||
$ch = curl_init();
|
||||
|
||||
// Configurer la requête
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'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 et si un token est présent
|
||||
if ($http_code >= 200 && $http_code < 300 && isset($response_data['access_token'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'access_token' => $response_data['access_token'],
|
||||
'http_code' => $http_code
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur lors de l\'échange du code',
|
||||
'http_code' => $http_code,
|
||||
'data' => $response_data
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche une note Mastodon sur le réseau fédéré de Misskey
|
||||
*
|
||||
* @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) {
|
||||
// Endpoint de recherche
|
||||
$endpoint = '/api/notes/search-by-url';
|
||||
|
||||
// Données pour la recherche
|
||||
$data = [
|
||||
'url' => $url
|
||||
];
|
||||
|
||||
// Effectuer la requête
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une URL d'autorisation OAuth pour Misskey
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @param string $client_id ID de l'application
|
||||
* @return string URL d'autorisation
|
||||
*/
|
||||
function generate_oauth_url($instance) {
|
||||
function generate_oauth_url($instance, $client_id) {
|
||||
global $config;
|
||||
|
||||
// Générer un état aléatoire pour la sécurité
|
||||
@ -185,7 +393,7 @@ function generate_oauth_url($instance) {
|
||||
|
||||
// Paramètres de la requête d'autorisation
|
||||
$params = [
|
||||
'client_id' => $config['client_id'],
|
||||
'client_id' => $client_id,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $callback_url,
|
||||
'scope' => 'write:favorites',
|
||||
|
@ -22,10 +22,13 @@ if (isset($_SESSION['messages'])) {
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<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">
|
||||
@ -35,6 +38,7 @@ if (isset($_SESSION['messages'])) {
|
||||
<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 -->
|
||||
|
175
js/app.js
175
js/app.js
@ -26,6 +26,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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) {
|
||||
@ -71,6 +89,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// 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);
|
||||
}
|
||||
@ -120,17 +158,109 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 depuis localStorage si nécessaire
|
||||
// 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;
|
||||
@ -142,6 +272,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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
|
||||
@ -160,10 +298,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
@ -190,6 +335,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
pauseMigration.classList.add('d-none');
|
||||
pauseMigration.textContent = 'Pause';
|
||||
currentProgress.classList.remove('active');
|
||||
|
||||
// Réinitialiser les données de migration
|
||||
resetMigration();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -202,12 +350,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (currentIndex >= totalItems) {
|
||||
// Migration terminée
|
||||
isProcessing = false;
|
||||
addLogEntry('Migration terminée avec succès !', 'success');
|
||||
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');
|
||||
});
|
||||
@ -215,6 +366,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Mettre à jour la progression à 100%
|
||||
updateProgress(100);
|
||||
|
||||
// Mettre à jour le statut de la migration
|
||||
updateMigrationData('completed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -252,6 +406,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -259,7 +422,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
currentIndex = endIndex;
|
||||
|
||||
// Mettre à jour la progression
|
||||
updateProgress();
|
||||
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);
|
||||
@ -271,6 +437,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
isPaused = true;
|
||||
pauseMigration.textContent = 'Reprendre';
|
||||
currentProgress.classList.remove('active');
|
||||
|
||||
// Mettre à jour le statut de la migration
|
||||
updateMigrationData('error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
63
oauth.php
63
oauth.php
@ -40,33 +40,48 @@ if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
|
||||
// Stocker l'instance dans la session
|
||||
$_SESSION['misskey_instance'] = $instance;
|
||||
|
||||
// En production, ici nous devrions vérifier si l'application est déjà enregistrée sur cette instance
|
||||
// Si ce n'est pas le cas, il faudrait l'enregistrer via l'API Misskey
|
||||
// Pour cette version initiale, nous utilisons des valeurs simulées
|
||||
// Vérifier si cette instance est déjà enregistrée
|
||||
$client_id = null;
|
||||
$client_secret = null;
|
||||
|
||||
if (isset($app_data['instances'][$instance])) {
|
||||
$client_id = $app_data['instances'][$instance]['client_id'];
|
||||
$client_secret = $app_data['instances'][$instance]['client_secret'];
|
||||
} else {
|
||||
// L'application n'est pas encore enregistrée, on l'enregistre
|
||||
$registration = register_misskey_app($instance);
|
||||
|
||||
if ($registration['success']) {
|
||||
// Enregistrement réussi, stocker les informations
|
||||
$client_id = $registration['client_id'];
|
||||
$client_secret = $registration['client_secret'];
|
||||
|
||||
// Sauvegarder dans le fichier de données
|
||||
$app_data['instances'][$instance] = [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'registered_at' => time()
|
||||
];
|
||||
|
||||
save_app_data();
|
||||
} else {
|
||||
// Échec de l'enregistrement
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Erreur lors de l\'enregistrement de l\'application sur ' . $instance . ': ' . $registration['message']
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Stocker le client_secret dans la session pour l'utiliser plus tard
|
||||
$_SESSION['misskey_client_secret'] = $client_secret;
|
||||
|
||||
// Générer l'URL d'autorisation
|
||||
try {
|
||||
// REMARQUE: Pour une implémentation réelle, $config['client_id'] et $config['client_secret']
|
||||
// devraient être stockés par instance car chaque instance Misskey nécessite une application distincte
|
||||
|
||||
// Générer un état aléatoire pour la sécurité
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$_SESSION['oauth_state'] = $state;
|
||||
|
||||
// Construire l'URL de callback (doit correspondre à celle configurée dans l'application Misskey)
|
||||
$callback_url = $config['app_url'] . '/callback.php';
|
||||
|
||||
// Paramètres de la requête d'autorisation
|
||||
$params = [
|
||||
'client_id' => $config['client_id'] ?: 'DEMO_CLIENT_ID', // En production, utiliser une valeur réelle
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $callback_url,
|
||||
'scope' => 'write:favorites',
|
||||
'state' => $state
|
||||
];
|
||||
|
||||
// Construire l'URL d'autorisation
|
||||
$auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params);
|
||||
// Générer l'URL avec le client_id obtenu
|
||||
$auth_url = generate_oauth_url($instance, $client_id);
|
||||
|
||||
// Rediriger vers l'URL d'autorisation
|
||||
header('Location: ' . $auth_url);
|
||||
|
90
process.php
90
process.php
@ -65,61 +65,15 @@ foreach ($batch as $index => $url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extraire l'identifiant du toot
|
||||
$pathParts = explode('/', trim($urlParts['path'], '/'));
|
||||
$tootId = end($pathParts);
|
||||
// Rechercher la note sur Misskey à partir de l'URL
|
||||
$searchResult = search_federated_note($misskey_instance, $url, $token);
|
||||
|
||||
if (!is_numeric($tootId)) {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Impossible d'extraire l'ID du toot: $url"
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construire l'URL pour la recherche fédérée sur Misskey
|
||||
$searchUrl = "https://" . $urlParts['host'] . "/@" . $pathParts[count($pathParts) - 3] . "/" . $tootId;
|
||||
|
||||
// En production, ici nous ferions une recherche sur Misskey pour trouver l'équivalent du toot
|
||||
// Pour cette version initiale, nous simulons la réussite/échec
|
||||
|
||||
// Simulation : 90% de réussite, 10% d'échec
|
||||
$success = (rand(1, 10) <= 9);
|
||||
|
||||
if ($success) {
|
||||
// Simulation de l'ajout aux favoris
|
||||
$results[] = [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url"
|
||||
];
|
||||
} else {
|
||||
// Simulation d'erreur
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Impossible d'ajouter aux favoris: $url (publication introuvable ou inaccessible)"
|
||||
];
|
||||
}
|
||||
|
||||
// Dans un cas réel, nous aurions un code similaire à celui-ci:
|
||||
/*
|
||||
// Rechercher la note sur Misskey
|
||||
$searchData = [
|
||||
'query' => $searchUrl,
|
||||
'limit' => 1
|
||||
];
|
||||
|
||||
$searchResult = misskey_api_request($misskey_instance, '/api/notes/search', $searchData, $token);
|
||||
|
||||
if ($searchResult['success'] && !empty($searchResult['data'])) {
|
||||
// Récupérer l'ID de la note trouvée
|
||||
$noteId = $searchResult['data'][0]['id'];
|
||||
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
|
||||
// Note trouvée, récupérer son ID
|
||||
$noteId = $searchResult['data']['id'];
|
||||
|
||||
// Ajouter aux favoris
|
||||
$favoriteData = [
|
||||
'noteId' => $noteId
|
||||
];
|
||||
|
||||
$favoriteResult = misskey_api_request($misskey_instance, '/api/notes/favorites/create', $favoriteData, $token);
|
||||
// Tenter d'ajouter la note aux favoris
|
||||
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteResult['success']) {
|
||||
$results[] = [
|
||||
@ -127,21 +81,35 @@ foreach ($batch as $index => $url) {
|
||||
'message' => "Ajouté aux favoris: $url"
|
||||
];
|
||||
} else {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['message']
|
||||
];
|
||||
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
|
||||
$errorMessage = isset($favoriteResult['data']['error']['message'])
|
||||
? $favoriteResult['data']['error']['message']
|
||||
: $favoriteResult['message'];
|
||||
|
||||
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 {
|
||||
// Note non trouvée
|
||||
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
|
||||
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Publication introuvable sur Misskey: $url"
|
||||
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)"
|
||||
];
|
||||
}
|
||||
*/
|
||||
|
||||
// Pause pour éviter de surcharger l'API (à utiliser en production)
|
||||
// usleep(200000); // 200 ms
|
||||
// Pause pour éviter le rate limiting
|
||||
usleep($config['delay_between_requests'] * 1000);
|
||||
}
|
||||
|
||||
// Renvoyer les résultats
|
||||
|
Loading…
x
Reference in New Issue
Block a user