rails won't send_data as file - ruby-on-rails

I'm having problems with Rails method: send_data
Here's my action:
def export
send_data(current_user.contacts.to_csv,
type: 'text/csv; charset=utf-8; header=present',
disposition: 'attachment; filename=contacts.csv'
)
end
This will not promt for a download, it just render the result on the screen. I've tried both pow and thin servers.
I can't figure out what I'm doing wrong?
I'm using rails 4.0.0.beta
EDIT:
CURL headers:
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-UA-Compatible: chrome=1
< X-XHR-Current-Location: /contacts/export
< Content-Disposition: attachment; filename=contacts.csv
< Content-Transfer-Encoding: binary
< Content-Type: text/csv; charset=utf-8; header=present
< Cache-Control: private
< ETag: "48d3d8bd1c8d25cafb82ab705e4875ab"
< Set-Cookie: request_method=GET; path=/
< X-Request-Id: c2588883-f3f9-4f68-8a8c-0de758c47288
< X-Runtime: 0.185206
< Connection: close
< Server: thin 1.5.0 codename Knife

The answers here, are for turbolinks classic.
There is a newer notation on newer versions of turbolinks:
Disabled
https://github.com/turbolinks/turbolinks#disabling-turbolinks-on-specific-links

I figured it out.
It was turbolinks that was messing it all up. I added data-no-turbolink to the export link and now it works as expected.

send_data has an option hash, so type, disposition and filename need to be set in a hash:
def export
send_data(current_user.contacts.to_csv,
type: 'text/csv', disposition: 'attachment', filename: 'contacts.csv')
end

Related

Ruby Rest-client keeps correcting the uri

I'm using rest-client to make a post request to a certain address. Unfortunately rest-client keeps correcting the web adres from 'https' to 'http'. Which then gives back an error.
I already looked at the documentation whether this maybe was a setting for the gem. Anyone knows how I can 'force' rest-client to access the desired address.
Thx in advance!
used code is:
uri = 'https://sandbox.api.online.unit4.nl/V19/OAuth/Token'
payload = {
code: params[:code],
client_id: '#{#client_id}',
client_secret: '#{#client_secret}',
redirect_uri: '#{#client_redirect_url}',
grant_type: 'authorization_code'
}
response = RestClient::Request.execute(method: :post, url: uri, headers: {params: payload})
curl:
> GET /V19/OAuth/Token HTTP/1.1
> Host: sandbox.api.online.unit4.nl
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< content-length: 27
< content-type: application/json; charset=utf-8
< date: Wed, 06 Dec 2017 15:21:44 GMT
< p3p: CP="NON CUR OTPi OUR NOR UNI"
< server: Microsoft-IIS/7.5
< x-aspnetmvc-version: 5.2
< cache-control: no-cache, no-store, max-age=0, must-revalidate
< access-control-allow-origin: *
< x-powered-by: ASP.NET
< x-aspnet-version: 4.0.30319
< pragma: no-cache
< Set-Cookie: PD_STATEFUL_88eb7504-dfe9-11e2-8624-005056af4a32=sandbox.api.online.unit4.nl; Path=/
< Set-Cookie: LB_online_unit4=1493375404.20480.0000; path=/
Could be that your variables aren't getting interpolated. With single quotes '#{#variable}' doesn't work. Try switching the single quotes to double quotes ('' => ""). What is happening is that instead of sending the actual client id and secret, you are sending the string #{#client_id}.
Try
uri = 'https://sandbox.api.online.unit4.nl/V19/OAuth/Token'
payload = {
code: params[:code],
client_id: #client_id,
client_secret: #client_secret,
redirect_uri: #client_redirect_url,
grant_type: 'authorization_code'
}
response = RestClient::Request.execute(method: :post, url: uri, headers: {params: payload})

create_session doesn't set SET-COOKIE header

