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