#!/usr/bin/perl -w
# BEGIN BPS TAGGED BLOCK {{{
# 
# COPYRIGHT:
#  
# This software is Copyright (c) 1996-2006 Best Practical Solutions, LLC 
#                                          <jesse@bestpractical.com>
# 
# (Except where explicitly superseded by other copyright notices)
# 
# 
# LICENSE:
# 
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
# 
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
# 
# 
# CONTRIBUTION SUBMISSION POLICY:
# 
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
# 
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
# 
# END BPS TAGGED BLOCK }}}
use strict;
use vars qw($PROMPT $VERSION $Handle $Nobody $SystemUser $item);
use vars
  qw(@Groups @Users @ACL @Queues @ScripActions @ScripConditions @Templates @CustomFields @Scrips @Attributes);

use lib "/usr/local/libdata/perl5/site_perl";
use lib "/usr/local/libdata/perl5/site_perl";

#This drags in  RT's config.pm
# We do it in a begin block because RT::Handle needs to know the type to do its
# inheritance
use RT;
use Carp;
use RT::User;
use RT::CurrentUser;
use RT::Template;
use RT::ScripAction;
use RT::ACE;
use RT::Group;
use RT::User;
use RT::Queue;
use RT::ScripCondition;
use RT::CustomField;
use RT::Scrip;

RT::LoadConfig();
use Term::ReadKey;
use Getopt::Long;

my %args;

GetOptions(
    \%args,
    'prompt-for-dba-password', 'force', 'debug',
    'action=s',                'dba=s', 'dba-password=s', 'datafile=s',
    'datadir=s'
);

unless ( $args{'action'} ) {
    help();
    exit(-1);
}

$| = 1;    #unbuffer that output.

require RT::Handle;
my $Handle = RT::Handle->new($RT::DatabaseType);
$Handle->BuildDSN;
my $dbh;

if ( $args{'prompt-for-dba-password'} ) {
    $args{'dba-password'} = get_dba_password();
    chomp( $args{'dba-password'} );
}

if ( $args{'action'} eq 'init' ) {
    $dbh = DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} )
      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
    print "Now creating a database for RT.\n";
    if ( $RT::DatabaseType ne 'Oracle' || $args{'dba'} ne $RT::DatabaseUser ) {
        create_db();
    } else {
        print "...skipped as ".$args{'dba'} ." is not " . $RT::DatabaseUser . " or we're working with Oracle.\n";
    }

    if ( $RT::DatabaseType eq "mysql" ) {
        # Check which version we're running
        my ($version) = $dbh->selectrow_hashref("show variables like 'version'")->{Value} =~ /^(\d\.\d+)/;
        print "*** Warning: RT is unsupported on MySQL versions before 4.0.x\n" if $version < 4;

        # MySQL must have InnoDB support
        my $innodb = $dbh->selectrow_hashref("show variables like 'have_innodb'")->{Value};
        if ( $innodb eq "NO" ) {
            die "RT requires that MySQL be compiled with InnoDB table support.\n".
              "See http://dev.mysql.com/doc/mysql/en/InnoDB.html\n";
        } elsif ( $innodb eq "DISABLED" ) {
            die "RT requires that MySQL InnoDB table support be enabled.\n".
              ($version < 4
               ? "Add 'innodb_data_file_path=ibdata1:10M:autoextend' to the [mysqld] section of my.cnf\n"
               : "Remove the 'skip-innodb' line from your my.cnf file, restart MySQL, and try again.\n");
        }
    }

    # SQLite can't deal with the disconnect/reconnect
    unless ( $RT::DatabaseType eq 'SQLite' ) {

        $dbh->disconnect;

        if ( $RT::DatabaseType eq "Oracle" ) {
            $RT::DatabasePassword = $RT::DatabasePassword; #Warning avidance
            $dbh = DBI->connect( $Handle->DSN, ${RT::DatabaseUser}, ${RT::DatabasePassword} ) || die $DBI::errstr;
        } else {	
            $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} ) || die $DBI::errstr;
        }
    }
    print "Now populating database schema.\n";
    insert_schema();
    print "Now inserting database ACLs\n";
    insert_acl() unless $RT::DatabaseType eq 'Oracle';
    print "Now inserting RT core system objects\n";
    insert_initial_data();
    print "Now inserting RT data\n";
    insert_data( $RT::EtcPath . "/initialdata" );
}
elsif ( $args{'action'} eq 'drop' ) {
    unless ( $dbh =
        DBI->connect( get_system_dsn(), $args{'dba'}, $args{'dba-password'} ) )
    {
        warn $DBI::errstr;
        warn "Database doesn't appear to exist. Aborting database drop.";
        exit;
    }
    drop_db();
}
elsif ( $args{'action'} eq 'insert' ) {
    insert_data( $args{'datafile'} || ($args{'datadir'}."/content") );
}
elsif ( $args{'action'} eq 'acl' ) {
    $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
    insert_acl($args{'datadir'});
}
elsif ( $args{'action'} eq 'schema' ) {
    $dbh = DBI->connect( $Handle->DSN, $args{'dba'}, $args{'dba-password'} )
      || die "Failed to connect to " . get_system_dsn() . " as $args{'dba'}: $DBI::errstr";
    insert_schema($args{'datadir'});
}
else {
    print STDERR "$0 called with an invalid --action parameter\n";
    exit(-1);
}

