package Silki::Schema::PageRevision; BEGIN { $Silki::Schema::PageRevision::VERSION = '0.26'; } use strict; use warnings; use namespace::autoclean; use Algorithm::Diff qw( sdiff ); use Encode qw( decode ); use List::AllUtils qw( all any ); use Markdent::CapturedEvents; use Markdent::Handler::CaptureEvents; use Markdent::Parser; use String::Diff qw( diff ); use Silki::Config; use Silki::Formatter::WikiToHTML; use Silki::Markdent::Handler::ExtractWikiLinks; use Silki::Schema; use Silki::Schema::Page; use Silki::Schema::PageLink; use Silki::Schema::PendingPageLink; use Silki::Types qw( Bool ); use Storable qw( nfreeze thaw ); use Text::TOC::HTML; use Fey::ORM::Table; use MooseX::ClassAttribute; use MooseX::Params::Validate qw( validated_list validated_hash ); with 'Silki::Role::Schema::URIMaker'; with 'Silki::Role::Schema::SystemLogger' => { methods => ['delete'] }; my $Schema = Silki::Schema->Schema(); has_policy 'Silki::Schema::Policy'; has_table( $Schema->table('PageRevision') ); has_one page => ( table => $Schema->table('Page'), handles => [qw( domain wiki wiki_id )], ); has_one( $Schema->table('User') ); transform content => deflate { return unless defined $_[1]; $_[1] =~ s/\r\n|\r/\n/g; return $_[1]; }; class_has _RenumberHigherRevisionsSQL => ( is => 'ro', isa => 'Fey::SQL::Update', lazy => 1, builder => '_BuildRenumberHigherRevisionsSQL', ); with 'Silki::Role::Schema::Serializes'; around insert => sub { my $orig = shift; my $class = shift; my $revision; my @args = @_; Silki::Schema->RunInTransaction( sub { $revision = $class->$orig(@args); $revision->_post_change(); } ); return $revision; }; around update => sub { my $orig = shift; my $self = shift; my @args = @_; Silki::Schema->RunInTransaction( sub { $self->$orig(@args); $self->_post_change(); } ); }; around delete => sub { my $orig = shift; my $self = shift; my %p = @_; Silki::Schema->RunInTransaction( sub { my $rev = $self->revision_number(); my $page = $self->page(); my $max_rev = $page->most_recent_revision()->revision_number(); my $is_most_recent = $rev == $max_rev; $self->$orig(%p); $page->_clear_most_recent_revision(); $self->_renumber_higher_revisions( $rev, $max_rev ); $page->_clear_revision_count(); if ( $page->revision_count() ) { $page->most_recent_revision()->_post_change() if $is_most_recent; } else { $page->delete( user => $p{user} ); } } ); }; sub _renumber_higher_revisions { my $self = shift; my $rev = shift; my $max_rev = shift; return if $rev == $max_rev; my $update = $self->_RenumberHigherRevisionsSQL(); my $dbh = Silki::Schema->DBIManager()->source_for_sql($update)->dbh(); for my $r ( $rev + 1 .. $max_rev ) { $dbh->do( $update->sql($dbh), {}, $self->page_id(), $r, ); } return; } sub _BuildRenumberHigherRevisionsSQL { my $class = shift; my $update = Silki::Schema->SQLFactoryClass()->new_update(); my $page_rev_t = $Schema->table('PageRevision'); my $minus_one = Fey::Literal::Term->new( $page_rev_t->column('revision_number'), ' - 1' ); #<<< $update ->update($page_rev_t) ->set( $page_rev_t->column('revision_number'), $minus_one ) ->where( $page_rev_t->column('page_id'), '=', Fey::Placeholder->new() ) ->and ( $page_rev_t->column('revision_number'), '=', Fey::Placeholder->new() ); #>>> return $update; } our $SkipPostChangeHack; sub _post_change { my $self = shift; return if $SkipPostChangeHack; my ( $existing, $pending, $capture ) = $self->_process_extracted_links(); my $delete_existing = Silki::Schema->SQLFactoryClass()->new_delete(); #<<< $delete_existing ->delete() ->from( $Schema->table('PageLink') ) ->where( $Schema->table('PageLink')->column('from_page_id'), '=', $self->page_id() ); #>>> my $delete_pending = Silki::Schema->SQLFactoryClass()->new_delete(); #<<< $delete_pending ->delete() ->from( $Schema->table('PendingPageLink') ) ->where( $Schema->table('PendingPageLink')->column('from_page_id'), '=', $self->page_id() ); #>>> my $update_cached_content = Silki::Schema->SQLFactoryClass()->new_update(); #<<< $update_cached_content ->update( $Schema->table('Page') ) ->set( $Schema->table('Page')->column('cached_content') => nfreeze( $capture->captured_events() ) ) ->where( $Schema->table('Page')->column('page_id'), '=', $self->page_id() ); #>>> my $dbh = Silki::Schema->DBIManager()->source_for_sql($delete_existing) ->dbh(); $dbh->do( $delete_existing->sql($dbh), {}, $delete_existing->bind_params() ); $dbh->do( $delete_pending->sql($dbh), {}, $delete_pending->bind_params() ); my $sth = $dbh->prepare( $update_cached_content->sql($dbh) ); my @bind = $update_cached_content->bind_params(); $sth->bind_param( 1, $bind[0], { pg_type => DBD::Pg::PG_BYTEA() } ); $sth->bind_param( 2, $bind[1] ); $sth->execute(); Silki::Schema::PageLink->insert_many( @{$existing} ) if @{$existing}; Silki::Schema::PendingPageLink->insert_many( @{$pending} ) if @{$pending}; } sub _process_extracted_links { my $self = shift; my $capture = Markdent::Handler::CaptureEvents->new(); my $linkex = Silki::Markdent::Handler::ExtractWikiLinks->new( page => $self->page(), wiki => $self->page()->wiki(), ); my $multi = Markdent::Handler::Multiplexer->new( handlers => [ $capture, $linkex ], ); my $filter = Markdent::Handler::HTMLFilter->new( handler => $multi ); my $parser = Markdent::Parser->new( dialect => 'Silki::Markdent::Dialect::Silki', handler => $filter, ); $parser->parse( markdown => $self->content() ); my $links = $linkex->links(); my @existing = map { { from_page_id => $self->page_id(), to_page_id => $links->{$_}{page}->page_id(), } } grep { $links->{$_}{page} } keys %{$links}; my @pending = map { { from_page_id => $self->page_id(), to_wiki_id => $links->{$_}{wiki}->wiki_id(), to_page_title => $links->{$_}{title}, } } grep { $links->{$_}{title} && !$links->{$_}{page} } keys %{$links}; return \@existing, \@pending, $capture; } sub _system_log_values_for_delete { my $self = shift; my $page = $self->page(); my $msg = 'Deleted revision ' . $self->revision_number() . ' of the ' . $page->title() . ' page, in wiki ' . $page->wiki()->title(); return ( wiki_id => $self->wiki_id(), page_id => $self->page_id(), message => $msg, data_blob => { revision_number => $self->revision_number(), content => $self->content(), user_id => $self->user_id(), creation_datetime => $self->creation_datetime_raw(), }, ); } sub _base_uri_path { my $self = shift; my $page = $self->page(); return $page->_base_uri_path() . '/revision/' . $self->revision_number(); } sub Diff { my $class = shift; my ( $rev1, $rev2 ) = validated_list( \@_, rev1 => { isa => 'Silki::Schema::PageRevision' }, rev2 => { isa => 'Silki::Schema::PageRevision' }, ); my @rev1 = map { s/^\s+|\s+$//; $_ } split /\n\n+/, $rev1->content(); my @rev2 = map { s/^\s+|\s+$//; $_ } split /\n\n+/, $rev2->content(); return $class->_BlockLevelDiff( \@rev1, \@rev2 ); } sub _BlockLevelDiff { my $class = shift; my $rev1 = shift; my $rev2 = shift; return $class->_ReorderIfTotalReplacement( [ sdiff( $rev1, $rev2, ) ] ); } # If the two revisions have nothing in common, we reorder the diff so all the # inserts come first and the deletes come second. This will show all new # content first, followed by all the removed old content. sub _ReorderIfTotalReplacement { my $class = shift; my $diff = shift; return $diff if any { $_->[0] =~ /[uc]/ } @{$diff}; return [ ( grep { $_->[0] eq q{+} } @{$diff} ), ( grep { $_->[0] eq q{-} } @{$diff} ), ]; } sub content_as_html { my $self = shift; my (%p) = validated_hash( \@_, user => { isa => 'Silki::Schema::User' }, include_toc => { isa => Bool, default => 0 }, for_editor => { isa => Bool, default => 0 }, ); my $page = $self->page(); my $formatter = Silki::Formatter::WikiToHTML->new( %p, page => $page, wiki => $self->wiki(), ); if ( $self->revision_number() == $page->most_recent_revision()->revision_number() ) { my $captured = thaw( $page->cached_content() ); return $formatter->captured_events_to_html($captured); } else { return $formatter->wiki_to_html( $self->content() ); } } __PACKAGE__->meta()->make_immutable(); 1; # ABSTRACT: Represents a page revision __END__ =pod =head1 NAME Silki::Schema::PageRevision - Represents a page revision =head1 VERSION version 0.26 =head1 AUTHOR Dave Rolsky <autarch@urth.org> =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2010 by Dave Rolsky. This is free software, licensed under: The GNU Affero General Public License, Version 3, November 2007 =cut