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