File Coverage

blib/lib/PAUSE/Permissions.pm
Criterion Covered Total %
statement 106 146 72.6
branch 28 48 58.3
condition 25 38 65.7
subroutine 20 23 86.9
pod 5 7 71.4
total 184 262 70.2


line stmt bran cond sub pod time code
1             package PAUSE::Permissions;
2             $PAUSE::Permissions::VERSION = '0.14';
3 6     6   82735 use strict;
  6         10  
  6         189  
4 6     6   18 use warnings;
  6         7  
  6         128  
5              
6 6     6   2677 use Moo;
  6         59555  
  6         26  
7 6     6   7949 use PAUSE::Permissions::Module;
  6         16  
  6         190  
8 6     6   2120 use PAUSE::Permissions::ModuleIterator;
  6         15  
  6         190  
9 6     6   2625 use PAUSE::Permissions::EntryIterator;
  6         16  
  6         236  
10 6     6   6035 use File::HomeDir;
  6         21750  
  6         335  
11              
12 6     6   2344 use File::Spec::Functions qw/ catfile /;
  6         3136  
  6         315  
13 6     6   3232 use HTTP::Date qw/ time2str / ;
  6         15574  
  6         298  
14 6     6   32 use Carp qw/ croak /;
  6         7  
  6         208  
15 6     6   2319 use Time::Duration::Parse qw/ parse_duration /;
  6         8288  
  6         27  
16              
17 6     6   3725 use HTTP::Tiny;
  6         189788  
  6         6935  
