diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index cb39ccd03..df70bde5b 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -308,6 +308,30 @@ algorithm Arguments have to be in that order, but can be reduced to `pbkdf2:4096` for example to override the iterations only. +User +---- + +Give yourself, your friends and/or your contributors lightweight accounts. These can be used +to later stylize your comments through CSS and distinguish them from the from anonymous commenters. + +.. code-block:: ini + + [user] + accounts = + Administrator,hunter9 + John Smith,passw0rd + +accounts + List of protected accounts. Each account is a name / password pair. + If a commenter enters a protected account name, they will be required to enter + the corresponding password in order to post their comment. + The password field will be offered in place of email. + + The "sluggified" user names will then be added as CSS classses on the comments. + For the above example, classes will be: `isso-known-user isso-user-administrator` + and `isso-known-user isso-user-john_smith`. + + Appendum -------- diff --git a/isso/css/isso.css b/isso/css/isso.css index b0ed6d1e4..de2978ef9 100644 --- a/isso/css/isso.css +++ b/isso/css/isso.css @@ -18,6 +18,8 @@ #isso-thread .textarea { min-height: 58px; outline: 0; + -webkit-transition: border-color 0.3s, color 0.3s; + transition: border-color 0.3s, color 0.3s; } #isso-thread .textarea.placeholder { color: #AAA; @@ -184,6 +186,8 @@ line-height: 1.4em; border: 1px solid rgba(0, 0, 0, 0.2); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-transition: border-color 0.3s, color 0.3s; + transition: border-color 0.3s, color 0.3s; } .isso-postbox > .form-wrapper > .auth-section .post-action { display: inline-block; @@ -206,6 +210,20 @@ .isso-postbox > .form-wrapper > .auth-section .post-action > input:active { background-color: #BBB; } +.isso-postbox > .form-wrapper > .auth-section .input-wrapper-password { + display: none; +} +.isso-postbox.isso-postbox-password-mode > .form-wrapper > .auth-section .input-wrapper-password { + display: inline-block; +} +.isso-postbox.isso-postbox-password-mode > .form-wrapper > .auth-section .input-wrapper-email { + display: none; +} +.textarea.has-error, +.isso-postbox > .form-wrapper > .auth-section input.has-error { + border-color: red!important; + color: red; +} @media screen and (max-width:600px) { .isso-postbox > .form-wrapper > .auth-section .input-wrapper { display: block; diff --git a/isso/js/app/api.js b/isso/js/app/api.js index d0fbf2f6f..d8cd0d77a 100644 --- a/isso/js/app/api.js +++ b/isso/js/app/api.js @@ -64,6 +64,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { xhr.open(method, url, true); xhr.withCredentials = true; xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("Accept", "application/json"); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { @@ -71,7 +72,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { } }; } catch (exception) { - (reject || console.log)(exception.message); + (reject || console.error)(exception.message); } xhr.send(data); @@ -89,6 +90,19 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { return rv.substring(0, rv.length - 1); // chop off trailing "&" }; + var info = function () { + var deferred = Q.defer(); + curl("GET", endpoint + "/info", null, + function (rv) { + if (rv.status >= 200 && rv.status < 300) { + deferred.resolve(JSON.parse(rv.body)); + } else { + deferred.reject({message: rv.body, status: rv.status}); + } + }); + return deferred.promise; + }; + var create = function(tid, data) { var deferred = Q.defer(); curl("POST", endpoint + "/new?" + qs({uri: tid || location}), JSON.stringify(data), @@ -96,7 +110,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { if (rv.status === 201 || rv.status === 202) { deferred.resolve(JSON.parse(rv.body)); } else { - deferred.reject(rv.body); + deferred.reject({message: rv.body, status: rv.status}); } }); return deferred.promise; @@ -110,7 +124,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { } else if (rv.status === 200) { deferred.resolve(JSON.parse(rv.body)); } else { - deferred.reject(rv.body); + deferred.reject({message: rv.body, status: rv.status}); } }); return deferred.promise; @@ -124,7 +138,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { } else if (rv.status === 200) { deferred.resolve(JSON.parse(rv.body) === null); } else { - deferred.reject(rv.body); + deferred.reject({message: rv.body, status: rv.status}); } }); return deferred.promise; @@ -159,7 +173,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { } else if (rv.status === 404) { deferred.resolve({total_replies: 0}); } else { - deferred.reject(rv.body); + deferred.reject({message: rv.body, status: rv.status}); } }); return deferred.promise; @@ -171,7 +185,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { if (rv.status === 200) { deferred.resolve(JSON.parse(rv.body)); } else { - deferred.reject(rv.body); + deferred.reject({message: rv.body, status: rv.status}); } }); return deferred.promise; @@ -195,6 +209,7 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) { endpoint: endpoint, salt: salt, + info: info, create: create, modify: modify, remove: remove, diff --git a/isso/js/app/dom.js b/isso/js/app/dom.js index 9a506097f..24fd5eed3 100644 --- a/isso/js/app/dom.js +++ b/isso/js/app/dom.js @@ -32,8 +32,16 @@ define(function() { /** * Shortcut for `Element.addEventListener`, prevents default event * by default, set :param prevents: to `false` to change that behavior. + * You can also provide an array of types, to listen on multiple events */ this.on = function(type, listener, prevent) { + if (Array.isArray(type)) { + var that = this; + type.forEach(function (type) { + that.on(type, listener, prevent); + }); + return; + } node.addEventListener(type, function(event) { listener(event); if (prevent === undefined || prevent) { diff --git a/isso/js/app/i18n/bg.js b/isso/js/app/i18n/bg.js index 45ac24b86..215c9dc78 100644 --- a/isso/js/app/i18n/bg.js +++ b/isso/js/app/i18n/bg.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Въведете коментара си тук (поне 3 знака)", "postbox-author": "Име/псевдоним (незадължително)", "postbox-email": "Ел. поща (незадължително)", + "postbox-password": "Парола", "postbox-website": "Уебсайт (незадължително)", "postbox-submit": "Публикуване", "num-comments": "1 коментар\n{{ n }} коментара", diff --git a/isso/js/app/i18n/cs.js b/isso/js/app/i18n/cs.js index 77e640142..ef9010138 100644 --- a/isso/js/app/i18n/cs.js +++ b/isso/js/app/i18n/cs.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Sem napiště svůj komentář (nejméně 3 znaky)", "postbox-author": "Jméno (nepovinné)", "postbox-email": "E-mail (nepovinný)", + "postbox-password": "Heslo", "postbox-website": "Web (nepovinný)", "postbox-submit": "Publikovat", "num-comments": "Jeden komentář\n{{ n }} Komentářů", diff --git a/isso/js/app/i18n/de.js b/isso/js/app/i18n/de.js index 5ac0610d1..d85b68928 100644 --- a/isso/js/app/i18n/de.js +++ b/isso/js/app/i18n/de.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Kommentar hier eintippen (mindestens 3 Zeichen)", "postbox-author": "Name (optional)", "postbox-email": "Email (optional)", + "postbox-password": "Passwort", "postbox-website": "Website (optional)", "postbox-submit": "Abschicken", "num-comments": "1 Kommentar\n{{ n }} Kommentare", diff --git a/isso/js/app/i18n/el_GR.js b/isso/js/app/i18n/el_GR.js index 5155a2d5a..0384968fb 100644 --- a/isso/js/app/i18n/el_GR.js +++ b/isso/js/app/i18n/el_GR.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Γράψτε το σχόλιο εδώ (τουλάχιστον 3 χαρακτήρες)", "postbox-author": "Όνομα (προαιρετικό)", "postbox-email": "E-mail (προαιρετικό)", + "postbox-password": "Κωδικός πρόσβασης", "postbox-website": "Ιστοσελίδα (προαιρετικό)", "postbox-submit": "Υποβολή", "num-comments": "Ένα σχόλιο\n{{ n }} σχόλια", diff --git a/isso/js/app/i18n/en.js b/isso/js/app/i18n/en.js index ec4b4d0f3..d3fbb67b7 100644 --- a/isso/js/app/i18n/en.js +++ b/isso/js/app/i18n/en.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Type Comment Here (at least 3 chars)", "postbox-author": "Name (optional)", "postbox-email": "E-mail (optional)", + "postbox-password": "Password", "postbox-website": "Website (optional)", "postbox-submit": "Submit", diff --git a/isso/js/app/i18n/eo.js b/isso/js/app/i18n/eo.js index 76150f3a9..6ecfd5fe5 100644 --- a/isso/js/app/i18n/eo.js +++ b/isso/js/app/i18n/eo.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Tajpu komenton ĉi-tie (almenaŭ 3 signoj)", "postbox-author": "Nomo (malnepra)", "postbox-email": "Retadreso (malnepra)", + "postbox-password": "Pasvorto", "postbox-website": "Retejo (malnepra)", "postbox-submit": "Sendu", "num-comments": "{{ n }} komento\n{{ n }} komentoj", diff --git a/isso/js/app/i18n/es.js b/isso/js/app/i18n/es.js index c25d6cd68..4438d9cdc 100644 --- a/isso/js/app/i18n/es.js +++ b/isso/js/app/i18n/es.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Escriba su comentario aquí (al menos 3 caracteres)", "postbox-author": "Nombre (opcional)", "postbox-email": "E-mail (opcional)", + "postbox-password": "Contraseña", "postbox-website": "Sitio web (opcional)", "postbox-submit": "Enviar", "num-comments": "Un Comentario\n{{ n }} Comentarios", diff --git a/isso/js/app/i18n/fr.js b/isso/js/app/i18n/fr.js index e29d024e9..a9b952ce8 100644 --- a/isso/js/app/i18n/fr.js +++ b/isso/js/app/i18n/fr.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Insérez votre commentaire ici (au moins 3 lettres)", "postbox-author": "Nom (optionnel)", "postbox-email": "Courriel (optionnel)", + "postbox-password": "Mot de passe", "postbox-website": "Site web (optionnel)", "postbox-submit": "Soumettre", "num-comments": "{{ n }} commentaire\n{{ n }} commentaires", diff --git a/isso/js/app/i18n/hr.js b/isso/js/app/i18n/hr.js index 1ae645243..b4f10d7c1 100644 --- a/isso/js/app/i18n/hr.js +++ b/isso/js/app/i18n/hr.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Napiši komentar ovdje (najmanje 3 znaka)", "postbox-author": "Ime (neobavezno)", "postbox-email": "E-mail (neobavezno)", + "postbox-password": "Lozinka", "postbox-website": "Web stranica (neobavezno)", "postbox-submit": "Pošalji", "num-comments": "Jedan komentar\n{{ n }} komentara", diff --git a/isso/js/app/i18n/it.js b/isso/js/app/i18n/it.js index 31eeb2ca1..e3e4108b0 100644 --- a/isso/js/app/i18n/it.js +++ b/isso/js/app/i18n/it.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Scrivi un commento qui (minimo 3 caratteri)", "postbox-author": "Nome (opzionale)", "postbox-email": "E-mail (opzionale)", + "postbox-password": "Parola d'ordine", "postbox-website": "Sito web (opzionale)", "postbox-submit": "Invia", "num-comments": "Un Commento\n{{ n }} Commenti", diff --git a/isso/js/app/i18n/nl.js b/isso/js/app/i18n/nl.js index 04164b663..5943ab1cc 100644 --- a/isso/js/app/i18n/nl.js +++ b/isso/js/app/i18n/nl.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Typ reactie hier (minstens 3 karakters)", "postbox-author": "Naam (optioneel)", "postbox-email": "E-mail (optioneel)", + "postbox-password": "Wachtwoord", "postbox-website": "Website (optioneel)", "postbox-submit": "Versturen", "num-comments": "Één reactie\n{{ n }} reacties", diff --git a/isso/js/app/i18n/pl.js b/isso/js/app/i18n/pl.js index d9afe7db7..166722408 100644 --- a/isso/js/app/i18n/pl.js +++ b/isso/js/app/i18n/pl.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Tutaj wpisz komentarz (co najmniej 3 znaki)", "postbox-author": "Imię/nick (opcjonalnie)", "postbox-email": "E-mail (opcjonalnie)", + "postbox-password": "Hasło", "postbox-website": "Strona (opcjonalnie)", "postbox-submit": "Wyślij", "num-comments": "Jeden komentarz\n{{ n }} komentarzy", diff --git a/isso/js/app/i18n/ru.js b/isso/js/app/i18n/ru.js index a5af03e21..4dc00eaac 100644 --- a/isso/js/app/i18n/ru.js +++ b/isso/js/app/i18n/ru.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Оставить комментарий (минимум 3 символа)", "postbox-author": "Имя (необязательно)", "postbox-email": "Email (необязательно)", + "postbox-password": "Пароль", "postbox-website": "Сайт (необязательно)", "postbox-submit": "Отправить", "num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев", diff --git a/isso/js/app/i18n/sv.js b/isso/js/app/i18n/sv.js index cafbdda40..534574c75 100644 --- a/isso/js/app/i18n/sv.js +++ b/isso/js/app/i18n/sv.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Skriv din kommentar här (minst 3 tecken)", "postbox-author": "Namn (frivilligt)", "postbox-email": "E-mail (frivilligt)", + "postbox-password": "Lösenord", "postbox-website": "Hemsida (frivilligt)", "postbox-submit": "Skicka", "num-comments": "En kommentar\n{{ n }} kommentarer", diff --git a/isso/js/app/i18n/vi.js b/isso/js/app/i18n/vi.js index 72a30929a..5077088f5 100644 --- a/isso/js/app/i18n/vi.js +++ b/isso/js/app/i18n/vi.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Nhập bình luận tại đây (tối thiểu 3 ký tự)", "postbox-author": "Tên (tùy chọn)", "postbox-email": "E-mail (tùy chọn)", + "postbox-password": "Mật khẩu", "postbox-website": "Website (tùy chọn)", "postbox-submit": "Gửi", diff --git a/isso/js/app/i18n/zh_CN.js b/isso/js/app/i18n/zh_CN.js index 70ae0817b..ab470476b 100644 --- a/isso/js/app/i18n/zh_CN.js +++ b/isso/js/app/i18n/zh_CN.js @@ -2,6 +2,7 @@ define({ "postbox-text": "在此输入评论(最少3个字符)", "postbox-author": "名字(可选)", "postbox-email": "E-mail(可选)", + "postbox-password": "密码", "postbox-website": "网站(可选)", "postbox-submit": "提交", diff --git a/isso/js/app/i18n/zh_TW.js b/isso/js/app/i18n/zh_TW.js index 20191da6b..f41d1b9ea 100644 --- a/isso/js/app/i18n/zh_TW.js +++ b/isso/js/app/i18n/zh_TW.js @@ -2,6 +2,7 @@ define({ "postbox-text": "在此輸入留言(至少3個字元)", "postbox-author": "名稱(非必填)", "postbox-email": "電子信箱(非必填)", + "postbox-password": "密碼", "postbox-website": "個人網站(非必填)", "postbox-submit": "送出", diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index 22841648f..f1f5780ad 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -5,89 +5,147 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", "use strict"; - var Postbox = function(parent) { + var validateText = function(el, config) { + if (utils.text(el.innerHTML).length < 3 || el.classList.contains("placeholder")) { + el.classList.add('has-error'); + el.focus(); + return false; + } + return true; + }; + + var validateAuthor = function(el, config) { + if (config["require-author"] && el.value.length <= 0) { + el.classList.add('has-error'); + el.focus(); + return false; + } + return true; + }; + + var validateEmail = function(el, config) { + if ((config["require-email"] && el.value.length <= 0) || (el.value.length && el.value.indexOf("@") < 0)) { + el.classList.add('has-error'); + el.focus(); + return false; + } + return true; + }; + + var Postbox = function(server, parent) { var localStorage = utils.localStorageImpl, el = $.htmlify(jade.render("postbox", { "author": JSON.parse(localStorage.getItem("author")), + "password": JSON.parse(localStorage.getItem("password")), "email": JSON.parse(localStorage.getItem("email")), "website": JSON.parse(localStorage.getItem("website")) })); + var inputs = { + author: $("[name=author]", el), + email: $("[name=email]", el), + password: $("[name=password]", el), + website: $("[name=website]", el), + text: $(".textarea", el), + submit: $("[type=submit]", el) + }; + + inputs.author.on(['change', 'keyup'], update); + inputs.email.on(['change', 'keyup'], update); + inputs.password.on(['change', 'keyup'], update); + inputs.website.on(['change', 'keyup'], update); + inputs.text.on(['change', 'keyup'], update); + + var passwordMode = false; + // callback on success (e.g. to toggle the reply button) el.onsuccess = function() {}; - el.validate = function() { - if (utils.text($(".textarea", this).innerHTML).length < 3 || - $(".textarea", this).classList.contains("placeholder")) - { - $(".textarea", this).focus(); - return false; - } - if (config["require-email"] && - $("[name='email']", this).value.length <= 0) - { - $("[name='email']", this).focus(); - return false; - } - if (config["require-author"] && - $("[name='author']", this).value.length <= 0) - { - $("[name='author']", this).focus(); - return false; - } - return true; - }; - // email is not optional if this config parameter is set if (config["require-email"]) { - $("[name='email']", el).placeholder = - $("[name='email']", el).placeholder.replace(/ \(.*\)/, ""); + inputs.email.placeholder = inputs.email.placeholder.replace(/ \(.*\)/, ""); } // author is not optional if this config parameter is set if (config["require-author"]) { - $("[name='author']", el).placeholder = - $("[name='author']", el).placeholder.replace(/ \(.*\)/, ""); + inputs.author.placeholder = inputs.author.placeholder.replace(/ \(.*\)/, ""); } // submit form, initialize optional fields with `null` and reset form. // If replied to a comment, remove form completely. - $("[type=submit]", el).on("click", function() { - if (! el.validate()) { + inputs.submit.on("click", function() { + if (3 > Number(validateText(inputs.text, config)) + + Number(validateAuthor(inputs.author, config)) + + (passwordMode ? 1 : Number(validateEmail(inputs.email, config)))) { return; } - var author = $("[name=author]", el).value || null, - email = $("[name=email]", el).value || null, - website = $("[name=website]", el).value || null; + var author = inputs.author.value || null, + email = inputs.email.value || null, + password = inputs.password.value || null, + website = inputs.website.value || null; localStorage.setItem("author", JSON.stringify(author)); localStorage.setItem("email", JSON.stringify(email)); + localStorage.setItem("password", JSON.stringify(password)); localStorage.setItem("website", JSON.stringify(website)); + ['author', 'email', 'password', 'website', 'text'].forEach(function (key) { + inputs[key].classList.remove("has-error"); + }); + api.create($("#isso-thread").getAttribute("data-isso-id"), { - author: author, email: email, website: website, + author: author, email: email, password: password, website: website, text: utils.text($(".textarea", el).innerHTML), parent: parent || null, title: $("#isso-thread").getAttribute("data-title") || null - }).then(function(comment) { - $(".textarea", el).innerHTML = ""; - $(".textarea", el).blur(); - insert(comment, true); - - if (parent !== null) { - el.onsuccess(); + }).then( + function(comment) { + inputs.text.innerHTML = ""; + inputs.text.blur(); + insert(comment, server, true); + + if (parent !== null) { + el.onsuccess(); + } + }, + function (err) { + if (err.status === 401) { + inputs.password.classList.add('has-error'); + } else { + console.error(err.message); + } } - }); + ); }); lib.editorify($(".textarea", el)); + update(); + return el; + + function update(e) { + if (e) { + e.target.classList.remove("has-error"); + } + + if (!e || e.target.name === "author") { + var isKnownUser = server.users.indexOf(inputs.author.value) >= 0; + if (isKnownUser && !passwordMode) { + el.classList.add('isso-postbox-password-mode'); + passwordMode = true; + } + else if (!isKnownUser && passwordMode) { + el.classList.remove('isso-postbox-password-mode'); + passwordMode = false; + } + } + } }; - var insert_loader = function(comment, lastcreated) { + var insert_loader = function(comment, server, lastcreated) { var entrypoint; if (comment.id === null) { entrypoint = $("#isso-root"); @@ -113,23 +171,23 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", var lastcreated = 0; rv.replies.forEach(function(commentObject) { - insert(commentObject, false); + insert(commentObject, server, false); if(commentObject.created > lastcreated) { lastcreated = commentObject.created; } }); if(rv.hidden_replies > 0) { - insert_loader(rv, lastcreated); + insert_loader(rv, server, lastcreated); } }, function(err) { - console.log(err); + console.error(err); }); }); }; - var insert = function(comment, scrollIntoView) { + var insert = function(comment, server, scrollIntoView) { var el = $.htmlify(jade.render("comment", {"comment": comment})); // update datetime every 60 seconds @@ -153,6 +211,11 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", entrypoint = $("#isso-" + comment.parent + " > .text-wrapper > .isso-follow-up"); } + if (server.users && server.users.indexOf(comment.author) >= 0) { + el.classList.add("isso-known-user"); + el.classList.add("isso-user-" + utils.slug(comment.author)); + } + entrypoint.append(el); if (scrollIntoView) { @@ -166,7 +229,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", var form = null; // XXX: probably a good place for a closure $("a.reply", footer).toggle("click", function(toggler) { - form = footer.insertAfter(new Postbox(comment.parent === null ? comment.id : comment.parent)); + form = footer.insertAfter(new Postbox(server, comment.parent === null ? comment.id : comment.parent)); form.onsuccess = function() { toggler.next(); }; $(".textarea", form).focus(); $("a.reply", footer).textContent = i18n.translate("comment-close"); @@ -241,16 +304,14 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null; if (! toggler.canceled && textarea !== null) { - if (utils.text(textarea.innerHTML).length < 3) { - textarea.focus(); + if (!validateText(textarea, config)) { toggler.wait(); return; - } else { - api.modify(comment.id, {"text": utils.text(textarea.innerHTML)}).then(function(rv) { - text.innerHTML = rv.text; - comment.text = rv.text; - }); } + api.modify(comment.id, {"text": utils.text(textarea.innerHTML)}).then(function(rv) { + text.innerHTML = rv.text; + comment.text = rv.text; + }); } else { text.innerHTML = comment.text; } @@ -325,14 +386,14 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", if(comment.hasOwnProperty('replies')) { var lastcreated = 0; comment.replies.forEach(function(replyObject) { - insert(replyObject, false); + insert(replyObject, server, false); if(replyObject.created > lastcreated) { lastcreated = replyObject.created; } }); if(comment.hidden_replies > 0) { - insert_loader(comment, lastcreated); + insert_loader(comment, server, lastcreated); } } diff --git a/isso/js/app/text/postbox.jade b/isso/js/app/text/postbox.jade index 0a85ae188..0e8e8cc3e 100644 --- a/isso/js/app/text/postbox.jade +++ b/isso/js/app/text/postbox.jade @@ -4,14 +4,17 @@ div(class='isso-postbox') div(class='textarea placeholder' contenteditable='true') = i18n('postbox-text') section(class='auth-section') - p(class='input-wrapper') - input(type='text' name='author' placeholder=i18n('postbox-author') + p(class='input-wrapper input-wrapper-author') + input(type='text' name='author' placeholder=i18n('postbox-author') title=i18n('postbox-author') value=author !== null ? '#{author}' : '') - p(class='input-wrapper') - input(type='email' name='email' placeholder=i18n('postbox-email') + p(class='input-wrapper input-wrapper-email') + input(type='email' name='email' placeholder=i18n('postbox-email') title=i18n('postbox-email') value=email != null ? '#{email}' : '') - p(class='input-wrapper') - input(type='text' name='website' placeholder=i18n('postbox-website') + p(class='input-wrapper input-wrapper-password') + input(type='password' name='password' placeholder=i18n('postbox-password') title=i18n('postbox-password') + value=password != null ? '#{password}' : '') + p(class='input-wrapper input-wrapper-website') + input(type='text' name='website' placeholder=i18n('postbox-website') title=i18n('postbox-website') value=website != null ? '#{website}' : '') p(class='post-action') input(type='submit' value=i18n('postbox-submit')) diff --git a/isso/js/app/utils.js b/isso/js/app/utils.js index f971770ae..bd7b99d69 100644 --- a/isso/js/app/utils.js +++ b/isso/js/app/utils.js @@ -68,6 +68,10 @@ define(["app/i18n"], function(i18n) { .replace(/\n/gi, '
'); }; + var slug = function (str) { + return str.replace(/[^A-Za-z0-9_-]/g, '_').toLowerCase(); + }; + // Safari private browsing mode supports localStorage, but throws QUOTA_EXCEEDED_ERR var localStorageImpl; try { @@ -94,6 +98,7 @@ define(["app/i18n"], function(i18n) { cookie: cookie, pad: pad, ago: ago, + slug: slug, text: text, detext: detext, localStorageImpl: localStorageImpl diff --git a/isso/js/embed.js b/isso/js/embed.js index 680880b7a..efe2ee40d 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -3,7 +3,7 @@ * Distributed under the MIT license */ -require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/count", "app/dom", "app/text/css", "app/text/svg", "app/jade"], function(domready, config, i18n, api, isso, count, $, css, svg, jade) { +require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/count", "app/dom", "app/text/css", "app/text/svg", "app/jade", "app/lib/promise"], function(domready, config, i18n, api, isso, count, $, css, svg, jade, Q) { "use strict"; @@ -27,41 +27,65 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/ return console.log("abort, #isso-thread is missing"); } - $("#isso-thread").append($.new('h4')); - $("#isso-thread").append(new isso.Postbox(null)); - $("#isso-thread").append('
'); + var server = null, + comments = null; - api.fetch($("#isso-thread").getAttribute("data-isso-id"), + api.info().then( + function (rv) { + server = rv; + + $("#isso-thread").append($.new('h4')); + $("#isso-thread").append(new isso.Postbox(server, null)); + $("#isso-thread").append('
'); + + tryInitComments(); + }, + errorHandler + ); + + api.fetch( + $("#isso-thread").getAttribute("data-isso-id"), config["max-comments-top"], config["max-comments-nested"]).then( - function(rv) { - if (rv.total_replies === 0) { - $("#isso-thread > h4").textContent = i18n.translate("no-comments"); - return; - } + function (rv) { + comments = rv; + tryInitComments(); + }, + errorHandler + ); - var lastcreated = 0; - var count = rv.total_replies; - rv.replies.forEach(function(comment) { - isso.insert(comment, false); - if(comment.created > lastcreated) { - lastcreated = comment.created; - } - count = count + comment.total_replies; - }); - $("#isso-thread > h4").textContent = i18n.pluralize("num-comments", count); - - if(rv.hidden_replies > 0) { - isso.insert_loader(rv, lastcreated); - } + function tryInitComments() { + if (!server || !comments) { + return; + } - if (window.location.hash.length > 0) { - $(window.location.hash).scrollIntoView(); + if (comments.total_replies === 0) { + $("#isso-thread > h4").textContent = i18n.translate("no-comments"); + return; + } + + var lastcreated = 0; + var count = comments.total_replies; + comments.replies.forEach(function(comment) { + isso.insert(comment, server, false); + if(comment.created > lastcreated) { + lastcreated = comment.created; } - }, - function(err) { - console.log(err); + count = count + comment.total_replies; + }); + $("#isso-thread > h4").textContent = i18n.pluralize("num-comments", count); + + if(comments.hidden_replies > 0) { + isso.insert_loader(comments, server, lastcreated); } - ); + + if (window.location.hash.length > 0) { + $(window.location.hash).scrollIntoView(); + } + } + + function errorHandler(err) { + console.log(err); + } }); }); diff --git a/isso/views/__init__.py b/isso/views/__init__.py index 0b995cdb7..1ca767373 100644 --- a/isso/views/__init__.py +++ b/isso/views/__init__.py @@ -51,6 +51,8 @@ class Info(object): def __init__(self, isso): self.moderation = isso.conf.getboolean("moderation", "enabled") + self.users = list(map(lambda line: str.strip(line.split(',')[0]), + isso.conf.getiter("user", "accounts"))) isso.urls.add(Rule('/info', endpoint=self.show)) def show(self, environ, request): @@ -60,6 +62,7 @@ def show(self, environ, request): "host": str(local("host")), "origin": str(local("origin")), "moderation": self.moderation, + "users": self.users } return Response(json.dumps(rv), 200, content_type="application/json") diff --git a/isso/views/comments.py b/isso/views/comments.py index d3283058f..0d25da28c 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -14,7 +14,7 @@ from werkzeug.utils import redirect from werkzeug.routing import Rule from werkzeug.wrappers import Response -from werkzeug.exceptions import BadRequest, Forbidden, NotFound +from werkzeug.exceptions import BadRequest, Forbidden, NotFound, Unauthorized from isso.compat import text_type as str @@ -75,7 +75,7 @@ class API(object): 'mode', 'created', 'modified', 'likes', 'dislikes', 'hash']) # comment fields, that can be submitted - ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title']) + ACCEPT = set(['text', 'author', 'password', 'website', 'email', 'parent', 'title']) VIEWS = [ ('fetch', ('GET', '/')), @@ -102,6 +102,8 @@ def __init__(self, isso, hasher): self.conf = isso.conf.section("general") self.moderated = isso.conf.getboolean("moderation", "enabled") + self.users = list(map(lambda line: tuple(map(str.strip, line.split(','))), + isso.conf.getiter("user", "accounts"))) self.guard = isso.db.guard self.threads = isso.db.threads @@ -112,7 +114,7 @@ def __init__(self, isso, hasher): Rule(path, methods=[method], endpoint=getattr(self, view))) @classmethod - def verify(cls, comment): + def verify(cls, comment, user_mode=False): if "text" not in comment: return False, "text is missing" @@ -120,7 +122,7 @@ def verify(cls, comment): if not isinstance(comment.get("parent"), (int, type(None))): return False, "parent must be an integer or null" - for key in ("text", "author", "website", "email"): + for key in ("text", "author", "website", "email", "password"): if not isinstance(comment.get(key), (str, type(None))): return False, "%s must be a string or null" % key @@ -133,6 +135,9 @@ def verify(cls, comment): if len(comment.get("email") or "") > 254: return False, "http://tools.ietf.org/html/rfc5321#section-4.5.3" + if not user_mode and "@" not in (comment.get("email") or ""): + return False, "Invalid email address (must contain @)" + if comment.get("website"): if len(comment["website"]) > 254: return False, "arbitrary length limit" @@ -150,13 +155,20 @@ def new(self, environ, request, uri): for field in set(data.keys()) - API.ACCEPT: data.pop(field) - for key in ("author", "email", "website", "parent"): + for key in ("author", "password", "email", "website", "parent"): data.setdefault(key, None) - valid, reason = API.verify(data) + user = next((user for user in self.users if user[0] == data["author"]), None) + + valid, reason = API.verify(data, user_mode=user is not None) if not valid: return BadRequest(reason) + if user: + if user[1] != data["password"]: + return Unauthorized("Invalid password") + data["email"] = "isso-user-" + user[0] # Intentionally no @, so anons can't spoof this + for field in ("author", "email", "website"): if data.get(field) is not None: data[field] = cgi.escape(data[field]) diff --git a/share/isso.conf b/share/isso.conf index be17a50c1..42aea373f 100644 --- a/share/isso.conf +++ b/share/isso.conf @@ -172,3 +172,18 @@ 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 + + +[user] +# Options to enable limited user account features + + +# List of protected accounts. Each account is a name / password pair. +# If a visitor enters a protected account name, they will be required to enter +# the corresponding password in order to post their comment. +# These comments can then later be stylized based on the user name: +# +# accounts = +# Administrator,hunter9 +# John Smith,passw0rd +accounts =