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