# {{{ sub insert_schema
sub insert_schema {
    my $base_path = (shift || $RT::EtcPath);
    my (@schema);
    print "Creating database schema.\n";

    if ( -f $base_path . "/schema." . $RT::DatabaseType ) {
        no warnings 'unopened';

        open( SCHEMA, "<" . $base_path . "/schema." . $RT::DatabaseType );
        open( SCHEMA_LOCAL, "<" . $RT::LocalEtcPath . "/schema." . $RT::DatabaseType );

        my $statement = "";
        foreach my $line (<SCHEMA>, ($_ = ';;'), <SCHEMA_LOCAL>) {
            $line =~ s/\#.*//g;
            $line =~ s/--.*//g;
            $statement .= $line;
            if ( $line =~ /;(\s*)$/ ) {
                $statement =~ s/;(\s*)$//g;
                push @schema, $statement;
                $statement = "";
            }
        }

        local $SIG{__WARN__} = sub {};
        my $is_local = 0; # local/etc/schema needs to be nonfatal.
        $dbh->begin_work or die $dbh->errstr;
        foreach my $statement (@schema) {
            if ( $statement =~ /^\s*;$/ ) { $is_local = 1; next; }

            print STDERR "SQL: $statement\n" if defined $args{'debug'};
            my $sth = $dbh->prepare($statement) or die $dbh->errstr;
            unless ( $sth->execute or $is_local ) {
                die "Problem with statement:\n $statement\n" . $sth->errstr;
            }
        }
        $dbh->commit or die $dbh->errstr;
    }
    else {
        die "Couldn't find schema file for " . $RT::DatabaseType . "\n";
    }
    print "Done setting up database schema.\n";
}

# }}}

# {{{ sub drop_db
sub drop_db {
    if ( $RT::DatabaseType eq 'Oracle' ) {
        print <<END;

To delete the tables and sequences of the RT Oracle database by running
    \@etc/drop.Oracle
through SQLPlus.

END
        return;
    }
    unless ( $args{'force'} ) {
        print <<END;

About to drop $RT::DatabaseType database $RT::DatabaseName on $RT::DatabaseHost.
WARNING: This will erase all data in $RT::DatabaseName.

END
        exit unless _yesno();

    }

    print "Dropping $RT::DatabaseType database $RT::DatabaseName.\n";

    if ( $RT::DatabaseType eq 'SQLite' ) {
        unlink $RT::DatabaseName or warn $!;
        return;
    }
    $dbh->do("Drop DATABASE $RT::DatabaseName") or warn $DBI::errstr;
}

# }}}

