# GetXML.pl
# Movable Type plugin tag for getting and displaying an XML feed
# by Kevin Shay
# http://www.staggernation.com/mtplugins/
# last modified July 12, 2004
package MT::Plugin::GetXML;
use strict;
use vars qw( $VERSION );
$VERSION = '1.1';
use MT;
use MT::Template::Context;
eval{ require MT::Plugin };
unless ($@) {
my $plugin = {
name => "GetXML $VERSION",
description => 'Fetch and display arbitrary XML data using the XML::Simple Perl module.',
doc_link => 'http://www.staggernation.com/mtplugins/GetXML'
};
MT->add_plugin(new MT::Plugin($plugin));
}
MT::Template::Context->add_container_tag('GetXML' => sub{&_hdlr_get_xml;});
MT::Template::Context->add_container_tag('GetXMLElement' => sub{&_hdlr_get_xml_element;});
MT::Template::Context->add_conditional_tag('IfXMLElementExists' => sub{&_hdlr_if_xml_element_exists;});
MT::Template::Context->add_conditional_tag('IfXMLElementNotExists' => sub{&_hdlr_if_xml_element_not_exists;});
MT::Template::Context->add_tag('GetXMLValue' => sub{&_hdlr_get_xml_value;});
MT::Template::Context->add_tag('GetXMLElementCount' => sub{&_hdlr_get_xml_element_count;});
MT::Template::Context->add_tag('GetXMLElementIndex' => sub{&_hdlr_get_xml_element_index;});
MT::Template::Context->add_tag('GetXMLURL' => sub{&_hdlr_get_xml_url;});
MT::Template::Context->add_tag('GetXMLCacheDate' => sub{&_hdlr_get_xml_cache_date;});
MT::Template::Context->add_tag('GetXMLDump' => sub{&_hdlr_get_xml_dump;});
MT::Template::Context->add_global_filter('space_to_plus' => sub{&_hdlr_space_to_plus;});
# edit this line to change the default caching time
my $DEFAULT_CACHE_MINS = 15;
# edit this line to change the default error behavior
# (display|warn|die|ignore)
my $DEFAULT_ERRORS = 'display';
my @xml_attrs = qw( noattr keeproot suppressempty );
# tag attributes to MTGetXML that we don't want to add to the URL
my $non_url_attrs = join('|', @xml_attrs, qw( cache location errors ));
sub _hdlr_get_xml {
my ($ctx, $args, $cond) = @_;
return $ctx->error('No location passed') unless $args->{'location'};
eval { require LWP::Simple; };
$@ && (return $ctx->error("Error loading LWP::Simple module: $@"));
eval { require XML::Simple; };
$@ && (return $ctx->error("Error loading XML::Simple module: $@"));
my $builder = $ctx->stash('builder');
my $tokens = $ctx->stash('tokens');
# build any MT tags in passed value
my $url_args = '';
use MT::Util qw( encode_url );
for my $key (keys %$args) {
next if ($key =~ /^$non_url_attrs$/);
my $val = build_value($ctx, $args->{$key});
next unless $val;
$val = encode_url($val);
# de-encode plus signs
$val =~ s/%2B/+/g;
$url_args .= "$key=" . $val . '&';
}
my $location = build_value($ctx, $args->{'location'});
$url_args =~ s/&$//;
my $url = $url_args ? "$location?$url_args" : $location;
my $xml_data = {};
my $cache_time = -1;
my %xml_opts = ();
for my $xml_attr (@xml_attrs) {
$xml_opts{$xml_attr} = 1 if $args->{$xml_attr};
}
$xml_opts{'keyattr'} = [];
# data structure will be different with different options, so
# append the options to the URL for the cache key
my $cache_key = $url . '[';
for (sort keys %xml_opts) {
if ($args->{$_}) {
$cache_key .= $_ . $args->{$_};
}
}
$cache_key .= ']';
# try to load a cached copy
my $cached = 0;
my $cachable = 1;
# pass cache="0" to suppress caching
if (exists($args->{'cache'}) && ($args->{'cache'} eq '0')) {
$cachable = 0;
} else {
# if we can't load MT::PluginData (either too old a version of MT
# or Storable is not available), just skip it
eval { require MT::PluginData; };
if ($@) {
$cachable = 0;
} else {
my ($pdata) = MT::PluginData->load({
plugin => 'GetXML', key => $cache_key
});
if ($pdata) {
my $data = $pdata->data;
my $mins_ago = (time() - $data->{'time'}) / 60;
if ($mins_ago < ($args->{'cache'} || $DEFAULT_CACHE_MINS)) {
$xml_data = $data->{'xml_data'};
$cached = 1;
$cache_time = $data->{'time'};
}
}
}
}
if (!$cached) {
my $xml = LWP::Simple::get($url);
return error($ctx, $args, "$url: page not found") unless $xml;
eval { $xml_data = XML::Simple::XMLin($xml, %xml_opts); };
if ($@) {
return error($ctx, $args, "$url: $@");
}
if ($cachable) {
my ($pdata) = MT::PluginData->load({
plugin => 'GetXML', key => $cache_key
});
if (!$pdata) {
$pdata = MT::PluginData->new;
}
$pdata->plugin('GetXML');
$pdata->key($cache_key);
$cache_time = time();
$pdata->data({'time' => $cache_time, 'xml_data' => $xml_data});
# no need to die on an error, we don't care that much
$pdata->save;
}
}
local $ctx->{__stash}{'mtgetxml_data'} = $xml_data;
local $ctx->{__stash}{'mtgetxml_cachetime'} = $cache_time;
local $ctx->{__stash}{'mtgetxml_url'} = $url;
defined(my $text = $builder->build($ctx, $tokens, $cond))
|| return $ctx->error($ctx->errstr);
return $text;
}
sub _hdlr_get_xml_element {
my ($ctx, $args, $cond) = @_;
return get_value($ctx, $args, $cond);
}
sub _hdlr_get_xml_value {
my ($ctx, $args, $cond) = @_;
return get_value($ctx, $args, $cond, 1);
}
sub get_value {
my ($ctx, $args, $cond, $want_val) = @_;
(my $data = $ctx->stash('mtgetxml_data'))
|| return $ctx->error('Not called from within MTGetXML container');
my $name = $args->{'name'};
if (!$name) {
# if containing tag was looping through an array of simple
# values, can call MTGetXMLValue without a name and it will
# return the stashed value
if ($want_val && !ref($data)) {
return $data;
} else {
return $ctx->error('No element name passed');
}
}
my $builder = $ctx->stash('builder');
my $tokens = $ctx->stash('tokens');
$name = build_value($ctx, $name);
if (ref($data) eq 'HASH') {
my $elem = $data->{$name};
if (!ref($elem)) {
return $elem ? $elem : '';
}
if ($want_val) {
if (ref($elem) eq 'ARRAY') {
# if an array consists entirely of simple values, just
# concatenate them and return the result
if (join('', map { ref($_) } @$elem) eq '') {
return join('', @$elem);
}
}
return error($ctx, $args, "XML element '$name' contains multiple values");
}
if (ref($elem) eq 'HASH') {
$elem = [ $elem ];
}
my $text = '';
my $i = 0;
for my $e (@$elem) {
$i++;
last if ($args->{'limit'} && ($i > $args->{'limit'}));
local $ctx->{__stash}{'mtgetxml_data'} = $e;
local $ctx->{__stash}{'mtgetxml_index'} = $i;
defined(my $iter = $builder->build($ctx, $tokens, $cond))
|| return $ctx->error($ctx->errstr);
$text .= $iter;
}
return $text;
}
}
sub _hdlr_if_xml_element_exists {
my ($ctx, $args) = @_;
return element_exists($ctx, $args, 1);
}
sub _hdlr_if_xml_element_not_exists {
my ($ctx, $args) = @_;
return element_exists($ctx, $args, 0);
}
sub element_exists {
my ($ctx, $args, $want_elem) = @_;
(my $data = $ctx->stash('mtgetxml_data'))
|| return $ctx->error('Not called from within MTGetXML container');
(my $name = $args->{'name'})
|| return $ctx->error('No element name passed');
my $exists = 1;
if ((ref($data) ne 'HASH') || !exists($data->{$name})) {
$exists = 0;
}
return ($exists == $want_elem) ? 1 : 0;
}
sub _hdlr_get_xml_element_count {
my ($ctx, $args) = @_;
(my $data = $ctx->stash('mtgetxml_data'))
|| return $ctx->error('Not called from within MTGetXML container');
(my $name = $args->{'name'})
|| return $ctx->error('No element name passed');
if ((ref($data) eq 'HASH') && (ref($data->{$name}) eq 'ARRAY')) {
return scalar(@{$data->{$name}});
}
return 1;
}
sub _hdlr_get_xml_element_index {
my ($ctx, $args) = @_;
(my $i = $ctx->stash('mtgetxml_index'))
|| return $ctx->error('Not called from within MTGetXMLElement container');
return $i;
}
sub _hdlr_get_xml_url {
my ($ctx, $args) = @_;
(my $url = $ctx->stash('mtgetxml_url'))
|| return $ctx->error('Not called from within MTGetXML container');
return $url;
}
sub _hdlr_get_xml_cache_date {
my ($ctx, $args) = @_;
(my $time = $ctx->stash('mtgetxml_cachetime'))
|| return $ctx->error('Not called from within MTGetXML container');
return '[Not cached]' if ($time == -1);
use MT::Util qw( offset_time_list format_ts );
my @tl = offset_time_list($time, $ctx->stash('blog_id'));
my $ts = sprintf "%04d%02d%02d%02d%02d%02d",
$tl[5]+1900, $tl[4]+1, @tl[3,2,1,0];
return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'});
}
sub _hdlr_get_xml_dump {
my ($ctx, $args) = @_;
(my $data = $ctx->stash('mtgetxml_data'))
|| return $ctx->error('Not called from within MTGetXML container');
require Data::Dumper;
my $dump = Data::Dumper->new([ $data ]);
$dump->Pad('
');
$dump->Indent(1);
my $dump_text = $dump->Dump;
$dump_text =~ s!^
\$VAR1 = !!;
$dump_text =~ s!(\n
)( +)!$1.(' ' x length($2))!eg;
return $dump_text;
}
sub _hdlr_space_to_plus {
# global filter that converts whitespace to plus signs
my ($val, $arg, $ctx) = @_;
if ($arg eq '1') {
$val =~ s/\s+/+/g;
$val =~ s/\+$//;
}
return $val;
}
sub error {
# error handling: display on page, ignore, warn, or die (throw build error)
my ($ctx, $args, $err) = @_;
my $behavior = ($args->{'errors'} || $DEFAULT_ERRORS);
if ($behavior eq 'die') {
return $ctx->error($err);
} elsif ($behavior eq 'warn') {
warn $err;
return '';
} elsif ($behavior eq 'ignore') {
return '';
} else {
return "[$err]";
}
}
sub build_value {
# convert and build MT template tags within a passed value.
my ($ctx, $value) = @_;
# within a value argument, you can use MT tags, but with
# square brackets instead of angle brackets and single quotes
# instead of double quotes; literal square brackets and single
# quotes must be escaped with a backslash
# convert non-escaped []'
$value =~ s/(?/g;
$value =~ s/(?stash('builder');
my $tok = $builder->compile($ctx, $value);
$value = $builder->build($ctx, $tok);
return $ctx->error($builder->errstr) unless defined($value);
}
return $value;
}
1;