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