Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Robin Ward 2013-02-11 10:37:44 -05:00
commit 37fa61ab51
28 changed files with 242 additions and 89 deletions

View File

@ -129,7 +129,6 @@ window.Discourse.ComposerController = Ember.Controller.extend Discourse.Presence
click: ->
if @get('content.composeState') == Discourse.Composer.DRAFT
@set('content.composeState', Discourse.Composer.OPEN)
false
shrink: ->
if @get('content.reply') == @get('content.originalText') then @close() else @collapse()

View File

@ -60,12 +60,13 @@ Handlebars.registerHelper 'avatar', (user, options) ->
user = Ember.Handlebars.get(this, user, options) if typeof user is 'string'
username = Em.get(user, 'username')
username ||= Em.get(user, options.hash.usernamePath)
title = Em.get(user, 'title') || Em.get(user, 'description') unless options.hash.ignoreTitle
new Handlebars.SafeString Discourse.Utilities.avatarImg(
size: options.hash.imageSize
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses
username: username
title: Em.get(user, 'title') || Em.get(user, 'description') || username
title: title || username
avatarTemplate: Ember.get(user, 'avatar_template') || options.hash.avatarTemplate
)
@ -125,4 +126,9 @@ Handlebars.registerHelper 'date', (property, options) ->
new Handlebars.SafeString("<span class='date' title='#{fullReadable}'>#{displayDate}</span>")
Handlebars.registerHelper 'personalizedName', (property, options) ->
name = Ember.Handlebars.get(this, property, options);
username = Ember.Handlebars.get(this, options.hash.usernamePath, options) if options.hash.usernamePath
return name unless username == Discourse.get('currentUser.username')
return Em.String.i18n('you')

View File

