gretel 3.0.0.beta2 → 3.0.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|