locomotivecms_steam 1.5.0.beta3 → 1.5.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -4
  3. data/Gemfile.lock +3 -6
  4. data/README.md +33 -0
  5. data/Rakefile +8 -3
  6. data/docker-compose.yml +15 -0
  7. data/lib/locomotive/steam/entities/content_type.rb +7 -2
  8. data/lib/locomotive/steam/liquid/drops/section_content_proxy.rb +4 -0
  9. data/lib/locomotive/steam/liquid/filters/translate.rb +2 -0
  10. data/lib/locomotive/steam/liquid/tags/link_to.rb +8 -1
  11. data/lib/locomotive/steam/liquid/tags/model_form.rb +7 -1
  12. data/lib/locomotive/steam/middlewares/auth.rb +10 -17
  13. data/lib/locomotive/steam/middlewares/concerns/auth_helpers.rb +30 -0
  14. data/lib/locomotive/steam/middlewares/concerns/helpers.rb +13 -3
  15. data/lib/locomotive/steam/middlewares/concerns/liquid_context.rb +1 -1
  16. data/lib/locomotive/steam/middlewares/concerns/recaptcha.rb +30 -0
  17. data/lib/locomotive/steam/middlewares/default_env.rb +5 -3
  18. data/lib/locomotive/steam/middlewares/entry_submission.rb +4 -1
  19. data/lib/locomotive/steam/middlewares/impersonated_entry.rb +73 -0
  20. data/lib/locomotive/steam/middlewares/locale.rb +14 -9
  21. data/lib/locomotive/steam/models/associations/belongs_to.rb +6 -0
  22. data/lib/locomotive/steam/server.rb +1 -0
  23. data/lib/locomotive/steam/services.rb +9 -1
  24. data/lib/locomotive/steam/services/action_service.rb +3 -3
  25. data/lib/locomotive/steam/services/auth_service.rb +1 -1
  26. data/lib/locomotive/steam/services/content_entry_service.rb +11 -1
  27. data/lib/locomotive/steam/services/cookie_service.rb +25 -0
  28. data/lib/locomotive/steam/services/page_finder_service.rb +12 -0
  29. data/lib/locomotive/steam/services/recaptcha_service.rb +33 -0
  30. data/lib/locomotive/steam/services/translator_service.rb +12 -13
  31. data/lib/locomotive/steam/version.rb +1 -1
  32. data/spec/integration/liquid/filters/translate_spec.rb +7 -0
  33. data/spec/integration/repositories/content_entry_repository_spec.rb +4 -0
  34. data/spec/integration/server/auth_spec.rb +8 -1
  35. data/spec/integration/services/content_entry_service_spec.rb +11 -0
  36. data/spec/support/helpers.rb +1 -1
  37. data/spec/unit/liquid/drops/section_content_proxy_spec.rb +8 -0
  38. data/spec/unit/liquid/tags/action_spec.rb +2 -1
  39. data/spec/unit/liquid/tags/global_section_spec.rb +2 -1
  40. data/spec/unit/liquid/tags/link_to_spec.rb +7 -0
  41. data/spec/unit/liquid/tags/model_form_spec.rb +7 -0
  42. data/spec/unit/liquid/tags/section_spec.rb +2 -1
  43. data/spec/unit/liquid/tags/snippet_spec.rb +2 -1
  44. data/spec/unit/middlewares/auth_spec.rb +1 -0
  45. data/spec/unit/middlewares/entry_submission_spec.rb +64 -20
  46. data/spec/unit/middlewares/helpers_spec.rb +5 -1
  47. data/spec/unit/middlewares/impersonated_entry_spec.rb +80 -0
  48. data/spec/unit/middlewares/locale_spec.rb +57 -36
  49. data/spec/unit/middlewares/section_spec.rb +6 -5
  50. data/spec/unit/middlewares/site_spec.rb +13 -0
  51. data/spec/unit/services/action_service_spec.rb +17 -11
  52. data/spec/unit/services/auth_service_spec.rb +7 -1
  53. data/spec/unit/services/content_entry_service_spec.rb +25 -4
  54. data/spec/unit/services/cookie_service_spec.rb +38 -0
  55. data/spec/unit/services/page_finder_service_spec.rb +69 -0
  56. data/spec/unit/services/recaptcha_service_spec.rb +71 -0
  57. data/spec/unit/services/translator_service_spec.rb +11 -1
  58. metadata +16 -2
