# Plugin for TWiki Collaboration Platform, http://TWiki.org/ # # Copyright (C) 2005-2018 TWiki Contributor. # Copyright (C) 2005 ILOG http://www.ilog.fr # Copyright (C) 2008-2009 Foswiki Contributors. # All Rights Reserved. TWiki Contributors are listed in the # AUTHORS file in the root of this distribution. # NOTE: Please extend that file, not this notice. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 3 # of the License, or (at your option) any later version. For # more details read LICENSE in the root of the TWiki distribution. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # As per the GPL, removal of this notice is prohibited. package TWiki::Plugins::WysiwygPlugin::Handlers; # This package contains the handler functions used to implement the # WysiwygPlugin. They are implemented here so we can 'lazy-load' this # module only when it is actually required. use strict; use warnings; use Assert; use Error (':try'); use CGI qw( :cgi -any ); use Encode (); use TWiki::Func (); # The plugins API use TWiki::Plugins (); # For the API version use TWiki::Plugins::WysiwygPlugin::Constants (); our $html2tml; our $imgMap; our @refs; our %xmltagPlugin; our $SECRET_ID = 'WYSIWYG content - do not remove this comment, and never use this identical text in your topics'; sub _SECRET_ID { $SECRET_ID; } sub _OWEBTAG { my ( $session, $params, $topic, $web ) = @_; my $query = TWiki::Func::getCgiQuery(); return $web unless $query; if ( defined( $query->param('templatetopic') ) ) { my @split = split( /\./, $query->param('templatetopic') ); if ( $#split == 0 ) { return $web; } else { return $split[0]; } } return $web; } sub _OTOPICTAG { my ( $session, $params, $topic, $web ) = @_; my $query = TWiki::Func::getCgiQuery(); return $topic unless $query; if ( defined( $query->param('templatetopic') ) ) { my @split = split( /\./, $query->param('templatetopic') ); return $split[$#split]; } return $topic; } # This handler is used to determine whether the topic is editable by # a WYSIWYG editor or not. The only thing it does is to redirect to a # normal edit url if the skin is set to WYSIWYGPLUGIN_WYSIWYGSKIN and # nasty content is found. sub beforeEditHandler { #my( $text, $topic, $web, $meta ) = @_; my $skin = TWiki::Func::getPreferencesValue('WYSIWYGPLUGIN_WYSIWYGSKIN'); if ( $skin && TWiki::Func::getSkin() =~ /\b$skin\b/o ) { if ( notWysiwygEditable( $_[0] ) ) { # redirect my $query = TWiki::Func::getCgiQuery(); foreach my $p (qw( skin cover )) { my $arg = $query->param($p); if ( $arg && $arg =~ s/\b$skin\b// ) { if ( $arg =~ /^[\s,]*$/ ) { $query->delete($p); } else { $query->param( -name => $p, -value => $arg ); } } } my $url = $query->url( -full => 1, -path => 1, -query => 1 ); TWiki::Func::redirectCgiQuery( $query, $url ); # Bring this session to an untimely end exit 0; } } } # This handler is only invoked *after* merging is complete sub beforeSaveHandler { #my( $text, $topic, $web ) = @_; my $query = TWiki::Func::getCgiQuery(); return unless $query; return unless defined( $query->param('wysiwyg_edit') ); $_[0] = TranslateHTML2TML( $_[0], $_[1], $_[2] ); } # This handler is invoked before a merge. Merges are done before the # afterEditHandler is called, so we need to translate here. sub beforeMergeHandler { #my( $text, $currRev, $currText, $origRev, $origText, $web, $topic ) = @_; afterEditHandler( $_[0], $_[6], $_[5] ); } # This handler is invoked *after* a merge, and only from the edit # script (so it's useless for a REST save) sub afterEditHandler { my ( $text, $topic, $web ) = @_; my $query = TWiki::Func::getCgiQuery(); return unless $query; return unless defined( $query->param('wysiwyg_edit') ) || $text =~ s///go; # Switch off wysiwyg_edit so it doesn't try to transform again in # the beforeSaveHandler $query->delete('wysiwyg_edit'); $text = TranslateHTML2TML( $text, $_[1], $_[2] ); $_[0] = $text; } # Invoked to convert HTML to TML (best efforts) sub TranslateHTML2TML { my ( $text, $topic, $web ) = @_; # $text must be in encoded in the site charset ASSERT( $text !~ /[^\x00-\xff]/, "only octets expected in input to TranslateHTML2TML" ) if DEBUG; unless ($html2tml) { require TWiki::Plugins::WysiwygPlugin::HTML2TML; $html2tml = new TWiki::Plugins::WysiwygPlugin::HTML2TML(); } # SMELL: really, really bad smell; bloody core should NOT pass text # with embedded meta to plugins! It is VERY BAD DESIGN!!! my $top = ''; if ( $text =~ s/^(%META:[A-Z]+{.*?}%\r?\n)//s ) { $top = $1; } my $bottom = ''; $text =~ s/^(%META:[A-Z]+{.*?}%\r?\n)/$bottom = "$1$bottom";''/gem; my $opts = { web => $web, topic => $topic, convertImage => \&_convertImage, rewriteURL => \&postConvertURL, very_clean => 1, # aggressively polish saved HTML }; $text = $html2tml->convert( $text, $opts ); $text =~ s/\s+$/\n/s; ASSERT( $text !~ /[^\x00-\xff]/, "only octets expected in topic text output from TranslateHTML2TML" ) if DEBUG; return $top . $text . $bottom; } # Handler used to process text in a =view= URL to generate text/html # containing the HTML of the topic to be edited. # # Invoked when the selected skin is in use to convert the text to HTML # We can't use the beforeEditHandler, because the editor loads up and then # uses a URL to fetch the text to be edited. This handler is designed to # provide the text for that request. It's a real struggle, because the # commonTagsHandler is called so many times that getting the right # call is hard, and then preventing a repeat call is harder! sub beforeCommonTagsHandler { #my ( $text, $topic, $web, $meta ) my $query = TWiki::Func::getCgiQuery(); # stop it from processing the template without expanded # %TEXT% (grr; we need a better way to tell where we # are in the processing pipeline) return if ( $_[0] =~ /^/ ) { return $tml; } my $html = TranslateTML2HTML( $tml, $session->{webName}, $session->{topicName} ); # Add the secret id to trigger reconversion. Doesn't work if the # editor eats HTML comments, so the editor may need to put it back # in during final cleanup. $html = '' . $html; returnRESTResult( $response, 200, $html ); return; # to prevent further processing } # Rest handler for use from Javascript sub _restHTML2TML { my ( $session, $plugin, $verb, $response ) = @_; unless ($html2tml) { require TWiki::Plugins::WysiwygPlugin::HTML2TML; $html2tml = new TWiki::Plugins::WysiwygPlugin::HTML2TML(); } my $html = TWiki::Func::getCgiQuery()->param('text'); #print STDERR "param [". TWiki::Plugins::WysiwygPlugin::HTML2TML::debugEncode($html). "]\n\n"; $html = RESTParameter2SiteCharSet($html); #print STDERR "paraminSC [". TWiki::Plugins::WysiwygPlugin::HTML2TML::debugEncode($html). "]\n\n"; $html =~ s///go; my $tml = $html2tml->convert( $html, { web => $session->{webName}, topic => $session->{topicName}, convertImage => \&_convertImage, rewriteURL => \&postConvertURL, very_clean => 1, } ); #print STDERR "tml inSc [". TWiki::Plugins::WysiwygPlugin::HTML2TML::debugEncode($tml). "]\n\n"; returnRESTResult( $response, 200, $tml ); return; # to prevent further processing } # SMELL: twiki supports proper REST usage of the upload script, # so debatable if this is required any more sub _restUpload { my ( $session, $plugin, $verb, $response ) = @_; my $query = TWiki::Func::getCgiQuery(); # Item1458 ignore uploads not using POST if ( $query && $query->method() && uc( $query->method() ) ne 'POST' ) { returnRESTResult( $response, 405, "Method not Allowed" ); return; } my ( $web, $topic ) = ( $session->{webName}, $session->{topicName} ); my $hideFile = $query->param('hidefile') || ''; my $fileComment = $query->param('filecomment') || ''; my $createLink = $query->param('createlink') || ''; my $doPropsOnly = $query->param('changeproperties'); my $filePath = $query->param('filepath') || ''; my $fileName = $query->param('filename') || ''; if ( $filePath && !$fileName ) { $filePath =~ m|([^/\\]*$)|; $fileName = $1; } $fileComment =~ s/\s+/ /go; $fileComment =~ s/^\s*//o; $fileComment =~ s/\s*$//o; $fileName =~ s/\s*$//o; $filePath =~ s/\s*$//o; unless ( TWiki::Func::checkAccessPermission( 'CHANGE', TWiki::Func::getWikiName(), undef, $topic, $web ) ) { returnRESTResult( $response, 401, "Access denied" ); return; # to prevent further processing } my ( $fileSize, $fileDate, $tmpFileName ); my $stream; $stream = $query->upload('filepath') unless $doPropsOnly; my $origName = $fileName; unless ($doPropsOnly) { # SMELL: call to unpublished function ( $fileName, $origName ) = TWiki::Sandbox::sanitizeAttachmentName($fileName); # check if upload has non zero size if ($stream) { my @stats = stat $stream; $fileSize = $stats[7]; $fileDate = $stats[9]; } unless ( $fileSize && $fileName ) { returnRESTResult( $response, 500, "Zero-sized file upload" ); return; # to prevent further processing } my $maxSize = TWiki::Func::getPreferencesValue('ATTACHFILESIZELIMIT') || 0; $maxSize =~ s/\s+$//; $maxSize = 0 unless ( $maxSize =~ /([0-9]+)/o ); if ( $maxSize && $fileSize > $maxSize * 1024 ) { returnRESTResult( $response, 500, "OVERSIZED UPLOAD" ); return; # to prevent further processing } } # SMELL: use of undocumented CGI::tmpFileName my $tfp = $query->tmpFileName( $query->param('filepath') ); my $error; try { TWiki::Func::saveAttachment( $web, $topic, $fileName, { dontlog => !$TWiki::cfg{Log}{upload}, comment => $fileComment, hide => $hideFile, createlink => $createLink, stream => $stream, filepath => $filePath, filesize => $fileSize, filedate => $fileDate, tmpFilename => $tfp, } ); } catch Error::Simple with { $error = shift->{-text}; } finally { close($stream) if $stream; }; if ($error) { returnRESTResult( $response, 500, $error ); return; # to prevent further processing } # Otherwise allow the rest dispatcher to write a 200 return "$origName attached to $web.$topic" . ( $origName ne $fileName ? " as $fileName" : '' ); } sub _unquote { my $text = shift; $text =~ s/\\/\\\\/g; $text =~ s/\n/\\n/g; $text =~ s/\r/\\r/g; $text =~ s/\t/\\t/g; $text =~ s/"/\\"/g; $text =~ s/'/\\'/g; return $text; } # Get, and return, a list of attachments using JSON sub _restAttachments { my ( $session, $plugin, $verb, $response ) = @_; my ( $web, $topic ) = ( $session->{webName}, $session->{topicName} ); my ( $meta, $text ) = TWiki::Func::readTopic( $web, $topic ); unless ( TWiki::Func::checkAccessPermission( 'VIEW', TWiki::Func::getWikiName(), $text, $topic, $web, $meta ) ) { returnRESTResult( $response, 401, "Access denied" ); return; # to prevent further processing } # Create a JSON list of attachment data, sorted by name my @atts; foreach my $att ( sort { $a->{name} cmp $b->{name} } $meta->find('FILEATTACHMENT') ) { push( @atts, '{' . join( ',', map { '"' . _unquote($_) . '":"' . _unquote( $att->{$_} ) . '"' } keys %$att ) . '}' ); } return '[' . join( ',', @atts ) . ']'; } 1;