Merge branch 'develop' into feature/wysiwyg-editor

* develop:
  Renamed Stories.render_action() to render_json_action()
  Added account image with link to https://gravatar.com
  Added HTML sanitizer for comments and stories. Fixes issue 64.
  Added button for easily adding a username to the troll filter
  Implemented “gaslighting” of troll comments, i.e. hiding a comment of a known troll for everyone else but themselves
  Removed misleading trailing “(Server)” from console output
  Fixes issue 143
  Separated rendering of core and custom CSS with Less and added more detailed error output
  Improved display of console messages
  Added bottom margin after article meta in case there is no title
  Introduced another value for the color of borders, vertical and horizontal lines

Conflicts:
	code/Global/0.node.js
	code/Global/Global.js
	code/Site/$Site.skin
	code/Story/$Story.skin
	code/Story/Story.js
	code/Story/Story.skin
This commit is contained in:
Tobi Schäfer 2015-03-25 10:56:53 +01:00
commit 95d3a7f6cb
13 changed files with 14822 additions and 123 deletions

View file

@ -221,9 +221,10 @@
</target> </target>
<target name="server" depends="init"> <target name="server" depends="init">
<concat destfile="code/Global/0.node.js"> <exec executable="node_modules/.bin/browserify" output="code/Global/0.node.js">
<header>module = {};</header> <arg line="build/server.js -d"/>
<fileset file="node_modules/JSON/json2.js"/> </exec>
<concat destfile="code/Global/0.node.js" append="true">
<fileset file="node_modules/less/dist/less-rhino-1.7.5.js"/> <fileset file="node_modules/less/dist/less-rhino-1.7.5.js"/>
<fileset file="node_modules/marked/lib/marked.js"/> <fileset file="node_modules/marked/lib/marked.js"/>
</concat> </concat>

5
build/server.js Normal file
View file

@ -0,0 +1,5 @@
require('JSON/json2.js');
global.node = {
sanitizeHtml: require('sanitize-html')
};

View file

