diff --git a/isso/__init__.py b/isso/__init__.py index 87c7256..63e019a 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -63,6 +63,7 @@ rules = Map([ Rule('/', methods=['GET'], endpoint=views.comment.fetch), Rule('/count', methods=['GET'], endpoint=views.comment.count), + Rule('/activate/', endpoint=views.comment.activate), Rule('/admin/', endpoint=views.admin.index), Rule('/check-ip', endpoint=views.comment.checkip) @@ -85,8 +86,8 @@ class Isso(object): def sign(self, obj): return self.signer.dumps(obj) - def unsign(self, obj): - return self.signer.loads(obj, max_age=self.conf.getint('general', 'max-age')) + def unsign(self, obj, max_age=None): + return self.signer.loads(obj, max_age=max_age or self.conf.getint('general', 'max-age')) def markdown(self, text): return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH \ diff --git a/isso/core.py b/isso/core.py index 87713db..838bbbd 100644 --- a/isso/core.py +++ b/isso/core.py @@ -40,7 +40,7 @@ class Config: "[general]", "dbpath = /tmp/isso.db", "secretkey = %r" % binascii.b2a_hex(os.urandom(24)), "host = http://localhost:8080/", "passphrase = p@$$w0rd", - "max-age = 900", + "max-age = 900", "moderated = false", "[server]", "host = localhost", "port = 8080", "reload = off", "[SMTP]", diff --git a/isso/db/comments.py b/isso/db/comments.py index 2027b67..37ffcd8 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -51,7 +51,7 @@ class Comments: ' ?, ?, ?, ?, ?', 'FROM threads WHERE threads.uri = ?;'], ( c.get('parent'), - c.get('created') or time.time(), None, self.db.mode, c['remote_addr'], + c.get('created') or time.time(), None, c["mode"], c['remote_addr'], c['text'], c.get('author'), c.get('email'), c.get('website'), buffer( Bloomfilter(iterable=[c['remote_addr']]).array), uri) @@ -61,11 +61,12 @@ class Comments: 'SELECT *, MAX(c.id) FROM comments AS c INNER JOIN threads ON threads.uri = ?', (uri, )).fetchone())) - # def activate(self, path, id): - # """Activate comment id if pending and return comment for (path, id).""" - # with sqlite3.connect(self.dbpath) as con: - # con.execute("UPDATE comments SET mode=1 WHERE path=? AND id=? AND mode=2", (path, id)) - # return self.get(path, id) + def activate(self, id): + """Activate comment id if pending and return comment for (path, id).""" + self.db.execute([ + 'UPDATE comments SET', + ' mode=1', + 'WHERE id=? AND mode=2'], (id, )) def update(self, id, data): """ diff --git a/isso/notify.py b/isso/notify.py index a5f4ccc..84d67ea 100644 --- a/isso/notify.py +++ b/isso/notify.py @@ -4,7 +4,7 @@ from smtplib import SMTP, SMTP_SSL from email.mime.text import MIMEText -def format(comment, permalink, remote_addr): +def format(comment, permalink, remote_addr, activation_key=None): rv = [] rv.append("%s schrieb:" % (comment["author"] or "Jemand")) @@ -18,6 +18,10 @@ def format(comment, permalink, remote_addr): rv.append("IP Adresse: %s" % remote_addr) rv.append("Link zum Kommentar: %s" % permalink) + if activation_key: + rv.append("") + rv.append("Kommentar freischalten: %s" % activation_key) + return u'\n'.join(rv) diff --git a/isso/views/comment.py b/isso/views/comment.py index d03b040..d9825c7 100644 --- a/isso/views/comment.py +++ b/isso/views/comment.py @@ -67,6 +67,7 @@ def new(app, environ, request, uri): if data.get(field): data[field] = cgi.escape(data[field]) + data['mode'] = (app.conf.getboolean('general', 'moderated') and 2) or 1 data['remote_addr'] = utils.anonymize(str(request.remote_addr)) with app.lock: @@ -83,8 +84,14 @@ def new(app, environ, request, uri): except db.IssoDBException: abort(403) - href = (app.conf.get('general', 'host').rstrip("/") + uri + "#isso-%i" % rv["id"]) - app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr)))) + host = app.conf.get('general', 'host').rstrip("/") + href = host + uri + "#isso-%i" % rv["id"] + auth = None + + if app.conf.getboolean('general', 'moderated'): + auth = host + environ["SCRIPT_NAME"] + "/activate/" + app.sign(str(rv["id"])) + + app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr)), auth)) # 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. @@ -146,11 +153,8 @@ def single(app, environ, request, id): data['modified'] = time.time() - try: + with app.lock: 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) @@ -218,5 +222,18 @@ def count(app, environ, request, uri): return Response(json.dumps(rv), 200, content_type='application/json') +def activate(app, environ, request, auth): + + try: + id = app.unsign(auth, max_age=2**32) + except (BadSignature, SignatureExpired): + abort(403) + + with app.lock: + app.db.comments.activate(id) + + return Response("Yo", 200) + + def checkip(app, env, req): return Response(utils.anonymize(str(req.remote_addr)), 200) diff --git a/specs/test_comment.py b/specs/test_comment.py index 13bf557..6d47042 100644 --- a/specs/test_comment.py +++ b/specs/test_comment.py @@ -256,3 +256,34 @@ class TestComments(unittest.TestCase): self.put('/id/1', data=json.dumps({"text": "Typo"})) assert loads(self.get('/id/1').data)["text"] == "

Typo

\n" + + +class TestModeratedComments(unittest.TestCase): + + def setUp(self): + fd, self.path = tempfile.mkstemp() + conf = core.Config.load(None) + conf.set("general", "dbpath", self.path) + conf.set("general", "moderated", "true") + conf.set("guard", "enabled", "off") + + class App(Isso, core.Mixin): + pass + + self.app = App(conf) + self.app.wsgi_app = FakeIP(self.app.wsgi_app) + self.client = Client(self.app, Response) + + def tearDown(self): + os.unlink(self.path) + + def testAddComment(self): + + rv = self.client.post('/new?uri=test', data=json.dumps({"text": "..."})) + assert rv.status_code == 202 + + assert self.client.get('/id/1').status_code == 200 + assert self.client.get('/?uri=test').status_code == 404 + + self.app.db.comments.activate(1) + assert self.client.get('/?uri=test').status_code == 200