authlogic-connect 0.0.3.9 → 0.0.4.03
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +9 -5
- data/Rakefile +7 -3
- data/lib/authlogic-connect.rb +2 -2
- data/lib/authlogic_connect/{token.rb → access_token.rb} +1 -1
- data/lib/authlogic_connect/authlogic_connect.rb +0 -0
- data/lib/authlogic_connect/common/session.rb +13 -10
- data/lib/authlogic_connect/common/state.rb +21 -5
- data/lib/authlogic_connect/common/user.rb +27 -65
- data/lib/authlogic_connect/common/variables.rb +75 -11
- data/lib/authlogic_connect/oauth/helper.rb +0 -0
- data/lib/authlogic_connect/oauth/process.rb +19 -12
- data/lib/authlogic_connect/oauth/session.rb +15 -11
- data/lib/authlogic_connect/oauth/state.rb +19 -13
- data/lib/authlogic_connect/oauth/tokens/aol_token.rb +2 -0
- data/lib/authlogic_connect/oauth/tokens/google_token.rb +11 -11
- data/lib/authlogic_connect/oauth/tokens/meetup_token.rb +12 -0
- data/lib/authlogic_connect/oauth/tokens/netflix_token.rb +10 -0
- data/lib/authlogic_connect/oauth/tokens/oauth_token.rb +6 -2
- data/lib/authlogic_connect/oauth/tokens/ohloh_token.rb +9 -0
- data/lib/authlogic_connect/oauth/user.rb +9 -14
- data/lib/authlogic_connect/oauth/variables.rb +9 -0
- data/lib/authlogic_connect/openid/process.rb +49 -5
- data/lib/authlogic_connect/openid/session.rb +13 -34
- data/lib/authlogic_connect/openid/state.rb +45 -44
- data/lib/authlogic_connect/openid/tokens/openid_token.rb +1 -1
- data/lib/authlogic_connect/openid/user.rb +5 -29
- data/lib/authlogic_connect/openid/variables.rb +2 -2
- data/lib/open_id_authentication.rb +0 -1
- data/test/controllers/test_users_controller.rb +0 -0
- data/test/libs/database.rb +0 -0
- data/test/libs/user.rb +6 -2
- data/test/libs/user_session.rb +0 -0
- data/test/test_helper.rb +1 -1
- data/test/test_user.rb +5 -66
- metadata +10 -6
data/README.markdown
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# AuthlogicConnect
|
2
2
|
|
3
|
-
AuthlogicConnect is an extension of the Authlogic library to add
|
3
|
+
AuthlogicConnect is an extension of the Authlogic library that aims to add complete and seamless Oauth and OpenID support to your application.
|
4
4
|
|
5
|
-
It allows you to login through any of the 30+ Oauth and OpenID providers on the Internet.
|
5
|
+
It allows you to login through any of the 30+ Oauth and OpenID providers on the Internet without having to write any of the logic yourself.
|
6
6
|
|
7
7
|
That makes life easy and gives you a lot of power.
|
8
8
|
|
@@ -32,7 +32,7 @@ AuthlogicConnect currently allows you to login with 7 Oauth providers and all th
|
|
32
32
|
- [OpenID Providers](http://en.wikipedia.org/wiki/List_of_OpenID_providers)
|
33
33
|
- [More OpenID](http://openid.net/get-an-openid/)
|
34
34
|
|
35
|
-
## Install
|
35
|
+
## Install
|
36
36
|
|
37
37
|
### 1. Install AuthlogicConnect
|
38
38
|
|
@@ -57,7 +57,7 @@ Rails 3: `Gemfile`
|
|
57
57
|
gem "oauth2"
|
58
58
|
gem "authlogic-connect"
|
59
59
|
|
60
|
-
### 3. Add the
|
60
|
+
### 3. Add the OpenIdAuthentication.store
|
61
61
|
|
62
62
|
Do to [some strange problem](http://github.com/openid/ruby-openid/issues#issue/1) I have yet to really understand, Rails 2.3.5 doesn't like when `OpenIdAuthentication.store` is null, which means it uses the "in memory" store and for some reason fails.
|
63
63
|
|
@@ -237,4 +237,8 @@ Thanks for the people that are already extending the project, all the input maki
|
|
237
237
|
|
238
238
|
Feel free to add to the wiki if you figure things out or make new distinctions.
|
239
239
|
|
240
|
-
|
240
|
+
## Flow
|
241
|
+
|
242
|
+
- Try to create a session
|
243
|
+
- Session logs into provider
|
244
|
+
- On success, if no user, redirect to User#create
|
data/Rakefile
CHANGED
@@ -6,11 +6,11 @@ require 'rake/gempackagetask'
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = "authlogic-connect"
|
8
8
|
s.author = "Lance Pollard"
|
9
|
-
s.version = "0.0.
|
10
|
-
s.summary = "Authlogic Connect:
|
9
|
+
s.version = "0.0.4.03"
|
10
|
+
s.summary = "Authlogic Connect: Oauth and OpenID made dead simple"
|
11
11
|
s.homepage = "http://github.com/viatropos/authlogic-connect"
|
12
12
|
s.email = "lancejpollard@gmail.com"
|
13
|
-
s.description = "
|
13
|
+
s.description = "Oauth and OpenID made dead simple"
|
14
14
|
s.has_rdoc = true
|
15
15
|
s.rubyforge_project = "authlogic-connect"
|
16
16
|
s.platform = Gem::Platform::RUBY
|
@@ -68,4 +68,8 @@ Rake::RDocTask.new do |rdoc|
|
|
68
68
|
rdoc.rdoc_files.add(files)
|
69
69
|
rdoc.main = "README.markdown"
|
70
70
|
rdoc.title = spec.summary
|
71
|
+
end
|
72
|
+
|
73
|
+
task :yank do
|
74
|
+
`gem yank #{spec.name} -v #{spec.version}`
|
71
75
|
end
|
data/lib/authlogic-connect.rb
CHANGED
@@ -11,13 +11,13 @@ require "#{this}/open_id_authentication"
|
|
11
11
|
require "#{library}/ext"
|
12
12
|
require "#{library}/authlogic_connect"
|
13
13
|
require "#{library}/callback_filter"
|
14
|
-
require "#{library}/
|
14
|
+
require "#{library}/access_token"
|
15
15
|
require "#{library}/openid"
|
16
16
|
require "#{library}/oauth"
|
17
17
|
require "#{library}/common"
|
18
18
|
require "#{library}/engine" if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
19
19
|
|
20
|
-
custom_models = ["#{library}/
|
20
|
+
custom_models = ["#{library}/access_token"]
|
21
21
|
custom_models += Dir["#{library}/oauth/tokens"]
|
22
22
|
custom_models += Dir["#{library}/openid/tokens"]
|
23
23
|
|
File without changes
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module AuthlogicConnect::Common
|
2
2
|
module Session
|
3
3
|
|
4
|
-
def self.included(base)
|
4
|
+
def self.included(base)
|
5
5
|
base.class_eval do
|
6
6
|
include Variables
|
7
7
|
include InstanceMethods
|
@@ -10,18 +10,21 @@ module AuthlogicConnect::Common
|
|
10
10
|
|
11
11
|
module InstanceMethods
|
12
12
|
|
13
|
-
# core save method coordinating how to save the session
|
13
|
+
# core save method coordinating how to save the session.
|
14
|
+
# want to destroy the block if we redirect to a remote service, that's it.
|
15
|
+
# otherwise the block contains the render methods we wan to use
|
14
16
|
def save(&block)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
self.errors.clear
|
18
|
+
# log_state
|
19
|
+
authenticate_via_protocol(block_given?) do |redirecting|
|
20
|
+
block = nil if redirecting
|
21
|
+
result = super(&block)
|
22
|
+
cleanup_authentication_session unless block.nil?
|
23
|
+
result
|
20
24
|
end
|
21
|
-
block = nil if block_destroyed
|
22
|
-
super(&block)
|
23
25
|
end
|
26
|
+
|
24
27
|
end
|
25
28
|
|
26
29
|
end
|
27
|
-
end
|
30
|
+
end
|
@@ -1,16 +1,32 @@
|
|
1
1
|
# This class holds query/state variables common to oauth and openid
|
2
2
|
module AuthlogicConnect::Common::State
|
3
3
|
|
4
|
-
def
|
5
|
-
!
|
4
|
+
def auth_controller?
|
5
|
+
!auth_controller.blank?
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
def auth_params?
|
9
|
-
!auth_params.blank?
|
9
|
+
auth_controller? && !auth_params.blank?
|
10
|
+
end
|
11
|
+
|
12
|
+
def auth_session?
|
13
|
+
!auth_session.blank?
|
10
14
|
end
|
11
15
|
|
12
16
|
def is_auth_session?
|
13
17
|
self.is_a?(Authlogic::Session::Base)
|
14
18
|
end
|
15
19
|
|
16
|
-
|
20
|
+
def start_authentication?
|
21
|
+
start_oauth? || start_openid?
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_password_with_oauth?
|
25
|
+
!using_openid? && super
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_password_with_openid?
|
29
|
+
!using_oauth? && super
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -14,18 +14,18 @@ module AuthlogicConnect::Common::User
|
|
14
14
|
|
15
15
|
def self.included(base)
|
16
16
|
base.class_eval do
|
17
|
-
has_many :
|
18
|
-
belongs_to :active_token, :class_name => "
|
19
|
-
accepts_nested_attributes_for :
|
17
|
+
has_many :access_tokens, :class_name => "AccessToken", :dependent => :destroy
|
18
|
+
belongs_to :active_token, :class_name => "AccessToken", :dependent => :destroy
|
19
|
+
accepts_nested_attributes_for :access_tokens, :active_token
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def authenticated_with
|
24
|
-
@authenticated_with ||= self.
|
24
|
+
@authenticated_with ||= self.access_tokens.collect{|t| t.service_name.to_s}
|
25
25
|
end
|
26
26
|
|
27
27
|
def authenticated_with?(service)
|
28
|
-
self.
|
28
|
+
self.access_tokens.detect{|t| t.service_name.to_s == service.to_s}
|
29
29
|
end
|
30
30
|
|
31
31
|
def update_attributes(attributes, &block)
|
@@ -38,7 +38,7 @@ module AuthlogicConnect::Common::User
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def get_token(service_name)
|
41
|
-
self.
|
41
|
+
self.access_tokens.detect {|i| i.service_name.to_s == service_name.to_s}
|
42
42
|
end
|
43
43
|
|
44
44
|
# core save method coordinating how to save the user.
|
@@ -46,68 +46,30 @@ module AuthlogicConnect::Common::User
|
|
46
46
|
# authentication mission we are trying to accomplish.
|
47
47
|
# instead, we just return save as false.
|
48
48
|
# the next time around, when we recieve the callback,
|
49
|
-
# we will run the validations
|
49
|
+
# we will run the validations.
|
50
|
+
# when you call 'current_user_session' in ApplicationController,
|
51
|
+
# it leads to calling 'save' on this User object via "session.record.save",
|
52
|
+
# from the 'persisting?' method. So we don't want any of this to occur
|
53
|
+
# when that save is called, and the only way to check currently is
|
54
|
+
# to check if there is a block_given?
|
50
55
|
def save(options = {}, &block)
|
51
|
-
|
56
|
+
self.errors.clear
|
57
|
+
# log_state
|
52
58
|
options = {} if options == false
|
53
|
-
|
54
|
-
|
55
|
-
end
|
56
|
-
# forces you to validate, maybe get rid of if needed,
|
57
|
-
# but everything depends on this
|
58
|
-
if ActiveRecord::VERSION::MAJOR < 3
|
59
|
-
result = super(true) # validate!
|
60
|
-
else
|
61
|
-
result = super(options.merge(:validate => true))
|
62
|
-
end
|
63
|
-
# debug_user_save_post
|
64
|
-
yield(result) if block_given? # give back to controller
|
59
|
+
options[:validate] = true unless options.has_key?(:validate)
|
60
|
+
save_options = ActiveRecord::VERSION::MAJOR < 3 ? options[:validate] : options
|
65
61
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
# it only reaches this point once it has returned, or you
|
78
|
-
# have manually skipped the redirect and save was called directly.
|
79
|
-
def cleanup_auth_session
|
80
|
-
cleanup_oauth_session
|
81
|
-
cleanup_openid_session
|
82
|
-
end
|
83
|
-
|
84
|
-
def validate_password_with_oauth?
|
85
|
-
!using_openid? && super
|
86
|
-
end
|
87
|
-
|
88
|
-
def validate_password_with_openid?
|
89
|
-
!using_oauth? && super
|
90
|
-
end
|
91
|
-
|
92
|
-
# test methods for dev/debugging, commented out by default
|
93
|
-
def debug_user_save_pre(options = {}, &block)
|
94
|
-
puts "USER SAVE "
|
95
|
-
puts "block_given? #{block_given?.to_s}"
|
96
|
-
puts "using_oauth? #{using_oauth?.to_s}"
|
97
|
-
puts "using_openid? #{using_openid?.to_s}"
|
98
|
-
puts "authenticating_with_oauth? #{authenticating_with_oauth?.to_s}"
|
99
|
-
puts "authenticating_with_openid? #{authenticating_with_openid?.to_s}"
|
100
|
-
puts "validate_password_with_oauth? #{validate_password_with_oauth?.to_s}"
|
101
|
-
puts "validate_password_with_openid? #{validate_password_with_openid?.to_s}"
|
102
|
-
puts "!using_openid? && require_password? #{(!using_openid? && require_password?).to_s}"
|
103
|
-
end
|
104
|
-
|
105
|
-
def debug_user_save_post
|
106
|
-
puts "ERRORS: #{errors.full_messages}"
|
107
|
-
puts "using_oauth? #{using_oauth?.to_s}"
|
108
|
-
puts "using_openid? #{using_openid?.to_s}"
|
109
|
-
puts "validate_password_with_oauth? #{validate_password_with_oauth?.to_s}"
|
110
|
-
puts "validate_password_with_openid? #{validate_password_with_openid?.to_s}"
|
62
|
+
# kill the block if we're starting authentication
|
63
|
+
authenticate_via_protocol(block_given?, options) do |redirecting|
|
64
|
+
block = nil if redirecting
|
65
|
+
# forces you to validate, only if a block is given
|
66
|
+
result = super(save_options) # validate!
|
67
|
+
unless block.nil?
|
68
|
+
cleanup_authentication_session(options)
|
69
|
+
yield(result)
|
70
|
+
end
|
71
|
+
result
|
72
|
+
end
|
111
73
|
end
|
112
74
|
|
113
75
|
end
|
@@ -1,19 +1,18 @@
|
|
1
1
|
module AuthlogicConnect::Common::Variables
|
2
2
|
include AuthlogicConnect::Common::State
|
3
|
+
|
4
|
+
attr_reader :processing_authentication
|
3
5
|
|
4
|
-
def
|
5
|
-
is_auth_session? ?
|
6
|
+
def auth_class
|
7
|
+
is_auth_session? ? self.class : session_class
|
6
8
|
end
|
7
9
|
|
8
|
-
def
|
9
|
-
|
10
|
-
auth_controller.session.keys.each do |key|
|
11
|
-
auth_controller.session[key.to_s] = auth_controller.session.delete(key) if key.to_s =~ /^OpenID/
|
12
|
-
end
|
13
|
-
auth_controller.session
|
10
|
+
def auth_controller
|
11
|
+
is_auth_session? ? controller : session_class.controller
|
14
12
|
end
|
15
13
|
|
16
14
|
def auth_params
|
15
|
+
return nil unless auth_controller?
|
17
16
|
auth_controller.params.symbolize_keys!
|
18
17
|
auth_controller.params.keys.each do |key|
|
19
18
|
auth_controller.params[key.to_s] = auth_controller.params.delete(key) if key.to_s =~ /^OpenID/
|
@@ -21,6 +20,15 @@ module AuthlogicConnect::Common::Variables
|
|
21
20
|
auth_controller.params
|
22
21
|
end
|
23
22
|
|
23
|
+
def auth_session
|
24
|
+
return nil unless auth_controller?
|
25
|
+
auth_controller.session.symbolize_keys!
|
26
|
+
auth_controller.session.keys.each do |key|
|
27
|
+
auth_controller.session[key.to_s] = auth_controller.session.delete(key) if key.to_s =~ /^OpenID/
|
28
|
+
end
|
29
|
+
auth_controller.session
|
30
|
+
end
|
31
|
+
|
24
32
|
def auth_callback_url(options = {})
|
25
33
|
auth_controller.url_for({:controller => auth_controller.controller_name, :action => auth_controller.action_name}.merge(options))
|
26
34
|
end
|
@@ -31,10 +39,11 @@ module AuthlogicConnect::Common::Variables
|
|
31
39
|
end
|
32
40
|
|
33
41
|
# auth_params and auth_session attributes are all String!
|
34
|
-
def from_session_or_params(
|
35
|
-
|
42
|
+
def from_session_or_params(attribute)
|
43
|
+
return nil unless auth_controller?
|
44
|
+
key = attribute.is_a?(Symbol) ? attribute : attribute.to_sym
|
36
45
|
result = auth_params[key] if (auth_params && auth_params[key])
|
37
|
-
result = auth_session[key] if result.
|
46
|
+
result = auth_session[key] if (result.nil? || result.blank?)
|
38
47
|
result
|
39
48
|
end
|
40
49
|
|
@@ -42,6 +51,8 @@ module AuthlogicConnect::Common::Variables
|
|
42
51
|
# uncertain as to how they are saved. So this makes sure if we are
|
43
52
|
# logging in, it must be saving the session, otherwise the user.
|
44
53
|
def correct_request_class?
|
54
|
+
return false unless auth_params?
|
55
|
+
|
45
56
|
if is_auth_session?
|
46
57
|
auth_type.to_s == "session"
|
47
58
|
else
|
@@ -53,6 +64,55 @@ module AuthlogicConnect::Common::Variables
|
|
53
64
|
|
54
65
|
end
|
55
66
|
|
67
|
+
def remove_session_key(key)
|
68
|
+
keys = key.is_a?(Symbol) ? [key, key.to_s] : [key, key.to_sym]
|
69
|
+
keys.each {|k| auth_session.delete(k)}
|
70
|
+
end
|
71
|
+
|
72
|
+
# wraps the call to "save" (in yield).
|
73
|
+
# reason being, we need to somehow not allow oauth/openid validations to run
|
74
|
+
# when we don't have a block. We can't know that using class methods, so we create
|
75
|
+
# this property "processing_authentication", which is used in the validation method.
|
76
|
+
# it's value is set to "block_given", which is the value of block_given?
|
77
|
+
def authenticate_via_protocol(block_given = false, options = {}, &block)
|
78
|
+
@processing_authentication = auth_controller? && block_given
|
79
|
+
saved = yield start_authentication?
|
80
|
+
@processing_authentication = false
|
81
|
+
saved
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns boolean
|
85
|
+
def authentication_protocol(with, phase)
|
86
|
+
returning(send("#{phase.to_s}_#{with.to_s}?")) do |ready|
|
87
|
+
send("#{phase.to_s}_#{with.to_s}") if ready
|
88
|
+
end if send("using_#{with.to_s}?")
|
89
|
+
end
|
90
|
+
|
91
|
+
# it only reaches this point once it has returned, or you
|
92
|
+
# have manually skipped the redirect and save was called directly.
|
93
|
+
def cleanup_authentication_session(options = {}, &block)
|
94
|
+
unless (options.has_key?(:keep_session) && options[:keep_session])
|
95
|
+
%w(oauth openid).each do |type|
|
96
|
+
send("cleanup_#{type.to_s}_session")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def log(*methods)
|
102
|
+
methods.each do |method|
|
103
|
+
puts "#{method.to_s}: #{send(method).inspect}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def log_state
|
108
|
+
log(:correct_request_class?)
|
109
|
+
log(:using_oauth?, :start_oauth?, :complete_oauth?)
|
110
|
+
log(:oauth_request?, :oauth_response?, :stored_oauth_token_and_secret?)
|
111
|
+
log(:using_openid?, :start_openid?, :complete_openid?, :openid_request?, :openid_response?)
|
112
|
+
log(:authenticating_with_openid?)
|
113
|
+
log(:stored_oauth_token_and_secret)
|
114
|
+
end
|
115
|
+
|
56
116
|
# because we may need to store 6+ session variables, all with pretty lengthy names,
|
57
117
|
# might as well just tinify them.
|
58
118
|
# just an idea
|
@@ -70,4 +130,8 @@ module AuthlogicConnect::Common::Variables
|
|
70
130
|
@optimized_session_keys[key]
|
71
131
|
end
|
72
132
|
|
133
|
+
def auto_register?
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
73
137
|
end
|
File without changes
|
@@ -4,15 +4,13 @@ module AuthlogicConnect::Oauth::Process
|
|
4
4
|
|
5
5
|
# Step 2: after save is called, it runs this method for validation
|
6
6
|
def validate_by_oauth
|
7
|
-
|
8
|
-
|
9
|
-
restore_attributes
|
10
|
-
complete_oauth_transaction
|
7
|
+
if processing_authentication
|
8
|
+
authentication_protocol(:oauth, :start) || authentication_protocol(:oauth, :complete)
|
11
9
|
end
|
12
10
|
end
|
13
11
|
|
14
12
|
# Step 3: if new_oauth_request?, redirect to oauth provider
|
15
|
-
def
|
13
|
+
def start_oauth
|
16
14
|
save_oauth_session
|
17
15
|
authorize_url = token_class.authorize_url(auth_callback_url) do |request_token|
|
18
16
|
save_auth_session_token(request_token) # only for oauth version 1
|
@@ -20,6 +18,17 @@ module AuthlogicConnect::Oauth::Process
|
|
20
18
|
auth_controller.redirect_to authorize_url
|
21
19
|
end
|
22
20
|
|
21
|
+
# Step 4: on callback, run this method
|
22
|
+
def complete_oauth
|
23
|
+
# implemented in User and Session Oauth modules
|
24
|
+
unless new_oauth_request? # shouldn't be validating if it's redirecting...
|
25
|
+
restore_attributes
|
26
|
+
complete_oauth_transaction
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
23
32
|
# Step 3a: save our passed-parameters into the session,
|
24
33
|
# so we can retrieve them after the redirect calls back
|
25
34
|
def save_oauth_session
|
@@ -45,11 +54,6 @@ module AuthlogicConnect::Oauth::Process
|
|
45
54
|
def restore_attributes
|
46
55
|
end
|
47
56
|
|
48
|
-
# Step 4: on callback, run this method
|
49
|
-
def authenticate_with_oauth
|
50
|
-
# implemented in User and Session Oauth modules
|
51
|
-
end
|
52
|
-
|
53
57
|
# Step last, after the response
|
54
58
|
# having lots of trouble testing logging and out multiple times,
|
55
59
|
# so there needs to be a solid way to know when a user has messed up loggin in.
|
@@ -61,8 +65,11 @@ module AuthlogicConnect::Oauth::Process
|
|
61
65
|
:oauth_provider,
|
62
66
|
:auth_callback_method,
|
63
67
|
:oauth_request_token,
|
64
|
-
:oauth_request_token_secret
|
65
|
-
|
68
|
+
:oauth_request_token_secret,
|
69
|
+
:_key,
|
70
|
+
:_token,
|
71
|
+
:_secret,
|
72
|
+
].each {|key| remove_session_key(key)}
|
66
73
|
end
|
67
74
|
|
68
75
|
end
|