gretel 3.0.0.beta2 → 3.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -0
- data/README.md +9 -9
- data/Rakefile +1 -1
- data/gretel.gemspec +3 -0
- data/lib/generators/gretel/install_generator.rb +1 -1
- data/lib/generators/gretel/templates/trail_migration.rb +11 -0
- data/lib/generators/gretel/trail/migration_generator.rb +20 -0
- data/lib/gretel.rb +29 -0
- data/lib/gretel/link.rb +8 -0
- data/lib/gretel/renderer.rb +13 -4
- data/lib/gretel/trail.rb +43 -44
- data/lib/gretel/trail/stores.rb +4 -0
- data/lib/gretel/trail/stores/active_record_store.rb +59 -0
- data/lib/gretel/trail/stores/redis_store.rb +54 -0
- data/lib/gretel/trail/stores/store.rb +49 -0
- data/lib/gretel/trail/stores/url_store.rb +57 -0
- data/lib/gretel/trail/tasks.rb +5 -0
- data/lib/gretel/version.rb +1 -1
- data/test/dummy/db/migrate/20131015194052_create_gretel_trails.rb +11 -0
- data/test/dummy/db/schema.rb +16 -7
- data/test/gretel_test.rb +8 -0
- data/test/helper_methods_test.rb +11 -1
- data/test/test_helper.rb +1 -0
- data/test/trails/active_record_store_test.rb +52 -0
- data/test/trails/redis_store_test.rb +35 -0
- data/test/trails/trail_test.rb +27 -0
- data/test/{trail_test.rb → trails/url_store_test.rb} +13 -6
- metadata +62 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6dea6327b14a40f5f88c344548da90536778701
|
4
|
+
data.tar.gz: c527f98356d7ca6febb761ea239c030d3cfc6365
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e99745536c8bae608186322664689156e303f569961b1086278da7d66cf7ea2cdbcd84565eef961d6ab966d6044988057861656f7c6cec5b35e5a2905dfa53f6
|
7
|
+
data.tar.gz: e3eb531c6e4ac7e1a9e9c0d2e914f9b1321bdbb48f9aead24ff9a611fb3e5a4c93e6a26974c7fabd0529da3d6a89dbc7958af3f12449ad559c3b223f54610dc0
|
data/CHANGELOG.md
CHANGED
@@ -10,6 +10,7 @@ Version 3.0
|
|
10
10
|
* Breadcrumbs rendering is now done in a separate class to unclutter the view with helpers. The public API is still the same.
|
11
11
|
* Support for rendering the breadcrumbs in different styles like ul- and ol lists, and for use with [Twitter Bootstrap](http://getbootstrap.com/). See the `:style` option in the readme for more info.
|
12
12
|
* The `:show_root_alone` option is now called `:display_single_fragment` and can be used to display the breadcrumbs only when there are more than one link, also if it is not the root breadcrumb.
|
13
|
+
* Links yielded from `<%= breadcrumbs do |links| %>` now have a `current?` helper that returns true if the link is the last in the trail.
|
13
14
|
|
14
15
|
Version 2.1
|
15
16
|
-----------
|
data/README.md
CHANGED
@@ -7,15 +7,15 @@ Gretel also supports [semantic breadcrumbs](http://support.google.com/webmasters
|
|
7
7
|
|
8
8
|
Have fun! And please do write, if you (dis)like it – [lassebunk@gmail.com](mailto:lassebunk@gmail.com).
|
9
9
|
|
10
|
-
New in version 3.0
|
11
|
-
|
10
|
+
New in version 3.0 :muscle:
|
11
|
+
---------------------------
|
12
12
|
|
13
13
|
* You can now set trails via the URL – `params[:trail]`. This makes it possible to link back to a different breadcrumb trail than the one specified in your breadcrumb,
|
14
|
-
for example if you have a store with products that have a default parent to their category, but when
|
14
|
+
for example if you have a 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.
|
15
15
|
Read more about trails below.
|
16
16
|
* Breadcrumbs can now be rendered in different styles like ul- and ol lists, and for use with the [Twitter Bootstrap](http://getbootstrap.com/) framework. See the `:style` option below for more info.
|
17
17
|
* Defining breadcrumbs using `Gretel::Crumbs.layout do ... end` in an initializer has been removed. See below for details on how to upgrade.
|
18
|
-
* The `:show_root_alone` option is now called `:display_single_fragment` and can be used to hide the breadcrumbs when there
|
18
|
+
* The `:show_root_alone` option is now called `:display_single_fragment` and can be used to hide the breadcrumbs when there is only one link, also if it is not the root breadcrumb.
|
19
19
|
The old `:show_root_alone` option is still supported until Gretel version 4.0 and will show a deprecation warning when it's used.
|
20
20
|
|
21
21
|
I hope you find these changes as useful as I did when I made them – if you have more suggestions, please create an [Issue](https://github.com/lassebunk/gretel/issues) or [Pull Request](https://github.com/lassebunk/gretel/pulls).
|
@@ -224,7 +224,7 @@ If you supply a block to the `breadcrumbs` method, it will yield an array with t
|
|
224
224
|
<% if links.any? %>
|
225
225
|
You are here:
|
226
226
|
<% links.each do |link| %>
|
227
|
-
<%= link_to link.text, link.url %> (<%= link.key %>)
|
227
|
+
<%= link_to link.text, link.url, class: (link.current? ? "current" : nil) %> (<%= link.key %>)
|
228
228
|
<% end %>
|
229
229
|
<% end %>
|
230
230
|
<% end %>
|
@@ -235,7 +235,7 @@ Setting breadcrumb trails
|
|
235
235
|
|
236
236
|
You can set a breadcrumb trail via `params[:trail]`. This makes it possible to link back to a different breadcrumb trail than the one specified in your breadcrumb.
|
237
237
|
|
238
|
-
An example is if you have a store with products that have a default parent to their category, but when
|
238
|
+
An example is if you have a 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.
|
239
239
|
|
240
240
|
### Initial setup
|
241
241
|
|
@@ -252,7 +252,7 @@ This will create an initializer in *config/initializers/gretel.rb* that will con
|
|
252
252
|
If you want to do it manually, you can put the following in *config/initializers/gretel.rb*:
|
253
253
|
|
254
254
|
```
|
255
|
-
Gretel::Trail.secret = 'your_key_here' # Must be changed to something else to be secure
|
255
|
+
Gretel::Trail::UrlStore.secret = 'your_key_here' # Must be changed to something else to be secure
|
256
256
|
```
|
257
257
|
|
258
258
|
You can generate a key using `SecureRandom.hex(64)`.
|
@@ -287,9 +287,9 @@ Please use the trail functionality with care; the trails can get very long.
|
|
287
287
|
Nice to know
|
288
288
|
------------
|
289
289
|
|
290
|
-
### Access to view
|
290
|
+
### Access to view methods
|
291
291
|
|
292
|
-
When configuring breadcrumbs, you have access to all
|
292
|
+
When configuring breadcrumbs inside a `crumb :xx do ... end` block, you have access to all methods that are normally accessible in the view where the breadcrumbs are inserted. This includes your view helpers, `params`, `request`, etc.
|
293
293
|
|
294
294
|
### Using multiple breadcrumb configuration files
|
295
295
|
|
data/Rakefile
CHANGED
data/gretel.gemspec
CHANGED
@@ -17,6 +17,9 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^test/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
+
gem.add_dependency "rails", ">= 3.2.0"
|
20
21
|
gem.add_development_dependency "rails", "~> 3.2.13"
|
21
22
|
gem.add_development_dependency "sqlite3"
|
23
|
+
gem.add_development_dependency "fakeredis", "~> 0.4.2"
|
24
|
+
gem.add_development_dependency "timecop", "~> 0.6.3"
|
22
25
|
end
|
@@ -12,7 +12,7 @@ module Gretel
|
|
12
12
|
desc "Creates an initializer with trail secret"
|
13
13
|
def create_initializer
|
14
14
|
initializer "gretel.rb" do
|
15
|
-
%{Gretel::Trail.secret = '#{SecureRandom.hex(64)}'}
|
15
|
+
%{Gretel::Trail::UrlStore.secret = '#{SecureRandom.hex(64)}'}
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -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.rb
CHANGED
@@ -19,6 +19,26 @@ module Gretel
|
|
19
19
|
@breadcrumb_paths = paths
|
20
20
|
end
|
21
21
|
|
22
|
+
# Param to contain trail. See +Gretel::Trail.trail_param+ for details.
|
23
|
+
def trail_param
|
24
|
+
Gretel::Trail.trail_param
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the trail param. See +Gretel::Trail.trail_param+ for details.
|
28
|
+
def trail_param=(param)
|
29
|
+
Gretel::Trail.trail_param = param
|
30
|
+
end
|
31
|
+
|
32
|
+
# Trail store. See +Gretel::Trail.store+ for details.
|
33
|
+
def trail_store
|
34
|
+
Gretel::Trail.store
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets the trail store. See +Gretel::Trail.store+ for details.
|
38
|
+
def trail_store=(store)
|
39
|
+
Gretel::Trail.store = store
|
40
|
+
end
|
41
|
+
|
22
42
|
# Whether to suppress deprecation warnings.
|
23
43
|
def suppress_deprecation_warnings?
|
24
44
|
!!@suppress_deprecation_warnings
|
@@ -45,6 +65,15 @@ module Gretel
|
|
45
65
|
# Sets the Rails environment names with automatic configuration reload. Default is +["development"]+.
|
46
66
|
attr_writer :reload_environments
|
47
67
|
|
68
|
+
# Yields this +Gretel+ to be configured.
|
69
|
+
#
|
70
|
+
# Gretel.configure do |config|
|
71
|
+
# config.trail_param = :other_param
|
72
|
+
# end
|
73
|
+
def configure
|
74
|
+
yield self
|
75
|
+
end
|
76
|
+
|
48
77
|
# Resets all changes made to +Gretel+, +Gretel::Crumbs+, and +Gretel::Trail+. Used for testing.
|
49
78
|
def reset!
|
50
79
|
instance_variables.each { |var| remove_instance_variable var }
|
data/lib/gretel/link.rb
CHANGED
data/lib/gretel/renderer.rb
CHANGED
@@ -117,6 +117,9 @@ module Gretel
|
|
117
117
|
# Get trail
|
118
118
|
links.unshift *trail_for(crumb)
|
119
119
|
|
120
|
+
# Set last link to current
|
121
|
+
links.last.try(:current!)
|
122
|
+
|
120
123
|
links
|
121
124
|
else
|
122
125
|
[]
|
@@ -169,10 +172,10 @@ module Gretel
|
|
169
172
|
def render_semantic_fragment(fragment_tag, text, url, options = {})
|
170
173
|
if fragment_tag
|
171
174
|
text = content_tag(:span, text, itemprop: "title")
|
172
|
-
text =
|
175
|
+
text = render_link(text, url, itemprop: "url") if url.present?
|
173
176
|
content_tag(fragment_tag, text, class: options[:class], itemscope: "", itemtype: "http://data-vocabulary.org/Breadcrumb")
|
174
177
|
elsif url.present?
|
175
|
-
content_tag(:div,
|
178
|
+
content_tag(:div, render_link(content_tag(:span, text, itemprop: "title"), url, class: options[:class], itemprop: "url"), itemscope: "", itemtype: "http://data-vocabulary.org/Breadcrumb")
|
176
179
|
else
|
177
180
|
content_tag(:div, content_tag(:span, text, class: options[:class], itemprop: "title"), itemscope: "", itemtype: "http://data-vocabulary.org/Breadcrumb")
|
178
181
|
end
|
@@ -181,10 +184,10 @@ module Gretel
|
|
181
184
|
# Renders regular, non-semantic fragment HTML.
|
182
185
|
def render_nonsemantic_fragment(fragment_tag, text, url, options = {})
|
183
186
|
if fragment_tag
|
184
|
-
text =
|
187
|
+
text = render_link(text, url) if url.present?
|
185
188
|
content_tag(fragment_tag, text, class: options[:class])
|
186
189
|
elsif url.present?
|
187
|
-
|
190
|
+
render_link(text, url, class: options[:class])
|
188
191
|
elsif options[:class].present?
|
189
192
|
content_tag(:span, text, class: options[:class])
|
190
193
|
else
|
@@ -192,6 +195,12 @@ module Gretel
|
|
192
195
|
end
|
193
196
|
end
|
194
197
|
|
198
|
+
# Renders a link. It is really just a proxy for +link_to+, but this can be
|
199
|
+
# used in plugins that want to change how links are rendered.
|
200
|
+
def render_link(name, url, options = {})
|
201
|
+
link_to(name, url, options)
|
202
|
+
end
|
203
|
+
|
195
204
|
# Proxy to view context
|
196
205
|
def method_missing(method, *args, &block)
|
197
206
|
context.send(method, *args, &block)
|
data/lib/gretel/trail.rb
CHANGED
@@ -1,32 +1,53 @@
|
|
1
|
+
require "gretel/trail/stores"
|
2
|
+
require "gretel/trail/tasks"
|
3
|
+
|
1
4
|
module Gretel
|
2
5
|
module Trail
|
6
|
+
STORES = {
|
7
|
+
url: UrlStore,
|
8
|
+
db: ActiveRecordStore,
|
9
|
+
redis: RedisStore
|
10
|
+
}
|
11
|
+
|
3
12
|
class << self
|
4
|
-
#
|
5
|
-
#
|
6
|
-
|
7
|
-
|
13
|
+
# Gets the store that is used to encode and decode trails.
|
14
|
+
# Default: +Gretel::Trail::UrlStore+
|
15
|
+
def store
|
16
|
+
@store ||= UrlStore
|
17
|
+
end
|
8
18
|
|
9
|
-
#
|
19
|
+
# Sets the store that is used to encode and decode trails.
|
20
|
+
# Can be a subclass of +Gretel::Trail::Store+, or a symbol: +:url+, +:db+, or +:redis+.
|
21
|
+
def store=(value)
|
22
|
+
if value.is_a?(Symbol)
|
23
|
+
klass = STORES[value]
|
24
|
+
raise ArgumentError, "Unknown Gretel::Trail.store #{value.inspect}. Use any of #{STORES.inspect}." unless klass
|
25
|
+
self.store = klass
|
26
|
+
else
|
27
|
+
@store = value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Uses the store to encode an array of links to a unique key that can be used in URLs.
|
10
32
|
def encode(links)
|
11
|
-
|
12
|
-
|
33
|
+
store.encode(links)
|
34
|
+
end
|
13
35
|
|
14
|
-
|
36
|
+
# Uses the store to decode a unique key to an array of links.
|
37
|
+
def decode(key)
|
38
|
+
store.decode(key)
|
15
39
|
end
|
16
40
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
41
|
+
# Deletes expired keys from the store.
|
42
|
+
# Not all stores support expiring keys, and will raise an exception if they don't.
|
43
|
+
def delete_expired
|
44
|
+
store.delete_expired
|
45
|
+
end
|
20
46
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
decode_base64(base64)
|
26
|
-
else
|
27
|
-
Rails.logger.info "[Gretel] Trail decode failed: Invalid hash '#{hash}' in trail"
|
28
|
-
[]
|
29
|
-
end
|
47
|
+
# Returns the current number of trails in the store.
|
48
|
+
# Not all stores support counting keys, and will raise an exception if they don't.
|
49
|
+
def count
|
50
|
+
store.key_count
|
30
51
|
end
|
31
52
|
|
32
53
|
# Name of trail param. Default: +:trail+.
|
@@ -34,35 +55,13 @@ module Gretel
|
|
34
55
|
@trail_param ||= :trail
|
35
56
|
end
|
36
57
|
|
58
|
+
# Sets the trail param.
|
37
59
|
attr_writer :trail_param
|
38
60
|
|
39
61
|
# Resets all changes made to +Gretel::Trail+. Used for testing.
|
40
62
|
def reset!
|
41
63
|
instance_variables.each { |var| remove_instance_variable var }
|
42
|
-
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
# Encodes links array to Base64, internally using JSON for serialization.
|
47
|
-
def encode_base64(links)
|
48
|
-
arr = links.map { |link| [link.key, link.text, (link.text.html_safe? ? 1 : 0), link.url] }
|
49
|
-
Base64.urlsafe_encode64(arr.to_json)
|
50
|
-
end
|
51
|
-
|
52
|
-
# Decodes links array from Base64.
|
53
|
-
def decode_base64(base64)
|
54
|
-
json = Base64.urlsafe_decode64(base64)
|
55
|
-
arr = JSON.parse(json)
|
56
|
-
arr.map { |key, text, html_safe, url| Link.new(key.to_sym, (html_safe == 1 ? text.html_safe : text), url) }
|
57
|
-
rescue
|
58
|
-
Rails.logger.info "[Gretel] Trail decode failed: Invalid Base64 '#{base64}' in trail"
|
59
|
-
[]
|
60
|
-
end
|
61
|
-
|
62
|
-
# Generates a salted hash of +base64+.
|
63
|
-
def generate_hash(base64)
|
64
|
-
raise "Gretel::Trail.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?
|
65
|
-
Digest::SHA1.hexdigest([base64, secret].join)
|
64
|
+
STORES.each_value(&:reset!)
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Gretel
|
2
|
+
module Trail
|
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
|
+
end
|
37
|
+
|
38
|
+
class GretelTrail < ActiveRecord::Base
|
39
|
+
serialize :value, Array
|
40
|
+
|
41
|
+
def self.get(key)
|
42
|
+
find_by_key(key).try(:value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.set(key, value, expires_at)
|
46
|
+
find_or_initialize_by_key(key).tap do |rec|
|
47
|
+
rec.value = value
|
48
|
+
rec.expires_at = expires_at
|
49
|
+
rec.save
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.delete_expired
|
54
|
+
delete_all(["expires_at < ?", Time.now])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Gretel
|
2
|
+
module Trail
|
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,49 @@
|
|
1
|
+
module Gretel
|
2
|
+
module Trail
|
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
|
+
# Encode array of +links+ to unique trail key.
|
28
|
+
def encode(links)
|
29
|
+
arr = links.map { |link| [link.key, link.text, (link.text.html_safe? ? 1 : 0), link.url] }
|
30
|
+
save(arr)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Decode unique trail key to array of links.
|
34
|
+
def decode(key)
|
35
|
+
if arr = retrieve(key)
|
36
|
+
arr.map { |key, text, html_safe, url| Link.new(key.to_sym, (html_safe == 1 ? text.html_safe : text), url) }
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Resets all changes made to the store. Used for testing.
|
43
|
+
def reset!
|
44
|
+
instance_variables.each { |var| remove_instance_variable var }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Gretel
|
2
|
+
module Trail
|
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
|
data/lib/gretel/version.rb
CHANGED
@@ -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
|
data/test/dummy/db/schema.rb
CHANGED
@@ -11,19 +11,28 @@
|
|
11
11
|
#
|
12
12
|
# It's strongly recommended to check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(version
|
14
|
+
ActiveRecord::Schema.define(:version => 20131015194052) do
|
15
15
|
|
16
|
-
create_table "
|
16
|
+
create_table "gretel_trails", :force => true do |t|
|
17
|
+
t.string "key", :limit => 40
|
18
|
+
t.text "value"
|
19
|
+
t.datetime "expires_at"
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index "gretel_trails", ["expires_at"], :name => "index_gretel_trails_on_expires_at"
|
23
|
+
add_index "gretel_trails", ["key"], :name => "index_gretel_trails_on_key", :unique => true
|
24
|
+
|
25
|
+
create_table "issues", :force => true do |t|
|
17
26
|
t.string "title"
|
18
27
|
t.integer "project_id"
|
19
|
-
t.datetime "created_at", null
|
20
|
-
t.datetime "updated_at", null
|
28
|
+
t.datetime "created_at", :null => false
|
29
|
+
t.datetime "updated_at", :null => false
|
21
30
|
end
|
22
31
|
|
23
|
-
create_table "projects", force
|
32
|
+
create_table "projects", :force => true do |t|
|
24
33
|
t.string "name"
|
25
|
-
t.datetime "created_at", null
|
26
|
-
t.datetime "updated_at", null
|
34
|
+
t.datetime "created_at", :null => false
|
35
|
+
t.datetime "updated_at", :null => false
|
27
36
|
end
|
28
37
|
|
29
38
|
end
|
data/test/gretel_test.rb
CHANGED
@@ -11,4 +11,12 @@ class GretelTest < ActiveSupport::TestCase
|
|
11
11
|
assert_equal ["development"], Gretel.reload_environments
|
12
12
|
assert !Gretel.suppress_deprecation_warnings?
|
13
13
|
end
|
14
|
+
|
15
|
+
test "configuration block" do
|
16
|
+
Gretel.configure do |config|
|
17
|
+
config.trail_param = :other_param
|
18
|
+
end
|
19
|
+
|
20
|
+
assert_equal :other_param, Gretel.trail_param
|
21
|
+
end
|
14
22
|
end
|
data/test/helper_methods_test.rb
CHANGED
@@ -7,7 +7,7 @@ class HelperMethodsTest < ActionView::TestCase
|
|
7
7
|
|
8
8
|
setup do
|
9
9
|
Gretel.reset!
|
10
|
-
Gretel::Trail.secret = "128107d341e912db791d98bbe874a8250f784b0a0b4dbc5d5032c0fc1ca7bda9c6ece667bd18d23736ee833ea79384176faeb54d2e0d21012898dde78631cdf1"
|
10
|
+
Gretel::Trail::UrlStore.secret = "128107d341e912db791d98bbe874a8250f784b0a0b4dbc5d5032c0fc1ca7bda9c6ece667bd18d23736ee833ea79384176faeb54d2e0d21012898dde78631cdf1"
|
11
11
|
end
|
12
12
|
|
13
13
|
# Breadcrumb generation
|
@@ -153,6 +153,16 @@ class HelperMethodsTest < ActionView::TestCase
|
|
153
153
|
[:multiple_links_with_parent, "Contact form", "/about/contact/form"]], out
|
154
154
|
end
|
155
155
|
|
156
|
+
test "sets current on last link in array" do
|
157
|
+
breadcrumb :multiple_links_with_parent
|
158
|
+
|
159
|
+
out = breadcrumbs do |links|
|
160
|
+
links.map(&:current?)
|
161
|
+
end
|
162
|
+
|
163
|
+
assert_equal [false, false, false, true], out
|
164
|
+
end
|
165
|
+
|
156
166
|
test "without link" do
|
157
167
|
breadcrumb :without_link
|
158
168
|
assert_equal %{<div class="breadcrumbs"><a href="/">Home</a> › Also without link › <span class="current">Without link</span></div>},
|
data/test/test_helper.rb
CHANGED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ActiveRecordStoreTest < ActiveSupport::TestCase
|
4
|
+
setup do
|
5
|
+
Gretel.reset!
|
6
|
+
Gretel::Trail.store = :db
|
7
|
+
|
8
|
+
@links = [
|
9
|
+
[:root, "Home", "/"],
|
10
|
+
[:store, "Store <b>Test</b>".html_safe, "/store"],
|
11
|
+
[:search, "Search", "/store/search?q=test"]
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
test "defaults" do
|
16
|
+
assert_equal 1.day, Gretel::Trail::ActiveRecordStore.expires_in
|
17
|
+
end
|
18
|
+
|
19
|
+
test "encoding" do
|
20
|
+
assert_equal "684c211441e72225cee89477a2d1f59e657c9e26",
|
21
|
+
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
22
|
+
end
|
23
|
+
|
24
|
+
test "decoding" do
|
25
|
+
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
26
|
+
decoded = Gretel::Trail.decode("684c211441e72225cee89477a2d1f59e657c9e26")
|
27
|
+
assert_equal @links, decoded.map { |link| [link.key, link.text, link.url] }
|
28
|
+
assert_equal [false, true, false], decoded.map { |link| link.text.html_safe? }
|
29
|
+
end
|
30
|
+
|
31
|
+
test "invalid trail" do
|
32
|
+
assert_equal [], Gretel::Trail.decode("asdgasdg")
|
33
|
+
end
|
34
|
+
|
35
|
+
test "delete expired" do
|
36
|
+
10.times { Gretel::Trail.encode([Gretel::Link.new(:test, SecureRandom.hex(20), "/test")]) }
|
37
|
+
assert_equal 10, Gretel::Trail.count
|
38
|
+
|
39
|
+
Gretel::Trail.delete_expired
|
40
|
+
assert_equal 10, Gretel::Trail.count
|
41
|
+
|
42
|
+
Timecop.travel(14.hours.from_now) do
|
43
|
+
5.times { Gretel::Trail.encode([Gretel::Link.new(:test, SecureRandom.hex(20), "/test")]) }
|
44
|
+
assert_equal 15, Gretel::Trail.count
|
45
|
+
end
|
46
|
+
|
47
|
+
Timecop.travel(25.hours.from_now) do
|
48
|
+
Gretel::Trail.delete_expired
|
49
|
+
assert_equal 5, Gretel::Trail.count
|
50
|
+
end
|
51
|
+
end
|
52
|
+
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::Trail.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::Trail::RedisStore.expires_in
|
18
|
+
end
|
19
|
+
|
20
|
+
test "encoding" do
|
21
|
+
assert_equal "684c211441e72225cee89477a2d1f59e657c9e26",
|
22
|
+
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
23
|
+
end
|
24
|
+
|
25
|
+
test "decoding" do
|
26
|
+
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
27
|
+
decoded = Gretel::Trail.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::Trail.decode("asdgasdg")
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TrailTest < ActiveSupport::TestCase
|
4
|
+
setup do
|
5
|
+
Gretel.reset!
|
6
|
+
end
|
7
|
+
|
8
|
+
test "defaults" do
|
9
|
+
assert_equal :trail, Gretel::Trail.trail_param
|
10
|
+
end
|
11
|
+
|
12
|
+
test "setting invalid store" do
|
13
|
+
assert_raises ArgumentError do
|
14
|
+
Gretel::Trail.store = :xx
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
test "setting store options on main module" do
|
19
|
+
assert_equal :trail, Gretel.trail_param
|
20
|
+
Gretel.trail_param = :other_param
|
21
|
+
assert_equal :other_param, Gretel::Trail.trail_param
|
22
|
+
|
23
|
+
assert_equal Gretel::Trail::UrlStore, Gretel.trail_store
|
24
|
+
Gretel.trail_store = :redis
|
25
|
+
assert_equal Gretel::Trail::RedisStore, Gretel::Trail.store
|
26
|
+
end
|
27
|
+
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class
|
3
|
+
class UrlStoreTest < ActiveSupport::TestCase
|
4
4
|
setup do
|
5
|
-
Gretel
|
5
|
+
Gretel.reset!
|
6
|
+
|
7
|
+
Gretel::Trail.store = :url
|
8
|
+
Gretel::Trail::UrlStore.secret = "128107d341e912db791d98bbe874a8250f784b0a0b4dbc5d5032c0fc1ca7bda9c6ece667bd18d23736ee833ea79384176faeb54d2e0d21012898dde78631cdf1"
|
9
|
+
|
6
10
|
@links = [
|
7
11
|
[:root, "Home", "/"],
|
8
12
|
[:store, "Store <b>Test</b>".html_safe, "/store"],
|
@@ -10,10 +14,6 @@ class TrailTest < ActiveSupport::TestCase
|
|
10
14
|
]
|
11
15
|
end
|
12
16
|
|
13
|
-
test "defaults" do
|
14
|
-
assert_equal :trail, Gretel::Trail.trail_param
|
15
|
-
end
|
16
|
-
|
17
17
|
test "encoding" do
|
18
18
|
assert_equal "5543214e6d7bbc3ba5209b2362cd7513d500f61b_W1sicm9vdCIsIkhvbWUiLDAsIi8iXSxbInN0b3JlIiwiU3RvcmUgPGI-VGVzdDwvYj4iLDEsIi9zdG9yZSJdLFsic2VhcmNoIiwiU2VhcmNoIiwwLCIvc3RvcmUvc2VhcmNoP3E9dGVzdCJdXQ==",
|
19
19
|
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
@@ -28,4 +28,11 @@ class TrailTest < ActiveSupport::TestCase
|
|
28
28
|
test "invalid trail" do
|
29
29
|
assert_equal [], Gretel::Trail.decode("28f104524f5eaf6b3bd035710432fd2b9cbfd62c_X1sicm9vdCIsIkhvbWUiLDAsIi8iXSxbInN0b3JlIiwiU3RvcmUiLDAsIi9zdG9yZSJdLFsic2VhcmNoIiwiU2VhcmNoIiwwLCIvc3RvcmUvc2VhcmNoP3E9dGVzdCJdXQ==")
|
30
30
|
end
|
31
|
+
|
32
|
+
test "raises error if no secret set" do
|
33
|
+
Gretel::Trail::UrlStore.secret = nil
|
34
|
+
assert_raises RuntimeError do
|
35
|
+
Gretel::Trail.encode(@links.map { |key, text, url| Gretel::Link.new(key, text, url) })
|
36
|
+
end
|
37
|
+
end
|
31
38
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gretel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.0.
|
4
|
+
version: 3.0.0.beta3
|
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-
|
11
|
+
date: 2013-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.0
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rails
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,34 @@ dependencies:
|
|
38
52
|
- - '>='
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fakeredis
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.4.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.4.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.6.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.6.3
|
41
83
|
description: Gretel is a Ruby on Rails plugin that makes it easy yet flexible to create
|
42
84
|
breadcrumbs.
|
43
85
|
email:
|
@@ -57,6 +99,8 @@ files:
|
|
57
99
|
- lib/generators/gretel/USAGE
|
58
100
|
- lib/generators/gretel/install_generator.rb
|
59
101
|
- lib/generators/gretel/templates/breadcrumbs.rb
|
102
|
+
- lib/generators/gretel/templates/trail_migration.rb
|
103
|
+
- lib/generators/gretel/trail/migration_generator.rb
|
60
104
|
- lib/gretel.rb
|
61
105
|
- lib/gretel/crumb.rb
|
62
106
|
- lib/gretel/crumbs.rb
|
@@ -64,6 +108,12 @@ files:
|
|
64
108
|
- lib/gretel/link.rb
|
65
109
|
- lib/gretel/renderer.rb
|
66
110
|
- lib/gretel/trail.rb
|
111
|
+
- lib/gretel/trail/stores.rb
|
112
|
+
- lib/gretel/trail/stores/active_record_store.rb
|
113
|
+
- lib/gretel/trail/stores/redis_store.rb
|
114
|
+
- lib/gretel/trail/stores/store.rb
|
115
|
+
- lib/gretel/trail/stores/url_store.rb
|
116
|
+
- lib/gretel/trail/tasks.rb
|
67
117
|
- lib/gretel/version.rb
|
68
118
|
- lib/gretel/view_helpers.rb
|
69
119
|
- test/deprecated_test.rb
|
@@ -96,6 +146,7 @@ files:
|
|
96
146
|
- test/dummy/config/routes.rb
|
97
147
|
- test/dummy/db/migrate/20130122163007_create_projects.rb
|
98
148
|
- test/dummy/db/migrate/20130122163051_create_issues.rb
|
149
|
+
- test/dummy/db/migrate/20131015194052_create_gretel_trails.rb
|
99
150
|
- test/dummy/db/schema.rb
|
100
151
|
- test/dummy/lib/assets/.gitkeep
|
101
152
|
- test/dummy/log/.gitkeep
|
@@ -109,7 +160,10 @@ files:
|
|
109
160
|
- test/gretel_test.rb
|
110
161
|
- test/helper_methods_test.rb
|
111
162
|
- test/test_helper.rb
|
112
|
-
- test/
|
163
|
+
- test/trails/active_record_store_test.rb
|
164
|
+
- test/trails/redis_store_test.rb
|
165
|
+
- test/trails/trail_test.rb
|
166
|
+
- test/trails/url_store_test.rb
|
113
167
|
homepage: http://github.com/lassebunk/gretel
|
114
168
|
licenses:
|
115
169
|
- MIT
|
@@ -165,6 +219,7 @@ test_files:
|
|
165
219
|
- test/dummy/config/routes.rb
|
166
220
|
- test/dummy/db/migrate/20130122163007_create_projects.rb
|
167
221
|
- test/dummy/db/migrate/20130122163051_create_issues.rb
|
222
|
+
- test/dummy/db/migrate/20131015194052_create_gretel_trails.rb
|
168
223
|
- test/dummy/db/schema.rb
|
169
224
|
- test/dummy/lib/assets/.gitkeep
|
170
225
|
- test/dummy/log/.gitkeep
|
@@ -178,4 +233,7 @@ test_files:
|
|
178
233
|
- test/gretel_test.rb
|
179
234
|
- test/helper_methods_test.rb
|
180
235
|
- test/test_helper.rb
|
181
|
-
- test/
|
236
|
+
- test/trails/active_record_store_test.rb
|
237
|
+
- test/trails/redis_store_test.rb
|
238
|
+
- test/trails/trail_test.rb
|
239
|
+
- test/trails/url_store_test.rb
|