Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions actiontext/app/assets/javascripts/actiontext.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,14 +506,16 @@ var activestorage = {exports: {}};
}
}
class BlobRecord {
constructor(file, checksum, url) {
constructor(file, checksum, url, directUploadToken, attachmentName) {
this.file = file;
this.attributes = {
filename: file.name,
content_type: file.type || "application/octet-stream",
byte_size: file.size,
checksum: checksum
checksum: checksum,
};
this.directUploadToken = directUploadToken;
this.attachmentName = attachmentName;
this.xhr = new XMLHttpRequest;
this.xhr.open("POST", url, true);
this.xhr.responseType = "json";
Expand Down Expand Up @@ -541,7 +543,9 @@ var activestorage = {exports: {}};
create(callback) {
this.callback = callback;
this.xhr.send(JSON.stringify({
blob: this.attributes
blob: this.attributes,
direct_upload_token: this.directUploadToken,
attachment_name: this.attachmentName
}));
}
requestDidLoad(event) {
Expand Down Expand Up @@ -599,10 +603,12 @@ var activestorage = {exports: {}};
}
let id = 0;
class DirectUpload {
constructor(file, url, delegate) {
constructor(file, url, directUploadToken, attachmentName, delegate) {
this.id = ++id;
this.file = file;
this.url = url;
this.directUploadToken = directUploadToken;
this.attachmentName = attachmentName;
this.delegate = delegate;
}
create(callback) {
Expand All @@ -611,7 +617,7 @@ var activestorage = {exports: {}};
callback(error);
return;
}
const blob = new BlobRecord(this.file, checksum, this.url);
const blob = new BlobRecord(this.file, checksum, this.url, this.directUploadToken, this.attachmentName);
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
blob.create((error => {
if (error) {
Expand Down Expand Up @@ -640,7 +646,7 @@ var activestorage = {exports: {}};
constructor(input, file) {
this.input = input;
this.file = file;
this.directUpload = new DirectUpload(this.file, this.url, this);
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
this.dispatch("initialize");
}
start(callback) {
Expand Down Expand Up @@ -671,6 +677,12 @@ var activestorage = {exports: {}};
get url() {
return this.input.getAttribute("data-direct-upload-url");
}
get directUploadToken() {
return this.input.getAttribute("data-direct-upload-token");
}
get attachmentName() {
return this.input.getAttribute("data-direct-upload-attachment-name");
}
dispatch(name, detail = {}) {
detail.file = this.file;
detail.id = this.directUpload.id;
Expand Down Expand Up @@ -830,7 +842,7 @@ class AttachmentUpload {
constructor(attachment, element) {
this.attachment = attachment;
this.element = element;
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this);
this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this.directUploadToken, this.directUploadAttachmentName, this);
}

start() {
Expand Down Expand Up @@ -865,6 +877,14 @@ class AttachmentUpload {
return this.element.dataset.directUploadUrl
}

get directUploadToken() {
return this.element.dataset.directUploadToken
}

get directUploadAttachmentName() {
return this.element.dataset.directUploadAttachmentName
}

get blobUrlTemplate() {
return this.element.dataset.blobUrlTemplate
}
Expand Down
8 changes: 8 additions & 0 deletions actiontext/app/helpers/action_text/tag_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def rich_text_area_tag(name, value = nil, options = {})
options[:data][:direct_upload_url] ||= main_app.rails_direct_uploads_url
options[:data][:blob_url_template] ||= main_app.rails_service_blob_url(":signed_id", ":filename")

class_with_attachment = "ActionText::RichText#embeds"
options[:data][:direct_upload_attachment_name] ||= class_with_attachment
options[:data][:direct_upload_token] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
class_with_attachment,
ActiveStorage::Blob.service.name,
session
)

editor_tag = content_tag("trix-editor", "", options)
input_tag = hidden_field_tag(name, value.try(:to_trix_html) || value, id: options[:input], form: form)

Expand Down
243 changes: 135 additions & 108 deletions actiontext/test/template/form_helper_test.rb

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions actionview/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
* Add support for `button_to ..., authenticity_token: false`

```ruby
button_to "Create", Post.new, authenticity_token: false
# => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button></form>

button_to "Create", Post.new, authenticity_token: true
# => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button><input type="hidden" name="form_token" value="abc123..." autocomplete="off" /></form>

button_to "Create", Post.new, authenticity_token: "secret"
# => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button><input type="hidden" name="form_token" value="secret" autocomplete="off" /></form>
```

*Sean Doyle*

* Support rendering `<form>` elements _without_ `[action]` attributes by:

* `form_with url: false` or `form_with ..., html: { action: false }`
* `form_for ..., url: false` or `form_for ..., html: { action: false }`
* `form_tag false` or `form_tag ..., action: false`
* `button_to "...", false` or `button_to(false) { ... }`

*Sean Doyle*

* Add `:day_format` option to `date_select`

date_select("article", "written_on", day_format: ->(day) { day.ordinalize })
Expand Down
39 changes: 31 additions & 8 deletions actionview/lib/action_view/helpers/form_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ module FormHelper
# ...
# <% end %>
#
# You can omit the <tt>action</tt> attribute by passing <tt>url: false</tt>:
#
# <%= form_for(@post, url: false) do |f| %>
# ...
# <% end %>
#
# You can also set the answer format, like this:
#
# <%= form_for(@post, format: :json) do |f| %>
Expand Down Expand Up @@ -449,7 +455,7 @@ def form_for(record, options = {}, &block)
output = capture(builder, &block)
html_options[:multipart] ||= builder.multipart?

html_options = html_options_for_form(options[:url] || {}, html_options)
html_options = html_options_for_form(options.fetch(:url, {}), html_options)
form_tag_with_body(html_options, output)
end

Expand All @@ -465,10 +471,12 @@ def apply_form_for_options!(record, object, options) # :nodoc:
method: method
)

options[:url] ||= if options.key?(:format)
polymorphic_path(record, format: options.delete(:format))
else
polymorphic_path(record, {})
if options[:url] != false
options[:url] ||= if options.key?(:format)
polymorphic_path(record, format: options.delete(:format))
else
polymorphic_path(record, {})
end
end
end
private :apply_form_for_options!
Expand All @@ -488,6 +496,15 @@ def apply_form_for_options!(record, object, options) # :nodoc:
# <input type="text" name="title">
# </form>
#
# # With an intentionally empty URL:
# <%= form_with url: false do |form| %>
# <%= form.text_field :title %>
# <% end %>
# # =>
# <form method="post" data-remote="true">
# <input type="text" name="title">
# </form>
#
# # Adding a scope prefixes the input field names:
# <%= form_with scope: :post, url: posts_path do |form| %>
# <%= form.text_field :title %>
Expand Down Expand Up @@ -744,7 +761,9 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:skip_default_ids] = !form_with_generates_ids

if model
url ||= polymorphic_path(model, format: format)
if url != false
url ||= polymorphic_path(model, format: format)
end

model = model.last if model.is_a?(Array)
scope ||= model_name_from_record_or_class(model).param_key
Expand Down Expand Up @@ -1220,7 +1239,7 @@ def hidden_field(object_name, method, options = {})
# file_field(:attachment, :file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
def file_field(object_name, method, options = {})
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(method, options.dup)).render
end

# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
Expand Down Expand Up @@ -1559,7 +1578,11 @@ def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, loc

# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options[:action] = url_for(url_for_options || {})
if url_for_options == false || html_options[:action] == false
html_options.delete(:action)
else
html_options[:action] = url_for(url_for_options || {})
end
html_options[:"accept-charset"] = "UTF-8"
html_options[:"data-remote"] = true unless local

Expand Down
27 changes: 24 additions & 3 deletions actionview/lib/action_view/helpers/form_tag_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ module FormTagHelper
#
# <%= form_tag('/posts', remote: true) %>
# # => <form action="/posts" method="post" data-remote="true">

# form_tag(false, method: :get)
# # => <form method="get">
#
# form_tag('http://far.away.com/form', authenticity_token: false)
# # form without authenticity token
Expand Down Expand Up @@ -316,7 +319,7 @@ def hidden_field_tag(name, value = nil, options = {})
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
def file_field_tag(name, options = {})
text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
text_field_tag(name, nil, convert_direct_upload_option_to_url(name, options.merge(type: :file)))
end

# Creates a password field, a masked text field that will hide the users input behind a mask character.
Expand Down Expand Up @@ -875,7 +878,11 @@ def html_options_for_form(url_for_options, options)
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options)
if url_for_options == false || html_options["action"] == false
html_options.delete("action")
else
html_options["action"] = url_for(url_for_options)
end
html_options["accept-charset"] = "UTF-8"

html_options["data-remote"] = true if html_options.delete("remote")
Expand Down Expand Up @@ -954,9 +961,23 @@ def set_default_disable_with(value, tag_options)
tag_options.delete("data-disable-with")
end

def convert_direct_upload_option_to_url(options)
def convert_direct_upload_option_to_url(name, options)
if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
options["data-direct-upload-url"] = rails_direct_uploads_url

if options[:object] && options[:object].class.respond_to?(:reflect_on_attachment)
attachment_reflection = options[:object].class.reflect_on_attachment(name)

class_with_attachment = "#{options[:object].class.name.underscore}##{name}"
options["data-direct-upload-attachment-name"] = class_with_attachment

service_name = attachment_reflection.options[:service_name] || ActiveStorage::Blob.service.name
options["data-direct-upload-token"] = ActiveStorage::DirectUploadToken.generate_direct_upload_token(
class_with_attachment,
service_name,
session
)
end
end
options
end
Expand Down
29 changes: 24 additions & 5 deletions actionview/lib/action_view/helpers/url_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,15 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
# HTTP verb via the +:method+ option within +html_options+.
#
# ==== Options
# The +options+ hash accepts the same options as +url_for+.
# The +options+ hash accepts the same options as +url_for+. To generate a
# <tt><form></tt> element without an <tt>[action]</tt> attribute, pass
# <tt>false</tt>:
#
# <%= button_to "New", false %>
# # => "<form method="post" class="button_to">
# # <button type="submit">New</button>
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
#
# Most values in +html_options+ are passed through to the button element,
# but there are a few special options:
Expand Down Expand Up @@ -324,14 +332,20 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
# #
def button_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
options ||= {}
html_options ||= {}
html_options = html_options.stringify_keys

url = options.is_a?(String) ? options : url_for(options)
url =
case options
when FalseClass then nil
else url_for(options)
end

remote = html_options.delete("remote")
params = html_options.delete("params")

authenticity_token = html_options.delete("authenticity_token")

method = html_options.delete("method").to_s
method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe

Expand All @@ -344,7 +358,7 @@ def button_to(name = nil, options = nil, html_options = nil, &block)

request_token_tag = if form_method == "post"
request_method = method.empty? ? "post" : method
token_tag(nil, form_options: { action: url, method: request_method })
token_tag(authenticity_token, form_options: { action: url, method: request_method })
else
""
end
Expand Down Expand Up @@ -768,7 +782,12 @@ def method_not_get_method?(method)

def token_tag(token = nil, form_options: {})
if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
token ||= form_authenticity_token(form_options: form_options)
token =
if token == true || token.nil?
form_authenticity_token(form_options: form_options.merge(authenticity_token: token))
else
token
end
tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token, autocomplete: "off")
else
""
Expand Down
Loading