File Coverage

blib/lib/LinkEmbedder.pm
Criterion Covered Total %
statement 78 102 76.4
branch 27 48 56.2
condition 19 36 52.7
subroutine 17 23 73.9
pod 3 4 75.0
total 144 213 67.6


line stmt bran cond sub pod time code
1             package LinkEmbedder;
2 31     31   8882249 use Mojo::Base -base;
  31         370  
  31         293  
3              
4 31     31   22328 use LinkEmbedder::Link;
  31         115  
  31         328  
5 31     31   18711 use Mojo::JSON;
  31         719405  
  31         2014  
6 31     31   16670 use Mojo::Loader 'load_class';
  31         52073  
  31         2138  
7 31     31   17074 use Mojo::Promise;
  31         3458577  
  31         258  
8 31     31   21132 use Mojo::UserAgent;
  31         3009011  
  31         358  
9              
10 31     31   3032 use constant TLS => eval { require IO::Socket::SSL; IO::Socket::SSL->VERSION('2.009'); 1 };
  31         78  
  31         67  
  31         136  
  31         949  
  31         2721  
11              
12 31   50 31   218 use constant DEBUG => $ENV{LINK_EMBEDDER_DEBUG} || 0;
  31         70  
  31         62969  
13              
14             our $VERSION = '1.19';
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 4 my ($self, $args, $cb) = @_;
54              
55             $self->get_p($args)->then(sub {
56 1     1   111 $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         239 return $self;
64             }
65              
66             sub get_p {
67 11     11 1 3392 my ($self, $args) = @_;
68 11         26 my ($e, $link);
69              
70 11 100       68 $args = ref $args eq 'HASH' ? {%$args} : {url => $args};
71 11         68 $args->{force_secure} = $self->force_secure;
72 11 50 100     139 $args->{url} = Mojo::URL->new($args->{url} || '') unless ref $args->{url};
73 11         2095 $args->{ua} = $self->ua;
74              
75 11   33     109 $link ||= delete $args->{class};
76 11 100 33     101 $link ||= ucfirst $1 if $args->{url} =~ $PROTOCOL_RE;
77 11 100 100     3290 return $self->_invalid_input($args, 'Invalid URL') unless $link or $args->{url}->host;
78              
79 10   66     71 $link ||= _host_in_hash($args->{url}->host, $self->url_to_link);
80 10 100       52 $link = $link =~ /::/ ? $link : "LinkEmbedder::Link::$link";
81 10 100       46 return $self->_invalid_input($args, "Could not find $link") unless _load($link);
82              
83 5         14 warn "[$link] url=$args->{url})\n" if DEBUG;
84 5         32 $link = $link->new($args);
85 5     5   87 return $link->learn_p->then(sub { return $link });
  5         891  
86             }
87              
88             sub serve {
89 8     8 1 131506 my ($self, $c, $args) = @_;
90 8   100     38 my $format = $c->stash('format') || $c->param('format') || 'json';
91 8         1302 my $log_level;
92              
93 8   50     61 $args ||= {url => $c->param('url')};
94 8   50     2038 $log_level = delete $args->{log_level} || 'debug';
95              
96             # Some websites will not render complete pages without a proper User-Agent
97 8         38 my $user_agent = $c->req->headers->user_agent;
98 8 50       252 $self->ua->transactor->name($user_agent) if $user_agent;
99              
100 8         202 $c->render_later;
101             $self->get_p($args)->then(sub {
102 8     8   1637 my $link = shift;
103 8         49 my $err = $link->error;
104              
105 8 100 50     127 $c->stash(status => $err->{code} || 500) if $err;
106 8 100       87 return $c->render(data => $link->html) if $format eq 'html';
107              
108 7 100 50     47 my $json = $err ? {err => $err->{code} || 500} : $link->TO_JSON;
109 7 100       57 return $c->render(json => $json) unless $format eq 'jsonp';
110              
111 4   100     34 my $name = $c->param('callback') || 'oembed';
112 4         479 return $c->render(data => sprintf '%s(%s)', $name, Mojo::JSON::to_json($json));
113 8     0   203 })->catch(sub { $c->reply->exception(shift) });
  0         0  
114              
115 8         11777 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             my $subtest = sub {
123 0     0   0 my $link = shift;
124 0         0 my $json = $link->TO_JSON;
125 0 0       0 Test::More::isa_ok($link, $expect->{isa}) if $expect->{isa};
126             Test::More::is(Mojo::DOM->new($json->{html})->children->first->{class}, $expect->{class}, 'class')
127 0 0       0 if $expect->{class};
128              
129 0         0 for my $key (sort keys %$expect) {
130 0 0 0     0 next if $key eq 'isa' or $key eq 'class';
131 0 0       0 for my $exp (ref $expect->{$key} eq 'ARRAY' ? @{$expect->{$key}} : ($expect->{$key})) {
  0         0  
132 0 0       0 my $test_name = ref $exp eq 'Regexp' ? 'like' : 'is';
133 0         0 Test::More->can($test_name)->($json->{$key}, $exp, $key);
134             }
135             }
136 0         0 };
137              
138 0         0 my $ok;
139             $self->get_p($url)->then(
140             sub {
141 0     0   0 my $link = shift;
142 0         0 $ok = Test::More::subtest($link->url, sub { $subtest->($link) });
  0         0  
143 0 0       0 Test::More::diag(Test::More::explain($link->TO_JSON)) unless $ok;
144             },
145 0     0   0 sub { Test::More::diag(shift) }
146 0         0 )->wait;
147              
148 0         0 return $ok;
149             }
150              
151             sub _host_in_hash {
152 5     5   57 my ($host, $hash) = @_;
153 5 50       25 return $hash->{$host} if $hash->{$host};
154              
155 5 50       45 $host = $1 if $host =~ m!([^\.]+\.\w+)$!;
156 5 50       23 return $hash->{$host} if $hash->{$host};
157              
158 5 50       30 $host = $1 if $host =~ m!([^\.]+)\.\w+$!;
159 5   33     43 return $hash->{$host} || $hash->{default};
160             }
161              
162             sub _invalid_input {
163 6     6   27 my ($self, $args, $msg) = @_;
164 6         35 $args->{error} = {message => $msg, code => 400};
165 6         50 return Mojo::Promise->new->resolve(LinkEmbedder::Link->new($args));
166             }
167              
168             sub _load {
169 10     10   63 $@ = load_class $_[0];
170 10         4014 warn "[LinkEmbedder] load $_[0]: @{[$@ || 'Success']}\n" if DEBUG;
171 10 50       41 die $@ if ref $@;
172 10 100       65 return $@ ? 0 : 1;
173             }
174              
175             1;
176              
177             =encoding utf8
178              
179             =head1 NAME
180              
181             LinkEmbedder - Embed / expand oEmbed resources and other URL / links
182              
183             =head1 SYNOPSIS
184              
185             use LinkEmbedder;
186              
187             my $embedder = LinkEmbedder->new(force_secure => 1);
188              
189             # In some cases, you have to set a proper user_agent to get complete
190             # pages. This is done automatically by $embedder->serve()
191             $embedder->ua->transactor->name("Mozilla...");
192              
193             $embedder->get_p("https://xkcd.com/927")->then(sub {
194             my $link = shift;
195             print $link->html;
196             })->wait;
197              
198             =head1 DESCRIPTION
199              
200             L is a module that can expand an URL into a rich HTML snippet or
201             simply to extract information about the URL.
202              
203             This module replaces L.
204              
205             Go to L to see a demo of how it works.
206              
207             These web pages are currently supported:
208              
209             =over 2
210              
211             =item * L
212              
213             =item * L
214              
215             =item * L
216              
217             Instagram need some additional JavaScript. Please look at
218             L and
219             L
220             for more information.
221              
222             =item * L
223              
224             =item * L
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             Twitter need some additional JavaScript. Please look at
251             L and
252             L
253             for more information.
254              
255             =item * L
256              
257             =item * L
258              
259             =item * L
260              
261             =item * HTML
262              
263             Any web page will be parsed, and "og:", "twitter:", meta tags and other
264             significant elements will be used to generate a oEmbed response.
265              
266             =item * Images
267              
268             URLs that looks like an image is automatically converted into an img tag.
269              
270             =item * Video
271              
272             URLs that looks like a video resource is automatically converted into a video tag.
273              
274             =back
275              
276             =head1 ATTRIBUTES
277              
278             =head2 force_secure
279              
280             $bool = $self->force_secure;
281             $self = $self->force_secure(1);
282              
283             This attribute will translate any unknown http link to https.
284              
285             This attribute is EXPERIMENTAL. Feeback appreciated.
286              
287             =head2 ua
288              
289             $ua = $self->ua;
290              
291             Holds a L object.
292              
293             =head2 url_to_link
294              
295             $hash_ref = $self->url_to_link;
296              
297             Holds a mapping between host names and L to use.
298              
299             =head1 METHODS
300              
301             =head2 get
302              
303             $self = $self->get_p($url, sub { my ($self, $link) = @_ });
304              
305             Same as L, but takes a callback instead of returning a L.
306              
307             =head2 get_p
308              
309             $promise = $self->get_p($url)->then(sub { my $link = shift });
310              
311             Used to construct a new L object and retrieve information
312             about the URL.
313              
314             =head2 serve
315              
316             $self = $self->serve(Mojolicious::Controller->new, $url);
317              
318             Used as a helper for L web applications to reply to an oEmbed
319             request. Will also set L for L if
320             the incoming request contains a "User-Agent" header.
321              
322             Note that in L 9.11 and later, you must define the format for your
323             route to serve with extension .html, .json or .jsonp.
324              
325             =head1 AUTHOR
326              
327             Jan Henning Thorsen
328              
329             =head1 COPYRIGHT AND LICENSE
330              
331             This program is free software, you can redistribute it and/or modify it under
332             the terms of the Artistic License version 2.0.
333              
334             =cut