rt 4.0.20 (RT#13852)
[freeside.git] / rt / lib / RT / Handle.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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         DisconnectHandleOnDestroy => 1,
118         %args,
119     );
120
121     if ( $db_type eq 'mysql' ) {
122         my $version = $self->DatabaseVersion;
123         ($version) = $version =~ /^(\d+\.\d+)/;
124         $self->dbh->do("SET NAMES 'utf8'") if $version >= 4.1;
125     }
126
127
128     if ( $db_type eq 'Pg' ) {
129         my $version = $self->DatabaseVersion;
130         ($version) = $version =~ /^(\d+\.\d+)/;
131         $self->dbh->do("SET bytea_output = 'escape'") if $version >= 9.0;
132     }
133
134
135
136     $self->dbh->{'LongReadLen'} = RT->Config->Get('MaxAttachmentSize');
137 }
138
139 =head2 BuildDSN
140
141 Build the DSN for the RT database. Doesn't take any parameters, draws all that
142 from the config.
143
144 =cut
145
146
147 sub BuildDSN {
148     my $self = shift;
149     # Unless the database port is a positive integer, we really don't want to pass it.
150     my $db_port = RT->Config->Get('DatabasePort');
151     $db_port = undef unless (defined $db_port && $db_port =~ /^(\d+)$/);
152     my $db_host = RT->Config->Get('DatabaseHost');
153     $db_host = undef unless $db_host;
154     my $db_name = RT->Config->Get('DatabaseName');
155     my $db_type = RT->Config->Get('DatabaseType');
156     $db_name = File::Spec->catfile($RT::VarPath, $db_name)
157         if $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name);
158
159     my %args = (
160         Host       => $db_host,
161         Database   => $db_name,
162         Port       => $db_port,
163         Driver     => $db_type,
164         RequireSSL => RT->Config->Get('DatabaseRequireSSL'),
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 1;
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     elsif ( $db_type eq 'mysql' ) {
365         $status = $dbh->do("CREATE DATABASE $db_name DEFAULT CHARACTER SET utf8");
366     }
367     else {
368         $status = $dbh->do("CREATE DATABASE $db_name");
369     }
370     return ($status, $DBI::errstr);
371 }
372
373 =head3 DropDatabase $DBH
374
375 Drops RT's database. This method can be used as class method.
376
377 Takes DBI handle as first argument. Many database systems require
378 a special handle to allow you to drop a database, so you may have
379 to use L<SystemDSN> when acquiring the DBI handle.
380
381 Fetches the type and name of the database from the config.
382
383 =cut
384
385 sub DropDatabase {
386     my $self = shift;
387     my $dbh  = shift or return (0, "No DBI handle provided");
388
389     my $db_type = RT->Config->Get('DatabaseType');
390     my $db_name = RT->Config->Get('DatabaseName');
391
392     if ( $db_type eq 'Oracle' ) {
393         my $db_user = RT->Config->Get('DatabaseUser');
394         my $status = $dbh->do( "DROP USER $db_user CASCADE" );
395         unless ( $status ) {
396             return 0, "Couldn't drop user $db_user."
397                 ."\nError: ". $dbh->errstr;
398         }
399         return (1, "Successfully dropped user '$db_user' with his schema.");
400     }
401     elsif ( $db_type eq 'SQLite' ) {
402         my $path = $db_name;
403         $path = "$RT::VarPath/$path" unless substr($path, 0, 1) eq '/';
404         unlink $path or return (0, "Couldn't remove '$path': $!");
405         return (1);
406     } else {
407         $dbh->do("DROP DATABASE ". $db_name)
408             or return (0, $DBI::errstr);
409     }
410     return (1);
411 }
412
413 =head2 InsertACL
414
415 =cut
416
417 sub InsertACL {
418     my $self      = shift;
419     my $dbh       = shift;
420     my $base_path = shift || $RT::EtcPath;
421
422     my $db_type = RT->Config->Get('DatabaseType');
423     return (1) if $db_type eq 'SQLite';
424
425     $dbh = $self->dbh if !$dbh && ref $self;
426     return (0, "No DBI handle provided") unless $dbh;
427
428     return (0, "'$base_path' doesn't exist") unless -e $base_path;
429
430     my $path;
431     if ( -d $base_path ) {
432         $path = File::Spec->catfile( $base_path, "acl.$db_type");
433         $path = $self->GetVersionFile($dbh, $path);
434
435         $path = File::Spec->catfile( $base_path, "acl")
436             unless $path && -e $path;
437         return (0, "Couldn't find ACLs for $db_type")
438             unless -e $path;
439     } else {
440         $path = $base_path;
441     }
442
443     local *acl;
444     do $path || return (0, "Couldn't load ACLs: " . $@);
445     my @acl = acl($dbh);
446     foreach my $statement (@acl) {
447         my $sth = $dbh->prepare($statement)
448             or return (0, "Couldn't prepare SQL query:\n $statement\n\nERROR: ". $dbh->errstr);
449         unless ( $sth->execute ) {
450             return (0, "Couldn't run SQL query:\n $statement\n\nERROR: ". $sth->errstr);
451         }
452     }
453     return (1);
454 }
455
456 =head2 InsertSchema
457
458 =cut
459
460 sub InsertSchema {
461     my $self = shift;
462     my $dbh  = shift;
463     my $base_path = (shift || $RT::EtcPath);
464
465     $dbh = $self->dbh if !$dbh && ref $self;
466     return (0, "No DBI handle provided") unless $dbh;
467
468     my $db_type = RT->Config->Get('DatabaseType');
469
470     my $file;
471     if ( -d $base_path ) {
472         $file = $base_path . "/schema." . $db_type;
473     } else {
474         $file = $base_path;
475     }
476
477     $file = $self->GetVersionFile( $dbh, $file );
478     unless ( $file ) {
479         return (0, "Couldn't find schema file(s) '$file*'");
480     }
481     unless ( -f $file && -r $file ) {
482         return (0, "File '$file' doesn't exist or couldn't be read");
483     }
484
485     my (@schema);
486
487     open( my $fh_schema, '<', $file ) or die $!;
488
489     my $has_local = 0;
490     open( my $fh_schema_local, "<" . $self->GetVersionFile( $dbh, $RT::LocalEtcPath . "/schema." . $db_type ))
491         and $has_local = 1;
492
493     my $statement = "";
494     foreach my $line ( <$fh_schema>, ($_ = ';;'), $has_local? <$fh_schema_local>: () ) {
495         $line =~ s/\#.*//g;
496         $line =~ s/--.*//g;
497         $statement .= $line;
498         if ( $line =~ /;(\s*)$/ ) {
499             $statement =~ s/;(\s*)$//g;
500             push @schema, $statement;
501             $statement = "";
502         }
503     }
504     close $fh_schema; close $fh_schema_local;
505
506     if ( $db_type eq 'Oracle' ) {
507         my $db_user = RT->Config->Get('DatabaseUser');
508         my $status = $dbh->do( "ALTER SESSION SET CURRENT_SCHEMA=$db_user" );
509         unless ( $status ) {
510             return $status, "Couldn't set current schema to $db_user."
511                 ."\nError: ". $dbh->errstr;
512         }
513     }
514
515     local $SIG{__WARN__} = sub {};
516     my $is_local = 0;
517     $dbh->begin_work or return (0, "Couldn't begin transaction: ". $dbh->errstr);
518     foreach my $statement (@schema) {
519         if ( $statement =~ /^\s*;$/ ) {
520             $is_local = 1; next;
521         }
522
523         my $sth = $dbh->prepare($statement)
524             or return (0, "Couldn't prepare SQL query:\n$statement\n\nERROR: ". $dbh->errstr);
525         unless ( $sth->execute or $is_local ) {
526             return (0, "Couldn't run SQL query:\n$statement\n\nERROR: ". $sth->errstr);
527         }
528     }
529     $dbh->commit or return (0, "Couldn't commit transaction: ". $dbh->errstr);
530     return (1);
531 }
532
533 =head1 GetVersionFile
534
535 Takes base name of the file as argument, scans for <base name>-<version> named
536 files and returns file name with closest version to the version of the RT DB.
537
538 =cut
539
540 sub GetVersionFile {
541     my $self = shift;
542     my $dbh = shift;
543     my $base_name = shift;
544
545     my $db_version = ref $self
546         ? $self->DatabaseVersion
547         : do {
548             my $tmp = RT::Handle->new;
549             $tmp->dbh($dbh);
550             $tmp->DatabaseVersion;
551         };
552
553     require File::Glob;
554     my @files = File::Glob::bsd_glob("$base_name*");
555     return '' unless @files;
556
557     my %version = map { $_ =~ /\.\w+-([-\w\.]+)$/; ($1||0) => $_ } @files;
558     my $version;
559     foreach ( reverse sort cmp_version keys %version ) {
560         if ( cmp_version( $db_version, $_ ) >= 0 ) {
561             $version = $_;
562             last;
563         }
564     }
565
566     return defined $version? $version{ $version } : undef;
567 }
568
569 { my %word = (
570     a     => -4,
571     alpha => -4,
572     b     => -3,
573     beta  => -3,
574     pre   => -2,
575     rc    => -1,
576     head  => 9999,
577 );
578 sub cmp_version($$) {
579     my ($a, $b) = (@_);
580     my @a = grep defined, map { /^[0-9]+$/? $_ : /^[a-zA-Z]+$/? $word{$_}|| -10 : undef }
581         split /([^0-9]+)/, $a;
582     my @b = grep defined, map { /^[0-9]+$/? $_ : /^[a-zA-Z]+$/? $word{$_}|| -10 : undef }
583         split /([^0-9]+)/, $b;
584     @a > @b
585         ? push @b, (0) x (@a-@b)
586         : push @a, (0) x (@b-@a);
587     for ( my $i = 0; $i < @a; $i++ ) {
588         return $a[$i] <=> $b[$i] if $a[$i] <=> $b[$i];
589     }
590     return 0;
591 }
592
593 sub version_words {
594     return keys %word;
595 }
596
597 }
598
599
600 =head2 InsertInitialData
601
602 Inserts system objects into RT's DB, like system user or 'nobody',
603 internal groups and other records required. However, this method
604 doesn't insert any real users like 'root' and you have to use
605 InsertData or another way to do that.
606
607 Takes no arguments. Returns status and message tuple.
608
609 It's safe to call this method even if those objects already exist.
610
611 =cut
612
613 sub InsertInitialData {
614     my $self    = shift;
615
616     my @warns;
617
618     # create RT_System user and grant him rights
619     {
620         require RT::CurrentUser;
621
622         my $test_user = RT::User->new( RT::CurrentUser->new() );
623         $test_user->Load('RT_System');
624         if ( $test_user->id ) {
625             push @warns, "Found system user in the DB.";
626         }
627         else {
628             my $user = RT::User->new( RT::CurrentUser->new() );
629             my ( $val, $msg ) = $user->_BootstrapCreate(
630                 Name     => 'RT_System',
631                 RealName => 'The RT System itself',
632                 Comments => 'Do not delete or modify this user. '
633                     . 'It is integral to RT\'s internal database structures',
634                 Creator  => '1',
635                 LastUpdatedBy => '1',
636             );
637             return ($val, $msg) unless $val;
638         }
639         DBIx::SearchBuilder::Record::Cachable->FlushCache;
640     }
641
642     # init RT::SystemUser and RT::System objects
643     RT::InitSystemObjects();
644     unless ( RT->SystemUser->id ) {
645         return (0, "Couldn't load system user");
646     }
647
648     # grant SuperUser right to system user
649     {
650         my $test_ace = RT::ACE->new( RT->SystemUser );
651         $test_ace->LoadByCols(
652             PrincipalId   => ACLEquivGroupId( RT->SystemUser->Id ),
653             PrincipalType => 'Group',
654             RightName     => 'SuperUser',
655             ObjectType    => 'RT::System',
656             ObjectId      => 1,
657         );
658         if ( $test_ace->id ) {
659             push @warns, "System user has global SuperUser right.";
660         } else {
661             my $ace = RT::ACE->new( RT->SystemUser );
662             my ( $val, $msg ) = $ace->_BootstrapCreate(
663                 PrincipalId   => ACLEquivGroupId( RT->SystemUser->Id ),
664                 PrincipalType => 'Group',
665                 RightName     => 'SuperUser',
666                 ObjectType    => 'RT::System',
667                 ObjectId      => 1,
668             );
669             return ($val, $msg) unless $val;
670         }
671         DBIx::SearchBuilder::Record::Cachable->FlushCache;
672     }
673
674     # system groups
675     # $self->loc('Everyone'); # For the string extractor to get a string to localize
676     # $self->loc('Privileged'); # For the string extractor to get a string to localize
677     # $self->loc('Unprivileged'); # For the string extractor to get a string to localize
678     foreach my $name (qw(Everyone Privileged Unprivileged)) {
679         my $group = RT::Group->new( RT->SystemUser );
680         $group->LoadSystemInternalGroup( $name );
681         if ( $group->id ) {
682             push @warns, "System group '$name' already exists.";
683             next;
684         }
685
686         $group = RT::Group->new( RT->SystemUser );
687         my ( $val, $msg ) = $group->_Create(
688             Type        => $name,
689             Domain      => 'SystemInternal',
690             Description => 'Pseudogroup for internal use',  # loc
691             Name        => '',
692             Instance    => '',
693         );
694         return ($val, $msg) unless $val;
695     }
696
697     # nobody
698     {
699         my $user = RT::User->new( RT->SystemUser );
700         $user->Load('Nobody');
701         if ( $user->id ) {
702             push @warns, "Found 'Nobody' user in the DB.";
703         }
704         else {
705             my ( $val, $msg ) = $user->Create(
706                 Name     => 'Nobody',
707                 RealName => 'Nobody in particular',
708                 Comments => 'Do not delete or modify this user. It is integral '
709                     .'to RT\'s internal data structures',
710                 Privileged => 0,
711             );
712             return ($val, $msg) unless $val;
713         }
714
715         if ( $user->HasRight( Right => 'OwnTicket', Object => $RT::System ) ) {
716             push @warns, "User 'Nobody' has global OwnTicket right.";
717         } else {
718             my ( $val, $msg ) = $user->PrincipalObj->GrantRight(
719                 Right => 'OwnTicket',
720                 Object => $RT::System,
721             );
722             return ($val, $msg) unless $val;
723         }
724     }
725
726     # rerun to get init Nobody as well
727     RT::InitSystemObjects();
728
729     # system role groups
730     foreach my $name (qw(Owner Requestor Cc AdminCc)) {
731         my $group = RT::Group->new( RT->SystemUser );
732         $group->LoadSystemRoleGroup( $name );
733         if ( $group->id ) {
734             push @warns, "System role '$name' already exists.";
735             next;
736         }
737
738         $group = RT::Group->new( RT->SystemUser );
739         my ( $val, $msg ) = $group->_Create(
740             Type        => $name,
741             Domain      => 'RT::System-Role',
742             Description => 'SystemRolegroup for internal use',  # loc
743             Name        => '',
744             Instance    => '',
745         );
746         return ($val, $msg) unless $val;
747     }
748
749     push @warns, "You appear to have a functional RT database."
750         if @warns;
751
752     return (1, join "\n", @warns);
753 }
754
755 =head2 InsertData
756
757 Load some sort of data into the database, takes path to a file.
758
759 =cut
760
761 sub InsertData {
762     my $self     = shift;
763     my $datafile = shift;
764     my $root_password = shift;
765     my %args     = (
766         disconnect_after => 1,
767         @_
768     );
769
770     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
771     our (@Groups, @Users, @Members, @ACL, @Queues, @ScripActions, @ScripConditions,
772            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
773     local (@Groups, @Users, @Members, @ACL, @Queues, @ScripActions, @ScripConditions,
774            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
775
776     local $@;
777     $RT::Logger->debug("Going to load '$datafile' data file");
778     eval { require $datafile }
779       or return (0, "Couldn't load data from '$datafile' for import:\n\nERROR:". $@);
780
781     if ( @Initial ) {
782         $RT::Logger->debug("Running initial actions...");
783         foreach ( @Initial ) {
784             local $@;
785             eval { $_->(); 1 } or return (0, "One of initial functions failed: $@");
786         }
787         $RT::Logger->debug("Done.");
788     }
789     if ( @Groups ) {
790         $RT::Logger->debug("Creating groups...");
791         foreach my $item (@Groups) {
792             my $new_entry = RT::Group->new( RT->SystemUser );
793             $item->{Domain} ||= 'UserDefined';
794             my $member_of = delete $item->{'MemberOf'};
795             my $members = delete $item->{'Members'};
796             my ( $return, $msg ) = $new_entry->_Create(%$item);
797             unless ( $return ) {
798                 $RT::Logger->error( $msg );
799                 next;
800             } else {
801                 $RT::Logger->debug($return .".");
802             }
803             if ( $member_of ) {
804                 $member_of = [ $member_of ] unless ref $member_of eq 'ARRAY';
805                 foreach( @$member_of ) {
806                     my $parent = RT::Group->new(RT->SystemUser);
807                     if ( ref $_ eq 'HASH' ) {
808                         $parent->LoadByCols( %$_ );
809                     }
810                     elsif ( !ref $_ ) {
811                         $parent->LoadUserDefinedGroup( $_ );
812                     }
813                     else {
814                         $RT::Logger->error(
815                             "(Error: wrong format of MemberOf field."
816                             ." Should be name of user defined group or"
817                             ." hash reference with 'column => value' pairs."
818                             ." Use array reference to add to multiple groups)"
819                         );
820                         next;
821                     }
822                     unless ( $parent->Id ) {
823                         $RT::Logger->error("(Error: couldn't load group to add member)");
824                         next;
825                     }
826                     my ( $return, $msg ) = $parent->AddMember( $new_entry->Id );
827                     unless ( $return ) {
828                         $RT::Logger->error( $msg );
829                     } else {
830                         $RT::Logger->debug( $return ."." );
831                     }
832                 }
833             }
834             push @Members, map { +{Group => $new_entry->id,
835                                    Class => "RT::User", Name => $_} }
836                 @{ $members->{Users} || [] };
837             push @Members, map { +{Group => $new_entry->id,
838                                    Class => "RT::Group", Name => $_} }
839                 @{ $members->{Groups} || [] };
840         }
841         $RT::Logger->debug("done.");
842     }
843     if ( @Users ) {
844         $RT::Logger->debug("Creating users...");
845         foreach my $item (@Users) {
846             if ( $item->{'Name'} eq 'root' && $root_password ) {
847                 $item->{'Password'} = $root_password;
848             }
849             my $new_entry = RT::User->new( RT->SystemUser );
850             my ( $return, $msg ) = $new_entry->Create(%$item);
851             unless ( $return ) {
852                 $RT::Logger->error( $msg );
853             } else {
854                 $RT::Logger->debug( $return ."." );
855             }
856         }
857         $RT::Logger->debug("done.");
858     }
859     if ( @Members ) {
860         $RT::Logger->debug("Adding users and groups to groups...");
861         for my $item (@Members) {
862             my $group = RT::Group->new(RT->SystemUser);
863             $group->LoadUserDefinedGroup( delete $item->{Group} );
864             unless ($group->Id) {
865                 RT->Logger->error("Unable to find group '$group' to add members to");
866                 next;
867             }
868
869             my $class = delete $item->{Class} || 'RT::User';
870             my $member = $class->new( RT->SystemUser );
871             $item->{Domain} = 'UserDefined' if $member->isa("RT::Group");
872             $member->LoadByCols( %$item );
873             unless ($member->Id) {
874                 RT->Logger->error("Unable to find $class '".($item->{id} || $item->{Name})."' to add to ".$group->Name);
875                 next;
876             }
877
878             my ( $return, $msg) = $group->AddMember( $member->PrincipalObj->Id );
879             unless ( $return ) {
880                 $RT::Logger->error( $msg );
881             } else {
882                 $RT::Logger->debug( $return ."." );
883             }
884         }
885     }
886     if ( @Queues ) {
887         $RT::Logger->debug("Creating queues...");
888         for my $item (@Queues) {
889             my $new_entry = RT::Queue->new(RT->SystemUser);
890             my ( $return, $msg ) = $new_entry->Create(%$item);
891             unless ( $return ) {
892                 $RT::Logger->error( $msg );
893             } else {
894                 $RT::Logger->debug( $return ."." );
895             }
896         }
897         $RT::Logger->debug("done.");
898     }
899     if ( @CustomFields ) {
900         $RT::Logger->debug("Creating custom fields...");
901         for my $item ( @CustomFields ) {
902             my $new_entry = RT::CustomField->new( RT->SystemUser );
903             my $values    = delete $item->{'Values'};
904
905             my @queues;
906             # if ref then it's list of queues, so we do things ourself
907             if ( exists $item->{'Queue'} && ref $item->{'Queue'} ) {
908                 $item->{'LookupType'} ||= 'RT::Queue-RT::Ticket';
909                 @queues = @{ delete $item->{'Queue'} };
910             }
911
912             if ( $item->{'BasedOn'} ) {
913                 if ( $item->{'BasedOn'} =~ /^\d+$/) {
914                     # Already have an ID -- should be fine
915                 } elsif ( $item->{'LookupType'} ) {
916                     my $basedon = RT::CustomField->new($RT::SystemUser);
917                     my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
918                                                             LookupType => $item->{'LookupType'} );
919                     if ($ok) {
920                         $item->{'BasedOn'} = $basedon->Id;
921                     } else {
922                         $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF.  Skipping BasedOn: $msg");
923                         delete $item->{'BasedOn'};
924                     }
925                 } else {
926                     $RT::Logger->error("Unable to load CF $item->{BasedOn} because no LookupType was specified.  Skipping BasedOn");
927                     delete $item->{'BasedOn'};
928                 }
929
930             } 
931
932             my ( $return, $msg ) = $new_entry->Create(%$item);
933             unless( $return ) {
934                 $RT::Logger->error( $msg );
935                 next;
936             }
937
938             foreach my $value ( @{$values} ) {
939                 my ( $return, $msg ) = $new_entry->AddValue(%$value);
940                 $RT::Logger->error( $msg ) unless $return;
941             }
942
943             # apply by default
944             if ( !@queues && !exists $item->{'Queue'} && $item->{LookupType} ) {
945                 my $ocf = RT::ObjectCustomField->new(RT->SystemUser);
946                 $ocf->Create( CustomField => $new_entry->Id );
947             }
948
949             for my $q (@queues) {
950                 my $q_obj = RT::Queue->new(RT->SystemUser);
951                 $q_obj->Load($q);
952                 unless ( $q_obj->Id ) {
953                     $RT::Logger->error("Could not find queue ". $q );
954                     next;
955                 }
956                 my $OCF = RT::ObjectCustomField->new(RT->SystemUser);
957                 ( $return, $msg ) = $OCF->Create(
958                     CustomField => $new_entry->Id,
959                     ObjectId    => $q_obj->Id,
960                 );
961                 $RT::Logger->error( $msg ) unless $return and $OCF->Id;
962             }
963         }
964
965         $RT::Logger->debug("done.");
966     }
967     if ( @ACL ) {
968         $RT::Logger->debug("Creating ACL...");
969         for my $item (@ACL) {
970
971             my ($princ, $object);
972
973             # Global rights or Queue rights?
974             if ( $item->{'CF'} ) {
975                 $object = RT::CustomField->new( RT->SystemUser );
976                 my @columns = ( Name => $item->{'CF'} );
977                 push @columns, Queue => $item->{'Queue'} if $item->{'Queue'} and not ref $item->{'Queue'};
978                 $object->LoadByName( @columns );
979             } elsif ( $item->{'Queue'} ) {
980                 $object = RT::Queue->new(RT->SystemUser);
981                 $object->Load( $item->{'Queue'} );
982             } else {
983                 $object = $RT::System;
984             }
985
986             $RT::Logger->error("Couldn't load object") and next unless $object and $object->Id;
987
988             # Group rights or user rights?
989             if ( $item->{'GroupDomain'} ) {
990                 $princ = RT::Group->new(RT->SystemUser);
991                 if ( $item->{'GroupDomain'} eq 'UserDefined' ) {
992                   $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
993                 } elsif ( $item->{'GroupDomain'} eq 'SystemInternal' ) {
994                   $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
995                 } elsif ( $item->{'GroupDomain'} eq 'RT::System-Role' ) {
996                   $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
997                 } elsif ( $item->{'GroupDomain'} eq 'RT::Queue-Role' &&
998                           $item->{'Queue'} )
999                 {
1000                   $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
1001                                               Queue => $object->id);
1002                 } else {
1003                   $princ->Load( $item->{'GroupId'} );
1004                 }
1005                 unless ( $princ->Id ) {
1006                     RT->Logger->error("Unable to load Group: GroupDomain => $item->{GroupDomain}, GroupId => $item->{GroupId}, Queue => $item->{Queue}");
1007                     next;
1008                 }
1009             } else {
1010                 $princ = RT::User->new(RT->SystemUser);
1011                 my ($ok, $msg) = $princ->Load( $item->{'UserId'} );
1012                 unless ( $ok ) {
1013                     RT->Logger->error("Unable to load user: $item->{UserId} : $msg");
1014                     next;
1015                 }
1016             }
1017
1018             # Grant it
1019             my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
1020                 Right => $item->{'Right'},
1021                 Object => $object
1022             );
1023             unless ( $return ) {
1024                 $RT::Logger->error( $msg );
1025             }
1026             else {
1027                 $RT::Logger->debug( $return ."." );
1028             }
1029         }
1030         $RT::Logger->debug("done.");
1031     }
1032
1033     if ( @ScripActions ) {
1034         $RT::Logger->debug("Creating ScripActions...");
1035
1036         for my $item (@ScripActions) {
1037             my $new_entry = RT::ScripAction->new(RT->SystemUser);
1038             my ( $return, $msg ) = $new_entry->Create(%$item);
1039             unless ( $return ) {
1040                 $RT::Logger->error( $msg );
1041             }
1042             else {
1043                 $RT::Logger->debug( $return ."." );
1044             }
1045         }
1046
1047         $RT::Logger->debug("done.");
1048     }
1049
1050     if ( @ScripConditions ) {
1051         $RT::Logger->debug("Creating ScripConditions...");
1052
1053         for my $item (@ScripConditions) {
1054             my $new_entry = RT::ScripCondition->new(RT->SystemUser);
1055             my ( $return, $msg ) = $new_entry->Create(%$item);
1056             unless ( $return ) {
1057                 $RT::Logger->error( $msg );
1058             }
1059             else {
1060                 $RT::Logger->debug( $return ."." );
1061             }
1062         }
1063
1064         $RT::Logger->debug("done.");
1065     }
1066
1067     if ( @Templates ) {
1068         $RT::Logger->debug("Creating templates...");
1069
1070         for my $item (@Templates) {
1071             my $new_entry = RT::Template->new(RT->SystemUser);
1072             my ( $return, $msg ) = $new_entry->Create(%$item);
1073             unless ( $return ) {
1074                 $RT::Logger->error( $msg );
1075             }
1076             else {
1077                 $RT::Logger->debug( $return ."." );
1078             }
1079         }
1080         $RT::Logger->debug("done.");
1081     }
1082     if ( @Scrips ) {
1083         $RT::Logger->debug("Creating scrips...");
1084
1085         for my $item (@Scrips) {
1086             my $new_entry = RT::Scrip->new(RT->SystemUser);
1087
1088             my @queues = ref $item->{'Queue'} eq 'ARRAY'? @{ $item->{'Queue'} }: $item->{'Queue'} || 0;
1089             push @queues, 0 unless @queues; # add global queue at least
1090
1091             foreach my $q ( @queues ) {
1092                 my ( $return, $msg ) = $new_entry->Create( %$item, Queue => $q );
1093                 unless ( $return ) {
1094                     $RT::Logger->error( $msg );
1095                 }
1096                 else {
1097                     $RT::Logger->debug( $return ."." );
1098                 }
1099             }
1100         }
1101         $RT::Logger->debug("done.");
1102     }
1103     if ( @Attributes ) {
1104         $RT::Logger->debug("Creating attributes...");
1105         my $sys = RT::System->new(RT->SystemUser);
1106
1107         for my $item (@Attributes) {
1108             my $obj = delete $item->{Object}; # XXX: make this something loadable
1109             $obj ||= $sys;
1110             my ( $return, $msg ) = $obj->AddAttribute (%$item);
1111             unless ( $return ) {
1112                 $RT::Logger->error( $msg );
1113             }
1114             else {
1115                 $RT::Logger->debug( $return ."." );
1116             }
1117         }
1118         $RT::Logger->debug("done.");
1119     }
1120     if ( @Final ) {
1121         $RT::Logger->debug("Running final actions...");
1122         for ( @Final ) {
1123             local $@;
1124             eval { $_->(); };
1125             $RT::Logger->error( "Failed to run one of final actions: $@" )
1126                 if $@;
1127         }
1128         $RT::Logger->debug("done.");
1129     }
1130
1131     # XXX: This disconnect doesn't really belong here; it's a relict from when
1132     # this method was extracted from rt-setup-database.  However, too much
1133     # depends on it to change without significant testing.  At the very least,
1134     # we can provide a way to skip the side-effect.
1135     if ( $args{disconnect_after} ) {
1136         my $db_type = RT->Config->Get('DatabaseType');
1137         $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
1138     }
1139
1140     $RT::Logger->debug("Done setting up database content.");
1141
1142 # TODO is it ok to return 1 here? If so, the previous codes in this sub
1143 # should return (0, $msg) if error happens instead of just warning.
1144 # anyway, we need to return something here to tell if everything is ok
1145     return( 1, 'Done inserting data' );
1146 }
1147
1148 =head2 ACLEquivGroupId
1149
1150 Given a userid, return that user's acl equivalence group
1151
1152 =cut
1153
1154 sub ACLEquivGroupId {
1155     my $id = shift;
1156
1157     my $cu = RT->SystemUser;
1158     unless ( $cu ) {
1159         require RT::CurrentUser;
1160         $cu = RT::CurrentUser->new;
1161         $cu->LoadByName('RT_System');
1162         warn "Couldn't load RT_System user" unless $cu->id;
1163     }
1164
1165     my $equiv_group = RT::Group->new( $cu );
1166     $equiv_group->LoadACLEquivalenceGroup( $id );
1167     return $equiv_group->Id;
1168 }
1169
1170 =head2 QueryHistory
1171
1172 Returns the SQL query history associated with this handle. The top level array
1173 represents a lists of request. Each request is a hash with metadata about the
1174 request (such as the URL) and a list of queries. You'll probably not be using this.
1175
1176 =cut
1177
1178 sub QueryHistory {
1179     my $self = shift;
1180
1181     return $self->{QueryHistory};
1182 }
1183
1184 =head2 AddRequestToHistory
1185
1186 Adds a web request to the query history. It must be a hash with keys Path (a
1187 string) and Queries (an array reference of arrays, where elements are time,
1188 sql, bind parameters, and duration).
1189
1190 =cut
1191
1192 sub AddRequestToHistory {
1193     my $self    = shift;
1194     my $request = shift;
1195
1196     push @{ $self->{QueryHistory} }, $request;
1197 }
1198
1199 =head2 Quote
1200
1201 Returns the parameter quoted by DBI. B<You almost certainly do not need this.>
1202 Use bind parameters (C<?>) instead. This is used only outside the scope of interacting
1203 with the database.
1204
1205 =cut
1206
1207 sub Quote {
1208     my $self = shift;
1209     my $value = shift;
1210
1211     return $self->dbh->quote($value);
1212 }
1213
1214 =head2 FillIn
1215
1216 Takes a SQL query and an array reference of bind parameters and fills in the
1217 query's C<?> parameters.
1218
1219 =cut
1220
1221 sub FillIn {
1222     my $self = shift;
1223     my $sql  = shift;
1224     my $bind = shift;
1225
1226     my $b = 0;
1227
1228     # is this regex sufficient?
1229     $sql =~ s{\?}{$self->Quote($bind->[$b++])}eg;
1230
1231     return $sql;
1232 }
1233
1234 # log a mason stack trace instead of a Carp::longmess because it's less painful
1235 # and uses mason component paths properly
1236 sub _LogSQLStatement {
1237     my $self = shift;
1238     my $statement = shift;
1239     my $duration = shift;
1240     my @bind = @_;
1241
1242     require HTML::Mason::Exceptions;
1243     push @{$self->{'StatementLog'}} , ([Time::HiRes::time(), $statement, [@bind], $duration, HTML::Mason::Exception->new->as_string]);
1244 }
1245
1246
1247 sub _TableNames {
1248     my $self = shift;
1249     my $dbh = shift || $self->dbh;
1250
1251     {
1252         local $@;
1253         if (
1254             $dbh->{Driver}->{Name} eq 'Pg'
1255             && $dbh->{'pg_server_version'} >= 90200
1256             && !eval { DBD::Pg->VERSION('2.19.3'); 1 }
1257         ) {
1258             die "You're using PostgreSQL 9.2 or newer. You have to upgrade DBD::Pg module to 2.19.3 or newer: $@";
1259         }
1260     }
1261
1262     my @res;
1263
1264     my $sth = $dbh->table_info( '', undef, undef, "'TABLE'");
1265     while ( my $table = $sth->fetchrow_hashref ) {
1266         push @res, $table->{TABLE_NAME} || $table->{table_name};
1267     }
1268
1269     return @res;
1270 }
1271
1272 __PACKAGE__->FinalizeDatabaseType;
1273
1274 RT::Base->_ImportOverlays();
1275
1276 1;