root/veekun/trunk/lib/Vee/BBCode.pm @ 401

Revision 401, 8.7 KB (checked in by eevee, 22 months ago)

Fix for an infinite loop in bbcode parser. A replaced tag with content containing a newline would loop forever.

Line 
1package Vee::BBCode;
2
3use strict;
4use warnings;
5
6=head1 NAME
7
8Vee::BBCode - Two-step bbcode parser
9
10=head1 SYNOPSIS
11
12    use Vee::BBCode;
13    Vee::BBCode::validate_bbcode('I [i]really[/i] think you should [b]stop[/b].');       # success
14    Vee::BBCode::validate_bbcode('Nested [b]tags [i]are not[/b] that[/i] complicated');  # failure
15   
16    Vee::BBCode::apply_bbcode('[i]Always[/i] [code]use strict;[/code]!');  # HTMLized
17
18=head1 DESCRIPTION
19
20Quick authorization module, to check whether the currently logged-in user has
21been given a particular permission.
22
23=head1 METHODS
24
25=cut
26
27# keys are the tag names
28# start/end subs take one parameter: the tag's parameter
29# replace subs take two parameters: the tag's contents and its parameter
30# TODO:
31#  - docs?
32# BUGS:
33#  - replace won't work with no_close
34#  - replace won't work when nested
35our %tags = (
36    b       => { param => 0, start => '<strong class="bbcode-b">', end => '</strong>' },
37    i       => { param => 0, start => '<em class="bbcode-i">', end => '</em>' },
38    u       => { param => 0, start => '<span class="bbcode-u">', end => '</span>' },
39    s       => { param => 0, start => '<span class="bbcode-s">', end => '</span>' },
40    h       => { param => 0, start => '<span class="bbcode-h">', end => '</span>' },
41    color   => { param => 2, strip_quotes => 1, start => sub {
42        my $color = shift;
43        if ($color !~ /
44            \#[0-9a-f]{3,6}
45            | (?:rgb|hsl) \( \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* \)
46            | (?:rgb|hsl) \( \s* \d{,3}  \s* , \s* \d{,3}  \s* , \s* \d{,3}  \s* \)
47            | (?:rgb|hsl)a \( \s* -?\d+  \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d(\.\d+)? \s* \)
48            | [a-z]+[0-9]*
49         /ix) { $color = 'inherit' }
50        qq'<span class="bbcode-color" style="color: $color;">'
51    }, end => '</span>' },
52#    size    => { param => 2, },
53    url     => { param => 1, strip_quotes => 1, replace => sub { qq'<a href="@{[ $_[0] ? $_[0] : $_[1] ]}" class="bbcode-url">$_[1]</a>' }  },
54    img     => { param => 1, strip_quotes => 1, raw => 1, start => sub { $_[0] ? qq'<img src="$_[0]" alt="' : '<img alt="" src="' }, end => '" class="bbcode-img"/>' },
55    code    => { param => 1, raw => 1, start => sub { '<div class="bbcode-code-heading">' . ($_[0] ? "Code for $_[0]" : 'Code') . ':</div><div class="bbcode-code">' }, end => '</div>' },
56    quote   => { param => 1, start => sub { '<div class="bbcode-quote-heading">' . ($_[0] ? "Quote from $_[0]" : 'Quote') . ':</div><div class="bbcode-quote">' }, end => '</div>' },
57    mono    => { param => 1, start => '<span class="bbcode-prop">', end => '</span>' },
58    raw     => { param => 1, raw => 1, start => '', end => '' },
59    hr      => { param => 0, no_close => 1, start => '<hr class="bbcode-hr"/>' },
60    center  => { param => 0, no_close => 0, start => '<div class="bbcode-center">', end => '</div>' },
61#    list    => { param => 1, contents => { '*' => 1 }, },
62#    '*'     => { param => 0, container => { list => 1 }, self_closing => 1, },
63);
64
65# change start, end, and replaces to subroutines for ease of runnage
66for my $tag_data (values %tags) {
67    for my $replacement (qw/start end replace/) {
68        my $format = $tag_data->{ $replacement };
69        next unless defined $format;
70        next if ref $format;
71        $tag_data->{ $replacement } = sub { sprintf $format, @_ };
72    }
73}
74
75=head2 apply_bbcode
76
77Applies bbcode formatting to a text string.  This makes no attempt to verify
78the well-formed-ness of the input.
79
80=cut
81
82sub apply_bbcode {
83    my $input = shift;
84
85    for my $tag (keys %tags) {
86        if (exists $tags{$tag}{replace}) {
87            $input =~ s: (.*) \[ $tag \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->(undef, $2) :gexis;
88            $input =~ s: (.*) \[ $tag = (.+?) \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->($2, $3) :gexis;
89        } else {
90            $input =~ s: \[ $tag \] : $tags{$tag}{start}->() :gexi;
91            $input =~ s: \[ $tag = (.+?) \] : $tags{$tag}{start}->($1) :gexi;
92            $input =~ s: \[ / $tag \] : $tags{$tag}{end}->() :gexi;
93        }
94    }
95   
96    $input =~ s:\x0D\x0A:<br/>:g;
97    $input =~ s:\x0D|\x0A:<br/>:g;
98   
99    return $input;
100}
101
102=head2 apply_bbcode
103
104Attempts to gauge whether a provided string of bbcode is valid.  Returns the
105string, possibly modified (I forget), and a list of errors if applicable.
106
107This method is in transition at the moment, sorry.
108
109=cut
110
111sub validate_bbcode {
112    my $input = shift;
113
114    $input =~ s/&/&amp;/g;
115    $input =~ s/&amp;(#x?[0-9a-f]{1,10});/&$1;/gi;
116    $input =~ s/\"/&quot;/g;
117    $input =~ s/</&lt;/g;
118    $input =~ s/>/&gt;/g;
119
120    my @tag_stack;
121#    my @closed_tag_stack;
122
123    my $lb_entity = '&#91;';
124    my $rb_entity = '&#93;';
125    my @output = ('');
126    my @errors;
127    my @replace_params;
128    my $pos = 0;
129    my ($left_bracket, $right_bracket);
130    PARSE: while ($input) {
131        $left_bracket  = index $input, '[';                      last if $left_bracket  == -1;
132        $right_bracket = index $input, ']', $left_bracket + 1;   last if $right_bracket == -1;
133        $left_bracket = rindex $input, '[', $right_bracket;
134        my $tag = substr $input, $left_bracket + 1, $right_bracket - $left_bracket - 1;
135       
136        # tack any plaintext before the next tag onto the output
137        my $raw = substr $input, 0, $left_bracket;
138        $raw =~ s/ \[ /$lb_entity/gx;
139        $raw =~ s/ \] /$rb_entity/gx;
140        $output[-1] .= $raw;
141        # then remove the preceding plaintext and the tag from the input
142        substr($input, 0, $right_bracket + 1) = '';
143
144        $left_bracket = 0;
145        $right_bracket -= $left_bracket;
146
147        my $closing = 0;
148        if ('/' eq substr $tag, 0, 1) {
149            $closing = 1;
150            $tag = substr $tag, 1;
151        }
152
153        my ($tag_name, $param) = split /=/, $tag, 2;
154
155        # check: skip the tag if it's not one of ours
156        if (!exists $tags{$tag_name}) {
157            $output[-1] .= $lb_entity . ($closing ? '/' : '') . $tag . $rb_entity;
158            next;
159        }
160
161        if ($param and $tags{$tag_name}{strip_quotes}) {
162            $param =~ s/^&quot;|&quot;$//g;
163        }
164
165        # check: can't have a param with a closing tag
166        if ($param && $closing) {
167            push @errors, [ "Parameter given on closing tag '$tag_name'", $left_bracket + 1 + length $tag ];
168            substr($input, $left_bracket + length($tag) + 2, length($param) + 1) = '';
169            $param = '';
170        }
171
172        # check if parameter should be here
173        if (!$closing and $param and $tags{$tag_name}{param} == 0) {
174            push @errors, [ "Parameter given for parameter-less tag '$tag_name'", $left_bracket + 1 + length $tag ];
175            $param = undef;
176        }
177        if (!$closing and!$param and $tags{$tag_name}{param} == 2) {
178            push @errors, [ "Parameter required for tag '$tag_name'", $left_bracket + 1 + length $tag ];
179            $param = '';
180        }
181
182        if ($tags{$tag_name}{no_close}) {
183            # don't do anything
184        } elsif (!$closing) {
185            push @tag_stack, $tag_name;
186        } else {
187            # check tag closing order
188            my $next_tag;
189            while ($next_tag = pop @tag_stack) {
190                last if $next_tag eq $tag_name;
191
192                push @errors, [ "Tag '$tag_name' not closed before parent tag '$next_tag'", $left_bracket ];
193                $output[-1] .= qq'<span class="bbcode-error-wrongorder">[/$tag_name]</span>';
194            }
195
196            if (!defined $next_tag) {
197                # we have run out of initial closing tags!  replace this with something temporary for now
198                push @errors, [ "Tag '$tag_name' closed but not open", $left_bracket ];
199                $output[-1] .= qq'<span class="bbcode-error-notopen">[/$tag_name]</span>';
200                next PARSE;
201            }
202        }
203
204        $output[-1] .= '[';
205        $output[-1] .= '/' if $closing;
206        $output[-1] .= $tag;
207        $output[-1] .= ']';
208
209        # if the tag is marked as 'raw', then its entire contents should be moved directly to output
210        if ($tags{$tag_name}{raw} and !$closing) {
211            my $raw_end = index $input, "[/$tag_name]";
212            my $raw = substr $input, 0, $raw_end;
213            $raw =~ s/ \[ /$lb_entity/gx;
214            $raw =~ s/ \] /$rb_entity/gx;
215            $output[-1] .= $raw;
216            $input = substr $input, $raw_end;
217        }
218    }
219   
220    for (@tag_stack) {
221        push @errors, [ "Tag '$_' never closed" ];
222    }
223
224    if ($input) {
225        $input =~ s/ \[ /$lb_entity/gx;
226        $input =~ s/ \] /$rb_entity/gx;
227        $output[-1] .= $input;
228    }
229   
230    return $output[-1], @errors;
231}
232
233=head1 AUTHOR
234
235Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
236
237See the included F<AUTHORS> file for a full list of contributers.
238
239=head1 LICENSE
240
241See the included F<LICENSE> file.
242
243=cut
244
2451;
Note: See TracBrowser for help on using the browser.