| 1 | # See bottom of file for license and copyright information |
|---|
| 2 | |
|---|
| 3 | =begin TML |
|---|
| 4 | |
|---|
| 5 | ---+ package WysiwygPlugin |
|---|
| 6 | |
|---|
| 7 | This plugin is responsible for translating TML to HTML before an edit starts |
|---|
| 8 | and translating the resultant HTML back into TML. |
|---|
| 9 | |
|---|
| 10 | Note: In the case of a new topic, you might expect to see the "create topic" |
|---|
| 11 | screen in the editor when it goes back to Foswiki for the topic content. This |
|---|
| 12 | doesn't happen because the earliest possible handler is called on the topic |
|---|
| 13 | content and not the template. The template is effectively ignored and a blank |
|---|
| 14 | document is sent to the editor. |
|---|
| 15 | |
|---|
| 16 | Attachment uploads can be handled by URL requests from the editor to the rest |
|---|
| 17 | handler in this plugin. This avoids the need to add any scripts to the bin dir. |
|---|
| 18 | You will have to use a form, though, as XmlHttpRequest does not support file |
|---|
| 19 | uploads. |
|---|
| 20 | |
|---|
| 21 | =cut |
|---|
| 22 | |
|---|
| 23 | package Foswiki::Plugins::WysiwygPlugin; |
|---|
| 24 | |
|---|
| 25 | use CGI qw( :cgi -any ); |
|---|
| 26 | |
|---|
| 27 | use strict; |
|---|
| 28 | |
|---|
| 29 | use Assert; |
|---|
| 30 | use Encode (); |
|---|
| 31 | |
|---|
| 32 | use Foswiki::Func (); # The plugins API |
|---|
| 33 | use Foswiki::Plugins (); # For the API version |
|---|
| 34 | use Foswiki::Plugins::WysiwygPlugin::Constants (); |
|---|
| 35 | |
|---|
| 36 | use vars qw( $html2tml $tml2html $recursionBlock $imgMap ); |
|---|
| 37 | use vars qw( %FoswikiCompatibility @refs %xmltag %xmltagPlugin); |
|---|
| 38 | |
|---|
| 39 | our $SHORTDESCRIPTION = 'Translator framework for Wysiwyg editors'; |
|---|
| 40 | our $NO_PREFS_IN_TOPIC = 1; |
|---|
| 41 | our $VERSION = '$Rev$'; |
|---|
| 42 | |
|---|
| 43 | our $RELEASE = '18 Sep 2009'; |
|---|
| 44 | |
|---|
| 45 | our $SECRET_ID = |
|---|
| 46 | 'WYSIWYG content - do not remove this comment, and never use this identical text in your topics'; |
|---|
| 47 | |
|---|
| 48 | sub WHY { 1 } |
|---|
| 49 | |
|---|
| 50 | sub initPlugin { |
|---|
| 51 | my ( $topic, $web, $user, $installWeb ) = @_; |
|---|
| 52 | |
|---|
| 53 | # %OWEB%.%OTOPIC% is the topic where the initial content should be |
|---|
| 54 | # grabbed from, as defined in templates/edit.skin.tmpl |
|---|
| 55 | Foswiki::Func::registerTagHandler( 'OWEB', \&_OWEBTAG ); |
|---|
| 56 | Foswiki::Func::registerTagHandler( 'OTOPIC', \&_OTOPICTAG ); |
|---|
| 57 | Foswiki::Func::registerTagHandler( 'WYSIWYG_TEXT', \&_WYSIWYG_TEXT ); |
|---|
| 58 | Foswiki::Func::registerTagHandler( 'JAVASCRIPT_TEXT', \&_JAVASCRIPT_TEXT ); |
|---|
| 59 | Foswiki::Func::registerTagHandler( 'WYSIWYG_SECRET_ID', |
|---|
| 60 | sub { $SECRET_ID } ); |
|---|
| 61 | |
|---|
| 62 | Foswiki::Func::registerRESTHandler( 'tml2html', \&_restTML2HTML ); |
|---|
| 63 | Foswiki::Func::registerRESTHandler( 'html2tml', \&_restHTML2TML ); |
|---|
| 64 | Foswiki::Func::registerRESTHandler( 'upload', \&_restUpload ); |
|---|
| 65 | Foswiki::Func::registerRESTHandler( 'attachments', \&_restAttachments ); |
|---|
| 66 | |
|---|
| 67 | # Plugin correctly initialized |
|---|
| 68 | return 1; |
|---|
| 69 | } |
|---|
| 70 | |
|---|
| 71 | sub _OWEBTAG { |
|---|
| 72 | my ( $session, $params, $topic, $web ) = @_; |
|---|
| 73 | |
|---|
| 74 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 75 | |
|---|
| 76 | return $web unless $query; |
|---|
| 77 | |
|---|
| 78 | if ( defined( $query->param('templatetopic') ) ) { |
|---|
| 79 | my @split = split( /\./, $query->param('templatetopic') ); |
|---|
| 80 | |
|---|
| 81 | if ( $#split == 0 ) { |
|---|
| 82 | return $web; |
|---|
| 83 | } |
|---|
| 84 | else { |
|---|
| 85 | return $split[0]; |
|---|
| 86 | } |
|---|
| 87 | } |
|---|
| 88 | |
|---|
| 89 | return $web; |
|---|
| 90 | } |
|---|
| 91 | |
|---|
| 92 | sub _OTOPICTAG { |
|---|
| 93 | my ( $session, $params, $topic, $web ) = @_; |
|---|
| 94 | |
|---|
| 95 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 96 | |
|---|
| 97 | return $topic unless $query; |
|---|
| 98 | |
|---|
| 99 | if ( defined( $query->param('templatetopic') ) ) { |
|---|
| 100 | my @split = split( /\./, $query->param('templatetopic') ); |
|---|
| 101 | |
|---|
| 102 | return $split[$#split]; |
|---|
| 103 | } |
|---|
| 104 | |
|---|
| 105 | return $topic; |
|---|
| 106 | } |
|---|
| 107 | |
|---|
| 108 | $FoswikiCompatibility{startRenderingHandler} = 2.1; |
|---|
| 109 | sub startRenderingHandler { |
|---|
| 110 | $_[0] =~ s#</?sticky>##g; |
|---|
| 111 | } |
|---|
| 112 | |
|---|
| 113 | # This handler is used to determine whether the topic is editable by |
|---|
| 114 | # a WYSIWYG editor or not. The only thing it does is to redirect to a |
|---|
| 115 | # normal edit url if the skin is set to WYSIWYGPLUGIN_WYSIWYGSKIN and |
|---|
| 116 | # nasty content is found. |
|---|
| 117 | sub beforeEditHandler { |
|---|
| 118 | |
|---|
| 119 | #my( $text, $topic, $web, $meta ) = @_; |
|---|
| 120 | |
|---|
| 121 | my $skin = Foswiki::Func::getPreferencesValue('WYSIWYGPLUGIN_WYSIWYGSKIN'); |
|---|
| 122 | |
|---|
| 123 | if ( $skin && Foswiki::Func::getSkin() =~ /\b$skin\b/o ) { |
|---|
| 124 | if ( notWysiwygEditable( $_[0] ) ) { |
|---|
| 125 | |
|---|
| 126 | # redirect |
|---|
| 127 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 128 | foreach my $p qw( skin cover ) { |
|---|
| 129 | my $arg = $query->param($p); |
|---|
| 130 | if ( $arg && $arg =~ s/\b$skin\b// ) { |
|---|
| 131 | if ( $arg =~ /^[\s,]*$/ ) { |
|---|
| 132 | $query->delete($p); |
|---|
| 133 | } |
|---|
| 134 | else { |
|---|
| 135 | $query->param( -name => $p, -value => $arg ); |
|---|
| 136 | } |
|---|
| 137 | } |
|---|
| 138 | } |
|---|
| 139 | my $url = $query->url( -full => 1, -path => 1, -query => 1 ); |
|---|
| 140 | Foswiki::Func::redirectCgiQuery( $query, $url ); |
|---|
| 141 | |
|---|
| 142 | # Bring this session to an untimely end |
|---|
| 143 | exit 0; |
|---|
| 144 | } |
|---|
| 145 | } |
|---|
| 146 | } |
|---|
| 147 | |
|---|
| 148 | # This handler is only invoked *after* merging is complete |
|---|
| 149 | sub beforeSaveHandler { |
|---|
| 150 | |
|---|
| 151 | #my( $text, $topic, $web ) = @_; |
|---|
| 152 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 153 | return unless $query; |
|---|
| 154 | |
|---|
| 155 | return unless defined( $query->param('wysiwyg_edit') ); |
|---|
| 156 | |
|---|
| 157 | $_[0] = TranslateHTML2TML( $_[0], $_[1], $_[2] ); |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | # This handler is invoked before a merge. Merges are done before the |
|---|
| 161 | # afterEditHandler is called, so we need to translate here. |
|---|
| 162 | sub beforeMergeHandler { |
|---|
| 163 | |
|---|
| 164 | #my( $text, $currRev, $currText, $origRev, $origText, $web, $topic ) = @_; |
|---|
| 165 | afterEditHandler( $_[0], $_[6], $_[5] ); |
|---|
| 166 | } |
|---|
| 167 | |
|---|
| 168 | # This handler is invoked *after* a merge, and only from the edit |
|---|
| 169 | # script (so it's useless for a REST save) |
|---|
| 170 | sub afterEditHandler { |
|---|
| 171 | my ( $text, $topic, $web ) = @_; |
|---|
| 172 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 173 | return unless $query; |
|---|
| 174 | |
|---|
| 175 | if ( $Foswiki::cfg{Site}{CharSet} |
|---|
| 176 | && $Foswiki::cfg{Site}{CharSet} =~ /^utf-?8$/i ) |
|---|
| 177 | { |
|---|
| 178 | |
|---|
| 179 | # If the site charset is utf-8, then form POSTs (such as the one |
|---|
| 180 | # that got us here) are utf-8 encoded. we have to decode to prevent |
|---|
| 181 | # the HTML parser from going tits up when it sees utf-8 in the data. |
|---|
| 182 | $text = Encode::decode_utf8($text); |
|---|
| 183 | } |
|---|
| 184 | |
|---|
| 185 | return |
|---|
| 186 | unless defined( $query->param('wysiwyg_edit') ) |
|---|
| 187 | || $text =~ s/<!--$SECRET_ID-->//go; |
|---|
| 188 | |
|---|
| 189 | # Switch off wysiwyg_edit so it doesn't try to transform again in |
|---|
| 190 | # the beforeSaveHandler |
|---|
| 191 | $query->delete('wysiwyg_edit'); |
|---|
| 192 | |
|---|
| 193 | $text = TranslateHTML2TML( $text, $_[1], $_[2] ); |
|---|
| 194 | |
|---|
| 195 | $_[0] = $text; |
|---|
| 196 | } |
|---|
| 197 | |
|---|
| 198 | # Invoked to convert HTML to TML (best efforts) |
|---|
| 199 | sub TranslateHTML2TML { |
|---|
| 200 | my ( $text, $topic, $web ) = @_; |
|---|
| 201 | |
|---|
| 202 | unless ($html2tml) { |
|---|
| 203 | require Foswiki::Plugins::WysiwygPlugin::HTML2TML; |
|---|
| 204 | |
|---|
| 205 | $html2tml = new Foswiki::Plugins::WysiwygPlugin::HTML2TML(); |
|---|
| 206 | } |
|---|
| 207 | |
|---|
| 208 | # SMELL: really, really bad smell; bloody core should NOT pass text |
|---|
| 209 | # with embedded meta to plugins! It is VERY BAD DESIGN!!! |
|---|
| 210 | my $top = ''; |
|---|
| 211 | if ( $text =~ s/^(%META:[A-Z]+{.*?}%\r?\n)//s ) { |
|---|
| 212 | $top = $1; |
|---|
| 213 | } |
|---|
| 214 | my $bottom = ''; |
|---|
| 215 | $text =~ s/^(%META:[A-Z]+{.*?}%\r?\n)/$bottom = "$1$bottom";''/gem; |
|---|
| 216 | |
|---|
| 217 | my $opts = { |
|---|
| 218 | web => $web, |
|---|
| 219 | topic => $topic, |
|---|
| 220 | convertImage => \&_convertImage, |
|---|
| 221 | rewriteURL => \&postConvertURL, |
|---|
| 222 | very_clean => 1, # aggressively polish saved HTML |
|---|
| 223 | }; |
|---|
| 224 | |
|---|
| 225 | $text = $html2tml->convert( $text, $opts ); |
|---|
| 226 | |
|---|
| 227 | $text =~ s/\s+$/\n/s; |
|---|
| 228 | |
|---|
| 229 | return $top . $text . $bottom; |
|---|
| 230 | } |
|---|
| 231 | |
|---|
| 232 | # Handler used to process text in a =view= URL to generate text/html |
|---|
| 233 | # containing the HTML of the topic to be edited. |
|---|
| 234 | # |
|---|
| 235 | # Invoked when the selected skin is in use to convert the text to HTML |
|---|
| 236 | # We can't use the beforeEditHandler, because the editor loads up and then |
|---|
| 237 | # uses a URL to fetch the text to be edited. This handler is designed to |
|---|
| 238 | # provide the text for that request. It's a real struggle, because the |
|---|
| 239 | # commonTagsHandler is called so many times that getting the right |
|---|
| 240 | # call is hard, and then preventing a repeat call is harder! |
|---|
| 241 | sub beforeCommonTagsHandler { |
|---|
| 242 | |
|---|
| 243 | #my ( $text, $topic, $web, $meta ) |
|---|
| 244 | return if $recursionBlock; |
|---|
| 245 | return unless Foswiki::Func::getContext()->{body_text}; |
|---|
| 246 | |
|---|
| 247 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 248 | |
|---|
| 249 | return unless $query; |
|---|
| 250 | |
|---|
| 251 | return unless defined( $query->param('wysiwyg_edit') ); |
|---|
| 252 | |
|---|
| 253 | # stop it from processing the template without expanded |
|---|
| 254 | # %TEXT% (grr; we need a better way to tell where we |
|---|
| 255 | # are in the processing pipeline) |
|---|
| 256 | return if ( $_[0] =~ /^<!-- WysiwygPlugin Template/ ); |
|---|
| 257 | |
|---|
| 258 | # Have to re-read the topic because verbatim blocks have already been |
|---|
| 259 | # lifted out, and we need them. |
|---|
| 260 | my $topic = $_[1]; |
|---|
| 261 | my $web = $_[2]; |
|---|
| 262 | my ( $meta, $text ); |
|---|
| 263 | my $altText = $query->param('templatetopic'); |
|---|
| 264 | if ( $altText && Foswiki::Func::topicExists( $web, $altText ) ) { |
|---|
| 265 | ( $web, $topic ) = |
|---|
| 266 | Foswiki::Func::normalizeWebTopicName( $web, $altText ); |
|---|
| 267 | } |
|---|
| 268 | |
|---|
| 269 | $_[0] = _WYSIWYG_TEXT( $Foswiki::Plugins::SESSION, {}, $topic, $web ); |
|---|
| 270 | } |
|---|
| 271 | |
|---|
| 272 | # Handler used by editors that require pre-prepared HTML embedded in the |
|---|
| 273 | # edit template. |
|---|
| 274 | sub _WYSIWYG_TEXT { |
|---|
| 275 | my ( $session, $params, $topic, $web ) = @_; |
|---|
| 276 | |
|---|
| 277 | # Have to re-read the topic because content has already been munged |
|---|
| 278 | # by other plugins, or by the extraction of verbatim blocks. |
|---|
| 279 | my ( $meta, $text ) = Foswiki::Func::readTopic( $web, $topic ); |
|---|
| 280 | |
|---|
| 281 | $text = TranslateTML2HTML( $text, $web, $topic ); |
|---|
| 282 | |
|---|
| 283 | # Lift out the text to protect it from further Foswiki rendering. It will be |
|---|
| 284 | # put back in the postRenderingHandler. |
|---|
| 285 | return _liftOut($text); |
|---|
| 286 | } |
|---|
| 287 | |
|---|
| 288 | # Handler used to present the editable text in a javascript constant string |
|---|
| 289 | sub _JAVASCRIPT_TEXT { |
|---|
| 290 | my ( $session, $params, $topic, $web ) = @_; |
|---|
| 291 | |
|---|
| 292 | my $html = _dropBack( _WYSIWYG_TEXT(@_) ); |
|---|
| 293 | |
|---|
| 294 | $html =~ s/([\\'])/\\$1/sg; |
|---|
| 295 | $html =~ s/\r/\\r/sg; |
|---|
| 296 | $html =~ s/\n/\\n/sg; |
|---|
| 297 | $html =~ s/script/scri'+'pt/g; |
|---|
| 298 | |
|---|
| 299 | return _liftOut("'$html'"); |
|---|
| 300 | } |
|---|
| 301 | |
|---|
| 302 | sub postRenderingHandler { |
|---|
| 303 | return if ( $recursionBlock || !$tml2html ); |
|---|
| 304 | |
|---|
| 305 | # Replace protected content. |
|---|
| 306 | $_[0] = _dropBack( $_[0] ); |
|---|
| 307 | } |
|---|
| 308 | |
|---|
| 309 | # Commented out because of Bugs:Item1176 |
|---|
| 310 | # DEPRECATED in Dakar (modifyHeaderHandler does the job better) |
|---|
| 311 | #$TWikiCompatibility{writeHeaderHandler} = 1.1; |
|---|
| 312 | #sub writeHeaderHandler { |
|---|
| 313 | # my $query = shift; |
|---|
| 314 | # if( $query->param( 'wysiwyg_edit' )) { |
|---|
| 315 | # return "Expires: 0\nCache-control: max-age=0, must-revalidate"; |
|---|
| 316 | # } |
|---|
| 317 | # return ''; |
|---|
| 318 | #} |
|---|
| 319 | |
|---|
| 320 | # Dakar modify headers. |
|---|
| 321 | sub modifyHeaderHandler { |
|---|
| 322 | my ( $headers, $query ) = @_; |
|---|
| 323 | |
|---|
| 324 | if ( $query->param('wysiwyg_edit') ) { |
|---|
| 325 | $headers->{Expires} = 0; |
|---|
| 326 | $headers->{'Cache-control'} = 'max-age=0, must-revalidate'; |
|---|
| 327 | } |
|---|
| 328 | } |
|---|
| 329 | |
|---|
| 330 | # callback passed to the TML2HTML convertor |
|---|
| 331 | sub getViewUrl { |
|---|
| 332 | my ( $web, $topic ) = @_; |
|---|
| 333 | |
|---|
| 334 | # the Cairo documentation says getViewUrl defaults the web. It doesn't. |
|---|
| 335 | unless ( defined $Foswiki::Plugins::SESSION ) { |
|---|
| 336 | $web ||= $Foswiki::webName; |
|---|
| 337 | } |
|---|
| 338 | |
|---|
| 339 | return Foswiki::Func::getViewUrl( $web, $topic ); |
|---|
| 340 | } |
|---|
| 341 | |
|---|
| 342 | # The subset of vars for which bidirection transformation is supported |
|---|
| 343 | # in URLs only |
|---|
| 344 | use vars qw( @VARS ); |
|---|
| 345 | |
|---|
| 346 | # The set of macros that get "special treatment" in URLs |
|---|
| 347 | @VARS = ( |
|---|
| 348 | '%ATTACHURL%', |
|---|
| 349 | '%ATTACHURLPATH%', |
|---|
| 350 | '%PUBURL%', |
|---|
| 351 | '%PUBURLPATH%', |
|---|
| 352 | '%SCRIPTURLPATH{"view"}%', |
|---|
| 353 | '%SCRIPTURLPATH%', |
|---|
| 354 | '%SCRIPTURL{"view"}%', |
|---|
| 355 | '%SCRIPTURL%', |
|---|
| 356 | '%SCRIPTSUFFIX%', # bit dodgy, this one |
|---|
| 357 | ); |
|---|
| 358 | |
|---|
| 359 | # Initialises the mapping from var to URL and back |
|---|
| 360 | sub _populateVars { |
|---|
| 361 | my $opts = shift; |
|---|
| 362 | |
|---|
| 363 | return if ( $opts->{exp} ); |
|---|
| 364 | |
|---|
| 365 | local $recursionBlock = 1; # block calls to beforeCommonTagshandler |
|---|
| 366 | |
|---|
| 367 | my @exp = split( |
|---|
| 368 | /\0/, |
|---|
| 369 | Foswiki::Func::expandCommonVariables( |
|---|
| 370 | join( "\0", @VARS ), |
|---|
| 371 | $opts->{topic}, $opts->{web} |
|---|
| 372 | ) |
|---|
| 373 | ); |
|---|
| 374 | |
|---|
| 375 | for my $i ( 0 .. $#VARS ) { |
|---|
| 376 | my $nvar = $VARS[$i]; |
|---|
| 377 | $opts->{match}[$i] = $nvar; |
|---|
| 378 | $exp[$i] ||= ''; |
|---|
| 379 | } |
|---|
| 380 | $opts->{exp} = \@exp; |
|---|
| 381 | } |
|---|
| 382 | |
|---|
| 383 | # callback passed to the TML2HTML convertor on each |
|---|
| 384 | # variable in a URL used in a square bracketed link |
|---|
| 385 | sub expandVarsInURL { |
|---|
| 386 | my ( $url, $opts ) = @_; |
|---|
| 387 | |
|---|
| 388 | return '' unless $url; |
|---|
| 389 | |
|---|
| 390 | _populateVars($opts); |
|---|
| 391 | for my $i ( 0 .. $#VARS ) { |
|---|
| 392 | $url =~ s/$opts->{match}[$i]/$opts->{exp}->[$i]/g; |
|---|
| 393 | } |
|---|
| 394 | return $url; |
|---|
| 395 | } |
|---|
| 396 | |
|---|
| 397 | # callback passed to the HTML2TML convertor |
|---|
| 398 | sub postConvertURL { |
|---|
| 399 | my ( $url, $opts ) = @_; |
|---|
| 400 | |
|---|
| 401 | #my $orig = $url; #debug |
|---|
| 402 | |
|---|
| 403 | local $recursionBlock = 1; # block calls to beforeCommonTagshandler |
|---|
| 404 | |
|---|
| 405 | my $anchor = ''; |
|---|
| 406 | if ( $url =~ s/(#.*)$// ) { |
|---|
| 407 | $anchor = $1; |
|---|
| 408 | } |
|---|
| 409 | my $parameters = ''; |
|---|
| 410 | if ( $url =~ s/(\?.*)$// ) { |
|---|
| 411 | $parameters = $1; |
|---|
| 412 | } |
|---|
| 413 | |
|---|
| 414 | _populateVars($opts); |
|---|
| 415 | |
|---|
| 416 | for my $i ( 0 .. $#VARS ) { |
|---|
| 417 | next unless $opts->{exp}->[$i]; |
|---|
| 418 | $url =~ s/^$opts->{exp}->[$i]/$VARS[$i]/; |
|---|
| 419 | } |
|---|
| 420 | |
|---|
| 421 | if ( $url =~ m#^%SCRIPTURL(?:PATH)?(?:{"view"}%|%/+view[^/]*)/+([/\w.]+)$# |
|---|
| 422 | && !$parameters ) |
|---|
| 423 | { |
|---|
| 424 | my $orig = $1; |
|---|
| 425 | my ( $web, $topic ) = |
|---|
| 426 | Foswiki::Func::normalizeWebTopicName( $opts->{web}, $orig ); |
|---|
| 427 | |
|---|
| 428 | if ( $web && $web ne $opts->{web} ) { |
|---|
| 429 | |
|---|
| 430 | #print STDERR "$orig -> $web+$topic$anchor\n"; #debug |
|---|
| 431 | return $web . '.' . $topic . $anchor; |
|---|
| 432 | } |
|---|
| 433 | |
|---|
| 434 | #print STDERR "$orig -> $topic$anchor\n"; #debug |
|---|
| 435 | return $topic . $anchor; |
|---|
| 436 | } |
|---|
| 437 | |
|---|
| 438 | #print STDERR "$orig -> $url$anchor$parameters\n"; #debug |
|---|
| 439 | return $url . $anchor . $parameters; |
|---|
| 440 | } |
|---|
| 441 | |
|---|
| 442 | # Callback used to convert an image reference into a Foswiki variable. |
|---|
| 443 | sub _convertImage { |
|---|
| 444 | my ( $src, $opts ) = @_; |
|---|
| 445 | |
|---|
| 446 | return unless $src; |
|---|
| 447 | |
|---|
| 448 | local $recursionBlock = 1; # block calls to beforeCommonTagshandler |
|---|
| 449 | |
|---|
| 450 | unless ($imgMap) { |
|---|
| 451 | $imgMap = {}; |
|---|
| 452 | my $imgs = Foswiki::Func::getPreferencesValue('WYSIWYGPLUGIN_ICONS'); |
|---|
| 453 | if ($imgs) { |
|---|
| 454 | while ( $imgs =~ s/src="(.*?)" alt="(.*?)"// ) { |
|---|
| 455 | my ( $src, $alt ) = ( $1, $2 ); |
|---|
| 456 | $src = |
|---|
| 457 | Foswiki::Func::expandCommonVariables( $src, $opts->{topic}, |
|---|
| 458 | $opts->{web} ); |
|---|
| 459 | $alt .= '%' if $alt =~ /^%/; |
|---|
| 460 | $imgMap->{$src} = $alt; |
|---|
| 461 | } |
|---|
| 462 | } |
|---|
| 463 | } |
|---|
| 464 | |
|---|
| 465 | return $imgMap->{$src}; |
|---|
| 466 | } |
|---|
| 467 | |
|---|
| 468 | # Replace content with a marker to prevent it being munged by Foswiki |
|---|
| 469 | sub _liftOut { |
|---|
| 470 | my ($text) = @_; |
|---|
| 471 | my $n = scalar(@refs); |
|---|
| 472 | push( @refs, $text ); |
|---|
| 473 | return "\05$n\05"; |
|---|
| 474 | } |
|---|
| 475 | |
|---|
| 476 | # Substitute marker |
|---|
| 477 | sub _dropBack { |
|---|
| 478 | my ($text) = @_; |
|---|
| 479 | |
|---|
| 480 | # Restore everything that was lifted out |
|---|
| 481 | while ( $text =~ s/\05([0-9]+)\05/$refs[$1]/gi ) { |
|---|
| 482 | } |
|---|
| 483 | return $text; |
|---|
| 484 | } |
|---|
| 485 | |
|---|
| 486 | =begin TML |
|---|
| 487 | |
|---|
| 488 | ---++ StaticMethod notWysiwygEditable($text) -> $boolean |
|---|
| 489 | Determine if the given =$text= is WYSIWYG editable, based on the topic content |
|---|
| 490 | and the value of the Foswiki preferences WYSIWYG_EXCLUDE and |
|---|
| 491 | WYSIWYG_EDITABLE_CALLS. Returns a descriptive string if the text is not |
|---|
| 492 | editable, 0 otherwise. |
|---|
| 493 | |
|---|
| 494 | =cut |
|---|
| 495 | |
|---|
| 496 | sub notWysiwygEditable { |
|---|
| 497 | |
|---|
| 498 | #my ($text, $exclusions) = @_; |
|---|
| 499 | |
|---|
| 500 | my $exclusions = $_[1]; |
|---|
| 501 | unless ( defined($exclusions) ) { |
|---|
| 502 | $exclusions = Foswiki::Func::getPreferencesValue('WYSIWYG_EXCLUDE') |
|---|
| 503 | || ''; |
|---|
| 504 | } |
|---|
| 505 | |
|---|
| 506 | # Check for explicit exclusions before generic, non-configurable |
|---|
| 507 | # purely content-related reasons for exclusion |
|---|
| 508 | if ($exclusions) { |
|---|
| 509 | my $calls_ok = Foswiki::Func::getPreferencesValue('WYSIWYG_EDITABLE_CALLS') |
|---|
| 510 | || '---'; |
|---|
| 511 | $calls_ok =~ s/\s//g; |
|---|
| 512 | |
|---|
| 513 | my $ok = 1; |
|---|
| 514 | if ( $exclusions =~ /calls/ |
|---|
| 515 | && $_[0] =~ /%((?!($calls_ok){)[A-Z_]+{.*?})%/s ) |
|---|
| 516 | { |
|---|
| 517 | print STDERR "WYSIWYG_DEBUG: has calls $1 (not in $calls_ok)\n" |
|---|
| 518 | if (WHY); |
|---|
| 519 | return "Text contains calls"; |
|---|
| 520 | } |
|---|
| 521 | if ( $exclusions =~ /(macros|variables)/ && $_[0] =~ /%([A-Z_]+)%/s ) { |
|---|
| 522 | print STDERR "$exclusions WYSIWYG_DEBUG: has macros $1\n" |
|---|
| 523 | if (WHY); |
|---|
| 524 | return "Text contains macros"; |
|---|
| 525 | } |
|---|
| 526 | if ( $exclusions =~ /html/ |
|---|
| 527 | && $_[0] =~ /<\/?((?!literal|verbatim|noautolink|nop|br)\w+)/ ) |
|---|
| 528 | { |
|---|
| 529 | print STDERR "WYSIWYG_DEBUG: has html: $1\n" |
|---|
| 530 | if (WHY); |
|---|
| 531 | return "Text contains HTML"; |
|---|
| 532 | } |
|---|
| 533 | if ( $exclusions =~ /comments/ && $_[0] =~ /<[!]--/ ) { |
|---|
| 534 | print STDERR "WYSIWYG_DEBUG: has comments\n" |
|---|
| 535 | if (WHY); |
|---|
| 536 | return "Text contains comments"; |
|---|
| 537 | } |
|---|
| 538 | if ( $exclusions =~ /pre/ && $_[0] =~ /<pre\w/ ) { |
|---|
| 539 | print STDERR "WYSIWYG_DEBUG: has pre\n" |
|---|
| 540 | if (WHY); |
|---|
| 541 | return "Text contains PRE"; |
|---|
| 542 | } |
|---|
| 543 | } |
|---|
| 544 | |
|---|
| 545 | # Copy the content. |
|---|
| 546 | # Then crunch verbatim blocks, because verbatim blocks may contain *anything*. |
|---|
| 547 | my $text = $_[0]; |
|---|
| 548 | |
|---|
| 549 | # Look for combinations of sticky and other markup that cause problems together |
|---|
| 550 | for my $tag ('literal', keys %xmltag) { |
|---|
| 551 | while ($text =~ /<$tag\b[^>]*>(.*?)<\/$tag>/gsi) { |
|---|
| 552 | my $inner = $1; |
|---|
| 553 | if ($inner =~ /<sticky\b[^>]*>/i) { |
|---|
| 554 | print STDERR "WYSIWYG_DEBUG: <sticky> inside <$tag>\n" |
|---|
| 555 | if (WHY); |
|---|
| 556 | return "<sticky> inside <$tag>"; |
|---|
| 557 | } |
|---|
| 558 | } |
|---|
| 559 | } |
|---|
| 560 | |
|---|
| 561 | my $wasAVerbatimTag = "\000verbatim\001"; |
|---|
| 562 | while ($text =~ s/<verbatim\b[^>]*>(.*?)<\/verbatim>/$wasAVerbatimTag/i) { |
|---|
| 563 | #my $content = $1; |
|---|
| 564 | # If there is any content that breaks conversion if it is inside a verbatim block, |
|---|
| 565 | # check for it here: |
|---|
| 566 | } |
|---|
| 567 | |
|---|
| 568 | # Look for combinations of verbatim and other markup that cause problems together |
|---|
| 569 | for my $tag ('literal', keys %xmltag) { |
|---|
| 570 | while ($text =~ /<$tag\b[^>]*>(.*?)<\/$tag>/gsi) { |
|---|
| 571 | my $inner = $1; |
|---|
| 572 | if ($inner =~ /$wasAVerbatimTag/i) { |
|---|
| 573 | print STDERR "WYSIWYG_DEBUG: <verbatim> inside <$tag>\n" |
|---|
| 574 | if (WHY); |
|---|
| 575 | return "<verbatim> inside <$tag>"; |
|---|
| 576 | } |
|---|
| 577 | } |
|---|
| 578 | } |
|---|
| 579 | |
|---|
| 580 | # Look for combinations of literal and other markup that cause problems together |
|---|
| 581 | for my $tag (keys %xmltag) { |
|---|
| 582 | while ($text =~ /<$tag\b[^>]*>(.*?)<\/$tag>/gsi) { |
|---|
| 583 | my $inner = $1; |
|---|
| 584 | if ($inner =~ /<literal\b[^>]*>/i) { |
|---|
| 585 | print STDERR "WYSIWYG_DEBUG: <literal> inside <$tag>\n" |
|---|
| 586 | if (WHY); |
|---|
| 587 | return "<literal> inside <$tag>"; |
|---|
| 588 | } |
|---|
| 589 | } |
|---|
| 590 | } |
|---|
| 591 | |
|---|
| 592 | return 0; |
|---|
| 593 | } |
|---|
| 594 | |
|---|
| 595 | =pod |
|---|
| 596 | |
|---|
| 597 | ---++ StaticMethod addXMLTag($tag, \&fn) |
|---|
| 598 | |
|---|
| 599 | Instruct WysiwygPlugin to "lift out" the named tag |
|---|
| 600 | and pass it to &fn for processing. |
|---|
| 601 | &fn may modify the text of the tag. |
|---|
| 602 | &fn should return 0 if the tag is to be re-embedded immediately, |
|---|
| 603 | or 1 if it is to be re-embedded after all processing is complete. |
|---|
| 604 | The text passed (by reference) to &fn includes the |
|---|
| 605 | =<tag> ... </tag>= brackets. |
|---|
| 606 | |
|---|
| 607 | The simplest use of this function is something like this: |
|---|
| 608 | =Foswiki::Plugins::WysiwygPlugin::addXMLTag( 'mytag', sub { 1 } );= |
|---|
| 609 | |
|---|
| 610 | A plugin may call this function more than once |
|---|
| 611 | e.g. to change the processing function for a tag. |
|---|
| 612 | However, only the *original plugin* may change the processing |
|---|
| 613 | for a tag. |
|---|
| 614 | |
|---|
| 615 | Plugins should call this function from their =initPlugin= |
|---|
| 616 | handlers so that WysiwygPlugin will protect the XML-like tags |
|---|
| 617 | for all conversions, including REST conversions. |
|---|
| 618 | Plugins that are intended to be used with older versions of Foswiki |
|---|
| 619 | (e.g. 1.0.6) should check that this function is defined before calling it, |
|---|
| 620 | so that they degrade gracefully if an older version of WysiwygPlugin |
|---|
| 621 | (e.g. that shipped with 1.0.6) is installed. |
|---|
| 622 | |
|---|
| 623 | =cut |
|---|
| 624 | |
|---|
| 625 | sub addXMLTag { |
|---|
| 626 | my ( $tag, $fn ) = @_; |
|---|
| 627 | |
|---|
| 628 | my $plugin = caller; |
|---|
| 629 | $plugin =~ s/^Foswiki::Plugins:://; |
|---|
| 630 | |
|---|
| 631 | return if not defined $tag; |
|---|
| 632 | |
|---|
| 633 | if ( ( not exists $xmltag{$tag} and not exists $xmltagPlugin{$tag} ) |
|---|
| 634 | or ( $xmltagPlugin{$tag} eq $plugin ) ) |
|---|
| 635 | { |
|---|
| 636 | |
|---|
| 637 | # This is either a plugin adding a new tag |
|---|
| 638 | # or a plugin adding a tag it had previously added before. |
|---|
| 639 | # A plugin is allowed to add a tag that it had added before |
|---|
| 640 | # and the new function replaces the old. |
|---|
| 641 | # |
|---|
| 642 | $fn = sub { 1 } |
|---|
| 643 | unless $fn; # Default function |
|---|
| 644 | |
|---|
| 645 | $xmltag{$tag} = $fn; |
|---|
| 646 | $xmltagPlugin{$tag} = $plugin; |
|---|
| 647 | } |
|---|
| 648 | else { |
|---|
| 649 | |
|---|
| 650 | # DON'T replace the existing processing for this tag |
|---|
| 651 | printf STDERR "WysiwygPlugin::addXMLTag: " |
|---|
| 652 | . "$plugin cannot add XML tag $tag, " |
|---|
| 653 | . "that tag was already registered by $xmltagPlugin{$tag}\n"; |
|---|
| 654 | } |
|---|
| 655 | } |
|---|
| 656 | |
|---|
| 657 | sub TranslateTML2HTML { |
|---|
| 658 | my ( $text, $web, $topic ) = @_; |
|---|
| 659 | |
|---|
| 660 | # Translate the topic text to pure HTML. |
|---|
| 661 | unless ($tml2html) { |
|---|
| 662 | require Foswiki::Plugins::WysiwygPlugin::TML2HTML; |
|---|
| 663 | $tml2html = new Foswiki::Plugins::WysiwygPlugin::TML2HTML(); |
|---|
| 664 | } |
|---|
| 665 | return $tml2html->convert( |
|---|
| 666 | $_[0], |
|---|
| 667 | { |
|---|
| 668 | web => $web, |
|---|
| 669 | topic => $topic, |
|---|
| 670 | getViewUrl => \&getViewUrl, |
|---|
| 671 | expandVarsInURL => \&expandVarsInURL, |
|---|
| 672 | xmltag => \%xmltag, |
|---|
| 673 | } |
|---|
| 674 | ); |
|---|
| 675 | } |
|---|
| 676 | |
|---|
| 677 | # PACKAGE PRIVATE |
|---|
| 678 | # Determine if sticky attributes prevent a tag being converted to |
|---|
| 679 | # TML when this attribute is present. |
|---|
| 680 | my @protectedByAttr; |
|---|
| 681 | |
|---|
| 682 | sub protectedByAttr { |
|---|
| 683 | my ( $tag, $attr ) = @_; |
|---|
| 684 | |
|---|
| 685 | unless ( scalar(@protectedByAttr) ) { |
|---|
| 686 | |
|---|
| 687 | # See the WysiwygPluginSettings for information on stickybits |
|---|
| 688 | my $protection = |
|---|
| 689 | Foswiki::Func::getPreferencesValue('WYSIWYGPLUGIN_STICKYBITS') |
|---|
| 690 | || <<'DEFAULT'; |
|---|
| 691 | .*=id,lang,title,dir,on.*; |
|---|
| 692 | a=accesskey,coords,shape,target; |
|---|
| 693 | bdo=dir; |
|---|
| 694 | br=clear; |
|---|
| 695 | col=char,charoff,span,valign,width; |
|---|
| 696 | colgroup=align,char,charoff,span,valign,width; |
|---|
| 697 | dir=compact; |
|---|
| 698 | div=align; |
|---|
| 699 | dl=compact; |
|---|
| 700 | font=size,face; |
|---|
| 701 | h\d=align; |
|---|
| 702 | hr=align,noshade,size,width; |
|---|
| 703 | legend=accesskey,align; |
|---|
| 704 | li=value; |
|---|
| 705 | ol=compact,start,type; |
|---|
| 706 | p=align; |
|---|
| 707 | param=name,type,value,valuetype; |
|---|
| 708 | pre=width; |
|---|
| 709 | q=cite; |
|---|
| 710 | table=align,bgcolor,frame,rules,summary,width; |
|---|
| 711 | tbody=align,char,charoff,valign; |
|---|
| 712 | td=abbr,align,axis,bgcolor,char,charoff,headers,height,nowrap,rowspan,scope,valign,width; |
|---|
| 713 | tfoot=align,char,charoff,valign; |
|---|
| 714 | th=abbr,align,axis,bgcolor,char,charoff,height,nowrap,rowspan,scope,valign,width,headers; |
|---|
| 715 | thead=align,char,charoff,valign; |
|---|
| 716 | tr=bgcolor,char,charoff,valign; |
|---|
| 717 | ul=compact,type; |
|---|
| 718 | DEFAULT |
|---|
| 719 | foreach my $def ( split( /;\s*/s, $protection ) ) { |
|---|
| 720 | my ( $re, $ats ) = split( /\s*=\s*/s, $def, 2 ); |
|---|
| 721 | push( |
|---|
| 722 | @protectedByAttr, |
|---|
| 723 | { |
|---|
| 724 | tag => qr/$re/i, |
|---|
| 725 | attrs => join( '|', split( /\s*,\s*/, $ats ) ) |
|---|
| 726 | } |
|---|
| 727 | ); |
|---|
| 728 | } |
|---|
| 729 | } |
|---|
| 730 | foreach my $row (@protectedByAttr) { |
|---|
| 731 | if ( $tag =~ /^$row->{tag}$/i ) { |
|---|
| 732 | return 1 if ( $attr =~ /^($row->{attrs})$/i ); |
|---|
| 733 | } |
|---|
| 734 | } |
|---|
| 735 | return 0; |
|---|
| 736 | } |
|---|
| 737 | |
|---|
| 738 | # Text that is taken from a web page and added to the parameters of an XHR |
|---|
| 739 | # by JavaScript is UTF-8 encoded. This is because UTF-8 is the default encoding |
|---|
| 740 | # for XML, which XHR was designed to transport. |
|---|
| 741 | |
|---|
| 742 | # This function is used to decode such parameters to the currently selected |
|---|
| 743 | # Foswiki site character set. |
|---|
| 744 | |
|---|
| 745 | # Note that this transform is not as simple as an Encode::from_to, as |
|---|
| 746 | # a number of unicode code points must be remapped for certain encodings. |
|---|
| 747 | sub RESTParameter2SiteCharSet { |
|---|
| 748 | my ($text) = @_; |
|---|
| 749 | |
|---|
| 750 | $text = Encode::decode_utf8( $text, Encode::FB_PERLQQ ); |
|---|
| 751 | |
|---|
| 752 | WC::mapUnicode2HighBit($text); |
|---|
| 753 | |
|---|
| 754 | if ( $Foswiki::cfg{Site}{CharSet} ) { |
|---|
| 755 | $text = Encode::encode( $Foswiki::cfg{Site}{CharSet}, |
|---|
| 756 | $text, Encode::FB_PERLQQ ); |
|---|
| 757 | } |
|---|
| 758 | |
|---|
| 759 | return $text; |
|---|
| 760 | } |
|---|
| 761 | |
|---|
| 762 | # Text that is taken from a web page and added to the parameters of an XHR |
|---|
| 763 | # by JavaScript is UTF-8 encoded. This is because UTF-8 is the default encoding |
|---|
| 764 | # for XML, which XHR was designed to transport. For usefulness in Javascript |
|---|
| 765 | # the response to an XHR should also be UTF-8 encoded. |
|---|
| 766 | # This function generates such a response. |
|---|
| 767 | sub returnRESTResult { |
|---|
| 768 | my ( $response, $status, $text ) = @_; |
|---|
| 769 | |
|---|
| 770 | if ( $Foswiki::cfg{Site}{CharSet} ) { |
|---|
| 771 | $text = Encode::decode( $Foswiki::cfg{Site}{CharSet}, |
|---|
| 772 | $text, Encode::FB_PERLQQ ); |
|---|
| 773 | } |
|---|
| 774 | |
|---|
| 775 | WC::mapHighBit2Unicode($text); |
|---|
| 776 | |
|---|
| 777 | $text = Encode::encode_utf8($text); |
|---|
| 778 | |
|---|
| 779 | # Foswiki 1.0 introduces the Foswiki::Response object, which handles all |
|---|
| 780 | # responses. |
|---|
| 781 | if ( UNIVERSAL::isa( $response, 'Foswiki::Response' ) ) { |
|---|
| 782 | $response->header( |
|---|
| 783 | -status => $status, |
|---|
| 784 | -type => 'text/plain', |
|---|
| 785 | -charset => 'UTF-8' |
|---|
| 786 | ); |
|---|
| 787 | $response->print($text); |
|---|
| 788 | } |
|---|
| 789 | else { # Pre-Foswiki-1.0. |
|---|
| 790 | # Turn off AUTOFLUSH |
|---|
| 791 | # See http://perl.apache.org/docs/2.0/user/coding/coding.html |
|---|
| 792 | local $| = 0; |
|---|
| 793 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 794 | if ( defined($query) ) { |
|---|
| 795 | my $len; |
|---|
| 796 | { use bytes; $len = length($text); }; |
|---|
| 797 | print $query->header( |
|---|
| 798 | -status => $status, |
|---|
| 799 | -type => 'text/plain', |
|---|
| 800 | -charset => 'UTF-8', |
|---|
| 801 | -Content_length => $len |
|---|
| 802 | ); |
|---|
| 803 | print $text; |
|---|
| 804 | } |
|---|
| 805 | } |
|---|
| 806 | print STDERR $text if ( $status >= 400 ); |
|---|
| 807 | } |
|---|
| 808 | |
|---|
| 809 | # Rest handler for use from Javascript. The 'text' parameter is used to |
|---|
| 810 | # pass the text for conversion. The text must be URI-encoded (this is |
|---|
| 811 | # to support use of this handler from XMLHttpRequest, which gets it |
|---|
| 812 | # wrong). Example: |
|---|
| 813 | # |
|---|
| 814 | # var req = new XMLHttpRequest(); |
|---|
| 815 | # req.open("POST", url, true); |
|---|
| 816 | # req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); |
|---|
| 817 | # var params = "text=" + encodeURIComponent(escape(text)); |
|---|
| 818 | # request.req.setRequestHeader("Content-length", params.length); |
|---|
| 819 | # request.req.setRequestHeader("Connection", "close"); |
|---|
| 820 | # request.req.onreadystatechange = ...; |
|---|
| 821 | # req.send(params); |
|---|
| 822 | # |
|---|
| 823 | sub _restTML2HTML { |
|---|
| 824 | my ( $session, $plugin, $verb, $response ) = @_; |
|---|
| 825 | my $tml = Foswiki::Func::getCgiQuery()->param('text'); |
|---|
| 826 | |
|---|
| 827 | $tml = RESTParameter2SiteCharSet($tml); |
|---|
| 828 | |
|---|
| 829 | # if the secret ID is present, don't convert again. We are probably |
|---|
| 830 | # going 'back' to this page (doesn't work on IE :-( ) |
|---|
| 831 | if ( $tml =~ /<!--$SECRET_ID-->/ ) { |
|---|
| 832 | return $tml; |
|---|
| 833 | } |
|---|
| 834 | |
|---|
| 835 | my $html = |
|---|
| 836 | TranslateTML2HTML( $tml, $session->{webName}, $session->{topicName} ); |
|---|
| 837 | |
|---|
| 838 | # Add the secret id to trigger reconversion. Doesn't work if the |
|---|
| 839 | # editor eats HTML comments, so the editor may need to put it back |
|---|
| 840 | # in during final cleanup. |
|---|
| 841 | $html = '<!--' . $SECRET_ID . '-->' . $html; |
|---|
| 842 | |
|---|
| 843 | returnRESTResult( $response, 200, $html ); |
|---|
| 844 | |
|---|
| 845 | return; # to prevent further processing |
|---|
| 846 | } |
|---|
| 847 | |
|---|
| 848 | # Rest handler for use from Javascript |
|---|
| 849 | sub _restHTML2TML { |
|---|
| 850 | my ( $session, $plugin, $verb, $response ) = @_; |
|---|
| 851 | unless ($html2tml) { |
|---|
| 852 | require Foswiki::Plugins::WysiwygPlugin::HTML2TML; |
|---|
| 853 | |
|---|
| 854 | $html2tml = new Foswiki::Plugins::WysiwygPlugin::HTML2TML(); |
|---|
| 855 | } |
|---|
| 856 | my $html = Foswiki::Func::getCgiQuery()->param('text'); |
|---|
| 857 | |
|---|
| 858 | $html = RESTParameter2SiteCharSet($html); |
|---|
| 859 | |
|---|
| 860 | $html =~ s/<!--$SECRET_ID-->//go; |
|---|
| 861 | my $tml = $html2tml->convert( |
|---|
| 862 | $html, |
|---|
| 863 | { |
|---|
| 864 | web => $session->{webName}, |
|---|
| 865 | topic => $session->{topicName}, |
|---|
| 866 | getViewUrl => \&getViewUrl, |
|---|
| 867 | expandVarsInURL => \&expandVarsInURL, |
|---|
| 868 | very_clean => 1, |
|---|
| 869 | } |
|---|
| 870 | ); |
|---|
| 871 | |
|---|
| 872 | returnRESTResult( $response, 200, $tml ); |
|---|
| 873 | return; # to prevent further processing |
|---|
| 874 | } |
|---|
| 875 | |
|---|
| 876 | # SMELL: foswiki supports proper REST usage of the upload script, |
|---|
| 877 | # so debatable if this is required any more |
|---|
| 878 | sub _restUpload { |
|---|
| 879 | my ( $session, $plugin, $verb, $response ) = @_; |
|---|
| 880 | my $query = Foswiki::Func::getCgiQuery(); |
|---|
| 881 | |
|---|
| 882 | # Item1458 ignore uploads not using POST |
|---|
| 883 | if ( $query && $query->method() && uc( $query->method() ) ne 'POST' ) { |
|---|
| 884 | returnRESTResult( $response, 405, "Method not Allowed" ); |
|---|
| 885 | return; |
|---|
| 886 | } |
|---|
| 887 | my ( $web, $topic ) = |
|---|
| 888 | Foswiki::Func::normalizeWebTopicName( undef, $query->param('topic') ); |
|---|
| 889 | $web = |
|---|
| 890 | Foswiki::Sandbox::untaint( $web, \&Foswiki::Sandbox::validateWebName ); |
|---|
| 891 | $topic = Foswiki::Sandbox::untaint( $topic, |
|---|
| 892 | \&Foswiki::Sandbox::validateTopicName ); |
|---|
| 893 | unless ( defined $web && defined $topic ) { |
|---|
| 894 | returnRESTResult( $response, 401, "Access denied" ); |
|---|
| 895 | return; # to prevent further processing |
|---|
| 896 | } |
|---|
| 897 | my $hideFile = $query->param('hidefile') || ''; |
|---|
| 898 | my $fileComment = $query->param('filecomment') || ''; |
|---|
| 899 | my $createLink = $query->param('createlink') || ''; |
|---|
| 900 | my $doPropsOnly = $query->param('changeproperties'); |
|---|
| 901 | my $filePath = $query->param('filepath') || ''; |
|---|
| 902 | my $fileName = $query->param('filename') || ''; |
|---|
| 903 | if ( $filePath && !$fileName ) { |
|---|
| 904 | $filePath =~ m|([^/\\]*$)|; |
|---|
| 905 | $fileName = $1; |
|---|
| 906 | } |
|---|
| 907 | $fileComment =~ s/\s+/ /go; |
|---|
| 908 | $fileComment =~ s/^\s*//o; |
|---|
| 909 | $fileComment =~ s/\s*$//o; |
|---|
| 910 | $fileName =~ s/\s*$//o; |
|---|
| 911 | $filePath =~ s/\s*$//o; |
|---|
| 912 | |
|---|
| 913 | unless ( |
|---|
| 914 | Foswiki::Func::checkAccessPermission( |
|---|
| 915 | 'CHANGE', Foswiki::Func::getWikiName(), |
|---|
| 916 | undef, $topic, $web |
|---|
| 917 | ) |
|---|
| 918 | ) |
|---|
| 919 | { |
|---|
| 920 | returnRESTResult( $response, 401, "Access denied" ); |
|---|
| 921 | return; # to prevent further processing |
|---|
| 922 | } |
|---|
| 923 | |
|---|
| 924 | my ( $fileSize, $fileDate, $tmpFileName ); |
|---|
| 925 | |
|---|
| 926 | my $stream = $query->upload('filepath') unless $doPropsOnly; |
|---|
| 927 | my $origName = $fileName; |
|---|
| 928 | |
|---|
| 929 | unless ($doPropsOnly) { |
|---|
| 930 | |
|---|
| 931 | # SMELL: call to unpublished function |
|---|
| 932 | ( $fileName, $origName ) = |
|---|
| 933 | Foswiki::Sandbox::sanitizeAttachmentName($fileName); |
|---|
| 934 | |
|---|
| 935 | # check if upload has non zero size |
|---|
| 936 | if ($stream) { |
|---|
| 937 | my @stats = stat $stream; |
|---|
| 938 | $fileSize = $stats[7]; |
|---|
| 939 | $fileDate = $stats[9]; |
|---|
| 940 | } |
|---|
| 941 | |
|---|
| 942 | unless ( $fileSize && $fileName ) { |
|---|
| 943 | returnRESTResult( $response, 500, "Zero-sized file upload" ); |
|---|
| 944 | return; # to prevent further processing |
|---|
| 945 | } |
|---|
| 946 | |
|---|
| 947 | my $maxSize = Foswiki::Func::getPreferencesValue('ATTACHFILESIZELIMIT'); |
|---|
| 948 | $maxSize = 0 unless ( $maxSize =~ /([0-9]+)/o ); |
|---|
| 949 | |
|---|
| 950 | if ( $maxSize && $fileSize > $maxSize * 1024 ) { |
|---|
| 951 | returnRESTResult( $response, 500, "OVERSIZED UPLOAD" ); |
|---|
| 952 | return; # to prevent further processing |
|---|
| 953 | } |
|---|
| 954 | } |
|---|
| 955 | |
|---|
| 956 | # SMELL: use of undocumented CGI::tmpFileName |
|---|
| 957 | my $tfp = $query->tmpFileName( $query->param('filepath') ); |
|---|
| 958 | my $error = Foswiki::Func::saveAttachment( |
|---|
| 959 | $web, $topic, |
|---|
| 960 | $fileName, |
|---|
| 961 | { |
|---|
| 962 | dontlog => !$Foswiki::cfg{Log}{upload}, |
|---|
| 963 | comment => $fileComment, |
|---|
| 964 | hide => $hideFile, |
|---|
| 965 | createlink => $createLink, |
|---|
| 966 | stream => $stream, |
|---|
| 967 | filepath => $filePath, |
|---|
| 968 | filesize => $fileSize, |
|---|
| 969 | filedate => $fileDate, |
|---|
| 970 | tmpFilename => $tfp, |
|---|
| 971 | } |
|---|
| 972 | ); |
|---|
| 973 | |
|---|
| 974 | close($stream) if $stream; |
|---|
| 975 | |
|---|
| 976 | if ($error) { |
|---|
| 977 | returnRESTResult( $response, 500, $error ); |
|---|
| 978 | return; # to prevent further processing |
|---|
| 979 | } |
|---|
| 980 | |
|---|
| 981 | # Otherwise allow the rest dispatcher to write a 200 |
|---|
| 982 | return "$origName attached to $web.$topic" |
|---|
| 983 | . ( $origName ne $fileName ? " as $fileName" : '' ); |
|---|
| 984 | } |
|---|
| 985 | |
|---|
| 986 | sub _unquote { |
|---|
| 987 | my $text = shift; |
|---|
| 988 | $text =~ s/\\/\\\\/g; |
|---|
| 989 | $text =~ s/\n/\\n/g; |
|---|
| 990 | $text =~ s/\r/\\r/g; |
|---|
| 991 | $text =~ s/\t/\\t/g; |
|---|
| 992 | $text =~ s/"/\\"/g; |
|---|
| 993 | $text =~ s/'/\\'/g; |
|---|
| 994 | return $text; |
|---|
| 995 | } |
|---|
| 996 | |
|---|
| 997 | # Get, and return, a list of attachments using JSON |
|---|
| 998 | sub _restAttachments { |
|---|
| 999 | my ( $session, $plugin, $verb, $response ) = @_; |
|---|
| 1000 | my ( $web, $topic ) = |
|---|
| 1001 | Foswiki::Func::normalizeWebTopicName( undef, |
|---|
| 1002 | Foswiki::Func::getCgiQuery()->param('topic') ); |
|---|
| 1003 | $web = |
|---|
| 1004 | Foswiki::Sandbox::untaint( $web, \&Foswiki::Sandbox::validateWebName ); |
|---|
| 1005 | $topic = Foswiki::Sandbox::untaint( $topic, |
|---|
| 1006 | \&Foswiki::Sandbox::validateTopicName ); |
|---|
| 1007 | my ( $meta, $text ) = Foswiki::Func::readTopic( $web, $topic ); |
|---|
| 1008 | unless ( |
|---|
| 1009 | Foswiki::Func::checkAccessPermission( |
|---|
| 1010 | 'VIEW', Foswiki::Func::getWikiName(), |
|---|
| 1011 | $text, $topic, $web, $meta |
|---|
| 1012 | ) |
|---|
| 1013 | ) |
|---|
| 1014 | { |
|---|
| 1015 | returnRESTResult( $response, 401, "Access denied" ); |
|---|
| 1016 | return; # to prevent further processing |
|---|
| 1017 | } |
|---|
| 1018 | |
|---|
| 1019 | # Create a JSON list of attachment data, sorted by name |
|---|
| 1020 | my @atts; |
|---|
| 1021 | foreach my $att ( sort { $a->{name} cmp $b->{name} } |
|---|
| 1022 | $meta->find('FILEATTACHMENT') ) |
|---|
| 1023 | { |
|---|
| 1024 | push( |
|---|
| 1025 | @atts, |
|---|
| 1026 | '{' . join( |
|---|
| 1027 | ',', |
|---|
| 1028 | map { |
|---|
| 1029 | '"' |
|---|
| 1030 | . _unquote($_) . '":"' |
|---|
| 1031 | . _unquote( $att->{$_} ) . '"' |
|---|
| 1032 | } keys %$att |
|---|
| 1033 | ) |
|---|
| 1034 | . '}' |
|---|
| 1035 | ); |
|---|
| 1036 | |
|---|
| 1037 | } |
|---|
| 1038 | return '[' . join( ',', @atts ) . ']'; |
|---|
| 1039 | } |
|---|
| 1040 | |
|---|
| 1041 | 1; |
|---|
| 1042 | __DATA__ |
|---|
| 1043 | # Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/ |
|---|
| 1044 | # |
|---|
| 1045 | # Copyright (C) 2008 Foswiki Contributors. Foswiki Contributors |
|---|
| 1046 | # are listed in the AUTHORS file in the root of this distribution. |
|---|
| 1047 | # NOTE: Please extend that file, not this notice. |
|---|
| 1048 | # |
|---|
| 1049 | # Additional copyrights apply to some or all of the code in this file: |
|---|
| 1050 | # |
|---|
| 1051 | # Copyright (C) 2005 ILOG http://www.ilog.fr |
|---|
| 1052 | # and TWiki Contributors. All Rights Reserved. TWiki Contributors |
|---|
| 1053 | # are listed in the AUTHORS file in the root of your Foswiki (or TWiki) |
|---|
| 1054 | # distribution. |
|---|
| 1055 | # |
|---|
| 1056 | # This program is free software; you can redistribute it and/or |
|---|
| 1057 | # modify it under the terms of the GNU General Public License |
|---|
| 1058 | # as published by the Free Software Foundation; either version 2 |
|---|
| 1059 | # of the License, or (at your option) any later version. For |
|---|
| 1060 | # more details read LICENSE in the root of the TWiki distribution. |
|---|
| 1061 | # |
|---|
| 1062 | # This program is distributed in the hope that it will be useful, |
|---|
| 1063 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 1064 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
|---|
| 1065 | # |
|---|
| 1066 | # As per the GPL, removal of this notice is prohibited. |
|---|