@ -2,11 +2,12 @@
{{#collection contentBinding="stream" itemClass="item"}}
{{#with view.content}}
<div class='clearfix info'>
<a href="/users/{{unbound username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="large" extraClasses="actor" avatarTemplatePath="avatar_template"}}</div></a>
<a href="/users/{{unbound username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="large" extraClasses="actor" avatarTemplatePath="avatar_template" ignoreTitle="true"}}</div></a>
<span class='time'>{{date path="created_at" leaveAgo="true"}}</span>
<a class='name' href="{{unbound postUrl}}">{{unbound name}}</a><br>
<a class="title" href="{{unbound postUrl}}">{{unbound title}}</a><br>
<a class='name' href="/users/{{unbound username}}">{{personalizedName name usernamePath="username"}}</a>
<span class='type'>{{unbound description}}</span>
<span class='title'><span class="post-number">#{{unbound post_number}}</span> <a href="{{unbound postUrl}}">{{unbound title}}</a></span>
<a class="post-number" href="{{unbound postUrl}}">#{{unbound post_number}}</a>
</div>
<p class='excerpt'>
{{{unbound excerpt}}}

View File

@ -129,7 +129,7 @@ window.Discourse.ComposerView = window.Discourse.View.extend
Discourse.UserSearch.search
term: term,
callback: callback,
exclude: selected
exclude: selected.concat [Discourse.get('currentUser.username')]
onChangeItems: (items) =>
items = $.map items, (i) -> if i.username then i.username else i
@set('content.targetUsernames', items.join(","))

View File

@ -347,7 +347,7 @@
}
}
.reply-to-tab {
z-index: 999;
z-index: 980;
font-size: 12px;
color: $darkish_gray;
display: block;

View File

@ -241,10 +241,6 @@
color: lighten($black, 30%);
}
.item {
.post-number {
color: lighten($black, 40%);
margin-right: 4px;
}
padding: 10px 8px;
background-color: white;
border: 1px solid #b9b9b9;
@ -266,7 +262,7 @@
float: left;
margin-right: 10px;
}
.name {
.title {
display: inline-block;
margin-bottom: 4px;
font-size: 14px;

View File

@ -1419,7 +1419,7 @@ body {
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
}
:-moz-placeholder {
color: #999999;
color: #999999 !important;
}
::-webkit-input-placeholder {
color: #999999;

View File

@ -3,7 +3,7 @@ class Admin::UsersController < Admin::AdminController
def index
# Sort order
if params[:query] == "active"
@users = User.order("COALESCE(last_seen_at, '01-01-1970') DESC, username")
@users = User.order("COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD')) DESC, username")
else
@users = User.order("created_at DESC, username")
end

View File

@ -76,7 +76,10 @@ class ApplicationController < ActionController::Base
def store_preloaded(key, json)
@preloaded ||= {}
@preloaded[key] = json
# I dislike that there is a gsub as opposed to a gsub!
# but we can not be mucking with user input, I wonder if there is a way
# to inject this safty deeper in the library or even in AM serializer
@preloaded[key] = json.gsub("</", "<\\/")
end
# If we are rendering HTML, preload the session data

View File

@ -0,0 +1,15 @@
class RobotsTxtController < ApplicationController
layout false
skip_before_filter :check_xhr
skip_before_filter :check_restricted_access
def index
path = if SiteSetting.allow_index_in_robots_txt && !SiteSetting.restrict_access
:index
else
:no_index
end
render path, content_type: 'text/plain'
end
end

View File

@ -106,6 +106,7 @@ footer:after{ content: '#{error}' }"
@lock.synchronize do
style = self.where(key: key).first
style.ensure_stylesheet_on_disk!
@cache[key] = style
end
end
@ -140,9 +141,13 @@ footer:after{ content: '#{error}' }"
Digest::MD5.hexdigest(self.stylesheet)
end
def cache_fullpath
"#{Rails.root}/public/#{CACHE_PATH}"
end
def ensure_stylesheet_on_disk!
path = stylesheet_fullpath
dir = "#{Rails.root}/public/#{CACHE_PATH}"
dir = cache_fullpath
FileUtils.mkdir_p(dir)
unless File.exists?(path)
File.open(path, "w") do |f|
@ -152,23 +157,18 @@ footer:after{ content: '#{error}' }"
end
def stylesheet_filename
file = ""
dir = "#{Rails.root}/public/#{CACHE_PATH}"
path = dir + file
"/#{CACHE_PATH}/#{self.key}.css"
"/#{self.key}.css"
end
def stylesheet_fullpath
"#{Rails.root}/public#{self.stylesheet_filename}"
"#{self.cache_fullpath}#{self.stylesheet_filename}"
end
def stylesheet_link_tag
return "" unless self.stylesheet.present?
return @stylesheet_link_tag if @stylesheet_link_tag
ensure_stylesheet_on_disk!
@stylesheet_link_tag = "<link class=\"custom-css\" rel=\"stylesheet\" href=\"#{self.stylesheet_filename}?#{self.stylesheet_hash}\" type=\"text/css\" media=\"screen\">"
@stylesheet_link_tag = "<link class=\"custom-css\" rel=\"stylesheet\" href=\"/#{CACHE_PATH}#{self.stylesheet_filename}?#{self.stylesheet_hash}\" type=\"text/css\" media=\"screen\">"
end
end

View File

@ -30,8 +30,6 @@ class SiteSetting < ActiveRecord::Base
client_setting(:max_topic_title_length, 255)
client_setting(:flush_timings_secs, 5)
# settings only available server side
setting(:auto_track_topics_after, 60000)
setting(:long_polling_interval, 15000)
@ -91,6 +89,8 @@ class SiteSetting < ActiveRecord::Base
setting(:allow_duplicate_topic_titles, false)
setting(:add_rel_nofollow_to_user_content, true)
setting(:exclude_rel_nofollow_domains, '')
setting(:post_excerpt_maxlength, 300)
setting(:post_onebox_maxlength, 500)
setting(:best_of_score_threshold, 15)
@ -102,6 +102,8 @@ class SiteSetting < ActiveRecord::Base
# we need to think of a way to force users to enter certain settings, this is a minimal config thing
setting(:notification_email, 'info@discourse.org')
setting(:allow_index_in_robots_txt, true)
setting(:send_welcome_message, true)
setting(:twitter_consumer_key, '')

View File

@ -1,20 +1,19 @@
class UserSearch
def self.search term, topic_id = nil
User.find_by_sql sql(term, topic_id)
end
sql = User.sql_builder(
"select id, username, name, email from users u
/*left_join*/
/*where*/
/*order_by*/")
private
def self.sql term, topic_id
sql = "select id, username, name, email from users u "
if topic_id
sql << "left join (select distinct p.user_id from posts p where topic_id = :topic_id) s on
s.user_id = u.id "
sql.left_join "(select distinct p.user_id from posts p where topic_id = :topic_id) s on s.user_id = u.id", topic_id: topic_id
end
if term.present?
sql << "where username ilike :term_like or
if term.present?
sql.where("username_lower like :term_like or
to_tsvector('simple', name) @@
to_tsquery('simple',
regexp_replace(
@ -22,22 +21,18 @@ class UserSearch
cast(plainto_tsquery(:term) as text)
,'\''(?: |$)', ':*''', 'g'),
'''', '', 'g')
) "
)", term: term, term_like: "#{term.downcase}%")
sql.order_by "case when username_lower = :term then 0 else 1 end asc"
end
sql << "order by case when username_lower = :term then 0 else 1 end asc, "
if topic_id
sql << " case when s.user_id is null then 0 else 1 end desc, "
sql.order_by "case when s.user_id is null then 0 else 1 end desc"
end
sql << " case when last_seen_at is null then 0 else 1 end desc, last_seen_at desc, username asc limit(20)"
sql.order_by "case when last_seen_at is null then 0 else 1 end desc, last_seen_at desc, username asc limit(20)"
sanitize_sql_array(sql, topic_id: topic_id, term_like: "#{term}%", term: term)
sql.exec
end
def self.sanitize_sql_array *args
ActiveRecord::Base.send(:sanitize_sql_array, args)
end
end
end

View File

@ -1,5 +1,2 @@
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /

View File

@ -0,0 +1,6 @@
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
#
User-Agent: *
Disallow: /

View File

@ -20,8 +20,6 @@ module Discourse
# -- all .rb files in that directory are automatically loaded.
require 'discourse'
# initializes message bus too early, not picking on redis settings, needs to be fixed
# require 'message_bus_diags'
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/app/serializers)

View File

@ -0,0 +1 @@
require 'sql_builder'

View File

@ -219,6 +219,8 @@ en:
max_image_width: "maximum width for an image in a post"
category_featured_topics: "number of topics displayed in the category list"
popup_delay: "Length of time in ms before popups appear on the screen"
add_rel_nofollow_to_user_content: "Add rel nofollow to all submitted user content, except for internal links (including parent domains) changing this requires you update all your baked markdown"
exclude_rel_nofollow_domains: "A comma delimited list of domains where nofollow is not added (tld.com will automatically allow sub.tld.com as well)"
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt."
post_onebox_maxlength: "Maximum length of a oneboxed discourse post."
category_post_template: "The post template that appears once you create a category"
@ -257,6 +259,9 @@ en:
posts_per_page: "How many posts are returned on a topic page"
system_username: "Username that sends system messages"
send_welcome_message: "Do new users get a welcome private message?"
allow_index_in_robots_txt: "Site should be indexed by search engines (update robots.txt)"
port: "If you'd like to specify a port in the URL. Useful in development mode. Leave blank for none."
force_hostname: "If you'd like to specify a hostname in the URL. Useful in development mode. Leave blank for none."
@ -435,6 +440,7 @@ en:
show_more: "show more"
links: Links
faq: "FAQ"
you: "You"
suggested_topics:
title: "Suggested Topics"

View File

@ -207,6 +207,9 @@ Discourse::Application.routes.draw do
post 'draft' => 'draft#update'
delete 'draft' => 'draft#destroy'
get 'robots.txt' => 'robots_txt#index'
# You can have the root of your site routed with "root"
# just remember to delete public/index.html.
root :to => 'list#index'

Binary file not shown.

Binary file not shown.

View File

@ -172,7 +172,42 @@ module PrettyText
cloned = opts.dup
# we have a minor inconsistency
cloned[:topicId] = opts[:topic_id]
Sanitize.clean(markdown(text.dup, cloned), PrettyText.whitelist)
sanitized = Sanitize.clean(markdown(text.dup, cloned), PrettyText.whitelist)
if SiteSetting.add_rel_nofollow_to_user_content
sanitized = add_rel_nofollow_to_user_content(sanitized)
end
sanitized
end
def self.add_rel_nofollow_to_user_content(html)
whitelist = []
l = SiteSetting.exclude_rel_nofollow_domains
if l.present?
whitelist = l.split(",")
end
site_uri = nil
doc = Nokogiri::HTML.fragment(html)
doc.css("a").each do |l|
href = l["href"].to_s
begin
uri = URI(href)
site_uri ||= URI(Discourse.base_url)
if !uri.host.present? ||
uri.host.ends_with?(site_uri.host) ||
whitelist.any?{|u| uri.host.ends_with?(u)}
# we are good no need for nofollow
else
l["rel"] = "nofollow"
end
rescue URI::InvalidURIError
# add a nofollow anyway
l["rel"] = "nofollow"
end
end
doc.to_html
end
def self.extract_links(html)

View File

@ -21,7 +21,7 @@ module Search
NULL AS color
FROM users AS u
JOIN users_search s on s.id = u.id
WHERE s.search_data @@ TO_TSQUERY(:query)
WHERE s.search_data @@ TO_TSQUERY('english', :query)
ORDER BY last_posted_at desc
"
end
@ -36,13 +36,13 @@ module Search
FROM topics AS ft
JOIN posts AS p ON p.topic_id = ft.id AND p.post_number = 1
JOIN posts_search s on s.id = p.id
WHERE s.search_data @@ TO_TSQUERY(:query)
WHERE s.search_data @@ TO_TSQUERY('english', :query)
AND ft.deleted_at IS NULL
AND ft.visible
AND ft.archetype <> '#{Archetype.private_message}'
ORDER BY
TS_RANK_CD(TO_TSVECTOR('english', ft.title), TO_TSQUERY(:query)) desc,
TS_RANK_CD(search_data, TO_TSQUERY(:query)) desc,
TS_RANK_CD(TO_TSVECTOR('english', ft.title), TO_TSQUERY('english', :query)) desc,
TS_RANK_CD(search_data, TO_TSQUERY('english', :query)) desc,
bumped_at desc"
end
@ -57,13 +57,13 @@ module Search
FROM topics AS ft
JOIN posts AS p ON p.topic_id = ft.id AND p.post_number <> 1
JOIN posts_search s on s.id = p.id
WHERE s.search_data @@ TO_TSQUERY(:query)
WHERE s.search_data @@ TO_TSQUERY('english', :query)
AND ft.deleted_at IS NULL and p.deleted_at IS NULL
AND ft.visible
AND ft.archetype <> '#{Archetype.private_message}'
ORDER BY
TS_RANK_CD(TO_TSVECTOR('english', ft.title), TO_TSQUERY(:query)) desc,
TS_RANK_CD(search_data, TO_TSQUERY(:query)) desc,
TS_RANK_CD(TO_TSVECTOR('english', ft.title), TO_TSQUERY('english', :query)) desc,
TS_RANK_CD(search_data, TO_TSQUERY('english', :query)) desc,
bumped_at desc"
end
@ -76,7 +76,7 @@ module Search
c.color
FROM categories AS c
JOIN categories_search s on s.id = c.id
WHERE s.search_data @@ TO_TSQUERY(:query)
WHERE s.search_data @@ TO_TSQUERY('english', :query)
ORDER BY topics_month desc
"
end

View File

@ -1,9 +1,10 @@
class SqlBuilder
def initialize(template)
def initialize(template,klass=nil)
@args = {}
@sql = template
@sections = {}
@klass = klass
end
[:set, :where2,:where,:order_by,:limit,:left_join,:join,:offset].each do |k|
@ -40,9 +41,17 @@ class SqlBuilder
sql.sub!("/*#{k}*/", joined)
end
ActiveRecord::Base.exec_sql(sql,@args)
if @klass
@klass.find_by_sql(ActiveRecord::Base.send(:sanitize_sql_array, [sql, @args]))
else
ActiveRecord::Base.exec_sql(sql,@args)
end
end
end
class ActiveRecord::Base
def self.sql_builder(template)
SqlBuilder.new(template, self)
end
end

View File

@ -75,6 +75,33 @@ test
.should == "<pre><code>```\nhello\n```\n</code></pre>"
end
end
describe "rel nofollow" do
before do
SiteSetting.stubs(:add_rel_nofollow_to_user_content).returns(true)
SiteSetting.stubs(:exclude_rel_nofollow_domains).returns("foo.com,bar.com")
end
it "should inject nofollow in all user provided links" do
PrettyText.cook('<a href="http://cnn.com">cnn</a>').should =~ /nofollow/
end
it "should not inject nofollow in all local links" do
(PrettyText.cook("<a href='#{Discourse.base_url}/test.html'>cnn</a>") !~ /nofollow/).should be_true
end
it "should not inject nofollow in all subdomain links" do
(PrettyText.cook("<a href='#{Discourse.base_url.sub('http://', 'http://bla.')}/test.html'>cnn</a>") !~ /nofollow/).should be_true
end
it "should not inject nofollow for foo.com" do
(PrettyText.cook("<a href='http://foo.com/test.html'>cnn</a>") !~ /nofollow/).should be_true
end
it "should not inject nofollow for bar.foo.com" do
(PrettyText.cook("<a href='http://bar.foo.com/test.html'>cnn</a>") !~ /nofollow/).should be_true
end
end
describe "Excerpt" do
it "should preserve links" do
@ -130,6 +157,7 @@ test
end
end
describe "apply cdn" do
it "should detect bare links to images and apply a CDN" do
PrettyText.apply_cdn("<a href='/hello.png'>hello</a><img src='/a.jpeg'>","http://a.com").should ==

View File

@ -4,32 +4,48 @@ require_dependency 'sql_builder'
describe SqlBuilder do
before do
@builder = SqlBuilder.new("select * from (select :a A union all select :b) as X /*where*/ /*order_by*/ /*limit*/ /*offset*/")
describe "attached" do
before do
@builder = Post.sql_builder("select * from posts /*where*/ /*limit*/")
end
it "should find a post by id" do
p = Fabricate(:post)
@builder.where('id = :id and topic_id = :topic_id', id: p.id, topic_id: p.topic_id)
p2 = @builder.exec.first
p2.id.should == p.id
p2.should == p
end
end
it "should allow for 1 param exec" do
@builder.exec(a: 1, b: 2).values[0][0].should == '1'
end
describe "detached" do
before do
@builder = SqlBuilder.new("select * from (select :a A union all select :b) as X /*where*/ /*order_by*/ /*limit*/ /*offset*/")
end
it "should allow for a single where" do
@builder.where(":a = 1")
@builder.exec(a: 1, b: 2).values[0][0].should == '1'
end
it "should allow for 1 param exec" do
@builder.exec(a: 1, b: 2).values[0][0].should == '1'
end
it "should allow where chaining" do
@builder.where(":a = 1")
@builder.where("2 = 1")
@builder.exec(a: 1, b: 2).to_a.length.should == 0
end
it "should allow for a single where" do
@builder.where(":a = 1")
@builder.exec(a: 1, b: 2).values[0][0].should == '1'
end
it "should allow order by" do
@builder.order_by("A desc").limit(1)
.exec(a:1, b:2).values[0][0].should == "2"
end
it "should allow offset" do
@builder.order_by("A desc").offset(1)
.exec(a:1, b:2).values[0][0].should == "1"
it "should allow where chaining" do
@builder.where(":a = 1")
@builder.where("2 = 1")
@builder.exec(a: 1, b: 2).to_a.length.should == 0
end
it "should allow order by" do
@builder.order_by("A desc").limit(1)
.exec(a:1, b:2).values[0][0].should == "2"
end
it "should allow offset" do
@builder.order_by("A desc").offset(1)
.exec(a:1, b:2).values[0][0].should == "1"
end
end
end

View File

@ -0,0 +1,26 @@
require 'spec_helper'
describe RobotsTxtController do
context '.index' do
it "returns noindex when indexing is disallowed" do
SiteSetting.stubs(:allow_index_in_robots_txt).returns(true)
get :index
response.should render_template :index
end
it "returns index when indexing is allowed" do
SiteSetting.stubs(:allow_index_in_robots_txt).returns(false)
get :index
response.should render_template :no_index
end
it "serves it regardless if a site is in private mode" do
SiteSetting.stubs(:allow_index_in_robots_txt).returns(true)
SiteSetting.stubs(:restrict_access).returns(true)
get :index
response.should render_template :no_index
end
end
end

View File

@ -26,6 +26,7 @@ describe SiteCustomization do
SiteCustomization.enabled_style_key.should be_nil
end
it 'finds the enabled style' do
@customization.enabled = true
@customization.save
@ -45,6 +46,16 @@ describe SiteCustomization do
end
end
it 'ensure stylesheet is on disk on first fetch' do
c = customization
c.remove_from_cache!
File.delete(c.stylesheet_fullpath)
SiteCustomization.custom_stylesheet(c.key)
File.exists?(c.stylesheet_fullpath).should == true
end
it 'should allow me to lookup a filename containing my preview stylesheet' do
SiteCustomization.custom_stylesheet(customization.key).should ==
"<link class=\"custom-css\" rel=\"stylesheet\" href=\"/stylesheet-cache/#{customization.key}.css?#{customization.stylesheet_hash}\" type=\"text/css\" media=\"screen\">"