rt 4.2.15
[freeside.git] / rt / lib / RT / Migrate / Serializer.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
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.)
37 #
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.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Migrate::Serializer;
50
51 use strict;
52 use warnings;
53
54 use base 'RT::DependencyWalker';
55
56 use Storable qw//;
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';
62
63 sub Init {
64     my $self = shift;
65
66     my %args = (
67         AllUsers            => 1,
68         AllGroups           => 1,
69         FollowDeleted       => 1,
70
71         FollowScrips        => 0,
72         FollowTickets       => 1,
73         FollowACL           => 0,
74
75         Clone       => 0,
76         Incremental => 0,
77
78         Verbose => 1,
79         @_,
80     );
81
82     $self->{Verbose} = delete $args{Verbose};
83
84     $self->{$_} = delete $args{$_}
85         for qw/
86                   AllUsers
87                   AllGroups
88                   FollowDeleted
89                   FollowScrips
90                   FollowTickets
91                   FollowACL
92                   Queues
93                   CustomFields
94                   HyperlinkUnmigrated
95                   Clone
96                   Incremental
97               /;
98
99     $self->{Clone} = 1 if $self->{Incremental};
100
101     $self->SUPER::Init(@_, First => "top");
102
103     # Keep track of the number of each type of object written out
104     $self->{ObjectCount} = {};
105
106     if ($self->{Clone}) {
107         $self->PushAll;
108     } else {
109         $self->PushBasics;
110     }
111 }
112
113 sub Metadata {
114     my $self = shift;
115
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};
122
123     return {
124         Format       => "0.8",
125         VersionFrom  => $RT::VERSION,
126         Version      => $max,
127         Organization => $RT::Organization,
128         Clone        => $self->{Clone},
129         Incremental  => $self->{Incremental},
130         ObjectCount  => { $self->ObjectCount },
131         @_,
132     },
133 }
134
135 sub PushAll {
136     my $self = shift;
137
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
141     # membership).
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 );
147     }
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
152     # exists a B.
153
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));
158
159     # Users and groups
160     $self->PushCollections(qw(Users Groups GroupMembers));
161
162     # Tickets
163     $self->PushCollections(qw(Queues Tickets Transactions Attachments Links));
164
165     # Articles
166     $self->PushCollections(qw(Articles), map { ($_, "Object$_") } qw(Classes Topics));
167
168     # Custom Fields
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));
173     }
174
175     # ACLs
176     $self->PushCollections(qw(ACL));
177
178     # Scrips
179     $self->PushCollections(qw(Scrips ObjectScrips ScripActions ScripConditions Templates));
180
181     # Attributes
182     $self->PushCollections(qw(Attributes));
183 }
184
185 sub PushCollections {
186     my $self  = shift;
187
188     for my $type (@_) {
189         my $class = "RT::\u$type";
190
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' );
197
198         if ($self->{Clone}) {
199             if ($collection->isa('RT::Tickets')) {
200                 $collection->{allow_deleted_search} = 1;
201                 $collection->IgnoreType; # looking_at_type
202             }
203             elsif ($collection->isa('RT::ObjectCustomFieldValues')) {
204                 # FindAllRows (find_disabled_rows) isn't used by OCFVs
205                 $collection->{find_expired_rows} = 1;
206             }
207
208             if ($self->{Incremental}) {
209                 my $alias = $collection->Join(
210                     ALIAS1 => "main",
211                     FIELD1 => "id",
212                     TABLE2 => "IncrementalRecords",
213                     FIELD2 => "ObjectId",
214                 );
215                 $collection->DBIx::SearchBuilder::Limit(
216                     ALIAS => $alias,
217                     FIELD => "ObjectType",
218                     VALUE => ref($collection->NewItem),
219                 );
220             }
221         }
222
223         $self->PushObj( $collection );
224     }
225 }
226
227 sub PushBasics {
228     my $self = shift;
229
230     # System users
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;
236     }
237
238     # System groups
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;
244     }
245
246     # System role groups
247     my $systemroles = RT::Groups->new( RT->SystemUser );
248     $systemroles->LimitToRolesForObject( RT->System );
249     $self->PushObj( $systemroles );
250
251     # CFs on Users, Groups, Queues
252     my $cfs = RT::CustomFields->new( RT->SystemUser );
253     $cfs->Limit(
254         FIELD => 'LookupType',
255         OPERATOR => 'IN',
256         VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
257     );
258
259     if ($self->{CustomFields}) {
260         $cfs->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{CustomFields});
261     }
262
263     $self->PushObj( $cfs );
264
265     # Global attributes
266     my $attributes = RT::Attributes->new( RT->SystemUser );
267     $attributes->LimitToObject( $RT::System );
268     $self->PushObj( $attributes );
269
270     # Global ACLs
271     if ($self->{FollowACL}) {
272         my $acls = RT::ACL->new( RT->SystemUser );
273         $acls->LimitToObject( $RT::System );
274         $self->PushObj( $acls );
275     }
276
277     # Global scrips
278     if ($self->{FollowScrips}) {
279         my $scrips = RT::Scrips->new( RT->SystemUser );
280         $scrips->LimitToGlobal;
281
282         my $templates = RT::Templates->new( RT->SystemUser );
283         $templates->LimitToGlobal;
284
285         $self->PushObj( $scrips, $templates );
286         $self->PushCollections(qw(ScripActions ScripConditions));
287     }
288
289     if ($self->{AllUsers}) {
290         my $users = RT::Users->new( RT->SystemUser );
291         $users->LimitToPrivileged;
292         $self->PushObj( $users );
293     }
294
295     if ($self->{AllGroups}) {
296         my $groups = RT::Groups->new( RT->SystemUser );
297         $groups->LimitToUserDefinedGroups;
298         $self->PushObj( $groups );
299     }
300
301     if (RT::Articles->require) {
302         $self->PushCollections(qw(Topics Classes));
303     }
304
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);
309     }
310     else {
311         $self->PushCollections(qw(Queues));
312     }
313 }
314
315 sub InitStream {
316     my $self = shift;
317
318     # Write the initial metadata
319     my $meta = $self->Metadata;
320     $! = 0;
321     Storable::nstore_fd( $meta, $self->{Filehandle} );
322     die "Failed to write metadata: $!" if $!;
323
324     return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0;
325
326     my %transforms;
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};
330         }
331     }
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) = @_;
341             my @extra;
342             for my $c (@{$transforms{$ref}}) {
343                 push @extra, $c->($dat, $classref);
344                 return @extra if not $$classref;
345             }
346             return @extra;
347         };
348     }
349 }
350
351 sub NextPage {
352     my $self = shift;
353     my ($collection, $last) = @_;
354
355     $last ||= 0;
356
357     if ($self->{Clone}) {
358         # Clone provides guaranteed ordering by id and with no other id limits
359         # worry about trampling
360
361         # Use DBIx::SearchBuilder::Limit explicitly to avoid shenanigans in RT::Tickets
362         $collection->DBIx::SearchBuilder::Limit(
363             FIELD           => 'id',
364             OPERATOR        => '>',
365             VALUE           => $last,
366             ENTRYAGGREGATOR => 'none', # replaces last limit on this field
367         );
368     } else {
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(@_);
372     }
373 }
374
375 sub Process {
376     my $self = shift;
377     my %args = (
378         object => undef,
379         @_
380     );
381
382     my $obj = $args{object};
383     my $uid = $obj->UID;
384
385     # Skip all dependency walking if we're cloning; go straight to
386     # visiting them.
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);
391     }
392
393     return $self->SUPER::Process( @_ );
394 }
395
396 sub StackSize {
397     my $self = shift;
398     return scalar @{$self->{stack}};
399 }
400
401 sub ObjectCount {
402     my $self = shift;
403     return %{ $self->{ObjectCount} };
404 }
405
406 sub Observe {
407     my $self = shift;
408     my %args = (
409         object    => undef,
410         direction => undef,
411         from      => undef,
412         @_
413     );
414
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")) {
423         my $id = $obj->Id;
424         return 0 if $self->{Queues} && none { $id == $_ } @{ $self->{Queues} };
425         return 1;
426     } elsif ($obj->isa("RT::CustomField")) {
427         my $id = $obj->Id;
428         return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
429         return 1;
430     } elsif ($obj->isa("RT::ObjectCustomFieldValue")) {
431         my $id = $obj->CustomField;
432         return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
433         return 1;
434     } elsif ($obj->isa("RT::ObjectCustomField")) {
435         my $id = $obj->CustomField;
436         return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
437         return 1;
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;
448         }
449     }
450
451     return 1;
452 }
453
454 sub Visit {
455     my $self = shift;
456     my %args = (
457         object => undef,
458         @_
459     );
460
461     # Serialize it
462     my $obj = $args{object};
463     warn "Writing ".$obj->UID."\n" if $self->{Verbose};
464     my @store;
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)
472         }
473         @store = (
474             $class,
475             undef,
476             \%data,
477         );
478     } elsif ($self->{Clone}) {
479         # Short-circuit and get Just The Basics, Sir if we're cloning
480         my $class = ref($obj);
481         my $uid   = $obj->UID;
482         my %data  = $obj->RT::Record::Serialize( UIDs => 0 );
483
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);
488             for my $e (@extra) {
489                 $! = 0;
490                 Storable::nstore_fd($e, $self->{Filehandle});
491                 die "Failed to write: $!" if $!;
492                 $self->{ObjectCount}{$e->[0]}++;
493             }
494         }
495
496         # Upgrade the record if necessary
497         if ($self->{Transform}{$class}) {
498             $self->{Transform}{$class}->(\%data,\$class);
499         }
500
501         # Transforms set $class to undef to drop the record
502         return unless $class;
503
504         @store = (
505             $class,
506             $uid,
507             \%data,
508         );
509     } else {
510         my %serialized = $obj->Serialize(serializer => $self);
511         return unless %serialized;
512
513         @store = (
514             ref($obj),
515             $obj->UID,
516             \%serialized,
517         );
518     }
519
520     # Write it out; nstore_fd doesn't trap failures to write, so we have
521     # to; by clearing $! and checking it afterwards.
522     $! = 0;
523     Storable::nstore_fd(\@store, $self->{Filehandle});
524     die "Failed to write: $!" if $!;
525
526     $self->{ObjectCount}{$store[0]}++;
527 }
528
529 1;