File Coverage

blib/lib/WWW/Yahoo/Groups.pm
Criterion Covered Total %
statement 87 189 46.0
branch 20 66 30.3
condition 2 27 7.4
subroutine 25 29 86.2
pod 16 17 94.1
total 150 328 45.7


line stmt bran cond sub pod time code
1             package WWW::Yahoo::Groups;
2 12     12   533664 use strict;
  12         34  
  12         2104  
3 12     12   77 use warnings FATAL => 'all';
  12         26  
  12         1297  
4              
5             =head1 NAME
6              
7             WWW::Yahoo::Groups - Automated access to Yahoo! Groups archives.
8              
9             =head1 SYNOPSIS
10              
11             my $y = WWW::Yahoo::Groups->new();
12             $y->login( $user => $pass );
13             $y->list( 'Jade_Pagoda' );
14             my $email = $y->fetch_message( 2345 );
15              
16             # Error catching
17             my $email = eval { $y->fetch_message( 93848 ) };
18             if ( $@ and ref $@ and $@->isa('X::WWW::Yahoo::Groups') )
19             {
20             warn "Problem: ".$@->error;
21             }
22              
23             =head1 DESCRIPTION
24              
25             C retrieves messages from the archive of Yahoo
26             Groups. It provides a simple OO interface to logging in and retrieving
27             said messages which you may then do with as you will.
28              
29             =head2 Things it does
30              
31             =over 4
32              
33             =item *
34              
35             B It lets you login.
36              
37             =item *
38              
39             B It notes that it got one and
40             progresses straight to the message.
41              
42             =item *
43              
44             B It just goes straight on.
45              
46             =item *
47              
48             B We get the source which happens to be the raw stuff.
49              
50             =item *
51              
52             B Could be improved, but it will generally barf if it
53             doesn't understand something.
54              
55             =item *
56              
57             B
I've found that some groups' archives have
58             unusually corrupted headers. Evidently it would be beneficial to
59             restore these headers. As far as I can tell, it comes from not
60             being a moderator on the lists in question.
61              
62             =back
63              
64             =head1 USAGE
65              
66             Try to be a well behaved bot and C for a few seconds (at least)
67             after doing things. It's considered polite. There's an
68             L method that should be useful for this.
69             Recently, this has been set to a default of 1 second. Feel free to tweak
70             if necessary.
71              
72             If you're used to seeing munged email addresses when you view
73             the message archive (i.e. you're not a moderator or owner of
74             the group) then you'll be pleased to know that
75             C can demunge those email addresses.
76              
77             All exceptions are subclasses of C, itself a
78             subclass of C. See L for
79             details.
80              
81             =head1 OTHER DOCUMENTATION
82              
83             =head2 I, by Kevin Hemenway and Tara Calishain
84              
85             I from O'Reilly
86             (L) is a great book for anyone
87             wanting to know more about screen-scraping and spidering.
88              
89             There is a WWW::Yahoo::Groups based hack by Andy Lester:
90              
91             =over 4
92              
93             =item 44 Archiving Yahoo! Groups Messages with WWW::Yahoo::Groups
94              
95             =item
96              
97             =back
98              
99             and two hacks, not related to this module, by me, Iain Truskett:
100              
101             =over 4
102              
103             =item 19 Scraping with HTML::TreeBuilder
104              
105             =item 57 Related Amazon.com Products with Alexa
106              
107             =back
108              
109             =cut
110              
111             our $VERSION = '1.91';
112              
113 12     12   75 use Carp;
  12         35  
  12         1294  
114 12     12   16611 use HTTP::Cookies;
  12         236383  
  12         412  
115 12     12   11520 use HTML::Entities;
  12         154417  
  12         1580  
116 12     12   15829 use Params::Validate qw( :all );
  12         188877  
  12         3128  
117 12     12   8839 use WWW::Yahoo::Groups::Mechanize;
  12         48  
  12         72112  
