]> git.evergreen-ils.org Git - OpenSRF.git/blob - src/perl/lib/OpenSRF/Utils/Config.pm
Log redaction for sensitive input values, Perl side
[OpenSRF.git] / src / perl / lib / OpenSRF / Utils / Config.pm
1 package OpenSRF::Utils::Config::Section;
2
3 no strict 'refs';
4
5 use vars qw/@ISA $AUTOLOAD/;
6 push @ISA, qw/OpenSRF::Utils/;
7
8 use OpenSRF::Utils (':common');
9 use Net::Domain qw/hostfqdn/;
10
11 our $VERSION = "1.000";
12
13 my %SECTIONCACHE;
14 my %SUBSECTION_FIXUP;
15
16 #use overload '""' => \&OpenSRF::Utils::Config::dump_ini;
17
18 sub SECTION {
19         my $sec = shift;
20         return $sec->__id(@_);
21 }
22
23 sub new {
24         my $self = shift;
25         my $class = ref($self) || $self;
26
27         $self = bless {}, $class;
28
29         $self->_sub_builder('__id');
30         # Hard-code this to match old bootstrap.conf section name
31         # This hardcoded value is later overridden if the config is loaded
32         # with the 'base_path' option
33         $self->__id('bootstrap');
34
35         my $bootstrap = shift;
36
37         foreach my $key (sort keys %$bootstrap) {
38                 $self->_sub_builder($key);
39                 $self->$key($bootstrap->{$key});
40         }
41
42         return $self;
43 }
44
45 package OpenSRF::Utils::Config;
46
47 use vars qw/@ISA $AUTOLOAD $VERSION $OpenSRF::Utils::ConfigCache/;
48 push @ISA, qw/OpenSRF::Utils/;
49
50 use FileHandle;
51 use XML::LibXML;
52 use OpenSRF::Utils (':common');  
53 use OpenSRF::Utils::Logger;
54 use Net::Domain qw/hostfqdn/;
55
56 #use overload '""' => \&OpenSRF::Utils::Config::dump_ini;
57
58 sub import {
59         my $class = shift;
60         my $config_file = shift;
61
62         return unless $config_file;
63
64         $class->load( config_file => $config_file);
65 }
66
67 sub dump_ini {
68         no warnings;
69         my $self = shift;
70         my $string;
71         my $included = 0;
72         if ($self->isa('OpenSRF::Utils::Config')) {
73                 if (UNIVERSAL::isa(scalar(caller()), 'OpenSRF::Utils::Config' )) {
74                         $included = 1;
75                 } else {
76                         $string = "# Main File:  " . $self->FILE . "\n\n" . $string;
77                 }
78         }
79         for my $section ( ('__id', grep { $_ ne '__id' } sort keys %$self) ) {
80                 next if ($section eq 'env' && $self->isa('OpenSRF::Utils::Config'));
81                 if ($section eq '__id') {
82                         $string .= '['.$self->SECTION."]\n" if ($self->isa('OpenSRF::Utils::Config::Section'));
83                 } elsif (ref($self->$section)) {
84                         if (ref($self->$section) =~ /ARRAY/o) {
85                                 $string .= "list:$section = ". join(', ', @{$self->$section}) . "\n";
86                         } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config::Section')) {
87                                 if ($self->isa('OpenSRF::Utils::Config::Section')) {
88                                         $string .= "subsection:$section = " . $self->$section->SECTION . "\n";
89                                         next;
90                                 } else {
91                                         next if ($self->$section->{__sub} && !$included);
92                                         $string .= $self->$section . "\n";
93                                 }
94                         } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config')) {
95                                 $string .= $self->$section . "\n";
96                         }
97                 } else {
98                         next if $section eq '__sub';
99                         $string .= "$section = " . $self->$section . "\n";
100                 }
101         }
102         if ($included) {
103                 $string =~ s/^/## /gm;
104                 $string = "# Subfile:  " . $self->FILE . "\n#" . '-'x79 . "\n".'#include "'.$self->FILE."\"\n". $string;
105         }
106
107         return $string;
108 }
109
110 =head1 NAME
111  
112 OpenSRF::Utils::Config
113  
114
115 =head1 SYNOPSIS
116
117   use OpenSRF::Utils::Config;
118
119   my $config_obj = OpenSRF::Utils::Config->load( config_file   => '/config/file.cnf' );
120
121   my $attrs_href = $config_obj->bootstrap();
122
123   $config_obj->bootstrap->loglevel(0);
124
125   open FH, '>'.$config_obj->FILE() . '.new';
126   print FH $config_obj;
127   close FH;
128
129 =head1 DESCRIPTION
130
131 This module is mainly used by other OpenSRF modules to load an OpenSRF
132 configuration file.  OpenSRF configuration files are XML files that
133 contain a C<< <config> >> root element and an C<< <opensrf> >> child
134 element (in XPath notation, C</config/opensrf/>). Each child element
135 is converted into a hash key=>value pair. Elements that contain other
136 XML elements are pushed into arrays and added as an array reference to
137 the hash. Scalar values have whitespace trimmed from the left and
138 right sides.
139
140 =head1 EXAMPLE
141
142 Given an OpenSRF configuration file named F<opensrf_core.xml> with the
143 following content:
144
145   <?xml version='1.0'?>
146   <config>
147     <opensrf>
148       <router_name>router</router_name>
149
150       <routers> 
151         <router>localhost</router>
152         <router>otherhost</router>
153       </routers>
154
155       <logfile>/var/log/osrfsys.log</logfile>
156     </opensrf>
157   </config>
158
159 ... calling C<< OpenSRF::Utils::Config->load(config_file =>
160 'opensrf_core.xml') >> will create a hash with the following
161 structure:
162
163   {
164     router_name => 'router',
165     routers => ['localhost', 'otherhost'],
166     logfile => '/var/log/osrfsys.log'
167   }
168
169 You can retrieve any of these values by name from the bootstrap
170 section of C<$config_obj>; for example:
171
172   $config_obj->bootstrap->router_name
173
174 =head1 NOTES
175
176 For compatibility with previous versions of the OpenSRF configuration
177 files, the C<load()> method by default loads the C</config/opensrf>
178 section with the hardcoded name of B<bootstrap>.
179
180 However, it is possible to load child elements of C<< <config> >> other
181 than C<< <opensrf> >> by supplying a C<base_path> argument which specifies
182 the node you wish to begin loading from (in XPath notation). Doing so
183 will also replace the hardcoded C<bootstrap> name with the node name of
184 the last member of the given path.  For example:
185
186   my $config_obj = OpenSRF::Utils::Config->load(
187       config_file => '/config/file.cnf'
188       base_path => '/config/shared'
189   );
190
191   my $attrs_href = $config_obj->shared();
192
193 While it may be possible to load the entire file in this fashion (by
194 specifying an empty C<base_path>), doing so will break compatibility with
195 existing code which expects to find a C<bootstrap> member. Future
196 iterations of this module may extend its ability to parse the entire
197 OpenSRF configuration file in one pass while providing multiple base
198 sections named after the sibling elements of C</config/opensrf>.
199
200 Hashrefs of sections can be returned by calling a method of the object
201 of the same name as the section.  They can be set by passing a hashref
202 back to the same method.  Sections will B<NOT> be autovivicated,
203 though.
204
205
206 =head1 METHODS
207
208
209
210 =head2 OpenSRF::Utils::Config->load( config_file => '/some/config/file.cnf' )
211
212 Returns a OpenSRF::Utils::Config object representing the config file
213 that was loaded.  The most recently loaded config file (hopefully the
214 only one per app) is stored at $OpenSRF::Utils::ConfigCache. Use
215 OpenSRF::Utils::Config::current() to get at it.
216
217 =cut
218
219 sub load {
220         my $pkg = shift;
221         $pkg = ref($pkg) || $pkg;
222
223         my %args = @_;
224
225         (my $new_pkg = $args{config_file}) =~ s/\W+/_/g;
226         $new_pkg .= "::$pkg";
227         $new_section_pkg .= "${new_pkg}::Section";
228
229         {       eval <<"                PERL";
230
231                         package $new_pkg;
232                         use base $pkg;
233                         sub section_pkg { return '$new_section_pkg'; }
234
235                         package $new_section_pkg;
236                         use base "${pkg}::Section";
237         
238                 PERL
239         }
240
241         return $new_pkg->_load( %args );
242 }
243
244 sub _load {
245         my $pkg = shift;
246         $pkg = ref($pkg) || $pkg;
247         my $self = {@_};
248         bless $self, $pkg;
249
250         no warnings;
251         if ((exists $$self{config_file} and OpenSRF::Utils::Config->current) and (OpenSRF::Utils::Config->current->FILE eq $$self{config_file}) and (!$self->{force})) {
252                 delete $$self{force};
253                 return OpenSRF::Utils::Config->current();
254         }
255
256         $self->_sub_builder('__id');
257         $self->FILE($$self{config_file});
258         delete $$self{config_file};
259         return undef unless ($self->FILE);
260
261         my %load_args;
262         if (exists $$self{base_path}) { # blank != non-existent for this setting
263                 $load_args{base_path} = $$self{base_path};
264         }
265         $self->load_config(%load_args);
266         $self->load_env();
267         $self->mangle_dirs();
268         $self->mangle_logs();
269
270         $OpenSRF::Utils::ConfigCache = $self unless $self->nocache;
271         delete $$self{nocache};
272         delete $$self{force};
273         delete $$self{base_path};
274         return $self;
275 }
276
277 sub sections {
278         my $self = shift;
279         my %filters = @_;
280
281         my @parts = (grep { UNIVERSAL::isa($_,'OpenSRF::Utils::Config::Section') } values %$self);
282         if (keys %filters) {
283                 my $must_match = scalar(keys %filters);
284                 my @ok_parts;
285                 foreach my $part (@parts) {
286                         my $part_count = 0;
287                         for my $fkey (keys %filters) {
288                                 $part_count++ if ($part->$key eq $filters{$key});
289                         }
290                         push @ok_parts, $part if ($part_count == $must_match);
291                 }
292                 return @ok_parts;
293         }
294         return @parts;
295 }
296
297 sub current {
298         return $OpenSRF::Utils::ConfigCache;
299 }
300
301 sub FILE {
302         return shift()->__id(@_);
303 }
304
305 sub load_env {
306         my $self = shift;
307         my $host = $ENV{'OSRF_HOSTNAME'} || hostfqdn();
308         chomp $host;
309         $$self{env} = $self->section_pkg->new;
310         $$self{env}{hostname} = $host;
311 }
312
313 sub mangle_logs {
314         my $self = shift;
315         return unless ($self->logs && $self->dirs && $self->dirs->log_dir);
316         for my $i ( keys %{$self->logs} ) {
317                 next if ($self->logs->$i =~ /^\//);
318                 $self->logs->$i($self->dirs->log_dir."/".$self->logs->$i);
319         }
320 }
321
322 sub mangle_dirs {
323         my $self = shift;
324         return unless ($self->dirs && $self->dirs->base_dir);
325         for my $i ( keys %{$self->dirs} ) {
326                 if ( $i ne 'base_dir' ) {
327                         next if ($self->dirs->$i =~ /^\//);
328                         my $dir_tmp = $self->dirs->base_dir."/".$self->dirs->$i;
329                         $dir_tmp =~ s#//#/#go;
330                         $dir_tmp =~ s#/$##go;
331                         $self->dirs->$i($dir_tmp);
332                 }
333         }
334 }
335
336 sub load_config {
337         my $self = shift;
338         my $parser = XML::LibXML->new();
339         my %args = @_;
340
341         # Hash of config values
342         my %bootstrap;
343         
344         # Return an XML::LibXML::Document object
345         my $config = $parser->parse_file($self->FILE);
346
347         unless ($config) {
348                 OpenSRF::Utils::Logger->error("Could not open ".$self->FILE.": $!\n");
349                 die "Could not open ".$self->FILE.": $!\n";
350         }
351
352         # For backwards compatibility, we default to /config/opensrf
353         my $base_path;
354         if (exists $args{base_path}) { # allow for empty to import entire file
355                 $base_path = $args{base_path};
356         } else {
357                 $base_path = '/config/opensrf';
358         }
359         # Return an XML::LibXML::NodeList object matching all child elements
360         # of $base_path...
361         my $osrf_cfg = $config->findnodes("$base_path/child::*");
362
363         # Iterate through the nodes to pull out key=>value pairs of config settings
364         foreach my $node ($osrf_cfg->get_nodelist()) {
365                 my $child_state = 0;
366
367                 # This will be overwritten if it's a scalar setting
368                 $bootstrap{$node->nodeName()} = [];
369
370                 foreach my $child_node ($node->childNodes) {
371                         # from libxml/tree.h: nodeType 1 = ELEMENT_NODE
372                         next if $child_node->nodeType() != 1;
373
374                         # If the child node is an element, this element may
375                         # have multiple values; therefore, push it into an array
376             my $content = OpenSRF::Utils::Config::extract_child($child_node);
377                         push(@{$bootstrap{$node->nodeName()}}, $content) if $content;
378                         $child_state = 1;
379                 }
380                 if (!$child_state) {
381                         $bootstrap{$node->nodeName()} = OpenSRF::Utils::Config::extract_text($node->textContent);
382                 }
383         }
384
385         my $section = $self->section_pkg->new(\%bootstrap);
386         # if the Config was loaded with a 'base_path' option, overwrite the
387         # hardcoded 'bootstrap' name with something more reasonable
388         if (exists $$self{base_path}) { # blank != non-existent for this setting
389                 # name root node to reflect last member of base_path, or default to root
390                 my $root = (split('/', $$self{base_path}))[-1] || 'root';
391                 $section->__id($root);
392         }
393         my $sub_name = $section->SECTION;
394         $self->_sub_builder($sub_name);
395         $self->$sub_name($section);
396
397 }
398 sub extract_child {
399     my $node = shift;
400     use OpenSRF::Utils::SettingsParser;
401     return OpenSRF::Utils::SettingsParser::XML2perl($node);
402 }
403
404 sub extract_text {
405         my $self = shift;
406         $self =~ s/^\s*([.*?])\s*$//m;
407         return $self;
408 }
409
410 #------------------------------------------------------------------------------------------------------------------------------------
411
412 =head1 SEE ALSO
413
414         OpenSRF::Utils
415
416 =head1 LIMITATIONS
417
418 Elements containing heterogeneous child elements are treated as though they have the same element name;
419 for example:
420   <routers>
421     <router>localhost</router>
422     <furniture>chair</furniture>
423   </routers>
424
425 ... will simply generate a key=>value pair of C<< routers => ['localhost', 'chair'] >>.
426
427 =head1 BUGS
428
429 No known bugs, but report any to open-ils-dev@list.georgialibraries.org or mrylander@gmail.com.
430
431 =head1 COPYRIGHT AND LICENSING
432
433 Copyright (C) 2000-2007, Mike Rylander
434 Copyright (C) 2007, Laurentian University, Dan Scott <dscott@laurentian.ca>
435
436 The OpenSRF::Utils::Config module is free software. You may distribute under the terms
437 of the GNU General Public License version 2 or greater.
438
439 =cut
440
441
442 1;