557 lines
20 KiB
Perl
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;
|