améliorations pour la mise en production

This commit is contained in:
Esenjin 2025-03-20 22:20:19 +01:00
parent 32a61fdb81
commit e6cee6d426
13 changed files with 1007 additions and 244 deletions

@ -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

@ -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'][] = [

@ -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

@ -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

@ -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 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

@ -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

@ -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

@ -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 => {

@ -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);

@ -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