File Coverage

blib/lib/LinkEmbedder/Link.pm
Criterion Covered Total %
statement 82 93 88.1
branch 32 54 59.2
condition 7 24 29.1
subroutine 16 18 88.8
pod 2 3 66.6
total 139 192 72.4


line stmt bran cond sub pod time code
1             package LinkEmbedder::Link;
2 31     31   225 use Mojo::Base -base;
  31         67  
  31         209  
3              
4 31     31   23318 use Mojo::Template;
  31         1280537  
  31         343  
5 31     31   1611 use Mojo::Util qw(html_unescape trim);
  31         76  
  31         2284  
6              
7 31   50 31   228 use constant DEBUG => $ENV{LINK_EMBEDDER_DEBUG} || 0;
  31         73  
  31         77101  
8              
9             my %DOM_SEL = (
10             ':desc' => ['meta[property="og:description"]', 'meta[name="twitter:description"]', 'meta[name="description"]'],
11             ':image' => ['meta[property="og:image"]', 'meta[property="og:image:url"]', 'meta[name="twitter:image"]'],
12             ':site_name' => ['meta[property="og:site_name"]', 'meta[property="twitter:site"]'],
13             ':title' => ['meta[property="og:title"]', 'meta[name="twitter:title"]', 'title'],
14             );
15              
16             my @JSON_ATTRS = (
17             'author_name', 'author_url', 'cache_age', 'height', 'provider_name', 'provider_url',
18             'thumbnail_height', 'thumbnail_url', 'thumbnail_width', 'title', 'type', 'url',
19             'version', 'width'
20             );
21              
22             has author_name => undef;
23             has author_url => undef;
24             has cache_age => 0;
25             has description => '';
26             has error => undef; # {message => "", code => ""}
27             has force_secure => 0;
28             has height => sub { $_[0]->type =~ /^photo|video$/ ? 0 : undef };
29             has mimetype => '';
30             has placeholder_url => '';
31              
32             has provider_name => sub {
33             return undef unless my $name = shift->url->host;
34             return $name =~ /([^\.]+)\.(\w+)$/ ? ucfirst $1 : $name;
35             };
36              
37             has provider_url => sub { $_[0]->url->host ? $_[0]->url->clone->path('/') : undef };
38             has template => sub { [__PACKAGE__, sprintf '%s.html.ep', $_[0]->type] };
39             has thumbnail_height => undef;
40             has thumbnail_url => undef;
41             has thumbnail_width => undef;
42             has title => undef;
43             has type => 'link';
44             has ua => undef; # Mojo::UserAgent object
45             has url => sub { Mojo::URL->new }; # Mojo::URL
46             has version => '1.0';
47             has width => sub { $_[0]->type =~ /^photo|video$/ ? 0 : undef };
48              
49             sub html {
50 7     7 1 2520 my $self = shift;
51 7 50       16 my $template = Mojo::Loader::data_section(@{$self->template}) or return '';
  7         29  
52 7         1515 my $output = Mojo::Template->new({auto_escape => 1, prepend => 'my $l=shift'})->render($template, $self);
53 7 50       29925 die $output if ref $output;
54 7         54 return $output;
55             }
56              
57             sub learn_p {
58 5     5 1 11 my $self = shift;
59 5     5   19 return $self->_get_p($self->url)->then(sub { $self->_learn(shift) });
  5         708  
60             }
61              
62             sub TO_JSON {
63 5     5 0 11 my $self = shift;
64 5         13 my %json;
65              
66 5         17 for my $attr (grep { defined $self->$_ } @JSON_ATTRS) {
  70         1151  
67 32         2026 $json{$attr} = $self->$attr;
68 32 100       224 $json{$attr} = "$json{$attr}" if $attr =~ /url$/;
69             }
70              
71 5 100       20 $json{html} = $self->html unless $self->type eq 'link';
72              
73 5         36 return \%json;
74             }
75              
76 0     0   0 sub _dump { Mojo::Util::dumper($_[0]->TO_JSON); }
77              
78             sub _el {
79 28     28   70 my ($self, $dom, @sel) = @_;
80 28 100       86 @sel = @{$DOM_SEL{$sel[0]}} if $DOM_SEL{$sel[0]};
  12         40  
81              
82 28         54 for my $sel (@sel) {
83 52 100       7648 my $e = $dom->at($sel) or next;
84 4   100     1074 my ($val) = grep {$_} map { trim($_ // '') } $e->{content}, $e->{value}, $e->{href}, $e->text, $e->all_text;
  20         64  
  20         669  
85 4 50       33 return html_unescape $val if defined $val;
86             }
87              
88 24         8644 return '';
89             }
90              
91             sub _get_p {
92 5     5   48 my ($self, $url) = @_;
93 5 50       27 $url = $url->clone->scheme('https') if $self->force_secure;
94 5         30 warn sprintf "[%s] GET %s\n", ref($self), $url if DEBUG;
95             return $self->ua->get_p($url)->then(sub {
96 5     5   42115 my $tx = shift;
97 5 50 33     32 $self->url->scheme('https') if $self->force_secure and $tx->res->is_success;
98 5         55 return $tx;
99 5         23 });
100             }
101              
102             sub _learn {
103 5     5   14 my ($self, $tx) = @_;
104 5         19 my $h = $tx->res->headers;
105              
106 5         95 my $name = $h->header('X-Provider-Name');
107 5 100       62 $self->provider_name($name) if $name;
108              
109 5   50     23 my $ct = $h->content_type || '';
110 5 50       68 $self->type('photo')->_learn_from_url if $ct =~ m!^image/!;
111 5 50       21 $self->type('video')->_learn_from_url if $ct =~ m!^video/!;
112 5 100       23 $self->type('rich')->_learn_from_text($tx) if $ct =~ m!^text/plain!;
113 5 100       50 $self->type('rich')->_learn_from_dom($tx->res->dom) if $ct =~ m!^text/html!;
114 5         44 my $p = $self->_maybe_rebless_p($tx);
115              
116 5   33     53 return $p || $self;
117             }
118              
119             sub _learn_from_dom {
120 4     4   14 my ($self, $dom) = @_;
121 4         8 my $v;
122              
123 4 50       18 $self->author_name($v) if $v = $self->_el($dom, '[itemprop="author"] [itemprop="name"]');
124 4 50       16 $self->author_url($v) if $v = $self->_el($dom, '[itemprop="author"] [itemprop="email"]');
125 4 50       16 $self->description($v) if $v = $self->_el($dom, ':desc');
126 4 50       19 $self->thumbnail_height($v) if $v = $self->_el($dom, 'meta[property="og:image:height"]');
127 4 50       15 $self->thumbnail_url($v) if $v = $self->_el($dom, ':image');
128 4 50       17 $self->thumbnail_width($v) if $v = $self->_el($dom, 'meta[property="og:image:width"]');
129 4 50       14 $self->title($v) if $v = $self->_el($dom, ':title');
130              
131 4         135 return $self;
132             }
133              
134             sub _learn_from_json {
135 0     0   0 my ($self, $tx) = @_;
136 0         0 my $json = $tx->res->json;
137              
138 0         0 warn "[LinkEmbedder] " . $tx->res->text . "\n" if DEBUG;
139 0   0     0 $self->{$_} ||= $json->{$_} for keys %$json;
140 0 0 0     0 $self->{error} = {message => $self->{error}} if defined $self->{error} and !ref $self->{error};
141 0 0 0     0 $self->{error}{code} = $self->{status} if $self->{status} and $self->{status} =~ /^\d+$/;
142              
143 0         0 return $self;
144             }
145              
146             sub _learn_from_text {
147 1     1   15 my ($self, $tx) = @_;
148 1         8 $self->_learn_from_url;
149              
150 1         77 $self->{paste} = $tx->res->text;
151 1         73 $self->template->[1] = 'paste.html.ep';
152              
153 1         12 my $title = substr $self->{paste}, 0, 20;
154 1         4 $title =~ s![\r\n]+! !g;
155 1         4 $self->title($title);
156             }
157              
158             sub _learn_from_url {
159 1     1   3 my $self = shift;
160 1         4 my $path = $self->url->path;
161              
162 1 50       18 return $self->title(@$path ? $path->[-1] : 'Image');
163             }
164              
165             # TODO: Not sure if this is the best solution
166             sub _maybe_rebless_p {
167 5     5   28 my ($self, $tx) = @_;
168 5 50       23 return unless ref $self eq 'LinkEmbedder::Link::Basic';
169 5 50       23 return unless $self->type eq 'rich';
170              
171 5 50 33     51 if ($self->title eq 'Jitsi Meet' or $tx->res->body =~ m!\bJitsiMeetJS\b!) {
172 0         0 require LinkEmbedder::Link::Jitsi;
173 0         0 bless $self, 'LinkEmbedder::Link::Jitsi';
174 0         0 return $self->learn_p;
175             }
176              
177 5         291 return;
178             }
179              
180             1;
181              
182             =encoding utf8
183              
184             =head1 NAME
185              
186             LinkEmbedder::Link - Meta information for an URL
187              
188             =head1 SYNOPSIS
189              
190             See L.
191              
192             =head1 DESCRIPTION
193              
194             L is a class representing an expanded URL.
195              
196             =head1 ATTRIBUTES
197              
198             =head2 author_name
199              
200             $str = $self->author_name;
201              
202             Might hold the name of the author of L.
203              
204             =head2 author_url
205              
206             $str = $self->author_name;
207              
208             Might hold an URL to the author.
209              
210             =head2 cache_age
211              
212             $int = $self->cache_age;
213              
214             The suggested cache lifetime for this resource, in seconds.
215              
216             =head2 description
217              
218             $str = $self->description;
219              
220             Description of the L. Might be C.
221              
222             =head2 error
223              
224             $hash_ref = $self->author_name;
225              
226             C on success, hash-ref on error. Example:
227              
228             {message => "Oops!", code => 500};
229              
230             =head2 force_secure
231              
232             $bool = $self->force_secure;
233             $self = $self->force_secure(1);
234              
235             This attribute will translate any unknown http link to https.
236              
237             This attribute is EXPERIMENTAL. Feeback appreciated.
238              
239             =head2 height
240              
241             $int = $self->height;
242              
243             The height of L in pixels. Might be C.
244              
245             =head2 mimetype
246              
247             $str = $self->mimetype;
248              
249             =head2 provider_name
250              
251             $str = $self->provider_name;
252              
253             Name of the provider of L.
254              
255             =head2 provider_url
256              
257             $str = $self->provider_name;
258              
259             Main URL to the provider's home page.
260              
261             =head2 template
262              
263             $array_ref = $self->provider_name;
264              
265             Used to figure out which template to use to render L. Example:
266              
267             ["LinkEmbedder::Link", "rich.html.ep];
268              
269             =head2 thumbnail_height
270              
271             $int = $self->thumbnail_height;
272              
273             The height of the L in pixels. Might be C.
274              
275             =head2 thumbnail_url
276              
277             $str = $self->thumbnail_url;
278              
279             URL to the thumbnail which can be used in L.
280              
281             =head2 thumbnail_width
282              
283             $int = $self->thumbnail_width;
284              
285             The width of the L in pixels. Might be C.
286              
287             =head2 title
288              
289             $str = $self->title;
290              
291             Title/heading of the L. Might be C.
292              
293             =head2 type
294              
295             $str = $self->title;
296              
297             oEmbed type of URL: link, photo, rich or video.
298              
299             =head2 ua
300              
301             $ua = $self->ua;
302              
303             Holds a L object.
304              
305             =head2 url
306              
307             $str = $self->url;
308              
309             The resource to fetch.
310              
311             =head2 version
312              
313             $str = $self->version;
314              
315             oEmbed version. Example: "1.0".
316              
317             =head2 width
318              
319             $int = $self->width;
320              
321             The width in pixels. Might be C.
322              
323             =head1 METHODS
324              
325             =head2 html
326              
327             $str = $self->html;
328              
329             Returns the L as rich markup, if possible.
330              
331             =head2 learn_p
332              
333             $promise = $self->learn_p->then(sub { my $self = shift; });
334              
335             Used to learn about the L.
336              
337             =head1 AUTHOR
338              
339             Jan Henning Thorsen
340              
341             =head1 SEE ALSO
342              
343             L
344              
345             =cut
346              
347             __DATA__