File Coverage

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


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::PubSubHubbub;
2 3     3   7497 use Mojo::Base 'Mojolicious::Plugin';
  3         6  
  3         19  
3 3     3   453 use Mojo::UserAgent;
  3         12  
  3         23  
4 3     3   77 use Mojo::DOM;
  3         4  
  3         60  
5 3     3   12 use Mojo::ByteStream 'b';
  3         5  
  3         121  
6 3     3   16 use Mojo::Util qw/secure_compare hmac_sha1_sum/;
  3         4  
  3         12173  
7              
8             our $VERSION = '0.20';
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 3461 my ($plugin, $mojo, $param) = @_;
33              
34 3   50     10 $param ||= {};
35              
36             # Load parameter from Config file
37 3 100       19 if (my $config_param = $mojo->config('PubSubHubbub')) {
38 1         14 $param = { %$param, %$config_param };
39             };
40              
41 3         85 my $helpers = $mojo->renderer->helpers;
42              
43             # Load 'callback' plugin
44 3 100       37 unless (exists $helpers->{'callback'}) {
45 2         7 $mojo->plugin('Util::Callback');
46             };
47              
48             # Set callbacks on registration
49 3         2469 $mojo->callback([qw/pubsub_accept pubsub_verify/] => $param);
50              
51             # Load 'endpoint' plugin
52 3 100       477 unless (exists $helpers->{'endpoint'}) {
53 2         8 $mojo->plugin('Util::Endpoint');
54             };
55              
56             # Load 'randomstring' plugin
57 3         5122 $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       8923 if ($param->{hub}) {
66 2         8 $plugin->hub($param->{hub});
67             };
68              
69             # Establish an endpoint
70 3         23 $mojo->endpoint('pubsub-hub' => $plugin->hub);
71              
72             # Set lease_seconds attribute
73 3 100       729 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   1254 my ($route, $param) = @_;
81              
82             # Set param default to 'cb'
83 2   100     10 $param ||= 'cb';
84              
85             # 'hub' is currently not supported
86 2 100       10 return unless $param eq 'cb';
87              
88             # Set PubSubHubbub endpoints
89 1         16 $route->endpoint('pubsub-callback');
90              
91             # Add 'callback' route
92             $route->to(
93             cb => sub {
94 18         154405 my $c = shift;
95              
96             # Hook on verification
97 18 100       52 return $plugin->verify($c) if $c->param('hub.mode');
98              
99             # Hook on callback
100 9         1480 return $plugin->callback($c);
101 1         39 });
102 3         18 });
103              
104             # Return plugin object
105             $mojo->helper(
106             'pubsub._plugin' => sub {
107 2     2   1202 $plugin;
108 3         178 });
109              
110             $mojo->helper(
111             'pubsub.publish' => sub {
112 3     3   5736 $plugin->publish( @_ );
113 3         982 });
114              
115             # Add 'subscribe' and 'unsubscribe' helper
116 3         811 foreach my $action (qw(subscribe unsubscribe)) {
117             $mojo->helper(
118             "pubsub.${action}" => sub {
119 8     8   7505 $plugin->_change_subscription(shift, mode => $action, @_);
120 6         833 });
121             };
122              
123             $mojo->helper(
124             'pubsub.discover' => sub {
125 0     0   0 $plugin->discover( @_ )
126             }
127 3         894 );
128             };
129              
130              
131             # Ping a hub for topics
132             sub publish {
133 3     3 1 7 my $plugin = shift;
134 3         5 my $c = shift;
135              
136             # Nothing to publish or no hub defined
137 3 100 66     12 return unless @_ || !$plugin->hub;
138              
139             # Set all urls
140 2         11 my @urls = map($c->endpoint($_), @_);
141              
142             # Create post message
143 2         1369 my %post = (
144             'hub.mode' => 'publish',
145             'hub.url' => \@urls
146             );
147              
148             # Get user agent
149 2         15 my $ua = Mojo::UserAgent->new(
150             max_redirects => 3,
151             name => $UA_NAME
152             );
153              
154 2         14 my $msg = 'Cannot ping hub';
155 2 50       6 $msg .= ' - maybe no SSL support' if index($plugin->hub, 'https') == 0;
156              
157             # Blocking
158             # Post to hub
159 2         15 my $tx = $ua->post( $plugin->hub => form => \%post );
160              
161 2         21648 my $res = $tx->result;
162              
163             # No response
164 2 50       44 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       7 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 2713 my $plugin = shift;
180 9         13 my $c = shift;
181              
182             # Good request
183 9 100 100     26 if ($c->param('hub.topic') &&
      100        
184             $c->param('hub.challenge') &&
185             $c->param('hub.mode') =~ /^(?:un)?subscribe$/) {
186              
187 4         603 my $challenge = $c->param('hub.challenge');
188              
189 4         188 my %param;
190 4         10 foreach (qw/mode
191             topic
192             verify
193             lease_seconds
194             verify_token/) {
195 20 100       1101 $param{$_} = $c->param("hub.$_") if $c->param("hub.$_");
196             };
197              
198             # Get verification callback
199 4         184 my $ok = $c->callback(
200             pubsub_verify => \%param
201             );
202              
203             # Render challenge
204 4 100       2433 return $c->render(
205             'status' => 200,
206             'format' => 'txt',
207             'data' => $challenge
208             ) if $ok;
209             };
210              
211             # Not found
212 7         518 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   970 my $header = shift;
220              
221 1         4 my $header_hash = $header->to_hash(1);
222              
223 1   50     21 my @links = (@{$header_hash->{Link} // []}, @{$header_hash->{link} // []});
  1   50     4  
  1         7  
224 1         10 my %links;
225              
226             # Iterate through all header links
227 1         3 foreach (@links) {
228              
229             # Make multiline headers one line
230 11 50       17 $_ = join(' ', @$_) if ref $_;
231              
232             # Check for link with correct relation
233 11 100       68 if ($_ =~ /^\<([^>]+?)\>(.*?rel\s*=\s*"(self|hub|alternate)".*?)$/mi) {
234              
235             # Create new link hash
236 7         23 my %link = ( href => $1, rel => $3 );
237              
238             # There may be more than one reference
239 7         12 my $check = $2;
240              
241             # Set type
242 7 100       30 if ($check =~ /type\s*=\s*"([^"]+?)"/omi) {
243 4         7 my $type = $1;
244 4 50 33     40 next if $type && $type !~ $FEED_TYPE_RE;
245 4         9 $link{type} = $type;
246 4         9 $link{short_type} = $1;
247             };
248              
249             # Set title
250 7 100       34 if ($check =~ /title\s*=\s*"([^"]+?)"/omi) {
251 5         11 $link{title} = $1;
252             };
253              
254             # Check file ending for short type
255 7 100       14 unless ($link{short_type}) {
256 3 100       20 $link{short_type} = $1 if $link{href} =~ $FEED_ENDING_RE;
257             };
258              
259             # Push found link
260 7         12 my $rel = $link{rel};
261 7   100     21 $links{$rel} //= [];
262 7         8 push(@{$links{$rel}}, \%link);
  7         21  
263             };
264             };
265              
266             # Return array
267 1         7 return \%links;
268             };
269              
270              
271             # Discover links from dom tree
272             sub _discover_dom_links {
273 2     2   5252 my $dom = shift;
274              
275 2         2 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   3563 my ($href, $rel, $type, $title) = @{$_->attr}{qw/href rel type title/};
  13         24  
281              
282             # Is no supported type
283 13 50 66     198 return if $type && $type !~ $FEED_TYPE_RE;
284              
285             # Set short type
286 13 100       33 my $short_type = $1 if $1;
287              
288 13 50 33     37 return unless $href && $rel;
289              
290             # Create new link hash
291 13         29 my %link = ( href => $href, rel => $rel );
292              
293             # Short type yet not known
294 13 100       24 unless ($short_type) {
295              
296             # Set short type by file ending
297 5 100       25 $link{short_type} = $1 if $href =~ m/\.(r(?:ss|df)|atom)$/i;
298             }
299              
300             # Set short type
301             else {
302 8         12 $link{short_type} = $short_type;
303             };
304              
305             # Set title and type
306 13 100       27 $link{title} = $title if $title;
307 13 100       20 $link{type} = $type if $type;
308              
309             # Push found link
310 13   100     44 $links{$rel} //= [];
311 13         18 push(@{$links{$rel}}, \%link);
  13         38  
312             }
313 2         5 );
314              
315             # Return array
316 2         36 return \%links;
317             };
318              
319              
320             # Heuristically sort links to best match the topic
321             sub _discover_sort_links {
322 3     3   5 my $links = shift;
323              
324 3         5 my ($topic, $hub);
325              
326             # Get self link as topic
327 3 100       9 if ($links->{self}) {
328              
329             # Find best match of all returned links
330 2         3 foreach my $link (@{$links->{self}}) {
  2         5  
331 2   33     9 $topic ||= $link;
332 2 50 33     5 if ($link->{short_type} && !$topic->{short_type}) {
333 0         0 $topic = $link;
334             };
335             };
336             };
337              
338             # Get hub
339 3 50       7 if ($links->{hub}) {
340              
341             # Find best match of all returned links
342 3         5 foreach my $link (@{$links->{hub}}) {
  3         5  
343 3   33     15 $hub ||= $link;
344 3 50 33     7 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     15 return ($topic, $hub) if $topic && $hub;
352              
353             # Check alternates
354 1         2 my $alternate = $links->{alternate};
355              
356             # Search in alternate representations for best match
357 1 50       4 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       21 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       12 if ($link->{title} =~ /[ck]omment/i) {
373 2         4 $link->{pref} = 1;
374             }
375              
376             # This may be the correct feed
377             else {
378 3         5 $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       5 if ($a->{pref} < $b->{pref}) {
  8 100       19  
    50          
    0          
    0          
    0          
393 3         4 return 1;
394             }
395             elsif ($a->{pref} > $b->{pref}) {
396 1         2 return -1;
397             }
398             # Sort by type
399             elsif ($a->{short_type} gt $b->{short_type}) {
400 4         8 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         4 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   15 my $plugin = shift;
542 8         11 my $c = shift;
543 8         23 my %param = @_;
544              
545 8         22 my $log = $c->app->log;
546              
547             # Get callback endpoint
548             # Works only if endpoints provided
549 8 50 33     87 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     4827 unless (exists $param{topic} &&
      100        
555             $param{topic} =~ m{^https?://}i &&
556             exists $param{hub}) {
557 4         15 $log->warn('You have to specify a topic and a hub');
558 4         44 return;
559             };
560              
561 4         10 my $mode = $param{mode};
562              
563             # delete lease seconds if no integer
564 4 0 0     11 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     21 $param{lease_seconds} ||= $plugin->lease_seconds if $mode eq 'subscribe';
572              
573             # Render post string
574 4         20 my %post = ( callback => $param{callback} );
575 4         10 foreach ( qw/mode topic verify lease_seconds secret/ ) {
576 20 50 66     54 $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       21 $c->random_string('pubsub_challenge'));
585              
586 4         168 $post{verify} = "${_}sync" foreach ('a', '');
587              
588 4         12 my $mojo = $c->app;
589              
590 4         17 $mojo->plugins->emit_hook(
591             "before_pubsub_$mode" => ($c, \%param, \%post)
592             );
593              
594             # Prefix all parameters
595 4         1043 %post = map { 'hub.' . $_ => $post{$_} } keys %post;
  22         60  
596              
597             # Get user agent
598 4         25 my $ua = Mojo::UserAgent->new(
599             max_redirects => 3,
600             name => $UA_NAME
601             );
602              
603             # Send subscription change to hub
604 4         37 my $tx = $ua->post($param{hub} => form => \%post);
605              
606 4         50943 my $res = $tx->result;
607              
608             # No response
609 4 50       96 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         14 $c, $param{hub}, \%post, $res->code, $res->body
619             ));
620              
621             # is 2xx, incl. 204 aka successful and 202 aka accepted
622 4 100       3964 my $success = $res->is_success ? 1 : 0;
623              
624 4 50       68 return ($success, $res->{body}) if wantarray;
625 4         16 return $success;
626             };
627              
628              
629             # Incoming data callback
630             sub callback {
631 9     9 0 14 my $plugin = shift;
632 9         14 my $c = shift;
633 9         21 my $log = $c->app->log;
634              
635 9   100     56 my $ct = $c->req->headers->header('Content-Type') || 'unknown';
636 9         178 my $type;
637              
638             # Is Atom
639 9 100       36 if ($ct =~ m{^application/atom\+xml}) {
    100          
640 4         6 $type = 'atom';
641             }
642              
643             # Is RSS
644             elsif ($ct =~ m{^application/r(?:ss|df)\+xml}) {
645 3         5 $type = 'rss';
646             }
647              
648             # Unsupported content type
649             else {
650 2 100       5 $log->warn("Unsupported media type: $ct") if $c->req->body;
651 2         67 return _render_fail($c);
652             };
653              
654 7         23 my $dom = Mojo::DOM->new(xml => 1, charset => 'UTF-8');
655              
656             # Parse fat ping
657 7         462 $dom->parse(b($c->req->body)->decode->to_string);
658              
659             # Find topics in Payload
660 7         17679 my $topics = _find_topics($type, $dom);
661              
662             # No topics to process - but technically fine
663 7 50       19 return _render_success($c) unless $topics->[0];
664              
665             # Save unfiltered topics for later comparison
666 7         14 my @old_topics = @$topics;
667              
668             # Check for secret and which topics are wanted
669 7         25 ($topics, my $secret, my $x_hub_on_behalf_of) =
670             $c->callback(pubsub_accept => $type, $topics);
671              
672 7   50     13909 $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       18 return _render_success( $c => 1 ) unless scalar @$topics;
677              
678             # Todo: Async with on(finish => ..)
679              
680             # Secret is needed
681 7 100       32 if ($secret) {
682              
683             # Unable to verify secret
684 3 100       8 unless ( _check_signature( $c, $secret )) {
685              
686 2         18 $log->debug(
687             'Unable to verify secret for ' . join('; ', @$topics)
688             );
689              
690             # return _render_success( $c => $x_hub_on_behalf_of );
691 2         15 return _render_success( $c => 1 );
692             };
693             };
694              
695             # Some topics are unwanted
696 5 100       42 if (@$topics != @old_topics) {
697              
698             # filter dom based on topics
699 4         7 $topics = _filter_topics($dom, $topics);
700             };
701              
702 5         18 $c->app->plugins->emit_hook(
703             on_pubsub_content => $c, $type, $dom
704             );
705              
706             # Successful
707 5         1825 return _render_success( $c => $x_hub_on_behalf_of );
708             };
709              
710              
711             # Find topics of entries
712             sub _find_topics {
713 10     10   18105 my $type = shift;
714 10         14 my $dom = shift;
715              
716             # Get all source links
717 10         26 my $links = $dom->find('source > link[rel="self"][href]');
718              
719             # Save href as topics
720 10 50   10   10822 my @topics = @{ $links->map( sub { $_->attr('href') } ) } if $links;
  10         55  
  10         68  
721              
722             # Find all entries, regardless if rss or atom
723 10         221 my $entries = $dom->find('item, feed > entry');
724              
725             # Not every entry has a source
726 10 50       11411 if ($links->size != $entries->size) {
727              
728             # One feed or entry
729 10         83 my $link = $dom->at(
730             'feed > link[rel="self"][href],' .
731             'channel > link[rel="self"][href]'
732             );
733              
734 10         8473 my $self_href;
735              
736             # Channel or feed link
737 10 50 0     26 if ($link) {
    0          
738 10         55 $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       179 _add_topics($type, $dom, $self_href) if $self_href;
751              
752             # Get all source links
753 10         23 $links = $dom->find('source > link[rel="self"][href]');
754              
755             # Save href as topics
756 10 50   30   12401 @topics = @{ $links->map( sub { $_->attr('href') } ) } if $links;
  10         47  
  30         347  
757             };
758              
759             # Unify list
760 10 50       192 if (@topics > 1) {
761 10         18 my %topics = map { $_ => 1 } @topics;
  30         69  
762 10         56 @topics = sort keys %topics;
763             };
764              
765 10         46 return \@topics;
766             };
767              
768              
769             # Add topic to entries
770             sub _add_topics {
771 13     13   19967 state $atom_ns = 'http://www.w3.org/2005/Atom';
772              
773 13         31 my ($type, $dom, $self_href) = @_;
774              
775 13         34 my $link = qq{};
776              
777             # Add source information to each entry
778             $dom->find('item, entry')->each(
779             sub {
780 39     39   17904 my $entry = shift;
781 39         56 my $source;
782              
783             # Sources are found
784 39 50       79 if (my $sources = $entry->find('source')) {
785 39         7466 foreach my $s (@$sources) {
786 26 50 50     58 $source = $s and last if $s->namespace eq $atom_ns;
787             };
788             };
789              
790             # No source found
791 39 100 66     992 unless ($source) {
792 13         52 $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         8901 $source->append_content( $link );
803 13         36 });
804              
805 13         129 return $dom;
806             };
807              
808              
809             # filter entries based on their topic
810             sub _filter_topics {
811 7     7   5498 my $dom = shift;
812              
813 7         12 my %allowed = map { $_ => 1 } @{ shift(@_) };
  7         23  
  7         15  
814              
815 7         24 my $links = $dom->find(
816             'feed > entry > source > link[rel="self"][href],' .
817             'item > source > link[rel="self"][href]'
818             );
819              
820 7         16150 my %topics;
821              
822             # Delete entries that are not allowed
823             $links->each(
824             sub {
825 21     21   2626 my $l = shift;
826 21         49 my $href = $l->attr('href');
827              
828             # entry is not allowed
829 21 100       315 unless (exists $allowed{$href}) {
830 14         34 $l->parent->parent->replace('');
831             }
832              
833             # Entry is fine and found
834             else {
835 7         24 $topics{$href} = 1;
836             };
837 7         41 });
838              
839 7         83 return [ sort keys %topics ];
840             };
841              
842              
843             # Check signature
844             sub _check_signature {
845 3     3   6 my ($c, $secret) = @_;
846              
847 3         9 my $req = $c->req;
848              
849             # Get signature
850 3         33 my $signature = $req->headers->header('X-Hub-Signature');
851              
852             # Signature expected but not given
853 3 100       51 return unless $signature;
854              
855             # Delete signature prefix - don't remind, if it's not there.
856 2         9 $signature =~ s/^sha1=//i;
857              
858             # Generate check signature
859 2         6 my $signature_check = hmac_sha1_sum $req->body, $secret;
860              
861             # Return true if signature check succeeds
862 2         54 return secure_compare $signature, $signature_check;
863             };
864              
865              
866             # Render success
867             sub _render_success {
868 7     7   45 my $c = shift;
869 7         12 my $x_hub_on_behalf_of = shift;
870              
871             # Set X-Hub-On-Behalf-Of header
872 7 50 33     65 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         22 $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         287 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   3 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         15 return $c->render(
912             data => $fail,
913             status => 400 # bad request
914             );
915             };
916              
917              
918             1;
919              
920              
921             __END__