@@ -10,7 +10,7 @@ module Locomotive::Steam
10
10
  # /index => locale = :en (default one)
11
11
  #
12
12
  # /en/index?locale=fr => locale = :fr
13
- # /index => redirection to /en if the locale in session is :en
13
+ # /index => redirection to /en if the locale in cookie is :en
14
14
  #
15
15
  class Locale < ThreadSafe
16
16
 
@@ -19,7 +19,9 @@ module Locomotive::Steam
19
19
  def _call
20
20
  env['steam.path'] = request.path_info
21
21
 
22
- env['steam.locale'] = session[session_key_name] = services.locale = extract_locale
22
+ env['steam.locale'] = services.locale = extract_locale
23
+
24
+ set_locale_cookie
23
25
 
24
26
  log "Locale used: #{locale.upcase}"
25
27
 
@@ -34,7 +36,7 @@ module Locomotive::Steam
34
36
  # Regarding the index page (basically, "/"), we've to see if we could
35
37
  # guess the locale from the headers the browser sends to us.
36
38
  locale = if is_index_page?
37
- locale_from_params || locale_from_session || locale_from_header
39
+ locale_from_params || locale_from_cookie || locale_from_header
38
40
  else
39
41
  locale_from_path || locale_from_params
40
42
  end
@@ -75,20 +77,23 @@ module Locomotive::Steam
75
77
  end
76
78
  end
77
79
 
78
- def locale_from_session
79
- if locale = session[session_key_name]
80
- env['steam.locale_in_session'] = true
80
+ def locale_from_cookie
81
+ if locale = services.cookie.get(cookie_key_name)
81
82
 
82
- log 'Locale extracted from the session'
83
+ log 'Locale extracted from the cookie'
83
84
 
84
85
  locale.to_sym
85
86
  end
86
87
  end
87
88
 
88
- # The preview urls for all the sites share the same domain, so session[:locale]
89
+ def set_locale_cookie
90
+ services.cookie.set(cookie_key_name, {'value': locale, 'path': '/', 'max_age': 1.year})
91
+ end
92
+
93
+ # The preview urls for all the sites share the same domain, so cookie[:locale]
89
94
  # would be the same for all the preview urls and this is not good.
90
95
  # This is why we need to use a different key.
91
- def session_key_name
96
+ def cookie_key_name
92
97
  live_editing? ? "steam-locale-#{site.handle}" : 'steam-locale'
93
98
  end
94
99
 
@@ -21,6 +21,12 @@ module Locomotive::Steam
21
21
  :"#{__name__}_id"
22
22
  end
23
23
 
24
+ def __attach__(entity)
25
+ # setting a default nil value for the target key
26
+ entity[__target_key__] ||= nil
27
+ super
28
+ end
29
+
24
30
  end
25
31
 
26
32
  end
@@ -63,6 +63,7 @@ module Locomotive::Steam
63
63
  Middlewares::Locale,
64
64
  Middlewares::LocaleRedirection,
65
65
  Middlewares::Redirection,
66
+ Middlewares::ImpersonatedEntry,
66
67
  Middlewares::Auth,
67
68
  Middlewares::PrivateAccess,
68
69
  Middlewares::Path,
@@ -75,7 +75,7 @@ module Locomotive
75
75
  end
76
76
 
77
77
  register :action do
78
- Steam::ActionService.new(current_site, email, content_entry: content_entry, api: external_api, redirection: page_redirection)
78
+ Steam::ActionService.new(current_site, email, content_entry: content_entry, api: external_api, redirection: page_redirection, cookie: cookie)
79
79
  end
80
80
 
81
81
  register :content_entry do
@@ -142,10 +142,18 @@ module Locomotive
142
142
  Steam::AuthService.new(current_site, content_entry, email)
143
143
  end
144
144
 
145
+ register :recaptcha do
146
+ Steam::RecaptchaService.new(current_site, request)
147
+ end
148
+
145
149
  register :cache do
146
150
  Steam::NoCacheService.new
147
151
  end
148
152
 
153
+ register :cookie do
154
+ Steam::CookieService.new(request)
155
+ end
156
+
149
157
  register :configuration do
150
158
  Locomotive::Steam.configuration
151
159
  end
@@ -11,7 +11,7 @@ module Locomotive
11
11
 
12
12
  class ActionService
13
13
 
