From cbd4d90cb747e143a3b44514d77eb541f3611fe0 Mon Sep 17 00:00:00 2001 From: benjhess Date: Sun, 2 Apr 2017 21:13:06 +0200 Subject: [PATCH 01/32] 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 48a3bd72c810603bdd59673d869688b6761914f8 Mon Sep 17 00:00:00 2001 From: benjhess Date: Sun, 2 Apr 2017 21:30:47 +0200 Subject: [PATCH 02/32] Trigger From e9eebf58e3a5a0c67411622ee686eb4ec4f45d91 Mon Sep 17 00:00:00 2001 From: benjhess Date: Sun, 2 Apr 2017 19:48:03 +0000 Subject: [PATCH 03/32] 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 450291440fab0b9bdea222b09af17b69ab56a130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sat, 17 Feb 2018 00:00:15 +0100 Subject: [PATCH 04/32] update doc --- docs/docs/install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/install.rst b/docs/docs/install.rst index 8ce4f8c..44d829f 100644 --- a/docs/docs/install.rst +++ b/docs/docs/install.rst @@ -39,10 +39,10 @@ package manager. .. code-block:: sh # for Debian/Ubuntu - ~> sudo apt-get install python-setuptools python-virtualenv + ~> sudo apt-get install python-setuptools python-virtualenv python-dev # Fedora/Red Hat - ~> sudo yum install python-setuptools python-virtualenv + ~> sudo yum install python-setuptools python-virtualenv python-devel The next steps should be done as regular user, not as root (although possible but not recommended): From 28e154acfcce72f21662140d8b421ad3b9e4d178 Mon Sep 17 00:00:00 2001 From: Facundo Batista Date: Wed, 21 Mar 2018 19:39:02 -0300 Subject: [PATCH 05/32] Small typo --- docs/docs/configuration/server.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index cb39ccd..1a443f8 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -108,7 +108,7 @@ Enable moderation queue and handling of comments still in moderation queue enabled enable comment moderation queue. This option only affects new comments. - Comments in modertion queue are not visible to other users until you + Comments in moderation queue are not visible to other users until you activate them. purge-after From 361c596bf2d7f3b725d4ca3bd069dc683f3ac2c1 Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Wed, 21 Feb 2018 18:54:48 +0100 Subject: [PATCH 06/32] Adding support for ISSO_CORS_ORIGIN env variable to allow defining wildcard CORS origins --- isso/wsgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/isso/wsgi.py b/isso/wsgi.py index 1788d47..60c1a58 100644 --- a/isso/wsgi.py +++ b/isso/wsgi.py @@ -84,6 +84,8 @@ def origin(hosts): hosts = [urlsplit(h) for h in hosts] def func(environ): + if 'ISSO_CORS_ORIGIN' in environ: + return environ['ISSO_CORS_ORIGIN'] if not hosts: return "http://invalid.local" From 73c7933548fa9ea4d64a6a159d39af001cfdde2d Mon Sep 17 00:00:00 2001 From: Yuchen Pei Date: Mon, 2 Apr 2018 23:02:45 +0200 Subject: [PATCH 07/32] A quick dirty fix of #401 (#406) Fix URL in moderation mails if isso runs in a sub-URL (closes #401 ) --- isso/ext/notifications.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/isso/ext/notifications.py b/isso/ext/notifications.py index 80e1504..2b4a81c 100644 --- a/isso/ext/notifications.py +++ b/isso/ext/notifications.py @@ -37,6 +37,12 @@ class SMTP(object): self.isso = isso self.conf = isso.conf.section("smtp") + gh = isso.conf.get("general", "host") + if type(gh) == str: + self.general_host = gh + #if gh is not a string then gh is a list + else: + self.general_host = gh[0] # test SMTP connectivity try: @@ -109,7 +115,7 @@ class SMTP(object): (local("origin") + thread["uri"] + "#isso-%i" % comment["id"])) rv.write("\n") - uri = local("host") + "/id/%i" % comment["id"] + uri = self.general_host + "/id/%i" % comment["id"] key = self.isso.sign(comment["id"]) rv.write("---\n") From 4e2d2dfb204d1c70bfdf4000cdaf2546d9adad32 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 5 Apr 2018 22:46:17 +0800 Subject: [PATCH 08/32] i18n: add space between number and CJK character --- isso/js/app/i18n/zh_CN.js | 16 ++++++++-------- isso/js/app/i18n/zh_TW.js | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/isso/js/app/i18n/zh_CN.js b/isso/js/app/i18n/zh_CN.js index b9d4582..9c33957 100644 --- a/isso/js/app/i18n/zh_CN.js +++ b/isso/js/app/i18n/zh_CN.js @@ -1,11 +1,11 @@ define({ - "postbox-text": "在此输入评论 (最少3个字符)", + "postbox-text": "在此输入评论 (最少 3 个字符)", "postbox-author": "名字 (可选)", "postbox-email": "E-mail (可选)", "postbox-website": "网站 (可选)", "postbox-submit": "提交", - "num-comments": "1条评论\n{{ n }}条评论", + "num-comments": "1 条评论\n{{ n }} 条评论", "no-comments": "还没有评论", "comment-reply": "回复", @@ -21,10 +21,10 @@ define({ "comment-hidden": "{{ n }} 条评论已隐藏", "date-now": "刚刚", - "date-minute": "1分钟前\n{{ n }}分钟前", - "date-hour": "1小时前\n{{ n }}小时前", - "date-day": "昨天\n{{ n }}天前", - "date-week": "上周\n{{ n }}周前", - "date-month": "上个月\n{{ n }}个月前", - "date-year": "去年\n{{ n }}年前" + "date-minute": "1 分钟前\n{{ n }} 分钟前", + "date-hour": "1 小时前\n{{ n }} 小时前", + "date-day": "昨天\n{{ n }} 天前", + "date-week": "上周\n{{ n }} 周前", + "date-month": "上个月\n{{ n }} 个月前", + "date-year": "去年\n{{ n }} 年前" }); diff --git a/isso/js/app/i18n/zh_TW.js b/isso/js/app/i18n/zh_TW.js index 9bb59fa..68ad370 100644 --- a/isso/js/app/i18n/zh_TW.js +++ b/isso/js/app/i18n/zh_TW.js @@ -1,11 +1,11 @@ define({ - "postbox-text": "在此輸入留言(至少3個字元)", + "postbox-text": "在此輸入留言(至少 3 個字元)", "postbox-author": "名稱 (非必填)", "postbox-email": "電子信箱 (非必填)", "postbox-website": "個人網站 (非必填)", "postbox-submit": "送出", - "num-comments": "1則留言\n{{ n }}則留言", + "num-comments": "1 則留言\n{{ n }} 則留言", "no-comments": "尚無留言", "comment-reply": "回覆", @@ -18,13 +18,13 @@ define({ "comment-deleted": "留言已刪", "comment-queued": "留言待審", "comment-anonymous": "匿名", - "comment-hidden": "{{ n }}則隱藏留言", + "comment-hidden": "{{ n }} 則隱藏留言", "date-now": "剛剛", - "date-minute": "1分鐘前\n{{ n }}分鐘前", - "date-hour": "1小時前\n{{ n }}小時前", - "date-day": "昨天\n{{ n }}天前", - "date-week": "上週\n{{ n }}週前", - "date-month": "上個月\n{{ n }}個月前", - "date-year": "去年\n{{ n }}年前" + "date-minute": "1 分鐘前\n{{ n }} 分鐘前", + "date-hour": "1 小時前\n{{ n }} 小時前", + "date-day": "昨天\n{{ n }} 天前", + "date-week": "上週\n{{ n }} 週前", + "date-month": "上個月\n{{ n }} 個月前", + "date-year": "去年\n{{ n }} 年前" }); From 30fef390f55fd8c23ade69c54a0357a46945ac18 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 5 Apr 2018 23:10:46 +0800 Subject: [PATCH 09/32] 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 2b7c17a361492853fe86b0ce2f66515f100efec3 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Mon, 16 Apr 2018 20:43:54 +0200 Subject: [PATCH 10/32] Provide a multi-staged Dockerfile Many of the Docker images on hub.docker.com are outdated. The one specified in the documentation doesn't exist anymore. We provide a decent Dockerfile to build our own Docker image. This uses a multi-stage build to avoid polluting the final image with the intermediate artifacts. The final image is 155 MB. It should be possible to squeeze it even more by using Alpine Linux for the last two parts instead of Stretch. The service is using gunicorn. The user is expected to complete the installation with a reverse proxy configuration. --- Dockerfile | 33 +++++++++++++++++++++++++++++++++ docs/docs/install.rst | 13 ++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3a8203c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# First, compile JS stuff +FROM node +WORKDIR /src/ +COPY . . +RUN npm install -g requirejs uglify-js jade bower +RUN make init js + +# Second, create virtualenv +FROM python:3-stretch +WORKDIR /src/ +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 + +# Third, create final repository +FROM python:3-slim-stretch +WORKDIR /isso/ +COPY --from=1 /isso . + +# Configuration +VOLUME /db /config +EXPOSE 8080 +ENV ISSO_SETTINGS /config/isso.cfg +CMD ["/isso/bin/gunicorn", "-b", "0.0.0.0:8080", "-w", "4", "--preload", "isso.run"] + +# Example of use: +# +# docker build -t isso . +# docker run -it --rm -v /opt/isso:/config -v /opt/isso:/db -v $PWD:$PWD isso /isso/bin/isso -c \$ISSO_SETTINGS import disqus.xml +# docker run -d --rm --name isso -p 8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso diff --git a/docs/docs/install.rst b/docs/docs/install.rst index 44d829f..eb2ef35 100644 --- a/docs/docs/install.rst +++ b/docs/docs/install.rst @@ -149,7 +149,18 @@ Prebuilt Packages * Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr repository. Built from Pypi, includes a systemctl unit script. -* Docker Image: https://registry.hub.docker.com/u/bl4n/isso/ +Build a Docker image +-------------------- + +You can get a Docker image by running ``docker build . -t +isso``. Assuming you have your configuration in ``/opt/isso``, you can +use the following command to spawn the Docker container: + +.. code-block:: sh + + ~> docker run -d --rm --name isso -p 127.0.0.1:8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso + +Then, you can use a reverse proxy to expose port 8080. Install from Source ------------------- From 30f0c7eeb895163339d3515fc15f6d408137fee6 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 17 Apr 2018 07:03:54 +0200 Subject: [PATCH 11/32] docs: document data-isso-id --- docs/docs/configuration/client.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/configuration/client.rst b/docs/docs/configuration/client.rst index 14f0c86..9328357 100644 --- a/docs/docs/configuration/client.rst +++ b/docs/docs/configuration/client.rst @@ -7,6 +7,7 @@ preferably in the script tag which embeds the JS: .. code-block:: html Furthermore you can override the automatic title detection inside -the embed tag, e.g.: +the embed tag, as well as the thread ID, e.g.: .. code-block:: html -
+
data-isso --------- From 9618c0f3a3a8fb2f29096e88ac904a3a31ad5916 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 17 Apr 2018 22:32:16 +0200 Subject: [PATCH 12/32] 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 93ea72299241c594d9cd17ebb4ed6c775bb5a90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Tue, 17 Apr 2018 23:47:06 +0200 Subject: [PATCH 13/32] add contributors --- CHANGES.rst | 7 +++++-- CONTRIBUTORS.txt | 23 ++++++++++++++++++++++- docs/contribute.rst | 2 -- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2b290a6..5365c2e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,8 @@ Changelog for Isso 0.10.7 (unreleased) ------------------- -- Fix Chinese translation +- Fix Chinese translation & typo in CJK +- Fix link in moderation mails if isso is setup on a sub-url (e.g. domain.tld/comments/) - Add Danish translation - Add Hungarian translation - Add Persian translation @@ -12,7 +13,9 @@ Changelog for Isso - Add links highlighting in comments - Add apidoc - Add rc.d script for FreeBSD -- Some tests/travis/documentation improvements and fixes +- Add the possibility to set CORS Origin through ISSO_CORS_ORIGIN environ variable +- Some tests/travis/documentation improvements and fixes + pep8 +- Improvement on german translation 0.10.6 (2016-09-22) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0b5e649..db7dd27 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -51,12 +51,33 @@ In chronological order: * Added configuration to require email addresses (no validation) * Fix Vagrantfile -* Benoît Latinier +* Benoît Latinier @blatinier * Fix thread discovery * Added mandatory author + * Added admin interface * Ivan Pantic * Added vote levels +* Martin Schenck @schemar + * Improvement in the german translation + +* @cclauss + * Pep8 and drop of legacy supports (old python & debian version tested in travis) + * Make travis use pyflakes + +* Lucas Cimon @Lucas-C + * Added the possibility to define CORS origins through ISSO_CORS_ORIGIN environment variable + +* Yuchen Pei @ycpei + * Fix link in moderation emails when isso is installed in a sub URL + +* @Rocket1184 + * Fix typo in CJK translations + +* @vincentbernat + * Added documentation about data-isso-id attribute (overriding the standard isso-thread-id) + * Added multi-staged Dockerfile + * [Your name or handle] <[email or website]> * [Brief summary of your changes] diff --git a/docs/contribute.rst b/docs/contribute.rst index 8a3886d..c349a47 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -59,5 +59,3 @@ definitely need help: - delete or activate comments matching a filter (e.g. name, email, ip address) - close threads and remove threads completely - - - edit comments From 07ce742b7798a1140abcb292aebecd31ff242ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Wed, 18 Apr 2018 10:51:17 +0200 Subject: [PATCH 14/32] add documentation for uberspaces users (closes #409) --- CONTRIBUTORS.txt | 3 +++ docs/docs/troubleshooting.rst | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index db7dd27..ad1fc1e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,5 +79,8 @@ In chronological order: * Added documentation about data-isso-id attribute (overriding the standard isso-thread-id) * Added multi-staged Dockerfile +* @p-vitt & @M4a1x + * Documentation on troubleshooting for uberspace users + * [Your name or handle] <[email or website]> * [Brief summary of your changes] diff --git a/docs/docs/troubleshooting.rst b/docs/docs/troubleshooting.rst index 20ee8dd..a395a0a 100644 --- a/docs/docs/troubleshooting.rst +++ b/docs/docs/troubleshooting.rst @@ -1,6 +1,12 @@ Troubleshooting =============== +For uberspace users +------------------- +Some uberspace users experienced problems with isso and they solved their +issue by adding `DirectoryIndex disabled` as the first line in the `.htaccess` +file for the domain the isso server is running on. + pkg_ressources.DistributionNotFound ----------------------------------- From fcf576dd084f38f3c5fcb78f6fd8bd8565cb08bc Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 20 Apr 2018 20:11:32 +0200 Subject: [PATCH 15/32] 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 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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)