diff options
Diffstat (limited to 'IkiWiki')
39 files changed, 992 insertions, 232 deletions
diff --git a/IkiWiki/CGI.pm b/IkiWiki/CGI.pm index cb83319e6..1763828a4 100644 --- a/IkiWiki/CGI.pm +++ b/IkiWiki/CGI.pm @@ -58,12 +58,15 @@ sub cgitemplate ($$$;@) { my $template=template("page.tmpl"); - my $topurl = defined $cgi ? $cgi->url : $config{url}; + my $topurl = $config{url}; + if (defined $cgi && ! $config{w3mmode} && ! $config{reverse_proxy}) { + $topurl = $cgi->url; + } my $page=""; if (exists $params{page}) { $page=delete $params{page}; - $params{forcebaseurl}=urlabs(urlto($page), $topurl); + $params{forcebaseurl}=urlto($page); } run_hooks(pagetemplate => sub { shift->( @@ -74,12 +77,14 @@ sub cgitemplate ($$$;@) { }); templateactions($template, ""); + my $baseurl = baseurl(); + $template->param( dynamic => 1, title => $title, wikiname => $config{wikiname}, content => $content, - baseurl => urlabs(baseurl(), $topurl), + baseurl => $baseurl, html5 => $config{html5}, %params, ); @@ -90,7 +95,13 @@ sub cgitemplate ($$$;@) { sub redirect ($$) { my $q=shift; eval q{use URI}; - my $url=URI->new(urlabs(shift, $q->url)); + + my $topurl; + if (defined $q && ! $config{w3mmode} && ! $config{reverse_proxy}) { + $topurl = $q->url; + } + + my $url=URI->new(urlabs(shift, $topurl)); if (! $config{w3mmode}) { print $q->redirect($url); } @@ -105,16 +116,15 @@ sub decode_cgi_utf8 ($) { if ($] < 5.01) { my $cgi = shift; foreach my $f ($cgi->param) { - $cgi->param($f, map { decode_utf8 $_ } $cgi->param($f)); + $cgi->param($f, map { decode_utf8 $_ } + @{$cgi->param_fetch($f)}); } } } sub safe_decode_utf8 ($) { my $octets = shift; - # call decode_utf8 on >= 5.20 only if it's not already decoded, - # otherwise it balks, on < 5.20, always call it - if ($] < 5.02 || !Encode::is_utf8($octets)) { + if (!Encode::is_utf8($octets)) { return decode_utf8($octets); } else { @@ -326,16 +336,19 @@ sub check_banned ($$) { my $banned=0; my $name=$session->param("name"); + my $cloak=cloak($name) if defined $name; if (defined $name && - grep { $name eq $_ } @{$config{banned_users}}) { + grep { $name eq $_ || $cloak eq $_ } @{$config{banned_users}}) { $banned=1; } foreach my $b (@{$config{banned_users}}) { if (pagespec_match("", $b, ip => $session->remote_addr(), - name => defined $name ? $name : "", - )) { + name => defined $name ? $name : "") + || pagespec_match("", $b, + ip => cloak($session->remote_addr()), + name => defined $cloak ? $cloak : "")) { $banned=1; last; } diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index d56dd18ad..e8135a8fd 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -132,9 +132,11 @@ sub formbuilder (@) { return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ; - my $filename=Encode::decode_utf8($q->param('attachment')); + my $filename=Encode::decode_utf8(scalar $q->param('attachment')); + my $handle=$q->upload('attachment'); + if (defined $filename && length $filename) { - attachment_store($filename, $form, $q, $params{session}); + attachment_store($filename, $handle, $form, $q, $params{session}); } if ($form->submitted eq "Save Page") { @@ -142,9 +144,9 @@ sub formbuilder (@) { } if ($form->submitted eq "Insert Links") { - my $page=quotemeta(Encode::decode_utf8($q->param("page"))); + my $page=quotemeta(Encode::decode_utf8(scalar $q->param("page"))); my $add=""; - foreach my $f ($q->param("attachment_select")) { + foreach my $f (@{$q->param_fetch("attachment_select")}) { $f=Encode::decode_utf8($f); $f=~s/^$page\///; if (IkiWiki::isinlinableimage($f) && @@ -190,13 +192,20 @@ sub is_held_attachment { # Stores the attachment in a holding area, not yet in the wiki proper. sub attachment_store { my $filename=shift; + my $handle=shift; my $form=shift; my $q=shift; my $session=shift; - - # This is an (apparently undocumented) way to get the name - # of the temp file that CGI writes the upload to. - my $tempfile=$q->tmpFileName($filename); + + my $tempfile; + if (defined $handle) { + # This is what works in CGI.pm 4.09+: $q->tmpFileName($q->upload('attachment')) + $tempfile=$q->tmpFileName($handle); + } + if (! defined $tempfile || ! length $tempfile) { + # This is what is *documented* in CGI.pm 4.09: $q->tmpFileName($q->param('attachment')) + $tempfile=$q->tmpFileName($filename); + } if (! defined $tempfile || ! length $tempfile) { # perl 5.8 needs an alternative, awful method if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) { @@ -205,9 +214,9 @@ sub attachment_store { last if defined $tempfile && length $tempfile; } } - if (! defined $tempfile || ! length $tempfile) { - error("CGI::tmpFileName failed to return the uploaded file name"); - } + } + if (! defined $tempfile || ! length $tempfile) { + error("CGI::tmpFileName failed to return the uploaded file name"); } $filename=IkiWiki::basename($filename); diff --git a/IkiWiki/Plugin/autoindex.pm b/IkiWiki/Plugin/autoindex.pm index 78571b276..d5ee4b58f 100644 --- a/IkiWiki/Plugin/autoindex.pm +++ b/IkiWiki/Plugin/autoindex.pm @@ -71,7 +71,6 @@ sub refresh () { my (%pages, %dirs); foreach my $dir ($config{srcdir}, @{$config{underlaydirs}}, $config{underlaydir}) { - next if $dir eq $IkiWiki::Plugin::transient::transientdir; chdir($dir) || next; find({ @@ -90,7 +89,7 @@ sub refresh () { if (! -d _) { $pages{pagename($f)}=1; } - elsif ($dir eq $config{srcdir}) { + elsif ($dir eq $config{srcdir} || ! $config{autoindex_commit}) { $dirs{$f}=1; } } diff --git a/IkiWiki/Plugin/blogspam.pm b/IkiWiki/Plugin/blogspam.pm index e48ed729f..3eb4cf8b3 100644 --- a/IkiWiki/Plugin/blogspam.pm +++ b/IkiWiki/Plugin/blogspam.pm @@ -6,7 +6,8 @@ use strict; use IkiWiki 3.00; use Encode; -my $defaulturl='http://test.blogspam.net:8888/'; +my $defaulturl='http://test.blogspam.net:9999/'; +my $client; sub import { hook(type => "getsetup", id => "blogspam", call => \&getsetup); @@ -33,14 +34,14 @@ sub getsetup () { type => "string", example => "blacklist=1.2.3.4,blacklist=8.7.6.5,max-links=10", description => "options to send to blogspam server", - link => "http://blogspam.net/api/testComment.html#options", + link => "http://blogspam.net/api/2.0/testComment.html#options", safe => 1, rebuild => 0, }, blogspam_server => { type => "string", default => $defaulturl, - description => "blogspam server XML-RPC url", + description => "blogspam server JSON url", safe => 1, rebuild => 0, }, @@ -51,11 +52,23 @@ sub checkconfig () { # if the module is missing when a spam is posted would not # let the admin know about the problem. eval q{ - use RPC::XML; - use RPC::XML::Client; - $RPC::XML::ENCODING = 'utf-8'; + use JSON; + use HTTP::Request; }; error $@ if $@; + + eval q{use LWPx::ParanoidAgent}; + if (!$@) { + $client=LWPx::ParanoidAgent->new(agent => $config{useragent}); + } + else { + eval q{use LWP}; + if ($@) { + error $@; + return; + } + $client=useragent(); + } } sub checkcontent (@) { @@ -77,8 +90,6 @@ sub checkcontent (@) { my $url=$defaulturl; $url = $config{blogspam_server} if exists $config{blogspam_server}; - my $client = RPC::XML::Client->new($url); - my @options = split(",", $config{blogspam_options}) if exists $config{blogspam_options}; @@ -107,19 +118,28 @@ sub checkcontent (@) { site => encode_utf8($config{url}), version => "ikiwiki ".$IkiWiki::version, ); - my $res = $client->send_request('testComment', \%req); + eval q{use JSON; use HTTP::Request}; # errors handled in checkconfig() + my $res = $client->request( + HTTP::Request->new( + 'POST', + $url, + [ 'Content-Type' => 'application/json' ], + to_json(\%req), + ), + ); - if (! ref $res || ! defined $res->value) { + if (! ref $res || ! $res->is_success()) { debug("failed to get response from blogspam server ($url)"); return undef; } - elsif ($res->value =~ /^SPAM:(.*)/) { + my $details = from_json($res->content); + if ($details->{result} eq 'SPAM') { eval q{use Data::Dumper}; - debug("blogspam server reports ".$res->value.": ".Dumper(\%req)); - return gettext("Sorry, but that looks like spam to <a href=\"http://blogspam.net/\">blogspam</a>: ").$1; + debug("blogspam server reports $details->{reason}: ".Dumper(\%req)); + return gettext("Sorry, but that looks like spam to <a href=\"http://blogspam.net/\">blogspam</a>: ").$details->{reason}; } - elsif ($res->value ne 'OK') { - debug("blogspam server failure: ".$res->value); + elsif ($details->{result} ne 'OK') { + debug("blogspam server failure: ".$res->content); return undef; } else { diff --git a/IkiWiki/Plugin/brokenlinks.pm b/IkiWiki/Plugin/brokenlinks.pm index 8ee734bf9..2318298fe 100644 --- a/IkiWiki/Plugin/brokenlinks.pm +++ b/IkiWiki/Plugin/brokenlinks.pm @@ -39,7 +39,7 @@ sub preprocess (@) { htmllink($page, $params{destpage}, $link, noimageinline => 1), join(", ", map { htmllink($params{page}, $params{destpage}, $_, noimageinline => 1) - } @pages) + } sort @pages) ); } diff --git a/IkiWiki/Plugin/bzr.pm b/IkiWiki/Plugin/bzr.pm index e2b102dee..5ec254f84 100644 --- a/IkiWiki/Plugin/bzr.pm +++ b/IkiWiki/Plugin/bzr.pm @@ -133,10 +133,10 @@ sub bzr_author ($) { my $ipaddr=$session->remote_addr(); if (defined $user) { - return IkiWiki::possibly_foolish_untaint($user); + return IkiWiki::possibly_foolish_untaint(IkiWiki::cloak($user)); } elsif (defined $ipaddr) { - return "Anonymous from ".IkiWiki::possibly_foolish_untaint($ipaddr); + return "Anonymous from ".IkiWiki::possibly_foolish_untaint(IkiWiki::cloak($ipaddr)); } else { return "Anonymous"; diff --git a/IkiWiki/Plugin/calendar.pm b/IkiWiki/Plugin/calendar.pm index 682bfb6fb..c03b89667 100644 --- a/IkiWiki/Plugin/calendar.pm +++ b/IkiWiki/Plugin/calendar.pm @@ -1,4 +1,4 @@ -#! /usr/bin/perl +#!/usr/bin/perl # Copyright (c) 2006, 2007 Manoj Srivastava <srivasta@debian.org> # # This program is free software; you can redistribute it and/or modify @@ -25,11 +25,17 @@ use Time::Local; my $time=time; my @now=localtime($time); +my %changed; sub import { + hook(type => "checkconfig", id => "calendar", call => \&checkconfig); hook(type => "getsetup", id => "calendar", call => \&getsetup); hook(type => "needsbuild", id => "calendar", call => \&needsbuild); hook(type => "preprocess", id => "calendar", call => \&preprocess); + hook(type => "scan", id => "calendar", call => \&scan); + hook(type => "build_affected", id => "calendar", call => \&build_affected); + + IkiWiki::loadplugin("transient"); } sub getsetup () { @@ -49,11 +55,41 @@ sub getsetup () { archive_pagespec => { type => "pagespec", example => "page(posts/*) and !*/Discussion", - description => "PageSpec of pages to include in the archives; used by ikiwiki-calendar command", + description => "PageSpec of pages to include in the archives, if option `calendar_autocreate` is true.", link => 'ikiwiki/PageSpec', safe => 1, rebuild => 0, }, + calendar_autocreate => { + type => "boolean", + example => 1, + description => "autocreate new calendar pages?", + safe => 1, + rebuild => undef, + }, + calendar_fill_gaps => { + type => "boolean", + example => 1, + default => 1, + description => "if set, when building calendar pages, also build pages of year and month when no pages were published (building empty calendars).", + safe => 1, + rebuild => 0, + }, +} + +sub checkconfig () { + if (! defined $config{calendar_autocreate}) { + $config{calendar_autocreate} = defined $config{archivebase}; + } + if (! defined $config{archive_pagespec}) { + $config{archive_pagespec} = '*'; + } + if (! defined $config{archivebase}) { + $config{archivebase} = 'archives'; + } + if (! defined $config{calendar_fill_gaps}) { + $config{calendar_fill_gaps} = 1; + } } sub is_leap_year (@) { @@ -70,6 +106,184 @@ sub month_days { return $days_in_month; } +sub build_affected { + my %affected; + my ($ayear, $amonth, $valid); + + foreach my $year (keys %changed) { + ($ayear, $valid) = nextyear($year, $config{archivebase}); + $affected{calendarlink($ayear)} = sprintf(gettext("building calendar for %s, its previous or next year has changed"), $ayear) if ($valid); + ($ayear, $valid) = previousyear($year, $config{archivebase}); + $affected{calendarlink($ayear)} = sprintf(gettext("building calendar for %s, its previous or next year has changed"), $ayear) if ($valid); + foreach my $month (keys %{$changed{$year}}) { + ($ayear, $amonth, $valid) = nextmonth($year, $month, $config{archivebase}); + $affected{calendarlink($ayear, sprintf("%02d", $amonth))} = sprintf(gettext("building calendar for %s/%s, its previous or next month has changed"), $amonth, $ayear) if ($valid); + ($ayear, $amonth, $valid) = previousmonth($year, $month, $config{archivebase}); + $affected{calendarlink($ayear, sprintf("%02d", $amonth))} = sprintf(gettext("building calendar for %s/%s, its previous or next month has changed"), $amonth, $ayear) if ($valid); + } + } + + return %affected; +} + +sub autocreate { + my ($page, $pagefile, $year, $month) = @_; + my $message=sprintf(gettext("creating calendar page %s"), $page); + debug($message); + + my $template; + if (defined $month) { + $template=template("calendarmonth.tmpl"); + } else { + $template=template("calendaryear.tmpl"); + } + $template->param(year => $year); + $template->param(month => $month) if defined $month; + $template->param(pagespec => $config{archive_pagespec}); + + my $dir = $IkiWiki::Plugin::transient::transientdir; + + writefile($pagefile, $dir, $template->output); +} + +sub calendarlink($;$) { + my ($year, $month) = @_; + if (defined $month) { + return $config{archivebase} . "/" . $year . "/" . $month; + } else { + return $config{archivebase} . "/" . $year; + } +} + +sub gencalendarmonth{ + my $year = shift; + my $month = sprintf("%02d", shift); + + my $page = calendarlink($year, $month); + my $pagefile = newpagefile($page, $config{default_pageext}); + add_autofile( + $pagefile, "calendar", + sub {return autocreate($page, $pagefile, $year, $month);} + ); +} + +sub gencalendaryear { + my $year = shift; + my %params = @_; + + # Building year page + my $page = calendarlink($year); + my $pagefile = newpagefile($page, $config{default_pageext}); + add_autofile( + $pagefile, "calendar", + sub {return autocreate($page, $pagefile, $year);} + ); + + if (not exists $wikistate{calendar}{minyear}) { + $wikistate{calendar}{minyear} = $year; + } + if (not exists $wikistate{calendar}{maxyear}) { + $wikistate{calendar}{maxyear} = $year; + } + + if ($config{calendar_fill_gaps}) { + # Building month pages + foreach my $month (1 .. 12) { + gencalendarmonth($year, $month); + } + + # Filling potential gaps in years (e.g. calendar goes from 2010 to 2014, + # and we just added year 2005. We have to add years 2006 to 2009). + return if $params{norecurse}; + if ($wikistate{calendar}{minyear} > $year) { + foreach my $other ($year + 1 .. $wikistate{calendar}{minyear} - 1) { + gencalendaryear($other, norecurse => 1); + } + $wikistate{calendar}{minyear} = $year; + } + if ($wikistate{calendar}{maxyear} < $year) { + foreach my $other ($wikistate{calendar}{maxyear} + 1 .. $year - 1) { + gencalendaryear($other, norecurse => 1); + } + $wikistate{calendar}{maxyear} = $year; + } + } + if ($year < $wikistate{calendar}{minyear}) { + $wikistate{calendar}{minyear} = $year; + } + if ($year > $wikistate{calendar}{maxyear}) { + $wikistate{calendar}{maxyear} = $year; + } +} + +sub previousmonth($$$) { + my $year = shift; + my $month = shift; + my $archivebase = shift; + + if (not exists $wikistate{calendar}{minyear}) { + $wikistate{calendar}{minyear} = $year; + } + + my $pmonth = $month; + my $pyear = $year; + while ((not exists $pagesources{"$archivebase/$pyear/" . sprintf("%02d", $pmonth)}) or ($pmonth == $month and $pyear == $year)) { + $pmonth -= 1; + if ($pmonth == 0) { + $pyear -= 1; + $pmonth = 12; + return ($pyear, $pmonth, 0) unless $pyear >= $wikistate{calendar}{minyear}; + } + } + return ($pyear, $pmonth, 1); +} + +sub nextmonth($$$) { + my $year = shift; + my $month = shift; + my $archivebase = shift; + + if (not exists $wikistate{calendar}{maxyear}) { + $wikistate{calendar}{maxyear} = $year; + } + + my $nmonth = $month; + my $nyear = $year; + while ((not exists $pagesources{"$archivebase/$nyear/" . sprintf("%02d", $nmonth)}) or ($nmonth == $month and $nyear == $year)) { + $nmonth += 1; + if ($nmonth == 13) { + $nyear += 1; + $nmonth = 1; + return ($nyear, $nmonth, 0) unless $nyear <= $wikistate{calendar}{maxyear}; + } + } + return ($nyear, $nmonth, 1); +} + +sub previousyear($$) { + my $year = shift; + my $archivebase = shift; + + my $pyear = $year - 1; + while (not exists $pagesources{"$archivebase/$pyear"}) { + $pyear -= 1; + return ($pyear, 0) unless ($pyear >= $wikistate{calendar}{minyear}); + } + return ($pyear, 1); +} + +sub nextyear($$) { + my $year = shift; + my $archivebase = shift; + + my $nyear = $year + 1; + while (not exists $pagesources{"$archivebase/$nyear"}) { + $nyear += 1; + return ($nyear, 0) unless ($nyear <= $wikistate{calendar}{maxyear}); + } + return ($nyear, 1); +} + sub format_month (@) { my %params=@_; @@ -92,20 +306,12 @@ sub format_month (@) { push(@{$linkcache{"$year/$mtag/$mday"}}, $p); } - my $pmonth = $params{month} - 1; - my $nmonth = $params{month} + 1; - my $pyear = $params{year}; - my $nyear = $params{year}; - - # Adjust for January and December - if ($params{month} == 1) { - $pmonth = 12; - $pyear--; - } - if ($params{month} == 12) { - $nmonth = 1; - $nyear++; - } + my $archivebase = 'archives'; + $archivebase = $config{archivebase} if defined $config{archivebase}; + $archivebase = $params{archivebase} if defined $params{archivebase}; + + my ($pyear, $pmonth, $pvalid) = previousmonth($params{year}, $params{month}, $archivebase); + my ($nyear, $nmonth, $nvalid) = nextmonth($params{year}, $params{month}, $archivebase); # Add padding. $pmonth=sprintf("%02d", $pmonth); @@ -129,10 +335,6 @@ sub format_month (@) { my $pmonthname=strftime_utf8("%B", localtime(timelocal(0,0,0,1,$pmonth-1,$pyear-1900))); my $nmonthname=strftime_utf8("%B", localtime(timelocal(0,0,0,1,$nmonth-1,$nyear-1900))); - my $archivebase = 'archives'; - $archivebase = $config{archivebase} if defined $config{archivebase}; - $archivebase = $params{archivebase} if defined $params{archivebase}; - # Calculate URL's for monthly archives. my ($url, $purl, $nurl)=("$monthname $params{year}",'',''); if (exists $pagesources{"$archivebase/$params{year}/$params{month}"}) { @@ -274,7 +476,7 @@ EOF sub format_year (@) { my %params=@_; - + my @post_months; foreach my $p (pagespec_match_list($params{page}, "creation_year($params{year}) and ($params{pages})", @@ -290,18 +492,18 @@ sub format_year (@) { } my $calendar="\n"; + + my $archivebase = 'archives'; + $archivebase = $config{archivebase} if defined $config{archivebase}; + $archivebase = $params{archivebase} if defined $params{archivebase}; - my $pyear = $params{year} - 1; - my $nyear = $params{year} + 1; + my ($pyear, $pvalid) = previousyear($params{year}, $archivebase); + my ($nyear, $nvalid) = nextyear($params{year}, $archivebase); my $thisyear = $now[5]+1900; my $future_month = 0; $future_month = $now[4]+1 if $params{year} == $thisyear; - my $archivebase = 'archives'; - $archivebase = $config{archivebase} if defined $config{archivebase}; - $archivebase = $params{archivebase} if defined $params{archivebase}; - # calculate URL's for previous and next years my ($url, $purl, $nurl)=("$params{year}",'',''); if (exists $pagesources{"$archivebase/$params{year}"}) { @@ -431,6 +633,7 @@ sub preprocess (@) { } $params{month} = sprintf("%02d", $params{month}); + $changed{$params{year}}{$params{month}} = 1; if ($params{type} eq 'month' && $params{year} == $thisyear && $params{month} == $thismonth) { @@ -508,7 +711,22 @@ sub needsbuild (@) { } } } + return $needsbuild; } +sub scan (@) { + my %params=@_; + my $page=$params{page}; + + return unless $config{calendar_autocreate}; + + # Check if year pages have to be generated + if (pagespec_match($page, $config{archive_pagespec})) { + my @ctime = localtime($IkiWiki::pagectime{$page}); + gencalendaryear($ctime[5]+1900); + gencalendarmonth($ctime[5]+1900, $ctime[4]+1); + } +} + 1 diff --git a/IkiWiki/Plugin/comments.pm b/IkiWiki/Plugin/comments.pm index 98ae13810..ad813d19d 100644 --- a/IkiWiki/Plugin/comments.pm +++ b/IkiWiki/Plugin/comments.pm @@ -198,7 +198,6 @@ sub preprocess { $commentuser = $params{username}; my $oiduser = eval { IkiWiki::openiduser($commentuser) }; - if (defined $oiduser) { # looks like an OpenID $commentauthorurl = $commentuser; @@ -206,10 +205,17 @@ sub preprocess { $commentopenid = $commentuser; } else { - $commentauthorurl = IkiWiki::cgiurl( - do => 'goto', - page => IkiWiki::userpage($commentuser) - ); + my $emailuser = IkiWiki::emailuser($commentuser); + if (defined $emailuser) { + $commentuser=$emailuser; + } + + if (length $config{cgiurl}) { + $commentauthorurl = IkiWiki::cgiurl( + do => 'goto', + page => IkiWiki::userpage($commentuser) + ); + } $commentauthor = $commentuser; } @@ -221,22 +227,9 @@ sub preprocess { $commentauthor = gettext("Anonymous"); } - $commentstate{$page}{commentuser} = $commentuser; - $commentstate{$page}{commentopenid} = $commentopenid; - $commentstate{$page}{commentip} = $commentip; - $commentstate{$page}{commentauthor} = $commentauthor; - $commentstate{$page}{commentauthorurl} = $commentauthorurl; - $commentstate{$page}{commentauthoravatar} = $params{avatar}; - if (! defined $pagestate{$page}{meta}{author}) { - $pagestate{$page}{meta}{author} = $commentauthor; - } - if (! defined $pagestate{$page}{meta}{authorurl}) { - $pagestate{$page}{meta}{authorurl} = $commentauthorurl; - } - if ($config{comments_allowauthor}) { if (defined $params{claimedauthor}) { - $pagestate{$page}{meta}{author} = $params{claimedauthor}; + $commentauthor = $params{claimedauthor}; } if (defined $params{url}) { @@ -248,12 +241,21 @@ sub preprocess { } if (safeurl($url)) { - $pagestate{$page}{meta}{authorurl} = $url; + $commentauthorurl = $url; } } } - else { + + $commentstate{$page}{commentuser} = $commentuser; + $commentstate{$page}{commentopenid} = $commentopenid; + $commentstate{$page}{commentip} = $commentip; + $commentstate{$page}{commentauthor} = $commentauthor; + $commentstate{$page}{commentauthorurl} = $commentauthorurl; + $commentstate{$page}{commentauthoravatar} = $params{avatar}; + if (! defined $pagestate{$page}{meta}{author}) { $pagestate{$page}{meta}{author} = $commentauthor; + } + if (! defined $pagestate{$page}{meta}{authorurl}) { $pagestate{$page}{meta}{authorurl} = $commentauthorurl; } @@ -464,17 +466,20 @@ sub editcomment ($$) { my $content = "[[!comment format=$type\n"; if (defined $session->param('name')) { - my $username = $session->param('name'); + my $username = IkiWiki::cloak($session->param('name')); $username =~ s/"/"/g; $content .= " username=\"$username\"\n"; } + if (defined $session->param('nickname')) { my $nickname = $session->param('nickname'); $nickname =~ s/"/"/g; $content .= " nickname=\"$nickname\"\n"; } - elsif (defined $session->remote_addr()) { - $content .= " ip=\"".$session->remote_addr()."\"\n"; + + if (!(defined $session->param('name') || defined $session->param('nickname')) && + defined $session->remote_addr()) { + $content .= " ip=\"".IkiWiki::cloak($session->remote_addr())."\"\n"; } if ($config{comments_allowauthor}) { @@ -504,8 +509,7 @@ sub editcomment ($$) { $subject = "comment ".(num_comments($page, $config{srcdir}) + 1); } $content .= " subject=\"$subject\"\n"; - - $content .= " date=\"" . strftime_utf8('%Y-%m-%dT%H:%M:%SZ', gmtime) . "\"\n"; + $content .= " date=\"" . commentdate() . "\"\n"; my $editcontent = $form->field('editcontent'); $editcontent="" if ! defined $editcontent; @@ -633,6 +637,10 @@ sub editcomment ($$) { exit; } +sub commentdate () { + strftime_utf8('%Y-%m-%dT%H:%M:%SZ', gmtime); +} + sub getavatar ($) { my $user=shift; return undef unless defined $user; @@ -1009,7 +1017,7 @@ sub num_comments ($$) { return int @comments; } -sub unique_comment_location ($$$$) { +sub unique_comment_location ($$$;$) { my $page=shift; eval q{use Digest::MD5 'md5_hex'}; error($@) if $@; diff --git a/IkiWiki/Plugin/cvs.pm b/IkiWiki/Plugin/cvs.pm index 841aec914..8989a26e3 100644 --- a/IkiWiki/Plugin/cvs.pm +++ b/IkiWiki/Plugin/cvs.pm @@ -456,12 +456,12 @@ sub commitmessage (@) { if (defined $params{session}) { if (defined $params{session}->param("name")) { return "web commit by ". - $params{session}->param("name"). + IkiWiki::cloak($params{session}->param("name")). (length $params{message} ? ": $params{message}" : ""); } elsif (defined $params{session}->remote_addr()) { return "web commit from ". - $params{session}->remote_addr(). + IkiWiki::cloak($params{session}->remote_addr()). (length $params{message} ? ": $params{message}" : ""); } } diff --git a/IkiWiki/Plugin/darcs.pm b/IkiWiki/Plugin/darcs.pm index 646f65df1..9dccd95a4 100644 --- a/IkiWiki/Plugin/darcs.pm +++ b/IkiWiki/Plugin/darcs.pm @@ -147,10 +147,10 @@ sub commitauthor (@) { my $author="anon\@web"; if (defined $params{session}) { if (defined $params{session}->param("name")) { - return $params{session}->param("name").'@web'; + return IkiWiki::cloak($params{session}->param("name")).'@web'; } elsif (defined $params{session}->remote_addr()) { - return $params{session}->remote_addr().'@web'; + return IkiWiki::cloak($params{session}->remote_addr()).'@web'; } } return 'anon@web'; diff --git a/IkiWiki/Plugin/editpage.pm b/IkiWiki/Plugin/editpage.pm index 3047869c4..78d0704c7 100644 --- a/IkiWiki/Plugin/editpage.pm +++ b/IkiWiki/Plugin/editpage.pm @@ -342,7 +342,7 @@ sub cgi_editpage ($$) { else { # save page check_canedit($page, $q, $session); - checksessionexpiry($q, $session, $q->param('sid')); + checksessionexpiry($q, $session); my $exists=-e "$config{srcdir}/$file"; diff --git a/IkiWiki/Plugin/emailauth.pm b/IkiWiki/Plugin/emailauth.pm new file mode 100644 index 000000000..6674fe3d6 --- /dev/null +++ b/IkiWiki/Plugin/emailauth.pm @@ -0,0 +1,203 @@ +#!/usr/bin/perl +# Ikiwiki email address as login +package IkiWiki::Plugin::emailauth; + +use warnings; +use strict; +use IkiWiki 3.00; + +sub import { + hook(type => "getsetup", id => "emailauth", "call" => \&getsetup); + hook(type => "cgi", id => "emailauth", "call" => \&cgi); + hook(type => "formbuilder_setup", id => "emailauth", "call" => \&formbuilder_setup); + IkiWiki::loadplugin("loginselector"); + IkiWiki::Plugin::loginselector::register_login_plugin( + "emailauth", + \&email_setup, + \&email_check_input, + \&email_auth, + ); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => 0, + section => "auth", + }, + emailauth_sender => { + type => "string", + description => "email address to send emailauth mails as (default: adminemail)", + safe => 1, + rebuild => 0, + }, +} + +sub email_setup ($$) { + my $q=shift; + my $template=shift; + + return 1; +} + +sub email_check_input ($) { + my $cgi=shift; + defined $cgi->param('do') + && $cgi->param("do") eq "signin" + && defined $cgi->param('Email_entry') + && length $cgi->param('Email_entry'); +} + +# Send login link to email. +sub email_auth ($$$$) { + my $cgi=shift; + my $session=shift; + my $errordisplayer=shift; + my $infodisplayer=shift; + + my $email=$cgi->param('Email_entry'); + unless ($email =~ /.\@./) { + $errordisplayer->(gettext("Invalid email address.")); + return; + } + + # Implicit account creation. + my $userinfo=IkiWiki::userinfo_retrieve(); + if (! exists $userinfo->{$email} || ! ref $userinfo->{$email}) { + IkiWiki::userinfo_setall($email, { + 'email' => $email, + 'regdate' => time, + }); + } + + my $token=gentoken($email, $session); + my $template=template("emailauth.tmpl"); + $template->param( + wikiname => $config{wikiname}, + # Intentionally using short field names to keep link short. + authurl => IkiWiki::cgiurl_abs( + 'e' => $email, + 'v' => $token, + ), + ); + + eval q{use Mail::Sendmail}; + error($@) if $@; + my $shorturl=$config{url}; + $shorturl=~s/^https?:\/\///i; + my $emailauth_sender=$config{emailauth_sender}; + $emailauth_sender=$config{adminemail} unless defined $emailauth_sender; + sendmail( + To => $email, + From => "$config{wikiname} admin <". + (defined $emailauth_sender ? $emailauth_sender : "") + .">", + Subject => "$config{wikiname} login | $shorturl", + Message => $template->output, + ) or error(gettext("Failed to send mail")); + + $infodisplayer->(gettext("You have been sent an email, with a link you can open to complete the login process.")); +} + +# Finish login process. +sub cgi ($$) { + my $cgi=shift; + + my $email=$cgi->param('e'); + my $v=$cgi->param('v'); + if (defined $email && defined $v && length $email && length $v) { + my $token=gettoken($email); + if ($token eq $v) { + cleartoken($email); + my $session=getsession($email); + IkiWiki::cgi_postsignin($cgi, $session); + } + elsif (length $token ne length $cgi->param('v')) { + error(gettext("Wrong login token length. Please check that you pasted in the complete login link from the email!")); + } + else { + loginfailure(); + } + } +} + +sub formbuilder_setup (@) { + my %params=@_; + my $form=$params{form}; + my $session=$params{session}; + + if ($form->title eq "preferences" && + IkiWiki::emailuser($session->param("name"))) { + $form->field(name => "email", disabled => 1); + } +} + +# Generates the token that will be used in the authurl to log the user in. +# This needs to be hard to guess, and relatively short. Generating a cgi +# session id will make it as hard to guess as any cgi session. +# +# Store token in userinfo; this allows the user to log in +# using a different browser session, if it takes a while for the +# email to get to them. +# +# The postsignin value from the session is also stored in the userinfo +# to allow resuming in a different browser session. +sub gentoken ($$) { + my $email=shift; + my $session=shift; + eval q{use CGI::Session}; + error($@) if $@; + my $token = CGI::Session->new->id; + IkiWiki::userinfo_set($email, "emailauthexpire", time+(60*60*24)); + IkiWiki::userinfo_set($email, "emailauth", $token); + IkiWiki::userinfo_set($email, "emailauthpostsignin", defined $session->param("postsignin") ? $session->param("postsignin") : ""); + return $token; +} + +# Gets the token, checking for expiry. +sub gettoken ($) { + my $email=shift; + my $val=IkiWiki::userinfo_get($email, "emailauth"); + my $expire=IkiWiki::userinfo_get($email, "emailauthexpire"); + if (! length $val || time > $expire) { + loginfailure(); + } + return $val; +} + +# Generate a session to use after successful login. +sub getsession ($) { + my $email=shift; + + IkiWiki::lockwiki(); + IkiWiki::loadindex(); + my $session=IkiWiki::cgi_getsession(); + + my $postsignin=IkiWiki::userinfo_get($email, "emailauthpostsignin"); + IkiWiki::userinfo_set($email, "emailauthpostsignin", ""); + if (defined $postsignin && length $postsignin) { + $session->param(postsignin => $postsignin); + } + + $session->param(name => $email); + my $nickname=$email; + $nickname=~s/@.*//; + $session->param(nickname => Encode::decode_utf8($nickname)); + + IkiWiki::cgi_savesession($session); + + return $session; +} + +sub cleartoken ($) { + my $email=shift; + IkiWiki::userinfo_set($email, "emailauthexpire", 0); + IkiWiki::userinfo_set($email, "emailauth", ""); +} + +sub loginfailure () { + error "Bad email authentication token. Please retry login."; +} + +1 diff --git a/IkiWiki/Plugin/filecheck.pm b/IkiWiki/Plugin/filecheck.pm index cdea5c706..6e6528398 100644 --- a/IkiWiki/Plugin/filecheck.pm +++ b/IkiWiki/Plugin/filecheck.pm @@ -150,7 +150,7 @@ sub match_mimetype ($$;@) { chomp $mimetype; close $file_h; } - if (! defined $mimetype || $mimetype !~s /;.*//) { + if (! defined $mimetype) { # Fall back to default value. $mimetype=File::MimeInfo::Magic::default($file) if $mimeinfo_ok; @@ -158,6 +158,8 @@ sub match_mimetype ($$;@) { $mimetype="unknown"; } } + # Ignore any parameters, we only want the type itself + $mimetype =~ s/;.*//; my $regexp=IkiWiki::glob2re($wanted); if ($mimetype!~$regexp) { diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 75b89e476..f963f06ba 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -220,6 +220,21 @@ sub run_or_die ($@) { safe_git(\&error, undef, @_) } sub run_or_cry ($@) { safe_git(sub { warn @_ }, undef, @_) } sub run_or_non ($@) { safe_git(undef, undef, @_) } +sub ensure_committer { + if (! length $ENV{GIT_AUTHOR_NAME} || ! length $ENV{GIT_COMMITTER_NAME}) { + my $name = join('', run_or_non("git", "config", "user.name")); + if (! length $name) { + run_or_die("git", "config", "user.name", "IkiWiki"); + } + } + + if (! length $ENV{GIT_AUTHOR_EMAIL} || ! length $ENV{GIT_COMMITTER_EMAIL}) { + my $email = join('', run_or_non("git", "config", "user.email")); + if (! length $email) { + run_or_die("git", "config", "user.email", "ikiwiki.info"); + } + } +} sub merge_past ($$$) { # Unlike with Subversion, Git cannot make a 'svn merge -rN:M file'. @@ -258,6 +273,8 @@ sub merge_past ($$$) { my @undo; # undo stack for cleanup in case of an error my $conflict; # file content with conflict markers + ensure_committer(); + eval { # Hide local changes from Git by renaming the modified file. # Relative paths must be converted to absolute for renaming. @@ -526,6 +543,8 @@ sub rcs_get_current_rev () { sub rcs_update () { # Update working directory. + ensure_committer(); + if (length $config{gitorigin_branch}) { run_or_cry('git', 'pull', '--prune', $config{gitorigin_branch}); } @@ -579,7 +598,7 @@ sub rcs_commit_helper (@) { $u=$params{session}->remote_addr(); } if (defined $u) { - $u=encode_utf8($u); + $u=encode_utf8(IkiWiki::cloak($u)); $ENV{GIT_AUTHOR_NAME}=$u; } if (defined $params{session}->param("nickname")) { @@ -592,6 +611,8 @@ sub rcs_commit_helper (@) { } } + ensure_committer(); + $params{message} = IkiWiki::possibly_foolish_untaint($params{message}); my @opts; if ($params{message} !~ /\S/) { @@ -631,6 +652,8 @@ sub rcs_add ($) { my ($file) = @_; + ensure_committer(); + run_or_cry('git', 'add', $file); } @@ -639,12 +662,16 @@ sub rcs_remove ($) { my ($file) = @_; + ensure_committer(); + run_or_cry('git', 'rm', '-f', $file); } sub rcs_rename ($$) { my ($src, $dest) = @_; + ensure_committer(); + run_or_cry('git', 'mv', '-f', $src, $dest); } @@ -944,6 +971,8 @@ sub rcs_revert ($) { my $rev = shift; my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint + ensure_committer(); + if (run_or_non('git', 'revert', '--no-commit', $sha1)) { return undef; } diff --git a/IkiWiki/Plugin/goto.pm b/IkiWiki/Plugin/goto.pm index 6b596ac8b..3a946b19d 100644 --- a/IkiWiki/Plugin/goto.pm +++ b/IkiWiki/Plugin/goto.pm @@ -27,7 +27,7 @@ sub cgi_goto ($;$) { my $page = shift; if (!defined $page) { - $page = IkiWiki::decode_utf8($q->param("page")); + $page = IkiWiki::decode_utf8(scalar $q->param("page")); if (!defined $page) { error("missing page parameter"); diff --git a/IkiWiki/Plugin/haiku.pm b/IkiWiki/Plugin/haiku.pm index bf23dce67..7ce74696b 100644 --- a/IkiWiki/Plugin/haiku.pm +++ b/IkiWiki/Plugin/haiku.pm @@ -25,7 +25,13 @@ sub preprocess (@) { my $haiku; eval q{use Coy}; - if ($@ || ! Coy->can("Coy::with_haiku")) { + if ($config{deterministic}) { + $haiku = "Coy changes often. + For deterministic builds + try this substitute. + ", + } + elsif ($@ || ! Coy->can("Coy::with_haiku")) { my @canned=( "The lack of a Coy: No darting, subtle haiku. diff --git a/IkiWiki/Plugin/img.pm b/IkiWiki/Plugin/img.pm index 54c13d069..169f5e713 100644 --- a/IkiWiki/Plugin/img.pm +++ b/IkiWiki/Plugin/img.pm @@ -76,9 +76,13 @@ sub preprocess (@) { my $im = Image::Magick->new(); my $imglink; my $imgdatalink; - my $r = $im->Read("$srcfile\[$pagenumber]"); + my $r = $im->Read(":$srcfile\[$pagenumber]"); error sprintf(gettext("failed to read %s: %s"), $file, $r) if $r; - + + if (! defined $im->Get("width") || ! defined $im->Get("height")) { + error sprintf(gettext("failed to get dimensions of %s"), $file); + } + my ($dwidth, $dheight); if ($params{size} eq 'full') { diff --git a/IkiWiki/Plugin/inline.pm b/IkiWiki/Plugin/inline.pm index b122bd0f2..d68723dff 100644 --- a/IkiWiki/Plugin/inline.pm +++ b/IkiWiki/Plugin/inline.pm @@ -119,7 +119,7 @@ sub sessioncgi ($$) { my $session=shift; if ($q->param('do') eq 'blog') { - my $page=titlepage(decode_utf8($q->param('title'))); + my $page=titlepage(decode_utf8(scalar $q->param('title'))); $page=~s/(\/)/"__".ord($1)."__"/eg; # don't create subdirs # if the page already exists, munge it to be unique my $from=$q->param('from'); @@ -160,16 +160,17 @@ sub preprocess_inline (@) { # Running in scan mode: only do the essentials if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) { - # default to sorting age, the same as inline itself, - # but let the params override that - IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params); + # default to sorting by age with fallback to title, + # the same as inline itself, but let the params + # override that + IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age title', %params); } return; } if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) { - scalar IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params); + scalar IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age title', %params); } my $raw=yesno($params{raw}); @@ -256,7 +257,7 @@ sub preprocess_inline (@) { @list = pagespec_match_list($params{page}, $params{pages}, deptype => deptype($quick ? "presence" : "content"), filter => sub { $_[0] eq $params{page} }, - sort => exists $params{sort} ? $params{sort} : "age", + sort => exists $params{sort} ? $params{sort} : "age title", reverse => yesno($params{reverse}), ($num ? (num => $num) : ()), ); diff --git a/IkiWiki/Plugin/loginselector.pm b/IkiWiki/Plugin/loginselector.pm new file mode 100644 index 000000000..26c80b4ce --- /dev/null +++ b/IkiWiki/Plugin/loginselector.pm @@ -0,0 +1,132 @@ +#!/usr/bin/perl +package IkiWiki::Plugin::loginselector; + +use warnings; +use strict; +use IkiWiki 3.00; + +# Plugins that provide login methods can register themselves here. +# Note that the template and js file also have be be modifed to add a new +# login method. +our %login_plugins; + +sub register_login_plugin ($$$$) { + # Same as the name of the plugin that is registering itself as a + # login plugin. eg, "openid" + my $plugin_name=shift; + # This sub is passed a cgi object and a template object which it + # can manipulate. It should return true if the plugin can be used + # (it might load necessary modules for auth checking, for example). + my $plugin_setup=shift; + # This sub is passed a cgi object, and should return true + # if it looks like the user is logging in using the plugin. + my $plugin_check_input=shift; + # This sub is passed a cgi object, a session object, an error + # display callback, and an info display callback, and should + # handle the actual authentication. It can either exit w/o + # returning, if it is able to handle auth, or it can pass an + # error message to the error display callback to make the + # openid selector form be re-disiplayed with an error message + # on it. + my $plugin_auth=shift; + $login_plugins{$plugin_name}={ + setup => $plugin_setup, + check_input => $plugin_check_input, + auth => $plugin_auth, + }; +} + +sub login_selector { + my $real_cgi_signin=shift; + my $otherform_label=shift; + my $q=shift; + my $session=shift; + + my $template=IkiWiki::template("login-selector.tmpl"); + + foreach my $plugin (keys %login_plugins) { + if (! $login_plugins{$plugin}->{setup}->($template)) { + delete $login_plugins{$plugin}; + } + else { + $template->param("login_selector_$plugin", 1); + } + } + + foreach my $plugin (keys %login_plugins) { + if ($login_plugins{$plugin}->{check_input}->($q)) { + $login_plugins{$plugin}->{auth}->($q, $session, sub { + $template->param(login_error => shift()); + }, sub { + $template->param(login_info => shift()); + }); + last; + } + } + + $template->param( + cgiurl => IkiWiki::cgiurl(), + ($real_cgi_signin ? (otherform => $real_cgi_signin->($q, $session, 1)) : ()), + otherform_label => $otherform_label, + ); + + IkiWiki::printheader($session); + print IkiWiki::cgitemplate($q, "signin", $template->output); + exit; +} + +sub import { + add_underlay("login-selector"); + add_underlay("jquery"); + hook(type => "getsetup", id => "loginselector", call => \&getsetup); + hook(type => "checkconfig", id => "loginselector", call => \&checkconfig); + hook(type => "auth", id => "loginselector", call => \&authstub); +} + +sub checkconfig () { + if ($config{cgi}) { + # Intercept normal signin form, so the login selector + # can be displayed. + # + # When other auth hooks are registered, give the selector + # a reference to the normal signin form. + require IkiWiki::CGI; + my $real_cgi_signin; + my $otherform_label=gettext("Other"); + if (keys %{$IkiWiki::hooks{auth}} > 1) { + $real_cgi_signin=\&IkiWiki::cgi_signin; + # Special case to avoid labeling password auth as + # "Other" when it's the only auth plugin not + # integrated with the loginselector. + my %h=%{$IkiWiki::hooks{auth}}; + foreach my $p (keys %login_plugins) { + delete $h{$p}; + } + delete $h{loginselector}; + if (keys %h == 1 && exists $h{passwordauth}) { + $otherform_label=gettext("Password"); + } + } + inject(name => "IkiWiki::cgi_signin", call => sub ($$) { + login_selector($real_cgi_signin, $otherform_label, @_); + }); + } +} + +sub getsetup () { + return + plugin => { + # this plugin is safe but only makes sense as a + # dependency + safe => 0, + rebuild => 0, + }, +} + +sub authstub ($$) { + # While this hook is not currently used, it needs to exist + # so ikiwiki knows that the wiki supports logins, and will + # enable the Preferences page. +} + +1 diff --git a/IkiWiki/Plugin/mercurial.pm b/IkiWiki/Plugin/mercurial.pm index 8da4ceb07..9f0c5b721 100644 --- a/IkiWiki/Plugin/mercurial.pm +++ b/IkiWiki/Plugin/mercurial.pm @@ -183,10 +183,10 @@ sub rcs_commit_helper (@) { my $user="Anonymous"; if (defined $params{session}) { if (defined $params{session}->param("name")) { - $user = $params{session}->param("name"); + $user = IkiWiki::cloak($params{session}->param("name")); } elsif (defined $params{session}->remote_addr()) { - $user = $params{session}->remote_addr(); + $user = IkiWiki::cloak($params{session}->remote_addr()); } my $nickname=$user; diff --git a/IkiWiki/Plugin/meta.pm b/IkiWiki/Plugin/meta.pm index e7b96bdf1..ea099f955 100644 --- a/IkiWiki/Plugin/meta.pm +++ b/IkiWiki/Plugin/meta.pm @@ -294,6 +294,7 @@ sub preprocess (@) { elsif ($key eq 'name') { push @{$metaheaders{$page}}, scrub('<meta name="'. encode_entities($value). + '" '. join(' ', map { "$_=\"$params{$_}\"" } keys %params). ' />', $page, $destpage); } diff --git a/IkiWiki/Plugin/mirrorlist.pm b/IkiWiki/Plugin/mirrorlist.pm index b7e532485..5afd9ec1a 100644 --- a/IkiWiki/Plugin/mirrorlist.pm +++ b/IkiWiki/Plugin/mirrorlist.pm @@ -53,7 +53,7 @@ sub pagetemplate (@) { sub mirrorlist ($) { my $page=shift; - return ($config{html5} ? '<nav id="mirrorlist">' : '<div>'). + return ($config{html5} ? '<nav' : '<div').' id="mirrorlist">'. (keys %{$config{mirrorlist}} > 1 ? gettext("Mirrors") : gettext("Mirror")). ": ". join(", ", diff --git a/IkiWiki/Plugin/monotone.pm b/IkiWiki/Plugin/monotone.pm index 105627814..b0bba5661 100644 --- a/IkiWiki/Plugin/monotone.pm +++ b/IkiWiki/Plugin/monotone.pm @@ -310,10 +310,10 @@ sub commitauthor (@) { if (defined $params{session}) { if (defined $params{session}->param("name")) { - return "Web user: " . $params{session}->param("name"); + return "Web user: " . IkiWiki::cloak($params{session}->param("name")); } elsif (defined $params{session}->remote_addr()) { - return "Web IP: " . $params{session}->remote_addr(); + return "Web IP: " . IkiWiki::cloak($params{session}->remote_addr()); } } return "Web: Anonymous"; diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm index 3b96e4b8e..35ef52a58 100644 --- a/IkiWiki/Plugin/openid.pm +++ b/IkiWiki/Plugin/openid.pm @@ -7,31 +7,18 @@ use strict; use IkiWiki 3.00; sub import { - add_underlay("openid-selector"); - add_underlay("jquery"); - hook(type => "checkconfig", id => "openid", call => \&checkconfig); hook(type => "getsetup", id => "openid", call => \&getsetup); hook(type => "auth", id => "openid", call => \&auth); hook(type => "formbuilder_setup", id => "openid", call => \&formbuilder_setup, last => 1); -} - -sub checkconfig () { - if ($config{cgi}) { - # Intercept normal signin form, so the openid selector - # can be displayed. - # - # When other auth hooks are registered, give the selector - # a reference to the normal signin form. - require IkiWiki::CGI; - my $real_cgi_signin; - if (keys %{$IkiWiki::hooks{auth}} > 1) { - $real_cgi_signin=\&IkiWiki::cgi_signin; - } - inject(name => "IkiWiki::cgi_signin", call => sub ($$) { - openid_selector($real_cgi_signin, @_); - }); - } + IkiWiki::loadplugin("emailauth"); + IkiWiki::loadplugin("loginselector"); + IkiWiki::Plugin::loginselector::register_login_plugin( + "openid", + \&openid_setup, + \&openid_check_input, + \&openid_auth, + ); } sub getsetup () { @@ -55,38 +42,34 @@ sub getsetup () { }, } -sub openid_selector { - my $real_cgi_signin=shift; - my $q=shift; - my $session=shift; - - my $openid_url=$q->param('openid_identifier'); - my $openid_error; +sub openid_setup ($$) { + my $q=shift; + my $template=shift; - if (! load_openid_module()) { - if ($real_cgi_signin) { - $real_cgi_signin->($q, $session); - exit; + if (load_openid_module()) { + my $openid_url=$q->param('openid_identifier'); + if (defined $openid_url) { + $template->param(openid_url => $openid_url); } - error(sprintf(gettext("failed to load openid module: "), @_)); + return 1; } - elsif (defined $q->param("action") && $q->param("action") eq "verify") { - validate($q, $session, $openid_url, sub { - $openid_error=shift; - }); + else { + return 0; } +} - my $template=IkiWiki::template("openid-selector.tmpl"); - $template->param( - cgiurl => IkiWiki::cgiurl(), - (defined $openid_error ? (openid_error => $openid_error) : ()), - (defined $openid_url ? (openid_url => $openid_url) : ()), - ($real_cgi_signin ? (nonopenidform => $real_cgi_signin->($q, $session, 1)) : ()), - ); +sub openid_check_input ($) { + my $q=shift; + my $openid_url=$q->param('openid_identifier'); + defined $q->param("action") && $q->param("action") eq "verify" && defined $openid_url && length $openid_url; +} - IkiWiki::printheader($session); - print IkiWiki::cgitemplate($q, "signin", $template->output); - exit; +sub openid_auth ($$$$) { + my $q=shift; + my $session=shift; + my $errordisplayer=shift; + my $openid_url=$q->param('openid_identifier'); + validate($q, $session, $openid_url, $errordisplayer); } sub formbuilder_setup (@) { @@ -104,7 +87,6 @@ sub formbuilder_setup (@) { size => 1, force => 1, fieldset => "login", comment => $session->param("name")); - $form->field(name => "email", type => "hidden"); } } @@ -119,7 +101,9 @@ sub validate ($$$;$) { my $claimed_identity = $csr->claimed_identity($openid_url); if (! $claimed_identity) { if ($errhandler) { - $errhandler->($csr->err); + if (ref($errhandler) eq 'CODE') { + $errhandler->($csr->err); + } return 0; } else { @@ -223,7 +207,7 @@ sub auth ($$) { } elsif (defined $q->param('openid_identifier')) { # myopenid.com affiliate support - validate($q, $session, $q->param('openid_identifier')); + validate($q, $session, scalar $q->param('openid_identifier')); } } diff --git a/IkiWiki/Plugin/passwordauth.pm b/IkiWiki/Plugin/passwordauth.pm index 0cf2a26ea..3bdd9de2e 100644 --- a/IkiWiki/Plugin/passwordauth.pm +++ b/IkiWiki/Plugin/passwordauth.pm @@ -251,6 +251,12 @@ sub formbuilder_setup (@) { my $name=shift; length $name && $name=~/$config{wiki_file_regexp}/ && + # don't allow registering + # accounts that look like + # openids, or email + # addresses, even if the + # file regexp allows it + $name!~/[\/:\@]/ && ! IkiWiki::userinfo_get($name, "regdate"); }, ); @@ -277,7 +283,7 @@ sub formbuilder_setup (@) { } elsif ($form->title eq "preferences") { my $user=$session->param("name"); - if (! IkiWiki::openiduser($user)) { + if (! IkiWiki::openiduser($user) && ! IkiWiki::emailuser($user)) { $form->field(name => "name", disabled => 1, value => $user, force => 1, fieldset => "login"); diff --git a/IkiWiki/Plugin/po.pm b/IkiWiki/Plugin/po.pm index 6107a4a22..6b55ee351 100644 --- a/IkiWiki/Plugin/po.pm +++ b/IkiWiki/Plugin/po.pm @@ -993,10 +993,9 @@ sub refreshpofiles ($@) { } if (-e $pofile) { - system("msgmerge", "--previous", "-q", "-U", "--backup=none", $pofile, $potfile) == 0 - or error("po(refreshpofiles) ". - sprintf(gettext("failed to update %s"), - $pofile)); + if (! (system("msgmerge", "--previous", "-q", "-U", "--backup=none", $pofile, $potfile) == 0)) { + print STDERR ("po(refreshpofiles) ". sprintf(gettext("failed to update %s"), $pofile)); + } } else { File::Copy::syscopy($potfile,$pofile) diff --git a/IkiWiki/Plugin/poll.pm b/IkiWiki/Plugin/poll.pm index 3bd4af206..eb0e6ef04 100644 --- a/IkiWiki/Plugin/poll.pm +++ b/IkiWiki/Plugin/poll.pm @@ -99,7 +99,7 @@ sub sessioncgi ($$) { my $cgi=shift; my $session=shift; if (defined $cgi->param('do') && $cgi->param('do') eq "poll") { - my $choice=decode_utf8($cgi->param('choice')); + my $choice=decode_utf8(scalar $cgi->param('choice')); if (! defined $choice || not length $choice) { error("no choice specified"); } diff --git a/IkiWiki/Plugin/polygen.pm b/IkiWiki/Plugin/polygen.pm index 78e3611e1..8ce62b754 100644 --- a/IkiWiki/Plugin/polygen.pm +++ b/IkiWiki/Plugin/polygen.pm @@ -28,6 +28,7 @@ sub preprocess (@) { my %params=@_; my $grammar = ($params{grammar} or 'polygen'); my $symbol = ($params{symbol} or undef); + my $options = ($config{deterministic} ? '-seed 42' : ''); # Sanitize parameters $grammar =~ IkiWiki::basename($grammar); @@ -51,10 +52,10 @@ sub preprocess (@) { my $res; if (defined $symbol) { - $res = `polygen -S $symbol $grmfile 2>/dev/null`; + $res = `polygen -S $symbol $options $grmfile 2>/dev/null`; } else { - $res = `polygen $grmfile 2>/dev/null`; + $res = `polygen $options $grmfile 2>/dev/null`; } if ($?) { diff --git a/IkiWiki/Plugin/recentchanges.pm b/IkiWiki/Plugin/recentchanges.pm index eec9803be..2b2f43b7a 100644 --- a/IkiWiki/Plugin/recentchanges.pm +++ b/IkiWiki/Plugin/recentchanges.pm @@ -102,7 +102,7 @@ sub sessioncgi ($$) { IkiWiki::decode_form_utf8($form); if ($form->submitted eq 'Revert' && $form->validate) { - IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); + IkiWiki::checksessionexpiry($q, $session); my $message=sprintf(gettext("This reverts commit %s"), $rev); if (defined $form->field('revertmessage') && length $form->field('revertmessage')) { diff --git a/IkiWiki/Plugin/remove.pm b/IkiWiki/Plugin/remove.pm index d48b28f95..5c99b387e 100644 --- a/IkiWiki/Plugin/remove.pm +++ b/IkiWiki/Plugin/remove.pm @@ -219,7 +219,7 @@ sub sessioncgi ($$) { postremove($session); } elsif ($form->submitted eq 'Remove' && $form->validate) { - IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); + IkiWiki::checksessionexpiry($q, $session); my @pages=$form->field("page"); diff --git a/IkiWiki/Plugin/rename.pm b/IkiWiki/Plugin/rename.pm index 8387a1e32..6d56340b8 100644 --- a/IkiWiki/Plugin/rename.pm +++ b/IkiWiki/Plugin/rename.pm @@ -237,7 +237,7 @@ sub postrename ($$$;$$) { # on it. $oldcgi->param("editcontent", renamepage_hook($dest, $src, $dest, - $oldcgi->param("editcontent"))); + scalar $oldcgi->param("editcontent"))); # Get a new edit token; old was likely invalidated. $oldcgi->param("rcsinfo", @@ -297,7 +297,7 @@ sub sessioncgi ($$) { if ($q->param("do") eq 'rename') { my $session=shift; - my ($form, $buttons)=rename_form($q, $session, Encode::decode_utf8($q->param("page"))); + my ($form, $buttons)=rename_form($q, $session, Encode::decode_utf8(scalar $q->param("page"))); IkiWiki::decode_form_utf8($form); my $src=$form->field("page"); @@ -305,7 +305,7 @@ sub sessioncgi ($$) { postrename($q, $session, $src); } elsif ($form->submitted eq 'Rename' && $form->validate) { - IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); + IkiWiki::checksessionexpiry($q, $session); # These untaints are safe because of the checks # performed in check_canrename later. @@ -332,7 +332,7 @@ sub sessioncgi ($$) { IkiWiki::Plugin::attachment::is_held_attachment($src); if ($held) { rename($held, IkiWiki::Plugin::attachment::attachment_holding_location($dest)); - postrename($q, $session, $src, $dest, $q->param("attachment")) + postrename($q, $session, $src, $dest, scalar $q->param("attachment")) unless defined $srcfile; } @@ -438,7 +438,7 @@ sub sessioncgi ($$) { $renamesummary.=$template->output; } - postrename($q, $session, $src, $dest, $q->param("attachment")); + postrename($q, $session, $src, $dest, scalar $q->param("attachment")); } else { IkiWiki::showform($form, $buttons, $session, $q); diff --git a/IkiWiki/Plugin/svn.pm b/IkiWiki/Plugin/svn.pm index fd11f2c63..c46a52dcf 100644 --- a/IkiWiki/Plugin/svn.pm +++ b/IkiWiki/Plugin/svn.pm @@ -147,12 +147,12 @@ sub commitmessage (@) { if (defined $params{session}) { if (defined $params{session}->param("name")) { return "web commit by ". - $params{session}->param("name"). + IkiWiki::cloak($params{session}->param("name")). (length $params{message} ? ": $params{message}" : ""); } elsif (defined $params{session}->remote_addr()) { return "web commit from ". - $params{session}->remote_addr(). + IkiWiki::cloak($params{session}->remote_addr()). (length $params{message} ? ": $params{message}" : ""); } } diff --git a/IkiWiki/Plugin/templatebody.pm b/IkiWiki/Plugin/templatebody.pm new file mode 100644 index 000000000..3848b75c7 --- /dev/null +++ b/IkiWiki/Plugin/templatebody.pm @@ -0,0 +1,81 @@ +#!/usr/bin/perl +# Define self-documenting templates as wiki pages without HTML::Template +# markup leaking into IkiWiki's output. +# Copyright © 2013-2014 Simon McVittie. GPL-2+, see debian/copyright +package IkiWiki::Plugin::templatebody; + +use warnings; +use strict; +use IkiWiki 3.00; +use Encode; + +sub import { + hook(type => "getsetup", id => "templatebody", call => \&getsetup); + hook(type => "preprocess", id => "templatebody", call => \&preprocess, + scan => 1); + hook(type => "readtemplate", id => "templatebody", + call => \&readtemplate); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => undef, + section => "core", + }, +} + +# This doesn't persist between runs: we're going to read and scan the +# template file regardless, so there's no point in saving it to the index. +# Example contents: +# ("templates/note" => "<div class=\"notebox\">\n<TMPL_VAR text>\n</div>") +my %templates; + +sub preprocess (@) { + my %params=@_; + + # [[!templatebody "<div>hello</div>"]] results in + # preprocess("<div>hello</div>" => undef, page => ...) + my $content = $_[0]; + if (length $_[1]) { + error(gettext("first parameter must be the content")); + } + + $templates{$params{page}} = $content; + + return ""; +} + +sub readtemplate { + my %params = @_; + my $tpage = $params{page}; + my $content = $params{content}; + my $filename = $params{filename}; + + # pass-through if it's a .tmpl attachment or otherwise unsuitable + return $content unless defined $tpage; + return $content if $tpage =~ /\.tmpl$/; + my $src = $pagesources{$tpage}; + return $content unless defined $src; + return $content unless defined pagetype($src); + + # We might be using the template for [[!template]], which has to run + # during the scan stage so that templates can include scannable + # directives which are expanded in the resulting page. Calls to + # IkiWiki::scan are in arbitrary order, so the template might + # not have been scanned yet. Make sure. + require IkiWiki::Render; + IkiWiki::scan($src); + + # Having scanned it, we know whether it had a [[!templatebody]]. + if (exists $templates{$tpage}) { + return $templates{$tpage}; + } + + # If not, return the whole thing. (Eventually, after implementing + # a transition, this can become an error.) + return $content; +} + +1 diff --git a/IkiWiki/Plugin/teximg.pm b/IkiWiki/Plugin/teximg.pm index 3d6fa9942..e0f97614b 100644 --- a/IkiWiki/Plugin/teximg.pm +++ b/IkiWiki/Plugin/teximg.pm @@ -146,8 +146,8 @@ sub gen_image ($$$$) { my $tex = $config{teximg_prefix}; $tex .= '\['.$code.'\]'; $tex .= $config{teximg_postfix}; - $tex =~ s!\\documentclass{article}!\\documentclass[${height}pt]{article}!g; - $tex =~ s!\\documentclass{scrartcl}!\\documentclass[${height}pt]{scrartcl}!g; + $tex =~ s!\\documentclass\{article}!\\documentclass[${height}pt]{article}!g; + $tex =~ s!\\documentclass\{scrartcl}!\\documentclass[${height}pt]{scrartcl}!g; my $tmp = eval { create_tmp_dir($digest) }; if (! $@ && diff --git a/IkiWiki/Plugin/tla.pm b/IkiWiki/Plugin/tla.pm index 11be248e8..c2fffbced 100644 --- a/IkiWiki/Plugin/tla.pm +++ b/IkiWiki/Plugin/tla.pm @@ -108,12 +108,12 @@ sub rcs_commit (@) { if (defined $params{session}) { if (defined $params{session}->param("name")) { $message="web commit by ". - $params{session}->param("name"). + IkiWiki::cloak($params{session}->param("name")). (length $message ? ": $message" : ""); } elsif (defined $params{session}->remote_addr()) { $message="web commit from ". - $params{session}->remote_addr(). + IkiWiki::cloak($params{session}->remote_addr()). (length $message ? ": $message" : ""); } } diff --git a/IkiWiki/Plugin/websetup.pm b/IkiWiki/Plugin/websetup.pm index f95017c90..d3cbff0d5 100644 --- a/IkiWiki/Plugin/websetup.pm +++ b/IkiWiki/Plugin/websetup.pm @@ -460,12 +460,12 @@ sub showform ($$) { my @command; if ($form->submitted eq 'Rebuild Wiki') { - @command=("ikiwiki", "-setup", $config{setupfile}, - "-rebuild", "-v"); + @command=("ikiwiki", "--setup", $config{setupfile}, + "--rebuild", "-v"); } else { - @command=("ikiwiki", "-setup", $config{setupfile}, - "-refresh", "-wrappers", "-v"); + @command=("ikiwiki", "--setup", $config{setupfile}, + "--refresh", "--wrappers", "-v"); } close STDERR; diff --git a/IkiWiki/Render.pm b/IkiWiki/Render.pm index fa2940b01..4c998b156 100644 --- a/IkiWiki/Render.pm +++ b/IkiWiki/Render.pm @@ -6,7 +6,7 @@ use warnings; use strict; use IkiWiki; -my (%backlinks, %rendered); +my (%backlinks, %rendered, %scanned); our %brokenlinks; my $links_calculated=0; @@ -111,7 +111,7 @@ sub genpage ($$) { } templateactions($template, $page); - my @backlinks=sort { $a->{page} cmp $b->{page} } backlinks($page); + my @backlinks=sort { $a->{page} cmp $b->{page} || $a->{url} cmp $b->{url} } backlinks($page); my ($backlinks, $more_backlinks); if (@backlinks <= $config{numbacklinks} || ! $config{numbacklinks}) { $backlinks=\@backlinks; @@ -134,6 +134,7 @@ sub genpage ($$) { ctime => displaytime($pagectime{$page}, undef, 1), baseurl => baseurl($page), html5 => $config{html5}, + responsive_layout => $config{responsive_layout}, ); run_hooks(pagetemplate => sub { @@ -154,6 +155,8 @@ sub genpage ($$) { sub scan ($) { my $file=shift; + return if ($config{rebuild} && $phase > PHASE_SCAN) || $scanned{$file}; + $scanned{$file}=1; debug(sprintf(gettext("scanning %s"), $file)); @@ -471,7 +474,12 @@ sub find_new_files ($) { } $pagecase{lc $page}=$page; if (! exists $pagectime{$page}) { - $pagectime{$page}=(srcfile_stat($file))[10]; + my @stat=srcfile_stat($file, 1); + # For the creation time of the page, take the + # inode change time (not creation time!) or + # the modification time, whichever is older. + my $ctime=($stat[10] < $stat[9] ? $stat[10] : $stat[9]); + $pagectime{$page}=$ctime if defined $ctime; } } } @@ -531,10 +539,11 @@ sub find_changed ($) { my @internal_changed; foreach my $file (@$files) { my $page=pagename($file); - my ($srcfile, @stat)=srcfile_stat($file); - if (! exists $pagemtime{$page} || - $stat[9] > $pagemtime{$page} || - $forcerebuild{$page}) { + my ($srcfile, @stat)=srcfile_stat($file, 1); + if (defined $srcfile && + (! exists $pagemtime{$page} || + $stat[9] > $pagemtime{$page} || + $forcerebuild{$page})) { $pagemtime{$page}=$stat[9]; if (isinternal($page)) { @@ -825,6 +834,8 @@ sub gen_autofile ($$$) { } sub refresh () { + $phase = PHASE_SCAN; + srcdir_check(); run_hooks(refresh => sub { shift->() }); my ($files, $pages, $new, $internal_new, $del, $internal_del, $changed, $internal_changed); @@ -876,7 +887,14 @@ sub refresh () { } calculate_links(); - + + # At this point it becomes OK to start matching pagespecs. + $phase = PHASE_RENDER; + # Save some memory in full rebuilds: we no longer need to keep + # track of which pages we've scanned, because we can assume the + # answer is "all of them". + %scanned = () if $config{rebuild}; + remove_del(@$del, @$internal_del); foreach my $file (@$changed) { @@ -940,6 +958,10 @@ sub commandline_render () { loadindex(); unlockwiki(); + # This function behaves as though it's in the render phase; + # all other files are assumed to have been scanned last time. + $phase = PHASE_RENDER; + my $srcfile=possibly_foolish_untaint($config{render}); my $file=$srcfile; $file=~s/\Q$config{srcdir}\E\/?//; diff --git a/IkiWiki/Setup/Automator.pm b/IkiWiki/Setup/Automator.pm index 671438710..9239974ad 100644 --- a/IkiWiki/Setup/Automator.pm +++ b/IkiWiki/Setup/Automator.pm @@ -154,31 +154,33 @@ sub import (@) { foreach my $admin (@{$config{adminuser}}) { next if defined IkiWiki::openiduser($admin); - # Prompt for password w/o echo. - my ($password, $password2); - system('stty -echo 2>/dev/null'); - local $|=1; - print "\n\nCreating wiki admin $admin ...\n"; - for (;;) { - print "Choose a password: "; - chomp($password=<STDIN>); - print "\n"; - print "Confirm password: "; - chomp($password2=<STDIN>); - - last if $password2 eq $password; - - print "Password mismatch.\n\n"; - } - print "\n\n\n"; - system('stty sane 2>/dev/null'); + if (! defined IkiWiki::emailuser($admin)) { + # Prompt for password w/o echo. + my ($password, $password2); + system('stty -echo 2>/dev/null'); + local $|=1; + print "\n\nCreating wiki admin $admin ...\n"; + for (;;) { + print "Choose a password: "; + chomp($password=<STDIN>); + print "\n"; + print "Confirm password: "; + chomp($password2=<STDIN>); + + last if $password2 eq $password; + + print "Password mismatch.\n\n"; + } + print "\n\n\n"; + system('stty sane 2>/dev/null'); - if (IkiWiki::userinfo_setall($admin, { regdate => time }) && - IkiWiki::Plugin::passwordauth::setpassword($admin, $password)) { - IkiWiki::userinfo_set($admin, "email", $config{adminemail}) if defined $config{adminemail}; - } - else { - error("problem setting up $admin user"); + if (IkiWiki::userinfo_setall($admin, { regdate => time }) && + IkiWiki::Plugin::passwordauth::setpassword($admin, $password)) { + IkiWiki::userinfo_set($admin, "email", $config{adminemail}) if defined $config{adminemail}; + } + else { + error("problem setting up $admin user"); + } } } @@ -206,7 +208,7 @@ sub import (@) { prettydir($config{$key})."\n"; } print "To modify settings, edit ".prettydir($config{dumpsetup})." and then run:\n"; - print " ikiwiki -setup ".prettydir($config{dumpsetup})."\n"; + print " ikiwiki --setup ".prettydir($config{dumpsetup})."\n"; exit 0; } diff --git a/IkiWiki/Wrapper.pm b/IkiWiki/Wrapper.pm index b46bc6aa9..69500029c 100644 --- a/IkiWiki/Wrapper.pm +++ b/IkiWiki/Wrapper.pm @@ -52,6 +52,7 @@ sub gen_wrapper () { HTTP_COOKIE REMOTE_USER HTTPS REDIRECT_STATUS HTTP_HOST SERVER_PORT HTTPS HTTP_ACCEPT REDIRECT_URL} if $config{cgi}; + my $envsize=$#envsave; my $envsave=""; foreach my $var (@envsave) { $envsave.=<<"EOF"; @@ -59,6 +60,18 @@ sub gen_wrapper () { addenv("$var", s); EOF } + if (ref $config{ENV} eq 'HASH') { + foreach my $key (keys %{$config{ENV}}) { + my $val=$config{ENV}{$key}; + utf8::encode($val) if utf8::is_utf8($val); + $val =~ s/([^A-Za-z0-9])/sprintf '""\\x%02x""', ord($1)/ge; + $envsize += 1; + $envsave.=<<"EOF"; + addenv("$key", "$val"); +EOF + } + delete $config{ENV}; + } my @wrapper_hooks; run_hooks(genwrapper => sub { push @wrapper_hooks, shift->() }); @@ -171,21 +184,28 @@ EOF #include <sys/file.h> extern char **environ; -char *newenviron[$#envsave+7]; +char *newenviron[$envsize+7]; int i=0; void addenv(char *var, char *val) { char *s=malloc(strlen(var)+1+strlen(val)+1); - if (!s) + if (!s) { perror("malloc"); - sprintf(s, "%s=%s", var, val); - newenviron[i++]=s; + exit(1); + } + else { + sprintf(s, "%s=%s", var, val); + newenviron[i++]=s; + } } void set_cgilock_fd (int lockfd) { - char *fd_s=malloc(8); + char fd_s[12]; sprintf(fd_s, "%i", lockfd); - setenv("IKIWIKI_CGILOCK_FD", fd_s, 1); + if (setenv("IKIWIKI_CGILOCK_FD", fd_s, 1) != 0) { + perror("setenv"); + exit(1); + } } int main (int argc, char **argv) { |