import rt 3.8.10
[freeside.git] / rt / lib / RT / Handle.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 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 =head1 NAME
50
51 RT::Handle - RT's database handle
52
53 =head1 SYNOPSIS
54
55     use RT;
56     BEGIN { RT::LoadConfig() };
57     use RT::Handle;
58
59 =head1 DESCRIPTION
60
61 C<RT::Handle> is RT specific wrapper over one of L<DBIx::SearchBuilder::Handle>
62 classes. As RT works with different types of DBs we subclass repsective handler
63 from L<DBIx::SerachBuilder>. Type of the DB is defined by C<DatabasseType> RT's
64 config option. You B<must> load this module only when the configs have been
65 loaded.
66
67 =cut
68
69 package RT::Handle;
70
71 use strict;
72 use warnings;
73 use vars qw/@ISA/;
74
75 =head1 METHODS
76
77 =head2 FinalizeDatabaseType
78
79 Sets RT::Handle's superclass to the correct subclass of
80 L<DBIx::SearchBuilder::Handle>, using the C<DatabaseType> configuration.
81
82 =cut
83
84 sub FinalizeDatabaseType {
85     eval {
86         use base "DBIx::SearchBuilder::Handle::". RT->Config->Get('DatabaseType');
87     };
88
89     if ($@) {
90         die "Unable to load DBIx::SearchBuilder database handle for '". RT->Config->Get('DatabaseType') ."'.\n".
91             "Perhaps you've picked an invalid database type or spelled it incorrectly.\n".
92             $@;
93     }
94 }
95
96 =head2 Connect
97
98 Connects to RT's database using credentials and options from the RT config.
99 Takes nothing.
100
101 =cut
102
103 sub Connect {
104     my $self = shift;
105
106     my $db_type = RT->Config->Get('DatabaseType');
107     if ( $db_type eq 'Oracle' ) {
108         $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8";
109         $ENV{'NLS_NCHAR'} = "AL32UTF8";
110     }
111
112     $self->SUPER::Connect(
113         User => RT->Config->Get('DatabaseUser'),
114         Password => RT->Config->Get('DatabasePassword'),
115     );
116
117     if ( $db_type eq 'mysql' ) {
118         my $version = $self->DatabaseVersion;
119         ($version) = $version =~ /^(\d+\.\d+)/;
120         $self->dbh->do("SET NAMES 'utf8'") if $version >= 4.1;
121     }
122
123
124     if ( $db_type eq 'Pg' ) {
125         my $version = $self->DatabaseVersion;
126         ($version) = $version =~ /^(\d+\.\d+)/;
127         $self->dbh->do("SET bytea_output = 'escape'") if $version >= 9.0;
128     }
129
130
131
132     $self->dbh->{'LongReadLen'} = RT->Config->Get('MaxAttachmentSize');
133 }
134
135 =head2 BuildDSN
136
137 Build the DSN for the RT database. Doesn't take any parameters, draws all that
138 from the config.
139
140 =cut
141
142 require File::Spec;
143
144 sub BuildDSN {
145     my $self = shift;
146     # Unless the database port is a positive integer, we really don't want to pass it.
147     my $db_port = RT->Config->Get('DatabasePort');
148     $db_port = undef unless (defined $db_port && $db_port =~ /^(\d+)$/);
149     my $db_host = RT->Config->Get('DatabaseHost');
150     $db_host = undef unless $db_host;
151     my $db_name = RT->Config->Get('DatabaseName');
152     my $db_type = RT->Config->Get('DatabaseType');
153     $db_name = File::Spec->catfile($RT::VarPath, $db_name)
154         if $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name);
155
156     my %args = (
157         Host       => $db_host,
158         Database   => $db_name,
159         Port       => $db_port,
160         Driver     => $db_type,
161         RequireSSL => RT->Config->Get('DatabaseRequireSSL'),
162         DisconnectHandleOnDestroy => 1,
163     );
164     if ( $db_type eq 'Oracle' && $db_host ) {
165         $args{'SID'} = delete $args{'Database'};
166     }
167     $self->SUPER::BuildDSN( %args );
168 }
169
170 =head2 DSN
171
172 Returns the DSN for this handle. In order to get correct value you must
173 build DSN first, see L</BuildDSN>.
174
175 This is method can be called as class method, in this case creates
176 temporary handle object, L</BuildDSN builds DSN> and returns it.
177
178 =cut
179
180 sub DSN {
181     my $self = shift;
182     return $self->SUPER::DSN if ref $self;
183
184     my $handle = $self->new;
185     $handle->BuildDSN;
186     return $handle->DSN;
187 }
188
189 =head2 SystemDSN
190
191 Returns a DSN suitable for database creates and drops
192 and user creates and drops.
193
194 Gets RT's DSN first (see L<DSN>) and then change it according
195 to requirements of a database system RT's using.
196
197 =cut
198
199 sub SystemDSN {
200     my $self = shift;
201
202     my $db_name = RT->Config->Get('DatabaseName');
203     my $db_type = RT->Config->Get('DatabaseType');
204
205     my $dsn = $self->DSN;
206     if ( $db_type eq 'mysql' ) {
207         # with mysql, you want to connect sans database to funge things
208         $dsn =~ s/dbname=\Q$db_name//;
209     }
210     elsif ( $db_type eq 'Pg' ) {
211         # with postgres, you want to connect to template1 database
212         $dsn =~ s/dbname=\Q$db_name/dbname=template1/;
213     }
214     elsif ( $db_type eq 'Informix' ) {
215         # with Informix, you want to connect sans database:
216         $dsn =~ s/Informix:\Q$db_name/Informix:/;
217     }
218     return $dsn;
219 }
220
221 =head2 Database compatibility and integrity checks
222
223
224
225 =cut
226
227 sub CheckIntegrity {
228     my $self = shift;
229     
230     my $dsn = $self->DSN;
231     my $user = RT->Config->Get('DatabaseUser');
232     my $pass = RT->Config->Get('DatabasePassword');
233
234     my $dbh = DBI->connect(
235         $dsn, $user, $pass,
236         { RaiseError => 0, PrintError => 0 },
237     );
238     unless ( $dbh ) {
239         return (0, 'no connection', "Failed to connect to $dsn as user '$user': ". $DBI::errstr);
240     }
241
242     RT::ConnectToDatabase();
243     RT::InitLogging();
244
245     require RT::CurrentUser;
246     my $test_user = new RT::CurrentUser;
247     $test_user->Load('RT_System');
248     unless ( $test_user->id ) {
249         return (0, 'no system user', "Couldn't find RT_System user in the DB '$dsn'");
250     }
251
252     $test_user = new RT::CurrentUser;
253     $test_user->Load('Nobody');
254     unless ( $test_user->id ) {
255         return (0, 'no nobody user', "Couldn't find Nobody user in the DB '$dsn'");
256     }
257
258     return $dbh;
259 }
260
261 sub CheckCompatibility {
262     my $self = shift;
263     my $dbh = shift;
264     my $state = shift || 'post';
265
266     my $db_type = RT->Config->Get('DatabaseType');
267     if ( $db_type eq "mysql" ) {
268         # Check which version we're running
269         my $version = ($dbh->selectrow_array("show variables like 'version'"))[1];
270         return (0, "couldn't get version of the mysql server")
271             unless $version;
272
273         ($version) = $version =~ /^(\d+\.\d+)/;
274         return (0, "RT is unsupported on MySQL versions before 4.0.x, it's $version")
275             if $version < 4;
276
277         # MySQL must have InnoDB support
278         my $innodb = ($dbh->selectrow_array("show variables like 'have_innodb'"))[1];
279         if ( lc $innodb eq "no" ) {
280             return (0, "RT requires that MySQL be compiled with InnoDB table support.\n".
281                 "See http://dev.mysql.com/doc/mysql/en/InnoDB.html");
282         } elsif ( lc $innodb eq "disabled" ) {
283             return (0, "RT requires that MySQL InnoDB table support be enabled.\n".
284                 "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
285         }
286
287         if ( $state eq 'post' ) {
288             my $create_table = $dbh->selectrow_arrayref("SHOW CREATE TABLE Tickets")->[1];
289             unless ( $create_table =~ /(?:ENGINE|TYPE)\s*=\s*InnoDB/i ) {
290                 return (0, "RT requires that all its tables be of InnoDB type. Upgrade RT tables.");
291             }
292         }
293         if ( $version >= 4.1 && $state eq 'post' ) {
294             my $create_table = $dbh->selectrow_arrayref("SHOW CREATE TABLE Attachments")->[1];
295             unless ( $create_table =~ /\bContent\b[^,]*BLOB/i ) {
296                 return (0, "RT since version 3.8 has new schema for MySQL versions after 4.1.0\n"
297                     ."Follow instructions in the UPGRADING.mysql file.");
298             }
299         }
300     }
301     return (1)
302 }
303
304 =head2 Database maintanance
305
306 =head3 CreateDatabase $DBH
307
308 Creates a new database. This method can be used as class method.
309
310 Takes DBI handle. Many database systems require special handle to
311 allow you to create a new database, so you have to use L<SystemDSN>
312 method during connection.
313
314 Fetches type and name of the DB from the config.
315
316 =cut
317
318 sub CreateDatabase {
319     my $self = shift;
320     my $dbh  = shift or return (0, "No DBI handle provided");
321     my $db_type = RT->Config->Get('DatabaseType');
322     my $db_name = RT->Config->Get('DatabaseName');
323
324     my $status;
325     if ( $db_type eq 'SQLite' ) {
326         return (1, 'Skipped as SQLite doesn\'t need any action');
327     }
328     elsif ( $db_type eq 'Oracle' ) {
329         my $db_user = RT->Config->Get('DatabaseUser');
330         my $db_pass = RT->Config->Get('DatabasePassword');
331         $status = $dbh->do(
332             "CREATE USER $db_user IDENTIFIED BY $db_pass"
333             ." default tablespace USERS"
334             ." temporary tablespace TEMP"
335             ." quota unlimited on USERS"
336         );
337         unless ( $status ) {
338             return $status, "Couldn't create user $db_user identified by $db_pass."
339                 ."\nError: ". $dbh->errstr;
340         }
341         $status = $dbh->do( "GRANT connect, resource TO $db_user" );
342         unless ( $status ) {
343             return $status, "Couldn't grant connect and resource to $db_user."
344                 ."\nError: ". $dbh->errstr;
345         }
346         return (1, "Created user $db_user. All RT's objects should be in his schema.");
347     }
348     elsif ( $db_type eq 'Pg' ) {
349         # XXX: as we get external DBH we don't know if RaiseError or PrintError
350         # are enabled, so we have to setup it here and restore them back
351         $status = $dbh->do("CREATE DATABASE $db_name WITH ENCODING='UNICODE' TEMPLATE template0")
352             || $dbh->do("CREATE DATABASE $db_name TEMPLATE template0");
353     }
354     elsif ( $db_type eq 'Informix' ) {
355         local $ENV{'DB_LOCALE'} = 'en_us.utf8';
356         $status = $dbh->do("CREATE DATABASE $db_name WITH BUFFERED LOG");
357     }
358     else {
359         $status = $dbh->do("CREATE DATABASE $db_name");
360     }
361     return ($status, $DBI::errstr);
362 }
363
364 =head3 DropDatabase $DBH [Force => 0]
365
366 Drops RT's database. This method can be used as class method.
367
368 Takes DBI handle as first argument. Many database systems require
369 special handle to allow you to create a new database, so you have
370 to use L<SystemDSN> method during connection.
371
372 Fetches type and name of the DB from the config.
373
374 =cut
375
376 sub DropDatabase {
377     my $self = shift;
378     my $dbh  = shift or return (0, "No DBI handle provided");
379
380     my $db_type = RT->Config->Get('DatabaseType');
381     my $db_name = RT->Config->Get('DatabaseName');
382
383     if ( $db_type eq 'Oracle' || $db_type eq 'Informix' ) {
384         my $db_user = RT->Config->Get('DatabaseUser');
385         my $status = $dbh->do( "DROP USER $db_user CASCADE" );
386         unless ( $status ) {
387             return 0, "Couldn't drop user $db_user."
388                 ."\nError: ". $dbh->errstr;
389         }
390         return (1, "Successfully dropped user '$db_user' with his schema.");
391     }
392     elsif ( $db_type eq 'SQLite' ) {
393         my $path = $db_name;
394         $path = "$RT::VarPath/$path" unless substr($path, 0, 1) eq '/';
395         unlink $path or return (0, "Couldn't remove '$path': $!");
396         return (1);
397     } else {
398         $dbh->do("DROP DATABASE ". $db_name)
399             or return (0, $DBI::errstr);
400     }
401     return (1);
402 }
403
404 =head2 InsertACL
405
406 =cut
407
408 sub InsertACL {
409     my $self      = shift;
410     my $dbh       = shift;
411     my $base_path = shift || $RT::EtcPath;
412
413     my $db_type = RT->Config->Get('DatabaseType');
414     return (1) if $db_type eq 'SQLite';
415
416     $dbh = $self->dbh if !$dbh && ref $self;
417     return (0, "No DBI handle provided") unless $dbh;
418
419     return (0, "'$base_path' doesn't exist") unless -e $base_path;
420
421     my $path;
422     if ( -d $base_path ) {
423         $path = File::Spec->catfile( $base_path, "acl.$db_type");
424         $path = $self->GetVersionFile($dbh, $path);
425
426         $path = File::Spec->catfile( $base_path, "acl")
427             unless $path && -e $path;
428         return (0, "Couldn't find ACLs for $db_type")
429             unless -e $path;
430     } else {
431         $path = $base_path;
432     }
433
434     local *acl;
435     do $path || return (0, "Couldn't load ACLs: " . $@);
436     my @acl = acl($dbh);
437     foreach my $statement (@acl) {
438         my $sth = $dbh->prepare($statement)
439             or return (0, "Couldn't prepare SQL query:\n $statement\n\nERROR: ". $dbh->errstr);
440         unless ( $sth->execute ) {
441             return (0, "Couldn't run SQL query:\n $statement\n\nERROR: ". $sth->errstr);
442         }
443     }
444     return (1);
445 }
446
447 =head2 InsertSchema
448
449 =cut
450
451 sub InsertSchema {
452     my $self = shift;
453     my $dbh  = shift;
454     my $base_path = (shift || $RT::EtcPath);
455
456     $dbh = $self->dbh if !$dbh && ref $self;
457     return (0, "No DBI handle provided") unless $dbh;
458
459     my $db_type = RT->Config->Get('DatabaseType');
460
461     my $file;
462     if ( -d $base_path ) {
463         $file = $base_path . "/schema." . $db_type;
464     } else {
465         $file = $base_path;
466     }
467
468     $file = $self->GetVersionFile( $dbh, $file );
469     unless ( $file ) {
470         return (0, "Couldn't find schema file(s) '$file*'");
471     }
472     unless ( -f $file && -r $file ) {
473         return (0, "File '$file' doesn't exist or couldn't be read");
474     }
475
476     my (@schema);
477
478     open( my $fh_schema, '<', $file ) or die $!;
479
480     my $has_local = 0;
481     open( my $fh_schema_local, "<", $self->GetVersionFile( $dbh, $RT::LocalEtcPath . "/schema." . $db_type ))
482         and $has_local = 1;
483
484     my $statement = "";
485     foreach my $line ( <$fh_schema>, ($_ = ';;'), $has_local? <$fh_schema_local>: () ) {
486         $line =~ s/\#.*//g;
487         $line =~ s/--.*//g;
488         $statement .= $line;
489         if ( $line =~ /;(\s*)$/ ) {
490             $statement =~ s/;(\s*)$//g;
491             push @schema, $statement;
492             $statement = "";
493         }
494     }
495     close $fh_schema; close $fh_schema_local;
496
497     if ( $db_type eq 'Oracle' ) {
498         my $db_user = RT->Config->Get('DatabaseUser');
499         my $status = $dbh->do( "ALTER SESSION SET CURRENT_SCHEMA=$db_user" );
500         unless ( $status ) {
501             return $status, "Couldn't set current schema to $db_user."
502                 ."\nError: ". $dbh->errstr;
503         }
504     }
505
506     local $SIG{__WARN__} = sub {};
507     my $is_local = 0;
508     $dbh->begin_work or return (0, "Couldn't begin transaction: ". $dbh->errstr);
509     foreach my $statement (@schema) {
510         if ( $statement =~ /^\s*;$/ ) {
511             $is_local = 1; next;
512         }
513
514         my $sth = $dbh->prepare($statement)
515             or return (0, "Couldn't prepare SQL query:\n$statement\n\nERROR: ". $dbh->errstr);
516         unless ( $sth->execute or $is_local ) {
517             return (0, "Couldn't run SQL query:\n$statement\n\nERROR: ". $sth->errstr);
518         }
519     }
520     $dbh->commit or return (0, "Couldn't commit transaction: ". $dbh->errstr);
521     return (1);
522 }
523
524 =head1 GetVersionFile
525
526 Takes base name of the file as argument, scans for <base name>-<version> named
527 files and returns file name with closest version to the version of the RT DB.
528
529 =cut
530
531 sub GetVersionFile {
532     my $self = shift;
533     my $dbh = shift;
534     my $base_name = shift;
535
536     my $db_version = ref $self
537         ? $self->DatabaseVersion
538         : do {
539             my $tmp = RT::Handle->new;
540             $tmp->dbh($dbh);
541             $tmp->DatabaseVersion;
542         };
543
544     require File::Glob;
545     my @files = File::Glob::bsd_glob("$base_name*");
546     return '' unless @files;
547
548     my %version = map { $_ =~ /\.\w+-([-\w\.]+)$/; ($1||0) => $_ } @files;
549     my $version;
550     foreach ( reverse sort cmp_version keys %version ) {
551         if ( cmp_version( $db_version, $_ ) >= 0 ) {
552             $version = $_;
553             last;
554         }
555     }
556
557     return defined $version? $version{ $version } : undef;
558 }
559
560 sub cmp_version($$) {
561     my ($a, $b) = (@_);
562     $b =~ s/HEAD$/9999/;
563     my @a = split /[^0-9]+/, $a;
564     my @b = split /[^0-9]+/, $b;
565     for ( my $i = 0; $i < @a; $i++ ) {
566         return 1 unless defined $b[$i];
567         return $a[$i] <=> $b[$i] if $a[$i] <=> $b[$i];
568     }
569     return 0 if @a == @b;
570     return -1;
571 }
572
573
574 =head2 InsertInitialData
575
576 Inserts system objects into RT's DB, like system user or 'nobody',
577 internal groups and other records required. However, this method
578 doesn't insert any real users like 'root' and you have to use
579 InsertData or another way to do that.
580
581 Takes no arguments. Returns status and message tuple.
582
583 It's safe to call this method even if those objects already exist.
584
585 =cut
586
587 sub InsertInitialData {
588     my $self    = shift;
589
590     my @warns;
591
592     # create RT_System user and grant him rights
593     {
594         require RT::CurrentUser;
595
596         my $test_user = RT::User->new( new RT::CurrentUser );
597         $test_user->Load('RT_System');
598         if ( $test_user->id ) {
599             push @warns, "Found system user in the DB.";
600         }
601         else {
602             my $user = RT::User->new( new RT::CurrentUser );
603             my ( $val, $msg ) = $user->_BootstrapCreate(
604                 Name     => 'RT_System',
605                 RealName => 'The RT System itself',
606                 Comments => 'Do not delete or modify this user. '
607                     . 'It is integral to RT\'s internal database structures',
608                 Creator  => '1',
609                 LastUpdatedBy => '1',
610             );
611             return ($val, $msg) unless $val;
612         }
613         DBIx::SearchBuilder::Record::Cachable->FlushCache;
614     }
615
616     # init RT::SystemUser and RT::System objects
617     RT::InitSystemObjects();
618     unless ( $RT::SystemUser->id ) {
619         return (0, "Couldn't load system user");
620     }
621
622     # grant SuperUser right to system user
623     {
624         my $test_ace = RT::ACE->new( $RT::SystemUser );
625         $test_ace->LoadByCols(
626             PrincipalId   => ACLEquivGroupId( $RT::SystemUser->Id ),
627             PrincipalType => 'Group',
628             RightName     => 'SuperUser',
629             ObjectType    => 'RT::System',
630             ObjectId      => 1,
631         );
632         if ( $test_ace->id ) {
633             push @warns, "System user has global SuperUser right.";
634         } else {
635             my $ace = RT::ACE->new( $RT::SystemUser );
636             my ( $val, $msg ) = $ace->_BootstrapCreate(
637                 PrincipalId   => ACLEquivGroupId( $RT::SystemUser->Id ),
638                 PrincipalType => 'Group',
639                 RightName     => 'SuperUser',
640                 ObjectType    => 'RT::System',
641                 ObjectId      => 1,
642             );
643             return ($val, $msg) unless $val;
644         }
645         DBIx::SearchBuilder::Record::Cachable->FlushCache;
646     }
647
648     # system groups
649     # $self->loc('Everyone'); # For the string extractor to get a string to localize
650     # $self->loc('Privileged'); # For the string extractor to get a string to localize
651     # $self->loc('Unprivileged'); # For the string extractor to get a string to localize
652     foreach my $name (qw(Everyone Privileged Unprivileged)) {
653         my $group = RT::Group->new( $RT::SystemUser );
654         $group->LoadSystemInternalGroup( $name );
655         if ( $group->id ) {
656             push @warns, "System group '$name' already exists.";
657             next;
658         }
659
660         $group = RT::Group->new( $RT::SystemUser );
661         my ( $val, $msg ) = $group->_Create(
662             Type        => $name,
663             Domain      => 'SystemInternal',
664             Description => 'Pseudogroup for internal use',  # loc
665             Name        => '',
666             Instance    => '',
667         );
668         return ($val, $msg) unless $val;
669     }
670
671     # nobody
672     {
673         my $user = RT::User->new( $RT::SystemUser );
674         $user->Load('Nobody');
675         if ( $user->id ) {
676             push @warns, "Found 'Nobody' user in the DB.";
677         }
678         else {
679             my ( $val, $msg ) = $user->Create(
680                 Name     => 'Nobody',
681                 RealName => 'Nobody in particular',
682                 Comments => 'Do not delete or modify this user. It is integral '
683                     .'to RT\'s internal data structures',
684                 Privileged => 0,
685             );
686             return ($val, $msg) unless $val;
687         }
688
689         if ( $user->HasRight( Right => 'OwnTicket', Object => $RT::System ) ) {
690             push @warns, "User 'Nobody' has global OwnTicket right.";
691         } else {
692             my ( $val, $msg ) = $user->PrincipalObj->GrantRight(
693                 Right => 'OwnTicket',
694                 Object => $RT::System,
695             );
696             return ($val, $msg) unless $val;
697         }
698     }
699
700     # rerun to get init Nobody as well
701     RT::InitSystemObjects();
702
703     # system role groups
704     foreach my $name (qw(Owner Requestor Cc AdminCc)) {
705         my $group = RT::Group->new( $RT::SystemUser );
706         $group->LoadSystemRoleGroup( $name );
707         if ( $group->id ) {
708             push @warns, "System role '$name' already exists.";
709             next;
710         }
711
712         $group = RT::Group->new( $RT::SystemUser );
713         my ( $val, $msg ) = $group->_Create(
714             Type        => $name,
715             Domain      => 'RT::System-Role',
716             Description => 'SystemRolegroup for internal use',  # loc
717             Name        => '',
718             Instance    => '',
719         );
720         return ($val, $msg) unless $val;
721     }
722
723     push @warns, "You appear to have a functional RT database."
724         if @warns;
725
726     return (1, join "\n", @warns);
727 }
728
729 =head2 InsertData
730
731 Load some sort of data into the database, takes path to a file.
732
733 =cut
734
735 sub InsertData {
736     my $self     = shift;
737     my $datafile = shift;
738
739     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
740     our (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
741            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
742     local (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
743            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
744
745     local $@;
746     $RT::Logger->debug("Going to load '$datafile' data file");
747     eval { require $datafile }
748       or return (0, "Couldn't load data from '$datafile' for import:\n\nERROR:". $@);
749
750     if ( @Initial ) {
751         $RT::Logger->debug("Running initial actions...");
752         foreach ( @Initial ) {
753             local $@;
754             eval { $_->(); 1 } or return (0, "One of initial functions failed: $@");
755         }
756         $RT::Logger->debug("Done.");
757     }
758     if ( @Groups ) {
759         $RT::Logger->debug("Creating groups...");
760         foreach my $item (@Groups) {
761             my $new_entry = RT::Group->new( $RT::SystemUser );
762             my $member_of = delete $item->{'MemberOf'};
763             my ( $return, $msg ) = $new_entry->_Create(%$item);
764             unless ( $return ) {
765                 $RT::Logger->error( $msg );
766                 next;
767             } else {
768                 $RT::Logger->debug($return .".");
769             }
770             if ( $member_of ) {
771                 $member_of = [ $member_of ] unless ref $member_of eq 'ARRAY';
772                 foreach( @$member_of ) {
773                     my $parent = RT::Group->new($RT::SystemUser);
774                     if ( ref $_ eq 'HASH' ) {
775                         $parent->LoadByCols( %$_ );
776                     }
777                     elsif ( !ref $_ ) {
778                         $parent->LoadUserDefinedGroup( $_ );
779                     }
780                     else {
781                         $RT::Logger->error(
782                             "(Error: wrong format of MemberOf field."
783                             ." Should be name of user defined group or"
784                             ." hash reference with 'column => value' pairs."
785                             ." Use array reference to add to multiple groups)"
786                         );
787                         next;
788                     }
789                     unless ( $parent->Id ) {
790                         $RT::Logger->error("(Error: couldn't load group to add member)");
791                         next;
792                     }
793                     my ( $return, $msg ) = $parent->AddMember( $new_entry->Id );
794                     unless ( $return ) {
795                         $RT::Logger->error( $msg );
796                     } else {
797                         $RT::Logger->debug( $return ."." );
798                     }
799                 }
800             }
801         }
802         $RT::Logger->debug("done.");
803     }
804     if ( @Users ) {
805         $RT::Logger->debug("Creating users...");
806         foreach my $item (@Users) {
807             my $new_entry = new RT::User( $RT::SystemUser );
808             my ( $return, $msg ) = $new_entry->Create(%$item);
809             unless ( $return ) {
810                 $RT::Logger->error( $msg );
811             } else {
812                 $RT::Logger->debug( $return ."." );
813             }
814         }
815         $RT::Logger->debug("done.");
816     }
817     if ( @Queues ) {
818         $RT::Logger->debug("Creating queues...");
819         for my $item (@Queues) {
820             my $new_entry = new RT::Queue($RT::SystemUser);
821             my ( $return, $msg ) = $new_entry->Create(%$item);
822             unless ( $return ) {
823                 $RT::Logger->error( $msg );
824             } else {
825                 $RT::Logger->debug( $return ."." );
826             }
827         }
828         $RT::Logger->debug("done.");
829     }
830     if ( @CustomFields ) {
831         $RT::Logger->debug("Creating custom fields...");
832         for my $item ( @CustomFields ) {
833             my $new_entry = new RT::CustomField( $RT::SystemUser );
834             my $values    = delete $item->{'Values'};
835
836             my @queues;
837             # if ref then it's list of queues, so we do things ourself
838             if ( exists $item->{'Queue'} && ref $item->{'Queue'} ) {
839                 $item->{'LookupType'} ||= 'RT::Queue-RT::Ticket';
840                 @queues = @{ delete $item->{'Queue'} };
841             }
842
843             my ( $return, $msg ) = $new_entry->Create(%$item);
844             unless( $return ) {
845                 $RT::Logger->error( $msg );
846                 next;
847             }
848
849             if ( $item->{'BasedOn'} ) {
850                 my $basedon = RT::CustomField->new($RT::SystemUser);
851                 my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
852                                                         LookupType => $new_entry->LookupType );
853                 if ($ok) {
854                     ($ok, $msg) = $new_entry->SetBasedOn( $basedon );
855                     if ($ok) {
856                         $RT::Logger->debug("Added BasedOn $item->{BasedOn}: $msg");
857                     } else {
858                         $RT::Logger->error("Failed to add basedOn $item->{BasedOn}: $msg");
859                     }
860                 } else {
861                     $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF.  Skipping BasedOn");
862                 }
863             }
864
865             foreach my $value ( @{$values} ) {
866                 my ( $return, $msg ) = $new_entry->AddValue(%$value);
867                 $RT::Logger->error( $msg ) unless $return;
868             }
869
870             # apply by default
871             if ( !@queues && !exists $item->{'Queue'} && $item->{LookupType} ) {
872                 my $ocf = RT::ObjectCustomField->new($RT::SystemUser);
873                 $ocf->Create( CustomField => $new_entry->Id );
874             }
875
876             for my $q (@queues) {
877                 my $q_obj = RT::Queue->new($RT::SystemUser);
878                 $q_obj->Load($q);
879                 unless ( $q_obj->Id ) {
880                     $RT::Logger->error("Could not find queue ". $q );
881                     next;
882                 }
883                 my $OCF = RT::ObjectCustomField->new($RT::SystemUser);
884                 ( $return, $msg ) = $OCF->Create(
885                     CustomField => $new_entry->Id,
886                     ObjectId    => $q_obj->Id,
887                 );
888                 $RT::Logger->error( $msg ) unless $return and $OCF->Id;
889             }
890         }
891
892         $RT::Logger->debug("done.");
893     }
894     if ( @ACL ) {
895         $RT::Logger->debug("Creating ACL...");
896         for my $item (@ACL) {
897
898             my ($princ, $object);
899
900             # Global rights or Queue rights?
901             if ( $item->{'CF'} ) {
902                 $object = RT::CustomField->new( $RT::SystemUser );
903                 my @columns = ( Name => $item->{'CF'} );
904                 push @columns, Queue => $item->{'Queue'} if $item->{'Queue'} and not ref $item->{'Queue'};
905                 $object->LoadByName( @columns );
906             } elsif ( $item->{'Queue'} ) {
907                 $object = RT::Queue->new($RT::SystemUser);
908                 $object->Load( $item->{'Queue'} );
909             } else {
910                 $object = $RT::System;
911             }
912
913             $RT::Logger->error("Couldn't load object") and next unless $object and $object->Id;
914
915             # Group rights or user rights?
916             if ( $item->{'GroupDomain'} ) {
917                 $princ = RT::Group->new($RT::SystemUser);
918                 if ( $item->{'GroupDomain'} eq 'UserDefined' ) {
919                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
920                 } elsif ( $item->{'GroupDomain'} eq 'SystemInternal' ) {
921                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
922                 } elsif ( $item->{'GroupDomain'} eq 'RT::System-Role' ) {
923                   $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
924                 } elsif ( $item->{'GroupDomain'} eq 'RT::Queue-Role' &&
925                           $item->{'Queue'} )
926                 {
927                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
928                                               Queue => $object->id);
929                 } else {
930                   $princ->Load( $item->{'GroupId'} );
931                 }
932             } else {
933                 $princ = RT::User->new($RT::SystemUser);
934                 $princ->Load( $item->{'UserId'} );
935             }
936
937             # Grant it
938             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
939                 Right => $item->{'Right'},
940                 Object => $object
941             );
942             unless ( $return ) {
943                 $RT::Logger->error( $msg );
944             }
945             else {
946                 $RT::Logger->debug( $return ."." );
947             }
948         }
949         $RT::Logger->debug("done.");
950     }
951
952     if ( @ScripActions ) {
953         $RT::Logger->debug("Creating ScripActions...");
954
955         for my $item (@ScripActions) {
956             my $new_entry = RT::ScripAction->new($RT::SystemUser);
957             my ( $return, $msg ) = $new_entry->Create(%$item);
958             unless ( $return ) {
959                 $RT::Logger->error( $msg );
960             }
961             else {
962                 $RT::Logger->debug( $return ."." );
963             }
964         }
965
966         $RT::Logger->debug("done.");
967     }
968
969     if ( @ScripConditions ) {
970         $RT::Logger->debug("Creating ScripConditions...");
971
972         for my $item (@ScripConditions) {
973             my $new_entry = RT::ScripCondition->new($RT::SystemUser);
974             my ( $return, $msg ) = $new_entry->Create(%$item);
975             unless ( $return ) {
976                 $RT::Logger->error( $msg );
977             }
978             else {
979                 $RT::Logger->debug( $return ."." );
980             }
981         }
982
983         $RT::Logger->debug("done.");
984     }
985
986     if ( @Templates ) {
987         $RT::Logger->debug("Creating templates...");
988
989         for my $item (@Templates) {
990             my $new_entry = new RT::Template($RT::SystemUser);
991             my ( $return, $msg ) = $new_entry->Create(%$item);
992             unless ( $return ) {
993                 $RT::Logger->error( $msg );
994             }
995             else {
996                 $RT::Logger->debug( $return ."." );
997             }
998         }
999         $RT::Logger->debug("done.");
1000     }
1001     if ( @Scrips ) {
1002         $RT::Logger->debug("Creating scrips...");
1003
1004         for my $item (@Scrips) {
1005             my $new_entry = new RT::Scrip($RT::SystemUser);
1006
1007             my @queues = ref $item->{'Queue'} eq 'ARRAY'? @{ $item->{'Queue'} }: $item->{'Queue'} || 0;
1008             push @queues, 0 unless @queues; # add global queue at least
1009
1010             foreach my $q ( @queues ) {
1011                 my ( $return, $msg ) = $new_entry->Create( %$item, Queue => $q );
1012                 unless ( $return ) {
1013                     $RT::Logger->error( $msg );
1014                 }
1015                 else {
1016                     $RT::Logger->debug( $return ."." );
1017                 }
1018             }
1019         }
1020         $RT::Logger->debug("done.");
1021     }
1022     if ( @Attributes ) {
1023         $RT::Logger->debug("Creating predefined searches...");
1024         my $sys = RT::System->new($RT::SystemUser);
1025
1026         for my $item (@Attributes) {
1027             my $obj = delete $item->{Object}; # XXX: make this something loadable
1028             $obj ||= $sys;
1029             my ( $return, $msg ) = $obj->AddAttribute (%$item);
1030             unless ( $return ) {
1031                 $RT::Logger->error( $msg );
1032             }
1033             else {
1034                 $RT::Logger->debug( $return ."." );
1035             }
1036         }
1037         $RT::Logger->debug("done.");
1038     }
1039     if ( @Final ) {
1040         $RT::Logger->debug("Running final actions...");
1041         for ( @Final ) {
1042             local $@;
1043             eval { $_->(); };
1044             $RT::Logger->error( "Failed to run one of final actions: $@" )
1045                 if $@;
1046         }
1047         $RT::Logger->debug("done.");
1048     }
1049
1050     my $db_type = RT->Config->Get('DatabaseType');
1051     $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
1052
1053     $RT::Logger->debug("Done setting up database content.");
1054
1055 # TODO is it ok to return 1 here? If so, the previous codes in this sub
1056 # should return (0, $msg) if error happens instead of just warning.
1057 # anyway, we need to return something here to tell if everything is ok
1058     return( 1, 'Done inserting data' );
1059 }
1060
1061 =head2 ACLEquivGroupId
1062
1063 Given a userid, return that user's acl equivalence group
1064
1065 =cut
1066
1067 sub ACLEquivGroupId {
1068     my $id = shift;
1069
1070     my $cu = $RT::SystemUser;
1071     unless ( $cu ) {
1072         require RT::CurrentUser;
1073         $cu = new RT::CurrentUser;
1074         $cu->LoadByName('RT_System');
1075         warn "Couldn't load RT_System user" unless $cu->id;
1076     }
1077
1078     my $equiv_group = RT::Group->new( $cu );
1079     $equiv_group->LoadACLEquivalenceGroup( $id );
1080     return $equiv_group->Id;
1081 }
1082
1083 __PACKAGE__->FinalizeDatabaseType;
1084
1085 RT::Base->_ImportOverlays();
1086
1087 1;