repost 0.5.3 → 0.6.0
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 +35 -0
- data/README.md +11 -2
- data/lib/repost/extend_controller.rb +22 -30
- data/lib/repost/senpai.rb +60 -63
- data/lib/repost/version.rb +1 -1
- data/lib/repost.rb +6 -5
- metadata +25 -4
- data/Rakefile +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13ce32e38cce21aa7a91ca98660f2aa14472e8a909cb038c8d232d4b3ebcdc11
|
|
4
|
+
data.tar.gz: 1dc85d08c2671b04b7c6b59e87f5f56ef70680cce4e180f8c1251af23645b717
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50a8cde0aa5fbc95f090b53be99062f2faaa02a28aa3f49395fd7f70b53005c22e47a72bcb21b37becab3431b3a365607491514d9e76d59d3eff612ce3536820
|
|
7
|
+
data.tar.gz: 6edb7e357589700615ee12ac0f6b7582d8bfc6d3c06ce18d83d11e8663538f86126f291d7d68007f12711a66cece65e08782ae4213298ed3317ba2b7de001eed
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- The target URL must be `http`, `https` or relative. `javascript:`, `data:` and other schemes now raise `Repost::UnsafeUrlError`, because escaping can't make them safe. Allow more schemes with `options: { allowed_schemes: %w[https myapp] }`.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- All markup is built by one helper that escapes every attribute value, so a value can't be left unescaped by mistake. The output is the same apart from formatting: the autosubmit script is on one line, and the CSRF input lists `type` first.
|
|
10
|
+
- The Rails helpers now live in `Repost::ExtendController`, included into `ActionController::Base` through `on_load(:action_controller_base)`. Before, the gem reopened `ActionController::Base` from a hook that also ran for API controllers.
|
|
11
|
+
- No longer requires the `cgi` library, which prints a warning on Ruby 4.0.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Rails integration specs, run on Rails 7.1 through 8.x.
|
|
15
|
+
|
|
16
|
+
## 0.5.3 (2026-11-05)
|
|
17
|
+
- Fixed autosubmit failing when a param is named `submit`: the hidden input shadowed `form.submit`, leaving the user on a blank page.
|
|
18
|
+
- Fixed autosubmit failing when `form_id` contains `"`, `&`, `<` or `>`.
|
|
19
|
+
- CI moved from Travis to GitHub Actions (Ruby 2.7 to 4.0).
|
|
20
|
+
|
|
21
|
+
## 0.5.2 (2026-07-03)
|
|
22
|
+
- `method: :put`, `:patch` and `:delete` are sent as POST with a hidden `_method` field (Rails-style override).
|
|
23
|
+
- An explicit `autosubmit_nonce`, including `nil`, overrides the Rails CSP nonce. The Rails helper no longer modifies the options hash passed to it.
|
|
24
|
+
- `class` attributes are left out when no decor classes are set. Generated form ids are random instead of timestamp-based.
|
|
25
|
+
|
|
26
|
+
## 0.5.1 (2026-01-28)
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
- Fixed cross-site scripting ([#29](https://github.com/vergilet/repost/issues/29)): the URL, param names and values, and all options are now HTML-escaped. All earlier versions are affected; please upgrade.
|
|
30
|
+
|
|
31
|
+
## 0.5.0 (2026-01-28)
|
|
32
|
+
- Attempted fix for [#29](https://github.com/vergilet/repost/issues/29) by sanitizing the output of the Rails helper. This also stripped the form, so `repost` and `redirect_post` stopped working in Rails. Use 0.5.1 or later.
|
|
33
|
+
|
|
34
|
+
## 0.4.2 and earlier
|
|
35
|
+
See the [commit history](https://github.com/vergilet/repost/commits/master).
|
data/README.md
CHANGED
|
@@ -50,8 +50,8 @@ I faced with this problem when was dealing with bank transactions. You can see t
|
|
|
50
50
|
<img src="https://user-images.githubusercontent.com/2478436/55143646-d0da3500-5147-11e9-91a3-1bac9d560fb2.png"></a>
|
|
51
51
|
</p>
|
|
52
52
|
|
|
53
|
-
> **P.S. The `repost` gem was initially created
|
|
54
|
-
|
|
53
|
+
> **P.S. The `repost` gem was initially created for the redirect required by [Adyen 3D Secure 1](https://docs.adyen.com/online-payments/classic-integrations/api-integration-ecommerce/3d-secure/3d-secure-1/#step-2-redirect-to-the-card-issuer), which meant building an HTML form and submitting it via POST.** <br>
|
|
54
|
+
The same pattern is still needed wherever another site expects the browser to arrive with a POST: payment pages and 3D Secure 2 browser challenges, SAML's HTTP-POST binding, OpenID Connect's `form_post` response mode, and partner logins.
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
## Usage
|
|
@@ -143,6 +143,7 @@ redirect_post('http://examp.io/endpoint', # URL, looks understandable
|
|
|
143
143
|
form_id: 'CustomFormID', # OPTIONAL - DEFAULT is autogenerated
|
|
144
144
|
autosubmit: false, # OPTIONAL - DEFAULT is true, if you want to get a confirmation for redirect
|
|
145
145
|
autosubmit_nonce: '1d3n7i4ier', # RAILS - DEFAULT is content_security_policy_nonce, for pure Ruby - string identifier, more info - https://edgeguides.rubyonrails.org/security.html#content-security-policy
|
|
146
|
+
allowed_schemes: %w[https myapp], # OPTIONAL - DEFAULT is %w[http https]. Relative URLs are always allowed; other schemes raise Repost::UnsafeUrlError
|
|
146
147
|
decor: { # If autosubmit is turned off or Javascript is disabled on client
|
|
147
148
|
section: { # ... you can decorate confirmation section and button
|
|
148
149
|
classes: 'red-bg red-text', # OPTIONAL - <DIV> section, set classNames, separate with space
|
|
@@ -177,6 +178,14 @@ Currently you can pass the **authenticity token** in two ways:
|
|
|
177
178
|
```
|
|
178
179
|
|
|
179
180
|
|
|
181
|
+
## Security
|
|
182
|
+
|
|
183
|
+
- Every value repost writes into the page (URL, param names and values, all options) is HTML-escaped, so it's safe to pass user input. The one exception is `decor: { section: { html: ... } }`, which is inserted as-is by design: never put user input there.
|
|
184
|
+
- Only `http`, `https` and relative target URLs are allowed by default. `javascript:`, `data:` and other schemes raise `Repost::UnsafeUrlError`, because escaping can't make them safe. Use the `allowed_schemes` option to allow others.
|
|
185
|
+
- Versions before **0.5.1** don't escape these values and allow cross-site scripting ([#29](https://github.com/vergilet/repost/issues/29)). Please upgrade.
|
|
186
|
+
|
|
187
|
+
See [CHANGELOG.md](CHANGELOG.md) for what changed in each version.
|
|
188
|
+
|
|
180
189
|
|
|
181
190
|
## License
|
|
182
191
|
The gem is available as open source under the terms of the MIT License.
|
|
@@ -1,36 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
autosubmit_nonce =
|
|
15
|
-
|
|
16
|
-
html_payload = Repost::Senpai.perform(
|
|
17
|
-
url,
|
|
18
|
-
params: params,
|
|
19
|
-
options: options.merge({
|
|
20
|
-
authenticity_token: authenticity_token,
|
|
21
|
-
autosubmit_nonce: autosubmit_nonce
|
|
22
|
-
}.compact)
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
render html: html_payload.html_safe, status: status
|
|
1
|
+
module Repost
|
|
2
|
+
# Adds #repost and #redirect_post to Rails controllers. Included into
|
|
3
|
+
# ActionController::Base when it loads (not into ActionController::API).
|
|
4
|
+
module ExtendController
|
|
5
|
+
def repost(url, params: {}, options: {})
|
|
6
|
+
options = options.dup
|
|
7
|
+
status = options.delete(:status) || :ok
|
|
8
|
+
if ['auto', :auto].include?(options[:authenticity_token])
|
|
9
|
+
options[:authenticity_token] = form_authenticity_token
|
|
10
|
+
end
|
|
11
|
+
# Rails' per-request CSP nonce is the default; an explicitly passed
|
|
12
|
+
# :autosubmit_nonce (even nil, to opt out) takes precedence.
|
|
13
|
+
unless options.key?(:autosubmit_nonce)
|
|
14
|
+
options[:autosubmit_nonce] = content_security_policy_nonce if respond_to?(:content_security_policy_nonce, true)
|
|
26
15
|
end
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
# html_safe is correct here: Senpai escapes every value it inserts
|
|
18
|
+
# (except decor section html, which is raw by design).
|
|
19
|
+
render html: Repost::Senpai.perform(url, params: params, options: options).html_safe, status: status
|
|
29
20
|
end
|
|
21
|
+
|
|
22
|
+
alias redirect_post repost
|
|
30
23
|
end
|
|
31
24
|
end
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# env&.fetch('rack.session', :csrf)
|
|
26
|
+
if defined?(ActiveSupport) && ActiveSupport.respond_to?(:on_load)
|
|
27
|
+
ActiveSupport.on_load(:action_controller_base) { include Repost::ExtendController }
|
|
28
|
+
end
|
data/lib/repost/senpai.rb
CHANGED
|
@@ -1,127 +1,124 @@
|
|
|
1
|
-
require 'cgi'
|
|
2
1
|
require 'json'
|
|
3
2
|
require 'securerandom'
|
|
4
3
|
|
|
5
4
|
module Repost
|
|
5
|
+
# Builds an HTML form that submits itself, so a browser can be "redirected"
|
|
6
|
+
# with a POST body. All markup comes from #tag, which escapes every
|
|
7
|
+
# attribute value, so no call site can forget to escape.
|
|
6
8
|
class Senpai < Action
|
|
7
9
|
DEFAULT_SUBMIT_BUTTON_TEXT = 'Continue'
|
|
8
10
|
DEFAULT_CHARSET = 'UTF-8'
|
|
11
|
+
DEFAULT_ALLOWED_SCHEMES = %w[http https].freeze
|
|
9
12
|
# Browsers only submit forms via GET or POST; other verbs are sent as POST
|
|
10
13
|
# with a Rails-style "_method" override field.
|
|
11
14
|
OVERRIDE_METHODS = %w[put patch delete].freeze
|
|
15
|
+
HTML_ESCAPES = { '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' }.freeze
|
|
16
|
+
# Characters that must not appear raw in a JS string inside <script>.
|
|
17
|
+
JS_UNSAFE = /[<>&\u{2028}\u{2029}]/.freeze
|
|
12
18
|
|
|
13
19
|
def initialize(url, params: {}, options: {})
|
|
14
|
-
@url = url
|
|
15
|
-
@params = params
|
|
16
|
-
@
|
|
17
|
-
@method = options.fetch(:method, :post).to_s.downcase
|
|
20
|
+
@url = url.to_s
|
|
21
|
+
@params = params || {}
|
|
22
|
+
@http_method = options.fetch(:method, :post).to_s.downcase
|
|
18
23
|
@authenticity_token = options.fetch(:authenticity_token, nil)
|
|
19
24
|
@charset = options.fetch(:charset, DEFAULT_CHARSET)
|
|
20
|
-
@form_id = options.fetch(:form_id
|
|
25
|
+
@form_id = options.fetch(:form_id) { generated_form_id }
|
|
21
26
|
@autosubmit = options.fetch(:autosubmit, true)
|
|
22
27
|
@autosubmit_nonce = options.fetch(:autosubmit_nonce, nil)
|
|
28
|
+
@allowed_schemes = Array(options.fetch(:allowed_schemes, DEFAULT_ALLOWED_SCHEMES)).map { |s| s.to_s.downcase }
|
|
23
29
|
@section_classes = options.dig(:decor, :section, :classes)
|
|
24
30
|
@section_html = options.dig(:decor, :section, :html)
|
|
25
31
|
@submit_classes = options.dig(:decor, :submit, :classes)
|
|
26
32
|
@submit_text = options.dig(:decor, :submit, :text) || DEFAULT_SUBMIT_BUTTON_TEXT
|
|
33
|
+
check_url_scheme!
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def perform
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
form_body << submit_section
|
|
34
|
-
end
|
|
35
|
-
form_head << compiled_body << form_footer
|
|
37
|
+
controls = autosubmit ? auto_submit_script + tag(:noscript, submit_section) : submit_section
|
|
38
|
+
tag(:form, form_inputs + controls,
|
|
39
|
+
id: form_id, action: url, method: form_method, 'accept-charset': charset)
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
private
|
|
39
43
|
|
|
40
|
-
attr_reader :url, :params, :
|
|
44
|
+
attr_reader :url, :params, :http_method, :form_id, :autosubmit, :allowed_schemes,
|
|
41
45
|
:section_classes, :section_html, :submit_classes,
|
|
42
46
|
:submit_text, :authenticity_token, :charset, :autosubmit_nonce
|
|
43
47
|
|
|
48
|
+
# The only place markup is produced. Attribute values are always escaped
|
|
49
|
+
# (nil drops the attribute); content is markup built by this class, or
|
|
50
|
+
# decor section html, which is raw by design.
|
|
51
|
+
def tag(name, content = nil, **attributes)
|
|
52
|
+
attrs = attributes.compact.map { |key, value| %( #{key}="#{escape(value)}") }.join
|
|
53
|
+
content.nil? ? "<#{name}#{attrs}>" : "<#{name}#{attrs}>#{content}</#{name}>"
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def escape(value)
|
|
45
|
-
|
|
57
|
+
value.to_s.gsub(/[&<>"']/, HTML_ESCAPES)
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
# Escaping can't make a javascript: or data: URL safe, so only http(s)
|
|
61
|
+
# and relative URLs are allowed unless the caller allows more schemes.
|
|
62
|
+
def check_url_scheme!
|
|
63
|
+
# Browsers ignore leading whitespace/control characters and drop tabs
|
|
64
|
+
# and newlines anywhere, so "java\nscript:" still runs as javascript:.
|
|
65
|
+
normalized = url.gsub(/[\t\n\r]/, '').sub(/\A[\x00-\x20]+/, '')
|
|
66
|
+
scheme = normalized[/\A([a-z][a-z0-9+.\-]*):/i, 1]
|
|
67
|
+
return if scheme.nil? || allowed_schemes.include?(scheme.downcase)
|
|
68
|
+
|
|
69
|
+
raise UnsafeUrlError,
|
|
70
|
+
"repost refuses to submit a form to a #{scheme}: URL. Allowed schemes: " \
|
|
71
|
+
"#{allowed_schemes.join(', ')} (change with options[:allowed_schemes])"
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
def form_method
|
|
53
|
-
method_override? ? 'post' :
|
|
75
|
+
method_override? ? 'post' : http_method
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
def method_override?
|
|
57
|
-
OVERRIDE_METHODS.include?(
|
|
79
|
+
OVERRIDE_METHODS.include?(http_method)
|
|
58
80
|
end
|
|
59
81
|
|
|
60
|
-
def
|
|
61
|
-
inputs =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
inputs
|
|
65
|
-
inputs.unshift(method_override_input) if method_override?
|
|
82
|
+
def form_inputs
|
|
83
|
+
inputs = []
|
|
84
|
+
inputs << hidden_input('_method', http_method) if method_override?
|
|
85
|
+
inputs << hidden_input('authenticity_token', authenticity_token) if authenticity_token
|
|
86
|
+
params.each { |key, value| inputs << form_input(key, value) }
|
|
66
87
|
inputs.join
|
|
67
88
|
end
|
|
68
89
|
|
|
69
|
-
def
|
|
70
|
-
%Q(<input type="hidden" name="_method" value="#{escape(method)}">)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def form_input(key, value)
|
|
90
|
+
def form_input(name, value)
|
|
74
91
|
case value
|
|
75
|
-
when Hash
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end.join
|
|
79
|
-
when Array
|
|
80
|
-
value.map do |inner_value|
|
|
81
|
-
form_input("#{key}[]", inner_value)
|
|
82
|
-
end.join
|
|
83
|
-
else
|
|
84
|
-
%Q(<input type="hidden" name="#{escape(key)}" value="#{escape(value)}">)
|
|
92
|
+
when Hash then value.map { |key, inner| form_input("#{name}[#{key}]", inner) }.join
|
|
93
|
+
when Array then value.map { |inner| form_input("#{name}[]", inner) }.join
|
|
94
|
+
else hidden_input(name, value)
|
|
85
95
|
end
|
|
86
96
|
end
|
|
87
97
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def csrf_token
|
|
93
|
-
%Q(<input name="authenticity_token" value="#{escape(authenticity_token)}" type="hidden">)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def no_script
|
|
97
|
-
%Q(<noscript>#{submit_section}</noscript>)
|
|
98
|
+
def hidden_input(name, value)
|
|
99
|
+
tag(:input, type: 'hidden', name: name, value: value.to_s)
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
def submit_section
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def class_attr(classes)
|
|
105
|
-
%Q( class="#{escape(classes)}") if classes
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def generated_form_id
|
|
109
|
-
"repost-#{SecureRandom.hex(8)}"
|
|
103
|
+
button = tag(:input, class: submit_classes, type: 'submit', value: submit_text)
|
|
104
|
+
tag(:div, "#{section_html}#{button}", class: section_classes)
|
|
110
105
|
end
|
|
111
106
|
|
|
112
107
|
# Calls the prototype's submit() so a param named "submit" (which the
|
|
113
108
|
# browser exposes as form.submit, shadowing the method) can't break it.
|
|
114
109
|
def auto_submit_script
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
HTMLFormElement.prototype.submit.call(document.getElementById(#{js_string(form_id)}));
|
|
118
|
-
</script>)
|
|
110
|
+
tag(:script, "HTMLFormElement.prototype.submit.call(document.getElementById(#{js_string(form_id)}));",
|
|
111
|
+
nonce: autosubmit_nonce)
|
|
119
112
|
end
|
|
120
113
|
|
|
121
114
|
# A JS string literal that is also safe inside an HTML <script> element.
|
|
122
115
|
# HTML escaping is wrong here: entities are not decoded inside <script>.
|
|
123
116
|
def js_string(value)
|
|
124
|
-
JSON.generate(value.to_s).gsub(
|
|
117
|
+
JSON.generate(value.to_s).gsub(
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generated_form_id
|
|
121
|
+
"repost-#{SecureRandom.hex(8)}"
|
|
125
122
|
end
|
|
126
123
|
end
|
|
127
124
|
end
|
data/lib/repost/version.rb
CHANGED
data/lib/repost.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
require "repost/version"
|
|
2
|
-
require "repost/action"
|
|
3
|
-
require "repost/senpai"
|
|
4
|
-
require "repost/extend_controller"
|
|
5
|
-
|
|
6
1
|
module Repost
|
|
7
2
|
class Error < StandardError; end
|
|
3
|
+
# Raised when the target URL uses a scheme that isn't allowed (javascript:, data:, ...).
|
|
4
|
+
class UnsafeUrlError < Error; end
|
|
8
5
|
end
|
|
9
6
|
|
|
7
|
+
require "repost/version"
|
|
8
|
+
require "repost/action"
|
|
9
|
+
require "repost/senpai"
|
|
10
|
+
require "repost/extend_controller"
|
metadata
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: repost
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- YaroslavO
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actionpack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :development
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.1'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
13
33
|
- !ruby/object:Gem::Dependency
|
|
14
34
|
name: rake
|
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -46,9 +66,9 @@ executables: []
|
|
|
46
66
|
extensions: []
|
|
47
67
|
extra_rdoc_files: []
|
|
48
68
|
files:
|
|
69
|
+
- CHANGELOG.md
|
|
49
70
|
- LICENSE.txt
|
|
50
71
|
- README.md
|
|
51
|
-
- Rakefile
|
|
52
72
|
- lib/repost.rb
|
|
53
73
|
- lib/repost/action.rb
|
|
54
74
|
- lib/repost/extend_controller.rb
|
|
@@ -58,8 +78,9 @@ homepage: https://github.com/vergilet/repost
|
|
|
58
78
|
licenses:
|
|
59
79
|
- MIT
|
|
60
80
|
metadata:
|
|
61
|
-
homepage_uri: https://github.com/vergilet/repost
|
|
62
81
|
source_code_uri: https://github.com/vergilet/repost
|
|
82
|
+
changelog_uri: https://github.com/vergilet/repost/blob/master/CHANGELOG.md
|
|
83
|
+
bug_tracker_uri: https://github.com/vergilet/repost/issues
|
|
63
84
|
post_install_message:
|
|
64
85
|
rdoc_options: []
|
|
65
86
|
require_paths:
|