rt 4.2.14 (#13852)
[freeside.git] / rt / lib / RT / Migrate / Importer.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2017 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::Importer;
50
51 use strict;
52 use warnings;
53
54 use Storable qw//;
55 use File::Spec;
56 use Carp qw/carp/;
57
58 sub new {
59     my $class = shift;
60     my $self = bless {}, $class;
61     $self->Init(@_);
62     return $self;
63 }
64
65 sub Init {
66     my $self = shift;
67     my %args = (
68         OriginalId          => undef,
69         Progress            => undef,
70         Statefile           => undef,
71         DumpObjects         => undef,
72         HandleError         => undef,
73         ExcludeOrganization => undef,
74         @_,
75     );
76
77     # Should we attempt to preserve record IDs as they are created?
78     $self->{OriginalId} = $args{OriginalId};
79
80     $self->{ExcludeOrganization} = $args{ExcludeOrganization};
81
82     $self->{Progress} = $args{Progress};
83
84     $self->{HandleError} = sub { 0 };
85     $self->{HandleError} = $args{HandleError}
86         if $args{HandleError} and ref $args{HandleError} eq 'CODE';
87
88     if ($args{DumpObjects}) {
89         require Data::Dumper;
90         $self->{DumpObjects} = { map { $_ => 1 } @{$args{DumpObjects}} };
91     }
92
93     # Objects we've created
94     $self->{UIDs} = {};
95
96     # Columns we need to update when an object is later created
97     $self->{Pending} = {};
98
99     # Objects missing from the source database before serialization
100     $self->{Invalid} = [];
101
102     # What we created
103     $self->{ObjectCount} = {};
104
105     # To know what global CFs need to be unglobal'd and applied to what
106     $self->{NewQueues} = [];
107     $self->{NewCFs} = [];
108 }
109
110 sub Metadata {
111     my $self = shift;
112     return $self->{Metadata};
113 }
114
115 sub LoadMetadata {
116     my $self = shift;
117     my ($data) = @_;
118
119     return if $self->{Metadata};
120     $self->{Metadata} = $data;
121
122     die "Incompatible format version: ".$data->{Format}
123         if $data->{Format} ne "0.8";
124
125     $self->{Organization} = $data->{Organization};
126     $self->{Clone}        = $data->{Clone};
127     $self->{Incremental}  = $data->{Incremental};
128     $self->{Files}        = $data->{Files} if $data->{Final};
129 }
130
131 sub InitStream {
132     my $self = shift;
133
134     die "Stream initialized after objects have been recieved!"
135         if keys %{ $self->{UIDs} };
136
137     die "Cloning does not support importing the Original Id separately\n"
138         if $self->{OriginalId} and $self->{Clone};
139
140     die "RT already contains data; overwriting will not work\n"
141         if ($self->{Clone} and not $self->{Incremental})
142             and RT->SystemUser->Id;
143
144     # Basic facts of life, as a safety net
145     $self->Resolve( RT->System->UID => ref RT->System, RT->System->Id );
146     $self->SkipTransactions( RT->System->UID );
147
148     if ($self->{OriginalId}) {
149         # Where to shove the original ticket ID
150         my $cf = RT::CustomField->new( RT->SystemUser );
151         $cf->LoadByName( Name => $self->{OriginalId}, LookupType => RT::Ticket->CustomFieldLookupType, ObjectId => 0 );
152         unless ($cf->Id) {
153             warn "Failed to find global CF named $self->{OriginalId} -- creating one";
154             $cf->Create(
155                 Queue => 0,
156                 Name  => $self->{OriginalId},
157                 Type  => 'FreeformSingle',
158             );
159         }
160     }
161 }
162
163 sub Resolve {
164     my $self = shift;
165     my ($uid, $class, $id) = @_;
166     $self->{UIDs}{$uid} = [ $class, $id ];
167     return unless $self->{Pending}{$uid};
168
169     for my $ref (@{$self->{Pending}{$uid}}) {
170         my ($pclass, $pid) = @{ $self->Lookup( $ref->{uid} ) };
171         my $obj = $pclass->new( RT->SystemUser );
172         $obj->LoadByCols( Id => $pid );
173         $obj->__Set(
174             Field => $ref->{column},
175             Value => $id,
176         ) if defined $ref->{column};
177         $obj->__Set(
178             Field => $ref->{classcolumn},
179             Value => $class,
180         ) if defined $ref->{classcolumn};
181         $obj->__Set(
182             Field => $ref->{uri},
183             Value => $self->LookupObj($uid)->URI,
184         ) if defined $ref->{uri};
185         if (my $method = $ref->{method}) {
186             $obj->$method($self, $ref, $class, $id);
187         }
188     }
189     delete $self->{Pending}{$uid};
190 }
191
192 sub Lookup {
193     my $self = shift;
194     my ($uid) = @_;
195     unless (defined $uid) {
196         carp "Tried to lookup an undefined UID";
197         return;
198     }
199     return $self->{UIDs}{$uid};
200 }
201
202 sub LookupObj {
203     my $self = shift;
204     my ($uid) = @_;
205     my $ref = $self->Lookup( $uid );
206     return unless $ref;
207     my ($class, $id) = @{ $ref };
208
209     my $obj = $class->new( RT->SystemUser );
210     $obj->Load( $id );
211     return $obj;
212 }
213
214 sub Postpone {
215     my $self = shift;
216     my %args = (
217         for         => undef,
218         uid         => undef,
219         column      => undef,
220         classcolumn => undef,
221         uri         => undef,
222         @_,
223     );
224     my $uid = delete $args{for};
225
226     if (defined $uid) {
227         push @{$self->{Pending}{$uid}}, \%args;
228     } else {
229         push @{$self->{Invalid}}, \%args;
230     }
231 }
232
233 sub SkipTransactions {
234     my $self = shift;
235     my ($uid) = @_;
236     return if $self->{Clone};
237     $self->{SkipTransactions}{$uid} = 1;
238 }
239
240 sub ShouldSkipTransaction {
241     my $self = shift;
242     my ($uid) = @_;
243     return exists $self->{SkipTransactions}{$uid};
244 }
245
246 sub MergeValues {
247     my $self = shift;
248     my ($obj, $data) = @_;
249     for my $col (keys %{$data}) {
250         next if defined $obj->__Value($col) and length $obj->__Value($col);
251         next unless defined $data->{$col} and length $data->{$col};
252
253         if (ref $data->{$col}) {
254             my $uid = ${ $data->{$col} };
255             my $ref = $self->Lookup( $uid );
256             if ($ref) {
257                 $data->{$col} = $ref->[1];
258             } else {
259                 $self->Postpone(
260                     for => $obj->UID,
261                     uid => $uid,
262                     column => $col,
263                 );
264                 next;
265             }
266         }
267         $obj->__Set( Field => $col, Value => $data->{$col} );
268     }
269 }
270
271 sub SkipBy {
272     my $self = shift;
273     my ($column, $class, $uid, $data) = @_;
274
275     my $obj = $class->new( RT->SystemUser );
276     $obj->Load( $data->{$column} );
277     return unless $obj->Id;
278
279     $self->SkipTransactions( $uid );
280
281     $self->Resolve( $uid => $class => $obj->Id );
282     return $obj;
283 }
284
285 sub MergeBy {
286     my $self = shift;
287     my ($column, $class, $uid, $data) = @_;
288
289     my $obj = $self->SkipBy(@_);
290     return unless $obj;
291     $self->MergeValues( $obj, $data );
292     return 1;
293 }
294
295 sub Qualify {
296     my $self = shift;
297     my ($string) = @_;
298     return $string if $self->{Clone};
299     return $string if not defined $self->{Organization};
300     return $string if $self->{ExcludeOrganization};
301     return $string if $self->{Organization} eq $RT::Organization;
302     return $self->{Organization}.": $string";
303 }
304
305 sub Create {
306     my $self = shift;
307     my ($class, $uid, $data) = @_;
308
309     # Use a simpler pre-inflation if we're cloning
310     if ($self->{Clone}) {
311         $class->RT::Record::PreInflate( $self, $uid, $data );
312     } else {
313         # Non-cloning always wants to make its own id
314         delete $data->{id};
315         return unless $class->PreInflate( $self, $uid, $data );
316     }
317
318     my $obj = $class->new( RT->SystemUser );
319     my ($id, $msg) = eval {
320         # catch and rethrow on the outside so we can provide more info
321         local $SIG{__DIE__};
322         $obj->DBIx::SearchBuilder::Record::Create(
323             %{$data}
324         );
325     };
326     if (not $id or $@) {
327         $msg ||= ''; # avoid undef
328         my $err = "Failed to create $uid: $msg $@\n" . Data::Dumper::Dumper($data) . "\n";
329         if (not $self->{HandleError}->($self, $err)) {
330             die $err;
331         } else {
332             return;
333         }
334     }
335
336     $self->{ObjectCount}{$class}++;
337     $self->Resolve( $uid => $class, $id );
338
339     # Load it back to get real values into the columns
340     $obj = $class->new( RT->SystemUser );
341     $obj->Load( $id );
342     $obj->PostInflate( $self, $uid );
343
344     return $obj;
345 }
346
347 sub ReadStream {
348     my $self = shift;
349     my ($fh) = @_;
350
351     no warnings 'redefine';
352     local *RT::Ticket::Load = sub {
353         my $self = shift;
354         my $id   = shift;
355         $self->LoadById( $id );
356         return $self->Id;
357     };
358
359     my $loaded = Storable::fd_retrieve($fh);
360
361     # Metadata is stored at the start of the stream as a hashref
362     if (ref $loaded eq "HASH") {
363         $self->LoadMetadata( $loaded );
364         $self->InitStream;
365         return;
366     }
367
368     my ($class, $uid, $data) = @{$loaded};
369
370     if ($self->{Incremental}) {
371         my $obj = $class->new( RT->SystemUser );
372         $obj->Load( $data->{id} );
373         if (not $uid) {
374             # undef $uid means "delete it"
375             $obj->Delete;
376             $self->{ObjectCount}{$class}++;
377         } elsif ( $obj->Id ) {
378             # If it exists, update it
379             $class->RT::Record::PreInflate( $self, $uid, $data );
380             $obj->__Set( Field => $_, Value => $data->{$_} )
381                 for keys %{ $data };
382             $self->{ObjectCount}{$class}++;
383         } else {
384             # Otherwise, make it
385             $obj = $self->Create( $class, $uid, $data );
386         }
387         $self->{Progress}->($obj) if $obj and $self->{Progress};
388         return;
389     } elsif ($self->{Clone}) {
390         my $obj = $self->Create( $class, $uid, $data );
391         $self->{Progress}->($obj) if $obj and $self->{Progress};
392         return;
393     }
394
395     # If it's a queue, store its ID away, as we'll need to know
396     # it to split global CFs into non-global across those
397     # fields.  We do this before inflating, so that queues which
398     # got merged still get the CFs applied
399     push @{$self->{NewQueues}}, $uid
400         if $class eq "RT::Queue";
401
402     my $origid = $data->{id};
403     my $obj = $self->Create( $class, $uid, $data );
404     return unless $obj;
405
406     # If it's a ticket, we might need to create a
407     # TicketCustomField for the previous ID
408     if ($class eq "RT::Ticket" and $self->{OriginalId}) {
409         my $value = $self->{ExcludeOrganization}
410                   ? $origid
411                   : $self->Organization . ":$origid";
412
413         my ($id, $msg) = $obj->AddCustomFieldValue(
414             Field             => $self->{OriginalId},
415             Value             => $value,
416             RecordTransaction => 0,
417         );
418         warn "Failed to add custom field to $uid: $msg"
419             unless $id;
420     }
421
422     # If it's a CF, we don't know yet if it's global (the OCF
423     # hasn't been created yet) to store away the CF for later
424     # inspection
425     push @{$self->{NewCFs}}, $uid
426         if $class eq "RT::CustomField"
427             and $obj->LookupType =~ /^RT::Queue/;
428
429     $self->{Progress}->($obj) if $self->{Progress};
430 }
431
432 sub CloseStream {
433     my $self = shift;
434
435     $self->{Progress}->(undef, 'force') if $self->{Progress};
436
437     return if $self->{Clone};
438
439     # Take global CFs which we made and make them un-global
440     my @queues = grep {$_} map {$self->LookupObj( $_ )} @{$self->{NewQueues}};
441     for my $obj (map {$self->LookupObj( $_ )} @{$self->{NewCFs}}) {
442         my $ocf = $obj->IsGlobal or next;
443         $ocf->Delete;
444         $obj->AddToObject( $_ ) for @queues;
445     }
446     $self->{NewQueues} = [];
447     $self->{NewCFs} = [];
448 }
449
450
451 sub ObjectCount {
452     my $self = shift;
453     return %{ $self->{ObjectCount} };
454 }
455
456 sub Missing {
457     my $self = shift;
458     return wantarray ? sort keys %{ $self->{Pending} }
459         : keys %{ $self->{Pending} };
460 }
461
462 sub Invalid {
463     my $self = shift;
464     return wantarray ? sort { $a->{uid} cmp $b->{uid} } @{ $self->{Invalid} }
465                      : $self->{Invalid};
466 }
467
468 sub Organization {
469     my $self = shift;
470     return $self->{Organization};
471 }
472
473 sub Progress {
474     my $self = shift;
475     return defined $self->{Progress} unless @_;
476     return $self->{Progress} = $_[0];
477 }
478
479 1;