source: trunk/core/lib/Foswiki/UI/Save.pm @ 1050

Revision 1050, 20.5 KB checked in by OlivierRaginel, 4 years ago (diff)

Item329: Fixed tagline

Line 
1# See bottom of file for license and copyright information
2
3=begin TML
4
5---+ package Foswiki::UI::Save
6
7UI delegate for save function
8
9=cut
10
11package Foswiki::UI::Save;
12
13use strict;
14use Error qw( :try );
15use Assert;
16
17require Foswiki;
18require Foswiki::UI;
19require Foswiki::Meta;
20require Foswiki::OopsException;
21
22# Used by save and preview
23sub buildNewTopic {
24    my ( $session, $script ) = @_;
25
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;
31
32    unless ( scalar( $query->param() ) ) {
33
34        # insufficient parameters to save
35        throw Foswiki::OopsException(
36            'attention',
37            def    => 'bad_script_parameters',
38            web    => $session->{webName},
39            topic  => $session->{topicName},
40            params => [$script]
41        );
42    }
43
44    Foswiki::UI::checkMirror( $session, $webName, $topic );
45    Foswiki::UI::checkWebExists( $session, $webName, $topic, 'save' );
46
47    my $topicExists = $store->topicExists( $webName, $topic );
48
49    # Prevent saving existing topic?
50    my $onlyNewTopic = Foswiki::isTrue( $query->param('onlynewtopic') );
51    if ( $onlyNewTopic && $topicExists ) {
52
53        # Topic exists and user requested oops if it exists
54        throw Foswiki::OopsException(
55            'attention',
56            def   => 'topic_exists',
57            web   => $webName,
58            topic => $topic
59        );
60    }
61
62    # prevent non-Wiki names?
63    my $onlyWikiName = Foswiki::isTrue( $query->param('onlywikiname') );
64    if (   ($onlyWikiName)
65        && ( !$topicExists )
66        && ( !Foswiki::isValidTopicName($topic) ) )
67    {
68
69        # do not allow non-wikinames
70        throw Foswiki::OopsException(
71            'attention',
72            def    => 'not_wikiword',
73            web    => $webName,
74            topic  => $topic,
75            params => [$topic]
76        );
77    }
78
79    my $user = $session->{user};
80    Foswiki::UI::checkAccess( $session, $webName, $topic, 'CHANGE', $user );
81
82    my $saveOpts = {};
83    $saveOpts->{minor} = 1 if $query->param('dontnotify');
84    my $originalrev = $query->param('originalrev');    # rev edit started on
85
86    # Populate the new meta data
87    my $newMeta = new Foswiki::Meta( $session, $webName, $topic );
88
89    my ( $prevMeta,     $prevText );
90    my ( $templateText, $templateMeta );
91    my $templatetopic = $query->param('templatetopic');
92    my $templateweb   = $webName;
93
94    if ($templatetopic) {
95        ( $templateweb, $templatetopic ) =
96          $session->normalizeWebTopicName( $templateweb, $templatetopic );
97
98        unless ( $store->topicExists( $templateweb, $templatetopic ) ) {
99            throw Foswiki::OopsException(
100                'attention',
101                def   => 'no_such_topic_template',
102                web   => $templateweb,
103                topic => $templatetopic
104            );
105        }
106    }
107
108    if ($topicExists) {
109        ( $prevMeta, $prevText ) =
110          $store->readTopic( $user, $webName, $topic, $revision );
111        if ($prevMeta) {
112            foreach my $k ( keys %$prevMeta ) {
113                unless ( $k =~ /^_/
114                    || $k eq 'FORM'
115                    || $k eq 'TOPICPARENT'
116                    || $k eq 'FIELD' )
117                {
118                    $newMeta->copyFrom( $prevMeta, $k );
119                }
120            }
121        }
122    }
123    elsif ($templatetopic) {
124        ( $templateMeta, $templateText ) =
125          $store->readTopic( $user, $templateweb, $templatetopic, $revision );
126        $templateText = '' if $query->param('newtopic');    # created by edit
127        $templateText =
128          $session->expandVariablesOnTopicCreation( $templateText, $user,
129            $webName, $topic );
130        foreach my $k ( keys %$templateMeta ) {
131            unless ( $k =~ /^_/
132                || $k eq 'FORM'
133                || $k eq 'TOPICPARENT'
134                || $k eq 'FIELD'
135                || $k eq 'TOPICMOVED' )
136            {
137                $newMeta->copyFrom( $templateMeta, $k );
138            }
139        }
140
141        # topic creation, there is no original rev
142        $originalrev = 0;
143    }
144
145    # Determine the new text
146    my $newText = $query->param('text');
147
148    my $forceNewRev = $query->param('forcenewrevision');
149    $saveOpts->{forcenewrevision} = $forceNewRev;
150    my $newParent = $query->param('topicparent');
151
152    if ( defined($newText) ) {
153
154        # text is defined in the query, save that text
155        $newText =~ s/\r//g;
156        $newText .= "\n" unless $newText =~ /\n$/s;
157
158    }
159    elsif ( defined $templateText ) {
160
161        # no text in the query, but we have a templatetopic
162        $newText     = $templateText;
163        $originalrev = 0;               # disable merge
164
165    }
166    else {
167        $newText = '';
168        if ( defined $prevText ) {
169            $newText     = $prevText;
170            $originalrev = 0;           # disable merge
171        }
172    }
173
174    my $mum;
175    if ($newParent) {
176        if ( $newParent ne 'none' ) {
177            $mum = { 'name' => $newParent };
178        }
179    }
180    elsif ($templateMeta) {
181        $mum = $templateMeta->get('TOPICPARENT');
182    }
183    elsif ($prevMeta) {
184        $mum = $prevMeta->get('TOPICPARENT');
185    }
186    $newMeta->put( 'TOPICPARENT', $mum ) if $mum;
187
188    my $formName = $query->param('formtemplate');
189    my $formDef;
190    my $copyMeta;
191
192    if ($formName) {
193
194        # new form, default field values will be null
195        $formName = '' if ( $formName eq 'none' );
196    }
197    elsif ($templateMeta) {
198
199        # populate the meta-data with field values from the template
200        $formName = $templateMeta->get('FORM');
201        $formName = $formName->{name} if $formName;
202        $copyMeta = $templateMeta;
203    }
204    elsif ($prevMeta) {
205
206        # populate the meta-data with field values from the existing topic
207        $formName = $prevMeta->get('FORM');
208        $formName = $formName->{name} if $formName;
209        $copyMeta = $prevMeta;
210    }
211
212    if ($formName) {
213        require Foswiki::Form;
214        $formDef = new Foswiki::Form( $session, $webName, $formName );
215        unless ($formDef) {
216            unless ($prevMeta) {
217                throw Foswiki::OopsException(
218                    'attention',
219                    def    => 'no_form_def',
220                    web    => $session->{webName},
221                    topic  => $session->{topicName},
222                    params => [ $webName, $formName ]
223                );
224            }
225
226            # Recreate the form fields from the previous rev of the topic.
227            $formDef =
228              new Foswiki::Form( $session, $webName, $formName, $prevMeta );
229        }
230        $newMeta->put( 'FORM', { name => $formName } );
231    }
232    if ( $copyMeta && $formDef ) {
233
234        # Copy existing fields into new form, filtering on the
235        # known field names so we don't copy dead data. Though we
236        # really should, of course. That comes later.
237        my $filter = join( '|',
238            map    { $_->{name} }
239              grep { $_->{name} } @{ $formDef->getFields() } );
240        $newMeta->copyFrom( $copyMeta, 'FIELD', qr/^($filter)$/ );
241    }
242    if ($formDef) {
243
244        # override with values from the query
245        my ( $seen, $missing ) =
246          $formDef->getFieldValuesFromQuery( $query, $newMeta );
247        if ( $seen && @$missing ) {
248
249            # chuck up if there is at least one field value defined in the
250            # query and a mandatory field was not defined in the
251            # query or by an existing value.
252            # Item5428: clean up <nop>'s
253            @$missing = map {
254                s/<nop>//g;
255                $_
256            } @$missing;
257            throw Foswiki::OopsException(
258                'attention',
259                def    => 'mandatory_field',
260                web    => $session->{webName},
261                topic  => $session->{topicName},
262                params => [ join( ' ', @$missing ) ]
263            );
264        }
265    }
266
267    my $merged;
268
269    # assumes rev numbers start at 1
270    if ($originalrev) {
271        my ( $orev, $odate );
272        if ( $originalrev =~ /^(\d+)_(\d+)$/ ) {
273            ( $orev, $odate ) = ( $1, $2 );
274        }
275        elsif ( $originalrev =~ /^\d+$/ ) {
276            $orev = $originalrev;
277        }
278        else {
279            $orev = 0;
280        }
281        my ( $date, $author, $rev, $comment ) = $newMeta->getRevisionInfo();
282
283        # If the last save was by me, don't merge
284        if ( ( $orev ne $rev || $odate && $date && $odate ne $date )
285            && $author ne $user )
286        {
287
288            require Foswiki::Merge;
289
290            my $pti = $prevMeta->get('TOPICINFO');
291            if (   $pti->{reprev}
292                && $pti->{version}
293                && $pti->{reprev} == $pti->{version} )
294            {
295
296                # If the ancestor revision was generated by a reprev,
297                # then the original is lost and we can't 3-way merge
298
299                $session->{plugins}
300                  ->dispatch( 'beforeMergeHandler', $newText, $pti->{version},
301                    $prevText, undef, undef, $webName, $topic );
302
303                $newText =
304                  Foswiki::Merge::merge2( $pti->{version}, $prevText, $rev,
305                    $newText, '.*?\n', $session );
306            }
307            else {
308
309                # common ancestor; we can 3-way merge
310                my ( $ancestorMeta, $ancestorText ) =
311                  $store->readTopic( undef, $webName, $topic, $orev );
312
313                $session->{plugins}
314                  ->dispatch( 'beforeMergeHandler', $newText, $rev, $prevText,
315                    $orev, $ancestorText, $webName, $topic );
316
317                $newText =
318                  Foswiki::Merge::merge3( $orev, $ancestorText, $rev, $prevText,
319                    'new', $newText, '.*?\n', $session );
320            }
321            if ( $formDef && $prevMeta ) {
322                $newMeta->merge( $prevMeta, $formDef );
323            }
324            $merged =
325              [ $orev, $session->{users}->getWikiName($author), $rev || 1 ];
326        }
327    }
328
329    return ( $newMeta, $newText, $saveOpts, $merged );
330}
331
332=begin TML
333
334---++ StaticMethod save($session)
335
336Command handler for =save= command.
337This method is designed to be
338invoked via the =UI::run= method.
339
340See System.CommandAndCGIScripts for details of parameters.
341
342Note: =cmd= has been deprecated in favour of =action=. It will be deleted at
343some point.
344
345=cut
346
347sub save {
348    my $session = shift;
349
350    my $query = $session->{request};
351    my $web   = $session->{webName};
352    my $topic = $session->{topicName};
353    my $store = $session->{store};
354    my $user  = $session->{user};
355
356    # Do not remove, keep as undocumented feature for compatibility with
357    # TWiki 4.0.x: Allow for dynamic topic creation by replacing strings
358    # of at least 10 x's XXXXXX with a next-in-sequence number.
359    # See Codev.AllowDynamicTopicNameCreation
360    if ( $topic =~ /X{10}/ ) {
361        my $n         = 0;
362        my $baseTopic = $topic;
363        $store->clearLease( $web, $baseTopic );
364        do {
365            $topic = $baseTopic;
366            $topic =~ s/X{10}X*/$n/e;
367            $n++;
368        } while ( $store->topicExists( $web, $topic ) );
369        $session->{topicName} = $topic;
370    }
371
372    # Allow for more flexible topic creation with sortable names and
373    # better performance. See Codev.AutoIncTopicNameOnSave
374    if ( $topic =~ /AUTOINC([0-9]+)/ ) {
375        my $start     = $1;
376        my $baseTopic = $topic;
377        $store->clearLease( $web, $baseTopic );
378        my $nameFilter = $topic;
379        $nameFilter =~ s/AUTOINC([0-9]+)/([0-9]+)/;
380        my @list =
381          sort { $a <=> $b }
382          map { s/^$nameFilter$/$1/; s/^0*([0-9])/$1/; $_ }
383          grep { /^$nameFilter$/ } $store->getTopicNames($web);
384        if ( scalar @list ) {
385
386            # find last one, and increment by one
387            my $next = $list[$#list] + 1;
388            my $len  = length($start);
389            $start =~ s/^0*([0-9])/$1/;    # cut leading zeros
390            $next = $start if ( $start > $next );
391            my $pad = $len - length($next);
392            if ( $pad > 0 ) {
393                $next = '0' x $pad . $next;    # zero-pad
394            }
395            $topic =~ s/AUTOINC[0-9]+/$next/;
396        }
397        else {
398
399            # first auto-inc topic
400            $topic =~ s/AUTOINC[0-9]+/$start/;
401        }
402        $session->{topicName} = $topic;
403    }
404
405    my $saveaction = '';
406    foreach my $action qw( save checkpoint quietsave cancel preview
407      addform replaceform delRev repRev ) {
408        if ( $query->param( 'action_' . $action ) )
409        {
410            $saveaction = $action;
411            last;
412        }
413      }
414
415      # the 'action' parameter has been deprecated, though is still available
416      # for compatibility with old templates.
417      if ( !$saveaction && $query->param('action') ) {
418        $saveaction = lc( $query->param('action') );
419        $session->writeWarning(<<WARN);
420Use of deprecated "action" parameter to "save". Correct your templates!
421WARN
422
423        # handle old values for form-related actions:
424        $saveaction = 'addform'     if ( $saveaction eq 'add form' );
425        $saveaction = 'replaceform' if ( $saveaction eq 'replace form...' );
426    }
427
428    if ( $saveaction eq 'cancel' ) {
429        my $lease = $store->getLease( $web, $topic );
430        if ( $lease && $lease->{user} eq $user ) {
431            $store->clearLease( $web, $topic );
432        }
433
434        # redirect to a sensible place (a topic that exists)
435        my ( $w, $t ) = ( '', '' );
436        foreach my $test ( $topic, $query->param('topicparent'),
437            $Foswiki::cfg{HomeTopicName} )
438        {
439            ( $w, $t ) = $session->normalizeWebTopicName( $web, $test );
440            last if ( $store->topicExists( $w, $t ) );
441        }
442        my $viewURL = $session->getScriptUrl( 1, 'view', $w, $t );
443        $session->redirect( $viewURL, undef, 1 );
444
445        return;
446    }
447
448    if ( $saveaction eq 'preview' ) {
449        require Foswiki::UI::Preview;
450        Foswiki::UI::Preview::preview($session);
451        return;
452    }
453
454    my $editaction = lc( $query->param('editaction') ) || '';
455    my $edit       = $query->param('edit')             || 'edit';
456    my $editparams = $query->param('editparams')       || '';
457
458    ## SMELL: The form affecting actions do not preserve edit and editparams
459    if (   $saveaction eq 'addform'
460        || $saveaction eq 'replaceform'
461        || $saveaction eq 'preview' && $query->param('submitChangeForm') )
462    {
463        require Foswiki::UI::ChangeForm;
464        $session->writeCompletePage(
465            Foswiki::UI::ChangeForm::generate(
466                $session, $web, $topic, $editaction
467            )
468        );
469        return;
470    }
471
472    my $redirecturl;
473
474    if ( $saveaction eq 'checkpoint' ) {
475        $query->param( -name => 'dontnotify', -value => 'checked' );
476        my $edittemplate = $query->param( 'template' );
477        my $editURL = $session->getScriptUrl( 1, $edit, $web, $topic );
478        $redirecturl = $editURL . '?t=' . time();
479        $redirecturl .= '&redirectto=' . $query->param('redirectto')
480          if $query->param('redirectto');
481
482        # select the appropriate edit template
483        $redirecturl .= '&action=' . $editaction if $editaction;
484        $redirecturl .= '&template=' . $edittemplate if $edittemplate;
485
486        $redirecturl .= '&skin=' . $query->param('skin')
487          if $query->param('skin');
488        $redirecturl .= '&cover=' . $query->param('cover')
489          if $query->param('cover');
490        $redirecturl .= '&nowysiwyg=' . $query->param('nowysiwyg')
491          if $query->param('nowysiwyg');
492        $redirecturl .= $editparams
493          if $editparams;    # May contain anchor
494        my $lease = $store->getLease( $web, $topic );
495        if ( $lease && $lease->{user} eq $user ) {
496            $store->setLease( $web, $topic, $user, $Foswiki::cfg{LeaseLength} );
497        }
498
499        # drop through
500    }
501
502    if ( $saveaction eq 'quietsave' ) {
503        $query->param( -name => 'dontnotify', -value => 'checked' );
504        $saveaction = 'save';
505
506        # drop through
507    }
508
509    if ( $saveaction =~ /^(del|rep)Rev$/ ) {
510
511        # hidden, largely undocumented functions, used by administrators for
512        # reverting spammed topics. These functions support rewriting
513        # history, in a Joe Stalin kind of way. They should be replaced with
514        # mechanisms for hiding revisions.
515        $query->param( -name => 'cmd', -value => $saveaction );
516
517        # drop through
518    }
519
520    my $saveCmd = $query->param('cmd') || 0;
521    if ( $saveCmd && !$session->{users}->isAdmin( $session->{user} ) ) {
522        throw Foswiki::OopsException(
523            'accessdenied',
524            def    => 'only_group',
525            web    => $web,
526            topic  => $topic,
527            params => [ $Foswiki::cfg{SuperAdminGroup} ]
528        );
529    }
530
531    #success - redirect to topic view (unless its a checkpoint save)
532    $redirecturl ||= $session->getScriptUrl( 1, 'view', $web, $topic );
533
534    if ( $saveCmd eq 'delRev' ) {
535
536        # delete top revision
537        try {
538            $store->delRev( $user, $web, $topic );
539        }
540        catch Error::Simple with {
541            throw Foswiki::OopsException(
542                'attention',
543                def    => 'save_error',
544                web    => $web,
545                topic  => $topic,
546                params => [ shift->{-text} ]
547            );
548        };
549
550        $session->redirect( $redirecturl, undef, 1 );
551        return;
552    }
553
554    if ( $saveCmd eq 'repRev' ) {
555
556        # replace top revision with the text from the query, trying to
557        # make it look as much like the original as possible. The query
558        # text is expected to contain %META as well as text.
559        my $meta =
560          new Foswiki::Meta( $session, $web, $topic, $query->param('text') );
561        my $saveOpts = {
562            timetravel => 1,
563            operation  => 'cmd',
564        };
565        try {
566            $store->repRev( $user, $web, $topic, $meta->text(), $meta,
567                $saveOpts );
568        }
569        catch Error::Simple with {
570            throw Foswiki::OopsException(
571                'attention',
572                def    => 'save_error',
573                web    => $web,
574                topic  => $topic,
575                params => [ shift->{-text} ]
576            );
577        };
578
579        $session->redirect( $redirecturl, undef,
580            ( $saveaction ne 'checkpoint' ) );
581        return;
582    }
583
584    my ( $newMeta, $newText, $saveOpts, $merged ) =
585      buildNewTopic( $session, 'save' );
586
587    if ( $saveaction =~ /^(save|checkpoint)$/ ) {
588        $session->{plugins}
589          ->dispatch( 'afterEditHandler', $newText, $topic, $web, $newMeta );
590    }
591
592    try {
593        $store->saveTopic( $user, $web, $topic, $newText, $newMeta, $saveOpts );
594    }
595    catch Error::Simple with {
596        throw Foswiki::OopsException(
597            'attention',
598            def    => 'save_error',
599            web    => $web,
600            topic  => $topic,
601            params => [ shift->{-text} ]
602        );
603    };
604
605    my $lease = $store->getLease( $web, $topic );
606
607    # clear the lease, if (and only if) we own it
608    if ( $lease && $lease->{user} eq $user ) {
609        $store->clearLease( $web, $topic );
610    }
611
612    if ($merged) {
613        throw Foswiki::OopsException(
614            'attention',
615            def    => 'merge_notice',
616            web    => $web,
617            topic  => $topic,
618            params => $merged
619        );
620    }
621
622    $session->redirect( $redirecturl, undef, ( $saveaction ne 'checkpoint' ) );
623}
624
6251;
626__DATA__
627# Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/
628#
629# Copyright (C) 2008 Foswiki Contributors. Foswiki Contributors
630# are listed in the AUTHORS file in the root of this distribution.
631# NOTE: Please extend that file, not this notice.
632#
633# Additional copyrights apply to some or all of the code in this
634# file as follows:
635#
636# Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
637# and TWiki Contributors. All Rights Reserved. TWiki Contributors
638# are listed in the AUTHORS file in the root of this distribution.
639# Based on parts of Ward Cunninghams original Wiki and JosWiki.
640# Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
641# Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
642#
643# This program is free software; you can redistribute it and/or
644# modify it under the terms of the GNU General Public License
645# as published by the Free Software Foundation; either version 2
646# of the License, or (at your option) any later version. For
647# more details read LICENSE in the root of this distribution.
648#
649# This program is distributed in the hope that it will be useful,
650# but WITHOUT ANY WARRANTY; without even the implied warranty of
651# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
652#
653# As per the GPL, removal of this notice is prohibited.
Note: See TracBrowser for help on using the repository browser.