14
- SERVICES = %w(content_entry api redirection)
14
+ SERVICES = %w(content_entry api redirection cookie)
15
15
 
16
16
  BUILT_IN_FUNCTIONS = %w(
17
17
  getProp
@@ -84,11 +84,11 @@ module Locomotive
84
84
  end
85
85
 
86
86
  def get_cookies_prop_lambda(liquid_context)
87
- -> (name) { liquid_context.registers[:cookies][name.to_s].as_json }
87
+ -> (name) { cookie_service.get(name.to_s) }
88
88
  end
89
89
 
90
90
  def set_cookies_prop_lambda(liquid_context)
91
- -> (name, value) { liquid_context.registers[:cookies][name.to_s] = value.to_s }
91
+ -> (name, values) { cookie_service.set(name.to_s, values) }
92
92
  end
93
93
 
94
94
  def all_entries_lambda(liquid_context)
@@ -30,7 +30,7 @@ module Locomotive
30
30
  def sign_in(options, request)
31
31
  entry = entries.all(options.type, options.id_field => options.id).first
32
32
 
33
- if entry && entry.send(options.password_field)
33
+ if entry && (entry.send(options.password_field).present?)
34
34
  hashed_password = entry[:"#{options.password_field}_hash"]
35
35
  password = ::BCrypt::Engine.hash_secret(options.password, entry.send(options.password_field).try(:salt))
36
36
  same_password = secure_compare(password, hashed_password)
@@ -24,6 +24,16 @@ module Locomotive
24
24
  end
25
25
  end
26
26
 
27
+ def build(type_slug, attributes)
28
+ with_repository(type_slug) do |_repository|
29
+ _attributes = prepare_attributes(_repository.content_type, attributes)
30
+
31
+ entry = _repository.build(_attributes)
32
+
33
+ i18n_decorate { entry }
34
+ end
35
+ end
36
+
27
37
  # Warning: do not work with localized and file fields
28
38
  def create(type_slug, attributes, as_json = false)
29
39
  with_repository(type_slug) do |_repository|
@@ -144,7 +154,7 @@ module Locomotive
144
154
  # check if the entry has unique values for its
145
155
  # fields marked as unique
146
156
  content_type_repository.look_for_unique_fields(entry.content_type).each do |name, _|
147
- if _repository.exists?(name => entry.send(name))
157
+ if _repository.exists?(name => entry.send(name), :"_id.ne" => entry._id)
148
158
  entry.errors.add(name, :taken)
149
159
  end
150
160
  end
@@ -0,0 +1,25 @@
1
+ module Locomotive
2
+ module Steam
3
+
4
+ class CookieService
5
+
6
+ def initialize(request)
7
+ @request = request
8
+ @cookies = request.env['steam.cookies']
9
+ end
10
+
11
+ def set(key, vals)
12
+ @cookies[key] = vals
13
+ end
14
+
15
+ def get(key)
16
+ if @cookies.include?(key)
17
+ @cookies[key]['value']
18
+ else
19
+ @request.cookies[key]
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -16,6 +16,11 @@ module Locomotive
16
16
  def match(path)
17
17
  decorate do
18
18
  repository.matching_fullpath(path_combinations(path))
19
+ end.sort do |page_1, page_2|
20
+ # normal pages have priority over the templatized ones if they're not in the same "folder"
21
+ same_folder?(page_1, page_2) ?
22
+ page_1.position <=> page_2.position :
23
+ (page_2.fullpath.include?(WILDCARD) ? 0 : 1) <=> (page_1 .fullpath.include?(WILDCARD) ? 0 : 1)
19
24
  end
20
25
  end
21
26
 
@@ -58,6 +63,13 @@ module Locomotive
58
63
  _path_combinations(path.split('/'))
59
64
  end
60
65
 
66
+ def same_folder?(page_1, page_2)
67
+ (path_1 = page_1.fullpath.split('/')).pop
68
+ (path_2 = page_2.fullpath.split('/')).pop
69
+
70
+ path_1.join('/') == path_2.join('/')
71
+ end
72
+
61
73
  def _path_combinations(segments, can_include_template = true)
62
74
  return nil if segments.empty?
63
75
  segment = segments.shift
@@ -0,0 +1,33 @@
1
+ require 'httparty'
2
+
3
+ module Locomotive
4
+ module Steam
5
+
6
+ # This service supports Google Recaptcha or any API compatible with Google
7
+ class RecaptchaService
8
+
9
+ GOOGLE_API_URL = 'https://www.google.com/recaptcha/api/siteverify'.freeze
10
+
11
+ def initialize(site, request)
12
+ @api = site.metafields.dig(:google, :recaptcha_api_url) || GOOGLE_API_URL
13
+ @secret = site.metafields.dig(:google, :recaptcha_secret)
14
+ @ip = request.ip
15
+ end
16
+
17
+ def verify(response_code)
18
+ # save a HTTP query if there is no code
19
+ return false if response_code.blank?
20
+
21
+ _response = HTTParty.get(@api, { query: {
22
+ secret: @secret,
23
+ response: response_code,
24
+ remoteip: @ip
25
+ }})
26
+
27
+ _response.parsed_response['success']
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+ end
@@ -15,23 +15,22 @@ module Locomotive
15
15
  def translate(input, options = {})
16
16
  locale = options['locale'] || self.current_locale
17
17
  scope = options.delete('scope')
18
+ key = scope.present? ? "#{scope.gsub('.', '_')}_#{input}" : input
18
19
 
19
- if scope.blank?
20
- input = "#{input}_#{pluralize_prefix(options['count'])}" if options['count']
20
+ key = "#{key}_#{pluralize_prefix(options['count'])}" if options['count']
21
21
 
22
- values = find_values_by_key(input)
22
+ values = find_values_by_key(key)
23
23
 
24
- # FIXME: important to check if the returned value is nil (instead of nil + false)
25
- # false being reserved for an existing key but without provided translation)
26
- if (translation = values[locale.to_s]).present?
27
- _translate(translation, options)
28
- else
29
- Locomotive::Common::Logger.warn "Missing translation '#{input}' for the '#{locale}' locale".yellow
30
- ActiveSupport::Notifications.instrument('steam.missing_translation', input: input, locale: locale)
31
- input
32
- end
24
+ # FIXME: important to check if the returned value is nil (instead of nil + false)
25
+ # false being reserved for an existing key but without provided translation)
26
+ if (translation = values[locale.to_s]).present?
27
+ _translate(translation, options)
28
+ elsif output = I18n.t(input, scope: scope&.split('.'), locale: locale, default: nil)
29
+ output
33
30
  else
34
- I18n.t(input, scope: scope.split('.'), locale: locale)
31
+ Locomotive::Common::Logger.warn "Missing translation '#{input}' for the '#{locale}' locale".yellow
32
+ ActiveSupport::Notifications.instrument('steam.missing_translation', input: input, locale: locale)
33
+ input
35
34
  end
36
35
  end
37
36
 
@@ -3,6 +3,6 @@
3
3
  # 1.0.0.alpha < 1.0.0.alpha1 < 1.0.0.beta < 1.0.0.beta2 < 1.0.0.beta11 < 1.0.0.rc1 < 1.0.0
4
4
  module Locomotive
5
5
  module Steam
6
- VERSION = '1.5.0.beta3'
6
+ VERSION = '1.5.0.rc0'
7
7
  end
8
8
  end
@@ -48,6 +48,13 @@ describe Locomotive::Steam::Liquid::Filters::Translate do
48
48
 
49
49
  end
50
50
 
51
+ describe 'shortcut alias' do
52
+
53
+ let(:source) { "{{ 'welcome_message' | t: 'fr', 'locomotive.default' }}" }
54
+ it { expect(translator).to receive(:translate).with('welcome_message', 'locale' => 'fr', 'scope' => 'locomotive.default'); subject }
55
+
56
+ end
57
+
51
58
  end
52
59
 
53
60
  describe 'pluralization' do
@@ -79,6 +79,10 @@ describe Locomotive::Steam::ContentEntryRepository do
79
79
  before { target_repository.scope.locale = :fr }
80
80
  subject { target_repository.all(band: nil) }
81
81
  it { expect(subject.size).to eq 2 }
82
+
83
+ describe 'set a nil value to the attribute storing the id of the related element' do
84
+ it { expect(subject.map(&:band_id)).to eq([nil, nil]) }
85
+ end
82
86
  end
83
87
  end
84
88
 
@@ -152,9 +152,16 @@ describe 'Authentication' do
152
152
  'authenticated_entry_id' => 'john'
153
153
  } }
