Merge branch 'master' into pt_BR

This commit is contained in:
J. Bruni 2013-07-22 16:00:09 -03:00
commit e257cfc56c
81 changed files with 1524 additions and 485 deletions

69
Gemfile
View File

@ -1,5 +1,60 @@
source 'https://rubygems.org'
# monkey patching to support dual booting
module Bundler::SharedHelpers
def default_lockfile=(path)
@default_lockfile = path
end
def default_lockfile
@default_lockfile ||= Pathname.new("#{default_gemfile}.lock")
end
end
module ::Kernel
def rails4?
!!ENV["RAILS4"]
end
end
if rails4?
Bundler::SharedHelpers.default_lockfile = Pathname.new("#{Bundler::SharedHelpers.default_gemfile}_rails4.lock")
# Bundler::Dsl.evaluate already called with an incorrect lockfile ... fix it
class Bundler::Dsl
# A bit messy, this can be called multiple times by bundler, avoid blowing the stack
unless self.method_defined? :to_definition_unpatched
alias_method :to_definition_unpatched, :to_definition
puts "Booting in Rails 4 mode"
end
def to_definition(bad_lockfile, unlock)
to_definition_unpatched(Bundler::SharedHelpers.default_lockfile, unlock)
end
end
end
if rails4?
gem 'rails', '4.0.0'
gem 'redis-rails', :git => 'git://github.com/SamSaffron/redis-store.git'
gem 'rails-observers'
gem 'protected_attributes'
gem 'actionpack-action_caching'
gem 'seed-fu' , github: 'mbleigh/seed-fu'
else
# we had pain with the 3.2.13 upgrade so monkey patch the security fix
# next time around we hope to upgrade
gem 'rails', '3.2.12'
gem 'strong_parameters' # remove when we upgrade to Rails 4
# we are using a custom sprockets repo to work around: https://github.com/rails/rails/issues/8099#issuecomment-16137638
# REVIEW EVERY RELEASE
gem 'sprockets', git: 'https://github.com/SamSaffron/sprockets.git', branch: 'rails-compat'
gem 'redis-rails'
gem 'seed-fu'
end
gem 'redis'
gem 'hiredis'
gem 'em-redis'
gem 'active_model_serializers', git: 'https://github.com/rails-api/active_model_serializers.git'
# we had issues with latest, stick to the rev till we figure this out
@ -20,13 +75,11 @@ gem 'activerecord-postgres-hstore'
gem 'active_attr' # until we get ActiveModel::Model with Rails 4
gem 'airbrake', '3.1.2', require: false # errbit is broken with 3.1.3 for now
gem 'clockwork', require: false
gem 'em-redis'
gem 'eventmachine'
gem 'fast_xs'
gem 'fast_xor', git: 'https://github.com/CodeMonkeySteve/fast_xor.git'
gem 'fastimage'
gem 'fog', require: false
gem 'hiredis'
gem 'email_reply_parser', git: 'https://github.com/lawrencepit/email_reply_parser.git'
@ -49,22 +102,17 @@ gem 'omniauth-browserid', git: 'https://github.com/callahad/omniauth-browserid.g
gem 'omniauth-cas'
gem 'oj'
gem 'pg'
# we had pain with the 3.2.13 upgrade so monkey patch the security fix
# next time around we hope to upgrade
gem 'rails', '3.2.12'
gem 'rake'
gem 'redis'
gem 'redis-rails'
gem 'rest-client'
gem 'rinku'
gem 'sanitize'
gem 'sass'
gem 'seed-fu'
gem 'sidekiq'
gem 'sidekiq-failures'
gem 'sinatra', require: nil
gem 'slim' # required for sidekiq-web
gem 'strong_parameters' # remove when we upgrade to Rails 4
gem 'therubyracer', require: 'v8'
gem 'thin', require: false
gem 'diffy', require: false
@ -123,9 +171,6 @@ group :development do
gem 'annotate', :git => 'https://github.com/SamSaffron/annotate_models.git'
end
# we are using a custom sprockets repo to work around: https://github.com/rails/rails/issues/8099#issuecomment-16137638
# REVIEW EVERY RELEASE
gem 'sprockets', git: 'https://github.com/SamSaffron/sprockets.git', branch: 'rails-compat'
# this is an optional gem, it provides a high performance replacement

View File

@ -16,7 +16,7 @@ GIT
GIT
remote: https://github.com/SamSaffron/message_bus
revision: 9c16e7ebaafaf2a3933a84fa1c517c0eba44b052
revision: c357cbfea34eec3cca8f00754eb9d5bd2be98594
specs:
message_bus (0.0.2)
eventmachine
@ -93,7 +93,7 @@ PATH
remote: vendor/gems/simple_handlebars_rails
specs:
simple_handlebars_rails (0.0.1)
rails (~> 3.1)
rails (> 3.1)
GEM
remote: https://rubygems.org/

571
Gemfile_rails4.lock Normal file
View File

@ -0,0 +1,571 @@
GIT
remote: git://github.com/SamSaffron/redis-store.git
revision: 1eafaa3d8bfbcb61ad89d1a2831adbba4ea8e1e1
specs:
redis-actionpack (3.2.3)
actionpack (>= 3.2.3)
redis-rack (~> 1.4.0)
redis-store (~> 1.1.0)
redis-activesupport (3.2.3)
activesupport (>= 3.2.3)
redis-store (~> 1.1.0)
redis-rack (1.4.2)
rack (> 1.4.1)
redis-store (~> 1.1.0)
redis-rails (3.2.3)
redis-actionpack (>= 3.2.3)
redis-activesupport (>= 3.2.3)
redis-store (~> 1.1.0)
GIT
remote: git://github.com/mbleigh/seed-fu.git
revision: f89ea306472c500ec7911c7be111a4aad9c1bc78
specs:
seed-fu (2.2.0)
activerecord (>= 3.1, < 4.1)
activesupport (>= 3.1, < 4.1)
GIT
remote: https://github.com/CodeMonkeySteve/fast_xor.git
revision: 85b79ec6d116f9680f23bd2c5c8c2c2039d477d8
specs:
fast_xor (1.1.2)
rake
rake-compiler
GIT
remote: https://github.com/SamSaffron/annotate_models.git
revision: ebe4ba7e3f6ceeb43e4e40078da2b261a1bb71b2
specs:
annotate (2.6.0.beta1)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
GIT
remote: https://github.com/SamSaffron/message_bus
revision: 09392967940daf77943d1489ed3f1f71d6f8450a
specs:
message_bus (0.0.2)
eventmachine
rack (>= 1.1.3)
redis
thin
GIT
remote: https://github.com/SamSaffron/redis-rack-cache.git
revision: 379ef30e31d4e185cb1d7f8badca0cc06403eba2
specs:
redis-rack-cache (1.2.1)
rack-cache (~> 1.2)
redis-store (~> 1.1.0)
GIT
remote: https://github.com/callahad/omniauth-browserid.git
revision: af62d667626c1622de6fe13b60849c3640765ab1
branch: observer_api
specs:
omniauth-browserid (0.0.2)
faraday
multi_json
omniauth (~> 1.0)
GIT
remote: https://github.com/lawrencepit/email_reply_parser.git
revision: 67408dfb1b99fb8d5f145f782b9e22d1851a8e5a
specs:
email_reply_parser (0.6)
GIT
remote: https://github.com/rails-api/active_model_serializers.git
revision: 8ac4bf90067eef442a6208848f86e55892d724f1
specs:
active_model_serializers (0.8.1)
activemodel (>= 3.2)
GIT
remote: https://github.com/zhangyuan/vestal_versions
revision: 0ea75ec4e269b5a9e609639919ade0f36381a446
specs:
vestal_versions (1.2.2)
activerecord (>= 3.0.0)
activesupport (>= 3.0.0)
PATH
remote: vendor/gems/discourse_emoji
specs:
discourse_emoji (0.0.1)
PATH
remote: vendor/gems/discourse_plugin
specs:
discourse_plugin (0.0.1)
PATH
remote: vendor/gems/rails_multisite
specs:
rails_multisite (0.0.1)
PATH
remote: vendor/gems/simple_handlebars_rails
specs:
simple_handlebars_rails (0.0.1)
rails (> 3.1)
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.0.0)
actionpack (= 4.0.0)
mail (~> 2.5.3)
actionpack (4.0.0)
activesupport (= 4.0.0)
builder (~> 3.1.0)
erubis (~> 2.7.0)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionpack-action_caching (1.0.0)
actionpack (>= 4.0.0.beta, < 5.0)
active_attr (0.8.2)
activemodel (>= 3.0.2, < 4.1)
activesupport (>= 3.0.2, < 4.1)
activemodel (4.0.0)
activesupport (= 4.0.0)
builder (~> 3.1.0)
activerecord (4.0.0)
activemodel (= 4.0.0)
activerecord-deprecated_finders (~> 1.0.2)
activesupport (= 4.0.0)
arel (~> 4.0.0)
activerecord-deprecated_finders (1.0.3)
activerecord-postgres-hstore (0.7.6)
activerecord (>= 3.1)
pg-hstore (>= 1.1.5)
rake
activesupport (4.0.0)
i18n (~> 0.6, >= 0.6.4)
minitest (~> 4.2)
multi_json (~> 1.3)
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
addressable (2.3.5)
airbrake (3.1.2)
activesupport
builder
arel (4.0.0)
atomic (1.1.10)
barber (0.4.2)
ember-source
execjs
handlebars-source
better_errors (0.9.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.1.4)
celluloid (0.14.1)
timers (>= 1.0.0)
certified (0.1.1)
childprocess (0.3.9)
ffi (~> 1.0, >= 1.0.11)
clockwork (0.5.3)
activesupport (~> 4.0.0)
tzinfo (~> 0.3.35)
coderay (1.0.9)
connection_pool (1.1.0)
daemons (1.1.9)
debug_inspector (0.0.2)
diff-lcs (1.2.4)
diffy (3.0.1)
em-redis (0.3.0)
eventmachine
ember-data-source (0.13)
ember-source
ember-rails (0.13.0)
active_model_serializers
barber (>= 0.4.1)
ember-data-source
ember-source
execjs (>= 1.2)
handlebars-source
railties (>= 3.1)
ember-source (1.0.0.rc6.2)
handlebars-source (= 1.0.12)
erubis (2.7.0)
eventmachine (1.0.3)
excon (0.25.3)
execjs (1.4.0)
multi_json (~> 1.0)
fabrication (2.7.2)
fakeweb (1.3.0)
faraday (0.8.7)
multipart-post (~> 1.1)
fast_blank (0.0.1)
rake
rake-compiler
fast_xs (0.8.0)
fastimage (1.5.0)
ffi (1.9.0)
fog (1.14.0)
builder
excon (~> 0.25.0)
formatador (~> 0.2.0)
mime-types
multi_json (~> 1.0)
net-scp (~> 1.1)
net-ssh (>= 2.1.3)
nokogiri (~> 1.5)
ruby-hmac
formatador (0.2.4)
fspath (2.0.4)
given_core (3.0.0)
sorcerer (>= 0.3.7)
guard (1.8.1)
formatador (>= 0.2.4)
listen (>= 1.0.0)
lumberjack (>= 1.0.2)
pry (>= 0.9.10)
thor (>= 0.14.6)
guard-rspec (3.0.2)
guard (>= 1.8)
rspec (~> 2.13)
guard-spork (1.5.1)
childprocess (>= 0.2.3)
guard (>= 1.1)
spork (>= 0.8.4)
handlebars-source (1.0.12)
hashie (2.0.5)
highline (1.6.19)
hike (1.2.3)
hiredis (0.4.5)
httpauth (0.2.0)
i18n (0.6.4)
image_optim (0.8.1)
fspath (~> 2.0.3)
image_size (~> 1.1.2)
in_threads (~> 1.1.1)
progress (~> 2.4.0)
image_size (1.1.2)
image_sorcery (1.1.0)
in_threads (1.1.1)
json (1.8.0)
jwt (0.1.8)
multi_json (>= 1.5)
kgio (2.8.0)
librarian (0.1.0)
highline
thor (~> 0.15)
libv8 (3.11.8.17)
listen (1.2.2)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
rb-kqueue (>= 0.2)
lru_redux (0.0.6)
lumberjack (1.0.4)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
metaclass (0.0.1)
method_source (0.8.1)
mime-types (1.23)
mini_portile (0.5.1)
minitest (4.7.5)
mocha (0.14.0)
metaclass (~> 0.0.1)
multi_json (1.7.7)
multipart-post (1.2.0)
mustache (0.99.4)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.6.8)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
oauth (0.4.7)
oauth2 (0.8.1)
faraday (~> 0.8)
httpauth (~> 0.1)
jwt (~> 0.1.4)
multi_json (~> 1.0)
rack (~> 1.2)
oj (2.1.4)
omniauth (1.1.4)
hashie (>= 1.2, < 3)
rack
omniauth-cas (1.0.4)
addressable (~> 2.3)
nokogiri (~> 1.6)
omniauth (~> 1.1.0)
omniauth-facebook (1.4.1)
omniauth-oauth2 (~> 1.1.0)
omniauth-github (1.1.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth (1.0.1)
oauth
omniauth (~> 1.0)
omniauth-oauth2 (1.1.1)
oauth2 (~> 0.8.0)
omniauth (~> 1.0)
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
omniauth-twitter (1.0.0)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
openid-redis-store (0.0.2)
redis
ruby-openid
pg (0.15.1)
pg-hstore (1.1.7)
polyglot (0.3.3)
progress (2.4.0)
protected_attributes (1.0.3)
activemodel (>= 4.0.0, < 5.0)
pry (0.9.12.2)
coderay (~> 1.0.5)
method_source (~> 0.8)
slop (~> 3.4)
pry-nav (0.2.3)
pry (~> 0.9.10)
pry-rails (0.3.1)
pry (>= 0.9.10)
qunit-rails (0.0.3)
railties (>= 3.2.3)
rack (1.5.2)
rack-cache (1.2)
rack (>= 0.4)
rack-cors (0.2.8)
rack
rack-mini-profiler (0.1.27)
rack (>= 1.1.3)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-protection (1.5.0)
rack
rack-test (0.6.2)
rack (>= 1.0)
rails (4.0.0)
actionmailer (= 4.0.0)
actionpack (= 4.0.0)
activerecord (= 4.0.0)
activesupport (= 4.0.0)
bundler (>= 1.3.0, < 2.0)
railties (= 4.0.0)
sprockets-rails (~> 2.0.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
railties (4.0.0)
actionpack (= 4.0.0)
activesupport (= 4.0.0)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.11.0)
rake (10.1.0)
rake-compiler (0.8.3)
rake
rb-fsevent (0.9.3)
rb-inotify (0.9.0)
ffi (>= 0.5.0)
rb-kqueue (0.2.0)
ffi (>= 0.5.0)
redcarpet (3.0.0)
redis (3.0.4)
redis-namespace (1.3.0)
redis (~> 3.0.0)
redis-store (1.1.2)
redis (>= 2.2.0)
ref (1.0.5)
rest-client (1.6.7)
mime-types (>= 1.16)
rinku (1.7.3)
rspec (2.14.1)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.3)
rspec-expectations (2.14.0)
diff-lcs (>= 1.1.3, < 2.0)
rspec-given (3.0.0)
given_core (= 3.0.0)
rspec (>= 2.12)
rspec-mocks (2.14.1)
rspec-rails (2.14.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
ruby-hmac (0.4.0)
ruby-openid (2.2.3)
sanitize (2.0.6)
nokogiri (>= 1.4.4)
sass (3.2.9)
sass-rails (4.0.0)
railties (>= 4.0.0.beta, < 5.0)
sass (>= 3.1.10)
sprockets-rails (~> 2.0.0)
shoulda (3.5.0)
shoulda-context (~> 1.0, >= 1.0.1)
shoulda-matchers (>= 1.4.1, < 3.0)
shoulda-context (1.1.4)
shoulda-matchers (2.2.0)
activesupport (>= 3.0.0)
sidekiq (2.13.0)
celluloid (>= 0.14.1)
connection_pool (>= 1.0.0)
json
redis (>= 3.0)
redis-namespace
sidekiq-failures (0.2.1)
sidekiq (>= 2.2.1)
simplecov (0.7.1)
multi_json (~> 1.0)
simplecov-html (~> 0.7.1)
simplecov-html (0.7.1)
sinatra (1.4.3)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
slim (2.0.0)
temple (~> 0.6.5)
tilt (~> 1.3, >= 1.3.3)
slop (3.4.5)
sorcerer (1.0.0)
spork (0.9.2)
sprockets (2.10.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.0.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (~> 2.8)
temple (0.6.5)
terminal-notifier-guard (1.5.3)
therubyracer (0.11.4)
libv8 (~> 3.11.8.12)
ref
thin (1.5.1)
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thor (0.18.1)
thread_safe (0.1.0)
atomic
tilt (1.4.1)
timecop (0.6.2.2)
timers (1.1.0)
treetop (1.4.14)
polyglot
polyglot (>= 0.3.1)
turbo-sprockets-rails3 (0.2.9)
railties (>= 3.1.0)
sprockets (>= 2.0.0)
tzinfo (0.3.37)
uglifier (2.1.2)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
unicorn (4.6.3)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
PLATFORMS
ruby
DEPENDENCIES
actionpack-action_caching
active_attr
active_model_serializers!
activerecord-postgres-hstore
airbrake (= 3.1.2)
annotate!
barber
better_errors
binding_of_caller
certified
clockwork
diffy
discourse_emoji!
discourse_plugin!
em-redis
email_reply_parser!
ember-rails
ember-source (= 1.0.0.rc6.2)
eventmachine
fabrication
fakeweb (~> 1.3.0)
fast_blank
fast_xor!
fast_xs
fastimage
fog
guard-rspec
guard-spork
handlebars-source (= 1.0.12)
highline
hiredis
image_optim
image_sorcery
librarian (>= 0.0.25)
listen
lru_redux
message_bus!
minitest
mocha
multi_json
mustache
nokogiri
oj
omniauth
omniauth-browserid!
omniauth-cas
omniauth-facebook
omniauth-github
omniauth-openid
omniauth-twitter
openid-redis-store
pg
protected_attributes
pry-nav
pry-rails
qunit-rails
rack-cache
rack-cors
rack-mini-profiler (= 0.1.27)
rails (= 4.0.0)
rails-observers
rails_multisite!
rake
rb-fsevent
rb-inotify (~> 0.9)
redcarpet
redis
redis-rack-cache!
redis-rails!
rest-client
rinku
rspec-given
rspec-rails
sanitize
sass
sass-rails
seed-fu!
shoulda
sidekiq
sidekiq-failures
simple_handlebars_rails!
simplecov
sinatra
slim
terminal-notifier-guard
therubyracer
thin
timecop
turbo-sprockets-rails3
uglifier
unicorn
vestal_versions!

View File

@ -0,0 +1,14 @@
/**
Handles routes related to viewing flags.
@class AdminFlagsRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsRoute = Discourse.Route.extend({
redirect: function() {
this.transitionTo('adminFlags.active');
}
});

View File

@ -9,6 +9,9 @@
Discourse.AdminUsersListRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/users_list', {into: 'admin/templates/admin'});
},
redirect: function() {
this.transitionTo('adminUsersList.active');
}
});
@ -108,4 +111,4 @@ Discourse.AdminUsersListBannedRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('banned');
}
});
});

View File

@ -8,10 +8,10 @@
<li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/linkTo}}</li>
{{/if}}
<li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminUsersList'}}{{i18n admin.users.title}}{{/linkTo}}</li>
<li>{{#linkTo 'admin.groups'}}{{i18n admin.groups.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminEmail'}}{{i18n admin.email.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminFlags'}}{{i18n admin.flags.title}}{{/linkTo}}</li>
{{#if currentUser.admin}}
<li>{{#linkTo 'admin.customize'}}{{i18n admin.customize.title}}{{/linkTo}}</li>
<li>{{#linkTo 'admin.api'}}{{i18n admin.api.title}}{{/linkTo}}</li>

View File

@ -8,12 +8,9 @@
Discourse.Lightbox = {
apply: function($elem) {
var _this = this;
$('a.lightbox', $elem).each(function(i, e) {
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
$(e).magnificPopup({
type: 'image',
closeOnContentClick: true
});
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
$('a.lightbox', $elem).each(function(i, e) {
$(e).magnificPopup({ type: 'image', closeOnContentClick: true });
});
});
}

View File

@ -174,9 +174,13 @@ Discourse.Utilities = {
return false;
}
var upload = files[0];
// ensures that new users can upload image
if (Discourse.User.current('trust_level') === 0 && Discourse.SiteSettings.newuser_max_images === 0) {
bootbox.alert(I18n.t('post.errors.upload_not_allowed_for_new_user'));
// ensures that new users can upload image/attachment
if (Discourse.Utilities.isUploadForbidden(upload.name)) {
if (Discourse.Utilities.isAnImage(upload.name)) {
bootbox.alert(I18n.t('post.errors.image_upload_not_allowed_for_new_user'));
} else {
bootbox.alert(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user'));
}
return false;
}
// if the image was pasted, sets its name to a default one
@ -242,6 +246,17 @@ Discourse.Utilities = {
**/
maxUploadSizeInKB: function(path) {
return Discourse.Utilities.isAnImage(path) ? Discourse.SiteSettings.max_image_size_kb : Discourse.SiteSettings.max_attachment_size_kb;
},
/**
Test whether an upload is forbidden or not
@method isUploadForbidden
@param {String} path The path
**/
isUploadForbidden: function(path) {
if (Discourse.User.current('trust_level') > 0) { return false; }
return Discourse.Utilities.isAnImage(path) ? Discourse.SiteSettings.newuser_max_images === 0 : Discourse.SiteSettings.newuser_max_attachments === 0;
}
};

View File

@ -9,6 +9,8 @@
Discourse.ComposerController = Discourse.Controller.extend({
needs: ['modal', 'topic'],
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY),
togglePreview: function() {
this.get('model').togglePreview();
},
@ -32,13 +34,8 @@ Discourse.ComposerController = Discourse.Controller.extend({
}.property(),
save: function(force) {
var composer,
_this = this,
topic,
message,
buttons;
composer = this.get('model');
var composer = this.get('model'),
composerController = this;
if( composer.get('cantSubmitPost') ) {
this.set('view.showTitleTip', Date.now());
@ -52,12 +49,12 @@ Discourse.ComposerController = Discourse.Controller.extend({
// for now handle a very narrow use case
// if we are replying to a topic AND not on the topic pop the window up
if(!force && composer.get('replyingToTopic')) {
topic = this.get('topic');
var topic = this.get('topic');
if (!topic || topic.get('id') !== composer.get('topic.id'))
{
message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')});
var message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')});
buttons = [{
var buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
@ -70,7 +67,7 @@ Discourse.ComposerController = Discourse.Controller.extend({
"callback": function(){
composer.set('topic', topic);
composer.set('post', null);
_this.save(true);
composerController.save(true);
}
});
}
@ -79,7 +76,7 @@ Discourse.ComposerController = Discourse.Controller.extend({
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title'>" + this.get('model.topic.title') + "</div>",
"class": "btn-primary btn-reply-on-original",
"callback": function(){
_this.save(true);
composerController.save(true);
}
});
@ -91,8 +88,15 @@ Discourse.ComposerController = Discourse.Controller.extend({
return composer.save({
imageSizes: this.get('view').imageSizes()
}).then(function(opts) {
// If we replied as a new topic successfully, remove the draft.
if (composerController.get('replyAsNewTopicDraft')) {
composerController.destroyDraft();
}
opts = opts || {};
_this.close();
composerController.close();
var currentUser = Discourse.User.current();
if (composer.get('creatingTopic')) {
@ -101,6 +105,7 @@ Discourse.ComposerController = Discourse.Controller.extend({
currentUser.set('reply_count', currentUser.get('reply_count') + 1);
}
Discourse.URL.routeTo(opts.post.get('url'));
}, function(error) {
composer.set('disableDrafts', false);
bootbox.alert(error);

View File

@ -9,6 +9,10 @@
**/
Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
onShow: function() {
this.set('selected', null);
},
changePostActionType: function(action) {
this.set('selected', action);
},

View File

@ -332,7 +332,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
},
replyAsNewTopic: function(post) {
// TODO shut down topic draft cleanly if it exists ...
var composerController = this.get('controllers.composer');
var promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
@ -343,9 +342,9 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
promise.then(function() {
Discourse.Post.loadQuote(post.get('id')).then(function(q) {
composerController.appendText("" + (I18n.t("post.continue_discussion", {
composerController.appendText(I18n.t("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
}) + "\n\n" + q);
});
});
},

View File

@ -27,12 +27,12 @@ Discourse.Post = Discourse.Model.extend({
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
postDeletedBy: function() {
if (this.get('firstPost')) { return this.get('topic.deleted_by') }
if (this.get('firstPost')) { return this.get('topic.deleted_by'); }
return this.get('deleted_by');
}.property('firstPost', 'deleted_by', 'topic.deleted_by'),
postDeletedAt: function() {
if (this.get('firstPost')) { return this.get('topic.deleted_at') }
if (this.get('firstPost')) { return this.get('topic.deleted_at'); }
return this.get('deleted_at');
}.property('firstPost', 'deleted_at', 'topic.deleted_at'),
@ -199,13 +199,23 @@ Discourse.Post = Discourse.Model.extend({
@method recover
**/
recover: function() {
this.setProperties({
var post = this;
post.setProperties({
deleted_at: null,
deleted_by: null,
can_delete: true
user_deleted: false,
can_delete: false
});
return Discourse.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false });
return Discourse.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false }).then(function(data){
post.setProperties({
cooked: data.cooked,
raw: data.raw,
user_deleted: false,
can_delete: true,
version: data.version
});
});
},
/**
@ -226,7 +236,10 @@ Discourse.Post = Discourse.Model.extend({
this.setProperties({
cooked: Discourse.Markdown.cook(I18n.t("post.deleted_by_author")),
can_delete: false,
version: this.get('version') + 1
version: this.get('version') + 1,
can_recover: true,
can_edit: false,
user_deleted: true
});
}

View File

@ -23,6 +23,8 @@ Discourse.Route = Em.Route.extend({
$('.d-dropdown').hide();
$('header ul.icons li').removeClass('active');
$('[data-toggle="dropdown"]').parent().removeClass('open');
// close the lightbox
if ($.magnificPopup && $.magnificPopup.instance) { $.magnificPopup.instance.close(); }
Discourse.set('notifyCount',0);

View File

@ -11,18 +11,15 @@ Discourse.FlagView = Discourse.ModalBodyView.extend({
title: I18n.t('flagging.title'),
selectedChanged: function() {
var nameKey = this.get('controller.selected.name_key');
if (!nameKey) return;
var flagView = this;
Em.run.next(function() {
$('#radio_' + nameKey).prop('checked', 'true');
flagView.$("input[type='radio']").prop('checked', false);
var nameKey = flagView.get('controller.selected.name_key');
if (!nameKey) return;
flagView.$('#radio_' + nameKey).prop('checked', 'true');
});
}.observes('controller.selected.name_key'),
}.observes('controller.selected.name_key')
didInsertElement: function() {
this._super();
// Would be nice if there were an EmberJs radio button to do this for us. Oh well, one should be coming
// in an upcoming release.
this.$("input[type='radio']").prop('checked', false);
}
});

View File

@ -88,7 +88,7 @@ Discourse.PostMenuView = Discourse.View.extend({
} else {
// The delete actions target the post iteself
if (post.get('deleted_at')) {
if (post.get('deleted_at') || post.get('user_deleted')) {
if (!post.get('can_recover')) { return; }
label = "post.controls.undelete";
action = "recover";

View File

@ -12,10 +12,15 @@ Discourse.PostView = Discourse.View.extend({
classNameBindings: ['postTypeClass',
'selected',
'post.hidden:hidden',
'post.deleted',
'addDeletedClass:deleted',
'parentPost:replies-above'],
postBinding: 'content',
addDeletedClass: function() {
var post = this.get('post');
return post.get('deleted') || post.get('user_deleted');
}.property('post.deleted','post.user_deleted'),
postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular';
}.property('post.post_type'),

View File

@ -1,6 +1,3 @@
// Pagedown customizations
//= require ./pagedown_custom.js
// The rest of the externals
//= require_tree ./external
@ -9,6 +6,9 @@
//= require ./locales/date_locales.js
// Pagedown customizations
//= require ./pagedown_custom.js
// Stuff we need to load first
//= require_tree ./discourse/mixins
//= require ./discourse/components/computed
@ -16,6 +16,7 @@
//= require ./discourse/components/debounce
//= require ./discourse/models/model
//= require ./discourse/models/user_action
//= require ./discourse/models/composer
//= require ./discourse/controllers/controller
//= require ./discourse/controllers/object_controller
//= require ./discourse/views/modal/modal_body_view

View File

@ -238,5 +238,6 @@
.category {
float: left;
background-color: transparent;
line-height: 20px;
}
}

View File

@ -77,7 +77,6 @@
color: $topic-list-th-color;
font-weight: bold;
font-size: 13px;
text-shadow: 0 1px 0 $white;
@include box-shadow(inset 0 1px 0 $white);
&:first-of-type {
@include border-radius-all(4px 0 0 0);

View File

@ -374,8 +374,8 @@
}
.user-title {
font-size: 11px;
background-color: none;
margin-top: 2px;
line-height: 15px;
color: #555;
}
}

View File

@ -21,12 +21,10 @@
// --------------------------------------------------
.badge-category {
@extend %badge;
padding: 3px 8px;
padding: 5px 8px;
color: $white;
font-size: 12px;
text-shadow: 0 1px 0 rgba($black, 0.3);
@include box-shadow(inset 0 1px 0 rgba($white, 0.22));
font-weight: bold;
&[href] {
color: $white;
}

View File

@ -94,17 +94,13 @@ class PostsController < ApplicationController
@post = Post.where(topic_id: params[:topic_id], post_number: params[:post_number]).first
guardian.ensure_can_see!(@post)
@post.revert_to(params[:version].to_i) if params[:version].present?
post_serializer = PostSerializer.new(@post, scope: guardian, root: false)
post_serializer.add_raw = true
render_json_dump(post_serializer)
render_post_json(@post)
end
def show
@post = find_post_from_params
@post.revert_to(params[:version].to_i) if params[:version].present?
post_serializer = PostSerializer.new(@post, scope: guardian, root: false)
post_serializer.add_raw = true
render_json_dump(post_serializer)
render_post_json(@post)
end
def destroy
@ -120,10 +116,11 @@ class PostsController < ApplicationController
def recover
post = find_post_from_params
guardian.ensure_can_recover_post!(post)
post.recover!
post.topic.update_statistics
destroyer = PostDestroyer.new(current_user, post)
destroyer.recover
post.reload
render nothing: true
render_post_json(post)
end
def destroy_many
@ -188,6 +185,12 @@ class PostsController < ApplicationController
post
end
def render_post_json(post)
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.add_raw = true
render_json_dump(post_serializer)
end
private
def create_params
@ -200,9 +203,13 @@ class PostsController < ApplicationController
:target_usernames,
:reply_to_post_number,
:image_sizes,
:auto_close_days
:auto_close_days,
:auto_track
]
# param munging for WordPress
params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track]
if api_key_valid?
# php seems to be sending this incorrectly, don't fight with it
params[:skip_validations] = params[:skip_validations].to_s == "true"

View File

@ -1,5 +1,7 @@
require_dependency 'jobs'
require_dependency 'pretty_text'
require_dependency 'local_store'
require_dependency 's3_store'
require_dependency 'rate_limiter'
require_dependency 'post_revisor'
require_dependency 'enum'
@ -89,7 +91,7 @@ class Post < ActiveRecord::Base
@post_analyzer = PostAnalyzer.new(raw, topic_id)
end
%w{raw_mentions linked_hosts image_count link_count raw_links}.each do |attr|
%w{raw_mentions linked_hosts image_count attachment_count link_count raw_links}.each do |attr|
define_method(attr) do
PostAnalyzer.new(raw, topic_id).send(attr)
end
@ -263,12 +265,6 @@ class Post < ActiveRecord::Base
PostCreator.before_create_tasks(self)
end
# TODO: Move some of this into an asynchronous job?
# TODO: Move into PostCreator
after_create do
PostCreator.after_create_tasks(self)
end
# This calculates the geometric mean of the post timings and stores it along with
# each post.
def self.calculate_avg_time

View File

@ -39,6 +39,18 @@ class PostAnalyzer
end.count
end
# How many attachments are present in the post
def attachment_count
return 0 unless @raw.present?
if SiteSetting.enable_s3_uploads?
cooked_document.css("a.attachment[href^=\"#{S3Store.base_url}\"]")
else
cooked_document.css("a.attachment[href^=\"#{LocalStore.directory}\"]") +
cooked_document.css("a.attachment[href^=\"#{LocalStore.base_url}\"]")
end.count
end
def raw_mentions
return [] if @raw.blank?

View File

@ -212,6 +212,7 @@ class SiteSetting < ActiveRecord::Base
setting(:newuser_max_links, 2)
client_setting(:newuser_max_images, 0)
client_setting(:newuser_max_attachments, 0)
setting(:newuser_spam_host_threshold, 3)

View File

@ -51,7 +51,11 @@ class Topic < ActiveRecord::Base
self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty?
end
serialize :meta_data, ActiveRecord::Coders::Hstore
if rails4?
store_accessor :meta_data
else
serialize :meta_data, ActiveRecord::Coders::Hstore
end
belongs_to :category
has_many :posts
@ -130,7 +134,6 @@ class Topic < ActiveRecord::Base
after_create do
changed_to_category(category)
notifier.created_topic! user_id
if archetype == Archetype.private_message
DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE)
else

View File

@ -14,7 +14,7 @@ class TopicNotifier
end
def created_topic!(user_id)
def watch_topic!(user_id)
change_level user_id, :watching, :created_topic
end

View File

@ -91,7 +91,7 @@ class Upload < ActiveRecord::Base
end
def self.is_relative?(url)
url.start_with?("/uploads/")
url.start_with?(LocalStore.directory)
end
def self.is_local?(url)
@ -99,10 +99,12 @@ class Upload < ActiveRecord::Base
end
def self.is_on_s3?(url)
SiteSetting.enable_s3_uploads? && (url.start_with?(S3Store.base_url) || url.start_with?(S3Store.base_url_old))
SiteSetting.enable_s3_uploads? && url.start_with?(S3Store.base_url)
end
def self.get_from_url(url)
# we store relative urls, so we need to remove any host/cdn
url = url.gsub(/^#{LocalStore.asset_host}/i, "") if LocalStore.asset_host.present?
Upload.where(url: url).first if has_been_uploaded?(url)
end

View File

@ -208,6 +208,7 @@ ORDER BY p.created_at desc
end
def self.synchronize_target_topic_ids(post_ids = nil)
builder = SqlBuilder.new("UPDATE user_actions
SET target_topic_id = (select topic_id from posts where posts.id = target_post_id)
/*where*/")

View File

@ -41,7 +41,8 @@ class PostSerializer < BasicPostSerializer
:hidden_reason_id,
:trust_level,
:deleted_at,
:deleted_by
:deleted_by,
:user_deleted
def moderator?

View File

@ -112,7 +112,7 @@
</p>
<div class="more">
<p>
Rather than posting “+1” or “Agreed,” use the Like button. Rather than taking an existing topic in a radically different direction, use Reply as a New Topic.
Rather than posting “+1” or “Agreed”, use the Like button. Rather than taking an existing topic in a radically different direction, use Reply as a New Topic.
</p>
</div>
@ -132,4 +132,4 @@
<div class="more">
</div>
<% end %>
<% end %>

View File

@ -62,7 +62,7 @@
<div id="7"></div>
<h2><a href="#7">7. Content Posted on Other Websites</a></h2>
<p>We have not reviewed, and cannot review, all of the material, including computer software, made available through the websites and webpages to which <%= SiteSetting.company_domain %> links, and that link to <%= SiteSetting.company_domain %>. <%= SiteSetting.company_short_name %> does not have any control over those non-<%= SiteSetting.company_domain %> websites and webpages, and is not responsible for their contents or their use. By linking to a non-<%= SiteSetting.company_domain %> website or webpage, <%= SiteSetting.company_short_name %> does not represent or imply that it endorses such website or webpage. You are responsible for taking precautions as necessary to protect yourself and your computer systems from viruses, worms, Trojan horses, and other harmful or destructive content. <%= SiteSetting.company_short_name %> disclaims any responsibility for any harm resulting from your use of non-WordPress websites and webpages.
<p>We have not reviewed, and cannot review, all of the material, including computer software, made available through the websites and webpages to which <%= SiteSetting.company_domain %> links, and that link to <%= SiteSetting.company_domain %>. <%= SiteSetting.company_short_name %> does not have any control over those non-<%= SiteSetting.company_domain %> websites and webpages, and is not responsible for their contents or their use. By linking to a non-<%= SiteSetting.company_domain %> website or webpage, <%= SiteSetting.company_short_name %> does not represent or imply that it endorses such website or webpage. You are responsible for taking precautions as necessary to protect yourself and your computer systems from viruses, worms, Trojan horses, and other harmful or destructive content. <%= SiteSetting.company_short_name %> disclaims any responsibility for any harm resulting from your use of non-<%= SiteSetting.company_domain %> websites and webpages.
</p>
<div id="8"></div>

View File

@ -93,7 +93,7 @@ module Discourse
# dumping rack lock cause the message bus does not work with it (throw :async, it catches Exception)
# see: https://github.com/sporkrb/spork/issues/66
# rake assets:precompile also fails
config.threadsafe! unless $PROGRAM_NAME =~ /spork|rake/
config.threadsafe! unless rails4? || $PROGRAM_NAME =~ /spork|rake/
# route all exceptions via our router
config.exceptions_app = self.routes

View File

@ -34,5 +34,6 @@ module Clockwork
every(1.day, 'version_check')
every(1.minute, 'clockwork_heartbeat')
every(1.minute, 'poll_mailbox')
every(2.hours, 'destroy_old_deletion_stubs')
end

View File

@ -7,7 +7,8 @@ Discourse::Application.configure do
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
config.whiny_nils = true unless rails4?
config.eager_load = false if rails4?
# Show full error reports and disable caching
config.consider_all_requests_local = true

View File

@ -757,7 +757,8 @@ en:
upload_too_large: "Sorry, the file you are trying to upload is too big (maximum size is {{max_size_kb}}kb), please resize it and try again."
too_many_uploads: "Sorry, you can only upload one file at a time."
upload_not_authorized: "Sorry, the file you are trying to upload is not authorized (authorized extension: {{authorized_extensions}})."
upload_not_allowed_for_new_user: "Sorry, new users can not upload images."
image_upload_not_allowed_for_new_user: "Sorry, new users can not upload images."
attachment_upload_not_allowed_for_new_user: "Sorry, new users can not upload attachments."
abandon: "Are you sure you want to abandon your post?"

View File

@ -737,7 +737,8 @@ fr:
upload_too_large: "Désolé, le fichier que vous êtes en train d'envoyer est trop grand (maximum {{max_size_kb}}Kb). Merci de le redimensionner et de réessayer."
too_many_uploads: "Désolé, vous ne pouvez envoyer qu'un seul fichier à la fois."
upload_not_authorized: "Désole, le fichier que vous êtes en train d'uploader n'est pas autorisé (extensions autorisées : {{authorized_extensions}})."
upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader d'images."
image_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader d'image."
attachment_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader de fichier."
abandon: "Voulez-vous vraiment abandonner ce message ?"

View File

@ -13,13 +13,13 @@ ru:
number:
human:
storage_units:
format: ! '%n %u'
format: 0
units:
byte:
few: байта
many: байт
one: байт
other: байта
one: Байт
other: Байт
few: Байта
many: Байт
gb: ГБ
kb: КБ
mb: МБ
@ -199,6 +199,7 @@ ru:
"12": Отправленные
"13": Входящие
user:
said: '{{username}} писал(а):'
profile: Профайл
mute: Отключить
edit: Настройки
@ -455,8 +456,8 @@ ru:
quote_text: Цитата
code_title: Фрагмент кода
code_text: вводите код здесь
upload_title: Изображение
upload_description: введите здесь описание изображения
upload_title: Загрузить
upload_description: введите здесь описание загружаемого объекта
olist_title: Нумерованный список
ulist_title: Маркированный список
list_item: Элемент списка
@ -486,16 +487,16 @@ ru:
moved_post: "<i title='перенесенное сообщение' class='icon icon-arrow-right'></i> {{username}} перенес сообщение в {{link}}"
total_flagged: всего сообщений с жалобами
upload_selector:
title: Вставка изображения
title: Загрузить файл
from_my_computer: С устройства
from_the_web: Из интернета
add_image: Добавить изображение
remote_title: изображение из интернета
remote_tip: введите адрес изображения в формате http://example.com/image.jpg
local_title: локальное изображение
local_tip: выбрать изображение с устройства.
add_image: Добавить файл
remote_title: удаленный файл
remote_tip: введите адрес файла в формате http://example.com/image.jpg
local_title: локальный файл
local_tip: кликните для выбора файла на вашем устройстве.
upload: Загрузить
upload_file: Загрузка изображения
upload_file: Загрузка
search:
title: поиск по темам, сообщениям, пользователям или категориям
placeholder: условия поиска...
@ -887,6 +888,7 @@ ru:
few: Вы уверены, что хотите удалить сообщения?
many: Вы уверены, что хотите удалить сообщения?
category:
can: может&hellip;
none: (без категории)
edit: изменить
edit_long: Изменить категорию
@ -915,11 +917,10 @@ ru:
change_in_category_topic: Изменить описание
hotness: Популярность
already_used: Цвет уже используется другой категорией
is_secure: Обезопасить категорию?
add_group: Добавить группу
security: Безопасность
allowed_groups: 'Доступные группы:'
auto_close_label: 'Закрыть тему через:'
edit_permissions: Изменить права доступа
add_permission: Добавить права
flagging:
title: Выберите действие над сообщением
action: Пожаловаться
@ -1005,6 +1006,10 @@ ru:
many: '{{categoryName}} ({{count}})'
help: 'последние темы в категории {{categoryName}}'
browser_update: 'К сожалению, <a href="http://www.discourse.org/faq/#browser">ваш браузер слишком устарел</a> для комфортного просмотра нашего форума. Пожалуйста, <a href="http://browsehappy.com">обновите ваш браузер</a>.'
permission_types:
full: Создавать / Отвечать / Просматривать
create_post: Отвечать / Просматривать
readonly: Просматривать
admin_js:
type_to_filter: Введите текст для фильтрации...
admin:

View File

@ -39,6 +39,10 @@ en:
zero: "Sorry, new users can't put images in posts."
one: "Sorry, new users can only put one image in a post."
other: "Sorry, new users can only put %{count} images in a post."
too_many_attachments:
zero: "Sorry, new users can't put attachments in posts."
one: "Sorry, new users can only put one attachment in a post."
other: "Sorry, new users can only put %{count} attachments in a post."
too_many_links:
zero: "Sorry, new users can't put links in posts."
one: "Sorry, new users can only put one link in a post."
@ -582,7 +586,7 @@ en:
suggested_topics: "The number of suggested topics shown at the bottom of a topic"
enable_s3_uploads: "Place uploads on Amazon S3"
s3_upload_bucket: "The Amazon S3 bucket name that files will be uploaded into"
s3_upload_bucket: "The Amazon S3 bucket name that files will be uploaded into. WARNING: must be lowercase (cf. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)"
s3_access_key_id: "The Amazon S3 access key id that will be used to upload images"
s3_secret_access_key: "The Amazon S3 secret access key that will be used to upload images"
s3_region: "The Amazon S3 region name that will be used to upload images"
@ -606,6 +610,7 @@ en:
newuser_max_links: "How many links a new user can add to a post"
newuser_max_images: "How many images a new user can add to a post"
newuser_max_attachments: "How many attachments a new user can add to a post"
newuser_max_mentions_per_post: "Maximum number of @name notifications a new user can use in a post"
max_mentions_per_post: "Maximum number of @name notifications you can use in a post"

View File

@ -41,6 +41,10 @@ fr:
zero: "Désolé, les visiteurs ne peuvent pas ajouter d'image."
one: "Désolé, les visiteurs ne peuvent ajouter qu'une seule image."
other: "Désolé, les visiteurs ne peuvent ajouter que %{count} images."
too_many_attachments:
zero: "Désolé, les visiteurs ne peuvent pas ajouter de fichier."
one: "Désolé, les visiteurs ne peuvent ajouter qu'un seul fichier."
other: "Désolé, les visiteurs ne peuvent ajouter que %{count} fichiers."
too_many_links:
zero: "Désolé, les visiteurs ne peuvent pas insérer de liens."
one: "Désolé, les visiteurs ne peuvent insérer qu'un seul lien."
@ -534,6 +538,7 @@ fr:
newuser_max_links: "Nombre maximum de liens qu'un visiteur peut ajouter à un message"
newuser_max_images: "Nombre maximum d'images qu'un visiteur peut ajouter à un message"
newuser_max_attachments: "Nombre maximum de fichiers qu'un visiteur peut ajouter à un message"
newuser_max_mentions_per_post: "Nombre maximum de référence à un @utilisateur qu'un visiteur peut ajouter à un message"
max_mentions_per_post: "Le nombre maximal de @mentions que vous pouvez ajouter à un message"
@ -551,7 +556,7 @@ fr:
suggested_topics: "Nombre de discussions suggerées en dessous d'une discussion"
enable_s3_uploads: "S'il faut ou non uploader sur s3"
s3_upload_bucket: "Le bucket dans lequel uploader sur s3"
s3_upload_bucket: "Le nom du bucket s3 où seront uploader les fichiers. ATTENTION : le nom doit être en minuscule (cf. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)"
s3_secret_access_key: "La clé secrète Amazon S3 qui va être utilisée pour uploader des images"
s3_region: "Le nom de la région Amazon S3 qui va être utilisée pour uploader des images"

View File

@ -498,6 +498,9 @@ ru:
tos_miscellaneous:
title: 'Условия предоставления услуг: Miscellaneous'
description: Текст раздела Miscellaneous «Условий предоставления услуг»
login_required:
title: 'Требуется вход: Начальная страница'
description: Текст, показываемый неавторизованным пользователям, когда требуется вход на сайт.
site_settings:
default_locale: Язык по умолчанию для данного экземпляра Discourse (ISO 639-1 Code)
min_post_length: Минимальная длина сообщения в символах
@ -519,7 +522,7 @@ ru:
company_short_name: Короткое название компании, которой принадлежит сайт, используется в правовой документации как /tos
company_domain: Имя домена, принадлежащего компании, заведующей сайтом, используется в правовой документации как /tos
api_key: Секретный API ключ, используемый для создания и обновления тем. Зайдите в секцию /admin/api , чтобы его задать
queue_jobs: ВНИМАНИЕ! ТОЛЬКО ДЛЯ РАЗРАБОТЧИКОВ! Обрабатывать задачи асинхронно в очереди sidekiq, если выключено, задачи обрабатываются синхронно без использования очереди
queue_jobs: ТОЛЬКО ДЛЯ РАЗРАБОТЧИКОВ! ВНИМАНИЕ! По умолчанию задачи обрабатываются асинхронно в очереди sidekiq. Если настройка выключена, ваш сайт может не работать.
crawl_images: Разрешить извлечение изображений из сторонних источников, ширина и высота
ninja_edit_window: Количество секунд после размещения сообщения, в течение которых внесение правок в сообщение не повлечет его изменение
max_image_width: Максимальная ширина изображений, добавляемых в сообщение
@ -643,7 +646,8 @@ ru:
min_title_similar_length: Минимальная длина названия темы, при которой тема будет проверена на наличие похожих
min_body_similar_length: Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем
category_colors: Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий
max_image_size_kb: Максимальный размер файлов для загрузки пользователем в кб убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.
max_image_size_kb: Максимальный размер изображений для загрузки пользователем в КБ убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.
max_attachment_size_kb: Максимальный размер файлов для загрузки пользователем в КБ убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.
authorized_extensions: Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)
max_similar_results: Количество похожих тем, показываемых пользователю во время создания новой темы
title_prettify: Предотвращать распространенные опечатки и ошибки, включая КАПС, первый строчный символ, множественные ! и ?, лишние . в конце предложения и т.д.
@ -1067,6 +1071,7 @@ ru:
miscellaneous: 'This Agreement constitutes the entire agreement between %{company_short_name} and you concerning the subject matter hereof, and they may only be modified by a written amendment signed by an authorized executive of %{company_short_name}, or by the posting by %{company_short_name} of a revised version. Except to the extent applicable law, if any, provides otherwise, this Agreement, any access to or use of the Website will be governed by the laws of the state of California, U.S.A., excluding its conflict of law provisions, and the proper venue for any disputes arising out of or relating to any of the same will be the state and federal courts located in San Francisco County, California. Except for claims for injunctive or equitable relief or claims regarding intellectual property rights (which may be brought in any competent court without the posting of a bond), any dispute arising under this Agreement shall be finally settled in accordance with the Comprehensive Arbitration Rules of the Judicial Arbitration and Mediation Service, Inc. (“JAMS”) by three arbitrators appointed in accordance with such Rules. The arbitration shall take place in San Francisco, California, in the English language and the arbitral decision may be enforced in any court. The prevailing party in any action or proceeding to enforce this Agreement shall be entitled to costs and attorneys fees. If any part of this Agreement is held invalid or unenforceable, that part will be construed to reflect the parties original intent, and the remaining portions will remain in full force and effect. A waiver by either party of any term or condition of this Agreement or any breach thereof, in any one instance, will not waive such term or condition or any subsequent breach thereof. You may assign your rights under this Agreement to any party that consents to, and agrees to be bound by, its terms and conditions; %{company_short_name} may assign its rights under this Agreement without condition. This Agreement will be binding upon and will inure to the benefit of the parties, their successors and permitted assigns.'
deleted: удалено
upload:
unauthorized: 'К сожалению, вы не можете загрузить файл данного типа (список разрешенных типов файлов: %{authorized_extensions}).'
pasted_image_filename: Имя файла изображения
image:
fetch_failure: Извините, во время извлечения изображения произошла ошибка.

View File

@ -17,6 +17,7 @@ server {
sendfile on;
keepalive_timeout 65;
client_max_body_size 2m;
location / {
root /home/discourse/discourse/public;

View File

@ -0,0 +1,82 @@
# Alternative Install Options
Here lie some alternative installation options for Discourse. They're not the
recommended way of doing things, hence they're a bit out of the way.
Oh, and dragons. Lots of dragons.
## Web Server Alternative: apache2
If you instead want to use apache2 to serve the static pages:
# Run these commands as your normal login (e.g. "michael")
# If you don't have apache2 yet
sudo apt-get install apache2
# Edit your site details in a new apache2 config file
sudo vim /etc/apache2/sites-available/your-domain.com
# Put these info inside and change accordingly
<VirtualHost *:80>
ServerName your-domain.com
ServerAlias www.your-domain.com
DocumentRoot /srv/www/apps/discourse/public
<Directory /srv/www/apps/discourse/public>
AllowOverride all
Options -MultiViews
</Directory>
# Custom log file locations
ErrorLog /srv/www/apps/discourse/log/error.log
CustomLog /srv/www/apps/discourse/access.log combined
</VirtualHost>
# Install the Passenger Phusion gem and run the install
gem install passenger
passenger-install-apache2-module
# Next, we "create" a new apache2 module, passenger
sudo vim /etc/apache2/mods-available/passenger.load
# Inside paste (change the user accodingly)
LoadModule passenger_module /home/YOUR-USER/.rvm/gems/ruby-2.0.0-p0/gems/passenger-4.0.2/libout/apache2/mod_passenger.so
# Now the passenger module configuration
sudo vim /etc/apache2/mods-available/passenger.conf
# Inside, paste (change the user accodingly)
PassengerRoot /home/YOUR-USER/.rvm/gems/ruby-2.0.0-p0/gems/passenger-4.0.2
PassengerDefaultRuby /home/YOUR-USER/.rvm/wrappers/ruby-2.0.0-p0/ruby
# Now activate them all
sudo a2ensite your-domain.com
sudo a2enmod passenger
sudo service apache2 reload
sudo service apache2 restart
If you get any errors starting or reloading apache, please check the paths above - Ruby 2.0 should be there if you are using RVM, but it could get tricky.
## RVM Alternative: Systemwide installation
Taken from http://rvm.io/, the commands below installs RVM and users in the 'rvm' group have access to modify state:
# Run these commands as your normal login (e.g. "michael") \curl -s -S -L https://get.rvm.io | sudo bash -s stable
sudo adduser $USER rvm
newgrp rvm
. /etc/profile.d/rvm.sh
rvm requirements
# Build and install ruby
rvm install 2.0.0
gem install bundler
When creating the `discourse` user, add him/her/it to the RVM group:
# Run these commands as your normal login (e.g. "michael")
sudo adduser discourse rvm
RVM will be located in `/usr/local/rvm` directory instead of `/home/discourse/.rvm`, so update the crontab line respectively.

View File

@ -2,10 +2,16 @@
## What kind of hardware do you have?
- We *strongly* recommend 2GB of memory minimum if you don't want to deal with swap partitions during the install.
- We recommend at least a dual core CPU.
- Recommended minimum configuration is:
- 2GiB of RAM
- 2GiB of swap
- 2 processor cores
- With 2GB of memory and dual cores, you can run two instances of the thin
server (`NUM_WEBS=2`)
1 GB of memory and a single core CPU are the minimums for a steady state, running Discourse forum -- but it's simpler to just throw a bit more hardware at the problem if you can, particularly during the install.
1 GiB of memory, 3GiB of swap and a single core CPU are the minimums for a
steady state, running Discourse forum -- but it's simpler to just throw a bit
more hardware at the problem if you can, particularly during the install.
## Install Ubuntu Server 12.04 LTS with the package groups:
@ -51,7 +57,13 @@ Install necessary packages:
sudo apt-get update
sudo apt-get install redis-server
## Web Server Option: nginx
## Web Server: nginx
nginx is used for:
* reverse proxy (i.e. load balancer)
* static asset serving (since you don't want to do that from ruby)
* anonymous user cache
At Discourse, we recommend the latest version of nginx (we like the new and
shiny). To install on Ubuntu:
@ -73,83 +85,11 @@ shiny). To install on Ubuntu:
# install nginx
sudo apt-get update && sudo apt-get -y install nginx
## Web Server Option: apache2
If you instead want to use apache2 to serve the static pages:
# Run these commands as your normal login (e.g. "michael")
# If you don't have apache2 yet
sudo apt-get install apache2
# Edit your site details in a new apache2 config file
sudo vim /etc/apache2/sites-available/your-domain.com
# Put these info inside and change accordingly
<VirtualHost *:80>
ServerName your-domain.com
ServerAlias www.your-domain.com
DocumentRoot /srv/www/apps/discourse/public
<Directory /srv/www/apps/discourse/public>
AllowOverride all
Options -MultiViews
</Directory>
# Custom log file locations
ErrorLog /srv/www/apps/discourse/log/error.log
CustomLog /srv/www/apps/discourse/access.log combined
</VirtualHost>
# Install the Passenger Phusion gem and run the install
gem install passenger
passenger-install-apache2-module
# Next, we "create" a new apache2 module, passenger
sudo vim /etc/apache2/mods-available/passenger.load
# Inside paste (change the user accodingly)
LoadModule passenger_module /home/YOUR-USER/.rvm/gems/ruby-2.0.0-p0/gems/passenger-4.0.2/libout/apache2/mod_passenger.so
# Now the passenger module configuration
sudo vim /etc/apache2/mods-available/passenger.conf
# Inside, paste (change the user accodingly)
PassengerRoot /home/YOUR-USER/.rvm/gems/ruby-2.0.0-p0/gems/passenger-4.0.2
PassengerDefaultRuby /home/YOUR-USER/.rvm/wrappers/ruby-2.0.0-p0/ruby
# Now activate them all
sudo a2ensite your-domain.com
sudo a2enmod passenger
sudo service apache2 reload
sudo service apache2 restart
If you get any errors starting or reloading apache, please check the paths above - Ruby 2.0 should be there if you are using RVM, but it could get tricky.
## Install Ruby with RVM
### RVM Option: Systemwide installation
### RVM : Single-user installation
Taken from http://rvm.io/, the commands below installs RVM and users in the 'rvm' group have access to modify state:
# Run these commands as your normal login (e.g. "michael")
\curl -s -S -L https://get.rvm.io | sudo bash -s stable
sudo adduser $USER rvm
newgrp rvm
. /etc/profile.d/rvm.sh
rvm requirements
# Build and install ruby
rvm install 2.0.0
gem install bundler
### RVM Option: Single-user installation
Another sensible option (especially if only one Ruby app is on the machine) is
to install RVM isolated to a user's environment. Further instructions are
below.
We recommend installing RVM isolated to a single user's environment.
## Discourse setup
@ -157,9 +97,6 @@ Create Discourse user:
# Run these commands as your normal login (e.g. "michael")
sudo adduser --shell /bin/bash discourse
# If this fails, it's because you're doing the RVM single-user install.
# In that case, you could just not run it if errors make you squirrely
sudo adduser discourse rvm
Give Postgres database rights to the `discourse` user:
@ -172,7 +109,7 @@ Change to the 'discourse' user:
# Run this command as your normal login (e.g. "michael"), further commands should be run as 'discourse'
sudo su - discourse
Install RVM if doing a single-user RVM installation:
Install RVM
# As 'discourse'
# Install RVM
@ -304,7 +241,7 @@ Configure Bluepill:
Start Discourse:
# Run these commands as the discourse user
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ROOT=~/discourse RAILS_ENV=production NUM_WEBS=4 bluepill --no-privileged -c ~/.bluepill load ~/discourse/config/discourse.pill
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ROOT=~/discourse RAILS_ENV=production NUM_WEBS=2 bluepill --no-privileged -c ~/.bluepill load ~/discourse/config/discourse.pill
Add the Bluepill startup to crontab.
@ -313,10 +250,7 @@ Add the Bluepill startup to crontab.
Add the following lines:
@reboot RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ROOT=~/discourse RAILS_ENV=production NUM_WEBS=4 /home/discourse/.rvm/bin/bootup_bluepill --no-privileged -c ~/.bluepill load ~/discourse/config/discourse.pill
Note: in case of RVM system-wide installation RVM will be located in `/usr/local/rvm` directory instead of `/home/discourse/.rvm`, so update the line above respectively.
@reboot RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ROOT=~/discourse RAILS_ENV=production NUM_WEBS=2 /home/discourse/.rvm/bin/bootup_bluepill --no-privileged -c ~/.bluepill load ~/discourse/config/discourse.pill
## Log rotation setup
@ -360,6 +294,11 @@ The corresponding site setting is:
# Run these commands as the discourse user
bluepill stop
bluepill quit
# Back up your install
DATESTAMP=$(TZ=UTC date +%F-%T)
pg_dump --no-owner --clean discourse_prod | gzip -c > ~/discourse-db-$DATESTAMP.sql.gz
tar cfz ~/discourse-dir-$DATESTAMP.tar.gz -C ~ discourse
# Pull down the latest release
cd ~/discourse
git checkout master
@ -367,9 +306,65 @@ The corresponding site setting is:
git fetch --tags
# To run on the latest version instead of bleeding-edge:
#git checkout latest-release
#
# Follow the section below titled:
# "Check sample configuration files for new settings"
#
bundle install --without test --deployment
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ENV=production rake db:migrate
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ENV=production rake assets:precompile
bluepill start
# restart bluepill
crontab -l
# Here, run the command to start bluepill.
# Get it from the crontab output above.
Note that if bluepill *itself* needs to be restarted, it must be killed with `bluepill quit` and restarted with the same command that's in crontab
### Check sample configuration files for new settings
Check the sample configuration files provided in the repo with the ones being used for additional recommended settings and merge those in:
# Run these commands as the discourse user
cd ~/discourse
diff -u config/discourse.pill.sample config/discourse.pill
diff -u config/nginx.sample.conf /etc/nginx/conf.d/discourse.conf
diff -u config/environments/production.rb.sample config/environments/production.rb
#### Example 1
$ diff -u config/discourse.pill.sample config/discourse.pill
--- config/discourse.pill.sample 2013-07-15 17:38:06.501507001 +0000
+++ config/discourse.pill 2013-07-05 06:38:27.133506896 +0000
@@ -46,7 +46,7 @@
app.working_dir = rails_root
sockdir = "#{rails_root}/tmp/sockets"
- File.directory? sockdir or FileUtils.mkdir_p sockdir
+ File.directory? sockdir or Dir.mkdir sockdir
num_webs.times do |i|
app.process("thin-#{i}") do |process|
This change reflects us switching to using `FileUtils.mkdir_p` instead of `Dir.mkdir`.
#### Example 2
$ diff -u config/nginx.sample.conf /etc/nginx/conf.d/discourse.conf
--- config/nginx.sample.conf 2013-07-15 17:38:06.521507000 +0000
+++ /etc/nginx/conf.d/discourse.conf 2013-07-15 17:52:46.649507024 +0000
@@ -12,17 +12,18 @@
gzip_min_length 1000;
gzip_types application/json text/css application/x-javascript;
- server_name enter.your.web.hostname.here;
+ server_name webtier.discourse.org;
sendfile on;
keepalive_timeout 65;
- client_max_body_size 2m;
location / {
root /home/discourse/discourse/public;
This change reflects a change in placeholder information plus (importantly)
adding the `client_max_body_size 2m;` directive to the nginx.conf. This change
should also be made to your production file.

72
docs/MIGRATION.md Normal file
View File

@ -0,0 +1,72 @@
# Discourse Migration Guide
## Install new server
Complete a fresh install of Discourse on the new server, following the official guide, except for the initial database population (rake db:migrate).
## Review old server
On old server, run `git status` and review changes to the tree. For example:
# On branch master
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: app/assets/javascripts/external/Markdown.Editor.js
# modified: app/views/layouts/application.html.erb
# modified: config/application.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# app/views/layouts/application.html.erb.bitnami
# config/environments/production.rb
# log/clockworkd.clock.output
# log/clockworkd.clock.pid
# log/sidekiq.pid
# vendor/gems/active_model_serializers/
# vendor/gems/fast_blank/
# vendor/gems/message_bus/
# vendor/gems/redis-rack-cache/
# vendor/gems/sprockets/
# vendor/gems/vestal_versions/
### Review for changes
Review each of the changed files for changes that need to be manually moved over
* Ignore all files under vendor/gems
* Ignore files under log/
Check your config/environments/production.rb, config/discourse.pill,
config/database.yml (as per the upgrade instructions)
## Move DB
Take DB dump with:
pg_dump --no-owner -U user_name -W database_name
Copy it over to the new server
Run as discourse user:
* createdb discourse_prod
* psql discourse_prod
* \i discourse_dump_from_old_server.sql
On oldserver:
* rsync -avz -e ssh public newserver:public
bundle install --without test --deployment
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ENV=production rake db:migrate
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ENV=production rake assets:precompile
RUBY_GC_MALLOC_LIMIT=90000000 RAILS_ENV=production rake posts:rebake
Are you just testing your migration? Disable outgoing email by changing
`config/environments/production.rb` and adding the following below the mail
configuration:
config.action_mailer.perform_deliveries = false

View File

@ -3,56 +3,55 @@
Are you having trouble setting up Discourse? Here are some basic things to
check before reaching out to the community for help:
1. Are you running Ruby 1.9.3 or later?
Discourse is designed for Ruby 1.9.3 or later. You can check your version by
typing `ruby -v` and checking the response.
typing `ruby -v` (as the discourse user) and checking the response for
something like:
`ruby 2.0.0p195 (2013-05-14 revision 40734) [x86_64-linux]`
2. Are you on Postgres 9.1 or later with HSTORE enabled?
1. Are you on Postgres 9.1 or later with HSTORE enabled?
You can check your postgres version by typing `psql --version`. To see if hstore is
installed, open a session to postgres and type `\dx` and see if hstore is listed.
You can check your postgres version by typing `psql --version`. To see if
hstore is installed, open a session to postgres and type `\dx` and see if
hstore is listed.
3. Have you run `bundle install`?
1. Have you run `bundle install`?
We frequently update our dependencies to newer versions. It is a good idea to run
`bundle install` every time you check out Discourse, especially if it's been a while.
We frequently update our dependencies to newer versions. It is a good idea
to run `bundle install` every time you check out Discourse, especially if it's
been a while.
4. Did you run `bundle update`?
1. Did you run `bundle update`?
Don't. Running `bundle update` will download gem versions that we haven't tested with.
The Gemfile.lock has the gem versions that Discourse currently uses, so `bundle install`
will work. If you ran update, then you should uninstall the gems, run
`git checkout -- Gemfile.lock` and then run `bundle install`.
Don't. Running `bundle update` will download gem versions that we haven't
tested with. The Gemfile.lock has the gem versions that Discourse currently
uses, so `bundle install` will work. If you ran update, then you should
uninstall the gems, run `git checkout -- Gemfile.lock` and then run `bundle
install`.
5. Have you migrated your database?
1. Have you migrated your database?
Our schema changes fairly frequently. After checking out the source code, you should
run `rake db:migrate`
Our schema changes fairly frequently. After checking out the source code,
you should run `rake db:migrate`.
1. Do the tests pass?
6. Have you added the seed data?
If you are having other problems, it's useful to know if the test suite
passes. You can run it by first using `rake db:test:prepare` and then `rake
spec`. If you experience any failures, that's a bad sign! Our master branch
should *always* pass every test.
We depend on some basic seed data being present in the database. You should run
`rake db:seed_fu` to keep your database in sync.
1. Have you updated host_names in your database.yml?
7. Do the tests pass?
If you are having other problems, it's useful to know if the test suite passes. You
can run it by first using `rake db:test:prepare` and then `rake spec`. If you
experience any failures, that's a bad sign! Our master branch should *always* pass
every test.
8. Have you updated host_names in your database.yml?
If links in emails have localhost in them, then you are still using the default host_names
value in database.yml. Update it to use your site's host name(s).
If links in emails have localhost in them, then you are still using the
default `host_names` value in database.yml. Update it to use your site's host
name(s).
9. Are you having problems bundling:
1. Are you having problems bundling:
```
ArgumentError: invalid byte sequence in US-ASCII
@ -75,3 +74,13 @@ Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8
end
```
---
Check your ~/discourse/log/production.log file if you are getting HTTP 500
errors.
Some common situations:
**Problem:** `ActiveRecord::StatementInvalid (PG::Error: ERROR: column X does not exist`
**Solution**: run `db:migrate` task to apply migrations to the database

View File

@ -1,7 +1,7 @@
class AvatarLookup
def initialize(user_ids=[])
@user_ids = user_ids.tap(&:compact!).tap(&:uniq!)
@user_ids = user_ids.tap(&:compact!).tap(&:uniq!).tap(&:flatten!)
end
# Lookup a user by id

View File

@ -223,10 +223,12 @@ class CookedPostProcessor
def attachments
if SiteSetting.enable_s3_uploads?
@doc.css("a[href^=\"#{S3Store.base_url}\"]")
@doc.css("a.attachment[href^=\"#{S3Store.base_url}\"]")
else
# local uploads are identified using a relative uri
@doc.css("a[href^=\"#{LocalStore.directory}\"]")
@doc.css("a.attachment[href^=\"#{LocalStore.directory}\"]") +
# when cdn is enabled, we have the whole url
@doc.css("a.attachment[href^=\"#{LocalStore.base_url}\"]")
end
end

View File

@ -1,124 +1,125 @@
module HTML
class WhiteListSanitizer
# Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
def sanitize_css(style)
# disallow urls
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
unless Rails.version =~ /^4/
module HTML
class WhiteListSanitizer
# Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
def sanitize_css(style)
# disallow urls
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
# gauntlet
if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
return ''
end
# gauntlet
if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
return ''
end
clean = []
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
if allowed_css_properties.include?(prop.downcase)
clean << prop + ': ' + val + ';'
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
unless val.split().any? do |keyword|
!allowed_css_keywords.include?(keyword) &&
keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
end
clean << prop + ': ' + val + ';'
clean = []
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
if allowed_css_properties.include?(prop.downcase)
clean << prop + ': ' + val + ';'
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
unless val.split().any? do |keyword|
!allowed_css_keywords.include?(keyword) &&
keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
end
clean << prop + ': ' + val + ';'
end
end
end
clean.join(' ')
end
clean.join(' ')
end
end
end
module HTML
class WhiteListSanitizer
self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
module HTML
class WhiteListSanitizer
self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
def contains_bad_protocols?(attr_name, value)
uri_attributes.include?(attr_name) &&
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
def contains_bad_protocols?(attr_name, value)
uri_attributes.include?(attr_name) &&
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
end
end
end
end
module ActiveRecord
class Relation
module ActiveRecord
class Relation
def where_values_hash
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
def where_values_hash
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
Hash[equalities.map { |where| [where.left.name, where.right] }].with_indifferent_access
end
Hash[equalities.map { |where| [where.left.name, where.right] }].with_indifferent_access
end
end
end
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table, allow_table_name = true)
predicates = attributes.map do |column, value|
table = default_table
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table, allow_table_name = true)
predicates = attributes.map do |column, value|
table = default_table
if allow_table_name && value.is_a?(Hash)
table = Arel::Table.new(column, engine)
if allow_table_name && value.is_a?(Hash)
table = Arel::Table.new(column, engine)
if value.empty?
'1 = 2'
else
build_from_hash(engine, value, table, false)
end
else
column = column.to_s
if allow_table_name && column.include?('.')
table_name, column = column.split('.', 2)
table = Arel::Table.new(table_name, engine)
end
attribute = table[column]
case value
when ActiveRecord::Relation
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
array_predicates = ranges.map {|range| attribute.in(range)}
if values.include?(nil)
values = values.compact
if values.empty?
array_predicates << attribute.eq(nil)
else
array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
end
if value.empty?
'1 = 2'
else
array_predicates << attribute.in(values)
build_from_hash(engine, value, table, false)
end
else
column = column.to_s
if allow_table_name && column.include?('.')
table_name, column = column.split('.', 2)
table = Arel::Table.new(table_name, engine)
end
array_predicates.inject {|composite, predicate| composite.or(predicate)}
when Range, Arel::Relation
attribute.in(value)
when ActiveRecord::Base
attribute.eq(value.id)
when Class
# FIXME: I think we need to deprecate this behavior
attribute.eq(value.name)
when Integer, ActiveSupport::Duration
# Arel treats integers as literals, but they should be quoted when compared with strings
column = engine.connection.schema_cache.columns_hash[table.name][attribute.name.to_s]
attribute.eq(Arel::Nodes::SqlLiteral.new(engine.connection.quote(value, column)))
else
attribute.eq(value)
attribute = table[column]
case value
when ActiveRecord::Relation
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
array_predicates = ranges.map {|range| attribute.in(range)}
if values.include?(nil)
values = values.compact
if values.empty?
array_predicates << attribute.eq(nil)
else
array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
end
else
array_predicates << attribute.in(values)
end
array_predicates.inject {|composite, predicate| composite.or(predicate)}
when Range, Arel::Relation
attribute.in(value)
when ActiveRecord::Base
attribute.eq(value.id)
when Class
# FIXME: I think we need to deprecate this behavior
attribute.eq(value.name)
when Integer, ActiveSupport::Duration
# Arel treats integers as literals, but they should be quoted when compared with strings
column = engine.connection.schema_cache.columns_hash[table.name][attribute.name.to_s]
attribute.eq(Arel::Nodes::SqlLiteral.new(engine.connection.quote(value, column)))
else
attribute.eq(value)
end
end
end
end
predicates.flatten
predicates.flatten
end
end
end
end

View File

@ -267,7 +267,7 @@ class Guardian
end
def can_edit_post?(post)
is_staff? || (not(post.topic.archived?) && is_my_own?(post))
is_staff? || (!post.topic.archived? && is_my_own?(post) && !post.user_deleted &&!post.deleted_at)
end
def can_edit_user?(user)
@ -291,7 +291,7 @@ class Guardian
# Recovery Method
def can_recover_post?(post)
is_staff?
is_staff? || (is_my_own?(post) && post.user_deleted && !post.deleted_at)
end
def can_recover_topic?(topic)

View File

@ -0,0 +1,8 @@
module Jobs
# various consistency checks
class DestroyOldDeletionStubs < Jobs::Base
def execute(args)
PostDestroyer.destroy_stubs
end
end
end

View File

@ -48,10 +48,14 @@ module Jobs
def order_columns_for(model)
@order_columns_for_hash ||= {
'CategoryFeaturedTopic' => 'category_id, topic_id',
'CategorySearchData' => 'category_id',
'PostOneboxRender' => 'post_id, onebox_render_id',
'PostReply' => 'post_id, reply_id',
'PostSearchData' => 'post_id',
'PostTiming' => 'topic_id, post_number, user_id',
'SiteContent' => 'content_type',
'TopicUser' => 'topic_id, user_id',
'UserSearchData' => 'user_id',
'View' => 'parent_id, parent_type, ip_address, viewed_at'
}
@order_columns_for_hash[model.name]

View File

@ -4,7 +4,7 @@ module LocalStore
unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0,16]
extension = File.extname(file.original_filename)
clean_name = "#{unique_sha1}#{extension}"
url_root = "/uploads/#{RailsMultisite::ConnectionManagement.current_db}/#{upload_id}"
url_root = "#{directory}/#{upload_id}"
path = "#{Rails.root}/public#{url_root}"
FileUtils.mkdir_p path
@ -41,7 +41,7 @@ module LocalStore
end
def self.asset_host
ActionController::Base.asset_host
Rails.configuration.action_controller.asset_host
end
end

View File

@ -20,6 +20,7 @@ class PostCreator
# who is the post "author." For example when copying posts to a new
# topic.
# created_at - Post creation time (optional)
# auto_track - Automatically track this topic if needed (default true)
#
# When replying to a topic:
# topic_id - topic we're replying to
@ -65,6 +66,7 @@ class PostCreator
store_unique_post_key
send_notifications_for_private_message
track_topic
update_topic_stats
update_user_counts
publish
@post.advance_draft_sequence
@ -99,19 +101,6 @@ class PostCreator
post.last_version_at ||= Time.now
end
def self.after_create_tasks(post)
# Update attributes on the topic - featured users and last posted.
attrs = {last_posted_at: post.created_at, last_post_user_id: post.user_id}
attrs[:bumped_at] = post.created_at unless post.no_bump
post.topic.update_attributes(attrs)
# Update topic user data
TopicUser.change(post.user.id,
post.topic.id,
posted: true,
last_read_post_number: post.post_number,
seen_post_count: post.post_number)
end
protected
@ -183,6 +172,13 @@ class PostCreator
@topic = topic
end
def update_topic_stats
# Update attributes on the topic - featured users and last posted.
attrs = {last_posted_at: @post.created_at, last_post_user_id: @post.user_id}
attrs[:bumped_at] = @post.created_at unless @post.no_bump
@topic.update_attributes(attrs)
end
def setup_post
post = @topic.posts.new(raw: @opts[:raw],
user: @user,
@ -264,7 +260,15 @@ class PostCreator
end
def track_topic
TopicUser.auto_track(@user.id, @topic.id, TopicUser.notification_reasons[:created_post])
unless @opts[:auto_track] == false
TopicUser.auto_track(@user.id, @topic.id, TopicUser.notification_reasons[:created_post])
# Update topic user data
TopicUser.change(@post.user.id,
@post.topic.id,
posted: true,
last_read_post_number: @post.post_number,
seen_post_count: @post.post_number)
end
end
def enqueue_jobs

View File

@ -4,6 +4,13 @@
#
class PostDestroyer
def self.destroy_stubs
Post.where(deleted_at: nil, user_deleted: true)
.where('updated_at < ? AND post_number > 1', 1.day.ago).each do |post|
PostDestroyer.new(Discourse.system_user, post).destroy
end
end
def initialize(user, post)
@user, @post = user, post
end
@ -16,6 +23,19 @@ class PostDestroyer
end
end
def recover
if @user.staff? && @post.deleted_at
staff_recovered
elsif @user.staff? || @user.id == @post.user_id
user_recovered
end
@post.topic.update_statistics
end
def staff_recovered
@post.recover!
end
# When a post is properly deleted. Well, it's still soft deleted, but it will no longer
# show up in the topic
def staff_destroyed
@ -75,4 +95,12 @@ class PostDestroyer
end
end
def user_recovered
Post.transaction do
@post.update_column(:user_deleted, false)
@post.revise(@user, @post.versions.last.modifications["raw"][0], force_new_version: true)
@post.update_flagged_posts_count
end
end
end

View File

@ -13,10 +13,6 @@ module S3Store
end
def self.base_url
"//s3.amazonaws.com/#{SiteSetting.s3_upload_bucket}"
end
def self.base_url_old
"//#{SiteSetting.s3_upload_bucket.downcase}.s3.amazonaws.com"
end

View File

@ -2,6 +2,10 @@ class TopicCreator
attr_accessor :errors
def self.create(user, guardian, opts)
self.new(user, guardian, opts).create
end
def initialize(user, guardian, opts)
@user = user
@guardian = guardian
@ -17,11 +21,19 @@ class TopicCreator
process_private_message if @opts[:archetype] == Archetype.private_message
save_topic
watch_topic
@topic
end
private
def watch_topic
unless @opts[:auto_track] == false
@topic.notifier.watch_topic!(@topic.user_id)
end
end
def setup
topic_params = {title: @opts[:title], user_id: @user.id, last_post_user_id: @user.id}
topic_params[:archetype] = @opts[:archetype] if @opts[:archetype].present?

View File

@ -7,6 +7,7 @@ class Validators::PostValidator < ActiveModel::Validator
raw_quality(record)
max_mention_validator(record)
max_images_validator(record)
max_attachments_validator(record)
max_links_validator(record)
unique_post_validator(record)
end
@ -41,6 +42,11 @@ class Validators::PostValidator < ActiveModel::Validator
add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post)
end
# Ensure new users can not put too many attachments in a post
def max_attachments_validator(post)
add_error_if_count_exceeded(post, :too_many_attachments, post.attachment_count, SiteSetting.newuser_max_attachments) unless acting_user_is_trusted?(post)
end
# Ensure new users can not put too many links in a post
def max_links_validator(post)
add_error_if_count_exceeded(post, :too_many_links, post.link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted?(post)

View File

@ -65,13 +65,13 @@ describe CookedPostProcessor do
context "with locally uploaded images" do
let(:upload) { Fabricate(:upload) }
let(:post) { Fabricate(:post_with_uploaded_images) }
let(:post) { Fabricate(:post_with_uploaded_image) }
let(:cpp) { CookedPostProcessor.new(post) }
before { FastImage.stubs(:size) }
before { FastImage.stubs(:size).returns([200, 400]) }
# all in one test to speed things up
it "works" do
Upload.expects(:get_from_url).returns(upload).twice
Upload.expects(:get_from_url).returns(upload)
cpp.post_process_images
# ensures absolute urls on uploaded images
cpp.html.should =~ /#{LocalStore.base_url}/
@ -148,7 +148,7 @@ describe CookedPostProcessor do
context "topic image" do
let(:topic) { build(:topic, id: 1) }
let(:post) { Fabricate(:post_with_uploaded_images, topic: topic) }
let(:post) { Fabricate(:post_with_uploaded_image, topic: topic) }
let(:cpp) { CookedPostProcessor.new(post) }
it "adds a topic image if there's one in the post" do

View File

@ -485,6 +485,16 @@ describe Guardian do
Guardian.new(post.user).can_edit?(post).should be_true
end
it 'returns false if you are trying to edit a post you soft deleted' do
post.user_deleted = true
Guardian.new(post.user).can_edit?(post).should be_false
end
it 'returns false if you are trying to edit a deleted post' do
post.deleted_at = 1.day.ago
Guardian.new(post.user).can_edit?(post).should be_false
end
it 'returns false if another regular user tries to edit your post' do
Guardian.new(coding_horror).can_edit?(post).should be_false
end

View File

@ -12,12 +12,12 @@ describe Jobs::FeatureTopicUsers do
end
context 'with a topic' do
let!(:post) { Fabricate(:post) }
let!(:post) { create_post }
let(:topic) { post.topic }
let!(:coding_horror) { Fabricate(:coding_horror) }
let!(:evil_trout) { Fabricate(:evil_trout) }
let!(:second_post) { Fabricate(:post, topic: topic, user: coding_horror)}
let!(:third_post) { Fabricate(:post, topic: topic, user: evil_trout)}
let!(:second_post) { create_post(topic: topic, user: coding_horror)}
let!(:third_post) { create_post(topic: topic, user: evil_trout)}
it "won't feature the OP" do
Jobs::FeatureTopicUsers.new.execute(topic_id: topic.id)

View File

@ -21,6 +21,12 @@ describe PostCreator do
let(:creator_with_meta_data) { PostCreator.new(user, basic_topic_params.merge(meta_data: {hello: "world"} )) }
let(:creator_with_image_sizes) { PostCreator.new(user, basic_topic_params.merge(image_sizes: image_sizes)) }
it "can be created with auto tracking disabled" do
p = PostCreator.create(user, basic_topic_params.merge(auto_track: false))
# must be 0 otherwise it will think we read the topic which is clearly untrue
TopicUser.where(user_id: p.user_id, topic_id: p.topic_id).count.should == 0
end
it "ensures the user can create the topic" do
Guardian.any_instance.expects(:can_create?).with(Topic,nil).returns(false)
lambda { creator.create }.should raise_error(Discourse::InvalidAccess)
@ -148,8 +154,15 @@ describe PostCreator do
it 'increases topic response counts' do
first_post = creator.create
user2 = Fabricate(:coding_horror)
# ensure topic user is correct
topic_user = first_post.user.topic_users.where(topic_id: first_post.topic_id).first
topic_user.should be_present
topic_user.should be_posted
topic_user.last_read_post_number.should == first_post.post_number
topic_user.seen_post_count.should == first_post.post_number
user2 = Fabricate(:coding_horror)
user2.topic_reply_count.should == 0
first_post.user.reload.topic_reply_count.should == 0

View File

@ -8,7 +8,32 @@ describe PostDestroyer do
end
let(:moderator) { Fabricate(:moderator) }
let(:post) { Fabricate(:post) }
let(:post) { create_post }
describe 'destroy_old_stubs' do
it 'destroys stubs for deleted by user posts' do
Fabricate(:admin)
reply1 = create_post(topic: post.topic)
reply2 = create_post(topic: post.topic)
reply3 = create_post(topic: post.topic)
PostDestroyer.new(reply1.user, reply1).destroy
PostDestroyer.new(reply2.user, reply2).destroy
reply2.update_column(:updated_at, 2.days.ago)
PostDestroyer.destroy_stubs
reply1.reload
reply2.reload
reply3.reload
reply1.deleted_at.should == nil
reply2.deleted_at.should_not == nil
reply3.deleted_at.should == nil
end
end
describe 'basic destroying' do
@ -17,6 +42,7 @@ describe PostDestroyer do
context "as the creator of the post" do
before do
@orig = post.cooked
PostDestroyer.new(post.user, post).destroy
post.reload
end
@ -24,8 +50,16 @@ describe PostDestroyer do
it "doesn't delete the post" do
post.deleted_at.should be_blank
post.deleted_by.should be_blank
post.user_deleted.should be_true
post.raw.should == I18n.t('js.post.deleted_by_author')
post.version.should == 2
# lets try to recover
PostDestroyer.new(post.user, post).recover
post.reload
post.version.should == 3
post.user_deleted.should be_false
post.cooked.should == @orig
end
end
@ -56,10 +90,10 @@ describe PostDestroyer do
context 'deleting the second post in a topic' do
let(:user) { Fabricate(:user) }
let!(:post) { Fabricate(:post, user: user) }
let(:topic) { post.topic }
let!(:post) { create_post(user: user) }
let(:topic) { post.topic.reload }
let(:second_user) { Fabricate(:coding_horror) }
let!(:second_post) { Fabricate(:post, topic: topic, user: second_user) }
let!(:second_post) { create_post(topic: topic, user: second_user) }
before do
PostDestroyer.new(moderator, second_post).destroy

View File

@ -24,8 +24,7 @@ describe S3Store do
end
it 'returns the url of the S3 upload if successful' do
# NOTE: s3 bucket's name are case sensitive so we can't use it as a subdomain...
S3Store.store_file(file, "SHA", 1).should == '//s3.amazonaws.com/S3_Upload_Bucket/1SHA.png'
S3Store.store_file(file, "SHA", 1).should == '//s3_upload_bucket.s3.amazonaws.com/1SHA.png'
end
after(:each) do

View File

@ -230,7 +230,7 @@ describe TopicQuery do
end
context 'created topics' do
let!(:created_topic) { Fabricate(:post, user: user).topic }
let!(:created_topic) { create_post(user: user).topic }
it "includes the created topic" do
topics.include?(created_topic).should be_true
@ -238,8 +238,8 @@ describe TopicQuery do
end
context "topic you've posted in" do
let(:other_users_topic) { Fabricate(:post, user: creator).topic }
let!(:your_post) { Fabricate(:post, user: user, topic: other_users_topic )}
let(:other_users_topic) { create_post(user: creator).topic }
let!(:your_post) { create_post(user: user, topic: other_users_topic )}
it "includes the posted topic" do
topics.include?(other_users_topic).should be_true

View File

@ -3,7 +3,7 @@ require 'topic_view'
describe TopicView do
let(:topic) { Fabricate(:topic) }
let(:topic) { create_topic }
let(:coding_horror) { Fabricate(:coding_horror) }
let(:first_poster) { topic.user }
@ -109,53 +109,39 @@ describe TopicView do
let(:path) { "/1234" }
before do
topic.expects(:relative_url).returns(path)
described_class.any_instance.expects(:find_topic).with(1234).returns(topic)
topic.stubs(:relative_url).returns(path)
TopicView.any_instance.stubs(:find_topic).with(1234).returns(topic)
end
context "without a post number" do
context "without a page" do
it "generates a canonical path for a topic" do
described_class.new(1234, user).canonical_path.should eql(path)
end
end
context "with a page" do
let(:path_with_page) { "/1234?page=5" }
it "generates a canonical path for a topic" do
described_class.new(1234, user, page: 5).canonical_path.should eql(path_with_page)
end
end
it "generates canonical path correctly" do
TopicView.new(1234, user).canonical_path.should eql(path)
TopicView.new(1234, user, page: 5).canonical_path.should eql("/1234?page=5")
end
context "with a post number" do
let(:path_with_page) { "/1234?page=10" }
before { SiteSetting.stubs(:posts_per_page).returns(5) }
it "generates a canonical path for a topic" do
described_class.new(1234, user, post_number: 50).canonical_path.should eql(path_with_page)
end
it "generates a canonical correctly for paged results" do
SiteSetting.stubs(:posts_per_page).returns(5)
TopicView.new(1234, user, post_number: 50).canonical_path.should eql("/1234?page=10")
end
end
describe "#next_page" do
let(:p2) { stub(post_number: 2) }
let(:topic) do
topic = Fabricate(:topic)
topic = create_topic
topic.stubs(:highest_post_number).returns(5)
topic
end
let(:user) { Fabricate(:user) }
before do
described_class.any_instance.expects(:find_topic).with(1234).returns(topic)
described_class.any_instance.stubs(:filter_posts)
described_class.any_instance.stubs(:last_post).returns(p2)
TopicView.any_instance.expects(:find_topic).with(1234).returns(topic)
TopicView.any_instance.stubs(:filter_posts)
TopicView.any_instance.stubs(:last_post).returns(p2)
SiteSetting.stubs(:posts_per_page).returns(2)
end
it "should return the next page" do
described_class.new(1234, user).next_page.should eql(2)
TopicView.new(1234, user).next_page.should eql(2)
end
end
@ -183,17 +169,17 @@ describe TopicView do
end
context '.read?' do
it 'is unread with no logged in user' do
it 'tracks correctly' do
# anon has nothing
TopicView.new(topic.id).read?(1).should be_false
end
it 'makes posts as unread by default' do
# random user has nothing
topic_view.read?(1).should be_false
end
it 'knows a post is read when it has a PostTiming' do
PostTiming.create(topic: topic, user: coding_horror, post_number: 1, msecs: 1000)
topic_view.read?(1).should be_true
# a real user that just read it should have it marked
PostTiming.process_timings(coding_horror, topic.id, 1, [[1,1000]])
TopicView.new(topic.id, coding_horror).read?(1).should be_true
TopicView.new(topic.id, coding_horror).topic_user.should be_present
end
end
@ -201,10 +187,6 @@ describe TopicView do
it 'returns nil when there is no user' do
TopicView.new(topic.id, nil).topic_user.should be_blank
end
it 'returns a record once the user has some data' do
TopicView.new(topic.id, coding_horror).topic_user.should be_present
end
end
context '#recent_posts' do

View File

@ -6,13 +6,13 @@ describe Unread do
before do
@topic = Fabricate(:topic, posts_count: 13, highest_post_number: 13)
@topic.notifier.watch_topic!(@topic.user_id)
@topic_user = TopicUser.get(@topic, @topic.user)
@topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:tracking])
@topic_user.notification_level = TopicUser.notification_levels[:tracking]
@unread = Unread.new(@topic, @topic_user)
end
describe 'unread_posts' do
it 'should have 0 unread posts if the user has seen all posts' do
@topic_user.stubs(:last_read_post_number).returns(13)

View File

@ -124,10 +124,14 @@ describe PostsController do
response.should be_forbidden
end
it "calls recover and updates the topic's statistics" do
Post.any_instance.expects(:recover!)
Topic.any_instance.expects(:update_statistics)
it "recovers a post correctly" do
topic_id = create_post.topic_id
post = create_post(topic_id: topic_id)
PostDestroyer.new(user, post).destroy
xhr :put, :recover, post_id: post.id
post.reload
post.deleted_at.should == nil
end
end

View File

@ -43,11 +43,8 @@ Fabricator(:post_with_images_in_quote_and_onebox, from: :post) do
'
end
Fabricator(:post_with_uploaded_images, from: :post) do
cooked '
<img src="/uploads/default/2/3456789012345678.png" width="1500" height="2000">
<img src="/uploads/default/1/1234567890123456.jpg">
'
Fabricator(:post_with_uploaded_image, from: :post) do
cooked '<img src="/uploads/default/2/3456789012345678.png" width="1500" height="2000">'
end
Fabricator(:post_with_an_attachment, from: :post) do

View File

@ -19,8 +19,8 @@ describe PostAction do
Given(:spammer) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
context 'spammer post is not flagged enough times' do
Given!(:spam_post) { Fabricate(:post, user: spammer) }
Given!(:spam_post2) { Fabricate(:post, user: spammer) }
Given!(:spam_post) { create_post(user: spammer) }
Given!(:spam_post2) { create_post(user: spammer) }
When { PostAction.act(user1, spam_post, PostActionType.types[:spam]) }
Then { expect(spam_post.reload).to_not be_hidden }

View File

@ -14,6 +14,7 @@ describe PostAction do
describe "flagged_posts_report" do
it "operates correctly" do
post = create_post
PostAction.act(codinghorror, post, PostActionType.types[:spam])
mod_message = PostAction.act(Fabricate(:user), post, PostActionType.types[:notify_moderators], message: "this is a 10")
@ -30,6 +31,7 @@ describe PostAction do
describe "messaging" do
it "notify moderators integration test" do
post = create_post
mod = moderator
action = PostAction.act(codinghorror, post, PostActionType.types[:notify_moderators], message: "this is my special long message");
@ -100,6 +102,7 @@ describe PostAction do
end
it "should ignore validated flags" do
post = create_post
admin = Fabricate(:admin)
PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
post.hidden.should be_false
@ -193,7 +196,7 @@ describe PostAction do
context "flag_counts_for" do
it "returns the correct flag counts" do
post = Fabricate(:post)
post = create_post
SiteSetting.stubs(:flags_required_to_hide_post).returns(7)
@ -244,7 +247,7 @@ describe PostAction do
end
it 'should follow the rules for automatic hiding workflow' do
post = Fabricate(:post)
post = create_post
u1 = Fabricate(:evil_trout)
u2 = Fabricate(:walter_white)
admin = Fabricate(:admin) # we need an admin for the messages

View File

@ -184,6 +184,54 @@ describe Post do
end
describe "maximum attachments" do
let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
let(:post_no_attachments) { Fabricate.build(:post, post_args.merge(user: newuser)) }
let(:post_one_attachment) { post_with_body('<a class="attachment" href="/uploads/default/1/2082985.txt">file.txt</a>', newuser) }
let(:post_two_attachments) { post_with_body('<a class="attachment" href="/uploads/default/2/20947092.log">errors.log</a> <a class="attachment" href="/uploads/default/3/283572385.3ds">model.3ds</a>', newuser) }
it "returns 0 attachments for an empty post" do
Fabricate.build(:post).attachment_count.should == 0
end
it "finds attachments from HTML" do
post_two_attachments.attachment_count.should == 2
end
context "validation" do
before do
SiteSetting.stubs(:newuser_max_attachments).returns(1)
end
context 'newuser' do
it "allows a new user to post below the limit" do
post_one_attachment.should be_valid
end
it "doesn't allow more than the maximum" do
post_two_attachments.should_not be_valid
end
it "doesn't allow a new user to edit their post to insert an attachment" do
post_no_attachments.user.trust_level = TrustLevel.levels[:new]
post_no_attachments.save
-> {
post_no_attachments.revise(post_no_attachments.user, post_two_attachments.raw)
post_no_attachments.reload
}.should_not change(post_no_attachments, :raw)
end
end
it "allows more attachments from a not-new account" do
post_two_attachments.user.trust_level = TrustLevel.levels[:basic]
post_two_attachments.should be_valid
end
end
end
context "links" do
let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
let(:no_links) { post_with_body("hello world my name is evil trout", newuser) }
@ -570,19 +618,6 @@ describe Post do
post.replies.should be_blank
end
describe 'a forum topic user record for the topic' do
let(:topic_user) { post.user.topic_users.where(topic_id: topic.id).first }
it 'is set correctly' do
topic_user.should be_present
topic_user.should be_posted
topic_user.last_read_post_number.should == post.post_number
topic_user.seen_post_count.should == post.post_number
end
end
describe 'extract_quoted_post_numbers' do
let!(:post) { Fabricate(:post, post_args) }

View File

@ -339,7 +339,7 @@ describe Topic do
it 'updates the bumped_at field when a new post is made' do
@topic.bumped_at.should be_present
lambda {
Fabricate(:post, topic: @topic, user: @topic.user)
create_post(topic: @topic, user: @topic.user)
@topic.reload
}.should change(@topic, :bumped_at)
end
@ -621,8 +621,8 @@ describe Topic do
context 'last_poster info' do
before do
@user = Fabricate(:user)
@post = Fabricate(:post, user: @user)
@post = create_post
@user = @post.user
@topic = @post.topic
end
@ -633,7 +633,7 @@ describe Topic do
context 'after a second post' do
before do
@second_user = Fabricate(:coding_horror)
@new_post = Fabricate(:post, topic: @topic, user: @second_user)
@new_post = create_post(topic: @topic, user: @second_user)
@topic.reload
end

View File

@ -7,7 +7,7 @@ describe TopicTrackingState do
end
let(:post) do
Fabricate(:post)
create_post
end
it "can correctly publish unread" do
@ -20,6 +20,7 @@ describe TopicTrackingState do
report.length.should == 0
new_post = post
post.topic.notifier.watch_topic!(post.topic.user_id)
report = TopicTrackingState.report([user.id])
@ -38,7 +39,7 @@ describe TopicTrackingState do
TopicTrackingState.report([user.id], post.topic_id + 1).should be_empty
# when we reply the poster should have an unread row
Fabricate(:post, user: user, topic: post.topic)
create_post(user: user, topic: post.topic)
report = TopicTrackingState.report([post.user_id, user.id])
report.length.should == 1

View File

@ -11,8 +11,12 @@ describe TopicUser do
DateTime.expects(:now).at_least_once.returns(yesterday)
end
let!(:topic) { Fabricate(:topic) }
let!(:user) { Fabricate(:coding_horror) }
let!(:topic) {
user = Fabricate(:user)
guardian = Guardian.new(user)
TopicCreator.create(user, guardian, title: "this is my topic title")
}
let(:topic_user) { TopicUser.get(topic,user) }
let(:topic_creator_user) { TopicUser.get(topic, topic.user) }
@ -228,6 +232,7 @@ describe TopicUser do
it "is able to self heal" do
p1 = Fabricate(:post)
p2 = Fabricate(:post, user: p1.user, topic: p1.topic, post_number: 2)
p1.topic.notifier.watch_topic!(p1.user_id)
TopicUser.exec_sql("UPDATE topic_users set seen_post_count=100, last_read_post_number=0
WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: p1.topic_id, user_id: p1.user_id)

View File

@ -153,18 +153,18 @@ describe Upload do
it "identifies internal or relatives urls" do
Discourse.expects(:base_url_no_prefix).returns("http://discuss.site.com")
Upload.has_been_uploaded?("http://discuss.site.com/uploads/default/42/0123456789ABCDEF.jpg").should == true
Upload.has_been_uploaded?("/uploads/42/0123456789ABCDEF.jpg").should == true
Upload.has_been_uploaded?("/uploads/default/42/0123456789ABCDEF.jpg").should == true
end
it "identifies internal urls when using a CDN" do
ActionController::Base.expects(:asset_host).returns("http://my.cdn.com").twice
Rails.configuration.action_controller.expects(:asset_host).returns("http://my.cdn.com").twice
Upload.has_been_uploaded?("http://my.cdn.com/uploads/default/42/0123456789ABCDEF.jpg").should == true
end
it "identifies S3 uploads" do
SiteSetting.stubs(:enable_s3_uploads).returns(true)
SiteSetting.stubs(:s3_upload_bucket).returns("Bucket")
Upload.has_been_uploaded?("//s3.amazonaws.com/Bucket/1337.png").should == true
Upload.has_been_uploaded?("//bucket.s3.amazonaws.com/1337.png").should == true
end
it "identifies external urls" do
@ -174,30 +174,22 @@ describe Upload do
end
context ".is_on_s3?" do
before do
SiteSetting.stubs(:enable_s3_uploads).returns(true)
SiteSetting.stubs(:s3_upload_bucket).returns("BuCkEt")
end
it "case-insensitively matches the old subdomain format" do
Upload.is_on_s3?("//bucket.s3.amazonaws.com/1337.png").should == true
end
it "case-sensitively matches the new folder format" do
Upload.is_on_s3?("//s3.amazonaws.com/BuCkEt/1337.png").should == true
Upload.is_on_s3?("//s3.amazonaws.com/bucket/1337.png").should == false
end
end
context ".get_from_url" do
it "works when the file has been uploaded" do
Upload.expects(:where).returns([]).once
Upload.get_from_url("/uploads/default/1/10387531.jpg")
end
it "works when using a cdn" do
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
Upload.expects(:where).with(url: "/uploads/default/1/02395732905.jpg").returns([]).once
Upload.get_from_url("http://my.cdn.com/uploads/default/1/02395732905.jpg")
end
it "works only when the file has been uploaded" do
Upload.expects(:has_been_uploaded?).returns(false)
Upload.expects(:where).never
Upload.get_from_url("discourse.org")
Upload.get_from_url("http://domain.com/my/file.txt")
end
end

View File

@ -12,6 +12,11 @@ require 'fakeweb'
FakeWeb.allow_net_connect = false
module Helpers
def self.next_seq
@next_seq = (@next_seq || 0) + 1
end
def log_in(fabricator=nil)
user = Fabricate(fabricator || :user)
log_in_user(user)
@ -49,9 +54,8 @@ Spork.prefork do
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
# let's not run seed_fu every test
SeedFu.quiet = true
SeedFu.quiet = true if SeedFu.respond_to? :quiet
SeedFu.seed
RSpec.configure do |config|
@ -127,6 +131,21 @@ def build(*args)
Fabricate.build(*args)
end
def create_topic(args={})
args[:title] ||= "This is my title #{Helpers.next_seq}"
user = args.delete(:user) || Fabricate(:user)
guardian = Guardian.new(user)
TopicCreator.create(user, guardian, args)
end
def create_post(args={})
args[:title] ||= "This is my title #{Helpers.next_seq}"
args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
args[:topic_id] = args[:topic].id if args[:topic]
user = args.delete(:user) || Fabricate(:user)
PostCreator.create(user, args)
end
module MessageBus::DiagnosticsHelper
def publish(channel, data, opts = nil)
id = super(channel, data, opts)

View File

@ -22,13 +22,22 @@ test("uploading one file", function() {
ok(bootbox.alert.calledWith(I18n.t('post.errors.too_many_uploads')));
});
test("new user", function() {
test("new user cannot upload images", function() {
Discourse.SiteSettings.newuser_max_images = 0;
this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0);
this.stub(bootbox, "alert");
ok(!validUpload([1]));
ok(bootbox.alert.calledWith(I18n.t('post.errors.upload_not_allowed_for_new_user')));
ok(!validUpload([{name: "image.png"}]));
ok(bootbox.alert.calledWith(I18n.t('post.errors.image_upload_not_allowed_for_new_user')));
});
test("new user cannot upload attachments", function() {
Discourse.SiteSettings.newuser_max_attachments = 0;
this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0);
this.stub(bootbox, "alert");
ok(!validUpload([{name: "roman.txt"}]));
ok(bootbox.alert.calledWith(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user')));
});
test("ensures an authorized upload", function() {
@ -141,4 +150,4 @@ test("avatarImg", function() {
blank(Discourse.Utilities.avatarImg({username: 'weird*username', size: 'tiny'}),
"it doesn't render avatars for invalid usernames");
});
});

View File

@ -1,3 +1,3 @@
/*jshint maxlen:10000000 */
Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|favorited|categories","post_menu":"like|edit|flag|delete|share|bookmark|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"enable_local_logins":true,"enable_local_account_create":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif","relative_date_duration":14};
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);

View File

@ -58,7 +58,7 @@ test('destroy by staff', function() {
var user = Discourse.User.create({username: 'staff', staff: true});
var post = buildPost({user: user});
this.stub(Discourse, 'ajax');
this.stub(Discourse, 'ajax').returns(new Em.Deferred());
post.destroy(user);
present(post.get('deleted_at'), "it has a `deleted_at` field.");

View File

@ -10,9 +10,9 @@ Gem::Specification.new do |s|
s.summary = %q{Basic Mustache Support for Rails}
s.description = %q{Adds the Mustache plugin and a corresponding Sprockets engine to the asset pipeline in Rails applications.}
s.add_development_dependency "rails", ["~> 3.1"]
s.add_dependency 'rails', ['~> 3.1']
s.add_development_dependency "rails", ["> 3.1"]
s.add_dependency 'rails', ['> 3.1']
s.files = Dir["lib/**/*"]
s.require_paths = ["lib"]
end
end