118              
119             require WWW::Yahoo::Groups::Errors;
120             Params::Validate::validation_options(
121             WWW::Yahoo::Groups::Errors->import()
122             );
123              
124             =head1 METHODS
125              
126             =head2 Constructor
127              
128             =head3 new
129              
130             Create a new C robot.
131              
132             my $y = WWW::Yahoo::Groups->new();
133              
134             It can take a has of named arguments. Two arguments are defined:
135             C and C. They correspond to the methods of the same
136             name.
137              
138             my $y = WWW::Yahoo::Groups->new(
139             debug => 1,
140             autosleep => 4,
141             );
142              
143             =cut
144              
145             sub new
146             {
147 12     12 1 1351 my $class = shift;
148 12         85 my %args = ( debug => 0, autosleep => 1, @_ );
149 12         50 my $self = bless {}, $class;
150 12         147 my $w = WWW::Yahoo::Groups::Mechanize->new();
151 12         76 $self->agent($w);
152 12         65 $self->debug( $args{debug} );
153 12         65 $self->autosleep( $args{ autosleep } );
154 12         78 return bless $self, $class;
155             }
156              
157             =head2 Options
158              
159             =head3 debug
160              
161             Enable/disable/read debugging mode.
162              
163             $y->debug(0); # Disable
164             $y->debug(1); # Enable
165             warn "Debugging!" if $y->debug();
166              
167             The C method of the current L object will
168             be invoked with the truth of the argument. This usually means
169             L.
170              
171             =cut
172              
173             sub debug
174             {
175 12     12 1 25 my $self = shift;
176 12 50       57 if (@_) {
177 12 50       46 my $true = ($_[0] ? 1 : 0);
178 12         32 $self->{__PACKAGE__.'-debug'} = $true;
179 12         38 $self->agent->debug( $true );
180             }
181 12         36 $self->{__PACKAGE__.'-debug'};
182             }
183              
184             =head3 autosleep
185              
186             If given a parameter, it sets the numbers of seconds to sleep.
187             Otherwise, it returns the number. Defaults to 1 second.
188              
189             $y->autosleep( 5 ); # Set it to 5.
190             sleep ( $y->autosleep() );
191              
192             May throw C if given invalid parameters.
193              
194             This is used by L. If C is set, then C will
195             C for the specified period after every fetch.
196              
197             Implemented by the object returned by L. By default this
198             means L.
199              
200             =cut
201              
202 19     19 1 4956 sub autosleep { my $self = shift; $self->agent->autosleep(@_) }
  19         71  
