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