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