203              
204             =head2 Logging in and out
205              
206             =head3 login
207              
208             Logs the robot into the Yahoo! Groups system.
209              
210             $y->login( $user => $passwd );
211              
212             May throw:
213              
214             =over 4
215              
216             =item *
217              
218             C if it cannot fetch any of the
219             appropriate pages.
220              
221             =item *
222              
223             C if given invalid parameters.
224              
225             =item *
226              
227             C if unable to log in for some reason
228             (error will be given the text of the Yahoo error).
229              
230             =item *
231              
232             C if the object is already
233             logged in. I intend to make this exception redundant, perhaps by
234             just making C a null-op is we're already logged in, or by calling
235             L and then relogging in.
236              
237             =back
238              
239             =cut
240              
241             sub login
242             {
243 14     14 1 9691 my $self = shift;
244 14         27 my %p;
245 14         353 @p{qw( user pass )} = validate_pos( @_,
246             { type => SCALAR, }, # user
247             { type => SCALAR, }, # pass
248             );
249 9         59 my $w = $self->agent();
250 9         24 my $rv = eval {
251 9 50       73 X::WWW::Yahoo::Groups::AlreadyLoggedIn->throw(
252             "You must logout before you can log in again.")
253             if $self->loggedin;
254              
255 9         60 $w->get('http://groups.yahoo.com/');
256 9         220 $w->follow('Sign In');
257 0         0 $w->field( login => $p{user} );
258 0         0 $w->field( passwd => $p{pass} );
259 0         0 $w->click();
260 0 0       0 if (my ($error) = $w->res->content =~ m!
261             \Q\E
262             \s+
263             (.*?)
264             \s+
265             \Q
\E 266             !xsm) 267             { 268 0         0 X::WWW::Yahoo::Groups::BadLogin->throw( 269             fatal => 1, 270             error => $error); 271             } 272             else 273             { 274 0         0 while (my $url = $w->res->header('Location')) 275             { 276 0         0 $self->get( $url ); 277             } 278 0 0       0 if ( $w->content =~ m[ 279             \Qwindow.location.replace("http://groups.yahoo.com/");\E 280             ]x ) 281             { 282 0         0 $self->{__PACKAGE__.'-loggedin'} = 1; 283             } else { 284 0         0 X::WWW::Yahoo::Groups::BadLogin->throw( 285             fatal => 1, 286             error => "Nope. That's not a good login."); 287             } 288             } 289 0         0 0; 290             }; 291 9 50       305 if ($@) { 292 9 50       1599 die $@ unless ref $@; 293 0 0       0 $@->rethrow if $@->fatal; 294 0         0 $rv = $@; 295             } 296 0         0 return $rv; 297             } 298               299             =head3 logout 300               301             Logs the robot out of the Yahoo! Groups system. 302               303             $y->logout(); 304               305             May throw: 306               307             =over 4 308               309             =item * 310               311             C if it cannot fetch any of the 312             appropriate pages. 313               314             =item * 315               316             C if given invalid parameters. 317               318             =item * 319               320             C if the bot is already logged out 321             (or never logged in). 322               323             =back 324               325             =cut 326               327             sub logout 328             { 329 4     4 1 1637 my $self = shift; 330 4         19 my $w = $self->agent; 331 4         45 validate_pos( @_ ); 332 3         8 my $rv = eval { 333 3 50       8 X::WWW::Yahoo::Groups::NotLoggedIn->throw( 334             "You can not log out if you are not logged in.") 335             unless $self->loggedin; 336 0         0 delete $self->{__PACKAGE__.'-loggedin'}; 337               338 0         0 $w->get('http://groups.yahoo.com/'); 339               340 0 0       0 X::WWW::Yahoo::Groups::NotLoggedIn->throw( 341             "You can not log out if you are not logged in.") 342             unless $w->follow('Sign Out'); 343               344 0         0 $w->follow('Return to Yahoo! Groups'); 345 0         0 my $res = $w->res; 346 0         0 while ($res->is_redirect) 347             { 348             # We do this manually because it doesn't work automatically for 349             # some reason. I suspect we hit a redirection limit in LWP. 350 0         0 my $url = $res->header('Location'); 351 0         0 $w->get($url); 352 0         0 $res = $w->res; 353             } 354 0         0 0; 355             }; 356 3 50       1489 if ($@) { 357 3 50       27 die $@ unless ref $@; 358 3 50       87 $@->rethrow if $@->fatal; 359 3         177 $rv = $@; 360             } 361 3         13 return $rv; 362             } 363               364             =head3 loggedin 365               366             Returns 1 if you are logged in, else 0. Note that this merely tests if 367             you've used the L method successfully, not whether the Yahoo! 368             site has expired your session. 369               370             print "Logged in!\n" if $w->loggedin(); 371               372             =cut 373               374             sub loggedin 375             { 376 19     19 1 5694 my $self = shift; 377 19         607 validate_pos( @_ ); 378 18 50 33     107 if (exists $self->{__PACKAGE__.'-loggedin'} 379             and $self->{__PACKAGE__.'-loggedin'}) 380             { 381 0         0 return 1; 382             } 383 18         132 return 0; 384             } 385               386             =head2 Setting target list and finding possible lists 387               388             =head3 list 389               390             If given a parameter, it sets the list to use. Otherwise, it returns 391             the current list, or C if no list is set. 392               393             B: list name must be correctly cased as per how Yahoo! Groups 394             cases it. If not, you may experience odd behaviour. 395               396             $y->list( 'Jade_Pagoda' ); 397             my $list = $y->list(); 398               399             May throw C if given invalid parameters. 400               401             See also L for how to get a list of possible lists. 402               403             =cut 404               405             sub list 406             { 407 4     4 1 4079 my $self = shift; 408 4 100       18 if (@_) { 409             my ($list) = validate_pos( @_, 410             { type => SCALAR, callbacks => { 411             'defined and of length' => sub { 412 2 50   2   22 defined $_[0] and length $_[0] 413             }, 414             'appropriate characters' => sub { 415 3 50   3   40 defined $_[0] and $_[0] =~ /^ [\w-]+ $/x; 416             }, 417             }}, # list 418 3         67 ); 419 0         0 delete @{$self}{qw( first last )};   0         0   420 0         0 $self->{__PACKAGE__.'-list'} = $list; 421             } 422 1         4 return $self->{__PACKAGE__.'-list'}; 423             } 424               425             =head3 lists 426               427             If you'd like a list of the groups to which you are subscribed, 428             then use this method. 429               430             my @groups = $w->lists(); 431               432             May throw C if given invalid 433             parameters, or C if it cannot fetch any 434             of the appropriate pages from which it extracts the information. 435               436             Note that it does handle people with more than one page of groups. 437               438             =cut 439               440             sub lists 441             { 442 2     2 1 752 my $self = shift; 443 2         23 validate_pos( @_ ); 444 1 50       5 X::WWW::Yahoo::Groups::NotLoggedIn->throw( 445             "Must be logged in to get a list of groups.") 446             unless $self->loggedin; 447               448 0         0 my %lists; 449               450 0         0 my $next = 'http://groups.yahoo.com/mygroups'; 451 0         0 my $group_RE = qr# /group/ ([\w-]+?) \Q?yguid=\E #x; 452 0         0 my $w = $self->agent; 453 0         0 do { 454 0         0 $w->get( $next ); 455 0         0 undef $next; 456               457 0         0 my @lists = map { 458 0         0 $_->url =~ $group_RE; $1;   0         0   459             } $w->find_all_links( 460             url_regex => $group_RE, 461             ); 462 0         0 @lists{@lists} = 1; 463               464 0 0       0 if (my $url = $w->find_link( text => 'Next' ) ) 465             { 466 0         0 $next = $url->url; 467             } 468             } until ( not defined $next ); 469               470 0         0 return (sort keys %lists); 471             } 472               473             =head2 List information 474               475             =head3 first_msg_id 476               477             Returns the lowest message number with the archive. 478               479             my $first = $w->first_msg_id(); 480               481             It will throw C if no list has been 482             specified with L, C if 483             the page fetched does not contain anything we thought it would, and 484             C if it is unable to fetch the page it 485             needs. 486               487             =cut 488               489             sub get_extent 490             { 491 0     0 0 0 my $self = shift; 492 0         0 validate_pos( @_ ); 493 0         0 my $list = $self->list(); 494 0 0 0     0 X::WWW::Yahoo::Groups::NoListSet->throw( 495             "Cannot determine archive extent without a list being specified.") 496             unless defined $list and length $list; 497               498 0         0 my $w = $self->agent; 499 0         0 $w->get( "http://groups.yahoo.com/group/$list/messages/1" ); 500 0         0 my ($first, $last) = $w->res->content =~ m! 501             </td> </tr> <tr> <td class="h" > <a name="502">502</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> [^<]+? : \s+ </td> </tr> <tr> <td class="h" > <a name="503">503</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> (\d+)-\d+ \s+ (?:of|de|von|di|/) \s+ </td> </tr> <tr> <td class="h" > <a name="504">504</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> (\d+) </td> </tr> <tr> <td class="h" > <a name="505">505</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> [^<]*? </td> </tr> <tr> <td class="h" > <a name="506">506</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> 507             !six; 508               509 0 0       0 X::WWW::Yahoo::Groups::UnexpectedPage->throw( 510             "Unexpected title format. Perhaps group has no archive.") 511             unless defined $first; 512               513 0         0 @{$self}{qw( first last )} = ( $first, $last );   0         0   514 0         0 return ( $first, $last ); 515             } 516               517             sub first_msg_id 518             { 519 1     1 1 684 my $self = shift; 520 1         13 validate_pos( @_ ); 521 0 0       0 $self->get_extent unless exists $self->{first}; 522 0         0 return $self->{first}; 523             } 524               525             =head3 last_msg_id 526               527             Returns the highest message number with the archive. 528               529             my $last = $w->last_msg_id(); 530             # Fetch last 10 messages: 531             for my $number ( ($last-10) .. $last ) 532             { 533             push @messages, $w->fetch_message( $number ); 534             } 535               536             It will throw C if no list has been 537             specified with L, C if 538             the page fetched does not contain anything we thought it would, and 539             C if it is unable to fetch the page it 540             needs. 541               542             =cut 543               544             sub last_msg_id 545             { 546 1     1 1 684 my $self = shift; 547 1         15 validate_pos( @_ ); 548 0 0       0 $self->get_extent unless exists $self->{last}; 549 0         0 return $self->{last}; 550             } 551               552             =head2 Fetching an actual message 553               554             =head3 fetch_message 555               556             Fetches a specified message from the list's archives. Returns it as 557             a mail message (with headers) suitable for saving into a Maildir. 558               559             my $message = $y->fetch_message( 435 ); 560               561             May throw any of: 562               563             =over 4 564               565             =item * 566               567             C if it cannot fetch any of the 568             appropriate pages. 569               570             =item * 571               572             C if given invalid parameters. 573               574             =item * 575               576             C if no list is set. 577               578             =item * 579               580             C if we fetched a page and it was 581             not what we thought it was meant to be. 582               583             =item * 584               585             C if the message does not exist in the 586             archive (any of deleted, never archived or you're beyond the range of 587             the group). 588               589             =back 590               591             =cut 592               593             sub fetch_message 594             { 595 7     7 1 5215 my $self = shift; 596             my ($number) = validate_pos( @_, 597             { type => SCALAR, callbacks => { 598 5     5   53 'is positive integer' => sub { $_[0] =~ /^ (?!0+$) \d+ $/x }, 599             } }, # number 600 7         141 ); 601 1         10 my $list = $self->list(); 602 1 50 33     18 X::WWW::Yahoo::Groups::NoListSet->throw( 603             "Cannot fetch a message without a list being specified.") 604             unless defined $list and length $list; 605 0         0 my $template = "http://groups.yahoo.com/group/$list/message/%d?source=1&unwrap=1"; 606 0         0 my $w = $self->agent; 607 0         0 $w->get(sprintf $template, $number); 608 0         0 my $res = $w->res; 609 0         0 while ($res->is_redirect) 610             { 611             # We do this manually because it doesn't work automatically for 612             # some reason. I suspect we hit a redirection limit in LWP. 613 0         0 my $url = $res->header('Location'); 614 0         0 $w->get($url); 615 0         0 $res = $w->res; 616             } 617 0         0 my $content = $res->content; 618 0 0       0 if ( $w->uri =~ m,/interrupt\?st,gsm ) 619             { 620             # If it's one of those damn interrupting ads, then click 621             # through. 622 0         0 $w->follow_link( url_regex => qr{ /\Q$list\E/message/\d+ }x ); 623 0         0 $res = $w->res; 624 0         0 $content = $res->content; 625             } 626               627             # See if it's a missing article. 628 0 0       0 if ($content =~ m! 629            
630             \s+ 631            
632             \s+ 633             \QMessage $number does not exist in $list\E 634            
635             !smx) 636             { 637 0         0 X::WWW::Yahoo::Groups::NotThere->throw( 638             "Message $number is not there."); 639             } 640               641             # Strip content boundaries 642 0 0 0     0 $content =~ s/ ^ .*? \Q\E //sx and       0               0               0               0         643             $content =~ s/ \Q\E .* $ //sx and 644               645             # Strip table wrappings 646             $content =~ s/ ^ .*? ]+> .*? //sx and 647             $content =~ s!
\n !\n!xg and 648             $content =~ s!
\n \n .* $ !\n!sx and 649               650             # Munge content 651             $content =~ s{ ([^<]+) }{ 652 0         0 $self->_check_protected($1,$2) }egx or 653             X::WWW::Yahoo::Groups::UnexpectedPage->throw( 654             "Message $number doesn't appear to be formatted as we like it."); 655               656 0         0 for ($content) 657             { 658 0         0 s!
!!xg; 659 0         0 s/ ^ (--\w+--)
\n /$1\n\n/mgx; 660 0         0 s/
\n /\n/igx; 661 0         0 s/
//igx; 662 0         0 s/(\n)\n+$/$1/; 663 0         0 s{\Q[\E(\QAttachment content not displayed.\E)\Q]\E}{XXX $1 XXX\n}xg; 664             } 665 0         0 decode_entities($content); 666 0         0 $content = $self->reformat_headers( $content ); 667               668             # Return 669 0         0 return $content; 670             } 671               672             =head3 reformat_headers 673               674             This does some simple reformatting of headers. Yahoo!Groups seems to 675             manage to mangle multiline headers. This is particularly noticable with 676             the C header. 677               678             The rule is that any line that starts with a series of lowercase 679             letters or hyphens that is B immediately followed by a colon 680             is regarded as being part of the previous line and is indented with 681             a space character (as per RFC2822). 682               683             Input to this method should be a whole message. Output is that same 684             message, with the headers repaired. 685               686             This method is called by L but this was 687             not always the case. If you have archives that predate this implicit 688             call, you may want to run messages through this routine. 689               690             =cut 691               692             sub reformat_headers 693             { 694 0     0 1 0 my ($self, $msg) = @_; 695               696 0         0 my ($header, $body) = split /\n\n/, $msg, 2; 697               698 0         0 $header =~ s/^ (?! (?:From\ |[a-z-]+:) ) / /igmx; 699 0 0       0 $body = '' unless defined $body; 700               701 0         0 return $header."\n\n".$body; 702             } 703               704             =head2 Other methods 705               706             =head3 fetch_rss 707               708             Returns the RSS for the group's most recent messages. See 709             L for ways to process this RSS into 710             containing the message bodies. 711               712             my $rss = $w->fetch_rss(); 713               714             If a parameter is given, it will return that many items in the RSS file. 715             The number must be between 1 and 100 inclusive. 716               717             my $rss = $w->fetch_rss( 10 ); 718               719             =cut 720               721             sub fetch_rss 722             { 723 6     6 1 4482 my $self = shift; 724 6         7 my %opts; 725             @opts{qw( count )} = validate_pos( @_, 726             { type => SCALAR, optional => 1, callbacks => { 727             'is positive integer below 101' => sub { 728 5 100   5   66 $_[0] =~ /^ (?!0+$) \d+ $/x and $_[0] <= 100 729             }, 730             } }, # number 731 6         106 ); 732             # href="http://groups.yahoo.com/group/rss-dev/messages?rss=1&viscount=30"> 733 0         0 my $list = $self->list(); 734 0 0 0     0 X::WWW::Yahoo::Groups::NoListSet->throw( 735             "Cannot fetch a list's RSS without a list being specified.") 736             unless defined $list and length $list; 737 0         0 my $url = "http://groups.yahoo.com/group/$list/messages?rss=1"; 738 0 0       0 $url .= "&viscount=$opts{count}" if $opts{count}; 739 0         0 my $w = $self->agent; 740 0         0 $w->get( $url ); 741 0         0 my $content = $w->res->content; 742 0 0       0 X::WWW::Yahoo::Groups::UnexpectedPage->throw( 743             "Thought we were getting RSS. Got something else.") 744             unless $content =~ m[^ 745             \Q\E \s* 746             \Q\E \s* 747             \Q\E 748             ]sx; 749 0         0 return $content; 750             } 751               752             =head1 PRIVATE METHODS 753               754             =head2 agent 755               756             Returns or sets the C based agent. Not for general use. 757             If you must fiddle with it, your object's API must match that of 758             L and L. 759               760             =cut 761               762             sub agent 763             { 764 60     60 1 2170 my $self = shift; 765 60 100       370 @_ ? ( $self->{agent} = $_[0], $self ) : $self->{agent}; 766             } 767               768             =head2 get 769               770             Fetch a given URL. Delegated to L 771             (well, the C method of the object returned by L). 772               773             =cut 774               775 1     1 1 819 sub get { my $self = shift; $self->agent->get(@_) }   1         3   776               777             =head2 decode_protected 778               779             This method does nothing as Yahoo changed their algorithm. 780               781             =cut 782               783             sub decode_protected 784             { 785 0     0 1   my ($self, $code) = @_; 786 0           return; 787             } 788               789             =head2 _check_protected 790               791             This checks whether a given URL is to a protected email or not. It 792             returns C<$text> regardless as I do not have a decoding algorithm for 793             Yahoo's updated encoding scheme. 794               795             my $text = $self->_check_protected( $url, $text ); 796               797             =cut 798               799             sub _check_protected 800             { 801 0     0     my ( $self, $href, $text ) = @_; 802 0           return $text; 803             } 804               805             1; 806             __END__