initialisation du projet

This commit is contained in:
Esenjin 2025-03-20 21:42:31 +01:00
parent c43c42a34e
commit 32a61fdb81
9 changed files with 1256 additions and 0 deletions

72
.htaccess Normal file

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

128
callback.php Normal file

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

89
css/styles.css Normal file

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

56
includes/config.php Normal file

@ -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>';
}
}

199
includes/functions.php Normal file

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

151
index.php Normal file

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

323
js/app.js Normal file

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

82
oauth.php Normal file

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

156
process.php Normal file

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