File Coverage

blib/lib/Mojolicious/Plugin/LinkEmbedder.pm
Criterion Covered Total %
statement 44 89 49.4
branch 9 52 17.3
condition 6 35 17.1
subroutine 10 16 62.5
pod 2 2 100.0
total 71 194 36.6


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