line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Abilities; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# ABSTRACT: Simple, hierarchical user authorization for web applications, with optional support for plan-based (paid) services. |
4
|
|
|
|
|
|
|
|
5
|
2
|
|
|
2
|
|
106543
|
use Carp; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
139
|
|
6
|
2
|
|
|
2
|
|
1695
|
use Hash::Merge qw/merge/; |
|
2
|
|
|
|
|
4979
|
|
|
2
|
|
|
|
|
117
|
|
7
|
2
|
|
|
2
|
|
963
|
use Moo::Role; |
|
2
|
|
|
|
|
88090
|
|
|
2
|
|
|
|
|
19
|
|
8
|
2
|
|
|
2
|
|
1587
|
use namespace::autoclean; |
|
2
|
|
|
|
|
17240
|
|
|
2
|
|
|
|
|
16
|
|
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
our $VERSION = "0.5"; |
11
|
|
|
|
|
|
|
$VERSION = eval $VERSION; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
=head1 NAME |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
Abilities - Simple, hierarchical user authorization for web applications, with optional support for plan-based (paid) services. |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
=head1 VERSION |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
version 0.5 |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
=head1 SYNOPSIS |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
package User; |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
use Moose; # or Moo |
26
|
|
|
|
|
|
|
with 'Abilities'; |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# ... define required methods ... |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
# somewhere else in your code: |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
# get a user object that consumed the Abilities role |
33
|
|
|
|
|
|
|
my $user = MyApp->get_user('username'); # $user is a User object |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
# check if the user is able to do something |
36
|
|
|
|
|
|
|
if ($user->can_perform('something')) { |
37
|
|
|
|
|
|
|
do_something(); |
38
|
|
|
|
|
|
|
} else { |
39
|
|
|
|
|
|
|
die "Hey you can't do that, you can only do " . join(', ', keys %{$user->abilities}); |
40
|
|
|
|
|
|
|
} |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=head1 DESCRIPTION |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
Abilities is a simple yet powerful mechanism for authorizing users of web |
45
|
|
|
|
|
|
|
applications (or any applications) to perform certain actions in the application. This is an |
46
|
|
|
|
|
|
|
extension of the familiar role-based access control that is common in |
47
|
|
|
|
|
|
|
various systems and frameworks like L (See L |
48
|
|
|
|
|
|
|
for the role-based implementation and L |
49
|
|
|
|
|
|
|
for the ability-based implementation that inspired this module). |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
As opposed to role-based access control - where users are allowed access |
52
|
|
|
|
|
|
|
to a certain feature (here called 'action') only through their association |
53
|
|
|
|
|
|
|
to a certain role that is hard-coded into the program - in ability-based |
54
|
|
|
|
|
|
|
acccess control, a list of actions is assigned to every user, and they are |
55
|
|
|
|
|
|
|
only allowed to perform these actions. Actions are not assigned by the |
56
|
|
|
|
|
|
|
developer during development, but rather by the end-user during deployment. |
57
|
|
|
|
|
|
|
This allows for much more flexibility, and also speeds up development, |
58
|
|
|
|
|
|
|
as you (the developer) do not need to think about who should be allowed |
59
|
|
|
|
|
|
|
to perform a certain action, and can easily grant access later-on after |
60
|
|
|
|
|
|
|
deployment (assuming you're also the end-user). |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
Abilities to perform certain actions can be given to a user specifically, or |
63
|
|
|
|
|
|
|
via roles the user can assume (as in role-based access control). For example, |
64
|
|
|
|
|
|
|
if user 'user01' is a member of role 'admin', and this user wishes to perform |
65
|
|
|
|
|
|
|
some action, for example 'delete_foo', then they will only be able to do |
66
|
|
|
|
|
|
|
so if the 'delete_foo' ability was given to either the user itself or the |
67
|
|
|
|
|
|
|
'admin' role itself. Furthermore, roles can recursively inherit other roles; |
68
|
|
|
|
|
|
|
for example, the role 'mega_mods' can inherit the roles 'mods' and 'editors'. |
69
|
|
|
|
|
|
|
Users of the 'mega_mods' role will assume all actions owned by the 'mods' |
70
|
|
|
|
|
|
|
and 'editors' roles. |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
A commonly known use-case for this type of access control is message boards, |
73
|
|
|
|
|
|
|
where the administrator might wish to create roles with certain actions |
74
|
|
|
|
|
|
|
and associate users with the roles (more commonly called 'user groups'); |
75
|
|
|
|
|
|
|
for example, the admin can create an 'editor' role, giving users of this |
76
|
|
|
|
|
|
|
role the ability to edit and delete posts, but not any other administrative |
77
|
|
|
|
|
|
|
action. So in essence, this type of access control relieves the developer |
78
|
|
|
|
|
|
|
of deciding who gets to do what and passes these decisions to the |
79
|
|
|
|
|
|
|
end-user, which might actually be necessary in certain situations. |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
The C module is implemented as a L (which makes |
82
|
|
|
|
|
|
|
it compatible with L code). In order to be able to use this mechanism, |
83
|
|
|
|
|
|
|
applications must implement a user management system that will consume this role. |
84
|
|
|
|
|
|
|
More specifically, a user class and a role class must be implemented, consuming this role. L is a reference implementation that can be used by applications, or |
85
|
|
|
|
|
|
|
just taken as an example of an ability-based authorization system. L |
86
|
|
|
|
|
|
|
and L are the user and role classes that consume the Abilities |
87
|
|
|
|
|
|
|
role in the Entities distribution. |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=head2 CONSTRAINTS |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Generally, an ability is a yes/no option. Either the user can or can't perform |
92
|
|
|
|
|
|
|
a specific action. At times, this might not be flexible enough, and the user's |
93
|
|
|
|
|
|
|
ability to perform a certain action should be constrained. For example, a user |
94
|
|
|
|
|
|
|
might be granted the ability to edit posts in a blog, but this ability should |
95
|
|
|
|
|
|
|
be constrained to the user's posts only. The user is not to be allowed to edit |
96
|
|
|
|
|
|
|
posts created by other users. C supports constraints by allowing to |
97
|
|
|
|
|
|
|
set a name-based constraint when granting a user/role a certain ability. Then, |
98
|
|
|
|
|
|
|
checking the user's ability to perform an action can include the constraint, |
99
|
|
|
|
|
|
|
for example: |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
if ($post->{user_id} eq $user->id && $user->can_perform('edit_posts', 'only_his')) { |
102
|
|
|
|
|
|
|
# allow |
103
|
|
|
|
|
|
|
} else { |
104
|
|
|
|
|
|
|
# do not allow |
105
|
|
|
|
|
|
|
} |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
Here, the C module allows you to check if the user's ability is constrained, |
108
|
|
|
|
|
|
|
but the responsibility for making sure the constraint is actually relevant |
109
|
|
|
|
|
|
|
to the case is left to you. In the above example, it is the application that |
110
|
|
|
|
|
|
|
checks if the post the user is trying to edit was created by them, not the C |
111
|
|
|
|
|
|
|
module. |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=head2 (PAID) SUBSCRIPTION-BASED WEB SERVICES |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
Apart from the scenario described above, this module also provides optional |
116
|
|
|
|
|
|
|
support for subscription-based web services, such as those where customers |
117
|
|
|
|
|
|
|
subscribe to a certain paid (or free, doesn't matter) plan from a list |
118
|
|
|
|
|
|
|
of available plans (GitHub is an example of such a service). This functionality |
119
|
|
|
|
|
|
|
is also implemented as a Moo(se) role, in the L module provided |
120
|
|
|
|
|
|
|
with this distribution. Read its documentation for detailed information. |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
=head1 REQUIRED METHODS |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
Classes that consume this role are required to implement the following |
125
|
|
|
|
|
|
|
methods: |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=head2 roles() |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
Returns a list of all role names that a user object belongs to, or a role object |
130
|
|
|
|
|
|
|
inherits from. |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
Example return structure: |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
( 'moderator', 'supporter' ) |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
NOTE: In previous versions, this method was required to return |
137
|
|
|
|
|
|
|
an array of role objects, not a list of role names. This has been changed |
138
|
|
|
|
|
|
|
in version 0.3. |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=cut |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
requires 'roles'; |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
=head2 actions() |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
Returns a list of all action names that a user object has been explicitely granted, |
147
|
|
|
|
|
|
|
or that a role object has been granted. If a certain action is constrained, then |
148
|
|
|
|
|
|
|
it should be added to the list as an array reference with two items, the first being |
149
|
|
|
|
|
|
|
the name of the action, the second being the name of the constraint. |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
Example return structure: |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
( 'create_posts', ['edit_posts', 'only_his'], 'comment_on_posts' ) |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
NOTE: In previous versions, this method was required to return |
156
|
|
|
|
|
|
|
an array of action objects, not a list of action names. This has been changed |
157
|
|
|
|
|
|
|
in version 0.3. |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
=cut |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
requires 'actions'; |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
=head2 is_super() |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
This is a boolean attribute that both user and role objects should have. |
166
|
|
|
|
|
|
|
If a user/role object has a true value for this attribute, then they |
167
|
|
|
|
|
|
|
will be able to perform any action, even if it wasn't granted to them. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=cut |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
requires 'is_super'; |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
=head2 get_role( $name ) |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
This is a method that returns the object of the role named C<$name>. |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
=cut |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
requires 'get_role'; |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=head1 PROVIDED METHODS |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
Classes that consume this role will have the following methods available |
184
|
|
|
|
|
|
|
to them: |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
=head2 can_perform( $action, [ $constraint ] ) |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
Receives the name of an action, and possibly a constraint, and returns a true |
189
|
|
|
|
|
|
|
value if the user/role can perform the provided action. |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
=cut |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
sub can_perform { |
194
|
16
|
|
|
16
|
1
|
40
|
my ($self, $action, $constraint) = @_; |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# a super-user/super-role can do whatever they want |
197
|
16
|
100
|
|
|
|
69
|
return 1 if $self->is_super; |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
# return false if user/role doesn't have that ability |
200
|
12
|
100
|
|
|
|
27
|
return unless $self->abilities->{$action}; |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
# user/role has ability, but is there a constraint? |
203
|
11
|
100
|
100
|
|
|
64
|
if ($constraint && $constraint ne '_all_') { |
204
|
|
|
|
|
|
|
# return true if user/role's ability is not constrained |
205
|
4
|
100
|
|
|
|
10
|
return 1 if !ref $self->abilities->{$action}; |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
# it is constrained (or at least it should be, let's make |
208
|
|
|
|
|
|
|
# sure we have an array-ref of constraints) |
209
|
2
|
50
|
|
|
|
33
|
if (ref $self->abilities->{$action} eq 'ARRAY') { |
210
|
2
|
100
|
|
|
|
13
|
return 1 if $constraint eq '_any_'; # caller wants to know if |
211
|
|
|
|
|
|
|
# user/role has any constraint, |
212
|
|
|
|
|
|
|
# which we now know is true |
213
|
1
|
|
|
|
|
3
|
foreach (@{$self->abilities->{$action}}) { |
|
1
|
|
|
|
|
4
|
|
214
|
1
|
50
|
|
|
|
8
|
return 1 if $_ eq $constraint; |
215
|
|
|
|
|
|
|
} |
216
|
0
|
|
|
|
|
0
|
return; # constraint not met |
217
|
|
|
|
|
|
|
} else { |
218
|
0
|
|
|
|
|
0
|
carp "Expected an array-ref of constraints for action $action, received ".ref($self->abilities->{$action}).", returning false."; |
219
|
0
|
|
|
|
|
0
|
return; |
220
|
|
|
|
|
|
|
} |
221
|
|
|
|
|
|
|
} else { |
222
|
|
|
|
|
|
|
# no constraint, make sure user/role's ability is indeed |
223
|
|
|
|
|
|
|
# not constrained |
224
|
7
|
100
|
|
|
|
16
|
return if ref $self->abilities->{$action}; # implied: ref == 'ARRAY', thus constrained |
225
|
6
|
|
|
|
|
35
|
return 1; # not constrained |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
=head2 assigned_role( $role_name ) |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
This method receives a role name and returns a true value if the user/role |
232
|
|
|
|
|
|
|
is a direct member of the provided role. Only direct membership is checked, |
233
|
|
|
|
|
|
|
so the user/role must be specifically assigned to the provided role, and |
234
|
|
|
|
|
|
|
not to a role that inherits from that role (see L"does_role( $role )"> |
235
|
|
|
|
|
|
|
instead). |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
=cut |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
sub assigned_role { |
240
|
5
|
|
|
5
|
1
|
6016
|
my ($self, $role) = @_; |
241
|
|
|
|
|
|
|
|
242
|
5
|
50
|
|
|
|
18
|
return unless $role; |
243
|
|
|
|
|
|
|
|
244
|
5
|
|
|
|
|
144
|
foreach ($self->roles) { |
245
|
5
|
100
|
|
|
|
138
|
return 1 if $_ eq $role; |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
|
248
|
3
|
|
|
|
|
41
|
return; |
249
|
|
|
|
|
|
|
} |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
=head2 does_role( $role_name ) |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
Receives the name of a role, and returns a true value if the user/role |
254
|
|
|
|
|
|
|
inherits the abilities of the provided role. This method takes inheritance |
255
|
|
|
|
|
|
|
into account, so if a user was directly assigned to the 'admins' role, |
256
|
|
|
|
|
|
|
and the 'admins' role inherits from the 'devs' role, then C |
257
|
|
|
|
|
|
|
will return true for that user (while C returns false). |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
=cut |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
sub does_role { |
262
|
5
|
|
|
5
|
1
|
10
|
my ($self, $role) = @_; |
263
|
|
|
|
|
|
|
|
264
|
5
|
50
|
|
|
|
14
|
return unless $role; |
265
|
|
|
|
|
|
|
|
266
|
5
|
|
|
|
|
145
|
foreach (map([$_, $self->get_role($_)], $self->roles)) { |
267
|
4
|
100
|
100
|
|
|
148
|
return 1 if $_->[0] eq $role || $_->[1]->does_role($role); |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
3
|
|
|
|
|
80
|
return; |
271
|
|
|
|
|
|
|
} |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
=head2 abilities() |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
Returns a hash reference of all the abilities a user/role object can |
276
|
|
|
|
|
|
|
perform, after consolidating abilities inherited from roles (including |
277
|
|
|
|
|
|
|
recursively) and directly granted. Keys in the hash-ref will be names |
278
|
|
|
|
|
|
|
of actions, values will be 1 (for yes/no actions) or a single-item array-ref |
279
|
|
|
|
|
|
|
with the name of a constraint (for constrained actions). |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
=cut |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
sub abilities { |
284
|
70
|
|
|
70
|
1
|
255
|
my $self = shift; |
285
|
|
|
|
|
|
|
|
286
|
70
|
|
|
|
|
89
|
my $abilities = {}; |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
# load direct actions granted to this user/role |
289
|
70
|
|
|
|
|
1729
|
foreach ($self->actions) { |
290
|
|
|
|
|
|
|
# is this action constrained/scoped? |
291
|
131
|
100
|
33
|
|
|
8090
|
unless (ref $_) { |
|
|
50
|
|
|
|
|
|
292
|
88
|
|
|
|
|
229
|
$abilities->{$_} = 1; |
293
|
|
|
|
|
|
|
} elsif (ref $_ eq 'ARRAY' && scalar @$_ == 2) { |
294
|
43
|
|
|
|
|
166
|
$abilities->{$_->[0]} = [$_->[1]]; |
295
|
|
|
|
|
|
|
} else { |
296
|
0
|
|
|
|
|
0
|
carp "Can't handle action of reference ".ref($_); |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
} |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
# load actions from roles this user/role consumes |
301
|
70
|
|
|
|
|
1879
|
my @hashes = map { $self->get_role($_)->abilities } $self->roles; |
|
44
|
|
|
|
|
1093
|
|
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
# merge all abilities |
304
|
70
|
|
|
|
|
1108
|
while (scalar @hashes) { |
305
|
44
|
|
|
|
|
1043
|
$abilities = merge($abilities, shift @hashes); |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
|
308
|
70
|
|
|
|
|
10082
|
return $abilities; |
309
|
|
|
|
|
|
|
} |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
=head1 UPGRADING FROM v0.2 |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
Up to version 0.2, C required the C and C |
314
|
|
|
|
|
|
|
attributes to return objects. While this made it easier to calculate |
315
|
|
|
|
|
|
|
abilities, it made this system a bit less flexible. |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
In version 0.3, C changed the requirement such that both these |
318
|
|
|
|
|
|
|
attributes need to return strings (the names of the roles/actions). If your implementation |
319
|
|
|
|
|
|
|
has granted roles and actions stored in a database by names, this made life a bit easier |
320
|
|
|
|
|
|
|
for you. On other implementations, however, this has the potential of |
321
|
|
|
|
|
|
|
requiring you to write a bit more code. If that is the case, I apologize, |
322
|
|
|
|
|
|
|
but keep in mind that you can still store granted roles and actions |
323
|
|
|
|
|
|
|
any way you want in a database (either by names or by references), just |
324
|
|
|
|
|
|
|
as long as you correctly provide C and C. |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
Unfortunately, in both versions 0.3 and 0.4, I made a bit of a mess |
327
|
|
|
|
|
|
|
that rendered both versions unusable. While I documented the C |
328
|
|
|
|
|
|
|
attribute as requiring role names instead of role objects, the actual |
329
|
|
|
|
|
|
|
implementation still required role objects. This has now been fixed, |
330
|
|
|
|
|
|
|
but it also meant I had to add a new requirement: consuming classes |
331
|
|
|
|
|
|
|
now have to provide a method called C that takes the name |
332
|
|
|
|
|
|
|
of a role and returns its object. This will probably means loading the |
333
|
|
|
|
|
|
|
role from a database and blessing it into your role class that also consumes |
334
|
|
|
|
|
|
|
this module. |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
I apologize for any inconvenience this might have caused. |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
=head1 AUTHOR |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
Ido Perlmuter, C<< >> |
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
=head1 BUGS |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
Please report any bugs or feature requests to C, or through |
345
|
|
|
|
|
|
|
the web interface at L. I will be notified, and then you'll |
346
|
|
|
|
|
|
|
automatically be notified of progress on your bug as I make changes. |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
=head1 SUPPORT |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
perldoc Abilities |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
You can also look for information at: |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
=over 4 |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
=item * RT: CPAN's request tracker |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
L |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
=item * AnnoCPAN: Annotated CPAN documentation |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
L |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
=item * CPAN Ratings |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
L |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
=item * Search CPAN |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
L |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
=back |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
377
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
Copyright 2010-2013 Ido Perlmuter. |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
381
|
|
|
|
|
|
|
under the terms of either: the GNU General Public License as published |
382
|
|
|
|
|
|
|
by the Free Software Foundation; or the Artistic License. |
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
See http://dev.perl.org/licenses/ for more information. |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
=cut |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
1; |