PageRenderTime 51ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/App/DuckPAN/Web.pm

https://github.com/DavidMascio/p5-app-duckpan
Perl | 390 lines | 324 code | 43 blank | 23 comment | 27 complexity | edeadc8a5a05cba7fa85de3966cc8e99 MD5 | raw file
  1. package App::DuckPAN::Web;
  2. # ABSTRACT: Webserver for duckpan server
  3. use Moo;
  4. use DDG::Request;
  5. use DDG::Test::Location;
  6. use DDG::Test::Language;
  7. use Plack::Request;
  8. use Plack::Response;
  9. use Plack::MIME;
  10. use HTML::Entities;
  11. use HTML::TreeBuilder;
  12. use HTML::Element;
  13. use Data::Printer;
  14. use IO::All;
  15. use HTTP::Request;
  16. use LWP::UserAgent;
  17. use URI::Escape;
  18. use JSON;
  19. use Data::Dumper;
  20. has blocks => ( is => 'ro', required => 1 );
  21. has page_root => ( is => 'ro', required => 1 );
  22. has page_spice => ( is => 'ro', required => 1 );
  23. has page_css => ( is => 'ro', required => 1 );
  24. has page_js => ( is => 'ro', required => 1 );
  25. has page_templates => ( is => 'ro', required => 1 );
  26. has server_hostname => ( is => 'ro', required => 0 );
  27. has _share_dir_hash => ( is => 'rw' );
  28. has _path_hash => ( is => 'rw' );
  29. has _rewrite_hash => ( is => 'rw' );
  30. has ua => (
  31. is => 'ro',
  32. default => sub {
  33. LWP::UserAgent->new(
  34. agent => "Mozilla/5.0", #User Agent required for some API's (eg. Vimeo, IsItUp)
  35. timeout => 5,
  36. ssl_opts => { verify_hostname => 0 },
  37. env_proxy => 1,
  38. );
  39. },
  40. );
  41. sub BUILD {
  42. my ( $self ) = @_;
  43. my %share_dir_hash;
  44. my %path_hash;
  45. my %rewrite_hash;
  46. for (@{$self->blocks}) {
  47. for (@{$_->only_plugin_objs}) {
  48. if ($_->does('DDG::IsSpice')) {
  49. $rewrite_hash{ref $_} = $_->rewrite if $_->has_rewrite;
  50. }
  51. $share_dir_hash{$_->module_share_dir} = ref $_ if $_->can('module_share_dir');
  52. $path_hash{$_->path} = ref $_ if $_->can('path');
  53. }
  54. }
  55. $self->_share_dir_hash(\%share_dir_hash);
  56. $self->_path_hash(\%path_hash);
  57. $self->_rewrite_hash(\%rewrite_hash);
  58. }
  59. sub run_psgi {
  60. my ( $self, $env ) = @_;
  61. my $request = Plack::Request->new($env);
  62. my $response = $self->request($request);
  63. return $response->finalize;
  64. }
  65. my $has_common_js = 0;
  66. sub request {
  67. my ( $self, $request ) = @_;
  68. my $hostname = $self->server_hostname;
  69. my @path_parts = split(/\/+/,$request->request_uri);
  70. shift @path_parts;
  71. my $response = Plack::Response->new(200);
  72. my $body;
  73. if ($request->request_uri eq "/"){
  74. $response->content_type("text/html");
  75. $body = $self->page_root;
  76. } elsif (@path_parts && $path_parts[0] eq 'share') {
  77. my $filename = pop @path_parts;
  78. my $share_dir = join('/',@path_parts);
  79. # remove spice version from path when present
  80. # eg. get_asset_path returns `/share/spice/recipe/###/yummly.ico`
  81. $share_dir =~ s!/\d+!!;
  82. if ($filename =~ /\.js$/ and
  83. $has_common_js and
  84. $share_dir =~ /(share\/spice\/([^\/]+)\/?)(.*)/){
  85. my $parent_dir = $1;
  86. my $parent_name = $2;
  87. my $common_js = $parent_dir."$parent_name.js";
  88. $body = io($common_js)->slurp;
  89. warn "\nAppended $common_js to $filename\n\n";
  90. }
  91. my $filename_path = $self->_share_dir_hash->{$share_dir}->can('share')->($filename);
  92. my $content_type = Plack::MIME->mime_type($filename);
  93. $response->content_type($content_type);
  94. $body .= -f $filename_path ? io($filename_path)->slurp : "";
  95. } elsif (@path_parts && $path_parts[0] eq 'js' && $path_parts[1] eq 'spice') {
  96. for (keys %{$self->_path_hash}) {
  97. if ($request->request_uri =~ m/^$_/g) {
  98. my $path_remainder = $request->request_uri;
  99. $path_remainder =~ s/^$_//;
  100. $path_remainder =~ s/\/+/\//g;
  101. $path_remainder =~ s/^\///;
  102. my $spice_class = $self->_path_hash->{$_};
  103. my $rewrite = $self->_rewrite_hash->{$spice_class};
  104. die "Spice tested here must have a rewrite..." unless $rewrite;
  105. my $from = $rewrite->from;
  106. my $re = $rewrite->has_from ? qr{$from} : qr{(.*)};
  107. if (my @captures = $path_remainder =~ m/$re/) {
  108. my $to = $rewrite->parsed_to;
  109. for (1..@captures) {
  110. my $index = $_-1;
  111. my $cap_from = '\$'.$_;
  112. my $cap_to = $captures[$index];
  113. if (defined $cap_to) {
  114. $to =~ s/$cap_from/$cap_to/g;
  115. } else {
  116. $to =~ s/$cap_from//g;
  117. }
  118. }
  119. # Make sure we replace "${dollar}" with "$".
  120. $to =~ s/\$\{dollar\}/\$/g;
  121. # Check if environment variables (most likely the API key) is missing.
  122. # If it is missing, switch to the DDG endpoint.
  123. if(defined $rewrite->missing_envs) {
  124. $to = 'https://ddh1.duckduckgo.com' . $request->request_uri;
  125. # Display the URL that we used.
  126. print "\nAPI key not found. Using DuckDuckGo's endpoint:\n";
  127. }
  128. p($to);
  129. my $res = $self->ua->request(HTTP::Request->new(
  130. GET => $to,
  131. [ $rewrite->accept_header ? ("Accept", $rewrite->accept_header) : () ]
  132. ));
  133. if ($res->is_success) {
  134. $body = $res->decoded_content;
  135. # Encode utf8 api_responses to bytestream for Plack.
  136. utf8::encode $body if utf8::is_utf8 $body;
  137. warn "Cannot use wrap_jsonp_callback and wrap_string callback at the same time!" if $rewrite->wrap_jsonp_callback && $rewrite->wrap_string_callback;
  138. if ($rewrite->wrap_jsonp_callback && $rewrite->callback) {
  139. $body = $rewrite->callback.'('.$body.');' unless defined $rewrite->missing_envs;
  140. }
  141. elsif ($rewrite->wrap_string_callback && $rewrite->callback) {
  142. $body =~ s/"/\\"/g;
  143. $body =~ s/\n/\\n/g;
  144. $body =~ s/\R//g;
  145. $body = $rewrite->callback.'("'.$body.'");' unless defined $rewrite->missing_envs;
  146. }
  147. $response->code($res->code);
  148. $response->content_type($res->content_type);
  149. } else {
  150. warn $res->status_line, "\n";
  151. $body = "";
  152. }
  153. }
  154. }
  155. }
  156. } elsif ($request->param('duckduckhack_ignore')) {
  157. $response->status(204);
  158. $body = "";
  159. } elsif ($request->param('duckduckhack_css')) {
  160. $response->content_type('text/css');
  161. $body = $self->page_css;
  162. } elsif ($request->param('duckduckhack_js')) {
  163. $response->content_type('text/javascript');
  164. $body = $self->page_js;
  165. } elsif ($request->param('duckduckhack_templates')) {
  166. $response->content_type('text/javascript');
  167. $body = $self->page_templates;
  168. } elsif ($request->param('q') && $request->path_info eq '/') {
  169. my $query = $request->param('q');
  170. $query =~ s/^\s+|\s+$//g; # strip leading & trailing whitespace
  171. Encode::_utf8_on($query);
  172. my $ddg_request = DDG::Request->new(
  173. query_raw => $query,
  174. location => test_location_by_env(),
  175. language => test_language_by_env(),
  176. );
  177. my @results = ();
  178. my @calls_nrj = ();
  179. my @calls_nrc = ();
  180. my @calls_script = ();
  181. my %calls_template = ();
  182. for (@{$self->blocks}) {
  183. push(@results,$_->request($ddg_request));
  184. }
  185. my $page = $self->page_spice;
  186. my $uri_encoded_query = uri_escape_utf8($query, "^A-Za-z");
  187. my $html_encoded_query = encode_entities($query);
  188. my $uri_encoded_ddh = quotemeta(uri_escape('duckduckhack-template-for-spice2', "^A-Za-z0-9"));
  189. $page =~ s/duckduckhack-template-for-spice2/$html_encoded_query/g;
  190. $page =~ s/$uri_encoded_ddh/$uri_encoded_query/g;
  191. # For debugging query replacement.
  192. #p($uri_encoded_ddh);
  193. #p($page);
  194. my $root = HTML::TreeBuilder->new;
  195. $root->parse($page);
  196. # Check for no results
  197. if (!scalar(@results)) {
  198. print "NO RESULTS\n";
  199. $root = HTML::TreeBuilder->new;
  200. $root->parse($self->page_root);
  201. my $text_field = $root->look_down(
  202. "name", "q"
  203. );
  204. $text_field->attr( value => $query );
  205. $root->find_by_tag_name('body')->push_content(
  206. HTML::TreeBuilder->new_from_content(
  207. q(<script type="text/javascript">seterr('Sorry, no hit for your plugins')</script>)
  208. )->guts
  209. );
  210. $page = $root->as_HTML;
  211. }
  212. # Iterate over results,
  213. # checking if result is a Spice or Goodie
  214. # and sets up the page content accordingly
  215. foreach my $result (@results) {
  216. # Info for terminal.
  217. p($result) if $result;
  218. # NOTE -- this isn't designed to have both goodies and spice at once.
  219. # Check if we have a Spice result
  220. # if so grab the associated JS, Handlebars and CSS
  221. # and add them to correct arrays for injection into page
  222. if (ref $result eq 'DDG::ZeroClickInfo::Spice') {
  223. my $io;
  224. my @files;
  225. my $share_dir = $result->caller->module_share_dir;
  226. my @path = split(/\/+/, $share_dir);
  227. my $spice_name = join("_", @path[2..$#path]);
  228. $io = io($result->caller->module_share_dir);
  229. push(@files, @$io);
  230. foreach (@files){
  231. if ($_->filename =~ /$spice_name\.js$/){
  232. push (@calls_script, $_);
  233. } elsif ($_->filename =~ /$spice_name\.css$/){
  234. push (@calls_nrc, $_);
  235. } elsif ($_->filename =~ /^.+handlebars$/){
  236. my $template_name = $_->filename;
  237. $template_name =~ s/\.handlebars//;
  238. $calls_template{$spice_name}{$template_name}{"content"} = $_;
  239. $calls_template{$spice_name}{$template_name}{"is_ct_self"} = $result->call_type eq 'self';
  240. }
  241. }
  242. push (@calls_nrj, $result->call_path);
  243. # Check if we have a Goodie result
  244. # if so modify HTML and return content
  245. } elsif ( ref $result eq 'DDG::ZeroClickInfo' ){
  246. # Grab ZCI div, push in required HTML
  247. my $zci_container = HTML::Element->new('div', id => "zci-answer", class => "zci zci--answer is-active");
  248. $zci_container->push_content(
  249. HTML::TreeBuilder->new_from_content(
  250. q(<div class="cw">
  251. <div class="zci__main zci__main--detail">
  252. <div class="zci__body"></div>
  253. </div>
  254. </div>)
  255. )->guts
  256. );
  257. my $zci_body = $zci_container->look_down(class => 'zci__body');
  258. # Stick the answer inside $zci_body
  259. my $answer = $result->answer;
  260. if ($result->has_html) {
  261. my $tb = HTML::TreeBuilder->new();
  262. # Specifically allow unknown tags to support <svg> and <canvas>
  263. $tb->ignore_unknown(0);
  264. $answer = $tb->parse_content($result->html)->guts;
  265. }
  266. $zci_body->push_content($answer);
  267. my $zci_wrapper = $root->look_down(id => "zero_click_wrapper");
  268. $zci_wrapper->insert_element($zci_container);
  269. my $duckbar_home = $root->look_down(id => "duckbar_home");
  270. $duckbar_home->delete_content();
  271. $duckbar_home->attr(class => "zcm__menu");
  272. $duckbar_home->push_content(
  273. HTML::TreeBuilder->new_from_content(
  274. q(<li class="zcm__item">
  275. <a data-zci-link="answer" class="zcm__link zcm__link--answer is-active" href="javascript:;">Answer</a>
  276. </li>)
  277. )->guts
  278. );
  279. my $duckbar_static_sep = $root->look_down(id => "duckbar_static_sep");
  280. $duckbar_static_sep->attr(class => "zcm__sep--h");
  281. my $html = $root->look_down(_tag => "html");
  282. $html->attr(class => "set-header--fixed has-zcm js no-touch csstransforms3d csstransitions svg use-opts has-active-zci");
  283. # Make sure we only show one Goodie (this will change down the road)
  284. last;
  285. # If not Spice or Goodie,
  286. # inject raw Dumper() output from into page
  287. } else {
  288. my $content = $root->look_down(id => "bottom_spacing2");
  289. my $dump = HTML::Element->new('pre');
  290. $dump->push_content(Dumper $result);
  291. $content->insert_element($dump);
  292. $page = $root->as_HTML;
  293. }
  294. }
  295. # Setup various script tags:
  296. # calls_script : spice js files
  297. # calls_nrj : proxied spice api calls
  298. # calls_nrc : spice css calls
  299. # calls_template : spice handlebars templates
  300. my $calls_nrj = (scalar @calls_nrj) ? join(";",map { "nrj('".$_."')" } @calls_nrj) . ';' : '';
  301. my $calls_nrc = (scalar @calls_nrc) ? join(";",map { "nrc('".$_."')" } @calls_nrc) . ';' : '';
  302. my $calls_script = (scalar @calls_script)
  303. ? join("",map { "<script type='text/JavaScript' src='".$_."'></script>" } @calls_script)
  304. : '';
  305. if (%calls_template) {
  306. foreach my $spice_name ( keys %calls_template ){
  307. $calls_script .= join("",map {
  308. my $template_name = $_;
  309. my $is_ct_self = $calls_template{$spice_name}{$template_name}{"is_ct_self"};
  310. my $template_content = $calls_template{$spice_name}{$template_name}{"content"}->slurp;
  311. "<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>"
  312. } keys %{ $calls_template{$spice_name} });
  313. }
  314. }
  315. $page = $root->as_HTML;
  316. $page =~ s/####DUCKDUCKHACK-CALL-NRJ####/$calls_nrj/g;
  317. $page =~ s/####DUCKDUCKHACK-CALL-NRC####/$calls_nrc/g;
  318. $page =~ s/####DUCKDUCKHACK-CALL-SCRIPT####/$calls_script/g;
  319. $response->content_type('text/html');
  320. $body = $page;
  321. } else {
  322. my $res = $self->ua->request(HTTP::Request->new(GET => "http://".$hostname.$request->request_uri));
  323. if ($res->is_success) {
  324. $body = $res->decoded_content;
  325. $response->code($res->code);
  326. $response->content_type($res->content_type);
  327. } else {
  328. warn $res->status_line, "\n";
  329. $body = "";
  330. }
  331. }
  332. $response->body($body);
  333. return $response;
  334. }
  335. 1;