diff -r 000000000000 -r 414e01d06fd5 lib/TWiki/Net.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/TWiki/Net.pm Sat Jan 26 15:50:53 2008 +0100
@@ -0,0 +1,464 @@
+# See bottom of file for license and copyright information
+
+=begin twiki
+
+---+ package TWiki::Net
+
+Object that brokers access to network resources.
+
+=cut
+
+# This module is used by configure, and as such must *not* 'use TWiki',
+# or any other module that uses it. Always run configure to test after
+# changing the module.
+
+package TWiki::Net;
+
+use strict;
+use Assert;
+use Error qw( :try );
+
+# note that the session is *optional*
+sub new {
+ my ( $class, $session ) = @_;
+ my $this = bless( { session => $session }, $class );
+
+ $this->{mailHandler} = undef;
+
+ return $this;
+}
+
+=begin twiki
+
+---++ ObjectMethod finish()
+Break circular references.
+
+=cut
+
+# Note to developers; please undef *all* fields in the object explicitly,
+# whether they are references or not. That way this method is "golden
+# documentation" of the live fields in the object.
+sub finish {
+ my $this = shift;
+ undef $this->{mailHandler};
+ undef $this->{HELLO_HOST};
+ undef $this->{MAIL_HOST};
+ undef $this->{session};
+}
+
+=pod
+
+---+++ getExternalResource( $url ) -> $response
+
+Get whatever is at the other end of a URL (using an HTTP GET request). Will
+only work for encrypted protocols such as =https= if the =LWP= CPAN module is
+installed.
+
+Note that the =$url= may have an optional user and password, as specified by
+the relevant RFC. Any proxy set in =configure= is honoured.
+
+The =$response= is an object that is known to implement the following subset of
+the methods of =LWP::Response=. It may in fact be an =LWP::Response= object,
+but it may also not be if =LWP= is not available, so callers may only assume
+the following subset of methods is available:
+| =code()= |
+| =message()= |
+| =header($field)= |
+| =content()= |
+| =is_error()= |
+| =is_redirect()= |
+
+Note that if LWP is *not* available, this function:
+ 1 can only really be trusted for HTTP/1.0 urls. If HTTP/1.1 or another
+ protocol is required, you are *strongly* recommended to =require LWP=.
+ 1 Will not parse multipart content
+
+In the event of the server returning an error, then =is_error()= will return
+true, =code()= will return a valid HTTP status code
+as specified in RFC 2616 and RFC 2518, and =message()= will return the
+message that was received from
+the server. In the event of a client-side error (e.g. an unparseable URL)
+then =is_error()= will return true and =message()= will return an explanatory
+message. =code()= will return 400 (BAD REQUEST).
+
+Note: Callers can easily check the availability of other HTTP::Response methods
+as follows:
+
+
+my $response = TWiki::Func::getExternalResource($url);
+if (!$response->is_error() && $response->isa('HTTP::Response')) {
+ ... other methods of HTTP::Response may be called
+} else {
+ ... only the methods listed above may be called
+}
+
+
+=cut
+
+sub getExternalResource {
+ my ($this, $url) = @_;
+
+ my $protocol;
+ if( $url =~ m!^([a-z]+):! ) {
+ $protocol = $1;
+ } else {
+ require TWiki::Net::HTTPResponse;
+ return new TWiki::Net::HTTPResponse("Bad URL: $url");
+ }
+
+ eval "use LWP";
+ unless( $@ ) {
+ return _GETUsingLWP( $this, $url );
+ }
+
+ # Fallback mechanism
+ if( $protocol ne 'http') {
+ require TWiki::Net::HTTPResponse;
+ return new TWiki::Net::HTTPResponse(
+ "LWP not available for handling protocol: $url");
+ }
+
+ my $response;
+ try {
+ $url =~ s!^\w+://!!; # remove protocol
+ my ( $user, $pass );
+ if ($url =~ s!([^/\@:]+)(?::([^/\@:]+))?@!!) {
+ ( $user, $pass ) = ( $1, $2 || '');
+ }
+
+ unless ($url =~ s!([^:/]+)(?::([0-9]+))?!! ) {
+ die "Bad URL: $url";
+ }
+ my( $host, $port ) = ( $1, $2 || 80);
+
+ require Socket;
+ import Socket qw(:all);
+
+ $url = '/' unless( $url );
+ my $req = "GET $url HTTP/1.0\r\n";
+
+ $req .= "Host: $host:$port\r\n";
+ if( $user ) {
+ # Use MIME::Base64 at run-time if using outbound proxy with
+ # authentication
+ require MIME::Base64;
+ import MIME::Base64 ();
+ my $base64 = encode_base64( "$user:$pass", "\r\n" );
+ $req .= "Authorization: Basic $base64";
+ }
+
+ # SMELL: Reference to TWiki variables used for compatibility
+ my ($proxyHost, $proxyPort);
+ if ($this->{session} && $this->{session}->{prefs}) {
+ my $prefs = $this->{session}->{prefs};
+ $proxyHost = $prefs->getPreferencesValue('PROXYHOST');
+ $proxyPort = $prefs->getPreferencesValue('PROXYPORT');
+ }
+ $proxyHost ||= $TWiki::cfg{PROXY}{HOST};
+ $proxyPort ||= $TWiki::cfg{PROXY}{PORT};
+ if($proxyHost && $proxyPort) {
+ $req = "GET http://$host:$port$url HTTP/1.0\r\n";
+ $host = $proxyHost;
+ $port = $proxyPort;
+ }
+
+ '$Rev: 16278 (22 Jan 2008) $'=~/([0-9]+)/;
+ my $revstr=$1;
+
+ $req .= 'User-Agent: TWiki::Net/'.$revstr."\r\n";
+ $req .= "\r\n\r\n";
+
+ my ( $iaddr, $paddr, $proto );
+ $iaddr = inet_aton( $host );
+ $paddr = sockaddr_in( $port, $iaddr );
+ $proto = getprotobyname( 'tcp' );
+ unless( socket( *SOCK, &PF_INET, &SOCK_STREAM, $proto ) ) {
+ die "socket failed: $!";
+ }
+ unless( connect( *SOCK, $paddr ) ) {
+ die "connect failed: $!";
+ }
+ select SOCK; $| = 1;
+ local $/ = undef;
+ print SOCK $req;
+ my $result = '';
+ $result = ;
+ unless( close( SOCK )) {
+ die "close faied: $!";
+ }
+ select STDOUT;
+
+ # No LWP, but may have HTTP::Response which would make life easier
+ # (it has a much more thorough parser)
+ eval 'require HTTP::Response';
+ if ($@) {
+ # Nope, no HTTP::Response, have to do things the hard way :-(
+ require TWiki::Net::HTTPResponse;
+ $response = TWiki::Net::HTTPResponse->parse($result);
+ } else {
+ $response = HTTP::Response->parse($result);
+ }
+ } catch Error::Simple with {
+ $response = new TWiki::Net::HTTPResponse(shift);
+ };
+ return $response;
+}
+
+sub _GETUsingLWP {
+ my( $this, $url ) = @_;
+
+ my ( $user, $pass );
+ if ($url =~ s!([^/\@:]+)(?::([^/\@:]+))?@!!) {
+ ( $user, $pass ) = ( $1, $2 );
+ }
+ my $request;
+ require HTTP::Request;
+ $request = HTTP::Request->new(GET => $url);
+ '$Rev: 16278 (22 Jan 2008) $'=~/([0-9]+)/;
+ my $revstr=$1;
+ $request->header('User-Agent' => 'TWiki::Net/'.$revstr." libwww-perl/$LWP::VERSION");
+ require TWiki::Net::UserCredAgent;
+ my $ua = new TWiki::Net::UserCredAgent($user, $pass);
+ my $response = $ua->request($request);
+ return $response;
+}
+
+# pick a default mail handler
+sub _installMailHandler {
+ my $this = shift;
+ my $handler = 0; # Not undef
+ if ($this->{session} && $this->{session}->{prefs}) {
+ my $prefs = $this->{session}->{prefs};
+ $this->{MAIL_HOST} = $prefs->getPreferencesValue( 'SMTPMAILHOST' );
+ $this->{HELLO_HOST} = $prefs->getPreferencesValue( 'SMTPSENDERHOST' );
+ }
+
+ $this->{MAIL_HOST} ||= $TWiki::cfg{SMTP}{MAILHOST};
+ $this->{HELLO_HOST} ||= $TWiki::cfg{SMTP}{SENDERHOST};
+
+ if( $this->{MAIL_HOST} ) {
+ # See Codev.RegisterFailureInsecureDependencyCygwin for why
+ # this must be untainted
+ require TWiki::Sandbox;
+ $this->{MAIL_HOST} =
+ TWiki::Sandbox::untaintUnchecked( $this->{MAIL_HOST} );
+ eval { # May fail if Net::SMTP not installed
+ require Net::SMTP;
+ };
+ if( $@ ) {
+ $this->{session}->writeWarning( "SMTP not available: $@" )
+ if ($this->{session});
+ } else {
+ $handler = \&_sendEmailByNetSMTP;
+ }
+ }
+
+ if( !$handler && $TWiki::cfg{MailProgram} ) {
+ $handler = \&_sendEmailBySendmail;
+ }
+
+ $this->setMailHandler( $handler ) if $handler;
+}
+
+=pod
+
+---++ setMailHandler( \&fn )
+
+ * =\&fn= - reference to a function($) (see _sendEmailBySendmail for proto)
+Install a handler function to take over mail sending from the default
+SMTP or sendmail methods. This is provided mainly for tests that
+need to be told when a mail is sent, without actually sending it. It
+may also be useful in the event that someone needs to plug in an
+alternative mail handling method.
+
+=cut
+
+sub setMailHandler {
+ my( $this, $fnref ) = @_;
+ $this->{mailHandler} = $fnref;
+}
+
+=pod
+
+---++ ObjectMethod sendEmail ( $text, $retries ) -> $error
+
+ * =$text= - text of the mail, including MIME headers
+ * =$retries= - number of times to retry the send (default 1)
+
+Send an email specified as MIME format content.
+Date: ...\nFrom: ...\nTo: ...\nCC: ...\nSubject: ...\n\nMailBody...
+
+=cut
+
+sub sendEmail {
+ my( $this, $text, $retries ) = @_;
+ $retries ||= 1;
+
+ unless( $TWiki::cfg{EnableEmail} ) {
+ return 'Trying to send email while email functionality is disabled';
+ }
+
+ unless( defined $this->{mailHandler} ) {
+ _installMailHandler( $this );
+ }
+
+ return 'No mail handler available' unless $this->{mailHandler};
+
+ # Put in a Date header, mainly for Qmail
+ require TWiki::Time;
+ my $dateStr = TWiki::Time::formatTime(time, '$email');
+ $text = "Date: " . $dateStr . "\n" . $text;
+
+ my $errors = '';
+ my $back_off = 1; # seconds, doubles on each retry
+ while ( $retries-- ) {
+ try {
+ &{$this->{mailHandler}}( $this, $text );
+ $retries = 0;
+ } catch Error::Simple with {
+ my $e = shift->stringify();
+ $this->{session}->writeWarning( $e );
+ # be nasty to errors that we didn't throw. They may be
+ # caused by SMTP or perl, and give away info about the
+ # install that we don't want to share.
+ unless( $e =~ /^ERROR/ ) {
+ $e = "Mail could not be sent - see TWiki warning log.";
+ }
+ $errors .= $e."\n";
+ sleep( $back_off );
+ $back_off *= 2;
+ $errors .= "Too many failures sending mail"
+ unless $retries;
+ };
+ }
+ return $errors;
+}
+
+sub _fixLineLength {
+ my( $addrs ) = @_;
+ # split up header lines that are too long
+ $addrs =~ s/(.{60}[^,]*,\s*)/$1\n /go;
+ $addrs =~ s/\n\s*$//gos;
+ return $addrs;
+}
+
+sub _sendEmailBySendmail {
+ my( $this, $text ) = @_;
+
+ # send with sendmail
+ my ( $header, $body ) = split( "\n\n", $text, 2 );
+ $header =~ s/([\n\r])(From|To|CC|BCC)(\:\s*)([^\n\r]*)/$1.$2.$3._fixLineLength($4)/geois;
+ $text = "$header\n\n$body"; # rebuild message
+
+ open( MAIL, '|'.$TWiki::cfg{MailProgram} ) ||
+ die "ERROR: Can't send mail using TWiki::cfg{MailProgram}";
+ print MAIL $text;
+ close( MAIL );
+ die "ERROR: Exit code $? from TWiki::cfg{MailProgram}" if $?;
+}
+
+sub _sendEmailByNetSMTP {
+ my( $this, $text ) = @_;
+
+ my $from = '';
+ my @to = ();
+
+ my ( $header, $body ) = split( "\n\n", $text, 2 );
+ my @headerlines = split( /\r?\n/, $header );
+ $header =~ s/\nBCC\:[^\n]*//os; #remove BCC line from header
+ $header =~ s/([\n\r])(From|To|CC|BCC)(\:\s*)([^\n\r]*)/$1 . $2 . $3 . _fixLineLength( $4 )/geois;
+ $text = "$header\n\n$body"; # rebuild message
+
+ # extract 'From:'
+ my @arr = grep( /^From: /i, @headerlines );
+ if( scalar( @arr ) ) {
+ $from = $arr[0];
+ $from =~ s/^From:\s*//io;
+ $from =~ s/.*<(.*?)>.*/$1/o; # extract "user@host" out of "Name "
+ }
+ unless( $from ) {
+ # SMELL: should be a TWiki::inlineAlert
+ die "ERROR: Can't send mail, missing 'From:'";
+ }
+
+ # extract @to from 'To:', 'CC:', 'BCC:'
+ @arr = grep( /^To: /i, @headerlines );
+ my $tmp = '';
+ if( scalar( @arr ) ) {
+ $tmp = $arr[0];
+ $tmp =~ s/^To:\s*//io;
+ @arr = split( /,\s*/, $tmp );
+ push( @to, @arr );
+ }
+ @arr = grep( /^CC: /i, @headerlines );
+ if( scalar( @arr ) ) {
+ $tmp = $arr[0];
+ $tmp =~ s/^CC:\s*//io;
+ @arr = split( /,\s*/, $tmp );
+ push( @to, @arr );
+ }
+ @arr = grep( /^BCC: /i, @headerlines );
+ if( scalar( @arr ) ) {
+ $tmp = $arr[0];
+ $tmp =~ s/^BCC:\s*//io;
+ @arr = split( /,\s*/, $tmp );
+ push( @to, @arr );
+ }
+ if( ! ( scalar( @to ) ) ) {
+ # SMELL: should be a TWiki::inlineAlert
+ die "ERROR: Can't send mail, missing recipient";
+ }
+
+ return undef unless( scalar @to );
+
+ # Change SMTP protocol recipient format from
+ # "User Name " to "userid@domain"
+ # for those SMTP hosts that need it just that way.
+ foreach (@to) {
+ s/^.*<(.*)>$/$1/;
+ }
+
+ my $smtp = 0;
+ if( $this->{HELLO_HOST} ) {
+ $smtp = Net::SMTP->new( $this->{MAIL_HOST},
+ Hello => $this->{HELLO_HOST},
+ Debug => $TWiki::cfg{SMTP}{Debug} || 0 );
+ } else {
+ $smtp = Net::SMTP->new( $this->{MAIL_HOST},
+ Debug => $TWiki::cfg{SMTP}{Debug} || 0 );
+ }
+ my $status = '';
+ my $mess = "ERROR: Can't send mail using Net::SMTP. ";
+ die $mess."Can't connect to '$this->{MAIL_HOST}'" unless $smtp;
+
+ if( $TWiki::cfg{SMTP}{Username} ) {
+ $smtp->auth($TWiki::cfg{SMTP}{Username}, $TWiki::cfg{SMTP}{Password});
+ }
+ $smtp->mail( $from ) || die $mess.$smtp->message;
+ $smtp->to( @to, { SkipBad => 1 } ) || die $mess.$smtp->message;
+ $smtp->data( $text ) || die $mess.$smtp->message;
+ $smtp->dataend() || die $mess.$smtp->message;
+ $smtp->quit();
+}
+
+1;
+__DATA__
+
+Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
+
+Copyright (C) 2001-2007 Peter Thoeny, peter@thoeny.org
+and TWiki Contributors. All Rights Reserved. TWiki Contributors
+are listed in the AUTHORS file in the root of this distribution.
+NOTE: Please extend that file, not this notice.
+
+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 2
+of the License, or (at your option) any later version. For
+more details read LICENSE in the root of this distribution.
+
+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.
+
+As per the GPL, removal of this notice is prohibited.