App-DuckPAN
view release on metacpan or search on metacpan
lib/App/DuckPAN/Web.pm view on Meta::CPAN
package App::DuckPAN::Web;
our $AUTHORITY = 'cpan:DDG';
# ABSTRACT: Webserver for duckpan server
$App::DuckPAN::Web::VERSION = '1021';
use Moo;
use DDG::Request;
use DDG::Test::Location;
use DDG::Test::Language;
use DDG::Meta::Data;
use Path::Tiny;
use Plack::Request;
use Plack::Response;
use Plack::MIME;
use HTML::Entities;
use HTML::TreeBuilder;
use HTML::Element;
use Data::Printer return_value => 'dump';
use HTTP::Request;
use HTTP::Headers;
use LWP::UserAgent;
use URI::Escape;
use JSON;
has blocks => ( is => 'ro', required => 1 );
has page_root => ( is => 'ro', required => 1 );
has page_spice => ( is => 'ro', required => 1 );
has page_css => ( is => 'ro', required => 1 );
has page_js => ( is => 'ro', required => 1 );
has page_locales => ( is => 'ro', required => 1 );
has page_templates => ( is => 'ro', required => 1 );
has server_hostname => ( is => 'ro', required => 0 );
has _our_hostname => ( is => 'rw' );
has _share_dir_hash => ( is => 'rw' );
has _path_hash => ( is => 'rw' );
has _rewrite_hash => ( is => 'rw' );
has ua => (
is => 'ro',
default => sub {
LWP::UserAgent->new(
agent => "Mozilla/5.0", #User Agent required for some API's (eg. Vimeo, IsItUp)
timeout => 5,
ssl_opts => { verify_hostname => 0 },
env_proxy => 1,
);
},
);
sub BUILD {
my ( $self ) = @_;
my %share_dir_hash;
my %path_hash;
my %rewrite_hash;
for (@{$self->blocks}) {
for (@{$_->only_plugin_objs}) {
if ($_->does('DDG::IsSpice')) {
$rewrite_hash{ref $_} = $_->rewrite if $_->has_rewrite;
my $rewrites = $_->alt_rewrites;
while(my ($short_name, $rewrite) = each %$rewrites){
$rewrite_hash{$short_name} = $rewrite;
$path_hash{$rewrite->path} = $short_name;
}
}
$share_dir_hash{$_->module_share_dir} = ref $_ if $_->can('module_share_dir');
$path_hash{$_->path} = ref $_ if $_->can('path');
}
}
$self->_share_dir_hash(\%share_dir_hash);
$self->_path_hash(\%path_hash);
$self->_rewrite_hash(\%rewrite_hash);
}
sub run_psgi {
my ( $self, $app, $env ) = @_;
$self->_our_hostname($env->{HTTP_HOST}) unless $self->_our_hostname;
lib/App/DuckPAN/Web.pm view on Meta::CPAN
unless ($share_dir){
$response->status(404);
my $path = join "/", @path_parts;
my $errormsg = "ERROR: File not found - $path";
print "\n" . $errormsg . "\n";
$body = $errormsg;
}
}
elsif (@path_parts && $path_parts[0] eq 'js' && $path_parts[1] eq 'spice') {
my $rewrite;
for (keys %{$self->_path_hash}) {
if ($request->request_uri =~ m/^$_/g) {
my $path_remainder = $request->request_uri;
$path_remainder =~ s/^$_//;
$path_remainder =~ s/\/+/\//g;
$path_remainder =~ s/^\///;
my $spice_class = $self->_path_hash->{$_};
$rewrite = $self->_rewrite_hash->{$spice_class};
die "Spice tested here must have a rewrite..." unless $rewrite;
my $from = $rewrite->from;
my $re = $rewrite->has_from ? qr{$from} : qr{(.*)};
if (my @captures = $path_remainder =~ m/$re/) {
my $to = $rewrite->parsed_to;
my $post_body = $rewrite->post_body;
for (1..@captures) {
my $index = $_-1;
my $cap_from = '\$'.$_;
my $cap_to = $captures[$index];
if (defined $cap_to) {
$to =~ s/$cap_from/$cap_to/g;
$post_body =~ s/$cap_from/$cap_to/g if $post_body;
}
else {
$to =~ s/$cap_from//g;
$post_body =~ s/$cap_from//g;
}
}
# Make sure we replace "${dollar}" with "$".
$to =~ s/\$\{dollar\}/\$/g;
my ($wrap_jsonp_callback, $callback, $wrap_string_callback, $missing_envs, $headers) =
($rewrite->wrap_jsonp_callback, $rewrite->callback, $rewrite->wrap_string_callback, defined($rewrite->missing_envs), $rewrite->headers);
# Check if environment variables (most likely the API key) is missing.
# If it is missing, switch to the DDG endpoint.
my ($use_ddh, $request_uri);
if ($missing_envs) {
++$use_ddh;
$request_uri = $request->request_uri;
# Display the URL that we used.
print "\nAPI key not found. Using DuckDuckGo's endpoint:\n";
}
$to = "https://beta.duckduckgo.com$request_uri" if $use_ddh;
my $h = HTTP::Headers->new( %$headers );
my $res;
my $req;
if ( $post_body && !$use_ddh ) {
$req = HTTP::Request->new(
POST => $to,
$h,
$post_body
);
}
else {
$req = HTTP::Request->new(
GET => $to,
$h
);
}
p($req->as_string);
$res = $self->ua->request($req);
if ($res->is_success) {
$body = $res->decoded_content;
# Encode utf8 api_responses to bytestream for Plack.
utf8::encode $body if utf8::is_utf8 $body;
warn "Cannot use wrap_jsonp_callback and wrap_string callback at the same time!" if $wrap_jsonp_callback && $wrap_string_callback;
if ($wrap_jsonp_callback && $callback) {
$body = $callback.'('.$body.');' unless $missing_envs;
}
elsif ($wrap_string_callback && $callback) {
$body =~ s/"/\\"/g;
$body =~ s/\n/\\n/g;
$body =~ s/\R//g;
$body = qq{$callback("'.$body.'");} unless $missing_envs;
}
$response->code($res->code);
$response->content_type($res->content_type);
}
else {
p($res->status_line, color => { string => 'red' });
my $errormsg = (pop @{[split'::', $spice_class]}). ": ".$res->status_line;
$body = '$("#message").removeClass("is-hidden").append("<div class=\"msg msg--warning\">'. $errormsg .'</div>");';
}
}
}
}
unless ($rewrite){
$response->status(404);
my $path = join "/", @path_parts;
my $errormsg = "ERROR: Rewrite not found - $path";
print "\n" . $errormsg . "\n";
$body = $errormsg;
}
}
elsif ($request->param('duckduckhack_ignore')) {
$response->status(204);
$body = "";
}
elsif ($request->param('duckduckhack_css')) {
$response->content_type('text/css');
$body = $self->page_css;
}
elsif ($request->param('duckduckhack_js')) {
$response->content_type('text/javascript');
$body = $self->page_js;
}
elsif ($request->param('duckduckhack_locales')) {
$response->content_type('text/javascript');
$body = $self->page_locales;
}
elsif ($request->param('duckduckhack_templates')) {
$response->content_type('text/javascript');
$body = $self->page_templates;
lib/App/DuckPAN/Web.pm view on Meta::CPAN
}
# Setup various script tags for IAs that can template:
# calls_script : js files
# calls_nrj : proxied spice api calls or goodie future
# calls_nrc : css calls
# calls_template : handlebars templates
my $calls_nrj = join('', map {
DDG::Meta::Data->get_js(id => $_)
|| qq(DDH.$_=DDH.$_||{};DDH.$_.meta={"tab":"Answer", "id":"$_"};)
} @ids);
my $calls_script = join('', map { q|<script type='text/JavaScript' src='| . $_ . q|'></script>| } @calls_script);
# For now we only allow a single goodie. If that changes, we will do the
# same join/map as with spices.
if(@calls_goodie){
my $goodie = shift @calls_goodie;
$calls_nrj .= "DDG.duckbar.future_signal_tab({signal:'high',from:'$goodie->{id}'});",
# Uncomment following line and remove "setTimeout" line when javascript race condition is addressed
# $calls_script = q|<script type="text/JavaScript">/*DDH.add(| . encode_json($goodie) . q|);*/</script>|;
$calls_script .= q|<script type="text/JavaScript">DDG.ready(function(){ window.setTimeout(DDH.add.bind(DDH, | . encode_json($goodie) . q|), 100)});</script>|;
}
elsif(@calls_fathead){
my $fathead = shift @calls_fathead;
# $calls_nrj .= "DDG.duckbar.future_signal_tab({signal:'high',from:'$fathead->{id}'});",
# Uncomment following line and remove "setTimeout" line when javascript race condition is addressed
# $calls_script = q|<script type="text/JavaScript">/*DDH.add(| . encode_json($fathead) . q|);*/</script>|;
$calls_script .= q|<script type="text/JavaScript">DDG.ready(function(){ window.setTimeout(DDH.add.bind(DDH, | . encode_json($fathead) . q|), 100)});</script>|;
}
else{
$calls_nrj .= @calls_nrj ? join(';', map { "nrj('".$_."')" } @calls_nrj) . ';' : '';
}
my $calls_nrc = @calls_nrc ? join(';', map { "nrc('".$_."')" } @calls_nrc) . ';' : '';
if (%calls_template) {
foreach my $spice_name ( keys %calls_template ){
$calls_script .= join("",map {
my $template_name = $_;
my $is_ct_self = $calls_template{$spice_name}{$template_name}{"is_ct_self"};
my $template_content = $calls_template{$spice_name}{$template_name}{"content"}->slurp;
"<script class='duckduckhack_spice_template' spice-name='$spice_name' template-name='$template_name' is-ct-self='$is_ct_self' type='text/plain'>$template_content</script>"
} keys %{ $calls_template{$spice_name} });
}
}
$self->_inject_mock_content($root);
$page = $root->as_HTML;
$page =~ s/####DUCKDUCKHACK-CALL-NRJ####/$calls_nrj/g;
$page =~ s/####DUCKDUCKHACK-CALL-NRC####/$calls_nrc/g;
$page =~ s/####DUCKDUCKHACK-CALL-SCRIPT####/$calls_script/g;
$response->content_type('text/html');
$body = $page;
}
else {
my $res = $self->ua->request(HTTP::Request->new(GET => "http://".$hostname.$request->request_uri));
if ($res->is_success) {
$body = $res->decoded_content;
$response->code($res->code);
$response->content_type($res->content_type);
}
else {
p($res->status_line, color => { string => 'red' });
$body = "";
}
}
$response->body($body);
return $response;
}
sub _no_results_error {
my ($self, $query) = @_;
my $response = Plack::Response->new(200);
$response->content_type('text/html');
my $error = "Sorry, no results were returned from Instant Answer";
my $root = HTML::TreeBuilder->new;
$root->parse($self->page_root);
my $text_field = $root->look_down( "name", "q" );
$text_field->attr( value => $query );
$root->find_by_tag_name('body')->push_content(
HTML::TreeBuilder->new_from_content(
qq(<script type="text/javascript">seterr('$error')</script>)
)->guts
);
p($error, color => { string => 'red' });
my $body = $root->as_HTML;
$response->body($body);
return $response;
}
#inject some mock results into the SERP to make it look a little more real
sub _inject_mock_content {
my ($self, $root) = @_;
# ensure results and ad containers exist
my $ad_container = $root->look_down(id => "ads");
my $links_container = $root->look_down(id => "links");
return unless $ad_container && $links_container;
#inject a mock ad into the page
$ad_container->attr("style", "display: block");
$ad_container->push_content(
HTML::TreeBuilder->new_from_content(
q(<div id="ra-0" class="result results_links highlight_a result--ad highlight_sponsored sponsored highlight highlight_sponsored_hover" data-nir="1">
<div class="result__body links_main links_deep">
<a href="#" class="result__badge badge--ad">Ad</a>
<h2 class="result__title">
<a class="result__a" href="#">Lorem ipsum Culpa ex adipisicing.</a>
<a class="result__check" href="#">
<span class="result__check__tt">Lorem ipsum Consectetur nostrud id quis in ut.</span>
( run in 1.027 second using v1.01-cache-2.11-cpan-5a3173703d6 )