#!/usr/bin/perl # # Blosxom XML-RPC Server # Author: Allen Hutchison # Version: $Id: blosxom-xmlrpc.cgi,v 1.4 2004/09/11 02:00:17 allen Exp $ # Home/Docs/Licensing: http://www.hutchison.org/allen/source/bxr/ # BXR Mailing List: http://groups.yahoo.com/groups/bxr/ # # TODO: # 1. Web Interface # 1.1 DONE: Delete # 1.2 DONE: Add # 1.3 DONE: List # 1.4 DONE: Auth # 1.5 DONE: Welcome # 1.4 Plugins for other editing interfaces # 2. ATOM # 3. Multi-Author # 4. Unicode title support # 5. Bugs # 5.1 FIXED: Edit doesn't maintain postid with metaweblog # 5.2 Extra line endings inserted during edit # 5.3 Conditional Require for modules # 5.4 FIXED: Lot's of people don't have use warnings # 5.5 Can't change categories from edit commands (xml-rpc or web) # 5.6 FIXED: metaWebLog.newMediaObject should return a struct with a url entry instead of a string. # 5.7 FIXED: See if use vars is better than our for people with older versions of perl package BXR; # --- Configurable Variables ---- # What's this blog's title? $blogName = "BLOG NAME"; # Where are this blog's entries kept? $datadir = "/path/to/blosxom/data/dir/"; # What's my preferred base URL for this blog? $url = "http://www/path/to/blosxom/"; # Where do you want to store media objects? $mediaObjectDir = "/path/to/where/you/store/media/objects/"; # What is the URL for mediaObjectDir? $mediaObjectUrl = "http://www/path/to/media/objects//"; # What file extension signifies a blosxom entry? $file_extension = "blos"; # What username will you use with your client? $username = "USERNAME"; # What password will you use with your client? $password = "PASSWORD"; # What is your first name? $firstName = "FIRSTNAME"; # What is your last name? $lastName = "LASTNAME"; # What is your email address? (single quotes are important) $email = 'you@domain.org'; # What is your nickname? (required for blogger api) $nickname = "nick"; # Debug Level (0 for no debugging 4 for max) $debugLevel = 0; #---------------------------------------------------- use vars qw($datadir $mediaObjectDir $mediaObjectUrl $url $file_extension $username $password $blogName $firstName $lastName $email $nickname $debugLevel); use strict; use File::Find; use File::stat; use POSIX qw(strftime); use XMLRPC::Transport::HTTP; use CGI::Carp qw(fatalsToBrowser); use CGI; # Remove trailing / from url and datadir $datadir =~ s!/$!!; $url =~ s!/$!!; $mediaObjectDir =~ s!/$!!; $mediaObjectUrl =~ s!/$!!; sub auth { debug( 4, "BXR.auth", @_ ); my $user = shift @_; my $pass = shift @_; unless ( ( $user eq $username ) and ( $pass eq $password ) ) { die SOAP::Fault->faultcode('BXR.authError') ->faultstring('Incorrect Username or Password'); } } sub getPostIdFromFilename { debug( 4, "BXR.getPostIdFromFilename", @_ ); my $filename = shift @_; $filename =~ s!$datadir/!!; return $filename; } sub getFilenameFromPostId { debug( 4, "BXR.getFilenameFromPostId", @_ ); my $postid = shift @_; unless ( $postid =~ /$datadir/ ) { $postid = "$datadir/$postid"; } return $postid; } sub getCategoryFromFilename { debug( 4, "BXR.getCategoryFromFilename", @_ ); my $filename = shift @_; if ( $filename =~ /$file_extension$/ ) { $filename =~ s!$datadir/(.*)/.*$file_extension$!$1!; } else { $filename =~ s!$datadir/!!; } return $filename; } sub getIsoTime { debug( 4, "BXR.getIsoTime", @_ ); my $time = shift @_ || time; my $isoTime = strftime( "%Y-%m-%dT%T", localtime($time) ); return $isoTime; } # Debug Levels: # 1 = API Module Calls # 2 = Blosxom Module Calls # 3 = Information inside a method # 4 = BXR Module Calls sub debug { my $messDebugLevel = shift @_; if ( $messDebugLevel <= $debugLevel ) { my $method = shift @_; foreach (@_) { print STDERR "$method: $_\n"; } } } my $path_info = $ENV{'PATH_INFO'} || ''; if ( $path_info =~ /rpc2/i ) { XMLRPC::Transport::HTTP::CGI->dispatch_to( 'metaWeblog', 'blogger' ) ->handle; } elsif ( $path_info =~ /atom/i ) { # Do ATOM stuff here } else { my $c = BXR::Web->handle($path_info); } ######################################################## package BXR::Web; ######################################################## sub handle { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $p = shift @_; my $c = CGI->new(); if ( auth($c) ) { my @path = split m{/}, $p; shift @path; # Get's rid of leading / my $command = shift @path; if ( $command =~ m{new}i ) { newPost( cgi => $c ); } elsif ( $command =~ m{edit}i ) { editPost( cgi => $c, path => \@path ); } elsif ( $command =~ m{recent}i ) { getRecentPosts( cgi => $c, path => \@path ); } elsif ( $command =~ m{get}i ) { getPost( cgi => $c, path => \@path ); } elsif ( $command =~ m{delete}i ) { deletePost( cgi => $c, path => \@path ); } else { welcome( cgi => $c, path => \@path ); } } } sub auth { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $c = shift @_; my %cookie = $c->cookie('bxr'); defined( $c->param('username') ) ? $cookie{'username'} = $c->param('username') : 0; defined( $c->param('password') ) ? $cookie{'password'} = $c->param('password') : 0; if ( ( $cookie{'username'} ne $BXR::username ) or ( $cookie{'password'} ne $BXR::password ) ) { print( $c->header(), $c->start_html("Please Enter Password"), $c->start_form(), "Username: ", $c->textfield( -name => 'username' ), $c->br(), "Password: ", $c->password_field( -name => 'password' ), $c->br(), $c->submit( -label => "Authorize" ), $c->end_form, $c->end_html ); return 0; } else { my $cookie = $c->cookie( -name => 'bxr', -value => \%cookie, -expires => '+1h' ); print $c->header( -cookie => $cookie ); return 1; } } sub welcome { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; my $url = $c->url(); print( $c->start_html("Welcome to BXR for $BXR::blogName"), $c->h1("Welcome to BXR for $BXR::blogName"), $c->ul( $c->li( [ $c->a( { href => "$url/recent/10" }, "10 Most Recent Posts" ), $c->a( { href => "$url/new" }, "Create A New Post" ) ] ) ), $c->end_html ); } sub newPost { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; if ( defined( $c->param() ) ) { #process new post my %struct = ( title => $c->param('title'), categories => [ $c->param('category') ], description => $c->param('entry') ); my $rc = blosxom::newPost( \%struct ); getPost( cgi => $c, fileName => $rc ); } else { print( $c->start_html("New Post"), $c->start_form, "Title: ", $c->br(), $c->textfield( -name => 'title', -size => 80 ), $c->br(), "Category: ", $c->br(), getCategories( cgi => $c, path => $p, retFlag => 1 ), $c->br(), "Entry: ", $c->br(), $c->textarea( -name => 'entry', -rows => 20, -columns => 80 ), $c->p(), $c->submit( -label => "Post To Blog" ), $c->end_form, $c->end_html ); } } sub editPost { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; if ( defined( $c->param() ) ) { my %struct = ( title => $c->param('title'), postid => $c->param('postid'), categories => [ $c->param('category') ], description => $c->param('entry') ); my $rc = blosxom::editPost( \%struct ); getPost( cgi => $c, fileName => $rc ); } else { my $post = getPost( cgi => $c, path => $p, retFlag => 1 ); $post->{'postid'} = BXR::getPostIdFromFilename( $post->{'postid'} ); print( $c->start_html("Edit Post: $post->{'title'}"), $c->start_form, "Title: ", $c->br(), $c->textfield( -name => 'title', -size => 80, -default => $post->{'title'} ), $c->br(), "Category: ", $c->br(), getCategories( cgi => $c, path => $p, retFlag => 1, defCat => $post->{'categories'}[0] ), $c->br(), "Entry: ", $c->br(), $c->textarea( -name => 'entry', -rows => 20, -columns => 80, -default => $post->{'description'} ), $c->p(), $c->hidden( -name => 'postid', -value => $post->{'postid'} ), $c->submit( -label => "Save Changes" ), $c->reset( -label => "Discard Changes" ), $c->end_form, $c->end_html ); } } sub getPost { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; my $retFlag = $params{'retFlag'} || 0; my $fileName = $params{'fileName'}; unless ( defined($fileName) ) { $fileName = BXR::getFilenameFromPostId( join '/', @{$p} ); } my $post = blosxom::getPost($fileName); if ( $retFlag == 1 ) { # We've been called by someone else return $post; } else { # Print the post to the screen print( $c->start_html( $post->{'title'} ), $c->h2( $post->{'title'} ), $c->b("Category: $post->{'categories'}[0]"), $c->br(), $c->hr(), $post->{'description'}, $c->br(), $c->hr(), $c->end_html ); } } sub deletePost { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; if ( defined( $c->param() ) ) { # Actual Deletion Time my $rc = blosxom::deletePost( BXR::getFilenameFromPostId( $c->param('postid') ) ); print $c->start_html; $rc == 1 ? print "Post Was Deleted" : print "Post Was Not Deleted"; print $c->end_html; } else { my $post = getPost( cgi => $c, path => $p, retFlag => 1 ); print( $c->start_html("Delete Post: $post->{'title'}"), $c->start_form ); #TODO -- Total Copout getPost( cgi => $c, path => $p ); $post->{'postid'} = BXR::getPostIdFromFilename( $post->{'postid'} ); print( $c->hidden( -name => 'postid', -value => $post->{'postid'} ), $c->submit( -label => 'Yes, Delete This Post!' ), $c->end_form, $c->end_html ); } } sub getRecentPosts { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; my $postsWanted = shift @{$p}; my $posts = blosxom::getRecentPosts($postsWanted); my @table; foreach ( @{$posts} ) { my $post = getPost( cgi => $c, retFlag => 1, fileName => $_ ); my $url = $c->url(); my $postid = BXR::getPostIdFromFilename( $post->{'postid'} ); push( @table, $c->td( [ $c->a( { href => "$url/get/$postid" }, $post->{'title'} ), $c->a( { href => "$url/edit/$postid" }, 'edit' ), $c->a( { href => "$url/delete/$postid" }, 'delete' ) ] ) ); } print( $c->start_html("$postsWanted Most Recent Posts"), $c->table( { -border => undef, -width => '100%' }, $c->caption( $c->b("$postsWanted Most Recent Posts") ), $c->Tr( \@table ) ), $c->end_html ); } sub getCategories { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $p = $params{'path'}; my $retFlag = $params{'retFlag'} || 0; my $defCat = $params{'defCat'}; my $cats = blosxom::getCategories(); if ( $retFlag == 1 ) { my @retCats = sort( map( BXR::getCategoryFromFilename($_), ( keys %{$cats} ) ) ); my $popup = $c->popup_menu( -name => 'category', -Values => \@retCats, -default => $defCat ); return $popup; } else { # Allow for creation and editing of categories } } ######################################################## package blosxom; ######################################################## sub getCategories { BXR::debug( 2, "blosxom.getCategories", @_ ); my $start = shift @_ || $BXR::datadir; my $extension = shift @_ || $BXR::file_extension; my %cats; File::Find::find( sub { $File::Find::name =~ /$extension$/ ? $cats{$File::Find::dir}++ : 0; }, $start ); return ( \%cats ); } sub getRecentPosts { BXR::debug( 2, "blosxom.getRecentPosts", @_ ); my $postsWanted = shift @_; my $start = shift @_ || $BXR::datadir; my $extension = shift @_ || $BXR::file_extension; my %posts; File::Find::find( sub { $File::Find::name =~ /$extension$/ ? $posts{$File::Find::name} = File::stat::stat($File::Find::name)->mtime : 0; }, $start ); my @postList = sort { $posts{$b} <=> $posts{$a} } keys %posts; @postList = @postList[ 0 .. ( $postsWanted - 1 ) ]; return ( \@postList ); } sub getPost { BXR::debug( 2, "blosxom.getPost", @_ ); my $filename = shift @_; my $start = shift @_ || $BXR::datadir; my $extension = shift @_ || $BXR::file_extension; if ( -e $filename ) { open POST, "$filename"; my @post = ; close POST; my %struct; $struct{'postid'} = $filename; $struct{'dateCreated'} = File::stat::stat($filename)->mtime; $struct{'title'} = shift @post; foreach (@post) { $struct{'description'} .= $_; } my @cats; $filename =~ s!$start/(.*)/.*$extension$!$1!; push @cats, $filename; $struct{'categories'} = \@cats; return \%struct; } else { return 0; } } sub newPost { BXR::debug( 2, "blosxom.newPost", @_ ); my $struct = shift @_; my $start = shift @_ || $BXR::datadir; my $extension = shift @_ || $BXR::file_extension; my $filename; if ( defined( $struct->{'postid'} ) ) { $filename = BXR::getFilenameFromPostId( $struct->{'postid'} ); } else { $filename = lc( $struct->{'title'} ); $filename =~ s/\W+/_/g; $filename =~ s/_+$//; if ( $struct->{'categories'} ) { $filename = "$start/$struct->{'categories'}[0]/$filename.$extension"; } else { $filename = "$start/$filename.$extension"; } } unless ( -e $filename ) { open POST, ">$filename" or die "Can't Open File $filename: $!"; print POST "$struct->{'title'}\n\n"; print POST "$struct->{'description'}\n"; close POST; } return $filename; } sub editPost { BXR::debug( 2, "blosxom.editPost", @_ ); my $struct = shift @_; my $start = shift @_ || $BXR::datadir; my $extension = shift @_ || $BXR::file_extension; my $filename = BXR::getFilenameFromPostId( $struct->{'postid'} ); if ( -e $filename ) { unlink $filename; } return ( newPost($struct) ); } sub deletePost { BXR::debug( 2, "blosxom.deletePost", @_ ); my $filename = shift @_; if ( -e $filename ) { unlink $filename; return 1; } else { return 0; } } sub newMediaObject { BXR::debug( 2, "blosxom.newMediaObject", @_ ); my $struct = shift @_; my $start = shift @_ || $BXR::mediaObjectDir; my $url = shift @_ || $BXR::mediaObjectUrl; my $filename = $struct->{'name'}; $filename =~ s/\s+/_/g; $filename =~ s/_+$//; $url = "$url/$filename"; $filename = "$start/$filename"; open FILE, ">$filename"; print FILE "$struct->{'bits'}"; close FILE; return $url; } ######################################################## package metaWeblog; ######################################################## sub getPost { BXR::debug( 1, "metaWeblog.getPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $postid = shift; my $username = shift; my $password = shift; BXR::auth( $username, $password ); $postid = BXR::getFilenameFromPostId($postid); my $post = blosxom::getPost($postid); my $date = BXR::getIsoTime( $post->{'dateCreated'} ); $post->{'dateCreated'} = SOAP::Data->type( dateTime => "$date" ); $post->{'postid'} = BXR::getPostIdFromFilename( $post->{'postid'} ); ( $post->{'link'} = "$BXR::url/$post->{'postid'}" ) =~ s/$BXR::file_extension/html/; $post->{'permaLink'} = $post->{'link'}; return $post; } sub getRecentPosts { BXR::debug( 1, "metaWeblog.getRecentPosts", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $blogid = shift; my $username = shift; my $password = shift; my $numberOfPosts = shift; BXR::auth( $username, $password ); my $postIds = blosxom::getRecentPosts($numberOfPosts); my @retList; foreach ( @{$postIds} ) { my $post = getPost( $_, $username, $password ); push @retList, $post; } return \@retList; } sub getCategories { BXR::debug( 1, "metaWeblog.getCategories", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $blogid = shift; my $username = shift; my $password = shift; BXR::auth( $username, $password ); my $cats = blosxom::getCategories(); my %retList; foreach ( keys %{$cats} ) { $_ = BXR::getCategoryFromFilename($_); $retList{'struct'}{$_}{'description'} = "No Description"; $retList{'struct'}{$_}{'rssUrl'} = "$BXR::url/$_/index.rss"; $retList{'struct'}{$_}{'htmlUrl'} = "$BXR::url/$_"; } return %retList; } sub newPost { BXR::debug( 1, "metaWeblog.newPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $blogid = shift @_; my $username = shift @_; my $password = shift @_; my $struct = shift @_; my $publish = shift @_; BXR::auth( $username, $password ); my $postid = blosxom::newPost($struct); $postid =~ s!$BXR::datadir/!!; return $postid; } sub editPost { BXR::debug( 1, "metaWeblog.editPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $postid = shift @_; my $username = shift @_; my $password = shift @_; my $struct = shift @_; my $publish = shift @_; BXR::auth( $username, $password ); $struct->{'postid'} = $postid; my $rc = BXR::getPostIdFromFilename( blosxom::editPost($struct) ); return $rc; } sub newMediaObject { BXR::debug( 1, "metaWeblog.editPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $blogid = shift @_; my $username = shift @_; my $password = shift @_; my $struct = shift @_; BXR::auth( $username, $password ); my %retStruct; $retStruct{'url'} = blosxom::newMediaObject($struct); return \%retStruct; } ######################################################## package blogger; ######################################################## sub getUsersBlogs { BXR::debug( 1, "blogger.getUsersBlogs", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $username = shift @_; my $password = shift @_; BXR::auth( $username, $password ); my %struct; $struct{'url'} = $BXR::url; $struct{'blogid'} = $BXR::blogName; $struct{'blogName'} = $BXR::blogName; my @return; push @return, \%struct; return \@return; } sub getUserInfo { BXR::debug( 1, "blogger.getUserInfo", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $username = shift @_; my $password = shift @_; BXR::auth( $username, $password ); my %struct = ( userid => 1, firstname => $BXR::firstName, lastname => $BXR::lastName, nickname => $BXR::nickname, email => $BXR::email, url => $BXR::url ); return \%struct; } sub getRecentPosts { BXR::debug( 1, "blogger.getRecentPosts", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $blogid = shift @_; my $username = shift @_; my $password = shift @_; my $numberOfPosts = shift @_; BXR::auth( $username, $password ); my $postIds = blosxom::getRecentPosts($numberOfPosts); my @retList; foreach ( @{$postIds} ) { my $post = getPost( $appkey, $_, $username, $password ); push @retList, $post; } return \@retList; } sub getPost { BXR::debug( 1, "blogger.getPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $postid = shift @_; my $username = shift @_; my $password = shift @_; BXR::auth( $username, $password ); $postid = BXR::getFilenameFromPostId($postid); my $post = blosxom::getPost($postid); my $date = BXR::getIsoTime( $post->{'dateCreated'} ); $post->{'dateCreated'} = SOAP::Data->type( dateTime => "$date" ); $post->{'postid'} = BXR::getPostIdFromFilename( $post->{'postid'} ); $post->{'content'} = $post->{'description'}; delete $post->{'description'}; $post->{'userId'} = 1; return $post; } sub editPost { BXR::debug( 1, "blogger.editPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $postid = shift @_; my $username = shift @_; my $password = shift @_; my $content = shift @_; my $publish = shift @_; BXR::auth( $username, $password ); my $filename = BXR::getFilenameFromPostId($postid); my $post = blosxom::getPost($filename); $post->{'description'} = $content; my $rc = blosxom::editPost($post); return $rc; } sub newPost { BXR::debug( 1, "blogger.newPost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $blogid = shift @_; my $username = shift @_; my $password = shift @_; my $content = shift @_; my $publish = shift @_; BXR::auth( $username, $password ); my $title = $content; $title =~ s/((\S+\s+){4}\S+).*/$1/; my %struct; $struct{'description'} = $content; $struct{'title'} = $title; my $filename = blosxom::newPost( \%struct ); my $postid = BXR::getPostIdFromFilename($filename); return $postid; } sub deletePost { BXR::debug( 1, "blogger.deletePost", @_ ); shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $appkey = shift @_; my $postid = shift @_; my $username = shift @_; my $password = shift @_; BXR::auth( $username, $password ); my $filename = BXR::getFilenameFromPostId($postid); my $rc = blosxom::deletePost($filename); } =head1 NAME BXR: The Blosxom XML-RPC Interface =head1 SYNOPSIS Provides an XML-RPC interface to the Blosxom blogging system for the metaWeblog and blogger APIs. Once it's installed you will point your blogging client at the following URL as the XML-RPC access point: http://your/blosxom/path/blosxom-xmlrpc.cgi =head1 VERSION $Id: blosxom-xmlrpc.cgi,v 1.4 2004/09/11 02:00:17 allen Exp $ =head1 AUTHOR Allen Hutchison , http://www.hutchison.org/allen/ =head1 BUGS Currently there is a problem with the Ecto weblog editor. Otherwise things seem to be working. Address bug reports and comments to me or to the BXR mailing list [http://www.yahoogroups.com/groups/bxr/]. =head1 INSTALLATION =over 4 =item 1. Install the SOAP::Lite module from CPAN. =item 2. Download the script and save it in the same directory as blosxom.cgi. Make sure it's executable by the www server just like blosxom.cgi (chmod a+x blosxom-xmlrpc.cgi) =item 3. Fill out the variables at the top to match your blosxom.cgi varialbes. Make sure to set a username and password, they can be anything you like and you will be using them with your blog editor. =item 4. Use the URL of the script as the access-point for your client. =back =head1 LICENSE BXR (Blosxom XML-RPC) Copyright 2004, Allen Hutchison (This license is the same as Blosxom's) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =cut