wiki-archive/twiki/lib/TWiki/UI/Save.pm

557 lines
20 KiB
Perl

# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
#
# Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
# and TWiki 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.
#
# Additional copyrights apply to some or all of the code in this
# file as follows:
# Based on parts of Ward Cunninghams original Wiki and JosWiki.
# Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
# Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
#
# 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 2
# of the License, or (at your option) any later version. For
# more details read LICENSE in the root of this 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.
=pod
---+ package TWiki::UI::Save
UI delegate for save function
=cut
package TWiki::UI::Save;
use strict;
use TWiki;
use TWiki::UI;
use Error qw( :try );
use TWiki::OopsException;
use TWiki::Merge;
use Assert;
# Used by save and preview
sub buildNewTopic {
my( $session, $script ) = @_;
my $query = $session->{cgiQuery};
my $webName = $session->{webName};
my $topic = $session->{topicName};
my $store = $session->{store};
unless( scalar($query->param()) ) {
# insufficient parameters to save
throw TWiki::OopsException( 'attention',
def => 'bad_script_parameters',
web => $session->{webName},
topic => $session->{topicName},
params => [ $script ]);
}
TWiki::UI::checkMirror( $session, $webName, $topic );
TWiki::UI::checkWebExists( $session, $webName, $topic, 'save' );
my $topicExists = $store->topicExists( $webName, $topic );
# Prevent saving existing topic?
my $onlyNewTopic = TWiki::isTrue( $query->param( 'onlynewtopic' ));
if( $onlyNewTopic && $topicExists ) {
# Topic exists and user requested oops if it exists
throw TWiki::OopsException( 'attention',
def => 'topic_exists',
web => $webName,
topic => $topic );
}
# prevent non-Wiki names?
my $onlyWikiName = TWiki::isTrue( $query->param( 'onlywikiname' ));
if( ( $onlyWikiName )
&& ( ! $topicExists )
&& ( ! TWiki::isValidTopicName( $topic ) ) ) {
# do not allow non-wikinames
throw TWiki::OopsException( 'attention',
def => 'not_wikiword',
web => $webName,
topic => $topic,
params => [ $topic ] );
}
my $user = $session->{user};
TWiki::UI::checkAccess( $session, $webName, $topic,
'change', $user );
my $saveOpts = {};
$saveOpts->{minor} = 1 if $query->param( 'dontnotify' );
my $originalrev = $query->param( 'originalrev' ); # rev edit started on
# Populate the new meta data
my $newMeta = new TWiki::Meta( $session, $webName, $topic );
my ( $prevMeta, $prevText );
my ( $templateText, $templateMeta );
my $templatetopic = $query->param( 'templatetopic');
my $templateweb = $webName;
if( $topicExists ) {
( $prevMeta, $prevText ) =
$store->readTopic( $user, $webName, $topic, undef );
if( $prevMeta ) {
foreach my $k ( keys %$prevMeta ) {
unless( $k =~ /^_/ || $k eq 'FORM' || $k eq 'TOPICPARENT' ||
$k eq 'FIELD' ) {
$newMeta->copyFrom( $prevMeta, $k );
}
}
}
} elsif ($templatetopic) {
( $templateweb, $templatetopic ) =
$session->normalizeWebTopicName( $templateweb, $templatetopic );
( $templateMeta, $templateText ) =
$store->readTopic( $user, $templateweb,
$templatetopic, undef );
$templateText = '' if $query->param( 'newtopic' ); # created by edit
$templateText =
$session->expandVariablesOnTopicCreation( $templateText );
foreach my $k ( keys %$templateMeta ) {
unless( $k =~ /^_/ || $k eq 'FORM' || $k eq 'TOPICPARENT' ||
$k eq 'FIELD' ) {
$newMeta->copyFrom( $templateMeta, $k );
}
}
# topic creation, there is no original rev
$originalrev = 0;
}
# Determine the new text
my $newText = $query->param( 'text' );
my $forceNewRev = $query->param( 'forcenewrevision' );
$saveOpts->{forcenewrevision} = $forceNewRev;
my $newParent = $query->param( 'topicparent' );
if( defined( $newText) ) {
# text is defined in the query, save that text
$newText =~ s/\r//g;
$newText .= "\n" unless $newText =~ /\n$/s;
} elsif( defined $templateText ) {
# no text in the query, but we have a templatetopic
$newText = $templateText;
$originalrev = 0; # disable merge
} else {
$newText = '';
if( defined $prevText ) {
$newText = $prevText;
$originalrev = 0; # disable merge
}
}
my $mum;
if( $newParent ) {
if( $newParent ne 'none' ) {
$mum = { 'name' => $newParent };
}
} elsif( $templateMeta ) {
$mum = $templateMeta->get( 'TOPICPARENT' );
} elsif( $prevMeta ) {
$mum = $prevMeta->get( 'TOPICPARENT' );
}
$newMeta->put( 'TOPICPARENT', $mum ) if $mum;
my $formName = $query->param( 'formtemplate' );
my $formDef;
my $copyMeta;
if( $formName ) {
# new form, default field values will be null
$formName = '' if( $formName eq 'none' );
} elsif( $templateMeta ) {
# populate the meta-data with field values from the template
$formName = $templateMeta->get( 'FORM' );
$formName = $formName->{name} if $formName;;
$copyMeta = $templateMeta;
} elsif( $prevMeta ) {
# populate the meta-data with field values from the existing topic
$formName = $prevMeta->get( 'FORM' );
$formName = $formName->{name} if $formName;;
$copyMeta = $prevMeta;
}
if( $formName ) {
$formDef = new TWiki::Form( $session, $webName, $formName );
unless( $formDef ) {
throw TWiki::OopsException( 'attention',
def => 'no_form_def',
web => $session->{webName},
topic => $session->{topicName},
params => [ $webName, $formName ] );
}
$newMeta->put( 'FORM', { name => $formName });
}
if( $copyMeta && $formDef ) {
# Copy existing fields into new form, filtering on the
# known field names so we don't copy dead data. Though we
# really should, of course. That comes later.
my $filter = join(
'|',
map { $_->{name} }
grep { $_->{name} } @{$formDef->{fields}} );
$newMeta->copyFrom( $copyMeta, 'FIELD', qr/^($filter)$/ );
}
if( $formDef ) {
# override with values from the query
my( $seen, $missing ) =
$formDef->getFieldValuesFromQuery( $query, $newMeta );
if( $seen && @$missing ) {
# chuck up if there is at least one field value defined in the
# query and a mandatory field was not defined in the
# query or by an existing value.
throw TWiki::OopsException(
'attention',
def=>'mandatory_field',
web => $session->{webName},
topic => $session->{topicName},
params => [ join( ' ', @$missing ) ] );
}
}
my $merged;
# assumes rev numbers start at 1
if( $originalrev ) {
my( $orev, $odate );
if( $originalrev =~ /^(\d+)_(\d+)$/ ) {
( $orev, $odate ) = ( $1, $2 );
} elsif( $originalrev =~ /^\d+$/ ) {
$orev = $originalrev;
} else {
$orev = 0;
}
my( $date, $author, $rev, $comment ) = $newMeta->getRevisionInfo();
# If the last save was by me, don't merge
if(( $orev ne $rev ||
$odate && $date && $odate ne $date ) &&
!$author->equals( $user )) {
my $pti = $prevMeta->get( 'TOPICINFO' );
if( $pti->{reprev} && $pti->{version} &&
$pti->{reprev} == $pti->{version} ) {
# If the ancestor revision was generated by a reprev,
# then the original is lost and we can't 3-way merge
$newText = TWiki::Merge::merge2(
$pti->{version}, $prevText,
$rev, $newText,
'.*?\n',
$session );
} else {
# common ancestor; we can 3-way merge
my( $ancestorMeta, $ancestorText ) =
$store->readTopic( undef, $webName, $topic, $orev );
$newText = TWiki::Merge::merge3(
$orev, $ancestorText,
$rev, $prevText,
'new', $newText,
'.*?\n', $session );
}
if( $formDef && $prevMeta ) {
$newMeta->merge( $prevMeta, $formDef );
}
$merged = [ $orev, $author->wikiName(), $rev||1 ];
}
}
return( $newMeta, $newText, $saveOpts, $merged );
}
sub _getRedirectUrl {
my $session = shift;
my $query = $session->{cgiQuery};
my $redirecturl = $query->param( 'redirectto' );
return '' unless $redirecturl;
if( $redirecturl =~ /^$TWiki::regex{linkProtocolPattern}\:\/\//o ) {
# assuming URL
if ($TWiki::cfg{AllowRedirectUrl}) {
return $redirecturl;
} else {
return '';
}
}
# assuming 'web.topic' or 'topic'
my ( $w, $t ) = $session->normalizeWebTopicName( $session->{webName}, $redirecturl );
$redirecturl = $session->getScriptUrl( 1, 'view', $w, $t );
}
=pod
---++ StaticMethod save($session)
Command handler for =save= command.
This method is designed to be
invoked via the =TWiki::UI::run= method.
See TWiki.TWikiScripts for details of parameters.
Note: =cmd= has been deprecated in favour of =action=. It will be deleted at
some point.
=cut
sub save {
my $session = shift;
my $query = $session->{cgiQuery};
my $web = $session->{webName};
my $topic = $session->{topicName};
my $store = $session->{store};
my $user = $session->{user};
# Do not remove, keep as undocumented feature for compatibility with
# TWiki 4.0.x: Allow for dynamic topic creation by replacing strings
# of at least 10 x's XXXXXX with a next-in-sequence number.
# See Codev.AllowDynamicTopicNameCreation
if ( $topic =~ /X{10}/ ) {
my $n = 0;
my $baseTopic = $topic;
$store->clearLease( $web, $baseTopic );
do {
$topic = $baseTopic;
$topic =~ s/X{10}X*/$n/e;
$n++;
} while( $store->topicExists( $web, $topic ));
$session->{topicName} = $topic;
}
# Allow for more flexible topic creation with sortable names and
# better performance. See Codev.AutoIncTopicNameOnSave
if( $topic =~ /AUTOINC([0-9]+)/ ) {
my $start = $1;
my $baseTopic = $topic;
$store->clearLease( $web, $baseTopic );
my $nameFilter = $topic;
$nameFilter =~ s/AUTOINC([0-9]+)/([0-9]+)/;
my @list =
sort{ $a <=> $b }
map{ s/^$nameFilter$/$1/; s/^0*([0-9])/$1/; $_ }
grep{ /^$nameFilter$/ }
$store->getTopicNames( $web );
if( scalar @list ) {
# find last one, and increment by one
my $next = $list[$#list] + 1;
my $len = length( $start );
$start =~ s/^0*([0-9])/$1/; # cut leading zeros
$next = $start if( $start > $next );
my $pad = $len - length($next);
if( $pad > 0 ) {
$next = '0' x $pad . $next; # zero-pad
}
$topic =~ s/AUTOINC[0-9]+/$next/;
} else {
# first auto-inc topic
$topic =~ s/AUTOINC[0-9]+/$start/;
}
$session->{topicName} = $topic;
}
my $redirecturl = _getRedirectUrl( $session );
my $saveaction = '';
foreach my $action qw( save checkpoint quietsave cancel preview
addform replaceform delRev repRev ) {
if ($query->param('action_' . $action)) {
$saveaction = $action;
last;
}
}
# the 'action' parameter has been deprecated, though is still available
# for compatibility with old templates.
if( !$saveaction && $query->param( 'action' )) {
$saveaction = lc($query->param( 'action' ));
$session->writeWarning(<<WARN);
Use of deprecated "action" parameter to "save". Correct your templates!
WARN
# handle old values for form-related actions:
$saveaction = 'addform' if ( $saveaction eq 'add form');
$saveaction = 'replaceform' if ( $saveaction eq 'replace form...');
}
if( $saveaction eq 'cancel' ) {
my $lease = $store->getLease( $web, $topic );
if( $lease && $lease->{user}->equals( $user )) {
$store->clearLease( $web, $topic );
}
# redirect to a sensible place (a topic that exists)
my $viewURL = $redirecturl || '';
unless ( $viewURL ) {
my( $w, $t ) = ( '', '' );
foreach my $test ( $topic,
$query->param( 'topicparent' ),
$TWiki::cfg{HomeTopicName} ) {
( $w, $t ) =
$session->normalizeWebTopicName( $web, $test );
last if( $store->topicExists( $w, $t ));
}
$viewURL = $session->getScriptUrl( 1, 'view', $w, $t );
}
$session->redirect( $viewURL );
return;
}
if( $saveaction eq 'preview' ) {
require TWiki::UI::Preview;
TWiki::UI::Preview::preview( $session );
return;
}
my $editaction = lc($query->param( 'editaction' )) || '';
my $edit = $query->param( 'edit' ) || 'edit';
my $editparams = $query->param( 'editparams' ) || '';
## SMELL: The form affecting actions do not preserve edit and editparams
if( $saveaction eq 'addform' ||
$saveaction eq 'replaceform' ||
$saveaction eq 'preview' && $query->param( 'submitChangeForm' )) {
require TWiki::UI::ChangeForm;
$session->writeCompletePage
( TWiki::UI::ChangeForm::generate( $session, $web,
$topic, $editaction ) );
return;
}
if( $saveaction eq 'checkpoint' ) {
$query->param( -name=>'dontnotify', -value=>'checked' );
my $editURL = $session->getScriptUrl( 1, $edit, $web, $topic );
$redirecturl = $editURL.'?t='.time();
$redirecturl .= '&redirectto='.$query->param( 'redirectto' )
if $query->param( 'redirectto' );
# select the appropriate edit template
$redirecturl .= '&action='.$editaction if $editaction;
$redirecturl .= '&skin='.$query->param('skin')
if $query->param('skin');
$redirecturl .= '&cover='.$query->param('cover')
if $query->param('cover');
$redirecturl .= $editparams
if $editparams; # May contain anchor
my $lease = $store->getLease( $web, $topic );
if( $lease && $lease->{user}->equals( $user )) {
$store->setLease( $web, $topic, $user, $TWiki::cfg{LeaseLength} );
}
# drop through
}
if( $saveaction eq 'quietsave' ) {
$query->param( -name=>'dontnotify', -value=>'checked' );
# drop through
}
if( $saveaction =~ /^(del|rep)Rev$/ ) {
# hidden, largely undocumented functions, used by administrators for
# reverting spammed topics. These functions support rewriting
# history, in a Joe Stalin kind of way. They should be replaced with
# mechanisms for hiding revisions.
$query->param( -name => 'cmd', -value => $saveaction );
# drop through
}
my $saveCmd = $query->param( 'cmd' ) || 0;
if ( $saveCmd && ! $session->{user}->isAdmin()) {
throw TWiki::OopsException( 'accessdenied', def => 'only_group',
web => $web, topic => $topic,
params => $TWiki::cfg{UsersWebName}.
'.'.$TWiki::cfg{SuperAdminGroup} );
}
$redirecturl ||= $session->getScriptUrl( 1, 'view', $web, $topic );
if( $saveCmd eq 'delRev' ) {
# delete top revision
try {
$store->delRev( $user, $web, $topic );
} catch Error::Simple with {
throw TWiki::OopsException( 'attention',
def => 'save_error',
web => $web,
topic => $topic,
params => shift->{-text} );
};
$session->redirect( $redirecturl );
return;
}
if( $saveCmd eq 'repRev' ) {
# replace top revision with the text from the query, trying to
# make it look as much like the original as possible. The query
# text is expected to contain %META as well as text.
my $textQueryParam = $query->param( 'text' );
my $meta = new TWiki::Meta( $session, $web, $topic );
$store->extractMetaData( $meta, \$textQueryParam );
my $saveOpts = {
timetravel => 1,
operation => 'cmd',
};
try {
$store->repRev( $user, $web, $topic,
$textQueryParam, $meta, $saveOpts );
} catch Error::Simple with {
throw TWiki::OopsException( 'attention',
def => 'save_error',
web => $web,
topic => $topic,
params => shift->{-text} );
};
$session->redirect( $redirecturl );
return;
}
my( $newMeta, $newText, $saveOpts, $merged ) =
TWiki::UI::Save::buildNewTopic($session, 'save');
if( $saveaction =~ /^(save|checkpoint)$/ ) {
$session->{plugins}->afterEditHandler( $newText, $topic, $web );
}
try {
$store->saveTopic( $user, $web, $topic,
$newText, $newMeta, $saveOpts );
} catch Error::Simple with {
throw TWiki::OopsException( 'attention',
def => 'save_error',
web => $web,
topic => $topic,
params => shift->{-text} );
};
my $lease = $store->getLease( $web, $topic );
# clear the lease, if (and only if) we own it
if( $lease && $lease->{user}->equals( $user )) {
$store->clearLease( $web, $topic );
}
if( $merged ) {
throw TWiki::OopsException( 'attention',
def => 'merge_notice',
web => $web, topic => $topic,
params => $merged );
}
$session->redirect( $redirecturl );
}
1;