Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Migrate / Serializer.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 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
62 sub Init {
63     my $self = shift;
64
65     my %args = (
66         AllUsers            => 1,
67         AllGroups           => 1,
68         FollowDeleted       => 1,
69
70         FollowScrips        => 0,
71         FollowTickets       => 1,
72         FollowACL           => 0,
73
74         Clone       => 0,
75         Incremental => 0,
76
77         Verbose => 1,
78         @_,
79     );
80
81     $self->{Verbose} = delete $args{Verbose};
82
83     $self->{$_} = delete $args{$_}
84         for qw/
85                   AllUsers
86                   AllGroups
87                   FollowDeleted
88                   FollowScrips
89                   FollowTickets
90                   FollowACL
91                   Clone
92                   Incremental
93               /;
94
95     $self->{Clone} = 1 if $self->{Incremental};
96
97     $self->SUPER::Init(@_, First => "top");
98
99     # Keep track of the number of each type of object written out
100     $self->{ObjectCount} = {};
101
102     if ($self->{Clone}) {
103         $self->PushAll;
104     } else {
105         $self->PushBasics;
106     }
107 }
108
109 sub Metadata {
110     my $self = shift;
111
112     # Determine the highest upgrade step that we run
113     my @versions = ($RT::VERSION, keys %RT::Migrate::Incremental::UPGRADES);
114     my ($max) = reverse sort cmp_version @versions;
115     # we don't want to run upgrades to 4.2.x if we're running
116     # the serializier on an 4.0 instance.
117     $max = $RT::VERSION unless $self->{Incremental};
118
119     return {
120         Format       => "0.8",
121         VersionFrom  => $RT::VERSION,
122         Version      => $max,
123         Organization => $RT::Organization,
124         Clone        => $self->{Clone},
125         Incremental  => $self->{Incremental},
126         ObjectCount  => { $self->ObjectCount },
127         @_,
128     },
129 }
130
131 sub PushAll {
132     my $self = shift;
133
134     # To keep unique constraints happy, we need to remove old records
135     # before we insert new ones.  This fixes the case where a
136     # GroupMember was deleted and re-added (with a new id, but the same
137     # membership).
138     if ($self->{Incremental}) {
139         my $removed = RT::Migrate::Serializer::IncrementalRecords->new( RT->SystemUser );
140         $removed->Limit( FIELD => "UpdateType", VALUE => 3 );
141         $removed->OrderBy( FIELD => 'id' );
142         $self->PushObj( $removed );
143     }
144     # XXX: This is sadly not sufficient to deal with the general case of
145     # non-id unique constraints, such as queue names.  If queues A and B
146     # existed, and B->C and A->B renames were done, these will be
147     # serialized with A->B first, which will fail because there already
148     # exists a B.
149
150     # Principals first; while we don't serialize these separately during
151     # normal dependency walking (we fold them into users and groups),
152     # having them separate during cloning makes logic simpler.
153     $self->PushCollections(qw(Principals));
154
155     # Users and groups
156     $self->PushCollections(qw(Users Groups GroupMembers));
157
158     # Tickets
159     $self->PushCollections(qw(Queues Tickets Transactions Attachments Links));
160
161     # Articles
162     $self->PushCollections(qw(Articles), map { ($_, "Object$_") } qw(Classes Topics));
163
164     # Custom Fields
165     if (RT::ObjectCustomFields->require) {
166         $self->PushCollections(map { ($_, "Object$_") } qw(CustomFields CustomFieldValues));
167     } elsif (RT::TicketCustomFieldValues->require) {
168         $self->PushCollections(qw(CustomFields CustomFieldValues TicketCustomFieldValues));
169     }
170
171     # ACLs
172     $self->PushCollections(qw(ACL));
173
174     # Scrips
175     $self->PushCollections(qw(Scrips ObjectScrips ScripActions ScripConditions Templates));
176
177     # Attributes
178     $self->PushCollections(qw(Attributes));
179 }
180
181 sub PushCollections {
182     my $self  = shift;
183
184     for my $type (@_) {
185         my $class = "RT::\u$type";
186
187         $class->require or next;
188         my $collection = $class->new( RT->SystemUser );
189         $collection->FindAllRows;   # be explicit
190         $collection->CleanSlate;    # some collections (like groups and users) join in _Init
191         $collection->UnLimit;
192         $collection->OrderBy( FIELD => 'id' );
193
194         if ($self->{Clone}) {
195             if ($collection->isa('RT::Tickets')) {
196                 $collection->{allow_deleted_search} = 1;
197                 $collection->IgnoreType; # looking_at_type
198             }
199             elsif ($collection->isa('RT::ObjectCustomFieldValues')) {
200                 # FindAllRows (find_disabled_rows) isn't used by OCFVs
201                 $collection->{find_expired_rows} = 1;
202             }
203
204             if ($self->{Incremental}) {
205                 my $alias = $collection->Join(
206                     ALIAS1 => "main",
207                     FIELD1 => "id",
208                     TABLE2 => "IncrementalRecords",
209                     FIELD2 => "ObjectId",
210                 );
211                 $collection->DBIx::SearchBuilder::Limit(
212                     ALIAS => $alias,
213                     FIELD => "ObjectType",
214                     VALUE => ref($collection->NewItem),
215                 );
216             }
217         }
218
219         $self->PushObj( $collection );
220     }
221 }
222
223 sub PushBasics {
224     my $self = shift;
225
226     # System users
227     for my $name (qw/RT_System root nobody/) {
228         my $user = RT::User->new( RT->SystemUser );
229         my ($id, $msg) = $user->Load( $name );
230         warn "No '$name' user found: $msg" unless $id;
231         $self->PushObj( $user ) if $id;
232     }
233
234     # System groups
235     foreach my $name (qw(Everyone Privileged Unprivileged)) {
236         my $group = RT::Group->new( RT->SystemUser );
237         my ($id, $msg) = $group->LoadSystemInternalGroup( $name );
238         warn "No '$name' group found: $msg" unless $id;
239         $self->PushObj( $group ) if $id;
240     }
241
242     # System role groups
243     my $systemroles = RT::Groups->new( RT->SystemUser );
244     $systemroles->LimitToRolesForObject( RT->System );
245     $self->PushObj( $systemroles );
246
247     # CFs on Users, Groups, Queues
248     my $cfs = RT::CustomFields->new( RT->SystemUser );
249     $cfs->Limit(
250         FIELD => 'LookupType',
251         OPERATOR => 'IN',
252         VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
253     );
254     $self->PushObj( $cfs );
255
256     # Global attributes
257     my $attributes = RT::Attributes->new( RT->SystemUser );
258     $attributes->LimitToObject( $RT::System );
259     $self->PushObj( $attributes );
260
261     # Global ACLs
262     if ($self->{FollowACL}) {
263         my $acls = RT::ACL->new( RT->SystemUser );
264         $acls->LimitToObject( $RT::System );
265         $self->PushObj( $acls );
266     }
267
268     # Global scrips
269     if ($self->{FollowScrips}) {
270         my $scrips = RT::Scrips->new( RT->SystemUser );
271         $scrips->LimitToGlobal;
272
273         my $templates = RT::Templates->new( RT->SystemUser );
274         $templates->LimitToGlobal;
275
276         $self->PushObj( $scrips, $templates );
277         $self->PushCollections(qw(ScripActions ScripConditions));
278     }
279
280     if ($self->{AllUsers}) {
281         my $users = RT::Users->new( RT->SystemUser );
282         $users->LimitToPrivileged;
283         $self->PushObj( $users );
284     }
285
286     if ($self->{AllGroups}) {
287         my $groups = RT::Groups->new( RT->SystemUser );
288         $groups->LimitToUserDefinedGroups;
289         $self->PushObj( $groups );
290     }
291
292     if (RT::Articles->require) {
293         $self->PushCollections(qw(Topics Classes));
294     }
295
296     $self->PushCollections(qw(Queues));
297 }
298
299 sub InitStream {
300     my $self = shift;
301
302     # Write the initial metadata
303     my $meta = $self->Metadata;
304     $! = 0;
305     Storable::nstore_fd( $meta, $self->{Filehandle} );
306     die "Failed to write metadata: $!" if $!;
307
308     return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0;
309
310     my %transforms;
311     for my $v (sort cmp_version keys %RT::Migrate::Incremental::UPGRADES) {
312         for my $ref (keys %{$RT::Migrate::Incremental::UPGRADES{$v}}) {
313             push @{$transforms{$ref}}, $RT::Migrate::Incremental::UPGRADES{$v}{$ref};
314         }
315     }
316     for my $ref (keys %transforms) {
317         # XXX Does not correctly deal with updates of $classref, which
318         # should technically apply all later transforms of the _new_
319         # class.  This is not relevant in the current upgrades, as
320         # RT::ObjectCustomFieldValues do not have interesting later
321         # upgrades if you start from 3.2 (which does
322         # RT::TicketCustomFieldValues -> RT::ObjectCustomFieldValues)
323         $self->{Transform}{$ref} = sub {
324             my ($dat, $classref) = @_;
325             my @extra;
326             for my $c (@{$transforms{$ref}}) {
327                 push @extra, $c->($dat, $classref);
328                 return @extra if not $$classref;
329             }
330             return @extra;
331         };
332     }
333 }
334
335 sub NextPage {
336     my $self = shift;
337     my ($collection, $last) = @_;
338
339     $last ||= 0;
340
341     if ($self->{Clone}) {
342         # Clone provides guaranteed ordering by id and with no other id limits
343         # worry about trampling
344
345         # Use DBIx::SearchBuilder::Limit explicitly to avoid shenanigans in RT::Tickets
346         $collection->DBIx::SearchBuilder::Limit(
347             FIELD           => 'id',
348             OPERATOR        => '>',
349             VALUE           => $last,
350             ENTRYAGGREGATOR => 'none', # replaces last limit on this field
351         );
352     } else {
353         # XXX TODO: this could dig around inside the collection to see how it's
354         # limited and do the faster paging above under other conditions.
355         $self->SUPER::NextPage(@_);
356     }
357 }
358
359 sub Process {
360     my $self = shift;
361     my %args = (
362         object => undef,
363         @_
364     );
365
366     my $obj = $args{object};
367     my $uid = $obj->UID;
368
369     # Skip all dependency walking if we're cloning; go straight to
370     # visiting them.
371     if ($self->{Clone} and $uid) {
372         return if $obj->isa("RT::System");
373         $self->{progress}->($obj) if $self->{progress};
374         return $self->Visit(%args);
375     }
376
377     return $self->SUPER::Process( @_ );
378 }
379
380 sub StackSize {
381     my $self = shift;
382     return scalar @{$self->{stack}};
383 }
384
385 sub ObjectCount {
386     my $self = shift;
387     return %{ $self->{ObjectCount} };
388 }
389
390 sub Observe {
391     my $self = shift;
392     my %args = (
393         object    => undef,
394         direction => undef,
395         from      => undef,
396         @_
397     );
398
399     my $obj = $args{object};
400     my $from = $args{from};
401     if ($obj->isa("RT::Ticket")) {
402         return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
403         return $self->{FollowTickets};
404     } elsif ($obj->isa("RT::ACE")) {
405         return $self->{FollowACL};
406     } elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
407         return $self->{FollowScrips};
408     } elsif ($obj->isa("RT::GroupMember")) {
409         my $grp = $obj->GroupObj->Object;
410         if ($grp->Domain =~ /^RT::(Queue|Ticket)-Role$/) {
411             return 0 unless $grp->UID eq $from;
412         } elsif ($grp->Domain eq "SystemInternal") {
413             return 0 if $grp->UID eq $from;
414         }
415     }
416
417     return 1;
418 }
419
420 sub Visit {
421     my $self = shift;
422     my %args = (
423         object => undef,
424         @_
425     );
426
427     # Serialize it
428     my $obj = $args{object};
429     warn "Writing ".$obj->UID."\n" if $self->{Verbose};
430     my @store;
431     if ($obj->isa("RT::Migrate::Serializer::IncrementalRecord")) {
432         # These are stand-ins for record removals
433         my $class = $obj->ObjectType;
434         my %data  = ( id => $obj->ObjectId );
435         # -class is used for transforms when dropping a record
436         if ($self->{Transform}{"-$class"}) {
437             $self->{Transform}{"-$class"}->(\%data,\$class)
438         }
439         @store = (
440             $class,
441             undef,
442             \%data,
443         );
444     } elsif ($self->{Clone}) {
445         # Short-circuit and get Just The Basics, Sir if we're cloning
446         my $class = ref($obj);
447         my $uid   = $obj->UID;
448         my %data  = $obj->RT::Record::Serialize( UIDs => 0 );
449
450         # +class is used when seeing a record of one class might insert
451         # a separate record into the stream
452         if ($self->{Transform}{"+$class"}) {
453             my @extra = $self->{Transform}{"+$class"}->(\%data,\$class);
454             for my $e (@extra) {
455                 $! = 0;
456                 Storable::nstore_fd($e, $self->{Filehandle});
457                 die "Failed to write: $!" if $!;
458                 $self->{ObjectCount}{$e->[0]}++;
459             }
460         }
461
462         # Upgrade the record if necessary
463         if ($self->{Transform}{$class}) {
464             $self->{Transform}{$class}->(\%data,\$class);
465         }
466
467         # Transforms set $class to undef to drop the record
468         return unless $class;
469
470         @store = (
471             $class,
472             $uid,
473             \%data,
474         );
475     } else {
476         @store = (
477             ref($obj),
478             $obj->UID,
479             { $obj->Serialize },
480         );
481     }
482
483     # Write it out; nstore_fd doesn't trap failures to write, so we have
484     # to; by clearing $! and checking it afterwards.
485     $! = 0;
486     Storable::nstore_fd(\@store, $self->{Filehandle});
487     die "Failed to write: $!" if $!;
488
489     $self->{ObjectCount}{$store[0]}++;
490 }
491
492 1;