1 # -*- coding: utf-8 -*-
 
   2 # This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
 
   3 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
 
   6 Utilities for defining SSI variables.
 
   8 SSI variables are a way of providing values that need to be computed
 
   9 at request time to the prerendered templates.
 
  12 from __future__ import unicode_literals
 
  13 from hashlib import md5
 
  14 from django import template
 
  15 from django.utils.encoding import force_text, python_2_unicode_compatible
 
  16 from django.utils.functional import Promise
 
  17 from django.utils.safestring import mark_safe
 
  18 from .exceptions import SsiVarsDependencyCycleError
 
  21 @python_2_unicode_compatible
 
  22 class SsiVariable(object):
 
  24     Represents a variable computed by a template tag with given arguments.
 
  26     Instance of this class is returned from any template tag created
 
  27     with `decorators.ssi_variable` decorator. If renders as SSI echo
 
  28     statement, but you can also use it as an argument to {% ssi_include %},
 
  29     to other ssi_variable, or create SSI if statements by using
 
  30     its `if`, `else`, `endif` properties.
 
  32     Variable's name, as used in SSI statements, is a hash of its definition,
 
  33     so the user never has to deal with it directly.
 
  36     def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
 
  37         self.tagpath = tagpath
 
  38         self.args = list(args or [])
 
  39         self.kwargs = kwargs or {}
 
  44         """Variable name is a hash of its definition."""
 
  45         if self._name is None:
 
  46             self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
 
  51         Sometimes there's a need to reset the variable name.
 
  53         Typically, this is the case after finding real values for
 
  54         variables passed as arguments to {% ssi_include %}.
 
  61         """Variable is defined by path to template tag and its arguments."""
 
  63             return self.tagpath, self.args, self.kwargs
 
  65             return self.tagpath, self.args
 
  70         return "SsiVariable(%s: %s)" % (self.name, repr(self.definition))
 
  72     def get_value(self, request):
 
  73         """Computes the real value of the variable, using the request."""
 
  74         taglib, tagname = self.tagpath.rsplit('.', 1)
 
  75         return template.get_library(taglib).tags[tagname].get_value(
 
  76             request, *self.args, **self.kwargs)
 
  79         return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
 
  82         """Returns the form that can be used in SSI include's URL."""
 
  83         return '${%s}' % self.name
 
  85 # If-else-endif properties for use in templates.
 
  86 setattr(SsiVariable, 'if',
 
  87         lambda self: mark_safe("<!--#if expr='${%s}'-->" % self.name))
 
  88 setattr(SsiVariable, 'else',
 
  89         staticmethod(lambda: mark_safe("<!--#else-->")))
 
  90 setattr(SsiVariable, 'endif',
 
  91         staticmethod(lambda: mark_safe('<!--#endif-->')))
 
  94 class SsiExpect(object):
 
  95     """This class says: I want the real value of this variable here."""
 
  96     def __init__(self, name):
 
  99         return "SsiExpect(%s)" % (self.name,)
 
 102 def ssi_expect(var, type_):
 
 104     Helper function for defining get_ssi_vars on ssi_included views.
 
 106     The view needs a way of calculating all the needed variables from
 
 107     the view args. But the args are probably the wrong type
 
 108     (typically, str instead of int) or even are SsiVariables, not
 
 109     resolved until request time.
 
 111     This function provides a way to expect a real value of the needed type.
 
 114     if isinstance(var, SsiVariable):
 
 115         return SsiExpect(var.name)
 
 120 class SsiVariableNode(template.Node):
 
 121     """ Node for the SsiVariable tags. """
 
 122     def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
 
 123         self.tagpath = tagpath
 
 126         self.patch_response = patch_response
 
 130         return "<SsiVariableNode>"
 
 132     def render(self, context):
 
 133         """Renders the tag as SSI echo or sets the context variable."""
 
 134         resolved_args = [var.resolve(context) for var in self.args]
 
 135         resolved_kwargs = dict((k, v.resolve(context))
 
 136                                for k, v in self.kwargs.items())
 
 137         var = SsiVariable(self.tagpath, resolved_args, resolved_kwargs)
 
 139         request = context['request']
 
 140         if not hasattr(request, 'ssi_vars_needed'):
 
 141             request.ssi_vars_needed = {}
 
 142         request.ssi_vars_needed[var.name] = var
 
 143         if self.patch_response:
 
 144             if not hasattr(request, 'ssi_patch_response'):
 
 145                 request.ssi_patch_response = []
 
 146             request.ssi_patch_response.extend(self.patch_response)
 
 149             context.dicts[0][self.asvar] = var
 
 155 def ssi_set_statement(var, value):
 
 156     """Generates an SSI set statement for a variable."""
 
 157     if isinstance(value, Promise):
 
 158         # Yes, this is quite brutal. But we need to know
 
 159         # the real value now, we don't know the type,
 
 160         # and we only want to evaluate the lazy function once.
 
 161         value = value._proxy____cast()
 
 162     if value is False or value is None:
 
 164     return "<!--#set var='%s' value='%s'-->" % (
 
 166         force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
 
 169 def provide_vars(request, ssi_vars):
 
 171     Provides all the SSI set statements for ssi_vars variables.
 
 173     The main purpose of this function is to by called by SsifyMiddleware.
 
 175     def resolve_expects(var):
 
 176         if not hasattr(var, 'hash_dirty'):
 
 177             var.hash_dirty = False
 
 179         for i, arg in enumerate(var.args):
 
 180             if isinstance(arg, SsiExpect):
 
 181                 var.args[i] = resolved[arg.name]
 
 182                 var.hash_dirty = True
 
 183         for k, arg in var.kwargs.items():
 
 184             if isinstance(arg, SsiExpect):
 
 185                 var.kwargs[k] = resolved[arg.name]
 
 186                 var.hash_dirty = True
 
 188         for arg in var.args + list(var.kwargs.values()):
 
 189             if isinstance(arg, SsiVariable):
 
 190                 var.hash_dirty = resolve_expects(arg) or var.hash_dirty
 
 192         hash_dirty = var.hash_dirty
 
 194             # Rehash after calculating the SsiExpects with real
 
 195             # values, because that's what the included views expect.
 
 197             var.hash_dirty = False
 
 201     def resolve_args(var):
 
 203         for k, arg in var.kwargs.items():
 
 204             kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
 
 205         new_var = SsiVariable(var.tagpath,
 
 206             [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
 
 211     queue = list(ssi_vars.values())
 
 213     unresolved_streak = 0
 
 218             rv = resolve_args(var)
 
 219         except KeyError as e:
 
 221             unresolved_streak += 1
 
 222             if unresolved_streak > len(queue):
 
 223                 raise SsiVarsDependencyCycleError(request, queue, resolved)
 
 226         resolved[var.name] = rv.get_value(request)
 
 227         unresolved_streak = 0
 
 229     output = "".join(ssi_set_statement(var, value)
 
 230                       for (var, value) in resolved.items()
 
 235 from .serializers import json_encode