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