File Coverage

blib/lib/Web/Mention.pm
Criterion Covered Total %
statement 263 273 96.3
branch 87 108 80.5
condition 15 21 71.4
subroutine 44 45 97.7
pod 6 9 66.6
total 415 456 91.0


line stmt bran cond sub pod time code
1             package Web::Mention;
2              
3 7     7   9784 use Moo;
  7         72868  
  7         38  
4 7     7   13624 use MooX::ClassAttribute;
  7         126918  
  7         43  
5 7     7   5014 use MooX::Enumeration;
  7         18071  
  7         31  
6 7     7   4937 use Types::Standard qw(InstanceOf Maybe Str Bool Num Enum);
  7         526050  
  7         86  
7 7     7   13112 use LWP;
  7         224791  
  7         252  
8 7     7   3310 use HTTP::Link;
  7         67452  
  7         376  
9 7     7   6347 use DateTime;
  7         3522287  
  7         422  
10 7     7   4375 use String::Truncate qw(elide);
  7         86420  
  7         71  
11 7     7   1908 use Try::Tiny;
  7         19  
  7         437  
12 7     7   82 use Types::Standard qw(Enum);
  7         79  
  7         132  
13 7     7   5210 use Scalar::Util qw(blessed);
  7         18  
  7         436  
14 7     7   42 use Carp qw(carp croak);
  7         20  
  7         305  
15 7     7   4360 use Mojo::DOM58;
  7         216199  
  7         374  
16 7     7   74 use URI::Escape;
  7         11  
  7         449  
17 7     7   4440 use Encode qw(decode_utf8);
  7         77628  
  7         584  
18 7     7   3727 use Readonly;
  7         28301  
  7         383  
19 7     7   4252 use DateTime::Format::ISO8601;
  7         654828  
  7         419  
20 7     7   5080 use JSON;
  7         51388  
  7         50  
21              
22 7     7   4960 use Web::Microformats2::Parser;
  7         1125897  
  7         342  
23 7     7   3667 use Web::Mention::Author;
  7         34  
  7         23618  
