#!/usr/bin/perl package LJ::FBUpload; use strict; require "$ENV{LJHOME}/cgi-bin/ljconfig.pl"; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; use MIME::Words (); use XML::Simple; use IO::Handle; use LWP::UserAgent; use URI::Escape; use Digest::MD5 (); use File::Basename (); *hash = \&Digest::MD5::md5_hex; # This has bitten us one too many times. # Don't let startup continue unless LWP is ok. die "* Installed version of LWP is too old! *" if LWP->VERSION < 5.803; sub make_auth { my ($chal, $password) = @_; return unless $chal && $password; return "crp:$chal:" . hash($chal . hash($password)); } sub get_challenge { my ($u, $ua, $err) = @_; return unless $u && $ua; my $req = HTTP::Request->new(GET => "$LJ::FB_SITEROOT/interface/simple"); $req->push_header("X-FB-Mode" => "GetChallenge"); $req->push_header("X-FB-User" => $u->{'user'}); my $res = $$ua->request($req); if ($res->is_success()) { my $xmlres = XML::Simple::XMLin($res->content); my $methres = $xmlres->{GetChallengeResponse}; return $methres->{Challenge}; } else { $$err = $res->content(); return; } } # returns FB protocol data structure, regardless of FB # success or failure. it's the callers responsibility # to check the structure for FB return values. # # on http failure, returns numeric http error code, # and sets $rv reference with errorstring. # # returns undef on unrecoverable failure. # # opts: { path => path to image on disk, # or title to use if 'rawdata' isn't on disk. # rawdata => optional image data scalar ref # imgsec => bitmask for image security # caption => optional image description # galname => gallery to upload image to } sub do_upload { my ($u, $rv, $opts) = @_; unless ($u && $opts->{'path'}) { $$rv = "Invalid parameters to do_upload()"; return; } my $ua = LWP::UserAgent->new; $ua->agent("LiveJournal_FBUpload/0.2"); my $err; my $chal = get_challenge($u, \$ua, \$err); unless ($chal) { $$rv = "Error getting challenge from FB server: $err"; return; } my $rawdata = $opts->{'rawdata'}; unless ($rawdata) { # no rawdata was passed, so slurp it in ourselves unless (open (F, $opts->{'path'})) { $$rv = "Couldn't read image file: $!\n"; return; } binmode(F); my $data; { local $/ = undef; $data = ; } $rawdata = \$data; close F; } # convert strings to security masks/ # default to private on unknown strings. # lack of an imgsec opt means public. $opts->{imgsec} ||= 255; unless ($opts->{imgsec} =~ /^\d+$/) { my %groupmap = ( private => 0, regusers => 253, friends => 254, public => 255 ); $opts->{imgsec} = 'private' unless $groupmap{ $opts->{imgsec} }; $opts->{imgsec} = $groupmap{ $opts->{imgsec} }; } my $basename = File::Basename::basename($opts->{'path'}); my $length = length $$rawdata; my $req = HTTP::Request->new(PUT => "$LJ::FB_SITEROOT/interface/simple"); my %headers = ( 'X-FB-Mode' => 'UploadPic', 'X-FB-UploadPic.ImageLength' => $length, 'Content-Length' => $length, 'X-FB-UploadPic.Meta.Filename' => $basename, 'X-FB-UploadPic.MD5' => hash($$rawdata), 'X-FB-User' => $u->{'user'}, 'X-FB-Auth' => make_auth( $chal, $u->{'password'} ), ':X-FB-UploadPic.Gallery._size'=> 1, 'X-FB-UploadPic.PicSec' => $opts->{'imgsec'}, 'X-FB-UploadPic.Gallery.0.GalName' => $opts->{'galname'} || 'LJ_emailpost', 'X-FB-UploadPic.Gallery.0.GalSec' => 255 ); $headers{'X-FB-UploadPic.Meta.Description'} = $opts->{caption} if $opts->{caption}; $req->push_header($_, $headers{$_}) foreach keys %headers; $req->content($$rawdata); my $res = $ua->request($req); my $res_code = $1 if $res->status_line =~ /^(\d+)/; unless ($res->is_success) { $$rv = "HTTP error uploading pict: " . $res->content(); return $res_code; } my $xmlres; eval { $xmlres = XML::Simple::XMLin($res->content); }; if ($@) { $$rv = "Error parsing XML: $@"; return; } my $methres = $xmlres->{UploadPicResponse}; $methres->{Title} = $basename; return $methres; } # args: # $u, # arrayref of { title, url, width, height, caption } # optional opts overrides hashref. # (if not supplied, userprops are used.) # returns: html string suitable for entry post body # TODO: Hook this like the Fotobilder "post to journal" # caption posting page. More pretty. (layout keywords?) sub make_html { my ($u, $images, $opts) = @_; my ($icount, $html); $icount = scalar @$images; return "" unless $icount; # Merge overrides with userprops that might # have been passed in. $opts = {} unless $opts && ref $opts; my @props = qw/ emailpost_imgsize emailpost_imglayout emailpost_imgcut /; LJ::load_user_props( $u, @props ); foreach (@props) { my $prop = $_; $prop =~ s/emailpost_//; $opts->{$prop} = lc($opts->{$prop}) || $u->{$_}; } $opts->{imgcut} ||= 'totals'; $html .= "\n"; # set journal image display size my @valid_sizes = qw/ 100x100 320x240 640x480 /; $opts->{imgsize} = '320x240' unless grep { $opts->{imgsize} eq $_; } @valid_sizes; my ($width, $height) = split 'x', $opts->{imgsize}; my $size = '/s' . $opts->{imgsize}; # force lj-cut on images larger than 320 in either direction $opts->{imgcut} = 'totals' if $width > 320 || $height > 320; # insert image links into post body my $horiz = $opts->{imglayout} =~ /^horiz/i; $html .= "" if $opts->{imgcut} eq 'totals'; $html .= "" if $horiz; foreach my $i (@$images) { my $title = LJ::ehtml($i->{'title'}); # don't set a size on images smaller than the requested width/height # (we never scale larger, just smaller) undef $size if $i->{width} <= $width || $i->{height} <= $height; $html .= "' : '
'; $html .= " " if $opts->{imgcut} eq 'titles'; } $html .= "
" if $horiz; $html .= "" if $opts->{imgcut} eq 'titles'; $html .= "{url}/\">"; $html .= "{url}$size\" alt=\"$title\" border=\"0\">
"; $html .= "$i->{caption}
" if $i->{caption}; $html .= $horiz ? '
" if $horiz; $html .= "
\n" if $opts->{imgcut} eq 'totals'; return $html; } 1;