From cbd4d90cb747e143a3b44514d77eb541f3611fe0 Mon Sep 17 00:00:00 2001 From: benjhess Date: Sun, 2 Apr 2017 21:13:06 +0200 Subject: [PATCH 01/30] Added optional gravatar support --- docs/docs/configuration/client.rst | 8 ++++++++ docs/docs/configuration/server.rst | 12 ++++++++++++ isso/js/app/config.js | 1 + isso/js/app/isso.js | 4 ++-- isso/js/app/text/comment.jade | 3 +++ isso/utils/hash.py | 3 ++- isso/views/comments.py | 17 ++++++++++++++++- share/isso.conf | 7 +++++++ 8 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/docs/configuration/client.rst b/docs/docs/configuration/client.rst index ea7487a..37d32ea 100644 --- a/docs/docs/configuration/client.rst +++ b/docs/docs/configuration/client.rst @@ -106,6 +106,14 @@ scheme is based in `this color palette `_. Multiple colors must be separated by space. If you use less than eight colors and not a multiple of 2, the color distribution is not even. +data-isso-gravatar +------------------ + +Uses gravatar images instead of generating svg images. You have to set +"data-isso-avatar" to **false** when you want to use this. Otherwise +both the gravatar and avatar svg image will show up. Please also set +option "gravatar" to **true** in the server configuration... + data-isso-vote -------------- diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index cb39ccd..9cd11e7 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -91,6 +91,18 @@ notify log-file Log console messages to file instead of standard out. +gravatar + When set to ``true`` this will add the property "gravatar_image" + containing the link to a gravatar image to every comment. If a comment + does not contain an email address, gravatar will render a random icon. + This is only true when using the default value for "gravatar-url" + which contains the query string param ``d=identicon`` ... + +gravatar-url + Url for gravatar images. The "{}" is where the email hash will be placed. + Defaults to "https://www.gravatar.com/avatar/{}?d=identicon" + + .. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS diff --git a/isso/js/app/config.js b/isso/js/app/config.js index 952d588..2a4cde1 100644 --- a/isso/js/app/config.js +++ b/isso/js/app/config.js @@ -10,6 +10,7 @@ define(function() { "max-comments-top": "inf", "max-comments-nested": 5, "reveal-on-click": 5, + "gravatar": false, "avatar": true, "avatar-bg": "#f0f0f0", "avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6", diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index 0523e9b..7b908a1 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -228,7 +228,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", $("a.edit", footer).toggle("click", function(toggler) { var edit = $("a.edit", footer); - var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null; + var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null; edit.textContent = i18n.translate("comment-save"); edit.insertAfter($.new("a.cancel", i18n.translate("comment-cancel"))).on("click", function() { @@ -256,7 +256,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", }, function(toggler) { var textarea = $(".textarea", text); - var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null; + var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null; if (! toggler.canceled && textarea !== null) { if (utils.text(textarea.innerHTML).length < 3) { diff --git a/isso/js/app/text/comment.jade b/isso/js/app/text/comment.jade index 4884bf7..1d97499 100644 --- a/isso/js/app/text/comment.jade +++ b/isso/js/app/text/comment.jade @@ -1,4 +1,7 @@ div(class='isso-comment' id='isso-#{comment.id}') + if conf.gravatar + div(class='avatar') + img(src='#{comment.gravatar_image}') if conf.avatar div(class='avatar') svg(data-hash='#{comment.hash}') diff --git a/isso/utils/hash.py b/isso/utils/hash.py index 0e93a1a..2ade078 100644 --- a/isso/utils/hash.py +++ b/isso/utils/hash.py @@ -109,4 +109,5 @@ def new(conf): return Hash(salt, algorithm) -sha1 = Hash(func="sha1").uhash \ No newline at end of file +sha1 = Hash(func="sha1").uhash +md5 = Hash(func="md5").uhash diff --git a/isso/views/comments.py b/isso/views/comments.py index d328305..1be4831 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -22,6 +22,7 @@ from isso import utils, local from isso.utils import http, parse, JSONResponse as JSON from isso.views import requires from isso.utils.hash import sha1 +from isso.utils.hash import md5 # from Django appearently, looks good to me *duck* __url_re = re.compile( @@ -72,7 +73,7 @@ def xhr(func): class API(object): FIELDS = set(['id', 'parent', 'text', 'author', 'website', - 'mode', 'created', 'modified', 'likes', 'dislikes', 'hash']) + 'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image']) # comment fields, that can be submitted ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title']) @@ -206,6 +207,8 @@ class API(object): 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) @@ -426,7 +429,17 @@ class API(object): comment['replies'] = self._process_fetched_list(replies, plain) return JSON(rv, 200) + def _add_gravatar_image(self, item): + if not self.conf.getboolean('gravatar'): + return item + + email = item['email'] or "" + email_md5_hash = md5(email) + gravatar_url = self.conf.get('gravatar-url') + item['gravatar_image'] = gravatar_url.format(email_md5_hash) + + return item def _process_fetched_list(self, fetched_list, plain=False): for item in fetched_list: @@ -439,6 +452,8 @@ class API(object): item['hash'] = val + item = self._add_gravatar_image(item) + for key in set(item.keys()) - API.FIELDS: item.pop(key) diff --git a/share/isso.conf b/share/isso.conf index 7c275ac..4bb3ef3 100644 --- a/share/isso.conf +++ b/share/isso.conf @@ -46,6 +46,13 @@ notify = stdout # Log console messages to file instead of standard out. log-file = +# adds property "gravatar_image" to json response when true +# will automatically build md5 hash by email and use "gravatar_url" to build +# the url to the gravatar image +gravatar = false + +# default url for gravatar. {} is where the hash will be placed +gravatar-url = https://www.gravatar.com/avatar/{}?d=identicon [moderation] # enable comment moderation queue. This option only affects new comments. From e9eebf58e3a5a0c67411622ee686eb4ec4f45d91 Mon Sep 17 00:00:00 2001 From: benjhess Date: Sun, 2 Apr 2017 19:48:03 +0000 Subject: [PATCH 03/30] Refactored comments unittest to work with optional gravatar feature --- isso/tests/test_comments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index 1e2bdf1..b6fb393 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -305,7 +305,8 @@ class TestComments(unittest.TestCase): rv = loads(rv.data) for key in comments.API.FIELDS: - rv.pop(key) + if key in rv: + rv.pop(key) self.assertListEqual(list(rv.keys()), []) From 30fef390f55fd8c23ade69c54a0357a46945ac18 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 5 Apr 2018 23:10:46 +0800 Subject: [PATCH 04/30] fix: admin and demo view redirect issue --- isso/views/comments.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index d3b80c3..bfebb3f 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -843,14 +843,19 @@ class API(object): return JSON({'text': self.isso.render(data["text"])}, 200) def demo(self, env, req): - return redirect(get_current_url(env) + '/index.html') + return redirect( + get_current_url(env, strip_querystring=True) + '/index.html' + ) def login(self, env, req): data = req.form password = self.isso.conf.get("general", "admin_password") if data['password'] and data['password'] == password: - response = redirect(get_current_url( - env, host_only=True) + '/admin') + response = redirect(re.sub( + r'/login$', + '/admin', + get_current_url(env, strip_querystring=True) + )) cookie = functools.partial(dump_cookie, value=self.isso.sign({"logged": True}), expires=datetime.now() + timedelta(1)) From 9618c0f3a3a8fb2f29096e88ac904a3a31ad5916 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 17 Apr 2018 22:32:16 +0200 Subject: [PATCH 05/30] jade: avoid using eval once compiled Use of eval is handy when we need to automatically reload a template. However, in production, this is slow and unsafe. Moreover, when using CSP, we have to use 'unsafe-eval' which brings shame to most of us. It appears use of eval() is not needed because the template has already been translated to Javascript. We just need to bind "jade" to its local scope. So, we add an additional wrapper function binding "jade" to the local scope. Moreover, when compiling the template, we add a flag to the function to know it has already been compiled. In this case, we execute it with "jade" in its scope. Otherwise, we keep using eval. Quickly tested in both situations. Seem to work. Fix #274. --- isso/js/app/jade.js | 3 +++ isso/js/lib/requirejs-jade/jade.js | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/isso/js/app/jade.js b/isso/js/app/jade.js index 46d6269..0064d60 100644 --- a/isso/js/app/jade.js +++ b/isso/js/app/jade.js @@ -7,6 +7,9 @@ define(["libjs-jade-runtime", "app/utils", "jade!app/text/postbox", "jade!app/te var load = function(name, js) { templates[name] = (function(jade) { var fn; + if (js.compiled) { + return js(jade); + } eval("fn = " + js); return fn; })(runtime); diff --git a/isso/js/lib/requirejs-jade/jade.js b/isso/js/lib/requirejs-jade/jade.js index 59189a4..383d3f5 100644 --- a/isso/js/lib/requirejs-jade/jade.js +++ b/isso/js/lib/requirejs-jade/jade.js @@ -49,8 +49,12 @@ define(function() { write: function(plugin, name, write) { if (builds.hasOwnProperty(name)) { write("define('" + plugin + "!" + name +"', function () {" + - " var fn = " + builds[name] + ";" + - " return fn;" + + " var wfn = function (jade) {" + + " var fn = " + builds[name] + ";" + + " return fn;" + + " };" + + "wfn.compiled = true;" + + "return wfn;" + "});\n"); } } From fcf576dd084f38f3c5fcb78f6fd8bd8565cb08bc Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 20 Apr 2018 20:11:32 +0200 Subject: [PATCH 06/30] css: remove CSS code for avatar in postbox It has been removed in 0211322915be. --- isso/css/isso.css | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/isso/css/isso.css b/isso/css/isso.css index 47cf675..abfc805 100644 --- a/isso/css/isso.css +++ b/isso/css/isso.css @@ -32,22 +32,13 @@ .isso-follow-up .isso-comment { border-top: 1px solid rgba(0, 0, 0, 0.1); } -.isso-comment > div.avatar, -.isso-postbox > .avatar { +.isso-comment > div.avatar { display: block; float: left; width: 7%; margin: 3px 15px 0 0; } -.isso-postbox > .avatar { - float: left; - margin: 5px 10px 0 5px; - width: 48px; - height: 48px; - overflow: hidden; -} -.isso-comment > div.avatar > svg, -.isso-postbox > .avatar > svg { +.isso-comment > div.avatar > svg { max-width: 48px; max-height: 48px; border: 1px solid rgba(0, 0, 0, 0.2); From 8d8f9c8c59ad2960c25bb895e73b8dcbcb5020ed Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sat, 21 Apr 2018 10:25:12 +0200 Subject: [PATCH 07/30] html: add nofollow/noopener to links "nofollow" is a deterrent for spammers: they cannot put links and hope to increase their SEO when all these links have the nofollow relationship. "noopener" is a security for links opening a new window. They ensure the target cannot control us. Fix #373 --- isso/tests/test_html.py | 4 ++-- isso/utils/html.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/isso/tests/test_html.py b/isso/tests/test_html.py index 316fbf8..327357c 100644 --- a/isso/tests/test_html.py +++ b/isso/tests/test_html.py @@ -65,7 +65,7 @@ class TestHTML(unittest.TestCase): examples = [ ('Look: ', 'Look: '), ('Ha', - 'Ha'), + 'Ha'), ('Ha', 'Ha'), ('

Test

', '

Test

'), ('', 'alert("Onoe")')] @@ -93,4 +93,4 @@ class TestHTML(unittest.TestCase): }) renderer = html.Markup(conf.section("markup")).render self.assertEqual(renderer("http://example.org/ and sms:+1234567890"), - '

http://example.org/ and sms:+1234567890

') + '

http://example.org/ and sms:+1234567890

') diff --git a/isso/utils/html.py b/isso/utils/html.py index fca3c7e..1f5f8cd 100644 --- a/isso/utils/html.py +++ b/isso/utils/html.py @@ -50,6 +50,11 @@ def sanitize(tokenizer, document): if HTML5LIB_VERSION > HTML5LIB_SIMPLETREE: builder = "etree" + + for link in domtree.findall(".//{http://www.w3.org/1999/xhtml}a"): + if link.get('href', None): + link.set("rel", "nofollow noopener") + else: builder = "simpletree" From ebca06059aef40c09485ecbabc6f012cea9d3634 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sat, 21 Apr 2018 14:46:32 +0200 Subject: [PATCH 08/30] api: add /feed API to get an Atom feed for an URI We need absolute URL at some places. We assume the first host configured is the base of the URI we have. Fix #81 --- docs/docs/extras/api.rst | 13 +++++ isso/db/comments.py | 6 +- isso/utils/__init__.py | 7 +++ isso/views/comments.py | 120 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/docs/docs/extras/api.rst b/docs/docs/extras/api.rst index 348a256..bcb374b 100644 --- a/docs/docs/extras/api.rst +++ b/docs/docs/extras/api.rst @@ -185,3 +185,16 @@ uri : returns an integer +Get Atom feed +------------- + +Get an Atom feed of comments for thread `uri`: + +.. code-block:: text + + GET /feed?uri=%2Fhello-world%2F + +uri : + URI to get comments for, required. + +Returns an XML document as the Atom feed. diff --git a/isso/db/comments.py b/isso/db/comments.py index 0f359a3..71a4aa5 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -159,7 +159,8 @@ class Comments: for item in rv: yield dict(zip(fields_comments + fields_threads, item)) - def fetch(self, uri, mode=5, after=0, parent='any', order_by='id', limit=None): + def fetch(self, uri, mode=5, after=0, parent='any', + order_by='id', asc=1, limit=None): """ Return comments for :param:`uri` with :param:`mode`. """ @@ -181,7 +182,8 @@ class Comments: order_by = 'id' sql.append('ORDER BY ') sql.append(order_by) - sql.append(' ASC') + if not asc: + sql.append(' DESC') if limit: sql.append('LIMIT ?') diff --git a/isso/utils/__init__.py b/isso/utils/__init__.py index a0a07bc..254aab1 100644 --- a/isso/utils/__init__.py +++ b/isso/utils/__init__.py @@ -132,3 +132,10 @@ class JSONResponse(Response): kwargs["content_type"] = "application/json" super(JSONResponse, self).__init__( json.dumps(obj).encode("utf-8"), *args, **kwargs) + + +class XMLResponse(Response): + def __init__(self, obj, *args, **kwargs): + kwargs["content_type"] = "text/xml" + super(XMLResponse, self).__init__( + obj, *args, **kwargs) diff --git a/isso/views/comments.py b/isso/views/comments.py index d3b80c3..ddc2b09 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -9,6 +9,7 @@ import functools from datetime import datetime, timedelta from itsdangerous import SignatureExpired, BadSignature +from xml.etree import ElementTree as ET from werkzeug.http import dump_cookie from werkzeug.wsgi import get_current_url @@ -20,11 +21,21 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound from isso.compat import text_type as str from isso import utils, local -from isso.utils import (http, parse, JSONResponse as JSON, +from isso.utils import (http, parse, + JSONResponse as JSON, XMLResponse as XML, render_template) from isso.views import requires from isso.utils.hash import sha1 +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse +try: + from StringIO import StringIO +except ImportError: + from io import BytesIO as StringIO + # from Django appearently, looks good to me *duck* __url_re = re.compile( r'^' @@ -91,6 +102,7 @@ class API(object): ('new', ('POST', '/new')), ('count', ('GET', '/count')), ('counts', ('POST', '/count')), + ('feed', ('GET', '/feed')), ('view', ('GET', '/id/')), ('edit', ('PUT', '/id/')), ('delete', ('DELETE', '/id/')), @@ -834,6 +846,112 @@ class API(object): return JSON(self.comments.count(*data), 200) + """ + @api {get} /feed Atom feed for comments + @apiGroup Thread + @apiDescription + Provide an Atom feed for the given thread. + """ + @requires(str, 'uri') + def feed(self, environ, request, uri): + args = { + 'uri': uri, + 'order_by': 'id', + 'asc': 0, + 'limit': 100 + } + try: + args['limit'] = max(int(request.args.get('limit')), args['limit']) + except TypeError: + pass + except ValueError: + return BadRequest("limit should be integer") + comments = self.comments.fetch(**args) + base = self.conf.get('host').split()[0] + hostname = urlparse(base).netloc + + # Let's build an Atom feed. + # RFC 4287: https://tools.ietf.org/html/rfc4287 + # RFC 4685: https://tools.ietf.org/html/rfc4685 (threading extensions) + # For IDs: http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id + feed = ET.Element('feed', { + 'xmlns': 'http://www.w3.org/2005/Atom', + 'xmlns:thr': 'http://purl.org/syndication/thread/1.0' + }) + + # For feed ID, we would use thread ID, but we may not have + # one. Therefore, we use the URI. We don't have a year + # either... + id = ET.SubElement(feed, 'id') + id.text = 'tag:{hostname},2018:/isso/thread{uri}'.format( + hostname=hostname, uri=uri) + + # For title, we don't have much either. Be pretty generic. + title = ET.SubElement(feed, 'title') + title.text = 'Comments for {hostname}{uri}'.format( + hostname=hostname, uri=uri) + + comment0 = None + + for comment in comments: + if comment0 is None: + comment0 = comment + + entry = ET.SubElement(feed, 'entry') + # We don't use a real date in ID either to help with + # threading. + id = ET.SubElement(entry, 'id') + id.text = 'tag:{hostname},2018:/isso/{tid}/{id}'.format( + hostname=hostname, + tid=comment['tid'], + id=comment['id']) + title = ET.SubElement(entry, 'title') + title.text = 'Comment #{}'.format(comment['id']) + updated = ET.SubElement(entry, 'updated') + updated.text = '{}Z'.format(datetime.fromtimestamp( + comment['modified'] or comment['created']).isoformat()) + author = ET.SubElement(entry, 'author') + name = ET.SubElement(author, 'name') + name.text = comment['author'] + ET.SubElement(entry, 'link', { + 'href': '{base}{uri}#isso-{id}'.format( + base=base, + uri=uri, id=comment['id']) + }) + content = ET.SubElement(entry, 'content', { + 'type': 'html', + }) + content.text = comment['text'] + + if comment['parent']: + ET.SubElement(entry, 'thr:in-reply-to', { + 'ref': 'tag:{hostname},2018:/isso/{tid}/{id}'.format( + hostname=hostname, + tid=comment['tid'], + id=comment['parent']), + 'href': '{base}{uri}#isso-{id}'.format( + base=base, + uri=uri, id=comment['parent']) + }) + + # Updated is mandatory. If we have comments, we use the date + # of last modification of the first one (which is the last + # one). Otherwise, we use a fixed date. + updated = ET.Element('updated') + if comment0 is None: + updated.text = '1970-01-01T01:00:00Z' + else: + updated.text = datetime.fromtimestamp( + comment0['modified'] or comment0['created']).isoformat() + updated.text += 'Z' + feed.insert(0, updated) + + output = StringIO() + ET.ElementTree(feed).write(output, + encoding='utf-8', + xml_declaration=True) + return XML(output.getvalue(), 200) + def preview(self, environment, request): data = request.get_json() From 5c6e78b9c14a41a3fff9c77b1d952ced962c1622 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 22 Apr 2018 12:17:31 +0200 Subject: [PATCH 09/30] api: ensure /feed is easily cacheable by issuing etag/last-modified headers --- isso/views/comments.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index ddc2b09..120fef3 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -950,7 +950,16 @@ class API(object): ET.ElementTree(feed).write(output, encoding='utf-8', xml_declaration=True) - return XML(output.getvalue(), 200) + response = XML(output.getvalue(), 200) + + # Add an etag/last-modified value for caching purpose + if comment0 is None: + response.set_etag('empty') + response.last_modified = 0 + else: + response.set_etag('{tid}-{id}'.format(**comment0)) + response.last_modified = comment0['modified'] or comment0['created'] + return response.make_conditional(request) def preview(self, environment, request): data = request.get_json() From a89debbc9cb136339585db59829907eed99db68f Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 22 Apr 2018 12:38:51 +0200 Subject: [PATCH 10/30] api: add a simple test for /feed endpoint --- isso/tests/test_comments.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index cc27ae8..a58ae62 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import os import json +import re import tempfile import unittest @@ -30,6 +31,7 @@ class TestComments(unittest.TestCase): fd, self.path = tempfile.mkstemp() conf = config.load(os.path.join(dist.location, "share", "isso.conf")) conf.set("general", "dbpath", self.path) + conf.set("general", "host", "https://example.org") conf.set("guard", "enabled", "off") conf.set("hash", "algorithm", "none") @@ -324,6 +326,28 @@ class TestComments(unittest.TestCase): self.assertListEqual(list(rv.keys()), []) + def testFeedEmpty(self): + rv = self.get('/feed?uri=%2Fpath%2Fnothing') + self.assertEqual(rv.status_code, 200) + self.assertEqual(rv.headers['ETag'], '"empty"') + data = rv.data.decode('utf-8') + self.assertEqual(data, """ +1970-01-01T01:00:00Ztag:example.org,2018:/isso/thread/path/nothingComments for example.org/path/nothing""") + + def testFeed(self): + self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First'})) + self.post('/new?uri=%2Fpath%2F', + data=json.dumps({'text': 'First', 'parent': 1})) + + rv = self.get('/feed?uri=%2Fpath%2F') + self.assertEqual(rv.status_code, 200) + self.assertEqual(rv.headers['ETag'], '"1-2"') + data = rv.data.decode('utf-8') + data = re.sub('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z', + '2018-04-01T10:00:00Z', data) + self.assertEqual(data, """ +2018-04-01T10:00:00Ztag:example.org,2018:/isso/thread/path/Comments for example.org/path/tag:example.org,2018:/isso/1/2Comment #22018-04-01T10:00:00ZFirsttag:example.org,2018:/isso/1/1Comment #12018-04-01T10:00:00ZFirst""") + def testCounts(self): self.assertEqual(self.get('/count?uri=%2Fpath%2F').status_code, 404) From bceb69518b587a51e0a268fda770df35e46a6960 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 22 Apr 2018 13:00:22 +0200 Subject: [PATCH 11/30] js: put a link to Atom feed on top of the main postbox --- isso/css/isso.css | 11 +++++++++++ isso/js/app/api.js | 7 ++++++- isso/js/app/i18n/en.js | 1 + isso/js/app/i18n/fr.js | 1 + isso/js/embed.js | 5 +++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/isso/css/isso.css b/isso/css/isso.css index 47cf675..2a55658 100644 --- a/isso/css/isso.css +++ b/isso/css/isso.css @@ -15,6 +15,14 @@ color: #555; font-weight: bold; } +#isso-thread > .isso-feedlink { + float: right; + padding-left: 1em; +} +#isso-thread > .isso-feedlink > a { + font-size: 0.8em; + vertical-align: bottom; +} #isso-thread .textarea { min-height: 58px; outline: 0; @@ -119,10 +127,12 @@ color: gray !important; clear: left; } +.isso-feedlink, .isso-comment > div.text-wrapper > .isso-comment-footer a { font-weight: bold; text-decoration: none; } +.isso-feedlink:hover, .isso-comment > div.text-wrapper > .isso-comment-footer a:hover { color: #111111 !important; text-shadow: #aaaaaa 0 0 1px !important; @@ -152,6 +162,7 @@ .isso-postbox { max-width: 68em; margin: 0 auto 2em; + clear: right; } .isso-postbox > .form-wrapper { display: block; diff --git a/isso/js/app/api.js b/isso/js/app/api.js index d0fbf2f..30cca9c 100644 --- a/isso/js/app/api.js +++ b/isso/js/app/api.js @@ -191,6 +191,10 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { return deferred.promise; }; + var feed = function(tid) { + return endpoint + "/feed?" + qs({uri: tid || location}); + } + return { endpoint: endpoint, salt: salt, @@ -202,6 +206,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { fetch: fetch, count: count, like: like, - dislike: dislike + dislike: dislike, + feed: feed }; }); diff --git a/isso/js/app/i18n/en.js b/isso/js/app/i18n/en.js index ec4b4d0..d3968b8 100644 --- a/isso/js/app/i18n/en.js +++ b/isso/js/app/i18n/en.js @@ -7,6 +7,7 @@ define({ "num-comments": "One Comment\n{{ n }} Comments", "no-comments": "No Comments Yet", + "atom-feed": "Atom feed", "comment-reply": "Reply", "comment-edit": "Edit", diff --git a/isso/js/app/i18n/fr.js b/isso/js/app/i18n/fr.js index e29d024..f65cc6a 100644 --- a/isso/js/app/i18n/fr.js +++ b/isso/js/app/i18n/fr.js @@ -6,6 +6,7 @@ define({ "postbox-submit": "Soumettre", "num-comments": "{{ n }} commentaire\n{{ n }} commentaires", "no-comments": "Aucun commentaire pour l'instant", + "atom-feed": "Flux Atom", "comment-reply": "Répondre", "comment-edit": "Éditer", "comment-save": "Enregistrer", diff --git a/isso/js/embed.js b/isso/js/embed.js index 680880b..715413c 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -27,6 +27,11 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/ return console.log("abort, #isso-thread is missing"); } + var feedLink = $.new('a', i18n.translate('atom-feed')); + var feedLinkWrapper = $.new('span.isso-feedlink'); + feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id")); + feedLinkWrapper.append(feedLink); + $("#isso-thread").append(feedLinkWrapper); $("#isso-thread").append($.new('h4')); $("#isso-thread").append(new isso.Postbox(null)); $("#isso-thread").append('
'); From 45f6b1eda382f431d75a27e6bccf6b0fc9b9d240 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 22 Apr 2018 16:55:06 +0200 Subject: [PATCH 12/30] feed: make /feed API call configurable server and client-side On server-side, this can be enabled by providing a base URL to use to build the full URL. Limit also becomes configurable. On client-side, we need to add a switch to know whatever or not the additional link can be displayed. --- docs/docs/configuration/client.rst | 10 +++++++++- docs/docs/configuration/server.rst | 21 +++++++++++++++++++++ isso/js/app/config.js | 3 ++- isso/js/embed.js | 12 +++++++----- isso/tests/test_comments.py | 10 +++++++++- isso/views/comments.py | 8 ++++++-- share/isso.conf | 12 ++++++++++++ 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/docs/docs/configuration/client.rst b/docs/docs/configuration/client.rst index 9328357..779f47e 100644 --- a/docs/docs/configuration/client.rst +++ b/docs/docs/configuration/client.rst @@ -20,7 +20,8 @@ preferably in the script tag which embeds the JS: data-isso-avatar-bg="#f0f0f0" data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..." data-isso-vote="true" - data-vote-levels="" + data-isso-vote-levels="" + data-isso-feed="false" src="/prefix/js/embed.js"> Furthermore you can override the automatic title detection inside @@ -125,3 +126,10 @@ For example, the value `"-5,5"` will cause each `isso-comment` to be given one o - `isso-vote-level-2` for scores of `5` and greater These classes can then be used to customize the appearance of comments (eg. put a star on popular comments) + +data-isso-feed +-------------- + +Enable or disable the addition of a link to the feed for the comment +thread. The link will only be valid if the appropriate setting, in +``[rss]`` section, is also enabled server-side. diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index 1a443f8..5327c96 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -308,6 +308,27 @@ algorithm Arguments have to be in that order, but can be reduced to `pbkdf2:4096` for example to override the iterations only. +.. _configure-rss: + +RSS +--- + +Isso can provide an Atom feed for each comment thread. Users can use +them to subscribe to comments and be notified of changes. Atom feeds +are enabled as soon as there is a base URL defined in this section. + +.. code-block:: ini + + [rss] + base = + limit = 100 + +base + base URL to use to build complete URI to pages (by appending the URI from Isso) + +limit + number of most recent comments to return for a thread + Appendum -------- diff --git a/isso/js/app/config.js b/isso/js/app/config.js index 952d588..258da6d 100644 --- a/isso/js/app/config.js +++ b/isso/js/app/config.js @@ -15,7 +15,8 @@ define(function() { "avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6", "#be5168", "#f19670", "#e4bf80", "#447c69"].join(" "), "vote": true, - "vote-levels": null + "vote-levels": null, + "feed": false }; var js = document.getElementsByTagName("script"); diff --git a/isso/js/embed.js b/isso/js/embed.js index 715413c..a0a53da 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -27,11 +27,13 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/ return console.log("abort, #isso-thread is missing"); } - var feedLink = $.new('a', i18n.translate('atom-feed')); - var feedLinkWrapper = $.new('span.isso-feedlink'); - feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id")); - feedLinkWrapper.append(feedLink); - $("#isso-thread").append(feedLinkWrapper); + if (config["feed"]) { + var feedLink = $.new('a', i18n.translate('atom-feed')); + var feedLinkWrapper = $.new('span.isso-feedlink'); + feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id")); + feedLinkWrapper.append(feedLink); + $("#isso-thread").append(feedLinkWrapper); + } $("#isso-thread").append($.new('h4')); $("#isso-thread").append(new isso.Postbox(null)); $("#isso-thread").append('
'); diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index a58ae62..4ba67c8 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -31,9 +31,9 @@ class TestComments(unittest.TestCase): fd, self.path = tempfile.mkstemp() conf = config.load(os.path.join(dist.location, "share", "isso.conf")) conf.set("general", "dbpath", self.path) - conf.set("general", "host", "https://example.org") conf.set("guard", "enabled", "off") conf.set("hash", "algorithm", "none") + self.conf = conf class App(Isso, core.Mixin): pass @@ -326,7 +326,13 @@ class TestComments(unittest.TestCase): self.assertListEqual(list(rv.keys()), []) + def testNoFeed(self): + rv = self.get('/feed?uri=%2Fpath%2Fnothing') + self.assertEqual(rv.status_code, 404) + def testFeedEmpty(self): + self.conf.set("rss", "base", "https://example.org") + rv = self.get('/feed?uri=%2Fpath%2Fnothing') self.assertEqual(rv.status_code, 200) self.assertEqual(rv.headers['ETag'], '"empty"') @@ -335,6 +341,8 @@ class TestComments(unittest.TestCase): 1970-01-01T01:00:00Ztag:example.org,2018:/isso/thread/path/nothingComments for example.org/path/nothing""") def testFeed(self): + self.conf.set("rss", "base", "https://example.org") + self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First'})) self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First', 'parent': 1})) diff --git a/isso/views/comments.py b/isso/views/comments.py index 120fef3..87cf48d 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -854,11 +854,15 @@ class API(object): """ @requires(str, 'uri') def feed(self, environ, request, uri): + conf = self.isso.conf.section("rss") + if not conf.get('base'): + raise NotFound + args = { 'uri': uri, 'order_by': 'id', 'asc': 0, - 'limit': 100 + 'limit': conf.getint('limit') } try: args['limit'] = max(int(request.args.get('limit')), args['limit']) @@ -867,7 +871,7 @@ class API(object): except ValueError: return BadRequest("limit should be integer") comments = self.comments.fetch(**args) - base = self.conf.get('host').split()[0] + base = conf.get('base') hostname = urlparse(base).netloc # Let's build an Atom feed. diff --git a/share/isso.conf b/share/isso.conf index 883e2b1..d429215 100644 --- a/share/isso.conf +++ b/share/isso.conf @@ -180,3 +180,15 @@ salt = Eech7co8Ohloopo9Ol6baimi # strengthening. Arguments have to be in that order, but can be reduced to # pbkdf2:4096 for example to override the iterations only. algorithm = pbkdf2 + + +[rss] +# Provide an Atom feed for each comment thread for users to subscribe to. + +# The base URL of pages is needed to build the Atom feed. By appending +# the URI, we should get the complete URL to use to access the page +# with the comments. When empty, Atom feeds are disabled. +base = + +# Limit the number of elements to return for each thread. +limit = 100 From 01cf96eeb7e28218c0145071f027f7d0957f79fc Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Mon, 23 Apr 2018 11:33:47 +0200 Subject: [PATCH 13/30] feed: ensure Markdown rendering is applied to feeds --- isso/tests/test_comments.py | 4 ++-- isso/views/comments.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index 4ba67c8..2ffe2ad 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -345,7 +345,7 @@ class TestComments(unittest.TestCase): self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First'})) self.post('/new?uri=%2Fpath%2F', - data=json.dumps({'text': 'First', 'parent': 1})) + data=json.dumps({'text': '*Second*', 'parent': 1})) rv = self.get('/feed?uri=%2Fpath%2F') self.assertEqual(rv.status_code, 200) @@ -354,7 +354,7 @@ class TestComments(unittest.TestCase): data = re.sub('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z', '2018-04-01T10:00:00Z', data) self.assertEqual(data, """ -2018-04-01T10:00:00Ztag:example.org,2018:/isso/thread/path/Comments for example.org/path/tag:example.org,2018:/isso/1/2Comment #22018-04-01T10:00:00ZFirsttag:example.org,2018:/isso/1/1Comment #12018-04-01T10:00:00ZFirst""") +2018-04-01T10:00:00Ztag:example.org,2018:/isso/thread/path/Comments for example.org/path/tag:example.org,2018:/isso/1/2Comment #22018-04-01T10:00:00Z<p><em>Second</em></p>tag:example.org,2018:/isso/1/1Comment #12018-04-01T10:00:00Z<p>First</p>""") def testCounts(self): diff --git a/isso/views/comments.py b/isso/views/comments.py index 87cf48d..2e8b4f3 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -925,7 +925,7 @@ class API(object): content = ET.SubElement(entry, 'content', { 'type': 'html', }) - content.text = comment['text'] + content.text = self.isso.render(comment['text']) if comment['parent']: ET.SubElement(entry, 'thr:in-reply-to', { From b0264bc807454765a1d7df96b4dfe9e8f9ab74cb Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 20 Apr 2018 21:27:50 +0200 Subject: [PATCH 14/30] js: add a preview button to see a rendered preview When the button is clicked, the /preview endpoint is used to render a preview text. The preview is inserted and the preview button is replaced by an edit button to go back to edit mode. Alternatively, the use can click on the preview to edit. Some small CSS modifications are done to accomodate the modification. Also, the preview is wrapped into `.isso-common .text-wrapper .text` to not make the CSS more complex. When in preview mode, the background is stripped/greyish in case it's not easy to make a difference between preview and not preview (due to unformatted text). We avoid to modify borders/shadow boxes because it would make the design "jumpy". --- isso/css/isso.css | 46 ++++++++++++++++++++++++++--------- isso/js/app/api.js | 19 +++++++++++++-- isso/js/app/i18n/bg.js | 2 ++ isso/js/app/i18n/cs.js | 2 ++ isso/js/app/i18n/da.js | 2 ++ isso/js/app/i18n/de.js | 2 ++ isso/js/app/i18n/el_GR.js | 2 ++ isso/js/app/i18n/en.js | 2 ++ isso/js/app/i18n/eo.js | 2 ++ isso/js/app/i18n/es.js | 2 ++ isso/js/app/i18n/fa.js | 2 ++ isso/js/app/i18n/fi.js | 2 ++ isso/js/app/i18n/fr.js | 2 ++ isso/js/app/i18n/hr.js | 2 ++ isso/js/app/i18n/hu.js | 2 ++ isso/js/app/i18n/it.js | 2 ++ isso/js/app/i18n/nl.js | 2 ++ isso/js/app/i18n/pl.js | 2 ++ isso/js/app/i18n/ru.js | 2 ++ isso/js/app/i18n/sv.js | 2 ++ isso/js/app/i18n/vi.js | 2 ++ isso/js/app/i18n/zh_CN.js | 2 ++ isso/js/app/i18n/zh_TW.js | 2 ++ isso/js/app/isso.js | 21 +++++++++++++++- isso/js/app/text/postbox.jade | 10 ++++++++ 25 files changed, 124 insertions(+), 14 deletions(-) diff --git a/isso/css/isso.css b/isso/css/isso.css index 02f2a93..c0716f8 100644 --- a/isso/css/isso.css +++ b/isso/css/isso.css @@ -31,12 +31,16 @@ color: #757575; } -.isso-comment { +#isso-root .isso-comment { max-width: 68em; padding-top: 0.95em; margin: 0.95em auto; } -.isso-comment:not(:first-of-type), +#isso-root .preview .isso-comment { + padding-top: 0; + margin: 0; +} +#isso-root .isso-comment:not(:first-of-type), .isso-follow-up .isso-comment { border-top: 1px solid rgba(0, 0, 0, 0.1); } @@ -89,7 +93,8 @@ font-weight: bold; color: #555; } -.isso-comment > div.text-wrapper > .textarea-wrapper .textarea { +.isso-comment > div.text-wrapper > .textarea-wrapper .textarea, +.isso-comment > div.text-wrapper > .textarea-wrapper .preview { margin-top: 0.2em; } .isso-comment > div.text-wrapper > div.text p { @@ -107,7 +112,8 @@ font-size: 130%; font-weight: bold; } -.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea { +.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea, +.isso-comment > div.text-wrapper > div.textarea-wrapper .preview { width: 100%; border: 1px solid #f0f0f0; border-radius: 2px; @@ -163,7 +169,8 @@ .isso-postbox > .form-wrapper > .auth-section .post-action { display: block; } -.isso-postbox > .form-wrapper .textarea { +.isso-postbox > .form-wrapper .textarea, +.isso-postbox > .form-wrapper .preview { margin: 0 0 .3em; padding: .4em .8em; border-radius: 3px; @@ -193,7 +200,7 @@ .isso-postbox > .form-wrapper > .auth-section .post-action { display: inline-block; float: right; - margin: 0; + margin: 0 0 0 5px; } .isso-postbox > .form-wrapper > .auth-section .post-action > input { padding: calc(.3em - 1px); @@ -211,6 +218,28 @@ .isso-postbox > .form-wrapper > .auth-section .post-action > input:active { background-color: #BBB; } +.isso-postbox > .form-wrapper .preview, +.isso-postbox > .form-wrapper input[name="edit"], +.isso-postbox.preview-mode > .form-wrapper input[name="preview"], +.isso-postbox.preview-mode > .form-wrapper .textarea { + display: none; +} +.isso-postbox.preview-mode > .form-wrapper .preview { + display: block; +} +.isso-postbox.preview-mode > .form-wrapper input[name="edit"] { + display: inline; +} +.isso-postbox > .form-wrapper .preview { + background-color: #f8f8f8; + background: repeating-linear-gradient( + -45deg, + #f8f8f8, + #f8f8f8 10px, + #fff 10px, + #fff 20px + ); +} @media screen and (max-width:600px) { .isso-postbox > .form-wrapper > .auth-section .input-wrapper { display: block; @@ -220,9 +249,4 @@ .isso-postbox > .form-wrapper > .auth-section .input-wrapper input { width: 100%; } - .isso-postbox > .form-wrapper > .auth-section .post-action { - display: block; - float: none; - text-align: right; - } } diff --git a/isso/js/app/api.js b/isso/js/app/api.js index 30cca9c..1496892 100644 --- a/isso/js/app/api.js +++ b/isso/js/app/api.js @@ -191,9 +191,23 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { return deferred.promise; }; + var feed = function(tid) { return endpoint + "/feed?" + qs({uri: tid || location}); - } + }; + + var preview = function(text) { + var deferred = Q.defer(); + curl("POST", endpoint + "/preview", JSON.stringify({text: text}), + function(rv) { + if (rv.status === 200) { + deferred.resolve(JSON.parse(rv.body).text); + } else { + deferred.reject(rv.body); + } + }); + return deferred.promise; + }; return { endpoint: endpoint, @@ -207,6 +221,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { count: count, like: like, dislike: dislike, - feed: feed + feed: feed, + preview: preview }; }); diff --git a/isso/js/app/i18n/bg.js b/isso/js/app/i18n/bg.js index 45ac24b..a13ff09 100644 --- a/isso/js/app/i18n/bg.js +++ b/isso/js/app/i18n/bg.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Име/псевдоним (незадължително)", "postbox-email": "Ел. поща (незадължително)", "postbox-website": "Уебсайт (незадължително)", + "postbox-preview": "преглед", + "postbox-edit": "Редактиране", "postbox-submit": "Публикуване", "num-comments": "1 коментар\n{{ n }} коментара", "no-comments": "Все още няма коментари", diff --git a/isso/js/app/i18n/cs.js b/isso/js/app/i18n/cs.js index 77e6401..11dfcbb 100644 --- a/isso/js/app/i18n/cs.js +++ b/isso/js/app/i18n/cs.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Jméno (nepovinné)", "postbox-email": "E-mail (nepovinný)", "postbox-website": "Web (nepovinný)", + "postbox-preview": "Náhled", + "postbox-edit": "Upravit", "postbox-submit": "Publikovat", "num-comments": "Jeden komentář\n{{ n }} Komentářů", "no-comments": "Zatím bez komentářů", diff --git a/isso/js/app/i18n/da.js b/isso/js/app/i18n/da.js index eb64fc4..d97cd4c 100644 --- a/isso/js/app/i18n/da.js +++ b/isso/js/app/i18n/da.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Name (optional)", "postbox-email": "E-mail (optional)", "postbox-website": "Website (optional)", + "postbox-preview": "Eksempel", + "postbox-edit": "Rediger", "postbox-submit": "Submit", "num-comments": "One Comment\n{{ n }} Comments", diff --git a/isso/js/app/i18n/de.js b/isso/js/app/i18n/de.js index f7def26..04e1739 100644 --- a/isso/js/app/i18n/de.js +++ b/isso/js/app/i18n/de.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Name (optional)", "postbox-email": "Email (optional)", "postbox-website": "Website (optional)", + "postbox-preview": "Vorschau", + "postbox-edit": "Bearbeiten", "postbox-submit": "Abschicken", "num-comments": "1 Kommentar\n{{ n }} Kommentare", "no-comments": "Bisher keine Kommentare", diff --git a/isso/js/app/i18n/el_GR.js b/isso/js/app/i18n/el_GR.js index 5155a2d..db181cc 100644 --- a/isso/js/app/i18n/el_GR.js +++ b/isso/js/app/i18n/el_GR.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Όνομα (προαιρετικό)", "postbox-email": "E-mail (προαιρετικό)", "postbox-website": "Ιστοσελίδα (προαιρετικό)", + "postbox-preview": "Πρεμιέρα", + "postbox-edit": "Επεξεργασία", "postbox-submit": "Υποβολή", "num-comments": "Ένα σχόλιο\n{{ n }} σχόλια", "no-comments": "Δεν υπάρχουν σχόλια", diff --git a/isso/js/app/i18n/en.js b/isso/js/app/i18n/en.js index d3968b8..fd110fe 100644 --- a/isso/js/app/i18n/en.js +++ b/isso/js/app/i18n/en.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Name (optional)", "postbox-email": "E-mail (optional)", "postbox-website": "Website (optional)", + "postbox-preview": "Preview", + "postbox-edit": "Edit", "postbox-submit": "Submit", "num-comments": "One Comment\n{{ n }} Comments", diff --git a/isso/js/app/i18n/eo.js b/isso/js/app/i18n/eo.js index 76150f3..e9ee6c6 100644 --- a/isso/js/app/i18n/eo.js +++ b/isso/js/app/i18n/eo.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Nomo (malnepra)", "postbox-email": "Retadreso (malnepra)", "postbox-website": "Retejo (malnepra)", + "postbox-preview": "Antaŭrigardo", + "postbox-edit": "Redaktu", "postbox-submit": "Sendu", "num-comments": "{{ n }} komento\n{{ n }} komentoj", "no-comments": "Neniu komento ankoraŭ", diff --git a/isso/js/app/i18n/es.js b/isso/js/app/i18n/es.js index c25d6cd..565ef14 100644 --- a/isso/js/app/i18n/es.js +++ b/isso/js/app/i18n/es.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Nombre (opcional)", "postbox-email": "E-mail (opcional)", "postbox-website": "Sitio web (opcional)", + "postbox-preview": "Avance", + "postbox-edit": "Editar", "postbox-submit": "Enviar", "num-comments": "Un Comentario\n{{ n }} Comentarios", "no-comments": "Sin Comentarios Todavía", diff --git a/isso/js/app/i18n/fa.js b/isso/js/app/i18n/fa.js index c323778..9b6da58 100644 --- a/isso/js/app/i18n/fa.js +++ b/isso/js/app/i18n/fa.js @@ -3,6 +3,8 @@ define({ "postbox-author": "اسم (اختیاری)", "postbox-email": "ایمیل (اختیاری)", "postbox-website": "سایت (اختیاری)", + "postbox-preview": "پیشنمایش", + "postbox-edit": "ویرایش", "postbox-submit": "ارسال", "num-comments": "یک نظر\n{{ n }} نظر", diff --git a/isso/js/app/i18n/fi.js b/isso/js/app/i18n/fi.js index 80b6316..4def698 100644 --- a/isso/js/app/i18n/fi.js +++ b/isso/js/app/i18n/fi.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Nimi (valinnainen)", "postbox-email": "Sähköposti (valinnainen)", "postbox-website": "Web-sivu (valinnainen)", + "postbox-preview": "Esikatselu", + "postbox-edit": "Muokkaa", "postbox-submit": "Lähetä", "num-comments": "Yksi kommentti\n{{ n }} kommenttia", diff --git a/isso/js/app/i18n/fr.js b/isso/js/app/i18n/fr.js index f65cc6a..32e0ed6 100644 --- a/isso/js/app/i18n/fr.js +++ b/isso/js/app/i18n/fr.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Nom (optionnel)", "postbox-email": "Courriel (optionnel)", "postbox-website": "Site web (optionnel)", + "postbox-preview": "Aperçu", + "postbox-edit": "Éditer", "postbox-submit": "Soumettre", "num-comments": "{{ n }} commentaire\n{{ n }} commentaires", "no-comments": "Aucun commentaire pour l'instant", diff --git a/isso/js/app/i18n/hr.js b/isso/js/app/i18n/hr.js index 1ae6452..84c31f9 100644 --- a/isso/js/app/i18n/hr.js +++ b/isso/js/app/i18n/hr.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Ime (neobavezno)", "postbox-email": "E-mail (neobavezno)", "postbox-website": "Web stranica (neobavezno)", + "postbox-preview": "Pregled", + "postbox-edit": "Uredi", "postbox-submit": "Pošalji", "num-comments": "Jedan komentar\n{{ n }} komentara", "no-comments": "Još nema komentara", diff --git a/isso/js/app/i18n/hu.js b/isso/js/app/i18n/hu.js index e0bf7d6..e06c513 100644 --- a/isso/js/app/i18n/hu.js +++ b/isso/js/app/i18n/hu.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Név (nem kötelező)", "postbox-email": "Email (nem kötelező)", "postbox-website": "Website (nem kötelező)", + "postbox-preview": "Előnézet", + "postbox-edit": "Szerekesztés", "postbox-submit": "Elküld", "num-comments": "Egy hozzászólás\n{{ n }} hozzászólás", "no-comments": "Eddig nincs hozzászólás", diff --git a/isso/js/app/i18n/it.js b/isso/js/app/i18n/it.js index 31eeb2c..f193f95 100644 --- a/isso/js/app/i18n/it.js +++ b/isso/js/app/i18n/it.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Nome (opzionale)", "postbox-email": "E-mail (opzionale)", "postbox-website": "Sito web (opzionale)", + "postbox-preview": "Anteprima", + "postbox-edit": "Modifica", "postbox-submit": "Invia", "num-comments": "Un Commento\n{{ n }} Commenti", "no-comments": "Ancora Nessun Commento", diff --git a/isso/js/app/i18n/nl.js b/isso/js/app/i18n/nl.js index 04164b6..107a882 100644 --- a/isso/js/app/i18n/nl.js +++ b/isso/js/app/i18n/nl.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Naam (optioneel)", "postbox-email": "E-mail (optioneel)", "postbox-website": "Website (optioneel)", + "postbox-preview": "Voorbeeld", + "postbox-edit": "Bewerken", "postbox-submit": "Versturen", "num-comments": "Één reactie\n{{ n }} reacties", "no-comments": "Nog geen reacties", diff --git a/isso/js/app/i18n/pl.js b/isso/js/app/i18n/pl.js index d9afe7d..bba7bac 100644 --- a/isso/js/app/i18n/pl.js +++ b/isso/js/app/i18n/pl.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Imię/nick (opcjonalnie)", "postbox-email": "E-mail (opcjonalnie)", "postbox-website": "Strona (opcjonalnie)", + "postbox-preview": "Visualizar", + "postbox-edit": "Edytuj", "postbox-submit": "Wyślij", "num-comments": "Jeden komentarz\n{{ n }} komentarzy", "no-comments": "Jeszcze nie ma komentarzy", diff --git a/isso/js/app/i18n/ru.js b/isso/js/app/i18n/ru.js index a5af03e..662e825 100644 --- a/isso/js/app/i18n/ru.js +++ b/isso/js/app/i18n/ru.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Имя (необязательно)", "postbox-email": "Email (необязательно)", "postbox-website": "Сайт (необязательно)", + "postbox-preview": "анонс", + "postbox-edit": "Правка", "postbox-submit": "Отправить", "num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев", "no-comments": "Пока нет комментариев", diff --git a/isso/js/app/i18n/sv.js b/isso/js/app/i18n/sv.js index cafbdda..a1b50a3 100644 --- a/isso/js/app/i18n/sv.js +++ b/isso/js/app/i18n/sv.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Namn (frivilligt)", "postbox-email": "E-mail (frivilligt)", "postbox-website": "Hemsida (frivilligt)", + "postbox-preview": "Förhandsvisning", + "postbox-edit": "Redigera", "postbox-submit": "Skicka", "num-comments": "En kommentar\n{{ n }} kommentarer", "no-comments": "Inga kommentarer än", diff --git a/isso/js/app/i18n/vi.js b/isso/js/app/i18n/vi.js index 72a3092..6b54d23 100644 --- a/isso/js/app/i18n/vi.js +++ b/isso/js/app/i18n/vi.js @@ -3,6 +3,8 @@ define({ "postbox-author": "Tên (tùy chọn)", "postbox-email": "E-mail (tùy chọn)", "postbox-website": "Website (tùy chọn)", + "postbox-preview": "Xem trước", + "postbox-edit": "Sửa", "postbox-submit": "Gửi", "num-comments": "Một bình luận\n{{ n }} bình luận", diff --git a/isso/js/app/i18n/zh_CN.js b/isso/js/app/i18n/zh_CN.js index 9c33957..1bd1801 100644 --- a/isso/js/app/i18n/zh_CN.js +++ b/isso/js/app/i18n/zh_CN.js @@ -3,6 +3,8 @@ define({ "postbox-author": "名字 (可选)", "postbox-email": "E-mail (可选)", "postbox-website": "网站 (可选)", + "postbox-preview": "预习", + "postbox-edit": "编辑", "postbox-submit": "提交", "num-comments": "1 条评论\n{{ n }} 条评论", diff --git a/isso/js/app/i18n/zh_TW.js b/isso/js/app/i18n/zh_TW.js index 68ad370..7bdf7e5 100644 --- a/isso/js/app/i18n/zh_TW.js +++ b/isso/js/app/i18n/zh_TW.js @@ -3,6 +3,8 @@ define({ "postbox-author": "名稱 (非必填)", "postbox-email": "電子信箱 (非必填)", "postbox-website": "個人網站 (非必填)", + "postbox-preview": "預習", + "postbox-edit": "編輯", "postbox-submit": "送出", "num-comments": "1 則留言\n{{ n }} 則留言", diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index e779a76..1d3fc5d 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -11,7 +11,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", el = $.htmlify(jade.render("postbox", { "author": JSON.parse(localStorage.getItem("author")), "email": JSON.parse(localStorage.getItem("email")), - "website": JSON.parse(localStorage.getItem("website")) + "website": JSON.parse(localStorage.getItem("website")), + "preview": '' })); // callback on success (e.g. to toggle the reply button) @@ -51,9 +52,27 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", $("[name='author']", el).placeholder.replace(/ \(.*\)/, ""); } + // preview function + $("[name='preview']", el).on("click", function() { + api.preview(utils.text($(".textarea", el).innerHTML)).then( + function(html) { + $(".preview .text", el).innerHTML = html; + el.classList.add('preview-mode'); + }); + }); + + // edit function + var edit = function() { + $(".preview .text", el).innerHTML = ''; + el.classList.remove('preview-mode'); + }; + $("[name='edit']", el).on("click", edit); + $(".preview", el).on("click", edit); + // submit form, initialize optional fields with `null` and reset form. // If replied to a comment, remove form completely. $("[type=submit]", el).on("click", function() { + edit(); if (! el.validate()) { return; } diff --git a/isso/js/app/text/postbox.jade b/isso/js/app/text/postbox.jade index 0a85ae1..908326b 100644 --- a/isso/js/app/text/postbox.jade +++ b/isso/js/app/text/postbox.jade @@ -3,6 +3,10 @@ div(class='isso-postbox') div(class='textarea-wrapper') div(class='textarea placeholder' contenteditable='true') = i18n('postbox-text') + div(class='preview') + div(class='isso-comment') + div(class='text-wrapper') + div(class='text') section(class='auth-section') p(class='input-wrapper') input(type='text' name='author' placeholder=i18n('postbox-author') @@ -15,3 +19,9 @@ div(class='isso-postbox') value=website != null ? '#{website}' : '') p(class='post-action') input(type='submit' value=i18n('postbox-submit')) + p(class='post-action') + input(type='button' name='preview' + value=i18n('postbox-preview')) + p(class='post-action') + input(type='button' name='edit' + value=i18n('postbox-edit')) From 501bdc59bf5643a3a273e1468cca092e2603d9c2 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Wed, 25 Apr 2018 10:55:37 +0200 Subject: [PATCH 15/30] Bugfix to support elements in SVG diagrams - close #421 --- isso/js/app/count.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/js/app/count.js b/isso/js/app/count.js index a1fff4b..58eb230 100644 --- a/isso/js/app/count.js +++ b/isso/js/app/count.js @@ -4,7 +4,7 @@ define(["app/api", "app/dom", "app/i18n"], function(api, $, i18n) { var objs = {}; $.each("a", function(el) { - if (! el.href.match(/#isso-thread$/)) { + if (! el.href.match || ! el.href.match(/#isso-thread$/)) { return; } From 3d648c2d290941f75ab730ab1990e9df311889b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 25 Apr 2018 21:20:42 +0200 Subject: [PATCH 16/30] fix tests due to non predictive html rendering --- isso/tests/test_html.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/isso/tests/test_html.py b/isso/tests/test_html.py index 327357c..42baa82 100644 --- a/isso/tests/test_html.py +++ b/isso/tests/test_html.py @@ -65,13 +65,17 @@ class TestHTML(unittest.TestCase): examples = [ ('Look: ', 'Look: '), ('Ha', - 'Ha'), + ['Ha', + 'Ha']), ('Ha', 'Ha'), ('

Test

', '

Test

'), ('', 'alert("Onoe")')] for (input, expected) in examples: - self.assertEqual(html.sanitize(sanitizer, input), expected) + if isinstance(expected, list): + self.assertIn(html.sanitize(sanitizer, input), expected) + else: + self.assertEqual(html.sanitize(sanitizer, input), expected) @unittest.skipIf(html.HTML5LIB_VERSION <= html.HTML5LIB_SIMPLETREE, "backport") def test_sanitizer_extensions(self): @@ -92,5 +96,6 @@ class TestHTML(unittest.TestCase): } }) renderer = html.Markup(conf.section("markup")).render - self.assertEqual(renderer("http://example.org/ and sms:+1234567890"), - '

http://example.org/ and sms:+1234567890

') + self.assertIn(renderer("http://example.org/ and sms:+1234567890"), + ['

http://example.org/ and sms:+1234567890

', + '

http://example.org/ and sms:+1234567890

']) From c14f639e755c88d498cf51736badd5dbd4f10f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 25 Apr 2018 22:24:05 +0200 Subject: [PATCH 17/30] add: CONTRIBUTORS update --- CONTRIBUTORS.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ad1fc1e..4ed0265 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -78,6 +78,9 @@ In chronological order: * @vincentbernat * Added documentation about data-isso-id attribute (overriding the standard isso-thread-id) * Added multi-staged Dockerfile + * Added atom feed + * Added a nofollow/noopener on links inside comments to protect against bots + * Added a preview using the existing preview endpoint * @p-vitt & @M4a1x * Documentation on troubleshooting for uberspace users From 466654b50d3d97bf1046cddc56a290507c5251b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 25 Apr 2018 22:31:01 +0200 Subject: [PATCH 18/30] update CONTRIBUTORS --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4ed0265..576f9cb 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -68,6 +68,7 @@ In chronological order: * Lucas Cimon @Lucas-C * Added the possibility to define CORS origins through ISSO_CORS_ORIGIN environment variable + * Fix a bug with in * Yuchen Pei @ycpei * Fix link in moderation emails when isso is installed in a sub URL From 94d031444d5e38d3e76b7a95d1e9aaf4725b42ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 25 Apr 2018 23:03:55 +0200 Subject: [PATCH 19/30] add: CONTRIBUTORS + pep8 --- CONTRIBUTORS.txt | 3 +++ isso/migrate.py | 2 +- isso/tests/test_comments.py | 8 ++++---- isso/utils/__init__.py | 6 +++--- isso/utils/hash.py | 2 +- isso/views/comments.py | 3 ++- isso/wsgi.py | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 576f9cb..10537ff 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -86,5 +86,8 @@ In chronological order: * @p-vitt & @M4a1x * Documentation on troubleshooting for uberspace users +* @benjhess + * Optionnal gravatar support + * [Your name or handle] <[email or website]> * [Brief summary of your changes] diff --git a/isso/migrate.py b/isso/migrate.py index f6297b7..2296e3c 100644 --- a/isso/migrate.py +++ b/isso/migrate.py @@ -55,7 +55,7 @@ class Progress(object): if time() - self.last > 0.2: sys.stdout.write("\r{0}".format(" " * cols)) - sys.stdout.write("\r[{0:.0%}] {1}".format(i/self.end, message)) + sys.stdout.write("\r[{0:.0%}] {1}".format(i / self.end, message)) sys.stdout.flush() self.last = time() diff --git a/isso/tests/test_comments.py b/isso/tests/test_comments.py index a4ae5a5..fa32039 100644 --- a/isso/tests/test_comments.py +++ b/isso/tests/test_comments.py @@ -134,13 +134,13 @@ class TestComments(unittest.TestCase): self.assertFalse(verify({"text": text})) # email/website length - self.assertTrue(verify({"text": "...", "email": "*"*254})) + self.assertTrue(verify({"text": "...", "email": "*" * 254})) self.assertTrue( - verify({"text": "...", "website": "google.de/" + "a"*128})) + verify({"text": "...", "website": "google.de/" + "a" * 128})) - self.assertFalse(verify({"text": "...", "email": "*"*1024})) + self.assertFalse(verify({"text": "...", "email": "*" * 1024})) self.assertFalse( - verify({"text": "...", "website": "google.de/" + "*"*1024})) + verify({"text": "...", "website": "google.de/" + "*" * 1024})) # valid website url self.assertTrue(comments.isurl("example.tld")) diff --git a/isso/utils/__init__.py b/isso/utils/__init__.py index 254aab1..ed925a9 100644 --- a/isso/utils/__init__.py +++ b/isso/utils/__init__.py @@ -36,7 +36,7 @@ def anonymize(remote_addr): ipv6 = ipaddress.IPv6Address(remote_addr) if ipv6.ipv4_mapped is not None: return anonymize(ipv6.ipv4_mapped) - return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000']*5) + return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000'] * 5) except ipaddress.AddressValueError: return u'0.0.0.0' @@ -89,11 +89,11 @@ class Bloomfilter: def add(self, key): for i in self.get_probes(key): - self.array[i//8] |= 2 ** (i % 8) + self.array[i // 8] |= 2 ** (i % 8) self.elements += 1 def __contains__(self, key): - return all(self.array[i//8] & (2 ** (i % 8)) for i in self.get_probes(key)) + return all(self.array[i // 8] & (2 ** (i % 8)) for i in self.get_probes(key)) def __len__(self): return self.elements diff --git a/isso/utils/hash.py b/isso/utils/hash.py index d964036..93a084e 100644 --- a/isso/utils/hash.py +++ b/isso/utils/hash.py @@ -110,4 +110,4 @@ def new(conf): sha1 = Hash(func="sha1").uhash -md5 = Hash(func="md5").uhash \ No newline at end of file +md5 = Hash(func="md5").uhash diff --git a/isso/views/comments.py b/isso/views/comments.py index a6f39fc..b76be46 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -732,6 +732,7 @@ class API(object): comment['replies'] = self._process_fetched_list(replies, plain) return JSON(rv, 200) + def _add_gravatar_image(self, item): if not self.conf.getboolean('gravatar'): return item @@ -743,6 +744,7 @@ class API(object): item['gravatar_image'] = gravatar_url.format(email_md5_hash) return item + def _process_fetched_list(self, fetched_list, plain=False): for item in fetched_list: @@ -851,7 +853,6 @@ class API(object): @apiSuccessExample Counts of 5 threads: [2, 18, 4, 0, 3] """ - def counts(self, environ, request): data = request.get_json() diff --git a/isso/wsgi.py b/isso/wsgi.py index 60c1a58..864d017 100644 --- a/isso/wsgi.py +++ b/isso/wsgi.py @@ -30,7 +30,7 @@ def host(environ): # pragma: no cover of http://www.python.org/dev/peps/pep-0333/#url-reconstruction """ - url = environ['wsgi.url_scheme']+'://' + url = environ['wsgi.url_scheme'] + '://' if environ.get('HTTP_HOST'): url += environ['HTTP_HOST'] From ce98f939348eaf7b842b5f933cec990b382b6232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 25 Apr 2018 23:13:55 +0200 Subject: [PATCH 20/30] update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5365c2e..5005f64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,11 @@ Changelog for Isso - Add apidoc - Add rc.d script for FreeBSD - Add the possibility to set CORS Origin through ISSO_CORS_ORIGIN environ variable +- Add preview button +- Add Atom feed at /feed?uri={thread-id} +- Add optionnal gravatar support +- Add nofollow noopener on links inside comments +- Add Dockerfile - Some tests/travis/documentation improvements and fixes + pep8 - Improvement on german translation From c38851a6cd6f0d7da19fc48d4c9ac98596f32600 Mon Sep 17 00:00:00 2001 From: Steffen Prince Date: Wed, 28 Oct 2015 19:39:57 -0700 Subject: [PATCH 21/30] Update to misaka 2.0 Fixes #208 --- isso/utils/html.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isso/utils/html.py b/isso/utils/html.py index 1f5f8cd..8ed5738 100644 --- a/isso/utils/html.py +++ b/isso/utils/html.py @@ -72,7 +72,7 @@ def Markdown(extensions=("strikethrough", "superscript", "autolink")): md = misaka.Markdown(Unofficial(), extensions=flags) def inner(text): - rv = md.render(text).rstrip("\n") + rv = md(text).rstrip("\n") if rv.startswith("

