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