diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..b9e5b64 --- /dev/null +++ b/.htaccess @@ -0,0 +1,72 @@ +# FavMasToKey - Configuration Apache + +# 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> + +# Bloquer l'accès au répertoire includes +<IfModule mod_rewrite.c> + RewriteRule ^includes/ - [F,L] +</IfModule> + +# Bloquer l'accès aux fichiers cachés +<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 +<LimitExcept GET POST HEAD> + Order Allow,Deny + Deny from all +</LimitExcept> + +# PHP settings +<IfModule mod_php.c> + # Désactiver l'affichage des erreurs en production + # php_flag display_errors Off + + # Limiter le temps d'exécution des scripts + php_value max_execution_time 120 + + # Limiter la taille des téléchargements + php_value upload_max_filesize 10M + php_value post_max_size 10M + + # Sécuriser les cookies de session + php_value session.cookie_httponly 1 + php_value session.use_only_cookies 1 + + # Utiliser des cookies sécurisés en production + # php_value session.cookie_secure 1 +</IfModule> \ No newline at end of file diff --git a/callback.php b/callback.php new file mode 100644 index 0000000..dbd47e6 --- /dev/null +++ b/callback.php @@ -0,0 +1,128 @@ +<?php +/** + * FavMasToKey - Callback OAuth pour Misskey + */ + +// Définir la constante pour inclure les fichiers +define('FAVMASTOKEY', true); + +// Inclure les fichiers requis +require_once 'includes/config.php'; +require_once 'includes/functions.php'; + +// Vérifier si l'état est valide (protection CSRF) +if (!isset($_GET['state']) || !isset($_SESSION['oauth_state']) || $_GET['state'] !== $_SESSION['oauth_state']) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Paramètre d\'état invalide. Veuillez réessayer.' + ]; + header('Location: index.php'); + exit; +} + +// Vérifier si le code d'autorisation est présent +if (!isset($_GET['code']) || empty($_GET['code'])) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Aucun code d\'autorisation reçu. L\'authentification a échoué ou a été annulée.' + ]; + header('Location: index.php'); + exit; +} + +// Récupérer l'instance Misskey depuis la session +if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance'])) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Instance Misskey non définie. Veuillez recommencer.' + ]; + header('Location: index.php'); + 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) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . ($error ?: 'HTTP ' . $http_code) + ]; + header('Location: index.php'); + exit; +} + +// Décoder la réponse +$response_data = json_decode($response, true); + +// Vérifier que le token est présent +if (!isset($response_data['access_token']) || empty($response_data['access_token'])) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Aucun token d\'accès reçu. L\'authentification a échoué.' + ]; + header('Location: index.php'); + exit; +} + +// Stocker le token dans la session +$_SESSION['misskey_token'] = $response_data['access_token']; +*/ + +// Nettoyer l'état OAuth +unset($_SESSION['oauth_state']); + +// Ajouter un message de succès +$_SESSION['messages'][] = [ + 'type' => 'success', + 'text' => 'Connecté avec succès à ' . $instance . '.' +]; + +// Rediriger vers la page de migration (étape 3) +header('Location: index.php#step3'); \ No newline at end of file diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..9a49066 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,89 @@ +/* FavMasToKey - Styles personnalisés */ + +body { + background-color: #f8f9fa; +} + +.container { + max-width: 900px; +} + +h1 { + color: #563d7c; + margin-bottom: 0.5rem; +} + +.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 */ +@keyframes progress-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +.progress-bar.active { + animation: progress-pulse 2s infinite; +} + +/* Styles pour les boutons */ +.btn-primary { + background-color: #563d7c; + border-color: #563d7c; +} + +.btn-primary:hover, .btn-primary:focus { + background-color: #452d6b; + border-color: #452d6b; +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .card-body { + padding: 1.25rem; + } +} \ No newline at end of file diff --git a/includes/config.php b/includes/config.php new file mode 100644 index 0000000..679b10c --- /dev/null +++ b/includes/config.php @@ -0,0 +1,56 @@ +<?php +/** + * FavMasToKey - Configuration + */ + +// Empêcher l'accès direct au fichier +if (!defined('FAVMASTOKEY')) { + die('Accès direct interdit'); +} + +// Environnement (development ou production) +define('ENVIRONMENT', 'development'); + +// Gestion des erreurs selon l'environnement +if (ENVIRONMENT === 'development') { + error_reporting(E_ALL); + ini_set('display_errors', 1); +} else { + error_reporting(0); + ini_set('display_errors', 0); +} + +// Configuration de l'application +$config = [ + // Informations de l'application (à remplir lors de la création de l'app sur Misskey) + '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 + + // URLs de base + 'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), + + // Paramètres Misskey API + 'misskey_api_endpoint' => '/api/notes/favorites/create', + + // 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 +]; + +// Session +session_start(); + +// Fonctions utilitaires +function debug($data) { + if (ENVIRONMENT === 'development') { + echo '<pre>'; + print_r($data); + echo '</pre>'; + } +} \ No newline at end of file diff --git a/includes/functions.php b/includes/functions.php new file mode 100644 index 0000000..7182c02 --- /dev/null +++ b/includes/functions.php @@ -0,0 +1,199 @@ +<?php +/** + * FavMasToKey - Fonctions utilitaires + */ + +// Empêcher l'accès direct au fichier +if (!defined('FAVMASTOKEY')) { + die('Accès direct interdit'); +} + +/** + * Valide un fichier JSON de favoris Mastodon + * + * @param string $file_path Chemin vers le fichier JSON + * @return array|bool Tableau contenant les données du fichier ou false en cas d'erreur + */ +function validate_mastodon_json($file_path) { + // Vérifier si le fichier existe + if (!file_exists($file_path)) { + return [ + 'success' => false, + 'message' => 'Le fichier n\'existe pas.' + ]; + } + + // Lire le contenu du fichier + $content = file_get_contents($file_path); + if (!$content) { + return [ + 'success' => false, + 'message' => 'Impossible de lire le contenu du fichier.' + ]; + } + + // Décoder le JSON + $json = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + 'success' => false, + 'message' => 'Le fichier n\'est pas un JSON valide: ' . json_last_error_msg() + ]; + } + + // Vérifier la structure du fichier + if (!isset($json['@context']) || !isset($json['type']) || !isset($json['orderedItems'])) { + return [ + 'success' => false, + 'message' => 'Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.' + ]; + } + + // Vérifier que orderedItems est un tableau + if (!is_array($json['orderedItems'])) { + return [ + 'success' => false, + 'message' => 'Le format des favoris dans le fichier est invalide.' + ]; + } + + // Tout est OK + return [ + 'success' => true, + 'data' => $json, + 'count' => count($json['orderedItems']) + ]; +} + +/** + * Extrait les identifiants de publications à partir des URLs Mastodon + * + * @param array $urls Tableau d'URLs Mastodon + * @return array Tableau d'identifiants extraits + */ +function extract_toot_ids($urls) { + $ids = []; + + foreach ($urls as $url) { + // Format attendu: https://instance.tld/users/username/statuses/id + $parts = explode('/', $url); + + // L'ID devrait être le dernier élément après "statuses" + $id = end($parts); + + if (is_numeric($id)) { + $ids[] = [ + 'original_url' => $url, + 'toot_id' => $id, + 'instance' => parse_url($url, PHP_URL_HOST), + 'username' => isset($parts[count($parts) - 3]) ? $parts[count($parts) - 3] : null + ]; + } + } + + return $ids; +} + +/** + * Effectue une requête cURL vers l'API Misskey + * + * @param string $instance Instance Misskey (ex: misskey.io) + * @param string $endpoint Point d'accès API (ex: /api/notes/favorites/create) + * @param array $data Données à envoyer + * @param string $token Token d'accès OAuth + * @return array Résultat de la requête + */ +function misskey_api_request($instance, $endpoint, $data, $token) { + global $config; + + // Construire l'URL complète + $url = "https://{$instance}{$endpoint}"; + + // Ajouter le token d'accès aux données + $data['i'] = $token; + + // Initialiser cURL + $ch = curl_init(); + + // Configurer la requête + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'User-Agent: FavMasToKey/' . $config['app_version'] + ], + CURLOPT_TIMEOUT => $config['timeout'], + CURLOPT_SSL_VERIFYPEER => true + ]); + + // Exécuter la requête + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + // Fermer la session cURL + curl_close($ch); + + // Vérifier les erreurs + if ($error) { + return [ + 'success' => false, + 'message' => 'Erreur cURL: ' . $error, + 'http_code' => $http_code + ]; + } + + // Décoder la réponse + $response_data = json_decode($response, true); + + // Vérifier si la requête a réussi + if ($http_code >= 200 && $http_code < 300) { + return [ + 'success' => true, + 'data' => $response_data, + 'http_code' => $http_code + ]; + } else { + return [ + 'success' => false, + 'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur API Misskey', + 'http_code' => $http_code, + 'data' => $response_data + ]; + } +} + +/** + * Génère une URL d'autorisation OAuth pour Misskey + * + * @param string $instance Instance Misskey + * @return string URL d'autorisation + */ +function generate_oauth_url($instance) { + global $config; + + // Générer un état aléatoire pour la sécurité + $state = bin2hex(random_bytes(16)); + $_SESSION['oauth_state'] = $state; + $_SESSION['misskey_instance'] = $instance; + + // Construire l'URL de callback + $callback_url = $config['app_url'] . '/callback.php'; + + // Paramètres de la requête d'autorisation + $params = [ + 'client_id' => $config['client_id'], + '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); + + return $auth_url; +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..a7123be --- /dev/null +++ b/index.php @@ -0,0 +1,151 @@ +<?php +/** + * FavMasToKey - Page d'accueil + */ + +// Définir la constante pour inclure les fichiers +define('FAVMASTOKEY', true); + +// Inclure les fichiers requis +require_once 'includes/config.php'; +require_once 'includes/functions.php'; + +// Vérifier si l'utilisateur est authentifié +$is_authenticated = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']); +$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : ''; + +// Initialiser les messages +$messages = []; +if (isset($_SESSION['messages'])) { + $messages = $_SESSION['messages']; + unset($_SESSION['messages']); +} +?> +<!DOCTYPE html> +<html lang="fr"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>FavMasToKey - Transférer vos favoris de Mastodon vers Misskey</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="css/styles.css"> +</head> +<body> + <div class="container py-5"> + <header class="text-center mb-5"> + <h1>FavMasToKey</h1> + <p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p> + </header> + + <!-- Messages d'alerte --> + <?php if (!empty($messages)): ?> + <?php foreach ($messages as $message): ?> + <div class="alert alert-<?php echo $message['type']; ?> alert-dismissible fade show" role="alert"> + <?php echo $message['text']; ?> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button> + </div> + <?php endforeach; ?> + <?php endif; ?> + + <div class="row justify-content-center"> + <div class="col-md-8"> + <div class="card shadow-sm"> + <div class="card-body"> + <div class="steps"> + <!-- Étape 1: Téléchargement du fichier JSON --> + <div class="step" id="step1"> + <h3 class="card-title">1. Importer vos favoris Mastodon</h3> + <p>Téléchargez d'abord votre fichier d'export de favoris depuis Mastodon.</p> + + <div class="card bg-light mb-3"> + <div class="card-body"> + <h5>Comment obtenir mon fichier de favoris ?</h5> + <ol> + <li>Connectez-vous à votre compte Mastodon</li> + <li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li> + <li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li> + <li>Une fois le fichier prêt, téléchargez-le</li> + </ol> + </div> + </div> + + <form id="upload-form" class="mt-4"> + <div class="mb-3"> + <label for="json-file" class="form-label">Fichier JSON des favoris</label> + <input type="file" class="form-control" id="json-file" name="json_file" accept=".json" required> + </div> + <button type="submit" class="btn btn-primary">Analyser le fichier</button> + </form> + </div> + + <!-- Étape 2: Connexion à Misskey --> + <div class="step d-none" id="step2"> + <h3 class="card-title">2. Connexion à Misskey</h3> + <p>Connectez-vous à votre compte Misskey pour y importer vos favoris.</p> + + <div id="file-summary" class="alert alert-info mb-4"></div> + + <form id="misskey-form" class="mt-4"> + <div class="mb-3"> + <label for="misskey-instance" class="form-label">Instance Misskey</label> + <input type="url" class="form-control" id="misskey-instance" name="misskey_instance" + placeholder="https://misskey.io" required> + <div class="form-text">Entrez l'URL complète de votre instance Misskey (ex: https://misskey.io)</div> + </div> + <button type="submit" class="btn btn-primary">Se connecter à Misskey</button> + <button type="button" class="btn btn-link" id="back-to-step1">Retour</button> + </form> + </div> + + <!-- Étape 3: Migration des favoris --> + <div class="step d-none" id="step3"> + <h3 class="card-title">3. Migration des favoris</h3> + <p>Nous allons maintenant transférer vos favoris vers Misskey.</p> + + <?php if ($is_authenticated): ?> + <div class="alert alert-success mb-4"> + <strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong> + <p class="mb-0">Vous êtes authentifié et prêt à importer vos favoris.</p> + </div> + <?php endif; ?> + + <div class="mb-4"> + <label class="form-label">Progression globale</label> + <div class="progress" style="height: 20px;"> + <div id="global-progress" class="progress-bar" role="progressbar" + style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> + </div> + </div> + + <div class="mb-4"> + <label class="form-label">Opération en cours</label> + <div class="progress"> + <div id="current-progress" class="progress-bar bg-info" role="progressbar" + style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> + </div> + </div> + + <div class="mb-4"> + <h5>Journal des opérations</h5> + <div id="log-container" class="border p-3 bg-light" style="max-height: 200px; overflow-y: auto;"> + <div id="operation-log"></div> + </div> + </div> + + <div class="d-flex justify-content-between"> + <button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button> + <button type="button" class="btn btn-warning d-none" id="pause-migration">Pause</button> + <button type="button" class="btn btn-danger" id="cancel-migration">Annuler</button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + <script src="js/app.js"></script> +</body> +</html> \ No newline at end of file diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..ad9f085 --- /dev/null +++ b/js/app.js @@ -0,0 +1,323 @@ +/** + * FavMasToKey - Script JavaScript principal + */ + +// Attendre que le DOM soit chargé +document.addEventListener('DOMContentLoaded', function() { + // Éléments DOM + const uploadForm = document.getElementById('upload-form'); + const misskeyForm = document.getElementById('misskey-form'); + const jsonFileInput = document.getElementById('json-file'); + const step1 = document.getElementById('step1'); + const step2 = document.getElementById('step2'); + const step3 = document.getElementById('step3'); + const fileSummary = document.getElementById('file-summary'); + const backToStep1 = document.getElementById('back-to-step1'); + const startMigration = document.getElementById('start-migration'); + const pauseMigration = document.getElementById('pause-migration'); + const cancelMigration = document.getElementById('cancel-migration'); + const globalProgress = document.getElementById('global-progress'); + const currentProgress = document.getElementById('current-progress'); + const operationLog = document.getElementById('operation-log'); + + // Variables globales + let favoritesList = []; + let currentIndex = 0; + let totalItems = 0; + let isProcessing = false; + let isPaused = false; + + // Gérer le téléchargement et l'analyse du fichier JSON + if (uploadForm) { + uploadForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const file = jsonFileInput.files[0]; + if (!file) { + alert('Veuillez sélectionner un fichier JSON.'); + return; + } + + // Vérifier l'extension du fichier + if (!file.name.endsWith('.json')) { + alert('Le fichier doit être au format JSON.'); + return; + } + + // Lire le fichier + const reader = new FileReader(); + reader.onload = function(event) { + try { + const json = JSON.parse(event.target.result); + + // Vérifier la structure du fichier + if (!json['@context'] || !json.type || !json.orderedItems) { + alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.'); + return; + } + + favoritesList = json.orderedItems; + totalItems = favoritesList.length; + + // Afficher un résumé + fileSummary.innerHTML = ` + <strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon. + `; + + // Passer à l'étape 2 + step1.classList.add('d-none'); + step2.classList.remove('d-none'); + + // Stocker les données dans localStorage pour les conserver + localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList)); + + } catch (error) { + alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message); + } + }; + + reader.onerror = function() { + alert('Erreur lors de la lecture du fichier.'); + }; + + reader.readAsText(file); + }); + } + + // Gestion du retour à l'étape 1 + if (backToStep1) { + backToStep1.addEventListener('click', function() { + step2.classList.add('d-none'); + step1.classList.remove('d-none'); + }); + } + + // Gérer la connexion à Misskey + if (misskeyForm) { + misskeyForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const instanceInput = document.getElementById('misskey-instance'); + let instance = instanceInput.value.trim(); + + // Vérifier que l'instance est valide + if (!instance) { + alert('Veuillez entrer l\'URL de votre instance Misskey.'); + return; + } + + // Supprimer le protocole et les slash de fin si présents + instance = instance.replace(/^https?:\/\//, '').replace(/\/$/, ''); + + // Vérifier que l'URL semble valide + if (!/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(instance)) { + alert('L\'URL de l\'instance Misskey semble invalide.'); + return; + } + + // Rediriger vers l'authentification OAuth + window.location.href = `oauth.php?instance=${encodeURIComponent(instance)}`; + }); + } + + // 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 + if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) { + favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); + totalItems = favoritesList.length; + } + + if (favoritesList.length === 0) { + addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error'); + return; + } + + // Démarrer la migration + isProcessing = true; + isPaused = false; + startMigration.classList.add('d-none'); + pauseMigration.classList.remove('d-none'); + + addLogEntry('Démarrage de la migration...', 'info'); + + // Lancer le processus de migration + processBatch(); + }); + } + + // Gérer la pause de la migration + if (pauseMigration) { + pauseMigration.addEventListener('click', function() { + if (!isProcessing) return; + + isPaused = !isPaused; + + if (isPaused) { + pauseMigration.textContent = 'Reprendre'; + addLogEntry('Migration en pause.', 'warning'); + currentProgress.classList.remove('active'); + } else { + pauseMigration.textContent = 'Pause'; + addLogEntry('Reprise de la migration...', 'info'); + currentProgress.classList.add('active'); + processBatch(); + } + }); + } + + // Gérer l'annulation de la migration + if (cancelMigration) { + cancelMigration.addEventListener('click', function() { + if (!isProcessing && currentIndex === 0) { + // Retour à l'étape 1 si rien n'a commencé + step3.classList.add('d-none'); + step1.classList.remove('d-none'); + return; + } + + const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?'); + if (confirmCancel) { + isProcessing = false; + isPaused = false; + addLogEntry('Migration annulée.', 'error'); + + // Réinitialiser l'interface + startMigration.classList.remove('d-none'); + pauseMigration.classList.add('d-none'); + pauseMigration.textContent = 'Pause'; + currentProgress.classList.remove('active'); + } + }); + } + + /** + * Traite un lot de favoris + */ + function processBatch() { + if (!isProcessing || isPaused || currentIndex >= totalItems) { + if (currentIndex >= totalItems) { + // Migration terminée + isProcessing = false; + addLogEntry('Migration terminée avec succès !', 'success'); + startMigration.classList.remove('d-none'); + startMigration.textContent = 'Terminer'; + startMigration.addEventListener('click', function() { + // Nettoyer localStorage et retourner à l'étape 1 + localStorage.removeItem('favmastokey_favorites'); + step3.classList.add('d-none'); + step1.classList.remove('d-none'); + }); + pauseMigration.classList.add('d-none'); + + // Mettre à jour la progression à 100% + updateProgress(100); + } + return; + } + + // Nombre d'éléments à traiter dans ce lot + const batchSize = 5; + const endIndex = Math.min(currentIndex + batchSize, totalItems); + + // Préparer les éléments du lot + const batch = favoritesList.slice(currentIndex, endIndex); + + // Mettre à jour la progression actuelle + currentProgress.classList.add('active'); + updateProgress(); + + // Simuler le traitement (à remplacer par l'appel API réel) + addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info'); + + // Envoyer la requête au serveur + fetch('process.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + batch: batch, + currentIndex: currentIndex, + totalItems: totalItems + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Traiter les résultats + if (data.results && data.results.length) { + data.results.forEach(result => { + addLogEntry(result.message, result.status); + }); + } + + // Mettre à jour l'index + currentIndex = endIndex; + + // Mettre à jour la progression + updateProgress(); + + // Traiter le lot suivant après un court délai + setTimeout(processBatch, 1000); + } else { + // Gérer l'erreur + addLogEntry('Erreur: ' + data.message, 'error'); + + // Pause en cas d'erreur + isPaused = true; + pauseMigration.textContent = 'Reprendre'; + currentProgress.classList.remove('active'); + } + }) + .catch(error => { + // Gérer l'erreur réseau + addLogEntry('Erreur de connexion: ' + error.message, 'error'); + + // Pause en cas d'erreur + isPaused = true; + pauseMigration.textContent = 'Reprendre'; + currentProgress.classList.remove('active'); + }); + } + + /** + * Met à jour la barre de progression + */ + function updateProgress(forcedValue = null) { + const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100; + globalProgress.style.width = progress + '%'; + globalProgress.textContent = Math.round(progress) + '%'; + globalProgress.setAttribute('aria-valuenow', progress); + + if (forcedValue === null) { + // Mettre à jour la progression actuelle + const batchProgress = ((currentIndex % 5) / 5) * 100; + currentProgress.style.width = batchProgress + '%'; + currentProgress.setAttribute('aria-valuenow', batchProgress); + } else { + currentProgress.style.width = '100%'; + currentProgress.setAttribute('aria-valuenow', 100); + } + } + + /** + * Ajoute une entrée dans le journal des opérations + */ + function addLogEntry(message, status = 'info') { + const entry = document.createElement('div'); + entry.className = `log-entry ${status}`; + + const timestamp = new Date().toLocaleTimeString(); + entry.textContent = `[${timestamp}] ${message}`; + + operationLog.appendChild(entry); + + // Scroll vers le bas pour voir la dernière entrée + const logContainer = document.getElementById('log-container'); + logContainer.scrollTop = logContainer.scrollHeight; + } +}); \ No newline at end of file diff --git a/oauth.php b/oauth.php new file mode 100644 index 0000000..da6d5d0 --- /dev/null +++ b/oauth.php @@ -0,0 +1,82 @@ +<?php +/** + * FavMasToKey - Authentification OAuth avec Misskey + */ + +// Définir la constante pour inclure les fichiers +define('FAVMASTOKEY', true); + +// Inclure les fichiers requis +require_once 'includes/config.php'; +require_once 'includes/functions.php'; + +// Vérifier si l'instance est fournie +if (!isset($_GET['instance']) || empty($_GET['instance'])) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Aucune instance Misskey spécifiée.' + ]; + header('Location: index.php'); + exit; +} + +// Récupérer l'instance +$instance = trim($_GET['instance']); + +// Supprimer le protocole et les slash de fin si présents +$instance = preg_replace('/^https?:\/\//', '', $instance); +$instance = rtrim($instance, '/'); + +// Vérifier que l'instance semble valide +if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'L\'URL de l\'instance Misskey semble invalide.' + ]; + header('Location: index.php'); + exit; +} + +// 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 + +// 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); + + // Rediriger vers l'URL d'autorisation + header('Location: ' . $auth_url); + exit; + +} catch (Exception $e) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Erreur lors de la préparation de l\'authentification: ' . $e->getMessage() + ]; + header('Location: index.php'); + exit; +} \ No newline at end of file diff --git a/process.php b/process.php new file mode 100644 index 0000000..c9f88ac --- /dev/null +++ b/process.php @@ -0,0 +1,156 @@ +<?php +/** + * FavMasToKey - Traitement des favoris + */ + +// Définir la constante pour inclure les fichiers +define('FAVMASTOKEY', true); + +// Inclure les fichiers requis +require_once 'includes/config.php'; +require_once 'includes/functions.php'; + +// Vérifier que la requête est en POST +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']); + exit; +} + +// Vérifier que l'utilisateur est authentifié +if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) { + http_response_code(401); + echo json_encode(['success' => false, 'message' => 'Non authentifié']); + exit; +} + +// Récupérer l'instance Misskey +$misskey_instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : ''; +if (empty($misskey_instance)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Instance Misskey non définie']); + exit; +} + +// Récupérer le token d'accès +$token = $_SESSION['misskey_token']; + +// Récupérer les données envoyées +$input = json_decode(file_get_contents('php://input'), true); +if (!$input || !isset($input['batch']) || !is_array($input['batch'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Données invalides']); + exit; +} + +// Récupérer le lot à traiter +$batch = $input['batch']; +$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0; +$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch); + +// Résultats du traitement +$results = []; + +// Traiter chaque URL du lot +foreach ($batch as $index => $url) { + // Extraire les informations de l'URL + $urlParts = parse_url($url); + + // Vérifier que l'URL est valide + if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) { + $results[] = [ + 'status' => 'error', + 'message' => "URL invalide: $url" + ]; + continue; + } + + // Extraire l'identifiant du toot + $pathParts = explode('/', trim($urlParts['path'], '/')); + $tootId = end($pathParts); + + 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']; + + // Ajouter aux favoris + $favoriteData = [ + 'noteId' => $noteId + ]; + + $favoriteResult = misskey_api_request($misskey_instance, '/api/notes/favorites/create', $favoriteData, $token); + + if ($favoriteResult['success']) { + $results[] = [ + 'status' => 'success', + 'message' => "Ajouté aux favoris: $url" + ]; + } else { + $results[] = [ + 'status' => 'error', + 'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['message'] + ]; + } + } else { + $results[] = [ + 'status' => 'error', + 'message' => "Publication introuvable sur Misskey: $url" + ]; + } + */ + + // Pause pour éviter de surcharger l'API (à utiliser en production) + // usleep(200000); // 200 ms +} + +// Renvoyer les résultats +echo json_encode([ + 'success' => true, + 'results' => $results, + 'progress' => [ + 'current' => $currentIndex + count($batch), + 'total' => $totalItems, + 'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2) + ] +]); \ No newline at end of file