#?SuikaWikiConfig/2.0 Plugin: @Name: WikiEdit @Description: @@@: WikiPage editing components @@lang:en @License: %%Perl%% @Author: @@Name: @@@@: Wakaba @@@lang:ja @@@script:Latn @@Mail[list]: w@suika.fam.cx @Date.RCS: $Date: 2004/04/17 04:16:15 $ @RequiredModule[list]: Digest::SHA1 @RequiredPlugin[list]: WikiView WikiStruct WikiLinking WikiFormCore WikiFormText WikiResource HTML @Use: use Message::Util::Error; PluginConst: @NS_XHTML1: http://www.w3.org/1999/xhtml ViewDefinition: @Mode: edit @Condition: @@http-method[list]: GET HEAD @Description: @@@: Edit WikiPage content as whole, as a plaintext @@lang: en @template: @@http-status-code: 200 @@media-type: text/html @@use-media-type-charset: 1 @@expires: %%edit%% @@body: %html-document ( title => {%res(name=>{Edit:WebPageTitle});}p, link-meta => {%template (name => links); %html-meta(name => ROBOTS, content => NOINDEX);}p, content => { %template ( name => ws--page, -content => {%template (name => we--edit-body);}, ); }p, ); ViewDefinition: @Mode: write @Condition: @@http-method[list]: POST @Description: @@@: Saving modified (new) WikiPage content @@lang: en @Use: use Message::Util::Error; @method: @@@: my $touch = $self->{view}->{wiki}->{input}->parameter ('we--touch'); $self->{view}->{wiki}->{var}->{db}->{read_only}->{'content'} = 0; $self->{view}->{wiki}->{var}->{db}->{read_only}->{'lastmodified'} = 0 if $touch; $self->{view}->{wiki}->{var}->{db}->{read_only}->{'referer'} = 0; $self->{view}->init_db; my $page = $self->{view}->{wiki}->{var}->{page}; ## Check confliction my $current_content; try { $current_content = $self->{view}->{wiki}->{db}->get (content => $page); } catch SuikaWiki::DB::Util::Error with { my $err = shift; if ($err->{-type} eq 'ERROR_REPORTED') { $err->throw; } else { $self->{view}->{wiki}->view_in_mode (mode => '-wdb--fatal-error'); throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED'; } }; my $current_digest = ''; if (length $current_content) { $current_digest = __FUNCPACK__->digest ($current_content, normalize => 1); my $prev_digest = $self->{view}->{wiki}->{input}->parameter ('we--digest'); if ($current_digest ne $prev_digest) { return $self->{view}->{wiki}->view_in_mode (mode => '-conflict'); } } my $new_content = $self->{view}->{wiki}->{input}->parameter ('we--content'); if (length $new_content) { ## Really modified? my $new_digest = __FUNCPACK__->digest ($new_content, normalize => 1); if ($current_digest ne $new_digest) { ## Update content try { $self->{view}->{wiki}->{db}->set (content => $page => $new_content); } catch SuikaWiki::DB::Util::Error with { my $err = shift; if ($err->{-type} eq 'ERROR_REPORTED') { $err->throw; } else { $self->{view}->{wiki}->view_in_mode (mode => '-wdb--fatal-error'); throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED'; } }; ## Update lastmodified if ($touch) { try { $self->{view}->{wiki}->{db}->set (lastmodified => $page => time); } catch SuikaWiki::DB::Util::Error with { my $err = shift; $err->throw if $err->{-type} eq 'ERROR_REPORTED'; }; } } my $new_mode = $self->{view}->{wiki}->{input}->parameter ('we--mode-modified'); $new_mode =~ s/[^0-9A-Za-z._-]+//g; my $uri = $self->{view}->{wiki}->uri_reference (page => $page, mode => $new_mode, up_to_date => 1, param => {we__mode_modified => $new_mode}); require SuikaWiki::Output::HTTP; my $output = SuikaWiki::Output::HTTP->new (wiki => $self->{view}->{wiki}); $output->set_redirect (uri => $uri, status_code => 303); $output->output (output => 'http-cgi'); } else { try { $self->{view}->{wiki}->{db}->delete (content => $page); $self->{view}->{wiki}->{db}->delete (lastmodified => $page) if $touch; } catch SuikaWiki::DB::Util::Error with { my $err = shift; if ($err->{-type} eq 'ERROR_REPORTED') { $err->throw; } else { $self->{view}->{wiki}->view_in_mode (mode => '-wdb--fatal-error'); throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED'; } }; $self->{view}->{wiki}->view_in_mode (mode => '-deleted'); } @@Name: main ViewDefinition: @Mode: adminedit @Condition: @@http-method[list]: GET HEAD @Description: @@@: Edit WikiPage content as whole, as a plaintext (administrator mode) @@lang: en @template: @@http-status-code: 200 @@media-type: application/xhtml+xml @@use-media-type-charset: 1 @@expires: %%edit%% @@body: %html-document ( title => {%res(name=>{Edit:Admin:WebPageTitle});}p, link-meta => {%template (name => links); %html-meta(name=>ROBOTS,content=>NOINDEX);}p, content => { %template ( name => ws--page, -content => {%template (name => we--adminedit-body);}, ); }p, ); ViewDefinition: @Mode: -conflict @Condition: @@http-method[list]: GET HEAD @Description: @@@: Confliction message @@lang: en @template: @@http-status-code: 409 @@media-type: text/html @@use-media-type-charset: 1 @@body: %html-document ( title => {%res(name=>{Edit:Conflict:WebPageTitle});}p, link-meta => {%template(name=>links); %html-meta(name=>ROBOTS,content=>NOINDEX);}p, content => { %template ( name => ws--page, -content => { %section ( title => {%res(name=>{Edit:Conflict:Title});}p, heading, content => {%template (name => we--conflict-body);}p, ); }, ); }p, ); ViewDefinition: @Mode: -deleted @Condition: @@http-method[list]: GET HEAD @Expires:%%view%% @Description: @@@: Confliction message @@lang: en @template: @@http-status-code: 200 @@media-type: text/html @@use-media-type-charset: 1 @@body: %html-document ( title => {%res (name => {Edit:Deleted:WebPageTitle});}p, link-meta => {%template (name => links); %html-meta (name => ROBOTS, content => NOINDEX);}p, content => { %template ( name => ws--page, -content => { %section ( title => {%res(name=>{Edit:Deleted:Title});}p, heading, content => { %paragraph (content => {%res (name => {Edit:Deleted:Description});}p); }p, ); }, ); }p, ); ViewFragment: @Name: links @Description: @@@: Link to edit mode of the WikiPage @@lang:en @Formatting: %link-wiki(mode => edit, up-to-date, rel => edit, class => wiki-cmd, description => {%res(name=>{Link:Edit:Description});}p); %link-wiki(mode => adminedit, up-to-date, rel=>edit, class => wiki-cmd, description => {%res(name=>{Link:AdminEdit:Description});}p); ViewFragment: @Name: navbar @Description: @@@: Link to edit mode of the WikiPage @Order: 10 @Formatting: %link-to-wikipage ( mode => edit, rel => edit, up-to-date, label => {%link-to-it ( class => wiki-cmd, label => {%res (name => EditThisPage);}p, description => {%res (name => EditThisPageLong);}p, );}, page-anchor-name => edit, ); ViewFragment: @Template[list]: we--conflict-body @Order: 30 @Description: @@@: Conflict report @@lang:en @Formatting: %paragraph (content=>{%res(name=>{Edit:Conflict:Description});}p); %we--conflict-modified-content; %section ( id => edit-conflict-diff, title => {%res(name=>{Edit:Conflict:Diff:Title});}p, heading, content => { %paragraph ( content => {%res(name=>{Edit:Conflict:Diff:Description});}p); %conflict-diff; }p, ); ViewFragment: @Template[list]: we--edit-body @Order:80 @Description: @@@: Editing WikiPage form section @@lang:en @Formatting: %section ( id => edit, title => {%res (name => {Edit:Title});}p, heading, content => {%edit-form;}p, ); ViewFragment: @Template[list]: we--adminedit-body @Order:80 @Description: @@@: Editing WikiPage form section (Admin edit mode) @@lang:en @Formatting: %section ( id => edit, title => {%res(name=>{Edit:Title});}p, heading, content => {%edit-form(admin);}p, ); ViewFragment: @Template[list]: we--conflict-body @Order: 80 @Description: @@@: Editing-WikiPage-form section (conflict mode) @@lang:en @Formatting: %section ( id => edit, title => {%res(name=>{Edit:Title});}p, heading, content => { %paragraph(name=>{%res(name=>{Edit:Conflict:Edit:Description});}p); %edit-form; }p, ); ViewFragment: @Name: we--edit @Description: @@@: Edit form --- main textarea @@lang:en @Order: 0 @Formatting: %textarea (id => we--content, size => {%res (name=>{Edit:Form:Size});}p, source => content, lines => {%res (name=>{Edit:Form:Lines});}p); ViewFragment: @Name: we--edit @Description: @@@: Submit button @@lang:en @Order: 200 @Formatting: %submit(label=>{%res(name=>{Edit:Save});}p); FormattingRule: @Category[list]: view @Name: edit-form @Description: @@@: Provides WikiPage editing form @@lang: en @Parameter: @@Name: admin @@Type: boolean @@Default: {0} @@Description: @@@@: Whether administrator's editing mode or not @@@lang:en @Parameter: @@Name: page @@Type: WikiName @@Default: (auto) @@Description: @@@@: WikiPage that is editing @@@lang:en @Formatting: __ATTRTEXT:%admin__;__ATTRTEXT:%page__; my $page = $o->{wiki}->name ($p->{page} || $o->{wiki}->{var}->{page}); my $template = $o->{wiki}->{view}->assemble_template ('we__edit'); local $o->{var} = { #content is_admin_mode => $p->{admin}, is_conflict_mode => 0, #is_new_page_template }; $o->{var}->{content} = __FUNCPACK__->get_content ($o, $page); SuikaWiki::Plugin->module_package ('WikiFormCore') ->make_form_in_html ($p->{-parent}, $template, wiki => $o->{wiki}, o => $o, index => -1, output => { mode => 'write', page => $page, hidden => sub { my ($hidden, $o) = @_; for ($hidden->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'input')) { $_->set_attribute (type => 'hidden'); $_->set_attribute (name => 'we--digest'); $_->set_attribute (value => ($o->{var}->{is_new_page_template}?'': __FUNCPACK__->digest ($o->{var}->{content}, normalize => 1))); $_->option (use_EmptyElemTag => 1); } }, }); FormattingRule: @Category[list]: view @Name: we--conflict-modified-content @Description: @@@: Modified content that is conflicted with current content @@lang:en @Formatting: $p->{-parent}->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'pre') ->append_text (scalar $o->{wiki}->{input}->parameter ('we--content')); FormattingRule: @Category[list]:view @Name: conflict-diff @Description: @@@: Provides marked diff between latest WikiPage content and submitted one @@lang:en @Formatting: my $before = scalar $o->{wiki}->{input}->parameter ('we--content'); my $after; try { $after = $o->{wiki}->{db}->get (content => $o->{wiki}->{var}->{page}); } catch SuikaWiki::DB::Util::Error with { my $err = shift; $err->throw if $err->{-type} eq 'ERROR_REPORTED'; $after = undef; }; __FUNCPACK__->diff_in_html (\$before => \$after, $p->{-parent}); Function: @Name: diff_in_html @Main: my (undef, $before, $after, $parent, %opt) = @_; require Algorithm::Diff; my $diff = $parent->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'pre'); $diff->set_attribute (class => 'diff'); for (Algorithm::Diff::diff ([split /\x0D?\x0A/, $$before]=>[split /\x0D?\x0A/, $$after])) { for (@{$_}) { my ($sign, $lineno, $text) = @{$_}; my $line = $diff->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => ({qw/+ ins - del/}->{$sign}||'span')); $line->set_attribute (class => 'line'); for ($line->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'span')) { $_->set_attribute (class => 'lineno'); $_->append_text ($lineno); } $line->append_text (' '); for ($line->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'span')) { $_->set_attribute (class => 'sign'); $_->append_text ($sign); } $line->append_text (' '); for ($line->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'span')) { $_->set_attribute (class => 'content'); $_->append_text ($text); } $line->append_text ("\n"); } # diff lines } FormattingRule: @Name:mode-after-edit-selection @Category[list]:form-input @Parameter: @@Name: description @@Type: text @@Default: (none) @@Description: @@@@: Human readable description about this input @@@lang: en @Formatting: ## TODO: media-type prop support my $magic = ''; $magic = $1 if $o->{var}->{content} =~ m/^([^\x0A\x0D]+)/s; my $name = 'we--mode-modified'; my $selected_mode = $o->{wiki}->{input}->parameter ($name); unless ($selected_mode) { if ($magic =~ /C(?:on(?:fig|st)|SS)/) { $selected_mode = 'edit'; } else { $selected_mode = 'default'; } } my $SELECT = $p->{-parent}->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'select'); $SELECT->set_attribute (name => $name); __ATTRTEXT:%description__; $SELECT->set_attribute (title => $p->{description}) if length $p->{description}; for (qw/default read edit/) { for my $OPTION ($SELECT->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'option')) { $OPTION->set_attribute (value => $_); my $label = SuikaWiki::Plugin->module_package ('WikiResource') ->append_node (name => 'Edit:SaveAnd:'.$_.':Label', param => $o, wiki => $o->{wiki}); $OPTION->set_attribute (label => $label->inner_text); $OPTION->append_node ($label); $OPTION->set_attribute (title => SuikaWiki::Plugin->module_package ('WikiResource') ->append_node (name => 'Edit:SaveAnd:'.$_.':Description', param => $o, wiki => $o->{wiki})->inner_text); $OPTION->set_attribute (selected => 'selected') if $selected_mode eq $_; } } FormattingRule: @Category[list]:form-input @Name:we--update-lastmodified-datetime @Description: @@@: This direction force to update last-modified date-time of the WikiPage modified. \ Allowing user to select whether last-modified date-time should be updated, use another formatting rule provided by WikiFormSelection module, like: \ \ %check (id => we--touch, \ label => {%res (name => {Edit:UpdateTimeStamp});}p, \ description => {%res \ (name => {Edit:UpdateTimeStamp:Description});}p, \ default); \ WikiForm other that editing form might not be affected by this parameter. See also "we--touch" parameter. @@lang:en @Formatting: for ($p->{-parent}->append_new_node (type => '#element', namespace_uri => $NS_XHTML1, local_name => 'input')) { $_->set_attribute (type => 'hidden'); $_->set_attribute (name => 'we--touch'); $_->set_attribute (value => 'on'); $_->option (use_EmptyElemTag => 1); } Resource: @Edit:Conflict:Title: @@@:Confliction detected! @@lang:en @Edit:Conflict:WebPageTitle: @@@:%page-name; (Edit : 409 CONFLICTION) @@lang:en @Edit:Deleted:Description: @@@:Content of WikiPage %page-name; has been removed. @@lang:en @Edit:Deleted:Title: @@@:WikiPage Deleted @@lang:en @Edit:Deleted:WebPageTitle: @@@:%page-name; (Deleted) @@lang:en @Edit:Form:Lines:15 @Edit:Form:Size:18 @Edit:Save: @@@:Save @@lang:en @Edit:SaveAnd:Description: @@@: Select what to do after saving content @@lang: en @Edit:SaveAnd:default:Description: @@@: Save modified content and show content with your default view mode @@lang:en @Edit:SaveAnd:default:Label: @@@: Default view @@lang:en @Edit:SaveAnd:edit:Description: @@@: Save modified content and show content in the edit mode again @@lang:en @Edit:SaveAnd:edit:Label: @@@: Edit again @@lang:en @Edit:SaveAnd:read:Description: @@@: Save modified content and show content in the read mode @@lang:en @Edit:SaveAnd:read:Label: @@@: Show content @@lang:en @EditThisPage: @@@: Edit @@lang:en @EditThisPageLong: @@@: Edit this WikiPage @@lang:en @Edit:WebPageTitle: @@@: %page-name; (Modification) @@lang:en @Edit:Title: @@@: Modification of the WikiPage @@lang:en @Link:AdminEdit:Description: @@@: Edit this WikiPage (administrator's mode) @@lang:en @Link:Edit:Description: @@@: Edit this WikiPage @@lang:en @Mode:adminedit: @@@: Editing (for administrator) @@lang: en @Mode:edit: @@@: Editing @@lang: en Function: @Name:digest @Description: @@@: Compute digest of given string @@lang:en @Main: my (undef, $s, %opt) = @_; require Digest::SHA1; if ($opt{normalize}) { $s =~ s/\x0D\x0A/\x0A/g; $s =~ tr/\x0D/\x0A/; } Digest::SHA1::sha1_base64 ($s); Function: @Name:get_content @Description: @@@:Get WikiPage content to be modified. For new (currently not exist) WikiPage, new page template content is returned. @@lang:en @Main: my (undef, $o, $page) = @_; my $content; try { $content = $o->{wiki}->{db}->get (content => $page); unless (length $content) { ## TODO: use namespaced template page $content = $o->{wiki}->{db}->get (content => $o->{wiki}->{config}->{page}->{NewPageTemplate}); $o->{var}->{is_new_page_template} = 1; } } catch SuikaWiki::DB::Util::Error with { my $err = shift; $err->throw if $err->{-type} eq 'ERROR_REPORTED'; $o->{wiki}->view_in_mode (mode => '-wdb--fatal-error'); throw SuikaWiki::DB::Util::Error -type => 'ERROR_REPORTED'; }; $content; Parameter: @Name: we--content @Type: text @Default: "" @Description: @@@: Content of WikiPage (to be) @@lang:en Parameter: @Name: we--digest @Type: "Base64ed SHA1" @Default: {undef} @Description: @@@: SHA1 digest value of the WikiPage content before the modification. This value is used to detect confliction of modifying. Empty value (undef or length-zero-string) means that there were no WikiPage content. @@lang:en Parameter: @Name: we--mode-modified @Type: mode-name @Default: "default" @Description: @@@: WikiEngine running mode to be specified after modification is completed @@lang:en Parameter: @Name: we--touch @Type: form:checkbox-value @Default: "off" @Description: @@@: Whether last-modified date-time of the WikiPage should be updated or not when the WikiPage is modified. @@lang:en