Merge remote-tracking branch 'posativ/master' into reply_notification

Conflicts:
	isso/css/isso.css
	isso/ext/notifications.py
	isso/js/app/text/postbox.jade
	isso/views/comments.py
This commit is contained in:
Pelle Nilsson 2018-04-29 14:48:13 +02:00
commit f6f61c547a
52 changed files with 619 additions and 96 deletions

View File

@ -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,15 @@ 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
- Add preview button
- Add Atom feed at /feed?uri={thread-id}
- 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
0.10.6 (2016-09-22)

View File

@ -51,12 +51,46 @@ In chronological order:
* Added configuration to require email addresses (no validation)
* Fix Vagrantfile
* Benoît Latinier <benoit@latinier.fr>
* Benoît Latinier @blatinier <benoit@latinier.fr>
* Fix thread discovery
* Added mandatory author
* Added admin interface
* Ivan Pantic <ivanpantic82@gmail.com>
* 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
* Fix a bug with <a> in <svg>
* 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
* 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
* @benjhess
* Optionnal gravatar support
* Steffen Prince @sprin
* Upgrade to Misaka 2
* [Your name or handle] <[email or website]>
* [Brief summary of your changes]

33
Dockerfile Normal file
View File

@ -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

View File

@ -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

View File

@ -7,6 +7,7 @@ preferably in the script tag which embeds the JS:
.. code-block:: html
<script data-isso="/prefix/"
data-isso-id="thread-id"
data-isso-css="true"
data-isso-lang="ru"
data-isso-reply-to-self="false"
@ -19,15 +20,16 @@ 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"></script>
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
<section id="isso-thread" data-title="Foo!"></section>
<section id="isso-thread" data-title="Foo!" data-isso-id="/path/to/resource"></section>
data-isso
---------
@ -106,6 +108,14 @@ scheme is based in `this color palette <http://colrd.com/palette/19308/>`_.
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
--------------
@ -124,3 +134,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.

View File

@ -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
@ -108,7 +120,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
@ -308,6 +320,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
--------

View File

@ -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.

View File

@ -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):
@ -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
-------------------

View File

@ -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
-----------------------------------

View File

@ -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;
@ -23,31 +31,26 @@
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);
}
.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);
@ -90,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 {
@ -108,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;
@ -119,10 +124,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 +159,7 @@
.isso-postbox {
max-width: 68em;
margin: 0 auto 2em;
clear: right;
}
.isso-postbox > .form-wrapper {
display: block;
@ -161,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;
@ -191,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);
@ -209,6 +218,27 @@
.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
);
.isso-postbox > .form-wrapper > .notification-section {
display: none;
padding-bottom: 10px;
@ -222,9 +252,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;
}
}

View File

