mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
355 lines
9.8 KiB
PHP
355 lines
9.8 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\CategoryMapper;
|
|
use OCA\Forum\Db\CategoryPermMapper;
|
|
use OCA\Forum\Db\PostMapper;
|
|
use OCA\Forum\Db\Role;
|
|
use OCA\Forum\Db\RoleMapper;
|
|
use OCA\Forum\Db\ThreadMapper;
|
|
use OCA\Forum\Db\UserRoleMapper;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\IUserManager;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class PermissionService {
|
|
public function __construct(
|
|
private UserRoleMapper $userRoleMapper,
|
|
private RoleMapper $roleMapper,
|
|
private CategoryPermMapper $categoryPermMapper,
|
|
private CategoryMapper $categoryMapper,
|
|
private ThreadMapper $threadMapper,
|
|
private PostMapper $postMapper,
|
|
private IUserManager $userManager,
|
|
private LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Check if user has Admin role
|
|
*
|
|
* @param string $userId Nextcloud user ID
|
|
* @return bool True if user has Admin role
|
|
*/
|
|
private function hasAdminRole(string $userId): bool {
|
|
try {
|
|
$roles = $this->roleMapper->findByUserId($userId);
|
|
|
|
foreach ($roles as $role) {
|
|
if ($role->getRoleType() === Role::ROLE_TYPE_ADMIN) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (\Exception $e) {
|
|
$this->logger->error("Error checking admin role for user $userId: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user has Admin or Moderator role
|
|
*
|
|
* @param string $userId Nextcloud user ID
|
|
* @return bool True if user has Admin or Moderator role
|
|
*/
|
|
public function hasAdminOrModeratorRole(string $userId): bool {
|
|
try {
|
|
$roles = $this->roleMapper->findByUserId($userId);
|
|
|
|
foreach ($roles as $role) {
|
|
// Check if user has admin or moderator role type
|
|
if ($role->getRoleType() === Role::ROLE_TYPE_ADMIN || $role->getRoleType() === Role::ROLE_TYPE_MODERATOR) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (\Exception $e) {
|
|
$this->logger->error("Error checking admin/moderator role for user $userId: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user has global permission
|
|
*
|
|
* Global permissions are role-based permissions that apply across the entire forum
|
|
* Examples: canAccessAdminTools, canEditRoles, canEditCategories
|
|
*
|
|
* @param string|null $userId Nextcloud user ID (null for guest users)
|
|
* @param string $permission Permission name in camelCase (e.g., 'canEditRoles')
|
|
* @return bool True if user has the permission
|
|
*/
|
|
public function hasGlobalPermission(?string $userId, string $permission): bool {
|
|
try {
|
|
// Handle guest users (null userId) - use guest role
|
|
if ($userId === null) {
|
|
try {
|
|
$guestRole = $this->roleMapper->findByRoleType(Role::ROLE_TYPE_GUEST);
|
|
$roles = [$guestRole];
|
|
} catch (DoesNotExistException $e) {
|
|
return false;
|
|
}
|
|
} else {
|
|
$roles = $this->roleMapper->findByUserId($userId);
|
|
}
|
|
|
|
if (empty($roles)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($roles as $role) {
|
|
// Check permission using getter method
|
|
// Note: Nextcloud Entity uses magic methods, so we call directly without method_exists check
|
|
$getter = 'get' . ucfirst($permission);
|
|
|
|
try {
|
|
if ($role->$getter()) {
|
|
return true;
|
|
}
|
|
} catch (\BadMethodCallException $e) {
|
|
$this->logger->error("Invalid permission method $getter on Role entity: " . $e->getMessage());
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (\Exception $e) {
|
|
$this->logger->error("Error checking global permission '$permission': " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user has permission on a specific category
|
|
*
|
|
* Category permissions are resource-specific and defined per role per category
|
|
* Examples: canView, canPost, canReply, canModerate
|
|
*
|
|
* @param string|null $userId Nextcloud user ID (null for guest users)
|
|
* @param int $categoryId Category ID
|
|
* @param string $permission Permission name in camelCase (e.g., 'canView', 'canPost')
|
|
* @return bool True if user has the permission
|
|
*/
|
|
public function hasCategoryPermission(?string $userId, int $categoryId, string $permission): bool {
|
|
// Admin role has hardcoded full access to all categories (guests can never be admin)
|
|
if ($userId !== null && $this->hasAdminRole($userId)) {
|
|
return true;
|
|
}
|
|
|
|
$getter = 'get' . ucfirst($permission);
|
|
|
|
try {
|
|
// Handle guest users (null userId) - use guest role
|
|
if ($userId === null) {
|
|
try {
|
|
$guestRole = $this->roleMapper->findByRoleType(Role::ROLE_TYPE_GUEST);
|
|
$roles = [$guestRole];
|
|
} catch (DoesNotExistException $e) {
|
|
return false;
|
|
}
|
|
} else {
|
|
$roles = $this->roleMapper->findByUserId($userId);
|
|
}
|
|
|
|
// Check role-based permissions
|
|
foreach ($roles as $role) {
|
|
try {
|
|
$perm = $this->categoryPermMapper->findByCategoryAndRole(
|
|
$categoryId,
|
|
$role->getId()
|
|
);
|
|
|
|
try {
|
|
if ($perm->$getter()) {
|
|
return true;
|
|
}
|
|
} catch (\BadMethodCallException $e) {
|
|
$this->logger->error("Invalid permission method $getter on CategoryPerm entity: " . $e->getMessage());
|
|
continue;
|
|
}
|
|
} catch (DoesNotExistException $e) {
|
|
// No permission entry for this category+role combination, continue checking other roles
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check team/circle-based permissions (only for authenticated users)
|
|
if ($userId !== null) {
|
|
if ($this->hasTeamCategoryPermission($userId, $categoryId, $getter)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (\Exception $e) {
|
|
$this->logger->error("Error checking category permission '$permission' on category $categoryId: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get category ID from thread ID
|
|
*
|
|
* @param int $threadId Thread ID
|
|
* @return int Category ID
|
|
* @throws DoesNotExistException If thread doesn't exist
|
|
*/
|
|
public function getCategoryIdFromThread(int $threadId): int {
|
|
$thread = $this->threadMapper->find($threadId);
|
|
return $thread->getCategoryId();
|
|
}
|
|
|
|
/**
|
|
* Get category ID from post ID
|
|
*
|
|
* @param int $postId Post ID
|
|
* @return int Category ID
|
|
* @throws DoesNotExistException If post or thread doesn't exist
|
|
*/
|
|
public function getCategoryIdFromPost(int $postId): int {
|
|
$post = $this->postMapper->find($postId);
|
|
$thread = $this->threadMapper->find($post->getThreadId());
|
|
return $thread->getCategoryId();
|
|
}
|
|
|
|
/**
|
|
* Check if user is the author of a post
|
|
*
|
|
* @param string $userId Nextcloud user ID
|
|
* @param int $postId Post ID
|
|
* @return bool True if user is the author
|
|
*/
|
|
public function isPostAuthor(string $userId, int $postId): bool {
|
|
try {
|
|
$post = $this->postMapper->find($postId);
|
|
return $post->getAuthorId() === $userId;
|
|
} catch (DoesNotExistException $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user is the author of a thread
|
|
*
|
|
* @param string $userId Nextcloud user ID
|
|
* @param int $threadId Thread ID
|
|
* @return bool True if user is the author
|
|
*/
|
|
public function isThreadAuthor(string $userId, int $threadId): bool {
|
|
try {
|
|
$thread = $this->threadMapper->find($threadId);
|
|
return $thread->getAuthorId() === $userId;
|
|
} catch (DoesNotExistException $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all category IDs that the user has view permission for
|
|
*
|
|
* @param string|null $userId Nextcloud user ID (null for guest users)
|
|
* @return array<int> Array of category IDs
|
|
*/
|
|
public function getAccessibleCategories(?string $userId): array {
|
|
$accessibleCategoryIds = [];
|
|
|
|
try {
|
|
// Handle guest users (null userId) - use guest role
|
|
if ($userId === null) {
|
|
try {
|
|
$guestRole = $this->roleMapper->findByRoleType(Role::ROLE_TYPE_GUEST);
|
|
$roles = [$guestRole];
|
|
} catch (DoesNotExistException $e) {
|
|
return [];
|
|
}
|
|
} else {
|
|
// Get all user roles using JOIN
|
|
$roles = $this->roleMapper->findByUserId($userId);
|
|
|
|
if (empty($roles)) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get all categories
|
|
$categories = $this->categoryMapper->findAll();
|
|
|
|
// Check view permission for each category
|
|
foreach ($categories as $category) {
|
|
if ($this->hasCategoryPermission($userId, $category->getId(), 'canView')) {
|
|
$accessibleCategoryIds[] = $category->getId();
|
|
}
|
|
}
|
|
|
|
return $accessibleCategoryIds;
|
|
} catch (\Exception $e) {
|
|
$this->logger->error('Error getting accessible categories: ' . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user has a specific permission on a category via team (circle) membership
|
|
*
|
|
* @param string $userId Nextcloud user ID
|
|
* @param int $categoryId Category ID
|
|
* @param string $getter Getter method name (e.g., 'getCanView')
|
|
* @return bool True if user has the permission via a team
|
|
*/
|
|
private function hasTeamCategoryPermission(string $userId, int $categoryId, string $getter): bool {
|
|
try {
|
|
if (!class_exists(\OCA\Circles\CirclesManager::class)) {
|
|
return false;
|
|
}
|
|
|
|
$circlesManager = \OCP\Server::get(\OCA\Circles\CirclesManager::class);
|
|
|
|
$federatedUser = $circlesManager->getFederatedUser($userId, \OCA\Circles\Model\Member::TYPE_USER);
|
|
$circlesManager->startSession($federatedUser);
|
|
|
|
try {
|
|
$probe = new \OCA\Circles\Model\Probes\CircleProbe();
|
|
$probe->mustBeMember()
|
|
->filterHiddenCircles()
|
|
->filterBackendCircles()
|
|
->filterPersonalCircles()
|
|
->filterSingleCircles();
|
|
|
|
$circles = $circlesManager->getCircles($probe);
|
|
$circleIds = array_map(fn ($c) => $c->getSingleId(), $circles);
|
|
|
|
if (empty($circleIds)) {
|
|
return false;
|
|
}
|
|
|
|
$teamPerms = $this->categoryPermMapper->findByCategoryAndTeamIds($categoryId, $circleIds);
|
|
foreach ($teamPerms as $perm) {
|
|
try {
|
|
if ($perm->$getter()) {
|
|
return true;
|
|
}
|
|
} catch (\BadMethodCallException $e) {
|
|
continue;
|
|
}
|
|
}
|
|
} finally {
|
|
$circlesManager->stopSession();
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Circles app not available or other error - skip team permission check
|
|
$this->logger->debug('Team permission check skipped: ' . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|