Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import traceback | ||
from logging import getLogger | ||
|
||
import sys | ||
from django.conf import settings | ||
from evennia.contrib.collab.template.extensions import perm_bind, getter, setter, perm_check | ||
from evennia.utils.ansi import raw | ||
from jinja2 import BaseLoader | ||
|
||
from jinja2 import DebugUndefined | ||
from jinja2 import TemplateSyntaxError | ||
from jinja2.exceptions import TemplatesNotFound | ||
from jinja2.sandbox import SandboxedEnvironment | ||
|
||
|
||
logger = getLogger(__name__) | ||
|
||
|
||
class ExpressionLoader(BaseLoader): | ||
""" | ||
Take a template's 'name' and evaluate it literally as the template. | ||
""" | ||
def get_source(self, environment, template): | ||
return template, 'expression', None | ||
|
||
|
||
class EvTemplateEnvironment(SandboxedEnvironment): | ||
def is_safe_attribute(self, obj, attr, value): | ||
if hasattr(obj, 'template_permitted') and attr in obj.template_permitted: | ||
return True | ||
return False | ||
|
||
def is_safe_callable(self, obj): | ||
return getattr(obj, 'evtemplate_safe', False) | ||
|
||
|
||
ENV = None | ||
|
||
|
||
def gen_env(): | ||
global ENV | ||
ENV = EvTemplateEnvironment( | ||
autoescape=False, undefined=DebugUndefined, extensions=settings.COLLAB_TEMPLATE_EXTENSIONS, | ||
loader=ExpressionLoader(), | ||
) | ||
|
||
|
||
def evtemplate(string, run_as=None, me=None, this=None, how=None, here=None, context=None, **kwargs): | ||
""" | ||
Parses a Jinja2 template string in the sandboxed environment. | ||
run_as: The object the program should run as. | ||
me: The the one who is viewing the message. | ||
this: The relevant item that the message is being executed for | ||
(such as an object whose desc triggered this message) | ||
here: The location in which this message is being executed. | ||
how: A string that describes the cause for the rendering, | ||
like 'desc' or 'succ'. Optional but highly recommended. | ||
Jinja is not very consistent about its exception handling when it | ||
concerns with helping a developer figure out where an error is. | ||
There is probably a better way to handle the exceptions here, all the same. | ||
""" | ||
if not ENV: | ||
gen_env() | ||
if not me: | ||
raise ValueError("You cannot render a template without an observer.") | ||
if not run_as: | ||
# If there is no one that the script should run as, it might mean that there's | ||
# no owner for the object. That can't be trusted, so just return the raw | ||
# template text, and log the issue. | ||
logger.info("Refused to render template without run_as set. Returning raw string.") | ||
return string | ||
if not here: | ||
here = me.location | ||
if not this: | ||
raise ValueError("Templates must have a 'this' to render for.") | ||
kwargs.update({ | ||
'me': me, | ||
'run_as': run_as, | ||
'this': this, | ||
'here': here, | ||
'how': how, | ||
'perm_check': perm_check, | ||
'fetch': perm_bind(getter, run_as), | ||
'store': perm_bind(setter, run_as), | ||
}) | ||
context = context or {} | ||
try: | ||
return ENV.from_string(string, globals=kwargs).render(**context) | ||
except TemplateSyntaxError as e: | ||
# Something was wrong with the source template string. | ||
if e.source: | ||
source_lines = [raw(u"{}:{}".format(i, line)) for i, line in enumerate(e.source.split('\n'), start=1)] | ||
source_lines[e.lineno - 1] = u"|r|h" + source_lines[e.lineno - 1] + u"|n" | ||
source_lines.append(str(e)) | ||
else: | ||
source_lines = [u"Error on unknown line (possibly caused by an included template): {}".format(e)] | ||
|
||
return u'\n'.join(source_lines) | ||
except TemplatesNotFound: | ||
return u"Include error: Attempted to include a null value. Check to make sure your " \ | ||
u"include statement contains a template string." | ||
except Exception as e: | ||
# Now we have the sort of exceptions Jinja is (especially) bad at handling. | ||
ex_type, ex, tb = sys.exc_info() | ||
tb = traceback.extract_tb(tb) | ||
if tb[-1][0] == '<template>': | ||
extra = u" on line {}".format(tb[-1][1]) | ||
else: | ||
extra = u'' | ||
return u"Error when executing template{}. {}: {}".format(extra, e.__class__.__name__, e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import json | ||
from collections import MutableMapping | ||
from json import JSONEncoder | ||
|
||
import time | ||
from evennia.contrib.collab.perms import prefix_check, attr_check, collab_check | ||
from evennia.contrib.gendersub import GENDER_PRONOUN_MAP, gender_sub | ||
from evennia.typeclasses.models import TypedObject | ||
from evennia.utils import inherits_from | ||
|
||
from jinja2 import nodes | ||
from jinja2.exceptions import SecurityError, TemplateSyntaxError | ||
from jinja2.ext import Extension | ||
|
||
|
||
def safe(func): | ||
""" | ||
Decorator for functions so that they can be called by the sandboxed template system. | ||
""" | ||
func.evtemplate_safe = True | ||
return func | ||
|
||
|
||
class AttribJSONEncoder(JSONEncoder): | ||
""" | ||
Decoder which can serialize references to TypeClassed objects. | ||
It's not actually used for storing any information, but is written in a practical | ||
fashion anyway. | ||
""" | ||
def default(self, o): | ||
if isinstance(o, TypedObject): | ||
return {'##DBREF': [o.id, time.mktime(o.date_created.timetuple())]} | ||
return o | ||
|
||
|
||
def get_gender_map(target): | ||
gender = target.usrattributes.get("gender", default="neutral").lower() | ||
gender_map = dict(GENDER_PRONOUN_MAP) | ||
custom_map = target.usrattributes.get("custom_gender_map", default=None) | ||
if inherits_from(custom_map, MutableMapping): | ||
gender_map.update(custom_map) | ||
gender = gender if gender in gender_map else 'neutral' | ||
return gender, gender_map | ||
|
||
|
||
class PronounsExtension(Extension): | ||
""" | ||
Adds pronoun substitutions. | ||
""" | ||
tags = {'pro'} | ||
|
||
def parse(self, parser): | ||
# We get the line number so that we can give | ||
# that line number to the node we create by hand. | ||
lineno = next(parser.stream).lineno | ||
|
||
# now we parse a single expression that is used as the pronoun target. | ||
try: | ||
target = parser.parse_expression() | ||
except TemplateSyntaxError: | ||
target = nodes.Const(None) | ||
|
||
# now we parse the body of the cache block up to `endpro` and | ||
# drop the needle (which would always be `endpro` in that case) | ||
body = parser.parse_statements(['name:endpro'], drop_needle=True) | ||
ctx_ref = nodes.ContextReference() | ||
# now return a `CallBlock` node that calls our _pronoun_sub | ||
# helper method on this extension. | ||
block = nodes.CallBlock(self.call_method('pronoun_sub', args=[target, ctx_ref]), [], [], body) | ||
block.set_lineno(lineno) | ||
return block | ||
|
||
@safe | ||
def pronoun_sub(self, target, context, caller): | ||
if target is None: | ||
target = context['me'] | ||
return gender_sub(target, caller()) | ||
|
||
|
||
def perm_bind(func, run_as): | ||
""" | ||
To be used with the below get/set functions for getting/setting | ||
db attributes in a template. | ||
""" | ||
@safe | ||
def wrapped(*args, **kwargs): | ||
return func(run_as, *args, **kwargs) | ||
return wrapped | ||
|
||
|
||
def getter(run_as, target, prop_name): | ||
""" | ||
Fetches the value of a property. | ||
""" | ||
name, handler = prefix_check(target, prop_name) | ||
if attr_check(run_as, target, 'read', handler): | ||
result = handler.get(name) | ||
# Let's verify this object is primitive only all the way down, | ||
# and then make a different copy of it so that it is not saved | ||
# implicitly. Implicit saves could result in a security error | ||
# where a user who can read a mutable object could then change | ||
# it and affect something they should not have access to. | ||
try: | ||
result = json.dumps(result) | ||
except TypeError: | ||
raise SecurityError( | ||
"'{prop_name}' on {target} contains data which cannot be securely sandboxed.".format( | ||
prop_name=prop_name, target=target) | ||
) | ||
return json.loads(result) | ||
raise SecurityError( | ||
"{run_as} does not have read access to property '{prop_name}' on {target}".format( | ||
run_as=run_as, prop_name=prop_name, target=target, | ||
) | ||
) | ||
|
||
|
||
def setter(run_as, target, prop_name, prop_val): | ||
""" | ||
Sets the value of a property. | ||
""" | ||
name, handler = prefix_check(target, prop_name) | ||
if attr_check(run_as, target, 'write', handler): | ||
try: | ||
json.dumps(prop_val) | ||
except TypeError: | ||
raise SecurityError("{prop_val} contains data which cannot be securely sandboxed.".format( | ||
prop_val=repr(prop_val) | ||
)) | ||
handler.add(name, prop_val) | ||
return '' | ||
raise SecurityError( | ||
"{run_as} does not have write access to property '{prop_name}' on {target}".format( | ||
run_as=run_as, prop_name=prop_name, target=target, | ||
) | ||
) | ||
|
||
|
||
perm_check = safe(collab_check) |
Oops, something went wrong.