Add is_restricted boolean check for commands
[mudpy.git] / mudpy / command.py
index ebd9036..279da78 100644 (file)
@@ -1,17 +1,18 @@
 """User command functions for the mudpy engine."""
 
-# Copyright (c) 2004-2019 mudpy authors. Permission to use, copy,
+# Copyright (c) 2004-2020 mudpy authors. Permission to use, copy,
 # modify, and distribute this software is granted under terms
 # provided in the LICENSE file distributed with this software.
 
 import random
 import re
+import traceback
 import unicodedata
 
 import mudpy
 
 
-def chat(actor):
+def chat(actor, parameters):
     """Toggle chat mode."""
     mode = actor.get("mode")
     if not mode:
@@ -22,6 +23,7 @@ def chat(actor):
         actor.send("Exiting chat mode.")
     else:
         actor.send("Sorry, but you're already busy with something else!")
+    return True
 
 
 def create(actor, parameters):
@@ -56,6 +58,7 @@ def create(actor, parameters):
         elif len(arguments) > 2:
             message = "You can only specify an element and a filename."
     actor.send(message)
+    return True
 
 
 def delete(actor, parameters):
@@ -83,6 +86,7 @@ def delete(actor, parameters):
                            + '". Try "show element ' +
                            element + '" for verification.')
     actor.send(message)
+    return True
 
 
 def destroy(actor, parameters):
@@ -104,21 +108,30 @@ def destroy(actor, parameters):
                     6
                 )
         actor.send(message)
+    return True
 
 
 def error(actor, input_data):
     """Generic error for an unrecognized command word."""
 
     # 90% of the time use a generic error
-    if random.randrange(10):
+    # Allow the random.randrange() call in bandit since it's not used for
+    # security/cryptographic purposes
+    if random.randrange(10):  # nosec
         message = '''I'm not sure what "''' + input_data + '''" means...'''
 
     # 10% of the time use the classic diku error
     else:
         message = "Arglebargle, glop-glyf!?!"
 
-    # send the error message
-    actor.send(message)
+    # try to send the error message, and log if we can't
+    try:
+        actor.send(message)
+    except Exception:
+        mudpy.misc.log(
+            'Sending a command error to user %s raised exception...\n%s' % (
+                actor.owner.account.get("name"), traceback.format_exc()))
+    return True
 
 
 def halt(actor, parameters):
@@ -139,6 +152,7 @@ def halt(actor, parameters):
 
         # set a flag to terminate the world
         actor.universe.terminate_flag = True
+    return True
 
 
 def help(actor, parameters):
@@ -148,10 +162,7 @@ def help(actor, parameters):
     if parameters and actor.owner:
 
         # is the command word one for which we have data?
-        if parameters in actor.universe.groups["command"]:
-            command = actor.universe.groups["command"][parameters]
-        else:
-            command = None
+        command = mudpy.misc.find_command(parameters)
 
         # only for allowed commands
         if actor.can_run(command):
@@ -160,11 +171,12 @@ def help(actor, parameters):
             description = command.get("description")
             if not description:
                 description = "(no short description provided)"
-            if command.get("administrative"):
+            if command.is_restricted():
                 output = "$(red)"
             else:
                 output = "$(grn)"
-            output += parameters + "$(nrm) - " + description + "$(eol)$(eol)"
+            output = "%s%s$(nrm) - %s$(eol)$(eol)" % (
+                output, command.subkey, description)
 
             # add the help text if provided
             help_text = command.get("help")
@@ -182,7 +194,7 @@ def help(actor, parameters):
                         if actor.can_run(command):
                             if really_see_also:
                                 really_see_also += ", "
-                            if command.get("administrative"):
+                            if command.is_restricted():
                                 really_see_also += "$(red)"
                             else:
                                 really_see_also += "$(grn)"
@@ -197,26 +209,57 @@ def help(actor, parameters):
     # no specific command word was indicated
     else:
 
-        # give a sorted list of commands with descriptions if provided
-        output = "These are the commands available to you:$(eol)$(eol)"
-        sorted_commands = list(actor.universe.groups["command"].keys())
-        sorted_commands.sort()
-        for item in sorted_commands:
-            command = actor.universe.groups["command"][item]
+        # preamble text
+        output = ("These are the commands available to you [brackets indicate "
+                  "optional portion]:$(eol)$(eol)")
+
+        # list command names in alphabetical order
+        for command_name, command in sorted(
+                actor.universe.groups["command"].items()):
+
+            # skip over disallowed commands
             if actor.can_run(command):
-                description = command.get("description")
-                if not description:
-                    description = "(no short description provided)"
-                if command.get("administrative"):
-                    output += "   $(red)"
+
+                # start incrementing substrings
+                for position in range(1, len(command_name) + 1):
+
+                    # we've found our shortest possible abbreviation
+                    candidate = mudpy.misc.find_command(
+                            command_name[:position])
+                    try:
+                        if candidate.subkey == command_name:
+                            break
+                    except AttributeError:
+                        pass
+
+                # use square brackets to indicate optional part of command name
+                if position < len(command_name):
+                    abbrev = "%s[%s]" % (
+                        command_name[:position], command_name[position:])
+                else:
+                    abbrev = command_name
+
+                # supply a useful default if the short description is missing
+                description = command.get(
+                    "description", "(no short description provided)")
+
+                # administrative command names are in red, others in green
+                if command.is_restricted():
+                    color = "red"
                 else:
