Repository: wagtail/wagtail
File: wagtail/permission_policies/pages.py (Python)

Code before:
            ).values_list("group", flat=True)

            q |= Q(groups__in=add_groups) & Q(pk=instance.owner_id)

        return (
            get_user_model()
            ._default_manager.filter(is_active=True)
            .filter(q)
            .distinct()
        )

    def users_with_permission_for_instance(
        self, action, instance, include_superusers=True
    ):
        return self.users_with_any_permission_for_instance(
            {action}, instance, include_superusers
        )

    def instances_with_direct_explore_permission(self, user):
        # Get all pages that the user has direct add/change/publish/lock permission on
        if user.is_superuser:
            # superuser has implicit permission on the root node
            return Page.objects.filter(depth=1)
        else:
            codenames = self._get_permission_codenames(
                {"add", "change", "publish", "lock", "unlock"}
            )
            return [
                perm.page
                for perm in self.get_cached_permissions_for_user(user)
                if perm.permission.codename in codenames
            ]

    def explorable_root_instance(self, user):
        # This method is used all around the admin,
        # so cache the result on the user for the duration of the request
        if hasattr(user, self._explorable_root_instance_cache_name):
            return getattr(user, self._explorable_root_instance_cache_name)
        pages = self.instances_with_direct_explore_permission(user)
        try:
            root_page = Page.objects.first_common_ancestor_of(
                pages, include_self=True, strict=True
            )
        except Page.DoesNotExist:
            root_page = None
        setattr(user, self._explorable_root_instance_cache_name, root_page)
        return root_page

    def explorable_instances(self, user):
        base_queryset = self._base_queryset_for_user(user)
        if base_queryset is not None:

Review comment (security):
As mentioned in https://github.com/wagtail/wagtail/issues/12628#issuecomment-2559800970, while we're here, we should also add the `bulk_delete` permission here to be consistent with other "user has any page permission" checks

Code after:
            ).values_list("group", flat=True)

            q |= Q(groups__in=add_groups) & Q(pk=instance.owner_id)

        return (
            get_user_model()
            ._default_manager.filter(is_active=True)
            .filter(q)
            .distinct()
        )

    def users_with_permission_for_instance(
        self, action, instance, include_superusers=True
    ):
        return self.users_with_any_permission_for_instance(
            {action}, instance, include_superusers
        )

    def instances_with_direct_explore_permission(self, user):
        # Get all pages that the user has direct add/change/publish/lock permission on
        if user.is_superuser:
            # superuser has implicit permission on the root node
            return Page.objects.filter(depth=1)
        else:
            codenames = self._get_permission_codenames(
                {"add", "change", "publish", "lock", "unlock", "bulk_delete"}
            )
            return [
                perm.page
                for perm in self.get_cached_permissions_for_user(user)
                if perm.permission.codename in codenames
            ]

    def explorable_root_instance(self, user):
        # This method is used all around the admin,
        # so cache the result on the user for the duration of the request
        if hasattr(user, self._explorable_root_instance_cache_name):
            return getattr(user, self._explorable_root_instance_cache_name)
        pages = self.instances_with_direct_explore_permission(user)
        try:
            root_page = Page.objects.first_common_ancestor_of(
                pages, include_self=True, strict=True
            )
        except Page.DoesNotExist:
            root_page = None
        setattr(user, self._explorable_root_instance_cache_name, root_page)
        return root_page

    def explorable_instances(self, user):
        base_queryset = self._base_queryset_for_user(user)
        if base_queryset is not None: