mirror of
https://github.com/neocities/neocities.git
synced 2025-04-24 17:22:35 +02:00
fix migrations, acceptance testing for auth, 32 char username limit
This commit is contained in:
parent
fdd4017523
commit
5dfc715148
18 changed files with 247 additions and 125 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,3 +20,4 @@ tests/coverage
|
|||
config.yml
|
||||
.DS_Store
|
||||
domains
|
||||
public/sites_test
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -52,6 +52,10 @@ group :test do
|
|||
gem 'webmock'
|
||||
gem 'mocha', require: nil
|
||||
gem 'rake', require: nil
|
||||
gem 'poltergeist'
|
||||
gem 'phantomjs', require: 'phantomjs/poltergeist'
|
||||
gem 'capybara'
|
||||
gem 'capybara_minitest_spec'
|
||||
|
||||
platform :mri do
|
||||
gem 'simplecov', require: nil
|
||||
|
|
26
Gemfile.lock
26
Gemfile.lock
|
@ -6,10 +6,20 @@ GEM
|
|||
ansi (1.4.3)
|
||||
bcrypt (3.1.7)
|
||||
builder (3.2.2)
|
||||
capybara (2.2.1)
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
capybara_minitest_spec (1.0.1)
|
||||
capybara (>= 2)
|
||||
minitest (>= 2)
|
||||
celluloid (0.15.2)
|
||||
timers (~> 1.1.0)
|
||||
childprocess (0.5.2)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
cliver (0.3.2)
|
||||
coderay (1.1.0)
|
||||
columnize (0.3.6)
|
||||
connection_pool (2.0.0)
|
||||
|
@ -39,6 +49,7 @@ GEM
|
|||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mini_portile (0.5.3)
|
||||
minitest (5.3.1)
|
||||
minitest-reporters (1.0.2)
|
||||
ansi
|
||||
|
@ -48,7 +59,15 @@ GEM
|
|||
mocha (1.0.0)
|
||||
metaclass (~> 0.0.1)
|
||||
multi_json (1.9.2)
|
||||
nokogiri (1.6.1)
|
||||
mini_portile (~> 0.5.0)
|
||||
pg (0.17.1)
|
||||
phantomjs (1.9.7.0)
|
||||
poltergeist (1.5.0)
|
||||
capybara (~> 2.1)
|
||||
cliver (~> 0.3.1)
|
||||
multi_json (~> 1.0)
|
||||
websocket-driver (>= 0.2.0)
|
||||
polyglot (0.3.4)
|
||||
powerbar (1.0.11)
|
||||
ansi (~> 1.4.0)
|
||||
|
@ -129,6 +148,9 @@ GEM
|
|||
addressable (>= 2.2.7)
|
||||
crack (>= 0.3.2)
|
||||
websocket (1.0.7)
|
||||
websocket-driver (0.3.2)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -136,6 +158,8 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
ago
|
||||
bcrypt
|
||||
capybara
|
||||
capybara_minitest_spec
|
||||
fabrication
|
||||
faker
|
||||
hiredis
|
||||
|
@ -148,6 +172,8 @@ DEPENDENCIES
|
|||
minitest-reporters
|
||||
mocha
|
||||
pg
|
||||
phantomjs
|
||||
poltergeist
|
||||
pry
|
||||
pry-debugger
|
||||
puma
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -7,7 +7,7 @@ end
|
|||
desc "Run all tests"
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "spec"
|
||||
t.test_files = FileList['tests/*_test.rb']
|
||||
t.test_files = FileList['tests/*_tests.rb']
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
|
|
19
app.rb
19
app.rb
|
@ -103,12 +103,12 @@ end
|
|||
|
||||
get '/blog' do
|
||||
# expires 500, :public, :must_revalidate
|
||||
return File.read File.join(DIR_ROOT, 'public', 'sites', 'blog', 'index.html')
|
||||
return File.read File.join(Sites::SITE_FILES_ROOT, 'blog', 'index.html')
|
||||
end
|
||||
|
||||
get '/blog/:article' do |article|
|
||||
# expires 500, :public, :must_revalidate
|
||||
path = File.join DIR_ROOT, 'public', 'sites', 'blog', "#{article}.html"
|
||||
path = File.join Sites::SITE_FILES_ROOT, 'blog', "#{article}.html"
|
||||
pass if !File.exist?(path)
|
||||
File.read path
|
||||
end
|
||||
|
@ -137,9 +137,16 @@ end
|
|||
|
||||
post '/create' do
|
||||
dashboard_if_signed_in
|
||||
@site = Site.new username: params[:username], password: params[:password], email: params[:email], new_tags: params[:tags], is_nsfw: params[:is_nsfw], ip: request.ip
|
||||
@site = Site.new(
|
||||
username: params[:username],
|
||||
password: params[:password],
|
||||
email: params[:email],
|
||||
new_tags: params[:tags],
|
||||
is_nsfw: params[:is_nsfw],
|
||||
ip: request.ip
|
||||
)
|
||||
|
||||
recaptcha_is_valid = recaptcha_valid?
|
||||
recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?
|
||||
|
||||
if @site.valid? && recaptcha_is_valid
|
||||
|
||||
|
@ -148,7 +155,7 @@ post '/create' do
|
|||
DB.transaction {
|
||||
@site.save
|
||||
|
||||
FileUtils.mkdir base_path
|
||||
FileUtils.mkdir_p base_path
|
||||
|
||||
File.write File.join(base_path, 'index.html'), slim(:'templates/index', pretty: true, layout: false)
|
||||
File.write File.join(base_path, 'not_found.html'), slim(:'templates/not_found', pretty: true, layout: false)
|
||||
|
@ -661,7 +668,7 @@ def current_site
|
|||
end
|
||||
|
||||
def site_base_path(subname)
|
||||
File.join settings.public_folder, 'sites', subname
|
||||
File.join Site::SITE_FILES_ROOT, subname
|
||||
end
|
||||
|
||||
def site_file_path(filename)
|
||||
|
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :hits
|
||||
DB.drop_column :sites, :hits
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :is_admin
|
||||
DB.drop_column :sites, :is_admin
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :is_banned
|
||||
DB.drop_column :sites, :is_banned
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :ip
|
||||
DB.drop_column :sites, :ip
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :password_reset_token
|
||||
DB.drop_index :sites, :password_reset_token
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :site_changed
|
||||
DB.drop_index :sites, :site_changed
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :changed_count
|
||||
DB.drop_index :sites, :changed_count
|
||||
}
|
||||
end
|
|
@ -1,11 +1,38 @@
|
|||
class Site < Sequel::Model
|
||||
# We might need to include fonts in here..
|
||||
VALID_MIME_TYPES = ['text/plain', 'text/html', 'text/css', 'application/javascript', 'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'application/vnd.ms-fontobject', 'application/x-font-ttf', 'application/octet-stream', 'text/csv', 'text/tsv', 'text/cache-manifest', 'image/x-icon', 'application/pdf', 'application/pgp-keys', 'text/xml', 'application/xml', 'audio/midi']
|
||||
VALID_EXTENSIONS = %w{ html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json geojson csv tsv mf ico pdf asc key pgp xml mid midi }
|
||||
#USERNAME_SHITLIST = %w{ payment secure login signin www ww web } # I thought they were funny personally, but everybody is freaking out so..
|
||||
VALID_MIME_TYPES = %w{
|
||||
text/plain
|
||||
text/html
|
||||
text/css
|
||||
application/javascript
|
||||
image/png
|
||||
image/jpeg
|
||||
image/gif
|
||||
image/svg+xml
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/octet-stream
|
||||
text/csv
|
||||
text/tsv
|
||||
text/cache-manifest
|
||||
image/x-icon
|
||||
application/pdf
|
||||
application/pgp-keys
|
||||
text/xml
|
||||
application/xml
|
||||
audio/midi
|
||||
}
|
||||
VALID_EXTENSIONS = %w{
|
||||
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json
|
||||
geojson csv tsv mf ico pdf asc key pgp xml mid midi
|
||||
}
|
||||
MAX_SPACE = (5242880*2) # 10MB
|
||||
MINIMUM_PASSWORD_LENGTH = 5
|
||||
BAD_USERNAME_REGEX = /[^\w-]/i
|
||||
VALID_HOSTNAME = /^[a-z0-9][a-z0-9-]+?[a-z0-9]$/i # http://tools.ietf.org/html/rfc1123
|
||||
|
||||
SITE_FILES_ROOT = File.join(DIR_ROOT, 'public', (ENV['RACK_ENV'] == 'test' ? 'sites_test' : 'sites'))
|
||||
|
||||
many_to_one :server
|
||||
many_to_many :tags
|
||||
|
||||
|
@ -77,13 +104,15 @@ class Site < Sequel::Model
|
|||
errors.add :over_capacity, 'We are currently at capacity, and cannot create your home page. We will fix this shortly. Please come back later and try again, our apologies.'
|
||||
end
|
||||
|
||||
if values[:username].nil? || values[:username].empty? || values[:username].match(BAD_USERNAME_REGEX)
|
||||
errors.add :username, 'A valid username is required.'
|
||||
if !values[:username].match(VALID_HOSTNAME)
|
||||
errors.add :username, 'A valid user/site name is required.'
|
||||
end
|
||||
|
||||
if values[:username].length > 32
|
||||
errors.add :username, 'User/site name cannot exceed 32 characters.'
|
||||
end
|
||||
|
||||
# Check for existing user
|
||||
|
||||
|
||||
user = self.class.select(:id, :username).filter(username: values[:username]).first
|
||||
|
||||
if user
|
||||
|
@ -109,7 +138,7 @@ class Site < Sequel::Model
|
|||
end
|
||||
|
||||
def file_path
|
||||
File.join DIR_ROOT, 'public', 'sites', username
|
||||
File.join SITE_FILES_ROOT, username
|
||||
end
|
||||
|
||||
def file_list
|
||||
|
|
144
tests/acceptance_tests.rb
Normal file
144
tests/acceptance_tests.rb
Normal file
|
@ -0,0 +1,144 @@
|
|||
require_relative './environment'
|
||||
|
||||
Capybara.app = Sinatra::Application
|
||||
|
||||
def teardown
|
||||
Capybara.reset_sessions!
|
||||
Capybara.use_default_driver
|
||||
end
|
||||
|
||||
describe 'index' do
|
||||
include Capybara::DSL
|
||||
it 'goes to signup' do
|
||||
visit '/'
|
||||
click_button 'Create My Website'
|
||||
page.must_have_content('Create a New Home Page')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signup' do
|
||||
include Capybara::DSL
|
||||
|
||||
def fill_in_valid
|
||||
@site = Fabricate.attributes_for(:site)
|
||||
fill_in 'username', with: @site[:username]
|
||||
fill_in 'password', with: @site[:password]
|
||||
end
|
||||
|
||||
def visit_signup
|
||||
visit '/'
|
||||
click_button 'Create My Website'
|
||||
end
|
||||
|
||||
before do
|
||||
Capybara.reset_sessions!
|
||||
visit_signup
|
||||
end
|
||||
|
||||
it 'succeeds with valid data' do
|
||||
fill_in_valid
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Your Website'
|
||||
assert_equal(
|
||||
true,
|
||||
File.exist?(File.join(Site::SITE_FILES_ROOT, @site[:username], 'index.html'))
|
||||
)
|
||||
end
|
||||
|
||||
it 'fails to create for existing site' do
|
||||
fill_in_valid
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Your Website'
|
||||
Capybara.reset_sessions!
|
||||
visit_signup
|
||||
fill_in 'username', with: @site[:username]
|
||||
fill_in 'password', with: @site[:password]
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'already taken'
|
||||
end
|
||||
|
||||
it 'fails with missing password' do
|
||||
fill_in_valid
|
||||
fill_in 'password', with: ''
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Password must be at least 5 characters'
|
||||
end
|
||||
|
||||
it 'fails with short password' do
|
||||
fill_in_valid
|
||||
fill_in 'password', with: 'derp'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Password must be at least 5 characters'
|
||||
end
|
||||
|
||||
it 'fails with invalid hostname for username' do
|
||||
fill_in_valid
|
||||
fill_in 'username', with: '|\|0p|E'
|
||||
click_button 'Create Home Page'
|
||||
page.current_path.must_equal '/create'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
fill_in 'username', with: 'nope-'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
fill_in 'username', with: '-nope'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
end
|
||||
|
||||
it 'fails with username greater than 32 characters' do
|
||||
fill_in_valid
|
||||
fill_in 'username', with: SecureRandom.hex+'1'
|
||||
click_button 'Create Home Page'
|
||||
page.current_path.must_equal '/create'
|
||||
page.must_have_content 'cannot exceed 32 characters'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signin' do
|
||||
include Capybara::DSL
|
||||
|
||||
def fill_in_valid
|
||||
site = Fabricate.attributes_for :site
|
||||
fill_in 'username', with: site[:username]
|
||||
fill_in 'password', with: site[:password]
|
||||
end
|
||||
|
||||
before do
|
||||
Capybara.reset_sessions!
|
||||
end
|
||||
|
||||
it 'fails for invalid login' do
|
||||
visit '/'
|
||||
click_link 'Sign In'
|
||||
page.must_have_content 'Welcome back'
|
||||
fill_in_valid
|
||||
click_button 'Sign in'
|
||||
page.must_have_content 'Invalid login'
|
||||
end
|
||||
|
||||
it 'fails for missing login' do
|
||||
visit '/'
|
||||
click_link 'Sign In'
|
||||
auth = {username: SecureRandom.hex, password: Faker::Internet.password}
|
||||
fill_in 'username', with: auth[:username]
|
||||
fill_in 'password', with: auth[:password]
|
||||
click_button 'Sign in'
|
||||
page.must_have_content 'Invalid login'
|
||||
end
|
||||
|
||||
it 'logs in with proper credentials' do
|
||||
visit '/'
|
||||
click_button 'Create My Website'
|
||||
site = Fabricate.attributes_for(:site)
|
||||
fill_in 'username', with: site[:username]
|
||||
fill_in 'password', with: site[:password]
|
||||
click_button 'Create Home Page'
|
||||
Capybara.reset_sessions!
|
||||
visit '/'
|
||||
click_link 'Sign In'
|
||||
fill_in 'username', with: site[:username]
|
||||
fill_in 'password', with: site[:password]
|
||||
click_button 'Sign in'
|
||||
page.must_have_content 'Your Website'
|
||||
end
|
||||
end
|
|
@ -1,93 +0,0 @@
|
|||
require_relative './environment'
|
||||
|
||||
include Rack::Test::Methods
|
||||
|
||||
def app; App end
|
||||
def status; last_response.status end
|
||||
def headers; last_response.headers end
|
||||
def body; last_response.body end
|
||||
|
||||
describe 'index' do
|
||||
it 'loads' do
|
||||
get '/'
|
||||
status.must_equal 200
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signin' do
|
||||
it 'fails for missing login' do
|
||||
post '/signin', username: 'derpie', password: 'lol'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'fails for bad password' do
|
||||
@site = Fabricate :site
|
||||
post '/signin', username: @site.username, password: 'derp'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'fails for no input' do
|
||||
post '/signin'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'succeeds for valid input' do
|
||||
password = '1tw0rkz'
|
||||
@account = Fabricate :account, password: password
|
||||
post '/accounts/signin', username: @account.email, password: password
|
||||
headers['Location'].must_equal 'http://example.org/dashboard'
|
||||
mock_dashboard_calls @account.email
|
||||
get '/dashboard'
|
||||
body.must_match /Dashboard/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'account creation' do
|
||||
it 'fails for no input' do
|
||||
post '/accounts/create'
|
||||
status.must_equal 200
|
||||
body.must_match /There were some errors.+Valid email address is required.+Password must be/
|
||||
end
|
||||
|
||||
it 'fails with invalid email' do
|
||||
post '/accounts/create', email: 'derplol'
|
||||
status.must_equal 200
|
||||
body.must_match /errors.+valid email/i
|
||||
end
|
||||
|
||||
it 'fails with invalid password' do
|
||||
post '/accounts/create', 'email@example.com', password: 'sdd'
|
||||
status.must_equal 200
|
||||
body.must_match /errors.+Password must be at least #{Account::MINIMUM_PASSWORD_LENGTH} characters/i
|
||||
end
|
||||
|
||||
it 'succeeds with valid info' do
|
||||
account_attributes = Fabricate.attributes_for :account
|
||||
|
||||
mock_dashboard_calls account_attributes[:email]
|
||||
|
||||
post '/accounts/create', account_attributes
|
||||
status.must_equal 302
|
||||
headers['Location'].must_equal 'http://example.org/dashboard'
|
||||
|
||||
get '/dashboard'
|
||||
body.must_match /Dashboard/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'temporary account login' do
|
||||
end
|
||||
|
||||
def fail_signin
|
||||
headers['Location'].must_equal 'http://example.org/'
|
||||
get '/'
|
||||
body.must_match /invalid signin/i
|
||||
end
|
||||
|
||||
def api_url
|
||||
uri = Addressable::URI.parse $config['bitcoind_rpchost'] ? $config['bitcoind_rpchost'] : 'http://localhost'
|
||||
uri.port = 8332 if uri.port.nil?
|
||||
uri.user = $config['bitcoind_rpcuser'] if uri.user.nil?
|
||||
uri.password = $config['bitcoind_rpcpassword'] if uri.password.nil?
|
||||
"#{uri.to_s}/"
|
||||
end
|
|
@ -20,7 +20,7 @@ Bundler.require :test
|
|||
require 'minitest/autorun'
|
||||
require 'sidekiq/testing/inline'
|
||||
|
||||
Account.bcrypt_cost = BCrypt::Engine::MIN_COST
|
||||
Site.bcrypt_cost = BCrypt::Engine::MIN_COST
|
||||
|
||||
MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
|
||||
|
||||
|
@ -29,8 +29,11 @@ Sequel.extension :migration
|
|||
|
||||
Sequel::Migrator.apply DB, './migrations', 0
|
||||
Sequel::Migrator.apply DB, './migrations'
|
||||
Server.create ip: '127.0.0.1', slots_available: 999999
|
||||
|
||||
Fabrication.configure do |config|
|
||||
config.fabricator_path = 'tests/fabricators'
|
||||
config.path_prefix = DIR_ROOT
|
||||
end
|
||||
|
||||
I18n.enforce_available_locales = true
|
|
@ -1,4 +1,4 @@
|
|||
Fabricator(:site) do
|
||||
username { Faker::Internet.email }
|
||||
username { SecureRandom.hex }
|
||||
password { 'abcde' }
|
||||
end
|
|
@ -19,35 +19,39 @@ javascript:
|
|||
h2.txt-Center Create a New Home Page
|
||||
|
||||
.col.col-50 style="margin:0 auto; float:none"
|
||||
p.tiny First, enter a username. This will also be used as your site path.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It cannot contain spaces, and can only use the following characters: a-z A-Z 0-9 _ -
|
||||
hr
|
||||
p.tiny First, enter a username. This will also be used as your site name.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It can only contain letters, numbers, underscores and hyphens, and can only be 32 characters long.
|
||||
br
|
||||
h5 Username
|
||||
p.tiny <input class="input-Area" name="username" type="text" placeholder="yourusername" value="#{@site.username}" autocapitalize="off" autocorrect="off">.neocities.org
|
||||
|
||||
hr
|
||||
p.tiny Next, enter a password. This will be used to allow you to login. Minimum 5 characters. If you don't make it a good password, Dade Murphy from the movie Hackers will come in and steal your "garbage files".
|
||||
|
||||
hr
|
||||
|
||||
h5 Password
|
||||
input class="input-Area" name="password" type="password"
|
||||
|
||||
br
|
||||
hr
|
||||
p.tiny Now you can enter an e-mail address. Your e-mail address is private and we will not show it to anyone for any reason. You don't have to provide one, but <b>we will not be able to reset your password without it, so don't lose your username and password if you leave this blank!</b>
|
||||
|
||||
hr
|
||||
|
||||
h5 Email
|
||||
input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}"
|
||||
|
||||
br
|
||||
hr
|
||||
|
||||
p.tiny You can optionally enter some tags! Tags will allow others to find your site based on your interests, or your site's theme. <b>Separate multiple tags with commas</b>. Don't think too hard about this, you can change them later. You can have a maximum of ten tags, and there is a two word per tag maximum (extra words in a tag will be removed).
|
||||
|
||||
hr
|
||||
h5 Tags
|
||||
p: input class="input-Area" name="tags" type="text" style="width: 400px; max-width:100%" placeholder="pokemon, video games, bulbasaur" value="#{params[:tags]}" autocapitalize="off" autocorrect="off"
|
||||
|
||||
hr
|
||||
input name="is_nsfw" type="hidden" value="false"
|
||||
p: strong If your page will contain objectionable (adult) content, check this box: <input name="is_nsfw" type="checkbox" value="true" style="margin-top:0">
|
||||
p If your page will contain objectionable (adult) content, check this box: <input name="is_nsfw" type="checkbox" value="true" style="margin-top:0">
|
||||
|
||||
hr
|
||||
p.tiny <b>Last thing!</b> Enter these two words correctly (with spaces) so we know you're not a robot (don't worry robots, we still love you).
|
||||
div style="background:#fff; width:100%; overflow:auto"
|
||||
== recaptcha_tag :challenge, ssl: true
|
||||
|
|
Loading…
Add table
Reference in a new issue