#!/usr/bin/perl
# BEGIN BPS TAGGED BLOCK {{{
# 
# COPYRIGHT:
# 
# This software is Copyright (c) 1996-2008 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., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
# 
# 
# 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 warnings;

use vars qw($Nobody $SystemUser $item);

# fix lib paths, some may be relative
BEGIN {
    require File::Spec;
    my @libs = ("/usr/lib/perl5/vendor_perl/5.10.1", "/usr/local/lib/rt3/lib");
    my $bin_path;

    for my $lib (@libs) {
        unless ( File::Spec->file_name_is_absolute($lib) ) {
            unless ($bin_path) {
                if ( File::Spec->file_name_is_absolute(__FILE__) ) {
                    $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
                }
                else {
                    require FindBin;
                    no warnings "once";
                    $bin_path = $FindBin::Bin;
                }
            }
            $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
        }
        unshift @INC, $lib;
    }

}

#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
BEGIN {
    use RT;
    RT::LoadConfig();
    RT::InitClasses();
}

use Term::ReadKey;
use Getopt::Long;

$| = 1; # unbuffer all output.

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

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

# check and setup @actions
my @actions = grep $_, split /,/, $args{'action'};
if ( @actions > 1 && $args{'datafile'} ) {
    print STDERR "You can not use --datafile option with multiple actions.\n";
    exit(-1);
}
foreach ( @actions ) {
    unless ( /^(?:init|create|drop|schema|acl|coredata|insert|upgrade)$/ ) {
        print STDERR "$0 called with an invalid --action parameter.\n";
        exit(-1);
    }
    if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) {
        print STDERR "You can not mix init, drop or upgrade action with any action.\n";
        exit(-1);
    }
}

# convert init to multiple actions
my $init = 0;
if ( $actions[0] eq 'init' ) {
    @actions = qw(create schema acl coredata insert);
    $init = 1;
}

# set options from environment
foreach my $key(qw(Type Host Name User Password)) {
    next unless exists $ENV{ 'RT_DB_'. uc $key };
    print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n";
    RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key });
}

my $db_type = RT->Config->Get('DatabaseType') || '';
my $db_host = RT->Config->Get('DatabaseHost') || '';
my $db_name = RT->Config->Get('DatabaseName') || '';
my $db_user = RT->Config->Get('DatabaseUser') || '';
my $db_pass = RT->Config->Get('DatabasePassword') || '';

# load it here to get error immidiatly if DB type is not supported
require RT::Handle;

if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) {
    $db_name = File::Spec->catfile($RT::VarPath, $db_name);
    RT->Config->Set( DatabaseName => $db_name );
}

my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || $db_user || '';
my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};

if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) {
    $dba_pass = get_dba_password();
    chomp $dba_pass if defined($dba_pass);
}

print "Working with:\n"
    ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
    ."User:\t$db_user\nDBA:\t$dba_user\n";

foreach my $action ( @actions ) {
    no strict 'refs';
    my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args );
    error($action, $msg) unless $status;
    print $msg ."\n" if $msg;
    print "Done.\n";
}

sub action_create {
    my %args = @_;
    my $dbh = get_system_dbh();
    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
    return ($status, $msg) unless $status;

    print "Now creating a $db_type database $db_name for RT.\n";
    return RT::Handle->CreateDatabase( $dbh );
}

sub action_drop {
    my %args = @_;
    if ( $db_type eq 'Oracle' ) {
        print <<END;

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

END
        exit(-1);
    }

    print "Dropping $db_type database $db_name.\n";
    unless ( $args{'force'} ) {
        print <<END;

About to drop $db_type database $db_name on $db_host.
WARNING: This will erase all data in $db_name.

END
        exit(-2) unless _yesno();
    }

    my $dbh = get_system_dbh();
    return RT::Handle->DropDatabase( $dbh );
}

sub action_schema {
    my %args = @_;
    my $dbh = get_admin_dbh();
    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
    return ($status, $msg) unless $status;

    print "Now populating database schema.\n";
    return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} );
}

sub action_acl {
    my %args = @_;
    my $dbh = get_admin_dbh();
    my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' );
    return ($status, $msg) unless $status;

    print "Now inserting database ACLs\n";
    return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} );
}

sub action_coredata {
    my %args = @_;
    $RT::Handle = new RT::Handle;
    $RT::Handle->dbh( undef );
    RT::ConnectToDatabase();
    RT::InitLogging();
    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
    return ($status, $msg) unless $status;

    print "Now inserting RT core system objects\n";
    return $RT::Handle->InsertInitialData;
}

sub action_insert {
    my %args = @_;
    $RT::Handle = new RT::Handle;
    RT::Init();
    my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' );
    return ($status, $msg) unless $status;

    print "Now inserting data\n";
    my $file = $args{'datafile'};
    $file = $RT::EtcPath . "/initialdata" if $init && !$file;
    $file ||= $args{'datadir'}."/content";
    return $RT::Handle->InsertData( $file );
}

