You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
isso/isso/views/comment.py

206 lines
5.8 KiB

# -*- encoding: utf-8 -*-
import cgi
import json
import time
import thread
import hashlib
import sqlite3
import logging
from itsdangerous import SignatureExpired, BadSignature
from werkzeug.wrappers import Response
from werkzeug.exceptions import abort, BadRequest
from isso import utils, notify
FIELDS = set(['id', 'parent', 'text', 'author', 'website', 'email', 'mode',
'created', 'modified', 'likes', 'dislikes', 'hash'])
class requires:
def __init__(self, type, param):
self.param = param
self.type = type
def __call__(self, func):
def dec(app, env, req, *args, **kwargs):
if self.param not in req.args:
raise BadRequest("missing %s query" % self.param)
try:
kwargs[self.param] = self.type(req.args[self.param])
except TypeError:
raise BadRequest("invalid type for %s, expected %s" % (self.param, self.type))
return func(app, env, req, *args, **kwargs)
return dec
@requires(str, 'uri')
def new(app, environ, request, uri):
if uri not in app.db.threads and not utils.urlexists(app.ORIGIN, uri):
return Response('URI does not exist', 404)
try:
data = json.loads(request.data)
except ValueError:
return Response("No JSON object could be decoded", 400)
for field in set(data.keys()) - set(['text', 'author', 'website', 'email', 'parent']):
data.pop(field)
if not data.get("text"):
return Response("No text given.", 400)
if "id" in data and not isinstance(data["id"], int):
return Response("Parent ID must be an integer.")
for field in ("author", "email"):
if data.get(field):
data[field] = cgi.escape(data[field])
data['remote_addr'] = utils.anonymize(unicode(request.remote_addr))
if uri not in app.db.threads:
app.db.threads.new(uri, utils.heading(app.ORIGIN, uri))
title = app.db.threads[uri].title
try:
rv = app.db.comments.add(uri, data)
except sqlite3.Error:
logging.exception('uncaught SQLite3 exception')
abort(400)
href = (app.ORIGIN.rstrip("/") + uri + "#isso-%i" % rv["id"])
thread.start_new_thread(
app.notify,
notify.create(rv, title, href, utils.anonymize(unicode(request.remote_addr))))
# save checksum of text into cookie, so mallory can't modify/delete a comment, if
# he add a comment, then removed it but not the signed cookie.
checksum = hashlib.md5(rv["text"]).hexdigest()
rv["text"] = app.markdown(rv["text"])
rv["hash"] = hashlib.md5(rv.get('email') or utils.salt(rv['remote_addr'])).hexdigest()
for key in set(rv.keys()) - FIELDS:
rv.pop(key)
resp = Response(json.dumps(rv), 202 if rv["mode"] == 2 else 201,
content_type='application/json')
resp.set_cookie(str(rv["id"]), app.sign([rv["id"], checksum]),
max_age=app.MAX_AGE, path='/')
return resp
def single(app, environ, request, id):
if request.method == 'GET':
rv = app.db.comments.get(id)
if rv is None:
abort(404)
for key in set(rv.keys()) - FIELDS:
rv.pop(key)
if request.args.get('plain', '0') == '0':
rv['text'] = app.markdown(rv['text'])
return Response(json.dumps(rv), 200, content_type='application/json')
try:
rv = app.unsign(request.cookies.get(str(id), ''))
except (SignatureExpired, BadSignature):
try:
rv = app.unsign(request.cookies.get('admin', ''))
except (SignatureExpired, BadSignature):
abort(403)
if rv[0] != id:
abort(403)
# verify checksum, mallory might skip cookie deletion when he deletes a comment
if rv[1] != hashlib.md5(app.db.comments.get(id)["text"]).hexdigest():
abort(403)
if request.method == 'PUT':
try:
data = json.loads(request.data)
except ValueError:
return Response("No JSON object could be decoded", 400)
if data.get("text") is not None and len(data['text']) < 3:
return Response("No text given.", 400)
for key in set(data.keys()) - set(["text", "author", "website"]):
data.pop(key)
data['modified'] = time.time()
try:
rv = app.db.comments.update(id, data)
except sqlite3.Error:
logging.exception('uncaught SQLite3 exception')
abort(400)
for key in set(rv.keys()) - FIELDS:
rv.pop(key)
rv["text"] = app.markdown(rv["text"])
return Response(json.dumps(rv), 200, content_type='application/json')
if request.method == 'DELETE':
rv = app.db.comments.delete(id)
if rv:
for key in set(rv.keys()) - FIELDS:
rv.pop(key)
resp = Response(json.dumps(rv), 200, content_type='application/json')
resp.delete_cookie(str(id), path='/')
return resp
@requires(str, 'uri')
def fetch(app, environ, request, uri):
rv = list(app.db.comments.fetch(uri))
if not rv:
abort(404)
for item in rv:
item['hash'] = hashlib.md5(item['email'] or utils.salt(item['remote_addr'])).hexdigest()
for key in set(item.keys()) - FIELDS:
item.pop(key)
if request.args.get('plain', '0') == '0':
for item in rv:
item['text'] = app.markdown(item['text'])
return Response(json.dumps(rv), 200, content_type='application/json')
def like(app, environ, request, id):
nv = app.db.comments.like(id, utils.anonymize(unicode(request.remote_addr)))
return Response(str(nv), 200)
@requires(str, 'uri')
def count(app, environ, request, uri):
rv = app.db.comments.count(uri)[0]
if rv == 0:
abort(404)
return Response(json.dumps(rv), 200, content_type='application/json')