Files
nextcloud-forum/lib/Service/UserService.php

356 lines
10 KiB
PHP

<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Forum\Service;
use OCA\Forum\Db\BBCodeMapper;
use OCA\Forum\Db\ForumUserMapper;
use OCA\Forum\Db\Role;
use OCA\Forum\Db\RoleMapper;
use OCA\Forum\Db\UserRoleMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IL10N;
use OCP\IUserManager;
/**
* Service for user enrichment and display logic
* Handles Nextcloud user lookups and deleted user display
*/
class UserService {
public function __construct(
private IUserManager $userManager,
private ForumUserMapper $forumUserMapper,
private RoleMapper $roleMapper,
private UserRoleMapper $userRoleMapper,
private BBCodeMapper $bbCodeMapper,
private BBCodeService $bbCodeService,
private AdminSettingsService $adminSettingsService,
private GuestService $guestService,
private IL10N $l10n,
) {
}
/**
* Get display name for a user
* Returns "Deleted User" if user doesn't exist in Nextcloud
*/
public function getUserDisplayName(string $userId): string {
$user = $this->userManager->get($userId);
if ($user !== null) {
return $user->getDisplayName();
}
// User doesn't exist in Nextcloud - return generic deleted user name
return $this->l10n->t('Deleted user');
}
/**
* Check if a user has been deleted
* Checks both Nextcloud user existence and forum_users deleted_at flag
*/
public function isUserDeleted(string $userId): bool {
// First check if user exists in Nextcloud
$user = $this->userManager->get($userId);
if ($user === null) {
return true;
}
// Check if marked as deleted in forum_users
try {
$forumUser = $this->forumUserMapper->find($userId);
return $forumUser->getDeletedAt() !== null;
} catch (DoesNotExistException $e) {
// No forum user record, user is not deleted (just hasn't posted yet)
return false;
}
}
/**
* Enrich user data with display name, deleted status, roles, and signature
*
* @param string $userId
* @param array|null $roles Optional pre-fetched roles array
* @param array|null $bbcodes Optional pre-fetched BBCode definitions for parsing signatures
* @return array{userId: string, displayName: string, isDeleted: bool, roles: array, signature: ?string, signatureRaw: ?string}
*/
public function enrichUserData(string $userId, ?array $roles = null, ?array $bbcodes = null): array {
// Handle guest authors
if (GuestService::isGuestAuthor($userId)) {
$guestDisplayName = $this->guestService->getGuestDisplayName($userId) ?? $this->l10n->t('Guest');
try {
$guestRole = $this->roleMapper->findByRoleType(Role::ROLE_TYPE_GUEST);
$guestRoles = [$guestRole->jsonSerialize()];
} catch (\Exception $e) {
$guestRoles = [];
}
return [
'userId' => $userId,
'displayName' => $guestDisplayName,
'isDeleted' => false,
'isGuest' => true,
'roles' => $guestRoles,
'signature' => null,
'signatureRaw' => null,
];
}
$isDeleted = $this->isUserDeleted($userId);
$displayName = $this->getUserDisplayName($userId);
// If roles not provided, fetch them
if ($roles === null) {
$userRoles = $this->userRoleMapper->findByUserId($userId);
$roles = [];
foreach ($userRoles as $userRole) {
try {
$role = $this->roleMapper->find($userRole->getRoleId());
$roles[] = $role->jsonSerialize();
} catch (\Exception $e) {
// Role not found, skip
}
}
}
// Get signature from forum user (only if signatures are enabled)
$signatureRaw = null;
$signature = null;
$signaturesEnabled = (bool)$this->adminSettingsService->getSetting(AdminSettingsService::SETTING_ENABLE_SIGNATURES);
if ($signaturesEnabled) {
try {
$forumUser = $this->forumUserMapper->find($userId);
$signatureRaw = $forumUser->getSignature();
if ($signatureRaw !== null && $signatureRaw !== '') {
// Parse BBCode in signature
if ($bbcodes === null) {
$bbcodes = $this->bbCodeMapper->findAllEnabled();
}
$signature = $this->bbCodeService->parse($signatureRaw, $bbcodes);
}
} catch (DoesNotExistException $e) {
// No forum user record, no signature
}
}
return [
'userId' => $userId,
'displayName' => $displayName,
'isDeleted' => $isDeleted,
'roles' => $roles,
'signature' => $signature,
'signatureRaw' => $signatureRaw,
];
}
/**
* Enrich multiple users at once (for performance)
*
* @param array<string> $userIds
* @param array<string, array> $rolesMap Optional pre-fetched roles map (userId => roles[])
* @param array|null $bbcodes Optional pre-fetched BBCode definitions for parsing signatures
* @return array<string, array{userId: string, displayName: string, isDeleted: bool, roles: array, signature: ?string, signatureRaw: ?string}>
*/
public function enrichMultipleUsers(array $userIds, ?array $rolesMap = null, ?array $bbcodes = null): array {
$result = [];
// Separate guest and real user IDs
$guestIds = [];
$realUserIds = [];
foreach ($userIds as $userId) {
if (GuestService::isGuestAuthor($userId)) {
$guestIds[] = $userId;
} else {
$realUserIds[] = $userId;
}
}
// Handle guest users
if (!empty($guestIds)) {
$guestRoles = [];
try {
$guestRole = $this->roleMapper->findByRoleType(Role::ROLE_TYPE_GUEST);
$guestRoles = [$guestRole->jsonSerialize()];
} catch (\Exception $e) {
// Guest role not found
}
foreach ($guestIds as $guestId) {
$guestDisplayName = $this->guestService->getGuestDisplayName($guestId) ?? $this->l10n->t('Guest');
$result[$guestId] = [
'userId' => $guestId,
'displayName' => $guestDisplayName,
'isDeleted' => false,
'isGuest' => true,
'roles' => $guestRoles,
'signature' => null,
'signatureRaw' => null,
];
}
}
// Handle real users
if (!empty($realUserIds)) {
// If roles not provided, fetch them all at once
if ($rolesMap === null) {
$rolesMap = $this->fetchRolesForUsers($realUserIds);
}
// Fetch signatures only if enabled
$signaturesEnabled = (bool)$this->adminSettingsService->getSetting(AdminSettingsService::SETTING_ENABLE_SIGNATURES);
$signaturesMap = $signaturesEnabled ? $this->fetchSignaturesForUsers($realUserIds) : [];
if ($signaturesEnabled && $bbcodes === null) {
$bbcodes = $this->bbCodeMapper->findAllEnabled();
}
foreach ($realUserIds as $userId) {
$isDeleted = $this->isUserDeleted($userId);
$displayName = $this->getUserDisplayName($userId);
$signatureRaw = $signaturesMap[$userId] ?? null;
$signature = null;
if ($signaturesEnabled && $signatureRaw !== null && $signatureRaw !== '') {
$signature = $this->bbCodeService->parse($signatureRaw, $bbcodes);
}
$result[$userId] = [
'userId' => $userId,
'displayName' => $displayName,
'isDeleted' => $isDeleted,
'roles' => $rolesMap[$userId] ?? [],
'signature' => $signature,
'signatureRaw' => $signatureRaw,
];
}
}
return $result;
}
/**
* Fetch roles for multiple users efficiently
*
* @param array<string> $userIds
* @return array<string, array> Map of userId => roles[]
*/
private function fetchRolesForUsers(array $userIds): array {
if (empty($userIds)) {
return [];
}
$rolesMap = [];
// Initialize all user IDs with empty arrays
foreach ($userIds as $userId) {
$rolesMap[$userId] = [];
}
// Fetch all user roles for these users
$userRoles = $this->userRoleMapper->findByUserIds($userIds);
// Group by user ID and fetch role details
$roleIds = [];
$userRolesByUser = [];
foreach ($userRoles as $userRole) {
$userId = $userRole->getUserId();
$roleId = $userRole->getRoleId();
if (!isset($userRolesByUser[$userId])) {
$userRolesByUser[$userId] = [];
}
$userRolesByUser[$userId][] = $roleId;
$roleIds[$roleId] = true;
}
// Fetch all roles at once
$roles = [];
$roleEntities = $this->roleMapper->findByIds(array_keys($roleIds));
foreach ($roleEntities as $role) {
$roles[$role->getId()] = $role->jsonSerialize();
}
// Map roles to users
foreach ($userRolesByUser as $userId => $userRoleIds) {
foreach ($userRoleIds as $roleId) {
if (isset($roles[$roleId])) {
$rolesMap[$userId][] = $roles[$roleId];
}
}
}
return $rolesMap;
}
/**
* Search users for autocomplete
* Returns users matching the search query in the format expected by NcRichContenteditable
*
* @param string $search Search query (matches against user ID and display name)
* @param int $limit Maximum number of results to return
* @param string|null $excludeUserId User ID to exclude from results (e.g., current user)
* @return array<array{id: string, label: string, icon: string, source: string}> List of matching users
*/
public function searchUsersForAutocomplete(string $search = '', int $limit = 10, ?string $excludeUserId = null): array {
$results = [];
$search = strtolower(trim($search));
// Use IUserManager to search users
// The search method searches both user ID and display name
// Request one extra result in case we need to exclude the current user
$users = $this->userManager->search($search, $excludeUserId !== null ? $limit + 1 : $limit);
foreach ($users as $user) {
// Skip excluded user (e.g., current user)
if ($excludeUserId !== null && $user->getUID() === $excludeUserId) {
continue;
}
$results[] = [
'id' => $user->getUID(),
'label' => $user->getDisplayName(),
'icon' => 'icon-user',
'source' => 'users',
];
// Stop if we have enough results
if (count($results) >= $limit) {
break;
}
}
return $results;
}
/**
* Fetch signatures for multiple users efficiently
*
* @param array<string> $userIds
* @return array<string, ?string> Map of userId => signature (raw)
*/
private function fetchSignaturesForUsers(array $userIds): array {
if (empty($userIds)) {
return [];
}
$signaturesMap = [];
// Initialize all user IDs with null
foreach ($userIds as $userId) {
$signaturesMap[$userId] = null;
}
// Fetch all forum users for these users
$forumUsers = $this->forumUserMapper->findByUserIds($userIds);
// Extract signatures
foreach ($forumUsers as $forumUser) {
$signaturesMap[$forumUser->getUserId()] = $forumUser->getSignature();
}
return $signaturesMap;
}
}