File Coverage

blib/lib/Mojolicious/Plugin/LinkEmbedder.pm
Criterion Covered Total %
statement 80 89 89.8
branch 27 52 51.9
condition 13 35 37.1
subroutine 16 16 100.0
pod 2 2 100.0
total 138 194 71.1


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::LinkEmbedder;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::LinkEmbedder - Convert a URL to embedded content
6              
7             =head1 VERSION
8              
9             0.2301
10              
11             =head1 DESCRIPTION
12              
13             This module can transform a URL to an iframe, image or other embeddable
14             content.
15              
16             =head1 SYNOPSIS
17              
18             =head2 Simple version
19              
20             use Mojolicious::Lite;
21             plugin LinkEmbedder => { route => '/embed' };
22              
23             =head2 Full control
24              
25             plugin 'LinkEmbedder';
26              
27             get '/embed' => sub {
28             my $c = shift;
29              
30             $c->delay(
31             sub {
32             my ($delay) = @_;
33             $c->embed_link($c->param('url'), $delay->begin);
34             },
35             sub {
36             my ($delay, $link) = @_;
37              
38             $c->respond_to(
39             json => {
40             json => {
41             media_id => $link->media_id,
42             url => $link->url->to_string,
43             },
44             },
45             any => { text => $link->to_embed }
46             );
47             }
48             );
49             };
50              
51             =head2 Example with caching
52              
53             plugin 'LinkEmbedder';
54              
55             get '/embed' => sub {
56             my $c = shift;
57             my $url = $c->param('url');
58             my $cached;
59              
60             $c->delay(
61             sub {
62             my ($delay) = @_;
63             return $delay->pass($cached) if $cached = $c->cache->get($url);
64             return $c->embed_link($c->param('url'), $delay->begin);
65             },
66             sub {
67             my ($delay, $link) = @_;
68              
69             $link = $link->TO_JSON if UNIVERSAL::can($link, 'TO_JSON');
70             $c->cache->set($url => $link);
71              
72             $c->respond_to(
73             json => {
74             json => {
75             media_id => $link->{media_id},
76             url => $link->{url},
77             },
78             },
79             any => { text => $link->{html} }
80             );
81             }
82             );
83             };
84              
85             =head1 SUPPORTED LINKS
86              
87             =over 4
88              
89             =item * L
90              
91             =item * L
92              
93             =item * L
94              
95             =item * L
96              
97             =item * L
98              
99             =item * L
100              
101             =item * L
102              
103             =item * L
104              
105             =item * L
106              
107             =item * L
108              
109             =item * L
110              
111             =item * L
112              
113             =item * L
114              
115             =item * L
116              
117             =item * L
118              
119             =item * L
120              
121             =item * L
122              
123             =item * L
124              
125             =item * L
126              
127             =item * L
128              
129             =item * L
130              
131             =item * L
132              
133             =item * L
134              
135             =item * L
136              
137             =item * L
138              
139             =item * L
140              
141             =back
142              
143             =cut
144              
145 23     23   6738536 use Mojo::Base 'Mojolicious::Plugin';
  23         40  
  23         99  
146 23     23   2449 use Mojo::JSON;
  23         32  
  23         686  
147 23     23   86 use Mojo::UserAgent;
  23         26  
  23         126  
148 23     23   7826 use Mojolicious::Plugin::LinkEmbedder::Link;
  23         35  
  23         145  
149 23   50 23   769 use constant DEBUG => $ENV{MOJO_LINKEMBEDDER_DEBUG} || 0;
  23         23  
  23         30082  
