File Coverage

blib/lib/Mojolicious/Plugin/PubSubHubbub.pm
Criterion Covered Total %
statement 256 308 83.1
branch 105 172 61.0
condition 45 93 48.3
subroutine 28 30 93.3
pod 3 5 60.0
total 437 608 71.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::PubSubHubbub;
2 3     3   10042 use Mojo::Base 'Mojolicious::Plugin';
  3         7  
  3         30  
3 3     3   611 use Mojo::UserAgent;
  3         28  
  3         33  
4 3     3   97 use Mojo::DOM;
  3         8  
  3         85  
5 3     3   17 use Mojo::ByteStream 'b';
  3         7  
  3         149  
6 3     3   18 use Mojo::Util qw/secure_compare hmac_sha1_sum/;
  3         23  
  3         17307  
7              
8             our $VERSION = '0.22';
9              
10             # Todo:
11             # - Be compliant with https://www.w3.org/TR/websub/
12             # - Prevent log injection
13             # - Make everything async (top priority)
14             # - Maybe allow something like ->feed_to_json (look at superfeedr)
15             # - Test ->discover
16              
17             # Default lease seconds before automatic subscription refreshing
18             has lease_seconds => ( 9 * 24 * 60 * 60 );
19             has hub => 'http://pubsubhubbub.appspot.com/';
20              
21             my $FEED_TYPE_RE = qr{^(?i:application/(atom|r(?:ss|df))\+xml)};
22             my $FEED_ENDING_RE = qr{(?i:\.(r(?:ss|df)|atom))$};
23              
24             # User Agent Name
25             my $UA_NAME = __PACKAGE__ . ' v' . $VERSION;
26              
27             # Prototypes
28             sub _add_topics;
29              
30             # Register plugin
31             sub register {
32 3     3 1 4497 my ($plugin, $mojo, $param) = @_;
33              
34 3   50     16 $param ||= {};
35              
36             # Load parameter from Config file
37 3 100       29 if (my $config_param = $mojo->config('PubSubHubbub')) {
38 1         19 $param = { %$param, %$config_param };
39             };
40              
41 3         58 my $helpers = $mojo->renderer->helpers;
42              
43             # Load 'callback' plugin
44 3 100       51 unless (exists $helpers->{'callback'}) {
45 2         9 $mojo->plugin('Util::Callback');
46             };
47              
48             # Set callbacks on registration
49 3         3647 $mojo->callback([qw/pubsub_accept pubsub_verify/] => $param);
50              
51             # Load 'endpoint' plugin
52 3 100       646 unless (exists $helpers->{'endpoint'}) {
53 2         11 $mojo->plugin('Util::Endpoint');
54             };
55              
56             # Load 'randomstring' plugin
57 3         6904 $mojo->plugin('Util::RandomString' => {
58             pubsub_challenge => {
59             length => 12,
60             alphabet => [ 'A' .. 'Z', 'a' .. 'z', 0 .. 9 ]
61             }
62             });
63              
64             # Set hub attribute
65 3 100       12342 if ($param->{hub}) {
66 2         12 $plugin->hub($param->{hub});
67             };
68              
69             # Establish an endpoint
70 3         32 $mojo->endpoint('pubsub-hub' => $plugin->hub);
71              
72             # Set lease_seconds attribute
73 3 100       936 if ($param->{lease_seconds}) {
74 1         4 $plugin->lease_seconds($param->{lease_seconds});
75             };
76              
77             # Add 'pubsub' shortcut
78             $mojo->routes->add_shortcut(
79             pubsub => sub {
80 2     2   1782 my ($route, $param) = @_;
81              
82             # Set param default to 'cb'
83 2   100     13 $param ||= 'cb';
84              
85             # 'hub' is currently not supported
86 2 100       13 return unless $param eq 'cb';
87              
88             # Set PubSubHubbub endpoints
89 1         8 $route->endpoint('pubsub-callback');
90              
91             # Add 'callback' route
92             $route->to(
93             cb => sub {
94 18         205469 my $c = shift;
95              
96             # Hook on verification
97 18 100       65 return $plugin->verify($c) if $c->param('hub.mode');
98              
99             # Hook on callback
100 9         2187 return $plugin->callback($c);
101 1         43 });
102 3         25 });
103              
104             # Return plugin object
105             $mojo->helper(
106             'pubsub._plugin' => sub {
107 2     2   1527 $plugin;
108 3         215 });
109              
110             $mojo->helper(
111             'pubsub.publish' => sub {
112 3     3   7815 $plugin->publish( @_ );
113 3         1203 });
114              
115             # Add 'subscribe' and 'unsubscribe' helper
116 3         1006 foreach my $action (qw(subscribe unsubscribe)) {
117             $mojo->helper(
118             "pubsub.${action}" => sub {
119 8     8   12166 $plugin->_change_subscription(shift, mode => $action, @_);
120 6         1016 });
121             };
122              
123             $mojo->helper(
124             'pubsub.discover' => sub {
125 0     0   0 $plugin->discover( @_ )
126             }
127 3         1077 );
128             };
129              
130              
131             # Ping a hub for topics
132             sub publish {
133 3     3 1 7 my $plugin = shift;
134 3         7 my $c = shift;
135              
136             # Nothing to publish or no hub defined
137 3 100 66     16 return unless @_ || !$plugin->hub;
138              
139             # Set all urls
140 2         12 my @urls = map($c->endpoint($_), @_);
141              
142             # Create post message
143 2         1686 my %post = (
144             'hub.mode' => 'publish',
145             'hub.url' => \@urls
146             );
147              
148             # Get user agent
149 2         16 my $ua = Mojo::UserAgent->new(
150             max_redirects => 3,
151             name => $UA_NAME
152             );
153              
154 2         19 my $msg = 'Cannot ping hub';
155 2 50       9 $msg .= ' - maybe no SSL support' if index($plugin->hub, 'https') == 0;
156              
157             # Blocking
158             # Post to hub
159 2         18 my $tx = $ua->post( $plugin->hub => form => \%post );
160              
161 2         27834 my $res = $tx->result;
162              
163             # No response
164 2 50       57 unless ($res) {
165 0         0 $c->app->log->warn($msg);
166 0         0 return;
167             };
168              
169             # is 2xx, incl. 204 aka successful
170 2 50       10 return 1 if $res->is_success;
171              
172             # Not successful
173 0         0 return;
174             };
175              
176              
177             # Verify a changed subscription or automatically refresh
178             sub verify {
179 9     9 0 3376 my $plugin = shift;
180 9         18 my $c = shift;
181              
182             # Good request
183 9 100 100     28 if ($c->param('hub.topic') &&
      100        
184             $c->param('hub.challenge') &&
185             $c->param('hub.mode') =~ /^(?:un)?subscribe$/) {
186              
187 4         788 my $challenge = $c->param('hub.challenge');
188              
189 4         239 my %param;
190 4         12 foreach (qw/mode
191             topic
192             verify
193             lease_seconds
194             verify_token/) {
195 20 100       1369 $param{$_} = $c->param("hub.$_") if $c->param("hub.$_");
196             };
197              
198             # Get verification callback
199 4         225 my $ok = $c->callback(
200             pubsub_verify => \%param
201             );
202              
203             # Render challenge
204 4 100       3056 return $c->render(
205             'status' => 200,
206             'format' => 'txt',
207             'data' => $challenge
208             ) if $ok;
209             };
210              
211             # Not found
212 7         681 return $c->reply->not_found;
213             };
214              
215              
216             # Discover links from header
217             # This is extremely simplified from https://tools.ietf.org/html/rfc5988
218             sub _discover_header_links {
219 1     1   1241 my $header = shift;
220              
221 1         6 my $header_hash = $header->to_hash(1);
222              
223 1   50     27 my @links = (@{$header_hash->{Link} // []}, @{$header_hash->{link} // []});
  1   50     7  
  1         11  
224 1         3 my %links;
225              
226             # Iterate through all header links
227 1         3 foreach (@links) {
228              
229             # Make multiline headers one line
230 11 50       26 $_ = join(' ', @$_) if ref $_;
231              
232             # Check for link with correct relation
233 11 100       80 if ($_ =~ /^\<([^>]+?)\>(.*?rel\s*=\s*"(self|hub|alternate)".*?)$/mi) {
234              
235             # Create new link hash
236 7         30 my %link = ( href => $1, rel => $3 );
237              
238             # There may be more than one reference
239 7         16 my $check = $2;
240              
241             # Set type
242 7 100       35 if ($check =~ /type\s*=\s*"([^"]+?)"/omi) {
243 4         9 my $type = $1;
244 4 50 33     83 next if $type && $type !~ $FEED_TYPE_RE;
245 4         14 $link{type} = $type;
246 4         12 $link{short_type} = $1;
247             };
248              
249             # Set title
250 7 100       43 if ($check =~ /title\s*=\s*"([^"]+?)"/omi) {
251 5         14 $link{title} = $1;
252             };
253              
254             # Check file ending for short type
255 7 100       16 unless ($link{short_type}) {
256 3 100       29 $link{short_type} = $1 if $link{href} =~ $FEED_ENDING_RE;
257             };
258              
259             # Push found link
260 7         14 my $rel = $link{rel};
261 7   100     26 $links{$rel} //= [];
262 7         13 push(@{$links{$rel}}, \%link);
  7         22  
263             };
264             };
265              
266             # Return array
267 1         11 return \%links;
268             };
269              
270              
271             # Discover links from dom tree
272             sub _discover_dom_links {
273 2     2   6620 my $dom = shift;
274              
275 2         4 my %links;
276              
277             # Find alternate representations
278             $dom->find('link[rel="alternate"], link[rel="self"], link[rel="hub"]')->each(
279             sub {
280 13     13   4723 my ($href, $rel, $type, $title) = @{$_->attr}{qw/href rel type title/};
  13         36  
281              
282             # Is no supported type
283 13 50 66     247 return if $type && $type !~ $FEED_TYPE_RE;
284              
285             # Set short type
286 13 100       42 my $short_type = $1 if $1;
287              
288 13 50 33     47 return unless $href && $rel;
289              
290             # Create new link hash
291 13         41 my %link = ( href => $href, rel => $rel );
292              
293             # Short type yet not known
294 13 100       31 unless ($short_type) {
295              
296             # Set short type by file ending
297 5 100       35 $link{short_type} = $1 if $href =~ m/\.(r(?:ss|df)|atom)$/i;
298             }
299              
300             # Set short type
301             else {
302 8         18 $link{short_type} = $short_type;
303             };
304              
305             # Set title and type
306 13 100       29 $link{title} = $title if $title;
307 13 100       28 $link{type} = $type if $type;
308              
309             # Push found link
310 13   100     46 $links{$rel} //= [];
311 13         19 push(@{$links{$rel}}, \%link);
  13         47  
312             }
313 2         10 );
314              
315             # Return array
316 2         48 return \%links;
317             };
318              
319              
320             # Heuristically sort links to best match the topic
321             sub _discover_sort_links {
322 3     3   7 my $links = shift;
323              
324 3         8 my ($topic, $hub);
325              
326             # Get self link as topic
327 3 100       13 if ($links->{self}) {
328              
329             # Find best match of all returned links
330 2         5 foreach my $link (@{$links->{self}}) {
  2         9  
331 2   33     13 $topic ||= $link;
332 2 50 33     11 if ($link->{short_type} && !$topic->{short_type}) {
333 0         0 $topic = $link;
334             };
335             };
336             };
337              
338             # Get hub
339 3 50       9 if ($links->{hub}) {
340              
341             # Find best match of all returned links
342 3         6 foreach my $link (@{$links->{hub}}) {
  3         11  
343 3   33     17 $hub ||= $link;
344 3 50 33     14 if ($link->{short_type} && !$hub->{short_type}) {
345 0         0 $hub = $link;
346             };
347             };
348             };
349              
350             # Already found topic and hub
351 3 100 66     18 return ($topic, $hub) if $topic && $hub;
352              
353             # Check alternates
354 1         3 my $alternate = $links->{alternate};
355              
356             # Search in alternate representations for best match
357 1 50       5 if ($alternate) {
358              
359             # Iterate through all alternate links
360             # and check their titles
361 1         2 foreach my $link (@$alternate) {
362              
363             # No title given
364 5 50       29 unless ($link->{title}) {
    50          
365 0         0 $link->{pref} = 2;
366             }
367              
368             # Guess which feed is best based on the title
369 0         0 elsif ($link->{title} =~ /(?i:feed|stream)/i) {
370              
371             # This is more likely a comment feed
372 5 100       16 if ($link->{title} =~ /[ck]omment/i) {
373 2         6 $link->{pref} = 1;
374             }
375              
376             # This may be the correct feed
377             else {
378 3         8 $link->{pref} = 3;
379             };
380             }
381              
382             # Don't know ...
383             else {
384 0         0 $link->{pref} = 2;
385             };
386             };
387              
388             # Get best topic
389             ($topic) = (sort {
390              
391             # Sort by title
392 1 100       8 if ($a->{pref} < $b->{pref}) {
  8 100       24  
    50          
    0          
    0          
    0          
393 3         5 return 1;
394             }
395             elsif ($a->{pref} > $b->{pref}) {
396 1         3 return -1;
397             }
398             # Sort by type
399             elsif ($a->{short_type} gt $b->{short_type}) {
400 4         11 return 1;
401             }
402             elsif ($a->{short_type} lt $b->{short_type}) {
403 0         0 return -1;
404             }
405             # Sort by length
406             elsif (length($a->{href}) > length($b->{href})) {
407 0         0 return 1;
408             }
409             elsif (length($a->{href}) <= length($b->{href})) {
410 0         0 return -1;
411             }
412             # Equal
413             else {
414 0         0 return -1;
415             };
416             } @$alternate);
417             };
418              
419             # Maybe empty ... maybe not
420 1         6 return ($topic, $hub);
421             };
422              
423              
424             # Discover topic and hub based on a URI
425             # That's a rather complex heuristic, but should gain good results
426             sub discover {
427 0     0 1 0 my $plugin = shift;
428 0         0 my $c = shift;
429              
430             # No uri given
431 0 0       0 return () unless $_[0];
432              
433             # Get uri
434 0 0       0 my $base = Mojo::URL->new( shift ) or return ();
435              
436             # Set base to uri
437 0         0 $base->base($c->req->url);
438              
439             # Initialize UserAgent
440 0         0 my $ua = Mojo::UserAgent->new(
441             max_redirects => 3,
442             name => $UA_NAME
443             );
444              
445             # Initialize variables
446 0         0 my ($hub, $topic, $nbase, $ntopic);
447              
448             # Retrieve resource
449 0         0 my $tx = $ua->get($base);
450              
451 0 0       0 unless ($tx->error) {
452              
453             # Change base after possible redirects
454 0         0 $base = $tx->req->url;
455              
456             # Get response
457 0         0 my $res = $tx->res;
458              
459             # Check sorted header links
460 0         0 ($topic, $hub) = _discover_sort_links(
461             _discover_header_links($res->headers)
462             );
463              
464             # Fine
465 0 0 0     0 unless ($topic && $hub) {
466              
467 0         0 my $dom = $res->dom;
468              
469             # Check sorted dom links
470 0         0 ($topic, $hub) = _discover_sort_links(
471             _discover_dom_links($dom)
472             );
473             };
474              
475             # Fine
476 0 0 0     0 if ($topic && !$hub) {
477              
478             # Initialize new UserAgent
479 0         0 $ua = Mojo::UserAgent->new(
480             max_redirects => 3,
481             name => $UA_NAME
482             );
483              
484             # Set new base base
485 0         0 $nbase = Mojo::URL->new($topic->{href})->base($base)->to_abs;
486              
487             # Retrieve resource
488 0         0 $tx = $ua->get($nbase);
489              
490             # Request was successful
491 0 0       0 unless ($tx->error) {
492              
493             # Change nbase after possible redirects
494 0         0 $nbase = $tx->req->url;
495              
496             # Get response
497 0         0 $res = $tx->res;
498              
499             # Check sorted header links
500 0         0 ($ntopic, $hub) = _discover_sort_links(
501             _discover_header_links($res->headers)
502             );
503              
504              
505 0 0 0     0 unless ($ntopic && $hub) {
506              
507             # Check sorted dom links
508 0         0 ($ntopic, $hub) = _discover_sort_links(
509             _discover_dom_links($res->dom)
510             );
511             };
512             }
513              
514             # Reset nbase as no connection occurred
515             else {
516 0         0 $nbase = undef;
517             };
518             };
519             };
520              
521             # Make relative path for topics and hubs absolute
522 0 0 0     0 $hub = Mojo::URL->new($hub->{href})->base( $nbase || $base )->to_abs if $hub;
523              
524             # New topic is set
525 0 0       0 if ($ntopic) {
    0          
526 0         0 $topic = Mojo::URL->new($ntopic->{href})->base($nbase)->to_abs;
527             }
528              
529             # Old topic is set
530             elsif ($topic) {
531 0         0 $topic = Mojo::URL->new($topic->{href})->base($base)->to_abs;
532             };
533              
534             # Return
535 0         0 return ($topic, $hub);
536             };
537              
538              
539             # subscribe or unsubscribe from a topic
540             sub _change_subscription {
541 8     8   23 my $plugin = shift;
542 8         18 my $c = shift;
543 8         39 my %param = @_;
544              
545 8         37 my $log = $c->app->log;
546              
547             # Get callback endpoint
548             # Works only if endpoints provided
549 8 50 33     131 unless ($param{callback} ||= $c->endpoint('pubsub-callback')) {
550 0 0       0 $log->error('You have to specify a callback endpoint') and return;
551             };
552              
553             # No topic or hub url given
554 8 100 100     7088 unless (exists $param{topic} &&
      100        
555             $param{topic} =~ m{^https?://}i &&
556             exists $param{hub}) {
557 4         54 $log->warn('You have to specify a topic and a hub');
558 4         63 return;
559             };
560              
561 4         14 my $mode = $param{mode};
562              
563             # delete lease seconds if no integer
564 4 0 0     20 if (exists $param{lease_seconds} &&
      33        
565             ($mode eq 'unsubscribe' || $param{lease_seconds} !~ /^\d+$/)
566             ) {
567 0         0 delete $param{lease_seconds};
568             };
569              
570             # Set to default
571 4 100 33     30 $param{lease_seconds} ||= $plugin->lease_seconds if $mode eq 'subscribe';
572              
573             # Render post string
574 4         31 my %post = ( callback => $param{callback} );
575 4         18 foreach ( qw/mode topic verify lease_seconds secret/ ) {
576 20 50 66     77 $post{ $_ } = $param{ $_ } if exists $param{ $_ } && $param{ $_ };
577             };
578              
579             # Use verify token
580             $post{verify_token} =
581             exists $param{verify_token} ?
582             $param{verify_token} :
583             ($param{verify_token} =
584 4 50       40 $c->random_string('pubsub_challenge'));
585              
586 4         294 $post{verify} = "${_}sync" foreach ('a', '');
587              
588 4         17 my $mojo = $c->app;
589              
590 4         25 $mojo->plugins->emit_hook(
591             "before_pubsub_$mode" => ($c, \%param, \%post)
592             );
593              
594             # Prefix all parameters
595 4         1570 %post = map { 'hub.' . $_ => $post{$_} } keys %post;
  22         76  
596              
597             # Get user agent
598 4         41 my $ua = Mojo::UserAgent->new(
599             max_redirects => 3,
600             name => $UA_NAME
601             );
602              
603             # Send subscription change to hub
604 4         56 my $tx = $ua->post($param{hub} => form => \%post);
605              
606 4         79177 my $res = $tx->result;
607              
608             # No response
609 4 50       118 unless ($res) {
610 0         0 my $msg = 'Cannot ping hub';
611 0 0       0 $msg .= ' - maybe no SSL support' if index($param{hub}, 'https') == 0;
612 0         0 $log->warn($msg);
613 0         0 return;
614             };
615              
616             $mojo->plugins->emit_hook(
617             "after_pubsub_$mode" => (
618 4         24 $c, $param{hub}, \%post, $res->code, $res->body
619             ));
620              
621             # is 2xx, incl. 204 aka successful and 202 aka accepted
622 4 100       5761 my $success = $res->is_success ? 1 : 0;
623              
624 4 50       103 return ($success, $res->{body}) if wantarray;
625 4         27 return $success;
626             };
627              
628              
629             # Incoming data callback
630             sub callback {
631 9     9 0 22 my $plugin = shift;
632 9         17 my $c = shift;
633 9         29 my $log = $c->app->log;
634              
635 9   100     85 my $ct = $c->req->headers->header('Content-Type') || 'unknown';
636 9         262 my $type;
637              
638             # Is Atom
639 9 100       54 if ($ct =~ m{^application/atom\+xml}) {
    100          
640 4         12 $type = 'atom';
641             }
642              
643             # Is RSS
644             elsif ($ct =~ m{^application/r(?:ss|df)\+xml}) {
645 3         7 $type = 'rss';
646             }
647              
648             # Unsupported content type
649             else {
650 2 100       6 $log->warn("Unsupported media type: $ct") if $c->req->body;
651 2         89 return _render_fail($c);
652             };
653              
654 7         33 my $dom = Mojo::DOM->new(xml => 1, charset => 'UTF-8');
655              
656             # Parse fat ping
657 7         608 $dom->parse(b($c->req->body)->decode->to_string);
658              
659             # Find topics in Payload
660 7         22505 my $topics = _find_topics($type, $dom);
661              
662             # No topics to process - but technically fine
663 7 50       25 return _render_success($c) unless $topics->[0];
664              
665             # Save unfiltered topics for later comparison
666 7         21 my @old_topics = @$topics;
667              
668             # Check for secret and which topics are wanted
669 7         49 ($topics, my $secret, my $x_hub_on_behalf_of) =
670             $c->callback(pubsub_accept => $type, $topics);
671              
672 7   50     18251 $x_hub_on_behalf_of ||= 1;
673              
674             # No topics to process
675             # return _render_success( $c => $x_hub_on_behalf_of )
676 7 50       29 return _render_success( $c => 1 ) unless scalar @$topics;
677              
678             # Todo: Async with on(finish => ..)
679              
680             # Secret is needed
681 7 100       44 if ($secret) {
682              
683             # Unable to verify secret
684 3 50       16 unless ( _check_signature( $c, $secret )) {
685              
686 3         86 $log->debug(
687             'Unable to verify secret for ' . join('; ', @$topics)
688             );
689              
690             # return _render_success( $c => $x_hub_on_behalf_of );
691 3         40 return _render_success( $c => 1 );
692             };
693             };
694              
695             # Some topics are unwanted
696 4 100       16 if (@$topics != @old_topics) {
697              
698             # filter dom based on topics
699 3         13 $topics = _filter_topics($dom, $topics);
700             };
701              
702 4         29 $c->app->plugins->emit_hook(
703             on_pubsub_content => $c, $type, $dom
704             );
705              
706             # Successful
707 4         1506 return _render_success( $c => $x_hub_on_behalf_of );
708             };
709              
710              
711             # Find topics of entries
712             sub _find_topics {
713 10     10   23257 my $type = shift;
714 10         26 my $dom = shift;
715              
716             # Get all source links
717 10         43 my $links = $dom->find('source > link[rel="self"][href]');
718              
719             # Save href as topics
720 10 50   10   14142 my @topics = @{ $links->map( sub { $_->attr('href') } ) } if $links;
  10         76  
  10         101  
721              
722             # Find all entries, regardless if rss or atom
723 10         295 my $entries = $dom->find('item, feed > entry');
724              
725             # Not every entry has a source
726 10 50       14963 if ($links->size != $entries->size) {
727              
728             # One feed or entry
729 10         117 my $link = $dom->at(
730             'feed > link[rel="self"][href],' .
731             'channel > link[rel="self"][href]'
732             );
733              
734 10         11177 my $self_href;
735              
736             # Channel or feed link
737 10 50 0     46 if ($link) {
    0          
738 10         72 $self_href = $link->attr('href');
739             }
740              
741             # Source of first item in RSS
742             elsif (!$self_href && $type eq 'rss') {
743              
744             # Possible
745 0         0 $link = $dom->at('item > source');
746 0 0       0 $self_href = $link->attr('url') if $link;
747             };
748              
749             # Add topic to all entries
750 10 50       204 _add_topics($type, $dom, $self_href) if $self_href;
751              
752             # Get all source links
753 10         37 $links = $dom->find('source > link[rel="self"][href]');
754              
755             # Save href as topics
756 10 50   30   16135 @topics = @{ $links->map( sub { $_->attr('href') } ) } if $links;
  10         70  
  30         432  
757             };
758              
759             # Unify list
760 10 50       293 if (@topics > 1) {
761 10         27 my %topics = map { $_ => 1 } @topics;
  30         99  
762 10         72 @topics = sort keys %topics;
763             };
764              
765 10         66 return \@topics;
766             };
767              
768              
769             # Add topic to entries
770             sub _add_topics {
771 13     13   25662 state $atom_ns = 'http://www.w3.org/2005/Atom';
772              
773 13         29 my ($type, $dom, $self_href) = @_;
774              
775 13         49 my $link = qq{};
776              
777             # Add source information to each entry
778             $dom->find('item, entry')->each(
779             sub {
780 39     39   23545 my $entry = shift;
781 39         73 my $source;
782              
783             # Sources are found
784 39 50       114 if (my $sources = $entry->find('source')) {
785 39         10180 foreach my $s (@$sources) {
786 26 50 50     92 $source = $s and last if $s->namespace eq $atom_ns;
787             };
788             };
789              
790             # No source found
791 39 100 66     1271 unless ($source) {
792 13         72 $source = $entry->append_content(qq{})
793             ->at(qq{source[xmlns="$atom_ns"]});
794             }
795              
796             # Link already there
797             elsif ($source->at('link[rel="self"][href]')) {
798             return $dom;
799             };
800              
801             # Add link
802 26         11679 $source->append_content( $link );
803 13         42 });
804              
805 13         175 return $dom;
806             };
807              
808              
809             # filter entries based on their topic
810             sub _filter_topics {
811 6     6   6910 my $dom = shift;
812              
813 6         13 my %allowed = map { $_ => 1 } @{ shift(@_) };
  6         27  
  6         20  
814              
815 6         28 my $links = $dom->find(
816             'feed > entry > source > link[rel="self"][href],' .
817             'item > source > link[rel="self"][href]'
818             );
819              
820 6         17754 my %topics;
821              
822             # Delete entries that are not allowed
823             $links->each(
824             sub {
825 18     18   2856 my $l = shift;
826 18         56 my $href = $l->attr('href');
827              
828             # entry is not allowed
829 18 100       312 unless (exists $allowed{$href}) {
830 12         49 $l->parent->parent->replace('');
831             }
832              
833             # Entry is fine and found
834             else {
835 6         26 $topics{$href} = 1;
836             };
837 6         53 });
838              
839 6         94 return [ sort keys %topics ];
840             };
841              
842              
843             # Check signature
844             sub _check_signature {
845 3     3   11 my ($c, $secret) = @_;
846              
847 3         15 my $req = $c->req;
848              
849             # Get signature
850 3         61 my $signature = $req->headers->header('X-Hub-Signature');
851              
852             # Signature expected but not given
853 3 100       84 return unless $signature;
854              
855             # Delete signature prefix - don't remind, if it's not there.
856 2         17 $signature =~ s/^sha1=//i;
857              
858             # Generate check signature
859 2         13 my $signature_check = hmac_sha1_sum $req->body, $secret;
860              
861             # Return true if signature check succeeds
862 2         94 return secure_compare $signature, $signature_check;
863             };
864              
865              
866             # Render success
867             sub _render_success {
868 7     7   17 my $c = shift;
869 7         14 my $x_hub_on_behalf_of = shift;
870              
871             # Set X-Hub-On-Behalf-Of header
872 7 50 33     81 if ($x_hub_on_behalf_of &&
873             $x_hub_on_behalf_of =~ s/^\s*(\d+)\s*$/$1/) {
874              
875             # Set X-Hub-On-Behalf-Of header
876 7         38 $c->res->headers->header(
877             'X-Hub-On-Behalf-Of' => $x_hub_on_behalf_of
878             );
879             };
880              
881             # Render success with no content
882 7         534 return $c->render(
883             status => 204,
884             format => 'txt',
885             data => ''
886             );
887             };
888              
889              
890             # Render fail
891             sub _render_fail {
892 2     2   5 my $c = shift;
893              
894 2         3 my $fail =<<'FAIL';
895            
896            
897            
898             PubSubHubbub Endpoint
899            
900            
901            

PubSubHubbub Endpoint

902            

903             This is an endpoint for the
904             PubSubHubbub protocol
905            

906            

Your request was not correct.

907            
908            
909             FAIL
910              
911 2         9 return $c->render(
912             data => $fail,
913             status => 400 # bad request
914             );
915             };
916              
917              
918             1;
919              
920              
921             __END__