1#! /usr/bin/env perl
2# Shell access to Foswiki.spec, Config.spec and LocalSite.cfg
3# See bottom of file for POD documentation.
4#
5# Author: Crawford Currie http://c-dot.co.uk
6#
7# Foswiki - The Free and Open Source Wiki, http://foswiki.org/
8#
9# Copyright (C) 2013-2016 Foswiki Contributors. Foswiki Contributors
10# are listed in the AUTHORS file in the root of this distribution.
11# NOTE: Please extend that file, not this notice.
12#
13# This program is free software; you can redistribute it and/or
14# modify it under the terms of the GNU General Public License
15# as published by the Free Software Foundation; either version 2
16# of the License, or (at your option) any later version. For
17# more details read LICENSE in the root of this distribution.
18#
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
22#
23# As per the GPL, removal of this notice is prohibited.
24
25use warnings;
26use strict;
27
28use Encode;
29use Getopt::Long;
30use Pod::Usage   ();
31use Data::Dumper ();
32
33# Assume we are in the tools dir, and we can find bin and lib from there
34use FindBin ();
35$FindBin::Bin =~ /^(.*)$/;
36my $bin = $1;
37
38use lib "$FindBin::Bin/../bin";
39require 'setlib.cfg';
40
41# SMELL: setlib does "require CGI" which sets STDIN to binmode.
42binmode( STDIN, ':crlf' );
43
44# require, not use.  The libpath is resolved by setlib.cfg,
45# so "use SomeModule;" will fail.
46
47require Assert;
48
49require Foswiki::Configure::Root;
50require Foswiki::Configure::LoadSpec;
51require Foswiki::Configure::Load;
52require Foswiki::Configure::Query;
53
54{
55
56    package Foswiki::Configure::ShellReporter;
57
58    require Foswiki::Configure::Reporter;
59    our @ISA = ('Foswiki::Configure::Reporter');
60
61    sub new {
62        return bless( {}, $_[0] );
63    }
64
65    sub NOTE {
66        my $this = shift;
67        my $text = join( "\n", @_ ) . "\n" if ( scalar @_ );
68        $this->{notes}++;
69
70        # Take out block formatting tags
71        $text =~ s/<\/?verbatim>//g;
72
73        # Take out active elements
74        $text =~ s/<(button|select|option|textarea).*?<\/\1>//g;
75        print Encode::encode_utf8($text);
76    }
77
78    sub WARN {
79        my $this = shift;
80        $this->{warnings}++;
81        print "WARNING: ";
82        $this->NOTE(@_);
83    }
84
85    sub ERROR {
86        my $this = shift;
87        $this->{errors}++;
88        print "#### ERROR: ";
89        $this->NOTE(@_);
90    }
91
92    sub CHANGED {
93        my ( $this, $k ) = @_;
94        $this->SUPER::CHANGED($k);
95
96        print Encode::encode_utf8(
97            "\$Foswiki::cfg$k = $this->{changes}->{$k};\n");
98    }
99
100    sub WIZARD {
101        return '';
102    }
103
104    sub has_level {
105        my ( $this, $level ) = @_;
106        return $this->{$level};
107    }
108};
109
110# Command-line parameter handling
111
112my $params = {
113    keys   => [],
114    method => 'check_current_value',
115    set    => {},
116};
117
118my %actions;
119
120sub _keys {
121    my ( $name, $val ) = @_;
122    push( @{ $params->{keys} }, $val ) if defined $val && length($val);
123    $actions{$name} = 1;
124}
125
126sub _scalar {
127    my ( $name, $val ) = @_;
128    $params->{$name} = $val;
129    $actions{$name} = 1;
130}
131
132my $nonASCII = 0;
133
134unless ( ${^UNICODE} >= 32 ) {
135    foreach (@ARGV) {
136        if ( $_ =~ m/\P{Ascii}/ ) {
137            $nonASCII = 1;
138        }
139    }
140
141    if ($nonASCII) {
142        print STDERR
143"WARNING: non-ASCII input detected.  Should you be using the -CAS option?\n     perl -CAS tools/configure ...\n";
144
145    }
146}
147
148my $result = Getopt::Long::GetOptions(
149    'check_current_value:s' => \&_keys,
150    'dependencies'          => sub {
151        $params->{check_dependencies} = 1;
152    },
153    'depth=i',
154    \&_scalar,
155    'getcfg:s'   => \&_keys,
156    'getspec:s%' => sub {
157        my ( $name, $key, $val ) = @_;
158        $actions{$name} = 1;
159        if ( defined $key ) {
160            $params->{get}->{keys} = $key;
161        }
162    },
163    'help' => sub {
164        Pod::Usage::pod2usage( -exitstatus => 0, -verbose => 2 );
165    },
166    'json'     => \&_scalar,
167    'method=s' => \&_scalar,
168    'noprompt' => \&_scalar,
169    'search=s' => \&_scalar,
170    'save'     => \&_scalar,
171    'set=s%'   => \%{ $params->{set} },
172
173    #SMELL: This would allow utf-8 for any config parameters, but can't
174    #        be assumed for all shells.  Running "perl -CA configure ... "
175    #        works instead.
176    #'set=s%'   => sub {
177    #    $params->{set}->{$_[1]} = Encode::decode_utf8($_[2]);
178    #    },
179    'trace'    => \&_scalar,
180    'verbose'  => \&_scalar,
181    'wizard=s' => \&_scalar,
182    'expert'   => \&_scalar,
183    'args=s%'  => sub {
184        my ( $name, $key, $val ) = @_;
185        $params->{args}->{$key} = $val;
186    },
187);
188
189# Check parameters
190
191my $action = '';
192my %uniq;
193my @methods =
194  grep { defined &{"Foswiki::Configure::Query::$_"} }
195  grep { $_ =~ /^[a-z]/ }
196  keys %Foswiki::Configure::Query::;
197
198foreach my $a (@methods) {
199    if ( $actions{$a} ) {
200        $action = $a;
201        $uniq{$a} = 1;
202    }
203}
204
205if ( scalar( keys %uniq ) > 1 ) {
206    print "Only one of "
207      . join( ' ', map { $_ =~ /(\w+)$/; "-$1" } keys %uniq )
208      . " allowed\n";
209    exit 1;
210}
211
212if ( !scalar( keys %uniq ) && !$actions{save} ) {
213    Pod::Usage::pod2usage( -exitstatus => 0, -verbose => 2 );
214}
215
216if ( $action =~ /^get/ && scalar( keys %{ $params->{set} } ) ) {
217    print "-set doesn't work with -$action\n";
218    exit 1;
219}
220
221# ---++ Prompt for config values
222#    * $root - Configuration root
223#    * $keys - ={Configuration}{Key}{Path}= to a single variable
224#    * $default - Default if any,  undef to require a response.
225#    * $prompts - Alternate prompt. If undef, the help text from the configuration spec is used.
226#    * $opt - Flag for optional values.   Optional values can have an "empty" reponse, Pressing enter will save a "null", and the keyword 'none' will omit setting the option.
227#
228
229sub _prompt {
230    my ( $root, $keys, $default, $prompt, $opt ) = @_;
231    print "\n";
232
233    unless ( $params->{expert} ) {
234        if ($prompt) {
235            print $prompt;
236        }
237        else {
238            my $vob = $root->getValueObject($keys);
239            if ( $vob && $vob->{desc} ) {
240                print "$vob->{desc}\n";
241            }
242        }
243    }
244
245    print "\n";
246    local $/ = "\n";
247    my $reply;
248    while ( !defined $reply ) {
249        print $keys;
250        print " ($default)" if defined $default;
251        print ': ';
252        $reply = <STDIN>;
253        chomp($reply);
254        $reply ||= $default;
255        last if $opt;
256    }
257
258    $reply = '' unless ( defined $reply );
259    return if ( $opt && $reply eq 'none' );
260
261# Add keys to the params to be processed by the Save wizard.
262# This invokes all needed handlers like ONSAVE vs. modifying the config directly
263    eval "\$params->{set}{'$keys'} = '$reply'";
264    if ($@) {
265        print "Failed to set $keys: "
266          . Foswiki::Configure::Reporter::stripStacktrace($@);
267    }
268}
269
270#$Foswiki::Configure::LoadSpec::RAW_VALS = 1;
271
272# Initialise
273if ( Foswiki::Configure::Load::readConfig( 0, 0, 0 ) ) {
274    $Foswiki::cfg{isVALID} = 1;
275}
276
277my $root     = Foswiki::Configure::Root->new();
278my $reporter = Foswiki::Configure::ShellReporter->new();
279
280Foswiki::Configure::LoadSpec::readSpec( $root, $reporter );
281if ( $reporter->has_level('errors') ) {
282    exit 1;
283}
284
285unless ( $Foswiki::cfg{isVALID} ) {
286    %Foswiki::cfg = ();
287    print "LocalSite.cfg load failed\n"
288      . Foswiki::Configure::Reporter::stripStacktrace($@);
289
290    # Run the bootstrap process. This guesses all the critical path settings.
291    require Foswiki::Configure::Bootstrap;
292    Foswiki::Configure::Bootstrap::bootstrapConfig();
293
294#SMELL: Another way to do this would be to loop through $Foswiki::cfg{BOOTSTRAP} array
295#       of config keys, But this allows customized prompts and defaults.  Anything prompted
296#       here should also be in the BOOTSTRAP array.   And anything guessed in Load::bootstrapConfig
297#       should probably be verified here unless very certain that we guess corrrectly.
298
299    unless ( $params->{expert} || $params->{noprompt} ) {
300
301        # Ask for missing parameters that cannot bootstrap in CLI
302        print "\n** Enter values for critical configuration items.\n";
303        unless ( ${^UNICODE} >= 32 ) {
304            print
305"** If any input will use utf-8 data (non-ASCII), run as 'perl -CAS tools/configure ...'\n";
306        }
307        print
308"** type a new value or hit return to accept the value in brackets.\n";
309    }
310
311    unless ( $params->{noprompt} ) {
312        _prompt( $root, '{DefaultUrlHost}', 'http://localhost' );
313        _prompt( $root, '{ScriptUrlPath}',  '/foswiki/bin' );
314        _prompt(
315            $root,
316            '{ScriptUrlPaths}{view}',
317            undef,
318'Enter optional short URL for view script, Press enter for shortest URLs,  Enter "none" to use full URLs.',
319            1
320        );
321        _prompt( $root, '{PubUrlPath}', '/foswiki/pub' );
322
323        eval 'use Crypt::PasswdMD5';
324        unless ($@) {
325            _prompt( $root, '{Password}', undef,
326                "Enter a password for the 'admin' sudo account.\n" );
327            push( @{ $Foswiki::cfg{BOOTSTRAP} }, '{Password}' );
328        }
329        else {
330            print
331"*** Unable to set password - Module Crypt::PasswdMD5 is not available\n";
332        }
333
334        # And confirm the rest of the guesses
335        print
336" The following directory settings have been guessed.  Press enter to confirm each setting:\n";
337
338# Note:  Bootstrap will decode the bytes read from the path into utf-8 characters
339# But the encoding needs to be reversed when passing it through the command prompt
340
341        _prompt( $root, '{ScriptDir}',
342            Encode::encode_utf8( $Foswiki::cfg{ScriptDir} ) );
343        _prompt( $root, '{ScriptSuffix}', $Foswiki::cfg{ScriptSuffix},
344            undef, 1 );
345        _prompt( $root, '{DataDir}',
346            Encode::encode_utf8( $Foswiki::cfg{DataDir} ) );
347        _prompt( $root, '{PubDir}',
348            Encode::encode_utf8( $Foswiki::cfg{PubDir} ) );
349        _prompt( $root, '{TemplateDir}',
350            Encode::encode_utf8( $Foswiki::cfg{TemplateDir} ) );
351        _prompt( $root, '{LocalesDir}',
352            Encode::encode_utf8( $Foswiki::cfg{LocalesDir} ) );
353        _prompt( $root, '{WorkingDir}',
354            Encode::encode_utf8( $Foswiki::cfg{WorkingDir} ) );
355        _prompt( $root, '{ToolsDir}',
356            Encode::encode_utf8( $Foswiki::cfg{ToolsDir} ) );
357        _prompt( $root, '{Store}{Implementation}',
358            $Foswiki::cfg{Store}{Implementation} );
359        _prompt( $root, '{Store}{Encoding}', $Foswiki::cfg{Store}{Encoding},
360            undef, 1 );
361        _prompt( $root, '{Store}{SearchAlgorithm}',
362            $Foswiki::cfg{Store}{SearchAlgorithm} );
363
364        print
365"You should now run tools/configure -check to validate your new configuration!\n";
366    }
367
368}
369
370if ( $reporter->has_level('errors') ) {
371    exit 1;
372}
373
374# Create a Logger instance and insert in to params hash
375# Note:  The configure should be bootstrapped before this step so that the
376# file system paths for the logger have been defined.
377
378_set_logger($params);
379
380# There are three possible action paths for Checkers and Wizards:
381#  - Query::check_current_value()
382#    * $params->{keys} is an array of configure keys
383#    * $params->{method} is also check_current_value
384#  - query::wizard() -   Wizards:: modules
385#    * $params->{wizard} - undefined
386#    * $params->{keys} is a scalar value - identifies the checker module
387#    * method is the checker routine to be called
388#  - query::wizard() -   Checker:: modules
389#    * $params->{wizard} - undefined
390#    * $params->{keys} is a scalar value - identifies the checker module
391#    * method is the checker method to be called
392
393if (   $params->{method} ne 'check_current_value'
394    && $action eq 'check_current_value' )
395{
396    $action = 'wizard';
397    $params->{keys} = $params->{keys}[0] if ref( $params->{keys} ) eq 'ARRAY';
398}
399
400if ($action) {
401    $action = "Foswiki::Configure::Query::$action";
402
403    no strict 'refs';
404    my $response = &$action( $params, $reporter );
405    use strict 'refs';
406
407    # Copy the changes into the "set" hash to be applied by save.
408    if ( ref($response) eq 'HASH' && keys %{ $response->{changes} } ) {
409        if ( $actions{save} ) {
410            $params->{set} = $response->{changes};
411        }
412        else {
413            print
414"Changes made by $params->{wizard} wizard, but -save option not requested.  Nothing saved.\n";
415            print
416"Rerun the $params->{wizard} with the -save option to update the configuration.\n";
417
418            #print  Data::Dumper::Dumper( \$response->{changes} );
419        }
420    }
421
422    if ( $action =~ /(?:::getcfg|::getspec|search)$/ ) {
423        my $out = Data::Dumper::Dumper( \$response );
424        $out =~ s/^\$VAR1 = \\/          /;
425        print $out;
426    }
427    elsif ( $action =~ /::check_current_value/ ) {
428        _printResponse( $response, $params->{verbose} );
429    }
430
431}
432
433if ( $actions{save} ) {
434
435    # -save is functionally equivalent to -wizard Save -method save
436    # (except of course you can have another wizard call)
437    if ( $reporter->has_level('errors') ) {
438        print "Save aborted due to errors\n";
439        exit 1;
440    }
441    $params->{wizard} = 'Save';
442    $params->{method} = 'save';
443    my $response = Foswiki::Configure::Query::wizard( $params, $reporter );
444}
445
446sub _printResponse {
447    my $reparray = shift;
448    my $verbose  = shift;
449    foreach my $entry (@$reparray) {
450        my $report = $entry->{reports};
451        my $keys   = $entry->{keys};
452        my $path   = $entry->{path};
453
454        my $header = 0;
455        foreach my $msg (@$report) {
456            next if ( $msg->{level} eq 'notes' && !$verbose );
457            if ( !$header ) {
458                print "\nChecking:" . join( ' -> ', @$path ) . ":  $keys\n";
459                $header = 1;
460            }
461            print 'WARNING: '    if $msg->{level} eq 'warnings';
462            print '#### ERROR: ' if $msg->{level} eq 'errors';
463            print '   ' . $msg->{text} . "\n";
464        }
465    }
466}
467
468# This code copied from Foswiki.pm, with changes.
469# It inserts the Logger into the params hash.
470
471sub _set_logger {
472    my $params = shift;
473
474    unless ( $params->{logger} ) {
475        if ( $Foswiki::cfg{Log}{Implementation} eq 'none' ) {
476            $params->{logger} = Foswiki::Logger->new();
477        }
478        else {
479            eval "require $Foswiki::cfg{Log}{Implementation}";
480            if ($@) {
481                print "Logger load failed: $@";
482                $params->{logger} = Foswiki::Logger->new();
483            }
484            else {
485                $params->{logger} = $Foswiki::cfg{Log}{Implementation}->new();
486            }
487        }
488    }
489
490    return;
491}
492
4931;
494__END__
495
496=pod
497
498=head1 tools/configure
499
500Shell interface for Foswiki.spec, Config.spec and LocalSite.cfg
501
502=head1 SYNOPSIS
503
504 tools/configure [options]
505
506Use -search, -getspec and -getcfg to explore the configuration.
507
508Use -check, -wizard and -method to perform actions.
509
510Use -save to save a new configuration.
511
512Use -json and -trace to control the output of this script.
513
514If any command line arguments are utf-8 characters, be sure to run configure using the B<perl -CAS tools/configure ...> command.
515
516=head1 OPTIONS
517
518=over 8
519
520=item B<-json>
521
522If set then results will be output in JSON format rather than the
523default serialised perl format.
524
525=item B<-check> [key|section]
526
527Call checkers for the given key or section.
528No key|section means check all keys. You can have as many B<-check>
529options as you want when doing basic checking.
530
531=item B<-expert>
532
533Use minimal prompting.  Instead of displaying each item's help text
534only the item key is in the prompt.
535
536=item B<-getcfg> [key]
537
538Report the value of key. B<-getcfg> can be given
539as many times as you like to retrieve the values of several keys.
540Without a value, the option returns the value of all
541known keys.
542
543=item B<-getspec> [key|section]
544
545Get the Config.spec for a key or an entire section.
546No key|section will return the entire spec.
547Only the last B<-getspec> option will be processed.
548
549=item B<-method> name
550
551If B<-wizard> is given, this is the name of the
552wizard method to call (defaults to execute()). If B<-wizard> is not
553given then B<-method> is interpreted as the name of a checker
554method to call. The method will be called only on a single key.
555
556=item B<-noprompt>
557
558If B<-noprompt> is given, bootstrapped configuration keys
559are written to the LocalSite.cfg.  Other required settings that are
560not possible to bootstrap need to be set individually:
561B<tools/configure -save -set {key}=value>
562
563=item B<-save>
564
565Save a new configuration, with all items set using B<-set> (or set
566by a B<-wizard> call). Checkers are not run unless explicitly requested
567by B<-check>.
568
569=item B<-search> what
570
571Search headlines and keys for a fragment of text.
572Returns the path(s) to the item(s) matched.
573
574=item B<-set> key=value
575
576Set the value of a key for B<-check>, B<-wizard> and B<-save>. You
577can have as many B<-set> options as you want. B<-set> options are
578applied before any checkers or wizards are run, but will not
579persist unless B<-save> is specified. The value is expected to be
580a perl value - be careful about quotes, to pass a string value
581from the shell requires double quoting e.g.
582 -set {ScriptSuffix}='".pl"'
583
584The B<{Password}> setting is handled differently.  It will be encoded
585and stored as the hashed $apr1 value.
586
587=item B<-trace>
588
589Switch on limited tracing (mainly for debugging, traces are added to
590reports).  This is also useful when running a full -verbose -check
591of the configuration, as it lists each key checked.
592
593=item B<-wizard> name
594
595Wizard to call. You can only call a single wizard.
596
597=back
598
599=head1 EXAMPLES
600
601
602 $ configure  -save -set {Password}='mypass'
603
604will set the "sudo" admin password to mypass and save the
605hashed / encoded value into a new configuration. You can include multiple
606-set options.
607
608 $ configure -check {PubDir} -method validate_permissions
609
610will call validate_permissions() for the {PubDir} checker. You may
611only check a single key when a method is specified.
612
613 $ configure -check Extensions -verbose
614
615will call check_current_value() for all keys under the Extensions section.
616Verbose reporting will be used.
617
618 $ configure -wizard SendTestEmail -method send
619
620will send a test message to the {WebMasterEmail} address.  Email does not need to be enabled.
621
622 $ configure -set {ScriptSuffix}="" -set {UsersWebName}="Users" -save
623
624will set the values of {ScriptSuffix} and {UsersWebName} and save a new
625configuration.
626
627=head2 NOTES
628
629If you want to enter international characters into config variables, you will need to
630run the command with the perl -CAS option.
631
632 $ perl -CAS configure -save -set {Password}='passwordWithUTF8'
633
634will cause perl to treat the command line options as utf-8 strings and correctly
635encode international characters.
636
637=head1 WIZARDS
638
639The following wizards are shipped with the Foswiki distribution:
640
641=over 2
642
643=item B<AutoConfigureEmail>
644
645Implements method B<autoconfigure>.
646
647Probes the email servers to determine the protocols and methods implemented by the server and set the required configuration.
648Use with the B<-save> option to save changes discovered by AutoConfigure.
649
650 * {WebMasterEmail} must always be set.
651
652If direct connection to an email server using SMTP is required, the following settings are also needed.
653
654 * {SMTP}{MAILHOST} must be set to the server name.
655
656 * {SMPT}{Username} and {SMTP}{Password} are required if the server will require authentication.
657
658Example:  Configure a gmail connection, but don't save the changes:
659
660 $ configure -set {WebMasterEmail}='someuser@gmail.com' -set {SMTP}{MAILHOST}='smtp.gmail.com' -set {SMTP}{Username}='someuser@gmail.com' -set {SMTP}{Password}='mypassword' -wizard AutoConfigureEmail -method autoconfigure
661
662=item B<ExploreExtensions>
663Needs documentation
664
665=item B<InstallExtensions>
666
667Implements method B<add>, and method B<remove>
668
669Installs an extension into the system. Named arguments are used to pass parameters to the installer.
670
671=over 4
672
673=item -args B<ExtensionName>=RepositoryName
674
675Specifies the named extension that should be installed from the named repository (configured in {ExtensionsRepositories}
676Multiple extensions can be installed by repeating the -args option.
677
678=item -args B<USELOCAL>=0/1
679
680Set to 1 to use locally found extension archives, otherwise a fresh copy will be downloaded from the repository.
681
682=item -args B<SIMULATE>=0/1
683
684Set to 1 to simulate the installation. Nothing will be installed into the system.
685
686=item -args B<NODEPS>=0/1
687
688Set to 1 to bypass installing any other extensions that are listed of dependencies of the extension. (CPAN module dependencies are never installed).
689
690=item -args B<ENABLE>=0/1
691
692Set to 1 to enable the extension.
693
694=back
695
696Examples:
697
698 $ configure -wizard InstallExtensions -method add -args TreePlugin=Foswiki.org -args SIMULATE=1 -args ENABLE=1
699
700 $ configure -wizard InstallExtensions -method add -save -args TreePlugin=Foswiki.org -args USELOCAL=1 -args ENABLE=1
701
702=item B<Plugins>
703
704Implements method B<import>. (This is the default)
705
706Examines the Plugins and Extensions configuration.  Detects if a save is required to import new or changed .spec files.
707Sets any missing {Module} definitions.  Named arguments are used to pass parameters to the installer.
708
709=over 4
710
711=item -args B<ENABLE>=0/1
712
713Set to 1 to enable the extension.  Note that if the C<{Plugins}{someplugin}{Enabled}> is already defined, it will not be changed.
714Unless this argument is provided to specify a default, the C<{Enabled}> setting will be left undefined.
715
716=back
717
718Example:  Import new settings after installation of an extension using unzip. Enable any discovered plugins.
719
720 $ configure -save -wizard Plugins -args ENABLE=1
721
722=item B<SendTestEmail>
723
724Implements method B<send>.
725
726Sends a test email to the {WebMasterEmail} address.  Requires email be configured. Used to test the email configuration before enabling email.
727
728Example:
729
730 $ configure -wizard SendTestEmail -method send
731
732=item B<SMIMECertificate>
733
734Needs documentation.
735
736=item B<SSLCertificates>
737
738Needs documentation.
739
740=item B<StudyWebserver>
741
742Not currently operational
743
744=back
745
746