+ # Unless the database port is a positive integer, we really don't want to pass it.
+ my $db_port = RT->Config->Get('DatabasePort');
+ $db_port = undef unless (defined $db_port && $db_port =~ /^(\d+)$/);
+ my $db_host = RT->Config->Get('DatabaseHost');
+ $db_host = undef unless $db_host;
+ my $db_name = RT->Config->Get('DatabaseName');
+ my $db_type = RT->Config->Get('DatabaseType');
+ $db_name = File::Spec->catfile($RT::VarPath, $db_name)
+ if $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name);
+
+ my %args = (
+ Host => $db_host,
+ Database => $db_name,
+ Port => $db_port,
+ Driver => $db_type,
+ RequireSSL => RT->Config->Get('DatabaseRequireSSL'),
+ DisconnectHandleOnDestroy => 1,
+ );
+ if ( $db_type eq 'Oracle' && $db_host ) {
+ $args{'SID'} = delete $args{'Database'};
+ }
+ $self->SUPER::BuildDSN( %args );
+}
+
+=head2 DSN
+
+Returns the DSN for this handle. In order to get correct value you must
+build DSN first, see L</BuildDSN>.
+
+This is method can be called as class method, in this case creates
+temporary handle object, L</BuildDSN builds DSN> and returns it.
+
+=cut
+
+sub DSN {
+ my $self = shift;
+ return $self->SUPER::DSN if ref $self;
+
+ my $handle = $self->new;
+ $handle->BuildDSN;
+ return $handle->DSN;
+}
+
+=head2 SystemDSN
+
+Returns a DSN suitable for database creates and drops
+and user creates and drops.
+
+Gets RT's DSN first (see L<DSN>) and then change it according
+to requirements of a database system RT's using.
+
+=cut
+
+sub SystemDSN {
+ my $self = shift;
+
+ my $db_name = RT->Config->Get('DatabaseName');
+ my $db_type = RT->Config->Get('DatabaseType');
+
+ my $dsn = $self->DSN;
+ if ( $db_type eq 'mysql' ) {
+ # with mysql, you want to connect sans database to funge things
+ $dsn =~ s/dbname=\Q$db_name//;
+ }
+ elsif ( $db_type eq 'Pg' ) {
+ # with postgres, you want to connect to template1 database
+ $dsn =~ s/dbname=\Q$db_name/dbname=template1/;
+ }
+ return $dsn;
+}
+
+=head2 Database compatibility and integrity checks
+
+
+
+=cut
+
+sub CheckIntegrity {
+ my $self = shift;
+ $self = new $self unless ref $self;
+
+ unless ($RT::Handle and $RT::Handle->dbh) {
+ local $@;
+ unless ( eval { RT::ConnectToDatabase(); 1 } ) {
+ return (0, 'no connection', "$@");
+ }
+ }
+
+ require RT::CurrentUser;
+ my $test_user = RT::CurrentUser->new;
+ $test_user->Load('RT_System');
+ unless ( $test_user->id ) {
+ return (0, 'no system user', "Couldn't find RT_System user in the DB '". $self->DSN ."'");
+ }
+
+ $test_user = RT::CurrentUser->new;
+ $test_user->Load('Nobody');
+ unless ( $test_user->id ) {
+ return (0, 'no nobody user', "Couldn't find Nobody user in the DB '". $self->DSN ."'");
+ }
+
+ return $RT::Handle->dbh;
+}
+
+sub CheckCompatibility {
+ my $self = shift;
+ my $dbh = shift;
+ my $state = shift || 'post';
+
+ my $db_type = RT->Config->Get('DatabaseType');
+ if ( $db_type eq "mysql" ) {
+ # Check which version we're running
+ my $version = ($dbh->selectrow_array("show variables like 'version'"))[1];
+ return (0, "couldn't get version of the mysql server")
+ unless $version;
+
+ ($version) = $version =~ /^(\d+\.\d+)/;
+ return (0, "RT is unsupported on MySQL versions before 4.0.x, it's $version")
+ if $version < 4;
+
+ # MySQL must have InnoDB support
+ my $innodb = ($dbh->selectrow_array("show variables like 'have_innodb'"))[1];
+ if ( lc $innodb eq "no" ) {
+ return (0, "RT requires that MySQL be compiled with InnoDB table support.\n".
+ "See http://dev.mysql.com/doc/mysql/en/InnoDB.html");
+ } elsif ( lc $innodb eq "disabled" ) {
+ return (0, "RT requires that MySQL InnoDB table support be enabled.\n".
+ "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
+ }
+
+ if ( $state eq 'post' ) {
+ my $create_table = $dbh->selectrow_arrayref("SHOW CREATE TABLE Tickets")->[1];
+ unless ( $create_table =~ /(?:ENGINE|TYPE)\s*=\s*InnoDB/i ) {
+ return (0, "RT requires that all its tables be of InnoDB type. Upgrade RT tables.");
+ }
+ }
+ if ( $version >= 4.1 && $state eq 'post' ) {
+ my $create_table = $dbh->selectrow_arrayref("SHOW CREATE TABLE Attachments")->[1];
+ unless ( $create_table =~ /\bContent\b[^,]*BLOB/i ) {
+ return (0, "RT since version 3.8 has new schema for MySQL versions after 4.1.0\n"
+ ."Follow instructions in the UPGRADING.mysql file.");
+ }
+ }
+ }
+ return (1)
+}
+
+sub CheckSphinxSE {
+ my $self = shift;
+
+ my $dbh = $RT::Handle->dbh;
+ local $dbh->{'RaiseError'} = 0;
+ local $dbh->{'PrintError'} = 0;
+ my $has = ($dbh->selectrow_array("show variables like 'have_sphinx'"))[1];
+ $has ||= ($dbh->selectrow_array(
+ "select 'yes' from INFORMATION_SCHEMA.PLUGINS where PLUGIN_NAME = 'sphinx' AND PLUGIN_STATUS='active'"
+ ))[0];
+
+ return 0 unless lc($has||'') eq "yes";
+ return 1;
+}
+
+=head2 Database maintanance
+
+=head3 CreateDatabase $DBH
+
+Creates a new database. This method can be used as class method.
+
+Takes DBI handle. Many database systems require special handle to
+allow you to create a new database, so you have to use L<SystemDSN>
+method during connection.
+
+Fetches type and name of the DB from the config.
+
+=cut
+
+sub CreateDatabase {
+ my $self = shift;
+ my $dbh = shift or return (0, "No DBI handle provided");
+ my $db_type = RT->Config->Get('DatabaseType');
+ my $db_name = RT->Config->Get('DatabaseName');
+
+ my $status;
+ if ( $db_type eq 'SQLite' ) {
+ return (1, 'Skipped as SQLite doesn\'t need any action');
+ }
+ elsif ( $db_type eq 'Oracle' ) {
+ my $db_user = RT->Config->Get('DatabaseUser');
+ my $db_pass = RT->Config->Get('DatabasePassword');
+ $status = $dbh->do(
+ "CREATE USER $db_user IDENTIFIED BY $db_pass"
+ ." default tablespace USERS"
+ ." temporary tablespace TEMP"
+ ." quota unlimited on USERS"
+ );
+ unless ( $status ) {
+ return $status, "Couldn't create user $db_user identified by $db_pass."
+ ."\nError: ". $dbh->errstr;
+ }
+ $status = $dbh->do( "GRANT connect, resource TO $db_user" );
+ unless ( $status ) {
+ return $status, "Couldn't grant connect and resource to $db_user."
+ ."\nError: ". $dbh->errstr;
+ }
+ return (1, "Created user $db_user. All RT's objects should be in his schema.");
+ }
+ elsif ( $db_type eq 'Pg' ) {
+ $status = $dbh->do("CREATE DATABASE $db_name WITH ENCODING='UNICODE' TEMPLATE template0");
+ }
+ else {
+ $status = $dbh->do("CREATE DATABASE $db_name");
+ }
+ return ($status, $DBI::errstr);
+}
+
+=head3 DropDatabase $DBH
+
+Drops RT's database. This method can be used as class method.
+
+Takes DBI handle as first argument. Many database systems require
+a special handle to allow you to drop a database, so you may have
+to use L<SystemDSN> when acquiring the DBI handle.
+
+Fetches the type and name of the database from the config.
+
+=cut
+
+sub DropDatabase {
+ my $self = shift;
+ my $dbh = shift or return (0, "No DBI handle provided");
+
+ my $db_type = RT->Config->Get('DatabaseType');
+ my $db_name = RT->Config->Get('DatabaseName');
+
+ if ( $db_type eq 'Oracle' ) {
+ my $db_user = RT->Config->Get('DatabaseUser');
+ my $status = $dbh->do( "DROP USER $db_user CASCADE" );
+ unless ( $status ) {
+ return 0, "Couldn't drop user $db_user."
+ ."\nError: ". $dbh->errstr;
+ }
+ return (1, "Successfully dropped user '$db_user' with his schema.");
+ }
+ elsif ( $db_type eq 'SQLite' ) {
+ my $path = $db_name;
+ $path = "$RT::VarPath/$path" unless substr($path, 0, 1) eq '/';
+ unlink $path or return (0, "Couldn't remove '$path': $!");
+ return (1);
+ } else {
+ $dbh->do("DROP DATABASE ". $db_name)
+ or return (0, $DBI::errstr);
+ }
+ return (1);
+}
+
+=head2 InsertACL
+
+=cut
+
+sub InsertACL {
+ my $self = shift;
+ my $dbh = shift;
+ my $base_path = shift || $RT::EtcPath;
+
+ my $db_type = RT->Config->Get('DatabaseType');
+ return (1) if $db_type eq 'SQLite';
+
+ $dbh = $self->dbh if !$dbh && ref $self;
+ return (0, "No DBI handle provided") unless $dbh;
+
+ return (0, "'$base_path' doesn't exist") unless -e $base_path;
+
+ my $path;
+ if ( -d $base_path ) {
+ $path = File::Spec->catfile( $base_path, "acl.$db_type");
+ $path = $self->GetVersionFile($dbh, $path);
+
+ $path = File::Spec->catfile( $base_path, "acl")
+ unless $path && -e $path;
+ return (0, "Couldn't find ACLs for $db_type")
+ unless -e $path;
+ } else {
+ $path = $base_path;
+ }
+
+ local *acl;
+ do $path || return (0, "Couldn't load ACLs: " . $@);
+ my @acl = acl($dbh);
+ foreach my $statement (@acl) {
+ my $sth = $dbh->prepare($statement)
+ or return (0, "Couldn't prepare SQL query:\n $statement\n\nERROR: ". $dbh->errstr);
+ unless ( $sth->execute ) {
+ return (0, "Couldn't run SQL query:\n $statement\n\nERROR: ". $sth->errstr);
+ }
+ }
+ return (1);
+}
+
+=head2 InsertSchema
+
+=cut
+
+sub InsertSchema {
+ my $self = shift;
+ my $dbh = shift;
+ my $base_path = (shift || $RT::EtcPath);
+
+ $dbh = $self->dbh if !$dbh && ref $self;
+ return (0, "No DBI handle provided") unless $dbh;
+
+ my $db_type = RT->Config->Get('DatabaseType');
+
+ my $file;
+ if ( -d $base_path ) {
+ $file = $base_path . "/schema." . $db_type;
+ } else {
+ $file = $base_path;
+ }
+
+ $file = $self->GetVersionFile( $dbh, $file );
+ unless ( $file ) {
+ return (0, "Couldn't find schema file(s) '$file*'");
+ }
+ unless ( -f $file && -r $file ) {
+ return (0, "File '$file' doesn't exist or couldn't be read");
+ }
+
+ my (@schema);
+
+ open( my $fh_schema, '<', $file ) or die $!;
+
+ my $has_local = 0;
+ open( my $fh_schema_local, "<" . $self->GetVersionFile( $dbh, $RT::LocalEtcPath . "/schema." . $db_type ))
+ and $has_local = 1;
+
+ my $statement = "";
+ foreach my $line ( <$fh_schema>, ($_ = ';;'), $has_local? <$fh_schema_local>: () ) {
+ $line =~ s/\#.*//g;
+ $line =~ s/--.*//g;
+ $statement .= $line;
+ if ( $line =~ /;(\s*)$/ ) {
+ $statement =~ s/;(\s*)$//g;
+ push @schema, $statement;
+ $statement = "";
+ }
+ }
+ close $fh_schema; close $fh_schema_local;
+
+ if ( $db_type eq 'Oracle' ) {
+ my $db_user = RT->Config->Get('DatabaseUser');
+ my $status = $dbh->do( "ALTER SESSION SET CURRENT_SCHEMA=$db_user" );
+ unless ( $status ) {
+ return $status, "Couldn't set current schema to $db_user."
+ ."\nError: ". $dbh->errstr;
+ }
+ }
+
+ local $SIG{__WARN__} = sub {};
+ my $is_local = 0;
+ $dbh->begin_work or return (0, "Couldn't begin transaction: ". $dbh->errstr);
+ foreach my $statement (@schema) {
+ if ( $statement =~ /^\s*;$/ ) {
+ $is_local = 1; next;
+ }
+
+ my $sth = $dbh->prepare($statement)
+ or return (0, "Couldn't prepare SQL query:\n$statement\n\nERROR: ". $dbh->errstr);
+ unless ( $sth->execute or $is_local ) {
+ return (0, "Couldn't run SQL query:\n$statement\n\nERROR: ". $sth->errstr);
+ }
+ }
+ $dbh->commit or return (0, "Couldn't commit transaction: ". $dbh->errstr);
+ return (1);
+}
+
+=head1 GetVersionFile
+
+Takes base name of the file as argument, scans for <base name>-<version> named
+files and returns file name with closest version to the version of the RT DB.
+
+=cut
+
+sub GetVersionFile {
+ my $self = shift;
+ my $dbh = shift;
+ my $base_name = shift;
+
+ my $db_version = ref $self
+ ? $self->DatabaseVersion
+ : do {
+ my $tmp = RT::Handle->new;
+ $tmp->dbh($dbh);
+ $tmp->DatabaseVersion;
+ };
+
+ require File::Glob;
+ my @files = File::Glob::bsd_glob("$base_name*");
+ return '' unless @files;
+
+ my %version = map { $_ =~ /\.\w+-([-\w\.]+)$/; ($1||0) => $_ } @files;
+ my $version;
+ foreach ( reverse sort cmp_version keys %version ) {
+ if ( cmp_version( $db_version, $_ ) >= 0 ) {
+ $version = $_;
+ last;
+ }
+ }
+
+ return defined $version? $version{ $version } : undef;
+}
+
+{ my %word = (
+ a => -4,
+ alpha => -4,
+ b => -3,
+ beta => -3,
+ pre => -2,
+ rc => -1,
+ head => 9999,
+);
+sub cmp_version($$) {
+ my ($a, $b) = (@_);
+ my @a = grep defined, map { /^[0-9]+$/? $_ : /^[a-zA-Z]+$/? $word{$_}|| -10 : undef }
+ split /([^0-9]+)/, $a;
+ my @b = grep defined, map { /^[0-9]+$/? $_ : /^[a-zA-Z]+$/? $word{$_}|| -10 : undef }
+ split /([^0-9]+)/, $b;
+ @a > @b
+ ? push @b, (0) x (@a-@b)
+ : push @a, (0) x (@b-@a);
+ for ( my $i = 0; $i < @a; $i++ ) {
+ return $a[$i] <=> $b[$i] if $a[$i] <=> $b[$i];
+ }
+ return 0;
+}}
+
+
+=head2 InsertInitialData
+
+Inserts system objects into RT's DB, like system user or 'nobody',
+internal groups and other records required. However, this method
+doesn't insert any real users like 'root' and you have to use
+InsertData or another way to do that.
+
+Takes no arguments. Returns status and message tuple.
+
+It's safe to call this method even if those objects already exist.
+
+=cut
+
+sub InsertInitialData {
+ my $self = shift;
+
+ my @warns;
+
+ # create RT_System user and grant him rights
+ {
+ require RT::CurrentUser;
+
+ my $test_user = RT::User->new( RT::CurrentUser->new() );
+ $test_user->Load('RT_System');
+ if ( $test_user->id ) {
+ push @warns, "Found system user in the DB.";
+ }
+ else {
+ my $user = RT::User->new( RT::CurrentUser->new() );
+ my ( $val, $msg ) = $user->_BootstrapCreate(
+ Name => 'RT_System',
+ RealName => 'The RT System itself',
+ Comments => 'Do not delete or modify this user. '
+ . 'It is integral to RT\'s internal database structures',
+ Creator => '1',
+ LastUpdatedBy => '1',
+ );
+ return ($val, $msg) unless $val;
+ }
+ DBIx::SearchBuilder::Record::Cachable->FlushCache;
+ }
+
+ # init RT::SystemUser and RT::System objects
+ RT::InitSystemObjects();
+ unless ( RT->SystemUser->id ) {
+ return (0, "Couldn't load system user");
+ }
+
+ # grant SuperUser right to system user
+ {
+ my $test_ace = RT::ACE->new( RT->SystemUser );
+ $test_ace->LoadByCols(
+ PrincipalId => ACLEquivGroupId( RT->SystemUser->Id ),
+ PrincipalType => 'Group',
+ RightName => 'SuperUser',
+ ObjectType => 'RT::System',
+ ObjectId => 1,
+ );
+ if ( $test_ace->id ) {
+ push @warns, "System user has global SuperUser right.";
+ } else {
+ my $ace = RT::ACE->new( RT->SystemUser );
+ my ( $val, $msg ) = $ace->_BootstrapCreate(
+ PrincipalId => ACLEquivGroupId( RT->SystemUser->Id ),
+ PrincipalType => 'Group',
+ RightName => 'SuperUser',
+ ObjectType => 'RT::System',
+ ObjectId => 1,
+ );
+ return ($val, $msg) unless $val;
+ }
+ DBIx::SearchBuilder::Record::Cachable->FlushCache;
+ }
+
+ # system groups
+ # $self->loc('Everyone'); # For the string extractor to get a string to localize
+ # $self->loc('Privileged'); # For the string extractor to get a string to localize
+ # $self->loc('Unprivileged'); # For the string extractor to get a string to localize
+ foreach my $name (qw(Everyone Privileged Unprivileged)) {
+ my $group = RT::Group->new( RT->SystemUser );
+ $group->LoadSystemInternalGroup( $name );
+ if ( $group->id ) {
+ push @warns, "System group '$name' already exists.";
+ next;
+ }
+
+ $group = RT::Group->new( RT->SystemUser );
+ my ( $val, $msg ) = $group->_Create(
+ Type => $name,
+ Domain => 'SystemInternal',
+ Description => 'Pseudogroup for internal use', # loc
+ Name => '',
+ Instance => '',
+ );
+ return ($val, $msg) unless $val;
+ }
+
+ # nobody
+ {
+ my $user = RT::User->new( RT->SystemUser );
+ $user->Load('Nobody');
+ if ( $user->id ) {
+ push @warns, "Found 'Nobody' user in the DB.";
+ }
+ else {
+ my ( $val, $msg ) = $user->Create(
+ Name => 'Nobody',
+ RealName => 'Nobody in particular',
+ Comments => 'Do not delete or modify this user. It is integral '
+ .'to RT\'s internal data structures',
+ Privileged => 0,
+ );
+ return ($val, $msg) unless $val;
+ }
+
+ if ( $user->HasRight( Right => 'OwnTicket', Object => $RT::System ) ) {
+ push @warns, "User 'Nobody' has global OwnTicket right.";
+ } else {
+ my ( $val, $msg ) = $user->PrincipalObj->GrantRight(
+ Right => 'OwnTicket',
+ Object => $RT::System,
+ );
+ return ($val, $msg) unless $val;
+ }
+ }
+
+ # rerun to get init Nobody as well
+ RT::InitSystemObjects();