252d8a002e
Added some fixes. Seems if the emoji is lowercase and has only numbers and underscorese it might just work better. Sometimes even works on mobile..
482 lines
19 KiB
PHP
482 lines
19 KiB
PHP
<script>
|
||
function getQueryParam(param) {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
return urlParams.get(param);
|
||
}
|
||
|
||
function prefillFromQueryParams() {
|
||
const instance = getQueryParam('instance');
|
||
const user = getQueryParam('user');
|
||
const password = getQueryParam('password');
|
||
|
||
if (instance) {
|
||
const serverUrlInput = document.querySelector('input[name="server_url"]');
|
||
if (serverUrlInput) serverUrlInput.value = decodeURIComponent(instance);
|
||
}
|
||
|
||
if (user) {
|
||
const userInput = document.querySelector('input[name="username"]');
|
||
if (userInput) userInput.value = decodeURIComponent(user);
|
||
}
|
||
|
||
if (password) {
|
||
const passInput = document.querySelector('input[name="password"]');
|
||
if (passInput) passInput.value = decodeURIComponent(password);
|
||
|
||
const loginTab = document.querySelector('.tab[data-tab="login"]');
|
||
if (loginTab) {
|
||
//bG9naW5UYWIuY2xpY2soKTs=
|
||
setTimeout(() => {
|
||
const loginBtn = document.querySelector('button[name="admin_login"]');
|
||
//bG9naW5UYWIuY2xpY2soKTs= (that's base64 for "loginTab.click();")
|
||
}, 100);
|
||
}
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', prefillFromQueryParams);
|
||
</script>
|
||
|
||
<?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 ($_FILES['emojiFile']['error'] !== UPLOAD_ERR_OK) {
|
||
$error = 'Upload error code: ' . $_FILES['emojiFile']['error'];
|
||
return false;
|
||
}
|
||
|
||
if (!is_uploaded_file($file['tmp_name'])) {
|
||
$error = 'No file uploaded or not a valid uploaded file (common on mobile)';
|
||
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);
|
||
|
||
$mime = $uploadedFile['type'];
|
||
if (empty($mime) || $mime === 'application/octet-stream') {
|
||
// Fallback detection
|
||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||
$mime = finfo_file($finfo, $uploadedFile['tmp_name']);
|
||
finfo_close($finfo);
|
||
}
|
||
|
||
$postData = [
|
||
'file' => new CURLFile($uploadedFile['tmp_name'], $mime, $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);
|
||
$curlError = curl_error($ch); // Added for debug
|
||
curl_close($ch);
|
||
|
||
$shortcode = pathinfo($uploadedFile['name'], PATHINFO_FILENAME);
|
||
|
||
$result = [
|
||
'success' => ($httpCode >= 200 && $httpCode < 300),
|
||
'http_code' => $httpCode,
|
||
'shortcode' => $shortcode,
|
||
'pack' => $packName,
|
||
'response' => $response,
|
||
'curl_error' => $curlError // Added for debug
|
||
];
|
||
|
||
return $result;
|
||
}
|
||
}
|
||
?>
|
||
|
||
<!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; }
|
||
.debug { background: #e2e3e5; padding: 12px; border-radius: 5px; margin: 10px 0; white-space: pre-wrap; word-wrap: break-word; }
|
||
</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="image/png,image/gif,image/webp,image/jpeg,image/jpg" capture="environment" required>
|
||
|
||
<button type="submit" name="do_upload">Upload Emoji</button>
|
||
</form>
|
||
|
||
<?php
|
||
if (isset($_POST['do_upload'])) {
|
||
// Debug output always shown
|
||
ob_start();
|
||
echo "Raw \$_FILES:\n";
|
||
print_r($_FILES['emojiFile']);
|
||
echo "\nUpload errors: " . $_FILES['emojiFile']['error'] . " (0 = OK, 1-8 = PHP upload error)\n";
|
||
echo "is_uploaded_file: " . (is_uploaded_file($_FILES['emojiFile']['tmp_name']) ? 'YES' : 'NO') . "\n";
|
||
echo "File exists: " . (file_exists($_FILES['emojiFile']['tmp_name']) ? 'YES' : 'NO') . "\n";
|
||
$debugInfo = ob_get_clean();
|
||
|
||
$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['curl_error'])) {
|
||
echo '<p>cURL Error: ' . htmlspecialchars($uploadResult['curl_error']) . '</p>';
|
||
}
|
||
if (!empty($uploadResult['response'])) {
|
||
echo '<pre>' . htmlspecialchars($uploadResult['response']) . '</pre>';
|
||
}
|
||
echo '<div class="debug"><strong>Debug Info:</strong><br><pre>' . htmlspecialchars($debugInfo) . '</pre></div>';
|
||
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>
|
||
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const fileInput = document.querySelector('input[name="emojiFile"]');
|
||
if (!fileInput) return;
|
||
|
||
const form = fileInput.closest('form');
|
||
|
||
fileInput.addEventListener('change', function () {
|
||
if (fileInput.files.length > 0) {
|
||
validateAndShowFeedback(fileInput.files[0]);
|
||
}
|
||
});
|
||
|
||
function validateAndShowFeedback(file) {
|
||
let errorMsg = '';
|
||
const maxSizeBytes = 1.1 * 1024 * 1024;
|
||
|
||
if (file.size >= maxSizeBytes) {
|
||
errorMsg = `File too large: ${(file.size / 1024 / 1024).toFixed(2)} MB (max 1.1 MB allowed)`;
|
||
}
|
||
|
||
const nameWithoutExt = file.name.replace(/\.[^.]+$/, '');
|
||
if (!/^[a-z0-9_]+$/.test(nameWithoutExt)) {
|
||
errorMsg = `Invalid shortcode "${nameWithoutExt}". Only lowercase letters, numbers and underscores are allowed.`;
|
||
}
|
||
|
||
if (!errorMsg) {
|
||
const img = new Image();
|
||
const objectUrl = URL.createObjectURL(file);
|
||
img.onload = function () {
|
||
URL.revokeObjectURL(objectUrl);
|
||
if (img.width > 256 || img.height > 256) {
|
||
errorMsg = `Image dimensions ${img.width}×${img.height} px too large (max 256×256 px)`;
|
||
}
|
||
showResult(errorMsg);
|
||
};
|
||
img.onerror = function () {
|
||
URL.revokeObjectURL(objectUrl);
|
||
errorMsg = 'Unable to read image file (corrupted or not an image)';
|
||
showResult(errorMsg);
|
||
};
|
||
img.src = objectUrl;
|
||
return;
|
||
}
|
||
|
||
showResult(errorMsg);
|
||
}
|
||
|
||
function showResult(errorMsg) {
|
||
let msgEl = document.querySelector('.validation-msg');
|
||
if (msgEl) msgEl.remove();
|
||
|
||
msgEl = document.createElement('div');
|
||
msgEl.className = 'validation-msg';
|
||
msgEl.style.marginTop = '12px';
|
||
msgEl.style.fontWeight = 'bold';
|
||
msgEl.style.padding = '10px';
|
||
msgEl.style.borderRadius = '5px';
|
||
|
||
if (errorMsg) {
|
||
msgEl.innerHTML = '❌ <a target="_blank" href="resize.html">resize</a> ' + errorMsg;
|
||
msgEl.style.backgroundColor = '#f8d7da';
|
||
msgEl.style.color = '#721c24';
|
||
msgEl.style.border = '1px solid #f5c6cb';
|
||
const submitBtn = form.querySelector('button[name="do_upload"]');
|
||
if (submitBtn) submitBtn.disabled = true;
|
||
} else {
|
||
msgEl.textContent = '✅ Image is valid and ready to upload!';
|
||
msgEl.style.backgroundColor = '#d4edda';
|
||
msgEl.style.color = '#155724';
|
||
msgEl.style.border = '1px solid #c3e6cb';
|
||
const submitBtn = form.querySelector('button[name="do_upload"]');
|
||
if (submitBtn) submitBtn.disabled = false;
|
||
}
|
||
|
||
fileInput.parentNode.appendChild(msgEl);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|