@ -119,6 +119,7 @@ Comment.prototype.getPermission = function(action) {
this.story.getPermission(action) && this.story.getPermission(action) &&
this.status !== Comment.PENDING; this.status !== Comment.PENDING;
case 'delete': case 'delete':
case 'filter':
return this.story.getPermission.call(this, 'delete'); return this.story.getPermission.call(this, 'delete');
case 'edit': case 'edit':
return this.status !== Comment.DELETED && return this.status !== Comment.DELETED &&
@ -167,7 +168,17 @@ Comment.prototype.edit_action = function() {
res.data.body = this.renderSkinAsString('Comment#edit'); res.data.body = this.renderSkinAsString('Comment#edit');
this.site.renderSkin('Site#page'); this.site.renderSkin('Site#page');
return; return;
};
Comment.prototype.filter_action = function () {
var username = this.creator.name;
var trollFilter = this.site.trollFilter;
if (trollFilter.indexOf(username) < 0) {
trollFilter.push(username);
this.site.setMetadata('trollFilter', trollFilter);
} }
res.redirect(req.data.http_referer);
};
/** /**
* *
@ -179,9 +190,14 @@ Comment.prototype.update = function(data) {
} }
// Get difference to current content before applying changes // Get difference to current content before applying changes
var delta = this.getDelta(data); var delta = this.getDelta(data);
this.title = data.title;
this.text = data.text;
this.setMetadata(data); this.setMetadata(data);
this.title = this.title ? stripTags(data.title) : String.EMPTY;
if (User.require(User.TRUSTED) || Membership.require(Membership.CONTRIBUTOR)) {
this.text = data.text;
} else {
this.text = this.text ? node.sanitizeHtml(data.text) : String.EMPTY;
}
if (this.story.commentMode === Story.MODERATED) { if (this.story.commentMode === Story.MODERATED) {
this.status = Comment.PENDING; this.status = Comment.PENDING;
@ -217,6 +233,14 @@ Comment.prototype.getConfirmExtra = function () {
} }
}; };
Comment.prototype.isGaslighted = function () {
var creatorIsTroll = this.site.trollFilter.indexOf(this.creator.name) > -1;
if (session.user && creatorIsTroll) {
return session.user.name !== this.creator.name;
}
return creatorIsTroll;
};
/** /**
* *
* @param {String} name * @param {String} name
@ -245,6 +269,10 @@ Comment.prototype.text_macro = function() {
gettext('This comment was removed by the author.') : gettext('This comment was removed by the author.') :
gettext('This comment was removed.')); gettext('This comment was removed.'));
res.writeln('</em>'); res.writeln('</em>');
} else if (this.isGaslighted()) {
res.write('<em>');
res.write('This comment is gaslighted.');
res.write('</em>');
} else { } else {
res.write(this.text); res.write(this.text);
} }
@ -276,3 +304,9 @@ Comment.prototype.modifier_macro = function() {
Comment.prototype.link_macro = function(param, action, text) { Comment.prototype.link_macro = function(param, action, text) {
return HopObject.prototype.link_macro.call(this, param, action, text); return HopObject.prototype.link_macro.call(this, param, action, text);
} }
Comment.prototype.meta_macro = function (param) {
if (this.status === Comment.PUBLIC && !this.isGaslighted()) {
this.renderSkin('Comment#meta');
}
};

View file

@ -10,24 +10,28 @@
<% #content %> <% #content %>
<a id='<% comment.id %>'></a> <a id='<% comment.id %>'></a>
<article class='uk-comment uk-margin-top'> <article class='uk-comment uk-margin-top'>
<div class='uk-comment-header'> <header class='uk-comment-header'>
<% if <% comment.creator url %> is null then '' else <% comment.creator url prefix="<a href='" suffix="'>" %> %> <% comment.meta %>
<img alt='' class='uk-comment-avatar' src='<% comment.creator gravatar suffix='?s=50&amp;d=mm' %>'>
<% if <% comment.creator url %> is null then '' else </a> %>
<h4 class='uk-comment-title'><% comment.creator %></h4>
<ul class='uk-comment-meta uk-subnav uk-subnav-line'> <ul class='uk-comment-meta uk-subnav uk-subnav-line'>
<li><% comment.created short %></li> <li><% comment.created short %></li>
<% comment.link main "<i class='uk-icon-link'></i>" prefix=<li> suffix=</li> %> <% comment.link main "<i class='uk-icon-link'></i>" prefix=<li> suffix=</li> %>
<% comment.link delete "<i class='uk-icon-trash-o'></i>" prefix=<li> suffix=</li> %> <% comment.link delete "<i class='uk-icon-trash-o'></i>" prefix=<li> suffix=</li> %>
<% comment.link filter "<i class='uk-icon-filter'></i>" prefix=<li> suffix=</li> %>
<% comment.link edit "<i class='uk-icon-pencil'></i>" prefix=<li> suffix=</li> %> <% comment.link edit "<i class='uk-icon-pencil'></i>" prefix=<li> suffix=</li> %>
<% if <% param.commentLink %> is false then '' else <% comment.link comment#form "<i class='uk-icon-comment-o'></i> " prefix=<li> suffix=</li> %> %> <% if <% param.commentLink %> is false then '' else <% comment.link comment#form "<i class='uk-icon-comment-o'></i> " prefix=<li> suffix=</li> %> %>
</ul> </ul>
</div> </header>
<div class='uk-comment-body'> <div class='uk-comment-body'>
<% comment.text | comment.format %> <% comment.text | comment.format %>
</div> </div>
</article> </article>
<% #meta %>
<% if <% comment.creator url %> is null then '' else <% comment.creator url prefix="<a href='" suffix="'>" %> %>
<img alt='' class='uk-comment-avatar' src='<% comment.creator gravatar suffix='?s=50&amp;d=mm' %>'>
<% if <% comment.creator url %> is null then '' else </a> %>
<h4 class='uk-comment-title'><% comment.creator %></h4>
<% #comment %> <% #comment %>
<% comment.skin Comment#content %> <% comment.skin Comment#content %>

File diff suppressed because one or more lines are too long

View file

@ -244,7 +244,7 @@ var console = function (type) {
writeln(shellColors[type] + '[' + now + '] [' + type.toUpperCase() + '] [console] ' + argString + '\u001B[0m'); writeln(shellColors[type] + '[' + now + '] [' + type.toUpperCase() + '] [console] ' + argString + '\u001B[0m');
if (typeof res !== 'undefined') { if (typeof res !== 'undefined') {
res.debug('<script>console.' + type + '("%c%s (Server)", "font-style: italic;", ' + res.debug('<script>console.' + type + '("%c%s", "font-style: italic;", ' +
JSON.stringify(argString) + ');</script>'); JSON.stringify(argString) + ');</script>');
} }
} }

View file

@ -99,6 +99,33 @@
<% site.skin $Site#admin restricted=true %> <% site.skin $Site#admin restricted=true %>
<fieldset class='uk-margin-top'> <fieldset class='uk-margin-top'>
<legend><% gettext Advanced %></legend> <legend><% gettext Advanced %></legend>
<div class='uk-form-row'>
<label class='uk-form-label'>
<% gettext 'Troll Filter' %>
</label>
<div class='uk-form-controls'>
<% site.textarea trollFilter rows=5 class='uk-width-1-1' %>
</div>
</div>
<div class='uk-form-row'>
<label class='uk-form-label'>
<% gettext 'Referrer Filter' %>
</label>
<div class='uk-form-controls'>
<% site.textarea spamfilter rows=5 class='uk-width-1-1' %>
<p class="uk-form-help-block">
<% gettext "Enter one filter {0}pattern{1} per line to be applied on every URL in the referrer and backlink lists." '<a href="http://en.wikipedia.org/wiki/Regular_expression">' </a> %>
</p>
</div>
</div>
<div class='uk-form-row'>
<label class='uk-form-label'>
<% gettext Bookmarklet %>
</label>
<div class='uk-form-controls'>
<a class='uk-button' data-uk-tooltip='{pos: "right"}' href="javascript: var siteUrl = '<% site.href %>'; var selection = (window.getSelection) ? window.getSelection() : document.selection.createRange(); selection = selection.text || selection; selection = selection + ''; var url='<% root.static %>../../formica.html?s=' + encodeURIComponent(siteUrl) + '&amp;l=' + encodeURIComponent(location.href) + '&amp;r=' + encodeURIComponent(document.referrer) + '&amp;w=400&amp;h=400&amp;c=' + encodeURIComponent(selection || document.title); window.open(url, 'formica', 'width=630, height=350'); void 0;" title="<% gettext 'Drag to Bookmarks Bar' %>"><% gettext "Post to {0}" <% site.title %> %></a>
</div>
</div>
<div class='uk-form-row'> <div class='uk-form-row'>
<label class='uk-form-label' for='callbackUrl'> <label class='uk-form-label' for='callbackUrl'>
<% gettext 'Callback URL' %> <% gettext 'Callback URL' %>
@ -119,25 +146,6 @@
<% site.diskspace %> <% site.diskspace %>
</div> </div>
</div> </div>
<div class='uk-form-row'>
<label class='uk-form-label'>
<% gettext Bookmarklet %>
</label>
<div class='uk-form-controls'>
<a class='uk-button' data-uk-tooltip='{pos: "right"}' href="javascript: var siteUrl = '<% site.href %>'; var selection = (window.getSelection) ? window.getSelection() : document.selection.createRange(); selection = selection.text || selection; selection = selection + ''; var url='<% root.static %>../../formica.html?s=' + encodeURIComponent(siteUrl) + '&amp;l=' + encodeURIComponent(location.href) + '&amp;r=' + encodeURIComponent(document.referrer) + '&amp;w=400&amp;h=400&amp;c=' + encodeURIComponent(selection || document.title); window.open(url, 'formica', 'width=630, height=350'); void 0;" title="<% gettext 'Drag to Bookmarks Bar' %>"><% gettext "Post to {0}" <% site.title %> %></a>
</div>
</div>
</fieldset>
<fieldset class='uk-margin-top'>
<legend><% gettext "Referrer Filter" %></legend>
<div class='uk-form-row'>
<div class='uk-form-controls'>
<% site.textarea spamfilter rows=5 class='uk-width-1-1' %>
<p class="uk-form-help-block">
<% gettext "Enter one filter {0}pattern{1} per line to be applied on every URL in the referrer and backlink lists." '<a href="http://en.wikipedia.org/wiki/Regular_expression">' </a> %>
</p>
</div>
</div>
</fieldset> </fieldset>
<div class='uk-margin-top'> <div class='uk-margin-top'>
<button class='uk-button uk-button-primary' type="submit" id="submit" name="save" value="1"> <button class='uk-button uk-button-primary' type="submit" id="submit" name="save" value="1">
@ -609,6 +617,11 @@ h1 a, .uk-table a {
border-color: @border-color; border-color: @border-color;
} }
// Remove the left padding of the first meta subnav item for vertical alignment with the username
h4 + .uk-comment-meta li:first-child {
padding-left: 0;
}
.av-page { .av-page {
width: 900px; // FIXME: Could we use the `vw` unit already? width: 900px; // FIXME: Could we use the `vw` unit already?
} }

View file

@ -42,9 +42,10 @@ this.handleMetadata('spamfilter');
this.handleMetadata('tagline'); this.handleMetadata('tagline');
this.handleMetadata('timeZone'); this.handleMetadata('timeZone');
this.handleMetadata('title'); this.handleMetadata('title');
this.handleMetadata('trollFilter');
/** /**
* @function * Ffunction
* @returns {String[]} * @returns {String[]}
* @see defineConstants * @see defineConstants
*/ */
@ -367,7 +368,16 @@ Site.prototype.getFormOptions = function(name) {
default: default:
return HopObject.prototype.getFormOptions.apply(this, arguments); return HopObject.prototype.getFormOptions.apply(this, arguments);
} }
};
Site.prototype.getFormValue = function (name) {
switch (name) {
case 'trollFilter':
var trolls = this.getMetadata('trollFilter');
return trolls ? trolls.join('\n') : String.EMPTY;
} }
return HopObject.prototype.getFormValue.apply(this, arguments);
};
/** /**
* @returns {String} * @returns {String}
@ -407,7 +417,10 @@ Site.prototype.update = function(data) {
notificationMode: data.notificationMode || Site.NOBODY, notificationMode: data.notificationMode || Site.NOBODY,
timeZone: data.timeZone || root.getTimeZone().getID(), timeZone: data.timeZone || root.getTimeZone().getID(),
locale: data.locale || root.getLocale().toString(), locale: data.locale || root.getLocale().toString(),
spamfilter: data.spamfilter || '' spamfilter: data.spamfilter || '',
trollFilter: data.trollFilter.split(/\r\n|\r|\n/).filter(function (item) {
return item.length > 0;
})
}); });
if (User.require(User.PRIVILEGED)) { if (User.require(User.PRIVILEGED)) {
@ -934,7 +947,7 @@ Site.prototype.spamfilter_macro = function() {
} }
} }
return; return;
} };
/** /**
* *

View file

@ -53,7 +53,7 @@ Stories.prototype.getPermission = function(action) {
case 'top': case 'top':
case 'closed': case 'closed':
case 'create': case 'create':
case 'render': case 'render.json':
case 'user': case 'user':
return Site.require(Site.OPEN) && session.user || return Site.require(Site.OPEN) && session.user ||
Membership.require(Membership.CONTRIBUTOR) || Membership.require(Membership.CONTRIBUTOR) ||
@ -124,7 +124,7 @@ Stories.prototype.top_action = function() {
return; return;
} }
Stories.prototype.render_action = function () { Stories.prototype.render_json_action = function () {
var content = String(req.postParams.http_post_remainder); var content = String(req.postParams.http_post_remainder);
var story = new Story; var story = new Story;
story.site = res.handlers.site; story.site = res.handlers.site;

View file

@ -226,7 +226,7 @@ else
if ($(event.currentTarget).hasClass('uk-htmleditor-button-preview')) { if ($(event.currentTarget).hasClass('uk-htmleditor-button-preview')) {
// FIXME: Should we really render the macros via AJAX call? // FIXME: Should we really render the macros via AJAX call?
var raw = encodeURIComponent(editor.editor.getValue()); var raw = encodeURIComponent(editor.editor.getValue());
$.post('<% stories.href render %>', raw) $.post('<% stories.href render.json %>', raw)
.done(function (data) { .done(function (data) {
$('.uk-htmleditor-preview div').html(data); $('.uk-htmleditor-preview div').html(data);
}); });

View file

@ -241,8 +241,14 @@ Story.prototype.update = function(data) {
// Get difference to current content before applying changes // Get difference to current content before applying changes
var delta = this.getDelta(data); var delta = this.getDelta(data);
this.title = data.title ? stripTags(data.title.trim()) : String.EMPTY; this.title = data.title ? stripTags(data.title) : String.EMPTY;
this.text = data.text ? data.text.trim() : String.EMPTY;
if (User.require(User.TRUSTED) || Membership.require(Membership.CONTRIBUTOR)) {
this.text = data.text;
} else {
this.text = this.text ? node.sanitizeHtml(data.text) : String.EMPTY;
}
this.status = data.status || Story.PUBLIC; this.status = data.status || Story.PUBLIC;
this.mode = data.mode || Story.FEATURED; this.mode = data.mode || Story.FEATURED;
this.commentMode = data.commentMode || Story.OPEN; this.commentMode = data.commentMode || Story.OPEN;

View file

@ -53,12 +53,21 @@
<form id='edit' class='uk-form uk-form-stacked uk-margin-top' method="post"> <form id='edit' class='uk-form uk-form-stacked uk-margin-top' method="post">
<input type="hidden" name="digest" id="digest"> <input type="hidden" name="digest" id="digest">
<input type="hidden" name="hash" id="hash"> <input type="hidden" name="hash" id="hash">
<figure class='uk-float-right' style='height: 0'>
<a href='https://gravatar.com'>
<img alt='' src='<% membership.creator gravatar suffix='?s=160&amp;d=mm' %>' style='border: 1px solid #ddd; border-radius: 4px'>
<figcaption class='uk-text-center'>
<% gettext 'Account Image' %></a>
</figcaption>
</a>
</figure>
<div class='uk-width-2-3'>
<div class='uk-form-row'> <div class='uk-form-row'>
<label class='uk-form-label' for='email'> <label class='uk-form-label' for='email'>
<% gettext E-mail %> <% gettext E-mail %>
</label> </label>
<div class='uk-form-controls'> <div class='uk-form-controls'>
<% user.input email type=email class='uk-form uk-width-1-2' %> <% user.input email type=email class='uk-form uk-width-2-3' %>
<a href='mailto:<% user.email %>'><i class='uk-icon-envelope'></i></a> <a href='mailto:<% user.email %>'><i class='uk-icon-envelope'></i></a>
</div> </div>
</div> </div>
@ -68,7 +77,7 @@
<i class='uk-icon-info-circle uk-text-muted' title='<% gettext "If you enter a URL here your user name will appear as link next to your posted items." %>' data-uk-tooltip="{pos: 'right'}"></i> <i class='uk-icon-info-circle uk-text-muted' title='<% gettext "If you enter a URL here your user name will appear as link next to your posted items." %>' data-uk-tooltip="{pos: 'right'}"></i>
</label> </label>
<div class='uk-form-controls'> <div class='uk-form-controls'>
<% user.input url type=url class='uk-form uk-width-1-2' %> <% user.input url type=url class='uk-form uk-width-2-3' %>
<a href='<% user.url %>'><i class='uk-icon-link'></i></a> <a href='<% user.url %>'><i class='uk-icon-link'></i></a>
</div> </div>
</div> </div>
@ -76,7 +85,7 @@
<label class='uk-form-label' for='password'> <label class='uk-form-label' for='password'>
<% gettext "Password" %> <% gettext "Password" %>
</label> </label>
<div class='uk-form-controls uk-form-password uk-width-1-2'> <div class='uk-form-controls uk-form-password uk-width-2-3'>
<input type="password" name="password" id="password" class='uk-width-1-1'> <input type="password" name="password" id="password" class='uk-width-1-1'>
<a href='javascript:' class='uk-form-password-toggle' data-uk-form-password="{ <a href='javascript:' class='uk-form-password-toggle' data-uk-form-password="{
lblHide: '<% gettext Hide %>', lblHide: '<% gettext Hide %>',
@ -84,6 +93,7 @@
}"><% gettext Show %></a> }"><% gettext Show %></a>
</div> </div>
</div> </div>
</div>
<% user.skin $User#admin restricted=true %> <% user.skin $User#admin restricted=true %>
<div class='uk-form-row uk-margin-top'> <div class='uk-form-row uk-margin-top'>
<button class='uk-button uk-button-primary' type="submit" id="submit" name="save" value="1"> <button class='uk-button uk-button-primary' type="submit" id="submit" name="save" value="1">

View file

@ -33,6 +33,7 @@
"less": "git://github.com/less/less.js#v1.7.5", "less": "git://github.com/less/less.js#v1.7.5",
"marked": "^0.3.3", "marked": "^0.3.3",
"minifyify": "^6.1.0", "minifyify": "^6.1.0",
"napa": "^1.1.0" "napa": "^1.1.0",
"sanitize-html": "^1.6.1"
} }
} }