diff options
author | Blake DeMarcy <ofunknowndescent@gmail.com> | 2017-03-03 16:26:59 -0600 |
---|---|---|
committer | Blake DeMarcy <ofunknowndescent@gmail.com> | 2017-03-03 16:26:59 -0600 |
commit | e892d20e18a5a7002071f7ec0468437227e508a7 (patch) | |
tree | 21359d49dffdfa9cd71067ef919f41e1c8e053ff /src | |
parent | a9634b9aea64eb4e00c497e72814f6bd9c237556 (diff) | |
download | bbj-e892d20e18a5a7002071f7ec0468437227e508a7.tar.gz |
primitive message editing support; new sanity checks
Diffstat (limited to 'src')
-rw-r--r-- | src/db.py | 127 | ||||
-rw-r--r-- | src/endpoints.py | 103 | ||||
-rw-r--r-- | src/server.py | 7 |
3 files changed, 217 insertions, 20 deletions
@@ -23,6 +23,7 @@ except FileNotFoundError: f.write(json.dumps(USERDB)) path.os.chmod(path.join(PATH, "userdb"), 0o600) + ### THREAD MANAGEMENT ### def thread_index(key="lastmod", markup=True): @@ -36,8 +37,10 @@ def thread_index(key="lastmod", markup=True): def thread_create(author, body, title, tags): ID = uuid1().hex - # make sure None, False, and empty arrays are always repped consistently - tags = tags if tags else [] + if tags: + tags = [tag.strip() for tag in tags.split(",")] + else: # make sure None, False, and empty arrays are always repped consistently + tags = [] scheme = schema.thread(ID, author, body, title, tags) thread_dump(ID, scheme) return scheme @@ -46,14 +49,7 @@ def thread_create(author, body, title, tags): def thread_load(ID, markup=True): try: with open(path.join(PATH, "threads", ID), "r") as f: - thread = json.loads(f.read()) - if not markup: - thread["body"] = formatting.cleanse(thread["body"]) - for x in range(len(thread["replies"])): - thread["replies"][x]["body"] = formatting.cleanse( - thread["replies"][x]["body"]) - return thread - + return json.loads(f.read()) except FileNotFoundError: return False @@ -82,6 +78,44 @@ def thread_reply(ID, author, body): return reply +def index_reply(reply_list, post_id): + for index, reply in enumerate(reply_list): + if reply["post_id"] == post_id: + return index + else: + raise IndexError + + +def edit_handler(json, thread=None): + try: + target_id = json["post_id"] + if not thread: + thread = thread_load(json["thread_id"]) + if not thread: + return False, schema.error(7, "Requested thread does not exist.") + + + if target_id == 1: + target = thread + else: + target = thread["replies"][ + index_reply(thread["replies"], target_id)] + + if not user_is_admin(json["user"]): + if json["user"] != target["author"]: + return False, schema.error(10, + "non-admin attempt to edit another user's message") + + elif (time() - target["created"]) > 86400: + return False, schema.error(9, + "message is too old to edit (24hr limit)") + + return True, target + + except IndexError: + return False, schema.error(3, "post_id out of bounds for requested thread") + + ### USER MANAGEMENT ### def user_dbdump(dictionary): @@ -104,6 +138,15 @@ def user_register(auth_hash, name, quip, bio): if USERDB["namemap"].get(name): return schema.error(4, "Username taken.") + for ok, error in [ + user_namecheck(name), + user_authcheck(auth_hash), + user_quipcheck(quip), + user_biocheck(bio)]: + + if not ok: + return error + ID = uuid1().hex scheme = schema.user_internal(ID, auth_hash, name, quip, bio, False) USERDB.update({ID: scheme}) @@ -123,6 +166,70 @@ def user_auth(ID, auth_hash): return auth_hash == USERDB[ID]["auth_hash"] +def user_is_admin(ID): + return USERDB[ID]["admin"] + + def user_update(ID, **params): USERDB[ID].update(params) return USERDB[ID] + + +### SANITY CHECKS ### + +def contains_nonspaces(string): + return any([char in string for char in "\t\n\r\x0b\x0c"]) + + +def user_namecheck(name): + if not name: + return False, schema.error(4, + "Username may not be empty.") + + elif contains_nonspaces(name): + return False, schema.error(4, + "Username cannot contain whitespace chars besides spaces.") + + elif len(username) > 24: + return False, schema.error(4, + "Username is too long (max 24 chars)") + + return True, True + + +def user_authcheck(auth_hash): + if not auth_hash: + return schema.error(3, + "auth_hash may not be empty") + + elif len(auth_hash) != 64: + return False, schema.error(4, + "Client error: invalid SHA-256 hash.") + + return True, True + + +def user_quipcheck(quip): + if not quip: + return "" + + elif contains_nonspaces(quip): + return False, schema.error(4, + "Quip cannot contain whitespace chars besides spaces.") + + elif len(quip) > 120: + return False, schema.error(4, + "Quip is too long (max 120 chars)") + + return True, True + + +def user_biocheck(bio): + if not bio: + return "" + + elif len(bio) > 4096: + return False, schema.error(4, + "Bio is too long (max 4096 chars)") + + return True, True diff --git a/src/endpoints.py b/src/endpoints.py index 3042627..434f791 100644 --- a/src/endpoints.py +++ b/src/endpoints.py @@ -1,17 +1,22 @@ from src import formatting from src import schema +from time import time from src import db -from json import dumps + endpoints = { - "check_auth": ["user", "auth_hash"], - "is_registered": ["target_user"], - "thread_load": ["thread_id"], - "thread_index": [], - "thread_create": ["title", "body", "tags"], - "thread_reply": ["thread_id", "body"], - "user_register": ["user", "auth_hash", "quip", "bio"], - "user_get": ["target_user"], + "check_auth": ["user", "auth_hash"], + "is_registered": ["target_user"], + "is_admin": ["target_user"], + "thread_index": [], + "thread_load": ["thread_id"], + "thread_create": ["title", "body", "tags"], + "thread_reply": ["thread_id", "body"], + "edit_post": ["thread_id", "post_id", "body"], + "can_edit": ["thread_id", "post_id"], + "user_register": ["user", "auth_hash", "quip", "bio"], + "user_get": ["target_user"], + "user_name_to_id": ["target_user"] } @@ -21,6 +26,8 @@ authless = [ ] +# this is not actually an endpoint, but produces a required +# element of thread responses. def create_usermap(thread, index=False): if index: return {user: db.user_get(user) for user in @@ -31,23 +38,81 @@ def create_usermap(thread, index=False): return {x: db.user_get(x) for x in result} +def user_name_to_id(json): + """ + Returns a string of the target_user's ID when it is + part of the database: a non-existent user will return + a boolean false. + """ + return db.user_resolve(json["target_user"]) + + def is_registered(json): + """ + Returns true or false whether target_user is registered + in the system. This function only takes usernames: not + user IDs. + """ return bool(db.USERDB["namemap"].get(json["target_user"])) def check_auth(json): + "Returns true or false whether auth_hashes matches user." return bool(db.user_auth(json["user"], json["auth_hash"])) +def is_admin(json): + """ + Returns true or false whether target_user is a system + administrator. Takes a username or user ID. Nonexistent + users return false. + """ + user = db.user_resolve(json["target_user"]) + if user: + return db.user_is_admin(user) + return False + + def user_register(json): + """ + Registers a new user into the system. Returns the new internal user + object on success, or an error response. + + auth_hash should be a hexadecimal SHA-256 string, produced from a + UTF-8 password string. + + user should be a string containing no newlines and + under 24 characters in length. + + quip is a string, up to 120 characters, provided by the user + the acts as small bio, suitable for display next to posts + if the client wants to. Whitespace characters besides space + are not allowed. The string may be empty. + + bio is a string, up to 4096 chars, provided by the user that + can be shown on profiles. There are no character type limits + for this entry. The string may be empty. + + All errors for this endpoint with code 4 should show the + description direcrtly to the user. + + """ + return schema.response( db.user_register( - json["auth_hash"], json["user"], + json["auth_hash"], json["quip"], json["bio"])) +def user_get(json): + user = db.user_resolve(json["target_user"]) + if not user: + return False + return db.user_get(user) + + def thread_index(json): index = db.thread_index(markup=not json.get("nomarkup")) return schema.response({"threads": index}, create_usermap(index, True)) @@ -79,3 +144,21 @@ def thread_reply(json): if json.get("nomarkup"): reply["body"] = formatting.cleanse(reply["body"]) return schema.response(reply) + + +def can_edit(json): + return db.edit_handler(json)[0] + + +def edit_post(json): + thread = db.thread_load(json["thread_id"]) + admin = db.user_is_admin(json["user"]) + target_id = json["post_id"] + query, obj = db.edit_handler(json, thread) + + if query: + obj["body"] = json["body"] + obj["lastmod"] = time() + obj["edited"] = True + db.thread_dump(json["thread_id"], thread) + return obj diff --git a/src/server.py b/src/server.py index f5bf305..345cc68 100644 --- a/src/server.py +++ b/src/server.py @@ -43,6 +43,13 @@ class RequestHandler(StreamRequestHandler): db.user_auth(user, request.get("auth_hash")): return self.reply(schema.error(6, "Authorization failed.")) + # post_ids are always returned as integers, but for callers who + # provide them as something else, try to convert them. + if isinstance(request.get("post_id"), (float, str)): + try: request["post_id"] = int(request["post_id"]) + except Exception: + return schema.error(3, "Non-numeric post_id") + # exception handling is now passed to the endpoints; # anything unhandled beyond here is a code 1 self.reply(eval("endpoints." + endpoint)(request)) |