omnigroupcontacts 0.3.10 → 0.3.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/lib/omnigroupcontacts.rb +19 -0
- data/lib/omnigroupcontacts/authorization/oauth1.rb +122 -0
- data/lib/omnigroupcontacts/authorization/oauth2.rb +87 -0
- data/lib/omnigroupcontacts/builder.rb +30 -0
- data/lib/omnigroupcontacts/http_utils.rb +101 -0
- data/lib/omnigroupcontacts/importer.rb +5 -0
- data/lib/omnigroupcontacts/importer/gmailgroup.rb +238 -0
- data/lib/omnigroupcontacts/integration_test.rb +36 -0
- data/lib/omnigroupcontacts/middleware/base_oauth.rb +120 -0
- data/lib/omnigroupcontacts/middleware/oauth1.rb +70 -0
- data/lib/omnigroupcontacts/middleware/oauth2.rb +80 -0
- data/lib/omnigroupcontacts/parse_utils.rb +56 -0
- data/omnigroupcontacts-0.3.10.gem +0 -0
- data/omnigroupcontacts-0.3.8.gem +0 -0
- data/omnigroupcontacts-0.3.9.gem +0 -0
- data/omnigroupcontacts.gemspec +25 -0
- data/spec/omnicontacts/authorization/oauth1_spec.rb +82 -0
- data/spec/omnicontacts/authorization/oauth2_spec.rb +92 -0
- data/spec/omnicontacts/http_utils_spec.rb +79 -0
- data/spec/omnicontacts/importer/facebook_spec.rb +120 -0
- data/spec/omnicontacts/importer/gmail_spec.rb +194 -0
- data/spec/omnicontacts/importer/hotmail_spec.rb +106 -0
- data/spec/omnicontacts/importer/linkedin_spec.rb +67 -0
- data/spec/omnicontacts/importer/yahoo_spec.rb +124 -0
- data/spec/omnicontacts/integration_test_spec.rb +51 -0
- data/spec/omnicontacts/middleware/base_oauth_spec.rb +53 -0
- data/spec/omnicontacts/middleware/oauth1_spec.rb +78 -0
- data/spec/omnicontacts/middleware/oauth2_spec.rb +67 -0
- data/spec/omnicontacts/parse_utils_spec.rb +53 -0
- data/spec/spec_helper.rb +12 -0
- metadata +37 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 387500fc78883a2288cdf6b47f55178ffe173d3e
|
4
|
+
data.tar.gz: 6a755ae4559c0a760eb5b36b1fff75cf88c64e15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 043996490bf66a9c8dc5ce97a3c7e1c7e8e2fbb9ff08c253c7ca8fee83dfe7401ec31eecb9a4659118a12e78305b7b12ba1b6dd7ef4452f5777d818c5d694422
|
7
|
+
data.tar.gz: 7bb04a34aacc2d8ab1332f5881f67fab2d0cde42dd9397c6ec52fa8239f92ff2d09ae58211f10b3cf220230a53665fe67117f8221f0a2cbcc3835b5ee145d7d5
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
omnigroupcontacts (0.3.5)
|
5
|
+
json
|
6
|
+
rack
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
json (1.8.1)
|
13
|
+
multi_json (1.1.0)
|
14
|
+
rack (1.4.1)
|
15
|
+
rack-test (0.6.1)
|
16
|
+
rack (>= 1.0)
|
17
|
+
rake (0.9.2.2)
|
18
|
+
rspec (2.8.0)
|
19
|
+
rspec-core (~> 2.8.0)
|
20
|
+
rspec-expectations (~> 2.8.0)
|
21
|
+
rspec-mocks (~> 2.8.0)
|
22
|
+
rspec-core (2.8.0)
|
23
|
+
rspec-expectations (2.8.0)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.8.0)
|
26
|
+
simplecov (0.6.1)
|
27
|
+
multi_json (~> 1.0)
|
28
|
+
simplecov-html (~> 0.5.3)
|
29
|
+
simplecov-html (0.5.3)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
ruby
|
33
|
+
|
34
|
+
DEPENDENCIES
|
35
|
+
omnigroupcontacts!
|
36
|
+
rack-test
|
37
|
+
rake
|
38
|
+
rspec
|
39
|
+
simplecov
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# omnigroupcontacts
|
2
|
+
|
3
|
+
Inspired by the popular OmniContacts, OmniGroupContacts is a library that enables users of an application to import contacts
|
4
|
+
from their email accounts with respect to group. The email providers currently supported are Gmail.
|
5
|
+
OmniGroupContacts is a Rack middleware, therefore you can use it with Rails, Sinatra and any other Rack-based framework.
|
6
|
+
|
7
|
+
OmniGroupContacts uses the OAuth protocol to communicate with the contacts provider.
|
8
|
+
In order to use OmniGroupContacts, it is therefore necessary to first register your application with the provider and to obtain client_id and client_secret.
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
Add OmniGroupContacts as a dependency:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem "omnigroupcontacts"
|
16
|
+
|
17
|
+
```
|
18
|
+
|
19
|
+
As for OmniAuth, there is a Builder facilitating the usage of multiple contacts importers. In the case of a Rails application, the following code could be placed at `config/initializers/omnigroupcontacts.rb`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require "omnigroupcontacts"
|
23
|
+
|
24
|
+
Rails.application.middleware.use OmniGroupContacts::Builder do
|
25
|
+
importer :gmailgroup, "client_id", "client_secret", {:redirect_path => "/oauth2callback", :ssl_ca_file => "/etc/ssl/certs/curl-ca-bundle.crt"}
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
```
|
30
|
+
|
31
|
+
## Register your application
|
32
|
+
|
33
|
+
* For Gmail : [Google API Console](https://code.google.com/apis/console/)
|
34
|
+
|
35
|
+
|
36
|
+
## Integrating with your Application
|
37
|
+
|
38
|
+
To use the Gem you first need to redirect your users to `/group_contacts/:importer`, where `:importer` can be gmailgroup.
|
39
|
+
No changes to `config/routes.rb` are needed for this step since omnigroupcontacts will be listening on that path and redirect the user to the email provider's website in order to authorize your app to access his contact list.
|
40
|
+
Once that is done the user will be redirected back to your application, to the path specified in `:redirect_path`.
|
41
|
+
If nothing is specified the default value is `/group_contacts/:importer/callback` (e.g. `/group_contacts/gmailgroup/callback`). This makes things simpler and you can just add the following line to `config/routes.rb`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
match "/group_contacts/:importer/callback" => "your_controller#callback"
|
45
|
+
```
|
46
|
+
|
47
|
+
The list of contacts can be accessed via the `omnigroupcontacts.contacts` key in the environment hash and it consists of a simple array of hashes.
|
48
|
+
The following table shows which fields are supported by which provider:
|
49
|
+
|
50
|
+
<table>
|
51
|
+
<tr>
|
52
|
+
<th>Provider</th>
|
53
|
+
<th>:email</th>
|
54
|
+
<th>:id</th>
|
55
|
+
<th>:profile_picture</th>
|
56
|
+
<th>:name</th>
|
57
|
+
<th>:first_name</th>
|
58
|
+
<th>:last_name</th>
|
59
|
+
<th>:address_1</th>
|
60
|
+
<th>:address_2</th>
|
61
|
+
<th>:city</th>
|
62
|
+
<th>:region</th>
|
63
|
+
<th>:postcode</th>
|
64
|
+
<th>:country</th>
|
65
|
+
<th>:phone_number</th>
|
66
|
+
<th>:birthday</th>
|
67
|
+
<th>:gender</th>
|
68
|
+
<th>:relation</th>
|
69
|
+
</tr>
|
70
|
+
<tr>
|
71
|
+
<td>Gmail</td>
|
72
|
+
<td>X</td>
|
73
|
+
<td>X</td>
|
74
|
+
<td></td>
|
75
|
+
<td>X</td>
|
76
|
+
<td>X</td>
|
77
|
+
<td>X</td>
|
78
|
+
<td>X</td>
|
79
|
+
<td>X</td>
|
80
|
+
<td>X</td>
|
81
|
+
<td>X</td>
|
82
|
+
<td>X</td>
|
83
|
+
<td>X</td>
|
84
|
+
<td>X</td>
|
85
|
+
<td>X</td>
|
86
|
+
<td>X</td>
|
87
|
+
<td>X</td>
|
88
|
+
</tr>
|
89
|
+
</table>
|
90
|
+
|
91
|
+
Obviously it may happen that some fields are blank even if supported by the provider in the case that the contact did not provide any information about them.
|
92
|
+
|
93
|
+
The information for the logged in user can also be accessed via 'omnigroupcontacts.user' key in the environment hash. It consists of a simple hash which includes the same fields as above.
|
94
|
+
|
95
|
+
The following snippet shows how to simply print name and email of each contact, and also the the name of logged in user:
|
96
|
+
```ruby
|
97
|
+
def contacts_callback
|
98
|
+
@contacts = request.env['omnigroupcontacts.contacts']
|
99
|
+
@user = request.env['omnigroupcontacts.user']
|
100
|
+
puts "List of contacts of #{@user[:name]} obtained from #{params[:importer]}:"
|
101
|
+
@contacts.each do |group_name, contacts|
|
102
|
+
puts "Group: #{contacts}"
|
103
|
+
contacts.each do |contact|
|
104
|
+
puts "Contact found: name => #{contact[:name]}, email => #{contact[:email]}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
If the user does not authorize your application to access his/her contacts list, or any other inconvenience occurs, he/she is redirected to `/contacts/failure`. The query string will contain a parameter named `error_message` which specifies why the list of contacts could not be retrieved. `error_message` can have one of the following values: `not_authorized`, `timeout` and `internal_error`.
|
111
|
+
|
112
|
+
## License
|
113
|
+
|
114
|
+
Copyright (c) 2015 Mitesh Jain
|
115
|
+
|
116
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
117
|
+
copy of this software and associated documentation files (the "Software"),
|
118
|
+
to deal in the Software without restriction, including without limitation
|
119
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
120
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
121
|
+
Software is furnished to do so, subject to the following conditions:
|
122
|
+
|
123
|
+
The above copyright notice and this permission notice shall be included
|
124
|
+
in all copies or substantial portions of the Software.
|
125
|
+
|
126
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
127
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
128
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
129
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
130
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
131
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
132
|
+
DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module OmniGroupContacts
|
2
|
+
|
3
|
+
VERSION = "0.3.11"
|
4
|
+
|
5
|
+
MOUNT_PATH = "/group_contacts/"
|
6
|
+
|
7
|
+
autoload :Builder, "omnigroupcontacts/builder"
|
8
|
+
autoload :Importer, "omnigroupcontacts/importer"
|
9
|
+
autoload :IntegrationTest, "omnigroupcontacts/integration_test"
|
10
|
+
|
11
|
+
class AuthorizationError < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def self.integration_test
|
16
|
+
IntegrationTest.instance
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require "omnigroupcontacts/http_utils"
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
# This module represent a OAuth 1.0 Client.
|
5
|
+
#
|
6
|
+
# Classes including the module must implement
|
7
|
+
# the following methods:
|
8
|
+
# * auth_host -> the host of the authorization server
|
9
|
+
# * auth_token_path -> the path to query to obtain a request token
|
10
|
+
# * consumer_key -> the registered consumer key of the client
|
11
|
+
# * consumer_secret -> the registered consumer secret of the client
|
12
|
+
# * callback -> the callback to include during the redirection step
|
13
|
+
# * auth_path -> the path on the authorization server to redirect the user to
|
14
|
+
# * access_token_path -> the path to query in order to obtain the access token
|
15
|
+
module OmniGroupContacts
|
16
|
+
module Authorization
|
17
|
+
module OAuth1
|
18
|
+
include HTTPUtils
|
19
|
+
|
20
|
+
OAUTH_VERSION = "1.0"
|
21
|
+
|
22
|
+
# Obtain an authorization token from the server.
|
23
|
+
# The token is returned in an array along with the relative authorization token secret.
|
24
|
+
def fetch_authorization_token
|
25
|
+
request_token_response = https_post(auth_host, auth_token_path, request_token_req_params)
|
26
|
+
values_from_query_string(request_token_response, ["oauth_token", "oauth_token_secret"])
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def request_token_req_params
|
32
|
+
{
|
33
|
+
:oauth_consumer_key => consumer_key,
|
34
|
+
:oauth_nonce => encode(random_string),
|
35
|
+
:oauth_signature_method => "PLAINTEXT",
|
36
|
+
:oauth_signature => encode(consumer_secret + "&"),
|
37
|
+
:oauth_timestamp => timestamp,
|
38
|
+
:oauth_version => OAUTH_VERSION,
|
39
|
+
:oauth_callback => callback
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def random_string
|
44
|
+
(0...50).map { ('a'..'z').to_a[rand(26)] }.join
|
45
|
+
end
|
46
|
+
|
47
|
+
def timestamp
|
48
|
+
Time.now.to_i.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def values_from_query_string query_string, keys_to_extract
|
52
|
+
map = query_string_to_map(query_string)
|
53
|
+
keys_to_extract.collect do |key|
|
54
|
+
if map.has_key?(key)
|
55
|
+
map[key]
|
56
|
+
else
|
57
|
+
raise "No value found for #{key} in #{query_string}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
public
|
63
|
+
|
64
|
+
# Returns the url the user has to be redirected to do in order grant permission to the client application.
|
65
|
+
def authorization_url auth_token
|
66
|
+
"https://" + auth_host + auth_path + "?oauth_token=" + auth_token
|
67
|
+
end
|
68
|
+
|
69
|
+
# Fetches the access token from the authorization server.
|
70
|
+
# The method expects the authorization token, the authorization token secret and the authorization verifier.
|
71
|
+
# The result comprises the access token, the access token secret and a list of additional fields extracted from the server's response.
|
72
|
+
# The list of additional fields to extract is specified as last parameter
|
73
|
+
def fetch_access_token auth_token, auth_token_secret, auth_verifier, additional_fields_to_extract = []
|
74
|
+
access_token_resp = https_post(auth_host, access_token_path, access_token_req_params(auth_token, auth_token_secret, auth_verifier))
|
75
|
+
values_from_query_string(access_token_resp, (["oauth_token", "oauth_token_secret"] + additional_fields_to_extract))
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def access_token_req_params auth_token, auth_token_secret, auth_verifier
|
81
|
+
{
|
82
|
+
:oauth_consumer_key => consumer_key,
|
83
|
+
:oauth_nonce => encode(random_string),
|
84
|
+
:oauth_signature_method => "PLAINTEXT",
|
85
|
+
:oauth_signature => encode(consumer_secret + "&" + auth_token_secret),
|
86
|
+
:oauth_version => OAUTH_VERSION,
|
87
|
+
:oauth_timestamp => timestamp,
|
88
|
+
:oauth_token => auth_token,
|
89
|
+
:oauth_verifier => auth_verifier
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
public
|
94
|
+
|
95
|
+
# Calculates a signature using HMAC-SHA1 according to the OAuth 1.0 specifications.
|
96
|
+
#
|
97
|
+
# The base string is given is a RFC 3986 encoded concatenation of:
|
98
|
+
# * Uppercase HTTP method
|
99
|
+
# * An '&'
|
100
|
+
# * A url without any parameters
|
101
|
+
# * An '&'
|
102
|
+
# * All parameters to use in the request encoded themselves and sorted by key.
|
103
|
+
#
|
104
|
+
# The signature key is given by the concatenation of:
|
105
|
+
# * RFC 3986 encoded consumer secret
|
106
|
+
# * An '&'
|
107
|
+
# * RFC 3986 encoded token secret
|
108
|
+
def oauth_signature method, url, params, secret
|
109
|
+
encoded_method = encode(method.upcase)
|
110
|
+
encoded_url = encode(url)
|
111
|
+
# params must be in alphabetical order
|
112
|
+
encoded_params = encode(to_query_string(params.sort { |x, y| x.to_s <=> y.to_s }))
|
113
|
+
base_string = encoded_method + '&' + encoded_url + '&' + encoded_params
|
114
|
+
key = encode(consumer_secret) + '&' + secret
|
115
|
+
hmac_sha1 = OpenSSL::HMAC.digest('sha1', key, base_string)
|
116
|
+
# base64 encode results must be stripped
|
117
|
+
encode(Base64.encode64(hmac_sha1).strip)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "omnigroupcontacts/http_utils"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
# This module represents an OAuth 2.0 client.
|
5
|
+
#
|
6
|
+
# Classes including the module must implement
|
7
|
+
# the following methods:
|
8
|
+
# * auth_host -> the host of the authorization server
|
9
|
+
# * authorize_path -> the path on the authorization server the redirect the use to
|
10
|
+
# * client_id -> the registered client id of the client
|
11
|
+
# * client_secret -> the registered client secret of the client
|
12
|
+
# * redirect_path -> the path the authorization server has to redirect the user back after authorization
|
13
|
+
# * auth_token_path -> the path to query once the user has granted permission to the application
|
14
|
+
# * scope -> the scope necessary to acquire the contacts list.
|
15
|
+
module OmniGroupContacts
|
16
|
+
module Authorization
|
17
|
+
module OAuth2
|
18
|
+
include HTTPUtils
|
19
|
+
|
20
|
+
# Calculates the URL the user has to be redirected to in order to authorize
|
21
|
+
# the application to access his contacts list.
|
22
|
+
def authorization_url
|
23
|
+
"https://" + auth_host + authorize_path + "?" + authorize_url_params
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def authorize_url_params
|
29
|
+
to_query_string({
|
30
|
+
:client_id => client_id,
|
31
|
+
:scope => encode(scope),
|
32
|
+
:response_type => "code",
|
33
|
+
:access_type => "online",
|
34
|
+
:approval_prompt => "auto",
|
35
|
+
:redirect_uri => encode(redirect_uri)
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
public
|
40
|
+
|
41
|
+
# Fetches the access token from the authorization server using the given authorization code.
|
42
|
+
def fetch_access_token code
|
43
|
+
access_token_from_response https_post(auth_host, auth_token_path, token_req_params(code))
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def token_req_params code
|
49
|
+
{
|
50
|
+
:client_id => client_id,
|
51
|
+
:client_secret => client_secret,
|
52
|
+
:code => code,
|
53
|
+
:redirect_uri => encode(redirect_uri),
|
54
|
+
:grant_type => "authorization_code"
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def access_token_from_response response
|
59
|
+
if auth_host == "graph.facebook.com"
|
60
|
+
response = query_string_to_map(response).to_json
|
61
|
+
end
|
62
|
+
json = JSON.parse(response)
|
63
|
+
raise json["error"] if json["error"]
|
64
|
+
[json["access_token"], json["token_type"], json["refresh_token"]]
|
65
|
+
end
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
# Refreshes the access token using the provided refresh_token.
|
70
|
+
def refresh_access_token refresh_token
|
71
|
+
access_token_from_response https_post(auth_host, auth_token_path, refresh_token_req_params(refresh_token))
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def refresh_token_req_params refresh_token
|
77
|
+
{
|
78
|
+
:client_id => client_id,
|
79
|
+
:client_secret => client_secret,
|
80
|
+
:refresh_token => refresh_token,
|
81
|
+
:grant_type => "refresh_token"
|
82
|
+
}
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "omnigroupcontacts"
|
2
|
+
|
3
|
+
module OmniGroupContacts
|
4
|
+
class Builder < Rack::Builder
|
5
|
+
def initialize(app, &block)
|
6
|
+
if rack14?
|
7
|
+
super
|
8
|
+
else
|
9
|
+
@app = app
|
10
|
+
super(&block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def rack14?
|
15
|
+
Rack.release.split('.')[1].to_i >= 4
|
16
|
+
end
|
17
|
+
|
18
|
+
def importer importer, *args
|
19
|
+
middleware = OmniGroupContacts::Importer.const_get(importer.to_s.capitalize)
|
20
|
+
use middleware, *args
|
21
|
+
rescue NameError
|
22
|
+
raise LoadError, "Could not find importer #{importer}."
|
23
|
+
end
|
24
|
+
|
25
|
+
def call env
|
26
|
+
@ins << @app unless rack14? || @ins.include?(@app)
|
27
|
+
to_app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|