File Coverage

blib/lib/LinkEmbedder.pm
Criterion Covered Total %
statement 78 105 74.2
branch 27 52 51.9
condition 19 39 48.7
subroutine 17 23 73.9
pod 3 4 75.0
total 144 223 64.5


line stmt bran cond sub pod time code
1             package LinkEmbedder;
2 31     31   9002400 use Mojo::Base -base;
  31         389  
  31         303  
3              
4 31     31   22026 use LinkEmbedder::Link;
  31         115  
  31         353  
5 31     31   19619 use Mojo::JSON;
  31         730028  
  31         2009  
6 31     31   16857 use Mojo::Loader 'load_class';
  31         52650  
  31         2172  
7 31     31   17656 use Mojo::Promise;
  31         3529933  
  31         258  
8 31     31   22141 use Mojo::UserAgent;
  31         3062096  
  31         358  
9              
10 31     31   3093 use constant TLS => eval { require IO::Socket::SSL; IO::Socket::SSL->VERSION('2.009'); 1 };
  31         81  
  31         68  
  31         132  
  31         1001  
  31         2665  
11              
12 31   50 31   225 use constant DEBUG => $ENV{LINK_EMBEDDER_DEBUG} || 0;
  31         77  
  31         66286  
13              
14             our $VERSION = '1.20';
15              
16             my $PROTOCOL_RE = qr!^(\w+):\w+!i; # Examples: mail:, spotify:, ...
17              
18             has force_secure => sub { $ENV{LINK_EMBEDDER_FORCE_SECURE} || 0 };
19              
20             has ua => sub { Mojo::UserAgent->new->max_redirects(3); };
21              
22             has url_to_link => sub {
23             return {
24             'default' => 'LinkEmbedder::Link::Basic',
25             'dropbox.com' => 'LinkEmbedder::Link::Dropbox',
26             'git.io' => 'LinkEmbedder::Link::Github',
27             'github.com' => 'LinkEmbedder::Link::Github',
28             'google' => 'LinkEmbedder::Link::Google',
29             'goo.gl' => 'LinkEmbedder::Link::Google',
30             'imgur.com' => 'LinkEmbedder::Link::Imgur',
31             'instagram.com' => 'LinkEmbedder::Link::Instagram',
32             'ix.io' => 'LinkEmbedder::Link::Ix',
33             'meet.jit.si' => 'LinkEmbedder::Link::Jitsi',
34             'metacpan.org' => 'LinkEmbedder::Link::Metacpan',
35             'nhl.com' => 'LinkEmbedder::Link::NHL',
36             'paste.opensuse.org' => 'LinkEmbedder::Link::OpenSUSE',
37             'paste.scsys.co.uk' => 'LinkEmbedder::Link::Shadowcat',
38             'pastebin.com' => 'LinkEmbedder::Link::Pastebin',
39             'spotify' => 'LinkEmbedder::Link::Spotify',
40             'ted.com' => 'LinkEmbedder::Link::oEmbed',
41             'travis-ci.org' => 'LinkEmbedder::Link::Travis',
42             'twitter.com' => 'LinkEmbedder::Link::Twitter',
43             'vimeo.com' => 'LinkEmbedder::Link::oEmbed',
44             'xkcd.com' => 'LinkEmbedder::Link::Xkcd',
45             'webex.com' => 'LinkEmbedder::Link::Webex',
46             'whereby.com' => 'LinkEmbedder::Link::Whereby',
47             'youtube.com' => 'LinkEmbedder::Link::oEmbed',
48             'youtu.be' => 'LinkEmbedder::Link::oEmbed',
49             };
50             };
51              
52             sub get {
53 1     1 1 5 my ($self, $args, $cb) = @_;
54              
55             $self->get_p($args)->then(sub {
56 1     1   109 $self->$cb(shift);
57             })->catch(sub {
58 0   0 0   0 my $err = pop // 'Unknown error.';
59 0 0       0 $err = {message => "$err", code => 500} unless ref $err eq 'HASH';
60 0         0 $self->$cb(LinkEmbedder::Link->new(error => $err, force_secure => $self->force_secure));
61 1         3 });
62              
63 1         240 return $self;
64             }
65              
66             sub get_p {
67 11     11 1 3365 my ($self, $args) = @_;
68 11         26 my ($e, $link);
69              
70 11 100       65 $args = ref $args eq 'HASH' ? {%$args} : {url => $args};
71 11         52 $args->{force_secure} = $self->force_secure;
72 11 50 100     143 $args->{url} = Mojo::URL->new($args->{url} || '') unless ref $args->{url};
73 11         2016 $args->{ua} = $self->ua;
74              
75 11   33     109 $link ||= delete $args->{class};
76 11 100 33     95 $link ||= ucfirst $1 if $args->{url} =~ $PROTOCOL_RE;
77 11 100 100     3434 return $self->_invalid_input($args, 'Invalid URL') unless $link or $args->{url}->host;
78              
79 10   66     75 $link ||= _host_in_hash($args->{url}->host, $self->url_to_link);
80 10 100       56 $link = $link =~ /::/ ? $link : "LinkEmbedder::Link::$link";
81 10 100       705 return $self->_invalid_input($args, "Could not find $link") unless _load($link);
82              
83 5         11 warn "[$link] url=$args->{url})\n" if DEBUG;
84 5         29 $link = $link->new($args);
85 5     5   83 return $link->learn_p->then(sub { return $link });
  5         769  
86             }
87              
88             sub serve {
89 8     8 1 140605 my ($self, $c, $args) = @_;
90 8   100     35 my $format = $c->stash('format') || $c->param('format') || 'json';
91 8         1287 my $log_level;
92              
93 8   50     62 $args ||= {url => $c->param('url')};
94 8   50     2141 $log_level = delete $args->{log_level} || 'debug';
95              
96             # Some websites will not render complete pages without a proper User-Agent
97 8         35 my $user_agent = $c->req->headers->user_agent;
98 8 50       276 $self->ua->transactor->name($user_agent) if $user_agent;
99              
100 8         191 $c->render_later;
101             $self->get_p($args)->then(sub {
102 8     8   1779 my $link = shift;
103 8         45 my $err = $link->error;
104              
105 8 100 50     72 $c->stash(status => $err->{code} || 500) if $err;
106 8 100       90 return $c->render(data => $link->html) if $format eq 'html';
107              
108 7 100 50     48 my $json = $err ? {err => $err->{code} || 500} : $link->TO_JSON;
109 7 100       49 return $c->render(json => $json) unless $format eq 'jsonp';
110              
111 4   100     31 my $name = $c->param('callback') || 'oembed';
112 4         493 return $c->render(data => sprintf '%s(%s)', $name, Mojo::JSON::to_json($json));
113 8     0   204 })->catch(sub { $c->reply->exception(shift) });
  0         0  
114              
115 8         13171 return $self;
116             }
117              
118             # This should probably be in a different test-module, but keeping it here for now
119             sub test_ok {
120 0     0 0 0 my ($self, $url, $expect) = @_;
121              
122 0         0 state $n = 0;
123 0 0 0     0 return if $ENV{N} and $ENV{N} ne ++$n;
124              
125             my $subtest = sub {
126 0     0   0 my $link = shift;
127 0         0 my $json = $link->TO_JSON;
128 0 0       0 Test::More::isa_ok($link, $expect->{isa}) if $expect->{isa};
129             Test::More::is(Mojo::DOM->new($json->{html})->children->first->{class}, $expect->{class}, 'class')
130 0 0       0 if $expect->{class};
131 0 0       0 Test::More::unlike($json->{html}, qr{"}, 'avoid double " HTML entities') if $json->{html};
132              
133 0         0 for my $key (sort keys %$expect) {
134 0 0 0     0 next if $key eq 'isa' or $key eq 'class';
135 0 0       0 for my $exp (ref $expect->{$key} eq 'ARRAY' ? @{$expect->{$key}} : ($expect->{$key})) {
  0         0  
136 0 0       0 my $test_name = ref $exp eq 'Regexp' ? 'like' : 'is';
137 0         0 Test::More->can($test_name)->($json->{$key}, $exp, $key);
138             }
139             }
140 0         0 };
141              
142 0         0 my $ok;
143             $self->get_p($url)->then(
144             sub {
145 0     0   0 my $link = shift;
146 0         0 $ok = Test::More::subtest($link->url, sub { $subtest->($link) });
  0         0  
147 0 0       0 Test::More::diag(Test::More::explain($link->TO_JSON)) unless $ok;
148             },
149 0     0   0 sub { Test::More::diag(shift) }
150 0         0 )->wait;
151              
152 0         0 return $ok;
153             }
154              
155             sub _host_in_hash {
156 5     5   54 my ($host, $hash) = @_;
157 5 50       20 return $hash->{$host} if $hash->{$host};
158              
159 5 50       47 $host = $1 if $host =~ m!([^\.]+\.\w+)$!;
160 5 50       22 return $hash->{$host} if $hash->{$host};
161              
162 5 50       64 $host = $1 if $host =~ m!([^\.]+)\.\w+$!;
163 5   33     41 return $hash->{$host} || $hash->{default};
164             }
165              
166             sub _invalid_input {
167 6     6   29 my ($self, $args, $msg) = @_;
168 6         31 $args->{error} = {message => $msg, code => 400};
169 6         48 return Mojo::Promise->new->resolve(LinkEmbedder::Link->new($args));
170             }
171              
172             sub _load {
173 10     10   66 $@ = load_class $_[0];
174 10         4106 warn "[LinkEmbedder] load $_[0]: @{[$@ || 'Success']}\n" if DEBUG;
175 10 50       48 die $@ if ref $@;
176 10 100       69 return $@ ? 0 : 1;
177             }
178              
179             1;
180              
181             =encoding utf8
182              
183             =head1 NAME
184              
185             LinkEmbedder - Embed / expand oEmbed resources and other URL / links
186              
187             =head1 SYNOPSIS
188              
189             use LinkEmbedder;
190              
191             my $embedder = LinkEmbedder->new(force_secure => 1);
192              
193             # In some cases, you have to set a proper user_agent to get complete
194             # pages. This is done automatically by $embedder->serve()
195             $embedder->ua->transactor->name("Mozilla...");
196              
197             $embedder->get_p("https://xkcd.com/927")->then(sub {
198             my $link = shift;
199             print $link->html;
200             })->wait;
201              
202             =head1 DESCRIPTION
203              
204             L is a module that can expand an URL into a rich HTML snippet or
205             simply to extract information about the URL.
206              
207             This module replaces L.
208              
209             Go to L to see a demo of how it works.
210              
211             These web pages are currently supported:
212              
213             =over 2
214              
215             =item * L
216              
217             =item * L
218              
219             =item * L
220              
221             Instagram need some additional JavaScript. Please look at
222             L and
223             L
224             for more information.
225              
226             =item * L
227              
228             =item * L
229              
230             =item * L
231              
232             =item * L
233              
234             =item * L
235              
236             =item * L
237              
238             =item * L
239              
240             =item * L
241              
242             =item * L
243              
244             =item * L
245              
246             =item * L
247              
248             =item * L
249              
250             =item * L
251              
252             =item * L
253              
254             Twitter need some additional JavaScript. Please look at
255             L and
256             L
257             for more information.
258              
259             =item * L
260              
261             =item * L
262              
263             =item * L
264              
265             =item * HTML
266              
267             Any web page will be parsed, and "og:", "twitter:", meta tags and other
268             significant elements will be used to generate a oEmbed response.
269              
270             =item * Images
271              
272             URLs that looks like an image is automatically converted into an img tag.
273              
274             =item * Video
275              
276             URLs that looks like a video resource is automatically converted into a video tag.
277              
278             =back
279              
280             =head1 ATTRIBUTES
281              
282             =head2 force_secure
283              
284             $bool = $self->force_secure;
285             $self = $self->force_secure(1);
286              
287             This attribute will translate any unknown http link to https.
288              
289             This attribute is EXPERIMENTAL. Feeback appreciated.
290              
291             =head2 ua
292              
293             $ua = $self->ua;
294              
295             Holds a L object.
296              
297             =head2 url_to_link
298              
299             $hash_ref = $self->url_to_link;
300              
301             Holds a mapping between host names and L to use.
302              
303             =head1 METHODS
304              
305             =head2 get
306              
307             $self = $self->get_p($url, sub { my ($self, $link) = @_ });
308              
309             Same as L, but takes a callback instead of returning a L.
310              
311             =head2 get_p
312              
313             $promise = $self->get_p($url)->then(sub { my $link = shift });
314              
315             Used to construct a new L object and retrieve information
316             about the URL.
317              
318             =head2 serve
319              
320             $self = $self->serve(Mojolicious::Controller->new, $url);
321              
322             Used as a helper for L web applications to reply to an oEmbed
323             request. Will also set L for L if
324             the incoming request contains a "User-Agent" header.
325              
326             Note that in L 9.11 and later, you must define the format for your
327             route to serve with extension .html, .json or .jsonp.
328              
329             =head1 AUTHOR
330              
331             Jan Henning Thorsen
332              
333             =head1 COPYRIGHT AND LICENSE
334              
335             This program is free software, you can redistribute it and/or modify it under
336             the terms of the Artistic License version 2.0.
337              
338             =cut