1
# See bottom of file for license and copyright information
5
---+ package Foswiki::UI::Save
7
UI delegate for save function
11
package Foswiki::UI::Save;
19
require Foswiki::Meta;
20
require Foswiki::OopsException;
22
# Used by save and preview
24
my ( $session, $script ) = @_;
26
my $query = $session->{request};
27
my $webName = $session->{webName};
28
my $topic = $session->{topicName};
29
my $store = $session->{store};
30
my $revision = $query->param('rev') || undef;
32
unless ( scalar( $query->param() ) ) {
34
# insufficient parameters to save
35
throw Foswiki::OopsException(
37
def => 'bad_script_parameters',
38
web => $session->{webName},
39
topic => $session->{topicName},
44
Foswiki::UI::checkWebExists( $session, $webName, $topic, 'save' );
46
my $topicExists = $store->topicExists( $webName, $topic );
48
# Prevent saving existing topic?
49
my $onlyNewTopic = Foswiki::isTrue( $query->param('onlynewtopic') );
50
if ( $onlyNewTopic && $topicExists ) {
52
# Topic exists and user requested oops if it exists
53
throw Foswiki::OopsException(
55
def => 'topic_exists',
61
# prevent non-Wiki names?
62
my $onlyWikiName = Foswiki::isTrue( $query->param('onlywikiname') );
65
&& ( !Foswiki::isValidTopicName($topic) ) )
68
# do not allow non-wikinames
69
throw Foswiki::OopsException(
71
def => 'not_wikiword',
78
my $user = $session->{user};
79
Foswiki::UI::checkAccess( $session, $webName, $topic, 'CHANGE', $user );
82
$saveOpts->{minor} = 1 if $query->param('dontnotify');
83
my $originalrev = $query->param('originalrev'); # rev edit started on
85
# Populate the new meta data
86
my $newMeta = new Foswiki::Meta( $session, $webName, $topic );
88
my ( $prevMeta, $prevText );
89
my ( $templateText, $templateMeta );
90
my $templatetopic = $query->param('templatetopic');
91
my $templateweb = $webName;
94
( $templateweb, $templatetopic ) =
95
$session->normalizeWebTopicName( $templateweb, $templatetopic );
97
if ( $store->topicExists( $templateweb, $templatetopic ) ) {
100
Foswiki::Sandbox::untaintUnchecked( $templateweb );
102
Foswiki::Sandbox::untaintUnchecked( $templatetopic );
104
throw Foswiki::OopsException(
106
def => 'no_such_topic_template',
108
topic => $templatetopic
114
( $prevMeta, $prevText ) =
115
$store->readTopic( $user, $webName, $topic, $revision );
117
foreach my $k ( keys %$prevMeta ) {
120
|| $k eq 'TOPICPARENT'
123
$newMeta->copyFrom( $prevMeta, $k );
128
elsif ($templatetopic) {
129
( $templateMeta, $templateText ) =
130
$store->readTopic( $user, $templateweb, $templatetopic, $revision );
131
$templateText = '' if $query->param('newtopic'); # created by edit
133
$session->expandVariablesOnTopicCreation( $templateText, $user,
135
foreach my $k ( keys %$templateMeta ) {
138
|| $k eq 'TOPICPARENT'
140
|| $k eq 'TOPICMOVED' )
142
$newMeta->copyFrom( $templateMeta, $k );
146
# topic creation, there is no original rev
150
# Determine the new text
151
my $newText = $query->param('text');
153
my $forceNewRev = $query->param('forcenewrevision');
154
$saveOpts->{forcenewrevision} = $forceNewRev;
155
my $newParent = $query->param('topicparent');
157
if ( defined($newText) ) {
159
# text is defined in the query, save that text
161
$newText .= "\n" unless $newText =~ /\n$/s;
164
elsif ( defined $templateText ) {
166
# no text in the query, but we have a templatetopic
167
$newText = $templateText;
168
$originalrev = 0; # disable merge
173
if ( defined $prevText ) {
174
$newText = $prevText;
175
$originalrev = 0; # disable merge
181
if ( $newParent ne 'none' ) {
182
$mum = { 'name' => $newParent };
185
elsif ($templateMeta) {
186
$mum = $templateMeta->get('TOPICPARENT');
189
$mum = $prevMeta->get('TOPICPARENT');
191
$newMeta->put( 'TOPICPARENT', $mum ) if $mum;
193
my $formName = $query->param('formtemplate');
199
# new form, default field values will be null
200
$formName = '' if ( $formName eq 'none' );
202
elsif ($templateMeta) {
204
# populate the meta-data with field values from the template
205
$formName = $templateMeta->get('FORM');
206
$formName = $formName->{name} if $formName;
207
$copyMeta = $templateMeta;
211
# populate the meta-data with field values from the existing topic
212
$formName = $prevMeta->get('FORM');
213
$formName = $formName->{name} if $formName;
214
$copyMeta = $prevMeta;
218
require Foswiki::Form;
219
$formDef = new Foswiki::Form( $session, $webName, $formName );
222
throw Foswiki::OopsException(
224
def => 'no_form_def',
225
web => $session->{webName},
226
topic => $session->{topicName},
227
params => [ $webName, $formName ]
231
# Recreate the form fields from the previous rev of the topic.
233
new Foswiki::Form( $session, $webName, $formName, $prevMeta );
235
$newMeta->put( 'FORM', { name => $formName } );
237
if ( $copyMeta && $formDef ) {
239
# Copy existing fields into new form, filtering on the
240
# known field names so we don't copy dead data. Though we
241
# really should, of course. That comes later.
242
my $filter = join( '|',
244
grep { $_->{name} } @{ $formDef->getFields() } );
245
$newMeta->copyFrom( $copyMeta, 'FIELD', qr/^($filter)$/ );
249
# override with values from the query
250
my ( $seen, $missing ) =
251
$formDef->getFieldValuesFromQuery( $query, $newMeta );
252
if ( $seen && @$missing ) {
254
# chuck up if there is at least one field value defined in the
255
# query and a mandatory field was not defined in the
256
# query or by an existing value.
257
# Item5428: clean up <nop>'s
262
throw Foswiki::OopsException(
264
def => 'mandatory_field',
265
web => $session->{webName},
266
topic => $session->{topicName},
267
params => [ join( ' ', @$missing ) ]
274
# If the topic exists, see if we need to merge
275
if ( $topicExists && $originalrev ) {
276
my ( $orev, $odate );
277
if ( $originalrev =~ /^(\d+)_(\d+)$/ ) {
278
( $orev, $odate ) = ( $1, $2 );
280
elsif ( $originalrev =~ /^\d+$/ ) {
281
$orev = $originalrev;
286
my ( $date, $author, $rev, $comment ) = $newMeta->getRevisionInfo();
288
# If the last save was by me, don't merge
289
if ( ( $orev ne $rev || $odate && $date && $odate ne $date )
290
&& $author ne $user )
293
require Foswiki::Merge;
295
my $pti = $prevMeta->get('TOPICINFO');
299
&& $pti->{reprev} == $pti->{version} )
302
# If the ancestor revision was generated by a reprev,
303
# then the original is lost and we can't 3-way merge
306
->dispatch( 'beforeMergeHandler', $newText, $pti->{version},
307
$prevText, undef, undef, $webName, $topic );
310
Foswiki::Merge::merge2( $pti->{version}, $prevText, $rev,
311
$newText, '.*?\n', $session );
315
# common ancestor; we can 3-way merge
316
my ( $ancestorMeta, $ancestorText ) =
317
$store->readTopic( undef, $webName, $topic, $orev );
320
->dispatch( 'beforeMergeHandler', $newText, $rev, $prevText,
321
$orev, $ancestorText, $webName, $topic );
324
Foswiki::Merge::merge3( $orev, $ancestorText, $rev, $prevText,
325
'new', $newText, '.*?\n', $session );
327
if ( $formDef && $prevMeta ) {
328
$newMeta->merge( $prevMeta, $formDef );
331
[ $orev, $session->{users}->getWikiName($author), $rev || 1 ];
335
return ( $newMeta, $newText, $saveOpts, $merged );
340
---++ StaticMethod save($session)
342
Command handler for =save= command.
343
This method is designed to be
344
invoked via the =UI::run= method.
346
See System.CommandAndCGIScripts for details of parameters.
348
Note: =cmd= has been deprecated in favour of =action=. It will be deleted at
356
my $query = $session->{request};
357
my $web = $session->{webName};
358
my $topic = $session->{topicName};
359
my $store = $session->{store};
360
my $user = $session->{user};
362
# Do not remove, keep as undocumented feature for compatibility with
363
# TWiki 4.0.x: Allow for dynamic topic creation by replacing strings
364
# of at least 10 x's XXXXXX with a next-in-sequence number.
365
# See Codev.AllowDynamicTopicNameCreation
366
if ( $topic =~ /X{10}/ ) {
368
my $baseTopic = $topic;
369
$store->clearLease( $web, $baseTopic );
372
$topic =~ s/X{10}X*/$n/e;
374
} while ( $store->topicExists( $web, $topic ) );
375
$session->{topicName} = $topic;
378
# Allow for more flexible topic creation with sortable names and
379
# better performance. See Codev.AutoIncTopicNameOnSave
380
if ( $topic =~ /AUTOINC([0-9]+)/ ) {
382
my $baseTopic = $topic;
383
$store->clearLease( $web, $baseTopic );
384
my $nameFilter = $topic;
385
$nameFilter =~ s/AUTOINC([0-9]+)/([0-9]+)/;
388
map { s/^$nameFilter$/$1/; s/^0*([0-9])/$1/; $_ }
389
grep { /^$nameFilter$/ } $store->getTopicNames($web);
390
if ( scalar @list ) {
392
# find last one, and increment by one
393
my $next = $list[$#list] + 1;
394
my $len = length($start);
395
$start =~ s/^0*([0-9])/$1/; # cut leading zeros
396
$next = $start if ( $start > $next );
397
my $pad = $len - length($next);
399
$next = '0' x $pad . $next; # zero-pad
401
$topic =~ s/AUTOINC[0-9]+/$next/;
405
# first auto-inc topic
406
$topic =~ s/AUTOINC[0-9]+/$start/;
408
$session->{topicName} = $topic;
412
foreach my $action qw( save checkpoint quietsave cancel preview
413
addform replaceform delRev repRev ) {
414
if ( $query->param( 'action_' . $action ) )
416
$saveaction = $action;
421
# the 'action' parameter has been deprecated, though is still available
422
# for compatibility with old templates.
423
if ( !$saveaction && $query->param('action') ) {
424
$saveaction = lc( $query->param('action') );
425
$session->logger->log('warning',<<WARN);
426
Use of deprecated "action" parameter to "save". Correct your templates!
429
# handle old values for form-related actions:
430
$saveaction = 'addform' if ( $saveaction eq 'add form' );
431
$saveaction = 'replaceform' if ( $saveaction eq 'replace form...' );
434
if ( $saveaction eq 'cancel' ) {
435
my $lease = $store->getLease( $web, $topic );
436
if ( $lease && $lease->{user} eq $user ) {
437
$store->clearLease( $web, $topic );
440
# redirect to a sensible place (a topic that exists)
441
my ( $w, $t ) = ( '', '' );
442
foreach my $test ( $topic, $query->param('topicparent'),
443
$Foswiki::cfg{HomeTopicName} )
445
( $w, $t ) = $session->normalizeWebTopicName( $web, $test );
446
last if ( $store->topicExists( $w, $t ) );
448
my $viewURL = $session->getScriptUrl( 1, 'view', $w, $t );
449
$session->redirect( $session->redirectto($viewURL) );
454
if ( $saveaction eq 'preview' ) {
455
require Foswiki::UI::Preview;
456
Foswiki::UI::Preview::preview($session);
460
# Do this *before* we do any query parameter rewriting
461
Foswiki::UI::checkValidationKey($session, 'save', $web, $topic);
463
my $editaction = lc( $query->param('editaction') ) || '';
464
my $edit = $query->param('edit') || 'edit';
465
my $editparams = $query->param('editparams') || '';
467
## SMELL: The form affecting actions do not preserve edit and editparams
468
if ( $saveaction eq 'addform'
469
|| $saveaction eq 'replaceform'
470
|| $saveaction eq 'preview' && $query->param('submitChangeForm') )
472
require Foswiki::UI::ChangeForm;
473
$session->writeCompletePage(
474
Foswiki::UI::ChangeForm::generate(
475
$session, $web, $topic, $editaction
483
if ( $saveaction eq 'checkpoint' ) {
484
$query->param( -name => 'dontnotify', -value => 'checked' );
485
my $edittemplate = $query->param( 'template' );
486
my $editURL = $session->getScriptUrl( 1, $edit, $web, $topic );
487
$redirecturl = $editURL . '?t=' . time();
488
$redirecturl .= '&redirectto=' . $query->param('redirectto')
489
if $query->param('redirectto');
491
# select the appropriate edit template
492
$redirecturl .= '&action=' . $editaction if $editaction;
493
$redirecturl .= '&template=' . $edittemplate if $edittemplate;
495
$redirecturl .= '&skin=' . $query->param('skin')
496
if $query->param('skin');
497
$redirecturl .= '&cover=' . $query->param('cover')
498
if $query->param('cover');
499
$redirecturl .= '&nowysiwyg=' . $query->param('nowysiwyg')
500
if $query->param('nowysiwyg');
501
$redirecturl .= '&action=' . $query->param('action')
502
if $query->param('action');
503
$redirecturl .= $editparams
504
if $editparams; # May contain anchor
505
my $lease = $store->getLease( $web, $topic );
506
if ( $lease && $lease->{user} eq $user ) {
507
$store->setLease( $web, $topic, $user, $Foswiki::cfg{LeaseLength} );
512
$redirecturl = $session->getScriptUrl( 1, 'view', $web, $topic );
515
# Do we have ?redirectto=
516
if ($saveaction ne 'checkpoint') {
517
$redirecturl = $session->redirectto($redirecturl);
520
if ( $saveaction eq 'quietsave' ) {
521
$query->param( -name => 'dontnotify', -value => 'checked' );
522
$saveaction = 'save';
527
if ( $saveaction =~ /^(del|rep)Rev$/ ) {
529
# hidden, largely undocumented functions, used by administrators for
530
# reverting spammed topics. These functions support rewriting
531
# history, in a Joe Stalin kind of way. They should be replaced with
532
# mechanisms for hiding revisions.
533
$query->param( -name => 'cmd', -value => $saveaction );
538
my $saveCmd = $query->param('cmd') || 0;
539
if ( $saveCmd && !$session->{users}->isAdmin( $session->{user} ) ) {
540
throw Foswiki::OopsException(
541
'accessdenied', status => 403,
545
params => [ $Foswiki::cfg{SuperAdminGroup} ]
549
#success - redirect to topic view (unless its a checkpoint save)
551
if ( $saveCmd eq 'delRev' ) {
553
# delete top revision
555
$store->delRev( $user, $web, $topic );
557
catch Error::Simple with {
558
throw Foswiki::OopsException(
563
params => [ shift->{-text} ]
567
$session->redirect( $redirecturl );
571
if ( $saveCmd eq 'repRev' ) {
573
# replace top revision with the text from the query, trying to
574
# make it look as much like the original as possible. The query
575
# text is expected to contain %META as well as text.
577
new Foswiki::Meta( $session, $web, $topic, $query->param('text') );
584
$store->repRev( $user, $web, $topic, $meta->text(), $meta,
587
catch Error::Simple with {
588
throw Foswiki::OopsException(
593
params => [ shift->{-text} ]
597
$session->redirect( $redirecturl);
601
my ( $newMeta, $newText, $saveOpts, $merged ) =
602
buildNewTopic( $session, 'save' );
604
if ( $saveaction =~ /^(save|checkpoint)$/ ) {
606
->dispatch( 'afterEditHandler', $newText, $topic, $web, $newMeta );
610
$store->saveTopic( $user, $web, $topic, $newText, $newMeta, $saveOpts );
612
catch Error::Simple with {
613
throw Foswiki::OopsException(
618
params => [ shift->{-text} ]
622
my $lease = $store->getLease( $web, $topic );
624
# clear the lease, if (and only if) we own it
625
if ( $lease && $lease->{user} eq $user ) {
626
$store->clearLease( $web, $topic );
630
throw Foswiki::OopsException(
631
'attention', status => 200,
632
def => 'merge_notice',
639
$session->redirect( $redirecturl );
644
# Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/
646
# Copyright (C) 2008-2009 Foswiki Contributors. Foswiki Contributors
647
# are listed in the AUTHORS file in the root of this distribution.
648
# NOTE: Please extend that file, not this notice.
650
# Additional copyrights apply to some or all of the code in this
653
# Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
654
# and TWiki Contributors. All Rights Reserved. TWiki Contributors
655
# are listed in the AUTHORS file in the root of this distribution.
656
# Based on parts of Ward Cunninghams original Wiki and JosWiki.
657
# Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
658
# Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
660
# This program is free software; you can redistribute it and/or
661
# modify it under the terms of the GNU General Public License
662
# as published by the Free Software Foundation; either version 2
663
# of the License, or (at your option) any later version. For
664
# more details read LICENSE in the root of this distribution.
666
# This program is distributed in the hope that it will be useful,
667
# but WITHOUT ANY WARRANTY; without even the implied warranty of
668
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
670
# As per the GPL, removal of this notice is prohibited.