18              
19             my $DISTNAME = 'PAUSE-Permissions';
20             my $BASENAME = '06perms.txt';
21             my $DEFAULT_PERMISSION_REQUESTED = 'upload';
22              
23             has 'url' =>
24             (
25             is => 'ro',
26             default => sub { return 'http://www.cpan.org/modules/06perms.txt'; },
27             );
28              
29             has 'path' => (is => 'ro' );
30             has 'cache_path' => (is => 'lazy' );
31             has 'max_age' => (is => 'ro');
32             has 'preload' => (is => 'ro', default => sub { 0 });
33             has 'module_cache' => (is => 'lazy');
34              
35             sub _build_cache_path
36             {
37 0     0   0 my $self = shift;
38              
39 0         0 my $basename = $self->url;
40 0         0 $basename =~ s!^.*[/\\]!!;
41 0         0 my $classid = ref($self);
42 0         0 $classid =~ s/::/-/g;
43              
44 0         0 return catfile(File::HomeDir->my_dist_data( $classid, { create => 1 } ), $basename);
45             }
46              
47             sub _build_module_cache
48             {
49 4     4   860 my $self = shift;
50 4         12 my $iterator = $self->module_iterator;
51 4         2111 my $cache = {};
52              
53 4         19 while (my $module = $iterator->next_module) {
54 20         2316 $cache->{ $module->name } = $module;
55             }
56              
57 4         89 return $cache;
58             }
59              
60             sub BUILD
61             {
62 7     7 0 35 my $self = shift;
63              
64 7 50       36 if ($self->path) {
65 7 50       219 return if -f $self->path;
66 0         0 croak "the file you specified with 'path' doesn't exist";
67             }
68              
69             # If we already have a locally cached copy, and the max_age was specified,
70             # then check if our cache has expired
71 0 0 0     0 if (-f $self->cache_path && $self->max_age) {
72 0         0 my $max_age_in_seconds = parse_duration($self->max_age);
73 0 0       0 return unless time() - $max_age_in_seconds > (stat($self->cache_path))[9];
74             }
75              
76 0         0 $self->_cache_file_if_needed();
77             }
78              
79             sub _cache_file_if_needed
80             {
81 0     0   0 my $self = shift;
82 0         0 my $options = {};
83 0         0 my $ua = HTTP::Tiny->new();
84              
85 0 0       0 if (-f $self->cache_path) {
86 0         0 $options->{'If-Modified-Since'} = time2str( (stat($self->cache_path))[9]);
87             }
88 0         0 my $response = $ua->get($self->url, $options);
89              
90 0 0       0 return if $response->{status} == 304; # Not Modified
91              
92 0 0       0 if ($response->{status} == 200) {
93 0         0 $self->_transform_and_cache($response);
94 0         0 return;
95             }
96              
97 0         0 croak("request for 06perms.txt failed: $response->{status} $response->{reason}");
98             }
99              
100             sub _transform_and_cache
101             {
102 0     0   0 my ($self, $response) = @_;
103 0         0 my $inheader = 1;
104 0         0 my @lines;
105              
106             LINE:
107 0         0 while ($response->{content} =~ m!^(.*)$!gm) {
108 0         0 my $line = $1;
109 0 0 0     0 if ($line =~ /^$/ && $inheader) {
110 0         0 $inheader = 0;
111 0         0 next;
112             }
113 0 0       0 next LINE if $inheader;
114 0         0 my ($package, $user, $perm) = split(/,/, $1);
115 0         0 push(@lines, [lc($package), lc($user), $package, $user, $perm]);
116             }
117              
118 0         0 open(my $fh, '>', $self->cache_path);
119 0         0 print $fh <<'END_HEADER';
120             File: PAUSE Permissions data
121             Format: 2
122             Source: CPAN/modules/06perms.txt
123              
124             END_HEADER
125              
126 0 0       0 foreach my $line (sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @lines) {
  0         0  
127 0         0 printf $fh "%s,%s,%s\n", (@$line)[2,3,4];
128             }
129              
130 0         0 close($fh);
131             }
132              
133             sub entry_iterator
134             {
135 1     1 1 742 my $self = shift;
136              
137 1         18 return PAUSE::Permissions::EntryIterator->new( permissions => $self );
138             }
139              
140             sub module_iterator
141             {
142 5     5 1 596 my $self = shift;
143              
144 5         43 return PAUSE::Permissions::ModuleIterator->new( permissions => $self );
145             }
146              
147             sub open_file
148             {
149 12     12 0 12 my $self = shift;
150 12 50       44 my $filename = defined($self->path) ? $self->path : $self->cache_path;
151 12 50       329 open(my $fh, '<', $filename) || croak "can't open $filename: $!";
152 12         28 return $fh;
153             }
154              
155             sub can_upload
156             {
157 8     8 1 1945 my ($self, $pause_id, $module_name) = @_;
158 8         9 my $PAUSE_ID = uc($pause_id);
159 8         17 my $mp = $self->module_permissions($module_name);
160              
161 8 100       57 return 1 unless defined($mp);
162              
163 5         12 return !! grep { $PAUSE_ID eq $_ } $mp->all_maintainers;
  10         18  
164             }
165              
166             my %known_permission_types =
167             (
168             'upload' => 'author can upload (either owner or comaint)',
169             'owner' => 'author is the owner of the package',
170             'comaint' => 'author has comaint but is not the owner',
171             );
172              
173             sub has_permission_for
174             {
175 8     8 1 2174 my $self = shift;
176 8         8 my $author = shift;
177 8 100       14 my $what = @_ > 0 ? shift : $DEFAULT_PERMISSION_REQUESTED;
178 8   33     127 my $cache = $self->module_cache // croak "module cache is undef\n";
179 8         33 my $AUTHOR = uc($author);
180 8         8 my $matches = [];
181 8         9 local $_;
182              
183 8         5 foreach my $module (values %{ $self->module_cache }) {
  8         124  
184 48         112 push(@$matches, $module->name) if ($what eq 'upload' && grep { $_ eq $AUTHOR } $module->all_maintainers)
  0         0  
185             || ($what eq 'owner' && defined($module->owner) && $module->owner eq $AUTHOR)
186 40 100 100     149 || ($what eq 'comaint' && grep { $_ eq $AUTHOR } $module->co_maintainers);
      100        
      100        
      66        
      33        
      66        
187             }
188 8         12 return [map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [lc($_),$_] } @$matches];
  10         18  
  4         7  
  10         22  
