source: branches/Release01x00/WysiwygPlugin/lib/Foswiki/Plugins/WysiwygPlugin.pm @ 5235

Revision 5235, 32.3 KB checked in by MichaelTempest, 4 years ago (diff)

Item1905: endRenderingHandler was never needed on Foswiki
startRenderingHandler will not be needed from 1.1 onwards

  • Property svn:keywords set to Revision Date
Line 
1# See bottom of file for license and copyright information
2
3=begin TML
4
5---+ package WysiwygPlugin
6
7This plugin is responsible for translating TML to HTML before an edit starts
8and translating the resultant HTML back into TML.
9
10Note: In the case of a new topic, you might expect to see the "create topic"
11screen in the editor when it goes back to Foswiki for the topic content. This
12doesn't happen because the earliest possible handler is called on the topic
13content and not the template. The template is effectively ignored and a blank
14document is sent to the editor.
15
16Attachment uploads can be handled by URL requests from the editor to the rest
17handler in this plugin. This avoids the need to add any scripts to the bin dir.
18You will have to use a form, though, as XmlHttpRequest does not support file
19uploads.
20
21=cut
22
23package Foswiki::Plugins::WysiwygPlugin;
24
25use CGI qw( :cgi -any );
26
27use strict;
28
29use Assert;
30use Encode ();
31
32use Foswiki::Func                              ();    # The plugins API
33use Foswiki::Plugins                           ();    # For the API version
34use Foswiki::Plugins::WysiwygPlugin::Constants ();
35
36use vars qw( $html2tml $tml2html $recursionBlock $imgMap );
37use vars qw( %FoswikiCompatibility @refs %xmltag %xmltagPlugin);
38
39our $SHORTDESCRIPTION  = 'Translator framework for Wysiwyg editors';
40our $NO_PREFS_IN_TOPIC = 1;
41our $VERSION           = '$Rev$';
42
43our $RELEASE = '18 Sep 2009';
44
45our $SECRET_ID =
46'WYSIWYG content - do not remove this comment, and never use this identical text in your topics';
47
48sub WHY { 1 }
49
50sub 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
71sub _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
92sub _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;
109sub 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.
117sub 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
149sub 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.
162sub 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)
170sub 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)
199sub 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!
241sub 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.
274sub _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
289sub _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
302sub 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.
321sub 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
331sub 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
344use 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
360sub _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
385sub 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
398sub 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.
443sub _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
469sub _liftOut {
470    my ($text) = @_;
471    my $n = scalar(@refs);
472    push( @refs, $text );
473    return "\05$n\05";
474}
475
476# Substitute marker
477sub _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
489Determine if the given =$text= is WYSIWYG editable, based on the topic content
490and the value of the Foswiki preferences WYSIWYG_EXCLUDE and
491WYSIWYG_EDITABLE_CALLS. Returns a descriptive string if the text is not
492editable, 0 otherwise.
493
494=cut
495
496sub 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
599Instruct WysiwygPlugin to "lift out" the named tag
600and 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,
603or 1 if it is to be re-embedded after all processing is complete.
604The text passed (by reference) to &fn includes the
605=<tag> ... </tag>= brackets.
606
607The simplest use of this function is something like this:
608=Foswiki::Plugins::WysiwygPlugin::addXMLTag( 'mytag', sub { 1 } );=
609
610A plugin may call this function more than once
611e.g. to change the processing function for a tag.
612However, only the *original plugin* may change the processing
613for a tag.
614
615Plugins should call this function from their =initPlugin=
616handlers so that WysiwygPlugin will protect the XML-like tags
617for all conversions, including REST conversions.
618Plugins 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,
620so 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
625sub 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
657sub 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.
680my @protectedByAttr;
681
682sub 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.*;
692a=accesskey,coords,shape,target;
693bdo=dir;
694br=clear;
695col=char,charoff,span,valign,width;
696colgroup=align,char,charoff,span,valign,width;
697dir=compact;
698div=align;
699dl=compact;
700font=size,face;
701h\d=align;
702hr=align,noshade,size,width;
703legend=accesskey,align;
704li=value;
705ol=compact,start,type;
706p=align;
707param=name,type,value,valuetype;
708pre=width;
709q=cite;
710table=align,bgcolor,frame,rules,summary,width;
711tbody=align,char,charoff,valign;
712td=abbr,align,axis,bgcolor,char,charoff,headers,height,nowrap,rowspan,scope,valign,width;
713tfoot=align,char,charoff,valign;
714th=abbr,align,axis,bgcolor,char,charoff,height,nowrap,rowspan,scope,valign,width,headers;
715thead=align,char,charoff,valign;
716tr=bgcolor,char,charoff,valign;
717ul=compact,type;
718DEFAULT
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.
747sub 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.
767sub 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#
823sub _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
849sub _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
878sub _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
986sub _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
998sub _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
10411;
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.
Note: See TracBrowser for help on using the repository browser.