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