def preview(self, environment, request): data = request.get_json() if "text" not in data or data["text"] is None: raise BadRequest("no text given") return JSON({'text': self.isso.render(data["text"])}, 200)
def threads(self, environ, request): def thread_freshness(thread): comments = self.comments.fetch(thread['uri'], order_by="created") comments = list(comments) if not comments: from isso.db import schema with schema.session(self.isso.conf.get('general', 'dbpath')) as session: posted = session.query(schema.Thread) \ .filter(schema.Thread.uri == thread['uri']) \ .one() \ .date_added return time.mktime(posted.timetuple()) return max(comment['created'] for comment in comments) threads = list(self._threads.get_all()) sorted_threads = list( sorted(((thread_freshness(t), t) for t in threads), key=lambda x: x[0])) return JSON({ "threads": [{ "uri": thread["uri"], "title": thread["title"], "last_update_time": time } for (time, thread) in sorted_threads], "hidden_threads": 0 })
def delete(self, environ, request, id, key=None): data = request.get_json() for field in set(data.keys()) - API.ACCEPT: data.pop(field) valid, reason = self.authenticate(data) if not valid: return BadRequest(reason) item = self.comments.get(id) if item is None: raise NotFound self.authorize(id, data, item, request.cookies.get(str(id), '')) self.cache.delete('hash', (item['email'] or item['remote_addr']).encode('utf-8')) with self.isso.lock: rv = self.comments.delete(id) if rv: for key in set(rv.keys()) - API.FIELDS: rv.pop(key) self.signal("comments.delete", id) resp = JSON(rv, 200) cookie = functools.partial(dump_cookie, expires=0, max_age=0) resp.headers.add("Set-Cookie", cookie(str(id))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id)) return resp
def delete(self, environ, request, id, key=None): try: rv = self.isso.unsign(request.cookies.get(str(id), "")) except (SignatureExpired, BadSignature): raise Forbidden else: if rv[0] != id: raise Forbidden # verify checksum, mallory might skip cookie deletion when he deletes a comment if rv[1] != sha1(self.comments.get(id)["text"]): raise Forbidden item = self.comments.get(id) if item is None: raise NotFound self.cache.delete('hash', (item['email'] or item['remote_addr']).encode('utf-8')) with self.isso.lock: rv = self.comments.delete(id) if rv: for key in set(rv.keys()) - API.FIELDS: rv.pop(key) self.signal("comments.delete", id) resp = JSON(rv, 200) cookie = functools.partial(dump_cookie, expires=0, max_age=0) resp.headers.add("Set-Cookie", cookie(str(id))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id)) return resp
def author(self, environ, request): rv = self.isso.author if rv == "": raise NotFound return JSON(rv, 200)
def counts(self, environ, request): data = request.get_json() if not isinstance(data, list) and not all(isinstance(x, str) for x in data): raise BadRequest("JSON must be a list of URLs") return JSON(self.comments.count(*data), 200)
def count(self, environ, request, uri): rv = self.comments.count(uri)[0] if rv == 0: raise NotFound return JSON(rv, 200)
def moderate(self, environ, request, id, action, key): try: id = self.isso.unsign(key, max_age=2**32) except (BadSignature, SignatureExpired): raise Forbidden item = self.comments.get(id) thread = self.threads.get(item['tid']) link = local("origin") + thread["uri"] + "#isso-%i" % item["id"] if item is None: raise NotFound if request.method == "GET": modal = ( "<!DOCTYPE html>" "<html>" "<head>" "<script>" " if (confirm('%s: Are you sure?')) {" " xhr = new XMLHttpRequest;" " xhr.open('POST', window.location.href);" " xhr.send(null);" " xhr.onload = function() {" " window.location.href = %s;" " };" " }" "</script>" % (action.capitalize(), json.dumps(link))) return Response(modal, 200, content_type="text/html") if action == "activate": if item['mode'] == 1: return Response("Already activated", 200) with self.isso.lock: self.comments.activate(id) self.signal("comments.activate", thread, item) return Response("Yo", 200) elif action == "edit": data = request.get_json() with self.isso.lock: rv = self.comments.update(id, data) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) self.signal("comments.edit", rv) return JSON(rv, 200) else: with self.isso.lock: self.comments.delete(id) self.cache.delete( 'hash', (item['email'] or item['remote_addr']).encode('utf-8')) self.signal("comments.delete", id) return Response("Yo", 200) """
def view(self, environ, request, id): rv = self.comments.get(id) if rv is None: raise NotFound for key in set(rv.keys()) - API.FIELDS: rv.pop(key) if request.args.get('plain', '0') == '0': rv['text'] = self.isso.render(rv['text']) return JSON(rv, 200)
def view(self, environ, request, id): rv = self.comments.get(id) if rv is None: raise NotFound try: self.isso.unsign(request.cookies.get(str(id), '')) except (SignatureExpired, BadSignature): raise Forbidden for key in set(rv.keys()) - API.FIELDS: rv.pop(key) if request.args.get('plain', '0') == '0': rv['text'] = self.isso.render(rv['text']) return JSON(rv, 200)
def edit(self, environ, request, id): data = request.get_json() for field in set(data.keys()) - API.ACCEPT: data.pop(field) valid, reason = self.authenticate(data) if not valid: return BadRequest(reason) valid, reason = self.verify(data) if not valid: return BadRequest(reason) comment = self.comments.get(id) if time.time() > comment.get('created') + self.conf.getint('max-age'): raise Forbidden self.authorize(id, data, comment, request.cookies.get(str(id), '')) data['modified'] = time.time() for key in set(data.keys()) - API.FIELDS: data.pop(key) with self.isso.lock: rv = self.comments.update(id, data) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) self.signal("comments.edit", rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) resp = JSON(rv, 200) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def edit(self, environ, request, id): try: rv = self.isso.unsign(request.cookies.get(str(id), '')) except (SignatureExpired, BadSignature): raise Forbidden if rv[0] != id: raise Forbidden # verify checksum, mallory might skip cookie deletion when he deletes a comment if rv[1] != sha1(self.comments.get(id)["text"]): raise Forbidden data = request.get_json() if "text" not in data or data["text"] is None or len(data["text"]) < 3: raise BadRequest("no text given") for key in set(data.keys()) - set(["text", "author", "website"]): data.pop(key) data['modified'] = time.time() with self.isso.lock: rv = self.comments.update(id, data) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) self.signal("comments.edit", rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) resp = JSON(rv, 200) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def latest(self, environ, request): # if the feature is not allowed, don't present the endpoint if not self.conf.getboolean("latest-enabled"): return NotFound() # get and check the limit bad_limit_msg = "Query parameter 'limit' is mandatory (integer, >0)" try: limit = int(request.args['limit']) except (KeyError, ValueError): return BadRequest(bad_limit_msg) if limit <= 0: return BadRequest(bad_limit_msg) # retrieve the latest N comments from the DB all_comments_gen = self.comments.fetchall(limit=None, order_by='created', mode='1') comments = collections.deque(all_comments_gen, maxlen=limit) # prepare a special set of fields (except text which is rendered specifically) fields = { 'author', 'created', 'dislikes', 'id', 'likes', 'mode', 'modified', 'parent', 'text', 'uri', 'website', } # process the retrieved comments and build results result = [] for comment in comments: processed = {key: comment[key] for key in fields} processed['text'] = self.isso.render(comment['text']) result.append(processed) return JSON(result, 200)
def dislike(self, environ, request, id): nv = self.comments.vote( False, id, utils.anonymize(str(request.remote_addr))) return JSON(nv, 200)
def fetch(self, environ, request, uri): args = { 'uri': uri, 'after': request.args.get('after', 0) } try: args['limit'] = int(request.args.get('limit')) except TypeError: args['limit'] = None except ValueError: return BadRequest("limit should be integer") if request.args.get('parent') is not None: try: args['parent'] = int(request.args.get('parent')) root_id = args['parent'] except ValueError: return BadRequest("parent should be integer") else: args['parent'] = None root_id = None plain = request.args.get('plain', '0') == '0' reply_counts = self.comments.reply_count(uri, after=args['after']) if args['limit'] == 0: root_list = [] else: root_list = list(self.comments.fetch(**args)) if not root_list: raise NotFound if root_id not in reply_counts: reply_counts[root_id] = 0 try: nested_limit = int(request.args.get('nested_limit')) except TypeError: nested_limit = None except ValueError: return BadRequest("nested_limit should be integer") rv = { 'id': root_id, 'total_replies': reply_counts[root_id], 'hidden_replies': reply_counts[root_id] - len(root_list), 'replies': self._process_fetched_list(root_list, plain) } # We are only checking for one level deep comments if root_id is None: for comment in rv['replies']: if comment['id'] in reply_counts: comment['total_replies'] = reply_counts[comment['id']] if nested_limit is not None: if nested_limit > 0: args['parent'] = comment['id'] args['limit'] = nested_limit replies = list(self.comments.fetch(**args)) else: replies = [] else: args['parent'] = comment['id'] replies = list(self.comments.fetch(**args)) else: comment['total_replies'] = 0 replies = [] comment['hidden_replies'] = comment['total_replies'] - \ len(replies) comment['replies'] = self._process_fetched_list(replies, plain) return JSON(rv, 200)
def new(self, environ, request, uri): data = request.get_json() for field in set(data.keys()) - API.ACCEPT: data.pop(field) for key in ("author", "email", "website", "parent"): data.setdefault(key, None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) for field in ("author", "email", "website"): if data.get(field) is not None: data[field] = cgi.escape(data[field]) if data.get("website"): data["website"] = normalize(data["website"]) data['mode'] = 2 if self.moderated else 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with self.isso.lock: if uri not in self.threads: if 'title' not in data: with http.curl('GET', local("origin"), uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist %s') else: title = data['title'] thread = self.threads.new(uri, title) self.signal("comments.new:new-thread", thread) else: thread = self.threads[uri] # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, data) valid, reason = self.guard.validate(uri, data) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, data) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = self.hash(rv['email'] or rv['remote_addr']) self.cache.set( 'hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash']) rv = self._add_gravatar_image(rv) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp
def latest(self, env, req): comments = self.comments.latest(20) for c in comments: c["text"] = self.isso.render(c["text"]) return JSON({"comments":comments}, 200)
def dislike(self, environ, request, id): nv = self.comments.vote(False, id, self._remote_addr(request)) return JSON(nv, 200)
def like(self, environ, request, id): nv = self.comments.vote(True, id, str(request.remote_addr)) return JSON(nv, 200)
def fetch(self, environ, request, uri): args = {'uri': uri, 'after': request.args.get('after', 0)} try: args['limit'] = int(request.args.get('limit')) except TypeError: args['limit'] = None except ValueError: return BadRequest("limit should be integer") if request.args.get('parent') is not None: try: args['parent'] = int(request.args.get('parent')) root_id = args['parent'] except ValueError: return BadRequest("parent should be integer") else: args['parent'] = None root_id = None plain = request.args.get('plain', '0') == '0' order = request.args.get('order') if not order: order = "new" if order not in ("hot", "new"): return BadRequest('order should be "hot" or "new"') if order in ('hot', 'new'): args['order_by'] = 'created' if args['limit'] == 0: root_list = [] else: root_list = list(self.comments.fetch(**args)) if not root_list: raise NotFound reply_counts = self.comments.reply_count(uri, after=args['after']) if root_id not in reply_counts: reply_counts[root_id] = 0 try: nested_limit = int(request.args.get('nested_limit')) except TypeError: nested_limit = None except ValueError: return BadRequest("nested_limit should be integer") def do_sort(comments): if order == "hot": # busted! needs to iterate the whole tree. duh def find_freshness(comment): child = None for c in child['replies']: if c['parent'] == comment['id']: child = c break if child is None: return child['created'] return find_freshness(child) return (c for score, c in sorted( (find_freshness(comment), comment) for comment in comments)) else: return comments from isso.db import schema with schema.session(self.isso.conf.get('general', 'dbpath')) as session: date_added = session.query(schema.Thread) \ .filter(schema.Thread.uri == uri) \ .one() \ .date_added rv = { 'id': root_id, 'total_replies': reply_counts[root_id], # direct replies! 'hidden_replies': reply_counts[root_id] - len(root_list), 'replies': list(self._process_fetched_list(do_sort(root_list), plain)), 'date_added': date_added.isoformat() } def fetch_replies(comment, level): args['parent'] = comment['id'] replies = do_sort(self.comments.fetch(**args)) comment['replies'] = list( self._process_fetched_list(replies, plain)) comment['total_replies'] = len(comment['replies']) comment['hidden_replies'] = 0 for reply in comment['replies']: fetch_replies(reply, level + 1) for comment in rv['replies']: fetch_replies(comment, 0) return JSON(rv, 200)
def new(self, environ, request, uri, key): data = request.get_json() # check access keys rv = self.db.execute(['SELECT uri FROM access WHERE key = ? ;'], (key, )).fetchall() if not rv or rv[0][0] != uri: raise Forbidden for field in set(data.keys()) - API.ACCEPT: data.pop(field) for key in ("author", "parent"): data.setdefault(key, None) if data['parent'] is not None: data.setdefault('place', None) valid, reason = API.verify(data) if not valid: return BadRequest(reason) escaped = dict((key, cgi.escape(value) if value is not None else None) for key, value in data.items()) added = {} if escaped.get("website") is not None: added["website"] = normalize(escaped["website"]) added['mode'] = 2 if self.moderated else 1 prepared = dict(escaped) prepared.update(added) with self.isso.lock: if uri in self._threads: thread = self._threads[uri] else: if 'title' in prepared: title = prepared['title'] else: with http.curl('GET', local("origin"), uri) as resp: if resp and resp.status == 200: uri, title = parse.thread(resp.read(), id=uri) else: return NotFound('URI does not exist %s') thread = self._threads.new(uri, title) self.signal("comments.new:new-thread", thread) # notify extensions that the new comment is about to save self.signal("comments.new:before-save", thread, prepared) valid, reason = self.guard.validate(uri, prepared) if not valid: self.signal("comments.new:guard", reason) raise Forbidden(reason) with self.isso.lock: rv = self.comments.add(uri, prepared) # notify extension, that the new comment has been successfully saved self.signal("comments.new:after-save", thread, rv) cookie = functools.partial(dump_cookie, value=self.isso.sign( [rv["id"], sha1(rv["text"])]), max_age=self.conf.getint('max-age')) rv["text"] = self.isso.render(rv["text"]) rv["hash"] = self.hash(rv['remote_addr']) self.cache.set('hash', (rv['remote_addr']).encode('utf-8'), rv['hash']) for key in set(rv.keys()) - API.FIELDS: rv.pop(key) # success! self.signal("comments.new:finish", thread, rv) resp = JSON(rv, 202 if rv["mode"] == 2 else 201) resp.headers.add("Set-Cookie", cookie(str(rv["id"]))) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp