# Copyright (C) 2004–2019 Alex Schroeder # Copyright (C) 2015 Alex-Daniel Jakimenko # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . package OddMuse; use lib '.'; use XML::LibXML; use utf8; use Encode qw(encode_utf8 decode_utf8); use vars qw($raw); # Test::More explains how to fix wide character in print issues my $builder = Test::More->builder; binmode $builder->output, ":encoding(UTF-8)"; binmode $builder->failure_output, ":encoding(UTF-8)"; binmode $builder->todo_output, ":encoding(UTF-8)"; # Import the functions $raw = 0; # capture utf8 is the default $RunCGI = 0; # don't print HTML on stdout $UseConfig = 0; # don't read module files $DataDir = 'test-data'; while (not mkdir($DataDir)) { $DataDir = sprintf("test-data-%03d", int(rand(1000))); } $ENV{WikiDataDir} = $DataDir; require 'wiki.pl'; # Try to guess which Perl we should be using. Since we loaded wiki.pl, # our $ENV{PATH} is set to /bin:/usr/bin in order to find diff and # grep. if ($ENV{PERLBREW_PATH}) { $ENV{PATH} = join(':', split(/ /, $ENV{PERLBREW_PATH}), $ENV{PATH}); } elsif (-f '/usr/local/bin/perl') { $ENV{PATH} = '/usr/local/bin:' . $ENV{PATH}; } clear_pages(); Init(); use vars qw($redirect); undef $/; $| = 1; # no output buffering sub url_encode { my $str = shift; return '' unless $str; my @letters = split(//, encode_utf8($str)); my @safe = ('a' .. 'z', 'A' .. 'Z', '0' .. '9', '-', '_', '.'); # shell metachars are unsafe foreach my $letter (@letters) { my $pattern = quotemeta($letter); if (not grep(/$pattern/, @safe)) { $letter = sprintf("%%%02x", ord($letter)); } } return join('', @letters); } # Run perl in a subprocess and make sure it prints UTF-8 and not Latin-1 # If you use the download action, the output will be raw bytes. Use # something like the following: # { # local $raw = 1; # $page = get_page('action=download id=Trogs'); # } sub capture { my $command = shift; if ($raw) { open (CL, '-|', $command) or die "Can't run $command: $!"; } else { open (CL, '-|:encoding(utf-8)', $command) or die "Can't run $command: $!"; } my $result = ; close CL; return $result; } sub update_page { my ($id, $text, $summary, $minor, $admin, @rest) = @_; $id = FreeToNormal($id); my $pwd = $admin ? 'foo' : 'wrong'; my $page = url_encode($id); $text = url_encode($text); $summary = url_encode($summary); $minor = $minor ? 'on' : 'off'; my $rest = join(' ', @rest); $redirect = capture("perl wiki.pl 'Save=1' 'title=$page' 'summary=$summary' 'recent_edit=$minor' 'text=$text' 'pwd=$pwd' $rest"); $output = capture("perl wiki.pl action=browse id=$page $rest"); if ($redirect =~ /^Status: 302 /) { # just in case a new page got created or NearMap or InterMap $IndexHash{$id} = 1; @IndexList = sort(keys %IndexHash); ReInit($id); # if $id eq $InterMap, we need it to be in the $IndexHash before running ReInit() } return $output; } sub get_page { return capture("perl wiki.pl @_"); } sub name { $_ = shift; s/\n/\\n/g; $_ = '...' . substr($_, -67) if length > 70; return $_; } sub newlines { my @strings = @_; return map { s/\\n/\n/g; $_; } @strings; } # alternating input and output strings for applying rules sub run_tests { # translate embedded newlines (other backslashes remain untouched) my @tests = newlines(@_); my ($input, $output); while (($input, $output, @tests) = @tests) { my $result = apply_rules($input); is($result, $output, name($input)); } } # alternating input and output strings for applying rules sub run_tests_negative { # translate embedded newlines (other backslashes remain untouched) my @tests = newlines(@_); my ($input, $output); while (($input, $output, @tests) = @tests) { my $result = apply_rules($input); isnt($result, $output, name($input)); } } sub apply_rules { my $input = shift; local *STDOUT; $output = ''; open(STDOUT, '>', \$output) or die "Can't open memory file: $!"; $FootnoteNumber = 0; ApplyRules(QuoteHtml($input), 1); return $output; } # alternating input and output strings for applying macros instead of rules sub run_macro_tests { # translate embedded newlines (other backslashes remain untouched) my %test = map { s/\\n/\n/g; $_; } @_; # Note that the order of tests is not specified! foreach my $input (keys %test) { $_ = $input; foreach my $macro (@MyMacros) { &$macro; } is($_, $test{$input}, $input); } } # one string, many tests sub test_page { my ($page, @tests) = @_; foreach my $test (@tests) { like($page, qr($test), name($test)); } } # one file, many tests sub test_file { my ($file, @tests) = @_; if (open(F, '< :encoding(UTF-8)', $file)) { local $/ = undef; test_page(, @tests); close(F); } else { warn "cannot open $file\n"; } } # one string, many negative tests sub test_page_negative { my $page = shift; foreach my $str (@_) { unlike($page, qr($str), name("not $str")); } } sub xpath_do { my ($check, $message, $page, @tests) = @_; $page =~ s/^(.+\r\n)*\r\n//; # strip headers my $xml = $page =~ s/^.*?<\?xml.*?>\s*//s; # strip xml processing my $page_shown = 0; my $parser = XML::LibXML->new(recover => 1, suppress_errors => 1); # allow HTML5 tags my $doc; my @result; SKIP: { if ($xml) { eval { $doc = $parser->parse_string($page) }; } else { eval { $doc = $parser->parse_html_string($page) }; } if ($@) { skip("Cannot parse ".name($page).": $@", $#tests + 1); return; } # warn "Doc: '$doc'\n"; foreach my $test (@tests) { my $nodelist; # libxml2 is not aware of UTF8 flag eval { $nodelist = $doc->findnodes(encode_utf8($test)) }; if ($@) { fail(&$check(1) ? "$test: $@" : "not $test: $@"); } elsif (ok(&$check($nodelist->size()), name(&$check(1) ? $test : "not $test"))) { push(@result, $nodelist->string_value()); } else { $page =~ s/^.*? 0; }, "No Matches\n", @_); } sub xpath_test_file { my ($file, @tests) = @_; if (open(F, '< :encoding(UTF-8)', $file)) { local $/ = undef; xpath_test(, @tests); close(F); } else { warn "cannot open $file\n"; } } sub negative_xpath_test { xpath_do(sub { shift == 0; }, "Unexpected Matches\n", @_); } # alias sub xpath_test_negative { return negative_xpath_test(@_); } sub xpath_run_tests { # translate embedded newlines (other backslashes remain untouched) my @tests = newlines(@_); my ($input, $output); while (($input, $output, @tests) = @tests) { my $result = apply_rules($input); xpath_test("
$result
", $output); } } sub xpath_run_tests_negative { # translate embedded newlines (other backslashes remain untouched) my @tests = newlines(@_); my ($input, $output); while (($input, $output, @tests) = @tests) { my $result = apply_rules($input); xpath_test_negative("
$result
", $output); } } sub remove_rule { my $rule = shift; my @list = (); my $found = 0; foreach my $item (@MyRules) { if ($item ne $rule) { push @list, $item; } else { $found = 1; } } die "Rule not found" unless $found; @MyRules = @list; } sub add_module { my ($mod, $subdir) = @_; $subdir .= '/' if $subdir and substr($subdir, -1) ne '/'; mkdir $ModuleDir unless -d $ModuleDir; my $dir = `/bin/pwd`; chop($dir); if (-l "$ModuleDir/$mod") { # do nothing } elsif (eval{ symlink("$dir/modules/$subdir$mod", "$ModuleDir/$mod"); 1; }) { # do nothing } else { system('copy', "$dir/modules/$subdir$mod", "$ModuleDir/$mod"); } die "Cannot symlink $mod: $!" unless -e "$ModuleDir/$mod"; do "$ModuleDir/$mod"; @MyRules = sort {$RuleOrder{$a} <=> $RuleOrder{$b}} @MyRules; } sub remove_module { my $mod = shift; mkdir $ModuleDir unless -d $ModuleDir; unlink("$ModuleDir/$mod") or die "Cannot unlink: $!"; } sub write_config_file { open(F, '>:encoding(utf-8)', "$DataDir/config"); print F "\$AdminPass = 'foo';\n"; # this used to be the default in earlier CGI.pm versions print F "\$ScriptName = 'http://localhost/wiki.pl';\n"; print F "\$SurgeProtection = 0;\n"; close(F); $ScriptName = 'http://localhost/test.pl'; # different! $IndexInit = 0; %IndexHash = (); @IndexList = (); $InterSiteInit = 0; %InterSite = (); $NearSiteInit = 0; %NearSite = (); %NearSearch = (); } sub clear_pages { if (-f "/bin/rm") { system('/bin/rm', '-rf', $DataDir); } else { system('c:/cygwin/bin/rm.exe', '-rf', $DataDir); } die "Cannot remove '$DataDir'!\n" if -e $DataDir; mkdir $DataDir; if ($^O eq 'darwin') { # On a Mac we are probably using the HFS filesystem which uses NFD instead # of NFC for filenames. Since clear_pages runs as the very first thing, the # modules directory doesn't exist, yet. And as Init() hasn't run, $ModuleDir # is not set either. All we have is $DataDir. $ModuleDir = "$DataDir/modules"; add_module('mac.pl'); } write_config_file(); } # Find an unused port sub random_port { use Errno qw( EADDRINUSE ); use Socket qw( PF_INET SOCK_STREAM INADDR_ANY sockaddr_in ); my $family = PF_INET; my $type = SOCK_STREAM; my $proto = getprotobyname('tcp') or die "getprotobyname: $!"; my $host = INADDR_ANY; # Use inet_aton for a specific interface for my $i (1..3) { my $port = 1024 + int(rand(65535 - 1024)); socket(my $sock, $family, $type, $proto) or die "socket: $!"; my $name = sockaddr_in($port, $host) or die "sockaddr_in: $!"; setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1); bind($sock, $name) and close($sock) and return $port; die "bind: $!" if $! != EADDRINUSE; print "Port $port in use, retrying...\n"; } die "Tried 3 random ports and failed.\n" } my @pids; # Fork a simple test server sub start_server { my $num = shift||1; die "A server already exists: @pids\n" unless @pids < $num; my $port = random_port(); $ScriptName = "http://localhost:$port"; AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n"); my $pid = fork(); if (!defined $pid) { die "Cannot fork: $!"; } elsif ($pid == 0) { use Config; my $secure_perl_path = $Config{perlpath}; exec($secure_perl_path, "stuff/server.pl", "./wiki.pl", $port) or die "Cannot exec: $!"; } else { push(@pids, $pid); # give the server some time to start up sleep 1; } } # Fork a Mojolicious server sub start_mojolicious_server { my $num = shift||1; die "A server already exists: @pids\n" unless @pids < $num; my $port = random_port(); my $listen = "http://127.0.0.1:$port"; $ScriptName = "http://127.0.0.1:$port/wiki"; AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n"); my $pid = fork(); if (!defined $pid) { die "Cannot fork: $!"; } elsif ($pid == 0) { use Config; my $secure_perl_path = $Config{perlpath}; exec($secure_perl_path, "server.pl", "daemon", "-l", $listen) or die "Cannot exec: $!"; } else { push(@pids, $pid); # give the server some time to start up sleep 1; } } END { # kill servers for my $pid (@pids) { kill 'KILL', $pid or warn "Could not kill server $pid"; } } sub RunAndTerminate { # runs a command for 1 second and then sends SIGTERM my $pid = fork(); if (not $pid) { # child open(STDOUT, '>', '/dev/null'); # we don't want to see the output open(STDERR, '>', '/dev/null'); exec(@_) or die "Cannot start a new process: $!"; } # parent sleep 1; kill 'TERM', $pid; wait; # let it finish } sub AppendToConfig { my @data = @_; # one or more strings open(my $fh, '>>', "$DataDir/config") or die "Could not append to config file: $!"; print $fh join("\n", @data); close $fh; } 1;