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 warnings;
15use Error qw( :try );
16use Assert;
17
18use Foswiki                 ();
19use Foswiki::UI             ();
20use Foswiki::Meta           ();
21use Foswiki::OopsException  ();
22use Foswiki::Prefs::Request ();
23
24BEGIN {
25    if ( $Foswiki::cfg{UseLocale} ) {
26        require locale;
27        import locale();
28    }
29}
30
31# Used by save and preview
32sub buildNewTopic {
33    my ( $session, $topicObject, $script ) = @_;
34
35    my $query = $session->{request};
36
37    unless ( $query->param() ) {
38
39        # insufficient parameters to save
40        throw Foswiki::OopsException(
41            'attention',
42            def    => 'bad_script_parameters',
43            web    => $topicObject->web,
44            topic  => $topicObject->topic,
45            params => [$script]
46        );
47    }
48
49    Foswiki::UI::checkWebExists( $session, $topicObject->web, 'save' );
50
51    my $topicExists =
52      $session->topicExists( $topicObject->web, $topicObject->topic );
53
54    # Prevent creating a topic in a web without change access
55    unless ($topicExists) {
56        my $webObject = Foswiki::Meta->new( $session, $topicObject->web );
57        Foswiki::UI::checkAccess( $session, 'CHANGE', $webObject );
58    }
59
60    # Prevent saving existing topic?
61    my $onlyNewTopic =
62      Foswiki::isTrue( scalar( $query->param('onlynewtopic') ) );
63    if ( $onlyNewTopic && $topicExists ) {
64
65        # Topic exists and user requested oops if it exists
66        throw Foswiki::OopsException(
67            'attention',
68            def   => 'topic_exists',
69            web   => $topicObject->web,
70            topic => $topicObject->topic
71        );
72    }
73
74    # prevent non-Wiki names?
75    my $onlyWikiName =
76      Foswiki::isTrue( scalar( $query->param('onlywikiname') ) );
77    if (   ($onlyWikiName)
78        && ( !$topicExists )
79        && ( !Foswiki::isValidTopicName( $topicObject->topic ) ) )
80    {
81
82        # do not allow non-wikinames
83        throw Foswiki::OopsException(
84            'attention',
85            def    => 'not_wikiword',
86            web    => $topicObject->web,
87            topic  => $topicObject->topic,
88            params => [ $topicObject->topic ]
89        );
90    }
91
92    my $saveOpts = {};
93    $saveOpts->{minor}            = 1 if $query->param('dontnotify');
94    $saveOpts->{forcenewrevision} = 1 if $query->param('forcenewrevision');
95    my ( $ancestorRev, $ancestorDate );
96
97    my $templateTopic = $query->param('templatetopic');
98
99    my $templateWeb = $topicObject->web;
100    my $ttom;    # template topic
101
102    my $text = $topicObject->text();
103
104    my @attachments;
105    if ($topicExists) {
106
107        # Initialise from existing topic
108
109        Foswiki::UI::checkAccess( $session, 'VIEW',   $topicObject );
110        Foswiki::UI::checkAccess( $session, 'CHANGE', $topicObject );
111        $text        = $topicObject->text();            # text of last rev
112        $ancestorRev = $query->param('originalrev');    # rev edit started on
113
114    }
115    elsif ($templateTopic) {
116
117        # User specified template. Validate it.
118
119        my ( $invalidTemplateWeb, $invalidTemplateTopic ) =
120          $session->normalizeWebTopicName( $templateWeb, $templateTopic );
121
122        $templateWeb = Foswiki::Sandbox::untaint( $invalidTemplateWeb,
123            \&Foswiki::Sandbox::validateWebName );
124        $templateTopic = Foswiki::Sandbox::untaint( $invalidTemplateTopic,
125            \&Foswiki::Sandbox::validateTopicName );
126
127        unless ( $templateWeb && $templateTopic ) {
128            throw Foswiki::OopsException(
129                'attention',
130                def => 'invalid_topic_parameter',
131                params =>
132                  [ scalar( $query->param('templatetopic') ), 'templatetopic' ]
133            );
134        }
135        unless ( $session->topicExists( $templateWeb, $templateTopic ) ) {
136            throw Foswiki::OopsException(
137                'attention',
138                def   => 'no_such_topic_template',
139                web   => $templateWeb,
140                topic => $templateTopic
141            );
142        }
143
144        # Initialise new topic from template topic
145        $ttom = Foswiki::Meta->load( $session, $templateWeb, $templateTopic );
146        Foswiki::UI::checkAccess( $session, 'VIEW', $ttom );
147
148        $text = $ttom->text();
149        $text = '' if $query->param('newtopic');    # created by edit
150        $topicObject->text($text);
151
152        foreach my $k ( keys %$ttom ) {
153
154            # Skip internal fields and TOPICINFO, TOPICMOVED
155            unless ( $k =~ m/^(_|TOPIC)/ ) {
156                $topicObject->copyFrom( $ttom, $k );
157            }
158
159            # attachments to be copied later
160            if ( $k eq 'FILEATTACHMENT' ) {
161                foreach my $a ( @{ $ttom->{$k} } ) {
162                    push(
163                        @attachments,
164                        {
165                            name => $a->{name},
166                            tom  => $ttom,
167                        }
168                    );
169                }
170            }
171        }
172
173        $topicObject->expandNewTopic();
174        $text = $topicObject->text();
175
176        # topic creation, there is no original rev
177        $ancestorRev = 0;
178    }
179
180    # $text now contains either text from an existing topic.
181    # or text obtained from a template topic. Now determine if
182    # the query params will override it.
183    if ( defined $query->param('text') ) {
184
185        # text is defined in the query, save that text, overriding anything
186        # from the template or the previous rev of the topic
187        $text = $query->param('text');
188        $text =~ s/\r//g;
189        $text .= "\n" unless $text =~ m/\n$/s;
190    }
191
192    # Make sure that text is defined.
193    $text = '' unless defined $text;
194
195    # Change the parent, if appropriate
196    my $newParent = $query->param('topicparent');
197    if ($newParent) {
198        if ( $newParent eq 'none' ) {
199            $topicObject->remove('TOPICPARENT');
200        }
201        else {
202
203            # Validate the new parent (it must be a legal topic name)
204            my ( $vweb, $vtopic ) =
205              $session->normalizeWebTopicName( $topicObject->web(),
206                $newParent );
207            $vweb = Foswiki::Sandbox::untaint( $vweb,
208                \&Foswiki::Sandbox::validateWebName );
209            $vtopic = Foswiki::Sandbox::untaint( $vtopic,
210                \&Foswiki::Sandbox::validateTopicName );
211            unless ( $vweb && $vtopic ) {
212                throw Foswiki::OopsException(
213                    'attention',
214                    def    => 'invalid_topic_parameter',
215                    web    => $session->{webName},
216                    topic  => $session->{topicName},
217                    params => [ $newParent, 'topicparent' ]
218                );
219            }
220
221            # Re-untaint the raw parameter, so that a parent can be set with
222            # no web specification.
223            $topicObject->put( 'TOPICPARENT',
224                { 'name' => Foswiki::Sandbox::untaintUnchecked($newParent) } );
225        }
226    }
227
228    # Set preference values from query
229    Foswiki::Prefs::Request::set( $query, $topicObject );
230
231    my $formName = $query->param('formtemplate');
232    my $formDef;
233
234    if ($formName) {
235
236        # new form, default field values will be null
237        if ( $formName eq 'none' ) {
238
239            # No form, remove the old data
240            $topicObject->remove('FORM');
241            $topicObject->remove('FIELD');
242            $formName = undef;
243        }
244    }
245    else {
246
247        # Recover the existing form name
248        my $fm = $topicObject->get('FORM');
249        $formName = $fm->{name} if $fm;
250    }
251
252    if ($formName) {
253        require Foswiki::Form;
254        $formDef = new Foswiki::Form( $session, $topicObject->web, $formName );
255        $topicObject->put( 'FORM', { name => $formName } );
256
257        # Remove fields that don't exist on the new form def.
258        my $filter = join( '|',
259            map  { $_->{name} }
260            grep { $_->{name} } @{ $formDef->getFields() } );
261        foreach my $f ( $topicObject->find('FIELD') ) {
262            if ( $f->{name} !~ /^($filter)$/ ) {
263                $topicObject->remove( 'FIELD', $f->{name} );
264            }
265        }
266
267        # override existing fields with values from the query
268        my ( $seen, $missing ) =
269          $formDef->getFieldValuesFromQuery( $query, $topicObject );
270        if ( $seen && @$missing ) {
271
272            # chuck up if there is at least one field value defined in the
273            # query and a mandatory field was not defined in the
274            # query or by an existing value.
275            throw Foswiki::OopsException(
276                'attention',
277                def    => 'mandatory_field',
278                web    => $topicObject->web,
279                topic  => $topicObject->topic,
280                params => [ join( ' ', @$missing ) ]
281            );
282        }
283    }
284
285    if ($ancestorRev) {
286        if ( $ancestorRev =~ m/^(\d+)_(\d+)$/ ) {
287            ( $ancestorRev, $ancestorDate ) = ( $1, $2 );
288        }
289        elsif ( $ancestorRev !~ /^\d+$/ ) {
290
291            # Badly formatted ancestor
292            throw Foswiki::OopsException(
293                'attention',
294                def    => 'bad_script_parameters',
295                web    => $topicObject->web,
296                topic  => $topicObject->topic,
297                params => [$script]
298            );
299        }
300    }
301
302    my $merged;
303    if ($ancestorRev) {
304
305        # Get information for the most recently saved rev
306        my $info = $topicObject->getRevisionInfo();
307
308        # If the last save was done since we started the edit, and it
309        # wasn't saved by the current user, we need to merge. We also
310        # check the ancestor date, in case a repRev happened.
311        if (
312            (
313                   $ancestorRev ne $info->{version}
314                || $ancestorDate
315                && $info->{date}
316                && $ancestorDate ne $info->{date}
317            )
318            && $info->{author} ne $session->{user}
319          )
320        {
321
322            # Load the prev rev again, so we can do a 3 way merge
323            my $prevTopicObject =
324              Foswiki::Meta->load( $session, $topicObject->web,
325                $topicObject->topic );
326
327            require Foswiki::Merge;
328
329            $topicObject->getRevisionInfo();
330            my $pti = $topicObject->get('TOPICINFO');
331            if (   $pti->{reprev}
332                && $pti->{version}
333                && $pti->{reprev} == $pti->{version} )
334            {
335
336                # If the ancestor revision was generated by a reprev,
337                # then the original is lost and we can't 3-way merge
338                $session->{plugins}->dispatch(
339                    'beforeMergeHandler', $text,
340                    $pti->{version},      $prevTopicObject->text,
341                    undef,                undef,
342                    $topicObject->web,    $topicObject->topic
343                );
344
345                $text =
346                  Foswiki::Merge::merge2( $pti->{version},
347                    $prevTopicObject->text, $info->{version}, $text, '.*?\n',
348                    $session );
349            }
350            else {
351
352                # common ancestor; we can 3-way merge
353                my $ancestorMeta =
354                  Foswiki::Meta->load( $session, $topicObject->web,
355                    $topicObject->topic, $ancestorRev );
356                $session->{plugins}->dispatch(
357                    'beforeMergeHandler', $text,
358                    $info->{version},     $prevTopicObject->text,
359                    $ancestorRev,         $ancestorMeta->text(),
360                    $topicObject->web,    $topicObject->topic
361                );
362
363                $text =
364                  Foswiki::Merge::merge3( $ancestorRev, $ancestorMeta->text(),
365                    $info->{version}, $prevTopicObject->text, 'new', $text,
366                    '.*?\n', $session );
367            }
368            if ($formDef) {
369                $topicObject->merge( $prevTopicObject, $formDef );
370            }
371            $merged = [ $ancestorRev, $info->{author}, $info->{version} || 1 ];
372        }
373    }
374    $topicObject->text($text);
375
376    return ( $saveOpts, $merged, \@attachments );
377}
378
379=begin TML
380
381---++ StaticMethod expandAUTOINC($session, $web, $topic) -> $topic
382Expand AUTOINC\d+ in the topic name to the next topic name available
383
384=cut
385
386sub expandAUTOINC {
387    my ( $session, $web, $topic ) = @_;
388
389    # Do not remove, keep as undocumented feature for compatibility with
390    # TWiki 4.0.x: Allow for dynamic topic creation by replacing strings
391    # of at least 10 x's XXXXXX with a next-in-sequence number.
392    if ( $topic =~ m/X{10}/ ) {
393        my $n           = 0;
394        my $baseTopic   = $topic;
395        my $topicObject = Foswiki::Meta->new( $session, $web, $baseTopic );
396        $topicObject->clearLease();
397        do {
398            $topic = $baseTopic;
399            $topic =~ s/X{10}X*/$n/e;
400            $n++;
401        } while ( $session->topicExists( $web, $topic ) );
402    }
403
404    # Allow for more flexible topic creation with sortable names.
405    # See Codev.AutoIncTopicNameOnSave
406    if ( $topic =~ m/^(.*)AUTOINC(\d+)(.*)$/ ) {
407        my $pre         = $1;
408        my $start       = $2;
409        my $pad         = length($start);
410        my $post        = $3;
411        my $topicObject = Foswiki::Meta->new( $session, $web, $topic );
412        $topicObject->clearLease();
413        my $webObject = Foswiki::Meta->new( $session, $web );
414        my $it = $webObject->eachTopic();
415
416        while ( $it->hasNext() ) {
417            my $tn = $it->next();
418            next unless $tn =~ m/^${pre}(\d+)${post}$/;
419            $start = $1 + 1 if ( $1 >= $start );
420        }
421        my $next = sprintf( "%0${pad}d", $start );
422        $topic =~ s/AUTOINC[0-9]+/$next/;
423    }
424    return $topic;
425}
426
427=begin TML
428
429---++ StaticMethod save($session)
430
431Command handler for =save= command.
432This method is designed to be
433invoked via the =UI::run= method.
434
435See System.CommandAndCGIScripts for details of parameters.
436
437Note: =cmd= has been deprecated in favour of =action=. It will be deleted at
438some point.
439
440=cut
441
442sub save {
443    my $session = shift;
444
445    my $query = $session->{request};
446
447    my $saveaction = '';
448    foreach my $action (
449        qw( save checkpoint quietsave cancel preview
450        addform replaceform delRev repRev )
451      )
452    {
453        if ( $query->param( 'action_' . $action ) ) {
454            $saveaction = $action;
455            last;
456        }
457    }
458
459    # the 'action' parameter has been deprecated, though is still available
460    # for compatibility with old templates.
461    if ( !$saveaction && $query->param('action') ) {
462        $saveaction = lc( $query->param('action') );
463        $session->logger->log( 'warning', <<WARN);
464Use of deprecated "action" parameter to "save". Correct your templates!
465WARN
466
467        # handle old values for form-related actions:
468        $saveaction = 'addform'     if ( $saveaction eq 'add form' );
469        $saveaction = 'replaceform' if ( $saveaction eq 'replace form...' );
470    }
471
472    if ( $saveaction eq 'preview' ) {
473        require Foswiki::UI::Preview;
474        Foswiki::UI::Preview::preview($session);
475        return;
476    }
477
478    my ( $web, $topic ) =
479      $session->normalizeWebTopicName( $session->{webName},
480        $session->{topicName} );
481
482    if ( $session->{invalidTopic} ) {
483        throw Foswiki::OopsException(
484            'accessdenied',
485            status => 404,
486            def    => 'invalid_topic_name',
487            web    => $web,
488            topic  => $topic,
489            params => [ $session->{invalidTopic} ]
490        );
491    }
492
493    $topic = expandAUTOINC( $session, $web, $topic );
494
495    my $topicObject = Foswiki::Meta->new( $session, $web, $topic );
496
497    if ( $saveaction eq 'cancel' ) {
498        my $lease = $topicObject->getLease();
499        if ( $lease && $lease->{user} eq $session->{user} ) {
500            $topicObject->clearLease();
501        }
502
503        # redirect to a sensible place (a topic that exists)
504        my ( $w, $t ) = ( '', '' );
505        foreach my $test (
506            $topic,
507            scalar( $query->param('topicparent') ),
508            $Foswiki::cfg{HomeTopicName}
509          )
510        {
511            ( $w, $t ) = $session->normalizeWebTopicName( $web, $test );
512
513            # Validate topic name
514            $t = Foswiki::Sandbox::untaint( $t,
515                \&Foswiki::Sandbox::validateTopicName );
516            last if ( $session->topicExists( $w, $t ) );
517        }
518        $session->redirect( $session->redirectto("$w.$t") );
519
520        return;
521    }
522
523    # Do this *before* we do any query parameter rewriting
524    Foswiki::UI::checkValidationKey($session);
525
526    my $editaction = lc( $query->param('editaction') || '' );
527    my $edit = $query->param('edit') || 'edit';
528
529    ## SMELL: The form affecting actions do not preserve edit and editparams
530    # preview+submitChangeForm is deprecated undocumented legacy
531    if (   $saveaction eq 'addform'
532        || $saveaction eq 'replaceform'
533        || $saveaction eq 'preview' && $query->param('submitChangeForm') )
534    {
535        require Foswiki::UI::ChangeForm;
536        $session->writeCompletePage(
537            Foswiki::UI::ChangeForm::generate(
538                $session, $topicObject, $editaction
539            )
540        );
541        return;
542    }
543
544    my $redirecturl;
545
546    if ( $saveaction eq 'checkpoint' ) {
547        $query->param( -name => 'dontnotify', -value => 'checked' );
548        my $edittemplate = $query->param('template');
549        my %p = ( t => time() );
550
551        # map editaction -> action and edittemplat -> template
552        $p{action}   = $editaction   if $editaction;
553        $p{template} = $edittemplate if $edittemplate;
554
555        # Pass through selected parameters
556        foreach my $pthru (qw(redirectto skin cover nowysiwyg action)) {
557            $p{$pthru} = $query->param($pthru);
558        }
559
560        $redirecturl = $session->getScriptUrl( 1, $edit, $web, $topic, %p );
561
562        $redirecturl .= $query->param('editparams')
563          if $query->param('editparams');    # May contain anchor
564
565        my $lease = $topicObject->getLease();
566
567        if ( $lease && $lease->{user} eq $session->{user} ) {
568            $topicObject->setLease( $Foswiki::cfg{LeaseLength} );
569        }
570
571        # drop through
572    }
573    else {
574
575        # redirect to topic view or any other redirectto
576        # specified as an url param
577        $redirecturl = $session->redirectto("$web.$topic");
578    }
579
580    if ( $saveaction eq 'quietsave' ) {
581        $query->param( -name => 'dontnotify', -value => 'checked' );
582        $saveaction = 'save';
583
584        # drop through
585    }
586
587    if ( $saveaction =~ m/^(del|rep)Rev$/ ) {
588
589        # hidden, largely undocumented functions, used by administrators for
590        # reverting spammed topics. These functions support rewriting
591        # history, in a Joe Stalin kind of way. They should be replaced with
592        # mechanisms for hiding revisions.
593        $query->param( -name => 'cmd', -value => $saveaction );
594
595        # drop through
596    }
597
598    my $adminCmd = $query->param('cmd') || 0;
599    if ( $adminCmd && !$session->{users}->isAdmin( $session->{user} ) ) {
600        throw Foswiki::OopsException(
601            'accessdenied',
602            status => 403,
603            def    => 'only_group',
604            web    => $web,
605            topic  => $topic,
606            params => [ $Foswiki::cfg{SuperAdminGroup} ]
607        );
608    }
609
610    if ( $adminCmd eq 'delRev' ) {
611
612        # delete top revision
613        try {
614            $topicObject->deleteMostRecentRevision();
615        }
616        catch Foswiki::OopsException with {
617            shift->throw();    # propagate
618        }
619        catch Error with {
620            $session->logger->log( 'error', shift->{-text} );
621            throw Foswiki::OopsException(
622                'attention',
623                def    => 'save_error',
624                web    => $web,
625                topic  => $topic,
626                params => [
627                    $session->i18n->maketext(
628                        'Operation [_1] failed with an internal error',
629                        'delRev'
630                    )
631                ],
632            );
633        };
634
635        $session->redirect($redirecturl);
636        return;
637    }
638
639    if ( $adminCmd eq 'repRev' ) {
640
641        # replace top revision with the text from the query, trying to
642        # make it look as much like the original as possible. The query
643        # text is expected to contain %META as well as text.
644        $topicObject->text( scalar $query->param('text') );
645
646        try {
647            $topicObject->replaceMostRecentRevision( forcedate => 1 );
648        }
649        catch Foswiki::OopsException with {
650            shift->throw();    # propagate
651        }
652        catch Error with {
653            $session->logger->log( 'error', shift->{-text} );
654            throw Foswiki::OopsException(
655                'attention',
656                def    => 'save_error',
657                web    => $web,
658                topic  => $topic,
659                params => [
660                    $session->i18n->maketext(
661                        'Operation [_1] failed with an internal error',
662                        'repRev'
663                    )
664                ],
665            );
666        };
667
668        $session->redirect($redirecturl);
669        return;
670    }
671
672    # This is where the permissions are checked.  Error will be thrown
673    # if the save won't be allowed.
674    my ( $saveOpts, $merged, $attachments ) =
675      buildNewTopic( $session, $topicObject, 'save' );
676
677    if ( $saveaction =~ m/^(save|checkpoint)$/ ) {
678        my $text = $topicObject->text();
679        $text = '' unless defined $text;
680        $session->{plugins}
681          ->dispatch( 'afterEditHandler', $text, $topicObject->topic,
682            $topicObject->web, $topicObject );
683        $topicObject->text($text);
684    }
685
686    try {
687        $topicObject->save(%$saveOpts);
688    }
689    catch Foswiki::OopsException with {
690        shift->throw();    # propagate
691    }
692    catch Error with {
693        $session->logger->log( 'error', shift->{-text} );
694        throw Foswiki::OopsException(
695            'attention',
696            def    => 'save_error',
697            web    => $topicObject->web,
698            topic  => $topicObject->topic,
699            params => [
700                $session->i18n->maketext(
701                    'Operation [_1] failed with an internal error', 'save'
702                )
703            ],
704        );
705    };
706
707    # Final version created during merge.
708    if ($merged) {
709        my $savedInfo = $topicObject->getRevisionInfo();
710        push @$merged, $savedInfo->{version};
711    }
712
713    if ($attachments) {
714        foreach $a ( @{$attachments} ) {
715            try {
716                $a->{tom}->copyAttachment( $a->{name}, $topicObject );
717            }
718            catch Foswiki::OopsException with {
719                shift->throw();    # propagate
720            }
721            catch Error with {
722                $session->logger->log( 'error', shift->{-text} );
723                throw Foswiki::OopsException(
724                    'attention',
725                    def    => 'save_error',
726                    web    => $topicObject->web,
727                    topic  => $topicObject->topic,
728                    params => [
729                        $session->i18n->maketext(
730                            'Operation [_1] failed with an internal error',
731                            'copyAttachment'
732                        )
733                    ],
734                );
735            };
736        }
737    }
738
739    my $lease = $topicObject->getLease();
740
741    # clear the lease, if (and only if) we own it
742    if ( $lease && $lease->{user} eq $session->{user} ) {
743        $topicObject->clearLease();
744    }
745
746    if ($merged) {
747        throw Foswiki::OopsException(
748            'attention',
749            status => 200,
750            def    => 'merge_notice',
751            web    => $topicObject->web,
752            topic  => $topicObject->topic,
753            params => $merged
754        );
755    }
756
757    $session->redirect($redirecturl);
758}
759
7601;
761__END__
762Foswiki - The Free and Open Source Wiki, http://foswiki.org/
763
764Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
765are listed in the AUTHORS file in the root of this distribution.
766NOTE: Please extend that file, not this notice.
767
768Additional copyrights apply to some or all of the code in this
769file as follows:
770
771Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
772and TWiki Contributors. All Rights Reserved.
773Based on parts of Ward Cunninghams original Wiki and JosWiki.
774Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
775Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
776
777This program is free software; you can redistribute it and/or
778modify it under the terms of the GNU General Public License
779as published by the Free Software Foundation; either version 2
780of the License, or (at your option) any later version. For
781more details read LICENSE in the root of this distribution.
782
783This program is distributed in the hope that it will be useful,
784but WITHOUT ANY WARRANTY; without even the implied warranty of
785MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
786
787As per the GPL, removal of this notice is prohibited.
788