]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Config.pm
355e64bfec6a19036519054b17a716641d2c7310
[public-inbox.git] / lib / PublicInbox / Config.pm
1 # Copyright (C) 2014-2018 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # Used throughout the project for reading configuration
5 #
6 # Note: I hate camelCase; but git-config(1) uses it, but it's better
7 # than alllowercasewithoutunderscores, so use lc('configKey') where
8 # applicable for readability
9
10 package PublicInbox::Config;
11 use strict;
12 use warnings;
13 require PublicInbox::Inbox;
14 use PublicInbox::Spawn qw(popen_rd);
15
16 sub _array ($) { ref($_[0]) eq 'ARRAY' ? $_[0] : [ $_[0] ] }
17
18 # returns key-value pairs of config directives in a hash
19 # if keys may be multi-value, the value is an array ref containing all values
20 sub new {
21         my ($class, $file) = @_;
22         $file = default_file() unless defined($file);
23         $file = ref $file ? $file : git_config_dump($file);
24         my $self = bless $file, $class;
25
26         # caches
27         $self->{-by_addr} ||= {};
28         $self->{-by_name} ||= {};
29         $self->{-by_newsgroup} ||= {};
30         $self->{-no_obfuscate} ||= {};
31         $self->{-limiters} ||= {};
32         $self->{-code_repos} ||= {}; # nick => PublicInbox::Git object
33
34         if (my $no = delete $self->{'publicinbox.noobfuscate'}) {
35                 $no = [ $no ] if ref($no) ne 'ARRAY';
36                 my @domains;
37                 foreach my $n (@$no) {
38                         my @n = split(/\s+/, $n);
39                         foreach (@n) {
40                                 if (/\S+@\S+/) { # full address
41                                         $self->{-no_obfuscate}->{lc $_} = 1;
42                                 } else {
43                                         # allow "example.com" or "@example.com"
44                                         s/\A@//;
45                                         push @domains, quotemeta($_);
46                                 }
47                         }
48                 }
49                 my $nod = join('|', @domains);
50                 $self->{-no_obfuscate_re} = qr/(?:$nod)\z/i;
51         }
52
53         $self;
54 }
55
56 sub lookup {
57         my ($self, $recipient) = @_;
58         my $addr = lc($recipient);
59         my $inbox = $self->{-by_addr}->{$addr};
60         return $inbox if $inbox;
61
62         my $pfx;
63
64         foreach my $k (keys %$self) {
65                 $k =~ m!\A(publicinbox\.[^/]+)\.address\z! or next;
66                 my $v = $self->{$k};
67                 if (ref($v) eq "ARRAY") {
68                         foreach my $alias (@$v) {
69                                 (lc($alias) eq $addr) or next;
70                                 $pfx = $1;
71                                 last;
72                         }
73                 } else {
74                         (lc($v) eq $addr) or next;
75                         $pfx = $1;
76                         last;
77                 }
78         }
79         defined $pfx or return;
80         _fill($self, $pfx);
81 }
82
83 sub lookup_name ($$) {
84         my ($self, $name) = @_;
85         $self->{-by_name}->{$name} || _fill($self, "publicinbox.$name");
86 }
87
88 sub each_inbox {
89         my ($self, $cb) = @_;
90         my %seen;
91         foreach my $k (keys %$self) {
92                 $k =~ m!\Apublicinbox\.([^/]+)\.mainrepo\z! or next;
93                 next if $seen{$1};
94                 $seen{$1} = 1;
95                 my $ibx = lookup_name($self, $1) or next;
96                 $cb->($ibx);
97         }
98 }
99
100 sub lookup_newsgroup {
101         my ($self, $ng) = @_;
102         $ng = lc($ng);
103         my $rv = $self->{-by_newsgroup}->{$ng};
104         return $rv if $rv;
105
106         foreach my $k (keys %$self) {
107                 $k =~ m!\A(publicinbox\.[^/]+)\.newsgroup\z! or next;
108                 my $v = $self->{$k};
109                 my $pfx = $1;
110                 if ($v eq $ng) {
111                         $rv = _fill($self, $pfx);
112                         return $rv;
113                 }
114         }
115         undef;
116 }
117
118 sub limiter {
119         my ($self, $name) = @_;
120         $self->{-limiters}->{$name} ||= do {
121                 require PublicInbox::Qspawn;
122                 my $max = $self->{"publicinboxlimiter.$name.max"};
123                 PublicInbox::Qspawn::Limiter->new($max);
124         };
125 }
126
127 sub config_dir { $ENV{PI_DIR} || "$ENV{HOME}/.public-inbox" }
128
129 sub default_file {
130         my $f = $ENV{PI_CONFIG};
131         return $f if defined $f;
132         config_dir() . '/config';
133 }
134
135 sub git_config_dump {
136         my ($file) = @_;
137         my ($in, $out);
138         my @cmd = (qw/git config/, "--file=$file", '-l');
139         my $cmd = join(' ', @cmd);
140         my $fh = popen_rd(\@cmd) or die "popen_rd failed for $file: $!\n";
141         my %rv;
142         local $/ = "\n";
143         while (defined(my $line = <$fh>)) {
144                 chomp $line;
145                 my ($k, $v) = split(/=/, $line, 2);
146                 my $cur = $rv{$k};
147
148                 if (defined $cur) {
149                         if (ref($cur) eq "ARRAY") {
150                                 push @$cur, $v;
151                         } else {
152                                 $rv{$k} = [ $cur, $v ];
153                         }
154                 } else {
155                         $rv{$k} = $v;
156                 }
157         }
158         close $fh or die "failed to close ($cmd) pipe: $?";
159
160         \%rv;
161 }
162
163 sub valid_inbox_name ($) {
164         my ($name) = @_;
165
166         # Similar rules found in git.git/remote.c::valid_remote_nick
167         # and git.git/refs.c::check_refname_component
168         # We don't reject /\.lock\z/, however, since we don't lock refs
169         if ($name eq '' || $name =~ /\@\{/ ||
170             $name =~ /\.\./ || $name =~ m![/:\?\[\]\^~\s\f[:cntrl:]\*]! ||
171             $name =~ /\A\./ || $name =~ /\.\z/) {
172                 return 0;
173         }
174
175         # Note: we allow URL-unfriendly characters; users may configure
176         # non-HTTP-accessible inboxes
177         1;
178 }
179
180 # parse a code repo
181 # Only git is supported at the moment, but SVN and Hg are possibilities
182 sub _fill_code_repo {
183         my ($self, $nick) = @_;
184         my $pfx = "coderepo.$nick";
185
186         my $dir = $self->{"$pfx.dir"}; # aka "GIT_DIR"
187         unless (defined $dir) {
188                 warn "$pfx.repodir unset";
189                 return;
190         }
191
192         my $git = PublicInbox::Git->new($dir);
193         foreach my $t (qw(blob commit tree tag)) {
194                 $git->{$t.'_url_format'} =
195                                 _array($self->{lc("$pfx.${t}UrlFormat")});
196         }
197
198         if (my $cgits = $self->{lc("$pfx.cgitUrl")}) {
199                 $git->{cgit_url} = $cgits = _array($cgits);
200
201                 # cgit supports "/blob/?id=%s", but it's only a plain-text
202                 # display and requires an unabbreviated id=
203                 foreach my $t (qw(blob commit tag)) {
204                         $git->{$t.'_url_format'} ||= map {
205                                 "$_/$t/?id=%s"
206                         } @$cgits;
207                 }
208         }
209         # TODO: support gitweb and other repository viewers?
210         # TODO: parse cgitrc
211
212         $git;
213 }
214
215 sub _fill {
216         my ($self, $pfx) = @_;
217         my $rv = {};
218
219         foreach my $k (qw(mainrepo filter url newsgroup
220                         infourl watch watchheader httpbackendmax
221                         replyto feedmax nntpserver indexlevel)) {
222                 my $v = $self->{"$pfx.$k"};
223                 $rv->{$k} = $v if defined $v;
224         }
225         foreach my $k (qw(obfuscate)) {
226                 my $v = $self->{"$pfx.$k"};
227                 defined $v or next;
228                 if ($v =~ /\A(?:false|no|off|0)\z/) {
229                         $rv->{$k} = 0;
230                 } elsif ($v =~ /\A(?:true|yes|on|1)\z/) {
231                         $rv->{$k} = 1;
232                 } else {
233                         warn "Ignoring $pfx.$k=$v in config, not boolean\n";
234                 }
235         }
236         # TODO: more arrays, we should support multi-value for
237         # more things to encourage decentralization
238         foreach my $k (qw(address altid nntpmirror coderepo)) {
239                 if (defined(my $v = $self->{"$pfx.$k"})) {
240                         $rv->{$k} = _array($v);
241                 }
242         }
243
244         return unless $rv->{mainrepo};
245         my $name = $pfx;
246         $name =~ s/\Apublicinbox\.//;
247
248         if (!valid_inbox_name($name)) {
249                 warn "invalid inbox name: '$name'\n";
250                 return;
251         }
252
253         $rv->{name} = $name;
254         $rv->{-pi_config} = $self;
255         $rv = PublicInbox::Inbox->new($rv);
256         foreach (@{$rv->{address}}) {
257                 my $lc_addr = lc($_);
258                 $self->{-by_addr}->{$lc_addr} = $rv;
259                 $self->{-no_obfuscate}->{$lc_addr} = 1;
260         }
261         if (my $ng = $rv->{newsgroup}) {
262                 $self->{-by_newsgroup}->{$ng} = $rv;
263         }
264         $self->{-by_name}->{$name} = $rv;
265         if ($rv->{obfuscate}) {
266                 $rv->{-no_obfuscate} = $self->{-no_obfuscate};
267                 $rv->{-no_obfuscate_re} = $self->{-no_obfuscate_re};
268                 each_inbox($self, sub {}); # noop to populate -no_obfuscate
269         }
270
271         if (my $ibx_code_repos = $rv->{coderepo}) {
272                 my $code_repos = $self->{-code_repos};
273                 my $repo_objs = $rv->{-repo_objs} = [];
274                 foreach my $nick (@$ibx_code_repos) {
275                         valid_inbox_name($nick) or next;
276                         my $repo = $code_repos->{$nick} ||=
277                                                 _fill_code_repo($self, $nick);
278                         push @$repo_objs, $repo if $repo;
279                 }
280         }
281
282         $rv
283 }
284
285 1;