Cleanup of examine, escape inlinefuncs. Resolve #2172

This commit is contained in:
Griatch 2020-07-18 15:51:29 +02:00
parent f7b4193c84
commit 7aa6883b94
5 changed files with 141 additions and 92 deletions

View file

@ -63,6 +63,7 @@ without arguments starts a full interactive Python console.
- EvTable should now correctly handle columns with wider asian-characters in them. - EvTable should now correctly handle columns with wider asian-characters in them.
- Update Twisted requirement to >=2.3.0 to close security vulnerability - Update Twisted requirement to >=2.3.0 to close security vulnerability
- Add `$random` inlinefunc, supports minval,maxval arguments that can be ints and floats. - Add `$random` inlinefunc, supports minval,maxval arguments that can be ints and floats.
- Add `evennia.utils.inlinefuncs.raw(<str>)` as a helper to escape inlinefuncs in a string.
## Evennia 0.9 (2018-2019) ## Evennia 0.9 (2018-2019)

View file

@ -16,11 +16,13 @@ from evennia.utils.utils import (
dbref, dbref,
interactive, interactive,
list_to_string, list_to_string,
display_len,
) )
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw from evennia.utils.ansi import raw as ansi_raw
from evennia.utils.inlinefuncs import raw as inlinefunc_raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2357,26 +2359,37 @@ class CmdExamine(ObjManipCommand):
arg_regex = r"(/\w+?(\s|$))|\s|$" arg_regex = r"(/\w+?(\s|$))|\s|$"
account_mode = False account_mode = False
detail_color = "|c"
header_color = "|w"
quell_color = "|r"
separator = "-"
def list_attribute(self, crop, attr, category, value): def list_attribute(self, crop, attr, category, value):
""" """
Formats a single attribute line. Formats a single attribute line.
Args:
crop (bool): If output should be cropped if too long.
attr (str): Attribute key.
category (str): Attribute category.
value (any): Attribute value.
Returns:
""" """
if crop: if crop:
if not isinstance(value, str): if not isinstance(value, str):
value = utils.to_str(value) value = utils.to_str(value)
value = utils.crop(value) value = utils.crop(value)
value = inlinefunc_raw(ansi_raw(value))
if category: if category:
string = "\n %s[%s] = %s" % (attr, category, value) return f"{attr}[{category}] = {value}"
else: else:
string = "\n %s = %s" % (attr, value) return f"{attr} = {value}"
string = raw(string)
return string
def format_attributes(self, obj, attrname=None, crop=True): def format_attributes(self, obj, attrname=None, crop=True):
""" """
Helper function that returns info about attributes and/or Helper function that returns info about attributes and/or
non-persistent data stored on object non-persistent data stored on object
""" """
if attrname: if attrname:
@ -2391,16 +2404,18 @@ class CmdExamine(ObjManipCommand):
ndb_attr = obj.nattributes.all(return_tuples=True) ndb_attr = obj.nattributes.all(return_tuples=True)
except Exception: except Exception:
ndb_attr = None ndb_attr = None
string = "" output = {}
if db_attr and db_attr[0]: if db_attr and db_attr[0]:
string += "\n|wPersistent attributes|n:" output["Persistent attribute(s)"] = "\n " + "\n ".join(
for attr, value, category in db_attr: sorted(self.list_attribute(crop, attr, category, value)
string += self.list_attribute(crop, attr, category, value) for attr, value, category in db_attr)
)
if ndb_attr and ndb_attr[0]: if ndb_attr and ndb_attr[0]:
string += "\n|wNon-Persistent attributes|n:" output["Non-Persistent attribute(s)"] = " \n" + " \n".join(
for attr, value in ndb_attr: sorted(self.list_attribute(crop, attr, None, value)
string += self.list_attribute(crop, attr, None, value) for attr, value in ndb_attr)
return string )
return output
def format_output(self, obj, avail_cmdset): def format_output(self, obj, avail_cmdset):
""" """
@ -2414,64 +2429,83 @@ class CmdExamine(ObjManipCommand):
str: The formatted string. str: The formatted string.
""" """
string = "\n|wName/key|n: |c%s|n (%s)" % (obj.name, obj.dbref) hclr = self.header_color
dclr = self.detail_color
qclr = self.quell_color
output = {}
# main key
output["Name/key"] = f"{dclr}{obj.name}|n ({obj.dbref})"
# aliases
if hasattr(obj, "aliases") and obj.aliases.all(): if hasattr(obj, "aliases") and obj.aliases.all():
string += "\n|wAliases|n: %s" % (", ".join(utils.make_iter(str(obj.aliases)))) output["Aliases"] = ", ".join(utils.make_iter(str(obj.aliases)))
# typeclass
output["Typeclass"] = f"{obj.typename} ({obj.typeclass_path})"
# sessions
if hasattr(obj, "sessions") and obj.sessions.all(): if hasattr(obj, "sessions") and obj.sessions.all():
string += "\n|wSession id(s)|n: %s" % ( output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
", ".join("#%i" % sess.sessid for sess in obj.sessions.all()) # email, if any
)
if hasattr(obj, "email") and obj.email: if hasattr(obj, "email") and obj.email:
string += "\n|wEmail|n: |c%s|n" % obj.email output["Email"] = f"{dclr}{obj.email}|n"
# account, for puppeted objects
if hasattr(obj, "has_account") and obj.has_account: if hasattr(obj, "has_account") and obj.has_account:
string += "\n|wAccount|n: |c%s|n" % obj.account.name output["Account"] = f"{dclr}{obj.account.name}|n ({obj.account.dbref})"
# account typeclass
output[" Account Typeclass"] = f"{obj.account.typename} ({obj.account.typeclass_path})"
# account permissions
perms = obj.account.permissions.all() perms = obj.account.permissions.all()
if obj.account.is_superuser: if obj.account.is_superuser:
perms = ["<Superuser>"] perms = ["<Superuser>"]
elif not perms: elif not perms:
perms = ["<None>"] perms = ["<None>"]
string += "\n|wAccount Perms|n: %s" % (", ".join(perms)) perms = ", ".join(perms)
if obj.account.attributes.has("_quell"): if obj.account.attributes.has("_quell"):
string += " |r(quelled)|n" perms += f" {qclr}(quelled)|n"
string += "\n|wTypeclass|n: %s (%s)" % (obj.typename, obj.typeclass_path) output[" Account Permissions"] = perms
# location
if hasattr(obj, "location"): if hasattr(obj, "location"):
string += "\n|wLocation|n: %s" % obj.location loc = str(obj.location)
if obj.location: if obj.location:
string += " (#%s)" % obj.location.id loc += f" (#{obj.location.id})"
output["Location"] = loc
# home
if hasattr(obj, "home"): if hasattr(obj, "home"):
string += "\n|wHome|n: %s" % obj.home home = str(obj.home)
if obj.home: if obj.home:
string += " (#%s)" % obj.home.id home += f" (#{obj.home.id})"
output["Home"] = home
# destination, for exits
if hasattr(obj, "destination") and obj.destination: if hasattr(obj, "destination") and obj.destination:
string += "\n|wDestination|n: %s" % obj.destination dest = str(obj.destination)
if obj.destination: if obj.destination:
string += " (#%s)" % obj.destination.id dest += f" (#{obj.destination.id})"
output["Destination"] = dest
# main permissions
perms = obj.permissions.all() perms = obj.permissions.all()
perms_string = ""
if perms: if perms:
perms_string = ", ".join(perms) perms_string = ", ".join(perms)
else:
perms_string = "<None>"
if obj.is_superuser: if obj.is_superuser:
perms_string += " [Superuser]" perms_string += " <Superuser>"
if perms_string:
string += "\n|wPermissions|n: %s" % perms_string output["Permissions"] = perms_string
# locks
locks = str(obj.locks) locks = str(obj.locks)
if locks: if locks:
locks_string = utils.fill("; ".join([lock for lock in locks.split(";")]), indent=6) locks_string = "\n" + utils.fill(
"; ".join([lock for lock in locks.split(";")]), indent=2)
else: else:
locks_string = " Default" locks_string = " Default"
string += "\n|wLocks|n:%s" % locks_string output["Locks"] = locks_string
# cmdsets
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"): if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
# all() returns a 'stack', so make a copy to sort. # all() returns a 'stack', so make a copy to sort.
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True) stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority,
string += "\n|wStored Cmdset(s)|n:\n %s" % ( reverse=True)
"\n ".join( output["Stored Cmdset(s)"] = (
"%s [%s] (%s, prio %s)" "\n " + "\n ".join(
% (cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority) f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
for cmdset in stored_cmdsets for cmdset in stored_cmdsets if cmdset.key != "_EMPTY_CMDSET"
if cmdset.key != "_EMPTY_CMDSET"
) )
) )
@ -2506,40 +2540,32 @@ class CmdExamine(ObjManipCommand):
pass pass
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()] all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
all_cmdsets.sort(key=lambda x: x.priority, reverse=True) all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
string += "\n|wMerged Cmdset(s)|n:\n %s" % ( output["Merged Cmdset(s)"] = (
"\n ".join( "\n " + "\n ".join(
"%s [%s] (%s, prio %s)" f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})"
% (cmdset.path, cmdset.key, cmdset.mergetype, cmdset.priority)
for cmdset in all_cmdsets for cmdset in all_cmdsets
) )
) )
# list the commands available to this object # list the commands available to this object
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")]) avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")])
cmdsetstr = utils.fill(", ".join(avail_cmdset), indent=2) cmdsetstr = "\n" + utils.fill(", ".join(avail_cmdset), indent=2)
string += "\n|wCommands available to %s (result of Merged CmdSets)|n:\n %s" % ( output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
obj.key, # scripts
cmdsetstr,
)
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all(): if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
string += "\n|wScripts|n:\n %s" % obj.scripts output["Scripts"] = "\n " + f"{obj.scripts}"
# add the attributes # add the attributes
string += self.format_attributes(obj) output.update(self.format_attributes(obj))
# Tags
# display Tags tags = obj.tags.all(return_key_and_category=True)
tags_string = utils.fill( tags_string = "\n" + utils.fill(
", ".join( ", ".join(
"%s[%s]" % (tag, category) sorted(f"{tag}[{category}]" for tag, category in tags )),
for tag, category in obj.tags.all(return_key_and_category=True) indent=2,
),
indent=5,
) )
if tags_string: if tags:
string += "\n|wTags[category]|n: %s" % tags_string.strip() output["Tags[category]"] = tags_string
# Contents of object
# add the contents
exits = [] exits = []
pobjs = [] pobjs = []
things = [] things = []
@ -2552,24 +2578,23 @@ class CmdExamine(ObjManipCommand):
else: else:
things.append(content) things.append(content)
if exits: if exits:
string += "\n|wExits|n: %s" % ", ".join( output["Exits (has .destination)"] = ", ".join(f"{exit.name}({exit.dbref})" for exit in exits)
["%s(%s)" % (exit.name, exit.dbref) for exit in exits]
)
if pobjs: if pobjs:
string += "\n|wCharacters|n: %s" % ", ".join( output["Characters"] = ", ".join(f"{dclr}{pobj.name}|n({pobj.dbref})" for pobj in pobjs)
["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs]
)
if things: if things:
string += "\n|wContents|n: %s" % ", ".join( output["Contents"] = ", ".join(
[ f"{cont.name}({cont.dbref})"
"%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
for cont in obj.contents if cont not in exits and cont not in pobjs
if cont not in exits and cont not in pobjs
]
) )
separator = "-" * _DEFAULT_WIDTH # format output
# output info max_width = -1
return "%s\n%s\n%s" % (separator, string.strip(), separator) for block in output.values():
max_width = max(max_width, max(display_len(line) for line in block.split("\n")))
sep = self.separator * max_width
mainstr = "\n".join(f"{hclr}{header}|n: {block}" for (header, block) in output.items())
return f"{sep}\n{mainstr}\n{sep}"
def func(self): def func(self):
"""Process command""" """Process command"""
@ -2584,8 +2609,7 @@ class CmdExamine(ObjManipCommand):
that function finishes. Taking the resulting cmdset, we continue that function finishes. Taking the resulting cmdset, we continue
to format and output the result. to format and output the result.
""" """
string = self.format_output(obj, cmdset) self.msg(self.format_output(obj, cmdset).strip())
self.msg(string.strip())
if not self.args: if not self.args:
# If no arguments are provided, examine the invoker's location. # If no arguments are provided, examine the invoker's location.
@ -2639,7 +2663,10 @@ class CmdExamine(ObjManipCommand):
if obj_attrs: if obj_attrs:
for attrname in obj_attrs: for attrname in obj_attrs:
# we are only interested in specific attributes # we are only interested in specific attributes
caller.msg(self.format_attributes(obj, attrname, crop=False), options={"raw": True}) ret = "\n".join(
f"{self.header_color}{header}|n:{value}"
for header, value in self.format_attributes(obj, attrname, crop=False).items())
self.caller.msg(ret)
else: else:
session = None session = None
if obj.sessions.count(): if obj.sessions.count():

View file

@ -150,11 +150,17 @@ class CommandTest(EvenniaTest):
returned_msg = msg_sep.join( returned_msg = msg_sep.join(
_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi)) for mess in stored_msg _RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi)) for mess in stored_msg
).strip() ).strip()
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()): msg = msg.strip()
if msg == "" and returned_msg or not returned_msg.startswith(msg):
prt = ""
for ic, char in enumerate(msg):
import re
prt += char
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n" sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n" sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
sep3 = "\n" + "=" * 78 sep3 = "\n" + "=" * 78
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3 retval = sep1 + msg + sep2 + returned_msg + sep3
raise AssertionError(retval) raise AssertionError(retval)
else: else:
returned_msg = "\n".join(str(msg) for msg in stored_msg) returned_msg = "\n".join(str(msg) for msg in stored_msg)
@ -457,10 +463,14 @@ class TestBuilding(CommandTest):
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount") self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
self.char1.db.test = "testval" self.char1.db.test = "testval"
self.call(building.CmdExamine(), "self/test", "Persistent attributes:\n test = testval") self.call(building.CmdExamine(), "self/test", "Persistent attribute(s):\n test = testval")
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.") self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
self.call(building.CmdExamine(), "out", "Name/key: out") self.call(building.CmdExamine(), "out", "Name/key: out")
# escape inlinefuncs
self.char1.db.test2 = "this is a $random() value."
self.call(building.CmdExamine(), "self/test2", "Persistent attribute(s):\n test2 = this is a \$random() value.")
self.room1.scripts.add(self.script.__class__) self.room1.scripts.add(self.script.__class__)
self.call(building.CmdExamine(), "") self.call(building.CmdExamine(), "")
self.account.scripts.add(self.script.__class__) self.account.scripts.add(self.script.__class__)

View file

@ -514,6 +514,17 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False
return retval return retval
def raw(string):
"""
Escape all inlinefuncs in a string so they won't get parsed.
Args:
string (str): String with inlinefuncs to escape.
"""
def _escape(match):
return "\\" + match.group(0)
return _RE_STARTTOKEN.sub(_escape, string)
# #
# Nick templating # Nick templating
# #

View file

@ -1842,8 +1842,8 @@ def display_len(target):
strip MXP patterns. strip MXP patterns.
Args: Args:
target (string): A string with potential MXP components target (any): Something to measure the length of. If a string, it will be
to search. measured keeping asian-character and MXP links in mind.
Return: Return:
int: The visible width of the target. int: The visible width of the target.