i was under the assumption that create_session if the endpoint.ex was configured to use the cookie store, would set the SET-COOKIE response header
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session,
log: :debug,
store: :cookie,
key: "some_key",
signing_salt: "some_salt"
this is my authentication controller ( just a part of it)
def callback(%{ assigns: %{ ueberauth_auth: auth } } = conn, params) do
params = build_params(auth)
user = find_or_create_user params
conn = put_session(conn, :current_user, user)
IO.inspect conn.resp_headers
IO.inspect get_session(conn, :current_user)
render conn, "index.html"
#Helpers.redirect!(conn, "/")
end
def build_params(auth) do
%{email: auth.info.email, github_token: auth.credentials.token, github_user: auth.info.nickname}
end
def find_or_create_user(params) do
case DBRepo.get_by(User, email: params.email) do
nil ->
User.changeset(%User{}, params)
|> DBRepo.insert
results ->
results
end
end
IO.inspect conn.resp_headers
returns
[{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "vh8l2deodne1k2iloa4c3e4qdpmh857n"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}]
IO.inspect get_session(conn, :current_user)
returns the user as expected
You don't see the session cookie in resp_headers because Plug.Session sets that cookie just before the response is actually sent, using Plug.Conn.register_before_send. If you make a request using any HTTP client (browser, curl, etc), you'll see the Set-Cookie header.
defmodule MyApp.PageController do
use MyApp.Web, :controller
def index(conn, _params) do
conn
|> put_session(:foo, :bar)
|> text("")
end
end
$ curl -I localhost:4000
HTTP/1.1 200 OK
server: Cowboy
date: Mon, 20 Feb 2017 08:57:36 GMT
content-length: 0
set-cookie: _my_app_key=SFMyNTY.g3QAAAABbQAAAANmb29kAANiYXI.F0G6lsgPxsYjq97tonLy1gRkOBUVcfwqKZdozgGRG-c; path=/; HttpOnly
content-type: text/plain; charset=utf-8
cache-control: max-age=0, private, must-revalidate
x-request-id: uoplksup9ndakf5sdr5shpjsjhvu849v
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff

Why am I unable to sign out using devise_token_auth and curl?

I'm using rails-api with devise_token_auth for authentication. I am able to sign in without issue via:
curl -i http://localhost:3000/api/v1/auth/sign_in -F email="user#nowhere.org" -F password="password"
However, sign out fails with a 404 and the error "User was not found or was not logged in." via:
curl -X DELETE -i http://localhost:3000/api/v1/auth/sign_out
I have tried variations of this command with multiple combinations of parameters and header values, such as:
curl -X DELETE -i http://localhost:3000/api/v1/auth/sign_out -F email="user#nowhere.org" -H Access-Token="Ae1yaTYLkSAgdhz3LtPAZg" -H Client="9AmYF6NS8tP6EOD5nPSuxw" -H Expiry="1443073493" -H Uid="user#nowhere.org" -H Token-Type="Bearer"
To no avail. A similarly constructed RSpec test also fails with the same response and error, and the logs indicate the request was processed via DeviseTokenAuth::SessionsController#destroy as JSON.
Of course, I'm not actually using curl for authentication; just verification of the request structure before writing the relevant code.
Answering my own question, I wasn't returning certain sign_in response header values correctly with the sign_out request.
I found a related post which pointed out some of the critical headers. The trick is to capture the access-token, client, and uid header values from the sign_in response, then include them as parameters in the sign_out request:
curl -i -X DELETE http://localhost:3000/api/v1/auth/sign_out -F access-token="Ae1yaTYLkSAgdhz3LtPAZg" -F client="9AmYF6NS8tP6EOD5nPSuxw" -F uid="user#nowhere.org"
Here's an RSpec test to illustrate and verify:
require 'rails_helper'
RSpec.describe "Authentication", type: :request do
it "logs a user out" do
user = User.create!(
name: "Some Dude",
email: "user#nowhere.org",
password: "password",
confirmed_at: Date.today
)
# initial sign in to generate a token and response
post api_v1_user_session_path, {
email: user.email,
password: user.password
}
expect(user.reload.tokens.count).to eq 1
# sign out request using header values from sign in response
delete destroy_api_v1_user_session_path, {
"access-token": response.header["access-token"],
client: response.header["client"],
uid: response.header["uid"]
}
response_body = JSON.load(response.body)
expect(response_body["errors"]).to be_blank
expect(response.status).to eq 200
# user token should be deleted following sign out
expect(user.reload.tokens.count).to eq 0
end
end
In details, do the following steps:
Step 1:
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST http://localhost:3000/api/v1/auth/sign_in -d "{\"email\":\"user#example.com\",\"password\":\"password\"}"
You'll get a response like this:
* Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> POST /api/v1/auth/sign_in HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Content-Type: application/json
> Accept: application/json
> Content-Length: 50
>
* upload completely sent off: 50 out of 50 bytes
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-Xss-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Access-Token: BqXcWQi0-9faLyxP1LnUKw
< Token-Type: Bearer
< Client: dYSqVgM9VT6fV9Y5MFWpJQ
< Expiry: 1465679853
< Uid: user#example.com
< Content-Type: application/json; charset=utf-8
< Etag: W/"9ad6a23f014a744a7ec83b4e0e9d27aa"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 6566bd38-1ad7-491a-a1ab-e41458b9b704
< X-Runtime: 0.184807
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
< Date: Sat, 28 May 2016 21:17:33 GMT
< Content-Length: 135
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
{"data":{"id":6,"provider":"email","uid":"user#example.com","name":"testuser","nickname":null,"image":null,"email":"user#example.com"}}%
Step 2:
Now, you want to sign the user out.
curl -i -X DELETE http://localhost:3000/api/v1/auth/sign_out -F access-token="BqXcWQi0-9faLyxP1LnUKw" -F client="dYSqVgM9VT6fV9Y5MFWpJQ" -F uid="user#example.com"
You'll get a response like this:
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Etag: W/"7363e85fe9edee6f053a4b319588c086"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 8f7a297a-6a72-4c9d-a210-48c29fb4bfe0
X-Runtime: 0.095060
Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
Date: Sat, 28 May 2016 21:19:18 GMT
Content-Length: 16
Connection: Keep-Alive
{"success":true}%
In step 2, I've added the access-token and client as per the response that we received in step 1 (Access-Token: BqXcWQi0-9faLyxP1LnUKw and Client: dYSqVgM9VT6fV9Y5MFWpJQ).
That's it! :)
I took the help of #user3006381's answer! All credits to him.

