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

Revision 402, 8.8 KB (checked in by eevee, 22 months ago)

BETTER fix for bbcode infinite loop, now that I realize why the original code looked like it did.

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