améliorations pour la mise en production
This commit is contained in:
parent
32a61fdb81
commit
e6cee6d426
97
.htaccess
97
.htaccess
@ -3,58 +3,75 @@
|
|||||||
# Activer le moteur de réécriture
|
# Activer le moteur de réécriture
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
|
||||||
# Protéger les fichiers sensibles
|
# Forcer HTTPS (à activer en production en supprimant le commentaire)
|
||||||
<FilesMatch "^(config\.php|functions\.php)$">
|
# RewriteCond %{HTTPS} !=on
|
||||||
Order Allow,Deny
|
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
Deny from all
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
# Bloquer l'accès au répertoire includes
|
# Protéger le répertoire includes
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteRule ^includes/ - [F,L]
|
RewriteRule ^includes/ - [F,L]
|
||||||
</IfModule>
|
</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 "^\.">
|
<FilesMatch "^\.">
|
||||||
Order Allow,Deny
|
Order Allow,Deny
|
||||||
Deny from all
|
Deny from all
|
||||||
</FilesMatch>
|
</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
|
# Désactiver l'affichage du contenu des répertoires
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
# Limiter les méthodes HTTP
|
# Limiter les méthodes HTTP autorisées
|
||||||
<LimitExcept GET POST HEAD>
|
<LimitExcept GET POST HEAD>
|
||||||
Order Allow,Deny
|
Order Allow,Deny
|
||||||
Deny from all
|
Deny from all
|
||||||
</LimitExcept>
|
</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>
|
<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_errors Off
|
||||||
|
# php_flag display_startup_errors Off
|
||||||
|
# php_value error_reporting 0
|
||||||
|
|
||||||
# Limiter le temps d'exécution des scripts
|
# Limiter le temps d'exécution des scripts
|
||||||
php_value max_execution_time 120
|
php_value max_execution_time 120
|
||||||
@ -63,10 +80,22 @@ Options -Indexes
|
|||||||
php_value upload_max_filesize 10M
|
php_value upload_max_filesize 10M
|
||||||
php_value post_max_size 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.cookie_httponly 1
|
||||||
php_value session.use_only_cookies 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
|
# php_value session.cookie_secure 1
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
# Compresser les fichiers texte pour réduire la taille de transfert
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Protection contre les scans de vulnérabilités communes
|
||||||
|
RedirectMatch 404 (?i)\.php\.suspected
|
||||||
|
RedirectMatch 404 (?i)wp-login\.php
|
||||||
|
RedirectMatch 404 (?i)wp-admin
|
||||||
|
RedirectMatch 404 (?i)xmlrpc\.php
|
85
callback.php
85
callback.php
@ -40,83 +40,50 @@ if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance']
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$instance = $_SESSION['misskey_instance'];
|
// Récupérer le client_secret depuis la session
|
||||||
$code = $_GET['code'];
|
if (!isset($_SESSION['misskey_client_secret']) || empty($_SESSION['misskey_client_secret'])) {
|
||||||
|
|
||||||
// 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'][] = [
|
$_SESSION['messages'][] = [
|
||||||
'type' => 'danger',
|
'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');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Décoder la réponse
|
$instance = $_SESSION['misskey_instance'];
|
||||||
$response_data = json_decode($response, true);
|
$code = $_GET['code'];
|
||||||
|
$client_secret = $_SESSION['misskey_client_secret'];
|
||||||
|
|
||||||
// Vérifier que le token est présent
|
// Récupérer le client_id depuis les données d'application
|
||||||
if (!isset($response_data['access_token']) || empty($response_data['access_token'])) {
|
if (!isset($app_data['instances'][$instance]) || !isset($app_data['instances'][$instance]['client_id'])) {
|
||||||
$_SESSION['messages'][] = [
|
$_SESSION['messages'][] = [
|
||||||
'type' => 'danger',
|
'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');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stocker le token dans la session
|
// 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['oauth_state']);
|
||||||
|
unset($_SESSION['misskey_client_secret']);
|
||||||
|
|
||||||
// Ajouter un message de succès
|
// Ajouter un message de succès
|
||||||
$_SESSION['messages'][] = [
|
$_SESSION['messages'][] = [
|
||||||
|
287
css/styles.css
287
css/styles.css
@ -1,61 +1,217 @@
|
|||||||
/* FavMasToKey - Styles personnalisés */
|
/* FavMasToKey - Thème sombre */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-dark: #121212;
|
||||||
|
--bg-card: #1e1e1e;
|
||||||
|
--bg-input: #2a2a2a;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #b0b0b0;
|
||||||
|
--text-muted: #8a8a8a;
|
||||||
|
--primary-color: #7e57c2;
|
||||||
|
--primary-hover: #9575cd;
|
||||||
|
--success-color: #4caf50;
|
||||||
|
--info-color: #29b6f6;
|
||||||
|
--warning-color: #ffb74d;
|
||||||
|
--danger-color: #f44336;
|
||||||
|
--border-color: #333333;
|
||||||
|
--card-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
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 {
|
.container {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #563d7c;
|
color: var(--text-primary);
|
||||||
margin-bottom: 0.5rem;
|
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 {
|
.step {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
/* Animations */
|
||||||
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 {
|
@keyframes progress-pulse {
|
||||||
0% { opacity: 1; }
|
0% { opacity: 1; }
|
||||||
50% { opacity: 0.7; }
|
50% { opacity: 0.7; }
|
||||||
@ -66,18 +222,43 @@ h1 {
|
|||||||
animation: progress-pulse 2s infinite;
|
animation: progress-pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles pour les boutons */
|
/* Customizing Bootstrap components */
|
||||||
.btn-primary {
|
.list-group-item {
|
||||||
background-color: #563d7c;
|
background-color: var(--bg-card);
|
||||||
border-color: #563d7c;
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover, .btn-primary:focus {
|
.table {
|
||||||
background-color: #452d6b;
|
color: var(--text-primary);
|
||||||
border-color: #452d6b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@ -86,4 +267,8 @@ h1 {
|
|||||||
.card-body {
|
.card-body {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
15
deploy.sh
Normal file
15
deploy.sh
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de déploiement simplifié pour FavMasToKey
|
||||||
|
|
||||||
|
# Créer les répertoires nécessaires
|
||||||
|
mkdir -p includes css js images
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod 755 ./ ./includes ./css ./js ./images
|
||||||
|
chmod 644 ./*.php ./css/*.css ./js/*.js ./.htaccess
|
||||||
|
|
||||||
|
# Créer app_data.php
|
||||||
|
touch includes/app_data.php
|
||||||
|
chmod 666 includes/app_data.php
|
||||||
|
|
||||||
|
echo "Déploiement terminé avec succès!"
|
147
doc.php
Normal file
147
doc.php
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* FavMasToKey - Documentation d'utilisation
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Définir la constante pour inclure les fichiers
|
||||||
|
define('FAVMASTOKEY', true);
|
||||||
|
|
||||||
|
// Inclure les fichiers requis
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#7e57c2">
|
||||||
|
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
|
||||||
|
<meta name="description" content="FavMasToKey - Documentation d'utilisation pour transférer vos favoris de Mastodon vers Misskey">
|
||||||
|
<title>Documentation - FavMasToKey</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container py-5">
|
||||||
|
<header class="text-center mb-5">
|
||||||
|
<h1>FavMasToKey</h1>
|
||||||
|
<p class="lead">Documentation d'utilisation</p>
|
||||||
|
<p><a href="index.php" class="btn btn-primary">Retour à l'application</a></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Guide étape par étape</h2>
|
||||||
|
|
||||||
|
<h3>1. Préparation</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4>1.1 Obtenir vos favoris depuis Mastodon</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Connectez-vous à votre compte Mastodon</li>
|
||||||
|
<li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li>
|
||||||
|
<li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li>
|
||||||
|
<li>Une fois le fichier prêt, téléchargez-le</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>1.2 Préparer votre compte Misskey</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Assurez-vous d'être connecté à votre compte Misskey</li>
|
||||||
|
<li>Vérifiez que vous avez suffisamment d'espace pour de nouveaux favoris</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>2. Utilisation de FavMasToKey</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4>2.1 Télécharger le fichier JSON</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Sur la page d'accueil de FavMasToKey, cliquez sur "Parcourir" pour sélectionner votre fichier JSON de favoris</li>
|
||||||
|
<li>Cliquez sur "Analyser le fichier" pour continuer</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>2.2 Connexion à votre compte Misskey</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Entrez l'URL de votre instance Misskey (ex: misskey.io)</li>
|
||||||
|
<li>Cliquez sur "Se connecter à Misskey"</li>
|
||||||
|
<li>Vous serez redirigé vers votre instance Misskey pour autoriser l'application</li>
|
||||||
|
<li>Suivez les instructions à l'écran pour autoriser FavMasToKey</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>2.3 Migration des favoris</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Une fois l'autorisation accordée, vous serez redirigé vers l'écran de migration</li>
|
||||||
|
<li>Cliquez sur "Démarrer la migration" pour commencer le processus</li>
|
||||||
|
<li>Vous pouvez mettre en pause, reprendre ou annuler la migration à tout moment</li>
|
||||||
|
<li>Le journal des opérations vous montre l'état de chaque favori traité</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>3. Résolution des problèmes courants</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4>3.1 Publications non trouvées</h4>
|
||||||
|
<p>Certaines publications peuvent ne pas être trouvées sur le réseau fédéré pour diverses raisons :</p>
|
||||||
|
<ul>
|
||||||
|
<li>La publication a été supprimée</li>
|
||||||
|
<li>L'instance d'origine est hors ligne</li>
|
||||||
|
<li>L'utilisateur a changé ses paramètres de confidentialité</li>
|
||||||
|
<li>Votre instance Misskey ne s'est jamais fédérée avec l'instance d'origine</li>
|
||||||
|
</ul>
|
||||||
|
<p>Solution : Malheureusement, il n'y a pas de solution simple pour ce problème, car il s'agit d'une limitation du réseau fédéré. Vous pouvez essayer de visiter manuellement les URLs qui ont échoué.</p>
|
||||||
|
|
||||||
|
<h4>3.2 Erreurs d'authentification</h4>
|
||||||
|
<p>Si vous rencontrez des problèmes lors de l'authentification avec Misskey :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Vérifiez que vous êtes bien connecté à votre compte Misskey</li>
|
||||||
|
<li>Assurez-vous que vous autorisez les cookies tiers dans votre navigateur</li>
|
||||||
|
<li>Essayez de vous déconnecter puis de vous reconnecter à votre compte Misskey</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>3.3 Migration interrompue</h4>
|
||||||
|
<p>Si votre migration est interrompue (par exemple, en fermant l'onglet ou en perdant la connexion Internet), FavMasToKey peut la reprendre :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Retournez simplement sur la page de FavMasToKey</li>
|
||||||
|
<li>L'application détectera automatiquement la migration en cours</li>
|
||||||
|
<li>Confirmez que vous souhaitez reprendre là où vous vous êtes arrêté</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Informations techniques</h2>
|
||||||
|
|
||||||
|
<h3>Comment ça marche ?</h3>
|
||||||
|
<p>FavMasToKey fonctionne en suivant ces étapes :</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Analyse du fichier JSON</strong> - L'application extrait les URLs des favoris depuis votre fichier Mastodon</li>
|
||||||
|
<li><strong>Authentification OAuth</strong> - L'application s'enregistre auprès de votre instance Misskey et obtient votre autorisation</li>
|
||||||
|
<li><strong>Recherche fédérée</strong> - Pour chaque favori, l'application recherche la publication équivalente sur le réseau fédéré</li>
|
||||||
|
<li><strong>Ajout aux favoris</strong> - Si la publication est trouvée, elle est ajoutée à vos favoris Misskey</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Confidentialité et sécurité</h3>
|
||||||
|
<p>FavMasToKey a été conçu en mettant l'accent sur la confidentialité et la sécurité :</p>
|
||||||
|
<ul>
|
||||||
|
<li>Aucune donnée n'est stockée sur le serveur, tout est traité localement dans votre navigateur</li>
|
||||||
|
<li>Les tokens d'authentification sont temporaires et ne sont stockés que pendant la durée de votre session</li>
|
||||||
|
<li>Le code est open source et peut être audité</li>
|
||||||
|
<li>L'application ne demande que les permissions minimales nécessaires (ajouter aux favoris)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Limitations connues</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Les publications qui n'existent plus ou qui sont privées ne peuvent pas être retrouvées</li>
|
||||||
|
<li>Les instances Misskey peuvent avoir des limites de taux (rate limits) qui ralentissent le processus</li>
|
||||||
|
<li>Les grandes collections de favoris peuvent prendre du temps à migrer</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
32
images/favicon.svg
Normal file
32
images/favicon.svg
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M1999 4846 c-2 -2 -85 -9 -184 -15 -208 -14 -278 -23 -430 -58 -318
|
||||||
|
-74 -569 -230 -763 -476 -148 -187 -206 -333 -247 -621 -26 -186 -27 -196 -21
|
||||||
|
-560 3 -204 10 -441 15 -526 5 -85 14 -267 20 -404 6 -149 19 -299 31 -375 52
|
||||||
|
-318 62 -380 76 -436 77 -311 211 -524 464 -740 112 -95 199 -151 362 -230
|
||||||
|
168 -83 346 -136 586 -175 259 -42 468 -42 738 0 270 42 549 133 648 211 46
|
||||||
|
36 56 75 56 217 0 136 -10 186 -40 202 -28 15 -88 12 -203 -10 -58 -11 -147
|
||||||
|
-24 -198 -30 -52 -5 -141 -16 -199 -25 -146 -23 -398 -16 -530 14 -161 37
|
||||||
|
-291 110 -351 198 -38 55 -89 192 -89 238 0 26 6 39 25 51 23 15 32 15 113 1
|
||||||
|
357 -65 299 -61 795 -60 408 1 475 4 620 23 383 50 577 95 722 167 325 162
|
||||||
|
543 453 601 803 29 170 36 336 41 910 4 544 3 568 -17 670 -55 276 -151 455
|
||||||
|
-345 646 -201 198 -479 322 -790 352 -306 31 -596 43 -1041 42 -253 0 -463 -2
|
||||||
|
-465 -4z m804 -824 c87 -30 116 -58 157 -152 18 -40 24 -74 25 -125 0 -100
|
||||||
|
-30 -155 -150 -277 -52 -54 -95 -99 -95 -101 0 -1 44 -3 98 -2 89 0 101 -2
|
||||||
|
144 -28 133 -80 271 -286 330 -493 32 -112 32 -314 -1 -428 -108 -383 -414
|
||||||
|
-651 -783 -687 -242 -24 -426 20 -619 147 -172 114 -321 320 -374 519 -23 84
|
||||||
|
-23 385 0 470 21 81 86 214 142 294 54 76 807 832 852 856 48 24 133 44 166
|
||||||
|
39 17 -3 65 -17 108 -32z"/>
|
||||||
|
<path d="M2363 2992 c-106 -104 -190 -196 -204 -222 -20 -38 -24 -59 -24 -140
|
||||||
|
0 -86 3 -100 29 -147 55 -99 158 -150 281 -141 120 10 206 71 243 171 45 119
|
||||||
|
20 242 -69 344 -68 78 -97 170 -79 250 6 30 10 57 7 59 -2 3 -85 -76 -184
|
||||||
|
-174z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After (image error) Size: 1.8 KiB |
0
includes/app_data.php
Normal file
0
includes/app_data.php
Normal file
@ -22,14 +22,10 @@ if (ENVIRONMENT === 'development') {
|
|||||||
|
|
||||||
// Configuration de l'application
|
// Configuration de l'application
|
||||||
$config = [
|
$config = [
|
||||||
// Informations de l'application (à remplir lors de la création de l'app sur Misskey)
|
// Informations de l'application
|
||||||
'app_name' => 'FavMasToKey',
|
'app_name' => 'FavMasToKey',
|
||||||
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
|
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
|
||||||
'app_version' => '0.1.0',
|
'app_version' => '0.2.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
|
// URLs de base
|
||||||
'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
|
'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
|
// Paramètres pour le traitement
|
||||||
'batch_size' => 10, // Nombre de favoris à traiter en une fois
|
'batch_size' => 10, // Nombre de favoris à traiter en une fois
|
||||||
'timeout' => 30, // Timeout des requêtes en secondes
|
'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
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Fonctions utilitaires
|
// Fonctions utilitaires
|
||||||
function debug($data) {
|
function debug($data) {
|
||||||
if (ENVIRONMENT === 'development') {
|
if (ENVIRONMENT === 'production') {
|
||||||
echo '<pre>';
|
echo '<pre>';
|
||||||
print_r($data);
|
print_r($data);
|
||||||
echo '</pre>';
|
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
|
* Génère une URL d'autorisation OAuth pour Misskey
|
||||||
*
|
*
|
||||||
* @param string $instance Instance Misskey
|
* @param string $instance Instance Misskey
|
||||||
|
* @param string $client_id ID de l'application
|
||||||
* @return string URL d'autorisation
|
* @return string URL d'autorisation
|
||||||
*/
|
*/
|
||||||
function generate_oauth_url($instance) {
|
function generate_oauth_url($instance, $client_id) {
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
// Générer un état aléatoire pour la sécurité
|
// 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
|
// Paramètres de la requête d'autorisation
|
||||||
$params = [
|
$params = [
|
||||||
'client_id' => $config['client_id'],
|
'client_id' => $client_id,
|
||||||
'response_type' => 'code',
|
'response_type' => 'code',
|
||||||
'redirect_uri' => $callback_url,
|
'redirect_uri' => $callback_url,
|
||||||
'scope' => 'write:favorites',
|
'scope' => 'write:favorites',
|
||||||
|
@ -22,10 +22,13 @@ if (isset($_SESSION['messages'])) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr" data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="css/styles.css">
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
@ -35,6 +38,7 @@ if (isset($_SESSION['messages'])) {
|
|||||||
<header class="text-center mb-5">
|
<header class="text-center mb-5">
|
||||||
<h1>FavMasToKey</h1>
|
<h1>FavMasToKey</h1>
|
||||||
<p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p>
|
<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>
|
</header>
|
||||||
|
|
||||||
<!-- Messages d'alerte -->
|
<!-- Messages d'alerte -->
|
||||||
|
175
js/app.js
175
js/app.js
@ -26,6 +26,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
let isProcessing = false;
|
let isProcessing = false;
|
||||||
let isPaused = 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
|
// Gérer le téléchargement et l'analyse du fichier JSON
|
||||||
if (uploadForm) {
|
if (uploadForm) {
|
||||||
@ -71,6 +89,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Stocker les données dans localStorage pour les conserver
|
// Stocker les données dans localStorage pour les conserver
|
||||||
localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList));
|
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) {
|
} catch (error) {
|
||||||
alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message);
|
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
|
// Gérer le processus de migration
|
||||||
if (startMigration) {
|
if (startMigration) {
|
||||||
startMigration.addEventListener('click', function() {
|
startMigration.addEventListener('click', function() {
|
||||||
if (isProcessing) return;
|
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')) {
|
if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) {
|
||||||
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
|
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
|
||||||
totalItems = favoritesList.length;
|
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) {
|
if (favoritesList.length === 0) {
|
||||||
addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
|
addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
|
||||||
return;
|
return;
|
||||||
@ -142,6 +272,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
startMigration.classList.add('d-none');
|
startMigration.classList.add('d-none');
|
||||||
pauseMigration.classList.remove('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');
|
addLogEntry('Démarrage de la migration...', 'info');
|
||||||
|
|
||||||
// Lancer le processus de migration
|
// Lancer le processus de migration
|
||||||
@ -160,10 +298,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
pauseMigration.textContent = 'Reprendre';
|
pauseMigration.textContent = 'Reprendre';
|
||||||
addLogEntry('Migration en pause.', 'warning');
|
addLogEntry('Migration en pause.', 'warning');
|
||||||
currentProgress.classList.remove('active');
|
currentProgress.classList.remove('active');
|
||||||
|
|
||||||
|
// Mettre à jour le statut de la migration
|
||||||
|
updateMigrationData('paused');
|
||||||
} else {
|
} else {
|
||||||
pauseMigration.textContent = 'Pause';
|
pauseMigration.textContent = 'Pause';
|
||||||
addLogEntry('Reprise de la migration...', 'info');
|
addLogEntry('Reprise de la migration...', 'info');
|
||||||
currentProgress.classList.add('active');
|
currentProgress.classList.add('active');
|
||||||
|
|
||||||
|
// Mettre à jour le statut de la migration
|
||||||
|
updateMigrationData('in_progress');
|
||||||
|
|
||||||
processBatch();
|
processBatch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -190,6 +335,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
pauseMigration.classList.add('d-none');
|
pauseMigration.classList.add('d-none');
|
||||||
pauseMigration.textContent = 'Pause';
|
pauseMigration.textContent = 'Pause';
|
||||||
currentProgress.classList.remove('active');
|
currentProgress.classList.remove('active');
|
||||||
|
|
||||||
|
// Réinitialiser les données de migration
|
||||||
|
resetMigration();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -202,12 +350,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (currentIndex >= totalItems) {
|
if (currentIndex >= totalItems) {
|
||||||
// Migration terminée
|
// Migration terminée
|
||||||
isProcessing = false;
|
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.classList.remove('d-none');
|
||||||
startMigration.textContent = 'Terminer';
|
startMigration.textContent = 'Terminer';
|
||||||
startMigration.addEventListener('click', function() {
|
startMigration.addEventListener('click', function() {
|
||||||
// Nettoyer localStorage et retourner à l'étape 1
|
// Nettoyer localStorage et retourner à l'étape 1
|
||||||
localStorage.removeItem('favmastokey_favorites');
|
localStorage.removeItem('favmastokey_favorites');
|
||||||
|
localStorage.removeItem('favmastokey_migration');
|
||||||
step3.classList.add('d-none');
|
step3.classList.add('d-none');
|
||||||
step1.classList.remove('d-none');
|
step1.classList.remove('d-none');
|
||||||
});
|
});
|
||||||
@ -215,6 +366,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Mettre à jour la progression à 100%
|
// Mettre à jour la progression à 100%
|
||||||
updateProgress(100);
|
updateProgress(100);
|
||||||
|
|
||||||
|
// Mettre à jour le statut de la migration
|
||||||
|
updateMigrationData('completed');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -252,6 +406,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (data.results && data.results.length) {
|
if (data.results && data.results.length) {
|
||||||
data.results.forEach(result => {
|
data.results.forEach(result => {
|
||||||
addLogEntry(result.message, result.status);
|
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;
|
currentIndex = endIndex;
|
||||||
|
|
||||||
// Mettre à jour la progression
|
// 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
|
// Traiter le lot suivant après un court délai
|
||||||
setTimeout(processBatch, 1000);
|
setTimeout(processBatch, 1000);
|
||||||
@ -271,6 +437,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
isPaused = true;
|
isPaused = true;
|
||||||
pauseMigration.textContent = 'Reprendre';
|
pauseMigration.textContent = 'Reprendre';
|
||||||
currentProgress.classList.remove('active');
|
currentProgress.classList.remove('active');
|
||||||
|
|
||||||
|
// Mettre à jour le statut de la migration
|
||||||
|
updateMigrationData('error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
63
oauth.php
63
oauth.php
@ -40,33 +40,48 @@ if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
|
|||||||
// Stocker l'instance dans la session
|
// Stocker l'instance dans la session
|
||||||
$_SESSION['misskey_instance'] = $instance;
|
$_SESSION['misskey_instance'] = $instance;
|
||||||
|
|
||||||
// En production, ici nous devrions vérifier si l'application est déjà enregistrée sur cette instance
|
// Vérifier si cette instance est déjà enregistrée
|
||||||
// Si ce n'est pas le cas, il faudrait l'enregistrer via l'API Misskey
|
$client_id = null;
|
||||||
// Pour cette version initiale, nous utilisons des valeurs simulées
|
$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
|
// Générer l'URL d'autorisation
|
||||||
try {
|
try {
|
||||||
// REMARQUE: Pour une implémentation réelle, $config['client_id'] et $config['client_secret']
|
// Générer l'URL avec le client_id obtenu
|
||||||
// devraient être stockés par instance car chaque instance Misskey nécessite une application distincte
|
$auth_url = generate_oauth_url($instance, $client_id);
|
||||||
|
|
||||||
// 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
|
// Rediriger vers l'URL d'autorisation
|
||||||
header('Location: ' . $auth_url);
|
header('Location: ' . $auth_url);
|
||||||
|
90
process.php
90
process.php
@ -65,61 +65,15 @@ foreach ($batch as $index => $url) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extraire l'identifiant du toot
|
// Rechercher la note sur Misskey à partir de l'URL
|
||||||
$pathParts = explode('/', trim($urlParts['path'], '/'));
|
$searchResult = search_federated_note($misskey_instance, $url, $token);
|
||||||
$tootId = end($pathParts);
|
|
||||||
|
|
||||||
if (!is_numeric($tootId)) {
|
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
|
||||||
$results[] = [
|
// Note trouvée, récupérer son ID
|
||||||
'status' => 'error',
|
$noteId = $searchResult['data']['id'];
|
||||||
'message' => "Impossible d'extraire l'ID du toot: $url"
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construire l'URL pour la recherche fédérée sur Misskey
|
// Tenter d'ajouter la note aux favoris
|
||||||
$searchUrl = "https://" . $urlParts['host'] . "/@" . $pathParts[count($pathParts) - 3] . "/" . $tootId;
|
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
|
||||||
|
|
||||||
// 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']) {
|
if ($favoriteResult['success']) {
|
||||||
$results[] = [
|
$results[] = [
|
||||||
@ -127,21 +81,35 @@ foreach ($batch as $index => $url) {
|
|||||||
'message' => "Ajouté aux favoris: $url"
|
'message' => "Ajouté aux favoris: $url"
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$results[] = [
|
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
|
||||||
'status' => 'error',
|
$errorMessage = isset($favoriteResult['data']['error']['message'])
|
||||||
'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['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 {
|
} else {
|
||||||
|
// Note non trouvée
|
||||||
|
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
|
||||||
|
|
||||||
$results[] = [
|
$results[] = [
|
||||||
'status' => 'error',
|
'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)
|
// Pause pour éviter le rate limiting
|
||||||
// usleep(200000); // 200 ms
|
usleep($config['delay_between_requests'] * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renvoyer les résultats
|
// Renvoyer les résultats
|
||||||
|
Loading…
x
Reference in New Issue
Block a user