Rspec test for GZIPed response

I recently enabled GZIP on my Rails 4 app following this Thoughtbot blog post and I also have added use Rack::Deflater to my config.ru file as suggested by this post. My Rails app seems to be serving compressed content, but when I test for it using RSpec the test fails because response.headers['Content-Encoding'] is nil.
Here is my application.rb:
module MyApp
class Application < Rails::Application
# Turn on GZIP compression
config.middleware.use Rack::Deflater
end
end
Here is my spec:
require 'rails_helper'
describe GeneralController, type: :controller, focus: true do
it "a visitor has a browser that supports compression" do
['deflate', 'gzip', 'deflate,gzip', 'gzip,deflate'].each do |compression_method|
get 'about', {}, {'HTTP_ACCEPT_ENCODING' => compression_method }
binding.pry
expect(response.headers['Content-Encoding']).to be
end
end
it "a visitor's browser does not support compression" do
get 'about'
expect(response.headers['Content-Encoding']).to_not be
end
end
When I run curl --head -H "Accept-Encoding: gzip" http://localhost:3000/ I get the following output:
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Ua-Compatible: chrome=1
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Content-Encoding: gzip
Etag: "f7e364f21dbb81b9580cd39e308a7c15"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 3f018f27-40ab-4a87-a836-67fdd6bd5b6e
X-Runtime: 0.067748
Server: WEBrick/1.3.1 (Ruby/2.0.0/2014-02-24)
When I load the site and look at the Network tab of the inspector I can see that the response size is smaller than before, but my test still fails. I'm not sure if I'm missing a step here with my test or if there is an issue with my implementation of Rack::Deflater.
As #andy-waite pointed, RSpec controller specs are not aware of middleware, but that's why, since RSpec 2.6 we have request specs.
Request specs are, according to the docs:
designed to drive behavior through the full stack
Therefore, using RSpec > 2.6 request specs, your code should look like:
require 'rails_helper'
describe GeneralController, type: :request, focus: true do
it "a visitor has a browser that supports compression" do
['deflate', 'gzip', 'deflate,gzip', 'gzip,deflate'].each do |compression_method|
get 'about', {}, {'HTTP_ACCEPT_ENCODING' => compression_method }
binding.pry
expect(response.headers['Content-Encoding']).to be
end
end
it "a visitor's browser does not support compression" do
get 'about'
expect(response.headers['Content-Encoding']).to_not be
end
end
RSpec controller specs are wrapped around Rails functional tests, which are not aware of middleware:
Making Rails tests aware of Rack middleware outside Rails's internal chain

can HTTP multipart/mixed xml part be converted to a Hash when mixed with other parts

