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