summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Migrate
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2015-07-09 22:18:55 -0700
committerIvan Kohler <ivan@freeside.biz>2015-07-09 22:18:55 -0700
commit1c538bfabc2cd31f27067505f0c3d1a46cba6ef0 (patch)
tree96922ad4459eda1e649327fd391d60c58d454c53 /rt/lib/RT/Migrate
parent4f5619288413a185e9933088d9dd8c5afbc55dfa (diff)
RT 4.2.11, ticket#13852
Diffstat (limited to 'rt/lib/RT/Migrate')
-rw-r--r--rt/lib/RT/Migrate/Importer.pm468
-rw-r--r--rt/lib/RT/Migrate/Importer/File.pm208
-rw-r--r--rt/lib/RT/Migrate/Incremental.pm657
-rw-r--r--rt/lib/RT/Migrate/Serializer.pm492
-rw-r--r--rt/lib/RT/Migrate/Serializer/File.pm171
-rw-r--r--rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm80
-rw-r--r--rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm69
7 files changed, 2145 insertions, 0 deletions
diff --git a/rt/lib/RT/Migrate/Importer.pm b/rt/lib/RT/Migrate/Importer.pm
new file mode 100644
index 0000000..58ee632
--- /dev/null
+++ b/rt/lib/RT/Migrate/Importer.pm
@@ -0,0 +1,468 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Importer;
+
+use strict;
+use warnings;
+
+use Storable qw//;
+use File::Spec;
+use Carp qw/carp/;
+
+sub new {
+ my $class = shift;
+ my $self = bless {}, $class;
+ $self->Init(@_);
+ return $self;
+}
+
+sub Init {
+ my $self = shift;
+ my %args = (
+ OriginalId => undef,
+ Progress => undef,
+ Statefile => undef,
+ DumpObjects => undef,
+ HandleError => undef,
+ @_,
+ );
+
+ # Should we attempt to preserve record IDs as they are created?
+ $self->{OriginalId} = $args{OriginalId};
+
+ $self->{Progress} = $args{Progress};
+
+ $self->{HandleError} = sub { 0 };
+ $self->{HandleError} = $args{HandleError}
+ if $args{HandleError} and ref $args{HandleError} eq 'CODE';
+
+ if ($args{DumpObjects}) {
+ require Data::Dumper;
+ $self->{DumpObjects} = { map { $_ => 1 } @{$args{DumpObjects}} };
+ }
+
+ # Objects we've created
+ $self->{UIDs} = {};
+
+ # Columns we need to update when an object is later created
+ $self->{Pending} = {};
+
+ # Objects missing from the source database before serialization
+ $self->{Invalid} = [];
+
+ # What we created
+ $self->{ObjectCount} = {};
+
+ # To know what global CFs need to be unglobal'd and applied to what
+ $self->{NewQueues} = [];
+ $self->{NewCFs} = [];
+}
+
+sub Metadata {
+ my $self = shift;
+ return $self->{Metadata};
+}
+
+sub LoadMetadata {
+ my $self = shift;
+ my ($data) = @_;
+
+ return if $self->{Metadata};
+ $self->{Metadata} = $data;
+
+ die "Incompatible format version: ".$data->{Format}
+ if $data->{Format} ne "0.8";
+
+ $self->{Organization} = $data->{Organization};
+ $self->{Clone} = $data->{Clone};
+ $self->{Incremental} = $data->{Incremental};
+ $self->{Files} = $data->{Files} if $data->{Final};
+}
+
+sub InitStream {
+ my $self = shift;
+
+ die "Stream initialized after objects have been recieved!"
+ if keys %{ $self->{UIDs} };
+
+ die "Cloning does not support importing the Original Id separately\n"
+ if $self->{OriginalId} and $self->{Clone};
+
+ die "RT already contains data; overwriting will not work\n"
+ if ($self->{Clone} and not $self->{Incremental})
+ and RT->SystemUser->Id;
+
+ # Basic facts of life, as a safety net
+ $self->Resolve( RT->System->UID => ref RT->System, RT->System->Id );
+ $self->SkipTransactions( RT->System->UID );
+
+ if ($self->{OriginalId}) {
+ # Where to shove the original ticket ID
+ my $cf = RT::CustomField->new( RT->SystemUser );
+ $cf->LoadByName( Name => $self->{OriginalId}, LookupType => RT::Ticket->CustomFieldLookupType, ObjectId => 0 );
+ unless ($cf->Id) {
+ warn "Failed to find global CF named $self->{OriginalId} -- creating one";
+ $cf->Create(
+ Queue => 0,
+ Name => $self->{OriginalId},
+ Type => 'FreeformSingle',
+ );
+ }
+ }
+}
+
+sub Resolve {
+ my $self = shift;
+ my ($uid, $class, $id) = @_;
+ $self->{UIDs}{$uid} = [ $class, $id ];
+ return unless $self->{Pending}{$uid};
+
+ for my $ref (@{$self->{Pending}{$uid}}) {
+ my ($pclass, $pid) = @{ $self->Lookup( $ref->{uid} ) };
+ my $obj = $pclass->new( RT->SystemUser );
+ $obj->LoadByCols( Id => $pid );
+ $obj->__Set(
+ Field => $ref->{column},
+ Value => $id,
+ ) if defined $ref->{column};
+ $obj->__Set(
+ Field => $ref->{classcolumn},
+ Value => $class,
+ ) if defined $ref->{classcolumn};
+ $obj->__Set(
+ Field => $ref->{uri},
+ Value => $self->LookupObj($uid)->URI,
+ ) if defined $ref->{uri};
+ }
+ delete $self->{Pending}{$uid};
+}
+
+sub Lookup {
+ my $self = shift;
+ my ($uid) = @_;
+ unless (defined $uid) {
+ carp "Tried to lookup an undefined UID";
+ return;
+ }
+ return $self->{UIDs}{$uid};
+}
+
+sub LookupObj {
+ my $self = shift;
+ my ($uid) = @_;
+ my $ref = $self->Lookup( $uid );
+ return unless $ref;
+ my ($class, $id) = @{ $ref };
+
+ my $obj = $class->new( RT->SystemUser );
+ $obj->Load( $id );
+ return $obj;
+}
+
+sub Postpone {
+ my $self = shift;
+ my %args = (
+ for => undef,
+ uid => undef,
+ column => undef,
+ classcolumn => undef,
+ uri => undef,
+ @_,
+ );
+ my $uid = delete $args{for};
+
+ if (defined $uid) {
+ push @{$self->{Pending}{$uid}}, \%args;
+ } else {
+ push @{$self->{Invalid}}, \%args;
+ }
+}
+
+sub SkipTransactions {
+ my $self = shift;
+ my ($uid) = @_;
+ return if $self->{Clone};
+ $self->{SkipTransactions}{$uid} = 1;
+}
+
+sub ShouldSkipTransaction {
+ my $self = shift;
+ my ($uid) = @_;
+ return exists $self->{SkipTransactions}{$uid};
+}
+
+sub MergeValues {
+ my $self = shift;
+ my ($obj, $data) = @_;
+ for my $col (keys %{$data}) {
+ next if defined $obj->__Value($col) and length $obj->__Value($col);
+ next unless defined $data->{$col} and length $data->{$col};
+
+ if (ref $data->{$col}) {
+ my $uid = ${ $data->{$col} };
+ my $ref = $self->Lookup( $uid );
+ if ($ref) {
+ $data->{$col} = $ref->[1];
+ } else {
+ $self->Postpone(
+ for => $obj->UID,
+ uid => $uid,
+ column => $col,
+ );
+ next;
+ }
+ }
+ $obj->__Set( Field => $col, Value => $data->{$col} );
+ }
+}
+
+sub SkipBy {
+ my $self = shift;
+ my ($column, $class, $uid, $data) = @_;
+
+ my $obj = $class->new( RT->SystemUser );
+ $obj->Load( $data->{$column} );
+ return unless $obj->Id;
+
+ $self->SkipTransactions( $uid );
+
+ $self->Resolve( $uid => $class => $obj->Id );
+ return $obj;
+}
+
+sub MergeBy {
+ my $self = shift;
+ my ($column, $class, $uid, $data) = @_;
+
+ my $obj = $self->SkipBy(@_);
+ return unless $obj;
+ $self->MergeValues( $obj, $data );
+ return 1;
+}
+
+sub Qualify {
+ my $self = shift;
+ my ($string) = @_;
+ return $string if $self->{Clone};
+ return $string if not defined $self->{Organization};
+ return $string if $self->{Organization} eq $RT::Organization;
+ return $self->{Organization}.": $string";
+}
+
+sub Create {
+ my $self = shift;
+ my ($class, $uid, $data) = @_;
+
+ # Use a simpler pre-inflation if we're cloning
+ if ($self->{Clone}) {
+ $class->RT::Record::PreInflate( $self, $uid, $data );
+ } else {
+ # Non-cloning always wants to make its own id
+ delete $data->{id};
+ return unless $class->PreInflate( $self, $uid, $data );
+ }
+
+ my $obj = $class->new( RT->SystemUser );
+ my ($id, $msg) = eval {
+ # catch and rethrow on the outside so we can provide more info
+ local $SIG{__DIE__};
+ $obj->DBIx::SearchBuilder::Record::Create(
+ %{$data}
+ );
+ };
+ if (not $id or $@) {
+ $msg ||= ''; # avoid undef
+ my $err = "Failed to create $uid: $msg $@\n" . Data::Dumper::Dumper($data) . "\n";
+ if (not $self->{HandleError}->($self, $err)) {
+ die $err;
+ } else {
+ return;
+ }
+ }
+
+ $self->{ObjectCount}{$class}++;
+ $self->Resolve( $uid => $class, $id );
+
+ # Load it back to get real values into the columns
+ $obj = $class->new( RT->SystemUser );
+ $obj->Load( $id );
+ $obj->PostInflate( $self );
+
+ return $obj;
+}
+
+sub ReadStream {
+ my $self = shift;
+ my ($fh) = @_;
+
+ no warnings 'redefine';
+ local *RT::Ticket::Load = sub {
+ my $self = shift;
+ my $id = shift;
+ $self->LoadById( $id );
+ return $self->Id;
+ };
+
+ my $loaded = Storable::fd_retrieve($fh);
+
+ # Metadata is stored at the start of the stream as a hashref
+ if (ref $loaded eq "HASH") {
+ $self->LoadMetadata( $loaded );
+ $self->InitStream;
+ return;
+ }
+
+ my ($class, $uid, $data) = @{$loaded};
+
+ if ($self->{Incremental}) {
+ my $obj = $class->new( RT->SystemUser );
+ $obj->Load( $data->{id} );
+ if (not $uid) {
+ # undef $uid means "delete it"
+ $obj->Delete;
+ $self->{ObjectCount}{$class}++;
+ } elsif ( $obj->Id ) {
+ # If it exists, update it
+ $class->RT::Record::PreInflate( $self, $uid, $data );
+ $obj->__Set( Field => $_, Value => $data->{$_} )
+ for keys %{ $data };
+ $self->{ObjectCount}{$class}++;
+ } else {
+ # Otherwise, make it
+ $obj = $self->Create( $class, $uid, $data );
+ }
+ $self->{Progress}->($obj) if $obj and $self->{Progress};
+ return;
+ } elsif ($self->{Clone}) {
+ my $obj = $self->Create( $class, $uid, $data );
+ $self->{Progress}->($obj) if $obj and $self->{Progress};
+ return;
+ }
+
+ # If it's a queue, store its ID away, as we'll need to know
+ # it to split global CFs into non-global across those
+ # fields. We do this before inflating, so that queues which
+ # got merged still get the CFs applied
+ push @{$self->{NewQueues}}, $uid
+ if $class eq "RT::Queue";
+
+ my $origid = $data->{id};
+ my $obj = $self->Create( $class, $uid, $data );
+ return unless $obj;
+
+ # If it's a ticket, we might need to create a
+ # TicketCustomField for the previous ID
+ if ($class eq "RT::Ticket" and $self->{OriginalId}) {
+ my ($id, $msg) = $obj->AddCustomFieldValue(
+ Field => $self->{OriginalId},
+ Value => $self->Organization . ":$origid",
+ RecordTransaction => 0,
+ );
+ warn "Failed to add custom field to $uid: $msg"
+ unless $id;
+ }
+
+ # If it's a CF, we don't know yet if it's global (the OCF
+ # hasn't been created yet) to store away the CF for later
+ # inspection
+ push @{$self->{NewCFs}}, $uid
+ if $class eq "RT::CustomField"
+ and $obj->LookupType =~ /^RT::Queue/;
+
+ $self->{Progress}->($obj) if $self->{Progress};
+}
+
+sub CloseStream {
+ my $self = shift;
+
+ $self->{Progress}->(undef, 'force') if $self->{Progress};
+
+ return if $self->{Clone};
+
+ # Take global CFs which we made and make them un-global
+ my @queues = grep {$_} map {$self->LookupObj( $_ )} @{$self->{NewQueues}};
+ for my $obj (map {$self->LookupObj( $_ )} @{$self->{NewCFs}}) {
+ my $ocf = $obj->IsGlobal or next;
+ $ocf->Delete;
+ $obj->AddToObject( $_ ) for @queues;
+ }
+ $self->{NewQueues} = [];
+ $self->{NewCFs} = [];
+}
+
+
+sub ObjectCount {
+ my $self = shift;
+ return %{ $self->{ObjectCount} };
+}
+
+sub Missing {
+ my $self = shift;
+ return wantarray ? sort keys %{ $self->{Pending} }
+ : keys %{ $self->{Pending} };
+}
+
+sub Invalid {
+ my $self = shift;
+ return wantarray ? sort { $a->{uid} cmp $b->{uid} } @{ $self->{Invalid} }
+ : $self->{Invalid};
+}
+
+sub Organization {
+ my $self = shift;
+ return $self->{Organization};
+}
+
+sub Progress {
+ my $self = shift;
+ return defined $self->{Progress} unless @_;
+ return $self->{Progress} = $_[0];
+}
+
+1;
diff --git a/rt/lib/RT/Migrate/Importer/File.pm b/rt/lib/RT/Migrate/Importer/File.pm
new file mode 100644
index 0000000..176bc26
--- /dev/null
+++ b/rt/lib/RT/Migrate/Importer/File.pm
@@ -0,0 +1,208 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Importer::File;
+
+use strict;
+use warnings;
+use base qw(RT::Migrate::Importer);
+
+sub Init {
+ my $self = shift;
+ my %args = (
+ Directory => undef,
+ Resume => undef,
+ @_
+ );
+
+ # Directory is required
+ die "Directory is required" unless $args{Directory};
+ die "Invalid path $args{Directory}" unless -d $args{Directory};
+ $self->{Directory} = $args{Directory};
+
+ # Load metadata, if present
+ if (-e "$args{Directory}/rt-serialized") {
+ my $dat = eval { Storable::retrieve("$args{Directory}/rt-serialized"); }
+ or die "Failed to load metadata" . ($@ ? ": $@" : "");
+ $self->LoadMetadata($dat);
+ }
+
+ # Support resuming
+ $self->{Statefile} = $args{Statefile} || "$args{Directory}/partial-import";
+ unlink $self->{Statefile}
+ if -f $self->{Statefile} and not $args{Resume};
+
+ return $self->SUPER::Init(@_);
+}
+
+sub Import {
+ my $self = shift;
+ my $dir = $self->{Directory};
+
+ if ($self->{Metadata} and $self->{Metadata}{Files}) {
+ $self->{Files} = [ map {s|^.*/|$dir/|;$_} @{$self->{Metadata}{Files}} ];
+ } else {
+ $self->{Files} = [ <$dir/*.dat> ];
+ }
+ $self->{Files} = [ map {File::Spec->rel2abs($_)} @{ $self->{Files} } ];
+
+ $self->RestoreState( $self->{Statefile} );
+
+ local $SIG{ INT } = sub { $self->{INT} = 1 };
+ local $SIG{__DIE__} = sub { warn "\n", @_; $self->SaveState; exit 1 };
+
+ $self->{Progress}->(undef) if $self->{Progress};
+ while (@{$self->{Files}}) {
+ $self->{Filename} = shift @{$self->{Files}};
+ open(my $fh, "<", $self->{Filename})
+ or die "Can't read $self->{Filename}: $!";
+ if ($self->{Seek}) {
+ seek($fh, $self->{Seek}, 0)
+ or die "Can't seek to $self->{Seek} in $self->{Filename}";
+ $self->{Seek} = undef;
+ }
+ while (not eof($fh)) {
+ $self->{Position} = tell($fh);
+
+ # Stop when we're at a good stopping point
+ die "Caught interrupt, quitting.\n" if $self->{INT};
+
+ $self->ReadStream( $fh );
+ }
+ }
+
+ $self->CloseStream;
+
+ # Return creation counts
+ return $self->ObjectCount;
+}
+
+sub List {
+ my $self = shift;
+ my $dir = $self->{Directory};
+
+ my %found = ( "RT::System" => 1 );
+ my @files = ($self->{Metadata} and $self->{Metadata}{Files}) ?
+ @{ $self->{Metadata}{Files} } : <$dir/*.dat>;
+ @files = map {File::Spec->rel2abs($_)} @files;
+
+ for my $filename (@files) {
+ open(my $fh, "<", $filename)
+ or die "Can't read $filename: $!";
+ while (not eof($fh)) {
+ my $loaded = Storable::fd_retrieve($fh);
+ if (ref $loaded eq "HASH") {
+ $self->LoadMetadata( $loaded );
+ next;
+ }
+
+ if ($self->{DumpObjects}) {
+ print STDERR Data::Dumper::Dumper($loaded), "\n"
+ if $self->{DumpObjects}{ $loaded->[0] };
+ }
+
+ my ($class, $uid, $data) = @{$loaded};
+ $self->{ObjectCount}{$class}++;
+ $found{$uid} = 1;
+ delete $self->{Pending}{$uid};
+ for (grep {ref $data->{$_}} keys %{$data}) {
+ my $uid_ref = ${ $data->{$_} };
+ unless (defined $uid_ref) {
+ push @{ $self->{Invalid} }, { uid => $uid, column => $_ };
+ next;
+ }
+ next if $found{$uid_ref};
+ next if $uid_ref =~ /^RT::Principal-/;
+ push @{$self->{Pending}{$uid_ref} ||= []}, {uid => $uid};
+ }
+ }
+ }
+
+ return $self->ObjectCount;
+}
+
+sub RestoreState {
+ my $self = shift;
+ my ($statefile) = @_;
+ return unless $statefile && -f $statefile;
+
+ my $state = Storable::retrieve( $self->{Statefile} );
+ $self->{$_} = $state->{$_} for keys %{$state};
+ unlink $self->{Statefile};
+
+ print STDERR "Resuming partial import...\n";
+ sleep 2;
+ return 1;
+}
+
+sub SaveState {
+ my $self = shift;
+
+ my %data;
+ unshift @{$self->{Files}}, $self->{Filename};
+ $self->{Seek} = $self->{Position};
+ $data{$_} = $self->{$_} for
+ qw/Filename Seek Position Files
+ Organization ObjectCount
+ NewQueues NewCFs
+ SkipTransactions Pending Invalid
+ UIDs
+ OriginalId Clone
+ /;
+ Storable::nstore(\%data, $self->{Statefile});
+
+ print STDERR <<EOT;
+
+Importer state has been written to the file:
+ $self->{Statefile}
+
+It may be possible to resume the import by re-running rt-importer.
+EOT
+}
+
+1;
diff --git a/rt/lib/RT/Migrate/Incremental.pm b/rt/lib/RT/Migrate/Incremental.pm
new file mode 100644
index 0000000..61aea6c
--- /dev/null
+++ b/rt/lib/RT/Migrate/Incremental.pm
@@ -0,0 +1,657 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Incremental;
+
+use strict;
+use warnings;
+require Storable;
+require MIME::Base64;
+
+our %UPGRADES = (
+ '3.3.0' => {
+ 'RT::Transaction' => sub {
+ my ($ref) = @_;
+ $ref->{ObjectType} = 'RT::Ticket';
+ $ref->{ObjectId} = delete $ref->{Ticket};
+ delete $ref->{EffectiveTicket};
+ },
+ 'RT::TicketCustomFieldValue' => sub {
+ my ($ref, $classref) = @_;
+ $$classref = "RT::ObjectCustomFieldValue";
+ $ref->{ObjectType} = 'RT::Ticket';
+ $ref->{ObjectId} = delete $ref->{Ticket};
+ },
+ '-RT::TicketCustomFieldValue' => sub {
+ my ($ref, $classref) = @_;
+ $$classref = "RT::ObjectCustomFieldValue";
+ },
+ 'RT::CustomField' => sub {
+ my ($ref) = @_;
+ $ref->{MaxValues} = 0 if $ref->{Type} =~ /Multiple$/;
+ $ref->{MaxValues} = 1 if $ref->{Type} =~ /Single$/;
+ $ref->{Type} = 'Select' if $ref->{Type} =~ /^Select/;
+ $ref->{Type} = 'Freeform' if $ref->{Type} =~ /^Freeform/;
+ $ref->{LookupType} = 'RT::Queue-RT::Ticket';
+ delete $ref->{Queue};
+ },
+ '+RT::CustomField' => sub {
+ my ($ref) = @_;
+ return [
+ "RT::ObjectCustomField" => rand(1),
+ {
+ id => undef,
+ CustomField => $ref->{id},
+ ObjectId => $ref->{Queue},
+ SortOrder => $ref->{SortOrder},
+ Creator => $ref->{Creator},
+ LastUpdatedBy => $ref->{LastUpdatedBy},
+ }
+ ];
+ }
+ },
+
+ '3.3.11' => {
+ 'RT::ObjectCustomFieldValue' => sub {
+ my ($ref) = @_;
+ $ref->{Disabled} = not delete $ref->{Current};
+ },
+ },
+
+ '3.7.19' => {
+ 'RT::Scrip' => sub {
+ my ($ref) = @_;
+ return if defined $ref->{Description} and length $ref->{Description};
+
+ my $scrip = RT::Scrip->new( $RT::SystemUser );
+ $scrip->Load( $ref->{id} );
+ my $condition = $scrip->ConditionObj->Name
+ || $scrip->ConditionObj->Description
+ || ('On Condition #'. $scrip->Condition);
+ my $action = $scrip->ActionObj->Name
+ || $scrip->ActionObj->Description
+ || ('Run Action #'. $scrip->Action);
+ $ref->{Description} = join ' ', $condition, $action;
+ },
+ },
+
+ # XXX BrandedQueues
+ # XXX iCal
+
+ '3.8.2' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Queue};
+
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ $queue->Load( $ref->{Queue} );
+ return unless $queue->Id and $queue->Name eq "___Approvals";
+
+ $ref->{Name} = "[OLD] ".$ref->{Name};
+ },
+ 'RT::Attribute' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq "Dashboard";
+
+ my $v = eval {
+ Storable::thaw(MIME::Base64::decode_base64($ref->{Content}))
+ };
+ return unless $v and exists $v->{Searches};
+ $v->{Panes} = {
+ body => [
+ map {
+ my ($privacy, $id, $desc) = @$_;
+ +{
+ portlet_type => 'search',
+ privacy => $privacy,
+ id => $id,
+ description => $desc,
+ pane => 'body',
+ }
+ } @{ delete $v->{Searches} }
+ ],
+ };
+ $ref->{Content} = MIME::Base64::encode_base64(
+ Storable::nfreeze($v) );
+ },
+ 'RT::Scrip' => sub {
+ my ($ref, $classref) = @_;
+ return unless $ref->{Queue};
+
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ $queue->Load( $ref->{Queue} );
+ return unless $queue->Id and $queue->Name eq "___Approvals";
+
+ $$classref = undef;
+ },
+ },
+
+ '3.8.3' => {
+ 'RT::ScripAction' => sub {
+ my ($ref) = @_;
+ return unless ($ref->{Argument}||"") eq "All";
+ if ($ref->{ExecModule} eq "Notify") {
+ $ref->{Name} = 'Notify Owner, Requestors, Ccs and AdminCcs';
+ $ref->{Description} = 'Send mail to owner and all watchers';
+ } elsif ($ref->{ExecModule} eq "NotifyAsComment") {
+ $ref->{Name} = 'Notify Owner, Requestors, Ccs and AdminCcs as Comment';
+ $ref->{Description} = 'Send mail to owner and all watchers as a "comment"';
+ }
+ },
+ },
+
+ '3.8.4' => {
+ 'RT::ScripAction' => sub {
+ my ($ref) = @_;
+ return unless $ref->{ExecModule} eq "NotifyGroup"
+ or $ref->{ExecModule} eq "NotifyGroupAsComment";
+
+ my $argument = $ref->{Argument};
+ if ( my $struct = eval { Storable::thaw( $argument ) } ) {
+ my @res;
+ foreach my $r ( @{ $struct } ) {
+ my $obj;
+ next unless $r->{'Type'};
+ if( lc $r->{'Type'} eq 'user' ) {
+ $obj = RT::User->new( $RT::SystemUser );
+ } elsif ( lc $r->{'Type'} eq 'group' ) {
+ $obj = RT::Group->new( $RT::SystemUser );
+ } else {
+ next;
+ }
+ $obj->Load( $r->{'Instance'} );
+ next unless $obj->id ;
+
+ push @res, $obj->id;
+ }
+ $ref->{Argument} = join ",", @res;
+ } else {
+ $ref->{Argument} = join ",", grep length, split /[^0-9]+/, $argument;
+ }
+ },
+ },
+
+ '3.8.8' => {
+ 'RT::ObjectCustomField' => sub {
+ # XXX Removing OCFs applied both global and non-global
+ # XXX Fixing SortOrder on OCFs
+ },
+ },
+
+ '3.8.9' => {
+ 'RT::Link' => sub {
+ my ($ref) = @_;
+ my $prefix = RT::URI::fsck_com_rt->LocalURIPrefix . '/ticket/';
+ for my $dir (qw(Target Base)) {
+ next unless $ref->{$dir} =~ /^$prefix(.*)/;
+ next unless int($1) eq $1;
+ next if $ref->{'Local'.$dir};
+ $ref->{'Local'.$dir} = $1;
+ }
+ },
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+
+ return unless $ref->{Name} =~
+ /^(All Approvals Passed|Approval Passed|Approval Rejected)$/;
+
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ $queue->Load( $ref->{Queue} );
+ return unless $queue->Id and $queue->Name eq "___Approvals";
+
+ $ref->{Content} =~
+s!(?<=Your ticket has been (?:approved|rejected) by \{ eval \{ )\$Approval->OwnerObj->Name!\$Approver->Name!;
+ },
+ },
+
+ '3.9.1' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ $ref->{Type} = 'Perl';
+ },
+ # XXX: Add ExecuteCode to principals that currently have ModifyTemplate or ModifyScrips
+ },
+
+ '3.9.2' => {
+ 'RT::ACE' => sub {
+ my ($ref, $classref) = @_;
+ $$classref = undef if $ref->{DelegatedBy} > 0
+ or $ref->{DelegatedFrom} > 0;
+ },
+
+ 'RT::GroupMember' => sub {
+ my ($ref, $classref) = @_;
+ my $group = RT::Group->new( $RT::SystemUser );
+ $group->Load( $ref->{GroupId} );
+ $$classref = undef if $group->Domain eq "Personal";
+ },
+ 'RT::Group' => sub {
+ my ($ref, $classref) = @_;
+ $$classref = undef if $ref->{Domain} eq "Personal";
+ },
+ 'RT::Principal' => sub {
+ my ($ref, $classref) = @_;
+ return unless $ref->{PrincipalType} eq "Group";
+ my $group = RT::Group->new( $RT::SystemUser );
+ $group->Load( $ref->{ObjectId} );
+ $$classref = undef if $group->Domain eq "Personal";
+ },
+ },
+
+ '3.9.3' => {
+ 'RT::ACE' => sub {
+ my ($ref) = @_;
+ delete $ref->{DelegatedBy};
+ delete $ref->{DelegatedFrom};
+ },
+ },
+
+ '3.9.5' => {
+ 'RT::CustomFieldValue' => sub {
+ my ($ref) = @_;
+ my $attr = RT::Attribute->new( $RT::SystemUser );
+ $attr->LoadByCols(
+ ObjectType => "RT::CustomFieldValue",
+ ObjectId => $ref->{Id},
+ Name => "Category",
+ );
+ $ref->{Category} = $attr->Content if $attr->id;
+ },
+ 'RT::Attribute' => sub {
+ my ($ref, $classref) = @_;
+ $$classref = undef if $ref->{Name} eq "Category"
+ and $ref->{ObjectType} eq "RT::CustomFieldValue";
+ },
+ },
+
+ '3.9.7' => {
+ 'RT::User' => sub {
+ my ($ref) = @_;
+ my $attr = RT::Attribute->new( $RT::SystemUser );
+ $attr->LoadByCols(
+ ObjectType => "RT::User",
+ ObjectId => $ref->{id},
+ Name => "AuthToken",
+ );
+ $ref->{AuthToken} = $attr->Content if $attr->id;
+ },
+ 'RT::CustomField' => sub {
+ my ($ref) = @_;
+ for my $name (qw/RenderType BasedOn ValuesClass/) {
+ my $attr = RT::Attribute->new( $RT::SystemUser );
+ $attr->LoadByCols(
+ ObjectType => "RT::CustomField",
+ ObjectId => $ref->{id},
+ Name => $name,
+ );
+ $ref->{$name} = $attr->Content if $attr->id;
+ }
+ },
+ 'RT::Queue' => sub {
+ my ($ref) = @_;
+ my $attr = RT::Attribute->new(
+ ObjectType => "RT::System",
+ ObjectId => 1,
+ Name => "BrandedSubjectTag",
+ );;
+ return unless $attr->id;
+ my $map = $attr->Content || {};
+ return unless $map->{$ref->{id}};
+ $ref->{SubjectTag} = $map->{$ref->{id}};
+ },
+ 'RT::Attribute' => sub {
+ my ($ref, $classref) = @_;
+ if ($ref->{ObjectType} eq "RT::User" and $ref->{Name} eq "AuthToken") {
+ $$classref = undef;
+ } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "RenderType") {
+ $$classref = undef;
+ } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "BasedOn") {
+ $$classref = undef;
+ } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "ValuesClass") {
+ $$classref = undef;
+ } elsif ($ref->{ObjectType} eq "RT::System" and $ref->{Name} eq "BrandedSubjectTag") {
+ $$classref = undef;
+ }
+ },
+ },
+
+ '3.9.8' => {
+ # XXX RTFM => Articles
+ },
+
+ '4.0.0rc7' => {
+ 'RT::Queue' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq '___Approvals';
+ $ref->{Lifecycle} = "approvals";
+ },
+ },
+
+ '4.0.1' => {
+ 'RT::ACE' => sub {
+ my ($ref, $classref) = @_;
+ my $group = RT::Group->new( $RT::SystemUser );
+ $group->LoadByCols(
+ id => $ref->{PrincipalId},
+ Domain => "Personal",
+ );
+ $$classref = undef if $group->id;
+ $$classref = undef if $ref->{RightName} =~
+ /^(AdminOwnPersonalGroups|AdminAllPersonalGroups|DelegateRights)$/;
+ $$classref = undef if $ref->{RightName} =~
+ /^(RejectTicket|ModifyTicketStatus)$/;
+ },
+ },
+
+ '4.0.4' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ $ref->{Type} ||= 'Perl';
+ },
+ },
+
+ '4.0.6' => {
+ 'RT::Transaction' => sub {
+ my ($ref) = @_;
+ return unless $ref->{ObjectType} eq "RT::User" and $ref->{Field} eq "Password";
+ $ref->{OldValue} = $ref->{NewValue} = '********';
+ },
+ },
+
+ '4.0.9' => {
+ 'RT::Queue' => sub {
+ my ($ref) = @_;
+ $ref->{Lifecycle} ||= 'default';
+ },
+ },
+
+ '4.0.19' => {
+ 'RT::CustomField' => sub {
+ my ($ref) = @_;
+ $ref->{LookupType} = 'RT::Class-RT::Article'
+ if $ref->{LookupType} eq 'RT::FM::Class-RT::FM::Article';
+ },
+ 'RT::ObjectCustomFieldValue' => sub {
+ my ($ref) = @_;
+ $ref->{ObjectType} = 'RT::Article'
+ if $ref->{ObjectType} eq 'RT::FM::Article';
+ },
+ },
+
+
+ '4.1.0' => {
+ 'RT::Attribute' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq "HomepageSettings";
+
+ my $v = eval {
+ Storable::thaw(MIME::Base64::decode_base64($ref->{Content}))
+ };
+ return if not $v or $v->{sidebar};
+ $v->{sidebar} = delete $v->{summary};
+ $ref->{Content} = MIME::Base64::encode_base64(
+ Storable::nfreeze($v) );
+ },
+ },
+
+ '4.1.1' => {
+ '+RT::Scrip' => sub {
+ my ($ref) = @_;
+ my $new = [
+ "RT::ObjectScrip" => rand(1),
+ {
+ id => undef,
+ Scrip => $ref->{id},
+ Stage => delete $ref->{Stage},
+ ObjectId => delete $ref->{Queue},
+ Creator => $ref->{Creator},
+ Created => $ref->{Created},
+ LastUpdatedBy => $ref->{LastUpdatedBy},
+ LastUpdated => $ref->{LastUpdated},
+ }
+ ];
+ if ( $new->[2]{Stage} eq "Disabled" ) {
+ $ref->{Disabled} = 1;
+ $new->[2]{Stage} = "TransactionCreate";
+ } else {
+ $ref->{Disabled} = 0;
+ }
+ # XXX SortOrder
+ return $new;
+ },
+ },
+
+ '4.1.4' => {
+ 'RT::Group' => sub {
+ my ($ref) = @_;
+ $ref->{Instance} = 1
+ if $ref->{Domain} eq "RT::System-Role"
+ and $ref->{Instance} = 0;
+ },
+ # XXX Invalid rights
+ },
+
+ '4.1.5' => {
+ 'RT::Scrip' => sub {
+ my ($ref) = @_;
+ my $template = RT::Template->new( $RT::SystemUser );
+ $template->Load( $ref->{Template} );
+ $ref->{Template} = $template->id ? $template->Name : 'Blank';
+ },
+ },
+
+ '4.1.6' => {
+ 'RT::Attribute' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq RT::User::_PrefName( RT->System )
+ and $ref->{ObjectType} eq "RT::User";
+ my $v = eval {
+ Storable::thaw(MIME::Base64::decode_base64($ref->{Content}))
+ };
+ return if not $v or $v->{ShowHistory};
+ $v->{ShowHistory} = delete $v->{DeferTransactionLoading}
+ ? "click" : "delay";
+ $ref->{Content} = MIME::Base64::encode_base64(
+ Storable::nfreeze($v) );
+ },
+ },
+
+ '4.1.7' => {
+ 'RT::Transaction' => sub {
+ my ($ref) = @_;
+ return unless $ref->{ObjectType} eq 'RT::Ticket'
+ and $ref->{Type} eq 'Set'
+ and $ref->{Field} eq 'TimeWorked';
+ $ref->{TimeTaken} = $ref->{NewValue} - $ref->{OldValue};
+ },
+ },
+
+ '4.1.8' => {
+ 'RT::Ticket' => sub {
+ my ($ref) = @_;
+ $ref->{IsMerged} = 1 if $ref->{id} != $ref->{EffectiveId};
+ },
+ },
+
+ '4.1.10' => {
+ 'RT::ObjectcustomFieldValue' => sub {
+ my ($ref) = @_;
+ $ref->{Content} = undef if defined $ref->{LargeContent}
+ and defined $ref->{Content} and $ref->{Content} eq '';
+ },
+ },
+
+ '4.1.11' => {
+ 'RT::CustomField' => sub {
+ my ($ref) = @_;
+ delete $ref->{Repeated};
+ },
+ },
+
+ '4.1.13' => {
+ 'RT::Group' => sub {
+ my ($ref) = @_;
+ $ref->{Name} = $ref->{Type}
+ if $ref->{Domain} =~ /^(ACLEquivalence|SystemInternal|.*-Role)$/;
+ },
+ },
+
+ '4.1.14' => {
+ 'RT::Scrip' => sub {
+ my ($ref) = @_;
+ delete $ref->{ConditionRules};
+ delete $ref->{ActionRules};
+ },
+ },
+
+ '4.1.17' => {
+ 'RT::Attribute' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq 'SavedSearch';
+ my $v = eval {
+ Storable::thaw(MIME::Base64::decode_base64($ref->{Content}))
+ };
+ return unless $v and ref $v and ($v->{SearchType}||'') eq 'Chart';
+
+ # Switch from PrimaryGroupBy to GroupBy name
+ # Switch from "CreatedMonthly" to "Created.Monthly"
+ $v->{GroupBy} ||= [delete $v->{PrimaryGroupBy}];
+ for (@{$v->{GroupBy}}) {
+ next if /\./;
+ s/(?<=[a-z])(?=[A-Z])/./;
+ }
+ $ref->{Content} = MIME::Base64::encode_base64(
+ Storable::nfreeze($v) );
+ },
+ },
+
+ '4.1.19' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ delete $ref->{Language};
+ delete $ref->{TranslationOf};
+ },
+ },
+
+ '4.1.20' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ if ($ref->{Name} eq 'Forward') {
+ $ref->{Description} = 'Forwarded message';
+ if ( $ref->{Content} =~
+ m/^\n*This is (a )?forward of transaction #\{\s*\$Transaction->id\s*\} of (a )?ticket #\{\s*\$Ticket->id\s*\}\n*$/
+ ) {
+ $ref->{Content} = q{
+{ $ForwardTransaction->Content =~ /\S/ ? $ForwardTransaction->Content : "This is a forward of transaction #".$Transaction->id." of ticket #". $Ticket->id }
+};
+ } else {
+ RT->Logger->error('Current "Forward" template is not the default version, please check docs/UPGRADING-4.2');
+ }
+ } elsif ($ref->{Name} eq 'Forward Ticket') {
+ $ref->{Description} = 'Forwarded ticket message';
+ if ( $ref->{Content} eq q{
+
+This is a forward of ticket #{ $Ticket->id }
+} ) {
+ $ref->{Content} = q{
+{ $ForwardTransaction->Content =~ /\S/ ? $ForwardTransaction->Content : "This is a forward of ticket #". $Ticket->id }
+};
+ } else {
+ RT->Logger->error('Current "Forward Ticket" template is not the default version, please check docs/UPGRADING-4.2');
+ }
+ }
+ },
+ },
+
+ '4.1.21' => {
+ # XXX User dashboards
+ },
+
+ '4.1.22' => {
+ 'RT::Template' => sub {
+ my ($ref) = @_;
+ return unless $ref->{Name} eq 'Error: bad GnuPG data';
+ $ref->{Name} = 'Error: bad encrypted data';
+ $ref->{Description} =
+ 'Inform user that a message he sent has invalid encryption data';
+ $ref->{Content} =~ s/GnuPG signature/signature/g;
+ },
+ # XXX SMIME keys
+ 'RT::Attribute' => sub {
+ my ($ref, $classref) = @_;
+ if ($ref->{ObjectType} eq "RT::User" and $ref->{Name} eq "SMIMEKeyNotAfter") {
+ $$classref = undef;
+ }
+ },
+ },
+
+ '4.2.1' => {
+ 'RT::Attribute' => sub {
+ my ($ref, $classref) = @_;
+ if ($ref->{ObjectType} eq "RT::System" and $ref->{Name} eq "BrandedSubjectTag") {
+ $$classref = undef;
+ }
+ },
+ },
+
+ '4.2.2' => {
+ 'RT::CustomField' => sub {
+ my ($ref) = @_;
+ $ref->{LookupType} = 'RT::Class-RT::Article'
+ if $ref->{LookupType} eq 'RT::FM::Class-RT::FM::Article';
+ },
+ 'RT::ObjectCustomFieldValue' => sub {
+ my ($ref) = @_;
+ $ref->{ObjectType} = 'RT::Article'
+ if $ref->{ObjectType} eq 'RT::FM::Article';
+ },
+ },
+
+);
+
+1;
diff --git a/rt/lib/RT/Migrate/Serializer.pm b/rt/lib/RT/Migrate/Serializer.pm
new file mode 100644
index 0000000..92be629
--- /dev/null
+++ b/rt/lib/RT/Migrate/Serializer.pm
@@ -0,0 +1,492 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Serializer;
+
+use strict;
+use warnings;
+
+use base 'RT::DependencyWalker';
+
+use Storable qw//;
+sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) };
+use RT::Migrate::Incremental;
+use RT::Migrate::Serializer::IncrementalRecord;
+use RT::Migrate::Serializer::IncrementalRecords;
+
+sub Init {
+ my $self = shift;
+
+ my %args = (
+ AllUsers => 1,
+ AllGroups => 1,
+ FollowDeleted => 1,
+
+ FollowScrips => 0,
+ FollowTickets => 1,
+ FollowACL => 0,
+
+ Clone => 0,
+ Incremental => 0,
+
+ Verbose => 1,
+ @_,
+ );
+
+ $self->{Verbose} = delete $args{Verbose};
+
+ $self->{$_} = delete $args{$_}
+ for qw/
+ AllUsers
+ AllGroups
+ FollowDeleted
+ FollowScrips
+ FollowTickets
+ FollowACL
+ Clone
+ Incremental
+ /;
+
+ $self->{Clone} = 1 if $self->{Incremental};
+
+ $self->SUPER::Init(@_, First => "top");
+
+ # Keep track of the number of each type of object written out
+ $self->{ObjectCount} = {};
+
+ if ($self->{Clone}) {
+ $self->PushAll;
+ } else {
+ $self->PushBasics;
+ }
+}
+
+sub Metadata {
+ my $self = shift;
+
+ # Determine the highest upgrade step that we run
+ my @versions = ($RT::VERSION, keys %RT::Migrate::Incremental::UPGRADES);
+ my ($max) = reverse sort cmp_version @versions;
+ # we don't want to run upgrades to 4.2.x if we're running
+ # the serializier on an 4.0 instance.
+ $max = $RT::VERSION unless $self->{Incremental};
+
+ return {
+ Format => "0.8",
+ VersionFrom => $RT::VERSION,
+ Version => $max,
+ Organization => $RT::Organization,
+ Clone => $self->{Clone},
+ Incremental => $self->{Incremental},
+ ObjectCount => { $self->ObjectCount },
+ @_,
+ },
+}
+
+sub PushAll {
+ my $self = shift;
+
+ # To keep unique constraints happy, we need to remove old records
+ # before we insert new ones. This fixes the case where a
+ # GroupMember was deleted and re-added (with a new id, but the same
+ # membership).
+ if ($self->{Incremental}) {
+ my $removed = RT::Migrate::Serializer::IncrementalRecords->new( RT->SystemUser );
+ $removed->Limit( FIELD => "UpdateType", VALUE => 3 );
+ $removed->OrderBy( FIELD => 'id' );
+ $self->PushObj( $removed );
+ }
+ # XXX: This is sadly not sufficient to deal with the general case of
+ # non-id unique constraints, such as queue names. If queues A and B
+ # existed, and B->C and A->B renames were done, these will be
+ # serialized with A->B first, which will fail because there already
+ # exists a B.
+
+ # Principals first; while we don't serialize these separately during
+ # normal dependency walking (we fold them into users and groups),
+ # having them separate during cloning makes logic simpler.
+ $self->PushCollections(qw(Principals));
+
+ # Users and groups
+ $self->PushCollections(qw(Users Groups GroupMembers));
+
+ # Tickets
+ $self->PushCollections(qw(Queues Tickets Transactions Attachments Links));
+
+ # Articles
+ $self->PushCollections(qw(Articles), map { ($_, "Object$_") } qw(Classes Topics));
+
+ # Custom Fields
+ if (RT::ObjectCustomFields->require) {
+ $self->PushCollections(map { ($_, "Object$_") } qw(CustomFields CustomFieldValues));
+ } elsif (RT::TicketCustomFieldValues->require) {
+ $self->PushCollections(qw(CustomFields CustomFieldValues TicketCustomFieldValues));
+ }
+
+ # ACLs
+ $self->PushCollections(qw(ACL));
+
+ # Scrips
+ $self->PushCollections(qw(Scrips ObjectScrips ScripActions ScripConditions Templates));
+
+ # Attributes
+ $self->PushCollections(qw(Attributes));
+}
+
+sub PushCollections {
+ my $self = shift;
+
+ for my $type (@_) {
+ my $class = "RT::\u$type";
+
+ $class->require or next;
+ my $collection = $class->new( RT->SystemUser );
+ $collection->FindAllRows; # be explicit
+ $collection->CleanSlate; # some collections (like groups and users) join in _Init
+ $collection->UnLimit;
+ $collection->OrderBy( FIELD => 'id' );
+
+ if ($self->{Clone}) {
+ if ($collection->isa('RT::Tickets')) {
+ $collection->{allow_deleted_search} = 1;
+ $collection->IgnoreType; # looking_at_type
+ }
+ elsif ($collection->isa('RT::ObjectCustomFieldValues')) {
+ # FindAllRows (find_disabled_rows) isn't used by OCFVs
+ $collection->{find_expired_rows} = 1;
+ }
+
+ if ($self->{Incremental}) {
+ my $alias = $collection->Join(
+ ALIAS1 => "main",
+ FIELD1 => "id",
+ TABLE2 => "IncrementalRecords",
+ FIELD2 => "ObjectId",
+ );
+ $collection->DBIx::SearchBuilder::Limit(
+ ALIAS => $alias,
+ FIELD => "ObjectType",
+ VALUE => ref($collection->NewItem),
+ );
+ }
+ }
+
+ $self->PushObj( $collection );
+ }
+}
+
+sub PushBasics {
+ my $self = shift;
+
+ # System users
+ for my $name (qw/RT_System root nobody/) {
+ my $user = RT::User->new( RT->SystemUser );
+ my ($id, $msg) = $user->Load( $name );
+ warn "No '$name' user found: $msg" unless $id;
+ $self->PushObj( $user ) if $id;
+ }
+
+ # System groups
+ foreach my $name (qw(Everyone Privileged Unprivileged)) {
+ my $group = RT::Group->new( RT->SystemUser );
+ my ($id, $msg) = $group->LoadSystemInternalGroup( $name );
+ warn "No '$name' group found: $msg" unless $id;
+ $self->PushObj( $group ) if $id;
+ }
+
+ # System role groups
+ my $systemroles = RT::Groups->new( RT->SystemUser );
+ $systemroles->LimitToRolesForObject( RT->System );
+ $self->PushObj( $systemroles );
+
+ # CFs on Users, Groups, Queues
+ my $cfs = RT::CustomFields->new( RT->SystemUser );
+ $cfs->Limit(
+ FIELD => 'LookupType',
+ OPERATOR => 'IN',
+ VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
+ );
+ $self->PushObj( $cfs );
+
+ # Global attributes
+ my $attributes = RT::Attributes->new( RT->SystemUser );
+ $attributes->LimitToObject( $RT::System );
+ $self->PushObj( $attributes );
+
+ # Global ACLs
+ if ($self->{FollowACL}) {
+ my $acls = RT::ACL->new( RT->SystemUser );
+ $acls->LimitToObject( $RT::System );
+ $self->PushObj( $acls );
+ }
+
+ # Global scrips
+ if ($self->{FollowScrips}) {
+ my $scrips = RT::Scrips->new( RT->SystemUser );
+ $scrips->LimitToGlobal;
+
+ my $templates = RT::Templates->new( RT->SystemUser );
+ $templates->LimitToGlobal;
+
+ $self->PushObj( $scrips, $templates );
+ $self->PushCollections(qw(ScripActions ScripConditions));
+ }
+
+ if ($self->{AllUsers}) {
+ my $users = RT::Users->new( RT->SystemUser );
+ $users->LimitToPrivileged;
+ $self->PushObj( $users );
+ }
+
+ if ($self->{AllGroups}) {
+ my $groups = RT::Groups->new( RT->SystemUser );
+ $groups->LimitToUserDefinedGroups;
+ $self->PushObj( $groups );
+ }
+
+ if (RT::Articles->require) {
+ $self->PushCollections(qw(Topics Classes));
+ }
+
+ $self->PushCollections(qw(Queues));
+}
+
+sub InitStream {
+ my $self = shift;
+
+ # Write the initial metadata
+ my $meta = $self->Metadata;
+ $! = 0;
+ Storable::nstore_fd( $meta, $self->{Filehandle} );
+ die "Failed to write metadata: $!" if $!;
+
+ return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0;
+
+ my %transforms;
+ for my $v (sort cmp_version keys %RT::Migrate::Incremental::UPGRADES) {
+ for my $ref (keys %{$RT::Migrate::Incremental::UPGRADES{$v}}) {
+ push @{$transforms{$ref}}, $RT::Migrate::Incremental::UPGRADES{$v}{$ref};
+ }
+ }
+ for my $ref (keys %transforms) {
+ # XXX Does not correctly deal with updates of $classref, which
+ # should technically apply all later transforms of the _new_
+ # class. This is not relevant in the current upgrades, as
+ # RT::ObjectCustomFieldValues do not have interesting later
+ # upgrades if you start from 3.2 (which does
+ # RT::TicketCustomFieldValues -> RT::ObjectCustomFieldValues)
+ $self->{Transform}{$ref} = sub {
+ my ($dat, $classref) = @_;
+ my @extra;
+ for my $c (@{$transforms{$ref}}) {
+ push @extra, $c->($dat, $classref);
+ return @extra if not $$classref;
+ }
+ return @extra;
+ };
+ }
+}
+
+sub NextPage {
+ my $self = shift;
+ my ($collection, $last) = @_;
+
+ $last ||= 0;
+
+ if ($self->{Clone}) {
+ # Clone provides guaranteed ordering by id and with no other id limits
+ # worry about trampling
+
+ # Use DBIx::SearchBuilder::Limit explicitly to avoid shenanigans in RT::Tickets
+ $collection->DBIx::SearchBuilder::Limit(
+ FIELD => 'id',
+ OPERATOR => '>',
+ VALUE => $last,
+ ENTRYAGGREGATOR => 'none', # replaces last limit on this field
+ );
+ } else {
+ # XXX TODO: this could dig around inside the collection to see how it's
+ # limited and do the faster paging above under other conditions.
+ $self->SUPER::NextPage(@_);
+ }
+}
+
+sub Process {
+ my $self = shift;
+ my %args = (
+ object => undef,
+ @_
+ );
+
+ my $obj = $args{object};
+ my $uid = $obj->UID;
+
+ # Skip all dependency walking if we're cloning; go straight to
+ # visiting them.
+ if ($self->{Clone} and $uid) {
+ return if $obj->isa("RT::System");
+ $self->{progress}->($obj) if $self->{progress};
+ return $self->Visit(%args);
+ }
+
+ return $self->SUPER::Process( @_ );
+}
+
+sub StackSize {
+ my $self = shift;
+ return scalar @{$self->{stack}};
+}
+
+sub ObjectCount {
+ my $self = shift;
+ return %{ $self->{ObjectCount} };
+}
+
+sub Observe {
+ my $self = shift;
+ my %args = (
+ object => undef,
+ direction => undef,
+ from => undef,
+ @_
+ );
+
+ my $obj = $args{object};
+ my $from = $args{from};
+ if ($obj->isa("RT::Ticket")) {
+ return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
+ return $self->{FollowTickets};
+ } elsif ($obj->isa("RT::ACE")) {
+ return $self->{FollowACL};
+ } elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
+ return $self->{FollowScrips};
+ } elsif ($obj->isa("RT::GroupMember")) {
+ my $grp = $obj->GroupObj->Object;
+ if ($grp->Domain =~ /^RT::(Queue|Ticket)-Role$/) {
+ return 0 unless $grp->UID eq $from;
+ } elsif ($grp->Domain eq "SystemInternal") {
+ return 0 if $grp->UID eq $from;
+ }
+ }
+
+ return 1;
+}
+
+sub Visit {
+ my $self = shift;
+ my %args = (
+ object => undef,
+ @_
+ );
+
+ # Serialize it
+ my $obj = $args{object};
+ warn "Writing ".$obj->UID."\n" if $self->{Verbose};
+ my @store;
+ if ($obj->isa("RT::Migrate::Serializer::IncrementalRecord")) {
+ # These are stand-ins for record removals
+ my $class = $obj->ObjectType;
+ my %data = ( id => $obj->ObjectId );
+ # -class is used for transforms when dropping a record
+ if ($self->{Transform}{"-$class"}) {
+ $self->{Transform}{"-$class"}->(\%data,\$class)
+ }
+ @store = (
+ $class,
+ undef,
+ \%data,
+ );
+ } elsif ($self->{Clone}) {
+ # Short-circuit and get Just The Basics, Sir if we're cloning
+ my $class = ref($obj);
+ my $uid = $obj->UID;
+ my %data = $obj->RT::Record::Serialize( UIDs => 0 );
+
+ # +class is used when seeing a record of one class might insert
+ # a separate record into the stream
+ if ($self->{Transform}{"+$class"}) {
+ my @extra = $self->{Transform}{"+$class"}->(\%data,\$class);
+ for my $e (@extra) {
+ $! = 0;
+ Storable::nstore_fd($e, $self->{Filehandle});
+ die "Failed to write: $!" if $!;
+ $self->{ObjectCount}{$e->[0]}++;
+ }
+ }
+
+ # Upgrade the record if necessary
+ if ($self->{Transform}{$class}) {
+ $self->{Transform}{$class}->(\%data,\$class);
+ }
+
+ # Transforms set $class to undef to drop the record
+ return unless $class;
+
+ @store = (
+ $class,
+ $uid,
+ \%data,
+ );
+ } else {
+ @store = (
+ ref($obj),
+ $obj->UID,
+ { $obj->Serialize },
+ );
+ }
+
+ # Write it out; nstore_fd doesn't trap failures to write, so we have
+ # to; by clearing $! and checking it afterwards.
+ $! = 0;
+ Storable::nstore_fd(\@store, $self->{Filehandle});
+ die "Failed to write: $!" if $!;
+
+ $self->{ObjectCount}{$store[0]}++;
+}
+
+1;
diff --git a/rt/lib/RT/Migrate/Serializer/File.pm b/rt/lib/RT/Migrate/Serializer/File.pm
new file mode 100644
index 0000000..2832365
--- /dev/null
+++ b/rt/lib/RT/Migrate/Serializer/File.pm
@@ -0,0 +1,171 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Serializer::File;
+
+use strict;
+use warnings;
+
+use base 'RT::Migrate::Serializer';
+
+sub Init {
+ my $self = shift;
+
+ my %args = (
+ Directory => undef,
+ Force => undef,
+ MaxFileSize => 32,
+
+ @_,
+ );
+
+ # Set up the output directory we'll be writing to
+ my ($y,$m,$d) = (localtime)[5,4,3];
+ $args{Directory} = $RT::Organization .
+ sprintf(":%d-%02d-%02d",$y+1900,$m+1,$d)
+ unless defined $args{Directory};
+ system("rm", "-rf", $args{Directory}) if $args{Force};
+ die "Output directory $args{Directory} already exists"
+ if -d $args{Directory};
+ mkdir $args{Directory}
+ or die "Can't create output directory $args{Directory}: $!\n";
+ $self->{Directory} = delete $args{Directory};
+
+ # How many megabytes each chunk should be, approximitely
+ $self->{MaxFileSize} = delete $args{MaxFileSize};
+
+ # Which file we're writing to
+ $self->{FileCount} = 1;
+
+ $self->SUPER::Init(@_);
+}
+
+sub Metadata {
+ my $self = shift;
+ return $self->SUPER::Metadata(
+ Files => [ $self->Files ],
+ @_,
+ )
+}
+
+sub Export {
+ my $self = shift;
+
+ # Set up our output file
+ $self->OpenFile;
+
+ # Write the initial metadata
+ $self->InitStream;
+
+ # Walk the objects
+ $self->Walk( @_ );
+
+ # Close everything back up
+ $self->CloseFile;
+
+ # Write the summary file
+ Storable::nstore(
+ $self->Metadata( Final => 1 ),
+ $self->Directory . "/rt-serialized"
+ );
+
+ return $self->ObjectCount;
+}
+
+sub Visit {
+ my $self = shift;
+
+ # Rotate if we get too big
+ my $maxsize = 1024 * 1024 * $self->{MaxFileSize};
+ $self->RotateFile if tell($self->{Filehandle}) > $maxsize;
+
+ # Serialize it
+ $self->SUPER::Visit( @_ );
+}
+
+
+sub Files {
+ my $self = shift;
+ return @{ $self->{Files} };
+}
+
+sub Filename {
+ my $self = shift;
+ return sprintf(
+ "%s/%03d.dat",
+ $self->{Directory},
+ $self->{FileCount}
+ );
+}
+
+sub Directory {
+ my $self = shift;
+ return $self->{Directory};
+}
+
+sub OpenFile {
+ my $self = shift;
+ open($self->{Filehandle}, ">", $self->Filename)
+ or die "Can't write to file @{[$self->Filename]}: $!";
+ push @{$self->{Files}}, $self->Filename;
+}
+
+sub CloseFile {
+ my $self = shift;
+ close($self->{Filehandle})
+ or die "Can't close @{[$self->Filename]}: $!";
+ $self->{FileCount}++;
+}
+
+sub RotateFile {
+ my $self = shift;
+ $self->CloseFile;
+ $self->OpenFile;
+}
+
+1;
diff --git a/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm b/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm
new file mode 100644
index 0000000..d5df8c8
--- /dev/null
+++ b/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm
@@ -0,0 +1,80 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Serializer::IncrementalRecord;
+use base qw/RT::Record/;
+
+use strict;
+use warnings;
+
+sub Table {'IncrementalRecords'}
+
+sub _CoreAccessible {
+ return {
+ id => { read => 1 },
+ ObjectType => { read => 1 },
+ ObjectId => { read => 1 },
+ UpdateType => { read => 1 },
+ AlteredAt => { read => 1 },
+ };
+};
+
+1;
+
+__END__
+
+CREATE TABLE IncrementalRecords (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ ObjectType VARCHAR(50) NOT NULL,
+ ObjectId INTEGER NOT NULL,
+ UpdateType TINYINT NOT NULL,
+ AlteredAt TIMESTAMP NOT NULL,
+ PRIMARY KEY(ObjectType, ObjectId),
+ UNIQUE KEY(id),
+ KEY(UpdateType)
+);
diff --git a/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm b/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm
new file mode 100644
index 0000000..a729caa
--- /dev/null
+++ b/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm
@@ -0,0 +1,69 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Serializer::IncrementalRecords;
+use base qw/RT::SearchBuilder/;
+
+use strict;
+use warnings;
+
+sub _Init {
+ my $self = shift;
+ $self->{'table'} = 'IncrementalRecords';
+ $self->{'primary_key'} = 'id';
+ return ( $self->SUPER::_Init(@_) );
+}
+
+sub Table {'IncrementalRecords'}
+
+sub NewItem {
+ my $self = shift;
+ return(RT::Migrate::Serializer::IncrementalRecord->new($self->CurrentUser));
+}
+
+1;