mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
Compare commits
1 Commits
v0.36.0
...
feature/ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 78052cda97 |
@@ -13,5 +13,5 @@ module.exports = {
|
||||
}
|
||||
return commands
|
||||
},
|
||||
'*Controller.php': [() => 'make openapi', () => 'git add openapi.json'],
|
||||
'*Controller.php': [() => 'make openapi', () => 'git add openapi-*.json'],
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ class ConfigLexicon implements ILexicon {
|
||||
new Entry('subtitle', ValueType::STRING, 'Welcome to the forum!', 'Forum subtitle displayed below the title', lazy: true),
|
||||
new Entry('allow_guest_access', ValueType::BOOL, false, 'Whether unauthenticated users can access the forum', lazy: true),
|
||||
new Entry('is_initialized', ValueType::BOOL, false, 'Whether the forum has been initialized with seed data', lazy: true),
|
||||
new Entry('public_edit_history', ValueType::BOOL, true, 'Whether all users can view edit history of posts', lazy: true),
|
||||
new Entry('allow_edit_history_user_override', ValueType::BOOL, false, 'Whether users can hide their own edit history from others', lazy: true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ class ConfigLexicon implements ILexicon {
|
||||
new Entry('auto_subscribe_created_threads', ValueType::BOOL, true, 'Automatically subscribe to threads the user creates'),
|
||||
new Entry('auto_subscribe_replied_threads', ValueType::BOOL, false, 'Automatically subscribe to threads the user replies to'),
|
||||
new Entry('upload_directory', ValueType::STRING, 'Forum', 'Directory in user storage for forum file uploads'),
|
||||
new Entry('hide_edit_history', ValueType::BOOL, false, 'Whether to hide edit history from other users'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,8 @@ class AdminController extends OCSController {
|
||||
* @param string|null $title Forum title
|
||||
* @param string|null $subtitle Forum subtitle
|
||||
* @param bool|null $allow_guest_access Allow unauthenticated users to view forum content
|
||||
* @param bool|null $public_edit_history Whether all users can view edit history of posts
|
||||
* @param bool|null $allow_edit_history_user_override Whether users can hide their own edit history from others
|
||||
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>
|
||||
*
|
||||
* 200: Settings updated
|
||||
@@ -208,7 +210,7 @@ class AdminController extends OCSController {
|
||||
#[NoAdminRequired]
|
||||
#[RequirePermission('canAccessAdminTools')]
|
||||
#[ApiRoute(verb: 'PUT', url: '/api/admin/settings')]
|
||||
public function updateSettings(?string $title = null, ?string $subtitle = null, ?bool $allow_guest_access = null): DataResponse {
|
||||
public function updateSettings(?string $title = null, ?string $subtitle = null, ?bool $allow_guest_access = null, ?bool $public_edit_history = null, ?bool $allow_edit_history_user_override = null): DataResponse {
|
||||
try {
|
||||
// Build settings array with only non-null values
|
||||
$settingsToUpdate = [];
|
||||
@@ -221,6 +223,12 @@ class AdminController extends OCSController {
|
||||
if ($allow_guest_access !== null) {
|
||||
$settingsToUpdate[AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS] = $allow_guest_access;
|
||||
}
|
||||
if ($public_edit_history !== null) {
|
||||
$settingsToUpdate[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY] = $public_edit_history;
|
||||
}
|
||||
if ($allow_edit_history_user_override !== null) {
|
||||
$settingsToUpdate[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE] = $allow_edit_history_user_override;
|
||||
}
|
||||
|
||||
// Update settings and return all settings
|
||||
$settings = $this->settingsService->updateSettings($settingsToUpdate);
|
||||
|
||||
@@ -106,10 +106,13 @@ class PostController extends OCSController {
|
||||
// Batch fetch author data (includes roles)
|
||||
$authors = $this->userService->enrichMultipleUsers($authorIds);
|
||||
|
||||
// Get category ID for permission checks
|
||||
$categoryId = $this->permissionService->getCategoryIdFromThread($threadId);
|
||||
|
||||
// Enrich posts with content, reactions, and pre-fetched author data
|
||||
return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $authors) {
|
||||
return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $authors, $categoryId) {
|
||||
$postReactions = $reactionsByPostId[$p->getId()] ?? [];
|
||||
return $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $authors[$p->getAuthorId()]);
|
||||
return $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $authors[$p->getAuthorId()], $categoryId);
|
||||
}, $posts));
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Error fetching posts by thread: ' . $e->getMessage());
|
||||
@@ -206,6 +209,9 @@ class PostController extends OCSController {
|
||||
// Batch fetch author data (includes roles)
|
||||
$authors = $this->userService->enrichMultipleUsers($authorIds);
|
||||
|
||||
// Get category ID for permission checks
|
||||
$categoryId = $this->permissionService->getCategoryIdFromThread($threadId);
|
||||
|
||||
// Enrich first post
|
||||
$enrichedFirstPost = null;
|
||||
if ($firstPost !== null) {
|
||||
@@ -215,14 +221,15 @@ class PostController extends OCSController {
|
||||
$bbcodes,
|
||||
$firstPostReactions,
|
||||
$currentUserId,
|
||||
$authors[$firstPost->getAuthorId()] ?? null
|
||||
$authors[$firstPost->getAuthorId()] ?? null,
|
||||
$categoryId,
|
||||
);
|
||||
}
|
||||
|
||||
// Enrich replies
|
||||
$enrichedReplies = array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $authors) {
|
||||
$enrichedReplies = array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $authors, $categoryId) {
|
||||
$postReactions = $reactionsByPostId[$p->getId()] ?? [];
|
||||
return $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $authors[$p->getAuthorId()] ?? null);
|
||||
return $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $authors[$p->getAuthorId()] ?? null, $categoryId);
|
||||
}, $replies);
|
||||
|
||||
return new DataResponse([
|
||||
@@ -283,11 +290,23 @@ class PostController extends OCSController {
|
||||
// For posts by a single author, we can optimize by fetching author data once
|
||||
$author = $this->userService->enrichUserData($authorId);
|
||||
|
||||
// Resolve category IDs for each post's thread (for edit history visibility)
|
||||
$threadIds = array_unique(array_map(fn ($p) => $p->getThreadId(), $posts));
|
||||
$categoryByThread = [];
|
||||
foreach ($threadIds as $tid) {
|
||||
try {
|
||||
$categoryByThread[$tid] = $this->permissionService->getCategoryIdFromThread($tid);
|
||||
} catch (\Exception $e) {
|
||||
// Skip if thread not found
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich posts with content, reactions, pre-fetched author data, and page number
|
||||
$perPage = 20;
|
||||
return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $author, $perPage) {
|
||||
return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $author, $perPage, $categoryByThread) {
|
||||
$postReactions = $reactionsByPostId[$p->getId()] ?? [];
|
||||
$enriched = $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $author);
|
||||
$categoryId = $categoryByThread[$p->getThreadId()] ?? null;
|
||||
$enriched = $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $author, $categoryId);
|
||||
|
||||
// Calculate the page number for direct linking
|
||||
if (!$p->getIsFirstPost()) {
|
||||
@@ -321,7 +340,9 @@ class PostController extends OCSController {
|
||||
public function show(int $id): DataResponse {
|
||||
try {
|
||||
$post = $this->postMapper->find($id);
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($post));
|
||||
$currentUserId = $this->userSession->getUser()?->getUID();
|
||||
$categoryId = $this->permissionService->getCategoryIdFromPost($id);
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($post, [], [], $currentUserId, null, $categoryId));
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Post not found'], Http::STATUS_NOT_FOUND);
|
||||
} catch (\Exception $e) {
|
||||
@@ -445,7 +466,9 @@ class PostController extends OCSController {
|
||||
$this->logger->warning('Failed to send mention notifications: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($createdPost), Http::STATUS_CREATED);
|
||||
$currentUserId = $user?->getUID();
|
||||
$categoryId = $this->permissionService->getCategoryIdFromThread($threadId);
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($createdPost, [], [], $currentUserId, null, $categoryId), Http::STATUS_CREATED);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Error creating post: ' . $e->getMessage());
|
||||
return new DataResponse(['error' => 'Failed to create post'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
@@ -517,7 +540,7 @@ class PostController extends OCSController {
|
||||
}
|
||||
}
|
||||
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($updatedPost));
|
||||
return new DataResponse($this->postEnrichmentService->enrichPost($updatedPost, [], [], $user->getUID(), null, $categoryId));
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Post not found'], Http::STATUS_NOT_FOUND);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -8,6 +8,9 @@ declare(strict_types=1);
|
||||
namespace OCA\Forum\Controller;
|
||||
|
||||
use OCA\Forum\Attribute\RequirePermission;
|
||||
use OCA\Forum\Db\PostMapper;
|
||||
use OCA\Forum\Service\EditHistoryVisibilityService;
|
||||
use OCA\Forum\Service\PermissionService;
|
||||
use OCA\Forum\Service\PostHistoryService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
@@ -23,7 +26,11 @@ class PostHistoryController extends OCSController {
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private PostHistoryService $postHistoryService,
|
||||
private PostMapper $postMapper,
|
||||
private PermissionService $permissionService,
|
||||
private EditHistoryVisibilityService $editHistoryVisibilityService,
|
||||
private LoggerInterface $logger,
|
||||
private ?string $userId,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
@@ -44,6 +51,13 @@ class PostHistoryController extends OCSController {
|
||||
#[ApiRoute(verb: 'GET', url: '/api/posts/{postId}/history')]
|
||||
public function getHistory(int $postId): DataResponse {
|
||||
try {
|
||||
$post = $this->postMapper->find($postId);
|
||||
$categoryId = $this->permissionService->getCategoryIdFromPost($postId);
|
||||
|
||||
if (!$this->editHistoryVisibilityService->canViewEditHistory($this->userId, $post->getAuthorId(), $categoryId)) {
|
||||
return new DataResponse(['error' => 'Insufficient permissions to view edit history'], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$history = $this->postHistoryService->getPostHistory($postId);
|
||||
return new DataResponse($history);
|
||||
} catch (DoesNotExistException $e) {
|
||||
|
||||
@@ -24,12 +24,20 @@ class AdminSettingsService {
|
||||
/** Setting key for initialization status */
|
||||
public const SETTING_IS_INITIALIZED = 'is_initialized';
|
||||
|
||||
/** Setting key for public edit history */
|
||||
public const SETTING_PUBLIC_EDIT_HISTORY = 'public_edit_history';
|
||||
|
||||
/** Setting key for allowing user override of edit history visibility */
|
||||
public const SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE = 'allow_edit_history_user_override';
|
||||
|
||||
/** @var array<string> List of valid setting keys */
|
||||
private const VALID_KEYS = [
|
||||
self::SETTING_TITLE,
|
||||
self::SETTING_SUBTITLE,
|
||||
self::SETTING_ALLOW_GUEST_ACCESS,
|
||||
self::SETTING_IS_INITIALIZED,
|
||||
self::SETTING_PUBLIC_EDIT_HISTORY,
|
||||
self::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -51,6 +59,8 @@ class AdminSettingsService {
|
||||
self::SETTING_SUBTITLE => $this->l10n->t('Welcome to the forum!'),
|
||||
self::SETTING_ALLOW_GUEST_ACCESS => false,
|
||||
self::SETTING_IS_INITIALIZED => false,
|
||||
self::SETTING_PUBLIC_EDIT_HISTORY => true,
|
||||
self::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => false,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
@@ -86,7 +96,9 @@ class AdminSettingsService {
|
||||
|
||||
return match ($key) {
|
||||
self::SETTING_ALLOW_GUEST_ACCESS,
|
||||
self::SETTING_IS_INITIALIZED => $this->config->getAppValueBool($key, $default, true),
|
||||
self::SETTING_IS_INITIALIZED,
|
||||
self::SETTING_PUBLIC_EDIT_HISTORY,
|
||||
self::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => $this->config->getAppValueBool($key, $default, true),
|
||||
default => $this->config->getAppValueString($key, $default, true),
|
||||
};
|
||||
}
|
||||
@@ -127,7 +139,8 @@ class AdminSettingsService {
|
||||
throw new \InvalidArgumentException("Invalid setting key: $key");
|
||||
}
|
||||
|
||||
if ($key === self::SETTING_ALLOW_GUEST_ACCESS || $key === self::SETTING_IS_INITIALIZED) {
|
||||
if ($key === self::SETTING_ALLOW_GUEST_ACCESS || $key === self::SETTING_IS_INITIALIZED
|
||||
|| $key === self::SETTING_PUBLIC_EDIT_HISTORY || $key === self::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE) {
|
||||
$this->config->setAppValueBool($key, (bool)$value, true);
|
||||
} else {
|
||||
$this->config->setAppValueString($key, (string)$value, true);
|
||||
|
||||
69
lib/Service/EditHistoryVisibilityService.php
Normal file
69
lib/Service/EditHistoryVisibilityService.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\Forum\Service;
|
||||
|
||||
/**
|
||||
* Centralized logic for determining whether a user can view edit history of a post
|
||||
*/
|
||||
class EditHistoryVisibilityService {
|
||||
public function __construct(
|
||||
private AdminSettingsService $adminSettingsService,
|
||||
private UserPreferencesService $userPreferencesService,
|
||||
private PermissionService $permissionService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a viewer can see the edit history of a post
|
||||
*
|
||||
* @param string|null $viewerId The user viewing (null for guests)
|
||||
* @param string $postAuthorId The author of the post
|
||||
* @param int $categoryId The category the post belongs to
|
||||
* @return bool Whether the viewer can see the edit history
|
||||
*/
|
||||
public function canViewEditHistory(?string $viewerId, string $postAuthorId, int $categoryId): bool {
|
||||
// Owners always see their own history
|
||||
if ($viewerId !== null && $viewerId === $postAuthorId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Global admins/moderators always see history
|
||||
if ($viewerId !== null && $this->permissionService->hasAdminOrModeratorRole($viewerId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Users with canModerate on the specific category always see history
|
||||
if ($viewerId !== null && $this->permissionService->hasCategoryPermission($viewerId, $categoryId, 'canModerate')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check admin setting: public edit history
|
||||
$publicEditHistory = (bool)$this->adminSettingsService->getSetting(
|
||||
AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY
|
||||
);
|
||||
if (!$publicEditHistory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check user override: post author can hide their edit history
|
||||
$allowOverride = (bool)$this->adminSettingsService->getSetting(
|
||||
AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE
|
||||
);
|
||||
if ($allowOverride) {
|
||||
$hideEditHistory = (bool)$this->userPreferencesService->getPreference(
|
||||
$postAuthorId,
|
||||
UserPreferencesService::PREF_HIDE_EDIT_HISTORY
|
||||
);
|
||||
if ($hideEditHistory) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class PostEnrichmentService {
|
||||
private BBCodeService $bbCodeService,
|
||||
private BBCodeMapper $bbCodeMapper,
|
||||
private UserService $userService,
|
||||
private EditHistoryVisibilityService $editHistoryVisibilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ class PostEnrichmentService {
|
||||
* @param array $reactions Post reactions
|
||||
* @param string|null $currentUserId Current user ID for reaction status
|
||||
* @param array|null $author Optional pre-loaded author data
|
||||
* @param int|null $categoryId Category ID for permission checks
|
||||
* @return array Enriched post data
|
||||
*/
|
||||
public function enrichPost(
|
||||
@@ -36,6 +38,7 @@ class PostEnrichmentService {
|
||||
array $reactions = [],
|
||||
?string $currentUserId = null,
|
||||
?array $author = null,
|
||||
?int $categoryId = null,
|
||||
): array {
|
||||
if (!is_array($post)) {
|
||||
$post = $post->jsonSerialize();
|
||||
@@ -57,6 +60,17 @@ class PostEnrichmentService {
|
||||
// Add reactions (grouped by emoji)
|
||||
$post['reactions'] = $this->groupReactions($reactions, $currentUserId);
|
||||
|
||||
// Add canViewHistory flag
|
||||
if ($categoryId !== null && !empty($post['isEdited'])) {
|
||||
$post['canViewHistory'] = $this->editHistoryVisibilityService->canViewEditHistory(
|
||||
$currentUserId,
|
||||
$post['authorId'],
|
||||
$categoryId,
|
||||
);
|
||||
} else {
|
||||
$post['canViewHistory'] = false;
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,16 @@ class UserPreferencesService {
|
||||
/** Preference key for user signature (stored in forum_users table) */
|
||||
public const PREF_SIGNATURE = 'signature';
|
||||
|
||||
/** Preference key for hiding edit history from others */
|
||||
public const PREF_HIDE_EDIT_HISTORY = 'hide_edit_history';
|
||||
|
||||
/** @var array<string, mixed> Default preference values */
|
||||
private const DEFAULTS = [
|
||||
self::PREF_AUTO_SUBSCRIBE_CREATED_THREADS => true,
|
||||
self::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS => false,
|
||||
self::PREF_UPLOAD_DIRECTORY => 'Forum',
|
||||
self::PREF_SIGNATURE => '',
|
||||
self::PREF_HIDE_EDIT_HISTORY => false,
|
||||
];
|
||||
|
||||
/** @var array<string> List of valid preference keys */
|
||||
@@ -40,6 +44,7 @@ class UserPreferencesService {
|
||||
self::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS,
|
||||
self::PREF_UPLOAD_DIRECTORY,
|
||||
self::PREF_SIGNATURE,
|
||||
self::PREF_HIDE_EDIT_HISTORY,
|
||||
];
|
||||
|
||||
/** @var array<string> Keys stored in forum_users table instead of config */
|
||||
|
||||
@@ -371,6 +371,18 @@
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Allow unauthenticated users to view forum content"
|
||||
},
|
||||
"public_edit_history": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Whether all users can view edit history of posts"
|
||||
},
|
||||
"allow_edit_history_user_override": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Whether users can hide their own edit history from others"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
openapi.json
12
openapi.json
@@ -371,6 +371,18 @@
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Allow unauthenticated users to view forum content"
|
||||
},
|
||||
"public_edit_history": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Whether all users can view edit history of posts"
|
||||
},
|
||||
"allow_edit_history_user_override": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "Whether users can hide their own edit history from others"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +223,8 @@ describe('PostCard', () => {
|
||||
expect(buttons.some((b) => b.text().includes('Edit'))).toBe(false)
|
||||
})
|
||||
|
||||
it('should show view history button when post is edited', () => {
|
||||
const post = createMockPost({ isEdited: true })
|
||||
it('should show view history button when canViewHistory is true', () => {
|
||||
const post = createMockPost({ isEdited: true, canViewHistory: true })
|
||||
const wrapper = mount(PostCard, {
|
||||
props: { post },
|
||||
})
|
||||
@@ -232,8 +232,17 @@ describe('PostCard', () => {
|
||||
expect(buttons.some((b) => b.text().includes('View edit history'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should not show view history button when canViewHistory is false', () => {
|
||||
const post = createMockPost({ isEdited: true, canViewHistory: false })
|
||||
const wrapper = mount(PostCard, {
|
||||
props: { post },
|
||||
})
|
||||
const buttons = wrapper.findAll('.nc-action-button')
|
||||
expect(buttons.some((b) => b.text().includes('View edit history'))).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show view history button when post is not edited', () => {
|
||||
const post = createMockPost({ isEdited: false })
|
||||
const post = createMockPost({ isEdited: false, canViewHistory: false })
|
||||
const wrapper = mount(PostCard, {
|
||||
props: { post },
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="post.isEdited" @click="handleViewHistory">
|
||||
<NcActionButton v-if="post.canViewHistory" @click="handleViewHistory">
|
||||
<template #icon>
|
||||
<HistoryIcon :size="20" />
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface PublicSettings {
|
||||
allow_guest_access: boolean
|
||||
/** Whether the forum has been initialized */
|
||||
is_initialized: boolean
|
||||
/** Whether all users can view edit history of posts */
|
||||
public_edit_history: boolean
|
||||
/** Whether users can hide their own edit history from others */
|
||||
allow_edit_history_user_override: boolean
|
||||
}
|
||||
|
||||
const settings = ref<PublicSettings | null>(null)
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface Post {
|
||||
// Thread context (added by SearchController for search results)
|
||||
threadTitle?: string
|
||||
threadSlug?: string
|
||||
canViewHistory?: boolean
|
||||
// Client-side enrichment
|
||||
reactions?: Array<{
|
||||
emoji: string
|
||||
|
||||
@@ -97,6 +97,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Section -->
|
||||
<div v-if="showPrivacySection" class="form-section">
|
||||
<h3>{{ strings.privacyTitle }}</h3>
|
||||
<p class="section-description muted">{{ strings.privacyDesc }}</p>
|
||||
|
||||
<div class="preference-item">
|
||||
<NcCheckboxRadioSwitch v-model="formData.hide_edit_history">
|
||||
{{ strings.hideEditHistory }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="preference-hint">{{ strings.hideEditHistoryHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton :disabled="saving || !hasChanges" @click="resetForm">
|
||||
@@ -138,12 +151,14 @@ import FolderIcon from '@icons/Folder.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { getFilePickerBuilder, FilePickerType } from '@nextcloud/dialogs'
|
||||
import { usePublicSettings } from '@/composables/usePublicSettings'
|
||||
|
||||
interface UserPreferences {
|
||||
auto_subscribe_created_threads: boolean
|
||||
auto_subscribe_replied_threads: boolean
|
||||
upload_directory: string
|
||||
signature: string
|
||||
hide_edit_history: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
@@ -162,6 +177,14 @@ export default defineComponent({
|
||||
CheckIcon,
|
||||
FolderIcon,
|
||||
},
|
||||
setup() {
|
||||
const { settings: publicSettings, fetchPublicSettings } = usePublicSettings()
|
||||
fetchPublicSettings()
|
||||
|
||||
return {
|
||||
publicSettings,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -173,12 +196,14 @@ export default defineComponent({
|
||||
auto_subscribe_replied_threads: false,
|
||||
upload_directory: 'Forum',
|
||||
signature: '',
|
||||
hide_edit_history: false,
|
||||
} as UserPreferences,
|
||||
formData: {
|
||||
auto_subscribe_created_threads: true,
|
||||
auto_subscribe_replied_threads: false,
|
||||
upload_directory: 'Forum',
|
||||
signature: '',
|
||||
hide_edit_history: false,
|
||||
} as UserPreferences,
|
||||
|
||||
strings: {
|
||||
@@ -219,6 +244,13 @@ export default defineComponent({
|
||||
signatureLabel: t('forum', 'Signature'),
|
||||
signatureHint: t('forum', 'You can use BBCode formatting in your signature'),
|
||||
signaturePlaceholder: t('forum', 'Enter your signature …'),
|
||||
privacyTitle: t('forum', 'Privacy'),
|
||||
privacyDesc: t('forum', 'Control the visibility of your activity'),
|
||||
hideEditHistory: t('forum', 'Hide my edit history from other accounts'),
|
||||
hideEditHistoryHint: t(
|
||||
'forum',
|
||||
'When enabled, other accounts cannot view the edit history of your posts. Administration and moderators can always view edit history.',
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -230,7 +262,14 @@ export default defineComponent({
|
||||
this.formData.auto_subscribe_replied_threads !==
|
||||
this.originalData.auto_subscribe_replied_threads ||
|
||||
this.formData.upload_directory !== this.originalData.upload_directory ||
|
||||
this.formData.signature !== this.originalData.signature
|
||||
this.formData.signature !== this.originalData.signature ||
|
||||
this.formData.hide_edit_history !== this.originalData.hide_edit_history
|
||||
)
|
||||
},
|
||||
showPrivacySection(): boolean {
|
||||
return (
|
||||
!!this.publicSettings?.public_edit_history &&
|
||||
!!this.publicSettings?.allow_edit_history_user_override
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,6 +57,25 @@
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection :title="strings.editHistoryTitle" :subtitle="strings.editHistoryDesc">
|
||||
<div class="form-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.public_edit_history" type="switch">
|
||||
{{ strings.publicEditHistory }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="hint">{{ strings.publicEditHistoryHint }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.public_edit_history" class="form-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="formData.allow_edit_history_user_override"
|
||||
type="switch"
|
||||
>
|
||||
{{ strings.allowEditHistoryUserOverride }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="hint">{{ strings.allowEditHistoryUserOverrideHint }}</p>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton :disabled="saving || !hasChanges" @click="resetForm">
|
||||
@@ -101,6 +120,8 @@ interface Settings {
|
||||
title: string
|
||||
subtitle: string
|
||||
allow_guest_access: boolean
|
||||
public_edit_history: boolean
|
||||
allow_edit_history_user_override: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
@@ -134,11 +155,15 @@ export default defineComponent({
|
||||
title: '',
|
||||
subtitle: '',
|
||||
allow_guest_access: false,
|
||||
public_edit_history: true,
|
||||
allow_edit_history_user_override: false,
|
||||
} as Settings,
|
||||
formData: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
allow_guest_access: false,
|
||||
public_edit_history: true,
|
||||
allow_edit_history_user_override: false,
|
||||
} as Settings,
|
||||
|
||||
strings: {
|
||||
@@ -162,6 +187,18 @@ export default defineComponent({
|
||||
'forum',
|
||||
'When enabled, unauthenticated users can view forum content in read-only mode',
|
||||
),
|
||||
editHistoryTitle: t('forum', 'Edit history'),
|
||||
editHistoryDesc: t('forum', 'Control who can view the edit history of posts'),
|
||||
publicEditHistory: t('forum', 'Allow all accounts to view edit history'),
|
||||
publicEditHistoryHint: t(
|
||||
'forum',
|
||||
'When enabled, any account can view the edit history of any post. When disabled, only post owners can view their own edit history. Administration and moderators can always view edit history.',
|
||||
),
|
||||
allowEditHistoryUserOverride: t('forum', 'Allow accounts to hide their own edit history'),
|
||||
allowEditHistoryUserOverrideHint: t(
|
||||
'forum',
|
||||
'When enabled, accounts can choose to hide their edit history from other accounts in their preferences.',
|
||||
),
|
||||
save: t('forum', 'Save'),
|
||||
cancel: t('forum', 'Cancel'),
|
||||
saveSuccess: t('forum', 'Settings saved'),
|
||||
@@ -173,7 +210,10 @@ export default defineComponent({
|
||||
return (
|
||||
this.formData.title !== this.originalData.title ||
|
||||
this.formData.subtitle !== this.originalData.subtitle ||
|
||||
this.formData.allow_guest_access !== this.originalData.allow_guest_access
|
||||
this.formData.allow_guest_access !== this.originalData.allow_guest_access ||
|
||||
this.formData.public_edit_history !== this.originalData.public_edit_history ||
|
||||
this.formData.allow_edit_history_user_override !==
|
||||
this.originalData.allow_edit_history_user_override
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -81,6 +81,93 @@ class AdminControllerTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Settings tests ───────────────────────────────────────────────
|
||||
|
||||
public function testGetSettingsReturnsAllSettings(): void {
|
||||
$allSettings = [
|
||||
'title' => 'My Forum',
|
||||
'subtitle' => 'Welcome!',
|
||||
'allow_guest_access' => false,
|
||||
'is_initialized' => true,
|
||||
'public_edit_history' => true,
|
||||
'allow_edit_history_user_override' => false,
|
||||
];
|
||||
|
||||
$this->settingsService->expects($this->once())
|
||||
->method('getAllSettings')
|
||||
->willReturn($allSettings);
|
||||
|
||||
$response = $this->controller->getSettings();
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals($allSettings, $response->getData());
|
||||
}
|
||||
|
||||
public function testUpdateSettingsPassesAllFieldsToService(): void {
|
||||
$expectedUpdate = [
|
||||
AdminSettingsService::SETTING_TITLE => 'New Title',
|
||||
AdminSettingsService::SETTING_SUBTITLE => 'New Subtitle',
|
||||
AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS => true,
|
||||
AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY => false,
|
||||
AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => true,
|
||||
];
|
||||
|
||||
$this->settingsService->expects($this->once())
|
||||
->method('updateSettings')
|
||||
->with($expectedUpdate)
|
||||
->willReturn(array_merge($expectedUpdate, ['is_initialized' => true]));
|
||||
|
||||
$response = $this->controller->updateSettings(
|
||||
'New Title',
|
||||
'New Subtitle',
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUpdateSettingsOmitsNullValues(): void {
|
||||
$this->settingsService->expects($this->once())
|
||||
->method('updateSettings')
|
||||
->with($this->callback(function (array $settings) {
|
||||
// Only public_edit_history and allow_edit_history_user_override should be present
|
||||
return count($settings) === 2
|
||||
&& array_key_exists(AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY, $settings)
|
||||
&& array_key_exists(AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE, $settings)
|
||||
&& $settings[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY] === true
|
||||
&& $settings[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE] === true;
|
||||
}))
|
||||
->willReturn([]);
|
||||
|
||||
$response = $this->controller->updateSettings(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUpdateSettingsHandlesException(): void {
|
||||
$this->settingsService->expects($this->once())
|
||||
->method('updateSettings')
|
||||
->willThrowException(new \Exception('DB error'));
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
->method('error')
|
||||
->with($this->stringContains('Error updating settings'));
|
||||
|
||||
$response = $this->controller->updateSettings('Title');
|
||||
|
||||
$this->assertEquals(500, $response->getStatus());
|
||||
}
|
||||
|
||||
// ── Dashboard tests ─────────────────────────────────────────────
|
||||
|
||||
public function testDashboardEnrichesContributorsWithDisplayNames(): void {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn('admin');
|
||||
|
||||
@@ -6,6 +6,10 @@ namespace OCA\Forum\Tests\Controller;
|
||||
|
||||
use OCA\Forum\AppInfo\Application;
|
||||
use OCA\Forum\Controller\PostHistoryController;
|
||||
use OCA\Forum\Db\Post;
|
||||
use OCA\Forum\Db\PostMapper;
|
||||
use OCA\Forum\Service\EditHistoryVisibilityService;
|
||||
use OCA\Forum\Service\PermissionService;
|
||||
use OCA\Forum\Service\PostHistoryService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
@@ -18,6 +22,12 @@ class PostHistoryControllerTest extends TestCase {
|
||||
private PostHistoryController $controller;
|
||||
/** @var PostHistoryService&MockObject */
|
||||
private PostHistoryService $postHistoryService;
|
||||
/** @var PostMapper&MockObject */
|
||||
private PostMapper $postMapper;
|
||||
/** @var PermissionService&MockObject */
|
||||
private PermissionService $permissionService;
|
||||
/** @var EditHistoryVisibilityService&MockObject */
|
||||
private EditHistoryVisibilityService $editHistoryVisibilityService;
|
||||
/** @var LoggerInterface&MockObject */
|
||||
private LoggerInterface $logger;
|
||||
/** @var IRequest&MockObject */
|
||||
@@ -26,18 +36,37 @@ class PostHistoryControllerTest extends TestCase {
|
||||
protected function setUp(): void {
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->postHistoryService = $this->createMock(PostHistoryService::class);
|
||||
$this->postMapper = $this->createMock(PostMapper::class);
|
||||
$this->permissionService = $this->createMock(PermissionService::class);
|
||||
$this->editHistoryVisibilityService = $this->createMock(EditHistoryVisibilityService::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->controller = new PostHistoryController(
|
||||
Application::APP_ID,
|
||||
$this->request,
|
||||
$this->postHistoryService,
|
||||
$this->logger
|
||||
$this->postMapper,
|
||||
$this->permissionService,
|
||||
$this->editHistoryVisibilityService,
|
||||
$this->logger,
|
||||
'user1',
|
||||
);
|
||||
}
|
||||
|
||||
private function setUpPermissionCheck(int $postId, string $authorId, int $categoryId, bool $canView): void {
|
||||
$post = new Post();
|
||||
$post->setId($postId);
|
||||
$post->setAuthorId($authorId);
|
||||
$this->postMapper->method('find')->with($postId)->willReturn($post);
|
||||
$this->permissionService->method('getCategoryIdFromPost')->with($postId)->willReturn($categoryId);
|
||||
$this->editHistoryVisibilityService->method('canViewEditHistory')
|
||||
->with('user1', $authorId, $categoryId)
|
||||
->willReturn($canView);
|
||||
}
|
||||
|
||||
public function testGetHistoryReturnsHistorySuccessfully(): void {
|
||||
$postId = 1;
|
||||
$this->setUpPermissionCheck($postId, 'user1', 10, true);
|
||||
|
||||
$historyData = [
|
||||
'current' => [
|
||||
@@ -83,11 +112,19 @@ class PostHistoryControllerTest extends TestCase {
|
||||
$this->assertCount(2, $data['history']);
|
||||
}
|
||||
|
||||
public function testGetHistoryReturnsForbiddenWhenNotAllowed(): void {
|
||||
$postId = 1;
|
||||
$this->setUpPermissionCheck($postId, 'other_user', 10, false);
|
||||
|
||||
$response = $this->controller->getHistory($postId);
|
||||
|
||||
$this->assertEquals(Http::STATUS_FORBIDDEN, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testGetHistoryReturnsNotFoundWhenPostDoesNotExist(): void {
|
||||
$postId = 999;
|
||||
|
||||
$this->postHistoryService->expects($this->once())
|
||||
->method('getPostHistory')
|
||||
$this->postMapper->method('find')
|
||||
->with($postId)
|
||||
->willThrowException(new DoesNotExistException('Post not found'));
|
||||
|
||||
@@ -99,6 +136,7 @@ class PostHistoryControllerTest extends TestCase {
|
||||
|
||||
public function testGetHistoryReturnsEmptyHistoryForNeverEditedPost(): void {
|
||||
$postId = 1;
|
||||
$this->setUpPermissionCheck($postId, 'user1', 10, true);
|
||||
|
||||
$historyData = [
|
||||
'current' => [
|
||||
@@ -127,6 +165,7 @@ class PostHistoryControllerTest extends TestCase {
|
||||
|
||||
public function testGetHistoryHandlesException(): void {
|
||||
$postId = 1;
|
||||
$this->setUpPermissionCheck($postId, 'user1', 10, true);
|
||||
|
||||
$this->postHistoryService->expects($this->once())
|
||||
->method('getPostHistory')
|
||||
@@ -145,6 +184,7 @@ class PostHistoryControllerTest extends TestCase {
|
||||
|
||||
public function testGetHistoryShowsModeratorEdits(): void {
|
||||
$postId = 1;
|
||||
$this->setUpPermissionCheck($postId, 'user1', 10, true);
|
||||
|
||||
$historyData = [
|
||||
'current' => [
|
||||
|
||||
@@ -49,12 +49,14 @@ class AdminSettingsServiceTest extends TestCase {
|
||||
};
|
||||
});
|
||||
|
||||
$this->config->expects($this->exactly(2))
|
||||
$this->config->expects($this->exactly(4))
|
||||
->method('getAppValueBool')
|
||||
->willReturnCallback(function ($key, $default, $lazy) {
|
||||
return match ($key) {
|
||||
AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS => true,
|
||||
AdminSettingsService::SETTING_IS_INITIALIZED => false,
|
||||
AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY => true,
|
||||
AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => false,
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
@@ -62,11 +64,13 @@ class AdminSettingsServiceTest extends TestCase {
|
||||
$result = $this->service->getAllSettings();
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertCount(4, $result);
|
||||
$this->assertCount(6, $result);
|
||||
$this->assertEquals('My Forum', $result[AdminSettingsService::SETTING_TITLE]);
|
||||
$this->assertEquals('Welcome!', $result[AdminSettingsService::SETTING_SUBTITLE]);
|
||||
$this->assertTrue($result[AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS]);
|
||||
$this->assertFalse($result[AdminSettingsService::SETTING_IS_INITIALIZED]);
|
||||
$this->assertTrue($result[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY]);
|
||||
$this->assertFalse($result[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE]);
|
||||
}
|
||||
|
||||
public function testGetSettingReturnsCorrectStringValue(): void {
|
||||
@@ -183,12 +187,14 @@ class AdminSettingsServiceTest extends TestCase {
|
||||
};
|
||||
});
|
||||
|
||||
$this->config->expects($this->exactly(2))
|
||||
$this->config->expects($this->exactly(4))
|
||||
->method('getAppValueBool')
|
||||
->willReturnCallback(function ($key, $default, $lazy) {
|
||||
return match ($key) {
|
||||
AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS => true,
|
||||
AdminSettingsService::SETTING_IS_INITIALIZED => false,
|
||||
AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY => true,
|
||||
AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => false,
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
@@ -232,12 +238,14 @@ class AdminSettingsServiceTest extends TestCase {
|
||||
};
|
||||
});
|
||||
|
||||
$this->config->expects($this->exactly(2))
|
||||
$this->config->expects($this->exactly(4))
|
||||
->method('getAppValueBool')
|
||||
->willReturnCallback(function ($key, $default, $lazy) {
|
||||
return match ($key) {
|
||||
AdminSettingsService::SETTING_ALLOW_GUEST_ACCESS => false,
|
||||
AdminSettingsService::SETTING_IS_INITIALIZED => false,
|
||||
AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY => true,
|
||||
AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE => false,
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
|
||||
172
tests/unit/Service/EditHistoryVisibilityServiceTest.php
Normal file
172
tests/unit/Service/EditHistoryVisibilityServiceTest.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Forum\Tests\Service;
|
||||
|
||||
use OCA\Forum\Service\AdminSettingsService;
|
||||
use OCA\Forum\Service\EditHistoryVisibilityService;
|
||||
use OCA\Forum\Service\PermissionService;
|
||||
use OCA\Forum\Service\UserPreferencesService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EditHistoryVisibilityServiceTest extends TestCase {
|
||||
private EditHistoryVisibilityService $service;
|
||||
/** @var AdminSettingsService&MockObject */
|
||||
private AdminSettingsService $adminSettingsService;
|
||||
/** @var UserPreferencesService&MockObject */
|
||||
private UserPreferencesService $userPreferencesService;
|
||||
/** @var PermissionService&MockObject */
|
||||
private PermissionService $permissionService;
|
||||
|
||||
private const CATEGORY_ID = 10;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->adminSettingsService = $this->createMock(AdminSettingsService::class);
|
||||
$this->userPreferencesService = $this->createMock(UserPreferencesService::class);
|
||||
$this->permissionService = $this->createMock(PermissionService::class);
|
||||
|
||||
$this->service = new EditHistoryVisibilityService(
|
||||
$this->adminSettingsService,
|
||||
$this->userPreferencesService,
|
||||
$this->permissionService,
|
||||
);
|
||||
}
|
||||
|
||||
public function testOwnerCanAlwaysSeeOwnHistory(): void {
|
||||
// No admin settings or permissions needed — owner always sees own history
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('user1', 'user1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testModeratorCanAlwaysSeeHistory(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')
|
||||
->with('mod1', self::CATEGORY_ID, 'canModerate')
|
||||
->willReturn(true);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')
|
||||
->willReturn(false);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('mod1', 'user1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testAdminRoleCanAlwaysSeeHistory(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')
|
||||
->willReturn(false);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')
|
||||
->with('admin1')
|
||||
->willReturn(true);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('admin1', 'user1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testPublicEditHistoryOffBlocksOtherUsers(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')->willReturn(false);
|
||||
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->with(AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY)
|
||||
->willReturn(false);
|
||||
|
||||
$this->assertFalse(
|
||||
$this->service->canViewEditHistory('viewer1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testPublicEditHistoryOnAllowsOtherUsers(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')->willReturn(false);
|
||||
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->willReturnMap([
|
||||
[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY, true],
|
||||
[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE, false],
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('viewer1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testUserOverrideHidesHistoryFromOthers(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')->willReturn(false);
|
||||
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->willReturnMap([
|
||||
[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY, true],
|
||||
[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE, true],
|
||||
]);
|
||||
|
||||
$this->userPreferencesService->method('getPreference')
|
||||
->with('author1', UserPreferencesService::PREF_HIDE_EDIT_HISTORY)
|
||||
->willReturn(true);
|
||||
|
||||
$this->assertFalse(
|
||||
$this->service->canViewEditHistory('viewer1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testUserOverrideDoesNotAffectOwner(): void {
|
||||
// Owner always sees own history regardless of override setting
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('author1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testUserOverrideDoesNotAffectModerator(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')
|
||||
->with('mod1', self::CATEGORY_ID, 'canModerate')
|
||||
->willReturn(true);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')
|
||||
->willReturn(false);
|
||||
|
||||
// Moderator can see history even when author has hidden it
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('mod1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGuestCannotSeeHistoryWhenPublicOff(): void {
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->with(AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY)
|
||||
->willReturn(false);
|
||||
|
||||
$this->assertFalse(
|
||||
$this->service->canViewEditHistory(null, 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGuestCanSeeHistoryWhenPublicOn(): void {
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->willReturnMap([
|
||||
[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY, true],
|
||||
[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE, false],
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory(null, 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
|
||||
public function testUserOverrideDisabledDoesNotHideHistory(): void {
|
||||
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
||||
$this->permissionService->method('hasAdminOrModeratorRole')->willReturn(false);
|
||||
|
||||
$this->adminSettingsService->method('getSetting')
|
||||
->willReturnMap([
|
||||
[AdminSettingsService::SETTING_PUBLIC_EDIT_HISTORY, true],
|
||||
[AdminSettingsService::SETTING_ALLOW_EDIT_HISTORY_USER_OVERRIDE, false],
|
||||
]);
|
||||
|
||||
// Even if user has hide_edit_history pref set, when override is disabled, history is visible
|
||||
$this->assertTrue(
|
||||
$this->service->canViewEditHistory('viewer1', 'author1', self::CATEGORY_ID)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
$userId = 'user1';
|
||||
|
||||
// Only config-based preferences (signature is from forum_users)
|
||||
$this->config->expects($this->exactly(3))
|
||||
$this->config->expects($this->exactly(4))
|
||||
->method('getUserValue')
|
||||
->willReturnCallback(function ($uid, $appId, $key, $default) use ($userId) {
|
||||
$this->assertEquals($userId, $uid);
|
||||
@@ -52,6 +52,7 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
UserPreferencesService::PREF_AUTO_SUBSCRIBE_CREATED_THREADS => 'true',
|
||||
UserPreferencesService::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS => 'false',
|
||||
UserPreferencesService::PREF_UPLOAD_DIRECTORY => 'Forum',
|
||||
UserPreferencesService::PREF_HIDE_EDIT_HISTORY => 'false',
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
@@ -59,11 +60,12 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
$result = $this->service->getAllPreferences($userId);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertCount(4, $result);
|
||||
$this->assertCount(5, $result);
|
||||
$this->assertTrue($result[UserPreferencesService::PREF_AUTO_SUBSCRIBE_CREATED_THREADS]);
|
||||
$this->assertFalse($result[UserPreferencesService::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS]);
|
||||
$this->assertEquals('Forum', $result[UserPreferencesService::PREF_UPLOAD_DIRECTORY]);
|
||||
$this->assertEquals('', $result[UserPreferencesService::PREF_SIGNATURE]);
|
||||
$this->assertFalse($result[UserPreferencesService::PREF_HIDE_EDIT_HISTORY]);
|
||||
}
|
||||
|
||||
public function testGetPreferenceReturnsCorrectValue(): void {
|
||||
@@ -146,7 +148,7 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
}
|
||||
});
|
||||
|
||||
$this->config->expects($this->exactly(3))
|
||||
$this->config->expects($this->exactly(4))
|
||||
->method('getUserValue')
|
||||
->willReturnCallback(function ($uid, $appId, $key, $default) use ($userId) {
|
||||
$this->assertEquals($userId, $uid);
|
||||
@@ -156,6 +158,7 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
UserPreferencesService::PREF_AUTO_SUBSCRIBE_CREATED_THREADS => 'false',
|
||||
UserPreferencesService::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS => 'false',
|
||||
UserPreferencesService::PREF_UPLOAD_DIRECTORY => 'Documents',
|
||||
UserPreferencesService::PREF_HIDE_EDIT_HISTORY => 'false',
|
||||
default => $default,
|
||||
};
|
||||
});
|
||||
@@ -163,11 +166,12 @@ class UserPreferencesServiceTest extends TestCase {
|
||||
$result = $this->service->updatePreferences($userId, $preferences);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertCount(4, $result);
|
||||
$this->assertCount(5, $result);
|
||||
$this->assertFalse($result[UserPreferencesService::PREF_AUTO_SUBSCRIBE_CREATED_THREADS]);
|
||||
$this->assertFalse($result[UserPreferencesService::PREF_AUTO_SUBSCRIBE_REPLIED_THREADS]);
|
||||
$this->assertEquals('Documents', $result[UserPreferencesService::PREF_UPLOAD_DIRECTORY]);
|
||||
$this->assertEquals('', $result[UserPreferencesService::PREF_SIGNATURE]);
|
||||
$this->assertFalse($result[UserPreferencesService::PREF_HIDE_EDIT_HISTORY]);
|
||||
}
|
||||
|
||||
public function testUpdatePreferencesThrowsExceptionForInvalidKey(): void {
|
||||
|
||||
Reference in New Issue
Block a user