189             }
190              
191             sub module_permissions
192             {
193 21     21 1 2413 my $self = shift;
194 21         20 my $module = shift;
195 21         16 my $fh;
196 21         18 local $_;
197 21         18 my $inheader = 1;
198 21         16 my $seen_module = 0;
199 21         18 my %perms;
200 21         17 my ($m, $u, $p);
201              
202 21 100 66     273 if ($self->preload && $self->module_cache) {
203 14   100     241 return $self->module_cache->{$module} // undef;
204             }
205              
206 7         13 $fh = $self->open_file();
207 7         75 while (<$fh>) {
208 86         59 chomp;
209 86 100 100     173 if ($inheader && /^\s*$/) {
210 7         8 $inheader = 0;
211 7         13 next;
212             }
213 79 100       98 next if $inheader;
214 65         104 ($m, $u, $p) = split(/,/, $_);
215 65 100       104 if (lc($m) eq lc($module)) {
216 16         11 push(@{ $perms{$p} }, uc($u));
  16         31  
217 16         10 $seen_module = 1;
218             }
219 65 100 100     164 last if $seen_module && lc($m) ne lc($module);
220             }
221 7         39 close($fh);
222              
223 7 100       11 if ($seen_module) {
224 6         5 my @args;
225 6         7 push(@args, name => $module);
226 6 100       15 push(@args, m => $perms{m}->[0]) if exists $perms{m};
227 6 100       12 push(@args, f => $perms{f}->[0]) if exists $perms{f};
228 6 100       13 push(@args, c => $perms{c}) if exists $perms{c};
229 6         120 return PAUSE::Permissions::Module->new(@args);
230             }
231              
232 1         5 return undef;
233             }
234              
235             1;
236              
237             =head1 NAME
238              
239             PAUSE::Permissions - interface to PAUSE's module permissions file (06perms.txt)
240              
241             =head1 SYNOPSIS
242              
243             use PAUSE::Permissions 0.08;
244            
245             my $pp = PAUSE::Permissions->new(max_age => '1 day');
246             my $mp = $pp->module_permissions('HTTP::Client');
247            
248             my $owner = $mp->owner;
249             my @comaints = $mp->co_maintainers;
250              
251             my $iterator = $pp->module_iterator();
252             while (my $mp = $iterator->next_module) {
253             print "module = ", $mp->name, "\n";
254             print " owner = ", $mp->owner // 'none', "\n";
255             }
256              
257             =head1 DESCRIPTION
258              
259             PAUSE::Permissions provides an interface to the C<06perms.txt> file produced by
260             the Perl Authors Upload Server (PAUSE).
261             The file records who has what permissions for every module on CPAN.
262             The format and interpretation of this file
263             are covered in L below.
264              
265             By default, the module will mirror C<06perms.txt> from CPAN,
266             using L to request it and store it locally
267             What gets cached locally is actually a transformed version of 06perms.txt
268             for easier processing.
269              
270             By default it will get the file from L, but you can
271             pass an alternate URI to the constructor:
272              
273             $perms_uri = "http://$CPAN_MIRROR/modules/06perms.txt";
274             $pp = PAUSE::Permissions->new(uri => $perms_uri);
275              
276             If you've already got a copy lying around, you can tell the module to use that:
277              
278             $pp = PAUSE::Permissions->new( path => '/tmp/my06perms.txt' );
279              
280             Note that the file you provide this way must be in the post-processed
281             format, and not a raw copy of C<06perms.txt>.
282              
283             Having created an instance of C,
284             you can then call the C method
285             to get the permissions for a particular module.
286             The SYNOPSIS gives the basic usage.
287              
288             B: you should make sure you're using version 0.08 or later.
289             PAUSE now treats package names case insensitively with respect to
290             permissions, so this module does now as well.
291              
292             =head2 Getting permissions for multiple modules
293              
294             Sometimes you might want to use the C method
295             to get permissions for multiple modules, for example if you've built
296             up a list of modules from elsewhere. If you're doing this, then you
297             should set the C attribute to a true value:
298              
299             use PAUSE::Permissions 0.12;
300              
301             my $pp = PAUSE::Permissions->new(preload => 1);
302             foreach my $module_name (@long_list_of_modules) {
303             my $mp = $pp->module_permissions($module_name);
304             # do something with $mp (instance of PAUSE::Permissions::Module)
305             }
306              
307             With the C option enabled, the permissions data for I
308             modules will be pre-loaded into memory, making the above code much
309             quicker, trading that off against the memory used.
310              
311             This attribute was introduced in version 0.12, so you should
312             specify the minimum version when C'ing C.
313              
314             =head1 METHODS
315              
316             There are only four methods you need to know:
317             the constructor (C),
318             getting an iterator over individual entries (C),
319             getting an iterator over modules (C),
320             and C.
321              
322             =head2 new
323              
324             The constructor takes a hash of options:
325              
326             =over 4
327              
328             =item *
329              
330             B: the full path to the location where you'd like
331             C to cache the transformed version of 06perms.txt.
332              
333             =item *
334              
335             B: your own local copy of the file, to use instead of the
336             version in the C.
337             Note that this must be in the post-processed format for the local cache,
338             and not the original raw format of C<06perms.txt>.
339              
340             The constructor will C if the file doesn't exist, or isn't readable.
341              
342             =item *
343              
344             B: the URL for 06perms.txt;
345             defaults to L
346              
347             =item *
348              
349             B: the expiration time for cached data, once C<06perms.txt> has been grabbed.
350             The age can be specified using any format supported by L,
351             such '1 day', '2 minutes and 30 seconds', or '02:30:00'.
352              
353             =item *
354              
355             B: load all module permissions data into memory,
356             to speed up repeated calls to C.
357             This currently (0.12 onwards) doesn't currently affect any
358             other methods, though it might in a future release.
359              
360             =back
361              
362             So you might use the following,
363             to get C<06perms.txt> from your 'local' CPAN mirror and store it somewhere
364             of your choosing:
365              
366             $pp = PAUSE::Permissions->new(
367             uri => 'http://cpan.inode.at/modules/06perms.txt',
368             cachdir => '/tmp/pause',
369             );
370              
371             =head2 module_iterator
372              
373             This is a method that returns an instance of L,
374             which provides a simple mechanism for iterating over the whole permissions file,
375             module by module:
376              
377             $pp = PAUSE::Permissions->new();
378             $iterator = $pp->module_iterator();
379            
380             while (my $module = $iterator->next_module) {
381             print "module = ", $module->name, "\n";
382             print "owner = ", $module->owner, "\n";
383             print "co-maints = ", $module->co_maintainers, "\n";
384             }
385              
386             The C method returns either an instance of L,
387             or C when the end of the file is reached.
388              
389             =head2 entry_iterator
390              
391             This is a method that returns an instance of L,
392             which provides a simple mechanism for iterating over the whole permissions file,
393             line by line:
394              
395             $pp = PAUSE::Permissions->new();
396             $iterator = $pp->entry_iterator();
397             while (my $entry = $iterator->next) {
398             print "module = ", $entry->module, "\n";
399             print "user = ", $entry->user, "\n";
400             print "perm = ", $entry->permission, "\n";
401             }
402              
403             The C method returns a module name;
404             C returns the PAUSE id of a PAUSE user;
405             C is one of the three permission identifiers ('m', 'f', or 'c').
406              
407             =head2 module_permissions
408              
409             The C method takes a single module name,
410             and returns an instance of L:
411              
412             $mp = $pp->module_permissions( $module_name );
413              
414             Refer to the documentation for L,
415             but the key methods are:
416              
417             =over 4
418              
419             =item *
420              
421             C
422             returns the PAUSE id of the owner (see L below),
423             or C if there isn't a defined owner.
424              
425             =item *
426              
427             C
428             returns a list of PAUSE ids, or an empty list if the module has no co-maintainers.
429              
430             =back
431              
432             C returns C
433             if the module wasn't found in the permissions list.
434             If you've only just registered your new module,
435             or only just uploaded the first release,
436             then it might not have made it into the file yet.
437              
438              
439             =head2 can_upload
440              
441             This method takes a PAUSE id and a module name, and returns true (specifically C<1>)
442             if the specified user has permission to upload the specified module,
443             otherwise false (0).
444              
445             use PAUSE::Permissions 0.13;
446             my $pp = PAUSE::Permissions->new(preload => 1);
447             if ($pp->can_upload('NEILB', 'Foo::Bar')) {
448             # User can upload package
449             }
450              
451             Having permission to upload a module means that either
452             (a) the module appears in 06perms.txt and the specified user is one of the entries, or
453             (b) the module doesn't appear, so we assume it's not on CPAN.
454              
455             There are some things you should be aware of, when interpreting this:
456              
457             =over 4
458              
459             =item * the username is handled case insensitively.
460              
461             =item * the module name is handled case-insensitively.
462              
463             =item * if the module is not in C<06perms.txt> then this returns true,
464             but there is a delay between permissions being assigned by PAUSE and their
465             appearing in C<06perms.txt>. Also, if you're running with a long C
466             parameter, it might be a while before you see the change anyway.
467              
468             =item * a user might theoretically have permission to upload a module,
469             but a specific upload might fail if the distribution doesn't have an
470             appropriately named I
. If you're not familiar with that restriction,
471             read this L.
472              
473             =back
474              
475             Note: this method was introduced in version 0.13, so you should specify
476             this as a minimum version number if you're using the method.
477              
478             =head2 has_permission_for
479              
480             This method takes an author's PAUSE id and an optional string which specifies what type of permission
481             you're interested in. It will return an array ref with all package names for which the
482             author has the specified permission.
483              
484             The following example takes a PAUSE id C and determines all modules that NEILB
485             can upload:
486              
487             use PAUSE::Permissions 0.14;
488             my $pp = PAUSE::Permissions->new(preload => 1);
489             my $ref = $pp->has_permission_for('NEILB', 'upload');
490             print "NEILB has upload permission on:\n";
491             foreach my $module_name (@$ref) {
492             print " $module_name\n";
493             }
494              
495             There are three different permission types you can request:
496              
497             =over 4
498              
499             =item * 'upload' - ability to upload, which means co-maint or owner.
500              
501             =item * 'owner' - author is the owner of the package.
502              
503             =item * 'comaint' - author is comaint of the package but not owner.
504              
505             =back
506              
507             The package names are returned in case-insensitive alphabetic order.
508              
509             Note: this method was introduced in version 0.14, so you should specify
510             this as a minimum version number if you're using the method.
511              
512              
513             =head1 The 06perms.txt file
514              
515             You can find the file on CPAN:
516              
517             =over 4
518              
519             L
520              
521             =back
522              
523             As of October 2012 this file is 8.4M in size.
524              
525             The file starts with a header, followed by one blank line, then the body.
526             The body contains one line per module per user:
527              
528             Config::Properties,CMANLEY,c
529             Config::Properties,RANDY,f
530             Config::Properties,SALVA,m
531              
532             Each line has three values, separated by commas:
533              
534             =over 4
535              
536             =item *
537              
538             The name of a module.
539              
540             =item *
541              
542             A PAUSE user id, which by convention is always given in upper case.
543              
544             =item *
545              
546             A single character that specifies what permissions the user has with
547             respect to the module. See below.
548              
549             =back
550              
551             Note that this file lists I, not distributions.
552             Every module in a CPAN distribution will be listed separately in this file.
553             Modules are listed in alphabetical order, and for a given module,
554             the PAUSE ids are listed in alphabetical order.
555              
556             There are three characters that can appear in the permissions column:
557              
558             =over 4
559              
560             =item *
561              
562             B> identifies the user as the registered I of the module.
563             A module can only ever have zero or one user listed with the 'm' permission.
564             For more details on registering a module,
565             see L<04pause.html|http://www.cpan.org/modules/04pause.html#namespace>.
566              
567             =item *
568              
569             B> identifies the user as the I person to upload the module to CPAN.
570             You don't have to register a module before uploading it, and ownership
571             in this case is first-come-first-served.
572             A module can only ever have zero or one user listed with the 'f' permission.
573              
574             =item *
575              
576             B> identifies the user as a I of the module.
577             A module can have any number of co-maintainers.
578              
579             =back
580              
581             If you first upload a module, you'll get an 'f' against you in the file.
582             If you subsequently register the module, you'll get an 'm' against you.
583             Internally PAUSE will have you recorded with both an 'm' and an 'f',
584             but C<06perms.txt> only lists the highest precedence permission for each user.
585              
586             =head2 What do the permissions mean?
587              
588             =over 4
589              
590             =item *
591              
592             Various places refer to the 'owner' of the module.
593             This will be either the 'm' or 'f' permission, with 'm' taking precedence.
594             If a module has both an 'm' and an 'f' user listed, then the 'm' user
595             is considered the owner, and the 'f' user isn't.
596             If a module has a user with 'f' listed, but no 'm', then the 'f' user is
597             considered the owner.
598              
599             =item *
600              
601             If a module is listed in C<06perms.txt>,
602             then only the people listed (m, f, or c)
603             are allowed to upload (new) versions of the module.
604             If anyone else uploads a version of the module,
605             then the offending I will not be indexed:
606             it will appear in the uploader's directory on CPAN,
607             but won't be indexed under the module.
608              
609             =item *
610              
611             Only the owner for a module can grant co-maintainer status for a module.
612             I.e. if you have the 'm' permission, you can always do it.
613             If you have the 'f' permission, you can only do it if no-one else has
614             the 'm' permission.
615             You can grant co-maintainer status using the PAUSE web interface.
616              
617             =item *
618              
619             Regardless of your permissions, you can only remove things from CPAN that
620             you uploaded. If you're the owner, you can't delete a version uploaded
621             by a co-maintainer. If you weren't happy with it, you could revoke their
622             co-maintainer status and then upload a superseding version. But we'd
623             recommend you talk to them (first).
624              
625             =item *
626              
627             If you upload a distribution containing a number of previously unseen modules,
628             and haven't pre-registered them,
629             then you'll get an 'f' permission for all of the modules.
630             Let's say you upload a second release of the distribution,
631             which doesn't include one of the modules,
632             and then delete the first release from CPAN (via the PAUSE web interface).
633             After some time the module will no longer be on CPAN,
634             but you'll still have the 'f' permission in 06perms.txt.
635             You can free up the namespace using the PAUSE interface ("Change Permissions").
636              
637             =item *
638              
639             If your first upload of a module is a
640             L,
641             then you won't get permissions for the module.
642             You don't get permissions for a module until you've uploaded a non-developer
643             release containing the module,
644             that was accepted for indexing.
645              
646             =item *
647              
648             If you L maintenance
649             of a module, then you'll generally be given the permissions of the previous maintainer.
650             So if the previous maintainer had 'm', then you'll get 'm', and (s)he will be
651             downgraded to 'c'.
652             If the previous maintainer had 'f', then you'll get 'f', and the previous owner
653             will be downgraded to 'c'.
654              
655             =back
656              
657             =head1 SEE ALSO
658              
659             L checks whether all modules in (your)
660             CPAN distributions have the same permissions.
661              
662             C in L is used to get a local directory for
663             caching 06perms.txt.
664              
665             L is used to mirror 06perms.txt from CPAN.
666              
667             =head1 TODO
668              
669             =over 4
670              
671             =item *
672              
673             Request the file gzip'd, if we've got an appropriate module that can be used
674             to gunzip it.
675              
676             =item *
677              
678             At construct time we currently mirror the file;
679             should do this lazily, triggering it the first time you want a module's perms.
680              
681             =item *
682              
683             Every time you ask for a module, I scan the file from the start, then close it
684             once I've got the details for the requested module. Would be a lot more efficient
685             to keep the file open and start the search from there, as the file is sorted.
686             A binary chop on the file would be much more efficient as well.
687              
688              
689             =item *
690              
691             A command-line script.
692              
693             =back
694              
695             =head1 REPOSITORY
696              
697             L
698              
699             =head1 AUTHOR
700              
701             Neil Bowers Eneilb@cpan.orgE
702              
703             Thanks to Andreas KEnig, for patiently answering many questions
704             on how this stuff all works.
705              
706             =head1 COPYRIGHT AND LICENSE
707              
708             This software is copyright (c) 2012-2013 by Neil Bowers .
709              
710             This is free software; you can redistribute it and/or modify it under
711             the same terms as the Perl 5 programming language system itself.
712              
713             =cut
714