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