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