# MTAmazon 2.22 # Amazon Associates plugin for Movable Type # # Copyright 2002 Kalsey Consulting Group # http://kalsey.com/ # Using this software signifies your acceptance of the license # agreement that accompanies this software. # # Installation and usage instructions can be found at # http://mtamazon.sourceforge.net/ # # Last update: # $Id: MTAmazon.pm,v 1.1 2007/08/29 13:11:45 iay Exp $ package MTPlugins::MTAmazon; use strict; # Set configuration variables my %config; $config{devtoken} = ""; $config{cachedir} = "./cache"; $config{associateid} = "kalsey-20"; my %months = qw(January 01 February 02 March 03 April 04 May 05 June 06 July 07 August 08 September 09 October 10 November 11 December 12); my $debug = 0; my $version = "2.22"; my @arg_related = qw(category title extended excerpt); my @arg_method = qw(Keyword BrowseNode Asin Upc Author Artist Actor Director Manufacturer Seller Exchange ListMania Wishlist Similarity Power); my @arg_line = qw(baby books classical dvd electronics garden kitchen magazines music pc-hardware photo software toys universal vhs videogames); # Sorting doesn't work reliably with Amazon at the moment. #my %arg_booksort = qw(featured +pmrank sales +salesrank userrating +reviewrank price_asc +pricerank price_desc +inverse-princerank date +daterank title_asc +titlerank title_desc -titlerank); #my %arg_softsort = qw(featured +pmrank sales +salesrank price title); #my %arg_outdoorsort = qw(featured sales brand price date title); #my %arg_toolsort = %arg_outdoorsort; #my %arg_camerasort = qw(featured +pmrank sales +salesrank title +titlerank); &readconfig; sub Amazon { my($ctx, $args) = @_; eval("use XML::Simple"); return $ctx->error("XML::Simple is not installed. You need to install it in order to run MTAmazon. See the documentation for instructions on installing the required modules.") if ($@); use LWP::UserAgent; my $url = ""; my $content = ""; my($entry, $search, $products, $details, $content_tree); if (exists $args->{debug}) { $debug = 1; } my $tokens = $ctx->stash('tokens'); my $builder = $ctx->stash('builder'); $args = handle_expressions($ctx, $args); my $format = chooseAmazonFormat($tokens); # Search string. Check for related attribute first. if (exists $args->{related}) { defined($entry = $ctx->stash('entry')) or return $ctx->error("You used and Related outside the context of an entry."); } # SWITCH: check for related searches and for MT tag searches SWITCH: { if ($args->{related}) { grep {/$args->{related}/} @arg_related or return $ctx->error("MTAmazon: " . $args->{related} . "isn't a valid value for the related attribute. Try one of these vaules: " . join(', ', @arg_related)); if ($args->{related} eq 'category') { return '' unless defined(my $cat = $entry->category); $search = $cat->label; last SWITCH; } if (($args->{related} eq 'title')) { return '' unless defined($search = $entry->title); last SWITCH; } if (($args->{related} eq 'extended')) { return '' unless defined($search = $entry->text_more); last SWITCH; } if (($args->{related} eq 'excerpt')) { return '' unless defined($search = $entry->excerpt); last SWITCH; } } if (!exists($args->{related}) || length($args->{related})==0) { $search = $args->{search}; last SWITCH; } return $ctx->error(" couldn't figure out what to use for a search."); } defined($search) or return $ctx->error("You used without a search string."); $search =~ s/\s+/%20/g; # Associate ID and DevToken my $associateid = ($args->{associateid}) ? $args->{associateid} : $config{associateid}; my $devtoken = ($args->{devtoken}) ? $args->{devtoken} : $config{devtoken}; length($devtoken) > 0 or return $ctx->error("You need an Amazon.com developer's token to use MTAmazon. Visit http://www.amazon.com/webservices/ to apply."); $ctx->stash('AmazonAssociateID', $associateid); $ctx->stash('AmazonDevToken', $devtoken); #Search method. Default is Keyword my $method = ($args->{method}) ? $args->{method} : "Keyword"; #Handle version 1.0 search methods $method = "Keyword" if ($method eq "search"); $method = "BrowseNode" if ($method eq "browse"); grep {/$method/} @arg_method or return $ctx->error("$method isn't a valid search method. Use one of these: " . join(', ', @arg_method)); # Product line. Default is books my $line = ($args->{line}) ? $args->{line} : "books"; grep {/$line/} @arg_line or return $ctx->error("$line isn't a valid product line. Use one of these: " . join(', ', @arg_line)); my $lastn = $args->{lastn} ? $args->{lastn} : 10; $lastn =~ m|^\d*$| or return $ctx->error("lastn must be a number."); my $sort = $args->{sort}; if ($sort) { ValidateSort($sort, $line) or return $ctx->error("You can't sort by $sort in $line."); } debug("Searching $method $search in $line"); return '' if $lastn == 0; # figure how many pages we need to retreive: # one page for every 10 or portion thereof my $pages = int($lastn/10 + .9999); for (my $page = 1; $page <= $pages; $page++) { $content = AmazonGetXML($search, $associateid, $method, $line, $devtoken, $page, $format, $sort); # Get the timestamp for the cache or use Now if the cache is new # If there's already a tmestamp tag, use it. Otherwise use the # new timestamp my $cacheTime = (@{$content}[0]) ? @{$content}[0] : time(); $cacheTime = $ctx->stash('AmazonCacheDateTime') ? $ctx->stash('AmazonCacheDateTime') : $cacheTime; $ctx->stash('AmazonCacheDateTime', $cacheTime); eval { $content_tree = XMLin(@{$content}[1]) }; return $ctx->error("Error reading XML content: $@.") if ($@); next unless (!$@ and ref $content_tree and ($content_tree->{Details} or $content_tree->{ListingProductDetails})); $ctx->stash('AmazonXML', XMLout($content_tree)); # Load the products from the current iteration of the XML doc if ($content_tree->{'Details'}) { if ((ref $content_tree->{'Details'}) eq 'HASH') { $details = [ $content_tree->{'Details'} ]; } else { $details = $content_tree->{'Details'}; } } elsif ($content_tree->{'ListingProductDetails'}) { if ((ref $content_tree->{'ListingProductDetails'}) eq 'HASH') { $details = [ $content_tree->{'ListingProductDetails'} ]; } else { $details = $content_tree->{'ListingProductDetails'}; } } # Load the curent iteration's products into the main array push @$products, @$details; } return '' unless defined @$products; my $count; my $prod = ''; for my $i (@$products) { last if ++$count > $lastn; $ctx->stash('AmazonItem', $i); my $out = $builder->build($ctx, $tokens); return $ctx->error( $builder->errstr ) unless defined $out; $prod .= $out; } $prod; } sub AmazonTitle { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{ProductName} || ''; } sub AmazonASIN { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{Asin} || ''; } sub AmazonArtists { my ($ctx, $args) = @_; defined(my $i = $ctx->stash('AmazonItem')) or return ''; defined(my $artists = $i->{Artists}->{Artist}) or return ''; my $out = ''; $args = handle_expressions($ctx, $args); my $glue = ($args->{glue} || ', '); if (ref $artists) { $out = join($glue, @$artists); } else { $out = $artists; } $out; } sub AmazonAuthors { my ($ctx, $args) = @_; defined(my $i = $ctx->stash('AmazonItem')) or return ''; defined(my $authors = $i->{Authors}->{Author}) or return ''; my $out = ''; $args = handle_expressions($ctx, $args); my $glue = ($args->{glue} || ', '); if (ref $authors) { $out = join($glue, @$authors); } else { $out = $authors; } $out; } sub AmazonCatalog { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{Catalog} || ''; } sub AmazonManufacturer { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{Manufacturer} || ''; } sub AmazonUsedPrice { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{UsedPrice} || ''; } sub AmazonMediumImage { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{ImageUrlMedium} || ''; } sub AmazonSmallImage { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{ImageUrlSmall} || ''; } sub AmazonLargeImage { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{ImageUrlLarge} || ''; } sub AmazonSalePrice { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{OurPrice} || ''; } sub AmazonListPrice { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{ListPrice} || $i->{OurPrice}; } sub AmazonReleaseDate { my($ctx, $args) = @_; $args = handle_expressions($ctx, $args); defined(my $i = $ctx->stash('AmazonItem')) or return ''; use MT::Util qw( format_ts ); return '' unless $i->{ReleaseDate}; return $i->{ReleaseDate} unless $args->{format}; my $date = $i->{ReleaseDate}; my (undef, $d, $m, $y) = $date =~ m|((\d+)\s+)?(\w+),\s+(\d+)|; $d ||= '00'; $m = $months{$m}; $date = "$y$m$d"; my $out = format_ts($args->{'format'}, $date . "000000", $ctx->stash('blog')); $out =~ s/\b0{1,2}[,\b]+//g; $out; } sub AmazonLink { my $ctx = shift; defined(my $i = $ctx->stash('AmazonItem')) or return ''; $i->{url}; } sub AmazonCacheDateTime { my($ctx, $args) = @_; $args = handle_expressions($ctx, $args); use MT::Util qw( format_ts ); my @ts = localtime($ctx->stash('AmazonCacheDateTime')); my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]; my $format = $args->{'format'} ? $args->{'format'} : "%B %e, %Y %I:%M %p"; my $out = format_ts($format, $ts, $ctx->stash('blog')); $out; } sub AmazonXML { my $ctx = shift; $ctx->stash('AmazonXML'); } sub AmazonAssociateID { my $ctx = shift; $ctx->stash('AmazonAssociateID'); } sub AmazonDevToken { my $ctx = shift; $ctx->stash('AmazonDevToken'); } sub AmazonField { my ($ctx, $args) = @_; $args = handle_expressions($ctx, $args); my $name = $args->{name}; return '' unless $name; defined(my $i = $ctx->stash('AmazonItem')) or return ''; if ($name =~ m|/|) { my @path = split(/\//, $name); $name = pop @path; for my $node (@path) { $i = $i->{$node}; } } $i->{$name} || ''; } # ---- Utility Functions -------- sub AmazongetRemoteXML { my($search, $associateid, $method, $line, $devtoken, $format, $page, $sort) = @_; # Amazon changed the naming of some searches in 2.0. We don't # want to break existing templates or require users to update # their templates to the new naming convention, so we do some # translations here. $method = 'Similarity' if $method eq 'Similar'; # Amazon uses the suffix "Search" on some parameters, "Request" # on others, and "Profile" on one so we need to determine which # suffix to use. Beware, though, the Amazon documentation is # incorrect. Some items are documented to use a Request sufix # but actually use a Search suffix. if (grep /$method/, qw(Keyword BrowseNode Asin Upc Author Artist Actor Director Manufacturer Seller Exchange ListMania Wishlist Similarity)) { $method .= "Search"; } elsif (grep /$method/, qw(Power)) { $method .= "Request"; } my $url = "http://xml-eu.amazon.com/onca/xml3?t=$associateid&" . "dev-t=$devtoken&" . "$method=$search&" . "mode=$line-uk&type=$format&page=$page&f=xml&locale=uk"; $url .= "&sort=$sort" if $sort; $url .= "&offerstatus=open" if ($method eq 'SellerSearch'); debug("Getting $url"); my $ua = new LWP::UserAgent; $ua->agent("MTAmazon/$version"); my $http_request = new HTTP::Request('GET', $url); my $http_response = $ua->request($http_request); my $content = $http_response->{'_content'}; $content =~ s/<[^\/]([^>]+)>\s+<\/[^>]+>/<$1 \/>/g; # convert nodes that contain only spaces to empty nodes $content; } sub AmazonSaveFile { my($content, $file) = @_; if ($config{cachedir}) { debug("Saving cache to $file"); open (CACHE, ">$file") or warn "Can't write to $file: $!\n"; print CACHE "$content"; close CACHE; } } sub AmazonGetXML { my($search, $associateid, $method, $line, $devtoken, $page, $format, $sort) = @_; my @content; my $file = "$associateid.$format.$method.$search.$line"; $file =~ s/(\W|%20)//g; $file = $config{cachedir}."/amazon." . $file . ".$page.xml"; open (CACHE, "<$file"); my @stat = stat($file) unless eof(CACHE); close CACHE; if (@stat) { # Cache exists debug("Cache Exists: $file"); my $CacheTTL = time() - $stat[9]; if ($CacheTTL > 3600) { # Cache is stale debug("The cache is stale"); $content[1] = AmazongetRemoteXML($search, $associateid, $method, $line, $devtoken, $format, $page, $sort); AmazonSaveFile($content[1], $file); return \@content; } } else { # Cache doesn't exist debug("No cache for this query"); $content[0] = 0; $content[1] = AmazongetRemoteXML($search, $associateid, $method, $line, $devtoken, $format, $page, $sort); AmazonSaveFile($content[1], $file); return \@content; } # if we got here, then the cache existed and was fresh # so load from cache $content[0] = $stat[9]; $content[1] = $file; \@content; } sub debug { my $msg = shift; warn "$msg\n" if $debug; } sub chooseAmazonFormat { my $tokens = shift; my @heavy_tags = qw(AmazonField); for my $tok (@$tokens) { for my $item (@$tok) { if ($item =~ m/AmazonField/) { return "heavy"; } } } return "lite"; } sub ValidateSort { my($sort, $line) = @_; my $ret = 0; ($sort, my $order) = split(/\s/,$sort); # SORT: { # if ($line eq 'books') { # $ret = 1 # if grep {/$sort/} %arg_booksort; # last SORT; # } # if ($line eq 'software') { # $ret = 1 # if grep {/$sort/} %arg_softsort; # last SORT; # } # if ($line eq 'garden') { # $ret = 1 # if grep {/$sort/} %arg_outdoorsort; # last SORT; # } # if ($line eq 'universal') { # $ret = 1 # if grep {/$sort/} %arg_toolsort; # last SORT; # } # if ($line eq 'photo') { # $ret = 1 # if grep {/$sort/} %arg_camerasort; # last SORT; # } # } $ret; } # Process MT tags in all arguments. Returns an argument reference # with all tags processed. sub handle_expressions { my($ctx, $args) = @_; use MT::Util qw(decode_html); my %new_args; my $builder = $ctx->stash('builder'); for my $arg (keys %$args) { my $expr = decode_html($args->{$arg}); if ( ($expr =~ m/\/g) || $expr =~ s/\[(MT(.*?))\]/<$1>/g) { my $tok = $builder->compile($ctx, $expr); my $out = $builder->build($ctx, $tok); return $ctx->error("Error in argument expression: ".$builder->errstr) unless defined $out; $new_args{$arg} = $out; } else { $new_args{$arg} = $expr; } } \%new_args; } sub readconfig { # start the filename with a dot to prevent errors on older MT versions use MT::ConfigMgr; my $cfg = MT::ConfigMgr->instance; my $path = $cfg->PluginPath; my $cfgfile = File::Spec->catfile($path,'.mtamazon.config'); open FH, $cfgfile or die("Error opening file '$path/.mtamazon.config': $!"); while () { chomp; next if !/\S/ || /^#/; my($var, $val) = $_ =~ /^\s*(\S+)\s+(.+)$/; $var = lc($var); $val =~ s/\s*$//; next unless $var && $val; die("mtamazon.config: variable '$var' not defined") unless exists $config{$var}; $config{$var} = $val; } close FH; 1; } 1;