diff --git a/dev-docs/plugins/hooks/check-browse-category-permission-hook.md b/dev-docs/plugins/hooks/check-browse-category-permission-hook.md new file mode 100644 index 0000000000..d8b3ce0f6c --- /dev/null +++ b/dev-docs/plugins/hooks/check-browse-category-permission-hook.md @@ -0,0 +1,112 @@ +# `check_browse_category_permission_hook` + +This hook wraps the standard function that Misago uses to check if the user has permission to browse a category. It also checks if the user can see the category. It raises Django's `Http404` if they can't see it or `PermissionDenied` with an error message if they can't browse it. + + +## Location + +This hook can be imported from `misago.permissions.hooks`: + +```python +from misago.permissions.hooks import check_browse_category_permission_hook +``` + + +## Filter + +```python +def custom_check_browse_category_permission_filter( + action: CheckBrowseCategoryPermissionHookAction, + permissions: 'UserPermissionsProxy', + category: Category, + can_delay: bool=False, +) -> None: + ... +``` + +A function implemented by a plugin that can be registered in this hook. + + +### Arguments + +#### `action: CheckBrowseCategoryPermissionHookAction` + +A standard Misago function used to check if the user has permission to browse a category. It also checks if the user can see the category. It raises Django's `Http404` if they can't see it or `PermissionDenied` with an error message if they can't browse it. + +Browse the [action](#action) section for details. + + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `category: Category` + +A category to check permissions for. + + +#### `can_delay: Bool = False` + +A `bool` that specifies if this check can be delayed. If the category can be seen by the user but they have no permission to browse it, and both `can_delay` and `category.delay_browse_check` are `True`, a `PermissionDenied` error will not be raised. + + +## Action + +```python +def check_browse_category_permission_action( + permissions: 'UserPermissionsProxy', + category: Category, + can_delay: bool=False, +) -> None: + ... +``` + +A standard Misago function used to check if the user has permission to browse a category. It also checks if the user can see the category. It raises Django's `Http404` if they can't see it or `PermissionDenied` with an error message if they can't browse it. + + +### Arguments + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `category: Category` + +A category to check permissions for. + + +#### `can_delay: Bool = False` + +A `bool` that specifies if this check can be delayed. If the category can be seen by the user but they have no permission to browse it, and both `can_delay` and `category.delay_browse_check` are `True`, a `PermissionDenied` error will not be raised. + + +## Example + +The code below implements a custom filter function that blocks a user from browsing a specified category if there is a custom flag set on their account. + +```python +from django.core.exceptions import PermissionDenied +from django.utils.translation import pgettext +from misago.categories.models import Category +from misago.permissions.hooks import check_browse_category_permission_hook +from misago.permissions.proxy import UserPermissionsProxy + +@check_browse_category_permission_hook.append_filter +def check_user_can_browse_category( + action, + permissions: UserPermissionsProxy, + category: Category, +) -> None: + # Run standard permission checks + action(permissions, category) + + if category.id in permissions.user.plugin_data.get("banned_categories", []): + raise PermissionDenied( + pgettext( + "category permission error", + "Site admin has removed your access to this category." + ) + ) +``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/check-private-threads-permission-hook.md b/dev-docs/plugins/hooks/check-private-threads-permission-hook.md new file mode 100644 index 0000000000..a44b8ca027 --- /dev/null +++ b/dev-docs/plugins/hooks/check-private-threads-permission-hook.md @@ -0,0 +1,84 @@ +# `check_private_threads_permission_hook` + +This hook wraps the standard function that Misago uses to check if the user has a permission to access private threads feature. Raises Django's `PermissionDenied` with an error message if they don't. + + +## Location + +This hook can be imported from `misago.permissions.hooks`: + +```python +from misago.permissions.hooks import check_private_threads_permission_hook +``` + + +## Filter + +```python +def custom_check_private_threads_permission_filter( + action: CheckPrivateThreadsPermissionHookAction, + permissions: 'UserPermissionsProxy', +) -> None: + ... +``` + +A function implemented by a plugin that can be registered in this hook. + + +### Arguments + +#### `action: CheckPrivateThreadsPermissionHookAction` + +A standard Misago function used to check if the user has a permission to access private threads feature. Raises Django's `PermissionDenied` with an error message if they don't. + +See the [action](#action) section for details. + + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +## Action + +```python +def check_private_threads_permission_action(permissions: 'UserPermissionsProxy') -> None: + ... +``` + +A standard Misago function used to check if the user has a permission to access private threads feature. Raises Django's `PermissionDenied` with an error message if they don't. + + +### Arguments + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +## Example + +The code below implements a custom filter function that blocks user from using private threads if there's a custom flag set on their account. + +```python +from django.core.exceptions import PermissionDenied +from django.utils.translation import pgettext +from misago.permissions.hooks import check_private_threads_permission_hook +from misago.permissions.proxy import UserPermissionsProxy + +@check_private_threads_permission_hook.append_filter +def check_user_is_banned_from_private_threads( + action, + permissions: UserPermissionsProxy, +) -> None: + # Run standard permission checks + action(permissions) + + if permissions.user.plugin_data.get("ban_private_threads"): + raise PermissionDenied( + pgettext( + "private threads permission error", + "Site admin has removed your access to private threads." + ) + ) +``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/check-see-category-permission-hook.md b/dev-docs/plugins/hooks/check-see-category-permission-hook.md new file mode 100644 index 0000000000..952110003d --- /dev/null +++ b/dev-docs/plugins/hooks/check-see-category-permission-hook.md @@ -0,0 +1,99 @@ +# `check_see_category_permission_hook` + +This hook wraps the standard function that Misago uses to check if the user has a permission to see a category. Raises Django's `Http404` error if they don't. + + +## Location + +This hook can be imported from `misago.permissions.hooks`: + +```python +from misago.permissions.hooks import check_see_category_permission_hook +``` + + +## Filter + +```python +def custom_check_see_category_permission_filter( + action: CheckSeeCategoryPermissionHookAction, + permissions: 'UserPermissionsProxy', + category: Category, +) -> None: + ... +``` + +A function implemented by a plugin that can be registered in this hook. + + +### Arguments + +#### `action: CheckSeeCategoryPermissionHookAction` + +A standard Misago function used to check if the user has a permission to see a category. Raises Django's `Http404` error if they don't. + +See the [action](#action) section for details. + + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `category: Category` + +A category to check permissions for. + + +## Action + +```python +def check_see_category_permission_action( + permissions: 'UserPermissionsProxy', category: Category +) -> None: + ... +``` + +A standard Misago function used to check if the user has a permission to see a category. Raises Django's `Http404` error if they don't. + + +### Arguments + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `category: Category` + +A category to check permissions for. + + +## Example + +The code below implements a custom filter function that blocks a user from seeing a specified category if there is a custom flag set on their account. + +```python +from django.core.exceptions import PermissionDenied +from django.utils.translation import pgettext +from misago.categories.models import Category +from misago.permissions.hooks import check_see_category_permission_hook +from misago.permissions.proxy import UserPermissionsProxy + +@check_see_category_permission_hook.append_filter +def check_user_can_see_category( + action, + permissions: UserPermissionsProxy, + category: Category, +) -> None: + # Run standard permission checks + action(permissions, category) + + if category.id in permissions.user.plugin_data.get("banned_categories", []): + raise PermissionDenied( + pgettext( + "category permission error", + "Site admin has removed your access to this category." + ) + ) +``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/filter-private-threads-queryset-hook.md b/dev-docs/plugins/hooks/filter-private-threads-queryset-hook.md new file mode 100644 index 0000000000..d92e66f7a3 --- /dev/null +++ b/dev-docs/plugins/hooks/filter-private-threads-queryset-hook.md @@ -0,0 +1,105 @@ +# `filter_private_threads_queryset_hook` + +This hook wraps the standard function that Misago uses set filters on a private threads queryset to limit it only to threads that the user has access to. + + +## Location + +This hook can be imported from `misago.permissions.hooks`: + +```python +from misago.permissions.hooks import filter_private_threads_queryset_hook +``` + + +## Filter + +```python +def custom_private_threads_queryset_filter( + action: FilterPrivateThreadsQuerysetHookAction, + permissions: 'UserPermissionsProxy', + queryset, +) -> None: + ... +``` + +A function implemented by a plugin that can be registered in this hook. + + +### Arguments + +#### `action: FilterPrivateThreadsQuerysetHookAction` + +A standard Misago function used to set filters on a private threads queryset to limit it only to threads that the user has access to. + +See the [action](#action) section for details. + + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `queryset: Queryset` + +A queryset returning all private threads. + + +#### Return value + +A `queryset` filtered to show only private threads that the user has access to. + + +## Action + +```python +def filter_private_threads_queryset_action(permissions: 'UserPermissionsProxy', queryset): + ... +``` + +A standard Misago function used to set filters on a private threads queryset to limit it only to threads that the user has access to. + + +### Arguments + +#### `user_permissions: UserPermissionsProxy` + +A proxy object with the current user's permissions. + + +#### `queryset: Queryset` + +A queryset returning all private threads. + + +#### Return value + +A `queryset` filtered to show only private threads that the user has access to. + + +## Example + +The code below implements a custom filter function that makes old private threads not available to the user. + +```python +from datetime import timedelta + +from django.utils import timezone +from misago.permissions.hooks import filter_private_threads_queryset_hook +from misago.permissions.proxy import UserPermissionsProxy + +@filter_private_threads_queryset_hook.append_filter +def exclude_old_private_threads_queryset_hook( + action, + permissions: UserPermissionsProxy, + queryset, +) -> None: + queryset = action(permissions, queryset) + + if permissions.private_threads_moderator: + return queryset + + return queryset.filter( + last_post_on__gt=timezone.now - timedelta(days=30), + ) +``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/get-category-threads-category-query-hook.md b/dev-docs/plugins/hooks/get-category-threads-category-query-hook.md index a1daf04200..1f31807f9b 100644 --- a/dev-docs/plugins/hooks/get-category-threads-category-query-hook.md +++ b/dev-docs/plugins/hooks/get-category-threads-category-query-hook.md @@ -111,7 +111,6 @@ A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, The code below implements a custom filter function that specifies a custom `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python -from django.db.models import Q from misago.permissions.enums import CategoryQueryContext from misago.permissions.hooks import get_category_threads_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -122,11 +121,11 @@ def get_category_threads_category_query( permissions: UserPermissionsProxy, category: dict, context: CategoryQueryContext, -) -> -> str | list[str] | None: +) -> str | list[str] | None: if ( category.get("plugin_flag") and context == CategoryQueryContext.CURRENT ): return "plugin-where" - return action(query, permissions, category, context) + return action(permissions, category, context) ``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/get-category-threads-pinned-category-query-hook.md b/dev-docs/plugins/hooks/get-category-threads-pinned-category-query-hook.md index fa47a502cb..13e1210c10 100644 --- a/dev-docs/plugins/hooks/get-category-threads-pinned-category-query-hook.md +++ b/dev-docs/plugins/hooks/get-category-threads-pinned-category-query-hook.md @@ -111,7 +111,6 @@ A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, The code below implements a custom filter function that specifies a custom `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python -from django.db.models import Q from misago.permissions.enums import CategoryQueryContext from misago.permissions.hooks import get_category_threads_pinned_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -122,11 +121,11 @@ def get_category_threads_pinned_category_query( permissions: UserPermissionsProxy, category: dict, context: CategoryQueryContext, -) -> -> str | list[str] | None: +) -> str | list[str] | None: if ( category.get("plugin_flag") and context == CategoryQueryContext.CURRENT ): return "plugin-where" - return action(query, permissions, category, context) + return action(permissions, category, context) ``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/get-threads-category-query-hook.md b/dev-docs/plugins/hooks/get-threads-category-query-hook.md index a9343491ef..f02a872481 100644 --- a/dev-docs/plugins/hooks/get-threads-category-query-hook.md +++ b/dev-docs/plugins/hooks/get-threads-category-query-hook.md @@ -90,7 +90,6 @@ A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, The code below implements a custom filter function that specifies a custom `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python -from django.db.models import Q from misago.permissions.hooks import get_threads_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -99,9 +98,9 @@ def get_threads_category_query( action, permissions: UserPermissionsProxy, category: dict, -) -> -> str | list[str] | None: +) -> str | list[str] | None: if category.get("plugin_flag"): return "plugin-where" - return action(query, permissions, category) + return action(permissions, category) ``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/get-threads-pinned-category-query-hook.md b/dev-docs/plugins/hooks/get-threads-pinned-category-query-hook.md index 7ea5b021b6..d72723d840 100644 --- a/dev-docs/plugins/hooks/get-threads-pinned-category-query-hook.md +++ b/dev-docs/plugins/hooks/get-threads-pinned-category-query-hook.md @@ -90,7 +90,6 @@ A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, The code below implements a custom filter function that specifies a custom `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python -from django.db.models import Q from misago.permissions.hooks import get_threads_pinned_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -99,9 +98,9 @@ def get_threads_category_pinned_query( action, permissions: UserPermissionsProxy, category: dict, -) -> -> str | list[str] | None: +) -> str | list[str] | None: if category.get("plugin_flag"): return "plugin-where" - return action(query, permissions, category) + return action(permissions, category) ``` \ No newline at end of file diff --git a/dev-docs/plugins/hooks/reference.md b/dev-docs/plugins/hooks/reference.md index 2bcf919cba..46bc5d843c 100644 --- a/dev-docs/plugins/hooks/reference.md +++ b/dev-docs/plugins/hooks/reference.md @@ -47,8 +47,12 @@ Hooks instances are importable from the following Python modules: - [`build_user_category_permissions_hook`](./build-user-category-permissions-hook.md) - [`build_user_permissions_hook`](./build-user-permissions-hook.md) +- [`check_browse_category_permission_hook`](./check-browse-category-permission-hook.md) +- [`check_private_threads_permission_hook`](./check-private-threads-permission-hook.md) +- [`check_see_category_permission_hook`](./check-see-category-permission-hook.md) - [`copy_category_permissions_hook`](./copy-category-permissions-hook.md) - [`copy_group_permissions_hook`](./copy-group-permissions-hook.md) +- [`filter_private_threads_queryset_hook`](./filter-private-threads-queryset-hook.md) - [`get_admin_category_permissions_hook`](./get-admin-category-permissions-hook.md) - [`get_category_threads_category_query_hook`](./get-category-threads-category-query-hook.md) - [`get_category_threads_pinned_category_query_hook`](./get-category-threads-pinned-category-query-hook.md) diff --git a/misago/permissions/categories.py b/misago/permissions/categories.py index 9d1c96c43b..d3e8310aa6 100644 --- a/misago/permissions/categories.py +++ b/misago/permissions/categories.py @@ -4,12 +4,25 @@ from ..categories.models import Category from .enums import CategoryPermission +from .hooks import ( + check_browse_category_permission_hook, + check_see_category_permission_hook, +) from .proxy import UserPermissionsProxy def check_see_category_permission( permissions: UserPermissionsProxy, category: Category, +): + check_see_category_permission_hook( + _check_see_category_permission_action, permissions, category + ) + + +def _check_see_category_permission_action( + permissions: UserPermissionsProxy, + category: Category, ): if category.id not in permissions.categories[CategoryPermission.SEE]: raise Http404() @@ -18,12 +31,25 @@ def check_see_category_permission( def check_browse_category_permission( permissions: UserPermissionsProxy, category: Category, - delay_browse_check: bool = False, + can_delay: bool = False, +): + check_browse_category_permission_hook( + _check_browse_category_permission_action, + permissions, + category, + can_delay, + ) + + +def _check_browse_category_permission_action( + permissions: UserPermissionsProxy, + category: Category, + can_delay: bool = False, ): check_see_category_permission(permissions, category) if category.id not in permissions.categories[CategoryPermission.BROWSE] and not ( - delay_browse_check and category.delay_browse_check + can_delay and category.delay_browse_check ): raise PermissionDenied( pgettext( diff --git a/misago/permissions/hooks/__init__.py b/misago/permissions/hooks/__init__.py index ac70780f9f..271efc7fc8 100644 --- a/misago/permissions/hooks/__init__.py +++ b/misago/permissions/hooks/__init__.py @@ -1,7 +1,11 @@ from .build_user_category_permissions import build_user_category_permissions_hook from .build_user_permissions import build_user_permissions_hook +from .check_browse_category_permission import check_browse_category_permission_hook +from .check_private_threads_permission import check_private_threads_permission_hook +from .check_see_category_permission import check_see_category_permission_hook from .copy_category_permissions import copy_category_permissions_hook from .copy_group_permissions import copy_group_permissions_hook +from .filter_private_threads_queryset import filter_private_threads_queryset_hook from .get_admin_category_permissions import get_admin_category_permissions_hook from .get_category_threads_category_query import ( get_category_threads_category_query_hook, @@ -17,8 +21,12 @@ __all__ = [ "build_user_category_permissions_hook", "build_user_permissions_hook", + "check_browse_category_permission_hook", + "check_private_threads_permission_hook", + "check_see_category_permission_hook", "copy_category_permissions_hook", "copy_group_permissions_hook", + "filter_private_threads_queryset_hook", "get_admin_category_permissions_hook", "get_category_threads_category_query_hook", "get_category_threads_pinned_category_query_hook", diff --git a/misago/permissions/hooks/check_browse_category_permission.py b/misago/permissions/hooks/check_browse_category_permission.py new file mode 100644 index 0000000000..82fd8204d3 --- /dev/null +++ b/misago/permissions/hooks/check_browse_category_permission.py @@ -0,0 +1,138 @@ +from typing import TYPE_CHECKING, Protocol + +from ...categories.models import Category +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class CheckBrowseCategoryPermissionHookAction(Protocol): + """ + A standard Misago function used to check if the user has permission to + browse a category. It also checks if the user can see the category. + It raises Django's `Http404` if they can't see it or `PermissionDenied` + with an error message if they can't browse it. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + + ## `can_delay: Bool = False` + + A `bool` that specifies if this check can be delayed. If the category can be + seen by the user but they have no permission to browse it, and both `can_delay` + and `category.delay_browse_check` are `True`, a `PermissionDenied` error + will not be raised. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + category: Category, + can_delay: bool = False, + ) -> None: ... + + +class CheckBrowseCategoryPermissionHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: CheckBrowseCategoryPermissionHookAction` + + A standard Misago function used to check if the user has permission to + browse a category. It also checks if the user can see the category. + It raises Django's `Http404` if they can't see it or `PermissionDenied` + with an error message if they can't browse it. + + Browse the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + + ## `can_delay: Bool = False` + + A `bool` that specifies if this check can be delayed. If the category can be + seen by the user but they have no permission to browse it, and both `can_delay` + and `category.delay_browse_check` are `True`, a `PermissionDenied` error + will not be raised. + """ + + def __call__( + self, + action: CheckBrowseCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + can_delay: bool = False, + ) -> None: ... + + +class CheckBrowseCategoryPermissionHook( + FilterHook[ + CheckBrowseCategoryPermissionHookAction, + CheckBrowseCategoryPermissionHookFilter, + ] +): + """ + This hook wraps the standard function that Misago uses to check if the user + has permission to browse a category. It also checks if the user can see the + category. It raises Django's `Http404` if they can't see it or `PermissionDenied` + with an error message if they can't browse it. + + # Example + + The code below implements a custom filter function that blocks a user from + browsing a specified category if there is a custom flag set on their account. + + ```python + from django.core.exceptions import PermissionDenied + from django.utils.translation import pgettext + from misago.categories.models import Category + from misago.permissions.hooks import check_browse_category_permission_hook + from misago.permissions.proxy import UserPermissionsProxy + + @check_browse_category_permission_hook.append_filter + def check_user_can_browse_category( + action, + permissions: UserPermissionsProxy, + category: Category, + ) -> None: + # Run standard permission checks + action(permissions, category) + + if category.id in permissions.user.plugin_data.get("banned_categories", []): + raise PermissionDenied( + pgettext( + "category permission error", + "Site admin has removed your access to this category." + ) + ) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: CheckBrowseCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + can_delay: bool = False, + ) -> None: + return super().__call__(action, permissions, category, can_delay) + + +check_browse_category_permission_hook = CheckBrowseCategoryPermissionHook() diff --git a/misago/permissions/hooks/check_private_threads_permission.py b/misago/permissions/hooks/check_private_threads_permission.py new file mode 100644 index 0000000000..72717be6e8 --- /dev/null +++ b/misago/permissions/hooks/check_private_threads_permission.py @@ -0,0 +1,104 @@ +from typing import TYPE_CHECKING, Protocol + +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class CheckPrivateThreadsPermissionHookAction(Protocol): + """ + A standard Misago function used to check if the user has a permission to access + private threads feature. Raises Django's `PermissionDenied` with an error + message if they don't. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + ) -> None: ... + + +class CheckPrivateThreadsPermissionHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: CheckPrivateThreadsPermissionHookAction` + + A standard Misago function used to check if the user has a permission to access + private threads feature. Raises Django's `PermissionDenied` with an error + message if they don't. + + See the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + """ + + def __call__( + self, + action: CheckPrivateThreadsPermissionHookAction, + permissions: "UserPermissionsProxy", + ) -> None: ... + + +class CheckPrivateThreadsPermissionHook( + FilterHook[ + CheckPrivateThreadsPermissionHookAction, + CheckPrivateThreadsPermissionHookFilter, + ] +): + """ + This hook wraps the standard function that Misago uses to check if the user + has a permission to access private threads feature. Raises Django's + `PermissionDenied` with an error message if they don't. + + # Example + + The code below implements a custom filter function that blocks user from using + private threads if there's a custom flag set on their account. + + ```python + from django.core.exceptions import PermissionDenied + from django.utils.translation import pgettext + from misago.permissions.hooks import check_private_threads_permission_hook + from misago.permissions.proxy import UserPermissionsProxy + + @check_private_threads_permission_hook.append_filter + def check_user_is_banned_from_private_threads( + action, + permissions: UserPermissionsProxy, + ) -> None: + # Run standard permission checks + action(permissions) + + if permissions.user.plugin_data.get("ban_private_threads"): + raise PermissionDenied( + pgettext( + "private threads permission error", + "Site admin has removed your access to private threads." + ) + ) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: CheckPrivateThreadsPermissionHookAction, + permissions: "UserPermissionsProxy", + ) -> None: + return super().__call__(action, permissions) + + +check_private_threads_permission_hook = CheckPrivateThreadsPermissionHook() diff --git a/misago/permissions/hooks/check_see_category_permission.py b/misago/permissions/hooks/check_see_category_permission.py new file mode 100644 index 0000000000..9b03957ab4 --- /dev/null +++ b/misago/permissions/hooks/check_see_category_permission.py @@ -0,0 +1,115 @@ +from typing import TYPE_CHECKING, Protocol + +from ...categories.models import Category +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class CheckSeeCategoryPermissionHookAction(Protocol): + """ + A standard Misago function used to check if the user has a permission to see + a category. Raises Django's `Http404` error if they don't. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckSeeCategoryPermissionHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: CheckSeeCategoryPermissionHookAction` + + A standard Misago function used to check if the user has a permission to see + a category. Raises Django's `Http404` error if they don't. + + See the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + action: CheckSeeCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckSeeCategoryPermissionHook( + FilterHook[ + CheckSeeCategoryPermissionHookAction, + CheckSeeCategoryPermissionHookFilter, + ] +): + """ + This hook wraps the standard function that Misago uses to check if the user + has a permission to see a category. Raises Django's `Http404` error if they don't. + + # Example + + The code below implements a custom filter function that blocks a user from seeing + a specified category if there is a custom flag set on their account. + + ```python + from django.core.exceptions import PermissionDenied + from django.utils.translation import pgettext + from misago.categories.models import Category + from misago.permissions.hooks import check_see_category_permission_hook + from misago.permissions.proxy import UserPermissionsProxy + + @check_see_category_permission_hook.append_filter + def check_user_can_see_category( + action, + permissions: UserPermissionsProxy, + category: Category, + ) -> None: + # Run standard permission checks + action(permissions, category) + + if category.id in permissions.user.plugin_data.get("banned_categories", []): + raise PermissionDenied( + pgettext( + "category permission error", + "Site admin has removed your access to this category." + ) + ) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: CheckSeeCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: + return super().__call__(action, permissions, category) + + +check_see_category_permission_hook = CheckSeeCategoryPermissionHook() diff --git a/misago/permissions/hooks/filter_private_threads_queryset.py b/misago/permissions/hooks/filter_private_threads_queryset.py new file mode 100644 index 0000000000..0e73b8fa2c --- /dev/null +++ b/misago/permissions/hooks/filter_private_threads_queryset.py @@ -0,0 +1,120 @@ +from typing import TYPE_CHECKING, Protocol + +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class FilterPrivateThreadsQuerysetHookAction(Protocol): + """ + A standard Misago function used to set filters on a private threads queryset + to limit it only to threads that the user has access to. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `queryset: Queryset` + + A queryset returning all private threads. + + ## Return value + + A `queryset` filtered to show only private threads that the user has access to. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + queryset, + ): ... + + +class FilterPrivateThreadsQuerysetHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: FilterPrivateThreadsQuerysetHookAction` + + A standard Misago function used to set filters on a private threads queryset to limit + it only to threads that the user has access to. + + See the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `queryset: Queryset` + + A queryset returning all private threads. + + ## Return value + + A `queryset` filtered to show only private threads that the user has access to. + """ + + def __call__( + self, + action: FilterPrivateThreadsQuerysetHookAction, + permissions: "UserPermissionsProxy", + queryset, + ) -> None: ... + + +class FilterPrivateThreadsQuerysetHook( + FilterHook[ + FilterPrivateThreadsQuerysetHookAction, + FilterPrivateThreadsQuerysetHookFilter, + ] +): + """ + This hook wraps the standard function that Misago uses set filters on + a private threads queryset to limit it only to threads that the user has access to. + + # Example + + The code below implements a custom filter function that makes old private threads + not available to the user. + + ```python + from datetime import timedelta + + from django.utils import timezone + from misago.permissions.hooks import filter_private_threads_queryset_hook + from misago.permissions.proxy import UserPermissionsProxy + + @filter_private_threads_queryset_hook.append_filter + def exclude_old_private_threads_queryset_hook( + action, + permissions: UserPermissionsProxy, + queryset, + ) -> None: + queryset = action(permissions, queryset) + + if permissions.private_threads_moderator: + return queryset + + return queryset.filter( + last_post_on__gt=timezone.now - timedelta(days=30), + ) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: FilterPrivateThreadsQuerysetHookAction, + permissions: "UserPermissionsProxy", + queryset, + ) -> None: + return super().__call__(action, permissions, queryset) + + +filter_private_threads_queryset_hook = FilterPrivateThreadsQuerysetHook() diff --git a/misago/permissions/hooks/get_category_threads_category_query.py b/misago/permissions/hooks/get_category_threads_category_query.py index 012d025bef..f8c8e23dc2 100644 --- a/misago/permissions/hooks/get_category_threads_category_query.py +++ b/misago/permissions/hooks/get_category_threads_category_query.py @@ -125,7 +125,6 @@ class GetCategoryThreadsCategoryQueryHook( `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python - from django.db.models import Q from misago.permissions.enums import CategoryQueryContext from misago.permissions.hooks import get_category_threads_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -136,13 +135,13 @@ def get_category_threads_category_query( permissions: UserPermissionsProxy, category: dict, context: CategoryQueryContext, - ) -> -> str | list[str] | None: + ) -> str | list[str] | None: if ( category.get("plugin_flag") and context == CategoryQueryContext.CURRENT ): return "plugin-where" - return action(query, permissions, category, context) + return action(permissions, category, context) ``` """ diff --git a/misago/permissions/hooks/get_category_threads_pinned_category_query.py b/misago/permissions/hooks/get_category_threads_pinned_category_query.py index 24d7bf2bae..5e0eacbc70 100644 --- a/misago/permissions/hooks/get_category_threads_pinned_category_query.py +++ b/misago/permissions/hooks/get_category_threads_pinned_category_query.py @@ -127,7 +127,6 @@ class GetCategoryThreadsPinnedCategoryQueryHook( `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python - from django.db.models import Q from misago.permissions.enums import CategoryQueryContext from misago.permissions.hooks import get_category_threads_pinned_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -138,13 +137,13 @@ def get_category_threads_pinned_category_query( permissions: UserPermissionsProxy, category: dict, context: CategoryQueryContext, - ) -> -> str | list[str] | None: + ) -> str | list[str] | None: if ( category.get("plugin_flag") and context == CategoryQueryContext.CURRENT ): return "plugin-where" - return action(query, permissions, category, context) + return action(permissions, category, context) ``` """ diff --git a/misago/permissions/hooks/get_threads_category_query.py b/misago/permissions/hooks/get_threads_category_query.py index 0c2643c378..f7a00aa52d 100644 --- a/misago/permissions/hooks/get_threads_category_query.py +++ b/misago/permissions/hooks/get_threads_category_query.py @@ -100,7 +100,6 @@ class GetThreadsCategoryQueryHook( `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python - from django.db.models import Q from misago.permissions.hooks import get_threads_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -109,11 +108,11 @@ def get_threads_category_query( action, permissions: UserPermissionsProxy, category: dict, - ) -> -> str | list[str] | None: + ) -> str | list[str] | None: if category.get("plugin_flag"): return "plugin-where" - return action(query, permissions, category) + return action(permissions, category) ``` """ diff --git a/misago/permissions/hooks/get_threads_pinned_category_query.py b/misago/permissions/hooks/get_threads_pinned_category_query.py index 8828a0f3b7..b69b7f0c6f 100644 --- a/misago/permissions/hooks/get_threads_pinned_category_query.py +++ b/misago/permissions/hooks/get_threads_pinned_category_query.py @@ -100,7 +100,6 @@ class GetThreadsCategoryPinnedQueryHook( `WHERE` clause supported by the `get_threads_query_orm_filter_hook`. ```python - from django.db.models import Q from misago.permissions.hooks import get_threads_pinned_category_query_hook from misago.permissions.proxy import UserPermissionsProxy @@ -109,11 +108,11 @@ def get_threads_category_pinned_query( action, permissions: UserPermissionsProxy, category: dict, - ) -> -> str | list[str] | None: + ) -> str | list[str] | None: if category.get("plugin_flag"): return "plugin-where" - return action(query, permissions, category) + return action(permissions, category) ``` """ diff --git a/misago/permissions/private_threads.py b/misago/permissions/private_threads.py index aad5c17b94..6225a222aa 100644 --- a/misago/permissions/private_threads.py +++ b/misago/permissions/private_threads.py @@ -1,12 +1,21 @@ from django.core.exceptions import PermissionDenied -from django.db.models import Q from django.utils.translation import pgettext from ..threads.models import ThreadParticipant +from .hooks import ( + check_private_threads_permission_hook, + filter_private_threads_queryset_hook, +) from .proxy import UserPermissionsProxy def check_private_threads_permission(permissions: UserPermissionsProxy): + check_private_threads_permission_hook( + _check_private_threads_permission_action, permissions + ) + + +def _check_private_threads_permission_action(permissions: UserPermissionsProxy): if permissions.user.is_anonymous: raise PermissionDenied( pgettext( @@ -24,12 +33,20 @@ def check_private_threads_permission(permissions: UserPermissionsProxy): ) -def filter_private_threads_queryset(user_permissions: UserPermissionsProxy, queryset): - if user_permissions.user.is_anonymous: +def filter_private_threads_queryset(permissions: UserPermissionsProxy, queryset): + return filter_private_threads_queryset_hook( + _filter_private_threads_queryset_action, permissions, queryset + ) + + +def _filter_private_threads_queryset_action( + permissions: UserPermissionsProxy, queryset +): + if permissions.user.is_anonymous: return queryset.none() return queryset.filter( - id__in=ThreadParticipant.objects.filter(user=user_permissions.user).values( + id__in=ThreadParticipant.objects.filter(user=permissions.user).values( "thread_id" ) ) diff --git a/misago/threads/views/list.py b/misago/threads/views/list.py index d10a2f1aca..865ceb697e 100644 --- a/misago/threads/views/list.py +++ b/misago/threads/views/list.py @@ -734,9 +734,7 @@ def get_category(self, request: HttpRequest, kwargs: dict): raise Http404() check_browse_category_permission( - request.user_permissions, - category, - delay_browse_check=True, + request.user_permissions, category, can_delay=True ) if category.slug != kwargs["slug"]: