-
{{avatar this imageSize="large"}}
-
+
{{avatar this imageSize="large"}}
+
{{date created_at}}
diff --git a/app/assets/javascripts/discourse/views/actions_history_view.js.coffee b/app/assets/javascripts/discourse/views/actions_history_view.js.coffee
index 43de981027..50f6a30a67 100644
--- a/app/assets/javascripts/discourse/views/actions_history_view.js.coffee
+++ b/app/assets/javascripts/discourse/views/actions_history_view.js.coffee
@@ -35,12 +35,20 @@ window.Discourse.ActionsHistoryView = Em.View.extend Discourse.Presence,
if c.get('can_undo')
alsoName = Em.String.i18n("post.actions.undo", alsoName: c.get('actionType.alsoNameLower'))
- buffer.push("
#{alsoName}.")
+ buffer.push("
#{alsoName}.")
+
+ if c.get('can_clear_flags')
+ buffer.push("
#{Em.String.i18n("post.actions.clear_flags",count: c.count)}.")
+
buffer.push("
")
click: (e) ->
$target = $(e.target)
+ if actionTypeId = $target.data('clear-flags')
+ @get('controller').clearFlags(@content.findProperty('id', actionTypeId))
+ return false
+
# User wants to know who actioned it
if actionTypeId = $target.data('who-acted')
@get('controller').whoActed(@content.findProperty('id', actionTypeId))
@@ -54,4 +62,4 @@ window.Discourse.ActionsHistoryView = Em.View.extend Discourse.Presence,
@get('controller').undoAction(@content.findProperty('id', actionTypeId))
return false
- false
\ No newline at end of file
+ false
diff --git a/app/assets/javascripts/discourse/views/composer_view.js.coffee b/app/assets/javascripts/discourse/views/composer_view.js.coffee
index 611ffd2a04..8e7e0df825 100644
--- a/app/assets/javascripts/discourse/views/composer_view.js.coffee
+++ b/app/assets/javascripts/discourse/views/composer_view.js.coffee
@@ -192,7 +192,7 @@ window.Discourse.ComposerView = window.Discourse.View.extend
done: (e, data) =>
@set('loadingImage', false)
upload = data.result
- html = "

"
+ html = "

"
@addMarkdown(html)
fail: (e, data) =>
diff --git a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee b/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee
index 7f6d5098dd..a0f1769560 100644
--- a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee
+++ b/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee
@@ -3,6 +3,8 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco
title: Em.String.i18n('create_account.title')
uniqueUsernameValidation: null
complete: false
+ accountPasswordConfirm: 0
+ accountChallenge: 0
submitDisabled: (->
@@ -22,6 +24,8 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco
# If blank, fail without a reason
return Discourse.InputValidation.create(failed: true) if @blank('accountName')
+ @fetchConfirmationValue() if @get('accountPasswordConfirm') == 0
+
# If too short
return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.name.too_short')) if @get('accountName').length < 3
@@ -120,13 +124,22 @@ window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Disco
).property('accountPassword')
+ fetchConfirmationValue: ->
+ $.ajax
+ url: '/users/hp.json',
+ success: (json) =>
+ @set('accountPasswordConfirm', json.value)
+ @set('accountChallenge', json.challenge.split("").reverse().join(""))
+
createAccount: ->
name = @get('accountName')
email = @get('accountEmail')
password = @get('accountPassword')
username = @get('accountUsername')
+ passwordConfirm = @get('accountPasswordConfirm')
+ challenge = @get('accountChallenge')
- Discourse.User.createAccount(name, email, password, username).then (result) =>
+ Discourse.User.createAccount(name, email, password, username, passwordConfirm, challenge).then (result) =>
if result.success
@flash(result.message)
diff --git a/app/assets/stylesheets/application/modal.css.scss b/app/assets/stylesheets/application/modal.css.scss
index 2f62896620..7361893f99 100644
--- a/app/assets/stylesheets/application/modal.css.scss
+++ b/app/assets/stylesheets/application/modal.css.scss
@@ -152,6 +152,9 @@
margin-bottom: 20px;
}
}
+ .password-confirmation {
+ display: none;
+ }
}
#move-selected {
diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss
index 6c774072b1..fd43495604 100644
--- a/app/assets/stylesheets/application/topic-post.css.scss
+++ b/app/assets/stylesheets/application/topic-post.css.scss
@@ -320,6 +320,12 @@
font-size: 14px;
line-height: 18px;
}
+
+ h3.moderator a {
+ background-color: #ffe;
+ border: 1px solid #ffd;
+ }
+
div {
display: block;
}
diff --git a/app/assets/stylesheets/vendor/bootstrap.css.scss b/app/assets/stylesheets/vendor/bootstrap.css.scss
index 813d5da2c0..b4e5717117 100644
--- a/app/assets/stylesheets/vendor/bootstrap.css.scss
+++ b/app/assets/stylesheets/vendor/bootstrap.css.scss
@@ -1418,7 +1418,13 @@ body {
-moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
}
- :-moz-placeholder, :-ms-input-placeholder, ::-webkit-input-placeholder {
+ :-moz-placeholder {
+ color: #999999;
+ }
+ ::-webkit-input-placeholder {
+ color: #999999;
+ }
+ :-ms-input-placeholder {
color: #999999;
}
.help-block, .help-inline {
diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb
index 2647029c99..4906327de4 100644
--- a/app/controllers/admin/flags_controller.rb
+++ b/app/controllers/admin/flags_controller.rb
@@ -3,7 +3,6 @@ require_dependency 'sql_builder'
class Admin::FlagsController < Admin::AdminController
def index
-
sql = SqlBuilder.new "select p.id, t.title, p.cooked, p.user_id, p.topic_id, p.post_number, p.hidden, t.visible topic_visible
from posts p
join topics t on t.id = topic_id
@@ -72,7 +71,7 @@ from post_actions a
sql.where('deleted_at is null')
end
- actions = sql.exec.each do |action|
+ sql.exec.each do |action|
p = map[action["post_id"]]
p[:post_actions] ||= []
p[:post_actions] << action
@@ -92,7 +91,6 @@ where id in (?)"
}
render json: MultiJson.dump({users: users, posts: posts})
-
end
def clear
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ccdcf23673..18fb2898fb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -135,14 +135,33 @@ class ApplicationController < ActionController::Base
render json: MultiJson.dump(obj)
end
+ def can_cache_content?
+ # Don't cache unless we're in production mode
+ return false unless Rails.env.production?
+
+ # Don't cache logged in users
+ return false if current_user.present?
+
+ # Don't cache if there's restricted access
+ return false if SiteSetting.restrict_access?
+
+ true
+ end
+
+ # Our custom cache method
+ def discourse_expires_in(time_length)
+ return unless can_cache_content?
+ expires_in time_length, public: true
+ end
+
# Helper method - if no logged in user (anonymous), use Rails' conditional GET
# support. Should be very fast behind a cache.
def anonymous_etag(*args)
- if current_user.blank? and Rails.env.production?
+ if can_cache_content?
yield if stale?(*args)
# Add a one minute expiry
- expires_in 1.minute, :public => true
+ expires_in 1.minute, public: true
else
yield
end
diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb
index 005b3d14c0..5d58635024 100644
--- a/app/controllers/list_controller.rb
+++ b/app/controllers/list_controller.rb
@@ -58,8 +58,7 @@ class ListController < ApplicationController
draft = Draft.get(current_user, list.draft_key, list.draft_sequence) if current_user
list.draft = draft
- # Add expiry of 1 minute for anonymous
- expires_in 1.minute, :public => true if current_user.blank?
+ discourse_expires_in 1.minute
respond_to do |format|
format.html do
diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb
index 9031a4d03e..f9d0e722e7 100644
--- a/app/controllers/post_actions_controller.rb
+++ b/app/controllers/post_actions_controller.rb
@@ -45,6 +45,21 @@ class PostActionsController < ApplicationController
render nothing: true
end
+ def clear_flags
+ requires_parameter(:post_action_type_id)
+ raise Discourse::InvalidAccess unless guardian.is_admin?
+
+ PostAction.clear_flags!(@post, current_user.id, params[:post_action_type_id].to_i)
+ @post.reload
+
+ if @post.is_flagged?
+ render json: {success: true, hidden: true}
+ else
+ @post.unhide!
+ render json: {success: true, hidden: false}
+ end
+ end
+
private
def fetch_post_from_params
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 877470553c..82138519c5 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -123,6 +123,12 @@ class UsersController < ApplicationController
end
def create
+
+ if params[:password_confirmation] != honeypot_value or params[:challenge] != challenge_value.try(:reverse)
+ # Don't give any indication that we caught you in the honeypot
+ return render(:json => {success: true, active: false, message: I18n.t("login.activate_email", email: params[:email]) })
+ end
+
user = User.new
user.name = params[:name]
user.email = params[:email]
@@ -183,6 +189,10 @@ class UsersController < ApplicationController
render json: {errors: [I18n.t("mothership.access_token_problem")]}
end
+ def get_honeypot_value
+ render json: {value: honeypot_value, challenge: challenge_value}
+ end
+
# all avatars are funneled through here
def avatar
@@ -320,6 +330,14 @@ class UsersController < ApplicationController
private
+ def honeypot_value
+ Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{Discourse::Application.config.secret_token}")[0,15]
+ end
+
+ def challenge_value
+ '3019774c067cc2b'
+ end
+
def fetch_user_from_params
username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '')
diff --git a/app/helpers/forum_helper.rb b/app/helpers/forum_helper.rb
deleted file mode 100644
index cfbd978b92..0000000000
--- a/app/helpers/forum_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ForumHelper
-end
diff --git a/app/helpers/list_helper.rb b/app/helpers/list_helper.rb
deleted file mode 100644
index eaa1d0e156..0000000000
--- a/app/helpers/list_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ListHelper
-end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
deleted file mode 100644
index 7342393a70..0000000000
--- a/app/helpers/notifications_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module NotificationsHelper
-end
diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb
deleted file mode 100644
index 721eba5f46..0000000000
--- a/app/helpers/user_notifications_helper.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module UserNotificationsHelper
-
-end
diff --git a/app/models/post.rb b/app/models/post.rb
index 1957cd5322..42d7852019 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -38,6 +38,7 @@ class Post < ActiveRecord::Base
validates_presence_of :raw, :user_id, :topic_id
validates :raw, length: {in: SiteSetting.min_post_length..SiteSetting.max_post_length}
+ validate :raw_quality
validate :max_mention_validator
validate :max_images_validator
validate :max_links_validator
@@ -68,6 +69,18 @@ class Post < ActiveRecord::Base
self.raw.strip! if self.raw.present?
end
+ def raw_quality
+
+ sentinel = TextSentinel.new(self.raw, min_entropy: SiteSetting.body_min_entropy)
+ if sentinel.valid?
+ # It's possible the sentinel has cleaned up the title a bit
+ self.raw = sentinel.text
+ else
+ errors.add(:raw, I18n.t(:is_invalid)) unless sentinel.valid?
+ end
+ end
+
+
# Stop us from posting the same thing too quickly
def unique_post_validator
return if SiteSetting.unique_posts_mins == 0
@@ -250,6 +263,17 @@ class Post < ActiveRecord::Base
result
end
+ def is_flagged?
+ post_actions.where('post_action_type_id in (?) and deleted_at is null', PostActionType.FlagTypes).count != 0
+ end
+
+ def unhide!
+ self.hidden = false
+ self.hidden_reason_id = nil
+ self.topic.update_attributes(visible: true)
+ self.save
+ end
+
# Update the body of a post. Will create a new version when appropriate
def revise(updated_by, new_raw, opts={})
diff --git a/app/models/post_action.rb b/app/models/post_action.rb
index 5c62edd7da..91e5bc7c98 100644
--- a/app/models/post_action.rb
+++ b/app/models/post_action.rb
@@ -2,6 +2,8 @@ require_dependency 'rate_limiter'
require_dependency 'system_message'
class PostAction < ActiveRecord::Base
+ class AlreadyFlagged < StandardError; end
+
include RateLimiter::OnCreateRecord
attr_accessible :deleted_at, :post_action_type_id, :post_id, :user_id, :post, :user, :post_action_type, :message
@@ -45,10 +47,14 @@ class PostAction < ActiveRecord::Base
user_actions
end
- def self.clear_flags!(post, moderator_id)
+ def self.clear_flags!(post, moderator_id, action_type_id = nil)
# -1 is the automatic system cleary
- actions = moderator_id == -1 ? PostActionType.AutoActionFlagTypes : PostActionType.FlagTypes
+ actions = if action_type_id
+ [action_type_id]
+ else
+ moderator_id == -1 ? PostActionType.AutoActionFlagTypes : PostActionType.FlagTypes
+ end
PostAction.exec_sql('update post_actions set deleted_at = ?, deleted_by = ?
where post_id = ? and deleted_at is null and post_action_type_id in (?)',
@@ -115,6 +121,15 @@ class PostAction < ActiveRecord::Base
end
end
+ before_create do
+ if is_flag?
+ if PostAction.where('user_id = ? and post_id = ? and post_action_type_id in (?) and deleted_at is null',
+ self.user_id, self.post_id, PostActionType.FlagTypes).exists?
+ raise AlreadyFlagged
+ end
+ end
+ end
+
after_save do
# Update denormalized counts
diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb
index 39b58a2a9f..a347c4fba3 100644
--- a/app/models/post_action_type.rb
+++ b/app/models/post_action_type.rb
@@ -1,4 +1,5 @@
class PostActionType < ActiveRecord::Base
+
attr_accessible :id, :is_flag, :name_key, :icon
def self.ordered
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index f5091b5d66..e7b88bed51 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -126,6 +126,11 @@ class SiteSetting < ActiveRecord::Base
setting(:basic_requires_read_posts, 100)
setting(:basic_requires_time_spent_mins, 30)
+ # Entropy checks
+ setting(:title_min_entropy, 10)
+ setting(:body_min_entropy, 7)
+ setting(:max_word_length, 30)
+
def self.call_mothership?
self.enforce_global_nicknames? and self.discourse_org_access_key.present?
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 13a93a661a..6d6e162a78 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -2,6 +2,7 @@ require_dependency 'slug'
require_dependency 'avatar_lookup'
require_dependency 'topic_view'
require_dependency 'rate_limiter'
+require_dependency 'text_sentinel'
class Topic < ActiveRecord::Base
include RateLimiter::OnCreateRecord
@@ -18,12 +19,14 @@ class Topic < ActiveRecord::Base
rate_limit :limit_topics_per_day
rate_limit :limit_private_messages_per_day
+ validate :title_quality
validates_presence_of :title
validates :title, length: {in: SiteSetting.min_topic_title_length..SiteSetting.max_topic_title_length}
serialize :meta_data, ActiveRecord::Coders::Hstore
validate :unique_title
+
belongs_to :category
has_many :posts
@@ -112,6 +115,23 @@ class Topic < ActiveRecord::Base
errors.add(:title, I18n.t(:has_already_been_used)) if finder.exists?
end
+
+ def title_quality
+ # We don't care about quality on private messages
+ return if private_message?
+
+ sentinel = TextSentinel.new(title,
+ min_entropy: SiteSetting.title_min_entropy,
+ max_word_length: SiteSetting.max_word_length,
+ remove_interior_spaces: true)
+ if sentinel.valid?
+ # It's possible the sentinel has cleaned up the title a bit
+ self.title = sentinel.text
+ else
+ errors.add(:title, I18n.t(:is_invalid)) unless sentinel.valid?
+ end
+ end
+
def new_version_required?
return true if title_changed?
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 45d01eedb2..bf74deea47 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -37,6 +37,7 @@ class PostSerializer < ApplicationSerializer
:bookmarked,
:raw,
:actions_summary,
+ :moderator?,
:avatar_template,
:user_id,
:draft_sequence,
@@ -45,6 +46,10 @@ class PostSerializer < ApplicationSerializer
:deleted_at
+ def moderator?
+ object.user.has_trust_level?(:moderator)
+ end
+
def avatar_template
object.user.avatar_template
end
@@ -140,6 +145,8 @@ class PostSerializer < ApplicationSerializer
next if !action_summary[:can_act] && !scope.current_user
+ action_summary[:can_clear_flags] = scope.is_admin? && PostActionType.FlagTypes.include?(id)
+
if post_actions.present? and post_actions.has_key?(id)
action_summary[:acted] = true
action_summary[:can_undo] = scope.can_delete?(post_actions[id])
diff --git a/app/views/static/faq.html.erb b/app/views/static/faq.html.erb
index 88b5722382..7c49b771cf 100644
--- a/app/views/static/faq.html.erb
+++ b/app/views/static/faq.html.erb
@@ -130,7 +130,7 @@
Terms of Service
- Yes, legalese is boring, but we must protect ourselves (and by extension, you and your data) against unfriendly folks. So, like everyone else, we have a Terms of Service TOS describing your (and our) behavior and rights related to content, privacy, and laws. To use this service, you must agree to abide by the TOS.
+ Yes, legalese is boring, but we must protect ourselves (and by extension, you and your data) against unfriendly folks. So, like everyone else, we have a Terms of Service TOS describing your (and our) behavior and rights related to content, privacy, and laws. To use this service, you must agree to abide by the TOS.
diff --git a/config/application.rb b/config/application.rb
index 0f3b40e2ed..85723bf218 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -93,10 +93,6 @@ module Discourse
# So open id logs somewhere sane
config.after_initialize do
OpenID::Util.logger = Rails.logger
-
- # latest possible so earliest in the stack
- # require 'rack/message_bus'
- # config.middleware.insert(0, Rack::MessageBus)
end
end
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4304928806..27c3ff8e3b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,6 +8,8 @@ en:
too_many_links: "has too many links"
just_posted_that: "is too similar to what you recently posted"
has_already_been_used: "has already been used"
+ invalid_characters: "contains invalid characters"
+ is_invalid: "is invalid; try to be a little more descriptive"
activerecord:
attributes:
@@ -301,6 +303,10 @@ en:
email_time_window_mins: "How many minutes we wait before sending a user mail, to give them a chance to see it first."
flush_timings_secs: "How frequently we flush timing data to the server, in seconds."
+ max_word_length: "The maximum word length in a topic title"
+ title_min_entropy: "The minimum entropy for a topic title"
+ body_min_entropy: "The minimum entropy for post body"
+
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "Type to Filter..."
@@ -313,6 +319,10 @@ en:
title: "Flags"
old: "Old"
active: "Active"
+ clear: "Clear Flags"
+ clear_title: "dismiss all flags on this post (will unhide hidden posts)"
+ delete: "Delete Post"
+ delete_title: "delete post (if its the first post delete topic)"
customize:
title: "Customize"
@@ -688,7 +698,7 @@ en:
no_posted: "You haven't posted in any topics yet."
no_popular: "There are no popular topics. That's sad."
- topic:
+ topic:
create_in: 'Create {{categoryName}} Topic'
create: 'Create Topic'
create_long: 'Create a new Topic'
@@ -852,6 +862,9 @@ en:
actions:
flag: 'Flag'
+ clear_flags:
+ one: "Clear flag"
+ other: "Clear flags"
it_too: "{{alsoName}} it too"
undo: "Undo {{alsoName}}"
by_you_and_others:
diff --git a/config/routes.rb b/config/routes.rb
index 3331be7f08..b0a8db0929 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -80,6 +80,7 @@ Discourse::Application.routes.draw do
put 'users/password-reset/:token' => 'users#password_reset'
get 'users/activate-account/:token' => 'users#activate_account'
get 'users/authorize-email/:token' => 'users#authorize_email'
+ get 'users/hp' => 'users#get_honeypot_value'
get 'user_preferences' => 'users#user_preferences_redirect'
get 'users/:username/private-messages' => 'user_actions#private_messages', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
@@ -132,6 +133,7 @@ Discourse::Application.routes.draw do
resources :post_actions do
collection do
get 'users' => 'post_actions#users'
+ post 'clear_flags' => 'post_actions#clear_flags'
end
end
resources :user_actions
diff --git a/db/fixtures/post_action_types.rb b/db/fixtures/post_action_types.rb
index 863fc8c3c5..820b05f76f 100644
--- a/db/fixtures/post_action_types.rb
+++ b/db/fixtures/post_action_types.rb
@@ -27,6 +27,13 @@ PostActionType.seed do |s|
s.position = 4
end
+PostActionType.seed do |s|
+ s.id = PostActionType.Types[:vote]
+ s.name_key = 'vote'
+ s.is_flag = false
+ s.position = 5
+end
+
PostActionType.seed do |s|
s.id = PostActionType.Types[:spam]
s.name_key = 'spam'
diff --git a/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb b/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb
index 7f313de0f9..99d646ae78 100644
--- a/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb
+++ b/db/migrate/20120921162512_add_meta_data_to_forum_threads.rb
@@ -1,6 +1,6 @@
class AddMetaDataToForumThreads < ActiveRecord::Migration
def change
- execute "CREATE EXTENSION hstore"
+ execute "CREATE EXTENSION IF NOT EXISTS hstore"
add_column :forum_threads, :meta_data, :hstore
end
end
diff --git a/lib/guardian.rb b/lib/guardian.rb
index d5d406f65b..72f475a4ee 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -1,4 +1,4 @@
-# The guardian is responsible for confirming access to various site resources and opreations
+# The guardian is responsible for confirming access to various site resources and operations
class Guardian
attr_reader :user
diff --git a/lib/multisite_i18n.rb b/lib/multisite_i18n.rb
new file mode 100644
index 0000000000..2d59c0c737
--- /dev/null
+++ b/lib/multisite_i18n.rb
@@ -0,0 +1,31 @@
+# Allow us to override i18n keys based on the current site you're viewing.
+module MultisiteI18n
+
+ class << self
+
+ # It would be nice if there was an easier way to detect if a key is missing.
+ def translation_or_nil(key, opts)
+ missing_text = "missing multisite translation"
+ result = I18n.t(key, opts.merge(default: missing_text))
+ return nil if result == missing_text
+ result
+ end
+
+ def site_translate(current_site, key, opts=nil)
+ opts ||= {}
+ translation = MultisiteI18n.translation_or_nil("#{current_site || ""}.#{key}", opts)
+ if translation.blank?
+ return I18n.t(key, opts)
+ else
+ return translation
+ end
+ end
+
+ def t(*args)
+ MultisiteI18n.site_translate(RailsMultisite::ConnectionManagement.current_db, *args)
+ end
+
+ alias :translate :t
+ end
+
+end
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 8a1bd85999..89e49d9aeb 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -31,8 +31,8 @@ module SiteSettingExtension
end
# just like a setting, except that it is available in javascript via DiscourseSession
- def client_setting(name, defualt = nil, type = nil)
- setting(name,defualt,type)
+ def client_setting(name, default = nil, type = nil)
+ setting(name,default,type)
@@client_settings ||= []
@@client_settings << name
end
diff --git a/lib/system_message.rb b/lib/system_message.rb
index 92aaf39903..ed54356f51 100644
--- a/lib/system_message.rb
+++ b/lib/system_message.rb
@@ -1,5 +1,6 @@
# Handle sending a message to a user from the system.
require_dependency 'post_creator'
+require_dependency 'multisite_i18n'
class SystemMessage
@@ -14,20 +15,20 @@ class SystemMessage
def create(type, params = {})
defaults = {site_name: SiteSetting.title,
- username: @recipient.username,
- user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences",
- new_user_tips: I18n.t("system_messages.usage_tips.text_body_template"),
- site_password: "",
- base_url: Discourse.base_url}
+ username: @recipient.username,
+ user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences",
+ new_user_tips: MultisiteI18n.t("system_messages.usage_tips.text_body_template"),
+ site_password: "",
+ base_url: Discourse.base_url}
params = defaults.merge(params)
if SiteSetting.restrict_access?
- params[:site_password] = I18n.t('system_messages.site_password', access_password: SiteSetting.access_password)
+ params[:site_password] = MultisiteI18n.t('system_messages.site_password', access_password: SiteSetting.access_password)
end
- title = I18n.t("system_messages.#{type}.subject_template", params)
- raw_body = I18n.t("system_messages.#{type}.text_body_template", params)
+ title = MultisiteI18n.t("system_messages.#{type}.subject_template", params)
+ raw_body = MultisiteI18n.t("system_messages.#{type}.text_body_template", params)
PostCreator.create(SystemMessage.system_user,
raw: raw_body,
diff --git a/lib/text_sentinel.rb b/lib/text_sentinel.rb
new file mode 100644
index 0000000000..3af20efd0f
--- /dev/null
+++ b/lib/text_sentinel.rb
@@ -0,0 +1,56 @@
+require 'iconv'
+
+#
+# Given a string, tell us whether or not is acceptable. Also, remove stuff we don't like
+# such as leading / trailing space.
+#
+class TextSentinel
+
+ attr_accessor :text
+
+ def self.non_symbols_regexp
+ /[\ -\/\[-\`\:-\@\{-\~]/m
+ end
+
+ def initialize(text, opts=nil)
+ if text.present?
+ @text = Iconv.new('UTF-8//IGNORE', 'UTF-8').iconv(text.dup)
+ end
+
+ @opts = opts || {}
+
+ if @text.present?
+ @text.strip!
+ @text.gsub!(/ +/m, ' ') if @opts[:remove_interior_spaces]
+ end
+ end
+
+ # Entropy is a number of how many unique characters the string needs.
+ def entropy
+ return 0 if @text.blank?
+ @entropy ||= @text.each_char.to_a.uniq.size
+ end
+
+ def valid?
+
+ # Blank strings are not valid
+ return false if @text.blank?
+
+ # Entropy check if required
+ return false if @opts[:min_entropy].present? and (entropy < @opts[:min_entropy])
+
+ # We don't have a comprehensive list of symbols, but this will eliminate some noise
+ non_symbols = @text.gsub(TextSentinel.non_symbols_regexp, '').size
+ return false if non_symbols == 0
+
+ # Don't allow super long strings without spaces
+
+ return false if @opts[:max_word_length] and @text =~ /\w{#{@opts[:max_word_length]},}(\s|$)/
+
+ # We don't allow all upper case content
+ return false if @text == @text.upcase
+
+ true
+ end
+
+end
diff --git a/spec/components/multisite_i18n_spec.rb b/spec/components/multisite_i18n_spec.rb
new file mode 100644
index 0000000000..abdedac99d
--- /dev/null
+++ b/spec/components/multisite_i18n_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require_dependency 'multisite_i18n'
+
+describe MultisiteI18n do
+
+ before do
+ I18n.stubs(:t).with('test', {}).returns('default i18n')
+ MultisiteI18n.stubs(:translation_or_nil).with("default.test", {}).returns(nil)
+ MultisiteI18n.stubs(:translation_or_nil).with("other_site.test", {}).returns("overwritten i18n")
+ end
+
+ context "no value for a multisite key" do
+ it "it returns the default i18n key" do
+ MultisiteI18n.site_translate('default', 'test').should == "default i18n"
+ end
+ end
+
+ context "with a value for the multisite key" do
+ it "returns the overwritten value" do
+ MultisiteI18n.site_translate('other_site', 'test').should == "overwritten i18n"
+ end
+ end
+
+ context "when we call t, it uses the current site" do
+
+ it "returns the original" do
+ MultisiteI18n.t('test').should == 'default i18n'
+ end
+
+ it "returns the overwritten" do
+ RailsMultisite::ConnectionManagement.stubs(:current_db).returns('other_site')
+ MultisiteI18n.t('test').should == "overwritten i18n"
+ end
+
+ end
+
+end
\ No newline at end of file
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index 2096062108..5c5b074ecd 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -11,7 +11,7 @@ describe PostCreator do
context 'new topic' do
let(:category) { Fabricate(:category, user: user) }
- let(:basic_topic_params) { {title: 'hello world', raw: 'my name is fred', archetype_id: 1} }
+ let(:basic_topic_params) { {title: 'hello world topic', raw: 'my name is fred', archetype_id: 1} }
let(:image_sizes) { {'http://an.image.host/image.jpg' => {'width' => 111, 'height' => 222}} }
let(:creator) { PostCreator.new(user, basic_topic_params) }
@@ -83,7 +83,7 @@ describe PostCreator do
let(:target_user1) { Fabricate(:coding_horror) }
let(:target_user2) { Fabricate(:moderator) }
let(:post) do
- PostCreator.create(user, title: 'hi there',
+ PostCreator.create(user, title: 'hi there welcome to my topic',
raw: 'this is my awesome message',
archetype: Archetype.private_message,
target_usernames: [target_user1.username, target_user2.username].join(','))
diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb
index c2699d9897..717f63b7cf 100644
--- a/spec/components/search_spec.rb
+++ b/spec/components/search_spec.rb
@@ -14,7 +14,7 @@ describe Search do
context 'post indexing observer' do
before do
@category = Fabricate(:category, name: 'america')
- @topic = Fabricate(:topic, title: 'sam test', category: @category)
+ @topic = Fabricate(:topic, title: 'sam test topic', category: @category)
@post = Fabricate(:post, topic: @topic, raw: 'this fun test
')
@indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"]
end
@@ -29,7 +29,7 @@ describe Search do
end
it "should pick up on title updates" do
- @topic.title = "harpi"
+ @topic.title = "harpi is the new title"
@topic.save!
@indexed = Topic.exec_sql("select search_data from posts_search where id = #{@post.id}").first["search_data"]
diff --git a/spec/components/text_sentinel_spec.rb b/spec/components/text_sentinel_spec.rb
new file mode 100644
index 0000000000..7467395145
--- /dev/null
+++ b/spec/components/text_sentinel_spec.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'text_sentinel'
+require 'iconv'
+
+describe TextSentinel do
+
+
+ context "entropy" do
+
+
+ it "returns 0 for an empty string" do
+ TextSentinel.new("").entropy.should == 0
+ end
+
+ it "returns 0 for a nil string" do
+ TextSentinel.new(nil).entropy.should == 0
+ end
+
+ it "returns 1 for a string with many leading spaces" do
+ TextSentinel.new((" " * 10) + "x").entropy.should == 1
+ end
+
+ it "returns 1 for one char, even repeated" do
+ TextSentinel.new("a" * 10).entropy.should == 1
+ end
+
+ it "returns an accurate count of many chars" do
+ TextSentinel.new("evil trout is evil").entropy.should == 10
+ end
+
+ end
+
+ context "cleaning up" do
+
+ it "strips leading or trailing whitespace" do
+ TextSentinel.new(" \t test \t ").text.should == "test"
+ end
+
+ it "allows utf-8 chars" do
+ TextSentinel.new("йȝîûηыეமிᚉ⠛").text.should == "йȝîûηыეமிᚉ⠛"
+ end
+
+ context "interior spaces" do
+
+ let(:spacey_string) { "hello there's weird spaces here." }
+
+ it "ignores intra spaces by default" do
+ TextSentinel.new(spacey_string).text.should == spacey_string
+ end
+
+ it "fixes intra spaces when enabled" do
+ TextSentinel.new(spacey_string, remove_interior_spaces: true).text.should == "hello there's weird spaces here."
+ end
+
+ end
+
+ end
+
+ context "validity" do
+
+ let(:valid_string) { "This is a cool topic about Discourse" }
+
+ it "allows a valid string" do
+ TextSentinel.new(valid_string).should be_valid
+ end
+
+ it "doesn't allow all caps topics" do
+ TextSentinel.new(valid_string.upcase).should_not be_valid
+ end
+
+ it "enforces the minimum entropy" do
+ TextSentinel.new(valid_string, min_entropy: 16).should be_valid
+ end
+
+ it "enforces the minimum entropy" do
+ TextSentinel.new(valid_string, min_entropy: 17).should_not be_valid
+ end
+
+ it "doesn't allow a long alphanumeric string with no spaces" do
+ TextSentinel.new("jfewjfoejwfojeojfoejofjeo38493824jfkjewfjeoifijeoijfoejofjeojfoewjfo834988394032jfiejoijofijeojfeojfojeofjewojfojeofjeowjfojeofjeojfoe3898439849032jfeijfwoijfoiewj",
+ max_word_length: 30).should_not be_valid
+ end
+
+ it "doesn't except junk symbols as a string" do
+ TextSentinel.new("[[[").should_not be_valid
+ TextSentinel.new("<<<").should_not be_valid
+ TextSentinel.new("{{$!").should_not be_valid
+ end
+
+
+ end
+
+
+end
\ No newline at end of file
diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb
index e0494ebdb5..be06ed5052 100644
--- a/spec/components/topic_query_spec.rb
+++ b/spec/components/topic_query_spec.rb
@@ -11,11 +11,11 @@ describe TopicQuery do
let(:admin) { Fabricate(:moderator) }
context 'a bunch of topics' do
- let!(:regular_topic) { Fabricate(:topic, title: 'regular', user: creator, bumped_at: 15.minutes.ago) }
- let!(:pinned_topic) { Fabricate(:topic, title: 'pinned', user: creator, pinned: true, bumped_at: 10.minutes.ago) }
- let!(:archived_topic) { Fabricate(:topic, title: 'archived', user: creator, archived: true, bumped_at: 6.minutes.ago) }
- let!(:invisible_topic) { Fabricate(:topic, title: 'invisible', user: creator, visible: false, bumped_at: 5.minutes.ago) }
- let!(:closed_topic) { Fabricate(:topic, title: 'closed', user: creator, closed: true, bumped_at: 1.minute.ago) }
+ let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) }
+ let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned: true, bumped_at: 10.minutes.ago) }
+ let!(:archived_topic) { Fabricate(:topic, title: 'this is an archived topic', user: creator, archived: true, bumped_at: 6.minutes.ago) }
+ let!(:invisible_topic) { Fabricate(:topic, title: 'this is an invisible topic', user: creator, visible: false, bumped_at: 5.minutes.ago) }
+ let!(:closed_topic) { Fabricate(:topic, title: 'this is a closed topic', user: creator, closed: true, bumped_at: 1.minute.ago) }
context 'list_popular' do
let(:topics) { topic_query.list_popular.topics }
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 961713e647..f6bac266fd 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -334,9 +334,9 @@ describe TopicsController do
end
it 'allows a change of title' do
- xhr :put, :update, topic_id: @topic.id, slug: @topic.title, title: 'new title'
+ xhr :put, :update, topic_id: @topic.id, slug: @topic.title, title: 'this is a new title for the topic'
@topic.reload
- @topic.title.should == 'new title'
+ @topic.title.should == 'this is a new title for the topic'
end
it 'triggers a change of category' do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index ac076be5ef..b67f6f7af2 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -2,6 +2,11 @@ require 'spec_helper'
describe UsersController do
+ before do
+ UsersController.any_instance.stubs(:honeypot_value).returns(nil)
+ UsersController.any_instance.stubs(:challenge_value).returns(nil)
+ end
+
describe '.show' do
let!(:user) { log_in }
@@ -339,7 +344,41 @@ describe UsersController do
User.where(username: @user.username).first.active.should be_false
end
end
-
+
+ shared_examples_for 'honeypot fails' do
+ it 'should not create a new user' do
+ expect {
+ xhr :post, :create, create_params
+ }.to_not change { User.count }
+ end
+
+ it 'should not send an email' do
+ User.any_instance.expects(:enqueue_welcome_message).never
+ xhr :post, :create, create_params
+ end
+
+ it 'should say it was successful' do
+ xhr :post, :create, create_params
+ json = JSON::parse(response.body)
+ json["success"].should be_true
+ end
+ end
+
+ context 'when honeypot value is wrong' do
+ before do
+ UsersController.any_instance.stubs(:honeypot_value).returns('abc')
+ end
+ let(:create_params) { {:name => @user.name, :username => @user.username, :password => "strongpassword", :email => @user.email, :password_confirmation => 'wrong'} }
+ it_should_behave_like 'honeypot fails'
+ end
+
+ context 'when challenge answer is wrong' do
+ before do
+ UsersController.any_instance.stubs(:challenge_value).returns('abc')
+ end
+ let(:create_params) { {:name => @user.name, :username => @user.username, :password => "strongpassword", :email => @user.email, :challenge => 'abc'} }
+ it_should_behave_like 'honeypot fails'
+ end
end
context '.username' do
diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb
index 10c6399000..6eb7827728 100644
--- a/spec/models/post_action_spec.rb
+++ b/spec/models/post_action_spec.rb
@@ -111,6 +111,13 @@ describe PostAction do
describe 'flagging' do
+ it 'does not allow you to flag stuff with 2 reasons' do
+ post = Fabricate(:post)
+ u1 = Fabricate(:evil_trout)
+ PostAction.act(u1, post, PostActionType.Types[:spam])
+ lambda { PostAction.act(u1, post, PostActionType.Types[:off_topic]) }.should raise_error(PostAction::AlreadyFlagged)
+ end
+
it 'should update counts when you clear flags' do
post = Fabricate(:post)
u1 = Fabricate(:evil_trout)
diff --git a/spec/models/post_alert_observer_spec.rb b/spec/models/post_alert_observer_spec.rb
index a1c9de1ef2..7b6ad2cf35 100644
--- a/spec/models/post_alert_observer_spec.rb
+++ b/spec/models/post_alert_observer_spec.rb
@@ -30,7 +30,7 @@ describe PostAlertObserver do
context 'when editing a post' do
it 'notifies a user of the revision' do
lambda {
- post.revise(evil_trout, "world")
+ post.revise(evil_trout, "world is the new body of the message")
}.should change(post.user.notifications, :count).by(1)
end
end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index 682200f1a3..ec698f02e0 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -54,12 +54,24 @@ describe Post do
topic.user.trust_level = TrustLevel.Levels[:moderator]
Fabricate.build(:post, post_args).should be_valid
end
-
-
end
end
+ describe 'flagging helpers' do
+ it 'isFlagged is accurate' do
+ post = Fabricate(:post)
+ user = Fabricate(:coding_horror)
+ PostAction.act(user, post, PostActionType.Types[:off_topic])
+
+ post.reload
+ post.is_flagged?.should == true
+
+ PostAction.remove_act(user, post, PostActionType.Types[:off_topic])
+ post.reload
+ post.is_flagged?.should == false
+ end
+ end
describe 'message bus' do
it 'enqueues the post on the message bus' do
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index a65d0350d6..af9e9461b4 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -1,11 +1,10 @@
+# encoding: UTF-8
+
require 'spec_helper'
describe Topic do
it { should validate_presence_of :title }
- it { should_not allow_value("x" * (SiteSetting.max_topic_title_length + 1)).for(:title) }
- it { should_not allow_value("x").for(:title) }
- it { should_not allow_value((" " * SiteSetting.min_topic_title_length) + "x").for(:title) }
it { should belong_to :category }
it { should belong_to :user }
@@ -24,6 +23,30 @@ describe Topic do
it { should rate_limit }
+ context '.title_quality' do
+
+ it "strips a title when identifying length" do
+ Fabricate.build(:topic, title: (" " * SiteSetting.min_topic_title_length) + "x").should_not be_valid
+ end
+
+ it "doesn't allow a long title" do
+ Fabricate.build(:topic, title: "x" * (SiteSetting.max_topic_title_length + 1)).should_not be_valid
+ end
+
+ it "doesn't allow a short title" do
+ Fabricate.build(:topic, title: "x" * (SiteSetting.min_topic_title_length + 1)).should_not be_valid
+ end
+
+ it "allows a regular title with a few ascii characters" do
+ Fabricate.build(:topic, title: "hello this is my cool topic! welcome: all;").should be_valid
+ end
+
+ it "allows non ascii" do
+ Fabricate.build(:topic, title: "Iñtërnâtiônàlizætiøn").should be_valid
+ end
+
+ end
+
context 'topic title uniqueness' do
@@ -816,7 +839,7 @@ describe Topic do
context 'changing title' do
before do
- topic.title = "new title"
+ topic.title = "new title for the topic"
topic.save
end