24              
25             our $VERSION = '0.720';
26              
27             Readonly my @VALID_RSVP_TYPES => qw(yes no maybe interested);
28              
29             has 'source' => (
30             isa => InstanceOf['URI'],
31             is => 'ro',
32             required => 1,
33             coerce => sub { URI->new($_[0]) },
34             );
35              
36             has 'original_source' => (
37             isa => InstanceOf['URI'],
38             is => 'lazy',
39             coerce => sub { URI->new($_[0]) },
40             clearer => '_clear_original_source',
41             );
42              
43             has 'source_html' => (
44             isa => Maybe[Str],
45             is => 'rw',
46             );
47              
48             has 'source_mf2_document' => (
49             isa => Maybe[InstanceOf['Web::Microformats2::Document']],
50             is => 'rw',
51             lazy => 1,
52             builder => '_build_source_mf2_document',
53             clearer => '_clear_mf2',
54             );
55              
56             has 'target' => (
57             isa => InstanceOf['URI'],
58             is => 'ro',
59             required => 1,
60             coerce => sub { URI->new($_[0]) },
61             );
62              
63             has 'endpoint' => (
64             isa => Maybe[InstanceOf['URI']],
65             is => 'lazy',
66             );
67              
68             has 'is_tested' => (
69             isa => Bool,
70             is => 'rw',
71             default => 0,
72             );
73              
74             has 'is_verified' => (
75             isa => Bool,
76             is => 'lazy',
77             );
78              
79             has 'time_verified' => (
80             isa => InstanceOf['DateTime'],
81             is => 'rw',
82             );
83              
84             has 'time_received' => (
85             isa => InstanceOf['DateTime'],
86             is => 'ro',
87             default => sub{ DateTime->now },
88             );
89              
90             has 'time_published' => (
91             isa => InstanceOf['DateTime'],
92             is => 'rw',
93             lazy => 1,
94             builder => '_build_time_published',
95             );
96              
97             has 'rsvp_type' => (
98             isa => Maybe[Str],
99             is => 'lazy',
100             );
101              
102             has 'author' => (
103             isa => InstanceOf['Web::Mention::Author'],
104             is => 'lazy',
105             clearer => '_clear_author',
106             );
107              
108             has 'type' => (
109             isa => Maybe[Enum[qw(rsvp reply like repost quotation mention)]],
110             traits => ['Enumeration'],
111             handles => [qw(is_rsvp is_reply is_like is_repost is_quotation is_mention)],
112             is => 'lazy',
113             clearer => '_clear_type',
114             );
115              
116             has 'content' => (
117             isa => Maybe[Str],
118             is => 'lazy',
119             clearer => '_clear_content',
120             );
121              
122             has 'title' => (
123             isa => Maybe[Str],
124             is => 'lazy',
125             clearer => '_clear_title',
126             );
127              
128             has 'response' => (
129             isa => Maybe[InstanceOf['HTTP::Response']],
130             is => 'rw',
131             clearer => '_clear_response',
132             );
133              
134             class_has 'ua' => (
135             isa => InstanceOf['LWP::UserAgent'],
136             is => 'rw',
137             default => sub {
138             # Set the user-agent string to e.g. "Web::Mention/0.711"
139             LWP::UserAgent->new( agent => "$_[0]/$VERSION" );
140             },
141             );
142              
143             class_has 'max_content_length' => (
144             isa => Num,
145             is => 'rw',
146             default => 280,
147             );
148              
149             class_has 'content_truncation_marker' => (
150             isa => Str,
151             is => 'rw',
152             default => '...',
153             );
154              
155             sub _build_is_verified {
156 26     26   961 my $self = shift;
157              
158 26         85 return $self->verify;
159             }
160              
161             sub BUILD {
162 41     41 0 14992 my $self = shift;
163              
164 41         324 my $source = $self->source->clone;
165 41         846 my $target = $self->target->clone;
166              
167 41         557 foreach ( $source, $target ) {
168 82         648 $_->fragment( undef );
169             }
170              
171 41 100       491 if ( $source->eq( $target ) ) {
172 2         270 die "Inavlid webmention; source and target have the same URL "
173             . "($source)\n";
174             }
175             }
176              
177             sub new_from_request {
178 1     1 1 528 my $class = shift;
179              
180 1         4 my ( $request ) = @_;
181              
182 1 50 33     19 unless ( blessed($request) && $request->can('param') ) {
183 0         0 croak 'The argument to new_from_request must be an object that '
184             . "supports a param() method. (Got: $request)\n";
185             }
186              
187 1         4 my @complaints;
188             my %new_args;
189 1         4 foreach ( qw(source target) ) {
190 2 50       7 if ( my $value = $request->param( $_ ) ) {
191 2         22 $new_args{ $_ } = $value;
192             }
193              
194 2 50       8 unless ( defined $new_args{ $_ } ) {
195 0         0 push @complaints, "No param value set for $_.";
196             }
197             }
198              
199 1 50       6 if ( @complaints ) {
200 0         0 croak join q{ }, @complaints;
201             }
202              
203 1         29 return $class->new( %new_args );
204             }
205              
206             sub new_from_html {
207 3     3 1 3906 my $class = shift;
208              
209 3         18 my %args = @_;
210 3         11 my $source = $args{ source };
211 3         10 my $html = $args{ html };
212              
213 3 50       18 unless ($source) {
214 0         0 croak "You must define a source URL when calling new_from_html.";
215             }
216              
217 3         37 my @webmentions;
218              
219 3         37 my $dom = Mojo::DOM58->new( $html );
220 3         5417 my $nodes_ref = $dom->find( 'a[href]' );
221 3         2964 for my $node ( @$nodes_ref ) {
222 10         383 push @webmentions,
223             $class->new( source => $source, target => $node->attr( 'href' ) );
224             }
225              
226 3         350 return @webmentions;
227             }
228              
229              
230             sub verify {
231 27     27 1 1053 my $self = shift;
232              
233 27         470 $self->is_tested(1);
234 27         1294 my $response = $self->ua->get( $self->source );
235              
236             # Search for both plain and escaped ("percent-encoded") versions of the
237             # target URL in the source doc. We search for the latter to account for
238             # sites like Tumblr, who treat outgoing hyperlinks as weird internally-
239             # pointing links that pass external URLs as query-string parameters.
240 27         226858 my $target = "$self->target";
241 27 100 100     128 if ( ($response->content =~ $self->target)
242             || ($response->content =~ uri_escape( $self->target ) )
243             ) {
244 25         1293 $self->time_verified( DateTime->now );
245 25         10166 $self->source_html( $response->decoded_content );
246 25         46900 $self->_clear_mf2;
247 25         648 $self->_clear_content;
248 25         535 $self->_clear_title;
249 25         545 $self->_clear_author;
250 25         527 $self->_clear_type;
251 25         674 return 1;
252             }
253             else {
254 2         187 return 0;
255             }
256             }
257              
258             sub send {
259 8     8 1 1798 my $self = shift;
260              
261 8         164 my $endpoint = $self->endpoint;
262 8         247 my $source = $self->source;
263 8         18 my $target = $self->target;
264              
265 8 100       22 unless ( $endpoint ) {
266 3         59 $self->_clear_response;
267 3         47 return 0;
268             }
269              
270             # Step three: send the webmention to the target!
271 5         70 my $request = HTTP::Request->new( POST => $endpoint );
272 5         322 $request->content_type('application/x-www-form-urlencoded');
273 5         104 $request->content("source=$source&target=$target");
274              
275 5         318 my $response = $self->ua->request($request);
276 5         4884 $self->response( $response );
277              
278 5         168 return $response->is_success;
279             }
280              
281             sub _build_source_mf2_document {
282 23     23   219 my $self = shift;
283              
284 23 100       393 return unless $self->is_verified;
285 22         594 my $doc;
286             try {
287 22     22   1963 my $parser = Web::Microformats2::Parser->new;
288 22         783 $doc = $parser->parse(
289             $self->source_html,
290             url_context => $self->source,
291             );
292             }
293             catch {
294 0     0   0 die "Error parsing source HTML: $_";
295 22         251 };
296 22         351616 return $doc;
297             }
298              
299             sub _build_author {
300 5     5   2454 my $self = shift;
301              
302 5 100       82 if ( $self->source_mf2_document ) {
303 4         180 return Web::Mention::Author->new_from_mf2_document(
304             $self->source_mf2_document
305             );
306             }
307             else {
308 1         59 return Web::Mention::Author->new;
309             }
310             }
311              
312             sub _build_type {
313 10     10   10519 my $self = shift;
314              
315 10 50       180 unless ( $self->source_mf2_document ) {
316 0         0 return 'mention';
317             }
318              
319 10         630 my $item = $self->source_mf2_document->get_first( 'h-entry' );
320 10 50       1774 return 'mention' unless $item;
321              
322             # This order comes from the W3C Post Type Detection algorithm:
323             # https://www.w3.org/TR/post-type-discovery/#response-algorithm
324             # ...except adding 'quotation' as a final allowed type, before
325             # defaulting to 'mention'.
326              
327 10 100 66     221 if ( $self->rsvp_type
    100          
    100          
    100          
    100          
328             && $self->_check_url_property( $item, 'in-reply-to' ) ) {
329 1         23 return 'rsvp';
330             }
331             elsif ( $self->_check_url_property( $item, 'repost-of' )) {
332 1         21 return 'repost';
333             }
334             elsif ( $self->_check_url_property( $item, 'like-of' ) ) {
335 2         39 return 'like';
336             }
337             elsif ( $self->_check_url_property( $item, 'in-reply-to' ) ) {
338 2         41 return 'reply';
339             }
340             elsif ( $self->_check_url_property( $item, 'quotation-of' )) {
341 1         19 return 'quotation';
342             }
343             else {
344 3         68 return 'mention';
345             }
346             }
347              
348             sub _build_content {
349 10     10   376 my $self = shift;
350              
351             # If the source page has MF2 data *and* an h-entry,
352             # then we apply the algorithm outlined at:
353             # https://indieweb.org/comments#How_to_display
354             #
355             # Otherwise, we can't extract any semantic information about it,
356             # so we'll just offer the page's title, if there is one.
357              
358 10         16 my $item;
359 10 50       162 if ( $self->source_mf2_document ) {
360 10         458 $item = $self->source_mf2_document->get_first( 'h-entry' );
361             }
362              
363 10 100       1670 unless ( $item ) {
364 1         6 return $self->_title_element_content;
365             }
366              
367 9         18 my $raw_content;
368 9 100       26 if ( $item->get_property( 'content' ) ) {
369 7         108 $raw_content = $item->get_property( 'content' )->{ value };
370             }
371 9 100       146 if ( defined $raw_content ) {
372 7 100       144 if ( length $raw_content <= $self->max_content_length ) {
373 2         44 return $raw_content;
374             }
375             }
376              
377 7 100       48 if ( my $summary = $item->get_property( 'summary' ) ) {
378 3         41 return $self->_truncate_content( $summary );
379             }
380              
381 4 100       54 if ( defined $raw_content ) {
382 2         8 return $self->_truncate_content( $raw_content );
383             }
384              
385 2 50       7 if ( my $name = $item->get_property( 'name' ) ) {
386 2         31 return $self->_truncate_content( $name );
387             }
388              
389 0         0 return $self->_truncate_content( $item->value );
390             }
391              
392             sub _build_rsvp_type {
393 10     10   133 my $self = shift;
394              
395 10         19 my $rsvp_type;
396 10 50       165 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
397 10 100       1451 if ( my $rsvp_property = $item->get_property( 'rsvp' ) ) {
398 1 50       59 if ( grep { $_ eq lc $rsvp_property } @VALID_RSVP_TYPES ) {
  4         42  
399 1         10 $rsvp_type = $rsvp_property;
400             }
401             }
402             }
403              
404 10         358 return $rsvp_type;
405             }
406              
407             sub _check_url_property {
408 28     28   394 my $self = shift;
409 28         60 my ( $item, $property ) = @_;
410              
411 28         74 my $urls_ref = $item->get_properties( $property );
412 28         173 my $found = 0;
413              
414 28         55 for my $url_prop ( @$urls_ref ) {
415 28         67 my $url;
416 28 100 66     106 if ( blessed($url_prop) && $url_prop->isa('Web::Microformats2::Item') ) {
417 3         60 $url = $url_prop->value;
418             }
419             else {
420 25         40 $url = $url_prop;
421             }
422              
423 28 100       126 if ( $url eq $self->target ) {
424 7         43 $found = 1;
425 7         15 last;
426             }
427             }
428              
429 28         184 return $found;
430             }
431              
432             sub _truncate_content {
433 8     8   14 my $self = shift;
434 8         15 my ( $content ) = @_;
435 8 50       17 unless ( defined $content ) {
436 0         0 $content = q{};
437             }
438              
439 8         138 return elide(
440             $content,
441             $self->max_content_length,
442             {
443             at_space => 1,
444             marker => $self->content_truncation_marker,
445             },
446             );
447             }
448              
449             sub _build_original_source {
450 2     2   127 my $self = shift;
451              
452 2 50       36 if ( $self->source_mf2_document ) {
453 2 100       117 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
454 1 50       186 if ( my $url = $item->get_property( 'url' ) ) {
455 1         26 return $url;
456             }
457             }
458             }
459              
460 1         144 return $self->source;
461             }
462              
463             sub _build_time_published {
464 2     2   1970 my $self = shift;
465              
466 2 50       35 if ( $self->source_mf2_document ) {
467 2 100       82 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
468 1 50       156 if ( my $time = $item->get_property( 'published' ) ) {
469 1         16 my $dt;
470             try {
471 1     1   67 $dt = DateTime::Format::ISO8601->parse_datetime( $time );
472 1         9 };
473 1 50       1106 return $dt if $dt;
474             }
475             }
476             }
477              
478 1         92 return $self->time_received;
479             }
480              
481             sub _build_endpoint {
482 8     8   92 my $self = shift;
483              
484 8         11 my $endpoint;
485 8         18 my $source = $self->source;
486 8         16 my $target = $self->target;
487              
488             # Is it in the Link HTTP header?
489 8         122 my $response = $self->ua->get( $target );
490 8 100       16398 if ( $response->header( 'Link' ) ) {
491 2         103 my @header_links = HTTP::Link->parse( $response->header( 'Link' ) . '' );
492 2         578 foreach (@header_links ) {
493 2         5 my $relation = $_->{relation};
494 2 100 66     13 if ($relation && $relation eq 'webmention') {
495 1         4 $endpoint = $_->{iri};
496             }
497             }
498             }
499              
500             # Is it in the HTML?
501 8 100       347 unless ( $endpoint ) {
502 7 100       25 if ( $response->header( 'Content-type' ) =~ m{^text/html\b} ) {
503 6         279 my $dom = Mojo::DOM58->new( $response->decoded_content );
504 6         10223 my $nodes_ref = $dom->find(
505             'link[rel~="webmention"], a[rel~="webmention"]'
506             );
507 6         3379 for my $node (@$nodes_ref) {
508 5         18 $endpoint = $node->attr( 'href' );
509 5 50       131 last if defined $endpoint;
510             }
511             }
512             }
513              
514 8 100       141 return undef unless defined $endpoint;
515              
516 6         22 $endpoint = URI->new_abs( $endpoint, $response->base );
517              
518 6         3375 my $host = $endpoint->host;
519 6 100 66     195 if (
520             ( lc($host) eq 'localhost' ) || ( $host =~ /^127\.\d+\.\d+\.\d+$/ )
521             ) {
522 1         7 carp "Warning: $source declares an apparent loopback address "
523             . "($endpoint) as a webmention endpoint. Ignoring.";
524 1         771 return undef;
525             }
526             else {
527 5         123 return $endpoint;
528             }
529             }
530              
531             sub _build_title {
532 5     5   4004 my $self = shift;
533              
534             # If the source doc has an h-entry with a p-name, return that, truncated.
535 5 50       86 if ( $self->source_mf2_document ) {
536 5         112 my $entry = $self->source_mf2_document->get_first( 'h-entry' );
537 5         685 my $name;
538 5 100       25 if ( $entry ) {
539 4         14 $name = $entry->get_property( 'name' );
540             }
541 5 100 100     77 if ( $entry && $name ) {
542 1         5 return $self->_truncate_content( $name );
543             }
544             }
545              
546             # Otherwise, try to return the HTML title element's content.
547 4         16 return $self->_title_element_content;
548              
549             }
550              
551             sub _title_element_content {
552 5     5   11 my $self = shift;
553              
554 5         93 my $title = Mojo::DOM58->new( $self->source_html )->at('title');
555 5 50       7171 if ($title) {
556 5         66 return $title->text;
557             }
558             else {
559 0         0 return undef;
560             }
561             }
562              
563             sub as_json {
564 2     2 1 26 my $self = shift;
565 2         20 return JSON->new->convert_blessed->encode( $self );
566             }
567              
568             sub new_from_json {
569 2     2 1 29 my ($class, $json) = @_;
570 2         33 return $class->FROM_JSON( JSON->new->decode( $json ) );
571             }
572              
573             # Called by the JSON module during JSON encoding.
574             # Contrary to the (required) name, returns an unblessed reference, not JSON.
575             # See https://metacpan.org/pod/JSON#OBJECT-SERIALISATION
576             sub TO_JSON {
577 3     3 0 808 my $self = shift;
578              
579 3         20 my $return_ref = {
580             source => $self->source->as_string,
581             target => $self->target->as_string,
582             time_received => $self->time_received->epoch,
583             };
584              
585 3 100       120 if ( $self->is_tested ) {
586 2         23 foreach (qw(
587             is_tested is_verified type content source_html title content
588             rsvp_type
589             )) {
590 16         649 $return_ref->{$_} = $self->$_;
591             }
592 2         49 $return_ref->{ time_verified } = $self->time_verified->epoch;
593 2         61 $return_ref->{ source_html } = $self->source_html;
594 2 50       42 if ( $self->source_mf2_document ) {
595             $return_ref->{ mf2_document_json } =
596 2         43 decode_utf8($self->source_mf2_document->as_json);
597             }
598             else {
599 0         0 $return_ref->{ mf2_document_json } = undef;
600             }
601 2         486 foreach (qw( name url photo ) ) {
602 6         3005 $return_ref->{ author }->{ $_ } = $self->author->$_;
603             }
604             }
605              
606 3         72 return $return_ref;
607             }
608              
609             # Class method to construct a Webmention object from an unblessed reference,
610             # as created from the TO_JSON method. All-caps-named for the sake of parity.
611             sub FROM_JSON {
612 3     3 0 39 my $class = shift;
613 3         7 my ( $data_ref ) = @_;
614              
615 3         7 foreach ( qw( time_received time_verified ) ) {
616 6 100       886 if ( defined $data_ref->{ $_ } ) {
617             $data_ref->{ $_ } =
618 5         18 DateTime->from_epoch( epoch => $data_ref->{ $_ } );
619             }
620             }
621              
622 3 100       510 if ( $data_ref->{ author } ) {
623             $data_ref->{ author } =
624 2         42 Web::Mention::Author->new( $data_ref->{ author } );
625             }
626              
627 3         203 my $webmention = $class->new( $data_ref );
628              
629 3 100       78 if ( my $mf2_json = $data_ref->{ mf2_document_json } ) {
630 2         8 my $doc = Web::Microformats2::Document->new_from_json( $mf2_json );
631 2         1909 $webmention->source_mf2_document( $doc );
632             }
633              
634 3         78 return $webmention;
635             }
636              
637             1;
638              
639             =pod
640              
641             =encoding UTF-8
642              
643             =head1 NAME
644              
645             Web::Mention - Implementation of the IndieWeb Webmention protocol
646              
647             =head1 SYNOPSIS
648              
649             use Web::Mention;
650             use Try::Tiny;
651             use v5.10;
652              
653             # Building a webmention from an incoming web request:
654              
655             my $wm;
656             try {
657             # $request can be any object that provides a 'param' method, such as
658             # Catalyst::Request or Mojo::Message::Request.
659             $wm = Web::Mention->new_from_request ( $request )
660             }
661             catch {
662             say "Oops, this wasn't a webmention at all: $_";
663             };
664              
665             if ( $wm && $wm->is_verified ) {
666             my $source = $wm->original_source;
667             my $target = $wm->target;
668             my $author = $wm->author;
669              
670             my $name;
671             if ( $author ) {
672             $name = $author->name;
673             }
674             else {
675             $name = $wm->source->host;
676             }
677              
678             if ( $wm->is_like ) {
679             say "Hooray, $name likes $target!";
680             }
681             elsif ( $wm->is_repost ) {
682             say "Gadzooks, over at $source, $name reposted $target!";
683             }
684             elsif ( $wm->is_reply ) {
685             say "Hmm, over at $source, $name said this about $target:";
686             say $wm->content;
687             }
688             else {
689             say "I'll be darned, $name mentioned $target at $source!";
690             }
691             }
692             else {
693             say "This webmention doesn't actually mention its target URL, "
694             . "so it is not verified.";
695             }
696              
697             # Manually buidling and sending a webmention:
698              
699             $wm = Web::Mention->new(
700             source => $url_of_the_thing_that_got_mentioned,
701             target => $url_of_the_thing_that_did_the_mentioning,
702             );
703              
704             my $success = $wm->send;
705             if ( $success ) {
706             say "Webmention sent successfully!";
707             }
708             else {
709             say "The webmention wasn't sent successfully.";
710             say "Here's the response we got back..."
711             say $wm->response->as_string;
712             }
713              
714             # Batch-sending a bunch of webmentions based on some published HTML
715              
716             my @wms = Web::Mention->new_from_html(
717             source => $url_of_a_web_page_i_just_published,
718             html => $relevant_html_content_of_that_web_page,
719             )
720              
721             for my $wm ( @wms ) {
722             my $success = $wm->send;
723             }
724              
725             =head1 DESCRIPTION
726              
727             This class implements the Webmention protocol, as defined by the W3C and
728             the IndieWeb community. (See L<this article by Chris
729             Aldrich|https://alistapart.com/article/webmentions-enabling-better-
730             communication-on-the-internet/> for an excellent high-level summary of
731             Webmention and its applications.)
732              
733             An object of this class represents a single webmention, with target and
734             source URLs. It can verify itself, determining whether or not the
735             document found at the source URL does indeed mention the target URL.
736              
737             It can also use IndieWeb algorithms to attempt identification of the
738             source document's author, and to provide a short summary of that
739             document's content, using Microformats2 metadata when available.
740              
741             =head1 METHODS
742              
743             =head2 Class Methods
744              
745             =head3 new
746              
747             $wm = Web::Mention->new(
748             source => $source_url,
749             target => $target_url,
750             );
751              
752             Basic constructor. The B<source> and B<target> URLs are both required
753             arguments. Either one can either be a L<URI> object, or a valid URL
754             string.
755              
756             Per the Webmention protocol, the B<source> URL represents the location
757             of the document that made the mention described here, and B<target>
758             describes the location of the document that got mentioned. The two
759             arguments cannot refer to the same URL (disregarding the C<#fragment>
760             part of either, if present).
761              
762             =head3 new_from_html
763              
764             @wms = Web::Mention->new_from_html(
765             source => $source_url,
766             html => $html,
767             );
768              
769             Convenience batch-construtor that returns a (possibly empty) I<list> of
770             Web::Mention objects based on the single source URL (or I<URI> object)
771             that you pass in, as well as a string containing HTML from which we can
772             extract zero or more target URLs. These extracted URLs include the
773             C<href> attribute value of every E<lt>aE<gt> tag in the provided HTML.
774              
775             Note that (as with all this class's constructors) this method won't
776             proceed to actually send the generated webmentions; that step remains
777             yours to take. (See L<"send">.)
778              
779             =head3 new_from_json
780              
781             $wm = Web::Mention->new_from_json( $json );
782              
783             Returns a new webmention based on the JSON output of L<"as_json">.
784              
785             =head3 new_from_request
786              
787             $wm = Web::Mention->new_from_request( $request_object );
788              
789             Convenience constructor that looks into the given web-request object for
790             B<source> and B<target> parameters, and attempts to build a new
791             Web::Mention object out of them.
792              
793             The object must provide a C<param( $param_name )> method that returns
794             the value of the named HTTP parameter. So it could be a
795             L<Catalyst::Request> object or a L<Mojo::Message::Request> object, for
796             example.
797              
798             Throws an exception if the given argument doesn't meet this requirement,
799             or if it does but does not define both required HTTP parameters.
800              
801             =head3 content_truncation_marker
802              
803             Web::Mention->content_truncation_marker( $new_truncation_marker )
804              
805             The text that the content method will append to text that it has
806             truncated, if it did truncate it. (See L<"content">.)
807              
808             Defaults to C<...>.
809              
810             =head3 max_content_length
811              
812             Web::Mention->max_content_length( $new_max_length )
813              
814             Gets or sets the maximum length, in characters, of the content displayed
815             by that object method prior to truncation. (See L<"content">.)
816              
817             Defaults to 280.
818              
819             =head2 Object Methods
820              
821             =head3 as_json
822              
823             $json = $wm->as_json;
824              
825             Returns a JSON representation of the webmention.
826              
827             See L<"SERIALIZATION">, below, for more information.
828              
829             =head3 author
830              
831             $author = $wm->author;
832              
833             A L<Web::Mention::Author> object representing the author of this
834             webmention's source document. You can get information about the author through
835             its C<name>, C<url>, and C<photo> methods.
836              
837             If the webmention's author is unknown or unset, then this method returns a
838             L<Web::Mention::Author> object with all its fields set to C<undef>.
839              
840             =head3 content
841              
842             $content = $wm->content;
843              
844             Returns a string containing this object's best determination of this
845             webmention's I<display-ready> content, based on a number of factors.
846              
847             If the source document uses Microformats2 metadata and contains an
848             C<h-entry> MF2 item, then returned content may come from a variety of
849             its constituent properties, according to L<the IndieWeb comment-display
850             algorithm|https://indieweb.org/comments#How_to_display>.
851              
852             If not, then it returns the content of the source document's
853             E<lt>titleE<gt> element, with any further HTML stripped away.
854              
855             In any case, the string will get truncated if it's too long. See
856             L<"max_content_length"> and L<"content_truncation_marker">.
857              
858             =head3 endpoint
859              
860             my $uri = $wm->endpoint;
861              
862             Attempts to determine the webmention endpoint URL of this webmention's
863             target. On success, returns a L<URI> object. On failure, returns undef.
864              
865             (If the endpoint is set to localhost or a loopback IP, will return undef
866             and also emit a warning, because that's terribly rude behavior on the
867             target's part.)
868              
869             =head3 is_tested
870              
871             $bool = $wm->is_tested;
872              
873             Returns 1 if this object's L<"verify"> method has been called at least
874             once, regardless of the results of that call. Returns 0 otherwise.
875              
876             =head3 is_verified
877              
878             $bool = $wm->is_verified;
879              
880             Returns 1 if the webmention's source document actually does seem to
881             mention the target URL. Otherwise returns 0.
882              
883             The first time this is called on a given webmention object, it will try
884             to fetch the source document at its designated URL by way of the
885             L<"verify"> method.
886              
887             =head3 original_source
888              
889             $original_url = $wm->original_source;
890              
891             If the document fetched from the source URL seems to point at yet
892             another URL as its original source, then this returns that URL. If not,
893             this has the same return value as L<"source">.
894              
895             (It makes this determination based on the possible presence a C<u-url>
896             property in an C<h-entry> found within the source document.)
897              
898             =head3 response
899              
900             my $response = $wm->response;
901              
902             Returns the L<HTTP::Response> object representing the response received
903             by this webmention instance during its most recent attempt to send
904             itself.
905              
906             Returns undef if this webmention instance hasn't tried to send itself.
907              
908             =head3 rsvp_type
909              
910             my $rsvp = $wm->rsvp_type;
911              
912             If this webmention is of type C<rsvp> (see L<"type">, below), then this
913             method returns the type of RSVP represented. It will be one of:
914              
915             =over
916              
917             =item *
918              
919             yes
920              
921             =item *
922              
923             no
924              
925             =item *
926              
927             maybe
928              
929             =item *
930              
931             interested
932              
933             =back
934              
935             Otherwise, returns undef.
936              
937             =head3 send
938              
939             my $bool = $wm->send;
940              
941             Attempts to send an HTTP-request representation of this webmention to
942             its target's designated webmention endpoint. This involves querying the
943             target URL to discover said endpoint's URL (via the C<endpoint> object
944             method), and then sending the actual webmention request via HTTP to that
945             endpoint.
946              
947             If that whole process goes through successfully and the endpoint returns
948             a success response (meaning that it has acknowledged the webmention, and
949             most likely queued it for later processing), then this method returns
950             true. Otherwise, it returns false.
951              
952             B<To determine why a webmention did not send itself successfully>, consult
953             the value of C<response>. If it is defined, then you can call
954             L<HTTP::Response> methods (such as C<code> or C<message>) to learn more
955             about the problem. Otherwise, if C<response> is not defined, then the
956             target URL did not advertise a Webmention endpoint.
957              
958             =head3 source
959              
960             $source_url = $wm->source;
961              
962             Returns the webmention's source URL, as a L<URI> object.
963              
964             =head3 source_html
965              
966             $html = $wm->source_html;
967              
968             The HTML of the document fetched from the source URL. If nothing got
969             fetched successfully, returns undef.
970              
971             =head3 source_mf2_document
972              
973             $mf2_doc = $wm->source_mf2_document;
974              
975             The L<Web::Microformats2::Document> object that resulted from parsing
976             the source document for Microformats2 metadata. If no such result,
977             returns undef.
978              
979             =head3 target
980              
981             $target_url = $wm->target;
982              
983             Returns the webmention's target URL, as a L<URI> object.
984              
985             =head3 time_published
986              
987             $published_dt = $wm->time_published;
988              
989             If the document fetched from the source URL explicitly declares a
990             publication time via microformats, then this will return an appropriate
991             L<DateTime> object.
992              
993             If not (or if the declared time seems to be invalid), then this will
994             instead have the same return value as L<"time_received">.
995              
996             (It makes this determination based on the possible presence a C<dt-published>
997             property in an C<h-entry> found within the source document.)
998              
999             =head3 time_received
1000              
1001             $received_dt = $wm->time_received;
1002              
1003             A L<DateTime> object corresponding to this object's creation time.
1004              
1005             =head3 time_verified
1006              
1007             $verified_dt = $wm->time_verified;
1008              
1009             If this webmention has been verified, then this will return a
1010             L<DateTime> object corresponding to the time of verification.
1011             (Otherwise, returns undef.)
1012              
1013             =head3 title
1014              
1015             my $title = $wm->title;
1016              
1017             Returns a string containing this object's best determination of the
1018             I<display-ready> title of this webmention's source document,
1019             considered separately from its content. (You can get its more complete
1020             content via the L<"content"> method.
1021              
1022             If the source document uses Microformats2 metadata and contains an
1023             C<h-entry> MF2 item, I<and> that item has a C<name> property, then this
1024             method will return the text content of that name property.
1025              
1026             If not, then it will return the content of the source document's
1027             E<lt>titleE<gt> element, with any further HTML stripped away.
1028              
1029             In any case, the string will get truncated if it's too long. See
1030             L<"max_content_length"> and L<"content_truncation_marker">.
1031              
1032             Note that in some circumstances, the title and content methods might
1033             return identical values. (If, for example, the source document defines
1034             an entry with an explicit name property and no summary or content
1035             properties.)
1036              
1037             =head3 type
1038              
1039             $type = $wm->type;
1040              
1041             The type of webmention this is. One of:
1042              
1043             =over
1044              
1045             =item *
1046              
1047             mention I<(default)>
1048              
1049             =item *
1050              
1051             reply
1052              
1053             =item *
1054              
1055             like
1056              
1057             =item *
1058              
1059             repost
1060              
1061             =item *
1062              
1063             quotation
1064              
1065             =item *
1066              
1067             rsvp
1068              
1069             =back
1070              
1071             This list is based on the W3C Post Type Discovery document
1072             (L<https://www.w3.org/TR/post-type-discovery/#response-algorithm>), and
1073             adds a "quotation" type.
1074              
1075             =head3 verify
1076              
1077             my $is_verified = $wm->verify
1078              
1079             This B<verifies> the webmention, confirming that the content located at
1080             the source URL contains the target URL. Returns 1 if so, and 0
1081             otherwise. Will also return 0 if it cannot fetch the content at all,
1082             after one try.
1083              
1084             Sets C<is_tested> to 1 as a side-effect.
1085              
1086             See also L<"is_verified">.
1087              
1088             =head1 SERIALIZATION
1089              
1090             To serialize a Web::Mention object, use L<"as_json">, which returns a
1091             JSON string that you can store in any way you wish. You can later
1092             "inflate" it into a Web::Mention object through the L<"new_from_json">
1093             class method.
1094              
1095             Note that a verified webmention might serialize to a significantly
1096             larger JSON string than an unverified one: it might include a complete
1097             copy of the source document, its parsed microformats (if any), author
1098             information, and various other metadata. Unverified webmentions, on the
1099             other hand, will likely contain little data other that their source and
1100             target URLs.
1101              
1102             This is all normal; verified webmentions just have more luggage.
1103              
1104             =head1 NOTES AND BUGS
1105              
1106             This software is B<beta>; its interface continues to develop and remains
1107             subject to change, but not without some effort at supporting its current
1108             API.
1109              
1110             This library does not, at this time, support L<the proposed "Vouch"
1111             anti-spam extension for Webmention|https://indieweb.org/Vouch>.
1112              
1113             =head1 SUPPORT
1114              
1115             To file issues or submit pull requests, please see L<this module's
1116             repository on GitHub|https://github.com/jmacdotorg/webmention-perl>.
1117              
1118             The author also welcomes any direct questions about this module via
1119             email.
1120              
1121             =head1 AUTHOR
1122              
1123             Jason McIntosh (jmac@jmac.org)
1124              
1125             =head1 CONTRIBUTORS
1126              
1127             =over
1128              
1129             =item *
1130              
1131             Mohammad S Anwar (mohammad.anwar@yahoo.com)
1132              
1133             =item *
1134              
1135             Tomaž Šolc (tomaz.solc@tablix.org)
1136              
1137             =back
1138              
1139             =head1 COPYRIGHT AND LICENSE
1140              
1141             This software is Copyright (c) 2018-2020 by Jason McIntosh.
1142              
1143             This is free software, licensed under:
1144              
1145             The MIT (X11) License
1146              
1147             =head1 A PERSONAL REQUEST
1148              
1149             My ability to share and maintain free, open-source software like this
1150             depends upon my living in a society that allows me the free time and
1151             personal liberty to create work benefiting people other than just myself
1152             or my immediate family. I recognize that I got a head start on this due
1153             to an accident of birth, and I strive to convert some of my unclaimed
1154             time and attention into work that, I hope, gives back to society in some
1155             small way.
1156              
1157             Worryingly, I find myself today living in a country experiencing a
1158             profound and unwelcome political upheaval, with its already flawed
1159             democracy under grave threat from powerful authoritarian elements. These
1160             powers wish to undermine this society, remolding it according to their
1161             deeply cynical and strictly zero-sum philosophies, where nobody can gain
1162             without someone else losing.
1163              
1164             Free and open-source software has no place in such a world. As such,
1165             these autocrats' further ascension would have a deleterious effect on my
1166             ability to continue working for the public good.
1167              
1168             Therefore, if you would like to financially support my work, I would ask
1169             you to consider a donation to one of the following causes. It would mean
1170             a lot to me if you did. (You can tell me about it if you'd like to, but
1171             you don't have to.)
1172              
1173             =over
1174              
1175             =item *
1176              
1177             L<The American Civil Liberties Union|https://aclu.org>
1178              
1179             =item *
1180              
1181             L<The Democratic National Committee|https://democrats.org>
1182              
1183             =item *
1184              
1185             L<Earthjustice|https://earthjustice.org>
1186              
1187             =back
1188