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}}
-
{{avatar this imageSize="large" extraClasses="actor" avatarTemplatePath="avatar_template"}}
+
{{avatar this imageSize="large" extraClasses="actor" avatarTemplatePath="avatar_template" ignoreTitle="true"}}
{{date path="created_at" leaveAgo="true"}} - {{unbound name}}
+ {{unbound title}}
+ {{personalizedName name usernamePath="username"}} {{unbound description}} - #{{unbound post_number}} {{unbound title}} + #{{unbound post_number}}

{{{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("" + @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","http://a.com").should == diff --git a/spec/components/sql_builder_spec.rb b/spec/components/sql_builder_spec.rb index 5edb367b091..da46943e6d8 100644 --- a/spec/components/sql_builder_spec.rb +++ b/spec/components/sql_builder_spec.rb @@ -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 diff --git a/spec/controllers/robots_txt_controller_spec.rb b/spec/controllers/robots_txt_controller_spec.rb new file mode 100644 index 00000000000..6bd739c062d --- /dev/null +++ b/spec/controllers/robots_txt_controller_spec.rb @@ -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 diff --git a/spec/models/site_customization_spec.rb b/spec/models/site_customization_spec.rb index 68a554cf9f2..004e71e4d0f 100644 --- a/spec/models/site_customization_spec.rb +++ b/spec/models/site_customization_spec.rb @@ -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 == ""