File Coverage

lib/User/Information/Base.pm
Criterion Covered Total %
statement 29 204 14.2
branch 0 122 0.0
condition 0 62 0.0
subroutine 10 23 43.4
pod 5 5 100.0
total 44 416 10.5


line stmt bran cond sub pod time code
1             # Copyright (c) 2025 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: generic module for extracting information from user accounts
6              
7              
8             package User::Information::Base;
9              
10 1     1   20 use v5.20;
  1         4  
11 1     1   5 use strict;
  1         2  
  1         35  
12 1     1   4 use warnings;
  1         1  
  1         57  
13              
14 1     1   26 use parent qw(Data::Identifier::Interface::Userdata Data::Identifier::Interface::Known);
  1         2  
  1         6  
15              
16 1     1   78 use File::Spec;
  1         1  
  1         33  
17              
18 1     1   3 use User::Information;
  1         1  
  1         15  
19 1     1   458 use User::Information::Path;
  1         2  
  1         34  
20 1     1   556 use User::Information::Source;
  1         2  
  1         5  
21              
22 1     1   4 use Carp;
  1         1  
  1         69  
23              
24             our $VERSION = v0.05;
25              
26             use constant {
27 1         6 PATH_LOCAL_SYSAPI => User::Information::Path->new([qw(local sysapi)]),
28             PATH_LOCAL_ISLOCAL => User::Information::Path->new([qw(local islocal)]),
29 1     1   5 };
  1         1  
30              
31             my %_types = (
32             db => 'Data::TagDB',
33             extractor => 'Data::URIID',
34             fii => 'File::Information',
35             store => 'File::FStore',
36             );
37              
38             my %_aux_sources = map {$_ => 1} qw(User::Information::Source::Aggregate User::Information::Source::Tagpool User::Information::Source::Defaults);
39              
40              
41             sub attach {
42 0     0 1   my ($self, %opts) = @_;
43 0           my $weak = delete $opts{weak};
44              
45 0           foreach my $key (keys %_types) {
46 0           my $v = delete $opts{$key};
47 0 0         next unless defined $v;
48 0 0         croak 'Invalid type for key: '.$key unless eval {$v->isa($_types{$key})};
  0            
49 0   0       $self->{$key} //= $v;
50 0 0         croak 'Missmatch for key: '.$key unless $self->{$key} == $v;
51 0 0         weaken($self->{$key}) if $weak;
52             }
53              
54 0 0         croak 'Stray options passed' if scalar keys %opts;
55              
56 0           return $self;
57             }
58              
59              
60             sub get {
61 0     0 1   my ($self, $key, %opts) = @_;
62 0           my $o_has_default = exists $opts{default};
63 0           my $o_default = delete $opts{default};
64 0           my $o_list = delete $opts{list};
65 0           my $o_as = delete $opts{as};
66 0           my $info;
67             my $values;
68 0           my @res;
69              
70 0 0         unless (eval {$key->isa('User::Information::Path')}) {
  0            
71 0           $key = User::Information::Path->new($key);
72             }
73              
74 0           delete $opts{no_defaults};
75 0 0         croak 'Stray options passed' if scalar keys %opts;
76              
77 0           $values = $self->{data}{$key->_hashkey};
78              
79 0 0         unless (defined $values) {
80 0   0       $info //= eval { $self->_key_info($key) };
  0            
81 0 0         if (defined($info)) {
82 0   0       my $loadpath = $info->{loadpath} // $key;
83 0           $loadpath = $loadpath->_hashkey;
84 0 0         unless ($self->{loaded}{$loadpath}) {
85 0           $self->{loaded}{$loadpath} = 1;
86              
87 0 0         if (defined($info->{loader})) {
88 0           $values = eval { $info->{loader}->($self, $info, $key) };
  0            
89 0 0         if (defined $values) {
90 0           $self->{data}{$key->_hashkey} = $values;
91             }
92             }
93              
94 0 0         $self->_value_add($key, @{$info->{values}}) if ref($info->{values}) eq 'ARRAY';
  0            
95 0 0         $self->_value_add($key, $info->{values} ) if ref($info->{values}) eq 'HASH';
96              
97 0   0       $values //= $self->{data}{$key->_hashkey};
98             }
99             }
100             }
101              
102 0 0         unless (defined $values) {
103 0 0         if ($o_has_default) {
104 0 0         if ($o_list) {
105 0           return @{$o_default};
  0            
106             } else {
107 0           return $o_default;
108             }
109             } else {
110 0           croak 'No value found';
111             }
112             }
113              
114 0 0         unless (defined($o_as)) {
115 0   0       $info //= eval { $self->_key_info($key) };
  0            
116              
117 0 0         $o_as = $info->{rawtype} if defined $info;
118              
119 0   0       $o_as //= 'raw';
120             }
121              
122 0           foreach my $v (@{$values}) {
  0            
123 0 0         if (defined($v->{$o_as})) {
124 0           push(@res, $v->{$o_as});
125             } else {
126 0           my $converted;
127              
128 0   0       $info //= $self->_key_info($key);
129              
130 0 0 0       if (defined($v->{raw}) && defined($info->{rawtype}) && $info->{rawtype} eq $o_as) {
      0        
131 0           $converted = $v->{raw};
132             }
133              
134 0 0 0       if (!defined($converted) && $o_as eq 'raw' && defined($info->{rawtype}) && defined($v->{$info->{rawtype}})) {
      0        
      0        
135 0           $converted = $v->{$info->{rawtype}};
136             }
137              
138 0 0 0       if (!defined($converted) && defined($info->{converter})) {
139 0           $converted = $info->{converter}->($self, $info, $key, $v, $o_as);
140             }
141              
142 0 0         if (defined $converted) {
143 0           push(@res, $converted);
144 0           $v->{$o_as} = $converted;
145             } else {
146 0           croak 'Converting types is currently not supported';
147             }
148             }
149             }
150              
151 0 0         return @res if $o_list;
152 0 0         if (scalar(@res) != 1) {
153 0           croak 'Wrong number of results and not in list mode';
154             }
155 0           return $res[0];
156             }
157              
158              
159             sub node {
160 0     0 1   my ($self, %opts) = @_;
161 0           my $o_has_default = exists $opts{default};
162 0           my $o_default = delete $opts{default};
163              
164 0           delete $opts{no_defaults};
165 0 0         croak 'Stray options passed' if scalar keys %opts;
166              
167 0 0         return $self->{node} if defined $self->{node};
168 0 0         return $self->{node} = User::Information->local_node if $self->_is_local;
169 0 0         return $o_default if $o_has_default;
170 0           croak 'No node known';
171             }
172              
173              
174             sub displayname {
175 0     0 1   my ($self, %opts) = @_;
176 0 0 0       $opts{default} //= 'no name' unless $opts{no_defaults};
177 0           return $self->get(['aggregate' => 'displayname'], %opts);
178             }
179              
180              
181             sub file {
182 0     0 1   my ($self, $key, %opts) = @_;
183 0           my @filename = $self->get($key, as => 'filename', list => 1);
184 0           my $o_extra = delete $opts{extra};
185 0           my $o_directory = delete $opts{directory};
186 0           my $o_open = delete $opts{open};
187 0           my $o_binmode = delete $opts{binmode};
188              
189 0 0         croak 'Stray options passed' if scalar keys %opts;
190              
191 0           foreach my $filename (@filename) {
192 0 0         if (defined $o_extra) {
193 0 0         my @extra = ref($o_extra) ? @{$o_extra} : ($o_extra);
  0            
194 0 0         $filename = $o_directory ? File::Spec->catdir($filename, @extra) : File::Spec->catfile($filename, @extra);
195             }
196              
197 0 0         if (defined $o_open) {
198 0           my $mode;
199              
200 0 0         croak 'Invalid open mode: '.$o_open unless $o_open =~ /^[abrw]+$/;
201              
202 0 0         if ($o_open =~ /w/) {
    0          
203 0           $mode = '>';
204             } elsif ($o_open =~ /a/) {
205 0           $mode = '>>';
206             } else {
207 0           $mode = '<';
208             }
209              
210 0 0 0       $o_binmode //= 1 if $o_open =~ /b/;
211              
212 0 0         if ($o_directory) {
213 0 0         if (opendir(my $fh, $filename)) {
214 0           return $fh;
215             }
216             } else {
217 0 0         if (open(my $fh, $mode, $filename)) {
218 0 0         $fh->binmode if $o_binmode;
219 0           return $fh;
220             }
221             }
222             }
223              
224 0 0         return $filename if -e $filename;
225             }
226              
227 0           croak 'No file found';
228             }
229              
230             # ---- Private helpers ----
231             sub _new {
232 0     0     my ($pkg, $type, $request, %opts) = @_;
233 0           my $self = bless {data => {}, discovered => {}, loaded => {}, sources => {}}, $pkg;
234              
235             # Attach subobjects:
236 0           $self->attach(map {$_ => delete $opts{$_}} keys(%_types), 'weak');
  0            
237              
238 0 0         croak 'Stray options passed' if scalar keys %opts;
239              
240 0           $self->_load('User::Information::Source::Aggregate');
241 0 0         if ($type eq 'from') {
    0          
    0          
242 0 0         if ($request eq User::Information->SPECIAL_ME) {
    0          
    0          
243 0           $self->_load('User::Information::Source::Local');
244 0           $self->_load('User::Information::Source::Env');
245 0 0         $self->_load('User::Information::Source::POSIX', data => {real_user => $<, effective_user => $>, real_group => $(, effective_group => $)}) if $self->_is_sysapi('posix');
246 0           $self->_load('User::Information::Source::XDG', me => 1);
247             } elsif ($request eq User::Information->SPECIAL_CGI) {
248 0           $self->_load('User::Information::Source::CGI');
249 0           $self->_load('User::Information::Source::Env', cgi => 1);
250             } elsif (User::Information->SPECIAL_LOCAL_NODE->eq($request)) {
251 0           $self->_load('User::Information::Source::Local');
252 0           $self->_load('User::Information::Source::VFS');
253 0           $self->_load('User::Information::Source::POSIX', data => {uname => undef});
254 0           $self->_load('User::Information::Source::LocalNodeMisc');
255             }
256             } elsif ($type eq 'sysuid') {
257 0           $self->_load('User::Information::Source::Local');
258 0 0         $self->_load('User::Information::Source::POSIX', data => {user => $request}) if $self->_is_sysapi('posix');
259             } elsif ($type eq 'sysgid') {
260 0           $self->_load('User::Information::Source::Local');
261 0 0         $self->_load('User::Information::Source::POSIX', data => {group => $request}) if $self->_is_sysapi('posix');
262             }
263 0 0 0       if (!$self->{sources}{'User::Information::Source::XDG'} && $self->_is_local && defined(my $username = $self->get(['aggregate', 'username'], default => undef))) {
      0        
264 0           $self->_load('User::Information::Source::XDG', username => $username);
265             }
266 0 0         if ($self->_is_local) {
267 0           $self->_load('User::Information::Source::Git');
268 0           $self->_load('User::Information::Source::Tagpool');
269             }
270 0           $self->_load('User::Information::Source::Defaults');
271              
272 0 0         croak 'No matching data sources found for type/request' unless scalar(grep {!$_aux_sources{$_}} keys %{$self->{sources}});
  0            
  0            
273              
274 0           return $self;
275             }
276              
277             sub _key_info {
278 0     0     my ($self, $key) = @_;
279 0   0       return $self->{discovered}{$key->_hashkey} // croak 'Unknown key';
280             }
281              
282             sub _key_add {
283 0     0     my ($self, @info) = @_;
284 0           my $discovered = $self->{discovered};
285              
286 0           foreach my $ent (@info) {
287 0   0       $discovered->{$ent->{path}->_hashkey} //= $ent;
288             }
289             }
290              
291             sub _load {
292 0     0     my ($self, $source, %opts) = @_;
293 0           $self->{sources}{$source} = undef;
294 0           $self->_key_add(User::Information::Source->_load($source, $self, %opts));
295             }
296              
297             sub _is_sysapi {
298 0     0     my ($self, $api) = @_;
299 0   0       my $sysapi = $self->{sysapi} //= $self->get(PATH_LOCAL_SYSAPI, default => '');
300 0           return $sysapi eq $api;
301             }
302              
303             sub _is_local {
304 0     0     my ($self) = @_;
305 0   0       return $self->{islocal} //= $self->get(PATH_LOCAL_ISLOCAL, default => '', as => 'bool');
306             }
307              
308             sub _value_add {
309 0     0     my ($self, $key, @values) = @_;
310 0           my $d;
311              
312 0           @values = grep {defined} @values;
  0            
313              
314 0 0         return unless scalar @values;
315              
316 0   0       $d = $self->{data}{$key->_hashkey} //= [];
317 0           push(@{$d}, @values);
  0            
318 0           $self->{loaded}{$key->_hashkey} = 1;
319             }
320              
321             sub _known_provider {
322 0     0     my ($self, $class, %opts) = @_;
323              
324 0 0         croak 'Unsupported options passed' if scalar(keys %opts);
325              
326 0 0         return [map {$_->{path}} values %{$self->{discovered}}], not_identifiers => 1 if $class eq 'paths';
  0            
  0            
327              
328 0           croak 'Unknown class';
329             }
330              
331             1;
332              
333             __END__