gtl.py
  1  """Gruyere Template Language, part of Gruyere, a web application with holes.
  2  
  3  Copyright 2010 Google Inc. All rights reserved.
  4  
  5  This code is licensed under the http://creativecommons.org/licenses/by-nd/3.0/us
  6  Creative Commons Attribution-No Derivative Works 3.0 United States license.
  7  
  8  DO NOT COPY THIS CODE!
  9  
 10  This application is a small self-contained web application with numerous
 11  security holes. It is provided for use with the Web Application Exploits and
 12  Defenses codelab. You may modify the code for your own use while doing the
 13  codelab but you may not distribute the modified code. Brief excerpts of this
 14  code may be used for educational or instructional purposes provided this
 15  notice is kept intact. By using Gruyere you agree to the Terms of Service
 16  http://code.google.com/terms.html
 17  """
 18  
 19  __author__ = 'Bruce Leban'
 20  
 21  # system modules
 22  import cgi
 23  import logging
 24  import operator
 25  import os
 26  import pprint
 27  import sys
 28  
 29  # our modules
 30  import gruyere
 31  import sanitize
 32  
 33  
 34  def ExpandTemplate(template, specials, params, name=''):
 35    """Expands a template.
 36  
 37    Args:
 38      template: a string template.
 39      specials: a dict of special values.
 40      params: a dict of parameter values.
 41      name: the name of the _this object.
 42  
 43    Returns:
 44      the expanded template.
 45  
 46    The template language includes these block structures:
 47  
 48      [[include:<filename>]] ...[[/include:<filename>]]
 49        Insert the file or if the file cannot be opened insert the contents of
 50        the block. The path should use / as a separator regardless of what
 51        the underlying operating system is.
 52  
 53      [[for:<variable>]] ... [[/for:<variable>]]
 54        Iterate over the variable (which should be a mapping or sequence) and
 55        insert the block once for each value. Inside the loop _key is bound to
 56        the key value for the iteration.
 57  
 58      [[if:<variable>]] ... [[/if:<variable>]]
 59        Expand the contents of the block if the variable is not 'false'.  There
 60        is no else; use [[if:!<variable>]] instead.
 61  
 62      Note that in each case the end tags must match the begin tags with a
 63      leading slash. This prevents mismatched tags and makes it easier to parse.
 64  
 65    The variable syntax is:
 66  
 67      {{<field>[.<field>]*[:<escaper>]}}
 68  
 69    where <field> is:
 70  
 71      a key to extract from a mapping
 72      a number to extract from a sequence
 73  
 74    Variable names that start with '_' are special values:
 75      _key = iteration key (inside loops)
 76      _this = iteration value (inside loop)
 77      _db = the database
 78      _cookie = the user's cookie
 79      _profile = the user's profile ~ _db.*(_cookie.user)
 80  
 81    If a field name starts with '*' it refers to a dereferenced parameter (orx
 82    *_this). For example, _db.*uid retrieves the entry from _db matching the
 83    uid parameter.
 84  
 85    The expression evaluation syntax is:
 86  
 87      {{@<expression>}}
 88  
 89    This allows more complicated expressions than simple variables. Five
 90    variables are passed to the expression: _key, _this, _db, _cookie, and
 91    _profile. The dotted form recognized by the variable template is NOT
 92    recognized. You will need to do that yourself, for example
 93      {{_cookie.is_admin}}
 94    becomes
 95      {{@_cookie.get('is_admin')}}.
 96    The expression can also use the function cond to do conditionals, however
 97    bear in mind that it does not shortcircuit so if one of the subexpressions
 98    throws an exception, it won't do what you want.
 99  
100    The comment syntax is:
101  
102      {{#<comment>}}
103    """
104    t = _ExpandBlocks(template, specials, params, name)
105    t = _ExpandVariables(t, specials, params, name)
106    return t
107  
108  
109  BLOCK_OPEN = '[['
110  END_BLOCK_OPEN = '[[/'
111  BLOCK_CLOSE = ']]'
112  
113  
114  def _ExpandBlocks(template, specials, params, name):
115    """Expands all the blocks in a template."""
116    result = []
117    rest = template
118    while rest:
119      tag, before_tag, after_tag = _FindTag(rest, BLOCK_OPEN, BLOCK_CLOSE)
120      if tag is None:
121        break
122      end_tag = END_BLOCK_OPEN + tag + BLOCK_CLOSE
123      before_end = rest.find(end_tag, after_tag)
124      if before_end < 0:
125        break
126      after_end = before_end + len(end_tag)
127  
128      result.append(rest[:before_tag])
129      block = rest[after_tag:before_end]
130      result.append(_ExpandBlock(tag, block, specials, params, name))
131      rest = rest[after_end:]
132    return ''.join(result) + rest
133  
134  
135  VAR_OPEN = '{{'
136  VAR_CLOSE = '}}'
137  
138  
139  def _ExpandVariables(template, specials, params, name):
140    """Expands all the variables in a template."""
141    result = []
142    rest = template
143    while rest:
144      tag, before_tag, after_tag = _FindTag(rest, VAR_OPEN, VAR_CLOSE)
145      if tag is None:
146        break
147      result.append(rest[:before_tag])
148      result.append(str(_ExpandVariable(tag, specials, params, name)))
149      rest = rest[after_tag:]
150    return ''.join(result) + rest
151  
152  
153  FOR_TAG = 'for'
154  IF_TAG = 'if'
155  INCLUDE_TAG = 'include'
156  EVAL_TAG = 'eval'
157  
158  
159  def _ExpandBlock(tag, template, specials, params, name):
160    """Expands a single template block."""
161  
162    tag_type, block_var = tag.split(':', 1)
163    if tag_type == INCLUDE_TAG:
164      return _ExpandInclude(tag, block_var, template, specials, params, name)
165    elif tag_type == IF_TAG:
166      block_data = _ExpandVariable(block_var, specials, params, name)
167      if block_data:
168        return ExpandTemplate(template, specials, params, name)
169      return ''
170    elif tag_type == FOR_TAG:
171      block_data = _ExpandVariable(block_var, specials, params, name)
172      return _ExpandFor(tag, template, specials, block_data)
173    else:
174      _Log('Error: Invalid block: %s' % (tag,))
175      return ''
176  
177  
178  def _ExpandInclude(tag, filename, template, specials, params, name):
179    """Expands an include block (or insert the template on an error)."""
180    result = ''
181    # replace /s with local file system equivalent
182    fname = os.sep + filename.replace('/', os.sep)
183    f = None
184    try:
185      try:
186        f = gruyere._Open(gruyere.RESOURCE_PATH, fname)
187        result = f.read()
188      except IOError:
189        _Log('Error: missing filename: %s' % (filename,))
190        result = template
191    finally:
192      if f: f.close()
193    return ExpandTemplate(result, specials, params, name)
194  
195  
196  def _ExpandFor(tag, template, specials, block_data):
197    """Expands a for block iterating over the block_data."""
198    result = []
199    if operator.isMappingType(block_data):
200      for v in block_data:
201        result.append(ExpandTemplate(template, specials, block_data[v], v))
202    elif operator.isSequenceType(block_data):
203      for i in xrange(len(block_data)):
204        result.append(ExpandTemplate(template, specials, block_data[i], str(i)))
205    else:
206      _Log('Error: Invalid type: %s' % (tag,))
207      return ''
208    return ''.join(result)
209  
210  
211  def _ExpandVariable(var, specials, params, name, default=''):
212    """Gets a variable value."""
213    if var.startswith('#'):  # this is a comment.
214      return ''
215    if var.startswith('@'):  # this is an expression
216      return _ExpandEval(var[1:], specials, params, name)
217  
218    # Strip out leading ! which negates value
219    inverted = var.startswith('!')
220    if inverted:
221      var = var[1:]
222  
223    # Strip out trailing :<escaper>
224    escaper_name = None
225    if var.find(':') >= 0:
226      (var, escaper_name) = var.split(':', 1)
227  
228    value = _ExpandValue(var, specials, params, name, default)
229    if inverted:
230      value = not value
231  
232    if escaper_name == 'text':
233      value = cgi.escape(str(value))
234    elif escaper_name == 'html':
235      value = sanitize.SanitizeHtml(str(value))
236    elif escaper_name == 'pprint':  # for debugging
237      value = '<pre>' + cgi.escape(pprint.pformat(value)) + '</pre>'
238  
239    if value is None:
240      value = ''
241    return value
242  
243  
244  def _ExpandValue(var, specials, params, name, default):
245    """Expand one value.
246  
247    This expands the <field>.<field>...<field> part of the variable
248    expansion. A field may be of the form *<param> to use the value
249    of a parameter as the field name.
250    """
251    finished = False
252    if var == '_key':
253      return name
254    elif var == '_this':
255      return params
256    if var.startswith('_'):
257      value = specials
258    else:
259      value = params
260  
261    for v in var.split('.'):
262      if v == '*_this':
263        v = params
264      if v.startswith('*'):
265        v = _GetValue(specials['_params'], v[1:])
266        if operator.isSequenceType(v):
267          v = v[0]  # reduce repeated url param to single value
268      value = _GetValue(value, str(v), default)
269    return value
270  
271  
272  def _GetValue(collection, index, default=''):
273    """Gets a single indexed value out of a collection.
274  
275    The index is either a key in a mapping or a numeric index into
276    a sequence.
277    """
278    if operator.isMappingType(collection) and index in collection:
279      value = collection[index]
280    elif (operator.isSequenceType(collection) and index.isdigit() and
281        int(index) < len(collection)):
282      value = collection[int(index)]
283    else:
284      value = default
285    return value
286  
287  
288  def _Cond(test, if_true, if_false):
289    """Substitute for 'if_true if test else if_false' in Python 2.4."""
290    if test:
291      return if_true
292    else:
293      return if_false
294  
295  
296  def _ExpandEval(expr, specials, params, name):
297    """Expands one eval expression."""
298    # Evaluating expressions is dangerous. This is safe because the expressions
299    # being evaluated can only come from the template which we control. However,
300    # to be even safer we pass a dictionary containing only values we want the
301    # expression to have access to and we pass this as both the globals and
302    # locals dict. But is anything ever really safe?
303    d = {
304        '_key': name,
305        '_this': params,
306        '_db': specials.get('_db'),
307        '_cookie': specials.get('_cookie'),
308        '_profile': specials.get('_profile'),
309        'cond': _Cond,
310    }
311    try:
312      result = eval(expr, d, d)
313    except Exception:  # catch everything so a bad expression can't make us fail
314      result = ''
315    return str(result)
316  
317  
318  def _FindTag(template, open_marker, close_marker):
319    """Finds a single tag.
320  
321    Args:
322      template: the template to search.
323      open_marker: the start of the tag (e.g., '{{').
324      close_marker: the end of the tag (e.g., '}}').
325  
326    Returns:
327      (tag, pos1, pos2) where the tag has the open and close markers
328      stripped off and pos1 is the start of the tag and pos2 is the end of
329      the tag. Returns (None, None, None) if there is no tag found.
330    """
331    open_pos = template.find(open_marker)
332    close_pos = template.find(close_marker, open_pos)
333    if open_pos < 0 or close_pos < 0 or open_pos > close_pos:
334      return (None, None, None)
335    return (template[open_pos + len(open_marker):close_pos],
336            open_pos,
337            close_pos + len(close_marker))
338  
339  
340  def _Log(message):
341    logging.warning('%s', message)
342    print >>sys.stderr, message