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