Initial Web IM deployment
This commit is contained in:
Vendored
+321
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
require __DIR__ . '/im_web_bootstrap.php';
|
||||
|
||||
const PROFILE_DISPLAY_NAME_FLAG = 0x01;
|
||||
const PROFILE_PORTRAIT_FLAG = 0x02;
|
||||
const PROFILE_PORTRAIT_MEDIA_TYPE = 5;
|
||||
const IM_LOCAL_MEDIA_ROOT = '/opt/im-server/media';
|
||||
const IM_LOCAL_MEDIA_PUBLIC_PORT = 18000;
|
||||
|
||||
function profileRequestValue(string $key, string $default = ''): string
|
||||
{
|
||||
return trim((string)($_POST[$key] ?? $_GET[$key] ?? $default));
|
||||
}
|
||||
|
||||
function normalizeProfileUser(array $user): array
|
||||
{
|
||||
$uid = (string)($user['userId'] ?? $user['uid'] ?? $user['_uid'] ?? '');
|
||||
return [
|
||||
'uid' => $uid,
|
||||
'userId' => $uid,
|
||||
'name' => (string)($user['name'] ?? $user['_name'] ?? ''),
|
||||
'displayName' => (string)($user['displayName'] ?? $user['_display_name'] ?? $user['name'] ?? $user['_name'] ?? $uid),
|
||||
'mobile' => (string)($user['mobile'] ?? $user['_mobile'] ?? ''),
|
||||
'portrait' => (string)($user['portrait'] ?? $user['_portrait'] ?? ''),
|
||||
'address' => (string)($user['address'] ?? $user['_address'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
function tryProfileUserInfo(array $payload): ?array
|
||||
{
|
||||
try {
|
||||
$result = requireImSuccess(imAdminPost('/admin/user/get_info', $payload), '用户资料加载失败');
|
||||
return $result ?: null;
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadProfileUserFromDb(string $identity): ?array
|
||||
{
|
||||
$statement = imDb()->prepare(
|
||||
'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait`, `_address`'
|
||||
. ' from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ?'
|
||||
. ' order by case when `_uid` = ? then 0 when `_mobile` = ? then 1 when `_name` = ? then 2 else 3 end'
|
||||
. ' limit 1'
|
||||
);
|
||||
$statement->execute([$identity, $identity, $identity, $identity, $identity, $identity]);
|
||||
$row = $statement->fetch();
|
||||
return $row ? normalizeProfileUser($row) : null;
|
||||
}
|
||||
|
||||
function mergeProfileUser(array $primary, ?array $fallback): array
|
||||
{
|
||||
if (!$fallback) {
|
||||
return $primary;
|
||||
}
|
||||
|
||||
$fallback = normalizeProfileUser($fallback);
|
||||
foreach (['name', 'displayName', 'mobile', 'portrait', 'address'] as $key) {
|
||||
if ((string)($primary[$key] ?? '') === '' && (string)($fallback[$key] ?? '') !== '') {
|
||||
$primary[$key] = (string)$fallback[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $primary;
|
||||
}
|
||||
|
||||
function persistProfileUserToDb(string $userId, string $displayName, string $portrait): void
|
||||
{
|
||||
if ($portrait !== '') {
|
||||
$statement = imDb()->prepare(
|
||||
'update `t_user` set `_display_name` = ?, `_portrait` = ?, `_dt` = ? where `_uid` = ? limit 1'
|
||||
);
|
||||
$statement->execute([$displayName, $portrait, webImNowMs(), $userId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = imDb()->prepare(
|
||||
'update `t_user` set `_display_name` = ?, `_dt` = ? where `_uid` = ? limit 1'
|
||||
);
|
||||
$statement->execute([$displayName, webImNowMs(), $userId]);
|
||||
}
|
||||
|
||||
function loadProfileUser(string $identity, bool $preferMobile = true): array
|
||||
{
|
||||
$identity = trim($identity);
|
||||
if ($identity === '') {
|
||||
respond(400, '缺少用户ID');
|
||||
}
|
||||
|
||||
$dbUser = loadProfileUserFromDb($identity);
|
||||
if ($dbUser) {
|
||||
$fallback = null;
|
||||
if ((string)($dbUser['portrait'] ?? '') === '') {
|
||||
$fallback = tryProfileUserInfo(['userId' => $dbUser['userId']]);
|
||||
if (!$fallback && (string)($dbUser['mobile'] ?? '') !== '') {
|
||||
$fallback = tryProfileUserInfo(['mobile' => $dbUser['mobile']]);
|
||||
}
|
||||
if (!$fallback && (string)($dbUser['name'] ?? '') !== '') {
|
||||
$fallback = tryProfileUserInfo(['name' => $dbUser['name']]);
|
||||
}
|
||||
}
|
||||
|
||||
return mergeProfileUser($dbUser, $fallback);
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if ($preferMobile && preg_match('/^\+?\d{5,32}$/', $identity)) {
|
||||
$user = tryProfileUserInfo(['mobile' => $identity]);
|
||||
}
|
||||
if (!$user) {
|
||||
$user = tryProfileUserInfo(['userId' => $identity]);
|
||||
}
|
||||
if (!$user) {
|
||||
$user = tryProfileUserInfo(['name' => $identity]);
|
||||
}
|
||||
if (!$user) {
|
||||
respond(404, '用户资料不存在');
|
||||
}
|
||||
|
||||
return normalizeProfileUser($user);
|
||||
}
|
||||
|
||||
function readProfileJavaSerializedString(string $data, int $offset): ?array
|
||||
{
|
||||
if ($offset + 3 > strlen($data) || ord($data[$offset]) !== 0x74) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$length = (ord($data[$offset + 1]) << 8) | ord($data[$offset + 2]);
|
||||
$start = $offset + 3;
|
||||
if ($start + $length > strlen($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [substr($data, $start, $length), $start + $length];
|
||||
}
|
||||
|
||||
function profileSessionUserId(string $sessionData): string
|
||||
{
|
||||
$marker = "\x74\x00\x06userId";
|
||||
$offset = strpos($sessionData, $marker);
|
||||
if ($offset === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decoded = readProfileJavaSerializedString($sessionData, $offset + strlen($marker));
|
||||
$userId = is_array($decoded) ? (string)$decoded[0] : '';
|
||||
if (!preg_match('/^[A-Za-z0-9_:@.+-]{1,128}$/', $userId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
function requireProfileUpdateAuth(string $targetUserId): void
|
||||
{
|
||||
$authToken = trim((string)($_SERVER['HTTP_AUTHTOKEN'] ?? $_SERVER['HTTP_AUTH_TOKEN'] ?? ''));
|
||||
if ($authToken === '') {
|
||||
respond(401, '请重新登录后再修改资料');
|
||||
}
|
||||
|
||||
$statement = imAppDb()->prepare('select `session_data` from `shiro_session` where `session_id` = ? limit 1');
|
||||
$statement->execute([$authToken]);
|
||||
$row = $statement->fetch();
|
||||
if (!$row) {
|
||||
respond(401, '登录状态已失效,请重新登录');
|
||||
}
|
||||
|
||||
$sessionUserId = profileSessionUserId((string)$row['session_data']);
|
||||
if ($sessionUserId === '' || $sessionUserId !== $targetUserId) {
|
||||
respond(403, '只能修改自己的资料');
|
||||
}
|
||||
}
|
||||
|
||||
function profileUploadedFileExtension(string $fileName, string $mime): string
|
||||
{
|
||||
$extension = strtolower((string)pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$extension = preg_replace('/[^a-z0-9]/', '', $extension) ?: '';
|
||||
if ($extension === 'jpeg') {
|
||||
$extension = 'jpg';
|
||||
}
|
||||
|
||||
$allowedExtensions = ['jpg', 'png', 'gif', 'webp', 'bmp', 'ico'];
|
||||
if (in_array($extension, $allowedExtensions, true)) {
|
||||
return $extension;
|
||||
}
|
||||
|
||||
return match ($mime) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp',
|
||||
'image/bmp', 'image/x-ms-bmp' => 'bmp',
|
||||
'image/x-icon', 'image/vnd.microsoft.icon' => 'ico',
|
||||
default => 'jpg',
|
||||
};
|
||||
}
|
||||
|
||||
function profilePublicMediaBaseUrl(): string
|
||||
{
|
||||
$isHttps = (string)($_SERVER['HTTPS'] ?? '') !== '' && strtolower((string)$_SERVER['HTTPS']) !== 'off';
|
||||
$scheme = $isHttps ? 'https' : 'http';
|
||||
$host = (string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '127.0.0.1');
|
||||
$host = trim(preg_replace('/:\d+$/', '', $host) ?: '127.0.0.1');
|
||||
|
||||
return $scheme . '://' . $host . ':' . IM_LOCAL_MEDIA_PUBLIC_PORT;
|
||||
}
|
||||
|
||||
function uploadProfilePortrait(array $file): string
|
||||
{
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException('头像上传失败');
|
||||
}
|
||||
if (($file['size'] ?? 0) <= 0 || ($file['size'] ?? 0) > 5 * 1024 * 1024) {
|
||||
throw new RuntimeException('头像不能超过5MB');
|
||||
}
|
||||
|
||||
$tmpName = (string)($file['tmp_name'] ?? '');
|
||||
$mime = mime_content_type($tmpName) ?: '';
|
||||
if (!str_starts_with($mime, 'image/')) {
|
||||
throw new RuntimeException('头像必须是图片文件');
|
||||
}
|
||||
|
||||
if (!is_dir(IM_LOCAL_MEDIA_ROOT) || !is_writable(IM_LOCAL_MEDIA_ROOT)) {
|
||||
throw new RuntimeException('头像存储目录不可写');
|
||||
}
|
||||
|
||||
$datePath = 'fs/' . PROFILE_PORTRAIT_MEDIA_TYPE . '/' . date('Y/m/d/H/i/s');
|
||||
$targetDir = rtrim(IM_LOCAL_MEDIA_ROOT, '/') . '/' . $datePath;
|
||||
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) {
|
||||
throw new RuntimeException('头像存储目录创建失败');
|
||||
}
|
||||
|
||||
$extension = profileUploadedFileExtension((string)($file['name'] ?? 'avatar.jpg'), $mime);
|
||||
$targetName = bin2hex(random_bytes(12)) . '.' . $extension;
|
||||
$targetPath = $targetDir . '/' . $targetName;
|
||||
if (!move_uploaded_file($tmpName, $targetPath)) {
|
||||
throw new RuntimeException('头像文件保存失败');
|
||||
}
|
||||
@chmod($targetPath, 0644);
|
||||
|
||||
return profilePublicMediaBaseUrl() . '/' . $datePath . '/' . $targetName;
|
||||
}
|
||||
|
||||
try {
|
||||
$contentType = (string)($_SERVER['CONTENT_TYPE'] ?? '');
|
||||
$isMultipart = stripos($contentType, 'multipart/form-data') !== false;
|
||||
$payload = $isMultipart ? [] : readJsonPayload();
|
||||
$action = $isMultipart
|
||||
? profileRequestValue('action', 'update')
|
||||
: trim((string)($payload['action'] ?? 'get'));
|
||||
$userId = $isMultipart
|
||||
? profileRequestValue('userId')
|
||||
: trim((string)($payload['userId'] ?? $_GET['userId'] ?? ''));
|
||||
|
||||
$currentProfile = loadProfileUser($userId);
|
||||
|
||||
if ($action === 'get') {
|
||||
respond(0, 'success', ['profile' => $currentProfile]);
|
||||
}
|
||||
|
||||
if ($action !== 'update') {
|
||||
respond(400, '不支持的资料操作');
|
||||
}
|
||||
requireProfileUpdateAuth((string)$currentProfile['userId']);
|
||||
|
||||
$displayName = $isMultipart
|
||||
? profileRequestValue('displayName')
|
||||
: trim((string)($payload['displayName'] ?? ''));
|
||||
if ($displayName === '') {
|
||||
respond(400, '昵称不能为空');
|
||||
}
|
||||
if (mb_strlen($displayName, 'UTF-8') > 64) {
|
||||
respond(400, '昵称不能超过64个字');
|
||||
}
|
||||
|
||||
$portrait = '';
|
||||
if (!empty($_FILES['portraitFile']) && ($_FILES['portraitFile']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
|
||||
$portrait = uploadProfilePortrait($_FILES['portraitFile']);
|
||||
}
|
||||
|
||||
$flag = PROFILE_DISPLAY_NAME_FLAG;
|
||||
$userInfo = [
|
||||
'userId' => $currentProfile['userId'],
|
||||
'displayName' => $displayName,
|
||||
];
|
||||
if ($portrait !== '') {
|
||||
$flag |= PROFILE_PORTRAIT_FLAG;
|
||||
$userInfo['portrait'] = $portrait;
|
||||
}
|
||||
|
||||
requireImSuccess(
|
||||
imAdminPost('/admin/user/update', [
|
||||
'flag' => $flag,
|
||||
'userInfo' => $userInfo,
|
||||
]),
|
||||
'用户资料保存失败'
|
||||
);
|
||||
persistProfileUserToDb($currentProfile['userId'], $displayName, $portrait);
|
||||
|
||||
$updatedProfile = loadProfileUser($currentProfile['userId'], false);
|
||||
if ($portrait === '' && (string)$updatedProfile['portrait'] === '' && (string)($currentProfile['portrait'] ?? '') !== '') {
|
||||
$updatedProfile['portrait'] = (string)$currentProfile['portrait'];
|
||||
}
|
||||
if ($portrait !== '' && (string)$updatedProfile['portrait'] === '') {
|
||||
$updatedProfile['portrait'] = $portrait;
|
||||
}
|
||||
if ((string)$updatedProfile['displayName'] === '') {
|
||||
$updatedProfile['displayName'] = $displayName;
|
||||
}
|
||||
|
||||
respond(0, 'success', ['profile' => $updatedProfile]);
|
||||
} catch (Throwable $error) {
|
||||
respond(500, $error->getMessage() ?: '用户资料服务暂不可用');
|
||||
}
|
||||
Reference in New Issue
Block a user