154
154
 
155
+ it 'redirect to the requested page' do
156
+ post '/account/sign-in', params, { 'rack.session' => rack_session }
157
+ expect(last_response.status).to eq 301
158
+ expect(last_response.location).to eq "/account/sign-in"
159
+ end
160
+
155
161
  it 'displays the profile page as described in the params' do
156
162
  post '/account/sign-in', params, { 'rack.session' => rack_session }
157
- expect(last_response.body).to include "You've been signed out"
163
+ follow_redirect!
164
+ expect(last_response.body).to include "Sign in"
158
165
  expect(last_response.body).not_to include "You're already authenticated!"
159
166
  end
160
167
 
@@ -76,6 +76,17 @@ describe Locomotive::Steam::ContentEntryService do
76
76
 
77
77
  after(:all) { Locomotive::Steam::Adapters::Filesystem::SimpleCacheStore.new.clear }
78
78
 
79
+ describe '#build' do
80
+
81
+ let(:attributes) { { name: 'John', email: 'john@doe.net', message: 'Hello world!' } }
82
+
83
+ subject { service.build('messages', attributes) }
84
+
85
+ it { expect(subject['name']).to eq 'John' }
86
+ it { expect(subject['errors'].blank?).to eq true }
87
+
88
+ end
89
+
79
90
  describe '#create' do
