Esempio n. 1
0
    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)
Esempio n. 2
0
    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
        })
Esempio n. 3
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
Esempio n. 4
0
    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
Esempio n. 5
0
    def author(self, environ, request):

        rv = self.isso.author

        if rv == "":
            raise NotFound

        return JSON(rv, 200)
Esempio n. 6
0
    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)
Esempio n. 7
0
    def count(self, environ, request, uri):

        rv = self.comments.count(uri)[0]

        if rv == 0:
            raise NotFound

        return JSON(rv, 200)
Esempio n. 8
0
    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)

        """
Esempio n. 9
0
    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)
Esempio n. 10
0
    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)
Esempio n. 11
0
    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
Esempio n. 12
0
    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
Esempio n. 13
0
    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)
Esempio n. 14
0
    def dislike(self, environ, request, id):

        nv = self.comments.vote(
            False, id, utils.anonymize(str(request.remote_addr)))
        return JSON(nv, 200)
Esempio n. 15
0
    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)
Esempio n. 16
0
    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
Esempio n. 17
0
 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)
Esempio n. 18
0
    def dislike(self, environ, request, id):

        nv = self.comments.vote(False, id, self._remote_addr(request))
        return JSON(nv, 200)
Esempio n. 19
0
    def like(self, environ, request, id):

        nv = self.comments.vote(True, id, str(request.remote_addr))
        return JSON(nv, 200)
Esempio n. 20
0
    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)
Esempio n. 21
0
    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