Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Kris Aubuchon 2014-07-25 00:19:38 -04:00
commit 328f9a4a39
24 changed files with 246 additions and 122 deletions

View file

@ -2057,7 +2057,13 @@ var html = (function(html4) {
} }
// Discourse modification: give us more flexibility with whitelists // Discourse modification: give us more flexibility with whitelists
if (opt_nmTokenPolicy && opt_nmTokenPolicy(tagName, attribName, value)) { continue; } if (opt_nmTokenPolicy) {
var newValue = opt_nmTokenPolicy(tagName, attribName, value);
if (newValue) {
attribs[i + 1] = newValue;
continue;
}
}
if (atype !== null) { if (atype !== null) {
switch (atype) { switch (atype) {

View file

@ -14,7 +14,8 @@ var PosterNameComponent = Em.Component.extend({
var name = post.get('name'), var name = post.get('name'),
username = post.get('username'), username = post.get('username'),
linkClass = 'username', linkClass = 'username',
primaryGroupName = post.get('primary_group_name'); primaryGroupName = post.get('primary_group_name'),
url = post.get('usernameUrl');
if (post.get('staff')) { linkClass += ' staff'; } if (post.get('staff')) { linkClass += ' staff'; }
if (post.get('admin')) { linkClass += ' admin'; } if (post.get('admin')) { linkClass += ' admin'; }
@ -25,7 +26,7 @@ var PosterNameComponent = Em.Component.extend({
linkClass += ' ' + primaryGroupName; linkClass += ' ' + primaryGroupName;
} }
// Main link // Main link
buffer.push("<span class='" + linkClass + "'><a href='#'>" + username + "</a>"); buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true'>" + username + "</a>");
// Add a glyph if we have one // Add a glyph if we have one
var glyph = this.posterGlyph(post); var glyph = this.posterGlyph(post);
@ -37,7 +38,7 @@ var PosterNameComponent = Em.Component.extend({
// Are we showing full names? // Are we showing full names?
if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) { if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) {
name = Handlebars.Utils.escapeExpression(name); name = Handlebars.Utils.escapeExpression(name);
buffer.push("<span class='full-name'><a href='#'>" + name + "</a></span>"); buffer.push("<span class='full-name'><a href='" + url + "' data-auto-route='true'>" + name + "</a></span>");
} }
// User titles // User titles
@ -60,9 +61,10 @@ var PosterNameComponent = Em.Component.extend({
click: function(e) { click: function(e) {
var $target = $(e.target), var $target = $(e.target),
href = $target.attr('href'); href = $target.attr('href'),
url = this.get('post.usernameUrl');
if (!Em.isEmpty(href) && href !== '#') { if (!Em.isEmpty(href) && href !== url) {
return true; return true;
} else { } else {
this.appEvents.trigger('poster:expand', $target); this.appEvents.trigger('poster:expand', $target);

View file

@ -34,7 +34,11 @@ export default Discourse.ObjectController.extend({
var currentUsername = this.get('username'), var currentUsername = this.get('username'),
wasVisible = this.get('visible'); wasVisible = this.get('visible');
this.set('avatar', {username: username, uploaded_avatar_id: uploadedAvatarId}); if (uploadedAvatarId) {
this.set('avatar', {username: username, uploaded_avatar_id: uploadedAvatarId});
} else {
this.set('avatar', null);
}
this.setProperties({visible: true, username: username}); this.setProperties({visible: true, username: username});

View file

@ -4,6 +4,8 @@ function daysSinceEpoch(dt) {
return dt.getTime() / 86400000; return dt.getTime() / 86400000;
} }
var safe = Handlebars.SafeString;
/** /**
Converts a date to a coldmap class Converts a date to a coldmap class
@ -68,7 +70,7 @@ function categoryLinkHTML(category, options) {
categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options); categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options);
} }
} }
return new Handlebars.SafeString(Discourse.HTML.categoryBadge(category, categoryOptions)); return new safe(Discourse.HTML.categoryBadge(category, categoryOptions));
} }
/** /**
@ -171,7 +173,7 @@ Handlebars.registerHelper('avatar', function(user, options) {
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'); var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
var avatarTemplate = Discourse.User.avatarTemplate(username,uploadedAvatarId); var avatarTemplate = Discourse.User.avatarTemplate(username,uploadedAvatarId);
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({ return new safe(Discourse.Utilities.avatarImg({
size: options.hash.imageSize, size: options.hash.imageSize,
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses, extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
title: title || username, title: title || username,
@ -189,7 +191,9 @@ Handlebars.registerHelper('avatar', function(user, options) {
@for Handlebars @for Handlebars
**/ **/
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
if (Em.isEmpty(user)) { return; } if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>");
}
var username = Em.get(user, 'username'); var username = Em.get(user, 'username');
if(arguments.length < 4){ if(arguments.length < 4){
@ -198,7 +202,7 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
var avatarTemplate = Discourse.User.avatarTemplate(username, uploadId); var avatarTemplate = Discourse.User.avatarTemplate(username, uploadId);
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({ return new safe(Discourse.Utilities.avatarImg({
size: size, size: size,
avatarTemplate: avatarTemplate avatarTemplate: avatarTemplate
})); }));
@ -208,7 +212,7 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
* Used when we only have a template * Used when we only have a template
*/ */
Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) { Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) {
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({ return new safe(Discourse.Utilities.avatarImg({
size: size, size: size,
avatarTemplate: avatarTemplate avatarTemplate: avatarTemplate
})); }));
@ -243,7 +247,7 @@ Em.Handlebars.helper('bound-raw-date', function (date) {
**/ **/
Handlebars.registerHelper('age', function(property, options) { Handlebars.registerHelper('age', function(property, options) {
var dt = new Date(Ember.Handlebars.get(this, property, options)); var dt = new Date(Ember.Handlebars.get(this, property, options));
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(dt)); return new safe(Discourse.Formatter.autoUpdatingRelativeAge(dt));
}); });
/** /**
@ -254,7 +258,7 @@ Handlebars.registerHelper('age', function(property, options) {
**/ **/
Handlebars.registerHelper('age-with-tooltip', function(property, options) { Handlebars.registerHelper('age-with-tooltip', function(property, options) {
var dt = new Date(Ember.Handlebars.get(this, property, options)); var dt = new Date(Ember.Handlebars.get(this, property, options));
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(dt, {title: true})); return new safe(Discourse.Formatter.autoUpdatingRelativeAge(dt, {title: true}));
}); });
/** /**
@ -286,7 +290,7 @@ Handlebars.registerHelper('number', function(property, options) {
} }
result += ">" + n + "</span>"; result += ">" + n + "</span>";
return new Handlebars.SafeString(result); return new safe(result);
}); });
/** /**
@ -310,12 +314,12 @@ Handlebars.registerHelper('date', function(property, options) {
var val = Ember.Handlebars.get(this, property, options); var val = Ember.Handlebars.get(this, property, options);
if (val) { if (val) {
var date = new Date(val); var date = new Date(val);
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(date, {format: 'medium', title: true, leaveAgo: leaveAgo})); return new safe(Discourse.Formatter.autoUpdatingRelativeAge(date, {format: 'medium', title: true, leaveAgo: leaveAgo}));
} }
}); });
Em.Handlebars.helper('bound-date', function(dt) { Em.Handlebars.helper('bound-date', function(dt) {
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(new Date(dt), {format: 'medium', title: true })); return new safe(Discourse.Formatter.autoUpdatingRelativeAge(new Date(dt), {format: 'medium', title: true }));
}); });
/** /**
@ -337,7 +341,7 @@ Handlebars.registerHelper('custom-html', function(name, contextString, options)
}); });
Em.Handlebars.helper('human-size', function(size) { Em.Handlebars.helper('human-size', function(size) {
return new Handlebars.SafeString(I18n.toHumanSize(size)); return new safe(I18n.toHumanSize(size));
}); });
/** /**
@ -356,7 +360,7 @@ Handlebars.registerHelper('link-domain', function(property, options) {
if (!Em.isEmpty(domain)) { if (!Em.isEmpty(domain)) {
var s = domain.split('.'); var s = domain.split('.');
domain = s[s.length-2] + "." + s[s.length-1]; domain = s[s.length-2] + "." + s[s.length-1];
return new Handlebars.SafeString("<span class='domain'>" + domain + "</span>"); return new safe("<span class='domain'>" + domain + "</span>");
} }
} }
} }
@ -378,5 +382,5 @@ Handlebars.registerHelper('icon', function(icon, options) {
if (labelKey) { if (labelKey) {
html += "<span class='sr-only'>" + I18n.t(labelKey) + "</span>"; html += "<span class='sr-only'>" + I18n.t(labelKey) + "</span>";
} }
return new Handlebars.SafeString(html); return new safe(html);
}); });

View file

@ -1,13 +1,15 @@
export default { export default {
name: "inject-app-events", name: "inject-app-events",
initialize: function(container, application) { initialize: function(container, application) {
var AppEvents = Ember.Object.extend(Ember.Evented); var appEvents = Ember.Object.createWithMixins(Ember.Evented);
application.register('app-events:main', AppEvents, { singleton: true }); application.register('app-events:main', appEvents, { instantiate: false });
application.inject('controller', 'appEvents', 'app-events:main'); application.inject('controller', 'appEvents', 'app-events:main');
application.inject('component', 'appEvents', 'app-events:main'); application.inject('component', 'appEvents', 'app-events:main');
application.inject('route', 'appEvents', 'app-events:main'); application.inject('route', 'appEvents', 'app-events:main');
application.inject('view', 'appEvents', 'app-events:main'); application.inject('view', 'appEvents', 'app-events:main');
application.inject('model', 'appEvents', 'app-events:main'); application.inject('model', 'appEvents', 'app-events:main');
Discourse.URL.appEvents = appEvents;
} }
}; };

View file

@ -14,15 +14,6 @@ var _validClasses = {},
function validateAttribute(tagName, attribName, value) { function validateAttribute(tagName, attribName, value) {
var tag = _validTags[tagName]; var tag = _validTags[tagName];
// Handle possible attacks
// if you include html in your markdown, it better be valid
//
// We are SUPER strict cause nokogiri will sometimes "correct"
// this stuff "incorrectly"
if(/[<>"'`]/.test(value)){
return;
}
// Handle classes // Handle classes
if (attribName === "class") { if (attribName === "class") {
if (_validClasses[value]) { return value; } if (_validClasses[value]) { return value; }

View file

@ -71,6 +71,20 @@ Discourse.URL = Em.Object.createWithMixins({
} }
}, },
// Scroll to the same page, different anchor
scrollToId: function(id) {
if (Em.isEmpty(id)) { return; }
jumpScheduled = true;
Em.run.schedule('afterRender', function() {
var $elem = $(id);
if ($elem.length > 0) {
$('html,body').scrollTop($elem.offset().top - $('header').height() - 15);
jumpScheduled = false;
}
});
},
/** /**
Our custom routeTo method is used to intelligently overwrite default routing Our custom routeTo method is used to intelligently overwrite default routing
behavior. behavior.
@ -98,12 +112,7 @@ Discourse.URL = Em.Object.createWithMixins({
// Scroll to the same page, different anchor // Scroll to the same page, different anchor
if (path.indexOf('#') === 0) { if (path.indexOf('#') === 0) {
var $elem = $(path); this.scrollToId(path);
if ($elem.length > 0) {
Em.run.schedule('afterRender', function() {
$('html,body').scrollTop($elem.offset().top - $('header').height() - 15);
});
}
return; return;
} }
@ -136,8 +145,10 @@ Discourse.URL = Em.Object.createWithMixins({
// TODO: Extract into rules we can inject into the URL handler // TODO: Extract into rules we can inject into the URL handler
if (this.navigatedToHome(oldPath, path)) { return; } if (this.navigatedToHome(oldPath, path)) { return; }
if (path.match(/^\/?users\/[^\/]+$/)) { if (oldPath === path) {
path += "/activity"; // If navigating to the same path send an app event. Views can watch it
// and tell their controllers to refresh
this.appEvents.trigger('url:refresh');
} }
return this.handleURL(path); return this.handleURL(path);
@ -235,12 +246,7 @@ Discourse.URL = Em.Object.createWithMixins({
var homepage = Discourse.Utilities.defaultHomepage(); var homepage = Discourse.Utilities.defaultHomepage();
if (window.history && window.history.pushState && path === "/" && (oldPath === "/" || oldPath === "/" + homepage)) { if (window.history && window.history.pushState && path === "/" && (oldPath === "/" || oldPath === "/" + homepage)) {
// refresh the list this.appEvents.trigger('url:refresh');
switch (homepage) {
case "top" : { this.controllerFor('discovery/top').send('refresh'); break; }
case "categories": { this.controllerFor('discovery/categories').send('refresh'); break; }
default: { this.controllerFor('discovery/topics').send('refresh'); break; }
}
return true; return true;
} }

View file

@ -0,0 +1,19 @@
// A Mixin that a view can use to listen for 'url:refresh' when
// it is on screen, and will send an action to the controller to
// refresh its data.
//
// This is useful if you want to get around Ember's default
// behavior of not refreshing when navigating to the same place.
export default Em.Mixin.create({
_initURLRefresh: function() {
this.appEvents.on('url:refresh', this, '_urlRefresh');
}.on('didInsertElement'),
_tearDownURLRefresh: function() {
this.appEvents.off('url:refresh', this, '_urlRefresh');
}.on('willDestroyElement'),
_urlRefresh: function() {
this.get('controller').send('refresh');
}
});

View file

@ -32,7 +32,15 @@ Ember.DiscourseLocation = Ember.Object.extend({
*/ */
initState: function() { initState: function() {
set(this, 'history', get(this, 'history') || window.history); set(this, 'history', get(this, 'history') || window.history);
this.replaceState(this.formatURL(this.getURL()));
var url = this.formatURL(this.getURL()),
loc = get(this, 'location');
if (loc && loc.hash) {
url += loc.hash;
}
this.replaceState(url);
}, },
/** /**

View file

@ -21,6 +21,13 @@ Discourse.StaticController.PAGES.forEach(function(page) {
} }
}, },
activate: function() {
this._super();
// Scroll to an element if exists
Discourse.URL.scrollToId(document.location.hash);
},
model: function() { model: function() {
return Discourse.StaticPage.find(page); return Discourse.StaticPage.find(page);
}, },

View file

@ -98,7 +98,11 @@ Discourse.TopicRoute = Discourse.Route.extend({
} }
}, },
willTransition: function() { isTransitioning = true; return true; } willTransition: function() {
Em.run.cancel(scheduledReplace);
isTransitioning = true;
return true;
}
}, },
// replaceState can be very slow on Android Chrome. This function debounces replaceState // replaceState can be very slow on Android Chrome. This function debounces replaceState

View file

@ -1,4 +1,6 @@
export default Discourse.View.extend({ import UrlRefresh from 'discourse/mixins/url-refresh';
export default Discourse.View.extend(UrlRefresh, {
orderingChanged: function(){ orderingChanged: function(){
if (this.get("controller.ordering")) { if (this.get("controller.ordering")) {

View file

@ -1 +1,3 @@
export default Discourse.View.extend(Discourse.ScrollTop); import UrlRefresh from 'discourse/mixins/url-refresh';
export default Discourse.View.extend(Discourse.ScrollTop, UrlRefresh);

View file

@ -1,13 +1,6 @@
/** import UrlRefresh from 'discourse/mixins/url-refresh';
This view handles rendering of a list of topics under discovery, with support
for loading more as well as remembering your scroll position.
@class DiscoveryTopicsView export default Discourse.View.extend(Discourse.LoadMore, UrlRefresh, {
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
export default Discourse.View.extend(Discourse.LoadMore, {
eyelineSelector: '.topic-list-item', eyelineSelector: '.topic-list-item',
actions: { actions: {

View file

@ -5,27 +5,8 @@ export default Discourse.View.extend({
classNameBindings: ['controller.visible::hidden', 'controller.showBadges'], classNameBindings: ['controller.visible::hidden', 'controller.showBadges'],
_setup: function() { _setup: function() {
var self = this, var self = this;
width = this.$().width(); this.appEvents.on('poster:expand', this, '_posterExpand');
this.appEvents.on('poster:expand', function(target) {
if (!target) { return; }
Em.run.schedule('afterRender', function() {
if (target) {
var position = target.offset();
if (position) {
position.left += target.width() + 10;
var overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 5;
}
self.$().css(position);
}
}
});
});
$('html').off(clickOutsideEventName).on(clickOutsideEventName, function(e) { $('html').off(clickOutsideEventName).on(clickOutsideEventName, function(e) {
if (self.get('controller.visible')) { if (self.get('controller.visible')) {
@ -40,9 +21,30 @@ export default Discourse.View.extend({
}); });
}.on('didInsertElement'), }.on('didInsertElement'),
_posterExpand: function(target) {
if (!target) { return; }
var self = this,
width = this.$().width();
Em.run.schedule('afterRender', function() {
if (target) {
var position = target.offset();
if (position) {
position.left += target.width() + 10;
var overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 5;
}
self.$().css(position);
}
}
});
},
_removeEvents: function() { _removeEvents: function() {
$('html').off(clickOutsideEventName); $('html').off(clickOutsideEventName);
this.appEvents.off('poster:expand'); this.appEvents.off('poster:expand', this, '_posterExpand');
}.on('willDestroyElement') }.on('willDestroyElement')
}); });

View file

@ -11,6 +11,13 @@
padding: 12px 12px 5px 12px; padding: 12px 12px 5px 12px;
border: 1px solid scale-color-diff(); border: 1px solid scale-color-diff();
.avatar-placeholder {
width: 120px;
height: 120px;
float: left;
padding-right: 10px;
}
h1 { h1 {
display: inline-block; display: inline-block;
min-width: 120px; min-width: 120px;

View file

@ -0,0 +1,15 @@
module Jobs
class CalculateAvgTime < Jobs::Scheduled
every 1.day
# PERF: these calculations can become exceedingly expnsive
# they run a huge gemoetric mean and are hard to optimise
# defer to only run once a day
def execute(args)
# Update the average times
Post.calculate_avg_time(2.days.ago)
Topic.calculate_avg_time(2.days.ago)
end
end
end

View file

@ -8,9 +8,6 @@ module Jobs
every 15.minutes every 15.minutes
def execute(args) def execute(args)
# Update the average times
Post.calculate_avg_time(1.day.ago)
Topic.calculate_avg_time(1.day.ago)
# Feature topics in categories # Feature topics in categories
CategoryFeaturedTopic.feature_topics CategoryFeaturedTopic.feature_topics

View file

@ -23,8 +23,15 @@ class ExcerptParser < Nokogiri::XML::SAX::Document
me.excerpt me.excerpt
end end
def escape_attribute(v)
v.gsub("&", "&amp;")
.gsub("\"", "&#34;")
.gsub("<", "&lt;")
.gsub(">", "&gt;")
end
def include_tag(name, attributes) def include_tag(name, attributes)
characters("<#{name} #{attributes.map{|k,v| "#{k}='#{v}'"}.join(' ')}>", false, false, false) characters("<#{name} #{attributes.map{|k,v| "#{k}=\"#{escape_attribute(v)}\""}.join(' ')}>", false, false, false)
end end
def start_element(name, attributes=[]) def start_element(name, attributes=[])

View file

@ -264,8 +264,21 @@ module SiteSettingExtension
refresh_settings.include?(name.to_sym) refresh_settings.include?(name.to_sym)
end end
def filter_value(name, value)
# filter domain name
if %w[disabled_image_download_domains onebox_domains_whitelist exclude_rel_nofollow_domains email_domains_blacklist email_domains_whitelist white_listed_spam_host_domains].include? name
domain_array = []
value.split('|').each { |url|
domain_array.push(get_hostname(url))
}
value = domain_array.join("|")
end
return value
end
def set(name, value) def set(name, value)
if has_setting?(name) if has_setting?(name)
value = filter_value(name, value)
self.send("#{name}=", value) self.send("#{name}=", value)
Discourse.request_refresh! if requires_refresh?(name) Discourse.request_refresh! if requires_refresh?(name)
else else
@ -365,5 +378,13 @@ module SiteSettingExtension
enums[name] enums[name]
end end
def get_hostname(url)
unless (URI.parse(url).scheme rescue nil).nil?
url = "http://#{url}" if URI.parse(url).scheme.nil?
url = URI.parse(url).host
end
return url
end
end end

View file

@ -75,6 +75,15 @@ describe PrettyText do
describe "Excerpt" do describe "Excerpt" do
it "sanitizes attempts to inject invalid attributes" do
spinner = "<a href=\"http://thedailywtf.com/\" data-bbcode=\"' class='fa fa-spin\">WTF</a>"
PrettyText.excerpt(spinner, 20).should match_html spinner
spinner = %q{<a href="http://thedailywtf.com/" title="' class=&quot;fa fa-spin&quot;&gt;&lt;img src='http://thedailywtf.com/Resources/Images/Primary/logo.gif"></a>}
PrettyText.excerpt(spinner, 20).should match_html spinner
end
context "images" do context "images" do
it "should dump images" do it "should dump images" do
@ -94,8 +103,8 @@ describe PrettyText do
end end
it "should keep spoilers" do it "should keep spoilers" do
PrettyText.excerpt("<div class='spoiler'><img src='http://cnn.com/a.gif'></div>", 100).should == "<span class='spoiler'>[image]</span>" PrettyText.excerpt("<div class='spoiler'><img src='http://cnn.com/a.gif'></div>", 100).should match_html "<span class='spoiler'>[image]</span>"
PrettyText.excerpt("<span class='spoiler'>spoiler</div>", 100).should == "<span class='spoiler'>spoiler</span>" PrettyText.excerpt("<span class='spoiler'>spoiler</div>", 100).should match_html "<span class='spoiler'>spoiler</span>"
end end
end end
@ -104,7 +113,7 @@ describe PrettyText do
end end
it "should preserve links" do it "should preserve links" do
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",100).should == "<a href='http://cnn.com'>cnn</a>" PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",100).should match_html "<a href='http://cnn.com'>cnn</a>"
end end
it "should deal with special keys properly" do it "should deal with special keys properly" do
@ -125,15 +134,15 @@ describe PrettyText do
end end
it "should not count the surrounds of a link" do it "should not count the surrounds of a link" do
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",3).should == "<a href='http://cnn.com'>cnn</a>" PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",3).should match_html "<a href='http://cnn.com'>cnn</a>"
end end
it "uses an ellipsis instead of html entities if provided with the option" do it "uses an ellipsis instead of html entities if provided with the option" do
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>", 2, text_entities: true).should == "<a href='http://cnn.com'>cn...</a>" PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>", 2, text_entities: true).should match_html "<a href='http://cnn.com'>cn...</a>"
end end
it "should truncate links" do it "should truncate links" do
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",2).should == "<a href='http://cnn.com'>cn&hellip;</a>" PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",2).should match_html "<a href='http://cnn.com'>cn&hellip;</a>"
end end
it "doesn't extract empty quotes as links" do it "doesn't extract empty quotes as links" do
@ -294,9 +303,6 @@ describe PrettyText do
PrettyText.cook("**你hello**").should match_html "<p><strong>你hello</strong></p>" PrettyText.cook("**你hello**").should match_html "<p><strong>你hello</strong></p>"
end end
it "sanitizes attempts to inject invalid attributes" do
PrettyText.cook("<a href=\"http://thedailywtf.com/\" data-bbcode=\"' class='fa fa-spin\">WTF</a>").should == "<p><a href=\"http://thedailywtf.com/\" rel=\"nofollow\">WTF</a></p>"
end
end end
end end

View file

@ -322,4 +322,21 @@ describe SiteSettingExtension do
end end
end end
describe "filter domain name" do
before do
settings.setting(:white_listed_spam_host_domains, "www.example.com")
settings.refresh!
end
it "filters domain" do
settings.set("white_listed_spam_host_domains", "http://www.discourse.org/")
settings.white_listed_spam_host_domains.should == "www.discourse.org"
end
it "returns invalid domain as is, without throwing exception" do
settings.set("white_listed_spam_host_domains", "test!url")
settings.white_listed_spam_host_domains.should == "test!url"
end
end
end end

View file

@ -57,7 +57,7 @@ describe UserProfile do
end end
context 'with a user that has a link in their bio' do context 'with a user that has a link in their bio' do
let(:user_profile) { Fabricate.build(:user_profile, bio_raw: "im sissy and i love http://ponycorns.com") } let(:user_profile) { Fabricate.build(:user_profile, bio_raw: "I love http://discourse.org") }
let(:user) do let(:user) do
user = Fabricate.build(:user, user_profile: user_profile) user = Fabricate.build(:user, user_profile: user_profile)
user_profile.user = user user_profile.user = user
@ -66,22 +66,22 @@ describe UserProfile do
let(:created_user) do let(:created_user) do
user = Fabricate(:user) user = Fabricate(:user)
user.user_profile.bio_raw = 'im sissy and i love http://ponycorns.com' user.user_profile.bio_raw = 'I love http://discourse.org'
user.user_profile.save! user.user_profile.save!
user user
end end
it 'includes the link as nofollow if the user is not new' do it 'includes the link as nofollow if the user is not new' do
user.user_profile.send(:cook) user.user_profile.send(:cook)
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>") expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>") expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
end end
it 'removes the link if the user is new' do it 'removes the link if the user is new' do
user.trust_level = TrustLevel.levels[:newuser] user.trust_level = TrustLevel.levels[:newuser]
user_profile.send(:cook) user_profile.send(:cook)
expect(user_profile.bio_excerpt).to eq("im sissy and i love http://ponycorns.com") expect(user_profile.bio_excerpt).to match_html("I love http://discourse.org")
expect(user_profile.bio_processed).to eq("<p>im sissy and i love http://ponycorns.com</p>") expect(user_profile.bio_processed).to eq("<p>I love http://discourse.org</p>")
end end
context 'leader_links_no_follow is false' do context 'leader_links_no_follow is false' do
@ -90,22 +90,22 @@ describe UserProfile do
it 'includes the link without nofollow if the user is trust level 3 or higher' do it 'includes the link without nofollow if the user is trust level 3 or higher' do
user.trust_level = TrustLevel.levels[:leader] user.trust_level = TrustLevel.levels[:leader]
user_profile.send(:cook) user_profile.send(:cook)
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com'>http://ponycorns.com</a>") expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org'>http://discourse.org</a>")
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\">http://ponycorns.com</a></p>") expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\">http://discourse.org</a></p>")
end end
it 'removes nofollow from links in bio when trust level is increased' do it 'removes nofollow from links in bio when trust level is increased' do
created_user.change_trust_level!(:leader) created_user.change_trust_level!(:leader)
expect(created_user.user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com'>http://ponycorns.com</a>") expect(created_user.user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org'>http://discourse.org</a>")
expect(created_user.user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\">http://ponycorns.com</a></p>") expect(created_user.user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\">http://discourse.org</a></p>")
end end
it 'adds nofollow to links in bio when trust level is decreased' do it 'adds nofollow to links in bio when trust level is decreased' do
created_user.trust_level = TrustLevel.levels[:leader] created_user.trust_level = TrustLevel.levels[:leader]
created_user.save created_user.save
created_user.change_trust_level!(:regular) created_user.change_trust_level!(:regular)
expect(created_user.user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>") expect(created_user.user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
expect(created_user.user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>") expect(created_user.user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
end end
end end
@ -115,8 +115,8 @@ describe UserProfile do
it 'includes the link with nofollow if the user is trust level 3 or higher' do it 'includes the link with nofollow if the user is trust level 3 or higher' do
user.trust_level = TrustLevel.levels[:leader] user.trust_level = TrustLevel.levels[:leader]
user_profile.send(:cook) user_profile.send(:cook)
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>") expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>") expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
end end
end end
end end

View file

@ -28,19 +28,21 @@ test("isInternal with a HTTPS url", function() {
// ok(Discourse.URL.routeTo("/t/topic-title/42"), "can route relative"); // ok(Discourse.URL.routeTo("/t/topic-title/42"), "can route relative");
// }); // });
test("navigatedToHome", function() { // TODO pending: this works but the test is too mocky and needs to be fixed
var fakeDiscoveryController = { send: function() { return true; } };
var mock = sinon.mock(fakeDiscoveryController);
this.stub(Discourse.URL, "controllerFor").returns(fakeDiscoveryController);
mock.expects("send").withArgs('refresh').twice(); // test("navigatedToHome", function() {
ok(Discourse.URL.navigatedToHome("/", "/")); // var fakeDiscoveryController = { send: function() { return true; } };
// var mock = sinon.mock(fakeDiscoveryController);
var homepage = "/" + Discourse.Utilities.defaultHomepage(); // this.stub(Discourse.URL, "controllerFor").returns(fakeDiscoveryController);
ok(Discourse.URL.navigatedToHome(homepage, "/")); //
// mock.expects("send").withArgs('refresh').twice();
not(Discourse.URL.navigatedToHome("/old", "/new")); // ok(Discourse.URL.navigatedToHome("/", "/"));
//
// make sure we called the .refresh() method // var homepage = "/" + Discourse.Utilities.defaultHomepage();
mock.verify(); // ok(Discourse.URL.navigatedToHome(homepage, "/"));
}); //
// not(Discourse.URL.navigatedToHome("/old", "/new"));
//
// // make sure we called the .refresh() method
// mock.verify();
// });