80
91
 
81
92
  let(:attributes) { { name: 'John', email: 'john@doe.net', message: 'Hello world!' } }
@@ -63,7 +63,7 @@ module Spec
63
63
 
64
64
  ActiveSupport::Notifications.unsubscribe(subscription)
65
65
 
66
- return payload
66
+ payload
67
67
  end
68
68
 
69
69
  end
@@ -46,6 +46,14 @@ describe Locomotive::Steam::Liquid::Drops::SectionContentProxy do
46
46
 
47
47
  end
48
48
 
49
+ context 'it outputs the target="_blank" A attribute if new window is true' do
50
+
51
+ subject { drop.before_method(:link).new_window_attribute }
52
+
53
+ it { is_expected.to eq('target="_blank"') }
54
+
55
+ end
56
+
49
57
  end
50
58
 
51
59
  describe 'image picker type setting' do
@@ -6,7 +6,8 @@ describe Locomotive::Steam::Liquid::Tags::Action do
6
6
  let(:source) { '{% action "random Javascript action" %}var foo = 42; setProp("foo", foo);{% endaction %}' }
7
7
  let(:assigns) { {} }
8
8
  let(:registers) { { services: services } }
9
- let(:services) { Locomotive::Steam::Services.build_instance }
9
+ let(:request) { instance_double('Request', env: {}) }
10
+ let(:services) { Locomotive::Steam::Services.build_instance(request) }
10
11
  let(:context) { ::Liquid::Context.new(assigns, {}, registers) }
11
12
 
12
13
  before { allow(services).to receive(:current_site).and_return(site) }
@@ -2,7 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe Locomotive::Steam::Liquid::Tags::GlobalSection do
4
4
 
5
- let(:services) { Locomotive::Steam::Services.build_instance(nil) }
5
+ let(:request) { instance_double('Request', env: {}) }
6
+ let(:services) { Locomotive::Steam::Services.build_instance(request) }
6
7
  let(:finder) { services.section_finder }
7
8
  let(:source) { 'Locomotive {% global_section header %}' }
8
9
  let(:live_editing) { true }
@@ -75,6 +75,13 @@ describe Locomotive::Steam::Liquid::Tags::PathTo do
75
75
 
76
76
  end
77
77
 
78
+ context 'with a custom CSS class' do
79
+
80
+ let(:source) { "{% link_to index, locale: 'fr', class: 'button' %}" }
81
+ it { is_expected.to eq '<a href="/fr" class="button">Accueil</a>' }
82
+
83
+ end
84
+
78
85
  end
79
86
 
80
87
  describe 'used as a block' do
@@ -34,6 +34,13 @@ describe Locomotive::Steam::Liquid::Tags::ModelForm do
34
34
 
35
35
  end
36
36
 
37
+ describe 'with recaptcha' do
38
+
39
+ let(:source) { "{% model_form 'newsletter_addresses', recaptcha: true %}Newsletter Form{% endmodel_form %}" }
40
+ it { is_expected.to include %(<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" />) }
41
+
42
+ end
43
+
37
44
  describe 'json enabled' do
38
45
 
39
46
  let(:source) { "{% model_form 'newsletter_addresses', json: true %}Newsletter Form{% endmodel_form %}" }