# {{{ sub create_db
sub create_db {
    print "Creating $RT::DatabaseType database $RT::DatabaseName.\n";
    if ( $RT::DatabaseType eq 'SQLite' ) {
        return;
    }
    elsif ( $RT::DatabaseType eq 'Pg' ) {
        $dbh->do("CREATE DATABASE $RT::DatabaseName WITH ENCODING='UNICODE'");
        if ( $DBI::errstr ) {
            $dbh->do("CREATE DATABASE $RT::DatabaseName") || die $DBI::errstr;
        }
    }
    elsif ( $RT::DatabaseType eq 'Oracle' ) {
        insert_acl();
    }
    elsif ( $RT::DatabaseType eq 'Informix' ) {
        $ENV{DB_LOCALE} = 'en_us.utf8';
        $dbh->do("CREATE DATABASE $RT::DatabaseName WITH BUFFERED LOG");
    }
    else {
        $dbh->do("CREATE DATABASE $RT::DatabaseName") or die $DBI::errstr;
    }
}

# }}}

sub get_dba_password {
    print "In order to create or update your RT database,";
    print "this script needs to connect to your "
      . $RT::DatabaseType
      . " instance on "
      . $RT::DatabaseHost . " as "
      . $args{'dba'} . ".\n";
    print "Please specify that user's database password below. If the user has no database\n";
    print "password, just press return.\n\n";
    print "Password: ";
    ReadMode('noecho');
    my $password = ReadLine(0);
    ReadMode('normal');
    print "\n";
    return ($password);
}

# {{{ sub _yesno
sub _yesno {
    print "Proceed [y/N]:";
    my $x = scalar(<STDIN>);
    $x =~ /^y/i;
}

# }}}

# {{{ insert_acls
sub insert_acl {
    my $base_path = (shift || $RT::EtcPath);

    if ( $RT::DatabaseType =~ /^oracle$/i ) {
        do $base_path . "/acl.Oracle"
          || die "Couldn't find ACLS for Oracle\n" . $@;
    }
    elsif ( $RT::DatabaseType =~ /^pg$/i ) {
        do $base_path . "/acl.Pg" || die "Couldn't find ACLS for Pg\n" . $@;
    }
    elsif ( $RT::DatabaseType =~ /^mysql$/i ) {
        do $base_path . "/acl.mysql"
          || die "Couldn't find ACLS for mysql in $base_path\n" . $@;
    }
    elsif ( $RT::DatabaseType =~ /^Sybase$/i ) {
        do $base_path . "/acl.Sybase"
          || die "Couldn't find ACLS for Sybase in $base_path\n" . $@;
    }
    elsif ( $RT::DatabaseType =~ /^informix$/i ) {
        do $base_path . "/acl.Informix"
          || die "Couldn't find ACLS for Informix in $base_path\n" . $@;
    }
    elsif ( $RT::DatabaseType =~ /^SQLite$/i ) {
        return;
    }
    else {
        die "Unknown RT database type";
    }

    my @acl = acl($dbh);
    foreach my $statement (@acl) {
        print STDERR $statement if $args{'debug'};
        my $sth = $dbh->prepare($statement) or die $dbh->errstr;
        unless ( $sth->execute ) {
            die "Problem with statement:\n $statement\n" . $sth->errstr;
        }
    }
    print "Done setting up database ACLs.\n";
}

# }}}

=head2 get_system_dsn

Returns a dsn suitable for database creates and drops
and user creates and drops

=cut

sub get_system_dsn {

    my $dsn = $Handle->DSN;

    #with mysql, you want to connect sans database to funge things
    if ( $RT::DatabaseType eq 'mysql' ) {
        $dsn =~ s/dbname=$RT::DatabaseName//;

        # with postgres, you want to connect to database1
    }
    elsif ( $RT::DatabaseType eq 'Pg' ) {
        $dsn =~ s/dbname=$RT::DatabaseName/dbname=template1/;
    }
    elsif ( $RT::DatabaseType eq 'Informix' ) {
        # with Informix, you want to connect sans database:
        $dsn =~ s/Informix:$RT::DatabaseName/Informix:/;
    }
    return $dsn;
}

