import operator import os import warnings from collections import OrderedDict, defaultdict from functools import cache from typing import TYPE_CHECKING from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import connection, models from django.template import ( Context, NodeList, Template, TemplateDoesNotExist, TemplateSyntaxError, Variable, engines, ) from django.template.base import VariableNode from django.template.loader import get_template from django.template.loader_tags import BlockNode, ExtendsNode, IncludeNode from sekizai.helpers import get_varname from cms.exceptions import DuplicatePlaceholderWarning from cms.models import EmptyPageContent, Placeholder from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning from cms.utils.conf import get_cms_setting if TYPE_CHECKING: from cms.templatetags.cms_tags import DeclaredPlaceholder RANGE_START = 128 def _get_nodelist(tpl): if hasattr(tpl, "template"): return tpl.template.nodelist else: return tpl.nodelist def get_context(): if engines is not None: context = Context() context.template = Template("") return context else: return {} def _get_placeholder_settings(): """Convert CMS_PLACEHOLDER_CONF into a faster to access format since it is accessed many times in each request response cycle: * setting key on the first level dict, scope on the second level * resolve inheritance * Check of template-specific configs exist (expensive to access) """ conf = get_cms_setting("PLACEHOLDER_CONF") template_in_conf = any(".htm" in key for key in conf if key) def resolve_inheritance(key, visited=None): if visited is None: visited = set() if key in visited: raise ImproperlyConfigured(f"Circular inheritance detected in CMS_PLACEHOLDER_CONF at key '{key}'") visited.add(key) if "inherit" in conf[key]: return resolve_inheritance(conf[key]["inherit"], visited) | { k: v for k, v in conf[key].items() if k != "inherit" } # Return a copy: the caller updates the result in place, which must # not leak into the inherited-from slot's own configuration. return dict(conf[key]) new_conf = {} for key, value in conf.items(): if "inherit" in value: new_conf[key] = resolve_inheritance(value["inherit"]) new_conf[key].update({k: v for k, v in value.items() if k != "inherit"}) else: new_conf[key] = value settings = defaultdict(dict) for key, value in new_conf.items(): for setting, setting_value in value.items(): settings[setting][key] = setting_value return settings, template_in_conf _placeholder_settings, _template_in_conf = _get_placeholder_settings() def _clear_placeholder_conf_cache(): # Needed by the override_placeholder_conf context manager for tests global _placeholder_settings, _template_in_conf _placeholder_settings, _template_in_conf = _get_placeholder_settings() def get_placeholder_conf(setting: str, placeholder: str, template: str | None = None, default=None): """ Returns the placeholder configuration for a given setting. The key would for example be 'plugins' or 'name'. Resulting value will be the last from: CMS_PLACEHOLDER_CONF[None] (global) CMS_PLACEHOLDER_CONF['template'] (if template is given) CMS_PLACEHOLDER_CONF['placeholder'] (where placeholder denotes a slot) CMS_PLACEHOLDER_CONF['template placeholder'] (if template is given) Template is only evaluated if the placeholder configuration contains key with ".html" or ".htm" """ if setting not in _placeholder_settings: return default if _template_in_conf and template is not None: keys = [f"{template} {placeholder}", placeholder, str(template), None] else: keys = [placeholder, None] conf = _placeholder_settings[setting] for key in keys: if (value := conf.get(key, None)) is not None: return value return default def get_toolbar_plugin_struct(plugins, slot=None, obj=None, page=None): """ Return the list of plugins to render in the toolbar. The dictionary contains the label, the classname and the module for the plugin. Names and modules can be defined on a per-placeholder basis using 'plugin_modules' and 'plugin_labels' attributes in CMS_PLACEHOLDER_CONF :param plugins: list of plugins :param slot: placeholder slot name :param page: the page :return: list of dictionaries """ template = None obj = obj or page if page: import warnings warnings.warn( "The 'page' argument is deprecated. Please use 'obj' instead.", RemovedInDjangoCMS60Warning, stacklevel=2, ) if obj and hasattr(obj, "get_template"): template = obj.get_template() modules = get_placeholder_conf("plugin_modules", slot, template, default={}) names = get_placeholder_conf("plugin_labels", slot, template, default={}) main_list = [] # plugin.value points to the class name of the plugin # It's added on registration. TIL. for plugin in plugins: main_list.append( { "value": plugin.value, "name": names.get(plugin.value, plugin.name), "module": modules.get(plugin.value, plugin.module), "add_form": plugin.show_add_form and not plugin.is_slot, } ) return sorted(main_list, key=operator.itemgetter("module")) def validate_placeholder_name(name): if not isinstance(name, str): raise ImproperlyConfigured("Placeholder identifier names need to be of type string. ") try: name.encode("ascii") except UnicodeEncodeError: raise ImproperlyConfigured( "Placeholder identifiers names may not " "contain non-ascii characters. If you wish your placeholder " "identifiers to contain non-ascii characters when displayed to " "users, please use the CMS_PLACEHOLDER_CONF setting with the 'name' " "key to specify a verbose name." ) def restore_sekizai_context(context, changes): varname = get_varname() sekizai_container = context.get(varname) for key, values in changes.items(): sekizai_namespace = sekizai_container[key] for value in values: sekizai_namespace.append(value) def _scan_placeholders(nodelist, node_class=None, current_block=None, ignore_blocks=None): from cms.templatetags.cms_tags import Placeholder if not node_class: node_class = Placeholder nodes = [] if ignore_blocks is None: # List of BlockNode instances to ignore. # This is important to avoid processing overridden block nodes. ignore_blocks = [] for node in nodelist: # check if this is a placeholder first if isinstance(node, node_class): nodes.append(node) elif isinstance(node, IncludeNode): # if there's an error in the to-be-included template, node.template becomes None if node.template: # Check if it quacks like a template object, if not # presume is a template path and get the object out of it if not callable(getattr(node.template, "render", None)): # If it's a variable there is no way to expand it at this stage so we # need to skip it if isinstance(node.template.var, Variable): continue else: try: template = get_template(node.template.var) except TemplateDoesNotExist: # Include might be inside if else, so no need to error out here # Actual errors will be raised at render time continue else: template = node.template nodes += _scan_placeholders(_get_nodelist(template), node_class, current_block) # handle {% extends ... %} tags elif isinstance(node, ExtendsNode): try: nodes += _get_placeholder_nodes_from_extend(node, node_class) except (TemplateSyntaxError, TemplateDoesNotExist): # If the extends tag uses a variable that's not available in the # scanning context (like CMS_TEMPLATE), we can't resolve the parent # template. Skip scanning this extends node. continue # in block nodes we have to scan for super blocks elif isinstance(node, VariableNode) and current_block: if node.filter_expression.token == "block.super": if not hasattr(current_block.super, "nodelist"): raise TemplateSyntaxError("Cannot render block.super for blocks without a parent.") nodes += _scan_placeholders(_get_nodelist(current_block.super), node_class, current_block.super) # ignore nested blocks which are already handled elif isinstance(node, BlockNode) and node.name in ignore_blocks: continue # if the node has the newly introduced 'child_nodelists' attribute, scan # those attributes for nodelists and recurse them elif hasattr(node, "child_nodelists"): for nodelist_name in node.child_nodelists: if hasattr(node, nodelist_name): subnodelist = getattr(node, nodelist_name) if isinstance(subnodelist, NodeList): if isinstance(node, BlockNode): current_block = node nodes += _scan_placeholders(subnodelist, node_class, current_block, ignore_blocks) # else just scan the node for nodelist instance attributes else: for attr in dir(node): obj = getattr(node, attr) if isinstance(obj, NodeList): if isinstance(node, BlockNode): current_block = node nodes += _scan_placeholders(obj, node_class, current_block, ignore_blocks) return nodes def get_placeholders(template: str) -> list["DeclaredPlaceholder"]: compiled_template = get_template(template) placeholders = [] nodes = _scan_placeholders(_get_nodelist(compiled_template)) clean_placeholders = [] for node in nodes: placeholder = node.get_declaration() slot = placeholder.slot if slot in clean_placeholders: warnings.warn( f'Duplicate {{% placeholder "{slot}" %}} ' f"in template {template}.", DuplicatePlaceholderWarning, ) else: validate_placeholder_name(slot) placeholders.append(placeholder) clean_placeholders.append(slot) return placeholders if settings.DEBUG is False or os.environ.get("DJANGO_TESTS"): # Cache in production only, so template changes in development # are always reflected without needing a server restart get_placeholders = cache(get_placeholders) def _get_block_nodes(extend_node): parent = extend_node.get_parent(get_context()) parent_nodelist = _get_nodelist(parent) parent_nodes = parent_nodelist.get_nodes_by_type(BlockNode) parent_extend_nodes = parent_nodelist.get_nodes_by_type(ExtendsNode) if parent_extend_nodes: # Start at the top # Scan the extends node from the parent (if any) nodes = _get_block_nodes(parent_extend_nodes[0]) else: nodes = OrderedDict() # Continue with the parent template nodes for node in parent_nodes: nodes[node.name] = node # Move on to the current template nodes current_nodes = _get_nodelist(extend_node).get_nodes_by_type(BlockNode) for node in current_nodes: if node.name in nodes: # set this node as the super node (for {{ block.super }}) node.super = nodes[node.name] nodes[node.name] = node return nodes def _get_placeholder_nodes_from_extend(extend_node, node_class): """ Returns a list of placeholders found in the parent template(s) of this ExtendsNode """ # This is a dictionary mapping all BlockNode instances found # in the template that contains the {% extends %} tag block_nodes = _get_block_nodes(extend_node) block_names = list(block_nodes.keys()) placeholders = [] for block in block_nodes.values(): placeholders.extend(_scan_placeholders(_get_nodelist(block), node_class, block, block_names)) # Scan topmost template for placeholder outside of blocks parent_template = _find_topmost_template(extend_node) placeholders += _scan_placeholders(_get_nodelist(parent_template), node_class, None, block_names) return placeholders def _find_topmost_template(extend_node): parent_template = extend_node.get_parent(get_context()) for node in _get_nodelist(parent_template).get_nodes_by_type(ExtendsNode): # Their can only be one extend block in a template, otherwise django raises an exception return _find_topmost_template(node) # No ExtendsNode return extend_node.get_parent(get_context()) def rescan_placeholders_for_obj(obj: models.Model) -> dict[str, Placeholder]: from cms.models import CMSPlugin, Placeholder declared_placeholders = get_declared_placeholders_for_obj(obj) placeholders = {pl.slot: None for pl in declared_placeholders} # Fix order of placeholders in dict # Fill in existing placeholders placeholders.update( { placeholder.slot: placeholder for placeholder in Placeholder.objects.get_for_obj(obj) if placeholder.slot in placeholders } ) # Create missing placeholders new_placeholders = [ Placeholder(slot=slot, source=obj) for slot, placeholder in placeholders.items() if placeholder is None ] if new_placeholders: if connection.features.can_return_rows_from_bulk_insert: Placeholder.objects.bulk_create(new_placeholders) for placeholder in new_placeholders: # No plugins in newly created placeholder yet placeholder._prefetched_objects_cache = {"cmsplugin_set": CMSPlugin.objects.none()} placeholders[placeholder.slot] = placeholder else: # Some MySql versions do not support returning IDs from bulk_create for placeholder in new_placeholders: placeholder.save() # No plugins in newly created placeholder yet placeholder._prefetched_objects_cache = {"cmsplugin_set": CMSPlugin.objects.none()} placeholders[placeholder.slot] = placeholder return placeholders def get_declared_placeholders_for_obj(obj: models.Model | EmptyPageContent | None) -> list["DeclaredPlaceholder"]: """Returns declared placeholders for an object. The object is supposed to either have a method ``get_placeholder_slots`` which returns the list of placeholders or a method ``get_template`` which returns the template path as a string that renders the object. ``get_declared_placeholders`` returns a list of placeholders used in the template by the ``{% placeholder %}`` template tag. """ template = getattr(obj, "get_template", lambda: None)() if template: return get_placeholders(template) if hasattr(obj, "get_placeholder_slots"): from cms.templatetags.cms_tags import DeclaredPlaceholder return [ DeclaredPlaceholder(slot=slot, inherit=False) if isinstance(slot, str) else DeclaredPlaceholder(**slot) for slot in obj.get_placeholder_slots() ] raise NotImplementedError( "%s should implement either get_placeholder_slots or get_template" % obj.__class__.__name__ ) def get_placeholder_from_slot( placeholder_relation: models.Manager, slot: str, template_obj=None, default_width: int | None = None ) -> Placeholder: """Retrieves the placeholder instance for a PlaceholderRelationField either by scanning the template of the template_obj (if given) or by creating or getting a Placeholder in the database """ if hasattr(template_obj, "get_template"): # Tries to get a placeholder (based on the template for the template_obj # or - if non exists - raises a Placeholder.DoesNotExist exception # Placeholders are marked in the template with {% placeholder %} try: return placeholder_relation.get(slot=slot) except Placeholder.DoesNotExist: return rescan_placeholders_for_obj(template_obj).get(slot) else: # Gets or creates the placeholder in any model. Placeholder is # rendered by {% render_placeholder %} return placeholder_relation.get_or_create(slot=slot, default_width=default_width)[0]