330 lines
14 KiB
Plaintext
330 lines
14 KiB
Plaintext
<?php
|
||
session_start();
|
||
|
||
class PleromaAdminAuth {
|
||
private $baseUrl;
|
||
private $userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
|
||
private $cookieFile;
|
||
|
||
public function __construct($baseUrl) {
|
||
$this->baseUrl = rtrim($baseUrl, '/');
|
||
$this->cookieFile = sys_get_temp_dir() . '/pleroma_admin_cookies_' . session_id() . '.txt';
|
||
}
|
||
|
||
public function login($username, $password) {
|
||
// Step 1: Initialise session by visiting admin page
|
||
$adminPage = $this->baseUrl . '/pleroma/admin/';
|
||
$ch = curl_init($adminPage);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_FOLLOWLOCATION => true,
|
||
CURLOPT_USERAGENT => $this->userAgent,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
CURLOPT_COOKIEJAR => $this->cookieFile,
|
||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||
CURLOPT_TIMEOUT => 30
|
||
]);
|
||
curl_exec($ch);
|
||
curl_close($ch);
|
||
|
||
// Step 2: OAuth password grant (exact flow used by modern AdminFE)
|
||
$oauthUrl = $this->baseUrl . '/oauth/token';
|
||
$postData = [
|
||
'grant_type' => 'password',
|
||
'username' => $username,
|
||
'password' => $password,
|
||
// These are the default AdminFE client credentials – work on nearly all instances
|
||
'client_id' => 'ECfTrzsW11bkG7simInOJf614Ee2sBhOlvE-69KDMzs',
|
||
'client_secret' => 'r5EbNudkfePUwvRmc7DbafP0yOXpUC4lvipSrU8QOpo',
|
||
];
|
||
|
||
$ch = curl_init($oauthUrl);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => http_build_query($postData),
|
||
CURLOPT_USERAGENT => $this->userAgent,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
CURLOPT_COOKIEJAR => $this->cookieFile,
|
||
CURLOPT_COOKIEFILE => $this->cookieFile,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/x-www-form-urlencoded',
|
||
'Accept: application/json'
|
||
],
|
||
CURLOPT_TIMEOUT => 30
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
$data = json_decode($response, true);
|
||
|
||
if ($httpCode === 200 && isset($data['access_token'])) {
|
||
$_SESSION['bearer_token'] = $data['access_token'];
|
||
return ['success' => true, 'message' => 'Admin OAuth login successful', 'token' => $data['access_token']];
|
||
}
|
||
|
||
return ['success' => false, 'message' => 'Login failed', 'http_code' => $httpCode, 'response' => $response];
|
||
}
|
||
|
||
public function getCookieFile() {
|
||
return $this->cookieFile;
|
||
}
|
||
|
||
public function cleanup() {
|
||
if (file_exists($this->cookieFile)) {
|
||
unlink($this->cookieFile);
|
||
}
|
||
}
|
||
}
|
||
|
||
class PleromaEmojiUploader {
|
||
private $baseUrl;
|
||
private $bearerToken;
|
||
private $maxFileSize = 1048576; // 1 MB
|
||
private $maxDimensions = 256; // 256×256 px
|
||
|
||
public function __construct($baseUrl, $bearerToken) {
|
||
$this->baseUrl = rtrim($baseUrl, '/');
|
||
$this->bearerToken = $bearerToken;
|
||
}
|
||
|
||
private function validateFile($file, &$error) {
|
||
$error = '';
|
||
|
||
if (!is_uploaded_file($file['tmp_name'])) {
|
||
$error = 'No file uploaded';
|
||
return false;
|
||
}
|
||
|
||
if ($file['size'] > $this->maxFileSize) {
|
||
$error = 'File too large (max 1 MB)';
|
||
return false;
|
||
}
|
||
|
||
$filename = pathinfo($file['name'], PATHINFO_FILENAME);
|
||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $filename)) {
|
||
$error = 'Shortcode (filename without extension) may only contain letters, numbers and underscores';
|
||
return false;
|
||
}
|
||
|
||
$allowed = ['png', 'gif', 'webp', 'jpg', 'jpeg'];
|
||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||
if (!in_array($ext, $allowed)) {
|
||
$error = 'Only PNG, GIF, WebP, JPG/JPEG allowed';
|
||
return false;
|
||
}
|
||
|
||
$info = getimagesize($file['tmp_name']);
|
||
if (!$info || $info[0] > $this->maxDimensions || $info[1] > $this->maxDimensions) {
|
||
$error = 'Image dimensions too large (max 256×256 px)';
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public function uploadEmoji($packName, $uploadedFile) {
|
||
$error = '';
|
||
if (!$this->validateFile($uploadedFile, $error)) {
|
||
return ['success' => false, 'error' => $error];
|
||
}
|
||
|
||
// Modern endpoint (2024–2026 Akkoma/Pleroma)
|
||
$url = $this->baseUrl . '/api/v1/pleroma/emoji/packs/files?name=' . urlencode($packName);
|
||
|
||
$postData = [
|
||
'file' => new CURLFile($uploadedFile['tmp_name'], $uploadedFile['type'], $uploadedFile['name'])
|
||
];
|
||
|
||
$ch = curl_init($url);
|
||
$headers = [
|
||
'Authorization: Bearer ' . $this->bearerToken,
|
||
'Accept: application/json'
|
||
];
|
||
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $postData,
|
||
CURLOPT_USERAGENT => 'Mozilla/5.0',
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
CURLOPT_HTTPHEADER => $headers,
|
||
CURLOPT_TIMEOUT => 60
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
$shortcode = pathinfo($uploadedFile['name'], PATHINFO_FILENAME);
|
||
|
||
return [
|
||
'success' => ($httpCode >= 200 && $httpCode < 300),
|
||
'http_code' => $httpCode,
|
||
'shortcode' => $shortcode,
|
||
'pack' => $packName,
|
||
'response' => $response
|
||
];
|
||
}
|
||
}
|
||
?>
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Pleroma/Akkoma Admin – Login + Emoji Upload (2026)</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
|
||
.container { max-width: 900px; margin: 0 auto; background: #fff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||
.tabs { display: flex; background: #8e44ad; }
|
||
.tab { flex: 1; padding: 16px; text-align: center; color: white; font-weight: bold; cursor: pointer; }
|
||
.tab.active { background: #9b59b6; }
|
||
.panel { padding: 30px; display: none; }
|
||
.panel.active { display: block; }
|
||
h1 { color: #333; border-bottom: 2px solid #8e44ad; padding-bottom: 10px; }
|
||
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
|
||
input[type="text"], input[type="password"], input[type="url"], input[type="file"] {
|
||
width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ddd; border-radius: 5px; font-size: 16px; box-sizing: border-box;
|
||
}
|
||
button { background: #8e44ad; color: white; padding: 12px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; width: 100%; }
|
||
button:hover { background: #9b59b6; }
|
||
.result { margin-top: 30px; padding: 20px; border-radius: 5px; }
|
||
.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||
.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||
.token-box { background: #f3e8ff; padding: 12px; border-radius: 5px; margin: 10px 0; word-break: break-all; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="login">1. Admin Login</div>
|
||
<div class="tab" data-tab="upload">2. Emoji Uploader</div>
|
||
</div>
|
||
|
||
<div id="login" class="panel active">
|
||
<h1>🔐 Admin Login (OAuth)</h1>
|
||
<div class="warning">
|
||
<strong>⚠️ Administrators only!</strong><br>
|
||
Uses the same OAuth password flow as the official admin panel.
|
||
</div>
|
||
|
||
<form method="POST">
|
||
<label>Server URL:</label>
|
||
<input type="url" name="server_url" placeholder="https://your-instance.com" required
|
||
value="<?php echo htmlspecialchars($_POST['server_url'] ?? ''); ?>">
|
||
|
||
<label>Admin Username/Email:</label>
|
||
<input type="text" name="username" placeholder="admin@domain.com" required
|
||
value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>">
|
||
|
||
<label>Admin Password:</label>
|
||
<input type="password" name="password" required>
|
||
|
||
<button type="submit" name="admin_login">Login as Admin</button>
|
||
</form>
|
||
|
||
<?php
|
||
if (isset($_POST['admin_login'])) {
|
||
$serverUrl = trim($_POST['server_url']);
|
||
$username = trim($_POST['username']);
|
||
$password = $_POST['password'];
|
||
|
||
if (empty($serverUrl) || empty($username) || empty($password)) {
|
||
echo '<div class="result error">Please fill all fields.</div>';
|
||
} elseif (!filter_var($serverUrl, FILTER_VALIDATE_URL)) {
|
||
echo '<div class="result error">Invalid URL.</div>';
|
||
} else {
|
||
$auth = new PleromaAdminAuth($serverUrl);
|
||
$result = $auth->login($username, $password);
|
||
|
||
if ($result['success']) {
|
||
$_SESSION['base_url'] = $serverUrl;
|
||
$_SESSION['bearer_token'] = $result['token'];
|
||
echo '<div class="result success">';
|
||
echo '<h3>✅ Login Successful!</h3>';
|
||
echo '<p>You are now authenticated.</p>';
|
||
if (!empty($result['token'])) {
|
||
echo '<div class="token-box"><strong>Bearer Token:</strong><br>' . htmlspecialchars($result['token']) . '</div>';
|
||
}
|
||
echo '<p>Switch to the Emoji Uploader tab.</p>';
|
||
echo '</div>';
|
||
} else {
|
||
$auth->cleanup();
|
||
echo '<div class="result error">';
|
||
echo '<h3>❌ Login Failed</h3>';
|
||
echo '<p>' . htmlspecialchars($result['message']) . '</p>';
|
||
if (isset($result['http_code'])) echo '<p>HTTP Code: ' . $result['http_code'] . '</p>';
|
||
echo '</div>';
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
</div>
|
||
|
||
<div id="upload" class="panel">
|
||
<h1>🎨 Emoji Uploader</h1>
|
||
<div class="warning">
|
||
Requires successful admin login.<br>
|
||
<strong>Pack must already exist</strong> (create it first in the web Admin → Settings → Emoji if needed).
|
||
</div>
|
||
|
||
<?php if (isset($_SESSION['bearer_token']) && isset($_SESSION['base_url'])): ?>
|
||
<form method="POST" enctype="multipart/form-data">
|
||
<label>Pack Name:</label>
|
||
<input type="text" name="packName" placeholder="CeaMoji" value="<?php echo htmlspecialchars($_POST['packName'] ?? 'CeaMoji'); ?>" required>
|
||
|
||
<label>Emoji Image (max 1 MB, 256×256 px):</label>
|
||
<input type="file" name="emojiFile" accept=".png,.gif,.webp,.jpg,.jpeg" required>
|
||
|
||
<button type="submit" name="do_upload">Upload Emoji</button>
|
||
</form>
|
||
|
||
<?php
|
||
if (isset($_POST['do_upload'])) {
|
||
$uploader = new PleromaEmojiUploader($_SESSION['base_url'], $_SESSION['bearer_token']);
|
||
$uploadResult = $uploader->uploadEmoji($_POST['packName'], $_FILES['emojiFile']);
|
||
|
||
echo '<div class="result ' . ($uploadResult['success'] ? 'success' : 'error') . '">';
|
||
if ($uploadResult['success']) {
|
||
echo '<h3>✅ Emoji Uploaded Successfully!</h3>';
|
||
echo '<p>Shortcode: <strong>:' . htmlspecialchars($uploadResult['shortcode']) . ':</strong></p>';
|
||
echo '<p>Pack: <strong>' . htmlspecialchars($uploadResult['pack']) . '</strong></p>';
|
||
echo '<p>You may need to refresh the admin panel or reload emojis for it to appear immediately.</p>';
|
||
} else {
|
||
echo '<h3>❌ Upload Failed</h3>';
|
||
echo '<p>HTTP Code: ' . $uploadResult['http_code'] . '</p>';
|
||
if (!empty($uploadResult['response'])) {
|
||
echo '<pre>' . htmlspecialchars($uploadResult['response']) . '</pre>';
|
||
}
|
||
}
|
||
echo '</div>';
|
||
}
|
||
?>
|
||
<?php else: ?>
|
||
<div class="result error">
|
||
<strong>No active admin session.</strong><br>
|
||
Please log in first on the Login tab.
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(tab.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |