gretel-trails 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 75ba4eea078b14bb0d61eb137262d08d24388554
4
- data.tar.gz: aacf75c20b0fea1e345149b423e73a38dc165c6f
3
+ metadata.gz: e37b779cdb1997c44fbffe04b0e99cdcaeffa9ac
4
+ data.tar.gz: d90ed701b34331eae74e4d60ea6f3c465b3841f6
5
5
  SHA512:
6
- metadata.gz: 1ccee9ba8ed783a89dc99844d5d090b562f582a28520e68360ebe1e97c13d9a1cde904d3ceb78b3cd7a626e6365a813905d5ee7ecbb46bea2934316b371d7b48
7
- data.tar.gz: 304d444bfdde884621754e8e9b3499dad62554e6092f7c2361cd117e9006ecdca79874f259b99a02f8c7071a543c8747d05d677e8f22458818a9c20b434fba0d
6
+ metadata.gz: c5351e3714d5dbaf54f423248c1919876e129b02fab2f2260097b63962f43def0598b485a11ca18596bc0c526387efce9813dbc1f8f07d233de0f6518eb9a0d7
7
+ data.tar.gz: 0f29f72ea2058d12e67a32408fffe33e40d2040f614968d88c12a0f38ec3eecf86b1c3585860b633e783c0687dff2f03e299d0e590d898b8dd096afd2cfad620
data/Gemfile CHANGED
@@ -15,4 +15,5 @@ gem "coffee-rails"
15
15
  # your gem to rubygems.org.
16
16
 
17
17
  # To use debugger
18
- # gem 'debugger'
18
+ # gem 'debugger'
19
+ gem "gretel", github: "lassebunk/gretel", branch: "3.0.0"
data/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  # Gretel::Trails
4
4
 
5
- Gretel::Trails makes it easy to hide [Gretel](https://github.com/lassebunk/gretel) breadcrumb trails from the user, so they don't see them in URLs when navigating your site.
5
+ Gretel::Trails makes it possible to set [Gretel](https://github.com/lassebunk/gretel) breadcrumb trails via the URL `params[:trail]`.
6
+ This makes it possible to link back to a different breadcrumb trail than the one specified in your breadcrumb, for example if you have a
7
+ store with products that have a default parent to their category, but when visiting from the reviews section, you want to link back to the reviews instead.
8
+
9
+ You can also hide trails from the user using the `:hidden` strategy, so they don't see them in URLs when navigating your site. See below for more info.
6
10
 
7
11
  ## Installation
8
12
 
@@ -18,7 +22,43 @@ And run:
18
22
  $ bundle
19
23
  ```
20
24
 
21
- In an initializer, e.g. *config/initializers/gretel.rb*:
25
+ Gretel::Trails has different stores that are used to serialize and deserialize the trails for use in URLs.
26
+
27
+ The default store is the URL store that encodes trails directly in the URL. Note that a trail stored in the URL can get very long, so the recommended way is to use the database or Redis store. See [Stores](#stores) below for more info.
28
+
29
+ In order to use the URL store, you must set a secret that's used to prevent cross-site scripting attacks. In an initializer, e.g. *config/initializers/gretel.rb*:
30
+
31
+ ```
32
+ Gretel::Trails::UrlStore.secret = 'your_key_here' # Must be changed to something else to be secure
33
+ ```
34
+
35
+ You can generate a secret using `SecureRandom.hex(64)` or `rake secret`.
36
+
37
+ Then you can set the breadcrumb trail:
38
+
39
+ ```erb
40
+ <% breadcrumb :reviews %>
41
+ ...
42
+ <% @products.each do |product| %>
43
+ <%= link_to @product.name, product_path(product, trail: breadcrumb_trail) %>
44
+ <% end %>
45
+ ```
46
+
47
+ The product view will now have the breadcrumb trail from the first page (reviews) instead of its default parent.
48
+
49
+ ## Custom trail param
50
+
51
+ The default trail param is `params[:trail]`. You can change it in an initializer:
52
+
53
+ ```ruby
54
+ Gretel::Trails.trail_param = :other_param
55
+ ```
56
+
57
+ ## Hiding trails in URLs
58
+
59
+ Gretel::Trails has a `:hidden` strategy that can be used to hide trails in URLs from the user while the server sees them. This is done via data attributes and `history.replaceState` in browsers that support it.
60
+
61
+ To hide trails, you set the strategy in an initializer, e.g. *config/initializers/gretel.rb*:
22
62
 
23
63
  ```ruby
24
64
  Gretel::Trails.strategy = :hidden
@@ -38,11 +78,11 @@ And finally, at the bottom of *app/assets/javascripts/application.js*:
38
78
  ```
39
79
 
40
80
  Breadcrumb trails are now hidden from the user so they don't see them in URLs. It uses data attributes and `history.replaceState` to hide the trails from the URL.
41
- For older browsers it falls back gracefully to showing trails in the URL, as specified by `Gretel.trail_param`.
81
+ For older browsers it falls back gracefully to showing trails in the URL, as specified by `Gretel::Trails.trail_param`.
42
82
 
43
83
  Note: If you use [Turbolinks](https://github.com/rails/turbolinks), it's important that you add the require *after* you require Turbolinks. Else it won't work.
44
84
 
45
- ## Usage
85
+ ### Usage
46
86
 
47
87
  When you want to invisibly add the current trail when the user clicks a link, you add a special JS selector to the link where you want the trail added on click:
48
88
 
@@ -54,9 +94,17 @@ When you want to invisibly add the current trail when the user clicks a link, yo
54
94
 
55
95
  Trails are now transferred invisibly to the next page when the user clicks a link.
56
96
 
57
- See Customization below for info on changing the `.js-append-trail` selector.
97
+ See [Customization](#customization) below for info on changing the `.js-append-trail` selector.
98
+
99
+ If you need to set the trail directly on a link without the JS selector, you can do so:
58
100
 
59
- ### Custom links
101
+ ```erb
102
+ <%= link_to "My Link", my_link_path, data: { trail: breadcrumb_trail } %>
103
+ ```
104
+
105
+ See [Customization](#customization) below for info on changing the `data-trail` attribute to something else.
106
+
107
+ ### Breadcrumb links
60
108
 
61
109
  Inside breadcrumbs, the links are automatically transformed with trails removed from the URLs and applied as data attributes instead.
62
110
  If you want to do custom breadcrumb links with these changes applied, you can use the `breadcrumb_link_to` helper:
@@ -89,14 +137,76 @@ The default trail data attribute for `<body>` and links is `data-trail` but you
89
137
  Gretel::Trails::HiddenStrategy.data_attribute = "other-data-attribute"
90
138
  ```
91
139
 
92
- That's it. :)
140
+ `data-` is added automatically, so if for example you want the attribute to be `data-my-attr`, you just set it to `my-attr`.
141
+
142
+ ## Stores
143
+
144
+ Gretel::Trails comes with different stores for encoding and decoding trails for use in the URL.
145
+
146
+ ### URL store
147
+
148
+ The default store is the URL store which is great for simple use, but if you have longer trails, it can get very long.
149
+
150
+ To use the URL store, set it in an initializer, e.g. *config/initializers/gretel.rb*:
151
+
152
+ ```ruby
153
+ Gretel::Trails.store = :url # Not really needed as this is the default
154
+ Gretel::Trails::UrlStore.secret = 'your_key_here' # Must be changed to something else to be secure
155
+ ```
156
+
157
+ The secret is used to prevent cross-site scripting attacks. You can generate a secure one using `SecureRandom.hex(64)` or `rake secret`.
158
+
159
+ ### Database store
160
+
161
+ The database store stores trails in the database so the trail keys have a maximum length of 40 characters (a SHA1 of the trail).
162
+
163
+ To use the database store, set it an initializer, e.g. *config/initializers/gretel.rb*:
164
+
165
+ ```ruby
166
+ Gretel::Trails.store = :db
167
+ ```
168
+
169
+ You also need to create a migration for the database table that holds the trails:
170
+
171
+ ```bash
172
+ $ rails generate gretel:trails:migration
173
+ ```
174
+
175
+ This creates a table named `gretel_trails` that hold the trails.
176
+
177
+ ActiveRecord doesn't delete expired records automatically, so to delete expired trails you need to run the following rake task, for example once daily:
178
+
179
+ ```bash
180
+ $ rake gretel:trails:delete_expired
181
+ ```
182
+
183
+ You can also run `Gretel::Trails.delete_expired` directly.
184
+
185
+ If you need a gem for managing recurring tasks, [Whenever](https://github.com/javan/whenever) is a solution that handles cron jobs via Ruby code.
186
+
187
+ The default expiration period is 1 day. To set a custom expiration period, in an initializer:
188
+
189
+ ```ruby
190
+ Gretel::Trails::ActiveRecordStore.expires_in = 2.days
191
+ ```
192
+
193
+ ### Redis store
194
+
195
+ If you want to store trails in [Redis](https://github.com/redis/redis), you can use the Redis store.
196
+
197
+ To use the Redis store, set it in an initializer, e.g. *config/initializers/gretel.rb*:
198
+
199
+ ```ruby
200
+ Gretel::Trails.store = :redis
201
+ Gretel::Trails::RedisStore.connect_options = { host: "10.0.1.1", port: 6380 }
202
+ ```
93
203
 
94
- ### Trail param
204
+ Trails are now stored in Redis and expired automatically after 1 day (by default).
95
205
 
96
- The trail param that's hidden from the user is `params[:trail]` by default. You can change this in an initializer:
206
+ To set a custom expiration period, in an initializer:
97
207
 
98
208
  ```ruby
99
- Gretel.trail_param = :other_param
209
+ Gretel::Trails::RedisStore.expires_in = 2.days
100
210
  ```
101
211
 
102
212
  ## Requirements
@@ -17,12 +17,14 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^test/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "gretel", ">= 3.0.0.beta4"
20
+ spec.add_dependency "gretel", ">= 3.0.0.beta5"
21
21
  spec.add_dependency "rails", ">= 3.2.0"
22
22
  spec.add_development_dependency "rails", "~> 3.2.13"
23
23
  spec.add_development_dependency "bundler", "~> 1.3"
24
24
  spec.add_development_dependency "sqlite3"
25
25
  spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "fakeredis", "~> 0.4.2"
27
+ spec.add_development_dependency "timecop", "~> 0.6.3"
26
28
  spec.add_development_dependency "capybara", "~> 2.1.0"
27
29
  spec.add_development_dependency "capybara-webkit", "~> 1.0.0"
28
30
  end
@@ -8,10 +8,10 @@ end
8
8
 
9
9
  # Remove trail from querystring
10
10
  removeTrailFromUrl = ->
11
+ return if location.href.indexOf("<%= Gretel::Trails.trail_param %>=") is -1
11
12
  if history.replaceState?
12
- return if location.href.indexOf("<%= Gretel.trail_param %>=") is -1
13
13
  uri = new Gretel.Trails.Uri(location.href)
14
- history.replaceState history.state, document.title, uri.deleteQueryParam("<%= Gretel.trail_param %>")
14
+ history.replaceState history.state, document.title, uri.deleteQueryParam("<%= Gretel::Trails.trail_param %>")
15
15
 
16
16
  # Remove trail on load
17
17
  removeTrailFromUrl()
@@ -26,7 +26,7 @@ $ ->
26
26
  if trail = $(this).data("<%= Gretel::Trails::HiddenStrategy.data_attribute %>") || $("body").data("<%= Gretel::Trails::HiddenStrategy.data_attribute %>")
27
27
  href = $(this).attr("href")
28
28
  uri = new Gretel.Trails.Uri(href)
29
- href = uri.deleteQueryParam("<%= Gretel.trail_param %>").addQueryParam("<%= Gretel.trail_param %>", trail)
29
+ href = uri.deleteQueryParam("<%= Gretel::Trails.trail_param %>").addQueryParam("<%= Gretel::Trails.trail_param %>", trail)
30
30
  $(this).attr("href", href)
31
31
  else
32
32
  console?.log "[Gretel] No `data-<%= Gretel::Trails::HiddenStrategy.data_attribute %>` was found on the <body> tag or the link you just clicked. Please set it using the `breadcrumb_trail` helper or see the Gretel::Trails readme for more info."
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a sample breadcrumbs configuration file for Gretel.
3
+
4
+ Example:
5
+ rails generate gretel:install
6
+
7
+ This will create:
8
+ config/breadcrumbs.rb
@@ -0,0 +1,19 @@
1
+ require 'rails/generators'
2
+
3
+ module Gretel
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ desc "Creates a sample configuration file in config/breadcrumbs.rb"
8
+ def create_config_file
9
+ copy_file "breadcrumbs.rb", "config/breadcrumbs.rb"
10
+ end
11
+
12
+ desc "Creates an initializer with trail secret"
13
+ def create_initializer
14
+ initializer "gretel.rb" do
15
+ %{Gretel::Trails::UrlStore.secret = '#{SecureRandom.hex(64)}'}
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ crumb :root do
2
+ link "Home", root_path
3
+ end
4
+
5
+ # crumb :projects do
6
+ # link "Projects", projects_path
7
+ # end
8
+
9
+ # crumb :project do |project|
10
+ # link project.name, project_path(project)
11
+ # parent :projects
12
+ # end
13
+
14
+ # crumb :project_issues do |project|
15
+ # link "Issues", project_issues_path(project)
16
+ # parent :project, project
17
+ # end
18
+
19
+ # crumb :issue do |issue|
20
+ # link issue.title, issue_path(issue)
21
+ # parent :project_issues, issue.project
22
+ # end
23
+
24
+ # If you want to split your breadcrumbs configuration over multiple files, you
25
+ # can create a folder named `config/breadcrumbs` and put your configuration
26
+ # files there. All *.rb files (e.g. `frontend.rb` or `products.rb`) in that
27
+ # folder are loaded and reloaded automatically when you change them, just like
28
+ # this file (`config/breadcrumbs.rb`).
@@ -0,0 +1,11 @@
1
+ class CreateGretelTrails < ActiveRecord::Migration
2
+ def change
3
+ create_table :gretel_trails do |t|
4
+ t.string :key, limit: 40
5
+ t.text :value
6
+ t.datetime :expires_at
7
+ end
8
+ add_index :gretel_trails, :key, unique: true
9
+ add_index :gretel_trails, :expires_at
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require 'rails/generators'
2
+
3
+ module Gretel
4
+ module Trail
5
+ class MigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ source_root File.expand_path('../../templates', __FILE__)
9
+
10
+ def self.next_migration_number(path)
11
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
12
+ end
13
+
14
+ desc "Creates a migration for a table to store trail data"
15
+ def create_migration
16
+ migration_template "trail_migration.rb", "db/migrate/create_gretel_trails.rb"
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/gretel/trails.rb CHANGED
@@ -1,9 +1,18 @@
1
1
  require "gretel"
2
+ require "gretel/trails/stores"
3
+ require "gretel/trails/tasks"
4
+ require "gretel/trails/patches"
2
5
  require "gretel/trails/version"
3
6
  require "gretel/trails/engine"
4
7
 
5
8
  module Gretel
6
9
  module Trails
10
+ STORES = {
11
+ url: UrlStore,
12
+ db: ActiveRecordStore,
13
+ redis: RedisStore
14
+ }
15
+
7
16
  class << self
8
17
  # Activated strategies
9
18
  def strategies
@@ -20,6 +29,76 @@ module Gretel
20
29
  end
21
30
 
22
31
  alias_method :strategy=, :strategies=
32
+
33
+ # Gets the store that is used to encode and decode trails.
34
+ # Default: +Gretel::Trails::UrlStore+
35
+ def store
36
+ @store ||= UrlStore
37
+ end
38
+
39
+ # Sets the store that is used to encode and decode trails.
40
+ # Can be a subclass of +Gretel::Trails::Store+, or a symbol: +:url+, +:db+, or +:redis+.
41
+ def store=(value)
42
+ if value.is_a?(Symbol)
43
+ klass = STORES[value]
44
+ raise ArgumentError, "Unknown Gretel::Trails.store #{value.inspect}. Use any of #{STORES.inspect}." unless klass
45
+ self.store = klass
46
+ else
47
+ @store = value
48
+ end
49
+ end
50
+
51
+ # Uses the store to encode an array of links to a unique key that can be used in URLs.
52
+ def encode(links)
53
+ store.encode(links)
54
+ end
55
+
56
+ # Uses the store to decode a unique key to an array of links.
57
+ def decode(key)
58
+ store.decode(key)
59
+ end
60
+
61
+ # Deletes expired keys from the store.
62
+ # Not all stores support expiring keys, and will raise an exception if they don't.
63
+ def delete_expired
64
+ store.delete_expired
65
+ end
66
+
67
+ # Returns the current number of trails in the store.
68
+ # Not all stores support counting keys, and will raise an exception if they don't.
69
+ def count
70
+ store.key_count
71
+ end
72
+
73
+ # Deletes all trails in the store.
74
+ # Not all stores support deleting trails, and will raise an exception if they don't.
75
+ def delete_all
76
+ store.delete_all_keys
77
+ end
78
+
79
+ # Name of trail param. Default: +:trail+.
80
+ def trail_param
81
+ @trail_param ||= :trail
82
+ end
83
+
84
+ # Sets the trail param.
85
+ attr_writer :trail_param
86
+
87
+ # Yields +self+ for configuration.
88
+ #
89
+ # Gretel::Trails.configure do |config|
90
+ # config.store = :db
91
+ # config.strategy = :hidden
92
+ # end
93
+ def configure
94
+ yield self
95
+ end
96
+
97
+ # Resets all changes made to +Gretel::Trail+. Used for testing.
98
+ def reset!
99
+ instance_variables.each { |var| remove_instance_variable var }
100
+ STORES.each_value(&:reset!)
101
+ end
23
102
  end
24
103
  end
25
104
  end
@@ -0,0 +1,3 @@
1
+ require "gretel/trails/patches/gretel"
2
+ require "gretel/trails/patches/renderer"
3
+ require "gretel/trails/patches/view_helpers"
@@ -0,0 +1,11 @@
1
+ Gretel.class_eval do
2
+ class << self
3
+ # Adds +Trails.reset!+ to +Gretel.reset!+.
4
+ def reset_with_trails!
5
+ reset_without_trails!
6
+ Gretel::Trails.reset!
7
+ end
8
+
9
+ alias_method_chain :reset!, :trails
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ Gretel::Renderer.class_eval do
2
+ # Loads parent links from trail if +params[:trail]+ is present.
3
+ def parent_links_for_with_trail(crumb)
4
+ if params[Gretel::Trails.trail_param].present?
5
+ Gretel::Trails.decode(params[Gretel::Trails.trail_param])
6
+ else
7
+ parent_links_for_without_trail(crumb)
8
+ end
9
+ end
10
+
11
+ alias_method_chain :parent_links_for, :trail
12
+
13
+ # Returns encoded trail for the breadcrumb.
14
+ def trail
15
+ @trail ||= Gretel::Trails.encode(links)
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ Gretel::ViewHelpers.class_eval do
2
+ # Encoded breadcrumb trail to be used in URLs.
3
+ def breadcrumb_trail
4
+ gretel_renderer.trail
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ require "gretel/trails/stores/store"
2
+ require "gretel/trails/stores/url_store"
3
+ require "gretel/trails/stores/active_record_store"
4
+ require "gretel/trails/stores/redis_store"
@@ -0,0 +1,64 @@
1
+ module Gretel
2
+ module Trails
3
+ class ActiveRecordStore < Store
4
+ class << self
5
+ # Number of seconds to keep the trails in the database.
6
+ # Default: +1.day+
7
+ def expires_in
8
+ @expires_in ||= 1.day
9
+ end
10
+
11
+ # Sets the number of seconds to keep the trails in the database.
12
+ attr_writer :expires_in
13
+
14
+ # Save array to database.
15
+ def save(array)
16
+ json = array.to_json
17
+ key = Digest::SHA1.hexdigest(json)
18
+ GretelTrail.set(key, array, expires_in.from_now)
19
+ key
20
+ end
21
+
22
+ # Retrieve array from database.
23
+ def retrieve(key)
24
+ GretelTrail.get(key)
25
+ end
26
+
27
+ # Delete expired keys.
28
+ def delete_expired
29
+ GretelTrail.delete_expired
30
+ end
31
+
32
+ # Gets the number of trails stored in the database.
33
+ def key_count
34
+ GretelTrail.count
35
+ end
36
+
37
+ # Deletes all trails stored in the database.
38
+ def delete_all_keys
39
+ GretelTrail.delete_all
40
+ end
41
+ end
42
+
43
+ class GretelTrail < ActiveRecord::Base
44
+ serialize :value, Array
45
+
46
+ def self.get(key)
47
+ find_by_key(key).try(:value)
48
+ end
49
+
50
+ def self.set(key, value, expires_at)
51
+ find_or_initialize_by_key(key).tap do |rec|
52
+ rec.value = value
53
+ rec.expires_at = expires_at
54
+ rec.save
55
+ end
56
+ end
57
+
58
+ def self.delete_expired
59
+ delete_all(["expires_at < ?", Time.now])
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ module Gretel
2
+ module Trails
3
+ class RedisStore < Store
4
+ class << self
5
+ # Options to connect to Redis.
6
+ def connect_options
7
+ @connect_options ||= {}
8
+ end
9
+
10
+ # Sets the Redis connect options.
11
+ attr_writer :connect_options
12
+
13
+ # Number of seconds to keep the trails in Redis.
14
+ # Default: +1.day+
15
+ def expires_in
16
+ @expires_in ||= 1.day
17
+ end
18
+
19
+ # Sets the number of seconds to keep the trails in Redis.
20
+ attr_writer :expires_in
21
+
22
+ # Save array to Redis.
23
+ def save(array)
24
+ json = array.to_json
25
+ key = Digest::SHA1.hexdigest(json)
26
+ redis.setex redis_key_for(key), expires_in, json
27
+ key
28
+ end
29
+
30
+ # Retrieve array from Redis.
31
+ def retrieve(key)
32
+ if json = redis.get(redis_key_for(key))
33
+ JSON.parse(json)
34
+ end
35
+ end
36
+
37
+ # Reference to the Redis connection.
38
+ def redis
39
+ @redis ||= begin
40
+ raise "Redis needs to be installed in order for #{name} to use it. Please add `gem \"redis\"` to your Gemfile." unless defined?(Redis)
41
+ Redis.new(connect_options)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Key to be stored in Redis.
48
+ def redis_key_for(key)
49
+ "gretel:trail:#{key}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ module Gretel
2
+ module Trails
3
+ class Store
4
+ class << self
5
+ # Save an encoded array to the store. It must return the trail key that
6
+ # can later be used to retrieve the array from the store.
7
+ def save(array)
8
+ raise "#{name} must implement #save to be able to save trails."
9
+ end
10
+
11
+ # Retrieve an encoded array from the store based on the saved key.
12
+ # It must return either the array, or nil if the key was not found.
13
+ def retrieve(key)
14
+ raise "#{name} must implement #retrieve to be able to retrieve trails."
15
+ end
16
+
17
+ # Deletes expired keys from the store.
18
+ def delete_expired
19
+ raise "#{name} doesn't support deleting expired keys."
20
+ end
21
+
22
+ # Gets the number of stored trail keys.
23
+ def key_count
24
+ raise "#{name} doesn't support counting trail keys."
25
+ end
26
+
27
+ # Deletes all stored trail keys.
28
+ def delete_all_keys
29
+ raise "#{name} doesn't support deleting all trail keys."
30
+ end
31
+
32
+ # Encode array of +links+ to unique trail key.
33
+ def encode(links)
34
+ arr = links.map { |link| [link.key, link.text, (link.text.html_safe? ? 1 : 0), link.url] }
35
+ save(arr)
36
+ end
37
+
38
+ # Decode unique trail key to array of links.
39
+ def decode(key)
40
+ if arr = retrieve(key)
41
+ arr.map { |key, text, html_safe, url| Link.new(key.to_sym, (html_safe == 1 ? text.html_safe : text), url) }
42
+ else
43
+ []
44
+ end
45
+ end
46
+
47
+ # Resets all changes made to the store. Used for testing.
48
+ def reset!
49
+ instance_variables.each { |var| remove_instance_variable var }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ module Gretel
2
+ module Trails
3
+ class UrlStore < Store
4
+ class << self
5
+ # Secret used for crypting trail in URL that should be set to something
6
+ # unguessable. This is required when using trails, for the reason that
7
+ # unencrypted trails would be vulnerable to cross-site scripting attacks.
8
+ attr_accessor :secret
9
+
10
+ # Securely encodes encoded array to a trail string to be used in URL.
11
+ def save(array)
12
+ base64 = encode_base64(array)
13
+ hash = generate_hash(base64)
14
+
15
+ [hash, base64].join("_")
16
+ end
17
+
18
+ # Securely decodes a URL trail string to encoded array.
19
+ def retrieve(key)
20
+ hash, base64 = key.split("_", 2)
21
+
22
+ if base64.blank?
23
+ Rails.logger.info "[Gretel] Trail decode failed: No Base64 in trail"
24
+ []
25
+ elsif hash == generate_hash(base64)
26
+ decode_base64(base64)
27
+ else
28
+ Rails.logger.info "[Gretel] Trail decode failed: Invalid hash '#{hash}' in trail"
29
+ []
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Encodes links array to Base64, internally using JSON for serialization.
36
+ def encode_base64(array)
37
+ Base64.urlsafe_encode64(array.to_json)
38
+ end
39
+
40
+ # Decodes links array from Base64.
41
+ def decode_base64(base64)
42
+ json = Base64.urlsafe_decode64(base64)
43
+ JSON.parse(json)
44
+ rescue
45
+ Rails.logger.info "[Gretel] Trail decode failed: Invalid Base64 '#{base64}' in trail"
46
+ []
47
+ end
48
+
49
+ # Generates a salted hash of +base64+.
50
+ def generate_hash(base64)
51
+ raise "#{name}.secret is not set. Please set it to an unguessable string, e.g. from `rake secret`, or use `rails generate gretel:install` to generate and set it automatically." if secret.blank?
52
+ Digest::SHA1.hexdigest([base64, secret].join)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -29,11 +29,11 @@ end
29
29
 
30
30
  Gretel::Renderer.class_eval do
31
31
  # Moves the trail from the querystring into a data attribute.
32
- def link_to_with_hidden_trail(name, url, options = {})
33
- if url.include?("#{Gretel.trail_param}=")
32
+ def breadcrumb_link_to_with_hidden_trail(name, url, options = {})
33
+ if url.include?("#{Gretel::Trails.trail_param}=")
34
34
  uri = URI.parse(url)
35
35
  query_hash = Hash[CGI.parse(uri.query.to_s).map { |k, v| [k, v.first] }]
36
- trail = query_hash.delete(Gretel.trail_param.to_s)
36
+ trail = query_hash.delete(Gretel::Trails.trail_param.to_s)
37
37
 
38
38
  options = options.dup
39
39
  options[:data] ||= {}
@@ -42,16 +42,16 @@ Gretel::Renderer.class_eval do
42
42
  uri.query = query_hash.to_query.presence
43
43
  url = uri.to_s
44
44
  end
45
- link_to_without_hidden_trail(name, url, options)
45
+ breadcrumb_link_to_without_hidden_trail(name, url, options)
46
46
  end
47
47
 
48
- alias_method_chain :link_to, :hidden_trail
48
+ alias_method_chain :breadcrumb_link_to, :hidden_trail
49
49
  end
50
50
 
51
51
  ActionView::Base.class_eval do
52
- # View helper proxy to the breadcrumb renderer's link_to that automatically
53
- # removes trails from URLs and adds them as data attributes.
52
+ # View helper proxy to the breadcrumb renderer's breadcrumb_link_to that
53
+ # automatically removes trails from URLs and adds them as data attributes.
54
54
  def breadcrumb_link_to(name, url, options = {})
55
- gretel_renderer.link_to(name, url, options)
55
+ gretel_renderer.breadcrumb_link_to(name, url, options)
56
56
  end unless method_defined?(:breadcrumb_link_to)
57
57
  end
@@ -0,0 +1,5 @@
1
+ require "rake"
2
+
3
+ Rake::Task.define_task("gretel:trails:delete_expired" => :environment) do
4
+ Gretel::Trails.delete_expired
5
+ end
@@ -1,5 +1,5 @@
1
1
  module Gretel
2
2
  module Trails
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -2,6 +2,10 @@ crumb :root do
2
2
  link "Home", root_path
3
3
  end
4
4
 
5
+ crumb :about do
6
+ link "About", about_path
7
+ end
8
+
5
9
  crumb :category do |category|
6
10
  link category.name, category
7
11
  end
@@ -1,2 +1,2 @@
1
- Gretel.trail_store = :db
1
+ Gretel::Trails.store = :db
2
2
  Gretel::Trails.strategy = :hidden
@@ -1,5 +1,7 @@
1
1
  Dummy::Application.routes.draw do
2
2
  root to: "home#index"
3
+
4
+ get "about" => "home#about", as: :about
3
5
 
4
6
  resources :categories
5
7
  resources :products do
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveRecordStoreTest < ActiveSupport::TestCase
4
+ setup do
5
+ Gretel.reset!
6
+ Gretel::Trails.store = :db
7
+ Gretel::Trails.delete_all
8
+
9
+ @links = [
10
+ [:root, "Home", "/"],
11
+ [:store, "Store <b>Test</b>".html_safe, "/store"],
12
+ [:search, "Search", "/store/search?q=test"]
13
+ ]
14
+ end
15
+
16
+ test "defaults" do
17
+ assert_equal 1.day, Gretel::Trails::ActiveRecordStore.expires_in
18
+ end
19
+
20
+ test "encoding" do
21
+ assert_equal "684c211441e72225cee89477a2d1f59e657c9e26",
22
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
23
+ end
24
+
25
+ test "decoding" do
26
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
27
+ decoded = Gretel::Trails.decode("684c211441e72225cee89477a2d1f59e657c9e26")
28
+ assert_equal @links, decoded.map { |link| [link.key, link.text, link.url] }
29
+ assert_equal [false, true, false], decoded.map { |link| link.text.html_safe? }
30
+ end
31
+
32
+ test "invalid trail" do
33
+ assert_equal [], Gretel::Trails.decode("asdgasdg")
34
+ end
35
+
36
+ test "delete expired" do
37
+ 10.times { Gretel::Trails.encode([Gretel::Link.new(:test, SecureRandom.hex(20), "/test")]) }
38
+ assert_equal 10, Gretel::Trails.count
39
+
40
+ Gretel::Trails.delete_expired
41
+ assert_equal 10, Gretel::Trails.count
42
+
43
+ Timecop.travel(14.hours.from_now) do
44
+ 5.times { Gretel::Trails.encode([Gretel::Link.new(:test, SecureRandom.hex(20), "/test")]) }
45
+ assert_equal 15, Gretel::Trails.count
46
+ end
47
+
48
+ Timecop.travel(25.hours.from_now) do
49
+ Gretel::Trails.delete_expired
50
+ assert_equal 5, Gretel::Trails.count
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+ require 'fakeredis'
3
+
4
+ class RedisStoreTest < ActiveSupport::TestCase
5
+ setup do
6
+ Gretel.reset!
7
+ Gretel::Trails.store = :redis
8
+
9
+ @links = [
10
+ [:root, "Home", "/"],
11
+ [:store, "Store <b>Test</b>".html_safe, "/store"],
12
+ [:search, "Search", "/store/search?q=test"]
13
+ ]
14
+ end
15
+
16
+ test "defaults" do
17
+ assert_equal 1.day, Gretel::Trails::RedisStore.expires_in
18
+ end
19
+
20
+ test "encoding" do
21
+ assert_equal "684c211441e72225cee89477a2d1f59e657c9e26",
22
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
23
+ end
24
+
25
+ test "decoding" do
26
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
27
+ decoded = Gretel::Trails.decode("684c211441e72225cee89477a2d1f59e657c9e26")
28
+ assert_equal @links, decoded.map { |link| [link.key, link.text, link.url] }
29
+ assert_equal [false, true, false], decoded.map { |link| link.text.html_safe? }
30
+ end
31
+
32
+ test "invalid trail" do
33
+ assert_equal [], Gretel::Trails.decode("asdgasdg")
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class UrlStoreTest < ActiveSupport::TestCase
4
+ setup do
5
+ Gretel.reset!
6
+
7
+ Gretel::Trails.store = :url
8
+ Gretel::Trails::UrlStore.secret = "128107d341e912db791d98bbe874a8250f784b0a0b4dbc5d5032c0fc1ca7bda9c6ece667bd18d23736ee833ea79384176faeb54d2e0d21012898dde78631cdf1"
9
+
10
+ @links = [
11
+ [:root, "Home", "/"],
12
+ [:store, "Store <b>Test</b>".html_safe, "/store"],
13
+ [:search, "Search", "/store/search?q=test"]
14
+ ]
15
+ end
16
+
17
+ test "encoding" do
18
+ assert_equal "5543214e6d7bbc3ba5209b2362cd7513d500f61b_W1sicm9vdCIsIkhvbWUiLDAsIi8iXSxbInN0b3JlIiwiU3RvcmUgPGI-VGVzdDwvYj4iLDEsIi9zdG9yZSJdLFsic2VhcmNoIiwiU2VhcmNoIiwwLCIvc3RvcmUvc2VhcmNoP3E9dGVzdCJdXQ==",
19
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
20
+ end
21
+
22
+ test "decoding" do
23
+ decoded = Gretel::Trails.decode("5543214e6d7bbc3ba5209b2362cd7513d500f61b_W1sicm9vdCIsIkhvbWUiLDAsIi8iXSxbInN0b3JlIiwiU3RvcmUgPGI-VGVzdDwvYj4iLDEsIi9zdG9yZSJdLFsic2VhcmNoIiwiU2VhcmNoIiwwLCIvc3RvcmUvc2VhcmNoP3E9dGVzdCJdXQ==")
24
+ assert_equal @links, decoded.map { |link| [link.key, link.text, link.url] }
25
+ assert_equal [false, true, false], decoded.map { |link| link.text.html_safe? }
26
+ end
27
+
28
+ test "invalid trail" do
29
+ assert_equal [], Gretel::Trails.decode("28f104524f5eaf6b3bd035710432fd2b9cbfd62c_X1sicm9vdCIsIkhvbWUiLDAsIi8iXSxbInN0b3JlIiwiU3RvcmUiLDAsIi9zdG9yZSJdLFsic2VhcmNoIiwiU2VhcmNoIiwwLCIvc3RvcmUvc2VhcmNoP3E9dGVzdCJdXQ==")
30
+ end
31
+
32
+ test "raises error if no secret set" do
33
+ Gretel::Trails::UrlStore.secret = nil
34
+ assert_raises RuntimeError do
35
+ Gretel::Trails.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
36
+ end
37
+ end
38
+ end
data/test/test_helper.rb CHANGED
@@ -11,6 +11,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
11
 
12
12
  require "capybara/rails"
13
13
  require "capybara-webkit"
14
+ require "timecop"
14
15
 
15
16
  class ActionDispatch::IntegrationTest
16
17
  # Make the Capybara DSL available in all integration tests
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ class TrailsTest < ActiveSupport::TestCase
4
+ setup do
5
+ Gretel.reset!
6
+ end
7
+
8
+ test "defaults" do
9
+ assert_equal :trail, Gretel::Trails.trail_param
10
+ end
11
+
12
+ test "configuration block" do
13
+ Gretel::Trails.configure do |config|
14
+ config.trail_param = :set_from_config
15
+ end
16
+
17
+ assert_equal :set_from_config, Gretel::Trails.trail_param
18
+ end
19
+
20
+ test "setting invalid store" do
21
+ assert_raises ArgumentError do
22
+ Gretel::Trails.store = :xx
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ class ViewHelpersTest < ActionView::TestCase
4
+ include Gretel::ViewHelpers
5
+
6
+ setup do
7
+ Gretel.reset!
8
+ Gretel::Trails::UrlStore.secret = "84f3196275c50b6fee3053c7b609b2633143f33f3536cb74abdf2753cca5a3e24b9dd93e4d7c75747c2f111821c7feb0e51e13485e4d772c17f60c1f8d832b72"
9
+ end
10
+
11
+ test "trail helper" do
12
+ breadcrumb :about
13
+
14
+ assert_equal "aec19c5388f02dd60151589ad01b4f3ec074598e_W1siYWJvdXQiLCJBYm91dCIsMCwiL2Fib3V0Il1d", breadcrumb_trail
15
+ end
16
+
17
+ test "loading trail" do
18
+ params[:trail] = "aec19c5388f02dd60151589ad01b4f3ec074598e_W1siYWJvdXQiLCJBYm91dCIsMCwiL2Fib3V0Il1d"
19
+ breadcrumb :recent_products
20
+
21
+ assert_equal %{<div class="breadcrumbs"><a href="/">Home</a> &rsaquo; <a href="/about">About</a> &rsaquo; <span class="current">Recent products</span></div>},
22
+ breadcrumbs
23
+ end
24
+
25
+ test "different trail param" do
26
+ Gretel::Trails.trail_param = :mytest
27
+ params[:mytest] = "aec19c5388f02dd60151589ad01b4f3ec074598e_W1siYWJvdXQiLCJBYm91dCIsMCwiL2Fib3V0Il1d"
28
+ breadcrumb :recent_products
29
+
30
+ assert_equal %{<div class="breadcrumbs"><a href="/">Home</a> &rsaquo; <a href="/about">About</a> &rsaquo; <span class="current">Recent products</span></div>},
31
+ breadcrumbs
32
+ end
33
+
34
+ test "unknown trail" do
35
+ params[:trail] = "notfound"
36
+ breadcrumb :recent_products
37
+
38
+ assert_equal %{<div class="breadcrumbs"><a href="/">Home</a> &rsaquo; <span class="current">Recent products</span></div>},
39
+ breadcrumbs
40
+ end
41
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gretel-trails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lasse Bunk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-17 00:00:00.000000000 Z
11
+ date: 2013-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gretel
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '>='
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0.beta4
19
+ version: 3.0.0.beta5
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '>='
25
25
  - !ruby/object:Gem::Version
26
- version: 3.0.0.beta4
26
+ version: 3.0.0.beta5
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rails
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: fakeredis
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 0.4.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 0.4.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: timecop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.6.3
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 0.6.3
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: capybara
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -138,10 +166,25 @@ files:
138
166
  - gretel-trails.gemspec
139
167
  - lib/assets/javascripts/gretel.trails.hidden.js.coffee.erb
140
168
  - lib/assets/javascripts/gretel.trails.jsuri.js.coffee
169
+ - lib/generators/gretel/USAGE
170
+ - lib/generators/gretel/install_generator.rb
171
+ - lib/generators/gretel/templates/breadcrumbs.rb
172
+ - lib/generators/gretel/templates/trail_migration.rb
173
+ - lib/generators/gretel/trail/migration_generator.rb
141
174
  - lib/gretel-trails.rb
142
175
  - lib/gretel/trails.rb
143
176
  - lib/gretel/trails/engine.rb
177
+ - lib/gretel/trails/patches.rb
178
+ - lib/gretel/trails/patches/gretel.rb
179
+ - lib/gretel/trails/patches/renderer.rb
180
+ - lib/gretel/trails/patches/view_helpers.rb
181
+ - lib/gretel/trails/stores.rb
182
+ - lib/gretel/trails/stores/active_record_store.rb
183
+ - lib/gretel/trails/stores/redis_store.rb
184
+ - lib/gretel/trails/stores/store.rb
185
+ - lib/gretel/trails/stores/url_store.rb
144
186
  - lib/gretel/trails/strategies/hidden_strategy.rb
187
+ - lib/gretel/trails/tasks.rb
145
188
  - lib/gretel/trails/version.rb
146
189
  - test/dummy/README.rdoc
147
190
  - test/dummy/Rakefile
@@ -193,7 +236,12 @@ files:
193
236
  - test/dummy/test/fixtures/categories.yml
194
237
  - test/dummy/test/fixtures/products.yml
195
238
  - test/gretel_trails_test.rb
239
+ - test/stores/active_record_store_test.rb
240
+ - test/stores/redis_store_test.rb
241
+ - test/stores/url_store_test.rb
196
242
  - test/test_helper.rb
243
+ - test/trails_test.rb
244
+ - test/view_helpers_test.rb
197
245
  homepage: https://github.com/lassebunk/gretel-trails
198
246
  licenses:
199
247
  - MIT
@@ -269,4 +317,9 @@ test_files:
269
317
  - test/dummy/test/fixtures/categories.yml
270
318
  - test/dummy/test/fixtures/products.yml
271
319
  - test/gretel_trails_test.rb
320
+ - test/stores/active_record_store_test.rb
321
+ - test/stores/redis_store_test.rb
322
+ - test/stores/url_store_test.rb
272
323
  - test/test_helper.rb
324
+ - test/trails_test.rb
325
+ - test/view_helpers_test.rb