locomotivecms_steam 1.2.1 → 1.3.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/Gemfile +3 -2
  4. data/Gemfile.lock +57 -50
  5. data/LICENSE +1 -1
  6. data/README.md +1 -1
  7. data/config/locales/nl.yml +2 -2
  8. data/lib/locomotive/steam/adapters/filesystem/yaml_loaders/content_entry.rb +8 -0
  9. data/lib/locomotive/steam/adapters/memory/condition.rb +1 -1
  10. data/lib/locomotive/steam/configuration.rb +1 -1
  11. data/lib/locomotive/steam/decorators/template_decorator.rb +1 -1
  12. data/lib/locomotive/steam/entities/content_entry.rb +17 -0
  13. data/lib/locomotive/steam/entities/content_type.rb +1 -0
  14. data/lib/locomotive/steam/entities/content_type_field.rb +1 -0
  15. data/lib/locomotive/steam/entities/editable_element.rb +4 -0
  16. data/lib/locomotive/steam/errors.rb +35 -5
  17. data/lib/locomotive/steam/initializers/sprockets.rb +6 -80
  18. data/lib/locomotive/steam/liquid/drops/page.rb +9 -2
  19. data/lib/locomotive/steam/liquid/filters/date.rb +109 -0
  20. data/lib/locomotive/steam/liquid/tags/action.rb +6 -1
  21. data/lib/locomotive/steam/liquid/tags/authorize.rb +65 -0
  22. data/lib/locomotive/steam/liquid/tags/snippet.rb +10 -2
  23. data/lib/locomotive/steam/liquid/template.rb +3 -1
  24. data/lib/locomotive/steam/middlewares/auth.rb +187 -0
  25. data/lib/locomotive/steam/middlewares/redirection.rb +24 -0
  26. data/lib/locomotive/steam/middlewares/renderer.rb +20 -3
  27. data/lib/locomotive/steam/middlewares/sitemap.rb +3 -1
  28. data/lib/locomotive/steam/middlewares/templatized_page.rb +1 -6
  29. data/lib/locomotive/steam/middlewares/thread_safe.rb +19 -0
  30. data/lib/locomotive/steam/middlewares/url_redirection.rb +14 -1
  31. data/lib/locomotive/steam/repositories.rb +5 -1
  32. data/lib/locomotive/steam/repositories/content_entry_repository.rb +1 -1
  33. data/lib/locomotive/steam/repositories/content_type_field_repository.rb +4 -0
  34. data/lib/locomotive/steam/server.rb +2 -0
  35. data/lib/locomotive/steam/services.rb +9 -1
  36. data/lib/locomotive/steam/services/action_service.rb +26 -7
  37. data/lib/locomotive/steam/services/asset_host_service.rb +6 -1
  38. data/lib/locomotive/steam/services/auth_service.rb +105 -0
  39. data/lib/locomotive/steam/services/content_entry_service.rb +14 -0
  40. data/lib/locomotive/steam/services/external_api_service.rb +59 -25
  41. data/lib/locomotive/steam/services/liquid_parser_service.rb +5 -1
  42. data/lib/locomotive/steam/services/page_redirection_service.rb +29 -0
  43. data/lib/locomotive/steam/version.rb +1 -1
  44. data/locomotivecms_steam.gemspec +18 -16
  45. data/spec/fixtures/default/app/content_types/accounts.yml +59 -0
  46. data/spec/fixtures/default/app/content_types/songs.yml +2 -1
  47. data/spec/fixtures/default/app/views/pages/about_us/john_doe.fr.liquid.haml +5 -1
  48. data/spec/fixtures/default/app/views/pages/about_us/john_doe.liquid.haml +1 -1
  49. data/spec/fixtures/default/app/views/pages/account/forgot_password.liquid +39 -0
  50. data/spec/fixtures/default/app/views/pages/account/me.liquid +15 -0
  51. data/spec/fixtures/default/app/views/pages/account/reset_password.liquid +42 -0
  52. data/spec/fixtures/default/app/views/pages/account/sign_in.liquid +49 -0
  53. data/spec/fixtures/default/app/views/pages/all.liquid.haml +1 -1
  54. data/spec/fixtures/default/app/views/pages/emails/reset_password.liquid +15 -0
  55. data/spec/fixtures/default/app/views/pages/songs/template.liquid.haml +1 -1
  56. data/spec/fixtures/default/config/metafields_schema.yml +10 -0
  57. data/spec/fixtures/default/config/site.yml +5 -0
  58. data/spec/fixtures/default/config/translations.yml +25 -1
  59. data/spec/fixtures/default/data/accounts.yml +15 -0
  60. data/spec/fixtures/mongodb/locomotive_accounts.bson +0 -0
  61. data/spec/fixtures/mongodb/locomotive_accounts.metadata.json +1 -1
  62. data/spec/fixtures/mongodb/locomotive_activities.bson +0 -0
  63. data/spec/fixtures/mongodb/locomotive_activities.metadata.json +1 -1
  64. data/spec/fixtures/mongodb/locomotive_content_assets.bson +0 -0
  65. data/spec/fixtures/mongodb/locomotive_content_assets.metadata.json +1 -1
  66. data/spec/fixtures/mongodb/locomotive_content_entries.bson +0 -0
  67. data/spec/fixtures/mongodb/locomotive_content_entries.metadata.json +1 -1
  68. data/spec/fixtures/mongodb/locomotive_content_types.bson +0 -0
  69. data/spec/fixtures/mongodb/locomotive_content_types.metadata.json +1 -1
  70. data/spec/fixtures/mongodb/locomotive_pages.bson +0 -0
  71. data/spec/fixtures/mongodb/locomotive_pages.metadata.json +1 -1
  72. data/spec/fixtures/mongodb/locomotive_sites.bson +0 -0
  73. data/spec/fixtures/mongodb/locomotive_sites.metadata.json +1 -1
  74. data/spec/fixtures/mongodb/locomotive_snippets.bson +0 -0
  75. data/spec/fixtures/mongodb/locomotive_snippets.metadata.json +1 -1
  76. data/spec/fixtures/mongodb/locomotive_theme_assets.bson +0 -0
  77. data/spec/fixtures/mongodb/locomotive_theme_assets.metadata.json +1 -1
  78. data/spec/fixtures/mongodb/locomotive_translations.bson +0 -0
  79. data/spec/fixtures/mongodb/locomotive_translations.metadata.json +1 -1
  80. data/spec/integration/repositories/content_entry_repository_spec.rb +1 -1
  81. data/spec/integration/repositories/content_type_repository_spec.rb +1 -1
  82. data/spec/integration/repositories/page_repository_spec.rb +3 -3
  83. data/spec/integration/repositories/theme_asset_repository_spec.rb +1 -1
  84. data/spec/integration/repositories/translation_repository_spec.rb +1 -1
  85. data/spec/integration/server/auth_spec.rb +196 -0
  86. data/spec/integration/server/basic_spec.rb +18 -0
  87. data/spec/integration/server/nav_spec.rb +1 -1
  88. data/spec/integration/server/sitemap_spec.rb +1 -1
  89. data/spec/integration/services/content_entry_service_spec.rb +1 -1
  90. data/spec/integration/services/external_api_service_spec.rb +9 -0
  91. data/spec/support/helpers.rb +1 -1
  92. data/spec/unit/adapters/filesystem/yaml_loaders/content_entry_spec.rb +17 -5
  93. data/spec/unit/adapters/filesystem/yaml_loaders/content_type_spec.rb +4 -4
  94. data/spec/unit/adapters/filesystem/yaml_loaders/page_spec.rb +7 -7
  95. data/spec/unit/adapters/filesystem/yaml_loaders/site_spec.rb +1 -1
  96. data/spec/unit/adapters/filesystem/yaml_loaders/translation_spec.rb +1 -1
  97. data/spec/unit/entities/content_entry_spec.rb +10 -0
  98. data/spec/unit/errors_spec.rb +2 -2
  99. data/spec/unit/initializers/sprockets_spec.rb +0 -14
  100. data/spec/unit/liquid/drops/page_spec.rb +3 -2
  101. data/spec/unit/liquid/filters/date_spec.rb +219 -0
  102. data/spec/unit/liquid/tags/action_spec.rb +9 -0
  103. data/spec/unit/liquid/tags/authorize_spec.rb +51 -0
  104. data/spec/unit/liquid/tags/link_to_spec.rb +1 -1
  105. data/spec/unit/liquid/tags/paginate_spec.rb +1 -1
  106. data/spec/unit/liquid/tags/snippet_spec.rb +10 -0
  107. data/spec/unit/middlewares/auth_spec.rb +31 -0
  108. data/spec/unit/middlewares/redirection_spec.rb +37 -0
  109. data/spec/unit/middlewares/url_redirection_spec.rb +20 -1
  110. data/spec/unit/services/action_service_spec.rb +57 -1
  111. data/spec/unit/services/asset_host_service_spec.rb +15 -0
  112. data/spec/unit/services/auth_service_spec.rb +156 -0
  113. data/spec/unit/services/external_api_service_spec.rb +22 -0
  114. data/spec/unit/services/page_redirection_service_spec.rb +49 -0
  115. metadata +96 -40
  116. data/CHANGELOG.md +0 -29
  117. data/spec/fixtures/mongodb/system.indexes.bson +0 -0
@@ -0,0 +1,24 @@
1
+ module Locomotive::Steam
2
+ module Middlewares
3
+
4
+ # When rendering the page, the developer can stop it at anytime by
5
+ # raising an RedirectionException exception. The exception message holds
6
+ # the url we want the user to be redirected to.
7
+ # This is specifically used by the authorize liquid tag.
8
+ #
9
+ class Redirection < ThreadSafe
10
+
11
+ include Helpers
12
+
13
+ def _call
14
+ begin
15
+ self.next
16
+ rescue Locomotive::Steam::RedirectionException => e
17
+ redirect_to e.url, 302
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+
24
+ end
@@ -37,7 +37,12 @@ module Locomotive::Steam
37
37
 
38
38
  def parse_and_render_liquid
39
39
  document = services.liquid_parser.parse(page)
40
- document.render(liquid_context)
40
+ begin
41
+ document.render(liquid_context)
42
+ rescue Locomotive::Steam::ParsingRenderingError => e
43
+ e.file = page.template_path if e.file.blank?
44
+ raise e
45
+ end
41
46
  end
42
47
 
43
48
  def liquid_context
@@ -62,7 +67,8 @@ module Locomotive::Steam
62
67
  _default_liquid_assigns.merge(
63
68
  _locale_liquid_assigns.merge(
64
69
  _request_liquid_assigns.merge(
65
- _steam_liquid_assigns)))
70
+ _http_actions_liquid_assigns.merge(
71
+ _steam_liquid_assigns))))
66
72
  end
67
73
 
68
74
  def _default_liquid_assigns
@@ -100,13 +106,24 @@ module Locomotive::Steam
100
106
  {
101
107
  'base_url' => request.base_url,
102
108
  'fullpath' => request.fullpath,
109
+ 'http_method' => request.request_method,
103
110
  'ip_address' => request.ip,
104
111
  'mounted_on' => mounted_on,
105
112
  'path' => request.path,
106
- 'post?' => request.post?,
107
113
  'referer' => request.referer,
108
114
  'url' => request.url,
109
115
  'user_agent' => request.user_agent,
116
+ 'host' => request.host_with_port
117
+ }
118
+ end
119
+
120
+ def _http_actions_liquid_assigns
121
+ {
122
+ 'head?' => request.head?,
123
+ 'get?' => request.get?,
124
+ 'post?' => request.post?,
125
+ 'put?' => request.put?,
126
+ 'delete?' => request.delete?
110
127
  }
111
128
  end
112
129
 
@@ -28,7 +28,7 @@ module Locomotive::Steam
28
28
 
29
29
  def build_pages_to_xml
30
30
  page_repository.published.map do |page|
31
- next if page.index? || page.not_found? || page.layout?
31
+ next if page.index? || page.not_found? || page.layout? || (!page.templatized? && !page.listed?)
32
32
 
33
33
  build_page_xml(page)
34
34
  end.flatten.join.strip
@@ -56,6 +56,8 @@ module Locomotive::Steam
56
56
  return nil unless build_templatized_page_xml?(page, content_type, locale)
57
57
 
58
58
  repositories.content_entry.with(content_type).all.map do |entry|
59
+ next unless entry.visible? # only visible content entry
60
+
59
61
  _entry = Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale)
60
62
 
61
63
  next if _entry._label.blank? # should be translated
@@ -38,7 +38,7 @@ module Locomotive::Steam
38
38
  # don't accept a non localized entry in a locale other than the default one
39
39
  return nil if type.localized_names.count == 0 && locale.to_s != default_locale.to_s
40
40
 
41
- decorate(content_entry_repository.with(type).by_slug(slug))
41
+ decorate_entry(content_entry_repository.with(type).by_slug(slug))
42
42
  else
43
43
  nil
44
44
  end
@@ -56,11 +56,6 @@ module Locomotive::Steam
56
56
  services.page_finder.find('404')
57
57
  end
58
58
 
59
- def decorate(entry)
60
- return nil if entry.nil?
61
- Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale, default_locale)
62
- end
63
-
64
59
  end
65
60
  end
66
61
  end
@@ -29,6 +29,10 @@ module Locomotive::Steam::Middlewares
29
29
  @services ||= env.fetch('steam.services')
30
30
  end
31
31
 
32
+ def repositories
33
+ @repositories ||= services.repositories
34
+ end
35
+
32
36
  def request
33
37
  @request ||= env.fetch('steam.request')
34
38
  end
@@ -69,6 +73,21 @@ module Locomotive::Steam::Middlewares
69
73
  !!env['steam.live_editing']
70
74
  end
71
75
 
76
+ def decorate_entry(entry)
77
+ return nil if entry.nil?
78
+ Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale, default_locale)
79
+ end
80
+
81
+ def default_liquid_context
82
+ ::Liquid::Context.new({ 'site' => site.to_liquid }, {}, {
83
+ request: request,
84
+ locale: locale,
85
+ site: site,
86
+ services: services,
87
+ repositories: services.repositories
88
+ }, true)
89
+ end
90
+
72
91
  end
73
92
 
74
93
  end
@@ -13,16 +13,29 @@ module Locomotive::Steam
13
13
 
14
14
  def _call
15
15
  if url = redirect_url
16
+ emit_event
17
+
16
18
  redirect_to url
17
19
  end
18
20
  end
19
21
 
20
22
  protected
21
23
 
24
+ def requested_url
25
+ request.env['locomotive.path'] || request.fullpath
26
+ end
27
+
28
+ def emit_event
29
+ ActiveSupport::Notifications.instrument('steam.serve.url_redirection', {
30
+ site_id: site._id,
31
+ url: requested_url
32
+ })
33
+ end
34
+
22
35
  def redirect_url
23
36
  return false if site.url_redirections.nil? || site.url_redirections.size == 0
24
37
 
25
- site.url_redirections.to_h[request.env['locomotive.path'] || request.fullpath]
38
+ site.url_redirections.to_h[requested_url]
26
39
  end
27
40
 
28
41
  end
@@ -43,7 +43,11 @@ module Locomotive
43
43
 
44
44
  def build_adapter(options)
45
45
  name = ((options || {})[:name] || :filesystem).to_s
46
- require_relative "adapters/#{name.downcase}"
46
+ begin
47
+ require_relative "adapters/#{name.downcase}"
48
+ rescue LoadError
49
+ puts 'Not a Steam built-in adapter'
50
+ end
47
51
  klass = "Locomotive::Steam::#{name.camelize}Adapter".constantize
48
52
  klass.new(options)
49
53
  end
@@ -57,7 +57,7 @@ module Locomotive
57
57
  end
58
58
 
59
59
  def find(id)
60
- conditions, _ = conditions_without_order_by(_id: id)
60
+ conditions, _ = conditions_without_order_by(_id: self.adapter.make_id(id))
61
61
  first { where(conditions) }
62
62
  end
63
63
 
@@ -26,6 +26,10 @@ module Locomotive
26
26
  query { where(type: :file) }.all
27
27
  end
28
28
 
29
+ def passwords
30
+ query { where(type: :password) }.all
31
+ end
32
+
29
33
  def dates_and_date_times
30
34
  query { where(k(:type, :in) => %i(date date_time)) }.all
31
35
  end
@@ -61,6 +61,8 @@ module Locomotive::Steam
61
61
  Middlewares::EntrySubmission,
62
62
  Middlewares::Locale,
63
63
  Middlewares::LocaleRedirection,
64
+ Middlewares::Redirection,
65
+ Middlewares::Auth,
64
66
  Middlewares::PrivateAccess,
65
67
  Middlewares::Path,
66
68
  Middlewares::Page,
@@ -63,7 +63,7 @@ module Locomotive
63
63
  end
64
64
 
65
65
  register :action do
66
- Steam::ActionService.new(current_site, email, content_entry)
66
+ Steam::ActionService.new(current_site, email, content_entry: content_entry, api: external_api, redirection: page_redirection)
67
67
  end
68
68
 
69
69
  register :content_entry do
@@ -82,6 +82,10 @@ module Locomotive
82
82
  Steam::UrlBuilderService.new(current_site, locale, request)
83
83
  end
84
84
 
85
+ register :page_redirection do
86
+ Steam::PageRedirectionService.new(page_finder, url_builder)
87
+ end
88
+
85
89
  register :theme_asset_url do
86
90
  Steam::ThemeAssetUrlService.new(repositories.theme_asset, asset_host, configuration.theme_assets_checksum)
87
91
  end
@@ -118,6 +122,10 @@ module Locomotive
118
122
  Steam::EmailService.new(page_finder, liquid_parser, asset_host, configuration.mode == :test)
119
123
  end
120
124
 
125
+ register :auth do
126
+ Steam::AuthService.new(content_entry, email)
127
+ end
128
+
121
129
  register :cache do
122
130
  Steam::NoCacheService.new
123
131
  end
@@ -11,6 +11,8 @@ module Locomotive
11
11
 
12
12
  class ActionService
13
13
 
14
+ SERVICES = %w(content_entry api redirection)
15
+
14
16
  BUILT_IN_FUNCTIONS = %w(
15
17
  getProp
16
18
  setProp
@@ -20,9 +22,11 @@ module Locomotive
20
22
  allEntries
21
23
  findEntry
22
24
  createEntry
23
- updateEntry)
25
+ updateEntry
26
+ callAPI
27
+ redirectTo)
24
28
 
25
- attr_accessor_initialize :site, :email, :content_entry_service
29
+ attr_accessor_initialize :site, :email, :services
26
30
 
27
31
  def run(script, params = {}, liquid_context)
28
32
  context = Duktape::Context.new
@@ -35,15 +39,22 @@ module Locomotive
35
39
  }
36
40
  JS
37
41
 
38
- # puts script.inspect # DEBUG
39
-
40
- context.exec_string script
41
-
42
- context.call_prop('locomotiveAction', site.as_json, params)
42
+ begin
43
+ context.exec_string script
44
+ context.call_prop('locomotiveAction', site.as_json, params)
45
+ rescue Locomotive::Steam::RedirectionException
46
+ raise
47
+ rescue Exception => e
48
+ raise Locomotive::Steam::ActionError.new(e, script)
49
+ end
43
50
  end
44
51
 
45
52
  private
46
53
 
54
+ SERVICES.each do |name|
55
+ define_method(:"#{name}_service") { self.services[:"#{name}"] }
56
+ end
57
+
47
58
  def define_built_in_functions(context, liquid_context)
48
59
  BUILT_IN_FUNCTIONS.each do |name|
49
60
  context.define_function name, &send(:"#{name.underscore}_lambda", liquid_context)
@@ -86,6 +97,14 @@ module Locomotive
86
97
  -> (type, id_or_slug, attributes) { content_entry_service.update(type, id_or_slug, attributes, true) }
87
98
  end
88
99
 
100
+ def call_api_lambda(liquid_context)
101
+ -> (method, url, options) { api_service.consume(url, (options || {}).with_indifferent_access.merge(method: method), true) }
102
+ end
103
+
104
+ def redirect_to_lambda(liquid_context)
105
+ -> (page_handle, locale = nil) { redirection_service.redirect_to(page_handle, locale) }
106
+ end
107
+
89
108
  end
90
109
 
91
110
  end
@@ -18,13 +18,18 @@ module Locomotive
18
18
 
19
19
  return add_timestamp_suffix(source, timestamp) if source =~ Steam::IsHTTP
20
20
 
21
- url = self.host ? URI.join(host, source).to_s : source
21
+ url = self.host ? build_url(host, source) : source
22
22
 
23
23
  add_timestamp_suffix(url, timestamp)
24
24
  end
25
25
 
26
26
  private
27
27
 
28
+ def build_url(host, source)
29
+ clean_source = source.sub(/\A^\//, '')
30
+ URI.join(host, clean_source).to_s
31
+ end
32
+
28
33
  def build_host(host, request, site)
29
34
  if host
30
35
  if host.respond_to?(:call)
@@ -0,0 +1,105 @@
1
+ module Locomotive
2
+ module Steam
3
+
4
+ class AuthService
5
+
6
+ MIN_PASSWORD_LENGTH = 6
7
+ RESET_TOKEN_LIFETIME = 1 * 3600 # 6 hours in seconds
8
+
9
+ attr_accessor_initialize :entries, :email_service
10
+
11
+ def find_authenticated_resource(type, id)
12
+ entries.find(type, id)
13
+ end
14
+
15
+ def sign_in(options)
16
+ entry = entries.all(options.type, options.id_field => options.id).first
17
+
18
+ if entry
19
+ hashed_password = entry[:"#{options.password_field}_hash"]
20
+ password = ::BCrypt::Engine.hash_secret(options.password, entry.send(options.password_field).try(:salt))
21
+ same_password = secure_compare(password, hashed_password)
22
+
23
+ return [:signed_in, entry] if same_password
24
+ end
25
+
26
+ :wrong_credentials
27
+ end
28
+
29
+ # options is an instance of the AuthOptions class
30
+ def forgot_password(options, context)
31
+ entry = entries.all(options.type, options.id_field => options.id).first
32
+
33
+ if entry.nil?
34
+ :"wrong_#{options.id_field}"
35
+ else
36
+ entries.update_decorated_entry(entry, {
37
+ '_auth_reset_token' => SecureRandom.hex,
38
+ '_auth_reset_sent_at' => Time.zone.now.iso8601
39
+ })
40
+
41
+ context['reset_password_url'] = options.reset_password_url + '?auth_reset_token=' + entry['_auth_reset_token']
42
+ context[options.type.singularize] = entry
43
+
44
+ send_reset_password_instructions(options, context)
45
+
46
+ :"reset_#{options.password_field}_instructions_sent"
47
+ end
48
+ end
49
+
50
+ def reset_password(options)
51
+ return :invalid_token if options.reset_token.blank?
52
+ return :password_too_short if options.password.to_s.size < MIN_PASSWORD_LENGTH
53
+
54
+ entry = entries.all(options.type, '_auth_reset_token' => options.reset_token).first
55
+
56
+ if entry
57
+ sent_at = Time.parse(entry[:_auth_reset_sent_at]).to_i
58
+ now = Time.zone.now.to_i - RESET_TOKEN_LIFETIME
59
+
60
+ if sent_at >= now
61
+ entries.update_decorated_entry(entry, {
62
+ "#{options.password_field}_hash" => BCrypt::Password.create(options.password),
63
+ '_auth_reset_token' => nil,
64
+ '_auth_reset_sent_at' => nil
65
+ })
66
+
67
+ return [:"#{options.password_field}_reset", entry]
68
+ end
69
+ end
70
+
71
+ :invalid_token
72
+ end
73
+
74
+ private
75
+
76
+ def send_reset_password_instructions(options, context)
77
+ email_options = { from: options.from, to: options.id, subject: options.subject, smtp: options.smtp }
78
+
79
+ if options.email_handle
80
+ email_options[:page_handle] = options.email_handle
81
+ else
82
+ email_options[:body] = <<-EMAIL
83
+ Hi,
84
+ To reset your password please follow the link below: #{context['reset_password_url']}.
85
+ Thanks!
86
+ EMAIL
87
+ end
88
+
89
+ email_service.send_email(email_options, context)
90
+ end
91
+
92
+ # https://github.com/plataformatec/devise/blob/88724e10adaf9ffd1d8dbfbaadda2b9d40de756a/lib/devise.rb#L485
93
+ def secure_compare(a, b)
94
+ return false if a.blank? || b.blank? || a.bytesize != b.bytesize
95
+ l = a.unpack "C#{a.bytesize}"
96
+
97
+ res = 0
98
+ b.each_byte { |byte| res |= byte ^ l.shift }
99
+ res == 0
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+ end