The question is just above the last code snippet. Thank you.
(environment details are the end )
posts_controller.rb
class PostsController < ApplicationController
def create
#post = Post.new(params[:post])
respond_to do |format|
format.xml { render :xml => #post.to_xml(:include => [ :assets])}
end
end
posts.rb
class Post < ActiveRecord::Base
has_many :assets, :as => :attachable, :dependent => :destroy
end
asset.rb
class Asset < ActiveRecord::Base
belongs_to :attachable, :polymorphic => true
has_attached_file :data,
:url => "/assets/:id",
:path =>":rails_root/assets/:id_partition/:style/:basename.:extension"
def name
data_file_name
end
def content_type
data_content_type
end
def file_size
data_file_size
end
end
now when we post this information
POST /posts.xml HTTP/1.1
Accept-Encoding: gzip,deflate
Accept: application/xml
Content-Type: application/xml
User-Agent: Jakarta Commons-HttpClient/3.1
Host: localhost:8080
Content-Length: 60
<post><body>postbody</body><title>post_title</title></post>
a post entry gets created
and when I post this
POST /posts.xml HTTP/1.1
Content-type: multipart/mixed; boundary=---------------------------7d226f700d0
Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.6.0_21
Host: 192.168.0.105:8080
Connection: keep-alive
Content-Length: 1710
-----------------------------7d226f700d0
content-disposition: form-data; name="post[title]"
Content-Length: 10
post_title
-----------------------------7d226f700d0
content-disposition: form-data; name="post[body]"
Content-Length: 8
postbody
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/files/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/Pictures/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0
a new post gets created with 2 file attachments.
now the question is, I want to get the following HTTP post ( please notice the xml part before the file attachments) to also create a post with 2 attachments, with no additional changes ( to posts_controller or routes.rb). is that possible?
POST /posts.xml HTTP/1.1
Content-type: multipart/mixed; boundary=---------------------------7d226f700d0
Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.6.0_21
Host: 192.168.0.105:8080
Connection: keep-alive
Content-Length: 1710
-----------------------------7d226f700d0
Content-type: application/xml; charset=UTF-8
Content-Length: 59
<post><body>postbody</body><title>post_title</title></post>
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/files/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0
content-disposition: form-data; name="post[assets_attributes][0][data]"; filename="C:/Users/mv288/Pictures/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------7d226f700d0Blockquotetest
using jruby 1.5.2/jdk1.6, rails 2.3.4, paperclip-2.3.3
on windows 2007 - 64 bit
At least rails 2.3.4 does not do this automatically. One needs to write a multipart/related parser and register it, inside initializers/mime_types.rb
NOTE : Feel free to update the hard coded values( like main and attachment part prefix etc.,) in your copy.
This same strategy can be used for multipart/related content as well. We are still debating whether to use multipart/related or multipart/mixed in this example below.
Microsoft's notes on this subject.
http://msdn.microsoft.com/en-us/library/ms527355(EXCHG.10).aspx
Mime::Type.register "multipart/mixed", :mixed
class ActionController::Request
def initialize(env)
Thread.current[:request]=self
super
end
end
class MultiPartParamsParser
def main_part_name
"main"
end
def attachment_part_prefix
"my_company_attachment"
end
def content_type(main_part)
# TODO ----
:xml_simple
end
def content(main_part)
# TODO implement this
if main_part.is_a?(String)
main_part.gsub!("Content-Type: application/xml",'') # remove content type if it exists
main_part.strip! # to remove any trailing or leading whitespaces
else
main_part[:tempfile].read
end
end
def request
Thread.current[:request]
end
def parse_formatted_parameters(data)
multi_parts = Rack::Utils::Multipart.parse_multipart(request.try(:env))
main_part = multi_parts[main_part_name]
data = content(main_part)
# TODO return an error if data is not found
params = case content_type(main_part)
when :xml_simple, :xml_node
data.blank? ? {} : Hash.from_xml(data).with_indifferent_access
when :yaml
YAML.load(data)
when :json
if data.blank?
{}
else
ret = ActiveSupport::JSON.decode(data)
ret = {:_json => data} unless data.is_a?(Hash)
ret.with_indifferent_access
end
else
{}
end
process_attachments(params, multi_parts)
params
end
def process_attachments(data, multi_parts)
data.each do |key, value|
value ||= key # when array value is nil
if value.is_a?(Hash) or value.is_a?(Array)
process_attachments(value, multi_parts)
elsif value.respond_to?(:match) and value.match("^#{attachment_part_prefix}") and (attachment=multi_parts[value]) # there could Time,Numbers etc.., but we match only string.
data[key] = create_uploaded_file(attachment) # TODO handle the scenarios for short strings
end
end
end
def create_uploaded_file (attachment)
upload = attachment[:tempfile]
upload.extend(ActionController::UploadedFile)
upload.original_path = attachment[:filename]
upload.content_type = attachment[:type]
upload
end
end
proc = Proc.new do |data|
MultiPartParamsParser.new.parse_formatted_parameters(data)
end
ActionController::Base.param_parsers[Mime::Type.lookup('multipart/mixed')] = proc
then, you can post your message like this. Nesting gets automatically taken with no further changes to the models or controllers.
POST /posts.xml HTTP/1.1
Content-type: multipart/mixed; boundary=---------------------------###987612345###
Accept: application/xml,text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Cache-Control: no-cache
Pragma: no-cache
Connection: keep-alive
Content-Length: ##
-----------------------------###987612345###
content-disposition: name="main"
Content-Length: ##
<post><title>post_title</title><body>post_body</body>
<assets_attributes type="array">
<asset><data>my_company_attachment_0</data> </asset>
<asset><data>my_company_attachment_1</data> </asset>
</assets_attributes>
</post>
-----------------------------###987612345###
content-disposition: name="my_company_attachment_0"; filename="C:/Users/mv288/files/1.txt"
content-type: application/octet-stream
ÿþ
sample file content
-----------------------------###987612345###
content-disposition: name="my_company_attachment_1"; filename="C:/Users/mv288/Pictures/1.png"
content-type: image/png
ÿþ
sample file content
-----------------------------###987612345###

Resources