opro 0.4.2 → 0.4.3

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.
@@ -1,5 +1,8 @@
1
1
  ## master
2
2
 
3
+ ## 0.4.3
4
+ - [#20] Bugfix: expires_in not correctly recalculated after auth_grant refreshed (@nvh)
5
+
3
6
  ## 0.4.2
4
7
  - Fix jRuby compatibility in `Opro.convert_to_lambda`
5
8
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.2
1
+ 0.4.3
@@ -7,15 +7,10 @@ class Opro::Oauth::TestsController < OproController
7
7
  end
8
8
 
9
9
  def show
10
- result = if valid_oauth?
11
- {:status => 200, :message => 'OAuth worked!', :params => params, :user_id => oauth_user.id }
12
- else
13
- {:status => :unauthorized, :message => "OAuth did not work :( #{generate_oauth_error_message!}", :params => params}
14
- end
15
-
10
+ result = oauth_result(params)
16
11
  respond_to do |format|
17
12
  format.html do
18
- render :text => result.to_json, :status => result[:status], :layout => true
13
+ render :text => result.to_json, :status => result[:status], :layout => true
19
14
  end
20
15
  format.json do
21
16
  render :json => result, :status => result[:status]
@@ -24,15 +19,10 @@ class Opro::Oauth::TestsController < OproController
24
19
  end
25
20
 
26
21
  def create
27
- result = if valid_oauth?
28
- {:status => 200, :message => 'OAuth worked!', :params => params, :user_id => oauth_user.id }
29
- else
30
- {:status => :unauthorized, :message => "OAuth did not work :( #{generate_oauth_error_message!}", :params => params}
31
- end
32
-
22
+ result = oauth_result(params)
33
23
  respond_to do |format|
34
24
  format.html do
35
- render :text => result.to_json, :status => result[:status], :layout => true
25
+ render :text => result.to_json, :status => result[:status], :layout => true
36
26
  end
37
27
  format.json do
38
28
  render :json => result, :status => result[:status]
@@ -56,4 +46,14 @@ class Opro::Oauth::TestsController < OproController
56
46
  end
57
47
  end
58
48
  end
49
+
50
+ private
51
+
52
+ def oauth_result(options)
53
+ if valid_oauth?
54
+ {:status => 200, :message => 'OAuth worked!', :params => options, :user_id => oauth_user.id }
55
+ else
56
+ {:status => :unauthorized, :message => "OAuth did not work :( #{generate_oauth_error_message!}", :params => params}
57
+ end
58
+ end
59
59
  end
@@ -11,33 +11,40 @@ class Opro::Oauth::TokenController < OproController
11
11
  application = Opro::Oauth::ClientApp.authenticate(params[:client_id], params[:client_secret])
12
12
 
13
13
  if application.nil?
14
- render :json => {:error => "Could not find application based on client_id=#{params[:client_id]}
15
- and client_secret=#{params[:client_secret]}"}, :status => :unauthorized
16
- return
14
+ render :json => {:error => app_not_found_error(params)}, :status => :unauthorized and return
17
15
  end
18
16
 
19
17
  if params[:code]
20
18
  auth_grant = Opro::Oauth::AuthGrant.auth_with_code!(params[:code], application.id)
21
19
  elsif params[:refresh_token]
22
- auth_grant = Opro::Oauth::AuthGrant.refresh_tokens!(params[:refresh_token], application.id)
20
+ auth_grant = Opro::Oauth::AuthGrant.find_for_refresh(params[:refresh_token], application.id)
23
21
  elsif params[:password].present? || params[:grant_type] == "password"|| params[:grant_type] == "bearer"
24
22
  user = ::Opro.find_user_for_all_auths!(self, params) if Opro.password_exchange_enabled? && oauth_valid_password_auth?(params[:client_id], params[:client_secret])
25
23
  auth_grant = Opro::Oauth::AuthGrant.auth_with_user!(user, application.id) if user.present?
26
24
  end
27
25
 
28
26
  if auth_grant.blank?
29
- msg = "Could not find a user that belongs to this application"
30
- msg << " & has a refresh_token=#{params[:refresh_token]}" if params[:refresh_token]
31
- msg << " & has been granted a code=#{params[:code]}" if params[:code]
32
- msg << " using username and password" if params[:password]
33
- render :json => {:error => msg }, :status => :unauthorized
34
- return
27
+ render :json => {:error => debug_error_msg(params) }, :status => :unauthorized and return
35
28
  end
36
29
 
37
- auth_grant.generate_expires_at!
30
+ auth_grant.refresh!
38
31
  render :json => { :access_token => auth_grant.access_token,
39
32
  :refresh_token => auth_grant.refresh_token,
40
33
  :expires_in => auth_grant.expires_in }
41
34
  end
42
35
 
36
+ private
37
+
38
+ def debug_error_msg(options)
39
+ msg = "Could not find a user that belongs to this application"
40
+ msg << " & has a refresh_token=#{options[:refresh_token]}" if options[:refresh_token]
41
+ msg << " & has been granted a code=#{options[:code]}" if options[:code]
42
+ msg << " using username and password" if options[:password]
43
+ msg
44
+ end
45
+
46
+ def app_not_found_error(options)
47
+ "Could not find application based on client_id=#{options[:client_id]} and client_secret=#{options[:client_secret]}"
48
+ end
49
+
43
50
  end
@@ -12,7 +12,7 @@ class Opro::Oauth::AuthGrant < ActiveRecord::Base
12
12
  validates :code, :uniqueness => true
13
13
  validates :access_token, :uniqueness => true
14
14
 
15
- before_create :generate_tokens!, :generate_expires_at!
15
+ before_create :refresh
16
16
 
17
17
  alias_attribute :token, :access_token
18
18
 
@@ -60,29 +60,21 @@ class Opro::Oauth::AuthGrant < ActiveRecord::Base
60
60
  auth_grant
61
61
  end
62
62
 
63
- def self.refresh_tokens!(refresh_token, application_id)
64
- auth_grant = self.where("refresh_token = ? AND application_id = ?", refresh_token, application_id).first
65
- if auth_grant.present?
66
- auth_grant.generate_tokens!
67
- auth_grant.generate_expires_at!
68
- auth_grant.save!
69
- end
70
- auth_grant
63
+ def self.find_for_refresh(refresh_token, application_id)
64
+ self.where("refresh_token = ? AND application_id = ?", refresh_token, application_id).first
71
65
  end
72
66
 
73
- def generate_expires_at!
74
- if ::Opro.require_refresh_within.present?
75
- self.access_token_expires_at = Time.now + ::Opro.require_refresh_within
76
- else
77
- self.access_token_expires_at = nil
78
- end
79
- true
67
+ # generates tokens, expires_at and saves
68
+ def refresh!
69
+ refresh
70
+ save!
80
71
  end
81
72
 
82
- def generate_tokens!
83
- self.code = unique_token_for(:code)
84
- self.access_token = unique_token_for(:access_token)
85
- self.refresh_token = unique_token_for(:refresh_token)
73
+ # generates tokens, expires_at
74
+ def refresh
75
+ generate_tokens!
76
+ generate_expires_at!
77
+ self
86
78
  end
87
79
 
88
80
  # used to guarantee that we are generating unique codes, access_tokens and refresh_tokens
@@ -102,4 +94,22 @@ class Opro::Oauth::AuthGrant < ActiveRecord::Base
102
94
  redirect_uri << "&state=#{state}" if state.present?
103
95
  redirect_uri
104
96
  end
97
+
98
+ private
99
+ # use refresh instead
100
+ def generate_expires_at!
101
+ if ::Opro.require_refresh_within.present?
102
+ self.access_token_expires_at = Time.now + ::Opro.require_refresh_within
103
+ else
104
+ self.access_token_expires_at = nil
105
+ end
106
+ true
107
+ end
108
+
109
+ # use refresh instead
110
+ def generate_tokens!
111
+ self.code = unique_token_for(:code)
112
+ self.access_token = unique_token_for(:access_token)
113
+ self.refresh_token = unique_token_for(:refresh_token)
114
+ end
105
115
  end
@@ -6,7 +6,8 @@ module ActionDispatch::Routing
6
6
  skip_routes = options[:except].is_a?(Array) ? options[:except] : [options[:except]]
7
7
  controllers = options[:controllers] || {}
8
8
 
9
- match 'oauth/new' => 'opro/oauth/auth#new', :as => 'oauth_new'
9
+ oauth_new_controller = controllers[:oauth_new] || 'opro/oauth/auth'
10
+ match 'oauth/new' => "#{oauth_new_controller}#new", :as => 'oauth_new'
10
11
  match 'oauth/authorize' => 'opro/oauth/auth#create', :as => 'oauth_authorize'
11
12
  match 'oauth/token' => 'opro/oauth/token#create', :as => 'oauth_token'
12
13
 
@@ -25,4 +26,3 @@ module ActionDispatch::Routing
25
26
  end
26
27
  end
27
28
  end
28
-
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "opro"
8
- s.version = "0.4.2"
8
+ s.version = "0.4.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["schneems"]
12
- s.date = "2012-11-07"
12
+ s.date = "2012-11-17"
13
13
  s.description = " Enable OAuth clients (iphone, android, web sites, etc.) to access and use your Rails application, what you do with it is up to you"
14
14
  s.email = "richard.schneeman@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -124,7 +124,7 @@ Gem::Specification.new do |s|
124
124
  s.homepage = "http://github.com/schneems/opro"
125
125
  s.licenses = ["MIT"]
126
126
  s.require_paths = ["lib"]
127
- s.rubygems_version = "1.8.24"
127
+ s.rubygems_version = "1.8.10"
128
128
  s.summary = "oPRO turns your Rails application into an OAuth Provider"
129
129
 
130
130
  if s.respond_to? :specification_version then
@@ -23,10 +23,10 @@ class OauthTokenTest < ActionDispatch::IntegrationTest
23
23
 
24
24
  json_hash = JSON.parse(response.body)
25
25
  assert json_hash["access_token"]
26
- assert_equal json_hash["access_token"], auth_grant.access_token
26
+ assert_equal json_hash["access_token"], auth_grant.reload.access_token
27
27
 
28
28
  assert json_hash["refresh_token"]
29
- assert_equal json_hash["refresh_token"], auth_grant.refresh_token
29
+ assert_equal json_hash["refresh_token"], auth_grant.reload.refresh_token
30
30
  end
31
31
 
32
32
 
@@ -25,7 +25,8 @@ class RefreshTokenTest < ActionDispatch::IntegrationTest
25
25
  params = {:client_id => @client_app.client_id ,
26
26
  :client_secret => @client_app.client_secret,
27
27
  :code => @auth_grant.code}
28
- as_user(@user).post oauth_token_path(params)
28
+
29
+ post oauth_token_path(params)
29
30
  json_hash = JSON.parse(response.body)
30
31
  assert_equal json_hash['expires_in'], @auth_grant.expires_in
31
32
  end
@@ -37,7 +38,7 @@ class RefreshTokenTest < ActionDispatch::IntegrationTest
37
38
 
38
39
  Timecop.travel(2.days.from_now)
39
40
 
40
- as_user(@user).post oauth_token_path(params)
41
+ post oauth_token_path(params)
41
42
 
42
43
  json_hash = JSON.parse(response.body)
43
44
  refute_equal json_hash['access_token'], @auth_grant.access_token
@@ -51,4 +52,37 @@ class RefreshTokenTest < ActionDispatch::IntegrationTest
51
52
  assert_equal json_hash['expires_in'], auth_grant.expires_in
52
53
  end
53
54
 
55
+ test "after expires in period, access_token is no longer valid" do
56
+ Timecop.freeze(@client_app.created_at)
57
+ params = {:client_id => @client_app.client_id ,
58
+ :client_secret => @client_app.client_secret,
59
+ :code => @auth_grant.code}
60
+
61
+ post oauth_token_path(params)
62
+ json_hash = JSON.parse(response.body)
63
+ expires_in = json_hash['expires_in']
64
+ access_token = json_hash['access_token']
65
+
66
+ # should be valid
67
+ Timecop.travel(expires_in.seconds.from_now - 1.second)
68
+ get oauth_test_path(:show_me_the_money, access_token: access_token)
69
+ assert_equal 200, response.status
70
+
71
+ # should not be valid
72
+ Timecop.travel(expires_in.seconds.from_now + 1.second)
73
+ get oauth_test_path(:show_me_the_money, access_token: access_token)
74
+ refute_equal 200, response.status
75
+
76
+
77
+ params = {:client_id => @client_app.client_id ,
78
+ :client_secret => @client_app.client_secret,
79
+ :refresh_token => @auth_grant.reload.refresh_token}
80
+
81
+ # make it valid again by refreshing the token
82
+ post oauth_token_path(params)
83
+ access_token = JSON.parse(response.body)['access_token']
84
+ get oauth_test_path(:show_me_the_money, access_token: access_token)
85
+ assert_equal 200, response.status
86
+ end
87
+
54
88
  end
metadata CHANGED
@@ -1,240 +1,159 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opro
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.4.2
4
+ version: 0.4.3
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - schneems
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-07 00:00:00.000000000 Z
12
+ date: 2012-11-17 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- version_requirements: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - ! '>='
19
- - !ruby/object:Gem::Version
20
- version: 3.1.0
16
+ requirement: &70297244933200 !ruby/object:Gem::Requirement
21
17
  none: false
22
- requirement: !ruby/object:Gem::Requirement
23
18
  requirements:
24
19
  - - ! '>='
25
20
  - !ruby/object:Gem::Version
26
21
  version: 3.1.0
27
- none: false
28
- prerelease: false
29
22
  type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70297244933200
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: rails
32
- version_requirements: !ruby/object:Gem::Requirement
33
- requirements:
34
- - - ! '>='
35
- - !ruby/object:Gem::Version
36
- version: 3.1.0
27
+ requirement: &70297244932520 !ruby/object:Gem::Requirement
37
28
  none: false
38
- requirement: !ruby/object:Gem::Requirement
39
29
  requirements:
40
30
  - - ! '>='
41
31
  - !ruby/object:Gem::Version
42
32
  version: 3.1.0
43
- none: false
44
- prerelease: false
45
33
  type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70297244932520
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: kramdown
48
- version_requirements: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - ! '>='
51
- - !ruby/object:Gem::Version
52
- version: !binary |-
53
- MA==
38
+ requirement: &70297244931780 !ruby/object:Gem::Requirement
54
39
  none: false
55
- requirement: !ruby/object:Gem::Requirement
56
40
  requirements:
57
41
  - - ! '>='
58
42
  - !ruby/object:Gem::Version
59
- version: !binary |-
60
- MA==
61
- none: false
62
- prerelease: false
43
+ version: '0'
63
44
  type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70297244931780
64
47
  - !ruby/object:Gem::Dependency
65
48
  name: mocha
66
- version_requirements: !ruby/object:Gem::Requirement
67
- requirements:
68
- - - ! '>='
69
- - !ruby/object:Gem::Version
70
- version: !binary |-
71
- MA==
49
+ requirement: &70297244931120 !ruby/object:Gem::Requirement
72
50
  none: false
73
- requirement: !ruby/object:Gem::Requirement
74
51
  requirements:
75
52
  - - ! '>='
76
53
  - !ruby/object:Gem::Version
77
- version: !binary |-
78
- MA==
79
- none: false
80
- prerelease: false
54
+ version: '0'
81
55
  type: :development
56
+ prerelease: false
57
+ version_requirements: *70297244931120
82
58
  - !ruby/object:Gem::Dependency
83
59
  name: timecop
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ! '>='
87
- - !ruby/object:Gem::Version
88
- version: !binary |-
89
- MA==
60
+ requirement: &70297244930600 !ruby/object:Gem::Requirement
90
61
  none: false
91
- requirement: !ruby/object:Gem::Requirement
92
62
  requirements:
93
63
  - - ! '>='
94
64
  - !ruby/object:Gem::Version
95
- version: !binary |-
96
- MA==
97
- none: false
98
- prerelease: false
65
+ version: '0'
99
66
  type: :development
67
+ prerelease: false
68
+ version_requirements: *70297244930600
100
69
  - !ruby/object:Gem::Dependency
101
70
  name: jeweler
102
- version_requirements: !ruby/object:Gem::Requirement
103
- requirements:
104
- - - ~>
105
- - !ruby/object:Gem::Version
106
- version: 1.6.4
71
+ requirement: &70297244930000 !ruby/object:Gem::Requirement
107
72
  none: false
108
- requirement: !ruby/object:Gem::Requirement
109
73
  requirements:
110
74
  - - ~>
111
75
  - !ruby/object:Gem::Version
112
76
  version: 1.6.4
113
- none: false
114
- prerelease: false
115
77
  type: :development
78
+ prerelease: false
79
+ version_requirements: *70297244930000
116
80
  - !ruby/object:Gem::Dependency
117
81
  name: bundler
118
- version_requirements: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - ! '>='
121
- - !ruby/object:Gem::Version
122
- version: 1.1.3
82
+ requirement: &70297244929400 !ruby/object:Gem::Requirement
123
83
  none: false
124
- requirement: !ruby/object:Gem::Requirement
125
84
  requirements:
126
85
  - - ! '>='
127
86
  - !ruby/object:Gem::Version
128
87
  version: 1.1.3
129
- none: false
130
- prerelease: false
131
88
  type: :development
89
+ prerelease: false
90
+ version_requirements: *70297244929400
132
91
  - !ruby/object:Gem::Dependency
133
92
  name: capybara
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ! '>='
137
- - !ruby/object:Gem::Version
138
- version: 0.4.0
93
+ requirement: &70297244928580 !ruby/object:Gem::Requirement
139
94
  none: false
140
- requirement: !ruby/object:Gem::Requirement
141
95
  requirements:
142
96
  - - ! '>='
143
97
  - !ruby/object:Gem::Version
144
98
  version: 0.4.0
145
- none: false
146
- prerelease: false
147
99
  type: :development
100
+ prerelease: false
101
+ version_requirements: *70297244928580
148
102
  - !ruby/object:Gem::Dependency
149
103
  name: launchy
150
- version_requirements: !ruby/object:Gem::Requirement
151
- requirements:
152
- - - ! '>='
153
- - !ruby/object:Gem::Version
154
- version: !binary |-
155
- MA==
104
+ requirement: &70297244927960 !ruby/object:Gem::Requirement
156
105
  none: false
157
- requirement: !ruby/object:Gem::Requirement
158
106
  requirements:
159
107
  - - ! '>='
160
108
  - !ruby/object:Gem::Version
161
- version: !binary |-
162
- MA==
163
- none: false
164
- prerelease: false
109
+ version: '0'
165
110
  type: :development
111
+ prerelease: false
112
+ version_requirements: *70297244927960
166
113
  - !ruby/object:Gem::Dependency
167
114
  name: sqlite3
168
- version_requirements: !ruby/object:Gem::Requirement
169
- requirements:
170
- - - ! '>='
171
- - !ruby/object:Gem::Version
172
- version: !binary |-
173
- MA==
115
+ requirement: &70297244927220 !ruby/object:Gem::Requirement
174
116
  none: false
175
- requirement: !ruby/object:Gem::Requirement
176
117
  requirements:
177
118
  - - ! '>='
178
119
  - !ruby/object:Gem::Version
179
- version: !binary |-
180
- MA==
181
- none: false
182
- prerelease: false
120
+ version: '0'
183
121
  type: :development
122
+ prerelease: false
123
+ version_requirements: *70297244927220
184
124
  - !ruby/object:Gem::Dependency
185
125
  name: activerecord-jdbcsqlite3-adapter
186
- version_requirements: !ruby/object:Gem::Requirement
187
- requirements:
188
- - - ! '>='
189
- - !ruby/object:Gem::Version
190
- version: !binary |-
191
- MA==
126
+ requirement: &70297244926520 !ruby/object:Gem::Requirement
192
127
  none: false
193
- requirement: !ruby/object:Gem::Requirement
194
128
  requirements:
195
129
  - - ! '>='
196
130
  - !ruby/object:Gem::Version
197
- version: !binary |-
198
- MA==
199
- none: false
200
- prerelease: false
131
+ version: '0'
201
132
  type: :development
133
+ prerelease: false
134
+ version_requirements: *70297244926520
202
135
  - !ruby/object:Gem::Dependency
203
136
  name: jdbc-sqlite3
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - ! '>='
207
- - !ruby/object:Gem::Version
208
- version: !binary |-
209
- MA==
137
+ requirement: &70297244925900 !ruby/object:Gem::Requirement
210
138
  none: false
211
- requirement: !ruby/object:Gem::Requirement
212
139
  requirements:
213
140
  - - ! '>='
214
141
  - !ruby/object:Gem::Version
215
- version: !binary |-
216
- MA==
217
- none: false
218
- prerelease: false
142
+ version: '0'
219
143
  type: :development
144
+ prerelease: false
145
+ version_requirements: *70297244925900
220
146
  - !ruby/object:Gem::Dependency
221
147
  name: devise
222
- version_requirements: !ruby/object:Gem::Requirement
223
- requirements:
224
- - - ! '>='
225
- - !ruby/object:Gem::Version
226
- version: !binary |-
227
- MA==
148
+ requirement: &70297244925300 !ruby/object:Gem::Requirement
228
149
  none: false
229
- requirement: !ruby/object:Gem::Requirement
230
150
  requirements:
231
151
  - - ! '>='
232
152
  - !ruby/object:Gem::Version
233
- version: !binary |-
234
- MA==
235
- none: false
236
- prerelease: false
153
+ version: '0'
237
154
  type: :development
155
+ prerelease: false
156
+ version_requirements: *70297244925300
238
157
  description: ! ' Enable OAuth clients (iphone, android, web sites, etc.) to access
239
158
  and use your Rails application, what you do with it is up to you'
240
159
  email: richard.schneeman@gmail.com
@@ -350,32 +269,29 @@ files:
350
269
  homepage: http://github.com/schneems/opro
351
270
  licenses:
352
271
  - MIT
353
- post_install_message:
272
+ post_install_message:
354
273
  rdoc_options: []
355
274
  require_paths:
356
275
  - lib
357
276
  required_ruby_version: !ruby/object:Gem::Requirement
277
+ none: false
358
278
  requirements:
359
279
  - - ! '>='
360
280
  - !ruby/object:Gem::Version
281
+ version: '0'
361
282
  segments:
362
283
  - 0
363
- hash: 2
364
- version: !binary |-
365
- MA==
366
- none: false
284
+ hash: -2443235425097345828
367
285
  required_rubygems_version: !ruby/object:Gem::Requirement
286
+ none: false
368
287
  requirements:
369
288
  - - ! '>='
370
289
  - !ruby/object:Gem::Version
371
- version: !binary |-
372
- MA==
373
- none: false
290
+ version: '0'
374
291
  requirements: []
375
- rubyforge_project:
376
- rubygems_version: 1.8.24
377
- signing_key:
292
+ rubyforge_project:
293
+ rubygems_version: 1.8.10
294
+ signing_key:
378
295
  specification_version: 3
379
296
  summary: oPRO turns your Rails application into an OAuth Provider
380
297
  test_files: []
381
- ...