from warnings import warn from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from wagtail import hooks from wagtail.coreutils import accepts_kwarg from wagtail.utils.deprecation import RemovedInWagtail80Warning class BaseItem: template = "wagtailadmin/userbar/item_base.html" def get_context_data(self, request): return {"self": self, "request": request} def render(self, request): return render_to_string(self.template, self.get_context_data(request)) class AdminItem(BaseItem): template = "wagtailadmin/userbar/item_admin.html" def render(self, request): # Don't render if user doesn't have permission to access the admin area if not request.user.has_perm("wagtailadmin.access_admin"): return "" return super().render(request) class AccessibilityItem(BaseItem): """A userbar item that runs the accessibility checker.""" #: The template to use for rendering the item. template = "wagtailadmin/userbar/item_accessibility.html" #: A list of CSS selector(s) to test specific parts of the page. #: For more details, see `Axe documentation `__. axe_include = ["body"] #: A list of CSS selector(s) to exclude specific parts of the page from testing. #: For more details, see `Axe documentation `__. axe_exclude = [] # Make sure that the userbar is not tested. _axe_default_exclude = [{"fromShadowDOM": ["wagtail-userbar"]}] #: A list of `axe-core tags `_ #: or a list of `axe-core rule IDs `_ #: (not a mix of both). #: Setting this to a falsy value (e.g. ``None``) will omit the ``runOnly`` option and make Axe run with all non-experimental rules enabled. axe_run_only = [ "button-name", "empty-heading", "empty-table-header", "frame-title", "heading-order", "input-button-name", "link-name", "p-as-heading", "alt-text-quality", ] #: A dictionary that maps axe-core rule IDs to a dictionary of rule options, #: commonly in the format of ``{"enabled": True/False}``. This can be used in #: conjunction with :attr:`axe_run_only` to enable or disable specific rules. #: For more details, see `Axe documentation `__. axe_rules = {} #: A list to add custom Axe rules or override their properties, #: alongside with ``axe_custom_checks``. Includes Wagtail’s custom rules. #: For more details, see `Axe documentation `_. axe_custom_rules = [ { "id": "alt-text-quality", "impact": "serious", "selector": "img[alt]", "tags": ["best-practice"], "any": ["check-image-alt-text"], # If omitted, defaults to True and overrides configs in `axe_run_only`. "enabled": True, }, ] #: A list to add custom Axe checks or override their properties. #: Should be used in conjunction with ``axe_custom_rules``. #: For more details, see `Axe documentation `_. axe_custom_checks = [ { "id": "check-image-alt-text", "options": {"pattern": "\\.(avif|gif|jpg|jpeg|png|svg|webp)$|_"}, }, ] #: A dictionary that maps axe-core rule IDs to custom translatable strings #: to use as the error messages. If an enabled rule does not exist in this #: dictionary, Axe's error message for the rule will be used as fallback. axe_messages = { "button-name": { "error_name": _("Button text is empty"), "help_text": _("Use meaningful text for screen reader users"), }, "empty-heading": { "error_name": _("Empty heading found"), "help_text": _("Use meaningful text for screen reader users"), }, "empty-table-header": { "error_name": _("Table header text is empty"), "help_text": _("Use meaningful text for screen reader users"), }, "frame-title": { "error_name": _("Empty frame title found"), "help_text": _("Use a meaningful title for screen reader users"), }, "heading-order": { "error_name": _("Incorrect heading hierarchy"), "help_text": _("Avoid skipping levels"), }, "input-button-name": { "error_name": _("Input button text is empty"), "help_text": _("Use meaningful text for screen reader users"), }, "link-name": { "error_name": _("Link text is empty"), "help_text": _("Use meaningful text for screen reader users"), }, "p-as-heading": { "error_name": _("Misusing paragraphs as headings"), "help_text": _("Use proper heading tags"), }, "alt-text-quality": { "error_name": _("Image alt text has inappropriate pattern"), "help_text": _("Use meaningful text"), }, } def get_axe_include(self, request): """Returns a list of CSS selector(s) to test specific parts of the page.""" return self.axe_include def get_axe_exclude(self, request): """Returns a list of CSS selector(s) to exclude specific parts of the page from testing.""" return self.axe_exclude + self._axe_default_exclude def get_axe_run_only(self, request): """Returns a list of axe-core tags or a list of axe-core rule IDs (not a mix of both).""" return self.axe_run_only def get_axe_rules(self, request): """Returns a dictionary that maps axe-core rule IDs to a dictionary of rule options.""" return self.axe_rules def get_axe_custom_rules(self, request): """List of rule objects per axe.run API.""" return self.axe_custom_rules def get_axe_custom_checks(self, request): """List of check objects per axe.run API, without evaluate function.""" return self.axe_custom_checks def get_axe_messages(self, request): """Returns a dictionary that maps axe-core rule IDs to custom translatable strings.""" return self.axe_messages def get_axe_context(self, request): """ Returns the `context object `_ to be passed as the `context parameter `_ for ``axe.run``. """ return { "include": self.get_axe_include(request), "exclude": self.get_axe_exclude(request), } def get_axe_options(self, request): """ Returns the options object to be passed as the `options parameter `_ for ``axe.run``. """ options = { "runOnly": self.get_axe_run_only(request), "rules": self.get_axe_rules(request), } # If the runOnly option is omitted, Axe will run all rules except those # with the "experimental" flag or that are disabled in the rules option. # The runOnly has to be omitted (instead of set to an empty list or null) # for this to work, so we remove it if it's falsy. if not options["runOnly"]: options.pop("runOnly") return options def get_axe_spec(self, request): """Returns spec for Axe, including custom rules and custom checks""" return { "rules": self.get_axe_custom_rules(request), "checks": self.get_axe_custom_checks(request), } def get_axe_configuration(self, request): return { "context": self.get_axe_context(request), "options": self.get_axe_options(request), "messages": self.get_axe_messages(request), "spec": self.get_axe_spec(request), } def get_context_data(self, request): return { **super().get_context_data(request), "axe_configuration": self.get_axe_configuration(request), } def render(self, request): # Don't render if user doesn't have permission to access the admin area if not request.user.has_perm("wagtailadmin.access_admin"): return "" return super().render(request) class AddPageItem(BaseItem): template = "wagtailadmin/userbar/item_page_add.html" def __init__(self, page): self.page = page self.parent_page = page.get_parent() def render(self, request): # Don't render if the page doesn't have an id if not self.page.id: return "" # Don't render if user doesn't have permission to access the admin area if not request.user.has_perm("wagtailadmin.access_admin"): return "" # Don't render if user doesn't have ability to add children here permission_checker = self.page.permissions_for_user(request.user) if not permission_checker.can_add_subpage(): return "" return super().render(request) class ExplorePageItem(BaseItem): template = "wagtailadmin/userbar/item_page_explore.html" def __init__(self, page): self.page = page self.parent_page = page.get_parent() def render(self, request): # Don't render if the page doesn't have an id if not self.page.id: return "" # Don't render if user doesn't have permission to access the admin area if not request.user.has_perm("wagtailadmin.access_admin"): return "" # Don't render if user doesn't have ability to edit or publish subpages on the parent page permission_checker = self.parent_page.permissions_for_user(request.user) if ( not permission_checker.can_edit() and not permission_checker.can_publish_subpage() ): return "" return super().render(request) class EditPageItem(BaseItem): template = "wagtailadmin/userbar/item_page_edit.html" def __init__(self, page): self.page = page def render(self, request): # Don't render if the page doesn't have an id if not self.page.id: return "" # Don't render if request is a preview. This is to avoid confusion that # might arise when the user clicks edit on a preview. try: if request.is_preview and request.is_editing: return "" except AttributeError: pass # Don't render if user doesn't have permission to access the admin area if not request.user.has_perm("wagtailadmin.access_admin"): return "" # Don't render if the user doesn't have permission to edit this page permission_checker = self.page.permissions_for_user(request.user) if not permission_checker.can_edit(): return "" return super().render(request) def apply_userbar_hooks(request, items, page): for fn in hooks.get_hooks("construct_wagtail_userbar"): if accepts_kwarg(fn, "page"): fn(request, items, page) else: warn( "`construct_wagtail_userbar` hook functions should accept a `page` argument in third position -" f" {fn.__module__}.{fn.__name__} needs to be updated", category=RemovedInWagtail80Warning, ) fn(request, items)