-                    output += "   $(grn)"
-                output += item + "$(nrm) - " + description + "$(eol)"
-        output += ('$(eol)Enter "help COMMAND" for help on a command '
-                   'named "COMMAND".')
+                    color = "grn"
+
+                # format the entry for this command
+                output = "%s   $(%s)%s$(nrm) - %s$(eol)" % (
+                    output, color, abbrev, description)
+
+        # add a footer with instructions on getting additional information
+        output = ('%s $(eol)Enter "help COMMAND" for help on a command named '
+                  '"COMMAND".' % output)
 
     # send the accumulated output to the user
     actor.send(output)
+    return True
 
 
 def look(actor, parameters):
@@ -225,37 +268,54 @@ def look(actor, parameters):
         actor.send("You can't look at or in anything yet.")
     else:
         actor.look_at(actor.get("location"))
+    return True
 
 
 def move(actor, parameters):
     """Move the avatar in a given direction."""
-    if parameters in actor.universe.contents[actor.get("location")].portals():
-        actor.move_direction(parameters)
-    else:
-        actor.send("You cannot go that way.")
+    for portal in sorted(
+            actor.universe.contents[actor.get("location")].portals()):
+        if portal.startswith(parameters):
+            actor.move_direction(portal)
+            return(portal)
+    actor.send("You cannot go that way.")
+    return True
 
 
 def preferences(actor, parameters):
     """List, view and change actor preferences."""
+
+    # Escape replacement macros in preferences
+    parameters = mudpy.misc.escape_macros(parameters)
+
     message = ""
     arguments = parameters.split()
     allowed_prefs = set()
+    base_prefs = []
     user_config = actor.universe.contents.get("mudpy.user")
     if user_config:
-        allowed_prefs.update(user_config.get("pref_allow", []))
+        base_prefs = user_config.get("pref_allow", [])
+        allowed_prefs.update(base_prefs)
         if actor.owner.account.get("administrator"):
             allowed_prefs.update(user_config.get("pref_admin", []))
     if not arguments:
         message += "These are your current preferences:"
-        for pref in allowed_prefs:
-            message += ("$(eol)   $(red)%s $(grn)%s$(nrm)"
-                        % (pref, actor.owner.account.get(pref)))
+
+        # color-code base and admin prefs
+        for pref in sorted(allowed_prefs):
+            if pref in base_prefs:
+                color = "grn"
+            else:
+                color = "red"
+            message += ("$(eol)   $(%s)%s$(nrm) - %s" % (
+                color, pref, actor.owner.account.get(pref, "<not set>")))
+
     elif arguments[0] not in allowed_prefs:
         message += (
             'Preference "%s" does not exist. Try the `preferences` command by '
             "itself for a list of valid preferences." % arguments[0])
     elif len(arguments) == 1:
-        message += "%s" % actor.owner.account.get(arguments[0])
+        message += "%s" % actor.owner.account.get(arguments[0], "<not set>")
     else:
         pref = arguments[0]
         value = " ".join(arguments[1:])
@@ -267,16 +327,18 @@ def preferences(actor, parameters):
                 'Preference "%s" cannot be set to type "%s".' % (
                     pref, type(value)))
     actor.send(message)
+    return True
 
 
-def quit(actor):
+def quit(actor, parameters):
     """Leave the world and go back to the main menu."""
     if actor.owner:
         actor.owner.state = "main_utility"
         actor.owner.deactivate_avatar()
+    return True
 
 
-def reload(actor):
+def reload(actor, parameters):
     """Reload all code modules, configs and data."""
     if actor.owner:
 
@@ -290,6 +352,7 @@ def reload(actor):
 
         # set a flag to reload
         actor.universe.reload_flag = True
+    return True
 
 
 def say(actor, parameters):
@@ -373,6 +436,7 @@ def say(actor, parameters):
     # there was no message
     else:
         actor.send("What do you want to say?")
+    return True
 
 
 def c_set(actor, parameters):
@@ -410,6 +474,7 @@ def c_set(actor, parameters):
                                + '". Try "show element ' +
                                element + '" for verification.')
     actor.send(message)
+    return True
 
 
 def show(actor, parameters):
@@ -421,9 +486,8 @@ def show(actor, parameters):
     elif arguments[0] == "version":
         message = repr(actor.universe.versions)
     elif arguments[0] == "time":
-        message = actor.universe.groups["internal"]["counters"].get(
-            "elapsed"
-        ) + " increments elapsed since the world was created."
+        message = "%s increments elapsed since the world was created." % (
+            str(actor.universe.groups["internal"]["counters"].get("elapsed")))
     elif arguments[0] == "groups":
         message = "These are the element groups:$(eol)"
         groups = list(actor.universe.groups.keys())
@@ -489,7 +553,12 @@ def show(actor, parameters):
             message = "You need to specify an expression."
         else:
             try:
-                message = repr(eval(" ".join(arguments[1:])))
+                # there is no other option than to use eval() for this, since
+                # its purpose is to evaluate arbitrary expressions, so do what
+                # we can to secure it and allow it for bandit analysis
+                message = repr(eval(  # nosec
+                    " ".join(arguments[1:]),
+                    {"mudpy": mudpy, "universe": actor.universe}))
             except Exception as e:
                 message = ("$(red)Your expression raised an exception...$(eol)"
                            "$(eol)$(bld)%s$(nrm)" % e)
@@ -526,3 +595,4 @@ def show(actor, parameters):
     else:
         message = '''I don't know what "''' + parameters + '" is.'
     actor.send(message)
+    return True