1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Migrate::Serializer;
54 use base 'RT::DependencyWalker';
57 sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) };
58 use RT::Migrate::Incremental;
59 use RT::Migrate::Serializer::IncrementalRecord;
60 use RT::Migrate::Serializer::IncrementalRecords;
61 use List::MoreUtils 'none';
82 $self->{Verbose} = delete $args{Verbose};
84 $self->{$_} = delete $args{$_}
99 $self->{Clone} = 1 if $self->{Incremental};
101 $self->SUPER::Init(@_, First => "top");
103 # Keep track of the number of each type of object written out
104 $self->{ObjectCount} = {};
106 if ($self->{Clone}) {
116 # Determine the highest upgrade step that we run
117 my @versions = ($RT::VERSION, keys %RT::Migrate::Incremental::UPGRADES);
118 my ($max) = reverse sort cmp_version @versions;
119 # we don't want to run upgrades to 4.2.x if we're running
120 # the serializier on an 4.0 instance.
121 $max = $RT::VERSION unless $self->{Incremental};
125 VersionFrom => $RT::VERSION,
127 Organization => $RT::Organization,
128 Clone => $self->{Clone},
129 Incremental => $self->{Incremental},
130 ObjectCount => { $self->ObjectCount },
138 # To keep unique constraints happy, we need to remove old records
139 # before we insert new ones. This fixes the case where a
140 # GroupMember was deleted and re-added (with a new id, but the same
142 if ($self->{Incremental}) {
143 my $removed = RT::Migrate::Serializer::IncrementalRecords->new( RT->SystemUser );
144 $removed->Limit( FIELD => "UpdateType", VALUE => 3 );
145 $removed->OrderBy( FIELD => 'id' );
146 $self->PushObj( $removed );
148 # XXX: This is sadly not sufficient to deal with the general case of
149 # non-id unique constraints, such as queue names. If queues A and B
150 # existed, and B->C and A->B renames were done, these will be
151 # serialized with A->B first, which will fail because there already
154 # Principals first; while we don't serialize these separately during
155 # normal dependency walking (we fold them into users and groups),
156 # having them separate during cloning makes logic simpler.
157 $self->PushCollections(qw(Principals));
160 $self->PushCollections(qw(Users Groups GroupMembers));
163 $self->PushCollections(qw(Queues Tickets Transactions Attachments Links));
166 $self->PushCollections(qw(Articles), map { ($_, "Object$_") } qw(Classes Topics));
169 if (RT::ObjectCustomFields->require) {
170 $self->PushCollections(map { ($_, "Object$_") } qw(CustomFields CustomFieldValues));
171 } elsif (RT::TicketCustomFieldValues->require) {
172 $self->PushCollections(qw(CustomFields CustomFieldValues TicketCustomFieldValues));
176 $self->PushCollections(qw(ACL));
179 $self->PushCollections(qw(Scrips ObjectScrips ScripActions ScripConditions Templates));
182 $self->PushCollections(qw(Attributes));
185 sub PushCollections {
189 my $class = "RT::\u$type";
191 $class->require or next;
192 my $collection = $class->new( RT->SystemUser );
193 $collection->FindAllRows; # be explicit
194 $collection->CleanSlate; # some collections (like groups and users) join in _Init
195 $collection->UnLimit;
196 $collection->OrderBy( FIELD => 'id' );
198 if ($self->{Clone}) {
199 if ($collection->isa('RT::Tickets')) {
200 $collection->{allow_deleted_search} = 1;
201 $collection->IgnoreType; # looking_at_type
203 elsif ($collection->isa('RT::ObjectCustomFieldValues')) {
204 # FindAllRows (find_disabled_rows) isn't used by OCFVs
205 $collection->{find_expired_rows} = 1;
208 if ($self->{Incremental}) {
209 my $alias = $collection->Join(
212 TABLE2 => "IncrementalRecords",
213 FIELD2 => "ObjectId",
215 $collection->DBIx::SearchBuilder::Limit(
217 FIELD => "ObjectType",
218 VALUE => ref($collection->NewItem),
223 $self->PushObj( $collection );
231 for my $name (qw/RT_System root nobody/) {
232 my $user = RT::User->new( RT->SystemUser );
233 my ($id, $msg) = $user->Load( $name );
234 warn "No '$name' user found: $msg" unless $id;
235 $self->PushObj( $user ) if $id;
239 foreach my $name (qw(Everyone Privileged Unprivileged)) {
240 my $group = RT::Group->new( RT->SystemUser );
241 my ($id, $msg) = $group->LoadSystemInternalGroup( $name );
242 warn "No '$name' group found: $msg" unless $id;
243 $self->PushObj( $group ) if $id;
247 my $systemroles = RT::Groups->new( RT->SystemUser );
248 $systemroles->LimitToRolesForObject( RT->System );
249 $self->PushObj( $systemroles );
251 # CFs on Users, Groups, Queues
252 my $cfs = RT::CustomFields->new( RT->SystemUser );
254 FIELD => 'LookupType',
256 VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
259 if ($self->{CustomFields}) {
260 $cfs->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{CustomFields});
263 $self->PushObj( $cfs );
266 my $attributes = RT::Attributes->new( RT->SystemUser );
267 $attributes->LimitToObject( $RT::System );
268 $self->PushObj( $attributes );
271 if ($self->{FollowACL}) {
272 my $acls = RT::ACL->new( RT->SystemUser );
273 $acls->LimitToObject( $RT::System );
274 $self->PushObj( $acls );
278 if ($self->{FollowScrips}) {
279 my $scrips = RT::Scrips->new( RT->SystemUser );
280 $scrips->LimitToGlobal;
282 my $templates = RT::Templates->new( RT->SystemUser );
283 $templates->LimitToGlobal;
285 $self->PushObj( $scrips, $templates );
286 $self->PushCollections(qw(ScripActions ScripConditions));
289 if ($self->{AllUsers}) {
290 my $users = RT::Users->new( RT->SystemUser );
291 $users->LimitToPrivileged;
292 $self->PushObj( $users );
295 if ($self->{AllGroups}) {
296 my $groups = RT::Groups->new( RT->SystemUser );
297 $groups->LimitToUserDefinedGroups;
298 $self->PushObj( $groups );
301 if (RT::Articles->require) {
302 $self->PushCollections(qw(Topics Classes));
305 if ($self->{Queues}) {
306 my $queues = RT::Queues->new(RT->SystemUser);
307 $queues->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{Queues});
308 $self->PushObj($queues);
311 $self->PushCollections(qw(Queues));
318 # Write the initial metadata
319 my $meta = $self->Metadata;
321 Storable::nstore_fd( $meta, $self->{Filehandle} );
322 die "Failed to write metadata: $!" if $!;
324 return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0;
327 for my $v (sort cmp_version keys %RT::Migrate::Incremental::UPGRADES) {
328 for my $ref (keys %{$RT::Migrate::Incremental::UPGRADES{$v}}) {
329 push @{$transforms{$ref}}, $RT::Migrate::Incremental::UPGRADES{$v}{$ref};
332 for my $ref (keys %transforms) {
333 # XXX Does not correctly deal with updates of $classref, which
334 # should technically apply all later transforms of the _new_
335 # class. This is not relevant in the current upgrades, as
336 # RT::ObjectCustomFieldValues do not have interesting later
337 # upgrades if you start from 3.2 (which does
338 # RT::TicketCustomFieldValues -> RT::ObjectCustomFieldValues)
339 $self->{Transform}{$ref} = sub {
340 my ($dat, $classref) = @_;
342 for my $c (@{$transforms{$ref}}) {
343 push @extra, $c->($dat, $classref);
344 return @extra if not $$classref;
353 my ($collection, $last) = @_;
357 if ($self->{Clone}) {
358 # Clone provides guaranteed ordering by id and with no other id limits
359 # worry about trampling
361 # Use DBIx::SearchBuilder::Limit explicitly to avoid shenanigans in RT::Tickets
362 $collection->DBIx::SearchBuilder::Limit(
366 ENTRYAGGREGATOR => 'none', # replaces last limit on this field
369 # XXX TODO: this could dig around inside the collection to see how it's
370 # limited and do the faster paging above under other conditions.
371 $self->SUPER::NextPage(@_);
382 my $obj = $args{object};
385 # Skip all dependency walking if we're cloning; go straight to
387 if ($self->{Clone} and $uid) {
388 return if $obj->isa("RT::System");
389 $self->{progress}->($obj) if $self->{progress};
390 return $self->Visit(%args);
393 return $self->SUPER::Process( @_ );
398 return scalar @{$self->{stack}};
403 return %{ $self->{ObjectCount} };
415 my $obj = $args{object};
416 my $from = $args{from};
417 if ($obj->isa("RT::Ticket")) {
418 return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
419 my $queue = $obj->Queue;
420 return 0 if $self->{Queues} && none { $queue == $_ } @{ $self->{Queues} };
421 return $self->{FollowTickets};
422 } elsif ($obj->isa("RT::Queue")) {
424 return 0 if $self->{Queues} && none { $id == $_ } @{ $self->{Queues} };
426 } elsif ($obj->isa("RT::CustomField")) {
428 return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
430 } elsif ($obj->isa("RT::ObjectCustomFieldValue")) {
431 my $id = $obj->CustomField;
432 return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
434 } elsif ($obj->isa("RT::ObjectCustomField")) {
435 my $id = $obj->CustomField;
436 return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
438 } elsif ($obj->isa("RT::ACE")) {
439 return $self->{FollowACL};
440 } elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
441 return $self->{FollowScrips};
442 } elsif ($obj->isa("RT::GroupMember")) {
443 my $grp = $obj->GroupObj->Object;
444 if ($grp->Domain =~ /^RT::(Queue|Ticket)-Role$/) {
445 return 0 unless $grp->UID eq $from;
446 } elsif ($grp->Domain eq "SystemInternal") {
447 return 0 if $grp->UID eq $from;
462 my $obj = $args{object};
463 warn "Writing ".$obj->UID."\n" if $self->{Verbose};
465 if ($obj->isa("RT::Migrate::Serializer::IncrementalRecord")) {
466 # These are stand-ins for record removals
467 my $class = $obj->ObjectType;
468 my %data = ( id => $obj->ObjectId );
469 # -class is used for transforms when dropping a record
470 if ($self->{Transform}{"-$class"}) {
471 $self->{Transform}{"-$class"}->(\%data,\$class)
478 } elsif ($self->{Clone}) {
479 # Short-circuit and get Just The Basics, Sir if we're cloning
480 my $class = ref($obj);
482 my %data = $obj->RT::Record::Serialize( UIDs => 0 );
484 # +class is used when seeing a record of one class might insert
485 # a separate record into the stream
486 if ($self->{Transform}{"+$class"}) {
487 my @extra = $self->{Transform}{"+$class"}->(\%data,\$class);
490 Storable::nstore_fd($e, $self->{Filehandle});
491 die "Failed to write: $!" if $!;
492 $self->{ObjectCount}{$e->[0]}++;
496 # Upgrade the record if necessary
497 if ($self->{Transform}{$class}) {
498 $self->{Transform}{$class}->(\%data,\$class);
501 # Transforms set $class to undef to drop the record
502 return unless $class;
510 my %serialized = $obj->Serialize(serializer => $self);
511 return unless %serialized;
520 # Write it out; nstore_fd doesn't trap failures to write, so we have
521 # to; by clearing $! and checking it afterwards.
523 Storable::nstore_fd(\@store, $self->{Filehandle});
524 die "Failed to write: $!" if $!;
526 $self->{ObjectCount}{$store[0]}++;