150              
151             our $VERSION = '0.2301';
152              
153             has _ua => sub { Mojo::UserAgent->new(max_redirects => 3) };
154              
155             =head1 METHODS
156              
157             =head2 embed_link
158              
159             See L.
160              
161             =cut
162              
163             sub embed_link {
164 7     7 1 10 my ($self, $c, $url, $cb) = @_;
165              
166 7 50 50     35 $url = Mojo::URL->new($url || '') unless ref $url;
167              
168 7 50       553 if ($url =~ m!\.(?:jpg|png|gif)\b!i) {
169 0 0       0 return $c if $self->_new_link_object(image => $c, {url => $url}, $cb);
170             }
171 7 50       678 if ($url =~ m!\.(?:mpg|mpeg|mov|mp4|ogv)\b!i) {
172 0 0       0 return $c if $self->_new_link_object(video => $c, {url => $url}, $cb);
173             }
174 7 50       537 if ($url =~ m!^spotify:\w+!i) {
175 0 0       0 return $c if $self->_new_link_object('open.spotify' => $c, {url => $url}, $cb);
176             }
177 7 100 33     443 if (!$url or !$url->host) {
178 3         29 return $c->tap(
179             $cb,
180             Mojolicious::Plugin::LinkEmbedder::Link->new(
181             url => Mojo::URL->new,
182             error => {message => 'Invalid input', code => 400,}
183             )
184             );
185             }
186              
187             return $c->delay(
188             sub {
189 4     4   909 my ($delay) = @_;
190 4         14 $self->_ua->head($url => $delay->begin);
191             },
192             sub {
193 4     4   26370 my ($delay, $tx) = @_;
194 4         13 $self->_learn($c, $tx, $cb);
195             }
196 4         74 );
197             }
198              
199             sub _learn {
200 4     4   5 my ($self, $c, $tx, $cb) = @_;
201 4   50     9 my $ct = $tx->res->headers->content_type || '';
202 4         64 my $etag = $tx->res->headers->etag;
203 4         50 my $url = $tx->req->url;
204              
205 4 50 0     24 if ($etag and $etag eq ($c->req->headers->etag // '')) {
      33        
206 0         0 return $c->$cb(Mojolicious::Plugin::LinkEmbedder::Link->new(_tx => $tx, etag => $etag));
207             }
208 4 50       9 if (my $err = $tx->error) {
209 0         0 return $c->$cb(Mojolicious::Plugin::LinkEmbedder::Link->new(_tx => $tx, error => $err));
210             }
211 4 50       82 if (my $type = lc $url->host) {
212 4         29 $type =~ s/^(?:www|my)\.//;
213 4         18 $type =~ s/\.\w+$//;
214 4 50       18 return if $self->_new_link_object($type => $c, {_tx => $tx}, $cb);
215             }
216              
217 4 50 33     15 return if $ct =~ m!^image/! and $self->_new_link_object(image => $c, {url => $url, _tx => $tx}, $cb);
218 4 50 33     12 return if $ct =~ m!^video/! and $self->_new_link_object(video => $c, {url => $url, _tx => $tx}, $cb);
219 4 50 33     12 return if $ct =~ m!^text/plain! and $self->_new_link_object(text => $c, {url => $url, _tx => $tx}, $cb);
220              
221 4 50       14 if ($ct =~ m!^text/html!) {
222 4 50       11 return if $self->_new_link_object(html => $c, {_tx => $tx}, $cb);
223             }
224              
225 0         0 warn "[LINK] New from $ct: Mojolicious::Plugin::LinkEmbedder::Link ($url)\n" if DEBUG;
226 0         0 $c->$cb(Mojolicious::Plugin::LinkEmbedder::Link->new(_tx => $tx));
227             }
228              
229             sub _new_link_object {
230 8     8   7 my ($self, $type, $c, $args, $cb) = @_;
231 8 100       28 my $class = $self->{classes}{$type} or return;
232              
233 4         4 warn "[LINK] New from $type: $class\n" if DEBUG;
234 4 50       351 eval "require $class;1" or die "Could not require $class: $@";
235 4         21 local $args->{ua} = $self->_ua;
236 4         56 my $link = $class->new($args);
237              
238             Mojo::IOLoop->delay(
239             sub {
240 4     4   477 my ($delay) = @_;
241 4         12 $link->learn($c, $delay->begin);
242             },
243             sub {
244 4     4   105 my ($delay) = @_;
245 4         5 do { local $link->{_tx}; local $link->{ua}; warn Data::Dumper::Dumper($link); } if DEBUG and 0;
246 4         9 $c->$cb($link);
247             },
248 4         68 );
249              
250 4         343 return $class;
251             }
252              
253             =head2 register
254              
255             $app->plugin('LinkEmbedder' => \%config);
256              
257             Will register the L helper which creates new objects from
258             L. C<%config> is optional but can
259             contain:
260              
261             =over 4
262              
263             =item * route => $str|$obj
264              
265             Use this if you want to have the default handler to do link embedding.
266             The default handler is shown in L. C<$str> is just a path,
267             while C<$obj> is a L object.
268              
269             =back
270              
271             =cut
272              
273             sub register {
274 23     23 1 689 my ($self, $app, $config) = @_;
275              
276 23         517 $self->{classes} = {
277             'appear' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::AppearIn',
278             '2play' => 'Mojolicious::Plugin::LinkEmbedder::Link::Game::_2play',
279             'beta.dbtv' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Dbtv',
280             'dbtv' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Dbtv',
281             'blip' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Blip',
282             'collegehumor' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Collegehumor',
283             'gist.github' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::GistGithub',
284             'github' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Github',
285             'html' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::HTML',
286             'image' => 'Mojolicious::Plugin::LinkEmbedder::Link::Image',
287             'imgur' => 'Mojolicious::Plugin::LinkEmbedder::Link::Image::Imgur',
288             'ix' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Ix',
289             'metacpan' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Metacpan',
290             'open.spotify' => 'Mojolicious::Plugin::LinkEmbedder::Link::Music::Spotify',
291             'paste.scsys.co' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::PasteScsysCoUk',
292             'pastebin' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Pastebin',
293             'pastie' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Pastie',
294             'ted' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Ted',
295             'text' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text',
296             'twitter' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Twitter',
297             'travis-ci' => 'Mojolicious::Plugin::LinkEmbedder::Link::Text::Travis',
298             'video' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video',
299             'vimeo' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Vimeo',
300             'youtube' => 'Mojolicious::Plugin::LinkEmbedder::Link::Video::Youtube',
301             'xkcd' => 'Mojolicious::Plugin::LinkEmbedder::Link::Image::Xkcd',
302             };
303              
304             $app->helper(
305             embed_link => sub {
306 8 100   8   530 return $self if @_ == 1;
307 7         47 return $self->embed_link(@_);
308             }
309 23         117 );
310              
311 23 50       519 if (my $route = $config->{route}) {
312 23         47 $self->_add_action($app, $route, $config);
313             }
314             }
315              
316             sub _add_action {
317 23     23   30 my ($self, $app, $route, $config) = @_;
318              
319 23   50     120 $config->{max_age} //= 60;
320 23 50       92 $route = $app->routes->route($route) unless ref $route;
321              
322             $route->to(
323             cb => sub {
324 7     7   65932 my $c = shift;
325 7         27 my $url = $c->param('url');
326 7         1463 my $if_none_match = $c->req->headers->if_none_match;
327              
328             $c->delay(
329             sub {
330 7         2283 my ($delay) = @_;
331 7         19 $c->embed_link($url, $delay->begin);
332             },
333             sub {
334 7         525 my ($delay, $link) = @_;
335 7         32 my $err = $link->error;
336              
337 7 100 33     45 if ($err) {
    50          
338 3   50     9 $c->res->code($err->{code} || 500);
339 3   50     50 $c->respond_to(json => {json => $err}, any => {text => $err->{message} || 'Unknown error.'});
340             }
341             elsif ($if_none_match and $if_none_match eq $link->etag) {
342 0         0 $c->res->code(304);
343 0         0 $c->rendered;
344             }
345             else {
346 4 50       21 $c->res->headers->etag($link->etag) if $link->etag;
347 4 50 33     125 $c->res->headers->cache_control("max-age=$config->{max_age}") if !$link->etag and $config->{max_age};
348 4         159 $c->respond_to(json => {json => $link}, any => {text => $link->to_embed});
349             }
350             }
351 7         235 );
352             }
353 23         4172 );
354             }
355              
356             =head1 DISCLAIMER
357              
358             This module might embed javascript from 3rd party services.
359              
360             Any damage caused by either evil DNS takeover or malicious code inside
361             the javascript is not taken into account by this module.
362              
363             If you are aware of any security risks, then please let us know.
364              
365             =head1 COPYRIGHT AND LICENSE
366              
367             Copyright (C) 2014, Jan Henning Thorsen
368              
369             This program is free software, you can redistribute it and/or modify
370             it under the terms of the Artistic License version 2.0.
371              
372             =head1 AUTHOR
373              
374             Jan Henning Thorsen - C
375              
376             Joel Berger, jberger@cpan.org
377              
378             Marcus Ramberg - C
379              
380             =cut
381              
382             1;