1#!/usr/bin/perl 2package IkiWiki::Plugin::websetup; 3 4use warnings; 5use strict; 6use IkiWiki 3.00; 7 8sub import { 9 hook(type => "getsetup", id => "websetup", call => \&getsetup); 10 hook(type => "checkconfig", id => "websetup", call => \&checkconfig); 11 hook(type => "sessioncgi", id => "websetup", call => \&sessioncgi); 12 hook(type => "formbuilder_setup", id => "websetup", 13 call => \&formbuilder_setup); 14} 15 16sub getsetup () { 17 return 18 plugin => { 19 safe => 1, 20 rebuild => 0, 21 section => "web", 22 }, 23 websetup_force_plugins => { 24 type => "string", 25 example => [], 26 description => "list of plugins that cannot be enabled/disabled via the web interface", 27 safe => 0, 28 rebuild => 0, 29 }, 30 websetup_unsafe => { 31 type => "string", 32 example => [], 33 description => "list of additional setup field keys to treat as unsafe", 34 safe => 0, 35 rebuild => 0, 36 }, 37 websetup_show_unsafe => { 38 type => "boolean", 39 example => 1, 40 description => "show unsafe settings, read-only, in web interface?", 41 safe => 0, 42 rebuild => 0, 43 }, 44} 45 46sub checkconfig () { 47 if (! exists $config{websetup_show_unsafe}) { 48 $config{websetup_show_unsafe}=1; 49 } 50} 51 52sub formatexample ($$) { 53 my $example=shift; 54 my $value=shift; 55 56 if (defined $value && length $value) { 57 return ""; 58 } 59 elsif (defined $example && ! ref $example && length $example) { 60 return "<br/ ><small>Example: <tt>$example</tt></small>"; 61 } 62 else { 63 return ""; 64 } 65} 66 67sub issafe ($) { 68 my $key=shift; 69 70 return ! grep { $_ eq $key } @{$config{websetup_unsafe}}; 71} 72 73sub showfields ($$$@) { 74 my $form=shift; 75 my $plugin=shift; 76 my $enabled=shift; 77 78 my @show; 79 my %plugininfo; 80 while (@_) { 81 my $key=shift; 82 my %info=%{shift()}; 83 84 if ($key eq 'plugin') { 85 %plugininfo=%info; 86 next; 87 } 88 89 # skip internal settings 90 next if defined $info{type} && $info{type} eq "internal"; 91 # XXX hashes not handled yet 92 next if ref $config{$key} && ref $config{$key} eq 'HASH' || ref $info{example} eq 'HASH'; 93 # maybe skip unsafe settings 94 next if ! ($config{websetup_show_unsafe} && $config{websetup_advanced}) && 95 (! $info{safe} || ! issafe($key)); 96 # maybe skip advanced settings 97 next if $info{advanced} && ! $config{websetup_advanced}; 98 # these are handled specially, so don't show 99 next if $key eq 'add_plugins' || $key eq 'disable_plugins'; 100 101 push @show, $key, \%info; 102 } 103 104 my $section=defined $plugin 105 ? sprintf(gettext("%s plugin:"), $plugininfo{section})." ".$plugin 106 : "main"; 107 my %enabledfields; 108 my $shownfields=0; 109 110 my $plugin_forced=defined $plugin && (! $plugininfo{safe} || 111 (exists $config{websetup_force_plugins} && grep { $_ eq $plugin } @{$config{websetup_force_plugins}})); 112 if ($plugin_forced && ! $enabled) { 113 # plugin is forced disabled, so skip its settings 114 @show=(); 115 } 116 117 my $section_fieldset; 118 if (defined $plugin) { 119 # Define the combined fieldset for the plugin's section. 120 # This ensures that this fieldset comes first. 121 $section_fieldset=sprintf(gettext("%s plugins"), $plugininfo{section}); 122 $form->field(name => "placeholder.$plugininfo{section}", 123 type => "hidden", 124 fieldset => $section_fieldset); 125 } 126 127 # show plugin toggle 128 if (defined $plugin && (! $plugin_forced || $config{websetup_advanced})) { 129 my $name="enable.$plugin"; 130 $form->field( 131 name => $name, 132 label => "", 133 type => "checkbox", 134 fieldset => $section, 135 options => [ [ 1 => sprintf(gettext("enable %s?"), $plugin) ]] 136 ); 137 if (! $form->submitted) { 138 $form->field(name => $name, value => $enabled); 139 } 140 if ($plugin_forced) { 141 $form->field(name => $name, disabled => 1); 142 } 143 else { 144 $enabledfields{$name}=[$name, \%plugininfo]; 145 } 146 } 147 148 # show plugin settings 149 while (@show) { 150 my $key=shift @show; 151 my %info=%{shift @show}; 152 153 my $description=$info{description}; 154 if (exists $info{htmldescription}) { 155 $description=$info{htmldescription}; 156 } 157 elsif (exists $info{link} && length $info{link}) { 158 if ($info{link} =~ /^\w+:\/\//) { 159 $description="<a href=\"$info{link}\">$description</a>"; 160 } 161 else { 162 $description=htmllink("", "", $info{link}, noimageinline => 1, linktext => $description); 163 } 164 } 165 166 # multiple plugins can have the same field 167 my $name=defined $plugin ? $plugin.".".$key : $section.".".$key; 168 169 my $value=$config{$key}; 170 if (! defined $value) { 171 $value=""; 172 } 173 174 if (ref $value eq 'ARRAY' || ref $info{example} eq 'ARRAY') { 175 $value=[(ref $value eq 'ARRAY' ? map { Encode::encode_utf8($_) } @{$value} : "")]; 176 push @$value, "", "" if $info{safe} && issafe($key); # blank items for expansion 177 } 178 else { 179 $value=Encode::encode_utf8($value); 180 } 181 182 if ($info{type} eq "string") { 183 $form->field( 184 name => $name, 185 label => $description, 186 comment => formatexample($info{example}, $value), 187 type => "text", 188 value => $value, 189 size => 60, 190 fieldset => $section, 191 ); 192 } 193 elsif ($info{type} eq "pagespec") { 194 $form->field( 195 name => $name, 196 label => $description, 197 comment => formatexample($info{example}, $value), 198 type => "text", 199 value => $value, 200 size => 60, 201 validate => \&IkiWiki::pagespec_valid, 202 fieldset => $section, 203 ); 204 } 205 elsif ($info{type} eq "integer") { 206 $form->field( 207 name => $name, 208 label => $description, 209 comment => formatexample($info{example}, $value), 210 type => "text", 211 value => $value, 212 size => 5, 213 validate => '/^[0-9]+$/', 214 fieldset => $section, 215 ); 216 } 217 elsif ($info{type} eq "boolean") { 218 $form->field( 219 name => $name, 220 label => "", 221 type => "checkbox", 222 options => [ [ 1 => $description ] ], 223 fieldset => $section, 224 ); 225 if (! $form->submitted || 226 ($info{advanced} && $form->submitted eq 'Advanced Mode')) { 227 $form->field(name => $name, value => $value); 228 } 229 } 230 231 if (! $info{safe} || ! issafe($key)) { 232 $form->field(name => $name, disabled => 1); 233 } 234 else { 235 $enabledfields{$name}=[$key, \%info]; 236 } 237 $shownfields++; 238 } 239 240 # if no fields were shown for the plugin, drop it into a combined 241 # fieldset for its section 242 if (defined $plugin && (! $plugin_forced || $config{websetup_advanced}) && 243 ! $shownfields) { 244 $form->field(name => "enable.$plugin", fieldset => $section_fieldset); 245 } 246 247 return %enabledfields; 248} 249 250sub enable_plugin ($) { 251 my $plugin=shift; 252 253 $config{disable_plugins}=[grep { $_ ne $plugin } @{$config{disable_plugins}}]; 254 push @{$config{add_plugins}}, $plugin; 255} 256 257sub disable_plugin ($) { 258 my $plugin=shift; 259 260 $config{add_plugins}=[grep { $_ ne $plugin } @{$config{add_plugins}}]; 261 push @{$config{disable_plugins}}, $plugin; 262} 263 264sub showform ($$) { 265 my $cgi=shift; 266 my $session=shift; 267 268 IkiWiki::needsignin($cgi, $session); 269 270 if (! defined $session->param("name") || 271 ! IkiWiki::is_admin($session->param("name"))) { 272 error(gettext("you are not logged in as an admin")); 273 } 274 275 if (! exists $config{setupfile}) { 276 error(gettext("setup file for this wiki is not known")); 277 } 278 279 eval q{use CGI::FormBuilder}; 280 error($@) if $@; 281 282 my $form = CGI::FormBuilder->new( 283 title => "setup", 284 name => "setup", 285 header => 0, 286 charset => "utf-8", 287 method => 'POST', 288 javascript => 0, 289 reset => 1, 290 params => $cgi, 291 fieldsets => [ 292 [main => gettext("main")], 293 ], 294 action => IkiWiki::cgiurl(), 295 template => {type => 'div'}, 296 stylesheet => 1, 297 ); 298 299 $form->field(name => "do", type => "hidden", value => "setup", 300 force => 1); 301 $form->field(name => "rebuild_asked", type => "hidden"); 302 $form->field(name => "showadvanced", type => "hidden"); 303 304 if ($form->submitted eq 'Basic Mode') { 305 $form->field(name => "showadvanced", type => "hidden", 306 value => 0, force => 1); 307 } 308 elsif ($form->submitted eq 'Advanced Mode') { 309 $form->field(name => "showadvanced", type => "hidden", 310 value => 1, force => 1); 311 } 312 my $advancedtoggle; 313 if ($form->field("showadvanced")) { 314 $config{websetup_advanced}=1; 315 $advancedtoggle="Basic Mode"; 316 } 317 else { 318 $config{websetup_advanced}=0; 319 $advancedtoggle="Advanced Mode"; 320 } 321 322 my $buttons=["Save Setup", $advancedtoggle, "Cancel"]; 323 324 IkiWiki::decode_form_utf8($form); 325 IkiWiki::run_hooks(formbuilder_setup => sub { 326 shift->(form => $form, cgi => $cgi, session => $session, 327 buttons => $buttons); 328 }); 329 330 my %fields=showfields($form, undef, undef, IkiWiki::getsetup()); 331 332 # record all currently enabled plugins before all are loaded 333 my %enabled_plugins=%IkiWiki::loaded_plugins; 334 335 # per-plugin setup 336 require IkiWiki::Setup; 337 foreach my $pair (IkiWiki::Setup::getsetup()) { 338 my $plugin=$pair->[0]; 339 my $setup=$pair->[1]; 340 341 my %shown=showfields($form, $plugin, $enabled_plugins{$plugin}, @{$setup}); 342 if (%shown) { 343 $fields{$_}=$shown{$_} foreach keys %shown; 344 } 345 } 346 347 IkiWiki::decode_form_utf8($form); 348 349 if ($form->submitted eq "Cancel") { 350 IkiWiki::redirect($cgi, IkiWiki::baseurl(undef)); 351 return; 352 } 353 elsif (($form->submitted eq 'Save Setup' || $form->submitted eq 'Rebuild Wiki') && $form->validate) { 354 # Push values from form into %config, avoiding unnecessary 355 # changes, and keeping track of which changes need a 356 # rebuild. 357 my %rebuild; 358 foreach my $field (keys %fields) { 359 my %info=%{$fields{$field}->[1]}; 360 my $key=$fields{$field}->[0]; 361 my @value=$form->field($field); 362 if (! @value) { 363 @value=0; 364 } 365 366 if (! $info{safe} || ! issafe($key)) { 367 error("unsafe field $key"); # should never happen 368 } 369 370 if (exists $info{rebuild} && 371 ($info{rebuild} || ! defined $info{rebuild})) { 372 $rebuild{$field}=$info{rebuild}; 373 } 374 375 if ($field=~/^enable\.(.*)/) { 376 my $plugin=$1; 377 $value[0]=0 if ! length $value[0]; 378 if ($value[0] != exists $enabled_plugins{$plugin}) { 379 if ($value[0]) { 380 enable_plugin($plugin); 381 } 382 else { 383 disable_plugin($plugin); 384 385 } 386 } 387 else { 388 delete $rebuild{$field}; 389 } 390 next; 391 } 392 393 if (ref $config{$key} eq "ARRAY" || ref $info{example} eq "ARRAY") { 394 @value=sort grep { length $_ } @value; 395 my @oldvalue=sort grep { length $_ } 396 (defined $config{$key} ? @{$config{$key}} : ()); 397 my $same=(@oldvalue) == (@value); 398 for (my $x=0; $same && $x < @value; $x++) { 399 $same=0 if $value[$x] ne $oldvalue[$x]; 400 } 401 if ($same) { 402 delete $rebuild{$field}; 403 } 404 else { 405 $config{$key}=\@value; 406 } 407 } 408 elsif (ref $config{$key} || ref $info{example}) { 409 error("complex field $key"); # should never happen 410 } 411 else { 412 if (defined $config{$key} && $config{$key} eq $value[0]) { 413 delete $rebuild{$field}; 414 } 415 elsif (! defined $config{$key} && ! length $value[0]) { 416 delete $rebuild{$field}; 417 } 418 elsif ((! defined $config{$key} || ! $config{$key}) && 419 ! $value[0] && $info{type} eq "boolean") { 420 delete $rebuild{$field}; 421 } 422 else { 423 $config{$key}=$value[0]; 424 } 425 } 426 } 427 428 if (%rebuild && ! $form->field("rebuild_asked")) { 429 my $required=0; 430 foreach my $field ($form->field) { 431 $required=1 if $rebuild{$field}; 432 next if exists $rebuild{$field}; 433 $form->field(name => $field, type => "hidden"); 434 } 435 if ($required) { 436 $form->text(gettext("The configuration changes shown below require a wiki rebuild to take effect.")); 437 $buttons=["Rebuild Wiki", "Cancel"]; 438 } 439 else { 440 $form->text(gettext("For the configuration changes shown below to fully take effect, you may need to rebuild the wiki.")); 441 $buttons=["Rebuild Wiki", "Save Setup", "Cancel"]; 442 } 443 $form->field(name => "rebuild_asked", value => 1, force => 1); 444 $form->reset(0); # doesn't really make sense here 445 } 446 else { 447 my $oldsetup=readfile($config{setupfile}); 448 IkiWiki::Setup::dump($config{setupfile}); 449 450 IkiWiki::saveindex(); 451 IkiWiki::unlockwiki(); 452 453 # Print the top part of a standard cgitemplate, 454 # then show the rebuild or refresh, live. 455 my $divider="\0"; 456 my $html=IkiWiki::cgitemplate($cgi, "setup", $divider); 457 IkiWiki::printheader($session); 458 my ($head, $tail)=split($divider, $html, 2); 459 print $head."<pre>\n"; 460 461 my @command; 462 if ($form->submitted eq 'Rebuild Wiki') { 463 @command=("ikiwiki", "--setup", $config{setupfile}, 464 "--rebuild", "-v"); 465 } 466 else { 467 @command=("ikiwiki", "--setup", $config{setupfile}, 468 "--refresh", "--wrappers", "-v"); 469 } 470 471 close STDERR; 472 open(STDERR, ">&STDOUT"); 473 my $ret=system(@command); 474 print "\n<\/pre>"; 475 if ($ret != 0) { 476 print '<p class="error">'. 477 sprintf(gettext("Error: %s exited nonzero (%s). Discarding setup changes."), 478 join(" ", @command), $ret). 479 '</p>'; 480 open(OUT, ">", $config{setupfile}) || error("$config{setupfile}: $!"); 481 print OUT Encode::encode_utf8($oldsetup); 482 close OUT; 483 } 484 485 print $tail; 486 exit 0; 487 } 488 } 489 490 IkiWiki::showform($form, $buttons, $session, $cgi); 491} 492 493sub sessioncgi ($$) { 494 my $cgi=shift; 495 my $session=shift; 496 497 if ($cgi->param("do") eq "setup") { 498 showform($cgi, $session); 499 exit; 500 } 501} 502 503sub formbuilder_setup (@) { 504 my %params=@_; 505 506 my $form=$params{form}; 507 if ($form->title eq "preferences" && 508 IkiWiki::is_admin($params{session}->param("name"))) { 509 push @{$params{buttons}}, "Setup"; 510 if ($form->submitted && $form->submitted eq "Setup") { 511 showform($params{cgi}, $params{session}); 512 exit; 513 } 514 } 515} 516 5171 518