line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Catalyst::Plugin::Session::Store::DBIC; |
2
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
841
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
26
|
|
4
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
31
|
|
5
|
1
|
|
|
1
|
|
4
|
use base qw/Catalyst::Plugin::Session::Store::Delegate/; |
|
1
|
|
|
|
|
10
|
|
|
1
|
|
|
|
|
842
|
|
6
|
1
|
|
|
1
|
|
603505
|
use Catalyst::Exception; |
|
1
|
|
|
|
|
184007
|
|
|
1
|
|
|
|
|
35
|
|
7
|
1
|
|
|
1
|
|
796
|
use Catalyst::Plugin::Session::Store::DBIC::Delegate; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
8
|
|
8
|
1
|
|
|
1
|
|
886
|
use MIME::Base64 (); |
|
1
|
|
|
|
|
712
|
|
|
1
|
|
|
|
|
27
|
|
9
|
1
|
|
|
1
|
|
6
|
use MRO::Compat; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
22
|
|
10
|
1
|
|
|
1
|
|
969
|
use Storable (); |
|
1
|
|
|
|
|
3462
|
|
|
1
|
|
|
|
|
813
|
|
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
our $VERSION = '0.14'; |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
=head1 NAME |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
Catalyst::Plugin::Session::Store::DBIC - Store your sessions via DBIx::Class |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 SYNOPSIS |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
# Create a table in your database for sessions |
21
|
|
|
|
|
|
|
CREATE TABLE sessions ( |
22
|
|
|
|
|
|
|
id CHAR(72) PRIMARY KEY, |
23
|
|
|
|
|
|
|
session_data TEXT, |
24
|
|
|
|
|
|
|
expires INTEGER |
25
|
|
|
|
|
|
|
); |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
# Create the corresponding table class |
28
|
|
|
|
|
|
|
package MyApp::Schema::Session; |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
use base qw/DBIx::Class/; |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
__PACKAGE__->load_components(qw/Core/); |
33
|
|
|
|
|
|
|
__PACKAGE__->table('sessions'); |
34
|
|
|
|
|
|
|
__PACKAGE__->add_columns(qw/id session_data expires/); |
35
|
|
|
|
|
|
|
__PACKAGE__->set_primary_key('id'); |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
1; |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# In your application |
40
|
|
|
|
|
|
|
use Catalyst qw/Session Session::Store::DBIC Session::State::Cookie/; |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
__PACKAGE__->config( |
43
|
|
|
|
|
|
|
# ... other items ... |
44
|
|
|
|
|
|
|
'Plugin::Session' => { |
45
|
|
|
|
|
|
|
dbic_class => 'DBIC::Session', # Assuming MyApp::Model::DBIC |
46
|
|
|
|
|
|
|
expires => 3600, |
47
|
|
|
|
|
|
|
}, |
48
|
|
|
|
|
|
|
); |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
# Later, in a controller action |
51
|
|
|
|
|
|
|
$c->session->{foo} = 'bar'; |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
=head1 DESCRIPTION |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
This L<Catalyst::Plugin::Session> storage module saves session data in |
56
|
|
|
|
|
|
|
your database via L<DBIx::Class>. It's actually just a wrapper around |
57
|
|
|
|
|
|
|
L<Catalyst::Plugin::Session::Store::Delegate>; if you need complete |
58
|
|
|
|
|
|
|
control over how your sessions are stored, you probably want to use |
59
|
|
|
|
|
|
|
that instead. |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
=head1 METHODS |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
=head2 setup_finished |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
Hook into the configured session class. |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=cut |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
sub setup_finished { |
70
|
0
|
|
|
0
|
1
|
|
my $c = shift; |
71
|
|
|
|
|
|
|
|
72
|
0
|
0
|
|
|
|
|
return $c->next::method unless @_; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
# Try to determine id_field if it isn't set |
75
|
0
|
0
|
|
|
|
|
unless ($c->_session_plugin_config->{id_field}) { |
76
|
0
|
|
|
|
|
|
my $model = $c->session_store_model; |
77
|
0
|
0
|
|
|
|
|
my $rs = ref $model ? $model |
|
|
0
|
|
|
|
|
|
78
|
|
|
|
|
|
|
: $model->can('resultset_instance') ? $model->resultset_instance |
79
|
|
|
|
|
|
|
: $model; |
80
|
0
|
|
|
|
|
|
my @primary_columns = $rs->result_source->primary_columns; |
81
|
|
|
|
|
|
|
|
82
|
0
|
0
|
|
|
|
|
Catalyst::Exception->throw( |
83
|
|
|
|
|
|
|
message => __PACKAGE__ . qq/: Primary key consists of more than one column; please set id_field manually/ |
84
|
|
|
|
|
|
|
) if @primary_columns > 1; |
85
|
|
|
|
|
|
|
|
86
|
0
|
|
|
|
|
|
$c->_session_plugin_config->{id_field} = $primary_columns[0]; |
87
|
|
|
|
|
|
|
} |
88
|
|
|
|
|
|
|
|
89
|
0
|
|
|
|
|
|
$c->next::method(@_); |
90
|
|
|
|
|
|
|
} |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
=head2 session_store_dbic_class |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
Return the L<DBIx::Class> class name to be passed to C<< $c->model >>. |
95
|
|
|
|
|
|
|
Defaults to C<DBIC::Session>. |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=cut |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
sub session_store_dbic_class { |
100
|
0
|
0
|
|
0
|
1
|
|
shift->_session_plugin_config->{dbic_class} || 'DBIC::Session'; |
101
|
|
|
|
|
|
|
} |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
=head2 session_store_dbic_id_field |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
Return the configured ID field name. Defaults to C<id>. |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
=cut |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
sub session_store_dbic_id_field { |
110
|
0
|
0
|
|
0
|
1
|
|
shift->_session_plugin_config->{id_field} || 'id'; |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=head2 session_store_dbic_data_field |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
Return the configured data field name. Defaults to C<session_data>. |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=cut |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
sub session_store_dbic_data_field { |
120
|
0
|
0
|
|
0
|
1
|
|
shift->_session_plugin_config->{data_field} || 'session_data'; |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
=head2 session_store_dbic_expires_field |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
Return the configured expires field name. Defaults to C<expires>. |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=cut |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
sub session_store_dbic_expires_field { |
130
|
0
|
0
|
|
0
|
1
|
|
shift->_session_plugin_config->{expires_field} || 'expires'; |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
=head2 session_store_model |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
Return the model used to find a session. |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
=cut |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
sub session_store_model { |
140
|
0
|
|
|
0
|
1
|
|
my ($c, $id) = @_; |
141
|
|
|
|
|
|
|
|
142
|
0
|
|
|
|
|
|
my $dbic_class = $c->session_store_dbic_class; |
143
|
0
|
0
|
|
|
|
|
$c->model($dbic_class, $id) or die "Couldn't find a model named $dbic_class"; |
144
|
|
|
|
|
|
|
} |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
=head2 get_session_store_delegate |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
Load the row corresponding to the specified session ID. If none is |
149
|
|
|
|
|
|
|
found, one is automatically created. |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
=cut |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
sub get_session_store_delegate { |
154
|
0
|
|
|
0
|
1
|
|
my ($c, $id) = @_; |
155
|
|
|
|
|
|
|
|
156
|
0
|
|
|
|
|
|
Catalyst::Plugin::Session::Store::DBIC::Delegate->new({ |
157
|
|
|
|
|
|
|
model => $c->session_store_model($id), |
158
|
|
|
|
|
|
|
id_field => $c->session_store_dbic_id_field, |
159
|
|
|
|
|
|
|
data_field => $c->session_store_dbic_data_field, |
160
|
|
|
|
|
|
|
}); |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
=head2 session_store_delegate_key_to_accessor |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
Match the specified key and operation to the session ID and field |
166
|
|
|
|
|
|
|
name. |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
=cut |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
sub session_store_delegate_key_to_accessor { |
171
|
0
|
|
|
0
|
1
|
|
my $c = shift; |
172
|
0
|
|
|
|
|
|
my $key = $_[0]; |
173
|
0
|
|
|
|
|
|
my ($field, @args) = $c->next::method(@_); |
174
|
|
|
|
|
|
|
|
175
|
0
|
|
|
|
|
|
my ($type) = ($key =~ /^(\w+):/); |
176
|
|
|
|
|
|
|
|
177
|
0
|
0
|
|
|
|
|
$field = $c->session_store_dbic_id_field if $field eq 'id'; |
178
|
0
|
0
|
|
|
|
|
$field = $c->session_store_dbic_expires_field if $field eq 'expires'; |
179
|
0
|
0
|
0
|
|
|
|
$field = $c->session_store_dbic_data_field if $field eq 'session' or $field eq 'flash'; |
180
|
|
|
|
|
|
|
|
181
|
0
|
|
|
0
|
|
|
my $accessor = sub { shift->$type($key)->$field(@_) }; |
|
0
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
|
183
|
0
|
0
|
|
|
|
|
if ($field eq $c->session_store_dbic_data_field) { |
184
|
0
|
|
0
|
|
|
|
@args = map { MIME::Base64::encode(Storable::nfreeze($_ || '')) } @args; |
|
0
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
$accessor = sub { |
186
|
0
|
|
|
0
|
|
|
my $value = shift->$type($key)->$field(@_); |
187
|
0
|
0
|
|
|
|
|
return unless $value; |
188
|
0
|
|
|
|
|
|
return Storable::thaw(MIME::Base64::decode($value)); |
189
|
0
|
|
|
|
|
|
}; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
|
192
|
0
|
|
|
|
|
|
return ($accessor, @args); |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 delete_session_data |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
Delete the specified session from the backend store. |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
=cut |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
sub delete_session_data { |
202
|
0
|
|
|
0
|
1
|
|
my ($c, $key) = @_; |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
# expires is stored on the session row for compatibility with Store::DBI |
205
|
0
|
0
|
|
|
|
|
return if $key =~ /^expires/; |
206
|
|
|
|
|
|
|
|
207
|
0
|
|
|
|
|
|
$c->session_store_model->search({ |
208
|
|
|
|
|
|
|
$c->session_store_dbic_id_field => $key, |
209
|
|
|
|
|
|
|
})->delete; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=head2 delete_expired_sessions |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Delete all expired sessions. |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
=cut |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub delete_expired_sessions { |
219
|
0
|
|
|
0
|
1
|
|
my $c = shift; |
220
|
|
|
|
|
|
|
|
221
|
0
|
|
|
|
|
|
$c->session_store_model->search({ |
222
|
|
|
|
|
|
|
$c->session_store_dbic_expires_field => { '<', time() }, |
223
|
|
|
|
|
|
|
})->delete; |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=head1 CONFIGURATION |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
The following parameters should be placed in your application |
229
|
|
|
|
|
|
|
configuration under the C<Plugin::Session> key. |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
=head2 dbic_class |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
(Required) The name of the L<DBIx::Class> that represents a session in |
234
|
|
|
|
|
|
|
the database. It is recommended that you provide only the part after |
235
|
|
|
|
|
|
|
C<MyApp::Model>, e.g. C<DBIC::Session>. |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
If you are using L<Catalyst::Model::DBIC::Schema>, the following |
238
|
|
|
|
|
|
|
layout is recommended: |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
=over 4 |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
=item * C<MyApp::Schema> - your L<DBIx::Class::Schema> class |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=item * C<MyApp::Schema::Session> - your session table class |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
=item * C<MyApp::Model::DBIC> - your L<Catalyst::Model::DBIC::Schema> class |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
=back |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
This module will then use C<< $c->model >> to access the appropriate |
251
|
|
|
|
|
|
|
result source from the composed schema matching the C<dbic_class> |
252
|
|
|
|
|
|
|
name. |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
For more information, please see L<Catalyst::Model::DBIC::Schema>. |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
=head2 expires |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
Number of seconds for which sessions are active. |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
Note that no automatic cleanup is done on your session data. To |
261
|
|
|
|
|
|
|
delete expired sessions, you can use the L</delete_expired_sessions> |
262
|
|
|
|
|
|
|
method with L<Catalyst::Plugin::Scheduler>. |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=head2 id_field |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
The name of the field on your sessions table which stores the session |
267
|
|
|
|
|
|
|
ID. Defaults to C<id>. |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
=head2 data_field |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
The name of the field on your sessions table which stores session |
272
|
|
|
|
|
|
|
data. Defaults to C<session_data> for compatibility with |
273
|
|
|
|
|
|
|
L<Catalyst::Plugin::Session::Store::DBI>. |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
=head2 expires_field |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
The name of the field on your sessions table which stores the |
278
|
|
|
|
|
|
|
expiration time of the session. Defaults to C<expires>. |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
=head1 SCHEMA |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
Your sessions table should contain the following columns: |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
id CHAR(72) PRIMARY KEY |
285
|
|
|
|
|
|
|
session_data TEXT |
286
|
|
|
|
|
|
|
expires INTEGER |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
The C<id> column should probably be 72 characters. It needs to handle |
289
|
|
|
|
|
|
|
the longest string that can be returned by |
290
|
|
|
|
|
|
|
L<Catalyst::Plugin::Session/generate_session_id>, plus another eight |
291
|
|
|
|
|
|
|
characters for internal use. This is less than 72 characters when |
292
|
|
|
|
|
|
|
SHA-1 or MD5 is used, but SHA-256 will need all 72 characters. |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
The C<session_data> column should be a long text field. Session data |
295
|
|
|
|
|
|
|
is encoded using L<MIME::Base64> before being stored in the database. |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
Note that MySQL C<TEXT> fields only store 64 kB, so if your session |
298
|
|
|
|
|
|
|
data will exceed that size you'll want to use C<MEDIUMTEXT>, |
299
|
|
|
|
|
|
|
C<MEDIUMBLOB>, or larger. If you configure your |
300
|
|
|
|
|
|
|
L<DBIx::Class::ResultSource> to include the size of the column, you |
301
|
|
|
|
|
|
|
will receive warnings for this problem: |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
This session requires 1180 bytes of storage, but your database |
304
|
|
|
|
|
|
|
column 'session_data' can only store 200 bytes. Storing this |
305
|
|
|
|
|
|
|
session may not be reliable; increase the size of your data field |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
See L<DBIx::Class::ResultSource/add_columns> for more information. |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
The C<expires> column stores the future expiration time of the |
310
|
|
|
|
|
|
|
session. This may be null for per-user and flash sessions. |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
Note that you can change the column names using the L</id_field>, |
313
|
|
|
|
|
|
|
L</data_field>, and L</expires_field> configuration parameters. |
314
|
|
|
|
|
|
|
However, the column types must match the above. |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=head1 AUTHOR |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
Daniel Westermann-Clark E<lt>danieltwc@cpan.orgE<gt> |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=head1 ACKNOWLEDGMENTS |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
=over 4 |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=item * Andy Grundman, for L<Catalyst::Plugin::Session::Store::DBI> |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=item * David Kamholz, for most of the testing code (from |
327
|
|
|
|
|
|
|
L<Catalyst::Plugin::Authentication::Store::DBIC>) |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
=item * Yuval Kogman, for assistance in converting to |
330
|
|
|
|
|
|
|
L<Catalyst::Plugin::Session::Store::Delegate> |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
=item * Jay Hannah, for tests and warning when session size |
333
|
|
|
|
|
|
|
exceeds DBIx::Class storage size. |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=back |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
=head1 COPYRIGHT |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
Copyright (c) 2006 - 2009 |
340
|
|
|
|
|
|
|
the Catalyst::Plugin::Session::Store::DBIC L</AUTHOR> |
341
|
|
|
|
|
|
|
as listed above. |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
344
|
|
|
|
|
|
|
under the same terms as Perl itself. |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
=cut |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
1; |