| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Catalyst::Authentication::Store::Tangram; |
|
2
|
1
|
|
|
1
|
|
874
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
32
|
|
|
3
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
30
|
|
|
4
|
1
|
|
|
1
|
|
12
|
use base qw/Class::Accessor::Fast/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
924
|
|
|
5
|
1
|
|
|
1
|
|
4407
|
use Scalar::Util qw/blessed/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
108
|
|
|
6
|
1
|
|
|
1
|
|
650
|
use Catalyst::Authentication::Store::Tangram::User; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
use Catalyst::Utils (); |
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
our $VERSION = '0.010'; |
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw/ |
|
12
|
|
|
|
|
|
|
tangram_model |
|
13
|
|
|
|
|
|
|
tangram_user_class |
|
14
|
|
|
|
|
|
|
user_class |
|
15
|
|
|
|
|
|
|
storage_method |
|
16
|
|
|
|
|
|
|
use_roles |
|
17
|
|
|
|
|
|
|
role_relation |
|
18
|
|
|
|
|
|
|
role_name_field |
|
19
|
|
|
|
|
|
|
user_results_filter |
|
20
|
|
|
|
|
|
|
/); |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
sub _get_storage { |
|
23
|
|
|
|
|
|
|
my ($self, $c) = @_; |
|
24
|
|
|
|
|
|
|
$c->model($self->tangram_model)->${\$self->storage_method}(); |
|
25
|
|
|
|
|
|
|
} |
|
26
|
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
sub new { |
|
28
|
|
|
|
|
|
|
my ($class, $config, $app, $realm) = @_; |
|
29
|
|
|
|
|
|
|
die("tangram_user_class key must be defined in config") |
|
30
|
|
|
|
|
|
|
unless $config->{tangram_user_class}; |
|
31
|
|
|
|
|
|
|
$config->{tangram_model} ||= 'Tangram'; |
|
32
|
|
|
|
|
|
|
$config->{storage_method} ||= 'storage'; |
|
33
|
|
|
|
|
|
|
$config->{user_class} ||= __PACKAGE__ . '::User'; |
|
34
|
|
|
|
|
|
|
$config->{use_roles} ||= 0; |
|
35
|
|
|
|
|
|
|
$config->{use_roles} = 0 if $config->{use_roles} =~ /false/i; |
|
36
|
|
|
|
|
|
|
die("No role_relation config option set, cannot use roles") |
|
37
|
|
|
|
|
|
|
if (!length($config->{role_relation}) && $config->{use_roles}); |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
Catalyst::Utils::ensure_class_loaded($config->{tangram_user_class}); |
|
40
|
|
|
|
|
|
|
Catalyst::Utils::ensure_class_loaded($config->{user_class}); |
|
41
|
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
bless { %$config }, $class; |
|
43
|
|
|
|
|
|
|
} |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
sub find_user { |
|
46
|
|
|
|
|
|
|
my ($self, $authinfo, $c) = @_; |
|
47
|
|
|
|
|
|
|
my $tangram_class = $self->tangram_user_class; |
|
48
|
|
|
|
|
|
|
my $storage = $self->_get_storage($c); |
|
49
|
|
|
|
|
|
|
my $remote = $storage->remote($tangram_class); |
|
50
|
|
|
|
|
|
|
my $filter; |
|
51
|
|
|
|
|
|
|
foreach my $key (keys %$authinfo) { |
|
52
|
|
|
|
|
|
|
if (defined $filter) { |
|
53
|
|
|
|
|
|
|
$filter = $filter & $remote->{$key} eq $authinfo->{$key}; |
|
54
|
|
|
|
|
|
|
} |
|
55
|
|
|
|
|
|
|
else { |
|
56
|
|
|
|
|
|
|
$filter = $remote->{$key} eq $authinfo->{$key}; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
} |
|
59
|
|
|
|
|
|
|
my @result = $storage->select($remote, filter => $filter); |
|
60
|
|
|
|
|
|
|
if ($self->user_results_filter) { |
|
61
|
|
|
|
|
|
|
@result = grep { $self->user_results_filter->($_) } @result; |
|
62
|
|
|
|
|
|
|
} |
|
63
|
|
|
|
|
|
|
if (@result) { |
|
64
|
|
|
|
|
|
|
return $self->user_class->new($storage, $result[0], $self); |
|
65
|
|
|
|
|
|
|
} |
|
66
|
|
|
|
|
|
|
return; |
|
67
|
|
|
|
|
|
|
} |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
sub for_session { |
|
70
|
|
|
|
|
|
|
my ($self, $user) = @_; |
|
71
|
|
|
|
|
|
|
return $user->id; |
|
72
|
|
|
|
|
|
|
} |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub from_session { |
|
75
|
|
|
|
|
|
|
my ($self, $id) = @_; |
|
76
|
|
|
|
|
|
|
my $tangram_class = $self->tangram_user_class; |
|
77
|
|
|
|
|
|
|
my $tangram_user; |
|
78
|
|
|
|
|
|
|
eval { $tangram_user = $self->_get_storage->load($id) }; |
|
79
|
|
|
|
|
|
|
return if $@ or !$tangram_user; |
|
80
|
|
|
|
|
|
|
return $self->user_class->new($self->_get_storage, $tangram_user, $self); # FIXME - $c arg for get_storage. |
|
81
|
|
|
|
|
|
|
} |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
sub user_supports { |
|
84
|
|
|
|
|
|
|
my $class = shift; |
|
85
|
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
return Catalyst::Authentication::Store::Tangram::User->supports(@_); |
|
87
|
|
|
|
|
|
|
} |
|
88
|
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
sub lookup_roles { |
|
90
|
|
|
|
|
|
|
my ($self, $user_ob) = @_; |
|
91
|
|
|
|
|
|
|
return undef unless $self->use_roles; |
|
92
|
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
my @roles = $user_ob->${ \$self->role_relation() }(); |
|
94
|
|
|
|
|
|
|
@roles = @{ $roles[0] } # Deal with either a list or listref return |
|
95
|
|
|
|
|
|
|
if (1 == scalar(@roles) and 'ARRAY' eq ref($roles[0])); |
|
96
|
|
|
|
|
|
|
if ($self->role_name_field) { |
|
97
|
|
|
|
|
|
|
return map { $_->${\$self->role_name_field}() } @roles; |
|
98
|
|
|
|
|
|
|
} |
|
99
|
|
|
|
|
|
|
else { |
|
100
|
|
|
|
|
|
|
return @roles; |
|
101
|
|
|
|
|
|
|
} |
|
102
|
|
|
|
|
|
|
} |
|
103
|
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
1; |
|
105
|
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
=head1 NAME |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
Catalyst::Authentication::Store::Tangram - A storage class for Catalyst authentication from a class stored in Tangram |
|
109
|
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
use Catalyst qw/ |
|
113
|
|
|
|
|
|
|
Authentication |
|
114
|
|
|
|
|
|
|
/; |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
__PACKAGE__->config( authentication => { |
|
117
|
|
|
|
|
|
|
default_realm => 'members', |
|
118
|
|
|
|
|
|
|
realms => { |
|
119
|
|
|
|
|
|
|
members => { |
|
120
|
|
|
|
|
|
|
credential => { |
|
121
|
|
|
|
|
|
|
class => 'Password', |
|
122
|
|
|
|
|
|
|
password_field => 'password', |
|
123
|
|
|
|
|
|
|
password_type => 'clear' |
|
124
|
|
|
|
|
|
|
}, |
|
125
|
|
|
|
|
|
|
store => { |
|
126
|
|
|
|
|
|
|
class => 'Tangram', |
|
127
|
|
|
|
|
|
|
tangram_user_class => 'Users', |
|
128
|
|
|
|
|
|
|
tangram_model => 'Tangram', |
|
129
|
|
|
|
|
|
|
storage_method => 'storage', # $c->model('Tangram')->storage use_roles => 1, |
|
130
|
|
|
|
|
|
|
role_relation -> 'authority', |
|
131
|
|
|
|
|
|
|
role_name_field => 'name', |
|
132
|
|
|
|
|
|
|
}, |
|
133
|
|
|
|
|
|
|
}, |
|
134
|
|
|
|
|
|
|
}, |
|
135
|
|
|
|
|
|
|
}); |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# Log a user in: |
|
138
|
|
|
|
|
|
|
sub login : Global { |
|
139
|
|
|
|
|
|
|
my ( $self, $c ) = @_; |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
$c->authenticate({ |
|
142
|
|
|
|
|
|
|
email_address => $c->req->param('email_address'), |
|
143
|
|
|
|
|
|
|
password => $c->req->param('password'), |
|
144
|
|
|
|
|
|
|
}); |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
148
|
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
The Catalyst::Authentication::Store::Tangram class provides access to |
|
150
|
|
|
|
|
|
|
authentication information stored in a database via L<Tangram>. |
|
151
|
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
=head1 CONFIGURATION |
|
153
|
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
The Tangram authentication store is activated by setting the store configuration |
|
155
|
|
|
|
|
|
|
class element to I<Tangram> as shown above. See the |
|
156
|
|
|
|
|
|
|
L<Catalyst::Plugin::Authentication> documentation for more details on |
|
157
|
|
|
|
|
|
|
configuring the store. |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
The Tangram storage module has several configuration options |
|
160
|
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
authentication => { |
|
162
|
|
|
|
|
|
|
default_realm => 'members', |
|
163
|
|
|
|
|
|
|
realms => { |
|
164
|
|
|
|
|
|
|
members => { |
|
165
|
|
|
|
|
|
|
credential => { |
|
166
|
|
|
|
|
|
|
# ... |
|
167
|
|
|
|
|
|
|
}, |
|
168
|
|
|
|
|
|
|
store => { |
|
169
|
|
|
|
|
|
|
class => 'Tangram', |
|
170
|
|
|
|
|
|
|
user_class => 'Users', |
|
171
|
|
|
|
|
|
|
tangram_model => 'Tangram', |
|
172
|
|
|
|
|
|
|
storage_method => 'storage', # $c->model('Tangram')->storage |
|
173
|
|
|
|
|
|
|
}, |
|
174
|
|
|
|
|
|
|
}, |
|
175
|
|
|
|
|
|
|
}, |
|
176
|
|
|
|
|
|
|
} |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=over |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
=item class |
|
181
|
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
Class is part of the core L<Catalyst::Plugin::Authentication> module, it contains the class name of the store to be used. |
|
183
|
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
=item tangram_user_class |
|
185
|
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
Contains the class name of the class persisted in your Tangram schema to use as |
|
187
|
|
|
|
|
|
|
the source for user information. |
|
188
|
|
|
|
|
|
|
This config item is B<REQUIRED>. This class name is used to get a Tangram remote |
|
189
|
|
|
|
|
|
|
object when constructing a search for your user when first authenticating, and |
|
190
|
|
|
|
|
|
|
also this is the class which the ->load method is called on to restore the user |
|
191
|
|
|
|
|
|
|
from a session. |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=item tangram_model |
|
194
|
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
Contains the class name (as passed to $c->model()) of the Tangram model to use |
|
196
|
|
|
|
|
|
|
as the source for user information. |
|
197
|
|
|
|
|
|
|
This config item is REQUIRED. The I<storage_method> method will be invoked on |
|
198
|
|
|
|
|
|
|
this class to get the L<Tangram::Storage> instance to restore the user from. |
|
199
|
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
=item storage_method |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
Contains the method to call on the I<tangram_model> to retrieve the instance of |
|
203
|
|
|
|
|
|
|
L<Tangram::Storage> which users are looked up from. |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=item user_class |
|
206
|
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
Contains the class which the user object is blessed into. This class is usually |
|
208
|
|
|
|
|
|
|
L<Catalyst::Authentication::Store::Tangram::User>, but you can sub-class that |
|
209
|
|
|
|
|
|
|
class and have your subclass used instead by setting this configuration |
|
210
|
|
|
|
|
|
|
parameter. You will not need to use this setting unless you are doing unusual |
|
211
|
|
|
|
|
|
|
things with the user class. |
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
=item use_roles |
|
214
|
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
Activates role support if set to '1' |
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
=item role_relation |
|
218
|
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
The name of the method to call on your Tangram user object to retrieve an array |
|
220
|
|
|
|
|
|
|
of roles for this user. |
|
221
|
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
This field may be a L<Tangram::Type::Array::FromMany>, or a |
|
223
|
|
|
|
|
|
|
L<Tangram::Type::Array::FromOne> (in which case you will also need to use |
|
224
|
|
|
|
|
|
|
I<role_name_field>), or it may be your own function which returns a list of |
|
225
|
|
|
|
|
|
|
roles.. |
|
226
|
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
=item role_name_field |
|
228
|
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
The name of the field to retrieve the name of the role from on the Tangram |
|
230
|
|
|
|
|
|
|
class representing roles. Note that if this configuration parameter isn't |
|
231
|
|
|
|
|
|
|
supplied, then the list returned by the method call to role_relation will be |
|
232
|
|
|
|
|
|
|
used directly. |
|
233
|
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
=back |
|
235
|
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=head1 METHODS |
|
237
|
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
=head2 new ( $config, $app, $realm ) |
|
239
|
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
Simple constructor, returns a blessed reference to the store object instance. |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
=head2 find_user ( $authinfo, $c ) |
|
243
|
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
I<$auth_info> is expected to be a hash with the keys being field names on your |
|
245
|
|
|
|
|
|
|
Tangram user object, and the values being what those fields should be matched |
|
246
|
|
|
|
|
|
|
against. A tangram select will be built from the supplied authentication |
|
247
|
|
|
|
|
|
|
information, and this select is used to retrieve the user from Tangram. |
|
248
|
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
=head2 for_session ( $c, $user ) |
|
250
|
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
This method returns the Tangram ID for the user, as that is all that is |
|
252
|
|
|
|
|
|
|
necessary to be persisted in the session to restore the user. |
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
=head2 from_session ( $c, $frozenuser ) |
|
255
|
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
This method is called whenever a user is being restored from the session. |
|
257
|
|
|
|
|
|
|
$frozenuser contains the Tangram ID of the user to restore. |
|
258
|
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
=head2 user_supports |
|
260
|
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
Delegates to the L<Catalyst::Authentication::Store::Tangram::User->supports|Catalyst::Authentication::Store::Tangram::User#supports> method. |
|
262
|
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
=head2 user_results_filter |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
This is a Perl CODE ref that can be used to filter out multiple results |
|
266
|
|
|
|
|
|
|
from your Tangram query. In theory, your Tangram query should only return one |
|
267
|
|
|
|
|
|
|
result and find_user() will throw an exception if it encounters more than one |
|
268
|
|
|
|
|
|
|
result. However, if you have, for whatever reason, a legitimate reason for |
|
269
|
|
|
|
|
|
|
returning multiple search results from your Tangram query, use |
|
270
|
|
|
|
|
|
|
C<user_results_filter> to filter out the Tangram entries you do not want |
|
271
|
|
|
|
|
|
|
considered. Your CODE ref should expect a single argument, an instance of |
|
272
|
|
|
|
|
|
|
your Tangram user object, and it should return exactly one value, which is |
|
273
|
|
|
|
|
|
|
used as a true/false. |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
Example: |
|
276
|
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
user_results_filter => sub { |
|
278
|
|
|
|
|
|
|
my $obj = shift; |
|
279
|
|
|
|
|
|
|
$obj->permissions =~ /catalystapp/ ? 1 : 0 |
|
280
|
|
|
|
|
|
|
} |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
Note: The above example is B<not> a best practice method for storing roles |
|
283
|
|
|
|
|
|
|
against a user, you really want a L<Tangram::Type::Array::FromMany> |
|
284
|
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
=head2 lookup_roles |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
Returns a list of roles that this user is authorised for. |
|
288
|
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
Calls the method specified by the role_relation configuration key, and expects |
|
290
|
|
|
|
|
|
|
either a list, or a reference to an array of roles to be returned. |
|
291
|
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
Note that this method will call the I<role_relation> method on the |
|
293
|
|
|
|
|
|
|
I<user_class>, not on the I<tangram_user_class> directly. This can therefore be |
|
294
|
|
|
|
|
|
|
used to add a custom role lookup without changing your underlying model class |
|
295
|
|
|
|
|
|
|
lookup by sub-classing I<Catalyst::Authentication::Storage::Tangram::User>, and |
|
296
|
|
|
|
|
|
|
adding the custom lookup there (then setting I<role_relation> and I<user_class> |
|
297
|
|
|
|
|
|
|
appropriately. |
|
298
|
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
=head1 SEE ALSO |
|
300
|
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
L<Catalyst::Authentication::Store::Tangram::User>, |
|
302
|
|
|
|
|
|
|
L<Catalyst::Plugin::Authentication>, |
|
303
|
|
|
|
|
|
|
L<Catalyst::Authentication::Store> |
|
304
|
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
=head1 AUTHOR |
|
306
|
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
Tomas Doran, <bobtfish at bobtfish dot net> |
|
308
|
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
With thanks to state51, my employer, for giving me the time to work on this. |
|
310
|
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
Various ideas stolen from other Catalyst::Authentication modules by other |
|
312
|
|
|
|
|
|
|
authors. |
|
313
|
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
=head1 BUGS |
|
315
|
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
All complex software has bugs, and I'm sure that this module is no exception. |
|
317
|
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
Please report bugs through the rt.cpan.org bug tracker. |
|
319
|
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=head1 COPYRIGHT |
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
Copyright (c) 2008, state51. Some rights reserved. |
|
323
|
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=head1 LICENSE |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
This module is free software; you can use, redistribute, and modify it |
|
327
|
|
|
|
|
|
|
under the same terms as Perl 5.8.x. |
|
328
|
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
=cut |
|
330
|
|
|
|
|
|
|
|