") or rv.endswith("

"): return rv return "

" + rv + "

" diff --git a/setup.py b/setup.py index ef866fe..5b90acb 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ import sys from setuptools import setup, find_packages -requires = ['html5lib==0.9999999', 'itsdangerous', 'Jinja2', - 'misaka>=1.0,<2.0', 'werkzeug>=0.9'] +requires = ['itsdangerous', 'Jinja2', 'misaka>=2.0,<3.0', 'html5lib<0.9999999', + 'werkzeug>=0.9'] if sys.version_info < (2, 7): raise SystemExit("Python 2 versions < 2.7 are not supported.") From eef7ea261dffa290d2d985d437e208b8cad8b557 Mon Sep 17 00:00:00 2001 From: Steffen Prince Date: Wed, 28 Oct 2015 20:00:04 -0700 Subject: [PATCH 22/30] Add `cffi` to `setup_requires` --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 5b90acb..74b1c54 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ setup( extras_require={ ':python_version=="2.7"': ['ipaddr>=2.1', 'configparser'] }, + setup_requires=["cffi>=1.3.0"], entry_points={ 'console_scripts': ['isso = isso:main'], From 7cdb47d8751b106414f85ec7409ece35f81c5d10 Mon Sep 17 00:00:00 2001 From: Steffen Prince Date: Thu, 29 Oct 2015 13:24:33 -0700 Subject: [PATCH 23/30] Update renderer to match Misaka 2.0 API --- CHANGES.rst | 1 + CONTRIBUTORS.txt | 3 +++ isso/tests/test_html.py | 2 +- isso/utils/html.py | 16 +++++++--------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5005f64..bb055b7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,7 @@ Changelog for Isso - Add optionnal gravatar support - Add nofollow noopener on links inside comments - Add Dockerfile +- Upgraded to Misaka 2 - Some tests/travis/documentation improvements and fixes + pep8 - Improvement on german translation diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 10537ff..4581ef3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -89,5 +89,8 @@ In chronological order: * @benjhess * Optionnal gravatar support +* Steffen Prince @sprin + * Upgrade to Misaka 2 + * [Your name or handle] <[email or website]> * [Brief summary of your changes] diff --git a/isso/tests/test_html.py b/isso/tests/test_html.py index 42baa82..2b3b15c 100644 --- a/isso/tests/test_html.py +++ b/isso/tests/test_html.py @@ -29,7 +29,7 @@ class TestHTML(unittest.TestCase): self.assertEqual(convert(input), expected) def test_github_flavoured_markdown(self): - convert = html.Markdown(extensions=("fenced_code", )) + convert = html.Markdown(extensions=("fenced-code", )) # without lang _in = textwrap.dedent("""\ diff --git a/isso/utils/html.py b/isso/utils/html.py index 8ed5738..4acfcc0 100644 --- a/isso/utils/html.py +++ b/isso/utils/html.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals -import operator import pkg_resources from distutils.version import LooseVersion as Version @@ -10,8 +9,6 @@ from distutils.version import LooseVersion as Version HTML5LIB_VERSION = Version(pkg_resources.get_distribution("html5lib").version) HTML5LIB_SIMPLETREE = Version("0.95") -from isso.compat import reduce - import html5lib from html5lib.sanitizer import HTMLSanitizer from html5lib.serializer import HTMLSerializer @@ -23,7 +20,8 @@ def Sanitizer(elements, attributes): class Inner(HTMLSanitizer): - # attributes found in Sundown's HTML serializer [1] except for tag, + # attributes found in Sundown's HTML serializer [1] + # except for tag, # because images are not generated anyways. # # [1] https://github.com/vmg/sundown/blob/master/html/html.c @@ -65,11 +63,11 @@ def sanitize(tokenizer, document): return serializer.render(stream) -def Markdown(extensions=("strikethrough", "superscript", "autolink")): +def Markdown(extensions=("strikethrough", "superscript", "autolink", + "fenced-code")): - flags = reduce(operator.xor, map( - lambda ext: getattr(misaka, 'EXT_' + ext.upper()), extensions), 0) - md = misaka.Markdown(Unofficial(), extensions=flags) + renderer = Unofficial() + md = misaka.Markdown(renderer, extensions=extensions) def inner(text): rv = md(text).rstrip("\n") @@ -88,7 +86,7 @@ class Unofficial(misaka.HtmlRenderer): to , compatible with Highlight.js. """ - def block_code(self, text, lang): + def blockcode(self, text, lang): lang = ' class="{0}"'.format(lang) if lang else '' return "
{0}
\n".format(text, lang) From 9b911f325719c56ab6f5e5896d8f96349a3cbb15 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Thu, 26 Apr 2018 08:13:25 +0200 Subject: [PATCH 24/30] docker: install cffi inside virtualenv `python setup.py install` installs it outside the virtualenv (because it's in `setup_requires`?), then reuse it as is when installing the dependencies. On the final container, cffi is therefore missing. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3a8203c..6da39c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,8 @@ COPY --from=0 /src . RUN apt-get -qqy update && apt-get -qqy install python3-dev sqlite3 RUN python3 -m venv /isso \ && . /isso/bin/activate \ - && python setup.py install \ - && pip install gunicorn + && pip install gunicorn cffi \ + && python setup.py install # Third, create final repository FROM python:3-slim-stretch From 311f65d52b42000c09da5dfecc2a3cd24016cd8c Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Sun, 29 Apr 2018 10:37:00 +0200 Subject: [PATCH 25/30] embed: fix feed link creation on older browsers When a browser doesn't support DOM manipulation convenience methods, the addition of the feed link was triggering an error because elements created by `$.new()` are regular elements, not elements from our own mini-DOM implementation. Therefore, the `append()` method may be absent. Use `appendChild()` instead. --- isso/js/embed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/js/embed.js b/isso/js/embed.js index a0a53da..7f7fbbe 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -31,7 +31,7 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/ var feedLink = $.new('a', i18n.translate('atom-feed')); var feedLinkWrapper = $.new('span.isso-feedlink'); feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id")); - feedLinkWrapper.append(feedLink); + feedLinkWrapper.appendChild(feedLink); $("#isso-thread").append(feedLinkWrapper); } $("#isso-thread").append($.new('h4')); From dda616eef3ce64661e941d98e3aac7261aea192f Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 29 Apr 2018 23:54:57 +0800 Subject: [PATCH 26/30] i18n: fix zh_CN/TW translate of 'postbox-preview' --- isso/js/app/i18n/zh_CN.js | 2 +- isso/js/app/i18n/zh_TW.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isso/js/app/i18n/zh_CN.js b/isso/js/app/i18n/zh_CN.js index 1bd1801..e1fbc37 100644 --- a/isso/js/app/i18n/zh_CN.js +++ b/isso/js/app/i18n/zh_CN.js @@ -3,7 +3,7 @@ define({ "postbox-author": "名字 (可选)", "postbox-email": "E-mail (可选)", "postbox-website": "网站 (可选)", - "postbox-preview": "预习", + "postbox-preview": "预览", "postbox-edit": "编辑", "postbox-submit": "提交", diff --git a/isso/js/app/i18n/zh_TW.js b/isso/js/app/i18n/zh_TW.js index 7bdf7e5..0a64c00 100644 --- a/isso/js/app/i18n/zh_TW.js +++ b/isso/js/app/i18n/zh_TW.js @@ -3,7 +3,7 @@ define({ "postbox-author": "名稱 (非必填)", "postbox-email": "電子信箱 (非必填)", "postbox-website": "個人網站 (非必填)", - "postbox-preview": "預習", + "postbox-preview": "預覽", "postbox-edit": "編輯", "postbox-submit": "送出", From 530b541f680807828c3b2f9c88c41ce3f1b47672 Mon Sep 17 00:00:00 2001 From: Sergey Lisakov Date: Mon, 30 Apr 2018 18:21:02 +0300 Subject: [PATCH 27/30] Correct translation for "preview" button --- isso/js/app/i18n/ru.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/js/app/i18n/ru.js b/isso/js/app/i18n/ru.js index 662e825..d45b621 100644 --- a/isso/js/app/i18n/ru.js +++ b/isso/js/app/i18n/ru.js @@ -3,7 +3,7 @@ define({ "postbox-author": "Имя (необязательно)", "postbox-email": "Email (необязательно)", "postbox-website": "Сайт (необязательно)", - "postbox-preview": "анонс", + "postbox-preview": "Предпросмотр", "postbox-edit": "Правка", "postbox-submit": "Отправить", "num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев", From 575c433fda6fcd041039036e0e1e6cad64b83ca9 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Sun, 6 May 2018 09:59:52 +0200 Subject: [PATCH 28/30] Substituting scss command in Makefile by node-sass + adding comment with instructions on how to install Makefile dependencies --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2a44cb3..41a0afa 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +# INSTALLATION: pip install sphinx && npm install --global node-sass + ISSO_JS_SRC := $(shell find isso/js/app -type f) \ $(shell ls isso/js/*.js | grep -vE "(min|dev)") \ isso/js/lib/requirejs-jade/jade.js @@ -53,7 +55,7 @@ man: $(DOCS_RST_SRC) mv man/isso.conf.5 man/man5/isso.conf.5 ${DOCS_CSS_DST}: $(DOCS_CSS_SRC) $(DOCS_CSS_DEP) - scss --no-cache $(DOCS_CSS_SRC) $@ + node-sass --no-cache $(DOCS_CSS_SRC) $@ ${DOCS_HTML_DST}: $(DOCS_RST_SRC) $(DOCS_CSS_DST) sphinx-build -b dirhtml docs/ $@ From b73255ec2e019445dc46a794c4ad56af3db26403 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Sun, 6 May 2018 10:17:02 +0200 Subject: [PATCH 29/30] Adding documentation on the ISSO_CORS_ORIGIN env variable - close #407 --- docs/docs/configuration/server.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index b856003..c3475b0 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -353,3 +353,18 @@ Timedelta You can add different types: `1m30s` equals to 90 seconds, `3h45m12s` equals to 3 hours, 45 minutes and 12 seconds (12512 seconds). + +Environment variables +--------------------- + +.. _environment-variables: + +Isso also support configuration through some environment variables: + +ISSO_CORS_ORIGIN + By default, `isso` will use the `Host` or else the `Referrer` HTTP header + of the request to defines a CORS `Access-Control-Allow-Origin` HTTP header + in the response. + This environent variable allows you to define a broader fixed value, + in order for example to share a single Isso instance among serveral of your + subdomains : `ISSO_CORS_ORIGIN=*.example.test` From 729a65f177d809807daed8874e8ac649984299a4 Mon Sep 17 00:00:00 2001 From: JuliusPC Date: Mon, 7 May 2018 02:13:23 +0200 Subject: [PATCH 30/30] =?UTF-8?q?Fix=20spelling=20of=20"E-Mail"=20?= =?UTF-8?q?=E2=86=92=20https://www.duden.de/rechtschreibung/E=5FMail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- isso/js/app/i18n/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isso/js/app/i18n/de.js b/isso/js/app/i18n/de.js index 04e1739..9625f7e 100644 --- a/isso/js/app/i18n/de.js +++ b/isso/js/app/i18n/de.js @@ -1,7 +1,7 @@ define({ "postbox-text": "Kommentar hier eingeben (mindestens 3 Zeichen)", "postbox-author": "Name (optional)", - "postbox-email": "Email (optional)", + "postbox-email": "E-Mail (optional)", "postbox-website": "Website (optional)", "postbox-preview": "Vorschau", "postbox-edit": "Bearbeiten",