#!/usr/bin/perl # # Blosxom XML-RPC Server # Author: Allen Hutchison # Version: $Id: blosxom-xmlrpc.cgi,v 1.7 2004/12/05 05:17:45 allen_hutchison Exp $ # Home/Docs/Licensing: http://www.hutchison.org/allen/source/bxr/ # BXR Mailing List: http://groups.yahoo.com/groups/bxr/ # 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/"; # Do you have a BXR style sheet? Put the URL here $stylesheet = "http://www/path/to/bxr/stylesheet/"; # What file extension signifies a blosxom entry? $file_extension = "blos"; # What permission should be set when a file is written? $file_permission = 0664; # 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($blogName $datadir $url $mediaObjectDir $mediaObjectUrl $stylesheet $file_extension $file_permission $username $password $firstName $lastName $email $nickname $debugLevel); # If you put all the config in # a file called bxrrc, then you can # leave the config above blank # The config file must be valid perl if ( -e ".bxrrc") { do ".bxrrc"; } 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/) { if ($postid =~ m!^/!) { #Post ID now has a preceeding / $postid = $datadir . $postid; } else { $postid = "$datadir/$postid"; } } debug(4, "BXR.getFilenameFromPostId", $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!!; } debug(4, "BXR.getCategoryFromFilename", $filename); return $filename; } sub getIsoTime { debug(4, "BXR.getIsoTime", @_); my $time = shift @_ || time; my $isoTime = strftime("%Y-%m-%dT%TZ", gmtime($time)); debug(4, "BXR.getIsoTime", $isoTime); return $isoTime; } sub getRandomFilename { debug(4, "BXR.getRandomFilename", @_); my $time = time; # Add a random number $time .= int(rand(10)); # Add another random number $time .= int(rand(10)); return $time; } # 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 @_; open FILE, ">>$datadir/bxrlog.txt" or print STDERR "Can't open error log\n"; foreach (@_) { print FILE "$method: $_\n"; } close FILE; } } 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(), startHtml( cgi => $c, title => "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, endHtml(cgi => $c)); 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(startHtml(cgi => $c, title => "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")])), endHtml(cgi => $c)); } 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(startHtml(cgi => $c, title => "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, endHtml(cgi => $c)); } } 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(startHtml(cgi => $c, title => "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, endHtml(cgi => $c)); } } 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 ($post == 0) { die "Can't find file $fileName"; } if ($retFlag == 1) { # We've been called by someone else return $post; } else { # Print the post to the screen print(startHtml(cgi => $c, title => $post->{'title'}), $c->h2($post->{'title'}), $c->b("Category: $post->{'categories'}[0]"), $c->br(), $c->hr(), $post->{'description'}, $c->br(), $c->hr(), endHtml(cgi => $c)); } } 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 startHtml(cgi => $c, title => "Delete Post"); $rc == 1 ? print "Post Was Deleted" : print "Post Was Not Deleted"; print endHtml(cgi => $c); } else { my $post = getPost(cgi => $c, path => $p, retFlag => 1); print (startHtml(cgi => $c, title => "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, endHtml(cgi => $c)); } } 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(startHtml(cgi => $c, title => "$postsWanted Most Recent Posts"), $c->table({-border=>undef, -width=>'100%'}, $c->caption($c->b("$postsWanted Most Recent Posts")), $c->Tr(\@table)), endHtml(cgi => $c)); } 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 } } sub startHtml { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; my $title = $params{'title'} || "No Title"; my $stylesheet = $params{'style'} || $BXR::stylesheet; my $ret; if (defined($stylesheet)) { $ret = $c->start_html(-title => $title, -style => {-src => $stylesheet}); } else { $ret = $c->start_html(-title => $title); } # create a navigation bar or something here return $ret; } sub endHtml { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my %params = @_; my $c = $params{'cgi'}; # create any footer text here. my $ret = $c->end_html(); } ######################################################## 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); foreach (keys(%cats)) { while (($_ =~ s{(.*)/.*$}{$1}) and ($_ ne $start)){ defined($cats{$_}) ? 0 : $cats{$_}++; } } $cats{"/"}++; 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; if ($#postList > $postsWanted) { @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! or $filename = "/"; 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/_+$//; $filename = BXR::getRandomFilename() unless $filename =~ m/[a-z]/; if ($struct->{'categories'}) { $filename = "$start/$struct->{'categories'}[0]/$filename.$extension"; } else { $filename = "$start/$filename.$extension"; } } chomp $struct->{'title'}; chomp $struct->{'description'}; unless (-e $filename) { open POST, ">$filename" or die "Can't Open File $filename: $!"; print POST "$struct->{'title'}\n"; print POST "$struct->{'description'}\n"; close POST; my $files = chmod $BXR::file_permission, $filename; } 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; } my $end = $filename; $end =~ s!.*(/.*\.$extension)!$1!; my $newfilename = $start . $struct->{'categories'}[0] . $end; unless ($newfilename eq $filename) { $struct->{'postid'} = BXR::getPostIdFromFilename($newfilename); $filename = $newfilename; } my $editedPost = newPost($struct); if ($editedPost eq $filename) { return 1; } else { return 0; } } 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); if ($post == 0) { die SOAP::Fault->faultcode('BXR.FileNotFoundError')->faultstring("File Not Found, Try Refreshing Your Post List"); } else { 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)); if ($rc == 1) { return "true"; } else { return "false"; } } 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 and Web Editor Interface =head1 SYNOPSIS Provides an XML-RPC interface to the Blosxom blogging system for the metaWeblog and blogger APIs. Also provides a web-based interface to edit your blog when you aren't near a client. 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/RPC2 If you want to use the web-based interface you will point your web browser to: http://your/blosxom/path/blosxom-xmlrpc.cgi =head1 VERSION $Id: blosxom-xmlrpc.cgi,v 1.7 2004/12/05 05:17:45 allen_hutchison Exp $ =head1 AUTHOR Allen Hutchison , http://www.hutchison.org/allen/ =head1 INSTALLATION =over 4 =item 1. Install the SOAP::Lite module from CPAN if you would like to use the XML-RPC portion of the script. =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. For example: http://your/blosxom/path/blosxom-xmlrpc.cgi/RPC2 =back =head1 TODO =over 4 =item 1. Web Interface needs lot's of work =item 2. ATOM =item 3. Multi-Author =back =head1 BUGS =over 4 =item 1 Conditional Require for modules =back Address bug reports and comments to me or to the BXR mailing list [http://www.yahoogroups.com/groups/bxr/]. =head1 THANKS Thanks to everyone on the BXR mailing list. Also thanks to the following people who have sent me patches, suggestions, and ideas: Wesley Griffin, http://www.fraktured.net/ Jim Nicholson, http://www.xmlhead.com/dftfh/ Steve Wills, http://www.stevenwills.com/blog/ Toru Marumoto =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