diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee b/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee index 5db48e7cfb7..8d2d9aaf8e0 100644 --- a/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee +++ b/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee @@ -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() diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee b/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee index 340285fccaa..269278afaeb 100644 --- a/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee +++ b/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee @@ -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("#{displayDate}") - +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') diff --git a/app/assets/javascripts/discourse/templates/user/stream.js.handlebars b/app/assets/javascripts/discourse/templates/user/stream.js.handlebars index fb280f331f8..6fa60aa59b7 100644 --- a/app/assets/javascripts/discourse/templates/user/stream.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/stream.js.handlebars @@ -2,11 +2,12 @@ {{#collection contentBinding="stream" itemClass="item"}} {{#with view.content}}
{{{unbound excerpt}}} diff --git a/app/assets/javascripts/discourse/views/composer_view.js.coffee b/app/assets/javascripts/discourse/views/composer_view.js.coffee index 78330400653..66638cb9444 100644 --- a/app/assets/javascripts/discourse/views/composer_view.js.coffee +++ b/app/assets/javascripts/discourse/views/composer_view.js.coffee @@ -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(",")) diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss index 8b5653fe0ae..18f18c941f6 100644 --- a/app/assets/stylesheets/application/topic-post.css.scss +++ b/app/assets/stylesheets/application/topic-post.css.scss @@ -347,7 +347,7 @@ } } .reply-to-tab { - z-index: 999; + z-index: 980; font-size: 12px; color: $darkish_gray; display: block; diff --git a/app/assets/stylesheets/application/user.css.scss b/app/assets/stylesheets/application/user.css.scss index f0320623b85..8a91c8377ec 100644 --- a/app/assets/stylesheets/application/user.css.scss +++ b/app/assets/stylesheets/application/user.css.scss @@ -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; diff --git a/app/assets/stylesheets/vendor/bootstrap.css.scss b/app/assets/stylesheets/vendor/bootstrap.css.scss index b4e57171177..56bb4508e0b 100644 --- a/app/assets/stylesheets/vendor/bootstrap.css.scss +++ b/app/assets/stylesheets/vendor/bootstrap.css.scss @@ -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; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index dc3651fce9f..a378f907997 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bf7bcd1c5e8..d01c3ab3481 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/robots_txt_controller.rb b/app/controllers/robots_txt_controller.rb new file mode 100644 index 00000000000..e494596dc30 --- /dev/null +++ b/app/controllers/robots_txt_controller.rb @@ -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 diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index f8946bd3b30..ed19ada587b 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -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 = "" + @stylesheet_link_tag = "" end - end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 6a0720b1616..37ec0810eab 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -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, '') diff --git a/app/models/user_search.rb b/app/models/user_search.rb index 42a01fc08e0..bf068268628 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -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 \ No newline at end of file +end diff --git a/public/robots.txt b/app/views/robots_txt/index.erb similarity index 50% rename from public/robots.txt rename to app/views/robots_txt/index.erb index 085187fa58b..376f3ae7184 100644 --- a/public/robots.txt +++ b/app/views/robots_txt/index.erb @@ -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: / diff --git a/app/views/robots_txt/no_index.erb b/app/views/robots_txt/no_index.erb new file mode 100644 index 00000000000..867036ff28a --- /dev/null +++ b/app/views/robots_txt/no_index.erb @@ -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: / + + diff --git a/config/application.rb b/config/application.rb index bbe222c2c4c..85723bf2180 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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) diff --git a/config/initializers/sql_builder.rb b/config/initializers/sql_builder.rb new file mode 100644 index 00000000000..fd87e9c645e --- /dev/null +++ b/config/initializers/sql_builder.rb @@ -0,0 +1 @@ +require 'sql_builder' diff --git a/config/locales/en.yml b/config/locales/en.yml index 372c3ca2eb7..26dae763371 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index c81a86d1776..9da1c576503 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/dbs/export/empty.tar.gz b/dbs/export/empty.tar.gz deleted file mode 100644 index e654e8f5668..00000000000 Binary files a/dbs/export/empty.tar.gz and /dev/null differ diff --git a/dbs/export/try.tar.gz b/dbs/export/try.tar.gz deleted file mode 100644 index 41fffdadfaf..00000000000 Binary files a/dbs/export/try.tar.gz and /dev/null differ diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 4842cd81712..075bc543b41 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -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) diff --git a/lib/search.rb b/lib/search.rb index ab186e4eebc..e91486da791 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -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 diff --git a/lib/sql_builder.rb b/lib/sql_builder.rb index 5bf295ed6c4..c0477c77b5c 100644 --- a/lib/sql_builder.rb +++ b/lib/sql_builder.rb @@ -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 diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index cb8536fd6d0..5aaa9330ede 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -75,6 +75,33 @@ test .should == "
```\nhello\n```\n
"
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('cnn').should =~ /nofollow/
+ end
+
+ it "should not inject nofollow in all local links" do
+ (PrettyText.cook("cnn") !~ /nofollow/).should be_true
+ end
+
+ it "should not inject nofollow in all subdomain links" do
+ (PrettyText.cook("cnn") !~ /nofollow/).should be_true
+ end
+
+ it "should not inject nofollow for foo.com" do
+ (PrettyText.cook("cnn") !~ /nofollow/).should be_true
+ end
+
+ it "should not inject nofollow for bar.foo.com" do
+ (PrettyText.cook("cnn") !~ /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("hello