@ -173,7 +173,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`.
"""
@ -195,7 +196,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 ?')

View File

@ -70,6 +70,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:
@ -118,7 +124,7 @@ class SMTP(object):
rv.write("---\n")
if admin:
uri = local("host") + "/id/%i" % comment["id"]
uri = self.general_host + "/id/%i" % comment["id"]
key = self.isso.sign(comment["id"])
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
@ -127,7 +133,7 @@ class SMTP(object):
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
else:
uri = local("host") + "/id/%i" % comment_parent["id"]
uri = self.general_host + "/id/%i" % comment_parent["id"]
key = self.isso.sign(('unsubscribe', comment_parent["id"]))
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key))

View File

@ -191,6 +191,24 @@ 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,
salt: salt,
@ -202,6 +220,8 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
fetch: fetch,
count: count,
like: like,
dislike: dislike
dislike: dislike,
feed: feed,
preview: preview
};
});

View File

@ -10,12 +10,14 @@ 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",
"#be5168", "#f19670", "#e4bf80", "#447c69"].join(" "),
"vote": true,
"vote-levels": null
"vote-levels": null,
"feed": false
};
var js = document.getElementsByTagName("script");

View File

@ -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;
}

View File

@ -3,6 +3,8 @@ define({
"postbox-author": "Име/псевдоним (незадължително)",
"postbox-email": "Ел. поща (незадължително)",
"postbox-website": "Уебсайт (незадължително)",
"postbox-preview": "преглед",
"postbox-edit": "Редактиране",
"postbox-submit": "Публикуване",
"num-comments": "1 коментар\n{{ n }} коментара",
"no-comments": "Все още няма коментари",

View File

@ -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ářů",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Δεν υπάρχουν σχόλια",

View File

@ -3,11 +3,14 @@ define({
"postbox-author": "Name (optional)",
"postbox-email": "E-mail (optional)",
"postbox-website": "Website (optional)",
"postbox-preview": "Preview",
"postbox-edit": "Edit",
"postbox-submit": "Submit",
"postbox-notification": "Subscribe to email notification of replies",
"num-comments": "One Comment\n{{ n }} Comments",
"no-comments": "No Comments Yet",
"atom-feed": "Atom feed",
"comment-reply": "Reply",
"comment-edit": "Edit",

View File

@ -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ŭ",

View File

@ -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",

View File

@ -3,6 +3,8 @@ define({
"postbox-author": "اسم (اختیاری)",
"postbox-email": "ایمیل (اختیاری)",
"postbox-website": "سایت (اختیاری)",
"postbox-preview": "پیشنمایش",
"postbox-edit": "ویرایش",
"postbox-submit": "ارسال",
"num-comments": "یک نظر\n{{ n }} نظر",

View File

@ -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",

View File

@ -3,10 +3,13 @@ define({
"postbox-author": "Nom (optionnel)",
"postbox-email": "Courriel (optionnel)",
"postbox-website": "Site web (optionnel)",
"postbox-preview": "Aperçu",
"postbox-edit": "Éditer",
"postbox-submit": "Soumettre",
"postbox-notification": "S'abonner aux notifications de réponses",
"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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Пока нет комментариев",

View File

@ -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",

View File

@ -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",

View File

@ -1,11 +1,13 @@
define({
"postbox-text": "在此输入评论 (最少3个字符)",
"postbox-text": "在此输入评论 (最少 3 个字符)",
"postbox-author": "名字 (可选)",
"postbox-email": "E-mail (可选)",
"postbox-website": "网站 (可选)",
"postbox-preview": "预习",
"postbox-edit": "编辑",
"postbox-submit": "提交",
"num-comments": "1条评论\n{{ n }}条评论",
"num-comments": "1 条评论\n{{ n }} 条评论",
"no-comments": "还没有评论",
"comment-reply": "回复",
@ -21,10 +23,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 }} 年前"
});

View File

@ -1,11 +1,13 @@
define({
"postbox-text": "在此輸入留言(至少3個字元",
"postbox-text": "在此輸入留言(至少 3 個字元)",
"postbox-author": "名稱 (非必填)",
"postbox-email": "電子信箱 (非必填)",
"postbox-website": "個人網站 (非必填)",
"postbox-preview": "預習",
"postbox-edit": "編輯",
"postbox-submit": "送出",
"num-comments": "1則留言\n{{ n }}則留言",
"num-comments": "1 則留言\n{{ n }} 則留言",
"no-comments": "尚無留言",
"comment-reply": "回覆",
@ -18,13 +20,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 }} 年前"
});

View File

@ -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)
@ -62,9 +63,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;
}
@ -240,7 +259,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() {
@ -268,7 +287,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) {

View File

@ -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);

View File

@ -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}')

View File

@ -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,6 +19,12 @@ 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'))
section(class='notification-section')
label
input(type='checkbox' name='notification')

View File

@ -27,6 +27,13 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/
return console.log("abort, #isso-thread is missing");
}
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('<div id="isso-root"></div>');

View File

@ -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");
}
}

View File

@ -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()

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import os
import json
import re
import tempfile
import unittest
@ -32,6 +33,7 @@ class TestComments(unittest.TestCase):
conf.set("general", "dbpath", self.path)
conf.set("guard", "enabled", "off")
conf.set("hash", "algorithm", "none")
self.conf = conf
class App(Isso, core.Mixin):
pass
@ -132,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"))
@ -320,10 +322,41 @@ 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()), [])
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"')
data = rv.data.decode('utf-8')
self.assertEqual(data, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>1970-01-01T01:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/nothing</id><title>Comments for example.org/path/nothing</title></feed>""")
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': '*Second*', '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, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>2018-04-01T10:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/</id><title>Comments for example.org/path/</title><entry><id>tag:example.org,2018:/isso/1/2</id><title>Comment #2</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-2" /><content type="html">&lt;p&gt;&lt;em&gt;Second&lt;/em&gt;&lt;/p&gt;</content><thr:in-reply-to href="https://example.org/path/#isso-1" ref="tag:example.org,2018:/isso/1/1" /></entry><entry><id>tag:example.org,2018:/isso/1/1</id><title>Comment #1</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-1" /><content type="html">&lt;p&gt;First&lt;/p&gt;</content></entry></feed>""")
def testCounts(self):
self.assertEqual(self.get('/count?uri=%2Fpath%2F').status_code, 404)

View File

@ -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("""\
@ -65,13 +65,17 @@ class TestHTML(unittest.TestCase):
examples = [
('Look: <img src="..." />', 'Look: '),
('<a href="http://example.org/">Ha</a>',
'<a href="http://example.org/">Ha</a>'),
['<a href="http://example.org/" rel="nofollow noopener">Ha</a>',
'<a rel="nofollow noopener" href="http://example.org/">Ha</a>']),
('<a href="sms:+1234567890">Ha</a>', '<a>Ha</a>'),
('<p style="visibility: hidden;">Test</p>', '<p>Test</p>'),
('<script>alert("Onoe")</script>', '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"),
'<p><a href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>')
self.assertIn(renderer("http://example.org/ and sms:+1234567890"),
['<p><a href="http://example.org/" rel="nofollow noopener">http://example.org/</a> and sms:+1234567890</p>',
'<p><a rel="nofollow noopener" href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>'])

View File

@ -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
@ -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)

View File

@ -110,3 +110,4 @@ def new(conf):
sha1 = Hash(func="sha1").uhash
md5 = Hash(func="md5").uhash

View File

@ -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 <img> tag,
# attributes found in Sundown's HTML serializer [1]
# except for <img> tag,
# because images are not generated anyways.
#
# [1] https://github.com/vmg/sundown/blob/master/html/html.c
@ -50,6 +48,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"
@ -60,14 +63,14 @@ 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.render(text).rstrip("\n")
rv = md(text).rstrip("\n")
if rv.startswith("<p>") or rv.endswith("</p>"):
return rv
return "<p>" + rv + "</p>"
@ -83,7 +86,7 @@ class Unofficial(misaka.HtmlRenderer):
to <code class="$lang">, compatible with Highlight.js.
"""
def block_code(self, text, lang):
def blockcode(self, text, lang):
lang = ' class="{0}"'.format(lang) if lang else ''
return "<pre><code{1}>{0}</code></pre>\n".format(text, lang)

View File

@ -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,10 +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
from isso.utils.hash import md5
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(
@ -81,7 +93,7 @@ def xhr(func):
class API(object):
FIELDS = set(['id', 'parent', 'text', 'author', 'website',
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'notification'])
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image', 'notification'])
# comment fields, that can be submitted
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
@ -91,6 +103,7 @@ class API(object):
('new', ('POST', '/new')),
('count', ('GET', '/count')),
('counts', ('POST', '/count')),
('feed', ('GET', '/feed')),
('view', ('GET', '/id/<int:id>')),
('edit', ('PUT', '/id/<int:id>')),
('delete', ('DELETE', '/id/<int:id>')),
@ -292,6 +305,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)
@ -779,6 +794,18 @@ class API(object):
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:
@ -791,6 +818,8 @@ class API(object):
item['hash'] = val
item = self._add_gravatar_image(item)
for key in set(item.keys()) - API.FIELDS:
item.pop(key)
@ -885,7 +914,6 @@ class API(object):
@apiSuccessExample Counts of 5 threads:
[2, 18, 4, 0, 3]
"""
def counts(self, environ, request):
data = request.get_json()
@ -895,6 +923,125 @@ 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):
conf = self.isso.conf.section("rss")
if not conf.get('base'):
raise NotFound
args = {
'uri': uri,
'order_by': 'id',
'asc': 0,
'limit': conf.getint('limit')
}
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 = conf.get('base')
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 = self.isso.render(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)
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()
@ -904,14 +1051,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))

View File

@ -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']
@ -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"

View File

@ -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.")
@ -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'],

View File

@ -46,6 +46,14 @@ notify = stdout
# Log console messages to file instead of standard output.
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
# Admin access password
admin_password = please_choose_a_strong_password
@ -180,3 +188,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