sub action_upgrade {
    my %args = @_;
    my $base_dir = $args{'datadir'} || "./etc/upgrade";
    return (0, "Couldn't read dir '$base_dir' with upgrade data")
        unless -d $base_dir || -r _;

    my $upgrading_from = undef;
    do {
        if ( defined $upgrading_from ) {
            print "Doesn't match #.#.#: ";
        } else {
            print "Enter RT version you're upgrading from: ";
        }
        $upgrading_from = scalar <STDIN>;
        chomp $upgrading_from;
        $upgrading_from =~ s/\s+//g;
    } while $upgrading_from !~ /^\d+\.\d+\.\d+$/;

    my $upgrading_to = $RT::VERSION;
    return (0, "The current version $upgrading_to is lower than $upgrading_from")
        if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0;

    return (1, "The version $upgrading_to you're upgrading to is up to date")
        if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0;

    my @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);

    return (1, "No DB changes between $upgrading_from and $upgrading_to")
        unless @versions;

    print "\nGoing to apply following upgrades:\n";
    print map "* $_\n", @versions;

    {
        my $custom_upgrading_to = undef;
        do {
            if ( defined $custom_upgrading_to ) {
                print "Doesn't match #.#.#: ";
            } else {
                print "\nEnter RT version if you want to stop upgrade at some point,\n";
                print "  or leave it blank if you want apply above upgrades: ";
            }
            $custom_upgrading_to = scalar <STDIN>;
            chomp $custom_upgrading_to;
            $custom_upgrading_to =~ s/\s+//g;
            last unless $custom_upgrading_to;
        } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+$/;

        if ( $custom_upgrading_to ) {
            return (
                0, "The version you entered ($custom_upgrading_to) is lower than\n"
                ."version you're upgrading from ($upgrading_from)"
            ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0;

            return (1, "The version you're upgrading to is up to date")
                if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0;

            if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) {
                print "Version you entered is greater than installed ($RT::VERSION).\n";
                _yesno() or exit(-2);
            }
            # ok, checked everything no let's refresh list
            $upgrading_to = $custom_upgrading_to;
            @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to);

            return (1, "No DB changes between $upgrading_from and $upgrading_to")
                unless @versions;

            print "\nGoing to apply following upgrades:\n";
            print map "* $_\n", @versions;
        }
    }

    print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n";
    _yesno() or exit(-2) unless $args{'force'};

    foreach my $v ( @versions ) {
        print "Processing $v\n";
        my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef);
        if ( -e "$base_dir/$v/schema.$db_type" ) {
            action_schema( %tmp );
        }
        if ( -e "$base_dir/$v/acl.$db_type" ) {
            action_acl( %tmp );
        }
        if ( -e "$base_dir/$v/content" ) {
            action_insert( %tmp );
        }
    }
    return 1;
}

sub get_versions_from_to {
    my ($base_dir, $from, $to) = @_;

    opendir my $dh, $base_dir or die "couldn't open dir: $!";
    my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
    closedir $dh;

    return
        grep RT::Handle::cmp_version($_, $to) <= 0,
        grep RT::Handle::cmp_version($_, $from) > 0,
        sort RT::Handle::cmp_version @versions;
}

sub error {
    my ($action, $msg) = @_;
    print STDERR "Couldn't finish '$action' step.\n\n";
    print STDERR "ERROR: $msg\n\n";
    exit(-1);
}

sub get_dba_password {
    print "In order to create or update your RT database,"
        . " this script needs to connect to your "
        . " $db_type instance on $db_host as $dba_user\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);
}

=head2 get_system_dbh

Returns L<DBI> database handle connected to B<system> with DBA credentials.

See also L<RT::Handle/SystemDSN>.

=cut

sub get_system_dbh {
    return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass );
}

sub get_admin_dbh {
    return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass );
}

=head2 get_rt_dbh [USER, PASSWORD]

Returns L<DBI> database handle connected to RT database,
you may specify credentials(USER and PASSWORD) to connect
with. By default connects with credentials from RT config.

=cut

sub get_rt_dbh {
    return _get_dbh( RT::Handle->DSN, $db_user, $db_pass );
}

sub _get_dbh {
    my ($dsn, $user, $pass) = @_;
    my $dbh = DBI->connect(
        $dsn, $user, $pass,
        { RaiseError => 0, PrintError => 0 },
    );
    unless ( $dbh ) {
        my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
        if ( $args{'debug'} ) {
            require Carp; Carp::confess( $msg );
        } else {
            print STDERR $msg; exit -1;
        }
    }
    return $dbh;
}

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

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;