sub insert_initial_data {

    RT::InitLogging();

    #connect to the db, for actual RT work
    require RT::Handle;
    $RT::Handle = RT::Handle->new();
    $RT::Handle->Connect();

    #Put together a current user object so we can create a User object
    my $CurrentUser = new RT::CurrentUser();

    print "Checking for existing system user...";
    my $test_user = RT::User->new($CurrentUser);
    $test_user->Load('RT_System');
    if ( $test_user->id ) {
        print "found!\n\nYou appear to have a functional RT database.\n"
          . "Exiting, so as not to clobber your existing data.\n";
        exit(-1);

    }
    else {
        print "not found.  This appears to be a new installation.\n";
    }

    print "Creating system user...";
    my $RT_System = new RT::User($CurrentUser);

    my ( $val, $msg ) = $RT_System->_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',
    );

    unless ( $val ) {
        print "$msg\n";
        exit(-1);
    }
    print "done.\n";
    $RT::Handle->Disconnect() unless $RT::DatabaseType eq 'SQLite';

}

# load some sort of data into the database

sub insert_data {
    my $datafile = shift;

    #Connect to the database and get RT::SystemUser and RT::Nobody loaded
    RT::Init;

    my $CurrentUser = RT::CurrentUser->new();
    $CurrentUser->LoadByName('RT_System');

    if ( $datafile eq $RT::EtcPath . "/initialdata" ) {

        print "Creating Superuser  ACL...";

        my $superuser_ace = RT::ACE->new($CurrentUser);
        $superuser_ace->_BootstrapCreate(
                             PrincipalId => ACLEquivGroupId( $CurrentUser->Id ),
                             PrincipalType => 'Group',
                             RightName     => 'SuperUser',
                             ObjectType    => 'RT::System',
                             ObjectId      => '1' );

        print "done.\n";
    }

    # Slurp in stuff to insert from the datafile. Possible things to go in here:-
    # @groups, @users, @acl, @queues, @ScripActions, @ScripConditions, @templates

    require $datafile
      || die "Couldn't find initial data for import\n" . $@;

    if ( @Groups ) {
        print "Creating groups...";
        foreach $item (@Groups) {
            my $new_entry = RT::Group->new($CurrentUser);
            my ( $return, $msg ) = $new_entry->_Create(%$item);
            print "(Error: $msg)" unless $return;
            print $return. ".";
        }
        print "done.\n";
    }
    if ( @Users ) {
        print "Creating users...";
        foreach $item (@Users) {
            my $new_entry = new RT::User($CurrentUser);
            my ( $return, $msg ) = $new_entry->Create(%$item);
            print "(Error: $msg)" unless $return;
            print $return. ".";
        }
        print "done.\n";
    }
    if ( @Queues ) {
        print "Creating queues...";
        for $item (@Queues) {
            my $new_entry = new RT::Queue($CurrentUser);
            my ( $return, $msg ) = $new_entry->Create(%$item);
            print "(Error: $msg)" unless $return;
            print $return. ".";
        }
        print "done.\n";
    }
    if ( @ACL ) {
        print "Creating ACL...";
        for my $item (@ACL) {

            my ($princ, $object);

            # Global rights or Queue rights?
            if ( $item->{'Queue'} ) {
                $object = RT::Queue->new($CurrentUser);
                $object->Load( $item->{'Queue'} );
            } else {
                $object = $RT::System;
            }

            # Group rights or user rights?
            if ( $item->{'GroupDomain'} ) {
                $princ = RT::Group->new($CurrentUser);
                if ( $item->{'GroupDomain'} eq 'UserDefined' ) {
                  $princ->LoadUserDefinedGroup( $item->{'GroupId'} );
                } elsif ( $item->{'GroupDomain'} eq 'SystemInternal' ) {
                  $princ->LoadSystemInternalGroup( $item->{'GroupType'} );
                } elsif ( $item->{'GroupDomain'} eq 'RT::System-Role' ) {
                  $princ->LoadSystemRoleGroup( $item->{'GroupType'} );
                } elsif ( $item->{'GroupDomain'} eq 'RT::Queue-Role' &&
                          $item->{'Queue'} )
                {
                  $princ->LoadQueueRoleGroup( Type => $item->{'GroupType'},
                                              Queue => $object->id);
                } else {
                  $princ->Load( $item->{'GroupId'} );
                }
            } else {
                $princ = RT::User->new($CurrentUser);
                $princ->Load( $item->{'UserId'} );
            }

            # Grant it
            my ( $return, $msg ) = $princ->PrincipalObj->GrantRight(
                                                     Right => $item->{'Right'},
                                                     Object => $object );

            if ( $return ) {
                print $return. ".";
            }
            else {
                print $msg . ".";

            }

        }
        print "done.\n";
    }
    if ( @CustomFields ) {
        print "Creating custom fields...";
        for $item (@CustomFields) {
            my $new_entry = new RT::CustomField($CurrentUser);
            my $values    = $item->{'Values'};
            delete $item->{'Values'};
            my ( $return, $msg ) = $new_entry->Create(%$item);
            unless( $return ) {
                print "(Error: $msg)\n";
                next;
            }

            foreach my $value ( @{$values} ) {
                my ( $eval, $emsg ) = $new_entry->AddValue(%$value);
                print "(Error: $emsg)\n" unless $eval;
            }

            if ($item->{LookupType}) { # enable by default
                my $ocf = RT::ObjectCustomField->new($CurrentUser);
                $ocf->Create( CustomField => $new_entry->Id );
            }

            print "(Error: $msg)\n" unless $return;
            print $return. ".";
        }

        print "done.\n";
    }

    if ( @ScripActions ) {
        print "Creating ScripActions...";

        for $item (@ScripActions) {
            my $new_entry = RT::ScripAction->new($CurrentUser);
            my $return    = $new_entry->Create(%$item);
            print $return. ".";
        }

        print "done.\n";
    }

    if ( @ScripConditions ) {
        print "Creating ScripConditions...";

        for $item (@ScripConditions) {
            my $new_entry = RT::ScripCondition->new($CurrentUser);
            my $return    = $new_entry->Create(%$item);
            print $return. ".";
        }

        print "done.\n";
    }

    if ( @Templates ) {
        print "Creating templates...";

        for $item (@Templates) {
            my $new_entry = new RT::Template($CurrentUser);
            my $return    = $new_entry->Create(%$item);
            print $return. ".";
        }
        print "done.\n";
    }
    if ( @Scrips ) {
        print "Creating scrips...";

        for $item (@Scrips) {
            my $new_entry = new RT::Scrip($CurrentUser);
            my ( $return, $msg ) = $new_entry->Create(%$item);
            if ( $return ) {
                print $return. ".";
            }
            else {
                print "(Error: $msg)\n";
            }
        }
        print "done.\n";
    }
    if ( @Attributes ) {
        print "Creating predefined searches...";
        my $sys = RT::System->new($CurrentUser);

        for $item (@Attributes) {
            my $obj = delete $item->{Object}; # XXX: make this something loadable
            $obj ||= $sys;
            my ( $return, $msg ) = $obj->AddAttribute (%$item);
            if ( $return ) {
                print $return. ".";
            }
            else {
                print "(Error: $msg)\n";
            }
        }
        print "done.\n";
    }
    $RT::Handle->Disconnect() unless $RT::DatabaseType eq 'SQLite';
    print "Done setting up database content.\n";
}

=head2 ACLEquivGroupId

Given a userid, return that user's acl equivalence group

=cut

sub ACLEquivGroupId {
    my $username = shift;
    my $user     = RT::User->new($RT::SystemUser);
    $user->Load($username);
    my $equiv_group = RT::Group->new($RT::SystemUser);
    $equiv_group->LoadACLEquivalenceGroup($user);
    return ( $equiv_group->Id );
}

sub help {

    print <<EOF;

$0: Set up RT's database

--action        init    Initialize the database
                drop    Drop the database.
                        This will ERASE ALL YOUR DATA
                insert  Insert data into RT's database.
                        By default, will use RT's installation data.
                        To use a local or supplementary datafile, specify it
                        using the '--datafile' option below.

                acl     Initialize only the database ACLs
                        To use a local or supplementary datafile, specify it
                        using the '--datadir' option below.

                schema  Initialize only the database schema
                        To use a local or supplementary datafile, specify it
                        using the '--datadir' option below.

--datafile /path/to/datafile
--datadir /path/to/              Used to specify a path to find the local
                                database schema and acls to be installed.


--dba                           dba's username
--dba-password                  dba's password
--prompt-for-dba-password       Ask for the database administrator's password interactively


EOF

}

1;
