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